portacode 1.4.11.dev5__py3-none-any.whl → 1.4.12.dev1__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.
Potentially problematic release.
This version of portacode might be problematic. Click here for more details.
- portacode/_version.py +2 -2
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +142 -15
- portacode/connection/handlers/__init__.py +8 -0
- portacode/connection/handlers/proxmox_infra.py +727 -100
- portacode/connection/terminal.py +3 -0
- {portacode-1.4.11.dev5.dist-info → portacode-1.4.12.dev1.dist-info}/METADATA +1 -1
- {portacode-1.4.11.dev5.dist-info → portacode-1.4.12.dev1.dist-info}/RECORD +11 -11
- {portacode-1.4.11.dev5.dist-info → portacode-1.4.12.dev1.dist-info}/WHEEL +0 -0
- {portacode-1.4.11.dev5.dist-info → portacode-1.4.12.dev1.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.11.dev5.dist-info → portacode-1.4.12.dev1.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.11.dev5.dist-info → portacode-1.4.12.dev1.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.dev1'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 12, 'dev1')
|
|
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.
|
|
@@ -380,6 +419,48 @@ Emitted after a successful `create_proxmox_container` action. Contains the new c
|
|
|
380
419
|
* `container` (object): Metadata such as `vmid`, `hostname`, `template`, `storage`, `disk_gib`, `ram_mib`, and `cpus`.
|
|
381
420
|
* `setup_steps` (array[object]): Detailed bootstrap step results (name, stdout/stderr, elapsed time, and status).
|
|
382
421
|
|
|
422
|
+
### `proxmox_container_progress`
|
|
423
|
+
|
|
424
|
+
Sent intermittently while `create_proxmox_container` is executing so callers can display a progress indicator. Each notification describes the currently running step (validation, provisioning, or each bootstrap command) and whether it succeeded or failed.
|
|
425
|
+
|
|
426
|
+
**Event Fields:**
|
|
427
|
+
|
|
428
|
+
* `step_index` (integer): 1-based index of the current step inside the entire provisioning sequence.
|
|
429
|
+
* `total_steps` (integer): Total number of steps that must run before provisioning completes.
|
|
430
|
+
* `step_name` (string): Internal step identifier (e.g., `create_container`, `apt_update`, `portacode_connect`).
|
|
431
|
+
* `step_label` (string): Human-friendly label suitable for UI (e.g., `Create container`, `Apt update`).
|
|
432
|
+
* `status` (string): One of `in_progress`, `completed`, or `failed`.
|
|
433
|
+
* `phase` (string): Either `lifecycle` (environment/container lifecycle) or `bootstrap` (per-command bootstrap work).
|
|
434
|
+
* `message` (string): Short description of what is happening or why a failure occurred.
|
|
435
|
+
* `details` (object, optional): Contains `attempt` (if retries were needed) and `error_summary` when a step fails.
|
|
436
|
+
* `request_id` (string, optional): Mirrors the request ID from the incoming `create_proxmox_container` payload when available.
|
|
437
|
+
|
|
438
|
+
### `start_portacode_service`
|
|
439
|
+
|
|
440
|
+
Runs `sudo portacode service install` inside the container after the dashboard has created the corresponding Device record with the supplied public key.
|
|
441
|
+
|
|
442
|
+
**Payload Fields:**
|
|
443
|
+
|
|
444
|
+
* `ctid` (string, required): Container ID target.
|
|
445
|
+
* `step_index` (integer, required): Next step index to render inside `proxmox_container_progress`.
|
|
446
|
+
* `total_steps` (integer, required): The overall total number of steps (including lifecycle, bootstrap, and service installation).
|
|
447
|
+
|
|
448
|
+
**Responses:**
|
|
449
|
+
|
|
450
|
+
* Emits additional [`proxmox_container_progress`](#proxmox_container_progress-event) events to report the authentication and service-install steps.
|
|
451
|
+
* On success, emits a [`proxmox_service_started`](#proxmox_service_started-event).
|
|
452
|
+
* On failure, emits a generic [`error`](#error) event.
|
|
453
|
+
|
|
454
|
+
### `proxmox_service_started`
|
|
455
|
+
|
|
456
|
+
Indicates that `portacode service install` finished successfully inside a managed container.
|
|
457
|
+
|
|
458
|
+
**Event Fields:**
|
|
459
|
+
|
|
460
|
+
* `success` (boolean): True when the install succeeded.
|
|
461
|
+
* `message` (string): Success summary (e.g., `Portacode service install completed`).
|
|
462
|
+
* `ctid` (string): Container ID.
|
|
463
|
+
|
|
383
464
|
### `clock_sync_request`
|
|
384
465
|
|
|
385
466
|
Internal event that devices send to the gateway to request the authoritative server timestamp (used for adjusting `portacode.utils.ntp_clock`). The gateway responds immediately with [`clock_sync_response`](#clock_sync_response).
|
|
@@ -1025,21 +1106,37 @@ Provides system information in response to a `system_info` action. Handled by [`
|
|
|
1025
1106
|
* `proxmox` (object): Detection hints for Proxmox VE nodes:
|
|
1026
1107
|
* `is_proxmox_node` (boolean): True when Proxmox artifacts (e.g., `/etc/proxmox-release`) exist.
|
|
1027
1108
|
* `version` (string|null): Raw contents of `/etc/proxmox-release` when readable.
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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.
|
|
1043
1140
|
* `portacode_version` (string): Installed CLI version returned by `portacode.__version__`.
|
|
1044
1141
|
|
|
1045
1142
|
### `proxmox_infra_configured`
|
|
@@ -1075,6 +1172,36 @@ Emitted after a successful `create_proxmox_container` action to report the newly
|
|
|
1075
1172
|
* `container` (object): Metadata such as `vmid`, `hostname`, `template`, `storage`, `disk_gib`, `ram_mib`, and `cpus`.
|
|
1076
1173
|
* `setup_steps` (array[object]): Detailed bootstrap step reports including stdout/stderr, elapsed time, and pass/fail status.
|
|
1077
1174
|
|
|
1175
|
+
### `proxmox_container_progress`
|
|
1176
|
+
|
|
1177
|
+
Sent continuously while `create_proxmox_container` runs so dashboards can show a progress bar tied to each lifecycle and bootstrap step.
|
|
1178
|
+
|
|
1179
|
+
**Event Fields:**
|
|
1180
|
+
|
|
1181
|
+
* `step_index` (integer): 1-based position of the step inside the entire provisioning workflow.
|
|
1182
|
+
* `total_steps` (integer): Total number of lifecycle and bootstrap steps for the current operation.
|
|
1183
|
+
* `step_name` (string): Internal identifier (e.g., `validate_environment`, `install_deps`, `portacode_connect`).
|
|
1184
|
+
* `step_label` (string): Friendly label suitable for the UI.
|
|
1185
|
+
* `status` (string): One of `in_progress`, `completed`, or `failed`.
|
|
1186
|
+
* `phase` (string): Either `lifecycle` (node validation/container lifecycle) or `bootstrap` (commands run inside the CT).
|
|
1187
|
+
* `message` (string): Short human-readable description of the action or failure.
|
|
1188
|
+
* `details` (object, optional): Contains `attempt` (when retries are used) and `error_summary` on failure.
|
|
1189
|
+
* `request_id` (string, optional): Mirrors the `create_proxmox_container` request when provided.
|
|
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
|
+
|
|
1078
1205
|
### <a name="clock_sync_response"></a>`clock_sync_response`
|
|
1079
1206
|
|
|
1080
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.
|
|
@@ -45,6 +45,10 @@ from .proxmox_infra import (
|
|
|
45
45
|
ConfigureProxmoxInfraHandler,
|
|
46
46
|
CreateProxmoxContainerHandler,
|
|
47
47
|
RevertProxmoxInfraHandler,
|
|
48
|
+
StartPortacodeServiceHandler,
|
|
49
|
+
StartProxmoxContainerHandler,
|
|
50
|
+
StopProxmoxContainerHandler,
|
|
51
|
+
RemoveProxmoxContainerHandler,
|
|
48
52
|
)
|
|
49
53
|
|
|
50
54
|
__all__ = [
|
|
@@ -84,6 +88,10 @@ __all__ = [
|
|
|
84
88
|
"ProjectStateGitUnstageHandler",
|
|
85
89
|
"ProjectStateGitRevertHandler",
|
|
86
90
|
"ProjectStateGitCommitHandler",
|
|
91
|
+
"StartPortacodeServiceHandler",
|
|
92
|
+
"StartProxmoxContainerHandler",
|
|
93
|
+
"StopProxmoxContainerHandler",
|
|
94
|
+
"RemoveProxmoxContainerHandler",
|
|
87
95
|
"UpdatePortacodeHandler",
|
|
88
96
|
"RevertProxmoxInfraHandler",
|
|
89
97
|
]
|
|
@@ -2,17 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import json
|
|
6
7
|
import logging
|
|
7
8
|
import os
|
|
9
|
+
import secrets
|
|
8
10
|
import shutil
|
|
9
11
|
import stat
|
|
10
12
|
import subprocess
|
|
11
13
|
import sys
|
|
12
14
|
import time
|
|
15
|
+
import threading
|
|
13
16
|
from datetime import datetime
|
|
14
17
|
from pathlib import Path
|
|
15
|
-
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
18
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
|
|
16
19
|
|
|
17
20
|
import platformdirs
|
|
18
21
|
|
|
@@ -38,6 +41,58 @@ DNS_SERVER = "1.1.1.1"
|
|
|
38
41
|
IFACES_PATH = Path("/etc/network/interfaces")
|
|
39
42
|
SYSCTL_PATH = Path("/etc/sysctl.d/99-portacode-forward.conf")
|
|
40
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()
|
|
47
|
+
|
|
48
|
+
ProgressCallback = Callable[[int, int, Dict[str, Any], str, Optional[Dict[str, Any]]], None]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _emit_progress_event(
|
|
52
|
+
handler: SyncHandler,
|
|
53
|
+
*,
|
|
54
|
+
step_index: int,
|
|
55
|
+
total_steps: int,
|
|
56
|
+
step_name: str,
|
|
57
|
+
step_label: str,
|
|
58
|
+
status: str,
|
|
59
|
+
message: str,
|
|
60
|
+
phase: str,
|
|
61
|
+
request_id: Optional[str],
|
|
62
|
+
details: Optional[Dict[str, Any]] = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
loop = handler.context.get("event_loop")
|
|
65
|
+
if not loop or loop.is_closed():
|
|
66
|
+
logger.debug(
|
|
67
|
+
"progress event skipped (no event loop) step=%s status=%s",
|
|
68
|
+
step_name,
|
|
69
|
+
status,
|
|
70
|
+
)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
payload: Dict[str, Any] = {
|
|
74
|
+
"event": "proxmox_container_progress",
|
|
75
|
+
"step_name": step_name,
|
|
76
|
+
"step_label": step_label,
|
|
77
|
+
"status": status,
|
|
78
|
+
"phase": phase,
|
|
79
|
+
"step_index": step_index,
|
|
80
|
+
"total_steps": total_steps,
|
|
81
|
+
"message": message,
|
|
82
|
+
}
|
|
83
|
+
if request_id:
|
|
84
|
+
payload["request_id"] = request_id
|
|
85
|
+
if details:
|
|
86
|
+
payload["details"] = details
|
|
87
|
+
|
|
88
|
+
future = asyncio.run_coroutine_threadsafe(handler.send_response(payload), loop)
|
|
89
|
+
future.add_done_callback(
|
|
90
|
+
lambda fut: logger.warning(
|
|
91
|
+
"Failed to emit progress event for %s: %s", step_name, fut.exception()
|
|
92
|
+
)
|
|
93
|
+
if fut.exception()
|
|
94
|
+
else None
|
|
95
|
+
)
|
|
41
96
|
|
|
42
97
|
|
|
43
98
|
def _call_subprocess(cmd: List[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
|
|
@@ -239,12 +294,185 @@ def _ensure_containers_dir() -> None:
|
|
|
239
294
|
CONTAINERS_DIR.mkdir(parents=True, exist_ok=True)
|
|
240
295
|
|
|
241
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
|
+
def _refresh_container_statuses(records: List[Dict[str, Any]], config: Dict[str, Any] | None) -> None:
|
|
368
|
+
if not records or not config:
|
|
369
|
+
return
|
|
370
|
+
try:
|
|
371
|
+
proxmox = _connect_proxmox(config)
|
|
372
|
+
node = _get_node_from_config(config)
|
|
373
|
+
statuses = {
|
|
374
|
+
str(ct.get("vmid")): (ct.get("status") or "unknown").lower()
|
|
375
|
+
for ct in proxmox.nodes(node).lxc.get()
|
|
376
|
+
}
|
|
377
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
378
|
+
logger.debug("Failed to refresh container statuses: %s", exc)
|
|
379
|
+
return
|
|
380
|
+
for record in records:
|
|
381
|
+
vmid = record.get("vmid")
|
|
382
|
+
if vmid is None:
|
|
383
|
+
continue
|
|
384
|
+
try:
|
|
385
|
+
vmid_key = str(int(vmid))
|
|
386
|
+
except (ValueError, TypeError):
|
|
387
|
+
continue
|
|
388
|
+
status = statuses.get(vmid_key)
|
|
389
|
+
if status:
|
|
390
|
+
record["status"] = status
|
|
391
|
+
|
|
392
|
+
now = time.monotonic()
|
|
393
|
+
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
394
|
+
cache_ts = _MANAGED_CONTAINERS_CACHE["timestamp"]
|
|
395
|
+
cached = _MANAGED_CONTAINERS_CACHE["summary"]
|
|
396
|
+
if not force and cached and now - cache_ts < _MANAGED_CONTAINERS_CACHE_TTL_S:
|
|
397
|
+
return cached
|
|
398
|
+
config = _load_config()
|
|
399
|
+
records = _load_managed_container_records()
|
|
400
|
+
_refresh_container_statuses(records, config)
|
|
401
|
+
summary = _build_managed_containers_summary(records)
|
|
402
|
+
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
403
|
+
_MANAGED_CONTAINERS_CACHE["timestamp"] = now
|
|
404
|
+
_MANAGED_CONTAINERS_CACHE["summary"] = summary
|
|
405
|
+
return summary
|
|
406
|
+
|
|
407
|
+
|
|
242
408
|
def _format_rootfs(storage: str, disk_gib: int, storage_type: str) -> str:
|
|
243
409
|
if storage_type in ("lvm", "lvmthin"):
|
|
244
410
|
return f"{storage}:{disk_gib}"
|
|
245
411
|
return f"{storage}:{disk_gib}G"
|
|
246
412
|
|
|
247
413
|
|
|
414
|
+
def _get_provisioning_user_info(message: Dict[str, Any]) -> Tuple[str, str, str]:
|
|
415
|
+
user = (message.get("username") or "svcuser").strip() if message else "svcuser"
|
|
416
|
+
user = user or "svcuser"
|
|
417
|
+
password = message.get("password")
|
|
418
|
+
if not password:
|
|
419
|
+
password = secrets.token_urlsafe(10)
|
|
420
|
+
ssh_key = (message.get("ssh_key") or "").strip() if message else ""
|
|
421
|
+
return user, password, ssh_key
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _friendly_step_label(step_name: str) -> str:
|
|
425
|
+
if not step_name:
|
|
426
|
+
return "Step"
|
|
427
|
+
normalized = step_name.replace("_", " ").strip()
|
|
428
|
+
return normalized.capitalize()
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _build_bootstrap_steps(user: str, password: str, ssh_key: str) -> List[Dict[str, Any]]:
|
|
432
|
+
steps = [
|
|
433
|
+
{
|
|
434
|
+
"name": "apt_update",
|
|
435
|
+
"cmd": "apt-get update -y",
|
|
436
|
+
"retries": 4,
|
|
437
|
+
"retry_delay_s": 5,
|
|
438
|
+
"retry_on": [
|
|
439
|
+
"Temporary failure resolving",
|
|
440
|
+
"Could not resolve",
|
|
441
|
+
"Failed to fetch",
|
|
442
|
+
],
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
"name": "install_deps",
|
|
446
|
+
"cmd": "apt-get install -y python3 python3-pip sudo --fix-missing",
|
|
447
|
+
"retries": 5,
|
|
448
|
+
"retry_delay_s": 5,
|
|
449
|
+
"retry_on": [
|
|
450
|
+
"lock-frontend",
|
|
451
|
+
"Unable to acquire the dpkg frontend lock",
|
|
452
|
+
"Temporary failure resolving",
|
|
453
|
+
"Could not resolve",
|
|
454
|
+
"Failed to fetch",
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
{"name": "user_exists", "cmd": f"id -u {user} >/dev/null 2>&1 || adduser --disabled-password --gecos '' {user}", "retries": 0},
|
|
458
|
+
{"name": "add_sudo", "cmd": f"usermod -aG sudo {user}", "retries": 0},
|
|
459
|
+
]
|
|
460
|
+
if password:
|
|
461
|
+
steps.append({"name": "set_password", "cmd": f"echo '{user}:{password}' | chpasswd", "retries": 0})
|
|
462
|
+
if ssh_key:
|
|
463
|
+
steps.append({
|
|
464
|
+
"name": "add_ssh_key",
|
|
465
|
+
"cmd": f"install -d -m 700 /home/{user}/.ssh && echo '{ssh_key}' >> /home/{user}/.ssh/authorized_keys && chown -R {user}:{user} /home/{user}/.ssh",
|
|
466
|
+
"retries": 0,
|
|
467
|
+
})
|
|
468
|
+
steps.extend([
|
|
469
|
+
{"name": "pip_upgrade", "cmd": "python3 -m pip install --upgrade pip", "retries": 0},
|
|
470
|
+
{"name": "install_portacode", "cmd": "python3 -m pip install --upgrade portacode", "retries": 0},
|
|
471
|
+
{"name": "portacode_connect", "type": "portacode_connect", "timeout_s": 30},
|
|
472
|
+
])
|
|
473
|
+
return steps
|
|
474
|
+
|
|
475
|
+
|
|
248
476
|
def _get_storage_type(storages: Iterable[Dict[str, Any]], storage_name: str) -> str:
|
|
249
477
|
for entry in storages:
|
|
250
478
|
if entry.get("storage") == storage_name:
|
|
@@ -252,14 +480,14 @@ def _get_storage_type(storages: Iterable[Dict[str, Any]], storage_name: str) ->
|
|
|
252
480
|
return ""
|
|
253
481
|
|
|
254
482
|
|
|
255
|
-
def
|
|
483
|
+
def _validate_positive_number(value: Any, default: float) -> float:
|
|
256
484
|
try:
|
|
257
|
-
candidate =
|
|
485
|
+
candidate = float(value)
|
|
258
486
|
if candidate > 0:
|
|
259
487
|
return candidate
|
|
260
488
|
except Exception:
|
|
261
489
|
pass
|
|
262
|
-
return default
|
|
490
|
+
return float(default)
|
|
263
491
|
|
|
264
492
|
|
|
265
493
|
def _wait_for_task(proxmox: Any, node: str, upid: str) -> Tuple[Dict[str, Any], float]:
|
|
@@ -311,10 +539,44 @@ def _start_container(proxmox: Any, node: str, vmid: int) -> Tuple[Dict[str, Any]
|
|
|
311
539
|
return _wait_for_task(proxmox, node, upid)
|
|
312
540
|
|
|
313
541
|
|
|
542
|
+
def _stop_container(proxmox: Any, node: str, vmid: int) -> Tuple[Dict[str, Any], float]:
|
|
543
|
+
status = proxmox.nodes(node).lxc(vmid).status.current.get()
|
|
544
|
+
if status.get("status") != "running":
|
|
545
|
+
return status, 0.0
|
|
546
|
+
upid = proxmox.nodes(node).lxc(vmid).status.stop.post()
|
|
547
|
+
return _wait_for_task(proxmox, node, upid)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _delete_container(proxmox: Any, node: str, vmid: int) -> Tuple[Dict[str, Any], float]:
|
|
551
|
+
upid = proxmox.nodes(node).lxc(vmid).delete()
|
|
552
|
+
return _wait_for_task(proxmox, node, upid)
|
|
553
|
+
|
|
554
|
+
|
|
314
555
|
def _write_container_record(vmid: int, payload: Dict[str, Any]) -> None:
|
|
315
556
|
_ensure_containers_dir()
|
|
316
557
|
path = CONTAINERS_DIR / f"ct-{vmid}.json"
|
|
317
558
|
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
559
|
+
_invalidate_managed_containers_cache()
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _read_container_record(vmid: int) -> Dict[str, Any]:
|
|
563
|
+
path = CONTAINERS_DIR / f"ct-{vmid}.json"
|
|
564
|
+
if not path.exists():
|
|
565
|
+
raise FileNotFoundError(f"Container record {path} missing")
|
|
566
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _update_container_record(vmid: int, updates: Dict[str, Any]) -> None:
|
|
570
|
+
record = _read_container_record(vmid)
|
|
571
|
+
record.update(updates)
|
|
572
|
+
_write_container_record(vmid, record)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _remove_container_record(vmid: int) -> None:
|
|
576
|
+
path = CONTAINERS_DIR / f"ct-{vmid}.json"
|
|
577
|
+
if path.exists():
|
|
578
|
+
path.unlink()
|
|
579
|
+
_invalidate_managed_containers_cache()
|
|
318
580
|
|
|
319
581
|
|
|
320
582
|
def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -326,16 +588,14 @@ def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) ->
|
|
|
326
588
|
|
|
327
589
|
bridge = config.get("network", {}).get("bridge", DEFAULT_BRIDGE)
|
|
328
590
|
hostname = (message.get("hostname") or "").strip()
|
|
329
|
-
disk_gib =
|
|
330
|
-
ram_mib =
|
|
331
|
-
cpus =
|
|
591
|
+
disk_gib = int(max(round(_validate_positive_number(message.get("disk_gib") or message.get("disk"), 3)), 1))
|
|
592
|
+
ram_mib = int(max(round(_validate_positive_number(message.get("ram_mib") or message.get("ram"), 2048)), 1))
|
|
593
|
+
cpus = _validate_positive_number(message.get("cpus"), 1)
|
|
332
594
|
storage = message.get("storage") or config.get("default_storage") or ""
|
|
333
595
|
if not storage:
|
|
334
596
|
raise ValueError("Storage pool could not be determined.")
|
|
335
597
|
|
|
336
|
-
user = (message
|
|
337
|
-
password = message.get("password") or ""
|
|
338
|
-
ssh_key = (message.get("ssh_key") or "").strip()
|
|
598
|
+
user, password, ssh_key = _get_provisioning_user_info(message)
|
|
339
599
|
|
|
340
600
|
payload = {
|
|
341
601
|
"template": template,
|
|
@@ -355,6 +615,38 @@ def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) ->
|
|
|
355
615
|
return payload
|
|
356
616
|
|
|
357
617
|
|
|
618
|
+
def _ensure_infra_configured() -> Dict[str, Any]:
|
|
619
|
+
config = _load_config()
|
|
620
|
+
if not config or not config.get("token_value"):
|
|
621
|
+
raise RuntimeError("Proxmox infrastructure is not configured.")
|
|
622
|
+
return config
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _get_node_from_config(config: Dict[str, Any]) -> str:
|
|
626
|
+
return config.get("node") or DEFAULT_NODE_NAME
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _parse_ctid(message: Dict[str, Any]) -> int:
|
|
630
|
+
for key in ("ctid", "vmid"):
|
|
631
|
+
value = message.get(key)
|
|
632
|
+
if value is not None:
|
|
633
|
+
try:
|
|
634
|
+
return int(str(value).strip())
|
|
635
|
+
except ValueError:
|
|
636
|
+
raise ValueError(f"{key} must be an integer") from None
|
|
637
|
+
raise ValueError("ctid is required")
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _ensure_container_managed(
|
|
641
|
+
proxmox: Any, node: str, vmid: int
|
|
642
|
+
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
643
|
+
record = _read_container_record(vmid)
|
|
644
|
+
ct_cfg = proxmox.nodes(node).lxc(str(vmid)).config.get()
|
|
645
|
+
if not ct_cfg or MANAGED_MARKER not in (ct_cfg.get("description") or ""):
|
|
646
|
+
raise RuntimeError(f"Container {vmid} is not managed by Portacode.")
|
|
647
|
+
return record, ct_cfg
|
|
648
|
+
|
|
649
|
+
|
|
358
650
|
def _connect_proxmox(config: Dict[str, Any]) -> Any:
|
|
359
651
|
ProxmoxAPI = _ensure_proxmoxer()
|
|
360
652
|
return ProxmoxAPI(
|
|
@@ -367,10 +659,10 @@ def _connect_proxmox(config: Dict[str, Any]) -> Any:
|
|
|
367
659
|
)
|
|
368
660
|
|
|
369
661
|
|
|
370
|
-
def _run_pct(vmid: int, cmd: str) -> Dict[str, Any]:
|
|
662
|
+
def _run_pct(vmid: int, cmd: str, input_text: Optional[str] = None) -> Dict[str, Any]:
|
|
371
663
|
full = ["pct", "exec", str(vmid), "--", "bash", "-lc", cmd]
|
|
372
664
|
start = time.time()
|
|
373
|
-
proc = subprocess.run(full, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
665
|
+
proc = subprocess.run(full, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, input=input_text)
|
|
374
666
|
return {
|
|
375
667
|
"cmd": cmd,
|
|
376
668
|
"returncode": proc.returncode,
|
|
@@ -471,18 +763,36 @@ def _summarize_error(res: Dict[str, Any]) -> str:
|
|
|
471
763
|
return "Command failed; see stdout/stderr for details."
|
|
472
764
|
|
|
473
765
|
|
|
474
|
-
def _run_setup_steps(
|
|
766
|
+
def _run_setup_steps(
|
|
767
|
+
vmid: int,
|
|
768
|
+
steps: List[Dict[str, Any]],
|
|
769
|
+
user: str,
|
|
770
|
+
progress_callback: Optional[ProgressCallback] = None,
|
|
771
|
+
start_index: int = 1,
|
|
772
|
+
total_steps: Optional[int] = None,
|
|
773
|
+
) -> Tuple[List[Dict[str, Any]], bool]:
|
|
475
774
|
results: List[Dict[str, Any]] = []
|
|
476
|
-
|
|
775
|
+
computed_total = total_steps if total_steps is not None else start_index + len(steps) - 1
|
|
776
|
+
for offset, step in enumerate(steps):
|
|
777
|
+
step_index = start_index + offset
|
|
778
|
+
if progress_callback:
|
|
779
|
+
progress_callback(step_index, computed_total, step, "in_progress", None)
|
|
780
|
+
|
|
477
781
|
if step.get("type") == "portacode_connect":
|
|
478
782
|
res = _portacode_connect_and_read_key(vmid, user, timeout_s=step.get("timeout_s", 10))
|
|
479
783
|
res["name"] = step["name"]
|
|
480
784
|
results.append(res)
|
|
481
785
|
if not res.get("ok"):
|
|
786
|
+
if progress_callback:
|
|
787
|
+
progress_callback(step_index, computed_total, step, "failed", res)
|
|
482
788
|
return results, False
|
|
789
|
+
if progress_callback:
|
|
790
|
+
progress_callback(step_index, computed_total, step, "completed", res)
|
|
483
791
|
continue
|
|
484
792
|
|
|
485
793
|
attempts = 0
|
|
794
|
+
retry_on = step.get("retry_on", [])
|
|
795
|
+
max_attempts = step.get("retries", 0) + 1
|
|
486
796
|
while True:
|
|
487
797
|
attempts += 1
|
|
488
798
|
res = _run_pct(vmid, step["cmd"])
|
|
@@ -492,61 +802,47 @@ def _run_setup_steps(vmid: int, steps: List[Dict[str, Any]], user: str) -> Tuple
|
|
|
492
802
|
res["error_summary"] = _summarize_error(res)
|
|
493
803
|
results.append(res)
|
|
494
804
|
if res["returncode"] == 0:
|
|
805
|
+
if progress_callback:
|
|
806
|
+
progress_callback(step_index, computed_total, step, "completed", res)
|
|
495
807
|
break
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
808
|
+
|
|
809
|
+
will_retry = False
|
|
810
|
+
if attempts < max_attempts and retry_on:
|
|
811
|
+
stderr_stdout = (res.get("stderr", "") + res.get("stdout", ""))
|
|
812
|
+
if any(tok in stderr_stdout for tok in retry_on):
|
|
813
|
+
will_retry = True
|
|
814
|
+
|
|
815
|
+
if progress_callback:
|
|
816
|
+
status = "retrying" if will_retry else "failed"
|
|
817
|
+
progress_callback(step_index, computed_total, step, status, res)
|
|
818
|
+
|
|
819
|
+
if will_retry:
|
|
500
820
|
time.sleep(step.get("retry_delay_s", 3))
|
|
501
821
|
continue
|
|
822
|
+
|
|
502
823
|
return results, False
|
|
503
824
|
return results, True
|
|
504
825
|
|
|
505
826
|
|
|
506
|
-
def _bootstrap_portacode(
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
"lock-frontend",
|
|
526
|
-
"Unable to acquire the dpkg frontend lock",
|
|
527
|
-
"Temporary failure resolving",
|
|
528
|
-
"Could not resolve",
|
|
529
|
-
"Failed to fetch",
|
|
530
|
-
],
|
|
531
|
-
},
|
|
532
|
-
{"name": "user_exists", "cmd": f"id -u {user} >/dev/null 2>&1 || adduser --disabled-password --gecos '' {user}", "retries": 0},
|
|
533
|
-
{"name": "add_sudo", "cmd": f"usermod -aG sudo {user}", "retries": 0},
|
|
534
|
-
]
|
|
535
|
-
if password:
|
|
536
|
-
steps.append({"name": "set_password", "cmd": f"echo '{user}:{password}' | chpasswd", "retries": 0})
|
|
537
|
-
if ssh_key:
|
|
538
|
-
steps.append({
|
|
539
|
-
"name": "add_ssh_key",
|
|
540
|
-
"cmd": f"install -d -m 700 /home/{user}/.ssh && echo '{ssh_key}' >> /home/{user}/.ssh/authorized_keys && chown -R {user}:{user} /home/{user}/.ssh",
|
|
541
|
-
"retries": 0,
|
|
542
|
-
})
|
|
543
|
-
steps.extend([
|
|
544
|
-
{"name": "pip_upgrade", "cmd": "python3 -m pip install --upgrade pip", "retries": 0},
|
|
545
|
-
{"name": "install_portacode", "cmd": "python3 -m pip install --upgrade portacode", "retries": 0},
|
|
546
|
-
{"name": "portacode_connect", "type": "portacode_connect", "timeout_s": 30},
|
|
547
|
-
])
|
|
548
|
-
|
|
549
|
-
results, ok = _run_setup_steps(vmid, steps, user)
|
|
827
|
+
def _bootstrap_portacode(
|
|
828
|
+
vmid: int,
|
|
829
|
+
user: str,
|
|
830
|
+
password: str,
|
|
831
|
+
ssh_key: str,
|
|
832
|
+
steps: Optional[List[Dict[str, Any]]] = None,
|
|
833
|
+
progress_callback: Optional[ProgressCallback] = None,
|
|
834
|
+
start_index: int = 1,
|
|
835
|
+
total_steps: Optional[int] = None,
|
|
836
|
+
) -> Tuple[str, List[Dict[str, Any]]]:
|
|
837
|
+
actual_steps = steps if steps is not None else _build_bootstrap_steps(user, password, ssh_key)
|
|
838
|
+
results, ok = _run_setup_steps(
|
|
839
|
+
vmid,
|
|
840
|
+
actual_steps,
|
|
841
|
+
user,
|
|
842
|
+
progress_callback=progress_callback,
|
|
843
|
+
start_index=start_index,
|
|
844
|
+
total_steps=total_steps,
|
|
845
|
+
)
|
|
550
846
|
if not ok:
|
|
551
847
|
raise RuntimeError("Portacode bootstrap steps failed.")
|
|
552
848
|
key_step = next((entry for entry in results if entry.get("name") == "portacode_connect"), None)
|
|
@@ -626,6 +922,7 @@ def configure_infrastructure(token_identifier: str, token_value: str, verify_ssl
|
|
|
626
922
|
_save_config(config)
|
|
627
923
|
snapshot = build_snapshot(config)
|
|
628
924
|
snapshot["node_status"] = status
|
|
925
|
+
snapshot["managed_containers"] = _get_managed_containers_summary(force=True)
|
|
629
926
|
return snapshot
|
|
630
927
|
|
|
631
928
|
|
|
@@ -634,6 +931,7 @@ def get_infra_snapshot() -> Dict[str, Any]:
|
|
|
634
931
|
snapshot = build_snapshot(config)
|
|
635
932
|
if config.get("node_status"):
|
|
636
933
|
snapshot["node_status"] = config["node_status"]
|
|
934
|
+
snapshot["managed_containers"] = _get_managed_containers_summary()
|
|
637
935
|
return snapshot
|
|
638
936
|
|
|
639
937
|
|
|
@@ -646,6 +944,7 @@ def revert_infrastructure() -> Dict[str, Any]:
|
|
|
646
944
|
snapshot["network"]["applied"] = False
|
|
647
945
|
snapshot["network"]["message"] = "Reverted to previous network state"
|
|
648
946
|
snapshot["network"]["bridge"] = DEFAULT_BRIDGE
|
|
947
|
+
snapshot["managed_containers"] = _get_managed_containers_summary(force=True)
|
|
649
948
|
return snapshot
|
|
650
949
|
|
|
651
950
|
|
|
@@ -692,49 +991,171 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
692
991
|
|
|
693
992
|
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
694
993
|
logger.info("create_proxmox_container command received")
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
994
|
+
request_id = message.get("request_id")
|
|
995
|
+
bootstrap_user, bootstrap_password, bootstrap_ssh_key = _get_provisioning_user_info(message)
|
|
996
|
+
bootstrap_steps = _build_bootstrap_steps(bootstrap_user, bootstrap_password, bootstrap_ssh_key)
|
|
997
|
+
total_steps = 3 + len(bootstrap_steps) + 2
|
|
998
|
+
current_step_index = 1
|
|
999
|
+
|
|
1000
|
+
def _run_lifecycle_step(
|
|
1001
|
+
step_name: str,
|
|
1002
|
+
step_label: str,
|
|
1003
|
+
start_message: str,
|
|
1004
|
+
success_message: str,
|
|
1005
|
+
action,
|
|
1006
|
+
):
|
|
1007
|
+
nonlocal current_step_index
|
|
1008
|
+
step_index = current_step_index
|
|
1009
|
+
_emit_progress_event(self,
|
|
1010
|
+
step_index=step_index,
|
|
1011
|
+
total_steps=total_steps,
|
|
1012
|
+
step_name=step_name,
|
|
1013
|
+
step_label=step_label,
|
|
1014
|
+
status="in_progress",
|
|
1015
|
+
message=start_message,
|
|
1016
|
+
phase="lifecycle",
|
|
1017
|
+
request_id=request_id,
|
|
1018
|
+
)
|
|
1019
|
+
try:
|
|
1020
|
+
result = action()
|
|
1021
|
+
except Exception as exc:
|
|
1022
|
+
_emit_progress_event(
|
|
1023
|
+
self,
|
|
1024
|
+
step_index=step_index,
|
|
1025
|
+
total_steps=total_steps,
|
|
1026
|
+
step_name=step_name,
|
|
1027
|
+
step_label=step_label,
|
|
1028
|
+
status="failed",
|
|
1029
|
+
message=f"{step_label} failed: {exc}",
|
|
1030
|
+
phase="lifecycle",
|
|
1031
|
+
request_id=request_id,
|
|
1032
|
+
details={"error": str(exc)},
|
|
1033
|
+
)
|
|
1034
|
+
raise
|
|
1035
|
+
_emit_progress_event(
|
|
1036
|
+
self,
|
|
1037
|
+
step_index=step_index,
|
|
1038
|
+
total_steps=total_steps,
|
|
1039
|
+
step_name=step_name,
|
|
1040
|
+
step_label=step_label,
|
|
1041
|
+
status="completed",
|
|
1042
|
+
message=success_message,
|
|
1043
|
+
phase="lifecycle",
|
|
1044
|
+
request_id=request_id,
|
|
1045
|
+
)
|
|
1046
|
+
current_step_index += 1
|
|
1047
|
+
return result
|
|
1048
|
+
|
|
1049
|
+
def _validate_environment():
|
|
1050
|
+
if os.geteuid() != 0:
|
|
1051
|
+
raise PermissionError("Container creation requires root privileges.")
|
|
1052
|
+
config = _load_config()
|
|
1053
|
+
if not config or not config.get("token_value"):
|
|
1054
|
+
raise ValueError("Proxmox infrastructure is not configured.")
|
|
1055
|
+
if not config.get("network", {}).get("applied"):
|
|
1056
|
+
raise RuntimeError("Proxmox bridge setup must be applied before creating containers.")
|
|
1057
|
+
return config
|
|
1058
|
+
|
|
1059
|
+
config = _run_lifecycle_step(
|
|
1060
|
+
"validate_environment",
|
|
1061
|
+
"Validating infrastructure",
|
|
1062
|
+
"Checking token, permissions, and bridge setup…",
|
|
1063
|
+
"Infrastructure validated.",
|
|
1064
|
+
_validate_environment,
|
|
720
1065
|
)
|
|
721
1066
|
|
|
722
|
-
|
|
1067
|
+
def _create_container():
|
|
1068
|
+
proxmox = _connect_proxmox(config)
|
|
1069
|
+
node = config.get("node") or DEFAULT_NODE_NAME
|
|
1070
|
+
payload = _build_container_payload(message, config)
|
|
1071
|
+
payload["cpuunits"] = max(int(payload["cpus"] * 1024), 10)
|
|
1072
|
+
payload["memory"] = int(payload["ram_mib"])
|
|
1073
|
+
payload["node"] = node
|
|
1074
|
+
logger.debug(
|
|
1075
|
+
"Provisioning container node=%s template=%s ram=%s cpu=%s storage=%s",
|
|
1076
|
+
node,
|
|
1077
|
+
payload["template"],
|
|
1078
|
+
payload["ram_mib"],
|
|
1079
|
+
payload["cpus"],
|
|
1080
|
+
payload["storage"],
|
|
1081
|
+
)
|
|
723
1082
|
vmid, _ = _instantiate_container(proxmox, node, payload)
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1083
|
+
payload["vmid"] = vmid
|
|
1084
|
+
payload["created_at"] = datetime.utcnow().isoformat() + "Z"
|
|
1085
|
+
payload["status"] = "creating"
|
|
1086
|
+
_write_container_record(vmid, payload)
|
|
1087
|
+
return proxmox, node, vmid, payload
|
|
1088
|
+
|
|
1089
|
+
proxmox, node, vmid, payload = _run_lifecycle_step(
|
|
1090
|
+
"create_container",
|
|
1091
|
+
"Creating container",
|
|
1092
|
+
"Provisioning the LXC container…",
|
|
1093
|
+
"Container created.",
|
|
1094
|
+
_create_container,
|
|
1095
|
+
)
|
|
731
1096
|
|
|
732
|
-
|
|
1097
|
+
def _start_container_step():
|
|
733
1098
|
_start_container(proxmox, node, vmid)
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1099
|
+
|
|
1100
|
+
_run_lifecycle_step(
|
|
1101
|
+
"start_container",
|
|
1102
|
+
"Starting container",
|
|
1103
|
+
"Booting the container…",
|
|
1104
|
+
"Container startup completed.",
|
|
1105
|
+
_start_container_step,
|
|
1106
|
+
)
|
|
1107
|
+
_update_container_record(vmid, {"status": "running"})
|
|
1108
|
+
|
|
1109
|
+
def _bootstrap_progress_callback(
|
|
1110
|
+
step_index: int,
|
|
1111
|
+
total: int,
|
|
1112
|
+
step: Dict[str, Any],
|
|
1113
|
+
status: str,
|
|
1114
|
+
result: Optional[Dict[str, Any]],
|
|
1115
|
+
):
|
|
1116
|
+
label = step.get("display_name") or _friendly_step_label(step.get("name", "bootstrap"))
|
|
1117
|
+
error_summary = (result or {}).get("error_summary") or (result or {}).get("error")
|
|
1118
|
+
attempt = (result or {}).get("attempt")
|
|
1119
|
+
if status == "in_progress":
|
|
1120
|
+
message_text = f"{label} is running…"
|
|
1121
|
+
elif status == "completed":
|
|
1122
|
+
message_text = f"{label} completed."
|
|
1123
|
+
elif status == "retrying":
|
|
1124
|
+
attempt_desc = f" (attempt {attempt})" if attempt else ""
|
|
1125
|
+
message_text = f"{label} failed{attempt_desc}; retrying…"
|
|
1126
|
+
else:
|
|
1127
|
+
message_text = f"{label} failed"
|
|
1128
|
+
if error_summary:
|
|
1129
|
+
message_text += f": {error_summary}"
|
|
1130
|
+
details: Dict[str, Any] = {}
|
|
1131
|
+
if attempt:
|
|
1132
|
+
details["attempt"] = attempt
|
|
1133
|
+
if error_summary:
|
|
1134
|
+
details["error_summary"] = error_summary
|
|
1135
|
+
_emit_progress_event(
|
|
1136
|
+
self,
|
|
1137
|
+
step_index=step_index,
|
|
1138
|
+
total_steps=total,
|
|
1139
|
+
step_name=step.get("name", "bootstrap"),
|
|
1140
|
+
step_label=label,
|
|
1141
|
+
status=status,
|
|
1142
|
+
message=message_text,
|
|
1143
|
+
phase="bootstrap",
|
|
1144
|
+
request_id=request_id,
|
|
1145
|
+
details=details or None,
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
public_key, steps = _bootstrap_portacode(
|
|
1149
|
+
vmid,
|
|
1150
|
+
payload["username"],
|
|
1151
|
+
payload["password"],
|
|
1152
|
+
payload["ssh_public_key"],
|
|
1153
|
+
steps=bootstrap_steps,
|
|
1154
|
+
progress_callback=_bootstrap_progress_callback,
|
|
1155
|
+
start_index=current_step_index,
|
|
1156
|
+
total_steps=total_steps,
|
|
1157
|
+
)
|
|
1158
|
+
current_step_index += len(bootstrap_steps)
|
|
738
1159
|
|
|
739
1160
|
return {
|
|
740
1161
|
"event": "proxmox_container_created",
|
|
@@ -755,6 +1176,212 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
755
1176
|
}
|
|
756
1177
|
|
|
757
1178
|
|
|
1179
|
+
class StartPortacodeServiceHandler(SyncHandler):
|
|
1180
|
+
"""Start the Portacode service inside a newly created container."""
|
|
1181
|
+
|
|
1182
|
+
@property
|
|
1183
|
+
def command_name(self) -> str:
|
|
1184
|
+
return "start_portacode_service"
|
|
1185
|
+
|
|
1186
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1187
|
+
ctid = message.get("ctid")
|
|
1188
|
+
if not ctid:
|
|
1189
|
+
raise ValueError("ctid is required")
|
|
1190
|
+
try:
|
|
1191
|
+
vmid = int(ctid)
|
|
1192
|
+
except ValueError:
|
|
1193
|
+
raise ValueError("ctid must be an integer")
|
|
1194
|
+
|
|
1195
|
+
record = _read_container_record(vmid)
|
|
1196
|
+
user = record.get("username")
|
|
1197
|
+
password = record.get("password")
|
|
1198
|
+
if not user or not password:
|
|
1199
|
+
raise RuntimeError("Container credentials unavailable")
|
|
1200
|
+
|
|
1201
|
+
start_index = int(message.get("step_index", 1))
|
|
1202
|
+
total_steps = int(message.get("total_steps", start_index + 2))
|
|
1203
|
+
request_id = message.get("request_id")
|
|
1204
|
+
|
|
1205
|
+
auth_step_name = "setup_device_authentication"
|
|
1206
|
+
auth_label = "Setting up device authentication"
|
|
1207
|
+
_emit_progress_event(
|
|
1208
|
+
self,
|
|
1209
|
+
step_index=start_index,
|
|
1210
|
+
total_steps=total_steps,
|
|
1211
|
+
step_name=auth_step_name,
|
|
1212
|
+
step_label=auth_label,
|
|
1213
|
+
status="in_progress",
|
|
1214
|
+
message="Notifying the server of the new device…",
|
|
1215
|
+
phase="service",
|
|
1216
|
+
request_id=request_id,
|
|
1217
|
+
)
|
|
1218
|
+
_emit_progress_event(
|
|
1219
|
+
self,
|
|
1220
|
+
step_index=start_index,
|
|
1221
|
+
total_steps=total_steps,
|
|
1222
|
+
step_name=auth_step_name,
|
|
1223
|
+
step_label=auth_label,
|
|
1224
|
+
status="completed",
|
|
1225
|
+
message="Authentication metadata recorded.",
|
|
1226
|
+
phase="service",
|
|
1227
|
+
request_id=request_id,
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
install_step = start_index + 1
|
|
1231
|
+
install_label = "Launching Portacode service"
|
|
1232
|
+
_emit_progress_event(
|
|
1233
|
+
self,
|
|
1234
|
+
step_index=install_step,
|
|
1235
|
+
total_steps=total_steps,
|
|
1236
|
+
step_name="launch_portacode_service",
|
|
1237
|
+
step_label=install_label,
|
|
1238
|
+
status="in_progress",
|
|
1239
|
+
message="Running sudo portacode service install…",
|
|
1240
|
+
phase="service",
|
|
1241
|
+
request_id=request_id,
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
cmd = f"su - {user} -c 'sudo -S portacode service install'"
|
|
1245
|
+
res = _run_pct(vmid, cmd, input_text=password + "\n")
|
|
1246
|
+
|
|
1247
|
+
if res["returncode"] != 0:
|
|
1248
|
+
_emit_progress_event(
|
|
1249
|
+
self,
|
|
1250
|
+
step_index=install_step,
|
|
1251
|
+
total_steps=total_steps,
|
|
1252
|
+
step_name="launch_portacode_service",
|
|
1253
|
+
step_label=install_label,
|
|
1254
|
+
status="failed",
|
|
1255
|
+
message=f"{install_label} failed: {res.get('stderr') or res.get('stdout')}",
|
|
1256
|
+
phase="service",
|
|
1257
|
+
request_id=request_id,
|
|
1258
|
+
details={
|
|
1259
|
+
"stderr": res.get("stderr"),
|
|
1260
|
+
"stdout": res.get("stdout"),
|
|
1261
|
+
},
|
|
1262
|
+
)
|
|
1263
|
+
raise RuntimeError(res.get("stderr") or res.get("stdout") or "Service install failed")
|
|
1264
|
+
|
|
1265
|
+
_emit_progress_event(
|
|
1266
|
+
self,
|
|
1267
|
+
step_index=install_step,
|
|
1268
|
+
total_steps=total_steps,
|
|
1269
|
+
step_name="launch_portacode_service",
|
|
1270
|
+
step_label=install_label,
|
|
1271
|
+
status="completed",
|
|
1272
|
+
message="Portacode service install finished.",
|
|
1273
|
+
phase="service",
|
|
1274
|
+
request_id=request_id,
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
return {
|
|
1278
|
+
"event": "proxmox_service_started",
|
|
1279
|
+
"success": True,
|
|
1280
|
+
"message": "Portacode service install completed",
|
|
1281
|
+
"ctid": str(vmid),
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
class StartProxmoxContainerHandler(SyncHandler):
|
|
1286
|
+
"""Start a managed container via the Proxmox API."""
|
|
1287
|
+
|
|
1288
|
+
@property
|
|
1289
|
+
def command_name(self) -> str:
|
|
1290
|
+
return "start_proxmox_container"
|
|
1291
|
+
|
|
1292
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1293
|
+
vmid = _parse_ctid(message)
|
|
1294
|
+
config = _ensure_infra_configured()
|
|
1295
|
+
proxmox = _connect_proxmox(config)
|
|
1296
|
+
node = _get_node_from_config(config)
|
|
1297
|
+
_ensure_container_managed(proxmox, node, vmid)
|
|
1298
|
+
|
|
1299
|
+
status, elapsed = _start_container(proxmox, node, vmid)
|
|
1300
|
+
_update_container_record(vmid, {"status": "running"})
|
|
1301
|
+
|
|
1302
|
+
infra = get_infra_snapshot()
|
|
1303
|
+
return {
|
|
1304
|
+
"event": "proxmox_container_action",
|
|
1305
|
+
"action": "start",
|
|
1306
|
+
"success": True,
|
|
1307
|
+
"ctid": str(vmid),
|
|
1308
|
+
"message": f"Started container {vmid} in {elapsed:.1f}s.",
|
|
1309
|
+
"details": {"exitstatus": status.get("exitstatus")},
|
|
1310
|
+
"status": status.get("status"),
|
|
1311
|
+
"infra": infra,
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
class StopProxmoxContainerHandler(SyncHandler):
|
|
1316
|
+
"""Stop a managed container via the Proxmox API."""
|
|
1317
|
+
|
|
1318
|
+
@property
|
|
1319
|
+
def command_name(self) -> str:
|
|
1320
|
+
return "stop_proxmox_container"
|
|
1321
|
+
|
|
1322
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1323
|
+
vmid = _parse_ctid(message)
|
|
1324
|
+
config = _ensure_infra_configured()
|
|
1325
|
+
proxmox = _connect_proxmox(config)
|
|
1326
|
+
node = _get_node_from_config(config)
|
|
1327
|
+
_ensure_container_managed(proxmox, node, vmid)
|
|
1328
|
+
|
|
1329
|
+
status, elapsed = _stop_container(proxmox, node, vmid)
|
|
1330
|
+
final_status = status.get("status") or "stopped"
|
|
1331
|
+
_update_container_record(vmid, {"status": final_status})
|
|
1332
|
+
|
|
1333
|
+
infra = get_infra_snapshot()
|
|
1334
|
+
message_text = (
|
|
1335
|
+
f"Container {vmid} is already stopped."
|
|
1336
|
+
if final_status != "running" and elapsed == 0.0
|
|
1337
|
+
else f"Stopped container {vmid} in {elapsed:.1f}s."
|
|
1338
|
+
)
|
|
1339
|
+
return {
|
|
1340
|
+
"event": "proxmox_container_action",
|
|
1341
|
+
"action": "stop",
|
|
1342
|
+
"success": True,
|
|
1343
|
+
"ctid": str(vmid),
|
|
1344
|
+
"message": message_text,
|
|
1345
|
+
"details": {"exitstatus": status.get("exitstatus")},
|
|
1346
|
+
"status": final_status,
|
|
1347
|
+
"infra": infra,
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
class RemoveProxmoxContainerHandler(SyncHandler):
|
|
1352
|
+
"""Delete a managed container via the Proxmox API."""
|
|
1353
|
+
|
|
1354
|
+
@property
|
|
1355
|
+
def command_name(self) -> str:
|
|
1356
|
+
return "remove_proxmox_container"
|
|
1357
|
+
|
|
1358
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1359
|
+
vmid = _parse_ctid(message)
|
|
1360
|
+
config = _ensure_infra_configured()
|
|
1361
|
+
proxmox = _connect_proxmox(config)
|
|
1362
|
+
node = _get_node_from_config(config)
|
|
1363
|
+
_ensure_container_managed(proxmox, node, vmid)
|
|
1364
|
+
|
|
1365
|
+
stop_status, stop_elapsed = _stop_container(proxmox, node, vmid)
|
|
1366
|
+
delete_status, delete_elapsed = _delete_container(proxmox, node, vmid)
|
|
1367
|
+
_remove_container_record(vmid)
|
|
1368
|
+
|
|
1369
|
+
infra = get_infra_snapshot()
|
|
1370
|
+
return {
|
|
1371
|
+
"event": "proxmox_container_action",
|
|
1372
|
+
"action": "remove",
|
|
1373
|
+
"success": True,
|
|
1374
|
+
"ctid": str(vmid),
|
|
1375
|
+
"message": f"Deleted container {vmid} in {delete_elapsed:.1f}s.",
|
|
1376
|
+
"details": {
|
|
1377
|
+
"stop_exitstatus": stop_status.get("exitstatus"),
|
|
1378
|
+
"delete_exitstatus": delete_status.get("exitstatus"),
|
|
1379
|
+
},
|
|
1380
|
+
"status": "deleted",
|
|
1381
|
+
"infra": infra,
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
|
|
758
1385
|
class ConfigureProxmoxInfraHandler(SyncHandler):
|
|
759
1386
|
@property
|
|
760
1387
|
def command_name(self) -> str:
|
portacode/connection/terminal.py
CHANGED
|
@@ -55,6 +55,7 @@ from .handlers import (
|
|
|
55
55
|
ConfigureProxmoxInfraHandler,
|
|
56
56
|
CreateProxmoxContainerHandler,
|
|
57
57
|
RevertProxmoxInfraHandler,
|
|
58
|
+
StartPortacodeServiceHandler,
|
|
58
59
|
)
|
|
59
60
|
from .handlers.project_aware_file_handlers import (
|
|
60
61
|
ProjectAwareFileWriteHandler,
|
|
@@ -414,6 +415,7 @@ class TerminalManager:
|
|
|
414
415
|
"mux": mux,
|
|
415
416
|
"use_content_caching": True, # Enable content caching optimization
|
|
416
417
|
"debug": self.debug,
|
|
418
|
+
"event_loop": asyncio.get_running_loop(),
|
|
417
419
|
}
|
|
418
420
|
|
|
419
421
|
# Initialize command registry
|
|
@@ -477,6 +479,7 @@ class TerminalManager:
|
|
|
477
479
|
# System management handlers
|
|
478
480
|
self._command_registry.register(ConfigureProxmoxInfraHandler)
|
|
479
481
|
self._command_registry.register(CreateProxmoxContainerHandler)
|
|
482
|
+
self._command_registry.register(StartPortacodeServiceHandler)
|
|
480
483
|
self._command_registry.register(RevertProxmoxInfraHandler)
|
|
481
484
|
self._command_registry.register(UpdatePortacodeHandler)
|
|
482
485
|
|
|
@@ -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=KBZ1gEZmvt1J4XR1FpwaCleg3ayMUckV-yZYhnXlSCU,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
|
|
@@ -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=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=Sd25UGM7edWDTIiatZvA2Dh3-4NOzfIDmixG0Fz5m0U,51343
|
|
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.dev1.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.dev1.dist-info/METADATA,sha256=o7psDA6l4eoRJLFQipCS3XcgsqTLxNMf39xhh6qYJ00,13051
|
|
94
|
+
portacode-1.4.12.dev1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
95
|
+
portacode-1.4.12.dev1.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
96
|
+
portacode-1.4.12.dev1.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
97
|
+
portacode-1.4.12.dev1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|