salt-api-cli 1.4.1__tar.gz → 1.4.2__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.2}/PKG-INFO +1 -1
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.2}/salt_api_cli/highlevel.py +83 -122
- salt_api_cli-1.4.2/salt_api_cli/version.py +1 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.2/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.2}/MANIFEST.in +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.2}/README.md +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.2}/pyproject.toml +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.2}/salt_api_cli/__init__.py +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.2}/salt_api_cli/__main__.py +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.2}/salt_api_cli/cli.py +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.2}/salt_api_cli/lowlevel.py +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.2}/salt_api_cli/py.typed +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.2}/salt_api_cli.egg-info/SOURCES.txt +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.2}/salt_api_cli.egg-info/dependency_links.txt +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.2}/salt_api_cli.egg-info/entry_points.txt +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.2}/salt_api_cli.egg-info/requires.txt +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.2}/salt_api_cli.egg-info/top_level.txt +0 -0
- {salt_api_cli-1.4.1 → salt_api_cli-1.4.2}/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
|
|
|
@@ -249,28 +249,17 @@ def _print_state_result(result: dict[str, Any]) -> None:
|
|
|
249
249
|
# How often to poll jobs.lookup_jid, and how long to keep waiting overall
|
|
250
250
|
# before giving up on minions that never reported. Each poll is a fast,
|
|
251
251
|
# self-contained request, so the proxy/gateway connection cap never bites.
|
|
252
|
+
#
|
|
253
|
+
# We don't probe minion liveness (saltutil.find_job): an empty probe is
|
|
254
|
+
# ambiguous — a busy-but-alive Windows minion mid-highstate can simply fail to
|
|
255
|
+
# answer in time and look identical to a down one, so probing wrongly dropped
|
|
256
|
+
# live minions. Instead we just poll until every targeted minion has returned
|
|
257
|
+
# or _POLL_DEADLINE trips, then render whatever came back. The job keeps
|
|
258
|
+
# running on the minions regardless; results stay fetchable later by jid. Press
|
|
259
|
+
# Ctrl+C to stop waiting early and render the partial results gathered so far.
|
|
252
260
|
_POLL_INTERVAL = 3.0
|
|
253
261
|
_POLL_DEADLINE = 1800.0 # 30 minutes (hard backstop)
|
|
254
262
|
|
|
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
263
|
|
|
275
264
|
def _first_return(resp: dict[str, Any]) -> Any:
|
|
276
265
|
"""The first element of a salt-api ``return`` list, or ``{}`` if absent."""
|
|
@@ -280,38 +269,6 @@ def _first_return(resp: dict[str, Any]) -> Any:
|
|
|
280
269
|
return {}
|
|
281
270
|
|
|
282
271
|
|
|
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
272
|
def _lookup_returns(raw: Any) -> dict[str, Any]:
|
|
316
273
|
"""Pull the ``{minion: state_return}`` map out of a jobs.lookup_jid reply.
|
|
317
274
|
|
|
@@ -363,7 +320,7 @@ def _live_view(
|
|
|
363
320
|
targeted: list[str],
|
|
364
321
|
returns: dict[str, Any],
|
|
365
322
|
done: set[str],
|
|
366
|
-
|
|
323
|
+
missing: set[str],
|
|
367
324
|
spinner: Spinner,
|
|
368
325
|
*,
|
|
369
326
|
n_cells: int,
|
|
@@ -371,8 +328,11 @@ def _live_view(
|
|
|
371
328
|
) -> Group:
|
|
372
329
|
"""A live checklist: a tick for finished minions (with ``cells_for`` of
|
|
373
330
|
their reply in aligned columns), a spinner for the ones still running, an x
|
|
374
|
-
for
|
|
375
|
-
|
|
331
|
+
for those that never reported, under a one-line status header. ``missing``
|
|
332
|
+
is only populated in the final frame (after the deadline or a Ctrl+C); while
|
|
333
|
+
polling it's empty, so still-pending minions show a spinner. ``n_cells`` is
|
|
334
|
+
how many trailing columns ``cells_for`` produces (so blank rows stay
|
|
335
|
+
aligned)."""
|
|
376
336
|
blanks = [Text("")] * n_cells
|
|
377
337
|
grid = Table.grid(padding=(0, 1))
|
|
378
338
|
grid.add_column(no_wrap=True) # marker
|
|
@@ -380,7 +340,7 @@ def _live_view(
|
|
|
380
340
|
for _ in range(n_cells): # per-command trailing columns
|
|
381
341
|
grid.add_column(no_wrap=True, justify="left")
|
|
382
342
|
for minion in targeted:
|
|
383
|
-
if minion in
|
|
343
|
+
if minion in missing:
|
|
384
344
|
grid.add_row(Text("X", style="red"), Text(minion, style="dim"), *blanks)
|
|
385
345
|
elif minion in done:
|
|
386
346
|
grid.add_row(
|
|
@@ -389,12 +349,12 @@ def _live_view(
|
|
|
389
349
|
else:
|
|
390
350
|
grid.add_row(spinner, Text(minion, style="dim"), *blanks)
|
|
391
351
|
|
|
392
|
-
pending = len(targeted) - len(done) - len(
|
|
352
|
+
pending = len(targeted) - len(done) - len(missing)
|
|
393
353
|
bits = [f"{len(done)}/{len(targeted)} done"]
|
|
394
354
|
if pending:
|
|
395
355
|
bits.append(f"{pending} running")
|
|
396
|
-
if
|
|
397
|
-
bits.append(f"[red]{len(
|
|
356
|
+
if missing:
|
|
357
|
+
bits.append(f"[red]{len(missing)} no response[/]")
|
|
398
358
|
header = Text.from_markup(f"[dim]{' '.join(bits)}[/]")
|
|
399
359
|
return Group(header, grid)
|
|
400
360
|
|
|
@@ -405,17 +365,19 @@ def _stream_job(
|
|
|
405
365
|
*,
|
|
406
366
|
n_cells: int,
|
|
407
367
|
cells_for: Callable[[Any], list[Text]],
|
|
408
|
-
) -> tuple[dict[str, Any], set[str],
|
|
368
|
+
) -> tuple[dict[str, Any], set[str], float, bool] | None:
|
|
409
369
|
"""Fire a job async, show a live checklist, and return its raw results.
|
|
410
370
|
|
|
411
371
|
Submits ``payload`` via the ``local_async`` client (returns a job id at
|
|
412
372
|
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
|
-
|
|
373
|
+
has returned, the deadline trips, or the user hits Ctrl+C. While polling it
|
|
374
|
+
shows a live per-minion checklist (spinner -> tick), whose trailing columns
|
|
375
|
+
come from ``cells_for(value)`` (``n_cells`` of them). In every case it then
|
|
376
|
+
renders the final checklist frame and returns ``(returns, outstanding,
|
|
377
|
+
start, interrupted)`` — ``outstanding`` being the targeted minions that
|
|
378
|
+
never reported — for the caller to render, or ``None`` if no job started
|
|
379
|
+
(already reported). ``call(name, **kw)`` invokes the named salt-api
|
|
380
|
+
client."""
|
|
419
381
|
submit = call("local_async", **payload)
|
|
420
382
|
info: Any = _first_return(submit)
|
|
421
383
|
jid = info.get("jid")
|
|
@@ -435,71 +397,70 @@ def _stream_job(
|
|
|
435
397
|
console.print("(no minions matched the target)")
|
|
436
398
|
return None
|
|
437
399
|
|
|
438
|
-
expected = set(targeted)
|
|
400
|
+
expected = set(targeted)
|
|
439
401
|
console.print(f"[dim]job {jid} -> {len(targeted)} minion(s)[/]")
|
|
440
402
|
start = time.monotonic()
|
|
441
403
|
returns: dict[str, Any] = {}
|
|
442
|
-
dead: set[str] = set() # probed and confirmed not running the job
|
|
443
404
|
spinner = Spinner("dots", style="cyan")
|
|
444
405
|
|
|
445
|
-
def view() -> Group:
|
|
406
|
+
def view(missing: set[str] | None = None) -> Group:
|
|
446
407
|
done = expected & set(returns)
|
|
447
408
|
return _live_view(
|
|
448
|
-
targeted,
|
|
409
|
+
targeted,
|
|
410
|
+
returns,
|
|
411
|
+
done,
|
|
412
|
+
missing or set(),
|
|
413
|
+
spinner,
|
|
414
|
+
n_cells=n_cells,
|
|
415
|
+
cells_for=cells_for,
|
|
449
416
|
)
|
|
450
417
|
|
|
451
|
-
#
|
|
452
|
-
#
|
|
418
|
+
# Poll lookup_jid until everyone's back or the deadline trips; Ctrl+C stops
|
|
419
|
+
# waiting early. The job keeps running on the minions either way — we just
|
|
420
|
+
# stop watching and render whatever was gathered. transient=False keeps the
|
|
421
|
+
# finished checklist on screen above the rendered tables.
|
|
422
|
+
interrupted = False
|
|
453
423
|
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:
|
|
424
|
+
try:
|
|
425
|
+
while True:
|
|
426
|
+
# lookup_jid is cumulative: each poll returns every minion that
|
|
427
|
+
# has reported so far, so we just keep the latest snapshot.
|
|
428
|
+
returns = _lookup_returns(
|
|
429
|
+
_first_return(
|
|
430
|
+
call("runner", fun="jobs.lookup_jid", kwarg={"jid": jid})
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
live.update(view())
|
|
434
|
+
if not expected - set(returns):
|
|
435
|
+
break
|
|
436
|
+
if time.monotonic() - start > _POLL_DEADLINE:
|
|
437
|
+
break
|
|
438
|
+
time.sleep(_POLL_INTERVAL)
|
|
439
|
+
except KeyboardInterrupt:
|
|
440
|
+
interrupted = True
|
|
441
|
+
# Final frame: mark whoever never reported so the persisted checklist
|
|
442
|
+
# reflects the true end state rather than a frozen spinner.
|
|
443
|
+
outstanding = expected - set(returns)
|
|
444
|
+
live.update(view(outstanding))
|
|
445
|
+
|
|
446
|
+
return returns, expected - set(returns), start, interrupted
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _print_outstanding(outstanding: set[str], interrupted: bool) -> None:
|
|
450
|
+
"""Trailer naming the minions that hadn't reported when we stopped waiting
|
|
451
|
+
— because the user interrupted, or the deadline tripped."""
|
|
452
|
+
if not outstanding:
|
|
453
|
+
return
|
|
454
|
+
names = ", ".join(sorted(outstanding, key=_natural_key))
|
|
455
|
+
if interrupted:
|
|
495
456
|
console.print(
|
|
496
|
-
f"[yellow]no
|
|
497
|
-
f"
|
|
457
|
+
f"[yellow]stopped waiting (Ctrl+C); no result yet from: {names} "
|
|
458
|
+
f"- the job may still be running on them[/]"
|
|
498
459
|
)
|
|
499
|
-
|
|
460
|
+
else:
|
|
500
461
|
console.print(
|
|
501
|
-
f"[yellow]
|
|
502
|
-
f"{
|
|
462
|
+
f"[yellow]no result from: {names} within the "
|
|
463
|
+
f"{int(_POLL_DEADLINE)}s deadline (still running, or down)[/]"
|
|
503
464
|
)
|
|
504
465
|
|
|
505
466
|
|
|
@@ -509,11 +470,11 @@ def _stream_state(call: Callable[..., dict[str, Any]], payload: dict[str, Any])
|
|
|
509
470
|
result = _stream_job(call, payload, n_cells=5, cells_for=_state_cells)
|
|
510
471
|
if result is None:
|
|
511
472
|
return
|
|
512
|
-
returns,
|
|
473
|
+
returns, outstanding, start, interrupted = result
|
|
513
474
|
|
|
514
475
|
# Live view cleared — render the coloured tables, one block per minion.
|
|
515
476
|
_print_state_result({"return": [returns]})
|
|
516
|
-
|
|
477
|
+
_print_outstanding(outstanding, interrupted)
|
|
517
478
|
|
|
518
479
|
# Fleet-wide summary: totals across all minions + wall-clock elapsed.
|
|
519
480
|
totals, n = _grand_totals(returns)
|
|
@@ -673,10 +634,10 @@ def _stream_cmd(call: Callable[..., dict[str, Any]], payload: dict[str, Any]) ->
|
|
|
673
634
|
result = _stream_job(call, payload, n_cells=1, cells_for=_cmd_cells)
|
|
674
635
|
if result is None:
|
|
675
636
|
return
|
|
676
|
-
returns,
|
|
637
|
+
returns, outstanding, start, interrupted = result
|
|
677
638
|
|
|
678
639
|
_print_cmd_result({"return": [returns]})
|
|
679
|
-
|
|
640
|
+
_print_outstanding(outstanding, interrupted)
|
|
680
641
|
|
|
681
642
|
n = len(returns)
|
|
682
643
|
if n:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.4.2"
|
|
@@ -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
|