hcs-core 0.1.283__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 -1
- hcs_core/ctxp/_init.py +8 -3
- hcs_core/ctxp/built_in_cmds/context.py +15 -1
- hcs_core/ctxp/built_in_cmds/profile.py +20 -12
- hcs_core/ctxp/cli_options.py +28 -8
- hcs_core/ctxp/cli_processor.py +23 -12
- hcs_core/ctxp/context.py +2 -0
- hcs_core/ctxp/data_util.py +35 -15
- hcs_core/ctxp/duration.py +1 -3
- hcs_core/ctxp/fstore.py +1 -1
- hcs_core/ctxp/jsondot.py +1 -1
- hcs_core/ctxp/logger.py +4 -4
- hcs_core/ctxp/profile.py +6 -7
- hcs_core/ctxp/recent.py +3 -3
- hcs_core/ctxp/state.py +2 -2
- hcs_core/ctxp/task_schd.py +0 -2
- hcs_core/ctxp/telemetry.py +145 -0
- hcs_core/ctxp/util.py +158 -25
- hcs_core/plan/__init__.py +1 -0
- hcs_core/plan/core.py +20 -17
- hcs_core/plan/dag.py +7 -8
- hcs_core/plan/kop.py +19 -7
- hcs_core/sglib/auth.py +111 -98
- hcs_core/sglib/cli_options.py +15 -1
- hcs_core/sglib/client_util.py +173 -75
- hcs_core/sglib/csp.py +71 -5
- hcs_core/sglib/ez_client.py +48 -32
- hcs_core/sglib/hcs_client.py +2 -6
- hcs_core/sglib/login_support.py +17 -9
- hcs_core/sglib/utils.py +4 -1
- hcs_core/util/check_license.py +0 -2
- hcs_core/util/job_view.py +28 -10
- hcs_core/util/query_util.py +17 -8
- hcs_core/util/versions.py +12 -10
- {hcs_core-0.1.283.dist-info → hcs_core-0.1.316.dist-info}/METADATA +19 -17
- hcs_core-0.1.316.dist-info/RECORD +69 -0
- {hcs_core-0.1.283.dist-info → hcs_core-0.1.316.dist-info}/WHEEL +1 -1
- hcs_core-0.1.283.dist-info/RECORD +0 -68
hcs_core/sglib/auth.py
CHANGED
|
@@ -22,7 +22,7 @@ import time
|
|
|
22
22
|
import jwt
|
|
23
23
|
from authlib.integrations.httpx_client import OAuth2Client
|
|
24
24
|
|
|
25
|
-
from hcs_core.ctxp import CtxpException,
|
|
25
|
+
from hcs_core.ctxp import CtxpException, profile
|
|
26
26
|
from hcs_core.ctxp.jsondot import dotdict, dotify
|
|
27
27
|
|
|
28
28
|
from .csp import CspClient
|
|
@@ -30,122 +30,141 @@ from .csp import CspClient
|
|
|
30
30
|
log = logging.getLogger(__name__)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def
|
|
34
|
-
csp = effective_profile.csp
|
|
35
|
-
text = json.dumps(csp, default=vars)
|
|
36
|
-
return profile.name() + "#" + hashlib.md5(text.encode("ascii"), usedforsecurity=False).hexdigest()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _is_auth_valid(auth_data, effective_profile):
|
|
40
|
-
if not auth_data:
|
|
41
|
-
return
|
|
42
|
-
if not auth_data.token:
|
|
43
|
-
return
|
|
44
|
-
if auth_data.hash != _get_profile_auth_hash(effective_profile):
|
|
45
|
-
return
|
|
33
|
+
def _is_auth_valid(auth_data):
|
|
46
34
|
leeway = 60
|
|
47
|
-
|
|
48
|
-
return
|
|
49
|
-
return True
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _decode_http_basic_auth_token(basic_token: str):
|
|
53
|
-
import base64
|
|
54
|
-
|
|
55
|
-
try:
|
|
56
|
-
decoded = base64.b64decode(basic_token).decode("utf-8")
|
|
57
|
-
client_id, client_secret = decoded.split(":")
|
|
58
|
-
return client_id, client_secret
|
|
59
|
-
except Exception as e:
|
|
60
|
-
raise CtxpException(f"Invalid basic http auth token: {e}")
|
|
35
|
+
return auth_data and time.time() + leeway < auth_data["expires_at"]
|
|
61
36
|
|
|
62
37
|
|
|
63
38
|
_login_lock = threading.Lock()
|
|
64
39
|
|
|
65
40
|
|
|
66
|
-
def login(force_refresh: bool = False,
|
|
41
|
+
def login(force_refresh: bool = False, verbose: bool = False):
|
|
67
42
|
"""Ensure login state, using credentials from the current profile. Return oauth token."""
|
|
43
|
+
return _populate_token_with_cache(profile.current().csp, force_refresh, verbose)
|
|
68
44
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
effective_profile = profile.current()
|
|
72
|
-
|
|
73
|
-
auth_data = profile.auth.get()
|
|
74
|
-
if force_refresh or not _is_auth_valid(auth_data, effective_profile):
|
|
75
|
-
oauth_token = _get_new_oauth_token(auth_data.token, effective_profile)
|
|
76
|
-
if oauth_token:
|
|
77
|
-
use_oauth_token(oauth_token, effective_profile)
|
|
78
|
-
elif panic_on_failure:
|
|
79
|
-
panic(
|
|
80
|
-
"Login failed. If this is configured API key or client credential, refresh the credential from CSP and update profile config. If this is browser based interactive login, login again."
|
|
81
|
-
)
|
|
82
|
-
else:
|
|
83
|
-
return None
|
|
84
|
-
else:
|
|
85
|
-
oauth_token = auth_data.token
|
|
86
|
-
return oauth_token
|
|
87
|
-
finally:
|
|
88
|
-
_login_lock.release()
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _get_new_oauth_token(old_oauth_token, effective_profile):
|
|
92
|
-
csp_config = effective_profile.csp
|
|
93
|
-
|
|
94
|
-
csp_client = CspClient(url=csp_config.url)
|
|
95
|
-
|
|
96
|
-
if csp_config.apiToken:
|
|
97
|
-
oauth_token = csp_client.login_with_api_token(csp_config.apiToken)
|
|
98
|
-
elif csp_config.clientId:
|
|
99
|
-
oauth_token = csp_client.login_with_client_id_and_secret(
|
|
100
|
-
csp_config.clientId, csp_config.clientSecret, csp_config.orgId
|
|
101
|
-
)
|
|
102
|
-
elif csp_config.basic:
|
|
103
|
-
client_id, client_secret = _decode_http_basic_auth_token(csp_config.basic)
|
|
104
|
-
oauth_token = csp_client.login_with_client_id_and_secret(client_id, client_secret, csp_config.orgId)
|
|
105
|
-
else:
|
|
106
|
-
# This should be a config from interactive login.
|
|
107
|
-
# Use existing oauth_token to refresh.
|
|
108
|
-
if not old_oauth_token:
|
|
109
|
-
old_oauth_token = profile.auth.get().token
|
|
110
|
-
if old_oauth_token:
|
|
111
|
-
try:
|
|
112
|
-
oauth_token = _refresh_oauth_token(old_oauth_token, csp_config.url)
|
|
113
|
-
except Exception as e:
|
|
114
|
-
oauth_token = None
|
|
115
|
-
log.warning(e)
|
|
116
|
-
else:
|
|
117
|
-
oauth_token = None
|
|
118
|
-
|
|
119
|
-
if oauth_token and not oauth_token.get("expires_at"):
|
|
120
|
-
oauth_token["expires_at"] = int(time.time() + oauth_token["expires_in"])
|
|
121
|
-
|
|
122
|
-
return oauth_token
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def _refresh_oauth_token(old_oauth_token: dict, csp_url: str):
|
|
45
|
+
|
|
46
|
+
def refresh_oauth_token(old_oauth_token: dict, csp_url: str):
|
|
126
47
|
with OAuth2Client(token=old_oauth_token) as client:
|
|
127
48
|
log.debug("Refresh auth token...")
|
|
128
|
-
|
|
49
|
+
token_url = csp_url + "/csp/gateway/am/api/auth/token"
|
|
50
|
+
from .login_support import identify_client_id
|
|
51
|
+
|
|
52
|
+
csp_specific_req_not_oauth_standard = (identify_client_id(csp_url), "")
|
|
53
|
+
new_token = client.refresh_token(token_url, auth=csp_specific_req_not_oauth_standard)
|
|
129
54
|
log.debug(f"New auth token: {new_token}")
|
|
130
55
|
if not new_token:
|
|
131
56
|
raise Exception("CSP auth refresh failed.")
|
|
132
57
|
if "cspErrorCode" in new_token:
|
|
133
58
|
raise Exception(f"CSP auth failed: {new_token.get('message')}")
|
|
59
|
+
|
|
134
60
|
return new_token
|
|
135
61
|
|
|
136
62
|
|
|
137
|
-
|
|
138
|
-
|
|
63
|
+
def _has_credential(auth_config: dict):
|
|
64
|
+
return (
|
|
65
|
+
auth_config.get("apiToken")
|
|
66
|
+
or auth_config.get("api_token")
|
|
67
|
+
or auth_config.get("clientId")
|
|
68
|
+
or auth_config.get("client_id")
|
|
69
|
+
or auth_config.get("basic")
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_auth_cache(auth_config: dict):
|
|
74
|
+
cache, hash, token = _get_auth_cache(auth_config)
|
|
75
|
+
return token
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _get_auth_cache(auth_config: dict):
|
|
79
|
+
text = json.dumps(auth_config, default=vars)
|
|
80
|
+
hash = hashlib.md5(text.encode("ascii"), usedforsecurity=False).hexdigest()
|
|
81
|
+
auth_cache = profile.auth.get()
|
|
82
|
+
token = auth_cache.get(hash, None)
|
|
83
|
+
if token and not _is_auth_valid(token):
|
|
84
|
+
token = None
|
|
85
|
+
return auth_cache, hash, token
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def save_auth_cache(auth_config: dict, token: dict):
|
|
89
|
+
"""Save the auth token to the profile auth cache."""
|
|
90
|
+
cache, hash, _ = _get_auth_cache(auth_config)
|
|
91
|
+
if not token:
|
|
92
|
+
if hash in cache:
|
|
93
|
+
del cache[hash]
|
|
94
|
+
return
|
|
95
|
+
if not token.get("expires_at"):
|
|
96
|
+
token["expires_at"] = int(time.time() + token["expires_in"])
|
|
97
|
+
cache[hash] = token
|
|
98
|
+
profile.auth.set(cache)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _populate_token_with_cache(auth_config: dict, force_refresh: bool = False, verbose: bool = False):
|
|
102
|
+
if verbose:
|
|
103
|
+
print("_populate_token_with_cache")
|
|
104
|
+
with _login_lock:
|
|
105
|
+
cache, hash, token = _get_auth_cache(auth_config)
|
|
106
|
+
if token and not force_refresh:
|
|
107
|
+
if verbose:
|
|
108
|
+
print("Using cached auth token.")
|
|
109
|
+
return token
|
|
110
|
+
|
|
111
|
+
# invalid token. Refresh or recreate it.
|
|
112
|
+
if token:
|
|
113
|
+
# try using refresh token if possible
|
|
114
|
+
if auth_config.get("provider", "vmwarecsp") == "vmwarecsp":
|
|
115
|
+
if verbose:
|
|
116
|
+
print("Provider: vmwarecsp.")
|
|
117
|
+
try:
|
|
118
|
+
token = refresh_oauth_token(token, auth_config.url)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
log.debug(f"Failed to refresh OAuth token: {e}")
|
|
121
|
+
token = None
|
|
122
|
+
else:
|
|
123
|
+
# hcs auth-service. Does not support refresh token.
|
|
124
|
+
if verbose:
|
|
125
|
+
print("Provider: hcs auth-service.")
|
|
126
|
+
token = None
|
|
127
|
+
|
|
128
|
+
if not token:
|
|
129
|
+
if _has_credential(auth_config):
|
|
130
|
+
if verbose:
|
|
131
|
+
print("Config:", auth_config)
|
|
132
|
+
token = CspClient.create(**auth_config).oauth_token()
|
|
133
|
+
if verbose:
|
|
134
|
+
print("Token:", json.dumps(token, indent=4))
|
|
135
|
+
else:
|
|
136
|
+
if auth_config.get("browser"):
|
|
137
|
+
from .login_support import login_via_browser
|
|
138
|
+
|
|
139
|
+
token = login_via_browser(auth_config.url, auth_config.orgId)
|
|
140
|
+
if not token:
|
|
141
|
+
raise CtxpException("Browser auth failed.")
|
|
142
|
+
else:
|
|
143
|
+
raise CtxpException("Browser auth was never attempted and no client credentials or API token provided.")
|
|
144
|
+
|
|
145
|
+
if not token.get("expires_at"):
|
|
146
|
+
token["expires_at"] = int(time.time() + token["expires_in"])
|
|
147
|
+
|
|
148
|
+
cache[hash] = token
|
|
149
|
+
profile.auth.set(cache)
|
|
150
|
+
return token
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class CustomOAuth2Client(OAuth2Client):
|
|
154
|
+
def __init__(self, auth_config: dict):
|
|
139
155
|
super().__init__()
|
|
156
|
+
self.auth_config = auth_config
|
|
140
157
|
|
|
141
158
|
def ensure_token(self):
|
|
142
159
|
# pylint: disable=access-member-before-definition
|
|
143
160
|
if self.token is None or not super().ensure_active_token():
|
|
144
|
-
self.token =
|
|
161
|
+
self.token = _populate_token_with_cache(self.auth_config)
|
|
145
162
|
|
|
146
163
|
|
|
147
|
-
def oauth_client():
|
|
148
|
-
|
|
164
|
+
def oauth_client(auth_config: dict = None):
|
|
165
|
+
if not auth_config:
|
|
166
|
+
auth_config = profile.current().csp
|
|
167
|
+
return CustomOAuth2Client(auth_config)
|
|
149
168
|
|
|
150
169
|
|
|
151
170
|
def details(get_org_details: bool = False) -> dotdict:
|
|
@@ -174,9 +193,3 @@ def details_from_token(oauth_token, get_org_details: bool = False):
|
|
|
174
193
|
def get_org_id_from_token(oauth_token: str) -> str:
|
|
175
194
|
decoded = jwt.decode(oauth_token["access_token"], options={"verify_signature": False})
|
|
176
195
|
return decoded["context_name"]
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def use_oauth_token(oauth_token, effective_profile=None):
|
|
180
|
-
if effective_profile is None:
|
|
181
|
-
effective_profile = profile.current()
|
|
182
|
-
profile.auth.set({"token": oauth_token, "hash": _get_profile_auth_hash(effective_profile)})
|
hcs_core/sglib/cli_options.py
CHANGED
|
@@ -18,7 +18,21 @@ import os
|
|
|
18
18
|
import click
|
|
19
19
|
|
|
20
20
|
from hcs_core.ctxp import CtxpException, recent
|
|
21
|
-
from hcs_core.ctxp.cli_options import
|
|
21
|
+
from hcs_core.ctxp.cli_options import apply_env as apply_env
|
|
22
|
+
from hcs_core.ctxp.cli_options import confirm as confirm
|
|
23
|
+
from hcs_core.ctxp.cli_options import env as env
|
|
24
|
+
from hcs_core.ctxp.cli_options import exclude_field as exclude_field
|
|
25
|
+
from hcs_core.ctxp.cli_options import field as field
|
|
26
|
+
from hcs_core.ctxp.cli_options import first as first
|
|
27
|
+
from hcs_core.ctxp.cli_options import force as force
|
|
28
|
+
from hcs_core.ctxp.cli_options import formatter as formatter
|
|
29
|
+
from hcs_core.ctxp.cli_options import ids as ids
|
|
30
|
+
from hcs_core.ctxp.cli_options import limit as limit
|
|
31
|
+
from hcs_core.ctxp.cli_options import output as output
|
|
32
|
+
from hcs_core.ctxp.cli_options import search as search
|
|
33
|
+
from hcs_core.ctxp.cli_options import sort as sort
|
|
34
|
+
from hcs_core.ctxp.cli_options import verbose as verbose
|
|
35
|
+
from hcs_core.ctxp.cli_options import wait as wait
|
|
22
36
|
|
|
23
37
|
org_id = click.option(
|
|
24
38
|
"--org",
|
hcs_core/sglib/client_util.py
CHANGED
|
@@ -14,95 +14,178 @@ limitations under the License.
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
import json
|
|
17
|
-
import
|
|
17
|
+
import logging
|
|
18
18
|
import sys
|
|
19
19
|
import threading
|
|
20
20
|
import time
|
|
21
21
|
from typing import Callable, Iterator
|
|
22
22
|
|
|
23
23
|
from hcs_core.ctxp import CtxpException, panic, profile
|
|
24
|
-
from hcs_core.sglib import hcs_client
|
|
25
24
|
from hcs_core.sglib.ez_client import EzClient
|
|
26
25
|
from hcs_core.util import duration, exit
|
|
27
26
|
from hcs_core.util.query_util import PageRequest, with_query
|
|
28
27
|
|
|
28
|
+
log = logging.getLogger(__name__)
|
|
29
|
+
|
|
29
30
|
_caches = {}
|
|
30
31
|
|
|
31
32
|
_client_instance_lock = threading.RLock()
|
|
32
33
|
|
|
33
34
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# service_name = service_name.replace("-", "_")
|
|
56
|
-
# env_name = f"hcs_{service_name}_url"
|
|
57
|
-
# url = os.environ.get(env_name)
|
|
58
|
-
# if url:
|
|
59
|
-
# print(f"Using env override for {env_name}: {url}")
|
|
60
|
-
# return url
|
|
61
|
-
# env_name = env_name.upper()
|
|
62
|
-
# url = os.environ.get(env_name)
|
|
63
|
-
# if url:
|
|
64
|
-
# print(f"Using env override for {env_name}: {url}")
|
|
65
|
-
# return url
|
|
66
|
-
|
|
67
|
-
# check per-service override in profile
|
|
68
|
-
service_override = profile.current().get("overrides", {}).get(service_name, {})
|
|
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)
|
|
69
60
|
service_override_url = service_override.get("url")
|
|
70
61
|
if service_override_url:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
74
127
|
|
|
75
128
|
|
|
76
|
-
def
|
|
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):
|
|
77
148
|
regions = profile.current().hcs.regions
|
|
78
|
-
if not
|
|
149
|
+
if not region:
|
|
79
150
|
return regions[0].url
|
|
80
151
|
for r in regions:
|
|
81
|
-
if r.name.lower() ==
|
|
152
|
+
if r.name.lower() == region.lower():
|
|
82
153
|
return r.url
|
|
83
154
|
names = []
|
|
84
155
|
for r in regions:
|
|
85
156
|
names.append(r.name)
|
|
86
|
-
panic(f"Region not found: {
|
|
157
|
+
panic(f"Region not found: {region}. Available regions: {names}")
|
|
87
158
|
|
|
88
159
|
|
|
89
|
-
def regional_service_client(
|
|
160
|
+
def regional_service_client(service_name: str, region: str = None):
|
|
90
161
|
# 'https://dev1b-westus2-cp103a.azcp.horizon.vmware.com/vmhub'
|
|
91
|
-
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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]
|
|
98
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
|
|
99
177
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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)
|
|
106
189
|
|
|
107
190
|
|
|
108
191
|
class default_crud:
|
|
@@ -178,7 +261,10 @@ class default_crud:
|
|
|
178
261
|
|
|
179
262
|
def wait_for_deleted(self, id: str, org_id: str, timeout: str, fn_is_error: Callable = None):
|
|
180
263
|
name = self._resource_type_name + "/" + id
|
|
181
|
-
|
|
264
|
+
|
|
265
|
+
def fn_get():
|
|
266
|
+
return self.get(id, org_id)
|
|
267
|
+
|
|
182
268
|
return wait_for_res_deleted(name, fn_get, timeout=timeout, fn_is_error=fn_is_error)
|
|
183
269
|
|
|
184
270
|
def update(self, id: str, org_id: str, data: dict, **kwargs):
|
|
@@ -202,10 +288,13 @@ def wait_for_res_deleted(
|
|
|
202
288
|
resource_name: str,
|
|
203
289
|
fn_get: Callable,
|
|
204
290
|
timeout: str,
|
|
205
|
-
|
|
291
|
+
polling_interval: int = 10,
|
|
206
292
|
fn_is_error: Callable = None,
|
|
207
293
|
):
|
|
208
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
|
|
209
298
|
start = time.time()
|
|
210
299
|
while True:
|
|
211
300
|
t = fn_get()
|
|
@@ -224,9 +313,10 @@ def wait_for_res_deleted(
|
|
|
224
313
|
sleep_seconds = remaining_seconds
|
|
225
314
|
if sleep_seconds > polling_interval_seconds:
|
|
226
315
|
sleep_seconds = polling_interval_seconds
|
|
227
|
-
|
|
316
|
+
time.sleep(sleep_seconds)
|
|
228
317
|
|
|
229
318
|
|
|
319
|
+
# flake8: noqa=E731
|
|
230
320
|
def wait_for_res_status(
|
|
231
321
|
resource_name: str,
|
|
232
322
|
fn_get: Callable,
|
|
@@ -250,21 +340,25 @@ def wait_for_res_status(
|
|
|
250
340
|
field_name = get_status
|
|
251
341
|
get_status = lambda t: t[field_name]
|
|
252
342
|
if status_map:
|
|
253
|
-
if isinstance(status_map["ready"], str):
|
|
254
|
-
status_map["ready"] = [status_map["ready"]]
|
|
255
|
-
if isinstance(status_map["transition"], str):
|
|
256
|
-
status_map["transition"] = [status_map["transition"]]
|
|
257
|
-
if isinstance(status_map["error"], str):
|
|
258
|
-
status_map["error"] = [status_map["error"]]
|
|
259
343
|
if is_ready:
|
|
260
344
|
raise CtxpException("Can not specify is_ready when status_map is provided.")
|
|
261
345
|
if is_error:
|
|
262
346
|
raise CtxpException("Can not specify is_error when status_map is provided.")
|
|
263
347
|
if is_transition:
|
|
264
348
|
raise CtxpException("Can not specify is_transition when status_map is provided.")
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
268
362
|
else:
|
|
269
363
|
if not is_ready:
|
|
270
364
|
raise CtxpException("Either status_map or is_ready must be specified.")
|
|
@@ -272,6 +366,9 @@ def wait_for_res_status(
|
|
|
272
366
|
raise CtxpException("Either status_map or is_error must be specified.")
|
|
273
367
|
if not is_transition:
|
|
274
368
|
raise CtxpException("Either status_map or is_transition must be specified.")
|
|
369
|
+
ready_status = None
|
|
370
|
+
error_status = None
|
|
371
|
+
transition_status = None
|
|
275
372
|
|
|
276
373
|
while True:
|
|
277
374
|
t = fn_get()
|
|
@@ -281,9 +378,9 @@ def wait_for_res_status(
|
|
|
281
378
|
raise CtxpException(prefix + "Not found.")
|
|
282
379
|
status = get_status(t)
|
|
283
380
|
if is_error(status):
|
|
284
|
-
msg = prefix + f"
|
|
285
|
-
if
|
|
286
|
-
msg += f", expected={
|
|
381
|
+
msg = prefix + f"Unexpected terminal state. Actual={status}"
|
|
382
|
+
if ready_status:
|
|
383
|
+
msg += f", expected={ready_status}"
|
|
287
384
|
print("-- DUMP START --", file=sys.stderr)
|
|
288
385
|
print(json.dumps(t, indent=4), file=sys.stderr)
|
|
289
386
|
print("-- DUMP END --", file=sys.stderr)
|
|
@@ -291,14 +388,15 @@ def wait_for_res_status(
|
|
|
291
388
|
if is_ready(status):
|
|
292
389
|
return t
|
|
293
390
|
if not is_transition(status):
|
|
294
|
-
raise CtxpException(
|
|
295
|
-
prefix + f"Unexpected status: {status}. If this is a transition, add it to status_map['transition']."
|
|
296
|
-
)
|
|
391
|
+
raise CtxpException(prefix + f"Unexpected status: {status}. If this is a transition, add it to status_map['transition'].")
|
|
297
392
|
|
|
298
393
|
now = time.time()
|
|
299
394
|
remaining_seconds = timeout_seconds - (now - start)
|
|
300
395
|
if remaining_seconds < 1:
|
|
301
|
-
|
|
396
|
+
msg = prefix + f"Timeout. Current: {status}."
|
|
397
|
+
if ready_status:
|
|
398
|
+
msg += f" Expected: {ready_status}."
|
|
399
|
+
raise TimeoutError(msg)
|
|
302
400
|
sleep_seconds = remaining_seconds
|
|
303
401
|
if sleep_seconds > polling_interval_seconds:
|
|
304
402
|
sleep_seconds = polling_interval_seconds
|