hcs-core 0.1.288__py3-none-any.whl → 0.1.290__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/built_in_cmds/profile.py +4 -2
- hcs_core/ctxp/cli_options.py +9 -1
- hcs_core/ctxp/cli_processor.py +5 -2
- hcs_core/ctxp/data_util.py +14 -9
- hcs_core/ctxp/fstore.py +1 -1
- hcs_core/ctxp/profile.py +4 -2
- hcs_core/ctxp/util.py +23 -3
- hcs_core/plan/__init__.py +1 -0
- hcs_core/plan/core.py +13 -2
- hcs_core/plan/kop.py +1 -1
- hcs_core/sglib/auth.py +93 -94
- hcs_core/sglib/client_util.py +125 -58
- hcs_core/sglib/csp.py +70 -2
- hcs_core/sglib/ez_client.py +35 -20
- hcs_core/sglib/hcs_client.py +2 -6
- hcs_core/util/job_view.py +3 -2
- {hcs_core-0.1.288.dist-info → hcs_core-0.1.290.dist-info}/METADATA +2 -1
- {hcs_core-0.1.288.dist-info → hcs_core-0.1.290.dist-info}/RECORD +20 -20
- {hcs_core-0.1.288.dist-info → hcs_core-0.1.290.dist-info}/WHEEL +0 -0
hcs_core/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.290"
|
|
@@ -53,7 +53,7 @@ def use(name: str):
|
|
|
53
53
|
if ret:
|
|
54
54
|
if profile.use(ret) is None:
|
|
55
55
|
panic("No such profile: " + name)
|
|
56
|
-
return
|
|
56
|
+
return profile.current()
|
|
57
57
|
else:
|
|
58
58
|
# aborted
|
|
59
59
|
return "", 1
|
|
@@ -103,9 +103,11 @@ def get(name: str):
|
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
@profile_cmd_group.command()
|
|
106
|
-
@click.argument("name")
|
|
106
|
+
@click.argument("name", required=False)
|
|
107
107
|
def delete(name: str):
|
|
108
108
|
"""Delete a profile by name."""
|
|
109
|
+
if not name:
|
|
110
|
+
name = profile.name()
|
|
109
111
|
profile.delete(name)
|
|
110
112
|
|
|
111
113
|
|
hcs_core/ctxp/cli_options.py
CHANGED
|
@@ -27,7 +27,7 @@ verbose = click.option(
|
|
|
27
27
|
output = click.option(
|
|
28
28
|
"--output",
|
|
29
29
|
"-o",
|
|
30
|
-
type=click.Choice(["json", "json-compact", "yaml", "text", "table"], case_sensitive=False),
|
|
30
|
+
type=click.Choice(["json", "json-compact", "yaml", "yml", "text", "table"], case_sensitive=False),
|
|
31
31
|
default=None,
|
|
32
32
|
hidden=True,
|
|
33
33
|
help="Specify output format",
|
|
@@ -41,6 +41,14 @@ field = click.option(
|
|
|
41
41
|
help="Specify fields to output, in comma separated field names.",
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
+
exclude_field = click.option(
|
|
45
|
+
"--exclude-field",
|
|
46
|
+
type=str,
|
|
47
|
+
required=False,
|
|
48
|
+
hidden=True,
|
|
49
|
+
help="Specify fields to exclude from output, in comma separated field names.",
|
|
50
|
+
)
|
|
51
|
+
|
|
44
52
|
wait = click.option(
|
|
45
53
|
"--wait",
|
|
46
54
|
"-w",
|
hcs_core/ctxp/cli_processor.py
CHANGED
|
@@ -75,6 +75,7 @@ def _ensure_sub_group(current: Group, mod_path: Path):
|
|
|
75
75
|
meta = _read_group_meta(mod_path)
|
|
76
76
|
help = meta.get("help")
|
|
77
77
|
extension = meta.get("extension")
|
|
78
|
+
hidden = meta.get("hidden", False)
|
|
78
79
|
|
|
79
80
|
subgroup = current.commands.get(name)
|
|
80
81
|
if subgroup and isinstance(subgroup, Group):
|
|
@@ -82,7 +83,7 @@ def _ensure_sub_group(current: Group, mod_path: Path):
|
|
|
82
83
|
subgroup.mod_path = mod_path
|
|
83
84
|
return subgroup
|
|
84
85
|
|
|
85
|
-
subgroup = LazyGroup(extension=extension, name=name, help=help, mod_path=mod_path)
|
|
86
|
+
subgroup = LazyGroup(extension=extension, name=name, help=help, mod_path=mod_path, hidden=hidden)
|
|
86
87
|
current.add_command(subgroup)
|
|
87
88
|
return subgroup
|
|
88
89
|
|
|
@@ -166,13 +167,14 @@ def _import_cmd_file(mod_path: Path, parent: click.core.Group):
|
|
|
166
167
|
# Create a new decorator that combines the individual decorators
|
|
167
168
|
def _default_io(cmd: click.Command):
|
|
168
169
|
|
|
169
|
-
from .cli_options import field, first, ids, output
|
|
170
|
+
from .cli_options import exclude_field, field, first, ids, output
|
|
170
171
|
|
|
171
172
|
cmd = output(cmd)
|
|
172
173
|
# cmd = cli_options.verbose(cmd)
|
|
173
174
|
cmd = field(cmd)
|
|
174
175
|
cmd = ids(cmd)
|
|
175
176
|
cmd = first(cmd)
|
|
177
|
+
cmd = exclude_field(cmd)
|
|
176
178
|
callback = cmd.callback
|
|
177
179
|
|
|
178
180
|
def inner(*args, **kwargs):
|
|
@@ -182,6 +184,7 @@ def _default_io(cmd: click.Command):
|
|
|
182
184
|
"field": kwargs.pop("field"),
|
|
183
185
|
"ids": kwargs.pop("ids"),
|
|
184
186
|
"first": kwargs.pop("first"),
|
|
187
|
+
"exclude_field": kwargs.pop("exclude_field"),
|
|
185
188
|
}
|
|
186
189
|
if io_args["output"] == "table":
|
|
187
190
|
|
hcs_core/ctxp/data_util.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import os
|
|
2
3
|
import re
|
|
3
4
|
from io import TextIOWrapper
|
|
4
5
|
from os import chmod, path
|
|
@@ -260,22 +261,26 @@ def process_variables(obj: dict, fn_get_var=None, use_env: bool = True):
|
|
|
260
261
|
fn_get_var = _fn_get_var
|
|
261
262
|
|
|
262
263
|
if use_env:
|
|
263
|
-
import os
|
|
264
264
|
|
|
265
265
|
prev_fn_get_var = fn_get_var
|
|
266
266
|
|
|
267
267
|
def _fn_get_var_from_env(name: str):
|
|
268
268
|
if not name.startswith("env."):
|
|
269
269
|
return prev_fn_get_var(name)
|
|
270
|
-
v = None
|
|
271
270
|
actual_name = name[4:]
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
271
|
+
if actual_name.endswith("?"):
|
|
272
|
+
# optional
|
|
273
|
+
actual_name = actual_name[:-1]
|
|
274
|
+
required = False
|
|
275
|
+
else:
|
|
276
|
+
# required
|
|
277
|
+
required = True
|
|
278
|
+
|
|
279
|
+
if actual_name not in os.environ:
|
|
280
|
+
if required:
|
|
281
|
+
raise CtxpException(f"Environment variable '{actual_name}' is used in template, but not found. ")
|
|
282
|
+
return None, True
|
|
283
|
+
return os.environ[actual_name], True
|
|
279
284
|
|
|
280
285
|
fn_get_var = _fn_get_var_from_env
|
|
281
286
|
|
hcs_core/ctxp/fstore.py
CHANGED
|
@@ -199,7 +199,7 @@ class fstore:
|
|
|
199
199
|
outfile.write(str(data))
|
|
200
200
|
elif format == "json-compact":
|
|
201
201
|
jsondot.save(data=data, file=file_path, pretty=False, lock=True)
|
|
202
|
-
elif format == "yaml":
|
|
202
|
+
elif format == "yaml" or format == "yml":
|
|
203
203
|
import yaml
|
|
204
204
|
|
|
205
205
|
with open(file_path, "w", encoding="utf-8") as outfile:
|
hcs_core/ctxp/profile.py
CHANGED
|
@@ -63,7 +63,7 @@ def create(name: str, data: dict, auto_use: bool = True, overwrite: bool = False
|
|
|
63
63
|
save_data_file(data, file(name), "yaml", 0o600)
|
|
64
64
|
if auto_use:
|
|
65
65
|
use(name)
|
|
66
|
-
return get(name)
|
|
66
|
+
return get(name, reload=True)
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
def copy(from_name: str, to_name: str, overwrite: bool = True):
|
|
@@ -126,6 +126,8 @@ def use(name: str) -> str:
|
|
|
126
126
|
return
|
|
127
127
|
|
|
128
128
|
_set_active_profile_name(name)
|
|
129
|
+
global _data
|
|
130
|
+
_data = _load_data_file_with_env_overrides(name, None)
|
|
129
131
|
return name
|
|
130
132
|
|
|
131
133
|
|
|
@@ -223,10 +225,10 @@ class auth:
|
|
|
223
225
|
if json.dumps(data) == json.dumps(_auth_cache):
|
|
224
226
|
return
|
|
225
227
|
_auth_cache = deepcopy(data)
|
|
226
|
-
file_name = auth._file_name()
|
|
227
228
|
if os.environ.get("CTXP_AUTH_PERSIST", "true").lower() == "false":
|
|
228
229
|
pass
|
|
229
230
|
else:
|
|
231
|
+
file_name = auth._file_name()
|
|
230
232
|
save_data_file(data, file_name, "yaml", 0o600)
|
|
231
233
|
|
|
232
234
|
@staticmethod
|
hcs_core/ctxp/util.py
CHANGED
|
@@ -57,6 +57,7 @@ def validate_error_return(reason, return_code):
|
|
|
57
57
|
def print_output(data: Any, args: dict, file=sys.stdout):
|
|
58
58
|
output = args.get("output", "json")
|
|
59
59
|
fields = args.get("field")
|
|
60
|
+
exclude_field = args.get("exclude_field")
|
|
60
61
|
ids = args.get("ids", False)
|
|
61
62
|
first = args.get("first", False)
|
|
62
63
|
|
|
@@ -74,14 +75,17 @@ def print_output(data: Any, args: dict, file=sys.stdout):
|
|
|
74
75
|
if fields:
|
|
75
76
|
raise CtxpException("--ids and --fields should not be used together.")
|
|
76
77
|
data = _convert_to_id_only(data)
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
else:
|
|
79
|
+
if fields:
|
|
80
|
+
data = _filter_fields(data, fields)
|
|
81
|
+
if exclude_field:
|
|
82
|
+
data = _exclude_fields(data, exclude_field)
|
|
79
83
|
|
|
80
84
|
if output is None or output == "json":
|
|
81
85
|
text = json.dumps(data, default=vars, indent=4)
|
|
82
86
|
elif output == "json-compact":
|
|
83
87
|
text = json.dumps(data, default=vars)
|
|
84
|
-
elif output == "yaml":
|
|
88
|
+
elif output == "yaml" or output == "yml":
|
|
85
89
|
from . import jsondot
|
|
86
90
|
|
|
87
91
|
text = yaml.dump(jsondot.plain(data), sort_keys=False)
|
|
@@ -168,6 +172,22 @@ def _filter_fields(obj: Any, fields: str):
|
|
|
168
172
|
return _filter_obj(obj)
|
|
169
173
|
|
|
170
174
|
|
|
175
|
+
def _exclude_fields(obj: Any, fields_exclude: str):
|
|
176
|
+
parts = fields_exclude.split(",")
|
|
177
|
+
|
|
178
|
+
def _filter_obj(o):
|
|
179
|
+
if not isinstance(o, dict):
|
|
180
|
+
return o
|
|
181
|
+
for k in list(o.keys()):
|
|
182
|
+
if k in parts:
|
|
183
|
+
del o[k]
|
|
184
|
+
return o
|
|
185
|
+
|
|
186
|
+
if isinstance(obj, list):
|
|
187
|
+
return list(map(_filter_obj, obj))
|
|
188
|
+
return _filter_obj(obj)
|
|
189
|
+
|
|
190
|
+
|
|
171
191
|
def panic(reason: Any = None, code: int = 1):
|
|
172
192
|
if isinstance(reason, Exception):
|
|
173
193
|
text = error_details(reason)
|
hcs_core/plan/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ from .core import clear as clear
|
|
|
5
5
|
from .core import destroy as destroy
|
|
6
6
|
from .core import get_deployment_data as get_deployment_data
|
|
7
7
|
from .core import graph as graph
|
|
8
|
+
from .core import resolve as resolve
|
|
8
9
|
from .helper import PlanException as PlanException
|
|
9
10
|
from .helper import PluginException as PluginException
|
|
10
11
|
from .kop import attach_job_view as attach_job_view
|
hcs_core/plan/core.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Copyright
|
|
2
|
+
Copyright 2025-2025 Omnissa Inc.
|
|
3
3
|
SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
5
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -93,6 +93,17 @@ def _prepare_data(data: dict, additional_context: dict, target_resource_name: st
|
|
|
93
93
|
# return True
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
def resolve(data: dict, additional_context: dict = None, target_resource_name: str = None):
|
|
97
|
+
blueprint, state, state_file = _prepare_data(data, additional_context, None)
|
|
98
|
+
if target_resource_name:
|
|
99
|
+
if target_resource_name in blueprint["resource"]:
|
|
100
|
+
return blueprint["resource"][target_resource_name]["data"]
|
|
101
|
+
if target_resource_name in blueprint["runtime"]:
|
|
102
|
+
return blueprint["runtime"][target_resource_name]["data"]
|
|
103
|
+
raise PlanException("Target resource or runtime not found: " + target_resource_name)
|
|
104
|
+
return blueprint
|
|
105
|
+
|
|
106
|
+
|
|
96
107
|
def apply(
|
|
97
108
|
data: dict,
|
|
98
109
|
additional_context: dict = None,
|
|
@@ -249,7 +260,7 @@ def _deploy_res(name, res, state):
|
|
|
249
260
|
action = handler.decide(res_data, res_state)
|
|
250
261
|
|
|
251
262
|
if action == actions.skip:
|
|
252
|
-
kop.skip(
|
|
263
|
+
kop.skip(_get_res_text(handler, res_state))
|
|
253
264
|
return
|
|
254
265
|
|
|
255
266
|
if action == actions.recreate:
|
hcs_core/plan/kop.py
CHANGED
hcs_core/sglib/auth.py
CHANGED
|
@@ -30,96 +30,17 @@ 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):
|
|
67
42
|
"""Ensure login state, using credentials from the current profile. Return oauth token."""
|
|
68
|
-
|
|
69
|
-
_login_lock.acquire()
|
|
70
|
-
try:
|
|
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
|
|
43
|
+
return _populate_token_with_cache(profile.current().csp, force_refresh)
|
|
123
44
|
|
|
124
45
|
|
|
125
46
|
def refresh_oauth_token(old_oauth_token: dict, csp_url: str):
|
|
@@ -139,18 +60,102 @@ def refresh_oauth_token(old_oauth_token: dict, csp_url: str):
|
|
|
139
60
|
return new_token
|
|
140
61
|
|
|
141
62
|
|
|
142
|
-
|
|
143
|
-
|
|
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):
|
|
102
|
+
|
|
103
|
+
with _login_lock:
|
|
104
|
+
cache, hash, token = _get_auth_cache(auth_config)
|
|
105
|
+
if token and not force_refresh:
|
|
106
|
+
return token
|
|
107
|
+
|
|
108
|
+
# invalid token. Refresh or recreate it.
|
|
109
|
+
if token:
|
|
110
|
+
# try using refresh token if possible
|
|
111
|
+
if auth_config.get("provider", "vmwarecsp") == "vmwarecsp":
|
|
112
|
+
try:
|
|
113
|
+
token = refresh_oauth_token(token, auth_config.url)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
log.debug(f"Failed to refresh OAuth token: {e}")
|
|
116
|
+
token = None
|
|
117
|
+
else:
|
|
118
|
+
# hcs auth-service. Does not support refresh token.
|
|
119
|
+
token = None
|
|
120
|
+
|
|
121
|
+
if not token:
|
|
122
|
+
if _has_credential(auth_config):
|
|
123
|
+
token = CspClient.create(**auth_config).oauth_token()
|
|
124
|
+
else:
|
|
125
|
+
if auth_config.get("browser"):
|
|
126
|
+
from .login_support import login_via_browser
|
|
127
|
+
|
|
128
|
+
token = login_via_browser(auth_config.url, auth_config.orgId)
|
|
129
|
+
if not token:
|
|
130
|
+
raise CtxpException("Browser auth failed.")
|
|
131
|
+
else:
|
|
132
|
+
raise CtxpException(
|
|
133
|
+
"Browser auth was never attempted and no client credentials or API token provided."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if not token.get("expires_at"):
|
|
137
|
+
token["expires_at"] = int(time.time() + token["expires_in"])
|
|
138
|
+
|
|
139
|
+
cache[hash] = token
|
|
140
|
+
profile.auth.set(cache)
|
|
141
|
+
return token
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class CustomOAuth2Client(OAuth2Client):
|
|
145
|
+
def __init__(self, auth_config: dict):
|
|
144
146
|
super().__init__()
|
|
147
|
+
self.auth_config = auth_config
|
|
145
148
|
|
|
146
149
|
def ensure_token(self):
|
|
147
150
|
# pylint: disable=access-member-before-definition
|
|
148
151
|
if self.token is None or not super().ensure_active_token():
|
|
149
|
-
self.token =
|
|
152
|
+
self.token = _populate_token_with_cache(self.auth_config)
|
|
150
153
|
|
|
151
154
|
|
|
152
|
-
def oauth_client():
|
|
153
|
-
|
|
155
|
+
def oauth_client(auth_config: dict = None):
|
|
156
|
+
if not auth_config:
|
|
157
|
+
auth_config = profile.current().csp
|
|
158
|
+
return CustomOAuth2Client(auth_config)
|
|
154
159
|
|
|
155
160
|
|
|
156
161
|
def details(get_org_details: bool = False) -> dotdict:
|
|
@@ -179,9 +184,3 @@ def details_from_token(oauth_token, get_org_details: bool = False):
|
|
|
179
184
|
def get_org_id_from_token(oauth_token: str) -> str:
|
|
180
185
|
decoded = jwt.decode(oauth_token["access_token"], options={"verify_signature": False})
|
|
181
186
|
return decoded["context_name"]
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def use_oauth_token(oauth_token, effective_profile=None):
|
|
185
|
-
if effective_profile is None:
|
|
186
|
-
effective_profile = profile.current()
|
|
187
|
-
profile.auth.set({"token": oauth_token, "hash": _get_profile_auth_hash(effective_profile)})
|
hcs_core/sglib/client_util.py
CHANGED
|
@@ -14,95 +14,162 @@ 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
|
-
|
|
39
|
-
|
|
40
|
-
def _get_url(): # make it deferred so no need to initialize profile
|
|
41
|
-
url = _get_hcs_url_considering_env_override(service_name)
|
|
42
|
-
if not url.endswith("/"):
|
|
43
|
-
url += "/"
|
|
44
|
-
url += service_name
|
|
45
|
-
return url
|
|
46
|
-
|
|
47
|
-
instance = hcs_client(_get_url)
|
|
48
|
-
_caches[service_name] = instance
|
|
49
|
-
return instance
|
|
50
|
-
finally:
|
|
51
|
-
_client_instance_lock.release()
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _get_hcs_url_considering_env_override(service_name: str):
|
|
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
|
|
35
|
+
def _lazy_init(
|
|
36
|
+
service_name: str, hdc: str = None, region: str = None
|
|
37
|
+
): # make it deferred so no need to initialize profile
|
|
38
|
+
if region and hdc:
|
|
39
|
+
raise Exception("region and hdc cannot be specified at the same time.")
|
|
66
40
|
|
|
67
41
|
# check per-service override in profile
|
|
68
|
-
|
|
42
|
+
profile_data = profile.current()
|
|
43
|
+
override = profile_data.get("override", {})
|
|
44
|
+
service_override = {}
|
|
45
|
+
for k, v in override.items():
|
|
46
|
+
if service_name == k.lower():
|
|
47
|
+
service_override = v
|
|
69
48
|
service_override_url = service_override.get("url")
|
|
70
49
|
if service_override_url:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
50
|
+
log.debug(f"Using per-service override for {service_name}: {service_override_url}")
|
|
51
|
+
url = service_override_url
|
|
52
|
+
else:
|
|
53
|
+
if hdc:
|
|
54
|
+
# prod only
|
|
55
|
+
hdc = hdc.upper()
|
|
56
|
+
if hdc == "US":
|
|
57
|
+
url = profile_data.hcs.url
|
|
58
|
+
elif hdc == "EU":
|
|
59
|
+
url = profile_data.hcs.url.replace("cloud-sg-us", "cloud-sg-eu")
|
|
60
|
+
elif hdc == "JP":
|
|
61
|
+
url = profile_data.hcs.url.replace("cloud-sg-us", "cloud-sg-jp")
|
|
62
|
+
else:
|
|
63
|
+
raise CtxpException(f"Invalid HDC name: {hdc}. Supported HDC names: US, EU, JP.")
|
|
64
|
+
elif region:
|
|
65
|
+
url = _get_region_url(region)
|
|
66
|
+
if not url:
|
|
67
|
+
panic("Missing profile property: hcs.regions")
|
|
68
|
+
else:
|
|
69
|
+
url = profile_data.hcs.url
|
|
70
|
+
url = url.rstrip("/") + "/" + service_name
|
|
71
|
+
|
|
72
|
+
# TODO
|
|
73
|
+
client_id = service_override.get("client-id", None)
|
|
74
|
+
if not client_id:
|
|
75
|
+
client_id = service_override.get("clientId", None)
|
|
76
|
+
client_secret = service_override.get("client-secret", None)
|
|
77
|
+
if not client_secret:
|
|
78
|
+
client_secret = service_override.get("clientSecret", None)
|
|
79
|
+
api_token = service_override.get("api-token", None)
|
|
80
|
+
if not api_token:
|
|
81
|
+
api_token = service_override.get("apiToken", None)
|
|
82
|
+
provider = service_override.get("provider", "vmwarecsp")
|
|
83
|
+
if provider == "vmwarecsp":
|
|
84
|
+
token_url = profile_data.csp.url
|
|
85
|
+
elif provider == "auth-service":
|
|
86
|
+
token_url = profile_data.auth.tokenUrl
|
|
87
|
+
else:
|
|
88
|
+
raise CtxpException(f"Unknown provider: {provider}. Supported providers: vmwarecsp, auth-service.")
|
|
89
|
+
|
|
90
|
+
if client_id:
|
|
91
|
+
if not client_secret:
|
|
92
|
+
panic(f"Client ID is specified but missing clientSecret for service {service_name} in profile override.")
|
|
93
|
+
else:
|
|
94
|
+
if client_secret:
|
|
95
|
+
panic(f"Client secret is specified but missing clientId for service {service_name} in profile override.")
|
|
96
|
+
|
|
97
|
+
if client_id:
|
|
98
|
+
auth_config = {
|
|
99
|
+
"url": token_url,
|
|
100
|
+
"client_id": client_id,
|
|
101
|
+
"client_secret": client_secret,
|
|
102
|
+
}
|
|
103
|
+
elif api_token:
|
|
104
|
+
auth_config = {
|
|
105
|
+
"url": token_url,
|
|
106
|
+
"api_token": api_token,
|
|
107
|
+
}
|
|
108
|
+
else:
|
|
109
|
+
auth_config = None
|
|
110
|
+
|
|
111
|
+
from hcs_core.sglib import auth
|
|
112
|
+
|
|
113
|
+
oauth_client = auth.oauth_client(auth_config=auth_config)
|
|
114
|
+
return url, oauth_client
|
|
74
115
|
|
|
75
116
|
|
|
76
|
-
def
|
|
117
|
+
def hdc_service_client(service_name: str, hdc: str = None) -> EzClient:
|
|
118
|
+
"""A client for HDC service. Cached per service name, thread-safe, lazy initialization."""
|
|
119
|
+
if hdc:
|
|
120
|
+
hdc = hdc.upper()
|
|
121
|
+
if hdc not in ["US", "EU", "JP"]:
|
|
122
|
+
panic(f"Invalid HDC name: {hdc}. Supported HDC names: US, EU, JP.")
|
|
123
|
+
else:
|
|
124
|
+
hdc = "US"
|
|
125
|
+
|
|
126
|
+
k = f"{service_name}#{hdc}"
|
|
127
|
+
with _client_instance_lock:
|
|
128
|
+
instance = _caches.get(k)
|
|
129
|
+
if not instance:
|
|
130
|
+
instance = EzClient(lazy_init=lambda: _lazy_init(service_name=service_name, hdc=hdc))
|
|
131
|
+
_caches[k] = instance
|
|
132
|
+
return instance
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _get_region_url(region: str):
|
|
77
136
|
regions = profile.current().hcs.regions
|
|
78
|
-
if not
|
|
137
|
+
if not region:
|
|
79
138
|
return regions[0].url
|
|
80
139
|
for r in regions:
|
|
81
|
-
if r.name.lower() ==
|
|
140
|
+
if r.name.lower() == region.lower():
|
|
82
141
|
return r.url
|
|
83
142
|
names = []
|
|
84
143
|
for r in regions:
|
|
85
144
|
names.append(r.name)
|
|
86
|
-
panic(f"Region not found: {
|
|
145
|
+
panic(f"Region not found: {region}. Available regions: {names}")
|
|
87
146
|
|
|
88
147
|
|
|
89
|
-
def regional_service_client(
|
|
148
|
+
def regional_service_client(service_name: str, region: str = None):
|
|
90
149
|
# 'https://dev1b-westus2-cp103a.azcp.horizon.vmware.com/vmhub'
|
|
91
|
-
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
150
|
+
region_names = [r.name.lower() for r in profile.current().hcs.regions]
|
|
151
|
+
if region:
|
|
152
|
+
region = region.lower()
|
|
153
|
+
if region not in region_names:
|
|
154
|
+
panic(f"Invalid region name: {region}. Available regions: {', '.join(region_names)}")
|
|
155
|
+
else:
|
|
156
|
+
region = region_names[0]
|
|
157
|
+
|
|
158
|
+
k = f"{service_name}#{region}"
|
|
159
|
+
with _client_instance_lock:
|
|
160
|
+
instance = _caches.get(k)
|
|
161
|
+
if not instance:
|
|
162
|
+
instance = EzClient(lazy_init=lambda: _lazy_init(service_name=service_name, region=region))
|
|
163
|
+
_caches[k] = instance
|
|
164
|
+
return instance
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def service_client(service_name: str, region: str = None, hdc: str = None):
|
|
168
|
+
regional_services = ["vmhub", "connection-service"]
|
|
169
|
+
if service_name in regional_services:
|
|
170
|
+
return regional_service_client(service_name, region)
|
|
171
|
+
else:
|
|
172
|
+
return hdc_service_client(service_name, hdc)
|
|
106
173
|
|
|
107
174
|
|
|
108
175
|
class default_crud:
|
hcs_core/sglib/csp.py
CHANGED
|
@@ -65,8 +65,25 @@ def _log_response(response: httpx.Response):
|
|
|
65
65
|
log.debug("\n")
|
|
66
66
|
|
|
67
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
|
+
|
|
68
79
|
class CspClient:
|
|
69
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"
|
|
70
87
|
self._base_url = url
|
|
71
88
|
self._oauth_token = oauth_token
|
|
72
89
|
self._org_id = org_id
|
|
@@ -81,6 +98,7 @@ class CspClient:
|
|
|
81
98
|
)
|
|
82
99
|
|
|
83
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}")
|
|
84
102
|
# https://console-stg.cloud.vmware.com/csp/gateway/authn/api/swagger-ui.html#/Authentication/getAccessTokenByApiRefreshTokenUsingPOST
|
|
85
103
|
|
|
86
104
|
# curl -X 'POST' \
|
|
@@ -88,7 +106,7 @@ class CspClient:
|
|
|
88
106
|
# -H 'accept: application/json' \
|
|
89
107
|
# -H 'Content-Type: application/x-www-form-urlencoded' \
|
|
90
108
|
# -d 'refresh_token=<the-refresh-token>'
|
|
91
|
-
|
|
109
|
+
# print(f"Logging in with api_token: {api_token}, url: {self._base_url}")
|
|
92
110
|
headers = {
|
|
93
111
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
94
112
|
"Accept": "application/json",
|
|
@@ -101,6 +119,7 @@ class CspClient:
|
|
|
101
119
|
return self._oauth_token
|
|
102
120
|
|
|
103
121
|
def login_with_client_id_and_secret(self, client_id: str, client_secret: str, org_id: str) -> dict:
|
|
122
|
+
log.debug(f"Logging in with client_id: {client_id}, org_id: {org_id}, url: {self._base_url}")
|
|
104
123
|
headers = {
|
|
105
124
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
106
125
|
"Accept": "application/json",
|
|
@@ -110,8 +129,9 @@ class CspClient:
|
|
|
110
129
|
}
|
|
111
130
|
if org_id:
|
|
112
131
|
params["orgId"] = org_id
|
|
132
|
+
url = "/auth/v1/oauth/token" if self._auth_mode == "hcs-auth-svc" else "/csp/gateway/am/api/auth/authorize"
|
|
113
133
|
resp = self._client.post(
|
|
114
|
-
|
|
134
|
+
url,
|
|
115
135
|
auth=(client_id, client_secret),
|
|
116
136
|
headers=headers,
|
|
117
137
|
params=params,
|
|
@@ -119,6 +139,54 @@ class CspClient:
|
|
|
119
139
|
self._oauth_token = resp.json()
|
|
120
140
|
return self._oauth_token
|
|
121
141
|
|
|
142
|
+
@staticmethod
|
|
143
|
+
def create(
|
|
144
|
+
url: str,
|
|
145
|
+
org_id: str = None,
|
|
146
|
+
client_id: str = None,
|
|
147
|
+
client_secret: str = None,
|
|
148
|
+
api_token: str = None,
|
|
149
|
+
basic: str = None,
|
|
150
|
+
**kwargs,
|
|
151
|
+
) -> "CspClient":
|
|
152
|
+
client = CspClient(url=url, org_id=org_id)
|
|
153
|
+
|
|
154
|
+
if not client_id:
|
|
155
|
+
client_id = kwargs.get("clientId")
|
|
156
|
+
if not client_secret:
|
|
157
|
+
client_secret = kwargs.get("clientSecret")
|
|
158
|
+
if not api_token:
|
|
159
|
+
api_token = kwargs.get("apiToken")
|
|
160
|
+
|
|
161
|
+
if client_id:
|
|
162
|
+
if not client_secret:
|
|
163
|
+
raise ValueError("client_secret is required when client_id is provided")
|
|
164
|
+
if api_token:
|
|
165
|
+
raise ValueError("api_token and client_id/client_secret cannot be used together")
|
|
166
|
+
if basic:
|
|
167
|
+
raise ValueError("basic auth and client_id/client_secret cannot be used together")
|
|
168
|
+
client.login_with_client_id_and_secret(client_id=client_id, client_secret=client_secret, org_id=org_id)
|
|
169
|
+
elif api_token:
|
|
170
|
+
if client_id or client_secret:
|
|
171
|
+
raise ValueError("api_token and client_id/client_secret cannot be used together")
|
|
172
|
+
if basic:
|
|
173
|
+
raise ValueError("api_token and basic auth cannot be used together")
|
|
174
|
+
client.login_with_api_token(api_token)
|
|
175
|
+
elif basic:
|
|
176
|
+
if client_id or client_secret:
|
|
177
|
+
raise ValueError("basic auth and client_id/client_secret cannot be used together")
|
|
178
|
+
if api_token:
|
|
179
|
+
raise ValueError("basic auth and api_token cannot be used together")
|
|
180
|
+
client_id, client_secret = _decode_http_basic_auth_token(basic)
|
|
181
|
+
client.login_with_client_id_and_secret(client_id=client_id, client_secret=client_secret, org_id=org_id)
|
|
182
|
+
else:
|
|
183
|
+
raise Exception("Unrecognized CSP authentication method.")
|
|
184
|
+
|
|
185
|
+
return client
|
|
186
|
+
|
|
187
|
+
def oauth_token(self) -> dict:
|
|
188
|
+
return self._oauth_token
|
|
189
|
+
|
|
122
190
|
# def get_oauth_token(self, force=False):
|
|
123
191
|
# if self._oauth_token and not force:
|
|
124
192
|
# return self._oauth_token
|
hcs_core/sglib/ez_client.py
CHANGED
|
@@ -133,32 +133,47 @@ def _raise_http_error(e: httpx.HTTPStatusError):
|
|
|
133
133
|
|
|
134
134
|
|
|
135
135
|
class EzClient:
|
|
136
|
-
def __init__(
|
|
137
|
-
self, base_url: Union[str, Callable], oauth_client: OAuth2Client = None, lazy_oauth_client: Callable = None
|
|
138
|
-
) -> None:
|
|
136
|
+
def __init__(self, base_url: str = None, oauth_client: OAuth2Client = None, lazy_init: Callable = None) -> None:
|
|
139
137
|
# self._client = httpx.Client(base_url=base_url, timeout=30, event_hooks=event_hooks)
|
|
140
|
-
|
|
141
|
-
self._client_impl =
|
|
142
|
-
self.
|
|
138
|
+
|
|
139
|
+
self._client_impl = None
|
|
140
|
+
self._lazy_init = lazy_init
|
|
143
141
|
self._lock = threading.Lock()
|
|
144
142
|
|
|
143
|
+
if lazy_init:
|
|
144
|
+
if base_url:
|
|
145
|
+
raise ValueError("Cannot use both base_url and lazy_init")
|
|
146
|
+
if oauth_client:
|
|
147
|
+
raise ValueError("Cannot use both oauth_client and lazy_init")
|
|
148
|
+
else:
|
|
149
|
+
if not base_url:
|
|
150
|
+
raise ValueError("base_url must be provided if lazy_init is not used")
|
|
151
|
+
if not oauth_client:
|
|
152
|
+
raise ValueError("oauth_client must be provided if lazy_init is not used")
|
|
153
|
+
self._init_client(base_url, oauth_client)
|
|
154
|
+
|
|
155
|
+
def _init_client(self, base_url: str, client: OAuth2Client):
|
|
156
|
+
if base_url.endswith("/"):
|
|
157
|
+
base_url = base_url[:-1]
|
|
158
|
+
client.base_url = base_url
|
|
159
|
+
client.timeout = int(os.environ.get("HCS_TIMEOUT", 30))
|
|
160
|
+
request_hooks = client.event_hooks["request"]
|
|
161
|
+
response_hooks = client.event_hooks["response"]
|
|
162
|
+
if _log_request not in request_hooks:
|
|
163
|
+
request_hooks.append(_log_request)
|
|
164
|
+
if _log_response not in response_hooks:
|
|
165
|
+
response_hooks.append(_log_response)
|
|
166
|
+
if _raise_on_4xx_5xx not in response_hooks:
|
|
167
|
+
response_hooks.append(_raise_on_4xx_5xx)
|
|
168
|
+
self._client_impl = client
|
|
169
|
+
|
|
145
170
|
def _client(self):
|
|
146
171
|
self._lock.acquire()
|
|
147
172
|
try:
|
|
148
|
-
if
|
|
149
|
-
client = self.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
client.timeout = int(os.environ.get("HCS_TIMEOUT", 30))
|
|
153
|
-
request_hooks = client.event_hooks["request"]
|
|
154
|
-
response_hooks = client.event_hooks["response"]
|
|
155
|
-
if _log_request not in request_hooks:
|
|
156
|
-
request_hooks.append(_log_request)
|
|
157
|
-
if _log_response not in response_hooks:
|
|
158
|
-
response_hooks.append(_log_response)
|
|
159
|
-
if _raise_on_4xx_5xx not in response_hooks:
|
|
160
|
-
response_hooks.append(_raise_on_4xx_5xx)
|
|
161
|
-
self._client_impl = client
|
|
173
|
+
if self._lazy_init:
|
|
174
|
+
base_url, client = self._lazy_init()
|
|
175
|
+
self._init_client(base_url, client)
|
|
176
|
+
self._lazy_init = None
|
|
162
177
|
|
|
163
178
|
self._client_impl.ensure_token()
|
|
164
179
|
return self._client_impl
|
hcs_core/sglib/hcs_client.py
CHANGED
|
@@ -13,13 +13,9 @@ See the License for the specific language governing permissions and
|
|
|
13
13
|
limitations under the License.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
from typing import Callable, Union
|
|
17
|
-
|
|
18
16
|
from . import auth as auth
|
|
19
17
|
from .ez_client import EzClient
|
|
20
18
|
|
|
21
19
|
|
|
22
|
-
def hcs_client(url:
|
|
23
|
-
|
|
24
|
-
url = url[:-1]
|
|
25
|
-
return EzClient(url, lazy_oauth_client=auth.oauth_client)
|
|
20
|
+
def hcs_client(url: str, custom_auth: dict = None) -> EzClient:
|
|
21
|
+
return EzClient(base_url=url, oauth_client=auth.oauth_client(custom_auth))
|
hcs_core/util/job_view.py
CHANGED
|
@@ -206,9 +206,10 @@ class JobView:
|
|
|
206
206
|
def skip(self, id: str, reason: str) -> None:
|
|
207
207
|
self._ensure_started(id)
|
|
208
208
|
task_id = self._map[id]
|
|
209
|
-
details = "skipped"
|
|
209
|
+
details = "<skipped>"
|
|
210
210
|
if reason:
|
|
211
|
-
details
|
|
211
|
+
details = reason + " " + details
|
|
212
|
+
|
|
212
213
|
self._job_ctl.update(task_id, completed=sys.float_info.max, details=details)
|
|
213
214
|
self._todo.discard(id)
|
|
214
215
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hcs-core
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.290
|
|
4
4
|
Summary: Horizon Cloud Service CLI module.
|
|
5
5
|
Project-URL: Homepage, https://github.com/euc-eng/hcs-cli
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/euc-eng/hcs-cli/issues
|
|
@@ -26,6 +26,7 @@ Requires-Dist: psutil>=5.9.4
|
|
|
26
26
|
Requires-Dist: pydantic>=2.0.0
|
|
27
27
|
Requires-Dist: pyjwt>=2.8.0
|
|
28
28
|
Requires-Dist: pyopenssl>=24.1.0
|
|
29
|
+
Requires-Dist: python-dotenv>=1.1.0
|
|
29
30
|
Requires-Dist: pyyaml>=6.0.1
|
|
30
31
|
Requires-Dist: questionary>=2.0.1
|
|
31
32
|
Requires-Dist: rel>=0.4.7
|
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
hcs_core/__init__.py,sha256=
|
|
1
|
+
hcs_core/__init__.py,sha256=bwKnrwO9ZdAegnigTknEQxy0oRqb3ET-uKciKxJ5eh0,24
|
|
2
2
|
hcs_core/ctxp/__init__.py,sha256=bHVHhJP10Luz1a3Kk3zFx14dAO4SY6Q20Lrv8rNWWGc,1075
|
|
3
3
|
hcs_core/ctxp/_init.py,sha256=yq46VOty4zs_WcLt1SrtePxbW8S4RIz4YUCP3xIBvCA,2797
|
|
4
|
-
hcs_core/ctxp/cli_options.py,sha256=
|
|
5
|
-
hcs_core/ctxp/cli_processor.py,sha256=
|
|
4
|
+
hcs_core/ctxp/cli_options.py,sha256=j4LUukG1k6etJFATb8mGXvqB7DiEC-VOP23w7KVO9sY,2751
|
|
5
|
+
hcs_core/ctxp/cli_processor.py,sha256=L4ai8ZAKBkhII_6dwehxNGPrrvFOrq0CFaMHav0F2Y0,7485
|
|
6
6
|
hcs_core/ctxp/cmd_util.py,sha256=_-VwQSmkfi52qWC3uHQI06mSiIPfsZroDruTDYHXiMA,3119
|
|
7
7
|
hcs_core/ctxp/config.py,sha256=vRdzPxi3Yrt04cnR6b5mJwEOtYBh21qvmlSSsgyGoI4,931
|
|
8
8
|
hcs_core/ctxp/context.py,sha256=y0ouOzXyYTg9MOuWjzjls9WfOKYaO31_MGJJ2S-Y9p4,3375
|
|
9
|
-
hcs_core/ctxp/data_util.py,sha256=
|
|
9
|
+
hcs_core/ctxp/data_util.py,sha256=ODVts-Hy8zB76dGv0KaPYVoVqqh214H3l_Ux7B1HOKA,14985
|
|
10
10
|
hcs_core/ctxp/dispatcher.py,sha256=GAK-jGXDpyX0C88oRDtDi4yeM0qlvID96k98z_vqJkg,2261
|
|
11
11
|
hcs_core/ctxp/duration.py,sha256=xmAw-nKnul3VeSXzvbGZ-W2ByBwyK23KxLLm3_aFW_M,2117
|
|
12
12
|
hcs_core/ctxp/extension.py,sha256=UqlDCXJMRKXcoceVak8hVGI_7HggjUWYSgKOFbGVetU,1636
|
|
13
13
|
hcs_core/ctxp/fn_util.py,sha256=bEYmj5WgUkRE5S4oQjT_zutuViXWYU9q5MR-MlTA_bY,1573
|
|
14
|
-
hcs_core/ctxp/fstore.py,sha256=
|
|
14
|
+
hcs_core/ctxp/fstore.py,sha256=lU6et0-w_QPYpQNjdA0QGEdnn7lsmBVuw9XbDvLjwso,8977
|
|
15
15
|
hcs_core/ctxp/jsondot.py,sha256=U2_ToR8j3-hex75X3OSqTgADiw5cqjRDVO38-IbvapM,11369
|
|
16
16
|
hcs_core/ctxp/logger.py,sha256=6Sh1WXUrjrQNU7K6zUlBzE34fIS6o6oMU3eqSknUOEo,6321
|
|
17
|
-
hcs_core/ctxp/profile.py,sha256=
|
|
17
|
+
hcs_core/ctxp/profile.py,sha256=JgDhRQD00p4jIsiTEjDzpRfKyicA-TgAheXxMBiNCvw,7766
|
|
18
18
|
hcs_core/ctxp/profile_store.py,sha256=v4RvKSaKSJhAt5rEGbC6v-NfaI3N67YffPm-qlrQWdg,1566
|
|
19
19
|
hcs_core/ctxp/recent.py,sha256=uKSEdU4L2szbyl-5nYLa3bigZSvSPW8G24Tuk_wYMck,1797
|
|
20
20
|
hcs_core/ctxp/state.py,sha256=gjcMRVONKBq7WiFQo-xca1726ngZe90mnPrF3vbXDrU,1634
|
|
21
21
|
hcs_core/ctxp/task_schd.py,sha256=mvZMeKDSSo2p7VidSoZY1XZj433TQn_YF9SGJEzl9lg,4586
|
|
22
22
|
hcs_core/ctxp/template_util.py,sha256=XslvIuRBlTVsUW0Y9M_D8gUPc1jWq6X2p4At2VAe1KU,731
|
|
23
23
|
hcs_core/ctxp/timeutil.py,sha256=RyRrIRdFHbIghdB8wbC8VdABvc7hki2v51b1x2JvHgo,445
|
|
24
|
-
hcs_core/ctxp/util.py,sha256=
|
|
24
|
+
hcs_core/ctxp/util.py,sha256=5RrAjSAM3GRu23a6mnKAnJ6X-WyW9NptngPEEDzm5QM,11434
|
|
25
25
|
hcs_core/ctxp/var_template.py,sha256=cTjj1UJ58ac6s5z4Oh5hSDQwKixq-rdbCF1D8akjAo0,3219
|
|
26
26
|
hcs_core/ctxp/built_in_cmds/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
27
|
hcs_core/ctxp/built_in_cmds/_ut.py,sha256=e50XBmPim2qRe0RYk_XAuRz1HWYyLJuMu-6dVxWR_fQ,3356
|
|
28
28
|
hcs_core/ctxp/built_in_cmds/context.py,sha256=bU-BG60DyA8rpv1sw3LpHnPQb1wQiZQCoIMYLW_g5_c,2389
|
|
29
|
-
hcs_core/ctxp/built_in_cmds/profile.py,sha256=
|
|
30
|
-
hcs_core/plan/__init__.py,sha256
|
|
29
|
+
hcs_core/ctxp/built_in_cmds/profile.py,sha256=XjFEIPBAgLJjOQpAPqL-HtTmycWZEps2_S6yY-0wT-k,4401
|
|
30
|
+
hcs_core/plan/__init__.py,sha256=5klVuXXLQLn2Hj6Gz2Hc-1XSyDXKE9o-BjQ82oXekiI,465
|
|
31
31
|
hcs_core/plan/actions.py,sha256=UvCynzApJUxi39HUoPIQ_uownlbvFbJ4aAs6pmulrLY,125
|
|
32
32
|
hcs_core/plan/base_provider.py,sha256=CSJFN8lopWTUblw8CW78Epnef9BbJVLhPLk9bPfZceM,1299
|
|
33
33
|
hcs_core/plan/context.py,sha256=5NI5Otk0jGKtBGvO8Sc1xdOHUCu_lXQYNrSctT3Hyq8,116
|
|
34
|
-
hcs_core/plan/core.py,sha256=
|
|
34
|
+
hcs_core/plan/core.py,sha256=1hZ9Y8ltEFGAT1MbbxpxLSwMU3TwEEEcbRJeO_KrZ-Y,21722
|
|
35
35
|
hcs_core/plan/dag.py,sha256=pyyoMSmbhCsS8BMZI6hW6YDkHqBgjmJOwys9CUDrIts,12926
|
|
36
36
|
hcs_core/plan/helper.py,sha256=__b8tlzLBT1Rm3vgiS-pWnZImaoZbsmKSRJtt_1gj8E,7763
|
|
37
|
-
hcs_core/plan/kop.py,sha256=
|
|
37
|
+
hcs_core/plan/kop.py,sha256=iDnRbYHFCtS3Rs95UVvRgZdSzs-NMwdyr8SztIQQqyg,5774
|
|
38
38
|
hcs_core/plan/provider/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
39
|
hcs_core/plan/provider/dev/__init__.py,sha256=WpPDB-h5z5uF2BhcRlsdeJyZxBU5dun6tCf5ZZeaTJU,30
|
|
40
40
|
hcs_core/plan/provider/dev/_prepare.py,sha256=PvVnheEQwpj8sWYz2lDONQVTs4pHPYuo2c4VD2if-hc,34
|
|
41
41
|
hcs_core/plan/provider/dev/dummy.py,sha256=zKEr9J4WHhlN5gFdmrFyEfCF0xlQlCJg0CC1dG9VaLA,1958
|
|
42
42
|
hcs_core/plan/provider/dev/fibonacci.py,sha256=8WhDr5c9harNAlVPZomQJEqbWe0hUqbppO6ZkwEUcJ0,1104
|
|
43
43
|
hcs_core/sglib/__init__.py,sha256=oT0etW7vsEbHlXiGL5x23ZXyyFqeLi81RxQQ5lfKSV0,654
|
|
44
|
-
hcs_core/sglib/auth.py,sha256=
|
|
44
|
+
hcs_core/sglib/auth.py,sha256=axgzgTRsXPHEo9VvZ6FLF2uitvFf8vVgHXc3ITTtgkM,6297
|
|
45
45
|
hcs_core/sglib/cli_options.py,sha256=NvdiHpX_o6IYPEM8cQYLb_R7T4aiXVvYLqn6Vk4Q2-Y,1761
|
|
46
|
-
hcs_core/sglib/client_util.py,sha256=
|
|
47
|
-
hcs_core/sglib/csp.py,sha256=
|
|
48
|
-
hcs_core/sglib/ez_client.py,sha256=
|
|
49
|
-
hcs_core/sglib/hcs_client.py,sha256=
|
|
46
|
+
hcs_core/sglib/client_util.py,sha256=JvULJXwZzg0NBVEEfqbmiHA123KwGbWJevOO9X-0oj4,13551
|
|
47
|
+
hcs_core/sglib/csp.py,sha256=o10XdRO1-vVzzgYcn8kvUJ6EMRsv6ruDh6Q_cqy4pvs,11013
|
|
48
|
+
hcs_core/sglib/ez_client.py,sha256=_u9HvPXW5H8zDjaBr3bsTZqzveOlehq4EWDMeF_cwFY,8135
|
|
49
|
+
hcs_core/sglib/hcs_client.py,sha256=pjrAVQDEy2tiQMypMpOzODFntT6aNHXjje8or_mkL2U,804
|
|
50
50
|
hcs_core/sglib/init.py,sha256=w_0ZU70Q1TGbSZsqSi7ewKQqpExFlepOT7mIqH0wnho,310
|
|
51
51
|
hcs_core/sglib/login_support.py,sha256=tSkfmThubbWDUFLfx0hzdHp_QW0TR3gYISzvw6FKpjs,7561
|
|
52
52
|
hcs_core/sglib/payload_util.py,sha256=Hnj7rjzrQ1j5gpbrwZX34biN8MIZjy6dOJZ63ulmzdw,685
|
|
@@ -57,12 +57,12 @@ hcs_core/util/check_license.py,sha256=-ZBMVoVcEUwICZ0QRK-e2uMMla7gFbjP9upun93lPE
|
|
|
57
57
|
hcs_core/util/duration.py,sha256=e7Nw22aYXJK-pLM59QDel5atLZxShKBiVILFgrtLJks,4194
|
|
58
58
|
hcs_core/util/exit.py,sha256=UStMZKlfCFN7GBouc1y3pPFGPFQ66qfcRZ_fYQXFD0E,838
|
|
59
59
|
hcs_core/util/hcs_constants.py,sha256=Ic1Tx_UNJiQchfsdnRDzgiOaCjKHnsWXx997nElppm4,1755
|
|
60
|
-
hcs_core/util/job_view.py,sha256=
|
|
60
|
+
hcs_core/util/job_view.py,sha256=SsACAqHTXSCmKFDD0DDDNK2eKykGgjo_DlLw8CJbRxM,8408
|
|
61
61
|
hcs_core/util/pki_util.py,sha256=Lt3-IzIoGcaQKNE7KUszxR7JSZkpXduVZJ262TszsIs,6685
|
|
62
62
|
hcs_core/util/query_util.py,sha256=5bh3bUVIQuY9qerndfuyfyzkTExYJ8zD0_e3PSN7y-4,3142
|
|
63
63
|
hcs_core/util/scheduler.py,sha256=bPpCmGUL1UctJMfLPAg-h4Hl2YZr96FiI78-G_Usn08,2958
|
|
64
64
|
hcs_core/util/ssl_util.py,sha256=MvU102fGwWWh9hhSmLnn1qQIWuD6TjZnN0iH0MXUtW0,1239
|
|
65
65
|
hcs_core/util/versions.py,sha256=urMtShfoBx_Eqq0D-450LP0i-rW447k9yX8q8j5H_qA,1721
|
|
66
|
-
hcs_core-0.1.
|
|
67
|
-
hcs_core-0.1.
|
|
68
|
-
hcs_core-0.1.
|
|
66
|
+
hcs_core-0.1.290.dist-info/METADATA,sha256=JsjF5lDDx6OWqmoe_8T2qZF5yRNEsPpNZ-N53y1r-po,1911
|
|
67
|
+
hcs_core-0.1.290.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
68
|
+
hcs_core-0.1.290.dist-info/RECORD,,
|
|
File without changes
|