hexaqueue-cli 0.4.0__tar.gz → 0.5.0__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.
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/PKG-INFO +10 -5
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/README.md +5 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/pyproject.toml +5 -5
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/pyproject.toml.orig +5 -5
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/adapters/local.py +24 -6
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/commands/collateral.py +11 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/commands/run.py +5 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/commands/workflow.py +13 -2
- hexaqueue_cli-0.5.0/src/hexaqueue_cli/domain/models.py +11 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/main.py +43 -17
- hexaqueue_cli-0.4.0/src/hexaqueue_cli/domain/models.py +0 -47
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/__init__.py +0 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/adapters/__init__.py +0 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/adapters/presenter.py +0 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/commands/__init__.py +0 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/commands/node.py +0 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/commands/suite.py +0 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/domain/__init__.py +0 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/domain/options.py +0 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/domain/parser.py +0 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/domain/session.py +0 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/infra/__init__.py +0 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/infra/bootstrap.py +0 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/infra/options.py +0 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/ports/__init__.py +0 -0
- {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/ports/client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexaqueue-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Hexaqueue CLI - Modern developer and operator terminal interface
|
|
5
5
|
Author: Richard West
|
|
6
6
|
Author-email: Richard West <dopplereffect.us@gmail.com>
|
|
@@ -9,10 +9,10 @@ Requires-Dist: hexaqueue-core
|
|
|
9
9
|
Requires-Dist: hexaqueue-server
|
|
10
10
|
Requires-Dist: hexaqueue-worker
|
|
11
11
|
Requires-Dist: hexaqueue-workflow
|
|
12
|
-
Requires-Dist: hexastack-cli>=0.
|
|
13
|
-
Requires-Dist: hexastack-core>=0.
|
|
14
|
-
Requires-Dist: hexastack-cqrs>=0.
|
|
15
|
-
Requires-Dist: hexastack-grpc>=0.
|
|
12
|
+
Requires-Dist: hexastack-cli>=0.8.0
|
|
13
|
+
Requires-Dist: hexastack-core>=0.8.0
|
|
14
|
+
Requires-Dist: hexastack-cqrs>=0.8.0
|
|
15
|
+
Requires-Dist: hexastack-grpc>=0.8.0
|
|
16
16
|
Requires-Dist: pyyaml>=6.0.2
|
|
17
17
|
Requires-Dist: rich>=13.8.0
|
|
18
18
|
Requires-Dist: typer>=0.15.0
|
|
@@ -26,8 +26,13 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
[](https://pypi.org/project/hexaqueue-cli/)
|
|
27
27
|
[](https://www.python.org/downloads/)
|
|
28
28
|
[](https://codecov.io/github/TheTrueSCU/hexaqueue)
|
|
29
|
+
[](https://dopplereffect.us/hexaqueue/)
|
|
30
|
+
[](https://dopplereffect.us/hexastack/)
|
|
31
|
+
[](https://dopplereffect.us/hexaqual/)
|
|
29
32
|
[](../../LICENSE)
|
|
30
33
|
|
|
34
|
+
> Part of the [**Hexaqueue Scheduler**](https://dopplereffect.us/hexaqueue/) · Built on [**Hexastack**](https://dopplereffect.us/hexastack/) · Governed by [**Hexaqual**](https://dopplereffect.us/hexaqual/).
|
|
35
|
+
|
|
31
36
|
---
|
|
32
37
|
|
|
33
38
|
## 🏗️ Architecture & Dependencies
|
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
[](https://pypi.org/project/hexaqueue-cli/)
|
|
6
6
|
[](https://www.python.org/downloads/)
|
|
7
7
|
[](https://codecov.io/github/TheTrueSCU/hexaqueue)
|
|
8
|
+
[](https://dopplereffect.us/hexaqueue/)
|
|
9
|
+
[](https://dopplereffect.us/hexastack/)
|
|
10
|
+
[](https://dopplereffect.us/hexaqual/)
|
|
8
11
|
[](../../LICENSE)
|
|
9
12
|
|
|
13
|
+
> Part of the [**Hexaqueue Scheduler**](https://dopplereffect.us/hexaqueue/) · Built on [**Hexastack**](https://dopplereffect.us/hexastack/) · Governed by [**Hexaqual**](https://dopplereffect.us/hexaqual/).
|
|
14
|
+
|
|
10
15
|
---
|
|
11
16
|
|
|
12
17
|
## 🏗️ Architecture & Dependencies
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hexaqueue-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "Hexaqueue CLI - Modern developer and operator terminal interface"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -10,10 +10,10 @@ dependencies = [
|
|
|
10
10
|
"hexaqueue-server",
|
|
11
11
|
"hexaqueue-worker",
|
|
12
12
|
"hexaqueue-workflow",
|
|
13
|
-
"hexastack-cli>=0.
|
|
14
|
-
"hexastack-core>=0.
|
|
15
|
-
"hexastack-cqrs>=0.
|
|
16
|
-
"hexastack-grpc>=0.
|
|
13
|
+
"hexastack-cli>=0.8.0",
|
|
14
|
+
"hexastack-core>=0.8.0",
|
|
15
|
+
"hexastack-cqrs>=0.8.0",
|
|
16
|
+
"hexastack-grpc>=0.8.0",
|
|
17
17
|
"pyyaml>=6.0.2",
|
|
18
18
|
"rich>=13.8.0",
|
|
19
19
|
"typer>=0.15.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hexaqueue-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "Hexaqueue CLI - Modern developer and operator terminal interface"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -13,10 +13,10 @@ dependencies = [
|
|
|
13
13
|
"hexaqueue-server",
|
|
14
14
|
"hexaqueue-worker",
|
|
15
15
|
"hexaqueue-workflow",
|
|
16
|
-
"hexastack-cli>=0.
|
|
17
|
-
"hexastack-core>=0.
|
|
18
|
-
"hexastack-cqrs>=0.
|
|
19
|
-
"hexastack-grpc>=0.
|
|
16
|
+
"hexastack-cli>=0.8.0",
|
|
17
|
+
"hexastack-core>=0.8.0",
|
|
18
|
+
"hexastack-cqrs>=0.8.0",
|
|
19
|
+
"hexastack-grpc>=0.8.0",
|
|
20
20
|
"pyyaml>=6.0.2",
|
|
21
21
|
"rich>=13.8.0",
|
|
22
22
|
"typer>=0.15.0",
|
|
@@ -8,6 +8,7 @@ Notes/Architectural Intent:
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
from collections.abc import AsyncIterator
|
|
11
|
+
from pathlib import Path
|
|
11
12
|
from uuid import uuid4
|
|
12
13
|
|
|
13
14
|
from hexaqueue_cli.domain.models import ClusterStatsReport
|
|
@@ -46,7 +47,7 @@ def _check_job_mutation_permission(
|
|
|
46
47
|
) -> None:
|
|
47
48
|
"""Verify that caller has permission to mutate the job."""
|
|
48
49
|
owner = _extract_job_owner(job)
|
|
49
|
-
if owner !=
|
|
50
|
+
if owner != user_id and not elevate:
|
|
50
51
|
msg = (
|
|
51
52
|
f"Permission denied: You are not the owner of job '{job.id}' (owned by '{owner}'). "
|
|
52
53
|
f"To {action_name} this job, explicit administrative elevation (--admin / elevate=true) is required."
|
|
@@ -105,7 +106,7 @@ class LocalClientAdapter(ClientPort):
|
|
|
105
106
|
self, run_id: str, user_id: str = "default", elevate: bool = False
|
|
106
107
|
) -> RunStatusReport:
|
|
107
108
|
"""Cancel run in local in-process controller."""
|
|
108
|
-
if not elevate
|
|
109
|
+
if not elevate:
|
|
109
110
|
jobs = await self._session.controller.list_jobs()
|
|
110
111
|
run_jobs = [j for j in jobs if j.run_id == run_id]
|
|
111
112
|
for j in run_jobs:
|
|
@@ -262,6 +263,22 @@ class LocalClientAdapter(ClientPort):
|
|
|
262
263
|
checksum = (
|
|
263
264
|
checksum_sha256 if len(checksum_sha256) == 64 else checksum_sha256.zfill(64)
|
|
264
265
|
)
|
|
266
|
+
staging_uri = f"memory://staging/collateral/{name}"
|
|
267
|
+
if target_path and Path(target_path).is_file():
|
|
268
|
+
if ".." in name or "/" in name or "\\" in name or Path(name).is_absolute():
|
|
269
|
+
raise ValueError(
|
|
270
|
+
f"Collateral name '{name}' contains illegal path components."
|
|
271
|
+
)
|
|
272
|
+
content = Path(target_path).read_bytes()
|
|
273
|
+
alloc = await self._session.storage.allocate_scratch(
|
|
274
|
+
job_id="collateral", size_mb=max(1, len(content) // (1024 * 1024))
|
|
275
|
+
)
|
|
276
|
+
mount_path = Path(alloc.mount_path).resolve()
|
|
277
|
+
dest = (mount_path / name).resolve()
|
|
278
|
+
if not dest.is_relative_to(mount_path):
|
|
279
|
+
raise ValueError("Path traversal detected in collateral destination.")
|
|
280
|
+
dest.write_bytes(content)
|
|
281
|
+
staging_uri = f"file://{dest}"
|
|
265
282
|
return CollateralBundle(
|
|
266
283
|
id=f"col-{uuid4().hex[:8]}",
|
|
267
284
|
job_id="global",
|
|
@@ -271,7 +288,7 @@ class LocalClientAdapter(ClientPort):
|
|
|
271
288
|
tier=tier,
|
|
272
289
|
kind=kind,
|
|
273
290
|
state=CollateralState.REGISTERED,
|
|
274
|
-
staging_uri=
|
|
291
|
+
staging_uri=staging_uri,
|
|
275
292
|
)
|
|
276
293
|
|
|
277
294
|
async def create_bastion_session(
|
|
@@ -288,13 +305,14 @@ class LocalClientAdapter(ClientPort):
|
|
|
288
305
|
"requires explicit administrative elevation (--admin)."
|
|
289
306
|
)
|
|
290
307
|
raise PermissionDeniedError(msg)
|
|
291
|
-
|
|
308
|
+
req = PtySessionRequest(
|
|
292
309
|
session_id=session_id or f"bastion-{uuid4().hex[:8]}",
|
|
293
310
|
job_id=f"bastion-{node_id}",
|
|
294
311
|
user_id=user_id,
|
|
295
|
-
|
|
296
|
-
|
|
312
|
+
roles=["admin", "operator"],
|
|
313
|
+
command=["/bin/sh"],
|
|
297
314
|
)
|
|
315
|
+
return await self._session.pty.create_session(req, job_owner=user_id)
|
|
298
316
|
|
|
299
317
|
|
|
300
318
|
__all__ = [
|
|
@@ -78,6 +78,16 @@ def push_collateral_cmd(
|
|
|
78
78
|
sha256_hash = hashlib.sha256(content).hexdigest()
|
|
79
79
|
size_bytes = len(content)
|
|
80
80
|
asset_name = name or file_path.name
|
|
81
|
+
if (
|
|
82
|
+
".." in asset_name
|
|
83
|
+
or "/" in asset_name
|
|
84
|
+
or "\\" in asset_name
|
|
85
|
+
or Path(asset_name).is_absolute()
|
|
86
|
+
):
|
|
87
|
+
console.print(
|
|
88
|
+
f"[bold red]Error:[/] Collateral name '{asset_name}' contains illegal path components."
|
|
89
|
+
)
|
|
90
|
+
raise typer.Exit(code=1)
|
|
81
91
|
|
|
82
92
|
resolved_tier = CollateralTier(tier.upper())
|
|
83
93
|
resolved_kind = CollateralKind(kind.upper())
|
|
@@ -91,6 +101,7 @@ def push_collateral_cmd(
|
|
|
91
101
|
checksum_sha256=sha256_hash,
|
|
92
102
|
tier=resolved_tier,
|
|
93
103
|
kind=resolved_kind,
|
|
104
|
+
target_path=str(file_path),
|
|
94
105
|
user_id=user,
|
|
95
106
|
elevate=admin,
|
|
96
107
|
)
|
|
@@ -66,6 +66,11 @@ async def _execute_submit_and_watch(
|
|
|
66
66
|
presenter.render_run_status(report, resolved_fmt)
|
|
67
67
|
|
|
68
68
|
if not watch:
|
|
69
|
+
if resolved_fmt in ("table", "rich"):
|
|
70
|
+
console.print(
|
|
71
|
+
"[bold yellow]Notice:[/] Local session submitted in detached mode. "
|
|
72
|
+
"In-process execution will terminate when the process exits. Use --watch to await completion."
|
|
73
|
+
)
|
|
69
74
|
return
|
|
70
75
|
|
|
71
76
|
with console.status(f"[bold blue]Executing run {report.run_id}...[/]"):
|
|
@@ -16,6 +16,7 @@ import typer
|
|
|
16
16
|
from hexaflow.adapters.storage.sqlite import SqliteStateStore
|
|
17
17
|
from hexaflow.domain.models import WorkflowDefinition
|
|
18
18
|
from hexaflow.dsl.builder import Workflow
|
|
19
|
+
from hexastack_core.adapters.storage import LocalStorageAdapter
|
|
19
20
|
from rich.console import Console
|
|
20
21
|
|
|
21
22
|
from hexaqueue_cli.adapters.presenter import CliPresenter
|
|
@@ -192,8 +193,12 @@ def submit_cmd(
|
|
|
192
193
|
port = None
|
|
193
194
|
dispatcher = NotificationDispatcher(notification_port=port)
|
|
194
195
|
store = SqliteStateStore(db_path=db_path)
|
|
196
|
+
artifacts_dir = Path(db_path).parent / "artifacts"
|
|
197
|
+
artifacts_dir.mkdir(parents=True, exist_ok=True)
|
|
198
|
+
storage = LocalStorageAdapter(root_dir=artifacts_dir)
|
|
195
199
|
engine = HexaqueueDistributedEngine(
|
|
196
200
|
state_store=store,
|
|
201
|
+
storage=storage,
|
|
197
202
|
config=config,
|
|
198
203
|
notification_dispatcher=dispatcher,
|
|
199
204
|
)
|
|
@@ -274,7 +279,10 @@ def resume_cmd(
|
|
|
274
279
|
resolved_fmt = resolve_format(format_type)
|
|
275
280
|
|
|
276
281
|
store = SqliteStateStore(db_path=db_path)
|
|
277
|
-
|
|
282
|
+
artifacts_dir = Path(db_path).parent / "artifacts"
|
|
283
|
+
artifacts_dir.mkdir(parents=True, exist_ok=True)
|
|
284
|
+
storage = LocalStorageAdapter(root_dir=artifacts_dir)
|
|
285
|
+
engine = HexaqueueDistributedEngine(state_store=store, storage=storage)
|
|
278
286
|
|
|
279
287
|
if resolved_fmt in ("table", "rich"):
|
|
280
288
|
console.print(f"[bold yellow]Resuming workflow run:[/] [cyan]{run_id}[/]")
|
|
@@ -311,7 +319,10 @@ def abort_cmd(
|
|
|
311
319
|
workflow = load_workflow_from_target(target)
|
|
312
320
|
resolved_fmt = resolve_format(format_type)
|
|
313
321
|
store = SqliteStateStore(db_path=db_path)
|
|
314
|
-
|
|
322
|
+
artifacts_dir = Path(db_path).parent / "artifacts"
|
|
323
|
+
artifacts_dir.mkdir(parents=True, exist_ok=True)
|
|
324
|
+
storage = LocalStorageAdapter(root_dir=artifacts_dir)
|
|
325
|
+
engine = HexaqueueDistributedEngine(state_store=store, storage=storage)
|
|
315
326
|
|
|
316
327
|
if resolved_fmt in ("table", "rich"):
|
|
317
328
|
console.print(f"[bold red]Aborting workflow run:[/] [cyan]{run_id}[/]")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Domain models for CLI cluster monitoring and statistics.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Defines presentation and reporting payloads for `hq stat`, `hq top`, and `hq nodes`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from hexaqueue_core.domain.cqrs import ClusterStatsReport
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ClusterStatsReport",
|
|
11
|
+
]
|
|
@@ -7,6 +7,7 @@ Notes/Architectural Intent:
|
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
9
|
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
10
11
|
from uuid import uuid4
|
|
11
12
|
|
|
12
13
|
import typer
|
|
@@ -148,7 +149,8 @@ async def _stream_live_logs(
|
|
|
148
149
|
async for chunk in client.stream_logs(job_id, follow=True, tail=tail):
|
|
149
150
|
_print_log_chunk(chunk)
|
|
150
151
|
except (asyncio.CancelledError, KeyboardInterrupt):
|
|
151
|
-
|
|
152
|
+
# User interruption (Ctrl+C) or coroutine cancellation; exit live log streaming cleanly.
|
|
153
|
+
return
|
|
152
154
|
|
|
153
155
|
|
|
154
156
|
@app.command("logs")
|
|
@@ -451,6 +453,16 @@ def top_cmd(
|
|
|
451
453
|
asyncio.run(_top())
|
|
452
454
|
|
|
453
455
|
|
|
456
|
+
def _extract_job_owner(job: Any) -> str:
|
|
457
|
+
"""Extract owner identity from job tags or user attribute."""
|
|
458
|
+
for tag in getattr(job, "tags", []):
|
|
459
|
+
if tag.startswith("owner:"):
|
|
460
|
+
return tag.split(":", 1)[1]
|
|
461
|
+
if getattr(job, "user", "default") != "default":
|
|
462
|
+
return job.user
|
|
463
|
+
return getattr(job, "env", {}).get("HEXAQUEUE_OWNER", "default")
|
|
464
|
+
|
|
465
|
+
|
|
454
466
|
@app.command("exec")
|
|
455
467
|
def exec_cmd(
|
|
456
468
|
job_id: str = typer.Argument(..., help="Job ID to execute command in"),
|
|
@@ -458,6 +470,9 @@ def exec_cmd(
|
|
|
458
470
|
..., help="Command vector to run inside job environment"
|
|
459
471
|
),
|
|
460
472
|
user: str = typer.Option("default", "--user", "-u", help="Requesting username"),
|
|
473
|
+
admin: bool = typer.Option(
|
|
474
|
+
False, "--admin", help="Assert explicit administrative elevation"
|
|
475
|
+
),
|
|
461
476
|
) -> None:
|
|
462
477
|
"""Spawn an interactive PTY session inside a running job environment.
|
|
463
478
|
|
|
@@ -465,6 +480,7 @@ def exec_cmd(
|
|
|
465
480
|
job_id: Target running job identifier.
|
|
466
481
|
command: Command vector to execute in terminal PTY.
|
|
467
482
|
user: Requesting user identity for RBAC validation.
|
|
483
|
+
admin: Whether explicit administrative elevation is requested.
|
|
468
484
|
|
|
469
485
|
Notes/Architectural Intent:
|
|
470
486
|
Connects local terminal to remote worker PTY master bridge with RBAC authorization.
|
|
@@ -472,15 +488,18 @@ def exec_cmd(
|
|
|
472
488
|
|
|
473
489
|
async def _exec() -> None:
|
|
474
490
|
client = LocalClientAdapter()
|
|
475
|
-
request = PtySessionRequest(
|
|
476
|
-
session_id=f"pty-{uuid4().hex[:8]}",
|
|
477
|
-
job_id=job_id,
|
|
478
|
-
user_id=user,
|
|
479
|
-
roles=["operator"],
|
|
480
|
-
command=command,
|
|
481
|
-
)
|
|
482
491
|
try:
|
|
483
|
-
|
|
492
|
+
job = await client.get_job(job_id)
|
|
493
|
+
job_owner = _extract_job_owner(job)
|
|
494
|
+
roles = ["operator"] if admin else []
|
|
495
|
+
request = PtySessionRequest(
|
|
496
|
+
session_id=f"pty-{uuid4().hex[:8]}",
|
|
497
|
+
job_id=job_id,
|
|
498
|
+
user_id=user,
|
|
499
|
+
roles=roles,
|
|
500
|
+
command=command,
|
|
501
|
+
)
|
|
502
|
+
session_info = await client.create_pty_session(request, job_owner=job_owner)
|
|
484
503
|
console.print(
|
|
485
504
|
f"[bold green]✓[/] Attached PTY session '{session_info.session_id}' "
|
|
486
505
|
f"to job '{job_id}' (pid: {session_info.pid})"
|
|
@@ -498,12 +517,16 @@ def exec_cmd(
|
|
|
498
517
|
def attach_cmd(
|
|
499
518
|
job_id: str = typer.Argument(..., help="Job ID to attach interactive shell to"),
|
|
500
519
|
user: str = typer.Option("default", "--user", "-u", help="Requesting username"),
|
|
520
|
+
admin: bool = typer.Option(
|
|
521
|
+
False, "--admin", help="Assert explicit administrative elevation"
|
|
522
|
+
),
|
|
501
523
|
) -> None:
|
|
502
524
|
"""Attach an interactive bash shell to a running job.
|
|
503
525
|
|
|
504
526
|
Args:
|
|
505
527
|
job_id: Target running job identifier.
|
|
506
528
|
user: Requesting user identity for RBAC validation.
|
|
529
|
+
admin: Whether explicit administrative elevation is requested.
|
|
507
530
|
|
|
508
531
|
Notes/Architectural Intent:
|
|
509
532
|
Convenience alias launching an interactive bash terminal inside the target job.
|
|
@@ -511,15 +534,18 @@ def attach_cmd(
|
|
|
511
534
|
|
|
512
535
|
async def _attach() -> None:
|
|
513
536
|
client = LocalClientAdapter()
|
|
514
|
-
request = PtySessionRequest(
|
|
515
|
-
session_id=f"pty-{uuid4().hex[:8]}",
|
|
516
|
-
job_id=job_id,
|
|
517
|
-
user_id=user,
|
|
518
|
-
roles=["operator"],
|
|
519
|
-
command=["/bin/bash"],
|
|
520
|
-
)
|
|
521
537
|
try:
|
|
522
|
-
|
|
538
|
+
job = await client.get_job(job_id)
|
|
539
|
+
job_owner = _extract_job_owner(job)
|
|
540
|
+
roles = ["operator"] if admin else []
|
|
541
|
+
request = PtySessionRequest(
|
|
542
|
+
session_id=f"pty-{uuid4().hex[:8]}",
|
|
543
|
+
job_id=job_id,
|
|
544
|
+
user_id=user,
|
|
545
|
+
roles=roles,
|
|
546
|
+
command=["/bin/bash"],
|
|
547
|
+
)
|
|
548
|
+
session_info = await client.create_pty_session(request, job_owner=job_owner)
|
|
523
549
|
console.print(
|
|
524
550
|
f"[bold green]✓[/] Attached shell session '{session_info.session_id}' "
|
|
525
551
|
f"to job '{job_id}' (pid: {session_info.pid})"
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"""Domain models for CLI cluster monitoring and statistics.
|
|
2
|
-
|
|
3
|
-
Notes/Architectural Intent:
|
|
4
|
-
Defines presentation and reporting payloads for `hq stat`, `hq top`, and `hq nodes`.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from datetime import UTC, datetime
|
|
8
|
-
|
|
9
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class ClusterStatsReport(BaseModel):
|
|
13
|
-
"""Aggregate cluster state, backlog, and worker capacity metrics.
|
|
14
|
-
|
|
15
|
-
Args:
|
|
16
|
-
total_runs: Count of active/historical runs.
|
|
17
|
-
total_jobs: Total jobs registered in cluster.
|
|
18
|
-
running_jobs: Jobs actively executing in compute worker slots.
|
|
19
|
-
pending_jobs: Jobs queued and awaiting eligible compute slots.
|
|
20
|
-
blocked_jobs: Jobs blocked on dependencies or administrative holds.
|
|
21
|
-
completed_jobs: Jobs terminated successfully.
|
|
22
|
-
failed_jobs: Jobs terminated with failure or error.
|
|
23
|
-
active_workers: Count of active worker daemons.
|
|
24
|
-
timestamp: Report generation timestamp.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
28
|
-
|
|
29
|
-
active_workers: int = Field(
|
|
30
|
-
default=1, ge=0, description="Active worker daemons count"
|
|
31
|
-
)
|
|
32
|
-
blocked_jobs: int = Field(default=0, ge=0, description="Blocked jobs count")
|
|
33
|
-
completed_jobs: int = Field(default=0, ge=0, description="Completed jobs count")
|
|
34
|
-
failed_jobs: int = Field(default=0, ge=0, description="Failed jobs count")
|
|
35
|
-
pending_jobs: int = Field(default=0, ge=0, description="Pending jobs count")
|
|
36
|
-
running_jobs: int = Field(default=0, ge=0, description="Running jobs count")
|
|
37
|
-
timestamp: datetime = Field(
|
|
38
|
-
default_factory=lambda: datetime.now(UTC),
|
|
39
|
-
description="Stats snapshot timestamp",
|
|
40
|
-
)
|
|
41
|
-
total_jobs: int = Field(default=0, ge=0, description="Total jobs count")
|
|
42
|
-
total_runs: int = Field(default=0, ge=0, description="Total runs count")
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
__all__ = [
|
|
46
|
-
"ClusterStatsReport",
|
|
47
|
-
]
|
|
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
|