dayhoff-tools 1.9.10__tar.gz → 1.9.11__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.
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/PKG-INFO +1 -1
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/cli/engine/engine_core.py +141 -227
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/pyproject.toml +1 -1
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/README.md +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/__init__.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/chemistry/standardizer.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/chemistry/utils.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/cli/__init__.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/cli/cloud_commands.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/cli/engine/__init__.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/cli/engine/engine_lifecycle.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/cli/engine/engine_maintenance.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/cli/engine/engine_management.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/cli/engine/shared.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/cli/engine/studio_commands.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/cli/main.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/cli/swarm_commands.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/cli/utility_commands.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/deployment/base.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/deployment/deploy_aws.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/deployment/deploy_gcp.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/deployment/deploy_utils.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/deployment/job_runner.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/deployment/processors.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/deployment/swarm.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/embedders.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/fasta.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/file_ops.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/h5.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/intake/gcp.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/intake/gtdb.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/intake/kegg.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/intake/mmseqs.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/intake/structure.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/intake/uniprot.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/logs.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/sqlite.py +0 -0
- {dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/warehouse.py +0 -0
@@ -231,45 +231,12 @@ def engine_status(
|
|
231
231
|
|
232
232
|
engines = response.json().get("engines", [])
|
233
233
|
engine = resolve_engine(name_or_id, engines)
|
234
|
-
|
234
|
+
|
235
|
+
# Always try to fetch live idle data from the engine for both views
|
236
|
+
live_idle_data = _fetch_live_idle_data(engine["instance_id"])
|
237
|
+
|
235
238
|
# Fast status display (default)
|
236
|
-
if not detailed:
|
237
|
-
# Fetch idle status via SSM with longer timeout
|
238
|
-
ssm = boto3.client("ssm", region_name="us-east-1")
|
239
|
-
idle_data = None # Use None to indicate no data received
|
240
|
-
|
241
|
-
if engine["state"].lower() == "running":
|
242
|
-
try:
|
243
|
-
resp = ssm.send_command(
|
244
|
-
InstanceIds=[engine["instance_id"]],
|
245
|
-
DocumentName="AWS-RunShellScript",
|
246
|
-
Parameters={
|
247
|
-
"commands": [
|
248
|
-
"cat /var/run/idle-detector/last_state.json 2>/dev/null || echo '{}'"
|
249
|
-
],
|
250
|
-
"executionTimeout": ["10"],
|
251
|
-
},
|
252
|
-
)
|
253
|
-
cid = resp["Command"]["CommandId"]
|
254
|
-
|
255
|
-
# Wait up to 3 seconds for result
|
256
|
-
for _ in range(6): # 6 * 0.5 = 3 seconds
|
257
|
-
time.sleep(0.5)
|
258
|
-
inv = ssm.get_command_invocation(
|
259
|
-
CommandId=cid, InstanceId=engine["instance_id"]
|
260
|
-
)
|
261
|
-
if inv["Status"] in ["Success", "Failed"]:
|
262
|
-
break
|
263
|
-
|
264
|
-
if inv["Status"] == "Success":
|
265
|
-
content = inv["StandardOutputContent"].strip()
|
266
|
-
if content and content != "{}":
|
267
|
-
idle_data = json.loads(content)
|
268
|
-
else:
|
269
|
-
idle_data = {} # Empty response but SSM worked
|
270
|
-
except Exception:
|
271
|
-
idle_data = None # SSM failed
|
272
|
-
|
239
|
+
if not detailed:
|
273
240
|
# Determine running state display
|
274
241
|
running_state = engine["state"].lower()
|
275
242
|
if running_state == "running":
|
@@ -282,59 +249,33 @@ def engine_status(
|
|
282
249
|
run_disp = "[dim]Stopped[/dim]"
|
283
250
|
else:
|
284
251
|
run_disp = engine["state"].capitalize()
|
285
|
-
|
286
|
-
#
|
287
|
-
idle_disp = ""
|
288
|
-
|
289
|
-
if idle_data is None:
|
290
|
-
# SSM failed - we don't know the status
|
291
|
-
idle_disp = " [dim]N/A[/dim]"
|
292
|
-
elif not idle_data:
|
293
|
-
# Empty data - likely very early in boot
|
294
|
-
idle_disp = " [dim]N/A[/dim]"
|
295
|
-
else:
|
296
|
-
# We have data
|
297
|
-
is_idle = idle_data.get("idle", False)
|
298
|
-
timeout_sec = idle_data.get("timeout_sec")
|
299
|
-
idle_seconds = idle_data.get("idle_seconds", 0) if is_idle else 0
|
300
|
-
|
301
|
-
if is_idle:
|
302
|
-
if isinstance(timeout_sec, int) and isinstance(idle_seconds, int):
|
303
|
-
remaining = max(0, timeout_sec - idle_seconds)
|
304
|
-
remaining_mins = remaining // 60
|
305
|
-
if remaining_mins == 0:
|
306
|
-
idle_disp = f" [yellow]Idle {idle_seconds//60}m/{timeout_sec//60}m: [red]<1m[/red] left[/yellow]"
|
307
|
-
else:
|
308
|
-
idle_disp = f" [yellow]Idle {idle_seconds//60}m/{timeout_sec//60}m: [red]{remaining_mins}m[/red] left[/yellow]"
|
309
|
-
else:
|
310
|
-
idle_disp = " [yellow]Idle ?/?[/yellow]"
|
311
|
-
else:
|
312
|
-
# Actively not idle
|
313
|
-
idle_disp = " [green]Active[/green]"
|
314
|
-
|
252
|
+
|
253
|
+
# Format idle display using the unified function
|
254
|
+
idle_disp = " " + _format_idle_status_display(live_idle_data, running_state)
|
255
|
+
|
315
256
|
# Build status lines - minimal info for fast view
|
316
257
|
status_lines = [
|
317
258
|
f"[blue]{engine['name']}[/blue] {run_disp}{idle_disp}",
|
318
259
|
]
|
319
|
-
|
320
|
-
# Add activity sensors if we have
|
321
|
-
if
|
260
|
+
|
261
|
+
# Add activity sensors if we have live data
|
262
|
+
if live_idle_data and live_idle_data.get("_reasons_raw"):
|
322
263
|
status_lines.append("") # blank line before sensors
|
323
|
-
|
264
|
+
|
324
265
|
sensor_map = {
|
325
266
|
"CoffeeLockSensor": ("☕", "Coffee"),
|
326
267
|
"ActiveLoginSensor": ("🐚", "SSH"),
|
327
268
|
"IDEConnectionSensor": ("🖥 ", "IDE"),
|
328
269
|
"DockerWorkloadSensor": ("🐳", "Docker"),
|
329
270
|
}
|
330
|
-
|
331
|
-
for r in
|
271
|
+
|
272
|
+
for r in live_idle_data.get("_reasons_raw", []):
|
332
273
|
sensor = r.get("sensor", "Unknown")
|
333
274
|
active = r.get("active", False)
|
334
275
|
icon, label = sensor_map.get(sensor, ("?", sensor))
|
335
276
|
status_str = "[green]YES[/green]" if active else "[dim]nope[/dim]"
|
336
277
|
status_lines.append(f" {icon} {label:6} {status_str}")
|
337
|
-
|
278
|
+
|
338
279
|
# Display in a nice panel
|
339
280
|
console.print(
|
340
281
|
Panel("\n".join(status_lines), title="Engine Status", border_style="blue")
|
@@ -352,6 +293,18 @@ def engine_status(
|
|
352
293
|
idle_detector = engine_details.get("idle_detector", {}) or {}
|
353
294
|
attached_studios = engine_details.get("attached_studios", [])
|
354
295
|
|
296
|
+
# Overlay stale API data with fresh data from the engine
|
297
|
+
if live_idle_data:
|
298
|
+
# If API didn't indicate availability, replace entirely; otherwise, update.
|
299
|
+
if not idle_detector.get("available"):
|
300
|
+
idle_detector = live_idle_data
|
301
|
+
else:
|
302
|
+
idle_detector.update(live_idle_data)
|
303
|
+
else:
|
304
|
+
# SSM failed - mark as unavailable if we don't have good data from API
|
305
|
+
if not idle_detector.get("available"):
|
306
|
+
idle_detector = {"available": False} # Mark as unavailable
|
307
|
+
|
355
308
|
# Calculate costs
|
356
309
|
launch_time = parse_launch_time(engine["launch_time"])
|
357
310
|
uptime = datetime.now(timezone.utc) - launch_time
|
@@ -406,37 +359,8 @@ def engine_status(
|
|
406
359
|
else:
|
407
360
|
run_disp = engine["state"].capitalize()
|
408
361
|
|
409
|
-
#
|
410
|
-
|
411
|
-
# If we don't have idle info or it's explicitly unavailable, show N/A
|
412
|
-
if not idle_info or idle_info.get("available") == False:
|
413
|
-
return "[dim]N/A[/dim]"
|
414
|
-
|
415
|
-
if idle_info.get("status") == "active":
|
416
|
-
return "[green]Active[/green]"
|
417
|
-
if running_state in ("stopped", "stopping"):
|
418
|
-
return "[dim]N/A[/dim]"
|
419
|
-
|
420
|
-
# If idle, show time/threshold with time remaining if available
|
421
|
-
if idle_info.get("status") == "idle":
|
422
|
-
idle_seconds_v = idle_info.get("idle_seconds")
|
423
|
-
thresh_v = idle_info.get("idle_threshold")
|
424
|
-
if isinstance(idle_seconds_v, (int, float)) and isinstance(thresh_v, (int, float)):
|
425
|
-
remaining = max(0, int(thresh_v) - int(idle_seconds_v))
|
426
|
-
remaining_mins = remaining // 60
|
427
|
-
if remaining_mins == 0:
|
428
|
-
return f"[yellow]Idle {int(idle_seconds_v)//60}m/{int(thresh_v)//60}m: [red]<1m[/red] left[/yellow]"
|
429
|
-
else:
|
430
|
-
return f"[yellow]Idle {int(idle_seconds_v)//60}m/{int(thresh_v)//60}m: [red]{remaining_mins}m[/red] left[/yellow]"
|
431
|
-
elif isinstance(thresh_v, (int, float)):
|
432
|
-
return f"[yellow]Idle ?/{int(thresh_v)//60}m[/yellow]"
|
433
|
-
else:
|
434
|
-
return "[yellow]Idle ?/?[/yellow]"
|
435
|
-
|
436
|
-
# Default to N/A if we can't determine status
|
437
|
-
return "[dim]N/A[/dim]"
|
438
|
-
|
439
|
-
active_disp = _compute_active_disp(idle_detector)
|
362
|
+
# Recompute header display with latest data
|
363
|
+
active_disp = _format_idle_status_display(idle_detector, running_state)
|
440
364
|
|
441
365
|
top_lines = [
|
442
366
|
f"[blue]{engine['name']}[/blue] {run_disp} {active_disp}\n",
|
@@ -553,122 +477,6 @@ def engine_status(
|
|
553
477
|
except Exception:
|
554
478
|
pass
|
555
479
|
|
556
|
-
# Try to enrich/fallback idle-detector details from on-engine summary file via SSM
|
557
|
-
def _fetch_idle_summary_via_ssm(instance_id: str) -> Optional[Dict]:
|
558
|
-
try:
|
559
|
-
ssm = boto3.client("ssm", region_name="us-east-1")
|
560
|
-
res = ssm.send_command(
|
561
|
-
InstanceIds=[instance_id],
|
562
|
-
DocumentName="AWS-RunShellScript",
|
563
|
-
Parameters={
|
564
|
-
"commands": [
|
565
|
-
"cat /var/run/idle-detector/last_state.json 2>/dev/null || true",
|
566
|
-
],
|
567
|
-
"executionTimeout": ["5"],
|
568
|
-
},
|
569
|
-
)
|
570
|
-
cid = res["Command"]["CommandId"]
|
571
|
-
# Wait up to 2 seconds for SSM command to complete (was 1 second)
|
572
|
-
for _ in range(4): # 4 * 0.5 = 2 seconds
|
573
|
-
time.sleep(0.5)
|
574
|
-
inv = ssm.get_command_invocation(CommandId=cid, InstanceId=instance_id)
|
575
|
-
if inv["Status"] in ["Success", "Failed"]:
|
576
|
-
break
|
577
|
-
if inv["Status"] != "Success":
|
578
|
-
return None
|
579
|
-
content = inv["StandardOutputContent"].strip()
|
580
|
-
if not content:
|
581
|
-
return None
|
582
|
-
data = json.loads(content)
|
583
|
-
# Convert last_state schema (new or old) to idle_detector schema used by CLI output
|
584
|
-
idle_info: Dict[str, Any] = {"available": True}
|
585
|
-
|
586
|
-
# Active/idle
|
587
|
-
idle_flag = bool(data.get("idle", False))
|
588
|
-
idle_info["status"] = "idle" if idle_flag else "active"
|
589
|
-
|
590
|
-
# Threshold and elapsed
|
591
|
-
if isinstance(data.get("timeout_sec"), (int, float)):
|
592
|
-
idle_info["idle_threshold"] = int(data["timeout_sec"]) # seconds
|
593
|
-
if isinstance(data.get("idle_seconds"), (int, float)):
|
594
|
-
idle_info["idle_seconds"] = int(data["idle_seconds"])
|
595
|
-
|
596
|
-
# Keep raw reasons for sensor display when available (new schema)
|
597
|
-
if isinstance(data.get("reasons"), list):
|
598
|
-
idle_info["_reasons_raw"] = data["reasons"]
|
599
|
-
else:
|
600
|
-
# Fallback: synthesize reasons from the old forensics layout
|
601
|
-
f_all = data.get("forensics", {}) or {}
|
602
|
-
synthesized = []
|
603
|
-
|
604
|
-
def _mk(sensor_name: str, key: str):
|
605
|
-
entry = f_all.get(key, {}) or {}
|
606
|
-
synthesized.append(
|
607
|
-
{
|
608
|
-
"sensor": sensor_name,
|
609
|
-
"active": bool(entry.get("active", False)),
|
610
|
-
"reason": entry.get("reason", ""),
|
611
|
-
"forensic": entry.get("forensic", {}),
|
612
|
-
}
|
613
|
-
)
|
614
|
-
|
615
|
-
_mk("CoffeeLockSensor", "coffee")
|
616
|
-
_mk("ActiveLoginSensor", "ssh")
|
617
|
-
_mk("IDEConnectionSensor", "ide")
|
618
|
-
_mk("DockerWorkloadSensor", "docker")
|
619
|
-
idle_info["_reasons_raw"] = synthesized
|
620
|
-
|
621
|
-
# Derive details from sensors
|
622
|
-
for r in idle_info.get("_reasons_raw", []):
|
623
|
-
if not r.get("active"):
|
624
|
-
continue
|
625
|
-
sensor = (r.get("sensor") or "").lower()
|
626
|
-
forensic = r.get("forensic") or {}
|
627
|
-
if sensor == "ideconnectionsensor":
|
628
|
-
# Prefer unique_pid_count written by new detector
|
629
|
-
cnt = forensic.get("unique_pid_count")
|
630
|
-
if not isinstance(cnt, int):
|
631
|
-
cnt = forensic.get("matches")
|
632
|
-
if isinstance(cnt, int):
|
633
|
-
idle_info["ide_connections"] = {"connection_count": cnt}
|
634
|
-
else:
|
635
|
-
idle_info["ide_connections"] = {"connection_count": 1}
|
636
|
-
elif sensor == "coffeelocksensor":
|
637
|
-
rem = forensic.get("remaining_sec")
|
638
|
-
if isinstance(rem, (int, float)) and rem > 0:
|
639
|
-
idle_info["coffee_lock"] = format_duration(
|
640
|
-
timedelta(seconds=int(rem))
|
641
|
-
)
|
642
|
-
elif sensor == "activeloginsensor":
|
643
|
-
sess = {
|
644
|
-
"tty": forensic.get("tty", "pts/?"),
|
645
|
-
"pid": forensic.get("pid", "?"),
|
646
|
-
"idle_time": forensic.get("idle_sec", 0),
|
647
|
-
"from_ip": forensic.get("remote_addr", "unknown"),
|
648
|
-
}
|
649
|
-
idle_info.setdefault("ssh_sessions", []).append(sess)
|
650
|
-
return idle_info
|
651
|
-
except Exception:
|
652
|
-
return None
|
653
|
-
|
654
|
-
# Always try to enrich from on-engine summary (fast, best-effort)
|
655
|
-
overlay = _fetch_idle_summary_via_ssm(engine["instance_id"])
|
656
|
-
if overlay:
|
657
|
-
# If API didn't indicate availability, replace entirely; otherwise fill gaps
|
658
|
-
if not idle_detector.get("available"):
|
659
|
-
idle_detector = overlay
|
660
|
-
else:
|
661
|
-
for k, v in overlay.items():
|
662
|
-
idle_detector.setdefault(k, v)
|
663
|
-
else:
|
664
|
-
# SSM failed - mark as unavailable if we don't have good data
|
665
|
-
if not idle_detector.get("available"):
|
666
|
-
idle_detector = {"available": False} # Mark as unavailable
|
667
|
-
|
668
|
-
# Recompute header display with latest data
|
669
|
-
active_disp = _compute_active_disp(idle_detector)
|
670
|
-
top_lines[0] = f"[blue]{engine['name']}[/blue] {run_disp} {active_disp}\n"
|
671
|
-
|
672
480
|
# Activity Sensors (show all with YES/no)
|
673
481
|
if idle_detector.get("available"):
|
674
482
|
status_lines.append("")
|
@@ -694,11 +502,6 @@ def engine_status(
|
|
694
502
|
status_lines.append(_sensor_line(" IDE ", "IDEConnectionSensor", "🖥"))
|
695
503
|
status_lines.append(_sensor_line("Docker", "DockerWorkloadSensor", "🐳"))
|
696
504
|
|
697
|
-
# Recompute display with latest idle detector data
|
698
|
-
active_disp = _compute_active_disp(idle_detector)
|
699
|
-
# Rewrite top header line (index 0) to include updated display
|
700
|
-
top_lines[0] = f"[blue]{engine['name']}[/blue] {run_disp} {active_disp}\n"
|
701
|
-
|
702
505
|
# Combine top summary and details
|
703
506
|
all_lines = top_lines + status_lines
|
704
507
|
console.print(
|
@@ -737,3 +540,114 @@ def engine_status(
|
|
737
540
|
console.print("[red]❌ Could not retrieve bootstrap log[/red]")
|
738
541
|
except Exception as e:
|
739
542
|
console.print(f"[red]❌ Error fetching log: {e}[/red]")
|
543
|
+
|
544
|
+
|
545
|
+
def _format_idle_status_display(
|
546
|
+
idle_info: Optional[Dict[str, Any]], running_state: str
|
547
|
+
) -> str:
|
548
|
+
"""Computes the rich string for active/idle status display."""
|
549
|
+
# If we don't have idle info or it's explicitly unavailable, show N/A
|
550
|
+
if not idle_info or idle_info.get("available") is False:
|
551
|
+
return "[dim]N/A[/dim]"
|
552
|
+
|
553
|
+
if idle_info.get("status") == "active":
|
554
|
+
return "[green]Active[/green]"
|
555
|
+
if running_state in ("stopped", "stopping"):
|
556
|
+
return "[dim]N/A[/dim]"
|
557
|
+
|
558
|
+
# If idle, show time/threshold with time remaining if available
|
559
|
+
if idle_info.get("status") == "idle":
|
560
|
+
idle_seconds_v = idle_info.get("idle_seconds")
|
561
|
+
thresh_v = idle_info.get("idle_threshold")
|
562
|
+
if isinstance(idle_seconds_v, (int, float)) and isinstance(
|
563
|
+
thresh_v, (int, float)
|
564
|
+
):
|
565
|
+
remaining = max(0, int(thresh_v) - int(idle_seconds_v))
|
566
|
+
remaining_mins = remaining // 60
|
567
|
+
if remaining_mins == 0:
|
568
|
+
return f"[yellow]Idle {int(idle_seconds_v)//60}m/{int(thresh_v)//60}m: [red]<1m[/red] left[/yellow]"
|
569
|
+
else:
|
570
|
+
return f"[yellow]Idle {int(idle_seconds_v)//60}m/{int(thresh_v)//60}m: [red]{remaining_mins}m[/red] left[/yellow]"
|
571
|
+
elif isinstance(thresh_v, (int, float)):
|
572
|
+
return f"[yellow]Idle ?/{int(thresh_v)//60}m[/yellow]"
|
573
|
+
else:
|
574
|
+
return "[yellow]Idle ?/?[/yellow]"
|
575
|
+
|
576
|
+
# Default to N/A if we can't determine status
|
577
|
+
return "[dim]N/A[/dim]"
|
578
|
+
|
579
|
+
|
580
|
+
def _fetch_live_idle_data(instance_id: str) -> Optional[Dict]:
|
581
|
+
"""
|
582
|
+
Fetch and parse the live idle detector state from an engine via SSM.
|
583
|
+
|
584
|
+
This is the single source of truth for on-engine idle status. It fetches
|
585
|
+
the `last_state.json` file, parses it, and transforms it into the schema
|
586
|
+
used by the CLI for display logic.
|
587
|
+
"""
|
588
|
+
try:
|
589
|
+
ssm = boto3.client("ssm", region_name="us-east-1")
|
590
|
+
res = ssm.send_command(
|
591
|
+
InstanceIds=[instance_id],
|
592
|
+
DocumentName="AWS-RunShellScript",
|
593
|
+
Parameters={
|
594
|
+
"commands": [
|
595
|
+
"cat /var/run/idle-detector/last_state.json 2>/dev/null || true",
|
596
|
+
],
|
597
|
+
"executionTimeout": ["5"],
|
598
|
+
},
|
599
|
+
)
|
600
|
+
cid = res["Command"]["CommandId"]
|
601
|
+
# Wait up to 3 seconds for SSM command to complete
|
602
|
+
for _ in range(6): # 6 * 0.5 = 3 seconds
|
603
|
+
time.sleep(0.5)
|
604
|
+
inv = ssm.get_command_invocation(CommandId=cid, InstanceId=instance_id)
|
605
|
+
if inv["Status"] in ["Success", "Failed"]:
|
606
|
+
break
|
607
|
+
if inv["Status"] != "Success":
|
608
|
+
return None
|
609
|
+
content = inv["StandardOutputContent"].strip()
|
610
|
+
if not content:
|
611
|
+
return None
|
612
|
+
data = json.loads(content)
|
613
|
+
# Convert last_state schema (new or old) to idle_detector schema used by CLI output
|
614
|
+
idle_info: Dict[str, Any] = {"available": True}
|
615
|
+
|
616
|
+
# Active/idle
|
617
|
+
idle_flag = bool(data.get("idle", False))
|
618
|
+
idle_info["status"] = "idle" if idle_flag else "active"
|
619
|
+
|
620
|
+
# Threshold and elapsed
|
621
|
+
if isinstance(data.get("timeout_sec"), (int, float)):
|
622
|
+
idle_info["idle_threshold"] = int(data["timeout_sec"]) # seconds
|
623
|
+
if isinstance(data.get("idle_seconds"), (int, float)):
|
624
|
+
idle_info["idle_seconds"] = int(data["idle_seconds"])
|
625
|
+
|
626
|
+
# Keep raw reasons for sensor display when available (new schema)
|
627
|
+
if isinstance(data.get("reasons"), list):
|
628
|
+
idle_info["_reasons_raw"] = data["reasons"]
|
629
|
+
else:
|
630
|
+
# Fallback: synthesize reasons from the old forensics layout
|
631
|
+
f_all = data.get("forensics", {}) or {}
|
632
|
+
synthesized = []
|
633
|
+
|
634
|
+
def _mk(sensor_name: str, key: str):
|
635
|
+
entry = f_all.get(key, {}) or {}
|
636
|
+
synthesized.append(
|
637
|
+
{
|
638
|
+
"sensor": sensor_name,
|
639
|
+
"active": bool(entry.get("active", False)),
|
640
|
+
"reason": entry.get("reason", ""),
|
641
|
+
"forensic": entry.get("forensic", {}),
|
642
|
+
}
|
643
|
+
)
|
644
|
+
|
645
|
+
_mk("CoffeeLockSensor", "coffee")
|
646
|
+
_mk("ActiveLoginSensor", "ssh")
|
647
|
+
_mk("IDEConnectionSensor", "ide")
|
648
|
+
_mk("DockerWorkloadSensor", "docker")
|
649
|
+
idle_info["_reasons_raw"] = synthesized
|
650
|
+
|
651
|
+
return idle_info
|
652
|
+
except Exception:
|
653
|
+
return None
|
@@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
|
|
5
5
|
|
6
6
|
[project]
|
7
7
|
name = "dayhoff-tools"
|
8
|
-
version = "1.9.
|
8
|
+
version = "1.9.11"
|
9
9
|
description = "Common tools for all the repos at Dayhoff Labs"
|
10
10
|
authors = [
|
11
11
|
{name = "Daniel Martin-Alarcon", email = "dma@dayhofflabs.com"}
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{dayhoff_tools-1.9.10 → dayhoff_tools-1.9.11}/dayhoff_tools/cli/engine/engine_maintenance.py
RENAMED
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
|
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
|