portacode 1.4.11.dev4__py3-none-any.whl → 1.4.11.dev6__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 +32 -0
- portacode/connection/handlers/proxmox_infra.py +304 -72
- portacode/connection/terminal.py +1 -0
- {portacode-1.4.11.dev4.dist-info → portacode-1.4.11.dev6.dist-info}/METADATA +1 -1
- {portacode-1.4.11.dev4.dist-info → portacode-1.4.11.dev6.dist-info}/RECORD +10 -10
- {portacode-1.4.11.dev4.dist-info → portacode-1.4.11.dev6.dist-info}/WHEEL +0 -0
- {portacode-1.4.11.dev4.dist-info → portacode-1.4.11.dev6.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.11.dev4.dist-info → portacode-1.4.11.dev6.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.11.dev4.dist-info → portacode-1.4.11.dev6.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.11.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 4, 11, '
|
|
31
|
+
__version__ = version = '1.4.11.dev6'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 11, 'dev6')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -380,6 +380,22 @@ Emitted after a successful `create_proxmox_container` action. Contains the new c
|
|
|
380
380
|
* `container` (object): Metadata such as `vmid`, `hostname`, `template`, `storage`, `disk_gib`, `ram_mib`, and `cpus`.
|
|
381
381
|
* `setup_steps` (array[object]): Detailed bootstrap step results (name, stdout/stderr, elapsed time, and status).
|
|
382
382
|
|
|
383
|
+
### `proxmox_container_progress`
|
|
384
|
+
|
|
385
|
+
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.
|
|
386
|
+
|
|
387
|
+
**Event Fields:**
|
|
388
|
+
|
|
389
|
+
* `step_index` (integer): 1-based index of the current step inside the entire provisioning sequence.
|
|
390
|
+
* `total_steps` (integer): Total number of steps that must run before provisioning completes.
|
|
391
|
+
* `step_name` (string): Internal step identifier (e.g., `create_container`, `apt_update`, `portacode_connect`).
|
|
392
|
+
* `step_label` (string): Human-friendly label suitable for UI (e.g., `Create container`, `Apt update`).
|
|
393
|
+
* `status` (string): One of `in_progress`, `completed`, or `failed`.
|
|
394
|
+
* `phase` (string): Either `lifecycle` (environment/container lifecycle) or `bootstrap` (per-command bootstrap work).
|
|
395
|
+
* `message` (string): Short description of what is happening or why a failure occurred.
|
|
396
|
+
* `details` (object, optional): Contains `attempt` (if retries were needed) and `error_summary` when a step fails.
|
|
397
|
+
* `request_id` (string, optional): Mirrors the request ID from the incoming `create_proxmox_container` payload when available.
|
|
398
|
+
|
|
383
399
|
### `clock_sync_request`
|
|
384
400
|
|
|
385
401
|
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).
|
|
@@ -1075,6 +1091,22 @@ Emitted after a successful `create_proxmox_container` action to report the newly
|
|
|
1075
1091
|
* `container` (object): Metadata such as `vmid`, `hostname`, `template`, `storage`, `disk_gib`, `ram_mib`, and `cpus`.
|
|
1076
1092
|
* `setup_steps` (array[object]): Detailed bootstrap step reports including stdout/stderr, elapsed time, and pass/fail status.
|
|
1077
1093
|
|
|
1094
|
+
### `proxmox_container_progress`
|
|
1095
|
+
|
|
1096
|
+
Sent continuously while `create_proxmox_container` runs so dashboards can show a progress bar tied to each lifecycle and bootstrap step.
|
|
1097
|
+
|
|
1098
|
+
**Event Fields:**
|
|
1099
|
+
|
|
1100
|
+
* `step_index` (integer): 1-based position of the step inside the entire provisioning workflow.
|
|
1101
|
+
* `total_steps` (integer): Total number of lifecycle and bootstrap steps for the current operation.
|
|
1102
|
+
* `step_name` (string): Internal identifier (e.g., `validate_environment`, `install_deps`, `portacode_connect`).
|
|
1103
|
+
* `step_label` (string): Friendly label suitable for the UI.
|
|
1104
|
+
* `status` (string): One of `in_progress`, `completed`, or `failed`.
|
|
1105
|
+
* `phase` (string): Either `lifecycle` (node validation/container lifecycle) or `bootstrap` (commands run inside the CT).
|
|
1106
|
+
* `message` (string): Short human-readable description of the action or failure.
|
|
1107
|
+
* `details` (object, optional): Contains `attempt` (when retries are used) and `error_summary` on failure.
|
|
1108
|
+
* `request_id` (string, optional): Mirrors the `create_proxmox_container` request when provided.
|
|
1109
|
+
|
|
1078
1110
|
### <a name="clock_sync_response"></a>`clock_sync_response`
|
|
1079
1111
|
|
|
1080
1112
|
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.
|
|
@@ -2,6 +2,7 @@
|
|
|
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
|
|
@@ -12,7 +13,7 @@ import sys
|
|
|
12
13
|
import time
|
|
13
14
|
from datetime import datetime
|
|
14
15
|
from pathlib import Path
|
|
15
|
-
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
16
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
|
|
16
17
|
|
|
17
18
|
import platformdirs
|
|
18
19
|
|
|
@@ -39,6 +40,8 @@ IFACES_PATH = Path("/etc/network/interfaces")
|
|
|
39
40
|
SYSCTL_PATH = Path("/etc/sysctl.d/99-portacode-forward.conf")
|
|
40
41
|
UNIT_DIR = Path("/etc/systemd/system")
|
|
41
42
|
|
|
43
|
+
ProgressCallback = Callable[[int, int, Dict[str, Any], str, Optional[Dict[str, Any]]], None]
|
|
44
|
+
|
|
42
45
|
|
|
43
46
|
def _call_subprocess(cmd: List[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
|
|
44
47
|
env = os.environ.copy()
|
|
@@ -245,6 +248,66 @@ def _format_rootfs(storage: str, disk_gib: int, storage_type: str) -> str:
|
|
|
245
248
|
return f"{storage}:{disk_gib}G"
|
|
246
249
|
|
|
247
250
|
|
|
251
|
+
def _get_provisioning_user_info(message: Dict[str, Any]) -> Tuple[str, str, str]:
|
|
252
|
+
user = (message.get("username") or "svcuser").strip() if message else "svcuser"
|
|
253
|
+
user = user or "svcuser"
|
|
254
|
+
password = message.get("password") or "" if message else ""
|
|
255
|
+
ssh_key = (message.get("ssh_key") or "").strip() if message else ""
|
|
256
|
+
return user, password, ssh_key
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _friendly_step_label(step_name: str) -> str:
|
|
260
|
+
if not step_name:
|
|
261
|
+
return "Step"
|
|
262
|
+
normalized = step_name.replace("_", " ").strip()
|
|
263
|
+
return normalized.capitalize()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _build_bootstrap_steps(user: str, password: str, ssh_key: str) -> List[Dict[str, Any]]:
|
|
267
|
+
steps = [
|
|
268
|
+
{
|
|
269
|
+
"name": "apt_update",
|
|
270
|
+
"cmd": "apt-get update -y",
|
|
271
|
+
"retries": 4,
|
|
272
|
+
"retry_delay_s": 5,
|
|
273
|
+
"retry_on": [
|
|
274
|
+
"Temporary failure resolving",
|
|
275
|
+
"Could not resolve",
|
|
276
|
+
"Failed to fetch",
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"name": "install_deps",
|
|
281
|
+
"cmd": "apt-get install -y python3 python3-pip sudo --fix-missing",
|
|
282
|
+
"retries": 5,
|
|
283
|
+
"retry_delay_s": 5,
|
|
284
|
+
"retry_on": [
|
|
285
|
+
"lock-frontend",
|
|
286
|
+
"Unable to acquire the dpkg frontend lock",
|
|
287
|
+
"Temporary failure resolving",
|
|
288
|
+
"Could not resolve",
|
|
289
|
+
"Failed to fetch",
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
{"name": "user_exists", "cmd": f"id -u {user} >/dev/null 2>&1 || adduser --disabled-password --gecos '' {user}", "retries": 0},
|
|
293
|
+
{"name": "add_sudo", "cmd": f"usermod -aG sudo {user}", "retries": 0},
|
|
294
|
+
]
|
|
295
|
+
if password:
|
|
296
|
+
steps.append({"name": "set_password", "cmd": f"echo '{user}:{password}' | chpasswd", "retries": 0})
|
|
297
|
+
if ssh_key:
|
|
298
|
+
steps.append({
|
|
299
|
+
"name": "add_ssh_key",
|
|
300
|
+
"cmd": f"install -d -m 700 /home/{user}/.ssh && echo '{ssh_key}' >> /home/{user}/.ssh/authorized_keys && chown -R {user}:{user} /home/{user}/.ssh",
|
|
301
|
+
"retries": 0,
|
|
302
|
+
})
|
|
303
|
+
steps.extend([
|
|
304
|
+
{"name": "pip_upgrade", "cmd": "python3 -m pip install --upgrade pip", "retries": 0},
|
|
305
|
+
{"name": "install_portacode", "cmd": "python3 -m pip install --upgrade portacode", "retries": 0},
|
|
306
|
+
{"name": "portacode_connect", "type": "portacode_connect", "timeout_s": 30},
|
|
307
|
+
])
|
|
308
|
+
return steps
|
|
309
|
+
|
|
310
|
+
|
|
248
311
|
def _get_storage_type(storages: Iterable[Dict[str, Any]], storage_name: str) -> str:
|
|
249
312
|
for entry in storages:
|
|
250
313
|
if entry.get("storage") == storage_name:
|
|
@@ -333,9 +396,7 @@ def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) ->
|
|
|
333
396
|
if not storage:
|
|
334
397
|
raise ValueError("Storage pool could not be determined.")
|
|
335
398
|
|
|
336
|
-
user = (message
|
|
337
|
-
password = message.get("password") or ""
|
|
338
|
-
ssh_key = (message.get("ssh_key") or "").strip()
|
|
399
|
+
user, password, ssh_key = _get_provisioning_user_info(message)
|
|
339
400
|
|
|
340
401
|
payload = {
|
|
341
402
|
"template": template,
|
|
@@ -471,15 +532,31 @@ def _summarize_error(res: Dict[str, Any]) -> str:
|
|
|
471
532
|
return "Command failed; see stdout/stderr for details."
|
|
472
533
|
|
|
473
534
|
|
|
474
|
-
def _run_setup_steps(
|
|
535
|
+
def _run_setup_steps(
|
|
536
|
+
vmid: int,
|
|
537
|
+
steps: List[Dict[str, Any]],
|
|
538
|
+
user: str,
|
|
539
|
+
progress_callback: Optional[ProgressCallback] = None,
|
|
540
|
+
start_index: int = 1,
|
|
541
|
+
total_steps: Optional[int] = None,
|
|
542
|
+
) -> Tuple[List[Dict[str, Any]], bool]:
|
|
475
543
|
results: List[Dict[str, Any]] = []
|
|
476
|
-
|
|
544
|
+
computed_total = total_steps if total_steps is not None else start_index + len(steps) - 1
|
|
545
|
+
for offset, step in enumerate(steps):
|
|
546
|
+
step_index = start_index + offset
|
|
547
|
+
if progress_callback:
|
|
548
|
+
progress_callback(step_index, computed_total, step, "in_progress", None)
|
|
549
|
+
|
|
477
550
|
if step.get("type") == "portacode_connect":
|
|
478
551
|
res = _portacode_connect_and_read_key(vmid, user, timeout_s=step.get("timeout_s", 10))
|
|
479
552
|
res["name"] = step["name"]
|
|
480
553
|
results.append(res)
|
|
481
554
|
if not res.get("ok"):
|
|
555
|
+
if progress_callback:
|
|
556
|
+
progress_callback(step_index, computed_total, step, "failed", res)
|
|
482
557
|
return results, False
|
|
558
|
+
if progress_callback:
|
|
559
|
+
progress_callback(step_index, computed_total, step, "completed", res)
|
|
483
560
|
continue
|
|
484
561
|
|
|
485
562
|
attempts = 0
|
|
@@ -492,7 +569,11 @@ def _run_setup_steps(vmid: int, steps: List[Dict[str, Any]], user: str) -> Tuple
|
|
|
492
569
|
res["error_summary"] = _summarize_error(res)
|
|
493
570
|
results.append(res)
|
|
494
571
|
if res["returncode"] == 0:
|
|
572
|
+
if progress_callback:
|
|
573
|
+
progress_callback(step_index, computed_total, step, "completed", res)
|
|
495
574
|
break
|
|
575
|
+
if progress_callback:
|
|
576
|
+
progress_callback(step_index, computed_total, step, "failed", res)
|
|
496
577
|
retry_on = step.get("retry_on", [])
|
|
497
578
|
if attempts >= step.get("retries", 0) + 1:
|
|
498
579
|
return results, False
|
|
@@ -503,50 +584,25 @@ def _run_setup_steps(vmid: int, steps: List[Dict[str, Any]], user: str) -> Tuple
|
|
|
503
584
|
return results, True
|
|
504
585
|
|
|
505
586
|
|
|
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)
|
|
587
|
+
def _bootstrap_portacode(
|
|
588
|
+
vmid: int,
|
|
589
|
+
user: str,
|
|
590
|
+
password: str,
|
|
591
|
+
ssh_key: str,
|
|
592
|
+
steps: Optional[List[Dict[str, Any]]] = None,
|
|
593
|
+
progress_callback: Optional[ProgressCallback] = None,
|
|
594
|
+
start_index: int = 1,
|
|
595
|
+
total_steps: Optional[int] = None,
|
|
596
|
+
) -> Tuple[str, List[Dict[str, Any]]]:
|
|
597
|
+
actual_steps = steps if steps is not None else _build_bootstrap_steps(user, password, ssh_key)
|
|
598
|
+
results, ok = _run_setup_steps(
|
|
599
|
+
vmid,
|
|
600
|
+
actual_steps,
|
|
601
|
+
user,
|
|
602
|
+
progress_callback=progress_callback,
|
|
603
|
+
start_index=start_index,
|
|
604
|
+
total_steps=total_steps,
|
|
605
|
+
)
|
|
550
606
|
if not ok:
|
|
551
607
|
raise RuntimeError("Portacode bootstrap steps failed.")
|
|
552
608
|
key_step = next((entry for entry in results if entry.get("name") == "portacode_connect"), None)
|
|
@@ -690,29 +746,205 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
690
746
|
def command_name(self) -> str:
|
|
691
747
|
return "create_proxmox_container"
|
|
692
748
|
|
|
749
|
+
def _emit_progress_event(
|
|
750
|
+
self,
|
|
751
|
+
*,
|
|
752
|
+
step_index: int,
|
|
753
|
+
total_steps: int,
|
|
754
|
+
step_name: str,
|
|
755
|
+
step_label: str,
|
|
756
|
+
status: str,
|
|
757
|
+
message: str,
|
|
758
|
+
phase: str,
|
|
759
|
+
request_id: Optional[str],
|
|
760
|
+
details: Optional[Dict[str, Any]] = None,
|
|
761
|
+
) -> None:
|
|
762
|
+
loop = self.context.get("event_loop")
|
|
763
|
+
if not loop or loop.is_closed():
|
|
764
|
+
logger.debug(
|
|
765
|
+
"progress event skipped (no event loop) step=%s status=%s",
|
|
766
|
+
step_name,
|
|
767
|
+
status,
|
|
768
|
+
)
|
|
769
|
+
return
|
|
770
|
+
|
|
771
|
+
payload: Dict[str, Any] = {
|
|
772
|
+
"event": "proxmox_container_progress",
|
|
773
|
+
"step_name": step_name,
|
|
774
|
+
"step_label": step_label,
|
|
775
|
+
"status": status,
|
|
776
|
+
"phase": phase,
|
|
777
|
+
"step_index": step_index,
|
|
778
|
+
"total_steps": total_steps,
|
|
779
|
+
"message": message,
|
|
780
|
+
}
|
|
781
|
+
if request_id:
|
|
782
|
+
payload["request_id"] = request_id
|
|
783
|
+
if details:
|
|
784
|
+
payload["details"] = details
|
|
785
|
+
|
|
786
|
+
future = asyncio.run_coroutine_threadsafe(self.send_response(payload), loop)
|
|
787
|
+
future.add_done_callback(
|
|
788
|
+
lambda fut: logger.warning(
|
|
789
|
+
"Failed to emit progress event for %s: %s", step_name, fut.exception()
|
|
790
|
+
)
|
|
791
|
+
if fut.exception()
|
|
792
|
+
else None
|
|
793
|
+
)
|
|
794
|
+
|
|
693
795
|
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
796
|
+
logger.info("create_proxmox_container command received")
|
|
797
|
+
request_id = message.get("request_id")
|
|
798
|
+
bootstrap_user, bootstrap_password, bootstrap_ssh_key = _get_provisioning_user_info(message)
|
|
799
|
+
bootstrap_steps = _build_bootstrap_steps(bootstrap_user, bootstrap_password, bootstrap_ssh_key)
|
|
800
|
+
total_steps = 3 + len(bootstrap_steps)
|
|
801
|
+
current_step_index = 1
|
|
802
|
+
|
|
803
|
+
def _run_lifecycle_step(
|
|
804
|
+
step_name: str,
|
|
805
|
+
step_label: str,
|
|
806
|
+
start_message: str,
|
|
807
|
+
success_message: str,
|
|
808
|
+
action,
|
|
809
|
+
):
|
|
810
|
+
nonlocal current_step_index
|
|
811
|
+
step_index = current_step_index
|
|
812
|
+
self._emit_progress_event(
|
|
813
|
+
step_index=step_index,
|
|
814
|
+
total_steps=total_steps,
|
|
815
|
+
step_name=step_name,
|
|
816
|
+
step_label=step_label,
|
|
817
|
+
status="in_progress",
|
|
818
|
+
message=start_message,
|
|
819
|
+
phase="lifecycle",
|
|
820
|
+
request_id=request_id,
|
|
821
|
+
)
|
|
822
|
+
try:
|
|
823
|
+
result = action()
|
|
824
|
+
except Exception as exc:
|
|
825
|
+
self._emit_progress_event(
|
|
826
|
+
step_index=step_index,
|
|
827
|
+
total_steps=total_steps,
|
|
828
|
+
step_name=step_name,
|
|
829
|
+
step_label=step_label,
|
|
830
|
+
status="failed",
|
|
831
|
+
message=f"{step_label} failed: {exc}",
|
|
832
|
+
phase="lifecycle",
|
|
833
|
+
request_id=request_id,
|
|
834
|
+
details={"error": str(exc)},
|
|
835
|
+
)
|
|
836
|
+
raise
|
|
837
|
+
self._emit_progress_event(
|
|
838
|
+
step_index=step_index,
|
|
839
|
+
total_steps=total_steps,
|
|
840
|
+
step_name=step_name,
|
|
841
|
+
step_label=step_label,
|
|
842
|
+
status="completed",
|
|
843
|
+
message=success_message,
|
|
844
|
+
phase="lifecycle",
|
|
845
|
+
request_id=request_id,
|
|
846
|
+
)
|
|
847
|
+
current_step_index += 1
|
|
848
|
+
return result
|
|
849
|
+
|
|
850
|
+
def _validate_environment():
|
|
851
|
+
if os.geteuid() != 0:
|
|
852
|
+
raise PermissionError("Container creation requires root privileges.")
|
|
853
|
+
config = _load_config()
|
|
854
|
+
if not config or not config.get("token_value"):
|
|
855
|
+
raise ValueError("Proxmox infrastructure is not configured.")
|
|
856
|
+
if not config.get("network", {}).get("applied"):
|
|
857
|
+
raise RuntimeError("Proxmox bridge setup must be applied before creating containers.")
|
|
858
|
+
return config
|
|
859
|
+
|
|
860
|
+
config = _run_lifecycle_step(
|
|
861
|
+
"validate_environment",
|
|
862
|
+
"Validating infrastructure",
|
|
863
|
+
"Checking token, permissions, and bridge setup…",
|
|
864
|
+
"Infrastructure validated.",
|
|
865
|
+
_validate_environment,
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
def _create_container():
|
|
869
|
+
proxmox = _connect_proxmox(config)
|
|
870
|
+
node = config.get("node") or DEFAULT_NODE_NAME
|
|
871
|
+
payload = _build_container_payload(message, config)
|
|
872
|
+
payload["cpuunits"] = max(int(payload["cpus"] * 1024), 10)
|
|
873
|
+
payload["memory"] = int(payload["ram_mib"])
|
|
874
|
+
payload["node"] = node
|
|
875
|
+
logger.debug(
|
|
876
|
+
"Provisioning container node=%s template=%s ram=%s cpu=%s storage=%s",
|
|
877
|
+
node,
|
|
878
|
+
payload["template"],
|
|
879
|
+
payload["ram_mib"],
|
|
880
|
+
payload["cpus"],
|
|
881
|
+
payload["storage"],
|
|
882
|
+
)
|
|
883
|
+
vmid, _ = _instantiate_container(proxmox, node, payload)
|
|
884
|
+
payload["vmid"] = vmid
|
|
885
|
+
payload["created_at"] = datetime.utcnow().isoformat() + "Z"
|
|
886
|
+
_write_container_record(vmid, payload)
|
|
887
|
+
return proxmox, node, vmid, payload
|
|
888
|
+
|
|
889
|
+
proxmox, node, vmid, payload = _run_lifecycle_step(
|
|
890
|
+
"create_container",
|
|
891
|
+
"Creating container",
|
|
892
|
+
"Provisioning the LXC container…",
|
|
893
|
+
"Container created.",
|
|
894
|
+
_create_container,
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
def _start_container_step():
|
|
898
|
+
_start_container(proxmox, node, vmid)
|
|
899
|
+
|
|
900
|
+
_run_lifecycle_step(
|
|
901
|
+
"start_container",
|
|
902
|
+
"Starting container",
|
|
903
|
+
"Booting the container…",
|
|
904
|
+
"Container startup completed.",
|
|
905
|
+
_start_container_step,
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
def _bootstrap_progress_callback(step_index: int, total: int, step: Dict[str, Any], status: str, result: Optional[Dict[str, Any]]):
|
|
909
|
+
label = step.get("display_name") or _friendly_step_label(step.get("name", "bootstrap"))
|
|
910
|
+
if status == "in_progress":
|
|
911
|
+
message_text = f"{label} is running…"
|
|
912
|
+
elif status == "completed":
|
|
913
|
+
message_text = f"{label} completed."
|
|
914
|
+
else:
|
|
915
|
+
summary = (result or {}).get("error_summary") or (result or {}).get("error")
|
|
916
|
+
message_text = f"{label} failed"
|
|
917
|
+
if summary:
|
|
918
|
+
message_text += f": {summary}"
|
|
919
|
+
details: Dict[str, Any] = {}
|
|
920
|
+
if status == "failed" and result:
|
|
921
|
+
if result.get("attempt"):
|
|
922
|
+
details["attempt"] = result["attempt"]
|
|
923
|
+
summary_detail = result.get("error_summary") or result.get("error")
|
|
924
|
+
if summary_detail:
|
|
925
|
+
details["error_summary"] = summary_detail
|
|
926
|
+
self._emit_progress_event(
|
|
927
|
+
step_index=step_index,
|
|
928
|
+
total_steps=total,
|
|
929
|
+
step_name=step.get("name", "bootstrap"),
|
|
930
|
+
step_label=label,
|
|
931
|
+
status=status,
|
|
932
|
+
message=message_text,
|
|
933
|
+
phase="bootstrap",
|
|
934
|
+
request_id=request_id,
|
|
935
|
+
details=details or None,
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
public_key, steps = _bootstrap_portacode(
|
|
939
|
+
vmid,
|
|
940
|
+
payload["username"],
|
|
941
|
+
payload["password"],
|
|
942
|
+
payload["ssh_public_key"],
|
|
943
|
+
steps=bootstrap_steps,
|
|
944
|
+
progress_callback=_bootstrap_progress_callback,
|
|
945
|
+
start_index=current_step_index,
|
|
946
|
+
total_steps=total_steps,
|
|
947
|
+
)
|
|
716
948
|
|
|
717
949
|
return {
|
|
718
950
|
"event": "proxmox_container_created",
|
portacode/connection/terminal.py
CHANGED
|
@@ -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=5aWhaBfuSdCUpEwbbl75wIVr6LiHBu_Cbrnz4tRTfac,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,9 +12,9 @@ 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=oImE33DgtDEEjVM9r1oUejl0miJgwZaJ9K3pAeHhzBo,44653
|
|
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=gtUP5DEoKSg5q2rO08DxM9r_fNhjXkNzy3sNc6dZArE,92824
|
|
18
18
|
portacode/connection/handlers/__init__.py,sha256=iiyF3smwiI0IeDYzWQTl2PPVfW6aSp-g2CSO1ZTo9Ho,2641
|
|
19
19
|
portacode/connection/handlers/base.py,sha256=oENFb-Fcfzwk99Qx8gJQriEMiwSxwygwjOiuCH36hM4,10231
|
|
20
20
|
portacode/connection/handlers/chunked_content.py,sha256=h6hXRmxSeOgnIxoU8CkmvEf2Odv-ajPrpHIe_W3GKcA,9251
|
|
@@ -22,7 +22,7 @@ portacode/connection/handlers/diff_handlers.py,sha256=iYTIRCcpEQ03vIPKZCsMTE5aZb
|
|
|
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=F2M7qQxllo32rdERCcYYAC6t8albYd8vAmemAXGwxPs,36836
|
|
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.11.
|
|
67
|
+
portacode-1.4.11.dev6.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.11.
|
|
94
|
-
portacode-1.4.11.
|
|
95
|
-
portacode-1.4.11.
|
|
96
|
-
portacode-1.4.11.
|
|
97
|
-
portacode-1.4.11.
|
|
93
|
+
portacode-1.4.11.dev6.dist-info/METADATA,sha256=qREiPXfLBIuIlBTY9tHtU34IHLIeTlE0yknR5gmUMW8,13051
|
|
94
|
+
portacode-1.4.11.dev6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
95
|
+
portacode-1.4.11.dev6.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
96
|
+
portacode-1.4.11.dev6.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
97
|
+
portacode-1.4.11.dev6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|