portacode 1.4.11.dev10__py3-none-any.whl → 1.4.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.
- portacode/_version.py +2 -2
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +84 -15
- portacode/connection/handlers/__init__.py +6 -0
- portacode/connection/handlers/proxmox_infra.py +408 -22
- portacode/connection/terminal.py +6 -0
- {portacode-1.4.11.dev10.dist-info → portacode-1.4.12.dist-info}/METADATA +1 -1
- {portacode-1.4.11.dev10.dist-info → portacode-1.4.12.dist-info}/RECORD +11 -11
- {portacode-1.4.11.dev10.dist-info → portacode-1.4.12.dist-info}/WHEEL +1 -1
- {portacode-1.4.11.dev10.dist-info → portacode-1.4.12.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.11.dev10.dist-info → portacode-1.4.12.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.11.dev10.dist-info → portacode-1.4.12.dist-info}/top_level.txt +0 -0
portacode/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.4.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 4,
|
|
31
|
+
__version__ = version = '1.4.12'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 12)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -367,6 +367,45 @@ Creates a Portacode-managed LXC container, starts it, and bootstraps the Portaco
|
|
|
367
367
|
* On success, the device will emit a [`proxmox_container_created`](#proxmox_container_created-event) event that includes the Portacode auth key produced inside the container.
|
|
368
368
|
* On failure, the device will emit an [`error`](#error) event.
|
|
369
369
|
|
|
370
|
+
### `start_proxmox_container`
|
|
371
|
+
|
|
372
|
+
Starts a previously provisioned, Portacode-managed LXC container. Handled by [`StartProxmoxContainerHandler`](./proxmox_infra.py).
|
|
373
|
+
|
|
374
|
+
**Payload Fields:**
|
|
375
|
+
|
|
376
|
+
* `ctid` (string, required): Identifier of the container to start.
|
|
377
|
+
|
|
378
|
+
**Responses:**
|
|
379
|
+
|
|
380
|
+
* Emits a [`proxmox_container_action`](#proxmox_container_action-event) event with `action="start"` and the refreshed infra snapshot.
|
|
381
|
+
* Emits an [`error`](#error) event when the request cannot be fulfilled (e.g., missing infra config, CT not tagged as managed, or API failure).
|
|
382
|
+
|
|
383
|
+
### `stop_proxmox_container`
|
|
384
|
+
|
|
385
|
+
Stops a running Portacode-managed container. Handled by [`StopProxmoxContainerHandler`](./proxmox_infra.py).
|
|
386
|
+
|
|
387
|
+
**Payload Fields:**
|
|
388
|
+
|
|
389
|
+
* `ctid` (string, required): Identifier of the container to stop.
|
|
390
|
+
|
|
391
|
+
**Responses:**
|
|
392
|
+
|
|
393
|
+
* Emits a [`proxmox_container_action`](#proxmox_container_action-event) event with `action="stop"` and the refreshed infra snapshot.
|
|
394
|
+
* Emits an [`error`](#error) event on failure.
|
|
395
|
+
|
|
396
|
+
### `remove_proxmox_container`
|
|
397
|
+
|
|
398
|
+
Deletes a managed container from Proxmox (stopping it first if necessary) and removes the stored metadata file. Handled by [`RemoveProxmoxContainerHandler`](./proxmox_infra.py).
|
|
399
|
+
|
|
400
|
+
**Payload Fields:**
|
|
401
|
+
|
|
402
|
+
* `ctid` (string, required): Identifier of the container to delete.
|
|
403
|
+
|
|
404
|
+
**Responses:**
|
|
405
|
+
|
|
406
|
+
* Emits a [`proxmox_container_action`](#proxmox_container_action-event) event with `action="remove"` and the refreshed infra snapshot after deletion.
|
|
407
|
+
* Emits an [`error`](#error) event on failure.
|
|
408
|
+
|
|
370
409
|
### `proxmox_container_created`
|
|
371
410
|
|
|
372
411
|
Emitted after a successful `create_proxmox_container` action. Contains the new container ID, the Portacode public key produced inside the container, and the bootstrap logs.
|
|
@@ -1067,21 +1106,37 @@ Provides system information in response to a `system_info` action. Handled by [`
|
|
|
1067
1106
|
* `proxmox` (object): Detection hints for Proxmox VE nodes:
|
|
1068
1107
|
* `is_proxmox_node` (boolean): True when Proxmox artifacts (e.g., `/etc/proxmox-release`) exist.
|
|
1069
1108
|
* `version` (string|null): Raw contents of `/etc/proxmox-release` when readable.
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1109
|
+
* `infra` (object): Portacode infrastructure configuration snapshot:
|
|
1110
|
+
* `configured` (boolean): True when `setup_proxmox_infra` stored an API token.
|
|
1111
|
+
* `host` (string|null): Hostname used for the API client (usually `localhost`).
|
|
1112
|
+
* `node` (string|null): Proxmox node name that was targeted.
|
|
1113
|
+
* `user` (string|null): API token owner (e.g., `root@pam`).
|
|
1114
|
+
* `token_name` (string|null): API token identifier.
|
|
1115
|
+
* `default_storage` (string|null): Storage pool chosen for future containers.
|
|
1116
|
+
* `templates` (array[string]): Cached list of available LXC templates.
|
|
1117
|
+
* `last_verified` (string|null): ISO timestamp when the token was last validated.
|
|
1118
|
+
* `network` (object):
|
|
1119
|
+
* `applied` (boolean): True when the bridge/NAT services were successfully configured.
|
|
1120
|
+
* `message` (string|null): Informational text about the network setup attempt.
|
|
1121
|
+
* `bridge` (string): The bridge interface configured (typically `vmbr1`).
|
|
1122
|
+
* `health` (string|null): `"healthy"` when the connectivity verification succeeded.
|
|
1123
|
+
* `node_status` (object|null): Status response returned by the Proxmox API when validating the token.
|
|
1124
|
+
* `managed_containers` (object): Cached summary of the Portacode-managed containers:
|
|
1125
|
+
* `updated_at` (string): ISO timestamp when this snapshot was last refreshed.
|
|
1126
|
+
* `count` (integer): Number of managed containers.
|
|
1127
|
+
* `total_ram_mib` (integer): RAM footprint summed across all containers.
|
|
1128
|
+
* `total_disk_gib` (integer): Disk footprint summed across all containers.
|
|
1129
|
+
* `total_cpu_share` (number): CPU shares requested across all containers.
|
|
1130
|
+
* `containers` (array[object]): Container summaries with the following fields:
|
|
1131
|
+
* `vmid` (string|null): Numeric CT ID.
|
|
1132
|
+
* `hostname` (string|null): Hostname configured in the CT.
|
|
1133
|
+
* `template` (string|null): Template identifier used.
|
|
1134
|
+
* `storage` (string|null): Storage pool backing the rootfs.
|
|
1135
|
+
* `disk_gib` (integer): Rootfs size in GiB.
|
|
1136
|
+
* `ram_mib` (integer): Memory size in MiB.
|
|
1137
|
+
* `cpu_share` (number): vCPU-equivalent share requested at creation.
|
|
1138
|
+
* `status` (string): Lowercase lifecycle status (e.g., `running`, `stopped`, `deleted`).
|
|
1139
|
+
* `created_at` (string|null): ISO timestamp recorded when the CT was provisioned.
|
|
1085
1140
|
* `portacode_version` (string): Installed CLI version returned by `portacode.__version__`.
|
|
1086
1141
|
|
|
1087
1142
|
### `proxmox_infra_configured`
|
|
@@ -1133,6 +1188,20 @@ Sent continuously while `create_proxmox_container` runs so dashboards can show a
|
|
|
1133
1188
|
* `details` (object, optional): Contains `attempt` (when retries are used) and `error_summary` on failure.
|
|
1134
1189
|
* `request_id` (string, optional): Mirrors the `create_proxmox_container` request when provided.
|
|
1135
1190
|
|
|
1191
|
+
### `proxmox_container_action`
|
|
1192
|
+
|
|
1193
|
+
Emitted after `start_proxmox_container`, `stop_proxmox_container`, or `remove_proxmox_container` commands complete. Each event includes the refreshed infra snapshot so dashboards can immediately display the latest managed container totals even though the `proxmox.infra.managed_containers` cache updates only every ~30 seconds.
|
|
1194
|
+
|
|
1195
|
+
**Event Fields:**
|
|
1196
|
+
|
|
1197
|
+
* `action` (string): The action that ran (`start`, `stop`, or `remove`).
|
|
1198
|
+
* `success` (boolean): True when the requested action succeeded.
|
|
1199
|
+
* `ctid` (string): Target CT ID.
|
|
1200
|
+
* `message` (string): Human-friendly summary (e.g., `Stopped container 103`).
|
|
1201
|
+
* `status` (string): The container’s new status (e.g., `running`, `stopped`, `deleted`).
|
|
1202
|
+
* `details` (object, optional): Exit status information (e.g., `exitstatus`, `stop_exitstatus`, `delete_exitstatus`).
|
|
1203
|
+
* `infra` (object): Same snapshot described under [`system_info`](#system_info-event) `proxmox.infra`, including the updated `managed_containers` summary.
|
|
1204
|
+
|
|
1136
1205
|
### <a name="clock_sync_response"></a>`clock_sync_response`
|
|
1137
1206
|
|
|
1138
1207
|
Reply sent by the gateway immediately after receiving a `clock_sync_request`. Devices use this event plus the measured round-trip time to keep their local `ntp_clock` offset accurate.
|
|
@@ -46,6 +46,9 @@ from .proxmox_infra import (
|
|
|
46
46
|
CreateProxmoxContainerHandler,
|
|
47
47
|
RevertProxmoxInfraHandler,
|
|
48
48
|
StartPortacodeServiceHandler,
|
|
49
|
+
StartProxmoxContainerHandler,
|
|
50
|
+
StopProxmoxContainerHandler,
|
|
51
|
+
RemoveProxmoxContainerHandler,
|
|
49
52
|
)
|
|
50
53
|
|
|
51
54
|
__all__ = [
|
|
@@ -86,6 +89,9 @@ __all__ = [
|
|
|
86
89
|
"ProjectStateGitRevertHandler",
|
|
87
90
|
"ProjectStateGitCommitHandler",
|
|
88
91
|
"StartPortacodeServiceHandler",
|
|
92
|
+
"StartProxmoxContainerHandler",
|
|
93
|
+
"StopProxmoxContainerHandler",
|
|
94
|
+
"RemoveProxmoxContainerHandler",
|
|
89
95
|
"UpdatePortacodeHandler",
|
|
90
96
|
"RevertProxmoxInfraHandler",
|
|
91
97
|
]
|
|
@@ -12,6 +12,7 @@ import stat
|
|
|
12
12
|
import subprocess
|
|
13
13
|
import sys
|
|
14
14
|
import time
|
|
15
|
+
import threading
|
|
15
16
|
from datetime import datetime
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
|
|
@@ -40,6 +41,9 @@ DNS_SERVER = "1.1.1.1"
|
|
|
40
41
|
IFACES_PATH = Path("/etc/network/interfaces")
|
|
41
42
|
SYSCTL_PATH = Path("/etc/sysctl.d/99-portacode-forward.conf")
|
|
42
43
|
UNIT_DIR = Path("/etc/systemd/system")
|
|
44
|
+
_MANAGED_CONTAINERS_CACHE_TTL_S = 30.0
|
|
45
|
+
_MANAGED_CONTAINERS_CACHE: Dict[str, Any] = {"timestamp": 0.0, "summary": None}
|
|
46
|
+
_MANAGED_CONTAINERS_CACHE_LOCK = threading.Lock()
|
|
43
47
|
|
|
44
48
|
ProgressCallback = Callable[[int, int, Dict[str, Any], str, Optional[Dict[str, Any]]], None]
|
|
45
49
|
|
|
@@ -55,6 +59,7 @@ def _emit_progress_event(
|
|
|
55
59
|
message: str,
|
|
56
60
|
phase: str,
|
|
57
61
|
request_id: Optional[str],
|
|
62
|
+
client_sessions: Optional[List[str]] = None,
|
|
58
63
|
details: Optional[Dict[str, Any]] = None,
|
|
59
64
|
) -> None:
|
|
60
65
|
loop = handler.context.get("event_loop")
|
|
@@ -80,6 +85,8 @@ def _emit_progress_event(
|
|
|
80
85
|
payload["request_id"] = request_id
|
|
81
86
|
if details:
|
|
82
87
|
payload["details"] = details
|
|
88
|
+
if client_sessions:
|
|
89
|
+
payload["client_sessions"] = client_sessions
|
|
83
90
|
|
|
84
91
|
future = asyncio.run_coroutine_threadsafe(handler.send_response(payload), loop)
|
|
85
92
|
future.add_done_callback(
|
|
@@ -290,6 +297,117 @@ def _ensure_containers_dir() -> None:
|
|
|
290
297
|
CONTAINERS_DIR.mkdir(parents=True, exist_ok=True)
|
|
291
298
|
|
|
292
299
|
|
|
300
|
+
def _invalidate_managed_containers_cache() -> None:
|
|
301
|
+
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
302
|
+
_MANAGED_CONTAINERS_CACHE["timestamp"] = 0.0
|
|
303
|
+
_MANAGED_CONTAINERS_CACHE["summary"] = None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _load_managed_container_records() -> List[Dict[str, Any]]:
|
|
307
|
+
_ensure_containers_dir()
|
|
308
|
+
records: List[Dict[str, Any]] = []
|
|
309
|
+
for path in sorted(CONTAINERS_DIR.glob("ct-*.json")):
|
|
310
|
+
try:
|
|
311
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
312
|
+
except Exception as exc: # pragma: no cover - best effort logging
|
|
313
|
+
logger.debug("Unable to read container record %s: %s", path, exc)
|
|
314
|
+
continue
|
|
315
|
+
records.append(payload)
|
|
316
|
+
return records
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _build_managed_containers_summary(records: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
320
|
+
total_ram = 0
|
|
321
|
+
total_disk = 0
|
|
322
|
+
total_cpu_share = 0.0
|
|
323
|
+
containers: List[Dict[str, Any]] = []
|
|
324
|
+
|
|
325
|
+
def _as_int(value: Any) -> int:
|
|
326
|
+
try:
|
|
327
|
+
return int(value)
|
|
328
|
+
except (TypeError, ValueError):
|
|
329
|
+
return 0
|
|
330
|
+
|
|
331
|
+
def _as_float(value: Any) -> float:
|
|
332
|
+
try:
|
|
333
|
+
return float(value)
|
|
334
|
+
except (TypeError, ValueError):
|
|
335
|
+
return 0.0
|
|
336
|
+
|
|
337
|
+
for record in sorted(records, key=lambda entry: _as_int(entry.get("vmid"))):
|
|
338
|
+
ram_mib = _as_int(record.get("ram_mib"))
|
|
339
|
+
disk_gib = _as_int(record.get("disk_gib"))
|
|
340
|
+
cpu_share = _as_float(record.get("cpus"))
|
|
341
|
+
total_ram += ram_mib
|
|
342
|
+
total_disk += disk_gib
|
|
343
|
+
total_cpu_share += cpu_share
|
|
344
|
+
status = (record.get("status") or "unknown").lower()
|
|
345
|
+
containers.append(
|
|
346
|
+
{
|
|
347
|
+
"vmid": str(_as_int(record.get("vmid"))) if record.get("vmid") is not None else None,
|
|
348
|
+
"hostname": record.get("hostname"),
|
|
349
|
+
"template": record.get("template"),
|
|
350
|
+
"storage": record.get("storage"),
|
|
351
|
+
"disk_gib": disk_gib,
|
|
352
|
+
"ram_mib": ram_mib,
|
|
353
|
+
"cpu_share": cpu_share,
|
|
354
|
+
"created_at": record.get("created_at"),
|
|
355
|
+
"status": status,
|
|
356
|
+
}
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
"updated_at": datetime.utcnow().isoformat() + "Z",
|
|
361
|
+
"count": len(containers),
|
|
362
|
+
"total_ram_mib": total_ram,
|
|
363
|
+
"total_disk_gib": total_disk,
|
|
364
|
+
"total_cpu_share": round(total_cpu_share, 2),
|
|
365
|
+
"containers": containers,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _get_managed_containers_summary(force: bool = False) -> Dict[str, Any]:
|
|
370
|
+
def _refresh_container_statuses(records: List[Dict[str, Any]], config: Dict[str, Any] | None) -> None:
|
|
371
|
+
if not records or not config:
|
|
372
|
+
return
|
|
373
|
+
try:
|
|
374
|
+
proxmox = _connect_proxmox(config)
|
|
375
|
+
node = _get_node_from_config(config)
|
|
376
|
+
statuses = {
|
|
377
|
+
str(ct.get("vmid")): (ct.get("status") or "unknown").lower()
|
|
378
|
+
for ct in proxmox.nodes(node).lxc.get()
|
|
379
|
+
}
|
|
380
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
381
|
+
logger.debug("Failed to refresh container statuses: %s", exc)
|
|
382
|
+
return
|
|
383
|
+
for record in records:
|
|
384
|
+
vmid = record.get("vmid")
|
|
385
|
+
if vmid is None:
|
|
386
|
+
continue
|
|
387
|
+
try:
|
|
388
|
+
vmid_key = str(int(vmid))
|
|
389
|
+
except (ValueError, TypeError):
|
|
390
|
+
continue
|
|
391
|
+
status = statuses.get(vmid_key)
|
|
392
|
+
if status:
|
|
393
|
+
record["status"] = status
|
|
394
|
+
|
|
395
|
+
now = time.monotonic()
|
|
396
|
+
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
397
|
+
cache_ts = _MANAGED_CONTAINERS_CACHE["timestamp"]
|
|
398
|
+
cached = _MANAGED_CONTAINERS_CACHE["summary"]
|
|
399
|
+
if not force and cached and now - cache_ts < _MANAGED_CONTAINERS_CACHE_TTL_S:
|
|
400
|
+
return cached
|
|
401
|
+
config = _load_config()
|
|
402
|
+
records = _load_managed_container_records()
|
|
403
|
+
_refresh_container_statuses(records, config)
|
|
404
|
+
summary = _build_managed_containers_summary(records)
|
|
405
|
+
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
406
|
+
_MANAGED_CONTAINERS_CACHE["timestamp"] = now
|
|
407
|
+
_MANAGED_CONTAINERS_CACHE["summary"] = summary
|
|
408
|
+
return summary
|
|
409
|
+
|
|
410
|
+
|
|
293
411
|
def _format_rootfs(storage: str, disk_gib: int, storage_type: str) -> str:
|
|
294
412
|
if storage_type in ("lvm", "lvmthin"):
|
|
295
413
|
return f"{storage}:{disk_gib}"
|
|
@@ -365,14 +483,14 @@ def _get_storage_type(storages: Iterable[Dict[str, Any]], storage_name: str) ->
|
|
|
365
483
|
return ""
|
|
366
484
|
|
|
367
485
|
|
|
368
|
-
def
|
|
486
|
+
def _validate_positive_number(value: Any, default: float) -> float:
|
|
369
487
|
try:
|
|
370
|
-
candidate =
|
|
488
|
+
candidate = float(value)
|
|
371
489
|
if candidate > 0:
|
|
372
490
|
return candidate
|
|
373
491
|
except Exception:
|
|
374
492
|
pass
|
|
375
|
-
return default
|
|
493
|
+
return float(default)
|
|
376
494
|
|
|
377
495
|
|
|
378
496
|
def _wait_for_task(proxmox: Any, node: str, upid: str) -> Tuple[Dict[str, Any], float]:
|
|
@@ -424,10 +542,24 @@ def _start_container(proxmox: Any, node: str, vmid: int) -> Tuple[Dict[str, Any]
|
|
|
424
542
|
return _wait_for_task(proxmox, node, upid)
|
|
425
543
|
|
|
426
544
|
|
|
545
|
+
def _stop_container(proxmox: Any, node: str, vmid: int) -> Tuple[Dict[str, Any], float]:
|
|
546
|
+
status = proxmox.nodes(node).lxc(vmid).status.current.get()
|
|
547
|
+
if status.get("status") != "running":
|
|
548
|
+
return status, 0.0
|
|
549
|
+
upid = proxmox.nodes(node).lxc(vmid).status.stop.post()
|
|
550
|
+
return _wait_for_task(proxmox, node, upid)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _delete_container(proxmox: Any, node: str, vmid: int) -> Tuple[Dict[str, Any], float]:
|
|
554
|
+
upid = proxmox.nodes(node).lxc(vmid).delete()
|
|
555
|
+
return _wait_for_task(proxmox, node, upid)
|
|
556
|
+
|
|
557
|
+
|
|
427
558
|
def _write_container_record(vmid: int, payload: Dict[str, Any]) -> None:
|
|
428
559
|
_ensure_containers_dir()
|
|
429
560
|
path = CONTAINERS_DIR / f"ct-{vmid}.json"
|
|
430
561
|
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
562
|
+
_invalidate_managed_containers_cache()
|
|
431
563
|
|
|
432
564
|
|
|
433
565
|
def _read_container_record(vmid: int) -> Dict[str, Any]:
|
|
@@ -437,6 +569,19 @@ def _read_container_record(vmid: int) -> Dict[str, Any]:
|
|
|
437
569
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
438
570
|
|
|
439
571
|
|
|
572
|
+
def _update_container_record(vmid: int, updates: Dict[str, Any]) -> None:
|
|
573
|
+
record = _read_container_record(vmid)
|
|
574
|
+
record.update(updates)
|
|
575
|
+
_write_container_record(vmid, record)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _remove_container_record(vmid: int) -> None:
|
|
579
|
+
path = CONTAINERS_DIR / f"ct-{vmid}.json"
|
|
580
|
+
if path.exists():
|
|
581
|
+
path.unlink()
|
|
582
|
+
_invalidate_managed_containers_cache()
|
|
583
|
+
|
|
584
|
+
|
|
440
585
|
def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) -> Dict[str, Any]:
|
|
441
586
|
templates = config.get("templates") or []
|
|
442
587
|
default_template = templates[0] if templates else ""
|
|
@@ -446,9 +591,9 @@ def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) ->
|
|
|
446
591
|
|
|
447
592
|
bridge = config.get("network", {}).get("bridge", DEFAULT_BRIDGE)
|
|
448
593
|
hostname = (message.get("hostname") or "").strip()
|
|
449
|
-
disk_gib =
|
|
450
|
-
ram_mib =
|
|
451
|
-
cpus =
|
|
594
|
+
disk_gib = int(max(round(_validate_positive_number(message.get("disk_gib") or message.get("disk"), 3)), 1))
|
|
595
|
+
ram_mib = int(max(round(_validate_positive_number(message.get("ram_mib") or message.get("ram"), 2048)), 1))
|
|
596
|
+
cpus = _validate_positive_number(message.get("cpus"), 0.2)
|
|
452
597
|
storage = message.get("storage") or config.get("default_storage") or ""
|
|
453
598
|
if not storage:
|
|
454
599
|
raise ValueError("Storage pool could not be determined.")
|
|
@@ -473,6 +618,38 @@ def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) ->
|
|
|
473
618
|
return payload
|
|
474
619
|
|
|
475
620
|
|
|
621
|
+
def _ensure_infra_configured() -> Dict[str, Any]:
|
|
622
|
+
config = _load_config()
|
|
623
|
+
if not config or not config.get("token_value"):
|
|
624
|
+
raise RuntimeError("Proxmox infrastructure is not configured.")
|
|
625
|
+
return config
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _get_node_from_config(config: Dict[str, Any]) -> str:
|
|
629
|
+
return config.get("node") or DEFAULT_NODE_NAME
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _parse_ctid(message: Dict[str, Any]) -> int:
|
|
633
|
+
for key in ("ctid", "vmid"):
|
|
634
|
+
value = message.get(key)
|
|
635
|
+
if value is not None:
|
|
636
|
+
try:
|
|
637
|
+
return int(str(value).strip())
|
|
638
|
+
except ValueError:
|
|
639
|
+
raise ValueError(f"{key} must be an integer") from None
|
|
640
|
+
raise ValueError("ctid is required")
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _ensure_container_managed(
|
|
644
|
+
proxmox: Any, node: str, vmid: int
|
|
645
|
+
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
646
|
+
record = _read_container_record(vmid)
|
|
647
|
+
ct_cfg = proxmox.nodes(node).lxc(str(vmid)).config.get()
|
|
648
|
+
if not ct_cfg or MANAGED_MARKER not in (ct_cfg.get("description") or ""):
|
|
649
|
+
raise RuntimeError(f"Container {vmid} is not managed by Portacode.")
|
|
650
|
+
return record, ct_cfg
|
|
651
|
+
|
|
652
|
+
|
|
476
653
|
def _connect_proxmox(config: Dict[str, Any]) -> Any:
|
|
477
654
|
ProxmoxAPI = _ensure_proxmoxer()
|
|
478
655
|
return ProxmoxAPI(
|
|
@@ -528,15 +705,22 @@ def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -
|
|
|
528
705
|
|
|
529
706
|
last_pub = last_priv = None
|
|
530
707
|
stable = 0
|
|
708
|
+
history: List[Dict[str, Any]] = []
|
|
709
|
+
|
|
710
|
+
process_exited = False
|
|
711
|
+
exit_out = exit_err = ""
|
|
531
712
|
while time.time() - start < timeout_s:
|
|
532
713
|
if proc.poll() is not None:
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
714
|
+
process_exited = True
|
|
715
|
+
exit_out, exit_err = proc.communicate(timeout=1)
|
|
716
|
+
history.append(
|
|
717
|
+
{
|
|
718
|
+
"timestamp_s": round(time.time() - start, 2),
|
|
719
|
+
"status": "process_exited",
|
|
720
|
+
"returncode": proc.returncode,
|
|
721
|
+
}
|
|
722
|
+
)
|
|
723
|
+
break
|
|
540
724
|
pub_size = file_size(pub_path)
|
|
541
725
|
priv_size = file_size(priv_path)
|
|
542
726
|
if pub_size and priv_size:
|
|
@@ -546,21 +730,60 @@ def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -
|
|
|
546
730
|
stable = 0
|
|
547
731
|
last_pub, last_priv = pub_size, priv_size
|
|
548
732
|
if stable >= 1:
|
|
733
|
+
history.append(
|
|
734
|
+
{
|
|
735
|
+
"timestamp_s": round(time.time() - start, 2),
|
|
736
|
+
"pub_size": pub_size,
|
|
737
|
+
"priv_size": priv_size,
|
|
738
|
+
"stable": stable,
|
|
739
|
+
}
|
|
740
|
+
)
|
|
549
741
|
break
|
|
742
|
+
history.append(
|
|
743
|
+
{
|
|
744
|
+
"timestamp_s": round(time.time() - start, 2),
|
|
745
|
+
"pub_size": pub_size,
|
|
746
|
+
"priv_size": priv_size,
|
|
747
|
+
"stable": stable,
|
|
748
|
+
}
|
|
749
|
+
)
|
|
550
750
|
time.sleep(1)
|
|
551
751
|
|
|
552
|
-
|
|
752
|
+
final_pub = file_size(pub_path)
|
|
753
|
+
final_priv = file_size(priv_path)
|
|
754
|
+
if final_pub and final_priv:
|
|
755
|
+
key_res = _run_pct(vmid, f"su - {user} -c 'cat {pub_path}'")
|
|
756
|
+
if not process_exited:
|
|
757
|
+
proc.terminate()
|
|
758
|
+
try:
|
|
759
|
+
proc.wait(timeout=3)
|
|
760
|
+
except subprocess.TimeoutExpired:
|
|
761
|
+
proc.kill()
|
|
762
|
+
return {
|
|
763
|
+
"ok": True,
|
|
764
|
+
"public_key": key_res["stdout"].strip(),
|
|
765
|
+
"history": history,
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if not process_exited:
|
|
553
769
|
proc.terminate()
|
|
554
770
|
try:
|
|
555
771
|
proc.wait(timeout=3)
|
|
556
772
|
except subprocess.TimeoutExpired:
|
|
557
773
|
proc.kill()
|
|
558
|
-
|
|
774
|
+
exit_out, exit_err = proc.communicate(timeout=1)
|
|
775
|
+
history.append(
|
|
776
|
+
{
|
|
777
|
+
"timestamp_s": round(time.time() - start, 2),
|
|
778
|
+
"status": "timeout_waiting_for_keys",
|
|
779
|
+
}
|
|
780
|
+
)
|
|
559
781
|
return {
|
|
560
782
|
"ok": False,
|
|
561
783
|
"error": "timed out waiting for portacode key files",
|
|
562
|
-
"stdout": (
|
|
563
|
-
"stderr": (
|
|
784
|
+
"stdout": (exit_out or "").strip(),
|
|
785
|
+
"stderr": (exit_err or "").strip(),
|
|
786
|
+
"history": history,
|
|
564
787
|
}
|
|
565
788
|
|
|
566
789
|
proc.terminate()
|
|
@@ -573,6 +796,7 @@ def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -
|
|
|
573
796
|
return {
|
|
574
797
|
"ok": True,
|
|
575
798
|
"public_key": key_res["stdout"].strip(),
|
|
799
|
+
"history": history,
|
|
576
800
|
}
|
|
577
801
|
|
|
578
802
|
|
|
@@ -670,6 +894,19 @@ def _bootstrap_portacode(
|
|
|
670
894
|
total_steps=total_steps,
|
|
671
895
|
)
|
|
672
896
|
if not ok:
|
|
897
|
+
details = results[-1] if results else {}
|
|
898
|
+
summary = details.get("error_summary") or details.get("stderr") or details.get("stdout") or details.get("name")
|
|
899
|
+
history = details.get("history")
|
|
900
|
+
history_snippet = ""
|
|
901
|
+
if isinstance(history, list) and history:
|
|
902
|
+
history_snippet = f" history={history[-3:]}"
|
|
903
|
+
if summary:
|
|
904
|
+
logger.warning(
|
|
905
|
+
"Portacode bootstrap failure summary=%s%s",
|
|
906
|
+
summary,
|
|
907
|
+
f" history_len={len(history)}" if history else "",
|
|
908
|
+
)
|
|
909
|
+
raise RuntimeError(f"Portacode bootstrap steps failed: {summary}{history_snippet}")
|
|
673
910
|
raise RuntimeError("Portacode bootstrap steps failed.")
|
|
674
911
|
key_step = next((entry for entry in results if entry.get("name") == "portacode_connect"), None)
|
|
675
912
|
public_key = key_step.get("public_key") if key_step else None
|
|
@@ -748,6 +985,7 @@ def configure_infrastructure(token_identifier: str, token_value: str, verify_ssl
|
|
|
748
985
|
_save_config(config)
|
|
749
986
|
snapshot = build_snapshot(config)
|
|
750
987
|
snapshot["node_status"] = status
|
|
988
|
+
snapshot["managed_containers"] = _get_managed_containers_summary(force=True)
|
|
751
989
|
return snapshot
|
|
752
990
|
|
|
753
991
|
|
|
@@ -756,6 +994,7 @@ def get_infra_snapshot() -> Dict[str, Any]:
|
|
|
756
994
|
snapshot = build_snapshot(config)
|
|
757
995
|
if config.get("node_status"):
|
|
758
996
|
snapshot["node_status"] = config["node_status"]
|
|
997
|
+
snapshot["managed_containers"] = _get_managed_containers_summary()
|
|
759
998
|
return snapshot
|
|
760
999
|
|
|
761
1000
|
|
|
@@ -768,6 +1007,7 @@ def revert_infrastructure() -> Dict[str, Any]:
|
|
|
768
1007
|
snapshot["network"]["applied"] = False
|
|
769
1008
|
snapshot["network"]["message"] = "Reverted to previous network state"
|
|
770
1009
|
snapshot["network"]["bridge"] = DEFAULT_BRIDGE
|
|
1010
|
+
snapshot["managed_containers"] = _get_managed_containers_summary(force=True)
|
|
771
1011
|
return snapshot
|
|
772
1012
|
|
|
773
1013
|
|
|
@@ -791,7 +1031,7 @@ def _instantiate_container(proxmox: Any, node: str, payload: Dict[str, Any]) ->
|
|
|
791
1031
|
rootfs=rootfs,
|
|
792
1032
|
memory=int(payload["ram_mib"]),
|
|
793
1033
|
swap=int(payload.get("swap_mb", 0)),
|
|
794
|
-
cores=int(payload.get("
|
|
1034
|
+
cores=max(int(payload.get("cores", 1)), 1),
|
|
795
1035
|
cpuunits=int(payload.get("cpuunits", 256)),
|
|
796
1036
|
net0=payload["net0"],
|
|
797
1037
|
unprivileged=int(payload.get("unprivileged", 1)),
|
|
@@ -815,6 +1055,8 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
815
1055
|
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
816
1056
|
logger.info("create_proxmox_container command received")
|
|
817
1057
|
request_id = message.get("request_id")
|
|
1058
|
+
source_client_session = message.get("source_client_session")
|
|
1059
|
+
client_sessions = [source_client_session] if source_client_session else None
|
|
818
1060
|
bootstrap_user, bootstrap_password, bootstrap_ssh_key = _get_provisioning_user_info(message)
|
|
819
1061
|
bootstrap_steps = _build_bootstrap_steps(bootstrap_user, bootstrap_password, bootstrap_ssh_key)
|
|
820
1062
|
total_steps = 3 + len(bootstrap_steps) + 2
|
|
@@ -838,6 +1080,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
838
1080
|
message=start_message,
|
|
839
1081
|
phase="lifecycle",
|
|
840
1082
|
request_id=request_id,
|
|
1083
|
+
client_sessions=client_sessions,
|
|
841
1084
|
)
|
|
842
1085
|
try:
|
|
843
1086
|
result = action()
|
|
@@ -852,6 +1095,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
852
1095
|
message=f"{step_label} failed: {exc}",
|
|
853
1096
|
phase="lifecycle",
|
|
854
1097
|
request_id=request_id,
|
|
1098
|
+
client_sessions=client_sessions,
|
|
855
1099
|
details={"error": str(exc)},
|
|
856
1100
|
)
|
|
857
1101
|
raise
|
|
@@ -865,6 +1109,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
865
1109
|
message=success_message,
|
|
866
1110
|
phase="lifecycle",
|
|
867
1111
|
request_id=request_id,
|
|
1112
|
+
client_sessions=client_sessions,
|
|
868
1113
|
)
|
|
869
1114
|
current_step_index += 1
|
|
870
1115
|
return result
|
|
@@ -905,6 +1150,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
905
1150
|
vmid, _ = _instantiate_container(proxmox, node, payload)
|
|
906
1151
|
payload["vmid"] = vmid
|
|
907
1152
|
payload["created_at"] = datetime.utcnow().isoformat() + "Z"
|
|
1153
|
+
payload["status"] = "creating"
|
|
908
1154
|
_write_container_record(vmid, payload)
|
|
909
1155
|
return proxmox, node, vmid, payload
|
|
910
1156
|
|
|
@@ -926,6 +1172,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
926
1172
|
"Container startup completed.",
|
|
927
1173
|
_start_container_step,
|
|
928
1174
|
)
|
|
1175
|
+
_update_container_record(vmid, {"status": "running"})
|
|
929
1176
|
|
|
930
1177
|
def _bootstrap_progress_callback(
|
|
931
1178
|
step_index: int,
|
|
@@ -963,6 +1210,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
963
1210
|
message=message_text,
|
|
964
1211
|
phase="bootstrap",
|
|
965
1212
|
request_id=request_id,
|
|
1213
|
+
client_sessions=client_sessions,
|
|
966
1214
|
details=details or None,
|
|
967
1215
|
)
|
|
968
1216
|
|
|
@@ -978,7 +1226,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
978
1226
|
)
|
|
979
1227
|
current_step_index += len(bootstrap_steps)
|
|
980
1228
|
|
|
981
|
-
|
|
1229
|
+
response = {
|
|
982
1230
|
"event": "proxmox_container_created",
|
|
983
1231
|
"success": True,
|
|
984
1232
|
"message": f"Container {vmid} is ready and Portacode key captured.",
|
|
@@ -995,6 +1243,9 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
995
1243
|
},
|
|
996
1244
|
"setup_steps": steps,
|
|
997
1245
|
}
|
|
1246
|
+
if client_sessions:
|
|
1247
|
+
response["client_sessions"] = client_sessions
|
|
1248
|
+
return response
|
|
998
1249
|
|
|
999
1250
|
|
|
1000
1251
|
class StartPortacodeServiceHandler(SyncHandler):
|
|
@@ -1022,6 +1273,8 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1022
1273
|
start_index = int(message.get("step_index", 1))
|
|
1023
1274
|
total_steps = int(message.get("total_steps", start_index + 2))
|
|
1024
1275
|
request_id = message.get("request_id")
|
|
1276
|
+
source_client_session = message.get("source_client_session")
|
|
1277
|
+
client_sessions = [source_client_session] if source_client_session else None
|
|
1025
1278
|
|
|
1026
1279
|
auth_step_name = "setup_device_authentication"
|
|
1027
1280
|
auth_label = "Setting up device authentication"
|
|
@@ -1035,6 +1288,7 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1035
1288
|
message="Notifying the server of the new device…",
|
|
1036
1289
|
phase="service",
|
|
1037
1290
|
request_id=request_id,
|
|
1291
|
+
client_sessions=client_sessions,
|
|
1038
1292
|
)
|
|
1039
1293
|
_emit_progress_event(
|
|
1040
1294
|
self,
|
|
@@ -1046,6 +1300,7 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1046
1300
|
message="Authentication metadata recorded.",
|
|
1047
1301
|
phase="service",
|
|
1048
1302
|
request_id=request_id,
|
|
1303
|
+
client_sessions=client_sessions,
|
|
1049
1304
|
)
|
|
1050
1305
|
|
|
1051
1306
|
install_step = start_index + 1
|
|
@@ -1060,6 +1315,7 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1060
1315
|
message="Running sudo portacode service install…",
|
|
1061
1316
|
phase="service",
|
|
1062
1317
|
request_id=request_id,
|
|
1318
|
+
client_sessions=client_sessions,
|
|
1063
1319
|
)
|
|
1064
1320
|
|
|
1065
1321
|
cmd = f"su - {user} -c 'sudo -S portacode service install'"
|
|
@@ -1076,6 +1332,7 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1076
1332
|
message=f"{install_label} failed: {res.get('stderr') or res.get('stdout')}",
|
|
1077
1333
|
phase="service",
|
|
1078
1334
|
request_id=request_id,
|
|
1335
|
+
client_sessions=client_sessions,
|
|
1079
1336
|
details={
|
|
1080
1337
|
"stderr": res.get("stderr"),
|
|
1081
1338
|
"stdout": res.get("stdout"),
|
|
@@ -1093,14 +1350,133 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1093
1350
|
message="Portacode service install finished.",
|
|
1094
1351
|
phase="service",
|
|
1095
1352
|
request_id=request_id,
|
|
1353
|
+
client_sessions=client_sessions,
|
|
1096
1354
|
)
|
|
1097
1355
|
|
|
1098
|
-
|
|
1356
|
+
response = {
|
|
1099
1357
|
"event": "proxmox_service_started",
|
|
1100
1358
|
"success": True,
|
|
1101
1359
|
"message": "Portacode service install completed",
|
|
1102
1360
|
"ctid": str(vmid),
|
|
1103
1361
|
}
|
|
1362
|
+
if client_sessions:
|
|
1363
|
+
response["client_sessions"] = client_sessions
|
|
1364
|
+
return response
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
class StartProxmoxContainerHandler(SyncHandler):
|
|
1368
|
+
"""Start a managed container via the Proxmox API."""
|
|
1369
|
+
|
|
1370
|
+
@property
|
|
1371
|
+
def command_name(self) -> str:
|
|
1372
|
+
return "start_proxmox_container"
|
|
1373
|
+
|
|
1374
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1375
|
+
vmid = _parse_ctid(message)
|
|
1376
|
+
config = _ensure_infra_configured()
|
|
1377
|
+
proxmox = _connect_proxmox(config)
|
|
1378
|
+
node = _get_node_from_config(config)
|
|
1379
|
+
_ensure_container_managed(proxmox, node, vmid)
|
|
1380
|
+
source_client_session = message.get("source_client_session")
|
|
1381
|
+
client_sessions = [source_client_session] if source_client_session else None
|
|
1382
|
+
|
|
1383
|
+
status, elapsed = _start_container(proxmox, node, vmid)
|
|
1384
|
+
_update_container_record(vmid, {"status": "running"})
|
|
1385
|
+
|
|
1386
|
+
infra = get_infra_snapshot()
|
|
1387
|
+
response = {
|
|
1388
|
+
"event": "proxmox_container_action",
|
|
1389
|
+
"action": "start",
|
|
1390
|
+
"success": True,
|
|
1391
|
+
"ctid": str(vmid),
|
|
1392
|
+
"message": f"Started container {vmid} in {elapsed:.1f}s.",
|
|
1393
|
+
"details": {"exitstatus": status.get("exitstatus")},
|
|
1394
|
+
"status": status.get("status"),
|
|
1395
|
+
"infra": infra,
|
|
1396
|
+
}
|
|
1397
|
+
if client_sessions:
|
|
1398
|
+
response["client_sessions"] = client_sessions
|
|
1399
|
+
return response
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
class StopProxmoxContainerHandler(SyncHandler):
|
|
1403
|
+
"""Stop a managed container via the Proxmox API."""
|
|
1404
|
+
|
|
1405
|
+
@property
|
|
1406
|
+
def command_name(self) -> str:
|
|
1407
|
+
return "stop_proxmox_container"
|
|
1408
|
+
|
|
1409
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1410
|
+
vmid = _parse_ctid(message)
|
|
1411
|
+
config = _ensure_infra_configured()
|
|
1412
|
+
proxmox = _connect_proxmox(config)
|
|
1413
|
+
node = _get_node_from_config(config)
|
|
1414
|
+
_ensure_container_managed(proxmox, node, vmid)
|
|
1415
|
+
source_client_session = message.get("source_client_session")
|
|
1416
|
+
client_sessions = [source_client_session] if source_client_session else None
|
|
1417
|
+
|
|
1418
|
+
status, elapsed = _stop_container(proxmox, node, vmid)
|
|
1419
|
+
final_status = status.get("status") or "stopped"
|
|
1420
|
+
_update_container_record(vmid, {"status": final_status})
|
|
1421
|
+
|
|
1422
|
+
infra = get_infra_snapshot()
|
|
1423
|
+
message_text = (
|
|
1424
|
+
f"Container {vmid} is already stopped."
|
|
1425
|
+
if final_status != "running" and elapsed == 0.0
|
|
1426
|
+
else f"Stopped container {vmid} in {elapsed:.1f}s."
|
|
1427
|
+
)
|
|
1428
|
+
response = {
|
|
1429
|
+
"event": "proxmox_container_action",
|
|
1430
|
+
"action": "stop",
|
|
1431
|
+
"success": True,
|
|
1432
|
+
"ctid": str(vmid),
|
|
1433
|
+
"message": message_text,
|
|
1434
|
+
"details": {"exitstatus": status.get("exitstatus")},
|
|
1435
|
+
"status": final_status,
|
|
1436
|
+
"infra": infra,
|
|
1437
|
+
}
|
|
1438
|
+
if client_sessions:
|
|
1439
|
+
response["client_sessions"] = client_sessions
|
|
1440
|
+
return response
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
class RemoveProxmoxContainerHandler(SyncHandler):
|
|
1444
|
+
"""Delete a managed container via the Proxmox API."""
|
|
1445
|
+
|
|
1446
|
+
@property
|
|
1447
|
+
def command_name(self) -> str:
|
|
1448
|
+
return "remove_proxmox_container"
|
|
1449
|
+
|
|
1450
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1451
|
+
vmid = _parse_ctid(message)
|
|
1452
|
+
config = _ensure_infra_configured()
|
|
1453
|
+
proxmox = _connect_proxmox(config)
|
|
1454
|
+
node = _get_node_from_config(config)
|
|
1455
|
+
_ensure_container_managed(proxmox, node, vmid)
|
|
1456
|
+
source_client_session = message.get("source_client_session")
|
|
1457
|
+
client_sessions = [source_client_session] if source_client_session else None
|
|
1458
|
+
|
|
1459
|
+
stop_status, stop_elapsed = _stop_container(proxmox, node, vmid)
|
|
1460
|
+
delete_status, delete_elapsed = _delete_container(proxmox, node, vmid)
|
|
1461
|
+
_remove_container_record(vmid)
|
|
1462
|
+
|
|
1463
|
+
infra = get_infra_snapshot()
|
|
1464
|
+
response = {
|
|
1465
|
+
"event": "proxmox_container_action",
|
|
1466
|
+
"action": "remove",
|
|
1467
|
+
"success": True,
|
|
1468
|
+
"ctid": str(vmid),
|
|
1469
|
+
"message": f"Deleted container {vmid} in {delete_elapsed:.1f}s.",
|
|
1470
|
+
"details": {
|
|
1471
|
+
"stop_exitstatus": stop_status.get("exitstatus"),
|
|
1472
|
+
"delete_exitstatus": delete_status.get("exitstatus"),
|
|
1473
|
+
},
|
|
1474
|
+
"status": "deleted",
|
|
1475
|
+
"infra": infra,
|
|
1476
|
+
}
|
|
1477
|
+
if client_sessions:
|
|
1478
|
+
response["client_sessions"] = client_sessions
|
|
1479
|
+
return response
|
|
1104
1480
|
|
|
1105
1481
|
|
|
1106
1482
|
class ConfigureProxmoxInfraHandler(SyncHandler):
|
|
@@ -1114,13 +1490,18 @@ class ConfigureProxmoxInfraHandler(SyncHandler):
|
|
|
1114
1490
|
verify_ssl = bool(message.get("verify_ssl"))
|
|
1115
1491
|
if not token_identifier or not token_value:
|
|
1116
1492
|
raise ValueError("token_identifier and token_value are required")
|
|
1493
|
+
source_client_session = message.get("source_client_session")
|
|
1494
|
+
client_sessions = [source_client_session] if source_client_session else None
|
|
1117
1495
|
snapshot = configure_infrastructure(token_identifier, token_value, verify_ssl=verify_ssl)
|
|
1118
|
-
|
|
1496
|
+
response = {
|
|
1119
1497
|
"event": "proxmox_infra_configured",
|
|
1120
1498
|
"success": True,
|
|
1121
1499
|
"message": "Proxmox infrastructure configured",
|
|
1122
1500
|
"infra": snapshot,
|
|
1123
1501
|
}
|
|
1502
|
+
if client_sessions:
|
|
1503
|
+
response["client_sessions"] = client_sessions
|
|
1504
|
+
return response
|
|
1124
1505
|
|
|
1125
1506
|
|
|
1126
1507
|
class RevertProxmoxInfraHandler(SyncHandler):
|
|
@@ -1129,10 +1510,15 @@ class RevertProxmoxInfraHandler(SyncHandler):
|
|
|
1129
1510
|
return "revert_proxmox_infra"
|
|
1130
1511
|
|
|
1131
1512
|
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1513
|
+
source_client_session = message.get("source_client_session")
|
|
1514
|
+
client_sessions = [source_client_session] if source_client_session else None
|
|
1132
1515
|
snapshot = revert_infrastructure()
|
|
1133
|
-
|
|
1516
|
+
response = {
|
|
1134
1517
|
"event": "proxmox_infra_reverted",
|
|
1135
1518
|
"success": True,
|
|
1136
1519
|
"message": "Proxmox infrastructure configuration reverted",
|
|
1137
1520
|
"infra": snapshot,
|
|
1138
1521
|
}
|
|
1522
|
+
if client_sessions:
|
|
1523
|
+
response["client_sessions"] = client_sessions
|
|
1524
|
+
return response
|
portacode/connection/terminal.py
CHANGED
|
@@ -56,6 +56,9 @@ from .handlers import (
|
|
|
56
56
|
CreateProxmoxContainerHandler,
|
|
57
57
|
RevertProxmoxInfraHandler,
|
|
58
58
|
StartPortacodeServiceHandler,
|
|
59
|
+
StartProxmoxContainerHandler,
|
|
60
|
+
StopProxmoxContainerHandler,
|
|
61
|
+
RemoveProxmoxContainerHandler,
|
|
59
62
|
)
|
|
60
63
|
from .handlers.project_aware_file_handlers import (
|
|
61
64
|
ProjectAwareFileWriteHandler,
|
|
@@ -480,6 +483,9 @@ class TerminalManager:
|
|
|
480
483
|
self._command_registry.register(ConfigureProxmoxInfraHandler)
|
|
481
484
|
self._command_registry.register(CreateProxmoxContainerHandler)
|
|
482
485
|
self._command_registry.register(StartPortacodeServiceHandler)
|
|
486
|
+
self._command_registry.register(StartProxmoxContainerHandler)
|
|
487
|
+
self._command_registry.register(StopProxmoxContainerHandler)
|
|
488
|
+
self._command_registry.register(RemoveProxmoxContainerHandler)
|
|
483
489
|
self._command_registry.register(RevertProxmoxInfraHandler)
|
|
484
490
|
self._command_registry.register(UpdatePortacodeHandler)
|
|
485
491
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
portacode/README.md,sha256=4dKtpvR8LNgZPVz37GmkQCMWIr_u25Ao63iW56s7Ke4,775
|
|
2
2
|
portacode/__init__.py,sha256=oB3sV1wXr-um-RXio73UG8E5Xx6cF2ZVJveqjNmC-vQ,1086
|
|
3
3
|
portacode/__main__.py,sha256=jmHTGC1hzmo9iKJLv-SSYe9BSIbPPZ2IOpecI03PlTs,296
|
|
4
|
-
portacode/_version.py,sha256=
|
|
4
|
+
portacode/_version.py,sha256=fngNnJDsfflZ81w9BH0O75dXs5-skjVThUqmwrIaOd0,706
|
|
5
5
|
portacode/cli.py,sha256=mGLKoZ-T2FBF7IA9wUq0zyG0X9__-A1ao7gajjcVRH8,21828
|
|
6
6
|
portacode/data.py,sha256=5-s291bv8J354myaHm1Y7CQZTZyRzMU3TGe5U4hb-FA,1591
|
|
7
7
|
portacode/keypair.py,sha256=0OO4vHDcF1XMxCDqce61xFTlFwlTcmqe5HyGsXFEt7s,5838
|
|
@@ -12,17 +12,17 @@ portacode/connection/README.md,sha256=f9rbuIEKa7cTm9C98rCiBbEtbiIXQU11esGSNhSMiJ
|
|
|
12
12
|
portacode/connection/__init__.py,sha256=atqcVGkViIEd7pRa6cP2do07RJOM0UWpbnz5zXjGktU,250
|
|
13
13
|
portacode/connection/client.py,sha256=jtLb9_YufqPkzi9t8VQH3iz_JEMisbtY6a8L9U5weiU,14181
|
|
14
14
|
portacode/connection/multiplex.py,sha256=L-TxqJ_ZEbfNEfu1cwxgJ5vUdyRzZjsMy2Kx1diiZys,5237
|
|
15
|
-
portacode/connection/terminal.py,sha256=
|
|
15
|
+
portacode/connection/terminal.py,sha256=oyLPOVLPlUuN_eRvHPGazB51yi8W8JEF3oOEYxucGTE,45069
|
|
16
16
|
portacode/connection/handlers/README.md,sha256=HsLZG1QK1JNm67HsgL6WoDg9nxzKXxwkc5fJPFJdX5g,12169
|
|
17
|
-
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=
|
|
18
|
-
portacode/connection/handlers/__init__.py,sha256=
|
|
17
|
+
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=7tBYNEY8EBGAPIMT606BqeHnyMOQIZVlQYpH7me26LY,97962
|
|
18
|
+
portacode/connection/handlers/__init__.py,sha256=WSeBmi65GWFQPYt9M3E10rn0uZ_EPCJzNJOzSf2HZyw,2921
|
|
19
19
|
portacode/connection/handlers/base.py,sha256=oENFb-Fcfzwk99Qx8gJQriEMiwSxwygwjOiuCH36hM4,10231
|
|
20
20
|
portacode/connection/handlers/chunked_content.py,sha256=h6hXRmxSeOgnIxoU8CkmvEf2Odv-ajPrpHIe_W3GKcA,9251
|
|
21
21
|
portacode/connection/handlers/diff_handlers.py,sha256=iYTIRCcpEQ03vIPKZCsMTE5aZbQw6sF04M3dM6rUV8Q,24477
|
|
22
22
|
portacode/connection/handlers/file_handlers.py,sha256=nAJH8nXnX07xxD28ngLpgIUzcTuRwZBNpEGEKdRqohw,39507
|
|
23
23
|
portacode/connection/handlers/project_aware_file_handlers.py,sha256=AqgMnDqX2893T2NsrvUSCwjN5VKj4Pb2TN0S_SuboOE,9803
|
|
24
24
|
portacode/connection/handlers/project_state_handlers.py,sha256=v6ZefGW9i7n1aZLq2jOGumJIjYb6aHlPI4m1jkYewm8,1686
|
|
25
|
-
portacode/connection/handlers/proxmox_infra.py,sha256=
|
|
25
|
+
portacode/connection/handlers/proxmox_infra.py,sha256=qjzMq51xig7p4Y5EVZ1Oo9GA9mAxq4RG0anszL6leKw,55909
|
|
26
26
|
portacode/connection/handlers/registry.py,sha256=qXGE60sYEWg6ZtVQzFcZ5YI2XWR6lMgw4hAL9x5qR1I,6181
|
|
27
27
|
portacode/connection/handlers/session.py,sha256=uNGfiO_1B9-_yjJKkpvmbiJhIl6b-UXlT86UTfd6WYE,42219
|
|
28
28
|
portacode/connection/handlers/system_handlers.py,sha256=AKh7IbwptlLYrbSw5f-DHigvlaKHsg9lDP-lkAUm8cE,10755
|
|
@@ -64,7 +64,7 @@ portacode/utils/__init__.py,sha256=NgBlWTuNJESfIYJzP_3adI1yJQJR0XJLRpSdVNaBAN0,3
|
|
|
64
64
|
portacode/utils/diff_apply.py,sha256=4Oi7ft3VUCKmiUE4VM-OeqO7Gk6H7PF3WnN4WHXtjxI,15157
|
|
65
65
|
portacode/utils/diff_renderer.py,sha256=S76StnQ2DLfsz4Gg0m07UwPfRp8270PuzbNaQq-rmYk,13850
|
|
66
66
|
portacode/utils/ntp_clock.py,sha256=VqCnWCTehCufE43W23oB-WUdAZGeCcLxkmIOPwInYHc,2499
|
|
67
|
-
portacode-1.4.
|
|
67
|
+
portacode-1.4.12.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
|
|
68
68
|
test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
|
|
69
69
|
test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
|
|
70
70
|
test_modules/test_device_online.py,sha256=QtYq0Dq9vME8Gp2O4fGSheqVf8LUtpsSKosXXk56gGM,1654
|
|
@@ -90,8 +90,8 @@ testing_framework/core/playwright_manager.py,sha256=Tw46qwxIhOFkS48C2IWIQHHNpEe-
|
|
|
90
90
|
testing_framework/core/runner.py,sha256=j2QwNJmAxVBmJvcbVS7DgPJUKPNzqfLmt_4NNdaKmZU,19297
|
|
91
91
|
testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
|
|
92
92
|
testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
|
|
93
|
-
portacode-1.4.
|
|
94
|
-
portacode-1.4.
|
|
95
|
-
portacode-1.4.
|
|
96
|
-
portacode-1.4.
|
|
97
|
-
portacode-1.4.
|
|
93
|
+
portacode-1.4.12.dist-info/METADATA,sha256=lAGJrKnZnwWgPbSOe99hICWCxFjU_UMdIRfYOVmqxLI,13046
|
|
94
|
+
portacode-1.4.12.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
95
|
+
portacode-1.4.12.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
96
|
+
portacode-1.4.12.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
97
|
+
portacode-1.4.12.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|