|
| 1 | +# Copyright (c) IBM Corporation 2020 |
| 2 | +# Apache License, Version 2.0 (see https://opensource.org/licenses/Apache-2.0) |
| 3 | + |
| 4 | +from __future__ import absolute_import, division, print_function |
| 5 | + |
| 6 | +import re |
| 7 | +from os import path |
| 8 | +from random import choice |
| 9 | +from string import ascii_uppercase |
| 10 | +from ansible.module_utils._text import to_bytes |
| 11 | + |
| 12 | +try: |
| 13 | + from zoautil_py import Datasets, MVSCmd, types |
| 14 | +except Exception: |
| 15 | + Datasets = "" |
| 16 | + MVSCmd = "" |
| 17 | + types = "" |
| 18 | + |
| 19 | +__metaclass__ = type |
| 20 | + |
| 21 | +LISTDS_COMMAND = " LISTDS '{0}'" |
| 22 | +LISTCAT_COMMAND = " LISTCAT ENT({0}) ALL" |
| 23 | + |
| 24 | + |
| 25 | +class DataSetUtils(object): |
| 26 | + def __init__(self, module, data_set): |
| 27 | + """A standard utility to gather information about |
| 28 | + a particular data set. Note that the input data set is assumed |
| 29 | + to be cataloged. |
| 30 | +
|
| 31 | + Arguments: |
| 32 | + module {AnsibleModule} -- The AnsibleModule object from currently |
| 33 | + running module. |
| 34 | + data_set {str} -- Name of the input data set |
| 35 | + """ |
| 36 | + self.module = module |
| 37 | + self.data_set = data_set |
| 38 | + self.uss_path = '/' in data_set |
| 39 | + self.ds_info = dict() |
| 40 | + if not self.uss_path: |
| 41 | + self.ds_info = self._gather_data_set_info() |
| 42 | + |
| 43 | + def data_set_exists(self): |
| 44 | + """Determines whether the input data set exists. The input data |
| 45 | + set can be VSAM or non-VSAM. |
| 46 | +
|
| 47 | + Returns: |
| 48 | + bool -- If the data set exists |
| 49 | + """ |
| 50 | + if self.uss_path: |
| 51 | + return path.exists(to_bytes(self.data_set)) |
| 52 | + return self.ds_info.get('exists') |
| 53 | + |
| 54 | + def data_set_member_exists(self, member): |
| 55 | + """Determines whether the input data set contains the given member. |
| 56 | +
|
| 57 | + Arguments: |
| 58 | + member {str} -- The name of the data set member |
| 59 | +
|
| 60 | + Returns: |
| 61 | + bool -- If the member exists |
| 62 | + """ |
| 63 | + if self.get_data_set_type() == "PO": |
| 64 | + rc, out, err = self.module.run_command( |
| 65 | + "head \"//'{0}({1})'\"".format(self.data_set, member) |
| 66 | + ) |
| 67 | + if rc == 0 and not re.findall(r"EDC5067I", err): |
| 68 | + return True |
| 69 | + return False |
| 70 | + |
| 71 | + def get_data_set_type(self): |
| 72 | + """Retrieves the data set type of the input data set. |
| 73 | +
|
| 74 | + Returns: |
| 75 | + str -- Type of the input data set. |
| 76 | + None -- If the data set does not exist or a non-existent USS file |
| 77 | +
|
| 78 | + Possible return values: |
| 79 | + 'PS' -- Physical Sequential |
| 80 | + 'PO' -- Partitioned (PDS or PDS(E)) |
| 81 | + 'VSAM' -- Virtual Storage Access Method |
| 82 | + 'DA' -- Direct Access |
| 83 | + 'IS' -- Indexed Sequential |
| 84 | + 'USS' -- USS file or directory |
| 85 | + """ |
| 86 | + if self.uss_path and self.data_set_exists(): |
| 87 | + return 'USS' |
| 88 | + return self.ds_info.get('dsorg') |
| 89 | + |
| 90 | + @staticmethod |
| 91 | + def create_temp_data_set( |
| 92 | + LLQ, |
| 93 | + ds_type="SEQ", |
| 94 | + size="5M", |
| 95 | + ds_format="FB", |
| 96 | + lrecl=80 |
| 97 | + ): |
| 98 | + """Creates a temporary data set with the given low level qualifier. |
| 99 | +
|
| 100 | + Arguments: |
| 101 | + LLQ {str} -- Low Level Qualifier to be used for temporary data set |
| 102 | + ds_type {str} -- The data set type, default: Sequential |
| 103 | + size {str} -- The size of the data set, default: 5M |
| 104 | + format {str} -- The record format of the data set, default: FB |
| 105 | + lrecl {int} -- The record length of the data set, default: 80 |
| 106 | +
|
| 107 | + Returns: |
| 108 | + str -- Name of the created data set |
| 109 | +
|
| 110 | + Raises: |
| 111 | + OSError: When non-zero return code is received |
| 112 | + from Datasets.create() |
| 113 | + """ |
| 114 | + chars = ascii_uppercase |
| 115 | + HLQ2 = ''.join(choice(chars) for i in range(5)) |
| 116 | + HLQ3 = ''.join(choice(chars) for i in range(6)) |
| 117 | + temp_ds_name = "{0}.{1}.{2}.{3}".format(Datasets.hlq(), HLQ2, HLQ3, LLQ) |
| 118 | + |
| 119 | + rc = Datasets.create(temp_ds_name, ds_type, size, ds_format, "", lrecl) |
| 120 | + if rc != 0: |
| 121 | + raise OSError("Unable to create temporary data set") |
| 122 | + |
| 123 | + return temp_ds_name |
| 124 | + |
| 125 | + def get_data_set_volume(self): |
| 126 | + """Retrieves the volume name where the input data set is stored. |
| 127 | +
|
| 128 | + Returns: |
| 129 | + str -- Volume where the data set is stored |
| 130 | + None -- If the data set does not exist |
| 131 | +
|
| 132 | + Raises: |
| 133 | + AttributeError -- When input data set is a USS file or directory |
| 134 | + """ |
| 135 | + if self.uss_path: |
| 136 | + raise AttributeError( |
| 137 | + "USS file or directory has no attribute 'Volume'" |
| 138 | + ) |
| 139 | + return self.ds_info.get('volser') |
| 140 | + |
| 141 | + def get_data_set_lrecl(self): |
| 142 | + """Retrieves the record length of the input data set. Record length |
| 143 | + specifies the length, in bytes, of each record in the data set. |
| 144 | +
|
| 145 | + Returns: |
| 146 | + int -- The record length, in bytes, of each record |
| 147 | + None -- If the data set does not exist or the data set is VSAM |
| 148 | +
|
| 149 | + Raises: |
| 150 | + AttributeError -- When input data set is a USS file or directory |
| 151 | + """ |
| 152 | + if self.uss_path: |
| 153 | + raise AttributeError( |
| 154 | + "USS file or directory has no attribute 'lrecl'" |
| 155 | + ) |
| 156 | + return self.ds_info.get('lrecl') |
| 157 | + |
| 158 | + def get_data_set_recfm(self): |
| 159 | + """Retrieves the record format of the input data set. |
| 160 | +
|
| 161 | + Returns: |
| 162 | + str -- Record format |
| 163 | + None -- If the data set does not exist or the data set is VSAM |
| 164 | +
|
| 165 | + Raises: |
| 166 | + AttributeError -- When input data set is a USS file or directory |
| 167 | +
|
| 168 | + Possible return values: |
| 169 | + 'F' -- Fixed |
| 170 | + 'FB' -- Fixed Blocked |
| 171 | + 'V' -- Variable |
| 172 | + 'VB' -- Variable Blocked |
| 173 | + 'U' -- Undefined |
| 174 | + 'VBS' -- Variable Blocked Spanned |
| 175 | + 'VS' -- Variable Spanned |
| 176 | + """ |
| 177 | + if self.uss_path: |
| 178 | + raise AttributeError( |
| 179 | + "USS file or directory has no attribute 'recfm'" |
| 180 | + ) |
| 181 | + return self.ds_info.get('recfm') |
| 182 | + |
| 183 | + def _gather_data_set_info(self): |
| 184 | + """Retrieves information about the input data set using LISTDS and |
| 185 | + LISTCAT commands. |
| 186 | +
|
| 187 | + Returns: |
| 188 | + dict -- Dictionary containing data set attributes |
| 189 | + """ |
| 190 | + result = dict() |
| 191 | + |
| 192 | + try: |
| 193 | + listds_out = self._run_mvs_cmd('IKJEFT01', LISTDS_COMMAND.format(self.data_set)) |
| 194 | + listcat_out = self._run_mvs_cmd('IDCAMS', LISTCAT_COMMAND.format(self.data_set)) |
| 195 | + except Exception: |
| 196 | + raise |
| 197 | + result.update(self._process_listds_output(listds_out)) |
| 198 | + result.update(self._process_listcat_output(listcat_out)) |
| 199 | + return result |
| 200 | + |
| 201 | + def _run_mvs_cmd(self, pgm, input_cmd): |
| 202 | + """Executes mvscmd with the given program and input command. |
| 203 | +
|
| 204 | + Arguments: |
| 205 | + pgm {str} -- The name of the MVS program to execute |
| 206 | + input_cmd {str} -- The command to execute |
| 207 | +
|
| 208 | + Returns: |
| 209 | + str -- The generated 'sysprint' of the executed command |
| 210 | +
|
| 211 | + Raises: |
| 212 | + MVSCmdExecError: When non-zero return code is received while |
| 213 | + executing MVSCmd |
| 214 | +
|
| 215 | + DatasetBusyError: When the data set is being edited by another user |
| 216 | + """ |
| 217 | + sysprint = "sysprint" |
| 218 | + sysin = "sysin" |
| 219 | + if pgm == "IKJEFT01": |
| 220 | + sysprint = "systsprt" |
| 221 | + sysin = "systsin" |
| 222 | + |
| 223 | + rc, out, err = self.module.run_command( |
| 224 | + "mvscmdauth --pgm={0} --{1}=* --{2}=stdin".format(pgm, sysprint, sysin), |
| 225 | + data=input_cmd |
| 226 | + ) |
| 227 | + if rc != 0: |
| 228 | + if (re.findall(r"ALREADY IN USE", out)): |
| 229 | + raise DatasetBusyError(self.data_set) |
| 230 | + if (re.findall(r"NOT IN CATALOG|NOT FOUND|NOT LISTED", out)): |
| 231 | + self.ds_info['exists'] = False |
| 232 | + else: |
| 233 | + raise MVSCmdExecError(rc, out, err) |
| 234 | + return out |
| 235 | + |
| 236 | + def _process_listds_output(self, output): |
| 237 | + """Parses the output generated by LISTDS command. |
| 238 | +
|
| 239 | + Arguments: |
| 240 | + output {str} -- The output of LISTDS command |
| 241 | +
|
| 242 | + Returns: |
| 243 | + dict -- Dictionary containing the output parameters of LISTDS |
| 244 | + """ |
| 245 | + result = dict() |
| 246 | + if "NOT IN CATALOG" in output: |
| 247 | + result['exists'] = False |
| 248 | + else: |
| 249 | + result['exists'] = True |
| 250 | + ds_search = re.search(r"(-|--)DSORG(-\s*|\s*)\n(.*)", output, re.MULTILINE) |
| 251 | + if ds_search: |
| 252 | + ds_params = ds_search.group(3).split() |
| 253 | + result['dsorg'] = ds_params[-1] |
| 254 | + if result.get('dsorg') != "VSAM": |
| 255 | + result['recfm'] = ds_params[0] |
| 256 | + result['lrecl'] = ds_params[1] |
| 257 | + return result |
| 258 | + |
| 259 | + def _process_listcat_output(self, output): |
| 260 | + """Parses the output generated by LISTCAT command. |
| 261 | +
|
| 262 | + Arguments: |
| 263 | + output {str} -- The output of LISTCAT command |
| 264 | +
|
| 265 | + Returns: |
| 266 | + dict -- Dictionary containing the output parameters of LISTCAT |
| 267 | + """ |
| 268 | + result = dict() |
| 269 | + if "NOT FOUND" not in output: |
| 270 | + volser_output = re.findall(r"VOLSER-*[A-Z|0-9]*", output) |
| 271 | + result['volser'] = ''.join( |
| 272 | + re.findall(r"-[A-Z|0-9]*", volser_output[0]) |
| 273 | + ).replace('-', '') |
| 274 | + return result |
| 275 | + |
| 276 | + |
| 277 | +class MVSCmdExecError(Exception): |
| 278 | + def __init__(self, rc, out, err): |
| 279 | + self.msg = ( |
| 280 | + "Failure during execution of mvscmd; Return code: {0}; " |
| 281 | + "stdout: {1}; stderr: {2}".format(rc, out, err) |
| 282 | + ) |
| 283 | + super().__init__(self.msg) |
| 284 | + |
| 285 | + |
| 286 | +class DatasetBusyError(Exception): |
| 287 | + def __init__(self, data_set): |
| 288 | + self.msg = ( |
| 289 | + "Dataset {0} may already be open by another user. " |
| 290 | + "Close the dataset and try again".format(data_set) |
| 291 | + ) |
| 292 | + super().__init__(self.msg) |
0 commit comments