dayhoff-tools 1.1.10__py3-none-any.whl → 1.13.12__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.
- dayhoff_tools/__init__.py +10 -0
- dayhoff_tools/cli/cloud_commands.py +179 -43
- dayhoff_tools/cli/engine1/__init__.py +323 -0
- dayhoff_tools/cli/engine1/engine_core.py +703 -0
- dayhoff_tools/cli/engine1/engine_lifecycle.py +136 -0
- dayhoff_tools/cli/engine1/engine_maintenance.py +431 -0
- dayhoff_tools/cli/engine1/engine_management.py +505 -0
- dayhoff_tools/cli/engine1/shared.py +501 -0
- dayhoff_tools/cli/engine1/studio_commands.py +825 -0
- dayhoff_tools/cli/engines_studios/__init__.py +6 -0
- dayhoff_tools/cli/engines_studios/api_client.py +351 -0
- dayhoff_tools/cli/engines_studios/auth.py +144 -0
- dayhoff_tools/cli/engines_studios/engine-studio-cli.md +1230 -0
- dayhoff_tools/cli/engines_studios/engine_commands.py +1151 -0
- dayhoff_tools/cli/engines_studios/progress.py +260 -0
- dayhoff_tools/cli/engines_studios/simulators/cli-simulators.md +151 -0
- dayhoff_tools/cli/engines_studios/simulators/demo.sh +75 -0
- dayhoff_tools/cli/engines_studios/simulators/engine_list_simulator.py +319 -0
- dayhoff_tools/cli/engines_studios/simulators/engine_status_simulator.py +369 -0
- dayhoff_tools/cli/engines_studios/simulators/idle_status_simulator.py +476 -0
- dayhoff_tools/cli/engines_studios/simulators/simulator_utils.py +180 -0
- dayhoff_tools/cli/engines_studios/simulators/studio_list_simulator.py +374 -0
- dayhoff_tools/cli/engines_studios/simulators/studio_status_simulator.py +164 -0
- dayhoff_tools/cli/engines_studios/studio_commands.py +755 -0
- dayhoff_tools/cli/main.py +106 -7
- dayhoff_tools/cli/utility_commands.py +896 -179
- dayhoff_tools/deployment/base.py +70 -6
- dayhoff_tools/deployment/deploy_aws.py +165 -25
- dayhoff_tools/deployment/deploy_gcp.py +78 -5
- dayhoff_tools/deployment/deploy_utils.py +20 -7
- dayhoff_tools/deployment/job_runner.py +9 -4
- dayhoff_tools/deployment/processors.py +230 -418
- dayhoff_tools/deployment/swarm.py +47 -12
- dayhoff_tools/embedders.py +28 -26
- dayhoff_tools/fasta.py +181 -64
- dayhoff_tools/warehouse.py +268 -1
- {dayhoff_tools-1.1.10.dist-info → dayhoff_tools-1.13.12.dist-info}/METADATA +20 -5
- dayhoff_tools-1.13.12.dist-info/RECORD +54 -0
- {dayhoff_tools-1.1.10.dist-info → dayhoff_tools-1.13.12.dist-info}/WHEEL +1 -1
- dayhoff_tools-1.1.10.dist-info/RECORD +0 -32
- {dayhoff_tools-1.1.10.dist-info → dayhoff_tools-1.13.12.dist-info}/entry_points.txt +0 -0
dayhoff_tools/__init__.py
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
# The package name here should match the 'name' field in your pyproject.toml
|
|
5
|
+
__version__ = importlib.metadata.version("dayhoff-tools")
|
|
6
|
+
except importlib.metadata.PackageNotFoundError:
|
|
7
|
+
# This is a fallback for when the package might not be installed (e.g., running from source
|
|
8
|
+
# without installation, or during development). You can set it to None, "unknown",
|
|
9
|
+
# or handle it as you see fit.
|
|
10
|
+
__version__ = "unknown"
|
|
@@ -256,6 +256,32 @@ def _get_adc_status() -> str:
|
|
|
256
256
|
return "Not configured"
|
|
257
257
|
|
|
258
258
|
|
|
259
|
+
def _is_adc_authenticated() -> bool:
|
|
260
|
+
"""Check if Application Default Credentials (ADC) are valid.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
True if `gcloud auth application-default print-access-token --quiet` succeeds,
|
|
264
|
+
False otherwise.
|
|
265
|
+
"""
|
|
266
|
+
try:
|
|
267
|
+
gcloud_path = _find_executable("gcloud")
|
|
268
|
+
returncode, _, _ = _run_command(
|
|
269
|
+
[
|
|
270
|
+
gcloud_path,
|
|
271
|
+
"auth",
|
|
272
|
+
"application-default",
|
|
273
|
+
"print-access-token",
|
|
274
|
+
"--quiet",
|
|
275
|
+
],
|
|
276
|
+
capture=True,
|
|
277
|
+
check=False,
|
|
278
|
+
suppress_output=True,
|
|
279
|
+
)
|
|
280
|
+
return returncode == 0
|
|
281
|
+
except FileNotFoundError:
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
|
|
259
285
|
def _is_gcp_user_authenticated() -> bool:
|
|
260
286
|
"""Check if the current gcloud user authentication is valid and non-interactive.
|
|
261
287
|
|
|
@@ -347,7 +373,7 @@ def _run_gcloud_login() -> None:
|
|
|
347
373
|
|
|
348
374
|
|
|
349
375
|
def _test_gcp_credentials(user: str, impersonation_sa: str) -> None:
|
|
350
|
-
"""Test GCP credentials.
|
|
376
|
+
"""Test GCP credentials. Prints output on failure (to stderr) and success (to stdout)."""
|
|
351
377
|
gcloud_path = _find_executable("gcloud")
|
|
352
378
|
user_short = _get_short_name(user)
|
|
353
379
|
impersonation_short = _get_short_name(impersonation_sa)
|
|
@@ -363,64 +389,73 @@ def _test_gcp_credentials(user: str, impersonation_sa: str) -> None:
|
|
|
363
389
|
]
|
|
364
390
|
|
|
365
391
|
if impersonation_sa != "None":
|
|
392
|
+
# Test 1: Access as the user directly (temporarily disable impersonation)
|
|
393
|
+
print(f" Testing direct access as user ({user_short})...")
|
|
366
394
|
orig_sa = impersonation_sa
|
|
367
395
|
unset_rc, _, unset_err = _gcloud_unset_config(
|
|
368
396
|
"auth/impersonate_service_account"
|
|
369
397
|
)
|
|
370
398
|
if unset_rc != 0:
|
|
371
|
-
# Failure to unset is an error state
|
|
372
399
|
print(
|
|
373
|
-
f"{RED}✗ Test Error: Failed to temporarily disable impersonation: {unset_err}{NC}",
|
|
400
|
+
f" {RED}✗ Test Error: Failed to temporarily disable impersonation: {unset_err}{NC}",
|
|
374
401
|
file=sys.stderr,
|
|
375
402
|
)
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
print(
|
|
381
|
-
f"{RED}✗ Test Failure: Cannot access resources directly as user '{user_short}'. Check roles/project.{NC}",
|
|
382
|
-
file=sys.stderr,
|
|
403
|
+
# Even if unsetting fails, attempt to restore and continue with impersonation test
|
|
404
|
+
else:
|
|
405
|
+
user_returncode, _, _ = _run_command(
|
|
406
|
+
cmd, suppress_output=True, check=False
|
|
383
407
|
)
|
|
408
|
+
if user_returncode != 0:
|
|
409
|
+
print(
|
|
410
|
+
f" {RED}✗ User Test Failure: Cannot access resources directly as user '{user_short}'. Check roles/project.{NC}",
|
|
411
|
+
file=sys.stderr,
|
|
412
|
+
)
|
|
413
|
+
else:
|
|
414
|
+
print(
|
|
415
|
+
f" {GREEN}✓ User Test ({user_short}): Direct access OK{NC}"
|
|
416
|
+
)
|
|
384
417
|
|
|
418
|
+
# Restore impersonation setting
|
|
385
419
|
set_rc, _, set_err = _gcloud_set_config(
|
|
386
420
|
"auth/impersonate_service_account", orig_sa
|
|
387
421
|
)
|
|
388
422
|
if set_rc != 0:
|
|
389
|
-
# Failure to restore is an error state
|
|
390
423
|
print(
|
|
391
|
-
f"{RED}✗ Test Error: Failed to restore impersonation config for {impersonation_short}: {set_err}{NC}",
|
|
424
|
+
f" {RED}✗ Test Error: Failed to restore impersonation config for {impersonation_short}: {set_err}{NC}",
|
|
392
425
|
file=sys.stderr,
|
|
393
426
|
)
|
|
427
|
+
# If restoring fails, it's a significant issue for the next test
|
|
394
428
|
|
|
429
|
+
# Test 2: Access while impersonating the SA
|
|
430
|
+
print(f" Testing access while impersonating SA ({impersonation_short})...")
|
|
395
431
|
impersonation_returncode, _, _ = _run_command(
|
|
396
432
|
cmd, suppress_output=True, check=False
|
|
397
433
|
)
|
|
398
434
|
if impersonation_returncode != 0:
|
|
399
|
-
# Failure to access while impersonating
|
|
400
435
|
print(
|
|
401
|
-
f"{RED}✗ Test Failure: Cannot access resources impersonating '{impersonation_short}'. Check permissions/config.{NC}",
|
|
436
|
+
f" {RED}✗ Impersonation Test Failure: Cannot access resources impersonating '{impersonation_short}'. Check permissions/config.{NC}",
|
|
402
437
|
file=sys.stderr,
|
|
403
438
|
)
|
|
439
|
+
else:
|
|
440
|
+
print(
|
|
441
|
+
f" {GREEN}✓ Impersonation Test ({impersonation_short}): Access OK{NC}"
|
|
442
|
+
)
|
|
404
443
|
|
|
405
444
|
else:
|
|
406
445
|
# Test user account directly (no impersonation config)
|
|
446
|
+
print(f" Testing direct access as user ({user_short})...")
|
|
407
447
|
returncode, _, _ = _run_command(cmd, suppress_output=True, check=False)
|
|
408
448
|
if returncode != 0:
|
|
409
|
-
# Failure to access as user
|
|
410
449
|
print(
|
|
411
|
-
f"{RED}✗ Test Failure: Cannot access resources directly as user '{user_short}'. Check roles/project.{NC}",
|
|
450
|
+
f" {RED}✗ User Test Failure: Cannot access resources directly as user '{user_short}'. Check roles/project.{NC}",
|
|
412
451
|
file=sys.stderr,
|
|
413
452
|
)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
# print(f"{GREEN}✓ Direct access as user {user_short}: OK{NC}")
|
|
417
|
-
# Correctly indented pass statement if no action needed on success
|
|
418
|
-
pass
|
|
453
|
+
else:
|
|
454
|
+
print(f" {GREEN}✓ User Test ({user_short}): Direct access OK{NC}")
|
|
419
455
|
else:
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
pass # Explicit pass for the outer else
|
|
456
|
+
print(
|
|
457
|
+
f" {YELLOW}User not authenticated, skipping credential access tests.{NC}"
|
|
458
|
+
)
|
|
424
459
|
|
|
425
460
|
|
|
426
461
|
# --- AWS Functions ---
|
|
@@ -450,10 +485,14 @@ def _get_current_aws_profile() -> str:
|
|
|
450
485
|
cmd = [aws_path, "configure", "list", "--no-cli-pager"]
|
|
451
486
|
_, stdout, _ = _run_command(cmd, capture=True, check=False)
|
|
452
487
|
|
|
453
|
-
# Extract profile from output
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
488
|
+
# Extract profile from output (format: "profile : <value> : <type> : <location>")
|
|
489
|
+
# Match the line starting with "profile" and capture the value after the first colon
|
|
490
|
+
profile_match = re.search(r"^profile\s+:\s+(\S+)", stdout, re.MULTILINE)
|
|
491
|
+
if profile_match:
|
|
492
|
+
profile_value = profile_match.group(1)
|
|
493
|
+
# Check if the profile is actually set (not "<not" or "not")
|
|
494
|
+
if profile_value not in ("<not", "not"):
|
|
495
|
+
return profile_value
|
|
457
496
|
except:
|
|
458
497
|
pass
|
|
459
498
|
|
|
@@ -516,30 +555,54 @@ aws_app = typer.Typer(help="Manage AWS SSO authentication using RC files.")
|
|
|
516
555
|
# --- GCP Commands ---
|
|
517
556
|
@gcp_app.command("status")
|
|
518
557
|
def gcp_status():
|
|
519
|
-
"""Show active GCP credentials for CLI and Libraries/ADC."""
|
|
558
|
+
"""Show active GCP credentials for CLI and Libraries/ADC, including staleness."""
|
|
520
559
|
cli_user = _get_current_gcp_user()
|
|
521
560
|
cli_impersonation = _get_current_gcp_impersonation()
|
|
522
|
-
|
|
561
|
+
adc_principal_raw = _get_adc_status() # Raw status string, potentially complex
|
|
562
|
+
|
|
563
|
+
user_auth_valid = _is_gcp_user_authenticated()
|
|
564
|
+
adc_auth_valid = _is_adc_authenticated()
|
|
523
565
|
|
|
524
566
|
# Determine active principal for CLI
|
|
525
567
|
if cli_impersonation != "None":
|
|
526
568
|
cli_active_short = _get_short_name(cli_impersonation)
|
|
569
|
+
cli_is_impersonating = True
|
|
527
570
|
else:
|
|
528
571
|
cli_active_short = _get_short_name(cli_user)
|
|
572
|
+
cli_is_impersonating = False
|
|
529
573
|
|
|
530
|
-
|
|
531
|
-
adc_active_short = _get_short_name(adc_principal)
|
|
574
|
+
adc_active_short = _get_short_name(adc_principal_raw)
|
|
532
575
|
|
|
533
|
-
|
|
534
|
-
|
|
576
|
+
print(f"{BLUE}--- GCP CLI Credentials ---{NC}")
|
|
577
|
+
print(f" Effective Principal: {GREEN}{cli_active_short}{NC}")
|
|
578
|
+
print(f" User Account ({_get_short_name(cli_user)}):")
|
|
579
|
+
if user_auth_valid:
|
|
580
|
+
print(f" └─ Authentication: {GREEN}VALID{NC}")
|
|
581
|
+
else:
|
|
582
|
+
print(
|
|
583
|
+
f" └─ Authentication: {RED}STALE/EXPIRED{NC} (Hint: run 'dh gcp login')"
|
|
584
|
+
)
|
|
535
585
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
f"
|
|
541
|
-
|
|
586
|
+
if cli_is_impersonating:
|
|
587
|
+
print(
|
|
588
|
+
f" Impersonation ({_get_short_name(cli_impersonation)}): {GREEN}Active{NC}"
|
|
589
|
+
)
|
|
590
|
+
print(f" └─ Access Test: (see results below)")
|
|
591
|
+
else:
|
|
592
|
+
print(f" Impersonation: {YELLOW}Not Active{NC}")
|
|
593
|
+
|
|
594
|
+
print(f"\n{BLUE}--- GCP Library/ADC Credentials ---{NC}")
|
|
595
|
+
print(f" Effective Principal: {GREEN}{adc_active_short}{NC}")
|
|
596
|
+
if adc_principal_raw in ["Not configured", "Error reading", "Invalid format"]:
|
|
597
|
+
print(f" └─ Status: {RED}{adc_principal_raw}{NC}")
|
|
598
|
+
elif adc_auth_valid:
|
|
599
|
+
print(f" └─ Authentication: {GREEN}VALID{NC}")
|
|
600
|
+
else:
|
|
601
|
+
print(
|
|
602
|
+
f" └─ Authentication: {RED}STALE/EXPIRED{NC} (Hint: run 'dh gcp use-...-adc' or 'gcloud auth application-default login ...')"
|
|
603
|
+
)
|
|
542
604
|
|
|
605
|
+
print(f"\n{BLUE}--- GCP Access Tests (for CLI configuration) ---{NC}")
|
|
543
606
|
# Run tests silently, they will print to stderr only on failure
|
|
544
607
|
_test_gcp_credentials(cli_user, cli_impersonation)
|
|
545
608
|
|
|
@@ -843,6 +906,72 @@ def gcp_use_devcon_adc():
|
|
|
843
906
|
sys.exit(1)
|
|
844
907
|
|
|
845
908
|
|
|
909
|
+
@gcp_app.command("logout")
|
|
910
|
+
def gcp_logout():
|
|
911
|
+
"""Clear all GCP credentials for testing or role switching purposes.
|
|
912
|
+
|
|
913
|
+
This removes the active user's gcloud login, disables impersonation,
|
|
914
|
+
and invalidates Application Default Credentials (ADC).
|
|
915
|
+
"""
|
|
916
|
+
print(f"{BLUE}Clearing all GCP credentials...{NC}")
|
|
917
|
+
|
|
918
|
+
try:
|
|
919
|
+
gcloud_path = _find_executable("gcloud")
|
|
920
|
+
errors = []
|
|
921
|
+
|
|
922
|
+
# 1. Revoke user-level credentials
|
|
923
|
+
print(f"{BLUE}Revoking active gcloud credentials...{NC}")
|
|
924
|
+
revoke_cmd = [gcloud_path, "auth", "revoke", "--all", "--quiet"]
|
|
925
|
+
revoke_code, _, revoke_err = _run_command(revoke_cmd, capture=True, check=False)
|
|
926
|
+
if revoke_code != 0 and revoke_err:
|
|
927
|
+
errors.append(f"Failed to revoke credentials: {revoke_err}")
|
|
928
|
+
|
|
929
|
+
# 2. Unset impersonation config
|
|
930
|
+
print(f"{BLUE}Disabling service account impersonation...{NC}")
|
|
931
|
+
unset_code, _, unset_err = _gcloud_unset_config(
|
|
932
|
+
"auth/impersonate_service_account"
|
|
933
|
+
)
|
|
934
|
+
if unset_code != 0 and unset_err:
|
|
935
|
+
errors.append(f"Failed to unset impersonation: {unset_err}")
|
|
936
|
+
|
|
937
|
+
# 3. Revoke ADC
|
|
938
|
+
print(f"{BLUE}Revoking Application Default Credentials (ADC)...{NC}")
|
|
939
|
+
adc_cmd = [gcloud_path, "auth", "application-default", "revoke", "--quiet"]
|
|
940
|
+
adc_code, _, adc_err = _run_command(adc_cmd, capture=True, check=False)
|
|
941
|
+
|
|
942
|
+
# 4. Additionally remove ADC file if it exists (belt-and-suspenders approach)
|
|
943
|
+
adc_file = (
|
|
944
|
+
Path.home() / ".config" / "gcloud" / "application_default_credentials.json"
|
|
945
|
+
)
|
|
946
|
+
if adc_file.exists():
|
|
947
|
+
try:
|
|
948
|
+
adc_file.unlink()
|
|
949
|
+
print(f"{BLUE}Removed ADC file: {adc_file}{NC}")
|
|
950
|
+
except Exception as e:
|
|
951
|
+
errors.append(f"Failed to delete ADC file: {e}")
|
|
952
|
+
|
|
953
|
+
if errors:
|
|
954
|
+
print(f"{YELLOW}Logged out with some warnings:{NC}")
|
|
955
|
+
for err in errors:
|
|
956
|
+
print(f"{YELLOW} - {err}{NC}")
|
|
957
|
+
else:
|
|
958
|
+
print(f"{GREEN}Successfully logged out from GCP.{NC}")
|
|
959
|
+
|
|
960
|
+
# Always show how to log back in
|
|
961
|
+
print(f"\n{BLUE}To log back in:{NC}")
|
|
962
|
+
print(f" {YELLOW}dh gcp login{NC}")
|
|
963
|
+
|
|
964
|
+
# Show current (now-cleared) status
|
|
965
|
+
print(f"\n{BLUE}Current status:{NC}")
|
|
966
|
+
gcp_status()
|
|
967
|
+
|
|
968
|
+
except Exception as e:
|
|
969
|
+
print(f"{RED}Error during logout: {e}{NC}", file=sys.stderr)
|
|
970
|
+
print(f"{YELLOW}You may need to manually run:{NC}")
|
|
971
|
+
print(f" {YELLOW}gcloud auth revoke --all{NC}")
|
|
972
|
+
print(f" {YELLOW}gcloud auth application-default revoke{NC}")
|
|
973
|
+
|
|
974
|
+
|
|
846
975
|
# === End NEW ADC Commands ===
|
|
847
976
|
|
|
848
977
|
|
|
@@ -862,12 +991,19 @@ def aws_status(
|
|
|
862
991
|
# Get detailed identity information
|
|
863
992
|
aws_path = _find_executable("aws")
|
|
864
993
|
_run_command(
|
|
865
|
-
[
|
|
994
|
+
[
|
|
995
|
+
aws_path,
|
|
996
|
+
"sts",
|
|
997
|
+
"get-caller-identity",
|
|
998
|
+
"--profile",
|
|
999
|
+
target_profile,
|
|
1000
|
+
"--no-cli-pager",
|
|
1001
|
+
]
|
|
866
1002
|
)
|
|
867
1003
|
else:
|
|
868
1004
|
print(f"Credential status: {RED}not authenticated{NC}")
|
|
869
1005
|
print(f"\nTo authenticate, run:")
|
|
870
|
-
print(f" {YELLOW}dh aws login
|
|
1006
|
+
print(f" {YELLOW}dh aws login{NC}")
|
|
871
1007
|
|
|
872
1008
|
|
|
873
1009
|
@aws_app.command("login")
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Engine and Studio management commands for DHT CLI."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
# Initialize Typer apps
|
|
8
|
+
engine_app = typer.Typer(help="Manage compute engines for development.")
|
|
9
|
+
studio_app = typer.Typer(help="Manage persistent development studios.")
|
|
10
|
+
|
|
11
|
+
# Use lazy loading pattern similar to main.py swarm commands
|
|
12
|
+
# Import functions only when commands are actually called
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Engine commands
|
|
16
|
+
@engine_app.command("launch")
|
|
17
|
+
def launch_engine_cmd(
|
|
18
|
+
name: str = typer.Argument(help="Name for the new engine"),
|
|
19
|
+
engine_type: str = typer.Option(
|
|
20
|
+
"cpu",
|
|
21
|
+
"--type",
|
|
22
|
+
"-t",
|
|
23
|
+
help="Engine type: cpu, cpumax, t4, a10g, a100, 4_t4, 8_t4, 4_a10g, 8_a10g",
|
|
24
|
+
),
|
|
25
|
+
user: str = typer.Option(None, "--user", "-u", help="Override username"),
|
|
26
|
+
boot_disk_size: int = typer.Option(
|
|
27
|
+
None,
|
|
28
|
+
"--size",
|
|
29
|
+
"-s",
|
|
30
|
+
help="Boot disk size in GB (default: 50GB, min: 20GB, max: 1000GB)",
|
|
31
|
+
),
|
|
32
|
+
availability_zone: str = typer.Option(
|
|
33
|
+
None,
|
|
34
|
+
"--az",
|
|
35
|
+
help="Prefer a specific Availability Zone (e.g., us-east-1b). If omitted the service will try all public subnets.",
|
|
36
|
+
),
|
|
37
|
+
):
|
|
38
|
+
"""Launch a new engine instance."""
|
|
39
|
+
from .engine_core import launch_engine
|
|
40
|
+
|
|
41
|
+
return launch_engine(name, engine_type, user, boot_disk_size, availability_zone)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@engine_app.command("list")
|
|
45
|
+
def list_engines_cmd(
|
|
46
|
+
user: str = typer.Option(None, "--user", "-u", help="Filter by user"),
|
|
47
|
+
running_only: bool = typer.Option(
|
|
48
|
+
False, "--running", help="Show only running engines"
|
|
49
|
+
),
|
|
50
|
+
stopped_only: bool = typer.Option(
|
|
51
|
+
False, "--stopped", help="Show only stopped engines"
|
|
52
|
+
),
|
|
53
|
+
detailed: bool = typer.Option(
|
|
54
|
+
False, "--detailed", "-d", help="Show detailed status (slower)"
|
|
55
|
+
),
|
|
56
|
+
):
|
|
57
|
+
"""List engines (shows all engines by default)."""
|
|
58
|
+
from .engine_core import list_engines
|
|
59
|
+
|
|
60
|
+
return list_engines(user, running_only, stopped_only, detailed)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@engine_app.command("status")
|
|
64
|
+
def engine_status_cmd(
|
|
65
|
+
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
|
66
|
+
detailed: bool = typer.Option(
|
|
67
|
+
False, "--detailed", "-d", help="Show detailed status (slower)"
|
|
68
|
+
),
|
|
69
|
+
show_log: bool = typer.Option(
|
|
70
|
+
False, "--show-log", help="Show bootstrap log (requires --detailed)"
|
|
71
|
+
),
|
|
72
|
+
):
|
|
73
|
+
"""Show engine status and information."""
|
|
74
|
+
from .engine_core import engine_status
|
|
75
|
+
|
|
76
|
+
return engine_status(name_or_id, detailed, show_log)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@engine_app.command("start")
|
|
80
|
+
def start_engine_cmd(
|
|
81
|
+
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
|
82
|
+
):
|
|
83
|
+
"""Start a stopped engine."""
|
|
84
|
+
from .engine_lifecycle import start_engine
|
|
85
|
+
|
|
86
|
+
return start_engine(name_or_id)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@engine_app.command("stop")
|
|
90
|
+
def stop_engine_cmd(
|
|
91
|
+
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
|
92
|
+
force: bool = typer.Option(
|
|
93
|
+
False, "--force", "-f", help="Force stop and detach all studios"
|
|
94
|
+
),
|
|
95
|
+
):
|
|
96
|
+
"""Stop an engine."""
|
|
97
|
+
from .engine_lifecycle import stop_engine
|
|
98
|
+
|
|
99
|
+
return stop_engine(name_or_id, force)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@engine_app.command("terminate")
|
|
103
|
+
def terminate_engine_cmd(
|
|
104
|
+
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
|
105
|
+
):
|
|
106
|
+
"""Permanently terminate an engine."""
|
|
107
|
+
from .engine_lifecycle import terminate_engine
|
|
108
|
+
|
|
109
|
+
return terminate_engine(name_or_id)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@engine_app.command("ssh")
|
|
113
|
+
def ssh_engine_cmd(
|
|
114
|
+
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
|
115
|
+
admin: bool = typer.Option(
|
|
116
|
+
False, "--admin", help="Connect as ec2-user instead of the engine owner user"
|
|
117
|
+
),
|
|
118
|
+
idle_timeout: int = typer.Option(
|
|
119
|
+
600,
|
|
120
|
+
"--idle-timeout",
|
|
121
|
+
help="Idle timeout (seconds) for the SSM port-forward (0 = disable)",
|
|
122
|
+
),
|
|
123
|
+
):
|
|
124
|
+
"""Connect to an engine via SSH."""
|
|
125
|
+
from .engine_management import ssh_engine
|
|
126
|
+
|
|
127
|
+
return ssh_engine(name_or_id, admin, idle_timeout)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@engine_app.command("config-ssh")
|
|
131
|
+
def config_ssh_cmd(
|
|
132
|
+
clean: bool = typer.Option(False, "--clean", help="Remove all managed entries"),
|
|
133
|
+
all_engines: bool = typer.Option(
|
|
134
|
+
False, "--all", "-a", help="Include all engines from all users"
|
|
135
|
+
),
|
|
136
|
+
admin: bool = typer.Option(
|
|
137
|
+
False,
|
|
138
|
+
"--admin",
|
|
139
|
+
help="Generate entries that use ec2-user instead of per-engine owner user",
|
|
140
|
+
),
|
|
141
|
+
):
|
|
142
|
+
"""Update SSH config with available engines."""
|
|
143
|
+
from .engine_management import config_ssh
|
|
144
|
+
|
|
145
|
+
return config_ssh(clean, all_engines, admin)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@engine_app.command("resize")
|
|
149
|
+
def resize_engine_cmd(
|
|
150
|
+
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
|
151
|
+
size: int = typer.Option(..., "--size", "-s", help="New size in GB"),
|
|
152
|
+
online: bool = typer.Option(
|
|
153
|
+
False,
|
|
154
|
+
"--online",
|
|
155
|
+
help="Resize while running (requires manual filesystem expansion)",
|
|
156
|
+
),
|
|
157
|
+
force: bool = typer.Option(
|
|
158
|
+
False, "--force", "-f", help="Force resize and detach all studios"
|
|
159
|
+
),
|
|
160
|
+
):
|
|
161
|
+
"""Resize an engine's boot disk."""
|
|
162
|
+
from .engine_management import resize_engine
|
|
163
|
+
|
|
164
|
+
return resize_engine(name_or_id, size, online, force)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@engine_app.command("gami")
|
|
168
|
+
def create_ami_cmd(
|
|
169
|
+
name_or_id: str = typer.Argument(
|
|
170
|
+
help="Engine name or instance ID to create AMI from"
|
|
171
|
+
),
|
|
172
|
+
):
|
|
173
|
+
"""Create a 'Golden AMI' from a running engine."""
|
|
174
|
+
from .engine_management import create_ami
|
|
175
|
+
|
|
176
|
+
return create_ami(name_or_id)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@engine_app.command("coffee")
|
|
180
|
+
def coffee_cmd(
|
|
181
|
+
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
|
182
|
+
duration: str = typer.Argument("4h", help="Duration (e.g., 2h, 30m, 2h30m)"),
|
|
183
|
+
cancel: bool = typer.Option(
|
|
184
|
+
False, "--cancel", help="Cancel existing coffee lock instead of extending"
|
|
185
|
+
),
|
|
186
|
+
):
|
|
187
|
+
"""Pour ☕ for an engine: keeps it awake for the given duration (or cancel)."""
|
|
188
|
+
from .engine_maintenance import coffee
|
|
189
|
+
|
|
190
|
+
return coffee(name_or_id, duration, cancel)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@engine_app.command("idle")
|
|
194
|
+
def idle_timeout_cmd_wrapper(
|
|
195
|
+
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
|
196
|
+
set: Optional[str] = typer.Option(
|
|
197
|
+
None, "--set", "-s", help="New timeout (e.g., 2h30m, 45m)"
|
|
198
|
+
),
|
|
199
|
+
slack: Optional[str] = typer.Option(
|
|
200
|
+
None, "--slack", help="Set Slack notifications: none, default, all"
|
|
201
|
+
),
|
|
202
|
+
):
|
|
203
|
+
"""Show or set engine idle-detector settings."""
|
|
204
|
+
from .engine_maintenance import idle_timeout_cmd
|
|
205
|
+
|
|
206
|
+
return idle_timeout_cmd(name_or_id=name_or_id, set=set, slack=slack)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@engine_app.command("debug")
|
|
210
|
+
def debug_engine_cmd(
|
|
211
|
+
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
|
212
|
+
):
|
|
213
|
+
"""Debug engine bootstrap status and files."""
|
|
214
|
+
from .engine_maintenance import debug_engine
|
|
215
|
+
|
|
216
|
+
return debug_engine(name_or_id)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@engine_app.command("repair")
|
|
220
|
+
def repair_engine_cmd(
|
|
221
|
+
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
|
222
|
+
):
|
|
223
|
+
"""Repair an engine that's stuck in a bad state (e.g., after GAMI creation)."""
|
|
224
|
+
from .engine_maintenance import repair_engine
|
|
225
|
+
|
|
226
|
+
return repair_engine(name_or_id)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# Studio commands
|
|
230
|
+
@studio_app.command("create")
|
|
231
|
+
def create_studio_cmd(
|
|
232
|
+
size_gb: int = typer.Option(50, "--size", "-s", help="Studio size in GB"),
|
|
233
|
+
):
|
|
234
|
+
"""Create a new studio for the current user."""
|
|
235
|
+
from .studio_commands import create_studio
|
|
236
|
+
|
|
237
|
+
return create_studio(size_gb)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@studio_app.command("status")
|
|
241
|
+
def studio_status_cmd(
|
|
242
|
+
user: str = typer.Option(
|
|
243
|
+
None, "--user", "-u", help="Check status for a different user (admin only)"
|
|
244
|
+
),
|
|
245
|
+
):
|
|
246
|
+
"""Show status of your studio."""
|
|
247
|
+
from .studio_commands import studio_status
|
|
248
|
+
|
|
249
|
+
return studio_status(user)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@studio_app.command("attach")
|
|
253
|
+
def attach_studio_cmd(
|
|
254
|
+
engine_name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
|
255
|
+
user: str = typer.Option(
|
|
256
|
+
None, "--user", "-u", help="Attach a different user's studio (admin only)"
|
|
257
|
+
),
|
|
258
|
+
):
|
|
259
|
+
"""Attach your studio to an engine."""
|
|
260
|
+
from .studio_commands import attach_studio
|
|
261
|
+
|
|
262
|
+
return attach_studio(engine_name_or_id, user)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@studio_app.command("detach")
|
|
266
|
+
def detach_studio_cmd(
|
|
267
|
+
user: str = typer.Option(
|
|
268
|
+
None, "--user", "-u", help="Detach a different user's studio (admin only)"
|
|
269
|
+
),
|
|
270
|
+
):
|
|
271
|
+
"""Detach your studio from its current engine."""
|
|
272
|
+
from .studio_commands import detach_studio
|
|
273
|
+
|
|
274
|
+
return detach_studio(user)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@studio_app.command("delete")
|
|
278
|
+
def delete_studio_cmd(
|
|
279
|
+
user: str = typer.Option(
|
|
280
|
+
None, "--user", "-u", help="Delete a different user's studio (admin only)"
|
|
281
|
+
),
|
|
282
|
+
):
|
|
283
|
+
"""Delete your studio permanently."""
|
|
284
|
+
from .studio_commands import delete_studio
|
|
285
|
+
|
|
286
|
+
return delete_studio(user)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@studio_app.command("list")
|
|
290
|
+
def list_studios_cmd(
|
|
291
|
+
all_users: bool = typer.Option(
|
|
292
|
+
False, "--all", "-a", help="Show all users' studios"
|
|
293
|
+
),
|
|
294
|
+
):
|
|
295
|
+
"""List studios."""
|
|
296
|
+
from .studio_commands import list_studios
|
|
297
|
+
|
|
298
|
+
return list_studios(all_users)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@studio_app.command("reset")
|
|
302
|
+
def reset_studio_cmd(
|
|
303
|
+
user: str = typer.Option(
|
|
304
|
+
None, "--user", "-u", help="Reset a different user's studio"
|
|
305
|
+
),
|
|
306
|
+
):
|
|
307
|
+
"""Reset a stuck studio (admin operation)."""
|
|
308
|
+
from .studio_commands import reset_studio
|
|
309
|
+
|
|
310
|
+
return reset_studio(user)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@studio_app.command("resize")
|
|
314
|
+
def resize_studio_cmd(
|
|
315
|
+
size: int = typer.Option(..., "--size", "-s", help="New size in GB"),
|
|
316
|
+
user: str = typer.Option(
|
|
317
|
+
None, "--user", "-u", help="Resize a different user's studio (admin only)"
|
|
318
|
+
),
|
|
319
|
+
):
|
|
320
|
+
"""Resize your studio volume (requires detachment)."""
|
|
321
|
+
from .studio_commands import resize_studio
|
|
322
|
+
|
|
323
|
+
return resize_studio(size, user)
|