hexaqueue-cli 0.3.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.3.0 → hexaqueue_cli-0.5.0}/PKG-INFO +10 -5
- {hexaqueue_cli-0.3.0 → hexaqueue_cli-0.5.0}/README.md +5 -0
- {hexaqueue_cli-0.3.0 → hexaqueue_cli-0.5.0}/pyproject.toml +5 -5
- {hexaqueue_cli-0.3.0 → hexaqueue_cli-0.5.0}/pyproject.toml.orig +5 -5
- hexaqueue_cli-0.5.0/src/hexaqueue_cli/adapters/local.py +320 -0
- hexaqueue_cli-0.5.0/src/hexaqueue_cli/adapters/presenter.py +846 -0
- {hexaqueue_cli-0.3.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/commands/__init__.py +6 -0
- hexaqueue_cli-0.5.0/src/hexaqueue_cli/commands/collateral.py +124 -0
- hexaqueue_cli-0.5.0/src/hexaqueue_cli/commands/node.py +100 -0
- hexaqueue_cli-0.5.0/src/hexaqueue_cli/commands/run.py +180 -0
- hexaqueue_cli-0.5.0/src/hexaqueue_cli/commands/suite.py +93 -0
- {hexaqueue_cli-0.3.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/commands/workflow.py +58 -3
- {hexaqueue_cli-0.3.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/domain/__init__.py +2 -0
- hexaqueue_cli-0.5.0/src/hexaqueue_cli/domain/models.py +11 -0
- hexaqueue_cli-0.5.0/src/hexaqueue_cli/domain/session.py +108 -0
- hexaqueue_cli-0.5.0/src/hexaqueue_cli/main.py +642 -0
- hexaqueue_cli-0.5.0/src/hexaqueue_cli/ports/client.py +148 -0
- hexaqueue_cli-0.3.0/src/hexaqueue_cli/adapters/local.py +0 -46
- hexaqueue_cli-0.3.0/src/hexaqueue_cli/adapters/presenter.py +0 -371
- hexaqueue_cli-0.3.0/src/hexaqueue_cli/commands/run.py +0 -78
- hexaqueue_cli-0.3.0/src/hexaqueue_cli/domain/session.py +0 -63
- hexaqueue_cli-0.3.0/src/hexaqueue_cli/main.py +0 -97
- hexaqueue_cli-0.3.0/src/hexaqueue_cli/ports/client.py +0 -40
- {hexaqueue_cli-0.3.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/__init__.py +0 -0
- {hexaqueue_cli-0.3.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/adapters/__init__.py +0 -0
- {hexaqueue_cli-0.3.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/domain/options.py +0 -0
- {hexaqueue_cli-0.3.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/domain/parser.py +0 -0
- {hexaqueue_cli-0.3.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/infra/__init__.py +0 -0
- {hexaqueue_cli-0.3.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/infra/bootstrap.py +0 -0
- {hexaqueue_cli-0.3.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/infra/options.py +0 -0
- {hexaqueue_cli-0.3.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/ports/__init__.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",
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""In-process local client adapter for hq CLI.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Implements ClientPort by routing directly to in-process session controller
|
|
5
|
+
and log stream port without network transport overhead.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import AsyncIterator
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
from hexaqueue_cli.domain.models import ClusterStatsReport
|
|
15
|
+
from hexaqueue_cli.domain.session import LocalCliSession, get_default_session
|
|
16
|
+
from hexaqueue_cli.ports.client import ClientPort
|
|
17
|
+
from hexaqueue_core.domain.collateral import (
|
|
18
|
+
CollateralBundle,
|
|
19
|
+
CollateralKind,
|
|
20
|
+
CollateralState,
|
|
21
|
+
CollateralTier,
|
|
22
|
+
)
|
|
23
|
+
from hexaqueue_core.domain.exceptions import PermissionDeniedError
|
|
24
|
+
from hexaqueue_core.domain.explainability import (
|
|
25
|
+
FairShareTreeReport,
|
|
26
|
+
SchedulingDecisionReport,
|
|
27
|
+
)
|
|
28
|
+
from hexaqueue_core.domain.job import JobSpec
|
|
29
|
+
from hexaqueue_core.domain.run import RunSpec
|
|
30
|
+
from hexaqueue_core.domain.suite import SuiteCompiler, SuiteSpec
|
|
31
|
+
from hexaqueue_core.ports.logging import LogChunk
|
|
32
|
+
from hexaqueue_server.domain.models import RunStatusReport, RunSubmission
|
|
33
|
+
from hexaqueue_worker.domain.pty import PtySessionInfo, PtySessionRequest
|
|
34
|
+
from hexaqueue_worker.domain.telemetry import NodeTelemetryPulse
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _extract_job_owner(job: JobSpec) -> str:
|
|
38
|
+
"""Extract owner tag or env from job specification."""
|
|
39
|
+
for tag in job.tags:
|
|
40
|
+
if tag.startswith("owner:"):
|
|
41
|
+
return tag.split(":", 1)[1]
|
|
42
|
+
return job.env.get("HEXAQUEUE_OWNER", "default")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _check_job_mutation_permission(
|
|
46
|
+
job: JobSpec, user_id: str, elevate: bool, action_name: str
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Verify that caller has permission to mutate the job."""
|
|
49
|
+
owner = _extract_job_owner(job)
|
|
50
|
+
if owner != user_id and not elevate:
|
|
51
|
+
msg = (
|
|
52
|
+
f"Permission denied: You are not the owner of job '{job.id}' (owned by '{owner}'). "
|
|
53
|
+
f"To {action_name} this job, explicit administrative elevation (--admin / elevate=true) is required."
|
|
54
|
+
)
|
|
55
|
+
raise PermissionDeniedError(msg)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class LocalClientAdapter(ClientPort):
|
|
59
|
+
"""In-process local client adapter."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, session: LocalCliSession | None = None) -> None:
|
|
62
|
+
self._session = session or get_default_session()
|
|
63
|
+
|
|
64
|
+
async def submit_run(self, submission: RunSubmission) -> RunStatusReport:
|
|
65
|
+
"""Submit a pipeline run to local in-process controller."""
|
|
66
|
+
return await self._session.controller.submit_run(submission)
|
|
67
|
+
|
|
68
|
+
async def submit_suite(
|
|
69
|
+
self,
|
|
70
|
+
suite: SuiteSpec,
|
|
71
|
+
user_id: str = "default",
|
|
72
|
+
elevate: bool = False,
|
|
73
|
+
) -> RunStatusReport:
|
|
74
|
+
"""Submit a hierarchical suite pipeline run to local controller."""
|
|
75
|
+
compiler = SuiteCompiler()
|
|
76
|
+
result = compiler.compile(suite, run_id=suite.id)
|
|
77
|
+
run_spec = RunSpec(
|
|
78
|
+
id=suite.id,
|
|
79
|
+
name=suite.name or suite.id,
|
|
80
|
+
tags=[f"owner:{user_id}"],
|
|
81
|
+
)
|
|
82
|
+
dependencies = {
|
|
83
|
+
child_id: [dep.parent_job_id for dep in deps]
|
|
84
|
+
for child_id, deps in result.dependencies.items()
|
|
85
|
+
}
|
|
86
|
+
submission = RunSubmission(
|
|
87
|
+
run_spec=run_spec,
|
|
88
|
+
jobs=result.jobs,
|
|
89
|
+
dependencies=dependencies,
|
|
90
|
+
)
|
|
91
|
+
return await self._session.controller.submit_run(submission)
|
|
92
|
+
|
|
93
|
+
async def get_run_status(self, run_id: str) -> RunStatusReport:
|
|
94
|
+
"""Get run status report from local in-process controller."""
|
|
95
|
+
return await self._session.controller.get_run_status(run_id)
|
|
96
|
+
|
|
97
|
+
async def get_job(self, job_id: str) -> JobSpec:
|
|
98
|
+
"""Get job metadata from local in-process controller."""
|
|
99
|
+
return await self._session.controller.get_job(job_id)
|
|
100
|
+
|
|
101
|
+
async def list_jobs(self) -> list[JobSpec]:
|
|
102
|
+
"""List all registered jobs across runs."""
|
|
103
|
+
return await self._session.controller.list_jobs()
|
|
104
|
+
|
|
105
|
+
async def cancel_run(
|
|
106
|
+
self, run_id: str, user_id: str = "default", elevate: bool = False
|
|
107
|
+
) -> RunStatusReport:
|
|
108
|
+
"""Cancel run in local in-process controller."""
|
|
109
|
+
if not elevate:
|
|
110
|
+
jobs = await self._session.controller.list_jobs()
|
|
111
|
+
run_jobs = [j for j in jobs if j.run_id == run_id]
|
|
112
|
+
for j in run_jobs:
|
|
113
|
+
_check_job_mutation_permission(j, user_id, elevate, "cancel run")
|
|
114
|
+
return await self._session.controller.cancel_run(run_id)
|
|
115
|
+
|
|
116
|
+
async def cancel_job(
|
|
117
|
+
self, job_id: str, user_id: str = "default", elevate: bool = False
|
|
118
|
+
) -> JobSpec:
|
|
119
|
+
"""Cancel an individual job."""
|
|
120
|
+
job = await self._session.controller.get_job(job_id)
|
|
121
|
+
_check_job_mutation_permission(job, user_id, elevate, "cancel")
|
|
122
|
+
return await self._session.controller.cancel_job(job_id)
|
|
123
|
+
|
|
124
|
+
async def hold_job(
|
|
125
|
+
self, job_id: str, user_id: str = "default", elevate: bool = False
|
|
126
|
+
) -> JobSpec:
|
|
127
|
+
"""Place an administrative hold on a job."""
|
|
128
|
+
job = await self._session.controller.get_job(job_id)
|
|
129
|
+
_check_job_mutation_permission(job, user_id, elevate, "hold")
|
|
130
|
+
return await self._session.controller.hold_job(job_id)
|
|
131
|
+
|
|
132
|
+
async def release_job(
|
|
133
|
+
self, job_id: str, user_id: str = "default", elevate: bool = False
|
|
134
|
+
) -> JobSpec:
|
|
135
|
+
"""Release an administrative hold on a job."""
|
|
136
|
+
job = await self._session.controller.get_job(job_id)
|
|
137
|
+
_check_job_mutation_permission(job, user_id, elevate, "release")
|
|
138
|
+
return await self._session.controller.release_job(job_id)
|
|
139
|
+
|
|
140
|
+
async def get_logs(self, job_id: str) -> list[LogChunk]:
|
|
141
|
+
"""Fetch captured logs from in-memory stream adapter."""
|
|
142
|
+
return [
|
|
143
|
+
c async for c in self._session.log_stream.stream_logs(job_id, follow=False)
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
async def stream_logs(
|
|
147
|
+
self, job_id: str, follow: bool = False, tail: int | None = None
|
|
148
|
+
) -> AsyncIterator[LogChunk]:
|
|
149
|
+
"""Stream logs for a job with optional real-time tail follow."""
|
|
150
|
+
async for chunk in self._session.log_stream.stream_logs(
|
|
151
|
+
job_id, follow=follow, tail=tail
|
|
152
|
+
):
|
|
153
|
+
yield chunk
|
|
154
|
+
|
|
155
|
+
async def get_cluster_stats(self) -> ClusterStatsReport:
|
|
156
|
+
"""Retrieve high-level cluster state and backlog statistics."""
|
|
157
|
+
from hexaqueue_core.domain.lifecycle import JobState
|
|
158
|
+
|
|
159
|
+
all_jobs = await self._session.controller.list_jobs()
|
|
160
|
+
running_cnt = sum(1 for j in all_jobs if j.state == JobState.RUNNING)
|
|
161
|
+
pending_cnt = sum(1 for j in all_jobs if j.state == JobState.PENDING)
|
|
162
|
+
blocked_cnt = sum(1 for j in all_jobs if j.state == JobState.BLOCKED)
|
|
163
|
+
completed_cnt = sum(
|
|
164
|
+
1
|
|
165
|
+
for j in all_jobs
|
|
166
|
+
if j.state == JobState.DONE and str(j.outcome) == "COMPLETED"
|
|
167
|
+
)
|
|
168
|
+
failed_cnt = sum(
|
|
169
|
+
1
|
|
170
|
+
for j in all_jobs
|
|
171
|
+
if j.state == JobState.DONE and str(j.outcome) != "COMPLETED"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
total_runs = len(getattr(self._session.controller, "_runs", {}))
|
|
175
|
+
active_workers = 1 if getattr(self._session.worker, "_running", False) else 0
|
|
176
|
+
|
|
177
|
+
return ClusterStatsReport(
|
|
178
|
+
total_runs=total_runs,
|
|
179
|
+
total_jobs=len(all_jobs),
|
|
180
|
+
running_jobs=running_cnt,
|
|
181
|
+
pending_jobs=pending_cnt,
|
|
182
|
+
blocked_jobs=blocked_cnt,
|
|
183
|
+
completed_jobs=completed_cnt,
|
|
184
|
+
failed_jobs=failed_cnt,
|
|
185
|
+
active_workers=active_workers,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
async def get_nodes(self) -> list[NodeTelemetryPulse]:
|
|
189
|
+
"""Retrieve telemetry pulses for registered compute worker nodes."""
|
|
190
|
+
worker = self._session.worker
|
|
191
|
+
active_jobs = len(getattr(worker, "_active_jobs", set()))
|
|
192
|
+
worker_id = getattr(worker._config, "worker_id", "local-worker")
|
|
193
|
+
pulse = self._session.telemetry.collect_pulse(
|
|
194
|
+
worker_id=worker_id, active_jobs=active_jobs
|
|
195
|
+
)
|
|
196
|
+
return [pulse]
|
|
197
|
+
|
|
198
|
+
async def create_pty_session(
|
|
199
|
+
self, request: PtySessionRequest, job_owner: str = "default"
|
|
200
|
+
) -> PtySessionInfo:
|
|
201
|
+
"""Create an interactive terminal PTY session inside a running job."""
|
|
202
|
+
return await self._session.pty.create_session(request, job_owner=job_owner)
|
|
203
|
+
|
|
204
|
+
async def explain_job(
|
|
205
|
+
self,
|
|
206
|
+
job_id: str,
|
|
207
|
+
requesting_user: str = "default",
|
|
208
|
+
is_admin: bool = False,
|
|
209
|
+
) -> SchedulingDecisionReport:
|
|
210
|
+
"""Generate explainability report for a job in the local session."""
|
|
211
|
+
from datetime import UTC, datetime
|
|
212
|
+
|
|
213
|
+
from hexaqueue_core.domain.explainability import SchedulerExplainabilityEngine
|
|
214
|
+
from hexaqueue_core.domain.redaction import MultiTenantRedactionFilter
|
|
215
|
+
from hexaqueue_core.domain.scheduling import ResourceSlotPool
|
|
216
|
+
|
|
217
|
+
engine = SchedulerExplainabilityEngine()
|
|
218
|
+
redaction = MultiTenantRedactionFilter()
|
|
219
|
+
all_jobs = await self._session.controller.list_jobs()
|
|
220
|
+
pool = ResourceSlotPool(total_slots=4)
|
|
221
|
+
report = engine.explain_job(
|
|
222
|
+
job_id=job_id,
|
|
223
|
+
all_jobs=all_jobs,
|
|
224
|
+
pool=pool,
|
|
225
|
+
current_timestamp=datetime.now(UTC).timestamp(),
|
|
226
|
+
)
|
|
227
|
+
return redaction.redact_decision_report(
|
|
228
|
+
report, requesting_user=requesting_user, is_admin=is_admin
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
async def get_fairshare_tree(
|
|
232
|
+
self,
|
|
233
|
+
requesting_user: str = "default",
|
|
234
|
+
is_admin: bool = False,
|
|
235
|
+
) -> FairShareTreeReport:
|
|
236
|
+
"""Generate fair-share tree report for the local session."""
|
|
237
|
+
from datetime import UTC, datetime
|
|
238
|
+
|
|
239
|
+
from hexaqueue_core.domain.explainability import (
|
|
240
|
+
SchedulerExplainabilityEngine,
|
|
241
|
+
)
|
|
242
|
+
from hexaqueue_core.domain.redaction import MultiTenantRedactionFilter
|
|
243
|
+
|
|
244
|
+
engine = SchedulerExplainabilityEngine()
|
|
245
|
+
redaction = MultiTenantRedactionFilter()
|
|
246
|
+
report = engine.explain_fairshare(datetime.now(UTC).timestamp())
|
|
247
|
+
return redaction.redact_fairshare_tree(
|
|
248
|
+
report, requesting_user=requesting_user, is_admin=is_admin
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
async def register_collateral(
|
|
252
|
+
self,
|
|
253
|
+
name: str,
|
|
254
|
+
size_bytes: int,
|
|
255
|
+
checksum_sha256: str,
|
|
256
|
+
tier: CollateralTier = CollateralTier.TEMPORARY,
|
|
257
|
+
kind: CollateralKind = CollateralKind.BUNDLE,
|
|
258
|
+
target_path: str = "",
|
|
259
|
+
user_id: str = "default",
|
|
260
|
+
elevate: bool = False,
|
|
261
|
+
) -> CollateralBundle:
|
|
262
|
+
"""Register and stage a collateral bundle."""
|
|
263
|
+
checksum = (
|
|
264
|
+
checksum_sha256 if len(checksum_sha256) == 64 else checksum_sha256.zfill(64)
|
|
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}"
|
|
282
|
+
return CollateralBundle(
|
|
283
|
+
id=f"col-{uuid4().hex[:8]}",
|
|
284
|
+
job_id="global",
|
|
285
|
+
filename=name,
|
|
286
|
+
size_bytes=size_bytes,
|
|
287
|
+
sha256_checksum=checksum,
|
|
288
|
+
tier=tier,
|
|
289
|
+
kind=kind,
|
|
290
|
+
state=CollateralState.REGISTERED,
|
|
291
|
+
staging_uri=staging_uri,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
async def create_bastion_session(
|
|
295
|
+
self,
|
|
296
|
+
node_id: str,
|
|
297
|
+
session_id: str = "",
|
|
298
|
+
user_id: str = "default",
|
|
299
|
+
elevate: bool = False,
|
|
300
|
+
) -> PtySessionInfo:
|
|
301
|
+
"""Create administrative bastion session (requires elevation)."""
|
|
302
|
+
if not elevate:
|
|
303
|
+
msg = (
|
|
304
|
+
f"Permission denied: Bastion shell access on node '{node_id}' "
|
|
305
|
+
"requires explicit administrative elevation (--admin)."
|
|
306
|
+
)
|
|
307
|
+
raise PermissionDeniedError(msg)
|
|
308
|
+
req = PtySessionRequest(
|
|
309
|
+
session_id=session_id or f"bastion-{uuid4().hex[:8]}",
|
|
310
|
+
job_id=f"bastion-{node_id}",
|
|
311
|
+
user_id=user_id,
|
|
312
|
+
roles=["admin", "operator"],
|
|
313
|
+
command=["/bin/sh"],
|
|
314
|
+
)
|
|
315
|
+
return await self._session.pty.create_session(req, job_owner=user_id)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
__all__ = [
|
|
319
|
+
"LocalClientAdapter",
|
|
320
|
+
]
|