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
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
_app_name = ""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def disable():
|
|
19
|
+
global _enabled
|
|
20
|
+
_enabled = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_disabled():
|
|
24
|
+
global _enabled
|
|
25
|
+
if _enabled is None:
|
|
26
|
+
_enabled = env.bool("HCS_CLI_TELEMETRY", True)
|
|
27
|
+
return not _enabled
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_version():
|
|
31
|
+
try:
|
|
32
|
+
from importlib.metadata import version
|
|
33
|
+
|
|
34
|
+
return version("hcs-cli")
|
|
35
|
+
except Exception as e:
|
|
36
|
+
log.debug(f"Failed to get hcs-cli version: {e}")
|
|
37
|
+
return "unknown"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _get_record():
|
|
41
|
+
global _record
|
|
42
|
+
if _record is None:
|
|
43
|
+
_record = {
|
|
44
|
+
"@timestamp": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
|
|
45
|
+
"app": _app_name,
|
|
46
|
+
"command": None,
|
|
47
|
+
"options": [],
|
|
48
|
+
"return": -1,
|
|
49
|
+
"error": None,
|
|
50
|
+
"time_ms": -1,
|
|
51
|
+
"version": _get_version(),
|
|
52
|
+
"env": {
|
|
53
|
+
"python_version": sys.version,
|
|
54
|
+
"platform": sys.platform,
|
|
55
|
+
"executable": sys.executable,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
return _record
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def start(app_name: str = None):
|
|
62
|
+
if _is_disabled():
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
global _app_name
|
|
66
|
+
_app_name = app_name
|
|
67
|
+
_get_record()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def update(cmd_path: str, params: dict):
|
|
71
|
+
if _is_disabled():
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
record = _get_record()
|
|
75
|
+
record["command"] = cmd_path
|
|
76
|
+
record["options"] = [k.replace("_", "-") for k, v in params.items() if v]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def end(return_code: int = 0, error: Exception = None):
|
|
80
|
+
if _is_disabled():
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
record = _get_record()
|
|
84
|
+
if error:
|
|
85
|
+
if isinstance(error, click.exceptions.Exit):
|
|
86
|
+
return_code = error.exit_code
|
|
87
|
+
elif isinstance(error, SystemExit):
|
|
88
|
+
return_code = error.code
|
|
89
|
+
else:
|
|
90
|
+
record["error"] = str(error)
|
|
91
|
+
if return_code == 0:
|
|
92
|
+
return_code = 1
|
|
93
|
+
record["return"] = return_code
|
|
94
|
+
record["time_ms"] = int((time.time() - datetime.fromisoformat(record["@timestamp"]).timestamp()) * 1000)
|
|
95
|
+
|
|
96
|
+
_fix_missing_commands(record)
|
|
97
|
+
_injest(record)
|
|
98
|
+
return record
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _fix_missing_commands(record):
|
|
102
|
+
if record["command"]:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
args = sys.argv[1:]
|
|
106
|
+
|
|
107
|
+
# this does not work for all cases, but only as best effort.
|
|
108
|
+
options_started = False
|
|
109
|
+
options = record["options"]
|
|
110
|
+
command = [_app_name]
|
|
111
|
+
for arg in args:
|
|
112
|
+
if arg.startswith("-"):
|
|
113
|
+
options_started = True
|
|
114
|
+
|
|
115
|
+
if options_started:
|
|
116
|
+
if arg.startswith("--"):
|
|
117
|
+
options.append(arg[2:])
|
|
118
|
+
elif arg.startswith("-"):
|
|
119
|
+
options.append(arg[1:])
|
|
120
|
+
else:
|
|
121
|
+
# value. For privacy no logging.
|
|
122
|
+
continue
|
|
123
|
+
else:
|
|
124
|
+
command.append(arg)
|
|
125
|
+
|
|
126
|
+
record["command"] = " ".join(command)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _injest(doc):
|
|
130
|
+
|
|
131
|
+
# print('TELEMETRY end', json.dumps(doc, indent=4), flush=True)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
response = httpx.post(
|
|
135
|
+
"https://collie.omnissa.com/es/hcs-cli/_doc",
|
|
136
|
+
auth=("append_user", "public"),
|
|
137
|
+
headers={"Content-Type": "application/json"},
|
|
138
|
+
content=json.dumps(doc),
|
|
139
|
+
timeout=4,
|
|
140
|
+
verify=False,
|
|
141
|
+
)
|
|
142
|
+
response.raise_for_status()
|
|
143
|
+
except Exception as e:
|
|
144
|
+
log.debug(f"Telemetry ingestion failed: {e}", exc_info=True)
|
|
145
|
+
return
|
hcs_core/ctxp/util.py
CHANGED
|
@@ -13,6 +13,7 @@ See the License for the specific language governing permissions and
|
|
|
13
13
|
limitations under the License.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
+
import datetime
|
|
16
17
|
import json
|
|
17
18
|
import os
|
|
18
19
|
import re
|
|
@@ -26,6 +27,7 @@ import click
|
|
|
26
27
|
import httpx
|
|
27
28
|
import questionary
|
|
28
29
|
import yaml
|
|
30
|
+
import yumako
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
class CtxpException(Exception):
|
|
@@ -57,6 +59,7 @@ def validate_error_return(reason, return_code):
|
|
|
57
59
|
def print_output(data: Any, args: dict, file=sys.stdout):
|
|
58
60
|
output = args.get("output", "json")
|
|
59
61
|
fields = args.get("field")
|
|
62
|
+
exclude_field = args.get("exclude_field")
|
|
60
63
|
ids = args.get("ids", False)
|
|
61
64
|
first = args.get("first", False)
|
|
62
65
|
|
|
@@ -68,20 +71,25 @@ def print_output(data: Any, args: dict, file=sys.stdout):
|
|
|
68
71
|
try:
|
|
69
72
|
data = _convert_generator(data)
|
|
70
73
|
if first and isinstance(data, list):
|
|
74
|
+
if len(data) == 0:
|
|
75
|
+
return
|
|
71
76
|
data = data[0]
|
|
72
77
|
|
|
73
78
|
if ids:
|
|
74
79
|
if fields:
|
|
75
80
|
raise CtxpException("--ids and --fields should not be used together.")
|
|
76
81
|
data = _convert_to_id_only(data)
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
else:
|
|
83
|
+
if fields:
|
|
84
|
+
data = _filter_fields(data, fields)
|
|
85
|
+
if exclude_field:
|
|
86
|
+
data = _exclude_fields(data, exclude_field)
|
|
79
87
|
|
|
80
88
|
if output is None or output == "json":
|
|
81
89
|
text = json.dumps(data, default=vars, indent=4)
|
|
82
90
|
elif output == "json-compact":
|
|
83
91
|
text = json.dumps(data, default=vars)
|
|
84
|
-
elif output == "yaml":
|
|
92
|
+
elif output == "yaml" or output == "yml":
|
|
85
93
|
from . import jsondot
|
|
86
94
|
|
|
87
95
|
text = yaml.dump(jsondot.plain(data), sort_keys=False)
|
|
@@ -89,7 +97,18 @@ def print_output(data: Any, args: dict, file=sys.stdout):
|
|
|
89
97
|
if isinstance(data, list):
|
|
90
98
|
text = ""
|
|
91
99
|
for i in data:
|
|
92
|
-
|
|
100
|
+
t = type(i)
|
|
101
|
+
if t is str:
|
|
102
|
+
line = i
|
|
103
|
+
elif isinstance(i, dict):
|
|
104
|
+
if len(i) == 0:
|
|
105
|
+
continue
|
|
106
|
+
if len(i) == 1:
|
|
107
|
+
line = str(next(iter(i.values())))
|
|
108
|
+
else:
|
|
109
|
+
line = json.dumps(i)
|
|
110
|
+
else:
|
|
111
|
+
line = json.dumps(i)
|
|
93
112
|
text += line + "\n"
|
|
94
113
|
elif isinstance(data, dict):
|
|
95
114
|
text = json.dumps(data, indent=4)
|
|
@@ -97,7 +116,7 @@ def print_output(data: Any, args: dict, file=sys.stdout):
|
|
|
97
116
|
text = data
|
|
98
117
|
else:
|
|
99
118
|
text = json.dumps(data, indent=4)
|
|
100
|
-
elif output == "table":
|
|
119
|
+
elif output == "table" or output == "t":
|
|
101
120
|
formatter = args["format"]
|
|
102
121
|
text = formatter(data)
|
|
103
122
|
else:
|
|
@@ -110,7 +129,7 @@ def print_output(data: Any, args: dict, file=sys.stdout):
|
|
|
110
129
|
|
|
111
130
|
|
|
112
131
|
def print_error(error):
|
|
113
|
-
critical_errors = [KeyError, TypeError]
|
|
132
|
+
critical_errors = [KeyError, TypeError, AttributeError, ValueError, IndentationError, ImportError]
|
|
114
133
|
for ex in critical_errors:
|
|
115
134
|
if isinstance(error, ex):
|
|
116
135
|
traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr)
|
|
@@ -157,7 +176,27 @@ def _filter_fields(obj: Any, fields: str):
|
|
|
157
176
|
return _filter_obj(obj)
|
|
158
177
|
|
|
159
178
|
|
|
179
|
+
def _exclude_fields(obj: Any, fields_exclude: str):
|
|
180
|
+
parts = fields_exclude.split(",")
|
|
181
|
+
|
|
182
|
+
def _filter_obj(o):
|
|
183
|
+
if not isinstance(o, dict):
|
|
184
|
+
return o
|
|
185
|
+
for k in list(o.keys()):
|
|
186
|
+
if k in parts:
|
|
187
|
+
del o[k]
|
|
188
|
+
return o
|
|
189
|
+
|
|
190
|
+
if isinstance(obj, list):
|
|
191
|
+
return list(map(_filter_obj, obj))
|
|
192
|
+
return _filter_obj(obj)
|
|
193
|
+
|
|
194
|
+
|
|
160
195
|
def panic(reason: Any = None, code: int = 1):
|
|
196
|
+
if isinstance(reason, SystemExit):
|
|
197
|
+
os._exit(reason.code)
|
|
198
|
+
if isinstance(reason, click.exceptions.Exit):
|
|
199
|
+
os._exit(reason.exit_code)
|
|
161
200
|
if isinstance(reason, Exception):
|
|
162
201
|
text = error_details(reason)
|
|
163
202
|
else:
|
|
@@ -184,7 +223,11 @@ def choose(prompt: str, items: list, fn_get_text: Callable = None, selected=None
|
|
|
184
223
|
panic(prompt + " ERROR: no item available.")
|
|
185
224
|
|
|
186
225
|
if fn_get_text is None:
|
|
187
|
-
|
|
226
|
+
|
|
227
|
+
def _default_fn_get_text(t):
|
|
228
|
+
return str(t)
|
|
229
|
+
|
|
230
|
+
fn_get_text = _default_fn_get_text
|
|
188
231
|
|
|
189
232
|
if select_by_default and len(items) == 1:
|
|
190
233
|
ret = items[0]
|
|
@@ -223,24 +266,37 @@ def input_array(prompt: str, default: list[str] = None):
|
|
|
223
266
|
return ret
|
|
224
267
|
|
|
225
268
|
|
|
226
|
-
def error_details(
|
|
227
|
-
if isinstance(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
details = e.__class__.__name__
|
|
232
|
-
msg = str(e)
|
|
233
|
-
if msg:
|
|
234
|
-
details += ": " + msg
|
|
235
|
-
cause = e.__cause__
|
|
236
|
-
if cause and cause != e:
|
|
237
|
-
details += " | Caused by: " + error_details(cause)
|
|
269
|
+
def error_details(ex):
|
|
270
|
+
if not isinstance(ex, Exception):
|
|
271
|
+
return str(ex)
|
|
272
|
+
|
|
273
|
+
collector = []
|
|
238
274
|
|
|
275
|
+
def _collect_details(e):
|
|
276
|
+
if isinstance(e, click.ClickException):
|
|
277
|
+
collector.append(str(e))
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
details = e.__class__.__name__
|
|
281
|
+
msg = str(e)
|
|
282
|
+
if msg:
|
|
283
|
+
details += ": " + msg
|
|
239
284
|
if isinstance(e, httpx.HTTPStatusError):
|
|
240
285
|
details += "\n" + e.response.text
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
286
|
+
collector.append(details)
|
|
287
|
+
|
|
288
|
+
cause = e.__cause__
|
|
289
|
+
if cause and cause != e:
|
|
290
|
+
_collect_details(cause)
|
|
291
|
+
|
|
292
|
+
_collect_details(ex)
|
|
293
|
+
|
|
294
|
+
# remove_consecutive_duplicates
|
|
295
|
+
result = [collector[0]]
|
|
296
|
+
for item in collector[1:]:
|
|
297
|
+
if item != result[-1]:
|
|
298
|
+
result.append(item)
|
|
299
|
+
return " | Caused by: ".join(result)
|
|
244
300
|
|
|
245
301
|
|
|
246
302
|
def avoid_trace_for_ctrl_c():
|
|
@@ -309,7 +365,7 @@ def format_table(data: list, fields_mapping: dict, columns_to_sum: list = None):
|
|
|
309
365
|
table = [[item.get(field) for field in headers] for item in flattened_data]
|
|
310
366
|
|
|
311
367
|
if columns_to_sum:
|
|
312
|
-
columns_to_sum_indices = {col: headers.index(col) for col in columns_to_sum}
|
|
368
|
+
columns_to_sum_indices = {col: headers.index(col) for col in columns_to_sum if col in headers}
|
|
313
369
|
footer = [""] * len(headers)
|
|
314
370
|
footer[0] = "Total"
|
|
315
371
|
for col_name, col_index in columns_to_sum_indices.items():
|
|
@@ -319,10 +375,12 @@ def format_table(data: list, fields_mapping: dict, columns_to_sum: list = None):
|
|
|
319
375
|
if isinstance(v, str):
|
|
320
376
|
v = strip_ansi(v)
|
|
321
377
|
v = int(v)
|
|
322
|
-
elif isinstance(v, int):
|
|
378
|
+
elif isinstance(v, int) or isinstance(v, float):
|
|
323
379
|
pass
|
|
380
|
+
elif v is None:
|
|
381
|
+
continue
|
|
324
382
|
else:
|
|
325
|
-
raise Exception(f"Unexpected cell value type. Type={type(v)}, value={v}")
|
|
383
|
+
raise Exception(f"Unexpected cell value type. Type={type(v)}, value={v}, col={col_name}")
|
|
326
384
|
total += v
|
|
327
385
|
footer[col_index] = total
|
|
328
386
|
separator = ["-" * len(header) for header in headers]
|
|
@@ -331,3 +389,78 @@ def format_table(data: list, fields_mapping: dict, columns_to_sum: list = None):
|
|
|
331
389
|
traceback.print_exc()
|
|
332
390
|
|
|
333
391
|
return tabulate(table, headers=headers) + "\n"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def colorize(data: dict, name: str, mapping: dict):
|
|
395
|
+
if os.environ.get("TERM_COLOR") == "0":
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
s = data.get(name)
|
|
399
|
+
if not s:
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
if isinstance(mapping, dict):
|
|
403
|
+
c = mapping.get(s)
|
|
404
|
+
if c:
|
|
405
|
+
if isinstance(c, str):
|
|
406
|
+
data[name] = click.style(s, fg=c)
|
|
407
|
+
elif callable(c):
|
|
408
|
+
color = c(data)
|
|
409
|
+
data[name] = click.style(s, fg=color)
|
|
410
|
+
else:
|
|
411
|
+
raise Exception(f"Unexpected color type: {type(c)} {c}")
|
|
412
|
+
elif callable(mapping):
|
|
413
|
+
c = mapping(s)
|
|
414
|
+
if c:
|
|
415
|
+
data[name] = click.style(s, fg=c)
|
|
416
|
+
else:
|
|
417
|
+
raise Exception(f"Unexpected mapping type: {type(mapping)} {mapping}")
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def default_table_formatter(data: Any, mapping: dict = None):
|
|
421
|
+
if not isinstance(data, list):
|
|
422
|
+
return data
|
|
423
|
+
|
|
424
|
+
def _restrict_readable_length(data: dict, name: str, length: int):
|
|
425
|
+
text = data.get(name)
|
|
426
|
+
if not text:
|
|
427
|
+
return
|
|
428
|
+
if len(text) > length:
|
|
429
|
+
data[name] = text[: length - 3] + "..."
|
|
430
|
+
|
|
431
|
+
field_mapping = {}
|
|
432
|
+
for d in data:
|
|
433
|
+
if "id" in d:
|
|
434
|
+
field_mapping["id"] = "Id"
|
|
435
|
+
if "name" in d:
|
|
436
|
+
field_mapping["name"] = "Name"
|
|
437
|
+
if "location" in d:
|
|
438
|
+
field_mapping["location"] = "Location"
|
|
439
|
+
if "type" in d:
|
|
440
|
+
field_mapping["type"] = "Type"
|
|
441
|
+
if "status" in d:
|
|
442
|
+
field_mapping["status"] = "Status"
|
|
443
|
+
if "createdAt" in d:
|
|
444
|
+
d["_createdStale"] = yumako.time.stale(d["createdAt"], datetime.timezone.utc)
|
|
445
|
+
field_mapping["_createdStale"] = "Created At"
|
|
446
|
+
if "updatedAt" in d:
|
|
447
|
+
d["_updatedStale"] = yumako.time.stale(d["updatedAt"], datetime.timezone.utc)
|
|
448
|
+
field_mapping["_updatedStale"] = "Updated At"
|
|
449
|
+
|
|
450
|
+
colorize(
|
|
451
|
+
d,
|
|
452
|
+
"status",
|
|
453
|
+
{
|
|
454
|
+
"READY": "green",
|
|
455
|
+
"SUCCESS": "green",
|
|
456
|
+
"ERROR": "red",
|
|
457
|
+
},
|
|
458
|
+
)
|
|
459
|
+
_restrict_readable_length(d, "name", 60)
|
|
460
|
+
if mapping:
|
|
461
|
+
for k, v in mapping.items():
|
|
462
|
+
if v is None:
|
|
463
|
+
field_mapping.pop(k, None)
|
|
464
|
+
else:
|
|
465
|
+
field_mapping[k] = v
|
|
466
|
+
return format_table(data, fields_mapping=field_mapping)
|
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");
|
|
@@ -41,19 +41,13 @@ def _prepare_data(data: dict, additional_context: dict, target_resource_name: st
|
|
|
41
41
|
data.update(additional_context)
|
|
42
42
|
blueprint, pending = process_template(data)
|
|
43
43
|
|
|
44
|
-
if
|
|
45
|
-
target_resource_name
|
|
46
|
-
and target_resource_name not in blueprint["resource"]
|
|
47
|
-
and target_resource_name not in blueprint["runtime"]
|
|
48
|
-
):
|
|
44
|
+
if target_resource_name and target_resource_name not in blueprint["resource"] and target_resource_name not in blueprint["runtime"]:
|
|
49
45
|
raise PlanException("Target resource or runtime not found: " + target_resource_name)
|
|
50
46
|
|
|
51
47
|
for k, v in pending.items():
|
|
52
48
|
if v.startswith("default.") or v.startswith("var."):
|
|
53
49
|
if not k.find(".conditions."):
|
|
54
|
-
raise PlanException(
|
|
55
|
-
f"Invalid blueprint. Unresolved static references. Variable not found: {v}. Required by {k}"
|
|
56
|
-
)
|
|
50
|
+
raise PlanException(f"Invalid blueprint. Unresolved static references. Variable not found: {v}. Required by {k}")
|
|
57
51
|
deployment_id = blueprint["deploymentId"]
|
|
58
52
|
state_file = deployment_id + ".state.yml"
|
|
59
53
|
prev = data_util.load_data_file(state_file, default={})
|
|
@@ -93,6 +87,17 @@ def _prepare_data(data: dict, additional_context: dict, target_resource_name: st
|
|
|
93
87
|
# return True
|
|
94
88
|
|
|
95
89
|
|
|
90
|
+
def resolve(data: dict, additional_context: dict = None, target_resource_name: str = None):
|
|
91
|
+
blueprint, state, state_file = _prepare_data(data, additional_context, None)
|
|
92
|
+
if target_resource_name:
|
|
93
|
+
if target_resource_name in blueprint["resource"]:
|
|
94
|
+
return blueprint["resource"][target_resource_name]["data"]
|
|
95
|
+
if target_resource_name in blueprint["runtime"]:
|
|
96
|
+
return blueprint["runtime"][target_resource_name]["data"]
|
|
97
|
+
raise PlanException("Target resource or runtime not found: " + target_resource_name)
|
|
98
|
+
return blueprint
|
|
99
|
+
|
|
100
|
+
|
|
96
101
|
def apply(
|
|
97
102
|
data: dict,
|
|
98
103
|
additional_context: dict = None,
|
|
@@ -127,7 +132,7 @@ def apply(
|
|
|
127
132
|
dag.process_blueprint(
|
|
128
133
|
blueprint=blueprint,
|
|
129
134
|
fn_process_node=process_resource_node,
|
|
130
|
-
|
|
135
|
+
fail_fast=True,
|
|
131
136
|
reverse=False,
|
|
132
137
|
concurrency=concurrency,
|
|
133
138
|
target_node_name=target_resource_name,
|
|
@@ -249,7 +254,7 @@ def _deploy_res(name, res, state):
|
|
|
249
254
|
action = handler.decide(res_data, res_state)
|
|
250
255
|
|
|
251
256
|
if action == actions.skip:
|
|
252
|
-
kop.skip(
|
|
257
|
+
kop.skip(_get_res_text(handler, res_state))
|
|
253
258
|
return
|
|
254
259
|
|
|
255
260
|
if action == actions.recreate:
|
|
@@ -280,9 +285,7 @@ def _deploy_res(name, res, state):
|
|
|
280
285
|
else:
|
|
281
286
|
new_state = handler.update(res_data, res_state)
|
|
282
287
|
else:
|
|
283
|
-
raise PlanException(
|
|
284
|
-
f"Unknown action. This is a problem of the concrete plugin.decide function. Plugin={name}, action={action}"
|
|
285
|
-
)
|
|
288
|
+
raise PlanException(f"Unknown action. This is a problem of the concrete plugin.decide function. Plugin={name}, action={action}")
|
|
286
289
|
|
|
287
290
|
if new_state:
|
|
288
291
|
fn_set_state(new_state)
|
|
@@ -524,7 +527,7 @@ def _destroy_res(name, res_node, state, force):
|
|
|
524
527
|
|
|
525
528
|
def destroy(
|
|
526
529
|
data,
|
|
527
|
-
|
|
530
|
+
fail_fast: bool,
|
|
528
531
|
target_resource_name: str = None,
|
|
529
532
|
include_dependencies: bool = True,
|
|
530
533
|
concurrency: int = 4,
|
|
@@ -542,7 +545,7 @@ def destroy(
|
|
|
542
545
|
if not node:
|
|
543
546
|
return dag.walker.next
|
|
544
547
|
|
|
545
|
-
_destroy_res(node_name, node, state,
|
|
548
|
+
_destroy_res(node_name, node, state, fail_fast)
|
|
546
549
|
data_util.save_data_file(state, state_file)
|
|
547
550
|
return dag.walker.next
|
|
548
551
|
|
|
@@ -550,7 +553,7 @@ def destroy(
|
|
|
550
553
|
dag.process_blueprint(
|
|
551
554
|
blueprint=blueprint,
|
|
552
555
|
fn_process_node=destroy_resource,
|
|
553
|
-
|
|
556
|
+
fail_fast=fail_fast,
|
|
554
557
|
reverse=True,
|
|
555
558
|
concurrency=concurrency,
|
|
556
559
|
target_node_name=target_resource_name,
|
hcs_core/plan/dag.py
CHANGED
|
@@ -15,7 +15,6 @@ limitations under the License.
|
|
|
15
15
|
|
|
16
16
|
import threading
|
|
17
17
|
import time
|
|
18
|
-
import traceback
|
|
19
18
|
from concurrent.futures import ThreadPoolExecutor
|
|
20
19
|
from copy import deepcopy
|
|
21
20
|
from graphlib import TopologicalSorter
|
|
@@ -72,7 +71,7 @@ class DAG:
|
|
|
72
71
|
def process_blueprint(
|
|
73
72
|
blueprint,
|
|
74
73
|
fn_process_node: Callable,
|
|
75
|
-
|
|
74
|
+
fail_fast: bool = False,
|
|
76
75
|
reverse: bool = False,
|
|
77
76
|
concurrency: int = 3,
|
|
78
77
|
target_node_name: str = None,
|
|
@@ -88,7 +87,7 @@ def process_blueprint(
|
|
|
88
87
|
return walker.next
|
|
89
88
|
return fn_process_node(name)
|
|
90
89
|
|
|
91
|
-
return _walkthrough(dag, fn_process_node_impl,
|
|
90
|
+
return _walkthrough(dag, fn_process_node_impl, fail_fast, concurrency)
|
|
92
91
|
|
|
93
92
|
|
|
94
93
|
def _filter_dag_by_target_node(dag: DAG, target_node_name, include_dependencies):
|
|
@@ -225,7 +224,7 @@ def _get_provider_id(node_data):
|
|
|
225
224
|
return provider_type
|
|
226
225
|
|
|
227
226
|
|
|
228
|
-
def _walkthrough(dag: DAG, fn_process_node: Callable,
|
|
227
|
+
def _walkthrough(dag: DAG, fn_process_node: Callable, fail_fast: bool, concurrency: int):
|
|
229
228
|
topological_sorter = TopologicalSorter(dag.graph)
|
|
230
229
|
topological_sorter.prepare()
|
|
231
230
|
lock = threading.Lock()
|
|
@@ -250,10 +249,10 @@ def _walkthrough(dag: DAG, fn_process_node: Callable, continue_on_error: bool, c
|
|
|
250
249
|
except Exception as e:
|
|
251
250
|
with lock:
|
|
252
251
|
flags["err"] = e
|
|
253
|
-
if
|
|
254
|
-
topological_sorter.done(node_id)
|
|
255
|
-
else:
|
|
252
|
+
if fail_fast:
|
|
256
253
|
flag_stop.set()
|
|
254
|
+
else:
|
|
255
|
+
topological_sorter.done(node_id)
|
|
257
256
|
except SystemExit as e:
|
|
258
257
|
with lock:
|
|
259
258
|
flags["err"] = e
|
|
@@ -292,7 +291,7 @@ def _walkthrough(dag: DAG, fn_process_node: Callable, continue_on_error: bool, c
|
|
|
292
291
|
if flag_stop.is_set():
|
|
293
292
|
break
|
|
294
293
|
with lock:
|
|
295
|
-
if
|
|
294
|
+
if fail_fast and flags["err"]:
|
|
296
295
|
break
|
|
297
296
|
read_nodes = topological_sorter.get_ready()
|
|
298
297
|
if not len(read_nodes):
|
hcs_core/plan/kop.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");
|
|
@@ -85,7 +85,8 @@ class KOP:
|
|
|
85
85
|
self._started = False
|
|
86
86
|
self._closed = False
|
|
87
87
|
job_id = kind + "/" + name
|
|
88
|
-
_job_view
|
|
88
|
+
if _job_view:
|
|
89
|
+
_job_view.add(job_id, job_id)
|
|
89
90
|
|
|
90
91
|
def _job_id(self):
|
|
91
92
|
return self._kind + "/" + self._name
|
|
@@ -103,7 +104,8 @@ class KOP:
|
|
|
103
104
|
|
|
104
105
|
def id(self, res_id: str):
|
|
105
106
|
self._id = res_id
|
|
106
|
-
_job_view
|
|
107
|
+
if _job_view:
|
|
108
|
+
_job_view.update(self._job_id(), res_id)
|
|
107
109
|
|
|
108
110
|
def start(self, mode: str = None, eta: str = None):
|
|
109
111
|
if self._started:
|
|
@@ -114,7 +116,8 @@ class KOP:
|
|
|
114
116
|
self._mode = mode
|
|
115
117
|
self._started = True
|
|
116
118
|
self._add_log(KopAction.start)
|
|
117
|
-
_job_view
|
|
119
|
+
if _job_view:
|
|
120
|
+
_job_view.start(self._job_id(), eta)
|
|
118
121
|
|
|
119
122
|
def _success(self):
|
|
120
123
|
if not self._started:
|
|
@@ -125,7 +128,8 @@ class KOP:
|
|
|
125
128
|
raise KopException("Kop already closed. This is a framework logging issue.")
|
|
126
129
|
self._add_log(KopAction.success)
|
|
127
130
|
self._closed = True
|
|
128
|
-
_job_view
|
|
131
|
+
if _job_view:
|
|
132
|
+
_job_view.success(self._job_id())
|
|
129
133
|
|
|
130
134
|
def error(self, err):
|
|
131
135
|
if isinstance(err, Exception):
|
|
@@ -136,12 +140,14 @@ class KOP:
|
|
|
136
140
|
details = str(err)
|
|
137
141
|
self._add_log(KopAction.error, details)
|
|
138
142
|
self._closed = True
|
|
139
|
-
_job_view
|
|
143
|
+
if _job_view:
|
|
144
|
+
_job_view.error(self._job_id(), details)
|
|
140
145
|
|
|
141
146
|
def skip(self, reason):
|
|
142
147
|
self._add_log(KopAction.skip, reason)
|
|
143
148
|
self._closed = True
|
|
144
|
-
_job_view
|
|
149
|
+
if _job_view:
|
|
150
|
+
_job_view.skip(self._job_id(), reason)
|
|
145
151
|
|
|
146
152
|
def _add_log(self, action: str, details: str = None):
|
|
147
153
|
labels = {
|
|
@@ -174,6 +180,12 @@ class KOP:
|
|
|
174
180
|
|
|
175
181
|
if _job_view == _DummyJobView:
|
|
176
182
|
log.info(msg)
|
|
183
|
+
elif _job_view is None:
|
|
184
|
+
# explicitly set by user to disable log
|
|
185
|
+
pass
|
|
186
|
+
else:
|
|
187
|
+
# custom job view
|
|
188
|
+
pass
|
|
177
189
|
entry = {"name": self._name, "time": int(time.time()), "action": action, "id": self._id}
|
|
178
190
|
if details:
|
|
179
191
|
entry["details"] = details
|