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,114 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class WorkflowAutomationStatus:
|
|
7
|
+
"""Automation progress and current-step details for a workflow."""
|
|
8
|
+
|
|
9
|
+
startUser: str | None = None
|
|
10
|
+
startedTime: int | str | None = None
|
|
11
|
+
stoppedTime: int | str | None = None
|
|
12
|
+
currentStepName: str | None = None
|
|
13
|
+
currentStepNumber: str | None = None
|
|
14
|
+
currentStepTitle: str | None = None
|
|
15
|
+
messageID: str | None = None
|
|
16
|
+
messageText: str | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class WorkflowInstance:
|
|
21
|
+
"""Current state and optional detailed data for a workflow instance."""
|
|
22
|
+
|
|
23
|
+
name: str = field(metadata={"json_name": "workflowName"})
|
|
24
|
+
owner: str
|
|
25
|
+
system: str
|
|
26
|
+
scope: str = ""
|
|
27
|
+
percentComplete: int | None = None
|
|
28
|
+
workflowDefinitionFileMD5Value: str | None = None
|
|
29
|
+
workflowArchiveSAFID: str | None = None
|
|
30
|
+
jobStatement: str | list[str] | None = None
|
|
31
|
+
globalVariableGroup: str | None = None
|
|
32
|
+
jobsOutputDirectory: str | None = None
|
|
33
|
+
status: str | None = field(default="", metadata={"json_name": "statusName"})
|
|
34
|
+
vendor: str | None = None
|
|
35
|
+
deleteCompletedJobs: bool | None = None
|
|
36
|
+
autoDeleteOnCompletion: bool | None = None
|
|
37
|
+
isInstanceVariableWithoutPrefix: bool | None = None
|
|
38
|
+
workflowVersion: str | None = None
|
|
39
|
+
steps: list[dict[str, Any]] = field(default_factory=list)
|
|
40
|
+
access: str | None = None
|
|
41
|
+
productVersion: str | None = None
|
|
42
|
+
productID: str | None = None
|
|
43
|
+
variables: list[dict[str, Any]] = field(default_factory=list)
|
|
44
|
+
category: str | None = None
|
|
45
|
+
workflowDescription: str | None = None
|
|
46
|
+
workflowID: str | None = None
|
|
47
|
+
accountInfo: str | None = None
|
|
48
|
+
automationStatus: WorkflowAutomationStatus | None = None
|
|
49
|
+
containsParallelSteps: bool | None = None
|
|
50
|
+
isCallable: bool | None = None
|
|
51
|
+
workflowKey: str | None = None
|
|
52
|
+
productName: str | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class WorkflowListItem:
|
|
57
|
+
"""Summary record returned when listing active workflow instances."""
|
|
58
|
+
|
|
59
|
+
instanceURI: str | None = None
|
|
60
|
+
owner: str | None = None
|
|
61
|
+
vendor: str | None = None
|
|
62
|
+
workflowDefinitionFileMD5Value: str | None = None
|
|
63
|
+
workflowDescription: str | None = None
|
|
64
|
+
workflowID: str | None = None
|
|
65
|
+
workflowKey: str | None = None
|
|
66
|
+
workflowName: str | None = None
|
|
67
|
+
workflowVersion: str | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class WorkflowArchivedListItem:
|
|
72
|
+
"""Summary record returned when listing archived workflow instances."""
|
|
73
|
+
|
|
74
|
+
archivedInstanceURI: str | None = None
|
|
75
|
+
workflowKey: str | None = None
|
|
76
|
+
workflowName: str | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class WorkflowVariableItem:
|
|
81
|
+
"""Name and value of a workflow input variable."""
|
|
82
|
+
|
|
83
|
+
name: str | None = None
|
|
84
|
+
value: str | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class WorkflowItem:
|
|
89
|
+
"""Request data used to create a z/OSMF workflow instance."""
|
|
90
|
+
|
|
91
|
+
name: str = field(metadata={"json_name": "workflowName"})
|
|
92
|
+
definition: str = field(metadata={"json_name": "workflowDefinitionFile"})
|
|
93
|
+
system: str
|
|
94
|
+
owner: str
|
|
95
|
+
system_definition: str | None = field(
|
|
96
|
+
default=None,
|
|
97
|
+
metadata={
|
|
98
|
+
"json_name": "workflowDefinitionFileSystem",
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
variableInputFile: str | None = None
|
|
102
|
+
variables: list[WorkflowVariableItem] = field(default_factory=list)
|
|
103
|
+
resolveGlobalConflictByUsing: str | None = None
|
|
104
|
+
workflowArchiveSAFID: str | None = None
|
|
105
|
+
comments: str | None = None
|
|
106
|
+
assignToOwner: bool | None = None
|
|
107
|
+
accessType: str | None = None
|
|
108
|
+
accountInfo: str | None = None
|
|
109
|
+
jobStatement: str | list[str] | None = None
|
|
110
|
+
deleteCompletedJobs: bool | None = None
|
|
111
|
+
autoDeleteOnCompletion: bool | None = None
|
|
112
|
+
jobsOutputDirectory: str | None = None
|
|
113
|
+
targetSystemuid: str | None = None
|
|
114
|
+
targetSystempwd: str | None = field(default=None, repr=False)
|
zosmfrestapi/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import ssl
|
|
3
|
+
from types import TracebackType
|
|
4
|
+
from typing import Literal, Self
|
|
5
|
+
|
|
6
|
+
import httpx2
|
|
7
|
+
from zosmfrestapi.exceptions import ZosmfAuthError, ZosmfError
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ZosmfBase:
|
|
13
|
+
"""Shared HTTP client and request handling for the service mixins.
|
|
14
|
+
|
|
15
|
+
:class:`ZosmfClient` inherits the operations provided by the service mixins;
|
|
16
|
+
this base class configures their shared ``httpx2.Client`` and manages its
|
|
17
|
+
lifetime. Use a client as a context manager when possible so the connection
|
|
18
|
+
pool is closed deterministically.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
server: str,
|
|
24
|
+
username: str | None = None,
|
|
25
|
+
password: str | None = None,
|
|
26
|
+
cert: str | tuple[str, str] | None = None,
|
|
27
|
+
key_password: str | None = None,
|
|
28
|
+
ltpa_token: str | None = None,
|
|
29
|
+
jwt_token: str | None = None,
|
|
30
|
+
jwt_cookie_name: str = "jwtToken",
|
|
31
|
+
proxy: str | None = None,
|
|
32
|
+
proxyusername: str = "",
|
|
33
|
+
proxypassword: str = "",
|
|
34
|
+
insecure: bool = False,
|
|
35
|
+
cafile: None | str = None,
|
|
36
|
+
encoding: str | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Create an HTTP client for communicating with z/OSMF.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
server: Base URL of the z/OSMF server, including its scheme and
|
|
42
|
+
optional port or base path.
|
|
43
|
+
username: Optional user ID for HTTP basic authentication. Basic
|
|
44
|
+
authentication is configured only when both ``username`` and
|
|
45
|
+
``password`` are non-empty.
|
|
46
|
+
password: Password paired with ``username`` for basic
|
|
47
|
+
authentication.
|
|
48
|
+
cert: Optional client certificate configuration. Pass a path to a
|
|
49
|
+
PEM file containing both the certificate and private key, or
|
|
50
|
+
a ``(certificate_path, private_key_path)`` tuple for separate
|
|
51
|
+
files.
|
|
52
|
+
key_password: Optional private key password. Pass the private key
|
|
53
|
+
password if the key is password protected.
|
|
54
|
+
ltpa_token: Optional LTPA token sent as the ``LtpaToken2`` cookie.
|
|
55
|
+
jwt_token: Optional JWT sent as the cookie named by
|
|
56
|
+
``jwt_cookie_name``. If both token types are provided, both
|
|
57
|
+
cookies are sent.
|
|
58
|
+
jwt_cookie_name: Cookie name used for ``jwt_token``. Defaults to
|
|
59
|
+
``"jwtToken"``.
|
|
60
|
+
proxy: Optional proxy URL used for HTTP requests.
|
|
61
|
+
proxyusername: Optional proxy user ID. Used with
|
|
62
|
+
``proxypassword`` when proxy authentication is configured.
|
|
63
|
+
proxypassword: Optional proxy password.
|
|
64
|
+
insecure: Disable TLS certificate verification when true. This
|
|
65
|
+
cannot be combined with ``cert`` or ``cafile``.
|
|
66
|
+
cafile: Optional CA certificate file added to the default trust
|
|
67
|
+
store. It can be combined with ``cert``.
|
|
68
|
+
encoding: Reserved compatibility parameter; currently unused.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ZosmfError: If ``insecure`` is combined with ``cert`` or ``cafile``.
|
|
72
|
+
"""
|
|
73
|
+
if insecure and (cafile or cert):
|
|
74
|
+
raise ZosmfError("Cannot set insecure mode when cafile or cert is provided.")
|
|
75
|
+
|
|
76
|
+
verify: bool | ssl.SSLContext
|
|
77
|
+
if insecure:
|
|
78
|
+
logger.debug("Insecure mode enabled, TLS certificate verification is disabled.")
|
|
79
|
+
verify = False
|
|
80
|
+
else:
|
|
81
|
+
ctx = ssl.create_default_context()
|
|
82
|
+
if isinstance(cert, tuple):
|
|
83
|
+
certfile, keyfile = cert
|
|
84
|
+
ctx.load_cert_chain(certfile=certfile, keyfile=keyfile, password=key_password)
|
|
85
|
+
elif cert:
|
|
86
|
+
ctx.load_cert_chain(certfile=cert)
|
|
87
|
+
if cafile:
|
|
88
|
+
ctx.load_verify_locations(cafile=cafile)
|
|
89
|
+
verify = ctx # <------ linea 88
|
|
90
|
+
|
|
91
|
+
proxy_config: str | httpx2.Proxy | None = proxy
|
|
92
|
+
if proxy:
|
|
93
|
+
proxy_config = httpx2.Proxy(proxy)
|
|
94
|
+
proxy_config.auth = (
|
|
95
|
+
(proxyusername, proxypassword) if proxyusername or proxypassword else None
|
|
96
|
+
)
|
|
97
|
+
auth = (username, password) if username and password else None
|
|
98
|
+
headers = {}
|
|
99
|
+
if ltpa_token:
|
|
100
|
+
headers = {"Cookie": f"LtpaToken2={ltpa_token}"}
|
|
101
|
+
if jwt_token:
|
|
102
|
+
headers = {"Cookie": f"{jwt_cookie_name}={jwt_token}"}
|
|
103
|
+
if ltpa_token and jwt_token:
|
|
104
|
+
headers = {"Cookie": f"LtpaToken2={ltpa_token}; {jwt_cookie_name}={jwt_token}"}
|
|
105
|
+
self.http = httpx2.Client(
|
|
106
|
+
base_url=server,
|
|
107
|
+
proxy=proxy_config,
|
|
108
|
+
verify=verify,
|
|
109
|
+
auth=auth,
|
|
110
|
+
headers=headers,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self.base_url = server
|
|
114
|
+
self.username = username
|
|
115
|
+
self.password = password
|
|
116
|
+
self.insecure = insecure
|
|
117
|
+
self.cert = cert
|
|
118
|
+
self.cafile = cafile
|
|
119
|
+
self.ltpa_token = ltpa_token
|
|
120
|
+
self.jwt_token = jwt_token
|
|
121
|
+
self.jwt_cookie_name = jwt_cookie_name
|
|
122
|
+
self.proxy = proxy_config
|
|
123
|
+
self.proxyusername = proxyusername
|
|
124
|
+
self.proxypassword = proxypassword
|
|
125
|
+
|
|
126
|
+
def _raise_for_status(
|
|
127
|
+
self,
|
|
128
|
+
response: httpx2.Response,
|
|
129
|
+
exc_handler: type[ZosmfError] = ZosmfError,
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Raise a package exception for an unsuccessful HTTP response.
|
|
132
|
+
|
|
133
|
+
HTTP 401 responses are translated to :class:`ZosmfAuthError`; other
|
|
134
|
+
HTTP status errors are wrapped using ``exc_handler``.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
response: Response returned by the shared HTTP client.
|
|
138
|
+
exc_handler: Exception class used for non-authentication status
|
|
139
|
+
errors. Defaults to :class:`ZosmfError`.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
ZosmfAuthError: If the response status is 401.
|
|
143
|
+
ZosmfError: Or ``exc_handler`` for another unsuccessful status.
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
response.raise_for_status()
|
|
147
|
+
except httpx2.HTTPStatusError as exc:
|
|
148
|
+
if exc.response.status_code == 401:
|
|
149
|
+
raise ZosmfAuthError(exc) from exc
|
|
150
|
+
raise exc_handler(exc) from exc
|
|
151
|
+
|
|
152
|
+
def close(self) -> None:
|
|
153
|
+
"""Close the shared HTTP client and release its connection resources.
|
|
154
|
+
|
|
155
|
+
Call this when the client is no longer needed. Exiting a ``with`` block
|
|
156
|
+
also calls this method.
|
|
157
|
+
"""
|
|
158
|
+
self.http.close()
|
|
159
|
+
|
|
160
|
+
def __enter__(self) -> Self:
|
|
161
|
+
"""Return this client for use in a ``with`` statement."""
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def __exit__(
|
|
165
|
+
self,
|
|
166
|
+
exception_type: type[BaseException] | None,
|
|
167
|
+
exception: BaseException | None,
|
|
168
|
+
traceback: TracebackType | None,
|
|
169
|
+
) -> Literal[False]:
|
|
170
|
+
"""Close the HTTP client without suppressing an exception from the block.
|
|
171
|
+
|
|
172
|
+
The exception arguments follow the context-manager protocol; they are
|
|
173
|
+
not inspected by this implementation.
|
|
174
|
+
"""
|
|
175
|
+
self.close()
|
|
176
|
+
return False
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from zosmfrestapi.exceptions import ZosmfAuthError
|
|
4
|
+
from zosmfrestapi.exceptions.authentication import ZosmfLoginError
|
|
5
|
+
from zosmfrestapi.models.authentication import LoginOk
|
|
6
|
+
|
|
7
|
+
from . import ZosmfBase
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthenticationServices(ZosmfBase):
|
|
13
|
+
"""Authentication operations for the shared z/OSMF client.
|
|
14
|
+
|
|
15
|
+
The configured credentials and tokens are sent by the shared HTTP client.
|
|
16
|
+
The client certificate, basic authentication, and token cookies are all
|
|
17
|
+
configured during :class:`ZosmfBase` initialization.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def authenticate(self) -> LoginOk | None:
|
|
21
|
+
"""Authenticate against the z/OSMF authentication endpoint.
|
|
22
|
+
|
|
23
|
+
This method posts to ``/zosmf/services/authenticate`` using the
|
|
24
|
+
authentication configured when the client was created. It does not
|
|
25
|
+
perform a separate credential negotiation or clear the configured
|
|
26
|
+
token cookies.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
A ``LoginOk`` containing the response return/reason codes and
|
|
30
|
+
message, plus any LTPA or JWT tokens returned as response cookies.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ZosmfLoginError: If z/OSMF responds with HTTP 401.
|
|
34
|
+
ZosmfAuthError: If another unsuccessful HTTP status is returned.
|
|
35
|
+
httpx2.RequestError: If the request fails before an HTTP response
|
|
36
|
+
is received.
|
|
37
|
+
"""
|
|
38
|
+
logger.debug("Authenticating with z/OSMF using all available methods")
|
|
39
|
+
resp = self.http.post("/zosmf/services/authenticate")
|
|
40
|
+
if resp.status_code == 200:
|
|
41
|
+
logger.debug("Authentication succeeded with z/OSMF")
|
|
42
|
+
return LoginOk(
|
|
43
|
+
**resp.json(),
|
|
44
|
+
ltpa_token=resp.cookies.get("LtpaToken2", self.ltpa_token),
|
|
45
|
+
jwt_token=resp.cookies.get(self.jwt_cookie_name, self.jwt_token),
|
|
46
|
+
)
|
|
47
|
+
elif resp.status_code == 401:
|
|
48
|
+
logger.debug("Authentication failed with z/OSMF")
|
|
49
|
+
raise ZosmfLoginError(resp)
|
|
50
|
+
logger.debug("Unexpected authentication failure with z/OSMF")
|
|
51
|
+
self._raise_for_status(resp, exc_handler=ZosmfAuthError)
|
|
52
|
+
raise AssertionError("Unreachable") # pragma: no cover
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from zosmfrestapi.exceptions.dataset import ZosmfDatasetError, ZosmfDatasetNotFoundError
|
|
4
|
+
|
|
5
|
+
from . import ZosmfBase
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DatasetServices(ZosmfBase):
|
|
11
|
+
"""Read sequential or partitioned data sets through z/OSMF Files REST APIs."""
|
|
12
|
+
|
|
13
|
+
def _read_dataset(self, path: str) -> str:
|
|
14
|
+
resp = self.http.get(path)
|
|
15
|
+
if resp.status_code == 404:
|
|
16
|
+
raise ZosmfDatasetNotFoundError(resp)
|
|
17
|
+
self._raise_for_status(resp, exc_handler=ZosmfDatasetError)
|
|
18
|
+
return resp.text
|
|
19
|
+
|
|
20
|
+
def read_dataset(self, dataset: str, volser: str | None = None) -> str:
|
|
21
|
+
"""Read a dataset or dataset member through the z/OSMF Files REST API.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
dataset: Fully qualified data set name, optionally including a
|
|
25
|
+
member in parentheses, for example ``USER.PDS(MEMBER)``.
|
|
26
|
+
volser: Optional volume serial for a data set that is not cataloged.
|
|
27
|
+
When supplied, it is included in the z/OSMF request path.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The contents returned by z/OSMF as text.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ZosmfDatasetNotFoundError: If z/OSMF returns HTTP 404 for the data
|
|
34
|
+
set request.
|
|
35
|
+
ZosmfAuthError: If z/OSMF rejects the request with HTTP 401.
|
|
36
|
+
ZosmfDatasetError: If z/OSMF returns another unsuccessful HTTP
|
|
37
|
+
status.
|
|
38
|
+
httpx2.HTTPError: If a transport-level HTTP error occurs.
|
|
39
|
+
"""
|
|
40
|
+
if volser:
|
|
41
|
+
return self._read_dataset(f"/zosmf/restfiles/ds/-({volser})/{dataset}")
|
|
42
|
+
return self._read_dataset(f"/zosmf/restfiles/ds/{dataset}")
|