portacode 1.4.11.dev10__py3-none-any.whl → 1.4.12.dev0__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 +258 -6
- {portacode-1.4.11.dev10.dist-info → portacode-1.4.12.dev0.dist-info}/METADATA +1 -1
- {portacode-1.4.11.dev10.dist-info → portacode-1.4.12.dev0.dist-info}/RECORD +10 -10
- {portacode-1.4.11.dev10.dist-info → portacode-1.4.12.dev0.dist-info}/WHEEL +0 -0
- {portacode-1.4.11.dev10.dist-info → portacode-1.4.12.dev0.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.11.dev10.dist-info → portacode-1.4.12.dev0.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.11.dev10.dist-info → portacode-1.4.12.dev0.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.dev0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 12, 'dev0')
|
|
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
|
|
|
@@ -290,6 +294,90 @@ def _ensure_containers_dir() -> None:
|
|
|
290
294
|
CONTAINERS_DIR.mkdir(parents=True, exist_ok=True)
|
|
291
295
|
|
|
292
296
|
|
|
297
|
+
def _invalidate_managed_containers_cache() -> None:
|
|
298
|
+
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
299
|
+
_MANAGED_CONTAINERS_CACHE["timestamp"] = 0.0
|
|
300
|
+
_MANAGED_CONTAINERS_CACHE["summary"] = None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _load_managed_container_records() -> List[Dict[str, Any]]:
|
|
304
|
+
_ensure_containers_dir()
|
|
305
|
+
records: List[Dict[str, Any]] = []
|
|
306
|
+
for path in sorted(CONTAINERS_DIR.glob("ct-*.json")):
|
|
307
|
+
try:
|
|
308
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
309
|
+
except Exception as exc: # pragma: no cover - best effort logging
|
|
310
|
+
logger.debug("Unable to read container record %s: %s", path, exc)
|
|
311
|
+
continue
|
|
312
|
+
records.append(payload)
|
|
313
|
+
return records
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _build_managed_containers_summary(records: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
317
|
+
total_ram = 0
|
|
318
|
+
total_disk = 0
|
|
319
|
+
total_cpu_share = 0.0
|
|
320
|
+
containers: List[Dict[str, Any]] = []
|
|
321
|
+
|
|
322
|
+
def _as_int(value: Any) -> int:
|
|
323
|
+
try:
|
|
324
|
+
return int(value)
|
|
325
|
+
except (TypeError, ValueError):
|
|
326
|
+
return 0
|
|
327
|
+
|
|
328
|
+
def _as_float(value: Any) -> float:
|
|
329
|
+
try:
|
|
330
|
+
return float(value)
|
|
331
|
+
except (TypeError, ValueError):
|
|
332
|
+
return 0.0
|
|
333
|
+
|
|
334
|
+
for record in sorted(records, key=lambda entry: _as_int(entry.get("vmid"))):
|
|
335
|
+
ram_mib = _as_int(record.get("ram_mib"))
|
|
336
|
+
disk_gib = _as_int(record.get("disk_gib"))
|
|
337
|
+
cpu_share = _as_float(record.get("cpus"))
|
|
338
|
+
total_ram += ram_mib
|
|
339
|
+
total_disk += disk_gib
|
|
340
|
+
total_cpu_share += cpu_share
|
|
341
|
+
status = (record.get("status") or "unknown").lower()
|
|
342
|
+
containers.append(
|
|
343
|
+
{
|
|
344
|
+
"vmid": str(_as_int(record.get("vmid"))) if record.get("vmid") is not None else None,
|
|
345
|
+
"hostname": record.get("hostname"),
|
|
346
|
+
"template": record.get("template"),
|
|
347
|
+
"storage": record.get("storage"),
|
|
348
|
+
"disk_gib": disk_gib,
|
|
349
|
+
"ram_mib": ram_mib,
|
|
350
|
+
"cpu_share": cpu_share,
|
|
351
|
+
"created_at": record.get("created_at"),
|
|
352
|
+
"status": status,
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
"updated_at": datetime.utcnow().isoformat() + "Z",
|
|
358
|
+
"count": len(containers),
|
|
359
|
+
"total_ram_mib": total_ram,
|
|
360
|
+
"total_disk_gib": total_disk,
|
|
361
|
+
"total_cpu_share": round(total_cpu_share, 2),
|
|
362
|
+
"containers": containers,
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _get_managed_containers_summary(force: bool = False) -> Dict[str, Any]:
|
|
367
|
+
now = time.monotonic()
|
|
368
|
+
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
369
|
+
cache_ts = _MANAGED_CONTAINERS_CACHE["timestamp"]
|
|
370
|
+
cached = _MANAGED_CONTAINERS_CACHE["summary"]
|
|
371
|
+
if not force and cached and now - cache_ts < _MANAGED_CONTAINERS_CACHE_TTL_S:
|
|
372
|
+
return cached
|
|
373
|
+
records = _load_managed_container_records()
|
|
374
|
+
summary = _build_managed_containers_summary(records)
|
|
375
|
+
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
376
|
+
_MANAGED_CONTAINERS_CACHE["timestamp"] = now
|
|
377
|
+
_MANAGED_CONTAINERS_CACHE["summary"] = summary
|
|
378
|
+
return summary
|
|
379
|
+
|
|
380
|
+
|
|
293
381
|
def _format_rootfs(storage: str, disk_gib: int, storage_type: str) -> str:
|
|
294
382
|
if storage_type in ("lvm", "lvmthin"):
|
|
295
383
|
return f"{storage}:{disk_gib}"
|
|
@@ -365,14 +453,14 @@ def _get_storage_type(storages: Iterable[Dict[str, Any]], storage_name: str) ->
|
|
|
365
453
|
return ""
|
|
366
454
|
|
|
367
455
|
|
|
368
|
-
def
|
|
456
|
+
def _validate_positive_number(value: Any, default: float) -> float:
|
|
369
457
|
try:
|
|
370
|
-
candidate =
|
|
458
|
+
candidate = float(value)
|
|
371
459
|
if candidate > 0:
|
|
372
460
|
return candidate
|
|
373
461
|
except Exception:
|
|
374
462
|
pass
|
|
375
|
-
return default
|
|
463
|
+
return float(default)
|
|
376
464
|
|
|
377
465
|
|
|
378
466
|
def _wait_for_task(proxmox: Any, node: str, upid: str) -> Tuple[Dict[str, Any], float]:
|
|
@@ -424,10 +512,24 @@ def _start_container(proxmox: Any, node: str, vmid: int) -> Tuple[Dict[str, Any]
|
|
|
424
512
|
return _wait_for_task(proxmox, node, upid)
|
|
425
513
|
|
|
426
514
|
|
|
515
|
+
def _stop_container(proxmox: Any, node: str, vmid: int) -> Tuple[Dict[str, Any], float]:
|
|
516
|
+
status = proxmox.nodes(node).lxc(vmid).status.current.get()
|
|
517
|
+
if status.get("status") != "running":
|
|
518
|
+
return status, 0.0
|
|
519
|
+
upid = proxmox.nodes(node).lxc(vmid).status.stop.post()
|
|
520
|
+
return _wait_for_task(proxmox, node, upid)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _delete_container(proxmox: Any, node: str, vmid: int) -> Tuple[Dict[str, Any], float]:
|
|
524
|
+
upid = proxmox.nodes(node).lxc(vmid).delete()
|
|
525
|
+
return _wait_for_task(proxmox, node, upid)
|
|
526
|
+
|
|
527
|
+
|
|
427
528
|
def _write_container_record(vmid: int, payload: Dict[str, Any]) -> None:
|
|
428
529
|
_ensure_containers_dir()
|
|
429
530
|
path = CONTAINERS_DIR / f"ct-{vmid}.json"
|
|
430
531
|
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
532
|
+
_invalidate_managed_containers_cache()
|
|
431
533
|
|
|
432
534
|
|
|
433
535
|
def _read_container_record(vmid: int) -> Dict[str, Any]:
|
|
@@ -437,6 +539,19 @@ def _read_container_record(vmid: int) -> Dict[str, Any]:
|
|
|
437
539
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
438
540
|
|
|
439
541
|
|
|
542
|
+
def _update_container_record(vmid: int, updates: Dict[str, Any]) -> None:
|
|
543
|
+
record = _read_container_record(vmid)
|
|
544
|
+
record.update(updates)
|
|
545
|
+
_write_container_record(vmid, record)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _remove_container_record(vmid: int) -> None:
|
|
549
|
+
path = CONTAINERS_DIR / f"ct-{vmid}.json"
|
|
550
|
+
if path.exists():
|
|
551
|
+
path.unlink()
|
|
552
|
+
_invalidate_managed_containers_cache()
|
|
553
|
+
|
|
554
|
+
|
|
440
555
|
def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) -> Dict[str, Any]:
|
|
441
556
|
templates = config.get("templates") or []
|
|
442
557
|
default_template = templates[0] if templates else ""
|
|
@@ -446,9 +561,9 @@ def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) ->
|
|
|
446
561
|
|
|
447
562
|
bridge = config.get("network", {}).get("bridge", DEFAULT_BRIDGE)
|
|
448
563
|
hostname = (message.get("hostname") or "").strip()
|
|
449
|
-
disk_gib =
|
|
450
|
-
ram_mib =
|
|
451
|
-
cpus =
|
|
564
|
+
disk_gib = int(max(round(_validate_positive_number(message.get("disk_gib") or message.get("disk"), 3)), 1))
|
|
565
|
+
ram_mib = int(max(round(_validate_positive_number(message.get("ram_mib") or message.get("ram"), 2048)), 1))
|
|
566
|
+
cpus = _validate_positive_number(message.get("cpus"), 1)
|
|
452
567
|
storage = message.get("storage") or config.get("default_storage") or ""
|
|
453
568
|
if not storage:
|
|
454
569
|
raise ValueError("Storage pool could not be determined.")
|
|
@@ -473,6 +588,38 @@ def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) ->
|
|
|
473
588
|
return payload
|
|
474
589
|
|
|
475
590
|
|
|
591
|
+
def _ensure_infra_configured() -> Dict[str, Any]:
|
|
592
|
+
config = _load_config()
|
|
593
|
+
if not config or not config.get("token_value"):
|
|
594
|
+
raise RuntimeError("Proxmox infrastructure is not configured.")
|
|
595
|
+
return config
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _get_node_from_config(config: Dict[str, Any]) -> str:
|
|
599
|
+
return config.get("node") or DEFAULT_NODE_NAME
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _parse_ctid(message: Dict[str, Any]) -> int:
|
|
603
|
+
for key in ("ctid", "vmid"):
|
|
604
|
+
value = message.get(key)
|
|
605
|
+
if value is not None:
|
|
606
|
+
try:
|
|
607
|
+
return int(str(value).strip())
|
|
608
|
+
except ValueError:
|
|
609
|
+
raise ValueError(f"{key} must be an integer") from None
|
|
610
|
+
raise ValueError("ctid is required")
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _ensure_container_managed(
|
|
614
|
+
proxmox: Any, node: str, vmid: int
|
|
615
|
+
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
616
|
+
record = _read_container_record(vmid)
|
|
617
|
+
ct_cfg = proxmox.nodes(node).lxc(str(vmid)).config.get()
|
|
618
|
+
if not ct_cfg or MANAGED_MARKER not in (ct_cfg.get("description") or ""):
|
|
619
|
+
raise RuntimeError(f"Container {vmid} is not managed by Portacode.")
|
|
620
|
+
return record, ct_cfg
|
|
621
|
+
|
|
622
|
+
|
|
476
623
|
def _connect_proxmox(config: Dict[str, Any]) -> Any:
|
|
477
624
|
ProxmoxAPI = _ensure_proxmoxer()
|
|
478
625
|
return ProxmoxAPI(
|
|
@@ -748,6 +895,7 @@ def configure_infrastructure(token_identifier: str, token_value: str, verify_ssl
|
|
|
748
895
|
_save_config(config)
|
|
749
896
|
snapshot = build_snapshot(config)
|
|
750
897
|
snapshot["node_status"] = status
|
|
898
|
+
snapshot["managed_containers"] = _get_managed_containers_summary(force=True)
|
|
751
899
|
return snapshot
|
|
752
900
|
|
|
753
901
|
|
|
@@ -756,6 +904,7 @@ def get_infra_snapshot() -> Dict[str, Any]:
|
|
|
756
904
|
snapshot = build_snapshot(config)
|
|
757
905
|
if config.get("node_status"):
|
|
758
906
|
snapshot["node_status"] = config["node_status"]
|
|
907
|
+
snapshot["managed_containers"] = _get_managed_containers_summary()
|
|
759
908
|
return snapshot
|
|
760
909
|
|
|
761
910
|
|
|
@@ -768,6 +917,7 @@ def revert_infrastructure() -> Dict[str, Any]:
|
|
|
768
917
|
snapshot["network"]["applied"] = False
|
|
769
918
|
snapshot["network"]["message"] = "Reverted to previous network state"
|
|
770
919
|
snapshot["network"]["bridge"] = DEFAULT_BRIDGE
|
|
920
|
+
snapshot["managed_containers"] = _get_managed_containers_summary(force=True)
|
|
771
921
|
return snapshot
|
|
772
922
|
|
|
773
923
|
|
|
@@ -905,6 +1055,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
905
1055
|
vmid, _ = _instantiate_container(proxmox, node, payload)
|
|
906
1056
|
payload["vmid"] = vmid
|
|
907
1057
|
payload["created_at"] = datetime.utcnow().isoformat() + "Z"
|
|
1058
|
+
payload["status"] = "creating"
|
|
908
1059
|
_write_container_record(vmid, payload)
|
|
909
1060
|
return proxmox, node, vmid, payload
|
|
910
1061
|
|
|
@@ -926,6 +1077,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
926
1077
|
"Container startup completed.",
|
|
927
1078
|
_start_container_step,
|
|
928
1079
|
)
|
|
1080
|
+
_update_container_record(vmid, {"status": "running"})
|
|
929
1081
|
|
|
930
1082
|
def _bootstrap_progress_callback(
|
|
931
1083
|
step_index: int,
|
|
@@ -1103,6 +1255,106 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1103
1255
|
}
|
|
1104
1256
|
|
|
1105
1257
|
|
|
1258
|
+
class StartProxmoxContainerHandler(SyncHandler):
|
|
1259
|
+
"""Start a managed container via the Proxmox API."""
|
|
1260
|
+
|
|
1261
|
+
@property
|
|
1262
|
+
def command_name(self) -> str:
|
|
1263
|
+
return "start_proxmox_container"
|
|
1264
|
+
|
|
1265
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1266
|
+
vmid = _parse_ctid(message)
|
|
1267
|
+
config = _ensure_infra_configured()
|
|
1268
|
+
proxmox = _connect_proxmox(config)
|
|
1269
|
+
node = _get_node_from_config(config)
|
|
1270
|
+
_ensure_container_managed(proxmox, node, vmid)
|
|
1271
|
+
|
|
1272
|
+
status, elapsed = _start_container(proxmox, node, vmid)
|
|
1273
|
+
_update_container_record(vmid, {"status": "running"})
|
|
1274
|
+
|
|
1275
|
+
infra = get_infra_snapshot()
|
|
1276
|
+
return {
|
|
1277
|
+
"event": "proxmox_container_action",
|
|
1278
|
+
"action": "start",
|
|
1279
|
+
"success": True,
|
|
1280
|
+
"ctid": str(vmid),
|
|
1281
|
+
"message": f"Started container {vmid} in {elapsed:.1f}s.",
|
|
1282
|
+
"details": {"exitstatus": status.get("exitstatus")},
|
|
1283
|
+
"status": status.get("status"),
|
|
1284
|
+
"infra": infra,
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
class StopProxmoxContainerHandler(SyncHandler):
|
|
1289
|
+
"""Stop a managed container via the Proxmox API."""
|
|
1290
|
+
|
|
1291
|
+
@property
|
|
1292
|
+
def command_name(self) -> str:
|
|
1293
|
+
return "stop_proxmox_container"
|
|
1294
|
+
|
|
1295
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1296
|
+
vmid = _parse_ctid(message)
|
|
1297
|
+
config = _ensure_infra_configured()
|
|
1298
|
+
proxmox = _connect_proxmox(config)
|
|
1299
|
+
node = _get_node_from_config(config)
|
|
1300
|
+
_ensure_container_managed(proxmox, node, vmid)
|
|
1301
|
+
|
|
1302
|
+
status, elapsed = _stop_container(proxmox, node, vmid)
|
|
1303
|
+
final_status = status.get("status") or "stopped"
|
|
1304
|
+
_update_container_record(vmid, {"status": final_status})
|
|
1305
|
+
|
|
1306
|
+
infra = get_infra_snapshot()
|
|
1307
|
+
message_text = (
|
|
1308
|
+
f"Container {vmid} is already stopped."
|
|
1309
|
+
if final_status != "running" and elapsed == 0.0
|
|
1310
|
+
else f"Stopped container {vmid} in {elapsed:.1f}s."
|
|
1311
|
+
)
|
|
1312
|
+
return {
|
|
1313
|
+
"event": "proxmox_container_action",
|
|
1314
|
+
"action": "stop",
|
|
1315
|
+
"success": True,
|
|
1316
|
+
"ctid": str(vmid),
|
|
1317
|
+
"message": message_text,
|
|
1318
|
+
"details": {"exitstatus": status.get("exitstatus")},
|
|
1319
|
+
"status": final_status,
|
|
1320
|
+
"infra": infra,
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
class RemoveProxmoxContainerHandler(SyncHandler):
|
|
1325
|
+
"""Delete a managed container via the Proxmox API."""
|
|
1326
|
+
|
|
1327
|
+
@property
|
|
1328
|
+
def command_name(self) -> str:
|
|
1329
|
+
return "remove_proxmox_container"
|
|
1330
|
+
|
|
1331
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1332
|
+
vmid = _parse_ctid(message)
|
|
1333
|
+
config = _ensure_infra_configured()
|
|
1334
|
+
proxmox = _connect_proxmox(config)
|
|
1335
|
+
node = _get_node_from_config(config)
|
|
1336
|
+
_ensure_container_managed(proxmox, node, vmid)
|
|
1337
|
+
|
|
1338
|
+
stop_status, stop_elapsed = _stop_container(proxmox, node, vmid)
|
|
1339
|
+
delete_status, delete_elapsed = _delete_container(proxmox, node, vmid)
|
|
1340
|
+
_remove_container_record(vmid)
|
|
1341
|
+
|
|
1342
|
+
infra = get_infra_snapshot()
|
|
1343
|
+
return {
|
|
1344
|
+
"event": "proxmox_container_action",
|
|
1345
|
+
"action": "remove",
|
|
1346
|
+
"success": True,
|
|
1347
|
+
"ctid": str(vmid),
|
|
1348
|
+
"message": f"Deleted container {vmid} in {delete_elapsed:.1f}s.",
|
|
1349
|
+
"details": {
|
|
1350
|
+
"stop_exitstatus": stop_status.get("exitstatus"),
|
|
1351
|
+
"delete_exitstatus": delete_status.get("exitstatus"),
|
|
1352
|
+
},
|
|
1353
|
+
"status": "deleted",
|
|
1354
|
+
"infra": infra,
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
|
|
1106
1358
|
class ConfigureProxmoxInfraHandler(SyncHandler):
|
|
1107
1359
|
@property
|
|
1108
1360
|
def command_name(self) -> str:
|
|
@@ -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=5exIeQXwAxVtu_nbVVxfGhZlbO5I6xDEDIBuQWTo_X4,719
|
|
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
|
|
@@ -14,15 +14,15 @@ portacode/connection/client.py,sha256=jtLb9_YufqPkzi9t8VQH3iz_JEMisbtY6a8L9U5wei
|
|
|
14
14
|
portacode/connection/multiplex.py,sha256=L-TxqJ_ZEbfNEfu1cwxgJ5vUdyRzZjsMy2Kx1diiZys,5237
|
|
15
15
|
portacode/connection/terminal.py,sha256=07wxG_55JMy3yQ9TXCBldW9h43qCW3U8rv2yzGMx4FM,44757
|
|
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=-t9I2ED-jT1FBpgpgkWNRRWcgtGEitTQw5ukScFZWyk,50300
|
|
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.dev0.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.dev0.dist-info/METADATA,sha256=Ofq1p3psLwNSji1a0CMinTlbgiVDWQZa2D3sYLcsAzg,13051
|
|
94
|
+
portacode-1.4.12.dev0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
95
|
+
portacode-1.4.12.dev0.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
96
|
+
portacode-1.4.12.dev0.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
97
|
+
portacode-1.4.12.dev0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|