hcs-core 0.1.289__tar.gz → 0.1.291__tar.gz
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-0.1.289 → hcs_core-0.1.291}/.gitignore +0 -1
- {hcs_core-0.1.289 → hcs_core-0.1.291}/PKG-INFO +2 -1
- hcs_core-0.1.291/hcs_core/__init__.py +1 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/_init.py +6 -3
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/built_in_cmds/profile.py +4 -2
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/cli_options.py +9 -1
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/cli_processor.py +12 -3
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/data_util.py +14 -9
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/fstore.py +1 -1
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/profile.py +4 -2
- hcs_core-0.1.291/hcs_core/ctxp/telemetry.py +88 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/util.py +27 -3
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/plan/__init__.py +1 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/plan/core.py +13 -2
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/plan/kop.py +1 -1
- hcs_core-0.1.291/hcs_core/sglib/auth.py +186 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/sglib/client_util.py +125 -58
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/sglib/csp.py +70 -2
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/sglib/ez_client.py +35 -20
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/sglib/hcs_client.py +2 -6
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/util/job_view.py +3 -2
- {hcs_core-0.1.289 → hcs_core-0.1.291}/pyproject.toml +1 -0
- hcs_core-0.1.289/hcs_core/__init__.py +0 -1
- hcs_core-0.1.289/hcs_core/sglib/auth.py +0 -187
- {hcs_core-0.1.289 → hcs_core-0.1.291}/README.md +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/__init__.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/built_in_cmds/__init__.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/built_in_cmds/_ut.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/built_in_cmds/context.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/cmd_util.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/config.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/context.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/dispatcher.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/duration.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/extension.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/fn_util.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/jsondot.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/logger.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/profile_store.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/recent.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/state.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/task_schd.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/template_util.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/timeutil.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/ctxp/var_template.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/plan/actions.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/plan/base_provider.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/plan/context.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/plan/dag.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/plan/helper.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/plan/provider/__init__.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/plan/provider/dev/__init__.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/plan/provider/dev/_prepare.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/plan/provider/dev/dummy.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/plan/provider/dev/fibonacci.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/sglib/__init__.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/sglib/cli_options.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/sglib/init.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/sglib/login_support.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/sglib/payload_util.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/sglib/requtil.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/sglib/utils.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/util/__init__.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/util/check_license.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/util/duration.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/util/exit.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/util/hcs_constants.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/util/pki_util.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/util/query_util.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/util/scheduler.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/util/ssl_util.py +0 -0
- {hcs_core-0.1.289 → hcs_core-0.1.291}/hcs_core/util/versions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hcs-core
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.291
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.291"
|
|
@@ -19,7 +19,7 @@ from pathlib import Path
|
|
|
19
19
|
|
|
20
20
|
import click
|
|
21
21
|
|
|
22
|
-
from . import cli_processor, config, profile, state
|
|
22
|
+
from . import cli_processor, config, profile, state, telemetry
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def _get_store_path():
|
|
@@ -79,8 +79,11 @@ def app_name():
|
|
|
79
79
|
|
|
80
80
|
def init_cli(main_cli: click.Group, commands_dir: str = "./cmds"):
|
|
81
81
|
try:
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
ret = cli_processor.init(main_cli, commands_dir)
|
|
83
|
+
telemetry.end()
|
|
84
|
+
return ret
|
|
85
|
+
except BaseException as e:
|
|
86
|
+
telemetry.end(error=e)
|
|
84
87
|
if _need_stack_trace(e):
|
|
85
88
|
raise e
|
|
86
89
|
else:
|
|
@@ -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
|
|
|
@@ -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",
|
|
@@ -24,7 +24,6 @@ from pathlib import Path
|
|
|
24
24
|
import click
|
|
25
25
|
from click.core import Group
|
|
26
26
|
|
|
27
|
-
from .extension import ensure_extension
|
|
28
27
|
from .util import avoid_trace_for_ctrl_c, print_error, print_output, validate_error_return
|
|
29
28
|
|
|
30
29
|
_eager_loading = os.environ.get("_CTXP_EAGER_LOAD")
|
|
@@ -61,6 +60,8 @@ class LazyGroup(click.Group):
|
|
|
61
60
|
|
|
62
61
|
def _ensure_extension(self):
|
|
63
62
|
if self._extension:
|
|
63
|
+
from .extension import ensure_extension
|
|
64
|
+
|
|
64
65
|
ensure_extension(self._extension)
|
|
65
66
|
|
|
66
67
|
|
|
@@ -75,6 +76,7 @@ def _ensure_sub_group(current: Group, mod_path: Path):
|
|
|
75
76
|
meta = _read_group_meta(mod_path)
|
|
76
77
|
help = meta.get("help")
|
|
77
78
|
extension = meta.get("extension")
|
|
79
|
+
hidden = meta.get("hidden", False)
|
|
78
80
|
|
|
79
81
|
subgroup = current.commands.get(name)
|
|
80
82
|
if subgroup and isinstance(subgroup, Group):
|
|
@@ -82,7 +84,7 @@ def _ensure_sub_group(current: Group, mod_path: Path):
|
|
|
82
84
|
subgroup.mod_path = mod_path
|
|
83
85
|
return subgroup
|
|
84
86
|
|
|
85
|
-
subgroup = LazyGroup(extension=extension, name=name, help=help, mod_path=mod_path)
|
|
87
|
+
subgroup = LazyGroup(extension=extension, name=name, help=help, mod_path=mod_path, hidden=hidden)
|
|
86
88
|
current.add_command(subgroup)
|
|
87
89
|
return subgroup
|
|
88
90
|
|
|
@@ -166,13 +168,14 @@ def _import_cmd_file(mod_path: Path, parent: click.core.Group):
|
|
|
166
168
|
# Create a new decorator that combines the individual decorators
|
|
167
169
|
def _default_io(cmd: click.Command):
|
|
168
170
|
|
|
169
|
-
from .cli_options import field, first, ids, output
|
|
171
|
+
from .cli_options import exclude_field, field, first, ids, output
|
|
170
172
|
|
|
171
173
|
cmd = output(cmd)
|
|
172
174
|
# cmd = cli_options.verbose(cmd)
|
|
173
175
|
cmd = field(cmd)
|
|
174
176
|
cmd = ids(cmd)
|
|
175
177
|
cmd = first(cmd)
|
|
178
|
+
cmd = exclude_field(cmd)
|
|
176
179
|
callback = cmd.callback
|
|
177
180
|
|
|
178
181
|
def inner(*args, **kwargs):
|
|
@@ -182,7 +185,13 @@ def _default_io(cmd: click.Command):
|
|
|
182
185
|
"field": kwargs.pop("field"),
|
|
183
186
|
"ids": kwargs.pop("ids"),
|
|
184
187
|
"first": kwargs.pop("first"),
|
|
188
|
+
"exclude_field": kwargs.pop("exclude_field"),
|
|
185
189
|
}
|
|
190
|
+
ctx = click.get_current_context()
|
|
191
|
+
from .telemetry import start as telemetry_start
|
|
192
|
+
|
|
193
|
+
telemetry_start(ctx.command_path, ctx.params)
|
|
194
|
+
|
|
186
195
|
if io_args["output"] == "table":
|
|
187
196
|
|
|
188
197
|
def _format(data):
|
|
@@ -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
|
|
|
@@ -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:
|
|
@@ -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
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import httpx
|
|
9
|
+
from yumako import env
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
_record = None
|
|
14
|
+
_enabled = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def disable():
|
|
18
|
+
global _enabled
|
|
19
|
+
_enabled = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _is_disabled():
|
|
23
|
+
global _enabled
|
|
24
|
+
if _enabled is None:
|
|
25
|
+
_enabled = env.bool("HCS_CLI_TELEMETRY", True)
|
|
26
|
+
return not _enabled
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def start(cmd_path: str, params: dict):
|
|
30
|
+
if _is_disabled():
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
global _record
|
|
34
|
+
_record = {
|
|
35
|
+
"@timestamp": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
|
|
36
|
+
"command": cmd_path,
|
|
37
|
+
"options": [k.replace("_", "-") for k, v in params.items() if v],
|
|
38
|
+
"return": -1,
|
|
39
|
+
"error": None,
|
|
40
|
+
"time_ms": -1,
|
|
41
|
+
"env": {
|
|
42
|
+
"python_version": sys.version,
|
|
43
|
+
"platform": sys.platform,
|
|
44
|
+
"executable": sys.executable,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def end(return_code: int = 0, error: Exception = None):
|
|
50
|
+
if _is_disabled():
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
if _record is None:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
if error:
|
|
57
|
+
if isinstance(error, click.exceptions.Exit):
|
|
58
|
+
return_code = error.exit_code
|
|
59
|
+
elif isinstance(error, SystemExit):
|
|
60
|
+
return_code = error.code
|
|
61
|
+
else:
|
|
62
|
+
_record["error"] = str(error)
|
|
63
|
+
if return_code == 0:
|
|
64
|
+
return_code = 1
|
|
65
|
+
_record["return"] = return_code
|
|
66
|
+
_record["time_ms"] = int((time.time() - datetime.fromisoformat(_record["@timestamp"]).timestamp()) * 1000)
|
|
67
|
+
|
|
68
|
+
# print('TELEMETRY end', json.dumps(_record, indent=4), flush=True)
|
|
69
|
+
|
|
70
|
+
_injest(_record)
|
|
71
|
+
return _record
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _injest(doc):
|
|
75
|
+
try:
|
|
76
|
+
response = httpx.post(
|
|
77
|
+
f"https://collie.omnissa.com/es/hcs-cli/_doc",
|
|
78
|
+
auth=("append_user", "public"),
|
|
79
|
+
headers={"Content-Type": "application/json"},
|
|
80
|
+
content=json.dumps(doc),
|
|
81
|
+
timeout=4,
|
|
82
|
+
)
|
|
83
|
+
response.raise_for_status()
|
|
84
|
+
except Exception as e:
|
|
85
|
+
log.debug(f"Telemetry ingestion failed: {e}", exc_info=True)
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
log.debug(f"Telemetry ingestion successful: {response.status_code}")
|
|
@@ -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,7 +172,27 @@ 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):
|
|
192
|
+
if isinstance(reason, SystemExit):
|
|
193
|
+
os._exit(reason.code)
|
|
194
|
+
if isinstance(reason, click.exceptions.Exit):
|
|
195
|
+
os._exit(reason.exit_code)
|
|
172
196
|
if isinstance(reason, Exception):
|
|
173
197
|
text = error_details(reason)
|
|
174
198
|
else:
|
|
@@ -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
|
|
@@ -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:
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2023-2023 VMware Inc.
|
|
3
|
+
SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
import jwt
|
|
23
|
+
from authlib.integrations.httpx_client import OAuth2Client
|
|
24
|
+
|
|
25
|
+
from hcs_core.ctxp import CtxpException, panic, profile
|
|
26
|
+
from hcs_core.ctxp.jsondot import dotdict, dotify
|
|
27
|
+
|
|
28
|
+
from .csp import CspClient
|
|
29
|
+
|
|
30
|
+
log = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _is_auth_valid(auth_data):
|
|
34
|
+
leeway = 60
|
|
35
|
+
return auth_data and time.time() + leeway < auth_data["expires_at"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_login_lock = threading.Lock()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def login(force_refresh: bool = False):
|
|
42
|
+
"""Ensure login state, using credentials from the current profile. Return oauth token."""
|
|
43
|
+
return _populate_token_with_cache(profile.current().csp, force_refresh)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def refresh_oauth_token(old_oauth_token: dict, csp_url: str):
|
|
47
|
+
with OAuth2Client(token=old_oauth_token) as client:
|
|
48
|
+
log.debug("Refresh auth token...")
|
|
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)
|
|
54
|
+
log.debug(f"New auth token: {new_token}")
|
|
55
|
+
if not new_token:
|
|
56
|
+
raise Exception("CSP auth refresh failed.")
|
|
57
|
+
if "cspErrorCode" in new_token:
|
|
58
|
+
raise Exception(f"CSP auth failed: {new_token.get('message')}")
|
|
59
|
+
|
|
60
|
+
return new_token
|
|
61
|
+
|
|
62
|
+
|
|
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):
|
|
146
|
+
super().__init__()
|
|
147
|
+
self.auth_config = auth_config
|
|
148
|
+
|
|
149
|
+
def ensure_token(self):
|
|
150
|
+
# pylint: disable=access-member-before-definition
|
|
151
|
+
if self.token is None or not super().ensure_active_token():
|
|
152
|
+
self.token = _populate_token_with_cache(self.auth_config)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def oauth_client(auth_config: dict = None):
|
|
156
|
+
if not auth_config:
|
|
157
|
+
auth_config = profile.current().csp
|
|
158
|
+
return CustomOAuth2Client(auth_config)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def details(get_org_details: bool = False) -> dotdict:
|
|
162
|
+
"""Get the auth details, for the current profile"""
|
|
163
|
+
oauth_token = login()
|
|
164
|
+
if not oauth_token:
|
|
165
|
+
return
|
|
166
|
+
return details_from_token(oauth_token, get_org_details)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def details_from_token(oauth_token, get_org_details: bool = False):
|
|
170
|
+
decoded = jwt.decode(oauth_token["access_token"], options={"verify_signature": False})
|
|
171
|
+
org_id = decoded["context_name"]
|
|
172
|
+
ret = {"token": oauth_token, "jwt": decoded, "org": {"id": org_id}}
|
|
173
|
+
|
|
174
|
+
if get_org_details:
|
|
175
|
+
csp_client = CspClient(url=profile.current().csp.url, oauth_token=oauth_token)
|
|
176
|
+
try:
|
|
177
|
+
org_details = csp_client.get_org_details(org_id)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
org_details = {"error": f"Fail retrieving org details: {e}"}
|
|
180
|
+
ret["org"].update(org_details)
|
|
181
|
+
return dotify(ret)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_org_id_from_token(oauth_token: str) -> str:
|
|
185
|
+
decoded = jwt.decode(oauth_token["access_token"], options={"verify_signature": False})
|
|
186
|
+
return decoded["context_name"]
|