hcs-core 0.1.250__py3-none-any.whl → 0.1.316__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.
- hcs_core/__init__.py +1 -0
- hcs_core/ctxp/__init__.py +12 -4
- hcs_core/ctxp/_init.py +94 -22
- hcs_core/ctxp/built_in_cmds/_ut.py +4 -3
- hcs_core/ctxp/built_in_cmds/context.py +16 -1
- hcs_core/ctxp/built_in_cmds/profile.py +30 -11
- hcs_core/ctxp/cli_options.py +34 -13
- hcs_core/ctxp/cli_processor.py +33 -20
- hcs_core/ctxp/cmd_util.py +87 -0
- hcs_core/ctxp/config.py +1 -1
- hcs_core/ctxp/context.py +82 -3
- hcs_core/ctxp/data_util.py +56 -20
- hcs_core/ctxp/dispatcher.py +82 -0
- hcs_core/ctxp/duration.py +65 -0
- hcs_core/ctxp/extension.py +7 -6
- hcs_core/ctxp/fn_util.py +57 -0
- hcs_core/ctxp/fstore.py +39 -22
- hcs_core/ctxp/jsondot.py +259 -78
- hcs_core/ctxp/logger.py +7 -6
- hcs_core/ctxp/profile.py +53 -21
- hcs_core/ctxp/profile_store.py +1 -0
- hcs_core/ctxp/recent.py +3 -3
- hcs_core/ctxp/state.py +4 -3
- hcs_core/ctxp/task_schd.py +168 -0
- hcs_core/ctxp/telemetry.py +145 -0
- hcs_core/ctxp/template_util.py +21 -0
- hcs_core/ctxp/timeutil.py +11 -0
- hcs_core/ctxp/util.py +194 -33
- hcs_core/ctxp/var_template.py +3 -4
- hcs_core/plan/__init__.py +11 -5
- hcs_core/plan/base_provider.py +1 -0
- hcs_core/plan/core.py +29 -26
- hcs_core/plan/dag.py +15 -12
- hcs_core/plan/helper.py +4 -2
- hcs_core/plan/kop.py +21 -8
- hcs_core/plan/provider/dev/dummy.py +3 -3
- hcs_core/sglib/auth.py +137 -95
- hcs_core/sglib/cli_options.py +20 -5
- hcs_core/sglib/client_util.py +230 -62
- hcs_core/sglib/csp.py +73 -6
- hcs_core/sglib/ez_client.py +139 -41
- hcs_core/sglib/hcs_client.py +3 -9
- hcs_core/sglib/init.py +17 -0
- hcs_core/sglib/login_support.py +22 -83
- hcs_core/sglib/payload_util.py +3 -1
- hcs_core/sglib/requtil.py +38 -0
- hcs_core/sglib/utils.py +107 -0
- hcs_core/util/check_license.py +0 -2
- hcs_core/util/duration.py +6 -3
- hcs_core/util/job_view.py +35 -15
- hcs_core/util/pki_util.py +48 -1
- hcs_core/util/query_util.py +54 -8
- hcs_core/util/scheduler.py +3 -3
- hcs_core/util/ssl_util.py +1 -1
- hcs_core/util/versions.py +15 -12
- hcs_core-0.1.316.dist-info/METADATA +54 -0
- hcs_core-0.1.316.dist-info/RECORD +69 -0
- {hcs_core-0.1.250.dist-info → hcs_core-0.1.316.dist-info}/WHEEL +1 -2
- hcs_core-0.1.250.dist-info/METADATA +0 -36
- hcs_core-0.1.250.dist-info/RECORD +0 -59
- hcs_core-0.1.250.dist-info/top_level.txt +0 -1
hcs_core/sglib/client_util.py
CHANGED
|
@@ -13,78 +13,228 @@ See the License for the specific language governing permissions and
|
|
|
13
13
|
limitations under the License.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import sys
|
|
19
|
+
import threading
|
|
17
20
|
import time
|
|
18
|
-
from
|
|
19
|
-
from hcs_core.sglib import hcs_client
|
|
20
|
-
from hcs_core.util.query_util import with_query, PageRequest
|
|
21
|
-
from hcs_core.util import duration
|
|
22
|
-
from hcs_core.util import exit
|
|
21
|
+
from typing import Callable, Iterator
|
|
23
22
|
|
|
23
|
+
from hcs_core.ctxp import CtxpException, panic, profile
|
|
24
|
+
from hcs_core.sglib.ez_client import EzClient
|
|
25
|
+
from hcs_core.util import duration, exit
|
|
26
|
+
from hcs_core.util.query_util import PageRequest, with_query
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
url = profile.current().hcs.url
|
|
27
|
-
if not url.endswith("/"):
|
|
28
|
-
url += "/"
|
|
29
|
-
url += service_name
|
|
30
|
-
return hcs_client(url)
|
|
28
|
+
log = logging.getLogger(__name__)
|
|
31
29
|
|
|
30
|
+
_caches = {}
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
_client_instance_lock = threading.RLock()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_service_override(service_name: str):
|
|
36
|
+
profile_data = profile.current()
|
|
37
|
+
override = profile_data.get("override", {})
|
|
38
|
+
service_override = {}
|
|
39
|
+
for k, v in override.items():
|
|
40
|
+
if service_name == k.lower():
|
|
41
|
+
service_override = v
|
|
42
|
+
|
|
43
|
+
if not service_override:
|
|
44
|
+
if service_name == "org-service":
|
|
45
|
+
service_override = override.get("org", {})
|
|
46
|
+
elif service_name.find("-") >= 0:
|
|
47
|
+
camel_name = "".join(word.capitalize() for word in service_name.split("-"))
|
|
48
|
+
camel_name = camel_name[0].lower() + camel_name[1:]
|
|
49
|
+
service_override = override.get(camel_name, {})
|
|
50
|
+
return service_override
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _lazy_init(service_name: str, hdc: str = None, region: str = None): # make it deferred so no need to initialize profile
|
|
54
|
+
if region and hdc:
|
|
55
|
+
raise Exception("region and hdc cannot be specified at the same time.")
|
|
56
|
+
|
|
57
|
+
profile_data = profile.current()
|
|
58
|
+
|
|
59
|
+
service_override = _get_service_override(service_name)
|
|
60
|
+
service_override_url = service_override.get("url")
|
|
61
|
+
if service_override_url:
|
|
62
|
+
log.debug(f"Using per-service override for {service_name}: {service_override_url}")
|
|
63
|
+
url = service_override_url
|
|
64
|
+
else:
|
|
65
|
+
if hdc:
|
|
66
|
+
# prod only
|
|
67
|
+
hdc = hdc.upper()
|
|
68
|
+
if hdc == "US":
|
|
69
|
+
url = profile_data.hcs.url
|
|
70
|
+
elif hdc == "EU":
|
|
71
|
+
url = profile_data.hcs.url.replace("cloud-sg-us", "cloud-sg-eu")
|
|
72
|
+
elif hdc == "JP":
|
|
73
|
+
url = profile_data.hcs.url.replace("cloud-sg-us", "cloud-sg-jp")
|
|
74
|
+
else:
|
|
75
|
+
raise CtxpException(f"Invalid HDC name: {hdc}. Supported HDC names: US, EU, JP.")
|
|
76
|
+
elif region:
|
|
77
|
+
url = _get_region_url(region)
|
|
78
|
+
if not url:
|
|
79
|
+
panic("Missing profile property: hcs.regions")
|
|
80
|
+
else:
|
|
81
|
+
url = profile_data.hcs.url
|
|
82
|
+
url = url.rstrip("/") + "/" + service_name
|
|
83
|
+
|
|
84
|
+
# TODO
|
|
85
|
+
client_id = service_override.get("client-id", None)
|
|
86
|
+
if not client_id:
|
|
87
|
+
client_id = service_override.get("clientId", None)
|
|
88
|
+
client_secret = service_override.get("client-secret", None)
|
|
89
|
+
if not client_secret:
|
|
90
|
+
client_secret = service_override.get("clientSecret", None)
|
|
91
|
+
api_token = service_override.get("api-token", None)
|
|
92
|
+
if not api_token:
|
|
93
|
+
api_token = service_override.get("apiToken", None)
|
|
94
|
+
provider = service_override.get("provider", "vmwarecsp")
|
|
95
|
+
if provider == "vmwarecsp":
|
|
96
|
+
token_url = profile_data.csp.url
|
|
97
|
+
elif provider == "auth-service":
|
|
98
|
+
token_url = profile_data.auth["tokenUrl"]
|
|
99
|
+
else:
|
|
100
|
+
raise CtxpException(f"Unknown provider: {provider}. Supported providers: vmwarecsp, auth-service.")
|
|
101
|
+
|
|
102
|
+
if client_id:
|
|
103
|
+
if not client_secret:
|
|
104
|
+
panic(f"Client ID is specified but missing clientSecret for service {service_name} in profile override.")
|
|
105
|
+
else:
|
|
106
|
+
if client_secret:
|
|
107
|
+
panic(f"Client secret is specified but missing clientId for service {service_name} in profile override.")
|
|
108
|
+
|
|
109
|
+
if client_id:
|
|
110
|
+
auth_config = {
|
|
111
|
+
"url": token_url,
|
|
112
|
+
"client_id": client_id,
|
|
113
|
+
"client_secret": client_secret,
|
|
114
|
+
}
|
|
115
|
+
elif api_token:
|
|
116
|
+
auth_config = {
|
|
117
|
+
"url": token_url,
|
|
118
|
+
"api_token": api_token,
|
|
119
|
+
}
|
|
120
|
+
else:
|
|
121
|
+
auth_config = None
|
|
122
|
+
|
|
123
|
+
from hcs_core.sglib import auth
|
|
124
|
+
|
|
125
|
+
oauth_client = auth.oauth_client(auth_config=auth_config)
|
|
126
|
+
return url, oauth_client
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def hdc_service_client(service_name: str, hdc: str = None) -> EzClient:
|
|
130
|
+
"""A client for HDC service. Cached per service name, thread-safe, lazy initialization."""
|
|
131
|
+
if hdc:
|
|
132
|
+
hdc = hdc.upper()
|
|
133
|
+
if hdc not in ["US", "EU", "JP"]:
|
|
134
|
+
panic(f"Invalid HDC name: {hdc}. Supported HDC names: US, EU, JP.")
|
|
135
|
+
else:
|
|
136
|
+
hdc = "US"
|
|
137
|
+
|
|
138
|
+
k = f"{service_name}#{hdc}"
|
|
139
|
+
with _client_instance_lock:
|
|
140
|
+
instance = _caches.get(k)
|
|
141
|
+
if not instance:
|
|
142
|
+
instance = EzClient(lazy_init=lambda: _lazy_init(service_name=service_name, hdc=hdc))
|
|
143
|
+
_caches[k] = instance
|
|
144
|
+
return instance
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _get_region_url(region: str):
|
|
34
148
|
regions = profile.current().hcs.regions
|
|
35
|
-
if not
|
|
149
|
+
if not region:
|
|
36
150
|
return regions[0].url
|
|
37
151
|
for r in regions:
|
|
38
|
-
if r.name.lower() ==
|
|
152
|
+
if r.name.lower() == region.lower():
|
|
39
153
|
return r.url
|
|
40
154
|
names = []
|
|
41
155
|
for r in regions:
|
|
42
156
|
names.append(r.name)
|
|
43
|
-
panic(f"Region not found: {
|
|
157
|
+
panic(f"Region not found: {region}. Available regions: {names}")
|
|
44
158
|
|
|
45
159
|
|
|
46
|
-
def regional_service_client(
|
|
160
|
+
def regional_service_client(service_name: str, region: str = None):
|
|
47
161
|
# 'https://dev1b-westus2-cp103a.azcp.horizon.vmware.com/vmhub'
|
|
48
|
-
|
|
49
|
-
if
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
162
|
+
region_names = [r.name.lower() for r in profile.current().hcs.regions]
|
|
163
|
+
if region:
|
|
164
|
+
region = region.lower()
|
|
165
|
+
if region not in region_names:
|
|
166
|
+
panic(f"Invalid region name: {region}. Available regions: {', '.join(region_names)}")
|
|
167
|
+
else:
|
|
168
|
+
region = region_names[0]
|
|
55
169
|
|
|
170
|
+
k = f"{service_name}#{region}"
|
|
171
|
+
with _client_instance_lock:
|
|
172
|
+
instance = _caches.get(k)
|
|
173
|
+
if not instance:
|
|
174
|
+
instance = EzClient(lazy_init=lambda: _lazy_init(service_name=service_name, region=region))
|
|
175
|
+
_caches[k] = instance
|
|
176
|
+
return instance
|
|
56
177
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
178
|
+
|
|
179
|
+
def is_regional_service(service_name: str):
|
|
180
|
+
regional_services = ["vmhub", "connection-service"]
|
|
181
|
+
return service_name in regional_services
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def service_client(service_name: str, region: str = None, hdc: str = None):
|
|
185
|
+
if is_regional_service(service_name):
|
|
186
|
+
return regional_service_client(service_name, region)
|
|
187
|
+
else:
|
|
188
|
+
return hdc_service_client(service_name, hdc)
|
|
63
189
|
|
|
64
190
|
|
|
65
191
|
class default_crud:
|
|
66
192
|
def __init__(self, client, base_context: str, resource_type_name: str):
|
|
67
|
-
self.
|
|
193
|
+
self._client_impl = client
|
|
68
194
|
self._base_context = base_context
|
|
69
195
|
self._resource_type_name = resource_type_name
|
|
70
196
|
|
|
197
|
+
def _client(self):
|
|
198
|
+
if callable(self._client_impl):
|
|
199
|
+
self._client_impl = self._client_impl()
|
|
200
|
+
elif isinstance(self._client_impl, str):
|
|
201
|
+
self._client_impl = hdc_service_client(self._client_impl)
|
|
202
|
+
else:
|
|
203
|
+
pass
|
|
204
|
+
if isinstance(self._client_impl, EzClient):
|
|
205
|
+
return self._client_impl
|
|
206
|
+
raise CtxpException(f"Invalid client implementation: {self._client_impl}")
|
|
207
|
+
|
|
71
208
|
def get(self, id: str, org_id: str, **kwargs):
|
|
72
209
|
if org_id:
|
|
73
210
|
kwargs["org_id"] = org_id
|
|
211
|
+
kwargs["orgId"] = org_id
|
|
74
212
|
url = with_query(f"{self._base_context}/{id}", **kwargs)
|
|
75
213
|
# print(url)
|
|
76
|
-
return self._client.get(url)
|
|
214
|
+
return self._client().get(url)
|
|
77
215
|
|
|
78
|
-
def list(self, org_id: str, **kwargs):
|
|
216
|
+
def list(self, org_id: str, fn_filter: Callable = None, **kwargs) -> list:
|
|
79
217
|
if org_id:
|
|
80
218
|
kwargs["org_id"] = org_id
|
|
219
|
+
kwargs["orgId"] = org_id
|
|
81
220
|
|
|
82
221
|
def _get_page(query_string):
|
|
83
222
|
url = self._base_context + "?" + query_string
|
|
84
223
|
# print(url)
|
|
85
|
-
return self._client.get(url)
|
|
224
|
+
return self._client().get(url)
|
|
225
|
+
|
|
226
|
+
return PageRequest(_get_page, fn_filter, **kwargs).get()
|
|
227
|
+
|
|
228
|
+
def items(self, org_id: str, fn_filter: Callable = None, **kwargs) -> Iterator:
|
|
229
|
+
if org_id:
|
|
230
|
+
kwargs["org_id"] = org_id
|
|
231
|
+
kwargs["orgId"] = org_id
|
|
232
|
+
|
|
233
|
+
def _get_page(query_string):
|
|
234
|
+
url = self._base_context + "?" + query_string
|
|
235
|
+
return self._client().get(url)
|
|
86
236
|
|
|
87
|
-
return PageRequest(_get_page, **kwargs).
|
|
237
|
+
return PageRequest(_get_page, fn_filter, **kwargs).items()
|
|
88
238
|
|
|
89
239
|
def create(self, payload: dict, headers: dict = None, **kwargs):
|
|
90
240
|
url = with_query(f"{self._base_context}", **kwargs)
|
|
@@ -92,32 +242,37 @@ class default_crud:
|
|
|
92
242
|
# import json
|
|
93
243
|
# print(json.dumps(payload, indent=4))
|
|
94
244
|
if isinstance(payload, str):
|
|
95
|
-
return self._client.post(url, text=payload, headers=headers)
|
|
245
|
+
return self._client().post(url, text=payload, headers=headers)
|
|
96
246
|
if isinstance(payload, dict):
|
|
97
|
-
return self._client.post(url, json=payload, headers=headers)
|
|
98
|
-
return self._client.post(url, json=payload, headers=headers)
|
|
247
|
+
return self._client().post(url, json=payload, headers=headers)
|
|
248
|
+
return self._client().post(url, json=payload, headers=headers)
|
|
99
249
|
|
|
100
250
|
def upload(self, files, **kwargs):
|
|
101
251
|
url = with_query(f"{self._base_context}", **kwargs)
|
|
102
|
-
return self._client.post(url, files=files)
|
|
252
|
+
return self._client().post(url, files=files)
|
|
103
253
|
|
|
104
254
|
def delete(self, id: str, org_id: str, **kwargs):
|
|
105
255
|
if org_id:
|
|
106
256
|
kwargs["org_id"] = org_id
|
|
257
|
+
kwargs["orgId"] = org_id
|
|
107
258
|
url = with_query(f"{self._base_context}/{id}", **kwargs)
|
|
108
259
|
# print(url)
|
|
109
|
-
return self._client.delete(url)
|
|
260
|
+
return self._client().delete(url)
|
|
110
261
|
|
|
111
262
|
def wait_for_deleted(self, id: str, org_id: str, timeout: str, fn_is_error: Callable = None):
|
|
112
263
|
name = self._resource_type_name + "/" + id
|
|
113
|
-
|
|
264
|
+
|
|
265
|
+
def fn_get():
|
|
266
|
+
return self.get(id, org_id)
|
|
267
|
+
|
|
114
268
|
return wait_for_res_deleted(name, fn_get, timeout=timeout, fn_is_error=fn_is_error)
|
|
115
269
|
|
|
116
270
|
def update(self, id: str, org_id: str, data: dict, **kwargs):
|
|
117
271
|
if org_id:
|
|
118
272
|
kwargs["org_id"] = org_id
|
|
273
|
+
kwargs["orgId"] = org_id
|
|
119
274
|
url = with_query(f"{self._base_context}/{id}")
|
|
120
|
-
return self._client.patch(url, data)
|
|
275
|
+
return self._client().patch(url, data)
|
|
121
276
|
|
|
122
277
|
|
|
123
278
|
def _parse_timeout(timeout: str):
|
|
@@ -133,10 +288,13 @@ def wait_for_res_deleted(
|
|
|
133
288
|
resource_name: str,
|
|
134
289
|
fn_get: Callable,
|
|
135
290
|
timeout: str,
|
|
136
|
-
|
|
291
|
+
polling_interval: int = 10,
|
|
137
292
|
fn_is_error: Callable = None,
|
|
138
293
|
):
|
|
139
294
|
timeout_seconds = _parse_timeout(timeout)
|
|
295
|
+
polling_interval_seconds = _parse_timeout(polling_interval)
|
|
296
|
+
if polling_interval_seconds < 3:
|
|
297
|
+
polling_interval_seconds = 3
|
|
140
298
|
start = time.time()
|
|
141
299
|
while True:
|
|
142
300
|
t = fn_get()
|
|
@@ -155,9 +313,10 @@ def wait_for_res_deleted(
|
|
|
155
313
|
sleep_seconds = remaining_seconds
|
|
156
314
|
if sleep_seconds > polling_interval_seconds:
|
|
157
315
|
sleep_seconds = polling_interval_seconds
|
|
158
|
-
|
|
316
|
+
time.sleep(sleep_seconds)
|
|
159
317
|
|
|
160
318
|
|
|
319
|
+
# flake8: noqa=E731
|
|
161
320
|
def wait_for_res_status(
|
|
162
321
|
resource_name: str,
|
|
163
322
|
fn_get: Callable,
|
|
@@ -181,21 +340,25 @@ def wait_for_res_status(
|
|
|
181
340
|
field_name = get_status
|
|
182
341
|
get_status = lambda t: t[field_name]
|
|
183
342
|
if status_map:
|
|
184
|
-
if isinstance(status_map["ready"], str):
|
|
185
|
-
status_map["ready"] = [status_map["ready"]]
|
|
186
|
-
if isinstance(status_map["transition"], str):
|
|
187
|
-
status_map["transition"] = [status_map["transition"]]
|
|
188
|
-
if isinstance(status_map["error"], str):
|
|
189
|
-
status_map["error"] = [status_map["error"]]
|
|
190
343
|
if is_ready:
|
|
191
344
|
raise CtxpException("Can not specify is_ready when status_map is provided.")
|
|
192
345
|
if is_error:
|
|
193
346
|
raise CtxpException("Can not specify is_error when status_map is provided.")
|
|
194
347
|
if is_transition:
|
|
195
348
|
raise CtxpException("Can not specify is_transition when status_map is provided.")
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
349
|
+
|
|
350
|
+
ready_status = status_map["ready"]
|
|
351
|
+
error_status = status_map["error"]
|
|
352
|
+
transition_status = status_map["transition"]
|
|
353
|
+
if isinstance(ready_status, str):
|
|
354
|
+
ready_status = [ready_status]
|
|
355
|
+
if isinstance(error_status, str):
|
|
356
|
+
error_status = [error_status]
|
|
357
|
+
if isinstance(transition_status, str):
|
|
358
|
+
transition_status = [transition_status]
|
|
359
|
+
is_ready = lambda s: s in ready_status
|
|
360
|
+
is_error = lambda s: error_status is None or s in error_status
|
|
361
|
+
is_transition = lambda s: transition_status is None or s in transition_status
|
|
199
362
|
else:
|
|
200
363
|
if not is_ready:
|
|
201
364
|
raise CtxpException("Either status_map or is_ready must be specified.")
|
|
@@ -203,6 +366,9 @@ def wait_for_res_status(
|
|
|
203
366
|
raise CtxpException("Either status_map or is_error must be specified.")
|
|
204
367
|
if not is_transition:
|
|
205
368
|
raise CtxpException("Either status_map or is_transition must be specified.")
|
|
369
|
+
ready_status = None
|
|
370
|
+
error_status = None
|
|
371
|
+
transition_status = None
|
|
206
372
|
|
|
207
373
|
while True:
|
|
208
374
|
t = fn_get()
|
|
@@ -211,26 +377,28 @@ def wait_for_res_status(
|
|
|
211
377
|
return
|
|
212
378
|
raise CtxpException(prefix + "Not found.")
|
|
213
379
|
status = get_status(t)
|
|
214
|
-
# print(f"DDD - status {status}")
|
|
215
380
|
if is_error(status):
|
|
216
|
-
msg = prefix + f"
|
|
217
|
-
if
|
|
218
|
-
msg += f", expected={
|
|
381
|
+
msg = prefix + f"Unexpected terminal state. Actual={status}"
|
|
382
|
+
if ready_status:
|
|
383
|
+
msg += f", expected={ready_status}"
|
|
384
|
+
print("-- DUMP START --", file=sys.stderr)
|
|
385
|
+
print(json.dumps(t, indent=4), file=sys.stderr)
|
|
386
|
+
print("-- DUMP END --", file=sys.stderr)
|
|
219
387
|
raise CtxpException(msg)
|
|
220
388
|
if is_ready(status):
|
|
221
389
|
return t
|
|
222
390
|
if not is_transition(status):
|
|
223
|
-
raise CtxpException(
|
|
224
|
-
prefix + f"Unexpected status: {status}. If this is a transition, add it to status_map['transition']."
|
|
225
|
-
)
|
|
391
|
+
raise CtxpException(prefix + f"Unexpected status: {status}. If this is a transition, add it to status_map['transition'].")
|
|
226
392
|
|
|
227
393
|
now = time.time()
|
|
228
394
|
remaining_seconds = timeout_seconds - (now - start)
|
|
229
395
|
if remaining_seconds < 1:
|
|
230
|
-
|
|
396
|
+
msg = prefix + f"Timeout. Current: {status}."
|
|
397
|
+
if ready_status:
|
|
398
|
+
msg += f" Expected: {ready_status}."
|
|
399
|
+
raise TimeoutError(msg)
|
|
231
400
|
sleep_seconds = remaining_seconds
|
|
232
401
|
if sleep_seconds > polling_interval_seconds:
|
|
233
402
|
sleep_seconds = polling_interval_seconds
|
|
234
403
|
|
|
235
|
-
# print(f"DDD sleeping {sleep_seconds}")
|
|
236
404
|
exit.sleep(sleep_seconds)
|
hcs_core/sglib/csp.py
CHANGED
|
@@ -13,10 +13,11 @@ See the License for the specific language governing permissions and
|
|
|
13
13
|
limitations under the License.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
import httpx
|
|
17
16
|
import json
|
|
18
17
|
import logging
|
|
19
18
|
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
20
21
|
log = logging.getLogger(__name__)
|
|
21
22
|
|
|
22
23
|
log_http_details = False
|
|
@@ -64,8 +65,25 @@ def _log_response(response: httpx.Response):
|
|
|
64
65
|
log.debug("\n")
|
|
65
66
|
|
|
66
67
|
|
|
68
|
+
def _decode_http_basic_auth_token(basic_token: str):
|
|
69
|
+
import base64
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
decoded = base64.b64decode(basic_token).decode("utf-8")
|
|
73
|
+
client_id, client_secret = decoded.split(":")
|
|
74
|
+
return client_id, client_secret
|
|
75
|
+
except Exception as e:
|
|
76
|
+
raise Exception(f"Invalid basic http auth token: {e}")
|
|
77
|
+
|
|
78
|
+
|
|
67
79
|
class CspClient:
|
|
68
80
|
def __init__(self, url: str, oauth_token: dict = None, org_id: str = None) -> None:
|
|
81
|
+
if url.endswith("/auth/v1/oauth/token"):
|
|
82
|
+
# To workaround that post with "" results "/" and fail the process.
|
|
83
|
+
url = url[: -len("/auth/v1/oauth/token")]
|
|
84
|
+
self._auth_mode = "hcs-auth-svc"
|
|
85
|
+
else:
|
|
86
|
+
self._auth_mode = "csp"
|
|
69
87
|
self._base_url = url
|
|
70
88
|
self._oauth_token = oauth_token
|
|
71
89
|
self._org_id = org_id
|
|
@@ -80,6 +98,7 @@ class CspClient:
|
|
|
80
98
|
)
|
|
81
99
|
|
|
82
100
|
def login_with_api_token(self, api_token: str) -> dict:
|
|
101
|
+
log.debug(f"Logging in with api_token: {api_token}, url: {self._base_url}")
|
|
83
102
|
# https://console-stg.cloud.vmware.com/csp/gateway/authn/api/swagger-ui.html#/Authentication/getAccessTokenByApiRefreshTokenUsingPOST
|
|
84
103
|
|
|
85
104
|
# curl -X 'POST' \
|
|
@@ -87,19 +106,18 @@ class CspClient:
|
|
|
87
106
|
# -H 'accept: application/json' \
|
|
88
107
|
# -H 'Content-Type: application/x-www-form-urlencoded' \
|
|
89
108
|
# -d 'refresh_token=<the-refresh-token>'
|
|
90
|
-
|
|
109
|
+
# print(f"Logging in with api_token: {api_token}, url: {self._base_url}")
|
|
91
110
|
headers = {
|
|
92
111
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
93
112
|
"Accept": "application/json",
|
|
94
113
|
}
|
|
95
114
|
# <no org id for this API>
|
|
96
|
-
resp = self._client.post(
|
|
97
|
-
"/csp/gateway/am/api/auth/api-tokens/authorize", headers=headers, data=f"api_token={api_token}"
|
|
98
|
-
)
|
|
115
|
+
resp = self._client.post("/csp/gateway/am/api/auth/api-tokens/authorize", headers=headers, data=f"api_token={api_token}")
|
|
99
116
|
self._oauth_token = resp.json()
|
|
100
117
|
return self._oauth_token
|
|
101
118
|
|
|
102
119
|
def login_with_client_id_and_secret(self, client_id: str, client_secret: str, org_id: str) -> dict:
|
|
120
|
+
log.debug(f"Logging in with client_id: {client_id}, org_id: {org_id}, url: {self._base_url}")
|
|
103
121
|
headers = {
|
|
104
122
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
105
123
|
"Accept": "application/json",
|
|
@@ -109,8 +127,9 @@ class CspClient:
|
|
|
109
127
|
}
|
|
110
128
|
if org_id:
|
|
111
129
|
params["orgId"] = org_id
|
|
130
|
+
url = "/auth/v1/oauth/token" if self._auth_mode == "hcs-auth-svc" else "/csp/gateway/am/api/auth/authorize"
|
|
112
131
|
resp = self._client.post(
|
|
113
|
-
|
|
132
|
+
url,
|
|
114
133
|
auth=(client_id, client_secret),
|
|
115
134
|
headers=headers,
|
|
116
135
|
params=params,
|
|
@@ -118,6 +137,54 @@ class CspClient:
|
|
|
118
137
|
self._oauth_token = resp.json()
|
|
119
138
|
return self._oauth_token
|
|
120
139
|
|
|
140
|
+
@staticmethod
|
|
141
|
+
def create(
|
|
142
|
+
url: str,
|
|
143
|
+
org_id: str = None,
|
|
144
|
+
client_id: str = None,
|
|
145
|
+
client_secret: str = None,
|
|
146
|
+
api_token: str = None,
|
|
147
|
+
basic: str = None,
|
|
148
|
+
**kwargs,
|
|
149
|
+
) -> "CspClient":
|
|
150
|
+
client = CspClient(url=url, org_id=org_id)
|
|
151
|
+
|
|
152
|
+
if not client_id:
|
|
153
|
+
client_id = kwargs.get("clientId")
|
|
154
|
+
if not client_secret:
|
|
155
|
+
client_secret = kwargs.get("clientSecret")
|
|
156
|
+
if not api_token:
|
|
157
|
+
api_token = kwargs.get("apiToken")
|
|
158
|
+
|
|
159
|
+
if client_id:
|
|
160
|
+
if not client_secret:
|
|
161
|
+
raise ValueError("client_secret is required when client_id is provided")
|
|
162
|
+
if api_token:
|
|
163
|
+
raise ValueError("api_token and client_id/client_secret cannot be used together")
|
|
164
|
+
if basic:
|
|
165
|
+
raise ValueError("basic auth and client_id/client_secret cannot be used together")
|
|
166
|
+
client.login_with_client_id_and_secret(client_id=client_id, client_secret=client_secret, org_id=org_id)
|
|
167
|
+
elif api_token:
|
|
168
|
+
if client_id or client_secret:
|
|
169
|
+
raise ValueError("api_token and client_id/client_secret cannot be used together")
|
|
170
|
+
if basic:
|
|
171
|
+
raise ValueError("api_token and basic auth cannot be used together")
|
|
172
|
+
client.login_with_api_token(api_token)
|
|
173
|
+
elif basic:
|
|
174
|
+
if client_id or client_secret:
|
|
175
|
+
raise ValueError("basic auth and client_id/client_secret cannot be used together")
|
|
176
|
+
if api_token:
|
|
177
|
+
raise ValueError("basic auth and api_token cannot be used together")
|
|
178
|
+
client_id, client_secret = _decode_http_basic_auth_token(basic)
|
|
179
|
+
client.login_with_client_id_and_secret(client_id=client_id, client_secret=client_secret, org_id=org_id)
|
|
180
|
+
else:
|
|
181
|
+
raise Exception("Unrecognized CSP authentication method.")
|
|
182
|
+
|
|
183
|
+
return client
|
|
184
|
+
|
|
185
|
+
def oauth_token(self) -> dict:
|
|
186
|
+
return self._oauth_token
|
|
187
|
+
|
|
121
188
|
# def get_oauth_token(self, force=False):
|
|
122
189
|
# if self._oauth_token and not force:
|
|
123
190
|
# return self._oauth_token
|