portacode 1.4.18.dev1__py3-none-any.whl → 1.4.19__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 +4 -3
- portacode/connection/handlers/base.py +26 -5
- portacode/connection/handlers/proxmox_infra.py +179 -13
- portacode/connection/handlers/registry.py +16 -3
- {portacode-1.4.18.dev1.dist-info → portacode-1.4.19.dist-info}/METADATA +1 -1
- {portacode-1.4.18.dev1.dist-info → portacode-1.4.19.dist-info}/RECORD +11 -11
- {portacode-1.4.18.dev1.dist-info → portacode-1.4.19.dist-info}/WHEEL +0 -0
- {portacode-1.4.18.dev1.dist-info → portacode-1.4.19.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.18.dev1.dist-info → portacode-1.4.19.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.18.dev1.dist-info → portacode-1.4.19.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.19'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 19)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -380,7 +380,7 @@ Starts a previously provisioned, Portacode-managed LXC container. Handled by [`S
|
|
|
380
380
|
|
|
381
381
|
**Payload Fields:**
|
|
382
382
|
|
|
383
|
-
* `ctid` (string,
|
|
383
|
+
* `ctid` (string, optional): Identifier of the container to start. If omitted, the handler resolves the CTID from the managed container records using `child_device_id`.
|
|
384
384
|
* `child_device_id` (string, required): Dashboard `Device.id` of the container that triggered the request; the handler validates the CT belongs to that device before issuing the start.
|
|
385
385
|
|
|
386
386
|
**Responses:**
|
|
@@ -394,7 +394,7 @@ Stops a running Portacode-managed container. Handled by [`StopProxmoxContainerHa
|
|
|
394
394
|
|
|
395
395
|
**Payload Fields:**
|
|
396
396
|
|
|
397
|
-
* `ctid` (string,
|
|
397
|
+
* `ctid` (string, optional): Identifier of the container to stop. If omitted, the handler resolves the CTID from managed container records and, if needed, by scanning managed container descriptions for the matching `device_id` marker.
|
|
398
398
|
* `child_device_id` (string, required): Dashboard `Device.id` that owns the container; the handler rejects the request if the CT is mapped to another device.
|
|
399
399
|
|
|
400
400
|
**Responses:**
|
|
@@ -408,12 +408,13 @@ Deletes a managed container from Proxmox (stopping it first if necessary) and re
|
|
|
408
408
|
|
|
409
409
|
**Payload Fields:**
|
|
410
410
|
|
|
411
|
-
* `ctid` (string,
|
|
411
|
+
* `ctid` (string, optional): Identifier of the container to delete. If omitted, the handler resolves the CTID from managed container records and, if needed, by scanning managed container descriptions for the matching `device_id` marker.
|
|
412
412
|
* `child_device_id` (string, required): Dashboard `Device.id` that should own the container metadata being purged.
|
|
413
413
|
|
|
414
414
|
**Responses:**
|
|
415
415
|
|
|
416
416
|
* Emits a [`proxmox_container_action`](#proxmox_container_action-event) event with `action="remove"` and the refreshed infra snapshot after deletion.
|
|
417
|
+
* If no managed container matches the requested `child_device_id`, emits a `proxmox_container_action` event with `success=false`, `status="not_found"`, and a human-readable message (no error event).
|
|
417
418
|
* Emits an [`error`](#error) event on failure.
|
|
418
419
|
|
|
419
420
|
### `proxmox_container_created`
|
|
@@ -62,16 +62,18 @@ class BaseHandler(ABC):
|
|
|
62
62
|
# Get client session manager from context
|
|
63
63
|
client_session_manager = self.context.get("client_session_manager")
|
|
64
64
|
|
|
65
|
+
bypass_session_gate = bool(payload.get("bypass_session_gate"))
|
|
65
66
|
if client_session_manager and client_session_manager.has_interested_clients():
|
|
66
67
|
# Get target sessions
|
|
67
68
|
target_sessions = client_session_manager.get_target_sessions(project_id)
|
|
68
|
-
if not target_sessions:
|
|
69
|
+
if not target_sessions and not bypass_session_gate:
|
|
69
70
|
logger.debug("handler: No target sessions found, skipping response send")
|
|
70
71
|
return
|
|
71
72
|
|
|
72
73
|
# Add session targeting information
|
|
73
74
|
enhanced_payload = dict(payload)
|
|
74
|
-
|
|
75
|
+
if target_sessions:
|
|
76
|
+
enhanced_payload["client_sessions"] = target_sessions
|
|
75
77
|
|
|
76
78
|
# Add backward compatibility reply_channel (first session if not provided)
|
|
77
79
|
if not reply_channel:
|
|
@@ -89,15 +91,24 @@ class BaseHandler(ABC):
|
|
|
89
91
|
payload["reply_channel"] = reply_channel
|
|
90
92
|
await self.control_channel.send(payload)
|
|
91
93
|
|
|
92
|
-
async def send_error(
|
|
94
|
+
async def send_error(
|
|
95
|
+
self,
|
|
96
|
+
message: str,
|
|
97
|
+
reply_channel: Optional[str] = None,
|
|
98
|
+
project_id: str = None,
|
|
99
|
+
request_id: Optional[str] = None,
|
|
100
|
+
) -> None:
|
|
93
101
|
"""Send an error response with client session awareness.
|
|
94
102
|
|
|
95
103
|
Args:
|
|
96
104
|
message: Error message
|
|
97
105
|
reply_channel: Optional reply channel for backward compatibility
|
|
98
106
|
project_id: Optional project filter for targeting specific sessions
|
|
107
|
+
request_id: Optional request_id to correlate error with a request
|
|
99
108
|
"""
|
|
100
109
|
payload = {"event": "error", "message": message}
|
|
110
|
+
if request_id:
|
|
111
|
+
payload["request_id"] = request_id
|
|
101
112
|
await self.send_response(payload, reply_channel, project_id)
|
|
102
113
|
|
|
103
114
|
|
|
@@ -163,7 +174,12 @@ class AsyncHandler(BaseHandler):
|
|
|
163
174
|
logger.exception("handler: Error in async handler %s: %s", self.command_name, exc)
|
|
164
175
|
# Extract project_id from original message for error targeting
|
|
165
176
|
project_id = message.get("project_id")
|
|
166
|
-
await self.send_error(
|
|
177
|
+
await self.send_error(
|
|
178
|
+
str(exc),
|
|
179
|
+
reply_channel,
|
|
180
|
+
project_id,
|
|
181
|
+
request_id=message.get("request_id"),
|
|
182
|
+
)
|
|
167
183
|
|
|
168
184
|
|
|
169
185
|
class SyncHandler(BaseHandler):
|
|
@@ -216,4 +232,9 @@ class SyncHandler(BaseHandler):
|
|
|
216
232
|
logger.exception("Error in sync handler %s: %s", self.command_name, exc)
|
|
217
233
|
# Extract project_id from original message for error targeting
|
|
218
234
|
project_id = message.get("project_id")
|
|
219
|
-
await self.send_error(
|
|
235
|
+
await self.send_error(
|
|
236
|
+
str(exc),
|
|
237
|
+
reply_channel,
|
|
238
|
+
project_id,
|
|
239
|
+
request_id=message.get("request_id"),
|
|
240
|
+
)
|
|
@@ -17,6 +17,7 @@ import sys
|
|
|
17
17
|
import tempfile
|
|
18
18
|
import time
|
|
19
19
|
import threading
|
|
20
|
+
import random
|
|
20
21
|
from datetime import datetime, timezone
|
|
21
22
|
from pathlib import Path
|
|
22
23
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
@@ -33,6 +34,44 @@ REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
|
33
34
|
NET_SETUP_SCRIPT = REPO_ROOT / "proxmox_management" / "net_setup.py"
|
|
34
35
|
CONTAINERS_DIR = CONFIG_DIR / "containers"
|
|
35
36
|
MANAGED_MARKER = "portacode-managed:true"
|
|
37
|
+
DEVICE_ID_MARKER = "device_id="
|
|
38
|
+
PROVISIONING_ID_MARKER = "provisioning_id="
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _sanitize_description_value(value: Any) -> str:
|
|
42
|
+
return str(value).strip().replace(";", "_")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _build_managed_description(
|
|
46
|
+
base: Optional[str],
|
|
47
|
+
*,
|
|
48
|
+
device_id: Optional[str] = None,
|
|
49
|
+
provisioning_id: Optional[str] = None,
|
|
50
|
+
) -> str:
|
|
51
|
+
parts = [part for part in (base or "").split(";") if part]
|
|
52
|
+
if MANAGED_MARKER not in parts:
|
|
53
|
+
parts.insert(0, MANAGED_MARKER)
|
|
54
|
+
if device_id:
|
|
55
|
+
device_id_value = _sanitize_description_value(device_id)
|
|
56
|
+
if not any(part.startswith(DEVICE_ID_MARKER) for part in parts):
|
|
57
|
+
parts.append(f"{DEVICE_ID_MARKER}{device_id_value}")
|
|
58
|
+
if provisioning_id:
|
|
59
|
+
provisioning_value = _sanitize_description_value(provisioning_id)
|
|
60
|
+
if not any(part.startswith(PROVISIONING_ID_MARKER) for part in parts):
|
|
61
|
+
parts.append(f"{PROVISIONING_ID_MARKER}{provisioning_value}")
|
|
62
|
+
return ";".join(parts)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _extract_marker_value(description: str, prefix: str) -> Optional[str]:
|
|
66
|
+
for part in description.split(";"):
|
|
67
|
+
part = part.strip()
|
|
68
|
+
if part.startswith(prefix):
|
|
69
|
+
return part[len(prefix) :].strip()
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _parse_device_id_from_description(description: str) -> Optional[str]:
|
|
74
|
+
return _extract_marker_value(description, DEVICE_ID_MARKER)
|
|
36
75
|
|
|
37
76
|
DEFAULT_HOST = "localhost"
|
|
38
77
|
DEFAULT_NODE_NAME = os.uname().nodename.split(".", 1)[0]
|
|
@@ -116,6 +155,7 @@ def _emit_progress_event(
|
|
|
116
155
|
payload["details"] = details
|
|
117
156
|
if on_behalf_of_device:
|
|
118
157
|
payload["on_behalf_of_device"] = str(on_behalf_of_device)
|
|
158
|
+
payload["bypass_session_gate"] = True
|
|
119
159
|
|
|
120
160
|
future = asyncio.run_coroutine_threadsafe(handler.send_response(payload), loop)
|
|
121
161
|
future.add_done_callback(
|
|
@@ -1256,12 +1296,21 @@ def _validate_positive_number(value: Any, default: float) -> float:
|
|
|
1256
1296
|
|
|
1257
1297
|
|
|
1258
1298
|
def _wait_for_task(proxmox: Any, node: str, upid: str) -> Tuple[Dict[str, Any], float]:
|
|
1299
|
+
from proxmoxer.core import ResourceException
|
|
1259
1300
|
start = time.time()
|
|
1301
|
+
poll = 2.0
|
|
1260
1302
|
while True:
|
|
1261
|
-
|
|
1303
|
+
try:
|
|
1304
|
+
status = proxmox.nodes(node).tasks(upid).status.get()
|
|
1305
|
+
except ResourceException as e:
|
|
1306
|
+
if str(e).startswith("599 "):
|
|
1307
|
+
# transient proxy glitch; task may still be running
|
|
1308
|
+
time.sleep(poll + random.random() * 0.5) # small jitter
|
|
1309
|
+
continue
|
|
1310
|
+
raise
|
|
1262
1311
|
if status.get("status") == "stopped":
|
|
1263
1312
|
return status, time.time() - start
|
|
1264
|
-
time.sleep(
|
|
1313
|
+
time.sleep(poll)
|
|
1265
1314
|
|
|
1266
1315
|
|
|
1267
1316
|
def _list_running_managed(proxmox: Any, node: str) -> List[Tuple[str, Dict[str, Any]]]:
|
|
@@ -1399,18 +1448,97 @@ def _parse_ctid(message: Dict[str, Any]) -> int:
|
|
|
1399
1448
|
raise ValueError("ctid is required")
|
|
1400
1449
|
|
|
1401
1450
|
|
|
1451
|
+
class _DeviceLookupError(ValueError):
|
|
1452
|
+
pass
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
def _resolve_vmid_for_device(device_id: str) -> int:
|
|
1456
|
+
_initialize_managed_containers_state()
|
|
1457
|
+
records = list(_MANAGED_CONTAINERS_STATE.get("records", {}).values())
|
|
1458
|
+
for record in records:
|
|
1459
|
+
record_device_id = record.get("device_id")
|
|
1460
|
+
if record_device_id is None:
|
|
1461
|
+
continue
|
|
1462
|
+
if str(record_device_id) != str(device_id):
|
|
1463
|
+
continue
|
|
1464
|
+
vmid = record.get("vmid")
|
|
1465
|
+
if vmid is None:
|
|
1466
|
+
continue
|
|
1467
|
+
try:
|
|
1468
|
+
return int(str(vmid).strip())
|
|
1469
|
+
except ValueError:
|
|
1470
|
+
raise ValueError("ctid must be an integer") from None
|
|
1471
|
+
raise _DeviceLookupError(
|
|
1472
|
+
f"No managed container record found for device_id {device_id!r}."
|
|
1473
|
+
)
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
def _resolve_vmid_for_device_in_proxmox(
|
|
1477
|
+
proxmox: Any, node: str, device_id: str
|
|
1478
|
+
) -> int:
|
|
1479
|
+
device_id_value = str(device_id).strip()
|
|
1480
|
+
matches: List[int] = []
|
|
1481
|
+
try:
|
|
1482
|
+
containers = proxmox.nodes(node).lxc.get()
|
|
1483
|
+
except Exception as exc: # pragma: no cover - proxmox failure
|
|
1484
|
+
raise _DeviceLookupError(
|
|
1485
|
+
f"Unable to query Proxmox containers for device_id {device_id_value!r}: {exc}"
|
|
1486
|
+
) from exc
|
|
1487
|
+
for entry in containers or []:
|
|
1488
|
+
vmid = entry.get("vmid")
|
|
1489
|
+
if vmid is None:
|
|
1490
|
+
continue
|
|
1491
|
+
try:
|
|
1492
|
+
cfg = proxmox.nodes(node).lxc(str(vmid)).config.get()
|
|
1493
|
+
except Exception:
|
|
1494
|
+
continue
|
|
1495
|
+
description = (cfg or {}).get("description") or ""
|
|
1496
|
+
if MANAGED_MARKER not in description:
|
|
1497
|
+
continue
|
|
1498
|
+
desc_device_id = _parse_device_id_from_description(description)
|
|
1499
|
+
if desc_device_id is None:
|
|
1500
|
+
continue
|
|
1501
|
+
if str(desc_device_id) != device_id_value:
|
|
1502
|
+
continue
|
|
1503
|
+
matches.append(int(str(vmid).strip()))
|
|
1504
|
+
if not matches:
|
|
1505
|
+
raise _DeviceLookupError(
|
|
1506
|
+
f"No managed container found for device_id {device_id_value!r}. It may already be deleted."
|
|
1507
|
+
)
|
|
1508
|
+
if len(matches) > 1:
|
|
1509
|
+
raise _DeviceLookupError(
|
|
1510
|
+
f"Multiple managed containers found for device_id {device_id_value!r}: {matches}."
|
|
1511
|
+
)
|
|
1512
|
+
return matches[0]
|
|
1513
|
+
|
|
1514
|
+
|
|
1402
1515
|
def _ensure_container_managed(
|
|
1403
1516
|
proxmox: Any, node: str, vmid: int, *, device_id: Optional[str] = None
|
|
1404
1517
|
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
1405
|
-
|
|
1518
|
+
try:
|
|
1519
|
+
record = _read_container_record(vmid)
|
|
1520
|
+
except FileNotFoundError:
|
|
1521
|
+
record = {}
|
|
1406
1522
|
ct_cfg = proxmox.nodes(node).lxc(str(vmid)).config.get()
|
|
1407
|
-
|
|
1523
|
+
description = (ct_cfg or {}).get("description") or ""
|
|
1524
|
+
if not ct_cfg or MANAGED_MARKER not in description:
|
|
1408
1525
|
raise RuntimeError(f"Container {vmid} is not managed by Portacode.")
|
|
1409
1526
|
record_device_id = record.get("device_id")
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
)
|
|
1527
|
+
description_device_id = _parse_device_id_from_description(description)
|
|
1528
|
+
if device_id:
|
|
1529
|
+
device_id_value = str(device_id)
|
|
1530
|
+
if description_device_id and str(description_device_id) != device_id_value:
|
|
1531
|
+
raise RuntimeError(
|
|
1532
|
+
f"Container {vmid} is managed for device {description_device_id!r}, not {device_id_value!r}."
|
|
1533
|
+
)
|
|
1534
|
+
if record_device_id and str(record_device_id) != device_id_value:
|
|
1535
|
+
raise RuntimeError(
|
|
1536
|
+
f"Container {vmid} is managed for device {record_device_id!r}, not {device_id_value!r}."
|
|
1537
|
+
)
|
|
1538
|
+
if not description_device_id and not record_device_id:
|
|
1539
|
+
raise RuntimeError(
|
|
1540
|
+
f"Container {vmid} is missing a device_id marker; refusing to operate without ctid."
|
|
1541
|
+
)
|
|
1414
1542
|
return record, ct_cfg
|
|
1415
1543
|
|
|
1416
1544
|
|
|
@@ -2095,7 +2223,11 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
2095
2223
|
payload, device_id=device_id, request_id=request_id
|
|
2096
2224
|
)
|
|
2097
2225
|
provisioning_id = secrets.token_hex(6)
|
|
2098
|
-
payload["description"] =
|
|
2226
|
+
payload["description"] = _build_managed_description(
|
|
2227
|
+
payload.get("description"),
|
|
2228
|
+
device_id=device_id,
|
|
2229
|
+
provisioning_id=provisioning_id,
|
|
2230
|
+
)
|
|
2099
2231
|
|
|
2100
2232
|
def _provision_background() -> None:
|
|
2101
2233
|
nonlocal current_step_index
|
|
@@ -2331,6 +2463,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
2331
2463
|
"setup_steps": steps,
|
|
2332
2464
|
"device_id": device_id,
|
|
2333
2465
|
"on_behalf_of_device": device_id,
|
|
2466
|
+
"bypass_session_gate": True,
|
|
2334
2467
|
"service_installed": service_installed,
|
|
2335
2468
|
"request_id": request_id,
|
|
2336
2469
|
},
|
|
@@ -2357,7 +2490,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
2357
2490
|
"event": "proxmox_container_accepted",
|
|
2358
2491
|
"success": True,
|
|
2359
2492
|
"message": "Provisioning accepted; resources reserved.",
|
|
2360
|
-
"device_id": device_id,
|
|
2361
2493
|
"request_id": request_id,
|
|
2362
2494
|
}
|
|
2363
2495
|
|
|
@@ -2485,13 +2617,19 @@ class StartProxmoxContainerHandler(SyncHandler):
|
|
|
2485
2617
|
return "start_proxmox_container"
|
|
2486
2618
|
|
|
2487
2619
|
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
2488
|
-
vmid = _parse_ctid(message)
|
|
2489
2620
|
child_device_id = (message.get("child_device_id") or "").strip()
|
|
2490
2621
|
if not child_device_id:
|
|
2491
2622
|
raise ValueError("child_device_id is required for start_proxmox_container")
|
|
2492
2623
|
config = _ensure_infra_configured()
|
|
2493
2624
|
proxmox = _connect_proxmox(config)
|
|
2494
2625
|
node = _get_node_from_config(config)
|
|
2626
|
+
try:
|
|
2627
|
+
vmid = _parse_ctid(message)
|
|
2628
|
+
except ValueError:
|
|
2629
|
+
try:
|
|
2630
|
+
vmid = _resolve_vmid_for_device(child_device_id)
|
|
2631
|
+
except _DeviceLookupError:
|
|
2632
|
+
vmid = _resolve_vmid_for_device_in_proxmox(proxmox, node, child_device_id)
|
|
2495
2633
|
_ensure_container_managed(proxmox, node, vmid, device_id=child_device_id)
|
|
2496
2634
|
|
|
2497
2635
|
status, elapsed = _start_container(proxmox, node, vmid)
|
|
@@ -2518,13 +2656,19 @@ class StopProxmoxContainerHandler(SyncHandler):
|
|
|
2518
2656
|
return "stop_proxmox_container"
|
|
2519
2657
|
|
|
2520
2658
|
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
2521
|
-
vmid = _parse_ctid(message)
|
|
2522
2659
|
child_device_id = (message.get("child_device_id") or "").strip()
|
|
2523
2660
|
if not child_device_id:
|
|
2524
2661
|
raise ValueError("child_device_id is required for stop_proxmox_container")
|
|
2525
2662
|
config = _ensure_infra_configured()
|
|
2526
2663
|
proxmox = _connect_proxmox(config)
|
|
2527
2664
|
node = _get_node_from_config(config)
|
|
2665
|
+
try:
|
|
2666
|
+
vmid = _parse_ctid(message)
|
|
2667
|
+
except ValueError:
|
|
2668
|
+
try:
|
|
2669
|
+
vmid = _resolve_vmid_for_device(child_device_id)
|
|
2670
|
+
except _DeviceLookupError:
|
|
2671
|
+
vmid = _resolve_vmid_for_device_in_proxmox(proxmox, node, child_device_id)
|
|
2528
2672
|
_ensure_container_managed(proxmox, node, vmid, device_id=child_device_id)
|
|
2529
2673
|
|
|
2530
2674
|
status, elapsed = _stop_container(proxmox, node, vmid)
|
|
@@ -2557,13 +2701,33 @@ class RemoveProxmoxContainerHandler(SyncHandler):
|
|
|
2557
2701
|
return "remove_proxmox_container"
|
|
2558
2702
|
|
|
2559
2703
|
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
2560
|
-
vmid = _parse_ctid(message)
|
|
2561
2704
|
child_device_id = (message.get("child_device_id") or "").strip()
|
|
2562
2705
|
if not child_device_id:
|
|
2563
2706
|
raise ValueError("child_device_id is required for remove_proxmox_container")
|
|
2707
|
+
request_id = message.get("request_id")
|
|
2564
2708
|
config = _ensure_infra_configured()
|
|
2565
2709
|
proxmox = _connect_proxmox(config)
|
|
2566
2710
|
node = _get_node_from_config(config)
|
|
2711
|
+
try:
|
|
2712
|
+
vmid = _parse_ctid(message)
|
|
2713
|
+
except ValueError:
|
|
2714
|
+
try:
|
|
2715
|
+
vmid = _resolve_vmid_for_device(child_device_id)
|
|
2716
|
+
except _DeviceLookupError:
|
|
2717
|
+
try:
|
|
2718
|
+
vmid = _resolve_vmid_for_device_in_proxmox(proxmox, node, child_device_id)
|
|
2719
|
+
except _DeviceLookupError as exc:
|
|
2720
|
+
infra = get_infra_snapshot()
|
|
2721
|
+
return {
|
|
2722
|
+
"event": "proxmox_container_action",
|
|
2723
|
+
"action": "remove",
|
|
2724
|
+
"success": False,
|
|
2725
|
+
"message": str(exc),
|
|
2726
|
+
"status": "not_found",
|
|
2727
|
+
"child_device_id": child_device_id,
|
|
2728
|
+
"request_id": request_id,
|
|
2729
|
+
"infra": infra,
|
|
2730
|
+
}
|
|
2567
2731
|
_ensure_container_managed(proxmox, node, vmid, device_id=child_device_id)
|
|
2568
2732
|
|
|
2569
2733
|
stop_status, stop_elapsed = _stop_container(proxmox, node, vmid)
|
|
@@ -2582,6 +2746,8 @@ class RemoveProxmoxContainerHandler(SyncHandler):
|
|
|
2582
2746
|
"delete_exitstatus": delete_status.get("exitstatus"),
|
|
2583
2747
|
},
|
|
2584
2748
|
"status": "deleted",
|
|
2749
|
+
"child_device_id": child_device_id,
|
|
2750
|
+
"request_id": request_id,
|
|
2585
2751
|
"infra": infra,
|
|
2586
2752
|
}
|
|
2587
2753
|
|
|
@@ -106,12 +106,25 @@ class CommandRegistry:
|
|
|
106
106
|
except Exception as exc:
|
|
107
107
|
logger.exception("registry: Error dispatching command %s: %s", command_name, exc)
|
|
108
108
|
# Send session-aware error response
|
|
109
|
-
await self._send_session_aware_error(
|
|
109
|
+
await self._send_session_aware_error(
|
|
110
|
+
str(exc),
|
|
111
|
+
reply_channel,
|
|
112
|
+
message.get("project_id"),
|
|
113
|
+
request_id=message.get("request_id"),
|
|
114
|
+
)
|
|
110
115
|
return False
|
|
111
116
|
|
|
112
|
-
async def _send_session_aware_error(
|
|
117
|
+
async def _send_session_aware_error(
|
|
118
|
+
self,
|
|
119
|
+
message: str,
|
|
120
|
+
reply_channel: Optional[str] = None,
|
|
121
|
+
project_id: str = None,
|
|
122
|
+
request_id: Optional[str] = None,
|
|
123
|
+
) -> None:
|
|
113
124
|
"""Send an error response with client session awareness."""
|
|
114
125
|
error_payload = {"event": "error", "message": message}
|
|
126
|
+
if request_id:
|
|
127
|
+
error_payload["request_id"] = request_id
|
|
115
128
|
|
|
116
129
|
# Get client session manager from context
|
|
117
130
|
client_session_manager = self.context.get("client_session_manager")
|
|
@@ -151,4 +164,4 @@ class CommandRegistry:
|
|
|
151
164
|
|
|
152
165
|
# Update context for all existing handlers
|
|
153
166
|
for handler in self._handlers.values():
|
|
154
|
-
handler.context = self.context
|
|
167
|
+
handler.context = self.context
|
|
@@ -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=keZQZ9CsYVpzJllmOkOL3XJxArYmisEYXuC-FCqDD4o,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
|
|
@@ -14,16 +14,16 @@ 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=n1Uu92JacV5K6d1Qwx94Tw9OB2Tpke5HqsW2NDn76Ls,49032
|
|
16
16
|
portacode/connection/handlers/README.md,sha256=HsLZG1QK1JNm67HsgL6WoDg9nxzKXxwkc5fJPFJdX5g,12169
|
|
17
|
-
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=
|
|
17
|
+
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=_h44VpS1P_7AK-4Yx9aE62G1vptkFhXoHmwvH5PAFvA,104266
|
|
18
18
|
portacode/connection/handlers/__init__.py,sha256=WSeBmi65GWFQPYt9M3E10rn0uZ_EPCJzNJOzSf2HZyw,2921
|
|
19
|
-
portacode/connection/handlers/base.py,sha256=
|
|
19
|
+
portacode/connection/handlers/base.py,sha256=tMsKJmnNhXA3a4YZA55a0WO2e8-4Uh4BsG9lWAr3Ris,10829
|
|
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=
|
|
26
|
-
portacode/connection/handlers/registry.py,sha256=
|
|
25
|
+
portacode/connection/handlers/proxmox_infra.py,sha256=Era8MwTKj6RrNBHWd8uk4lE379DaD2fLU0zJr2061jM,106048
|
|
26
|
+
portacode/connection/handlers/registry.py,sha256=elISoWpIXapMuY28x1hzXV-uErtAklsFzMb8yVgp7NU,6456
|
|
27
27
|
portacode/connection/handlers/session.py,sha256=uNGfiO_1B9-_yjJKkpvmbiJhIl6b-UXlT86UTfd6WYE,42219
|
|
28
28
|
portacode/connection/handlers/system_handlers.py,sha256=fr12QpOr_Z8KYGUU-AYrTQwRPAcrLK85hvj3SEq1Kw8,14757
|
|
29
29
|
portacode/connection/handlers/tab_factory.py,sha256=yn93h6GASjD1VpvW1oqpax3EpoT0r7r97zFXxML1wdA,16173
|
|
@@ -65,7 +65,7 @@ portacode/utils/__init__.py,sha256=NgBlWTuNJESfIYJzP_3adI1yJQJR0XJLRpSdVNaBAN0,3
|
|
|
65
65
|
portacode/utils/diff_apply.py,sha256=4Oi7ft3VUCKmiUE4VM-OeqO7Gk6H7PF3WnN4WHXtjxI,15157
|
|
66
66
|
portacode/utils/diff_renderer.py,sha256=S76StnQ2DLfsz4Gg0m07UwPfRp8270PuzbNaQq-rmYk,13850
|
|
67
67
|
portacode/utils/ntp_clock.py,sha256=VqCnWCTehCufE43W23oB-WUdAZGeCcLxkmIOPwInYHc,2499
|
|
68
|
-
portacode-1.4.
|
|
68
|
+
portacode-1.4.19.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
|
|
69
69
|
test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
|
|
70
70
|
test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
|
|
71
71
|
test_modules/test_device_online.py,sha256=QtYq0Dq9vME8Gp2O4fGSheqVf8LUtpsSKosXXk56gGM,1654
|
|
@@ -91,8 +91,8 @@ testing_framework/core/playwright_manager.py,sha256=Tw46qwxIhOFkS48C2IWIQHHNpEe-
|
|
|
91
91
|
testing_framework/core/runner.py,sha256=j2QwNJmAxVBmJvcbVS7DgPJUKPNzqfLmt_4NNdaKmZU,19297
|
|
92
92
|
testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
|
|
93
93
|
testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
|
|
94
|
-
portacode-1.4.
|
|
95
|
-
portacode-1.4.
|
|
96
|
-
portacode-1.4.
|
|
97
|
-
portacode-1.4.
|
|
98
|
-
portacode-1.4.
|
|
94
|
+
portacode-1.4.19.dist-info/METADATA,sha256=5aVx15tKibAZEqBLxn8FzTID82COP9Im2PEX3XYgSaY,13046
|
|
95
|
+
portacode-1.4.19.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
96
|
+
portacode-1.4.19.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
97
|
+
portacode-1.4.19.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
98
|
+
portacode-1.4.19.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|