portacode 1.4.11.dev9__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 +412 -23
- portacode/connection/terminal.py +6 -0
- {portacode-1.4.11.dev9.dist-info → portacode-1.4.12.dist-info}/METADATA +1 -1
- {portacode-1.4.11.dev9.dist-info → portacode-1.4.12.dist-info}/RECORD +11 -11
- {portacode-1.4.11.dev9.dist-info → portacode-1.4.12.dist-info}/WHEEL +1 -1
- {portacode-1.4.11.dev9.dist-info → portacode-1.4.12.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.11.dev9.dist-info → portacode-1.4.12.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.11.dev9.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,11 +1080,13 @@ 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()
|
|
844
1087
|
except Exception as exc:
|
|
845
1088
|
_emit_progress_event(
|
|
1089
|
+
self,
|
|
846
1090
|
step_index=step_index,
|
|
847
1091
|
total_steps=total_steps,
|
|
848
1092
|
step_name=step_name,
|
|
@@ -851,10 +1095,12 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
851
1095
|
message=f"{step_label} failed: {exc}",
|
|
852
1096
|
phase="lifecycle",
|
|
853
1097
|
request_id=request_id,
|
|
1098
|
+
client_sessions=client_sessions,
|
|
854
1099
|
details={"error": str(exc)},
|
|
855
1100
|
)
|
|
856
1101
|
raise
|
|
857
|
-
|
|
1102
|
+
_emit_progress_event(
|
|
1103
|
+
self,
|
|
858
1104
|
step_index=step_index,
|
|
859
1105
|
total_steps=total_steps,
|
|
860
1106
|
step_name=step_name,
|
|
@@ -863,6 +1109,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
863
1109
|
message=success_message,
|
|
864
1110
|
phase="lifecycle",
|
|
865
1111
|
request_id=request_id,
|
|
1112
|
+
client_sessions=client_sessions,
|
|
866
1113
|
)
|
|
867
1114
|
current_step_index += 1
|
|
868
1115
|
return result
|
|
@@ -903,6 +1150,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
903
1150
|
vmid, _ = _instantiate_container(proxmox, node, payload)
|
|
904
1151
|
payload["vmid"] = vmid
|
|
905
1152
|
payload["created_at"] = datetime.utcnow().isoformat() + "Z"
|
|
1153
|
+
payload["status"] = "creating"
|
|
906
1154
|
_write_container_record(vmid, payload)
|
|
907
1155
|
return proxmox, node, vmid, payload
|
|
908
1156
|
|
|
@@ -924,6 +1172,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
924
1172
|
"Container startup completed.",
|
|
925
1173
|
_start_container_step,
|
|
926
1174
|
)
|
|
1175
|
+
_update_container_record(vmid, {"status": "running"})
|
|
927
1176
|
|
|
928
1177
|
def _bootstrap_progress_callback(
|
|
929
1178
|
step_index: int,
|
|
@@ -952,6 +1201,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
952
1201
|
if error_summary:
|
|
953
1202
|
details["error_summary"] = error_summary
|
|
954
1203
|
_emit_progress_event(
|
|
1204
|
+
self,
|
|
955
1205
|
step_index=step_index,
|
|
956
1206
|
total_steps=total,
|
|
957
1207
|
step_name=step.get("name", "bootstrap"),
|
|
@@ -960,6 +1210,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
960
1210
|
message=message_text,
|
|
961
1211
|
phase="bootstrap",
|
|
962
1212
|
request_id=request_id,
|
|
1213
|
+
client_sessions=client_sessions,
|
|
963
1214
|
details=details or None,
|
|
964
1215
|
)
|
|
965
1216
|
|
|
@@ -975,7 +1226,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
975
1226
|
)
|
|
976
1227
|
current_step_index += len(bootstrap_steps)
|
|
977
1228
|
|
|
978
|
-
|
|
1229
|
+
response = {
|
|
979
1230
|
"event": "proxmox_container_created",
|
|
980
1231
|
"success": True,
|
|
981
1232
|
"message": f"Container {vmid} is ready and Portacode key captured.",
|
|
@@ -992,6 +1243,9 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
992
1243
|
},
|
|
993
1244
|
"setup_steps": steps,
|
|
994
1245
|
}
|
|
1246
|
+
if client_sessions:
|
|
1247
|
+
response["client_sessions"] = client_sessions
|
|
1248
|
+
return response
|
|
995
1249
|
|
|
996
1250
|
|
|
997
1251
|
class StartPortacodeServiceHandler(SyncHandler):
|
|
@@ -1019,6 +1273,8 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1019
1273
|
start_index = int(message.get("step_index", 1))
|
|
1020
1274
|
total_steps = int(message.get("total_steps", start_index + 2))
|
|
1021
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
|
|
1022
1278
|
|
|
1023
1279
|
auth_step_name = "setup_device_authentication"
|
|
1024
1280
|
auth_label = "Setting up device authentication"
|
|
@@ -1032,6 +1288,7 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1032
1288
|
message="Notifying the server of the new device…",
|
|
1033
1289
|
phase="service",
|
|
1034
1290
|
request_id=request_id,
|
|
1291
|
+
client_sessions=client_sessions,
|
|
1035
1292
|
)
|
|
1036
1293
|
_emit_progress_event(
|
|
1037
1294
|
self,
|
|
@@ -1043,6 +1300,7 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1043
1300
|
message="Authentication metadata recorded.",
|
|
1044
1301
|
phase="service",
|
|
1045
1302
|
request_id=request_id,
|
|
1303
|
+
client_sessions=client_sessions,
|
|
1046
1304
|
)
|
|
1047
1305
|
|
|
1048
1306
|
install_step = start_index + 1
|
|
@@ -1057,6 +1315,7 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1057
1315
|
message="Running sudo portacode service install…",
|
|
1058
1316
|
phase="service",
|
|
1059
1317
|
request_id=request_id,
|
|
1318
|
+
client_sessions=client_sessions,
|
|
1060
1319
|
)
|
|
1061
1320
|
|
|
1062
1321
|
cmd = f"su - {user} -c 'sudo -S portacode service install'"
|
|
@@ -1073,6 +1332,7 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1073
1332
|
message=f"{install_label} failed: {res.get('stderr') or res.get('stdout')}",
|
|
1074
1333
|
phase="service",
|
|
1075
1334
|
request_id=request_id,
|
|
1335
|
+
client_sessions=client_sessions,
|
|
1076
1336
|
details={
|
|
1077
1337
|
"stderr": res.get("stderr"),
|
|
1078
1338
|
"stdout": res.get("stdout"),
|
|
@@ -1090,14 +1350,133 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1090
1350
|
message="Portacode service install finished.",
|
|
1091
1351
|
phase="service",
|
|
1092
1352
|
request_id=request_id,
|
|
1353
|
+
client_sessions=client_sessions,
|
|
1093
1354
|
)
|
|
1094
1355
|
|
|
1095
|
-
|
|
1356
|
+
response = {
|
|
1096
1357
|
"event": "proxmox_service_started",
|
|
1097
1358
|
"success": True,
|
|
1098
1359
|
"message": "Portacode service install completed",
|
|
1099
1360
|
"ctid": str(vmid),
|
|
1100
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
|
|
1101
1480
|
|
|
1102
1481
|
|
|
1103
1482
|
class ConfigureProxmoxInfraHandler(SyncHandler):
|
|
@@ -1111,13 +1490,18 @@ class ConfigureProxmoxInfraHandler(SyncHandler):
|
|
|
1111
1490
|
verify_ssl = bool(message.get("verify_ssl"))
|
|
1112
1491
|
if not token_identifier or not token_value:
|
|
1113
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
|
|
1114
1495
|
snapshot = configure_infrastructure(token_identifier, token_value, verify_ssl=verify_ssl)
|
|
1115
|
-
|
|
1496
|
+
response = {
|
|
1116
1497
|
"event": "proxmox_infra_configured",
|
|
1117
1498
|
"success": True,
|
|
1118
1499
|
"message": "Proxmox infrastructure configured",
|
|
1119
1500
|
"infra": snapshot,
|
|
1120
1501
|
}
|
|
1502
|
+
if client_sessions:
|
|
1503
|
+
response["client_sessions"] = client_sessions
|
|
1504
|
+
return response
|
|
1121
1505
|
|
|
1122
1506
|
|
|
1123
1507
|
class RevertProxmoxInfraHandler(SyncHandler):
|
|
@@ -1126,10 +1510,15 @@ class RevertProxmoxInfraHandler(SyncHandler):
|
|
|
1126
1510
|
return "revert_proxmox_infra"
|
|
1127
1511
|
|
|
1128
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
|
|
1129
1515
|
snapshot = revert_infrastructure()
|
|
1130
|
-
|
|
1516
|
+
response = {
|
|
1131
1517
|
"event": "proxmox_infra_reverted",
|
|
1132
1518
|
"success": True,
|
|
1133
1519
|
"message": "Proxmox infrastructure configuration reverted",
|
|
1134
1520
|
"infra": snapshot,
|
|
1135
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
|