salt-api-cli 1.4.1__tar.gz → 1.4.3__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.
- {salt_api_cli-1.4.1/salt_api_cli.egg-info → salt_api_cli-1.4.3}/PKG-INFO +1 -1
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3}/salt_api_cli/highlevel.py +107 -126
- salt_api_cli-1.4.3/salt_api_cli/version.py +1 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3/salt_api_cli.egg-info}/PKG-INFO +1 -1
- salt_api_cli-1.4.1/salt_api_cli/version.py +0 -1
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3}/MANIFEST.in +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3}/README.md +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3}/pyproject.toml +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3}/salt_api_cli/__init__.py +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3}/salt_api_cli/__main__.py +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3}/salt_api_cli/cli.py +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3}/salt_api_cli/lowlevel.py +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3}/salt_api_cli/py.typed +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3}/salt_api_cli.egg-info/SOURCES.txt +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3}/salt_api_cli.egg-info/dependency_links.txt +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3}/salt_api_cli.egg-info/entry_points.txt +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3}/salt_api_cli.egg-info/requires.txt +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3}/salt_api_cli.egg-info/top_level.txt +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.3}/setup.cfg +0 -0
|
@@ -41,7 +41,7 @@ from rich.spinner import Spinner
|
|
|
41
41
|
from rich.table import Table
|
|
42
42
|
from rich.text import Text
|
|
43
43
|
|
|
44
|
-
from salt_api_cli.lowlevel import
|
|
44
|
+
from salt_api_cli.lowlevel import split_args
|
|
45
45
|
|
|
46
46
|
console = Console()
|
|
47
47
|
|
|
@@ -177,8 +177,23 @@ def _print_state_return(minion: str, states: dict[str, Any]) -> None:
|
|
|
177
177
|
if status == "ok":
|
|
178
178
|
detail: str | Text = ""
|
|
179
179
|
elif status == "change":
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
changes = state.get("changes", {})
|
|
181
|
+
if "stdout" in changes:
|
|
182
|
+
# cmd.run-style changes: show the command's output itself
|
|
183
|
+
# rather than the fixed pid/retcode/stdout/stderr key list.
|
|
184
|
+
out = (
|
|
185
|
+
str(changes.get("stdout") or "").strip()
|
|
186
|
+
or str(changes.get("stderr") or "").strip()
|
|
187
|
+
)
|
|
188
|
+
# Full output, folded across lines, so nothing is cut off.
|
|
189
|
+
detail = (
|
|
190
|
+
Text(out, no_wrap=False, overflow="fold")
|
|
191
|
+
if out
|
|
192
|
+
else "changed: (no output)"
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
changed = ", ".join(changes) or "(changes)"
|
|
196
|
+
detail = f"changed: {_short(changed)}"
|
|
182
197
|
elif status == "fail":
|
|
183
198
|
detail = Text(_short(state.get("comment", ""), 240), style="red")
|
|
184
199
|
else: # diff / skip
|
|
@@ -193,7 +208,12 @@ def _print_state_return(minion: str, states: dict[str, Any]) -> None:
|
|
|
193
208
|
fn_w = max((len(fn) for _, fn, _, _ in rows), default=8)
|
|
194
209
|
ref_w = max((len(ref) for _, _, ref, _ in rows), default=8)
|
|
195
210
|
nat_w = max(
|
|
196
|
-
(
|
|
211
|
+
(
|
|
212
|
+
len(line)
|
|
213
|
+
for _, _, _, d in rows
|
|
214
|
+
for line in (d.plain if isinstance(d, Text) else d).splitlines()
|
|
215
|
+
),
|
|
216
|
+
default=0,
|
|
197
217
|
)
|
|
198
218
|
detail_w = min(nat_w, max(20, console.width - 2 - 1 - fn_w - ref_w - 3 * 2))
|
|
199
219
|
|
|
@@ -242,35 +262,24 @@ def _print_state_result(result: dict[str, Any]) -> None:
|
|
|
242
262
|
if not ret:
|
|
243
263
|
console.print("(no minions responded)")
|
|
244
264
|
return
|
|
245
|
-
for minion in sorted(ret):
|
|
265
|
+
for minion in sorted(ret, key=_natural_key):
|
|
246
266
|
_print_one_minion(minion, ret[minion])
|
|
247
267
|
|
|
248
268
|
|
|
249
269
|
# How often to poll jobs.lookup_jid, and how long to keep waiting overall
|
|
250
270
|
# before giving up on minions that never reported. Each poll is a fast,
|
|
251
271
|
# self-contained request, so the proxy/gateway connection cap never bites.
|
|
272
|
+
#
|
|
273
|
+
# We don't probe minion liveness (saltutil.find_job): an empty probe is
|
|
274
|
+
# ambiguous — a busy-but-alive Windows minion mid-highstate can simply fail to
|
|
275
|
+
# answer in time and look identical to a down one, so probing wrongly dropped
|
|
276
|
+
# live minions. Instead we just poll until every targeted minion has returned
|
|
277
|
+
# or _POLL_DEADLINE trips, then render whatever came back. The job keeps
|
|
278
|
+
# running on the minions regardless; results stay fetchable later by jid. Press
|
|
279
|
+
# Ctrl+C to stop waiting early and render the partial results gathered so far.
|
|
252
280
|
_POLL_INTERVAL = 3.0
|
|
253
281
|
_POLL_DEADLINE = 1800.0 # 30 minutes (hard backstop)
|
|
254
282
|
|
|
255
|
-
# Once this many seconds pass with no new minion reporting, probe the
|
|
256
|
-
# still-outstanding minions with saltutil.find_job to tell "still running"
|
|
257
|
-
# apart from "down / lost the job" — the latter are dropped so we stop
|
|
258
|
-
# waiting on them.
|
|
259
|
-
#
|
|
260
|
-
# The probe passes BOTH a short publish ``timeout`` and a short
|
|
261
|
-
# ``gather_job_timeout``. The latter matters most: when a call targets an
|
|
262
|
-
# offline minion, the master runs its own internal find_job and waits
|
|
263
|
-
# gather_job_timeout (default ~10s on the master) for a reply that never
|
|
264
|
-
# comes — so without overriding it, flagging an offline minion costs ~10s+
|
|
265
|
-
# no matter how small ``timeout`` is. With both set low the cost drops to a
|
|
266
|
-
# few seconds (verified against this master: offline minion flagged in ~3s).
|
|
267
|
-
# find_job reports whether a minion is *running the job*, so an online minion
|
|
268
|
-
# answers within ``timeout`` and is never wrongly dropped. Instant detection
|
|
269
|
-
# would need presence_events on the master (manage.present/alived are empty).
|
|
270
|
-
_GATHER_TIMEOUT = 5.0
|
|
271
|
-
_FIND_JOB_TIMEOUT = 2.0
|
|
272
|
-
_FIND_JOB_GATHER = 2.0
|
|
273
|
-
|
|
274
283
|
|
|
275
284
|
def _first_return(resp: dict[str, Any]) -> Any:
|
|
276
285
|
"""The first element of a salt-api ``return`` list, or ``{}`` if absent."""
|
|
@@ -280,38 +289,6 @@ def _first_return(resp: dict[str, Any]) -> Any:
|
|
|
280
289
|
return {}
|
|
281
290
|
|
|
282
291
|
|
|
283
|
-
def _find_dead(
|
|
284
|
-
call: Callable[..., dict[str, Any]], jid: str, candidates: set[str]
|
|
285
|
-
) -> set[str]:
|
|
286
|
-
"""Return the candidates that are NOT running ``jid`` (down or lost it).
|
|
287
|
-
|
|
288
|
-
Probes only ``candidates`` via the local client + ``saltutil.find_job``
|
|
289
|
-
with a short timeout. A minion actively running the job answers with a
|
|
290
|
-
non-empty dict naming the jid; one that's down never answers, and one
|
|
291
|
-
that's up but no longer running it answers empty — both mean it won't
|
|
292
|
-
return, so it's reported dead. A failed probe reports nobody dead (we'd
|
|
293
|
-
rather wait than wrongly drop a live minion)."""
|
|
294
|
-
if not candidates:
|
|
295
|
-
return set()
|
|
296
|
-
try:
|
|
297
|
-
resp = call(
|
|
298
|
-
"local",
|
|
299
|
-
tgt=sorted(candidates),
|
|
300
|
-
tgt_type="list",
|
|
301
|
-
fun="saltutil.find_job",
|
|
302
|
-
arg=[jid],
|
|
303
|
-
timeout=_FIND_JOB_TIMEOUT,
|
|
304
|
-
gather_job_timeout=_FIND_JOB_GATHER,
|
|
305
|
-
)
|
|
306
|
-
except SaltApiError:
|
|
307
|
-
return set()
|
|
308
|
-
ret = _first_return(resp)
|
|
309
|
-
if not isinstance(ret, dict):
|
|
310
|
-
return set()
|
|
311
|
-
running = cast("dict[str, Any]", ret)
|
|
312
|
-
return {m for m in candidates if not running.get(m)}
|
|
313
|
-
|
|
314
|
-
|
|
315
292
|
def _lookup_returns(raw: Any) -> dict[str, Any]:
|
|
316
293
|
"""Pull the ``{minion: state_return}`` map out of a jobs.lookup_jid reply.
|
|
317
294
|
|
|
@@ -363,7 +340,7 @@ def _live_view(
|
|
|
363
340
|
targeted: list[str],
|
|
364
341
|
returns: dict[str, Any],
|
|
365
342
|
done: set[str],
|
|
366
|
-
|
|
343
|
+
missing: set[str],
|
|
367
344
|
spinner: Spinner,
|
|
368
345
|
*,
|
|
369
346
|
n_cells: int,
|
|
@@ -371,8 +348,11 @@ def _live_view(
|
|
|
371
348
|
) -> Group:
|
|
372
349
|
"""A live checklist: a tick for finished minions (with ``cells_for`` of
|
|
373
350
|
their reply in aligned columns), a spinner for the ones still running, an x
|
|
374
|
-
for
|
|
375
|
-
|
|
351
|
+
for those that never reported, under a one-line status header. ``missing``
|
|
352
|
+
is only populated in the final frame (after the deadline or a Ctrl+C); while
|
|
353
|
+
polling it's empty, so still-pending minions show a spinner. ``n_cells`` is
|
|
354
|
+
how many trailing columns ``cells_for`` produces (so blank rows stay
|
|
355
|
+
aligned)."""
|
|
376
356
|
blanks = [Text("")] * n_cells
|
|
377
357
|
grid = Table.grid(padding=(0, 1))
|
|
378
358
|
grid.add_column(no_wrap=True) # marker
|
|
@@ -380,7 +360,7 @@ def _live_view(
|
|
|
380
360
|
for _ in range(n_cells): # per-command trailing columns
|
|
381
361
|
grid.add_column(no_wrap=True, justify="left")
|
|
382
362
|
for minion in targeted:
|
|
383
|
-
if minion in
|
|
363
|
+
if minion in missing:
|
|
384
364
|
grid.add_row(Text("X", style="red"), Text(minion, style="dim"), *blanks)
|
|
385
365
|
elif minion in done:
|
|
386
366
|
grid.add_row(
|
|
@@ -389,12 +369,12 @@ def _live_view(
|
|
|
389
369
|
else:
|
|
390
370
|
grid.add_row(spinner, Text(minion, style="dim"), *blanks)
|
|
391
371
|
|
|
392
|
-
pending = len(targeted) - len(done) - len(
|
|
372
|
+
pending = len(targeted) - len(done) - len(missing)
|
|
393
373
|
bits = [f"{len(done)}/{len(targeted)} done"]
|
|
394
374
|
if pending:
|
|
395
375
|
bits.append(f"{pending} running")
|
|
396
|
-
if
|
|
397
|
-
bits.append(f"[red]{len(
|
|
376
|
+
if missing:
|
|
377
|
+
bits.append(f"[red]{len(missing)} no response[/]")
|
|
398
378
|
header = Text.from_markup(f"[dim]{' '.join(bits)}[/]")
|
|
399
379
|
return Group(header, grid)
|
|
400
380
|
|
|
@@ -405,17 +385,19 @@ def _stream_job(
|
|
|
405
385
|
*,
|
|
406
386
|
n_cells: int,
|
|
407
387
|
cells_for: Callable[[Any], list[Text]],
|
|
408
|
-
) -> tuple[dict[str, Any], set[str],
|
|
388
|
+
) -> tuple[dict[str, Any], set[str], float, bool] | None:
|
|
409
389
|
"""Fire a job async, show a live checklist, and return its raw results.
|
|
410
390
|
|
|
411
391
|
Submits ``payload`` via the ``local_async`` client (returns a job id at
|
|
412
392
|
once), then polls ``runner jobs.lookup_jid`` until every targeted minion
|
|
413
|
-
has returned or the
|
|
414
|
-
per-minion checklist (spinner -> tick), whose trailing columns
|
|
415
|
-
``cells_for(value)`` (``n_cells`` of them).
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
393
|
+
has returned, the deadline trips, or the user hits Ctrl+C. While polling it
|
|
394
|
+
shows a live per-minion checklist (spinner -> tick), whose trailing columns
|
|
395
|
+
come from ``cells_for(value)`` (``n_cells`` of them). In every case it then
|
|
396
|
+
renders the final checklist frame and returns ``(returns, outstanding,
|
|
397
|
+
start, interrupted)`` — ``outstanding`` being the targeted minions that
|
|
398
|
+
never reported — for the caller to render, or ``None`` if no job started
|
|
399
|
+
(already reported). ``call(name, **kw)`` invokes the named salt-api
|
|
400
|
+
client."""
|
|
419
401
|
submit = call("local_async", **payload)
|
|
420
402
|
info: Any = _first_return(submit)
|
|
421
403
|
jid = info.get("jid")
|
|
@@ -435,71 +417,70 @@ def _stream_job(
|
|
|
435
417
|
console.print("(no minions matched the target)")
|
|
436
418
|
return None
|
|
437
419
|
|
|
438
|
-
expected = set(targeted)
|
|
420
|
+
expected = set(targeted)
|
|
439
421
|
console.print(f"[dim]job {jid} -> {len(targeted)} minion(s)[/]")
|
|
440
422
|
start = time.monotonic()
|
|
441
423
|
returns: dict[str, Any] = {}
|
|
442
|
-
dead: set[str] = set() # probed and confirmed not running the job
|
|
443
424
|
spinner = Spinner("dots", style="cyan")
|
|
444
425
|
|
|
445
|
-
def view() -> Group:
|
|
426
|
+
def view(missing: set[str] | None = None) -> Group:
|
|
446
427
|
done = expected & set(returns)
|
|
447
428
|
return _live_view(
|
|
448
|
-
targeted,
|
|
429
|
+
targeted,
|
|
430
|
+
returns,
|
|
431
|
+
done,
|
|
432
|
+
missing or set(),
|
|
433
|
+
spinner,
|
|
434
|
+
n_cells=n_cells,
|
|
435
|
+
cells_for=cells_for,
|
|
449
436
|
)
|
|
450
437
|
|
|
451
|
-
#
|
|
452
|
-
#
|
|
438
|
+
# Poll lookup_jid until everyone's back or the deadline trips; Ctrl+C stops
|
|
439
|
+
# waiting early. The job keeps running on the minions either way — we just
|
|
440
|
+
# stop watching and render whatever was gathered. transient=False keeps the
|
|
441
|
+
# finished checklist on screen above the rendered tables.
|
|
442
|
+
interrupted = False
|
|
453
443
|
with Live(console=console, refresh_per_second=12, transient=False) as live:
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
time.sleep(_POLL_INTERVAL)
|
|
487
|
-
|
|
488
|
-
return returns, dead, expected, start
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
def _print_stragglers(dead: set[str], stalled: list[str]) -> None:
|
|
492
|
-
"""The shared trailer for a streamed job: who never answered and who was
|
|
493
|
-
still running when the deadline tripped."""
|
|
494
|
-
if dead:
|
|
444
|
+
try:
|
|
445
|
+
while True:
|
|
446
|
+
# lookup_jid is cumulative: each poll returns every minion that
|
|
447
|
+
# has reported so far, so we just keep the latest snapshot.
|
|
448
|
+
returns = _lookup_returns(
|
|
449
|
+
_first_return(
|
|
450
|
+
call("runner", fun="jobs.lookup_jid", kwarg={"jid": jid})
|
|
451
|
+
)
|
|
452
|
+
)
|
|
453
|
+
live.update(view())
|
|
454
|
+
if not expected - set(returns):
|
|
455
|
+
break
|
|
456
|
+
if time.monotonic() - start > _POLL_DEADLINE:
|
|
457
|
+
break
|
|
458
|
+
time.sleep(_POLL_INTERVAL)
|
|
459
|
+
except KeyboardInterrupt:
|
|
460
|
+
interrupted = True
|
|
461
|
+
# Final frame: mark whoever never reported so the persisted checklist
|
|
462
|
+
# reflects the true end state rather than a frozen spinner.
|
|
463
|
+
outstanding = expected - set(returns)
|
|
464
|
+
live.update(view(outstanding))
|
|
465
|
+
|
|
466
|
+
return returns, expected - set(returns), start, interrupted
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _print_outstanding(outstanding: set[str], interrupted: bool) -> None:
|
|
470
|
+
"""Trailer naming the minions that hadn't reported when we stopped waiting
|
|
471
|
+
— because the user interrupted, or the deadline tripped."""
|
|
472
|
+
if not outstanding:
|
|
473
|
+
return
|
|
474
|
+
names = ", ".join(sorted(outstanding, key=_natural_key))
|
|
475
|
+
if interrupted:
|
|
495
476
|
console.print(
|
|
496
|
-
f"[yellow]no
|
|
497
|
-
f"
|
|
477
|
+
f"[yellow]stopped waiting (Ctrl+C); no result yet from: {names} "
|
|
478
|
+
f"- the job may still be running on them[/]"
|
|
498
479
|
)
|
|
499
|
-
|
|
480
|
+
else:
|
|
500
481
|
console.print(
|
|
501
|
-
f"[yellow]
|
|
502
|
-
f"{
|
|
482
|
+
f"[yellow]no result from: {names} within the "
|
|
483
|
+
f"{int(_POLL_DEADLINE)}s deadline (still running, or down)[/]"
|
|
503
484
|
)
|
|
504
485
|
|
|
505
486
|
|
|
@@ -509,11 +490,11 @@ def _stream_state(call: Callable[..., dict[str, Any]], payload: dict[str, Any])
|
|
|
509
490
|
result = _stream_job(call, payload, n_cells=5, cells_for=_state_cells)
|
|
510
491
|
if result is None:
|
|
511
492
|
return
|
|
512
|
-
returns,
|
|
493
|
+
returns, outstanding, start, interrupted = result
|
|
513
494
|
|
|
514
495
|
# Live view cleared — render the coloured tables, one block per minion.
|
|
515
496
|
_print_state_result({"return": [returns]})
|
|
516
|
-
|
|
497
|
+
_print_outstanding(outstanding, interrupted)
|
|
517
498
|
|
|
518
499
|
# Fleet-wide summary: totals across all minions + wall-clock elapsed.
|
|
519
500
|
totals, n = _grand_totals(returns)
|
|
@@ -673,10 +654,10 @@ def _stream_cmd(call: Callable[..., dict[str, Any]], payload: dict[str, Any]) ->
|
|
|
673
654
|
result = _stream_job(call, payload, n_cells=1, cells_for=_cmd_cells)
|
|
674
655
|
if result is None:
|
|
675
656
|
return
|
|
676
|
-
returns,
|
|
657
|
+
returns, outstanding, start, interrupted = result
|
|
677
658
|
|
|
678
659
|
_print_cmd_result({"return": [returns]})
|
|
679
|
-
|
|
660
|
+
_print_outstanding(outstanding, interrupted)
|
|
680
661
|
|
|
681
662
|
n = len(returns)
|
|
682
663
|
if n:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.4.3"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.4.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|