zosmfrestapi 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zosmfrestapi/__init__.py +93 -0
- zosmfrestapi/client.py +47 -0
- zosmfrestapi/exceptions/__init__.py +18 -0
- zosmfrestapi/exceptions/authentication.py +17 -0
- zosmfrestapi/exceptions/dataset.py +17 -0
- zosmfrestapi/exceptions/software.py +34 -0
- zosmfrestapi/exceptions/workflow.py +30 -0
- zosmfrestapi/models/__init__.py +0 -0
- zosmfrestapi/models/authentication.py +16 -0
- zosmfrestapi/models/software.py +231 -0
- zosmfrestapi/models/software_updates.py +99 -0
- zosmfrestapi/models/topology.py +28 -0
- zosmfrestapi/models/workflow.py +114 -0
- zosmfrestapi/py.typed +0 -0
- zosmfrestapi/services/__init__.py +176 -0
- zosmfrestapi/services/authentication.py +52 -0
- zosmfrestapi/services/dataset.py +42 -0
- zosmfrestapi/services/software.py +477 -0
- zosmfrestapi/services/software_updates.py +949 -0
- zosmfrestapi/services/topology.py +49 -0
- zosmfrestapi/services/workflow.py +350 -0
- zosmfrestapi/testing/__init__.py +11 -0
- zosmfrestapi/testing/authentication.py +48 -0
- zosmfrestapi/testing/helpers.py +9 -0
- zosmfrestapi/testing/software.py +392 -0
- zosmfrestapi/testing/software_updates.py +281 -0
- zosmfrestapi/testing/topology.py +19 -0
- zosmfrestapi/testing/workflows.py +320 -0
- zosmfrestapi/utils.py +76 -0
- zosmfrestapi/zcrypto.py +55 -0
- zosmfrestapi/zcrypto_client.py +68 -0
- zosmfrestapi-0.2.0.dist-info/METADATA +172 -0
- zosmfrestapi-0.2.0.dist-info/RECORD +35 -0
- zosmfrestapi-0.2.0.dist-info/WHEEL +4 -0
- zosmfrestapi-0.2.0.dist-info/licenses/LICENSE +10 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
from zosmfrestapi.models.workflow import (
|
|
2
|
+
WorkflowArchivedListItem,
|
|
3
|
+
WorkflowInstance,
|
|
4
|
+
WorkflowListItem,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def make_workflow_list():
|
|
9
|
+
return [
|
|
10
|
+
WorkflowListItem(
|
|
11
|
+
instanceURI="/zosmf/workflow/rest/1.0/workflows/"
|
|
12
|
+
"8ed3a85e-6472-47eb-ba44-19e9511df450",
|
|
13
|
+
owner="FOOBAR",
|
|
14
|
+
vendor="Hooli Software",
|
|
15
|
+
workflowDefinitionFileMD5Value="c87342978ad879837a9872899cse2001",
|
|
16
|
+
workflowDescription="The Box III, Gavin Belson Signature Edition",
|
|
17
|
+
workflowID="HOOLMC",
|
|
18
|
+
workflowKey="8ed3a85e-6472-47eb-ba44-19e9511df450",
|
|
19
|
+
workflowName="HOOLIE",
|
|
20
|
+
workflowVersion="1.0",
|
|
21
|
+
),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def make_workflow(steps=[], variables=[]):
|
|
26
|
+
return WorkflowInstance(
|
|
27
|
+
name="HOOLIE",
|
|
28
|
+
percentComplete=0,
|
|
29
|
+
workflowDefinitionFileMD5Value="8e8fabbd0f72c54119a3748427fc0f25",
|
|
30
|
+
workflowArchiveSAFID="FOOBAR",
|
|
31
|
+
jobStatement=None,
|
|
32
|
+
globalVariableGroup=None,
|
|
33
|
+
jobsOutputDirectory="/foo/bar/log",
|
|
34
|
+
scope="sysplex",
|
|
35
|
+
status="in-progress",
|
|
36
|
+
vendor="Hooli Software",
|
|
37
|
+
deleteCompletedJobs=False,
|
|
38
|
+
autoDeleteOnCompletion=False,
|
|
39
|
+
isInstanceVariableWithoutPrefix=False,
|
|
40
|
+
workflowVersion="1.0",
|
|
41
|
+
steps=steps,
|
|
42
|
+
access="Public",
|
|
43
|
+
productVersion=None,
|
|
44
|
+
productID=None,
|
|
45
|
+
variables=variables,
|
|
46
|
+
category="general",
|
|
47
|
+
system="TST01.TSTA",
|
|
48
|
+
workflowDescription="The Box III, Gavin Belson Signature Edition",
|
|
49
|
+
workflowID="Hoolie",
|
|
50
|
+
owner="FOOBAR",
|
|
51
|
+
accountInfo=None,
|
|
52
|
+
automationStatus=None,
|
|
53
|
+
containsParallelSteps=False,
|
|
54
|
+
isCallable=None,
|
|
55
|
+
workflowKey="fc4fc0fa-1fa3-4247-bb2d-7f5203e0c3c7",
|
|
56
|
+
productName=None,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def make_new_workflow():
|
|
61
|
+
return WorkflowInstance(
|
|
62
|
+
name="HOOLIE",
|
|
63
|
+
owner="demouser",
|
|
64
|
+
system="TST01.TSTA (TSTA)",
|
|
65
|
+
scope="sysplex",
|
|
66
|
+
percentComplete=0,
|
|
67
|
+
workflowDefinitionFileMD5Value="aca0b684798fb2dfd49261ca4d211d93",
|
|
68
|
+
workflowArchiveSAFID="demouser",
|
|
69
|
+
jobStatement=None,
|
|
70
|
+
globalVariableGroup=None,
|
|
71
|
+
jobsOutputDirectory=None,
|
|
72
|
+
status="in-progress",
|
|
73
|
+
vendor="Hoolie Software",
|
|
74
|
+
deleteCompletedJobs=False,
|
|
75
|
+
autoDeleteOnCompletion=False,
|
|
76
|
+
isInstanceVariableWithoutPrefix=False,
|
|
77
|
+
workflowVersion="1.0",
|
|
78
|
+
steps=[],
|
|
79
|
+
access="Public",
|
|
80
|
+
productVersion=None,
|
|
81
|
+
productID=None,
|
|
82
|
+
variables=[],
|
|
83
|
+
category="general",
|
|
84
|
+
workflowDescription="The Box III, Gavin Belson Signature Edition",
|
|
85
|
+
workflowID="Hoolie",
|
|
86
|
+
accountInfo=None,
|
|
87
|
+
automationStatus=None,
|
|
88
|
+
containsParallelSteps=False,
|
|
89
|
+
isCallable=None,
|
|
90
|
+
workflowKey="fc4fc0fa-1fa3-4247-bb2d-7f5203e0c3c7",
|
|
91
|
+
productName=None,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def make_workflow_with_steps():
|
|
96
|
+
|
|
97
|
+
step1 = """//*********************************************************************
|
|
98
|
+
//* STEP - Create temporary directory using BPXBATCH *
|
|
99
|
+
//*********************************************************************
|
|
100
|
+
//*
|
|
101
|
+
//CREATE EXEC PGM=BPXBATCH
|
|
102
|
+
//*
|
|
103
|
+
//STDOUT DD SYSOUT=*
|
|
104
|
+
//*
|
|
105
|
+
//STDERR DD SYSOUT=*
|
|
106
|
+
//*
|
|
107
|
+
//STDPARM DD *
|
|
108
|
+
SH mkdir -m 700 /tmp/IZUWORKFLOW-17932912593740.328682
|
|
109
|
+
/*
|
|
110
|
+
//*
|
|
111
|
+
//*********************************************************************
|
|
112
|
+
//* STEP - Create script in temp directory using BPXCOPY *
|
|
113
|
+
//*********************************************************************
|
|
114
|
+
//*
|
|
115
|
+
//UXCOPY EXEC PGM=BPXCOPY,
|
|
116
|
+
// PARM='ELEMENT(MYSCRIPT) TYPE(TEXT) PATHMODE(0,7,0,0)'
|
|
117
|
+
//*
|
|
118
|
+
//SYSTSPRT DD SYSOUT=*
|
|
119
|
+
//*
|
|
120
|
+
//SYSIN DD DUMMY
|
|
121
|
+
//*
|
|
122
|
+
//SYSUT2 DD PATH='/tmp/IZUWORKFLOW-17932912593740.328682'
|
|
123
|
+
//*
|
|
124
|
+
//SYSUT1 DD *,DLM=$$
|
|
125
|
+
/bin/echo 'HELLO WORLD 1'
|
|
126
|
+
$$
|
|
127
|
+
//*
|
|
128
|
+
//*********************************************************************
|
|
129
|
+
//* STEP - Execute script using BPXBATCH *
|
|
130
|
+
//*********************************************************************
|
|
131
|
+
//*
|
|
132
|
+
//XBATCH EXEC PGM=BPXBATCH
|
|
133
|
+
//*
|
|
134
|
+
//STDOUT DD SYSOUT=*
|
|
135
|
+
//*
|
|
136
|
+
//STDERR DD SYSOUT=*
|
|
137
|
+
//*
|
|
138
|
+
//STDPARM DD *
|
|
139
|
+
SH /tmp/IZUWORKFLOW-17932912593740.328682/MYSCRIPT
|
|
140
|
+
/*
|
|
141
|
+
//*
|
|
142
|
+
//*********************************************************************
|
|
143
|
+
//* STEP - Delete temporary directory using BPXBATCH *
|
|
144
|
+
//*********************************************************************
|
|
145
|
+
//*
|
|
146
|
+
//REMOVE EXEC PGM=BPXBATCH
|
|
147
|
+
//*
|
|
148
|
+
//STDOUT DD SYSOUT=*
|
|
149
|
+
//*
|
|
150
|
+
//STDERR DD SYSOUT=*
|
|
151
|
+
//*
|
|
152
|
+
//STDPARM DD *
|
|
153
|
+
SH rm -r /tmp/IZUWORKFLOW-17932912593740.328682
|
|
154
|
+
/*
|
|
155
|
+
//*
|
|
156
|
+
//*********************************************************************"""
|
|
157
|
+
step2 = """//*********************************************************************
|
|
158
|
+
//* STEP - Create temporary directory using BPXBATCH *
|
|
159
|
+
//*********************************************************************
|
|
160
|
+
//*
|
|
161
|
+
//CREATE EXEC PGM=BPXBATCH
|
|
162
|
+
//*
|
|
163
|
+
//STDOUT DD SYSOUT=*
|
|
164
|
+
//*
|
|
165
|
+
//STDERR DD SYSOUT=*
|
|
166
|
+
//*
|
|
167
|
+
//STDPARM DD *
|
|
168
|
+
SH mkdir -m 700 /tmp/IZUWORKFLOW-17932912593750.432322
|
|
169
|
+
/*
|
|
170
|
+
//*
|
|
171
|
+
//*********************************************************************
|
|
172
|
+
//* STEP - Create script in temp directory using BPXCOPY *
|
|
173
|
+
//*********************************************************************
|
|
174
|
+
//*
|
|
175
|
+
//UXCOPY EXEC PGM=BPXCOPY,
|
|
176
|
+
// PARM='ELEMENT(MYSCRIPT) TYPE(TEXT) PATHMODE(0,7,0,0)'
|
|
177
|
+
//*
|
|
178
|
+
//SYSTSPRT DD SYSOUT=*
|
|
179
|
+
//*
|
|
180
|
+
//SYSIN DD DUMMY
|
|
181
|
+
//*
|
|
182
|
+
//SYSUT2 DD PATH='/tmp/IZUWORKFLOW-17932912593750.432322'
|
|
183
|
+
//*
|
|
184
|
+
//SYSUT1 DD *,DLM=$$
|
|
185
|
+
/bin/echo 'HELLO WORLD 2'
|
|
186
|
+
$$
|
|
187
|
+
//*
|
|
188
|
+
//*********************************************************************
|
|
189
|
+
//* STEP - Execute script using BPXBATCH *
|
|
190
|
+
//*********************************************************************
|
|
191
|
+
//*
|
|
192
|
+
//XBATCH EXEC PGM=BPXBATCH
|
|
193
|
+
//*
|
|
194
|
+
//STDOUT DD SYSOUT=*
|
|
195
|
+
//*
|
|
196
|
+
//STDERR DD SYSOUT=*
|
|
197
|
+
//*
|
|
198
|
+
//STDPARM DD *
|
|
199
|
+
SH /tmp/IZUWORKFLOW-17932912593750.432322/MYSCRIPT
|
|
200
|
+
/*
|
|
201
|
+
//*
|
|
202
|
+
//*********************************************************************
|
|
203
|
+
//* STEP - Delete temporary directory using BPXBATCH *
|
|
204
|
+
//*********************************************************************
|
|
205
|
+
//*
|
|
206
|
+
//REMOVE EXEC PGM=BPXBATCH
|
|
207
|
+
//*
|
|
208
|
+
//STDOUT DD SYSOUT=*
|
|
209
|
+
//*
|
|
210
|
+
//STDERR DD SYSOUT=*
|
|
211
|
+
//*
|
|
212
|
+
//STDPARM DD *
|
|
213
|
+
SH rm -r /tmp/IZUWORKFLOW-17932912593750.432322
|
|
214
|
+
/*
|
|
215
|
+
//*
|
|
216
|
+
//*********************************************************************"""
|
|
217
|
+
|
|
218
|
+
return make_workflow(
|
|
219
|
+
steps=[
|
|
220
|
+
{
|
|
221
|
+
"template": step1,
|
|
222
|
+
"instructions": "",
|
|
223
|
+
"maxLrecl": 1024,
|
|
224
|
+
"failedPattern": [],
|
|
225
|
+
"description": "The Box III, Gavin Belson Signature Edition: because"
|
|
226
|
+
" ordinary excellence was never enough",
|
|
227
|
+
"outputVariablesPrefix": None,
|
|
228
|
+
"variable-references": None,
|
|
229
|
+
"saveAsUnixFileSub": False,
|
|
230
|
+
"procName": "IZUFPROC",
|
|
231
|
+
"title": "Infinite Scale",
|
|
232
|
+
"jobInfo": None,
|
|
233
|
+
"timeout": 0,
|
|
234
|
+
"regionSize": 0,
|
|
235
|
+
"skills": None,
|
|
236
|
+
"isRestStep": False,
|
|
237
|
+
"output": None,
|
|
238
|
+
"outputSub": False,
|
|
239
|
+
"returnCode": None,
|
|
240
|
+
"outputSysoutDD": False,
|
|
241
|
+
"successPattern": None,
|
|
242
|
+
"state": "Unassigned",
|
|
243
|
+
"templateSub": False,
|
|
244
|
+
"owner": None,
|
|
245
|
+
"autoEnable": True,
|
|
246
|
+
"submitAs": "shell-JCL",
|
|
247
|
+
"userDefined": False,
|
|
248
|
+
"weight": "1",
|
|
249
|
+
"optional": False,
|
|
250
|
+
"steps": None,
|
|
251
|
+
"scriptParameters": None,
|
|
252
|
+
"saveAsUnixFile": None,
|
|
253
|
+
"instructionsSub": False,
|
|
254
|
+
"saveAsDatasetSub": False,
|
|
255
|
+
"isConditionStep": False,
|
|
256
|
+
"prereqStep": None,
|
|
257
|
+
"hasCalledWorkflow": False,
|
|
258
|
+
"name": "step1",
|
|
259
|
+
"stepNumber": "1",
|
|
260
|
+
"saveAsDataset": None,
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"template": step2,
|
|
264
|
+
"instructions": "",
|
|
265
|
+
"maxLrecl": 1024,
|
|
266
|
+
"failedPattern": [],
|
|
267
|
+
"description": "Engineered for those who don't follow the"
|
|
268
|
+
" future, but define it.",
|
|
269
|
+
"outputVariablesPrefix": None,
|
|
270
|
+
"variable-references": None,
|
|
271
|
+
"saveAsUnixFileSub": False,
|
|
272
|
+
"procName": "IZUFPROC",
|
|
273
|
+
"title": "Vision Engine",
|
|
274
|
+
"jobInfo": None,
|
|
275
|
+
"timeout": 0,
|
|
276
|
+
"regionSize": 0,
|
|
277
|
+
"skills": None,
|
|
278
|
+
"isRestStep": False,
|
|
279
|
+
"output": None,
|
|
280
|
+
"outputSub": False,
|
|
281
|
+
"returnCode": None,
|
|
282
|
+
"outputSysoutDD": False,
|
|
283
|
+
"successPattern": None,
|
|
284
|
+
"state": "Unassigned",
|
|
285
|
+
"templateSub": False,
|
|
286
|
+
"owner": None,
|
|
287
|
+
"autoEnable": True,
|
|
288
|
+
"submitAs": "shell-JCL",
|
|
289
|
+
"userDefined": False,
|
|
290
|
+
"weight": "1",
|
|
291
|
+
"optional": False,
|
|
292
|
+
"steps": None,
|
|
293
|
+
"scriptParameters": None,
|
|
294
|
+
"saveAsUnixFile": None,
|
|
295
|
+
"instructionsSub": False,
|
|
296
|
+
"saveAsDatasetSub": False,
|
|
297
|
+
"isConditionStep": False,
|
|
298
|
+
"prereqStep": None,
|
|
299
|
+
"hasCalledWorkflow": False,
|
|
300
|
+
"name": "step2",
|
|
301
|
+
"stepNumber": "2",
|
|
302
|
+
"saveAsDataset": None,
|
|
303
|
+
},
|
|
304
|
+
]
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def make_workflow_with_variables():
|
|
309
|
+
return make_workflow()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def make_archived_worfklow_list():
|
|
313
|
+
return [
|
|
314
|
+
WorkflowArchivedListItem(
|
|
315
|
+
archivedInstanceURI="/zosmf/workflow/rest/1.0/archivedworkflows/"
|
|
316
|
+
"07775b08-2dd9-4810-abb4-26f7cb05152a",
|
|
317
|
+
workflowKey="07775b08-2dd9-4810-abb4-26f7cb05152a",
|
|
318
|
+
workflowName="myworkflow",
|
|
319
|
+
)
|
|
320
|
+
]
|
zosmfrestapi/utils.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from dataclasses import fields, is_dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import pendulum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _parse_datetime(value: str) -> pendulum.DateTime:
|
|
8
|
+
"""Parse a string and require a Pendulum ``DateTime`` result.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
value: Date/time string accepted by :func:`pendulum.parse`.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
The parsed timezone-aware or naive Pendulum ``DateTime`` value.
|
|
15
|
+
|
|
16
|
+
Raises:
|
|
17
|
+
TypeError: If Pendulum parses the input as a date, time, duration, or
|
|
18
|
+
another non-``DateTime`` value.
|
|
19
|
+
"""
|
|
20
|
+
result = pendulum.parse(value)
|
|
21
|
+
|
|
22
|
+
if not isinstance(result, pendulum.DateTime):
|
|
23
|
+
raise TypeError(f"Expected DateTime, got {type(result)}")
|
|
24
|
+
|
|
25
|
+
return result
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def remap(data: dict[str, Any], cls: type[Any]) -> dict[str, Any]:
|
|
29
|
+
"""Translate JSON field aliases to the names used by a dataclass.
|
|
30
|
+
|
|
31
|
+
Dataclass fields with ``metadata={"json_name": ...}`` are mapped from
|
|
32
|
+
their external JSON key back to the Python field name. Other keys and
|
|
33
|
+
values are preserved.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
data: Mapping-like dictionary received from an API response.
|
|
37
|
+
cls: Dataclass type whose field metadata defines the aliases.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
A new dictionary with aliased keys remapped; ``data`` is not modified.
|
|
41
|
+
"""
|
|
42
|
+
aliases = {
|
|
43
|
+
f.metadata["json_name"]: f.name for f in fields(cls) if "json_name" in f.metadata
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {aliases.get(k, k): v for k, v in data.items()}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def to_dict(obj: object) -> Any:
|
|
50
|
+
"""Recursively convert dataclasses and containers to JSON payload values.
|
|
51
|
+
|
|
52
|
+
Dataclass fields are converted to dictionaries, respecting each field's
|
|
53
|
+
``json_name`` metadata. Lists and dictionaries are converted recursively;
|
|
54
|
+
other values are returned unchanged.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
obj: Dataclass instance, list, dictionary, or scalar value to convert.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The recursively converted value. Leaves are preserved, so callers
|
|
61
|
+
should supply JSON-compatible scalar values when using the result as an
|
|
62
|
+
HTTP JSON body.
|
|
63
|
+
"""
|
|
64
|
+
if is_dataclass(obj):
|
|
65
|
+
return {
|
|
66
|
+
f.metadata.get("json_name", f.name): to_dict(getattr(obj, f.name))
|
|
67
|
+
for f in fields(obj)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if isinstance(obj, list):
|
|
71
|
+
return [to_dict(item) for item in obj]
|
|
72
|
+
|
|
73
|
+
if isinstance(obj, dict):
|
|
74
|
+
return {key: to_dict(value) for key, value in obj.items()}
|
|
75
|
+
|
|
76
|
+
return obj
|
zosmfrestapi/zcrypto.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# pyright: reportMissingImports=false
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import cryptography
|
|
5
|
+
from cryptography.hazmat.primitives import serialization
|
|
6
|
+
|
|
7
|
+
USE_MODERN_CRYPTOGRAPHY = cryptography.__version__ != "3.3.2"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _extract_key_from_pkcs12(
|
|
11
|
+
p12_data: bytes,
|
|
12
|
+
password: str,
|
|
13
|
+
):
|
|
14
|
+
if USE_MODERN_CRYPTOGRAPHY:
|
|
15
|
+
# Use cryptography.pkcs12.load_key_and_certificates() whenever
|
|
16
|
+
# a modern cryptography release is available.
|
|
17
|
+
#
|
|
18
|
+
from cryptography.hazmat.primitives.serialization import pkcs12
|
|
19
|
+
|
|
20
|
+
private_key, _, _ = pkcs12.load_key_and_certificates(
|
|
21
|
+
p12_data,
|
|
22
|
+
password.encode(),
|
|
23
|
+
)
|
|
24
|
+
if private_key is None:
|
|
25
|
+
raise ValueError("PKCS#12 does not contain a private key")
|
|
26
|
+
|
|
27
|
+
return private_key.private_bytes(
|
|
28
|
+
encoding=serialization.Encoding.PEM,
|
|
29
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
30
|
+
encryption_algorithm=serialization.BestAvailableEncryption(password.encode()),
|
|
31
|
+
).decode("utf-8")
|
|
32
|
+
|
|
33
|
+
else: # pragma: no cover
|
|
34
|
+
# Fallback to OpenSSL.crypto.load_pkcs12() on legacy z/OS
|
|
35
|
+
# environments where only older cryptography versions are
|
|
36
|
+
# available.
|
|
37
|
+
#
|
|
38
|
+
# This fallback exists for two reasons:
|
|
39
|
+
# - Older z/OS environments could not easily use modern
|
|
40
|
+
# cryptography releases due to the Rust dependency.
|
|
41
|
+
# - cryptography 3.3.2 showed extremely poor PKCS#12
|
|
42
|
+
# performance on z/OS (~15 seconds versus <1 second
|
|
43
|
+
# with PyOpenSSL).
|
|
44
|
+
#
|
|
45
|
+
# Note that OpenSSL.crypto.load_pkcs12() was removed from
|
|
46
|
+
# PyOpenSSL 23.3.0 (2023-10-25). This code path is therefore
|
|
47
|
+
# intended solely for legacy environments.
|
|
48
|
+
from OpenSSL import crypto
|
|
49
|
+
|
|
50
|
+
return crypto.dump_privatekey(
|
|
51
|
+
crypto.FILETYPE_PEM,
|
|
52
|
+
crypto.load_pkcs12(p12_data, password.encode()).get_privatekey(),
|
|
53
|
+
cipher="aes-256-cbc",
|
|
54
|
+
passphrase=password.encode(),
|
|
55
|
+
).decode("utf-8")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import secrets
|
|
3
|
+
import ssl
|
|
4
|
+
from tempfile import NamedTemporaryFile
|
|
5
|
+
|
|
6
|
+
from zosmfrestapi.client import ZosmfClient
|
|
7
|
+
from zosmfrestapi.zcrypto import _extract_key_from_pkcs12
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ZosmfZcryptoClient(ZosmfClient):
|
|
11
|
+
"""Create an HTTP client for communicating with z/OSMF using a
|
|
12
|
+
client certificate stored in a SAF key ring.
|
|
13
|
+
|
|
14
|
+
This class behaves like ``ZosmfClient`` but retrieves the client
|
|
15
|
+
certificate and private key from a SAF key ring using ``py_zcrypto``.
|
|
16
|
+
The certificate material is exported to temporary PEM files and
|
|
17
|
+
configured automatically for mutual TLS (mTLS) authentication.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
Use a client with mTLS authentication using a certificate stored
|
|
21
|
+
in a SAF key ring:
|
|
22
|
+
|
|
23
|
+
with ZosmfClient(
|
|
24
|
+
"https://zosmf.example",
|
|
25
|
+
keyring="USERID/SAF_key_ring_name",
|
|
26
|
+
label="Certificate label",
|
|
27
|
+
) as client:
|
|
28
|
+
systems = client.get_systems()
|
|
29
|
+
|
|
30
|
+
Additional Args:
|
|
31
|
+
keyring: SAF key ring name. The format can be either
|
|
32
|
+
``keyring_name`` or ``USERID/keyring_name``.
|
|
33
|
+
|
|
34
|
+
label: Label of the certificate in the SAF key ring.
|
|
35
|
+
|
|
36
|
+
Notes:
|
|
37
|
+
The ``zcrypto`` optional dependency must be installed:
|
|
38
|
+
|
|
39
|
+
pip install "zosmfrestapi[zcrypto]"
|
|
40
|
+
|
|
41
|
+
All other arguments are inherited from ``ZosmfClient``.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, *args, **kwargs):
|
|
45
|
+
if platform.system() != "OS/390":
|
|
46
|
+
raise RuntimeError("The zcrypto backend is only available on z/OS.")
|
|
47
|
+
import py_zcrypto # pyright: ignore[reportMissingImports]
|
|
48
|
+
|
|
49
|
+
keyring = kwargs.pop("keyring")
|
|
50
|
+
label = kwargs.pop("label")
|
|
51
|
+
zcrypto_obj = py_zcrypto.zcrypto()
|
|
52
|
+
zcrypto_obj.open_key_ring(keyring)
|
|
53
|
+
password = secrets.token_urlsafe(32)
|
|
54
|
+
with NamedTemporaryFile(mode="w") as key_pem, NamedTemporaryFile(mode="w") as cert_pem:
|
|
55
|
+
|
|
56
|
+
key = _extract_key_from_pkcs12(
|
|
57
|
+
bytes(zcrypto_obj.export_key_to_buffer(password, label)), password
|
|
58
|
+
)
|
|
59
|
+
cert = ssl.DER_cert_to_PEM_cert(bytes(zcrypto_obj.export_cert_to_buffer(label)))
|
|
60
|
+
|
|
61
|
+
key_pem.write(key)
|
|
62
|
+
cert_pem.write(cert)
|
|
63
|
+
key_pem.flush()
|
|
64
|
+
cert_pem.flush()
|
|
65
|
+
|
|
66
|
+
super().__init__(
|
|
67
|
+
*args, **kwargs, cert=(cert_pem.name, key_pem.name), key_password=password
|
|
68
|
+
)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zosmfrestapi
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A Python wrapper for the z/OSMF REST API
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: z/OSMF,IBM Z,mainframe,REST API
|
|
8
|
+
Author: Oscar Curero
|
|
9
|
+
Author-email: dev@curero.es
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Provides-Extra: zcrypto
|
|
17
|
+
Requires-Dist: dacite (>=1.9.2,<2.0.0)
|
|
18
|
+
Requires-Dist: httpx2 (>=2.12.0,<3.0.0)
|
|
19
|
+
Requires-Dist: pendulum (>=3.2.0,<4.0.0)
|
|
20
|
+
Requires-Dist: py_zcrypto ; extra == "zcrypto"
|
|
21
|
+
Project-URL: Homepage, https://sr.ht/~ocurero/zosmfrestapi/
|
|
22
|
+
Project-URL: PyPI, https://pypi.org/project/zosmfrestapi/
|
|
23
|
+
Project-URL: Repository, https://sr.ht/~ocurero/zosmfrestapi/
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# A Python wrapper for the z/OSMF© REST API
|
|
27
|
+
|
|
28
|
+
[](https://www.repostatus.org/#active)
|
|
29
|
+
[](https://builds.sr.ht/~ocurero/zosmfrestapi/commits/main/.build.yml?)
|
|
30
|
+

|
|
31
|
+
[](https://opensource.org/licenses/MIT)
|
|
32
|
+
[](https://github.com/psf/black)
|
|
33
|
+
|
|
34
|
+
* Open Source: MIT License.
|
|
35
|
+
* Website: <https://sr.ht/~ocurero/zosmfrestapi/>
|
|
36
|
+
* Pypi: <https://pypi.org/project/zosmfrestapi/>
|
|
37
|
+
* Changelog: [CHANGELOG.md](CHANGELOG.md)
|
|
38
|
+
* Contributing: [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
39
|
+
|
|
40
|
+
Key features:
|
|
41
|
+
* **Pure Python**: Package and its dependencies are 100% pure Python (no C extensions required).
|
|
42
|
+
* **Cross-platform**: Works on Windows, Linux, macOS, and z/OS UNIX System Services.
|
|
43
|
+
* **Flexible authentication**: Supports user/password, LTPA and JWT tokens, and mutual TLS (mTLS).
|
|
44
|
+
* **Proxy ready**: Full support for standard HTTP/HTTPS proxies as well as authenticated proxies.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
Install
|
|
48
|
+
-------
|
|
49
|
+
|
|
50
|
+
**zosmfrestapi** requires Python 3.12 or newer and access to a z/OSMF server.
|
|
51
|
+
|
|
52
|
+
Install the package from PyPI:
|
|
53
|
+
|
|
54
|
+
```console
|
|
55
|
+
python -m pip install zosmfrestapi
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If you have downloaded or cloned the source code, run this from the project's
|
|
59
|
+
root folder instead:
|
|
60
|
+
|
|
61
|
+
```console
|
|
62
|
+
python -m pip install .
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## First request
|
|
66
|
+
|
|
67
|
+
Save this as `example.py` and run it with `python example.py`. Enter the base
|
|
68
|
+
URL of your z/OSMF server when prompted (for example,
|
|
69
|
+
`https://zosmf.example.com:443`). The URL should include `https://` and any
|
|
70
|
+
required port, but not an API path such as `/zosmf`.
|
|
71
|
+
|
|
72
|
+
The password is requested without displaying it and is not stored in the
|
|
73
|
+
script. Replace the sample operation with one your z/OSMF user is authorized to
|
|
74
|
+
perform.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from getpass import getpass
|
|
78
|
+
|
|
79
|
+
from zosmfrestapi import ZosmfClient, ZosmfError
|
|
80
|
+
|
|
81
|
+
server = input("z/OSMF server URL: ").strip().rstrip("/")
|
|
82
|
+
username = input("User ID: ").strip()
|
|
83
|
+
password = getpass("Password: ")
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
with ZosmfClient(server, username=username, password=password) as client:
|
|
87
|
+
for instance in client.list_swis():
|
|
88
|
+
print(f"{instance.name} ({instance.system})")
|
|
89
|
+
except ZosmfError as error:
|
|
90
|
+
print(f"The z/OSMF request failed: {error}")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The `with` block closes the network connection automatically, even if a request
|
|
94
|
+
fails. `list_swis()` returns Python objects; the example prints each software
|
|
95
|
+
instance's name and system. You can also query systems or read a data set. Put
|
|
96
|
+
these calls inside the `with` block shown above, before it ends:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
systems = client.get_systems()
|
|
100
|
+
contents = client.read_dataset("USER.DATASET")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
These calls must be made inside the `with` block, before the client is closed.
|
|
104
|
+
For a partitioned data set member, use a name such as
|
|
105
|
+
`USER.PDS(MEMBER)`.
|
|
106
|
+
|
|
107
|
+
## Authentication and TLS
|
|
108
|
+
|
|
109
|
+
The client uses the server's normal TLS certificate checks by default. Keep
|
|
110
|
+
those checks enabled for routine use. The HTTP transport is HTTPX2, a fork of
|
|
111
|
+
HTTPX; HTTPX2 uses `truststore` to integrate operating-system certificate
|
|
112
|
+
stores when it creates its default TLS context. This package currently passes
|
|
113
|
+
its own context, created with Python's `ssl.create_default_context()`, so the
|
|
114
|
+
certificates available to Python are used. If the z/OSMF server's CA is not
|
|
115
|
+
trusted in that environment, pass its certificate file with `cafile` below.
|
|
116
|
+
|
|
117
|
+
| Situation | Client option |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| User ID and password | `username="...", password="..."` |
|
|
120
|
+
| Client certificate and key in separate files | `cert=("client.crt", "client.key")` |
|
|
121
|
+
| Client certificate and key in one PEM file | `cert="client.pem"` |
|
|
122
|
+
| Internal certificate authority | `cafile="company-ca.pem"` |
|
|
123
|
+
| Existing LTPA or JWT token | `ltpa_token="..."` or `jwt_token="..."` |
|
|
124
|
+
| Network proxy | `proxy="http://proxy.example.com:8080"` |
|
|
125
|
+
|
|
126
|
+
For example, to trust an internal certificate authority, pass `cafile` when
|
|
127
|
+
creating the client:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
with ZosmfClient(server, username=username, password=password,
|
|
131
|
+
cafile="company-ca.pem") as client:
|
|
132
|
+
systems = client.get_systems()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Avoid putting passwords or tokens directly in scripts, committing them to
|
|
136
|
+
version control, or sharing them in logs. The `insecure=True` option disables
|
|
137
|
+
TLS certificate verification; use it only for temporary diagnostics, never as
|
|
138
|
+
a production fix. It cannot be combined with `cert` or `cafile`.
|
|
139
|
+
|
|
140
|
+
## What you can do
|
|
141
|
+
|
|
142
|
+
The client provides synchronous methods for:
|
|
143
|
+
|
|
144
|
+
* Authentication (`authenticate`).
|
|
145
|
+
* z/OSMF groups and systems (`get_groups`, `get_systems`).
|
|
146
|
+
* Data sets (`read_dataset`).
|
|
147
|
+
* Software instances and SMP/E information (`list_swis`, `get_swi_by_name`,
|
|
148
|
+
`get_swi_by_uuid`, and related query methods).
|
|
149
|
+
* Workflows (list, inspect, create, start, wait for, cancel, and archive).
|
|
150
|
+
* Software updates (search, inspect, start, monitor, and cancel).
|
|
151
|
+
|
|
152
|
+
Some operations change z/OSMF state. Check the method documentation and make
|
|
153
|
+
sure your account has the required permissions before using them.
|
|
154
|
+
|
|
155
|
+
## Troubleshooting
|
|
156
|
+
|
|
157
|
+
* **Python version error:** install Python 3.12 or newer, then recreate the
|
|
158
|
+
virtual environment with that interpreter.
|
|
159
|
+
* **401 / unauthorized:** check the server URL, user ID, password or token, and
|
|
160
|
+
whether the account is authorized for the requested API.
|
|
161
|
+
* **Certificate verification error:** ask your z/OSMF administrator for the
|
|
162
|
+
correct CA certificate and pass its path with `cafile`.
|
|
163
|
+
* **Connection or timeout error:** check that the server and port are reachable
|
|
164
|
+
from your machine and that any required proxy is configured.
|
|
165
|
+
* **PowerShell will not activate the virtual environment:** use the Python
|
|
166
|
+
executable inside `.venv\Scripts\python.exe` to run commands, or follow your
|
|
167
|
+
organization's PowerShell policy guidance. Do not change the machine-wide
|
|
168
|
+
execution policy just to install this package.
|
|
169
|
+
|
|
170
|
+
For the complete API, parameters, and return types, see the docstrings in the
|
|
171
|
+
package source and the project website.
|
|
172
|
+
|