portacode 1.4.17.dev5__py3-none-any.whl → 1.4.17.dev7__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/proxmox_infra.py +511 -350
- {portacode-1.4.17.dev5.dist-info → portacode-1.4.17.dev7.dist-info}/METADATA +1 -1
- {portacode-1.4.17.dev5.dist-info → portacode-1.4.17.dev7.dist-info}/RECORD +8 -8
- {portacode-1.4.17.dev5.dist-info → portacode-1.4.17.dev7.dist-info}/WHEEL +0 -0
- {portacode-1.4.17.dev5.dist-info → portacode-1.4.17.dev7.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.17.dev5.dist-info → portacode-1.4.17.dev7.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.17.dev5.dist-info → portacode-1.4.17.dev7.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.17.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 4, 17, '
|
|
31
|
+
__version__ = version = '1.4.17.dev7'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 17, 'dev7')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -45,16 +45,38 @@ DNS_SERVER = "1.1.1.1"
|
|
|
45
45
|
IFACES_PATH = Path("/etc/network/interfaces")
|
|
46
46
|
SYSCTL_PATH = Path("/etc/sysctl.d/99-portacode-forward.conf")
|
|
47
47
|
UNIT_DIR = Path("/etc/systemd/system")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
_MANAGED_CONTAINERS_STATE_LOCK = threading.Lock()
|
|
49
|
+
_MANAGED_CONTAINERS_STATE: Dict[str, Any] = {
|
|
50
|
+
"initialized": False,
|
|
51
|
+
"base_summary": None,
|
|
52
|
+
"initial_totals": {"ram_mib": 0, "disk_gib": 0, "cpu_share": 0.0},
|
|
53
|
+
"records": {},
|
|
54
|
+
"pending": {},
|
|
55
|
+
}
|
|
53
56
|
TEMPLATES_REFRESH_INTERVAL_S = 300
|
|
54
57
|
|
|
55
58
|
ProgressCallback = Callable[[int, int, Dict[str, Any], str, Optional[Dict[str, Any]]], None]
|
|
56
59
|
|
|
57
60
|
|
|
61
|
+
def _emit_host_event(
|
|
62
|
+
handler: SyncHandler,
|
|
63
|
+
payload: Dict[str, Any],
|
|
64
|
+
) -> None:
|
|
65
|
+
loop = handler.context.get("event_loop")
|
|
66
|
+
if not loop or loop.is_closed():
|
|
67
|
+
logger.debug("host event skipped (no event loop) event=%s", payload.get("event"))
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
future = asyncio.run_coroutine_threadsafe(handler.send_response(payload), loop)
|
|
71
|
+
future.add_done_callback(
|
|
72
|
+
lambda fut: logger.warning(
|
|
73
|
+
"Failed to emit host event %s: %s", payload.get("event"), fut.exception()
|
|
74
|
+
)
|
|
75
|
+
if fut.exception()
|
|
76
|
+
else None
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
58
80
|
def _emit_progress_event(
|
|
59
81
|
handler: SyncHandler,
|
|
60
82
|
*,
|
|
@@ -422,14 +444,7 @@ def _to_mib(value: Any) -> float:
|
|
|
422
444
|
|
|
423
445
|
|
|
424
446
|
def _pick_container_ram_mib(kind: str, cfg: Dict[str, Any], entry: Dict[str, Any]) -> float:
|
|
425
|
-
"""
|
|
426
|
-
Proxmox config `memory` is already MiB for both LXC and QEMU.
|
|
427
|
-
Fall back to list fields (bytes) only when it is absent/zero.
|
|
428
|
-
"""
|
|
429
|
-
mem_cfg = _safe_float(cfg.get("memory"))
|
|
430
|
-
if mem_cfg:
|
|
431
|
-
return mem_cfg
|
|
432
|
-
for candidate in (entry.get("maxmem"), entry.get("mem")):
|
|
447
|
+
for candidate in (cfg.get("memory"), entry.get("maxmem"), entry.get("mem")):
|
|
433
448
|
ram = _to_mib(candidate)
|
|
434
449
|
if ram:
|
|
435
450
|
return ram
|
|
@@ -583,10 +598,76 @@ def _ensure_containers_dir() -> None:
|
|
|
583
598
|
CONTAINERS_DIR.mkdir(parents=True, exist_ok=True)
|
|
584
599
|
|
|
585
600
|
|
|
586
|
-
def
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
601
|
+
def _copy_summary(summary: Dict[str, Any]) -> Dict[str, Any]:
|
|
602
|
+
snapshot = summary.copy()
|
|
603
|
+
containers = summary.get("containers")
|
|
604
|
+
if isinstance(containers, list):
|
|
605
|
+
snapshot["containers"] = [entry.copy() for entry in containers]
|
|
606
|
+
unmanaged = summary.get("unmanaged_containers")
|
|
607
|
+
if isinstance(unmanaged, list):
|
|
608
|
+
snapshot["unmanaged_containers"] = [entry.copy() for entry in unmanaged]
|
|
609
|
+
return snapshot
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _refresh_container_statuses(records: List[Dict[str, Any]], config: Dict[str, Any] | None) -> None:
|
|
613
|
+
if not records or not config:
|
|
614
|
+
return
|
|
615
|
+
try:
|
|
616
|
+
proxmox = _connect_proxmox(config)
|
|
617
|
+
node = _get_node_from_config(config)
|
|
618
|
+
statuses = {
|
|
619
|
+
str(ct.get("vmid")): (ct.get("status") or "unknown").lower()
|
|
620
|
+
for ct in proxmox.nodes(node).lxc.get()
|
|
621
|
+
}
|
|
622
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
623
|
+
logger.debug("Failed to refresh container statuses: %s", exc)
|
|
624
|
+
return
|
|
625
|
+
for record in records:
|
|
626
|
+
vmid = record.get("vmid")
|
|
627
|
+
if vmid is None:
|
|
628
|
+
continue
|
|
629
|
+
try:
|
|
630
|
+
vmid_key = str(int(vmid))
|
|
631
|
+
except (ValueError, TypeError):
|
|
632
|
+
continue
|
|
633
|
+
status = statuses.get(vmid_key)
|
|
634
|
+
if status:
|
|
635
|
+
record["status"] = status
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _initialize_managed_containers_state(force: bool = False) -> Dict[str, Any]:
|
|
639
|
+
with _MANAGED_CONTAINERS_STATE_LOCK:
|
|
640
|
+
if _MANAGED_CONTAINERS_STATE["initialized"] and not force:
|
|
641
|
+
base = _MANAGED_CONTAINERS_STATE["base_summary"]
|
|
642
|
+
return _copy_summary(base) if base else {}
|
|
643
|
+
|
|
644
|
+
config = _load_config()
|
|
645
|
+
records = _load_managed_container_records()
|
|
646
|
+
_refresh_container_statuses(records, config)
|
|
647
|
+
base_summary = _build_full_container_summary(records, config)
|
|
648
|
+
record_map: Dict[str, Dict[str, Any]] = {}
|
|
649
|
+
for record in records:
|
|
650
|
+
vmid = record.get("vmid")
|
|
651
|
+
if vmid is None:
|
|
652
|
+
continue
|
|
653
|
+
try:
|
|
654
|
+
record_map[str(int(vmid))] = record
|
|
655
|
+
except (TypeError, ValueError):
|
|
656
|
+
continue
|
|
657
|
+
|
|
658
|
+
initial_totals = {
|
|
659
|
+
"ram_mib": int(base_summary.get("total_ram_mib") or 0),
|
|
660
|
+
"disk_gib": int(base_summary.get("total_disk_gib") or 0),
|
|
661
|
+
"cpu_share": float(base_summary.get("total_cpu_share") or 0.0),
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
with _MANAGED_CONTAINERS_STATE_LOCK:
|
|
665
|
+
_MANAGED_CONTAINERS_STATE["initialized"] = True
|
|
666
|
+
_MANAGED_CONTAINERS_STATE["base_summary"] = base_summary
|
|
667
|
+
_MANAGED_CONTAINERS_STATE["initial_totals"] = initial_totals
|
|
668
|
+
_MANAGED_CONTAINERS_STATE["records"] = record_map
|
|
669
|
+
_MANAGED_CONTAINERS_STATE["pending"] = {}
|
|
670
|
+
return _copy_summary(base_summary)
|
|
590
671
|
|
|
591
672
|
|
|
592
673
|
def _load_managed_container_records() -> List[Dict[str, Any]]:
|
|
@@ -729,13 +810,6 @@ def _build_full_container_summary(records: List[Dict[str, Any]], config: Dict[st
|
|
|
729
810
|
"template": record.get("template") if record else None,
|
|
730
811
|
"created_at": record.get("created_at") if record else None,
|
|
731
812
|
}
|
|
732
|
-
# Fallback to recorded specs if live probing returned zeros (e.g., just-created CT).
|
|
733
|
-
if not merged["ram_mib"] and record:
|
|
734
|
-
merged["ram_mib"] = record.get("ram_mib") or merged["ram_mib"]
|
|
735
|
-
if not merged["disk_gib"] and record:
|
|
736
|
-
merged["disk_gib"] = record.get("disk_gib") or merged["disk_gib"]
|
|
737
|
-
if not merged["cpu_share"] and record:
|
|
738
|
-
merged["cpu_share"] = record.get("cpus") or merged["cpu_share"]
|
|
739
813
|
managed_entries.append(merged)
|
|
740
814
|
else:
|
|
741
815
|
unmanaged_entries.append(base_entry)
|
|
@@ -793,101 +867,131 @@ def _build_full_container_summary(records: List[Dict[str, Any]], config: Dict[st
|
|
|
793
867
|
return summary
|
|
794
868
|
|
|
795
869
|
|
|
796
|
-
def
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
return {
|
|
821
|
-
"ram_mib": free_ram,
|
|
822
|
-
"disk_gib": free_disk,
|
|
823
|
-
"cpu_share": free_cpu,
|
|
870
|
+
def _pending_totals(pending: Iterable[Dict[str, Any]]) -> Tuple[int, int, float]:
|
|
871
|
+
ram_total = 0
|
|
872
|
+
disk_total = 0
|
|
873
|
+
cpu_total = 0.0
|
|
874
|
+
for entry in pending:
|
|
875
|
+
ram_total += int(entry.get("ram_mib") or 0)
|
|
876
|
+
disk_total += int(entry.get("disk_gib") or 0)
|
|
877
|
+
cpu_total += float(entry.get("cpu_share") or 0.0)
|
|
878
|
+
return ram_total, disk_total, cpu_total
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def _compose_managed_containers_summary(
|
|
882
|
+
records: Iterable[Dict[str, Any]],
|
|
883
|
+
pending: Iterable[Dict[str, Any]],
|
|
884
|
+
base_summary: Dict[str, Any],
|
|
885
|
+
initial_totals: Dict[str, Any],
|
|
886
|
+
) -> Dict[str, Any]:
|
|
887
|
+
managed_summary = _build_managed_containers_summary(list(records))
|
|
888
|
+
summary = _copy_summary(base_summary)
|
|
889
|
+
base_by_vmid = {
|
|
890
|
+
str(entry.get("vmid")): entry
|
|
891
|
+
for entry in base_summary.get("containers", [])
|
|
892
|
+
if entry.get("vmid") is not None
|
|
824
893
|
}
|
|
894
|
+
default_storage = summary.get("default_storage")
|
|
895
|
+
merged_containers: List[Dict[str, Any]] = []
|
|
896
|
+
for entry in managed_summary["containers"]:
|
|
897
|
+
vmid = str(entry.get("vmid")) if entry.get("vmid") is not None else None
|
|
898
|
+
base_entry = base_by_vmid.get(vmid) if vmid is not None else None
|
|
899
|
+
if base_entry:
|
|
900
|
+
merged = base_entry.copy()
|
|
901
|
+
merged.update(entry)
|
|
902
|
+
else:
|
|
903
|
+
merged = entry.copy()
|
|
904
|
+
merged.setdefault("managed", True)
|
|
905
|
+
merged.setdefault("type", "lxc")
|
|
906
|
+
if default_storage and merged.get("storage"):
|
|
907
|
+
merged["matches_default_storage"] = (
|
|
908
|
+
str(merged["storage"]).lower() == str(default_storage).lower()
|
|
909
|
+
)
|
|
910
|
+
merged_containers.append(merged)
|
|
911
|
+
summary["updated_at"] = managed_summary["updated_at"]
|
|
912
|
+
summary["count"] = managed_summary["count"]
|
|
913
|
+
summary["total_ram_mib"] = managed_summary["total_ram_mib"]
|
|
914
|
+
summary["total_disk_gib"] = managed_summary["total_disk_gib"]
|
|
915
|
+
summary["total_cpu_share"] = managed_summary["total_cpu_share"]
|
|
916
|
+
summary["containers"] = merged_containers
|
|
917
|
+
|
|
918
|
+
pending_ram, pending_disk, pending_cpu = _pending_totals(pending)
|
|
919
|
+
delta_ram = managed_summary["total_ram_mib"] - int(initial_totals.get("ram_mib") or 0)
|
|
920
|
+
delta_disk = managed_summary["total_disk_gib"] - int(initial_totals.get("disk_gib") or 0)
|
|
921
|
+
delta_cpu = managed_summary["total_cpu_share"] - float(initial_totals.get("cpu_share") or 0.0)
|
|
922
|
+
|
|
923
|
+
if "allocated_ram_mib" in summary and summary.get("allocated_ram_mib") is not None:
|
|
924
|
+
summary["allocated_ram_mib"] = round(float(summary["allocated_ram_mib"]) + delta_ram + pending_ram, 2)
|
|
925
|
+
if "allocated_disk_gib" in summary and summary.get("allocated_disk_gib") is not None:
|
|
926
|
+
summary["allocated_disk_gib"] = round(float(summary["allocated_disk_gib"]) + delta_disk + pending_disk, 2)
|
|
927
|
+
if "allocated_cpu_share" in summary and summary.get("allocated_cpu_share") is not None:
|
|
928
|
+
summary["allocated_cpu_share"] = round(float(summary["allocated_cpu_share"]) + delta_cpu + pending_cpu, 2)
|
|
929
|
+
|
|
930
|
+
if "available_ram_mib" in summary and summary.get("available_ram_mib") is not None:
|
|
931
|
+
available_ram = float(summary["available_ram_mib"]) - delta_ram - pending_ram
|
|
932
|
+
summary["available_ram_mib"] = max(int(available_ram), 0)
|
|
933
|
+
if "available_disk_gib" in summary and summary.get("available_disk_gib") is not None:
|
|
934
|
+
available_disk = float(summary["available_disk_gib"]) - delta_disk - pending_disk
|
|
935
|
+
summary["available_disk_gib"] = max(available_disk, 0.0)
|
|
936
|
+
if "available_cpu_share" in summary and summary.get("available_cpu_share") is not None:
|
|
937
|
+
available_cpu = float(summary["available_cpu_share"]) - delta_cpu - pending_cpu
|
|
938
|
+
summary["available_cpu_share"] = max(available_cpu, 0.0)
|
|
825
939
|
|
|
940
|
+
return summary
|
|
826
941
|
|
|
827
|
-
def _assert_capacity_for_payload(payload: Dict[str, Any], summary: Dict[str, Any]) -> None:
|
|
828
|
-
"""Validate requested container resources against current free capacity."""
|
|
829
|
-
free = _compute_free_resources(summary)
|
|
830
|
-
shortages: List[str] = []
|
|
831
|
-
|
|
832
|
-
req_ram = float(payload.get("ram_mib", 0))
|
|
833
|
-
free_ram = free.get("ram_mib")
|
|
834
|
-
if free_ram is not None and req_ram > free_ram:
|
|
835
|
-
shortages.append(f"RAM (need {int(req_ram)} MiB, free {int(free_ram)} MiB)")
|
|
836
|
-
|
|
837
|
-
req_disk = float(payload.get("disk_gib", 0))
|
|
838
|
-
free_disk = free.get("disk_gib")
|
|
839
|
-
if free_disk is not None and req_disk > free_disk:
|
|
840
|
-
shortages.append(f"Disk (need {req_disk:.1f} GiB, free {free_disk:.1f} GiB)")
|
|
841
|
-
|
|
842
|
-
req_cpu = float(payload.get("cpus", 0))
|
|
843
|
-
free_cpu = free.get("cpu_share")
|
|
844
|
-
if free_cpu is not None and req_cpu > free_cpu:
|
|
845
|
-
shortages.append(f"CPU (need {req_cpu:.2f} vCPU, free {free_cpu:.2f} vCPU)")
|
|
846
942
|
|
|
847
|
-
|
|
848
|
-
|
|
943
|
+
def _get_managed_containers_summary(force: bool = False) -> Dict[str, Any]:
|
|
944
|
+
_initialize_managed_containers_state(force=force)
|
|
945
|
+
with _MANAGED_CONTAINERS_STATE_LOCK:
|
|
946
|
+
base_summary = _MANAGED_CONTAINERS_STATE.get("base_summary") or {}
|
|
947
|
+
records = list(_MANAGED_CONTAINERS_STATE.get("records", {}).values())
|
|
948
|
+
pending = list(_MANAGED_CONTAINERS_STATE.get("pending", {}).values())
|
|
949
|
+
initial_totals = _MANAGED_CONTAINERS_STATE.get("initial_totals", {})
|
|
950
|
+
if not base_summary:
|
|
951
|
+
return {}
|
|
952
|
+
return _compose_managed_containers_summary(records, pending, base_summary, initial_totals)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def _reserve_container_resources(payload: Dict[str, Any], *, device_id: str, request_id: Optional[str]) -> str:
|
|
956
|
+
_initialize_managed_containers_state()
|
|
957
|
+
ram_mib = int(payload.get("ram_mib") or 0)
|
|
958
|
+
disk_gib = int(payload.get("disk_gib") or 0)
|
|
959
|
+
cpu_share = float(payload.get("cpus") or 0.0)
|
|
960
|
+
reservation_id = secrets.token_hex(8)
|
|
961
|
+
with _MANAGED_CONTAINERS_STATE_LOCK:
|
|
962
|
+
base_summary = _MANAGED_CONTAINERS_STATE.get("base_summary") or {}
|
|
963
|
+
records = list(_MANAGED_CONTAINERS_STATE.get("records", {}).values())
|
|
964
|
+
pending = list(_MANAGED_CONTAINERS_STATE.get("pending", {}).values())
|
|
965
|
+
initial_totals = _MANAGED_CONTAINERS_STATE.get("initial_totals", {})
|
|
966
|
+
summary = _compose_managed_containers_summary(records, pending, base_summary, initial_totals)
|
|
967
|
+
|
|
968
|
+
available_ram = summary.get("available_ram_mib")
|
|
969
|
+
if available_ram is not None and ram_mib > available_ram:
|
|
970
|
+
raise RuntimeError("Not enough RAM to create this container.")
|
|
971
|
+
available_disk = summary.get("available_disk_gib")
|
|
972
|
+
if available_disk is not None and disk_gib > available_disk:
|
|
973
|
+
raise RuntimeError("Not enough disk space to create this container.")
|
|
974
|
+
available_cpu = summary.get("available_cpu_share")
|
|
975
|
+
if available_cpu is not None and cpu_share > available_cpu:
|
|
976
|
+
raise RuntimeError("Not enough CPU capacity to create this container.")
|
|
977
|
+
|
|
978
|
+
_MANAGED_CONTAINERS_STATE["pending"][reservation_id] = {
|
|
979
|
+
"reservation_id": reservation_id,
|
|
980
|
+
"device_id": device_id,
|
|
981
|
+
"request_id": request_id,
|
|
982
|
+
"ram_mib": ram_mib,
|
|
983
|
+
"disk_gib": disk_gib,
|
|
984
|
+
"cpu_share": cpu_share,
|
|
985
|
+
"created_at": datetime.utcnow().isoformat() + "Z",
|
|
986
|
+
}
|
|
987
|
+
return reservation_id
|
|
849
988
|
|
|
850
989
|
|
|
851
|
-
def
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
proxmox = _connect_proxmox(config)
|
|
857
|
-
node = _get_node_from_config(config)
|
|
858
|
-
statuses = {
|
|
859
|
-
str(ct.get("vmid")): (ct.get("status") or "unknown").lower()
|
|
860
|
-
for ct in proxmox.nodes(node).lxc.get()
|
|
861
|
-
}
|
|
862
|
-
except Exception as exc: # pragma: no cover - best effort
|
|
863
|
-
logger.debug("Failed to refresh container statuses: %s", exc)
|
|
864
|
-
return
|
|
865
|
-
for record in records:
|
|
866
|
-
vmid = record.get("vmid")
|
|
867
|
-
if vmid is None:
|
|
868
|
-
continue
|
|
869
|
-
try:
|
|
870
|
-
vmid_key = str(int(vmid))
|
|
871
|
-
except (ValueError, TypeError):
|
|
872
|
-
continue
|
|
873
|
-
status = statuses.get(vmid_key)
|
|
874
|
-
if status:
|
|
875
|
-
record["status"] = status
|
|
876
|
-
|
|
877
|
-
now = time.monotonic()
|
|
878
|
-
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
879
|
-
cache_ts = _MANAGED_CONTAINERS_CACHE["timestamp"]
|
|
880
|
-
cached = _MANAGED_CONTAINERS_CACHE["summary"]
|
|
881
|
-
if not force and cached and now - cache_ts < _MANAGED_CONTAINERS_CACHE_TTL_S:
|
|
882
|
-
return cached
|
|
883
|
-
config = _load_config()
|
|
884
|
-
records = _load_managed_container_records()
|
|
885
|
-
_refresh_container_statuses(records, config)
|
|
886
|
-
summary = _build_full_container_summary(records, config)
|
|
887
|
-
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
888
|
-
_MANAGED_CONTAINERS_CACHE["timestamp"] = now
|
|
889
|
-
_MANAGED_CONTAINERS_CACHE["summary"] = summary
|
|
890
|
-
return summary
|
|
990
|
+
def _release_container_reservation(reservation_id: Optional[str]) -> None:
|
|
991
|
+
if not reservation_id:
|
|
992
|
+
return
|
|
993
|
+
with _MANAGED_CONTAINERS_STATE_LOCK:
|
|
994
|
+
_MANAGED_CONTAINERS_STATE.get("pending", {}).pop(reservation_id, None)
|
|
891
995
|
|
|
892
996
|
|
|
893
997
|
def _format_rootfs(storage: str, disk_gib: int, storage_type: str) -> str:
|
|
@@ -1173,31 +1277,8 @@ def _start_container(proxmox: Any, node: str, vmid: int) -> Tuple[Dict[str, Any]
|
|
|
1173
1277
|
logger.info("Container %s already running (%ss)", vmid, uptime)
|
|
1174
1278
|
return status, 0.0
|
|
1175
1279
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
cfg = proxmox.nodes(node).lxc(vmid).config.get()
|
|
1179
|
-
payload = {
|
|
1180
|
-
"ram_mib": _to_mib(cfg.get("memory")),
|
|
1181
|
-
"disk_gib": _pick_container_disk_gib("lxc", cfg, {"rootfs": cfg.get("rootfs")}),
|
|
1182
|
-
"cpus": _pick_container_cpu_share("lxc", cfg, {}),
|
|
1183
|
-
}
|
|
1184
|
-
with _CAPACITY_LOCK:
|
|
1185
|
-
try:
|
|
1186
|
-
_assert_capacity_for_payload(payload, summary)
|
|
1187
|
-
except RuntimeError as exc:
|
|
1188
|
-
raise RuntimeError(f"Not enough resources to start container {vmid}: {exc}") from exc
|
|
1189
|
-
_PENDING_ALLOCATIONS["ram_mib"] += float(payload["ram_mib"] or 0)
|
|
1190
|
-
_PENDING_ALLOCATIONS["disk_gib"] += float(payload["disk_gib"] or 0)
|
|
1191
|
-
_PENDING_ALLOCATIONS["cpu_share"] += float(payload["cpus"] or 0)
|
|
1192
|
-
|
|
1193
|
-
try:
|
|
1194
|
-
upid = proxmox.nodes(node).lxc(vmid).status.start.post()
|
|
1195
|
-
return _wait_for_task(proxmox, node, upid)
|
|
1196
|
-
finally:
|
|
1197
|
-
with _CAPACITY_LOCK:
|
|
1198
|
-
_PENDING_ALLOCATIONS["ram_mib"] -= float(payload["ram_mib"] or 0)
|
|
1199
|
-
_PENDING_ALLOCATIONS["disk_gib"] -= float(payload["disk_gib"] or 0)
|
|
1200
|
-
_PENDING_ALLOCATIONS["cpu_share"] -= float(payload["cpus"] or 0)
|
|
1280
|
+
upid = proxmox.nodes(node).lxc(vmid).status.start.post()
|
|
1281
|
+
return _wait_for_task(proxmox, node, upid)
|
|
1201
1282
|
|
|
1202
1283
|
|
|
1203
1284
|
def _stop_container(proxmox: Any, node: str, vmid: int) -> Tuple[Dict[str, Any], float]:
|
|
@@ -1213,11 +1294,25 @@ def _delete_container(proxmox: Any, node: str, vmid: int) -> Tuple[Dict[str, Any
|
|
|
1213
1294
|
return _wait_for_task(proxmox, node, upid)
|
|
1214
1295
|
|
|
1215
1296
|
|
|
1216
|
-
def
|
|
1297
|
+
def _register_container_record(vmid: int, payload: Dict[str, Any], reservation_id: Optional[str] = None) -> None:
|
|
1298
|
+
_initialize_managed_containers_state()
|
|
1299
|
+
with _MANAGED_CONTAINERS_STATE_LOCK:
|
|
1300
|
+
if reservation_id:
|
|
1301
|
+
_MANAGED_CONTAINERS_STATE["pending"].pop(reservation_id, None)
|
|
1302
|
+
_MANAGED_CONTAINERS_STATE["records"][str(vmid)] = payload.copy()
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def _unregister_container_record(vmid: int) -> None:
|
|
1306
|
+
_initialize_managed_containers_state()
|
|
1307
|
+
with _MANAGED_CONTAINERS_STATE_LOCK:
|
|
1308
|
+
_MANAGED_CONTAINERS_STATE["records"].pop(str(vmid), None)
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
def _write_container_record(vmid: int, payload: Dict[str, Any], reservation_id: Optional[str] = None) -> None:
|
|
1217
1312
|
_ensure_containers_dir()
|
|
1218
1313
|
path = CONTAINERS_DIR / f"ct-{vmid}.json"
|
|
1219
1314
|
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
1220
|
-
|
|
1315
|
+
_register_container_record(vmid, payload, reservation_id=reservation_id)
|
|
1221
1316
|
|
|
1222
1317
|
|
|
1223
1318
|
def _read_container_record(vmid: int) -> Dict[str, Any]:
|
|
@@ -1237,7 +1332,7 @@ def _remove_container_record(vmid: int) -> None:
|
|
|
1237
1332
|
path = CONTAINERS_DIR / f"ct-{vmid}.json"
|
|
1238
1333
|
if path.exists():
|
|
1239
1334
|
path.unlink()
|
|
1240
|
-
|
|
1335
|
+
_unregister_container_record(vmid)
|
|
1241
1336
|
|
|
1242
1337
|
|
|
1243
1338
|
def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -1785,12 +1880,14 @@ def _allocate_vmid(proxmox: Any) -> int:
|
|
|
1785
1880
|
return int(proxmox.cluster.nextid.get())
|
|
1786
1881
|
|
|
1787
1882
|
|
|
1788
|
-
def _instantiate_container(
|
|
1883
|
+
def _instantiate_container(
|
|
1884
|
+
proxmox: Any, node: str, payload: Dict[str, Any], vmid: Optional[int] = None
|
|
1885
|
+
) -> Tuple[int, float]:
|
|
1789
1886
|
from proxmoxer.core import ResourceException
|
|
1790
1887
|
|
|
1791
1888
|
storage_type = _get_storage_type(proxmox.nodes(node).storage.get(), payload["storage"])
|
|
1792
1889
|
rootfs = _format_rootfs(payload["storage"], payload["disk_gib"], storage_type)
|
|
1793
|
-
vmid = _allocate_vmid(proxmox)
|
|
1890
|
+
vmid = vmid or _allocate_vmid(proxmox)
|
|
1794
1891
|
if not payload.get("hostname"):
|
|
1795
1892
|
payload["hostname"] = f"ct{vmid}"
|
|
1796
1893
|
try:
|
|
@@ -1822,6 +1919,43 @@ def _instantiate_container(proxmox: Any, node: str, payload: Dict[str, Any]) ->
|
|
|
1822
1919
|
raise RuntimeError(f"Failed to create container: {exc}") from exc
|
|
1823
1920
|
|
|
1824
1921
|
|
|
1922
|
+
def _cleanup_failed_container(
|
|
1923
|
+
proxmox: Any, node: str, vmid: int, provisioning_id: Optional[str]
|
|
1924
|
+
) -> None:
|
|
1925
|
+
from proxmoxer.core import ResourceException
|
|
1926
|
+
|
|
1927
|
+
try:
|
|
1928
|
+
cfg = proxmox.nodes(node).lxc(str(vmid)).config.get()
|
|
1929
|
+
except ResourceException as exc:
|
|
1930
|
+
msg = str(exc).lower()
|
|
1931
|
+
if "does not exist" in msg or "not found" in msg:
|
|
1932
|
+
return
|
|
1933
|
+
logger.warning("Failed to inspect container %s after create failure: %s", vmid, exc)
|
|
1934
|
+
return
|
|
1935
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
1936
|
+
logger.warning("Failed to inspect container %s after create failure: %s", vmid, exc)
|
|
1937
|
+
return
|
|
1938
|
+
|
|
1939
|
+
description = (cfg or {}).get("description") or ""
|
|
1940
|
+
if provisioning_id and provisioning_id not in description:
|
|
1941
|
+
logger.warning(
|
|
1942
|
+
"Skipping cleanup for vmid=%s; provisioning marker mismatch", vmid
|
|
1943
|
+
)
|
|
1944
|
+
return
|
|
1945
|
+
|
|
1946
|
+
try:
|
|
1947
|
+
status = proxmox.nodes(node).lxc(str(vmid)).status.current.get()
|
|
1948
|
+
if status.get("status") == "running":
|
|
1949
|
+
_stop_container(proxmox, node, vmid)
|
|
1950
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
1951
|
+
logger.warning("Failed to stop container %s after create failure: %s", vmid, exc)
|
|
1952
|
+
|
|
1953
|
+
try:
|
|
1954
|
+
_delete_container(proxmox, node, vmid)
|
|
1955
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
1956
|
+
logger.warning("Failed to delete container %s after create failure: %s", vmid, exc)
|
|
1957
|
+
|
|
1958
|
+
|
|
1825
1959
|
class CreateProxmoxContainerHandler(SyncHandler):
|
|
1826
1960
|
"""Provision a new managed LXC container via the Proxmox API."""
|
|
1827
1961
|
|
|
@@ -1924,239 +2058,266 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1924
2058
|
_validate_environment,
|
|
1925
2059
|
)
|
|
1926
2060
|
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
payload["memory"] = int(payload["ram_mib"])
|
|
1934
|
-
payload["node"] = node
|
|
1935
|
-
# Validate against current free resources (same math as dashboard charts) and place a short-lived reservation.
|
|
1936
|
-
summary = _get_managed_containers_summary(force=True)
|
|
1937
|
-
with _CAPACITY_LOCK:
|
|
1938
|
-
try:
|
|
1939
|
-
_assert_capacity_for_payload(payload, summary)
|
|
1940
|
-
except RuntimeError as exc:
|
|
1941
|
-
raise RuntimeError(f"Not enough resources to create the container safely: {exc}") from exc
|
|
1942
|
-
_PENDING_ALLOCATIONS["ram_mib"] += float(payload["ram_mib"] or 0)
|
|
1943
|
-
_PENDING_ALLOCATIONS["disk_gib"] += float(payload["disk_gib"] or 0)
|
|
1944
|
-
_PENDING_ALLOCATIONS["cpu_share"] += float(payload["cpus"] or 0)
|
|
1945
|
-
try:
|
|
1946
|
-
vmid, _ = _instantiate_container(proxmox, node, payload)
|
|
1947
|
-
finally:
|
|
1948
|
-
with _CAPACITY_LOCK:
|
|
1949
|
-
_PENDING_ALLOCATIONS["ram_mib"] -= float(payload["ram_mib"] or 0)
|
|
1950
|
-
_PENDING_ALLOCATIONS["disk_gib"] -= float(payload["disk_gib"] or 0)
|
|
1951
|
-
_PENDING_ALLOCATIONS["cpu_share"] -= float(payload["cpus"] or 0)
|
|
1952
|
-
logger.debug(
|
|
1953
|
-
"Provisioning container node=%s template=%s ram=%s cpu=%s storage=%s",
|
|
1954
|
-
node,
|
|
1955
|
-
payload["template"],
|
|
1956
|
-
payload["ram_mib"],
|
|
1957
|
-
payload["cpus"],
|
|
1958
|
-
payload["storage"],
|
|
1959
|
-
)
|
|
1960
|
-
payload["vmid"] = vmid
|
|
1961
|
-
payload["created_at"] = datetime.utcnow().isoformat() + "Z"
|
|
1962
|
-
payload["status"] = "creating"
|
|
1963
|
-
payload["device_id"] = device_id
|
|
1964
|
-
_write_container_record(vmid, payload)
|
|
1965
|
-
return proxmox, node, vmid, payload
|
|
1966
|
-
|
|
1967
|
-
proxmox, node, vmid, payload = _run_lifecycle_step(
|
|
1968
|
-
"create_container",
|
|
1969
|
-
"Creating container",
|
|
1970
|
-
"Provisioning the LXC container…",
|
|
1971
|
-
"Container created.",
|
|
1972
|
-
_create_container,
|
|
1973
|
-
)
|
|
1974
|
-
|
|
1975
|
-
def _start_container_step():
|
|
1976
|
-
_start_container(proxmox, node, vmid)
|
|
1977
|
-
|
|
1978
|
-
_run_lifecycle_step(
|
|
1979
|
-
"start_container",
|
|
1980
|
-
"Starting container",
|
|
1981
|
-
"Booting the container…",
|
|
1982
|
-
"Container startup completed.",
|
|
1983
|
-
_start_container_step,
|
|
1984
|
-
)
|
|
1985
|
-
_update_container_record(vmid, {"status": "running"})
|
|
2061
|
+
reservation_id: Optional[str] = None
|
|
2062
|
+
provisioning_id: Optional[str] = None
|
|
2063
|
+
created_record = False
|
|
2064
|
+
proxmox: Any = None
|
|
2065
|
+
node: Optional[str] = None
|
|
2066
|
+
vmid: Optional[int] = None
|
|
1986
2067
|
|
|
1987
|
-
def
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
message_text = f"{label} is running…"
|
|
1999
|
-
elif status == "completed":
|
|
2000
|
-
message_text = f"{label} completed."
|
|
2001
|
-
elif status == "retrying":
|
|
2002
|
-
attempt_desc = f" (attempt {attempt})" if attempt else ""
|
|
2003
|
-
message_text = f"{label} failed{attempt_desc}; retrying…"
|
|
2004
|
-
else:
|
|
2005
|
-
message_text = f"{label} failed"
|
|
2006
|
-
if error_summary:
|
|
2007
|
-
message_text += f": {error_summary}"
|
|
2008
|
-
details: Dict[str, Any] = {}
|
|
2009
|
-
if attempt:
|
|
2010
|
-
details["attempt"] = attempt
|
|
2011
|
-
if error_summary:
|
|
2012
|
-
details["error_summary"] = error_summary
|
|
2013
|
-
_emit_progress_event(
|
|
2014
|
-
self,
|
|
2015
|
-
step_index=step_index,
|
|
2016
|
-
total_steps=total,
|
|
2017
|
-
step_name=step.get("name", "bootstrap"),
|
|
2018
|
-
step_label=label,
|
|
2019
|
-
status=status,
|
|
2020
|
-
message=message_text,
|
|
2021
|
-
phase="bootstrap",
|
|
2022
|
-
request_id=request_id,
|
|
2023
|
-
details=details or None,
|
|
2024
|
-
on_behalf_of_device=device_id,
|
|
2068
|
+
def _create_container():
|
|
2069
|
+
nonlocal reservation_id, provisioning_id, created_record, proxmox, node, vmid
|
|
2070
|
+
proxmox = _connect_proxmox(config)
|
|
2071
|
+
node = config.get("node") or DEFAULT_NODE_NAME
|
|
2072
|
+
payload = _build_container_payload(message, config)
|
|
2073
|
+
payload["cpulimit"] = float(payload["cpus"])
|
|
2074
|
+
payload["cores"] = int(max(math.ceil(payload["cpus"]), 1))
|
|
2075
|
+
payload["memory"] = int(payload["ram_mib"])
|
|
2076
|
+
payload["node"] = node
|
|
2077
|
+
reservation_id = _reserve_container_resources(
|
|
2078
|
+
payload, device_id=device_id, request_id=request_id
|
|
2025
2079
|
)
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
total_steps=total_steps,
|
|
2036
|
-
default_public_key=device_public_key if has_device_keypair else None,
|
|
2037
|
-
)
|
|
2038
|
-
current_step_index += len(bootstrap_steps)
|
|
2039
|
-
|
|
2040
|
-
service_installed = False
|
|
2041
|
-
if has_device_keypair:
|
|
2042
|
-
logger.info(
|
|
2043
|
-
"deploying dashboard-provided Portacode keypair (device_id=%s) into container %s",
|
|
2044
|
-
device_id,
|
|
2045
|
-
vmid,
|
|
2080
|
+
_emit_host_event(
|
|
2081
|
+
self,
|
|
2082
|
+
{
|
|
2083
|
+
"event": "proxmox_container_accepted",
|
|
2084
|
+
"success": True,
|
|
2085
|
+
"message": "Provisioning accepted; resources reserved.",
|
|
2086
|
+
"device_id": device_id,
|
|
2087
|
+
"request_id": request_id,
|
|
2088
|
+
},
|
|
2046
2089
|
)
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2090
|
+
provisioning_id = secrets.token_hex(6)
|
|
2091
|
+
payload["description"] = f"{payload.get('description', MANAGED_MARKER)};provisioning_id={provisioning_id}"
|
|
2092
|
+
logger.debug(
|
|
2093
|
+
"Provisioning container node=%s template=%s ram=%s cpu=%s storage=%s",
|
|
2094
|
+
node,
|
|
2095
|
+
payload["template"],
|
|
2096
|
+
payload["ram_mib"],
|
|
2097
|
+
payload["cpus"],
|
|
2098
|
+
payload["storage"],
|
|
2052
2099
|
)
|
|
2053
|
-
|
|
2054
|
-
|
|
2100
|
+
try:
|
|
2101
|
+
vmid = _allocate_vmid(proxmox)
|
|
2102
|
+
vmid, _ = _instantiate_container(proxmox, node, payload, vmid=vmid)
|
|
2103
|
+
except Exception:
|
|
2104
|
+
_release_container_reservation(reservation_id)
|
|
2105
|
+
if vmid is not None:
|
|
2106
|
+
_cleanup_failed_container(proxmox, node, vmid, provisioning_id)
|
|
2107
|
+
raise
|
|
2108
|
+
payload["vmid"] = vmid
|
|
2109
|
+
payload["created_at"] = datetime.utcnow().isoformat() + "Z"
|
|
2110
|
+
payload["status"] = "creating"
|
|
2111
|
+
payload["device_id"] = device_id
|
|
2112
|
+
try:
|
|
2113
|
+
_write_container_record(vmid, payload, reservation_id=reservation_id)
|
|
2114
|
+
created_record = True
|
|
2115
|
+
except Exception:
|
|
2116
|
+
_release_container_reservation(reservation_id)
|
|
2117
|
+
_cleanup_failed_container(proxmox, node, vmid, provisioning_id)
|
|
2118
|
+
raise
|
|
2119
|
+
return proxmox, node, vmid, payload
|
|
2055
2120
|
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
step_label=auth_label,
|
|
2064
|
-
status="in_progress",
|
|
2065
|
-
message="Notifying the server of the new device…",
|
|
2066
|
-
phase="service",
|
|
2067
|
-
request_id=request_id,
|
|
2068
|
-
on_behalf_of_device=device_id,
|
|
2121
|
+
try:
|
|
2122
|
+
proxmox, node, vmid, payload = _run_lifecycle_step(
|
|
2123
|
+
"create_container",
|
|
2124
|
+
"Creating container",
|
|
2125
|
+
"Provisioning the LXC container…",
|
|
2126
|
+
"Container created.",
|
|
2127
|
+
_create_container,
|
|
2069
2128
|
)
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
on_behalf_of_device=device_id,
|
|
2129
|
+
|
|
2130
|
+
def _start_container_step():
|
|
2131
|
+
_start_container(proxmox, node, vmid)
|
|
2132
|
+
|
|
2133
|
+
_run_lifecycle_step(
|
|
2134
|
+
"start_container",
|
|
2135
|
+
"Starting container",
|
|
2136
|
+
"Booting the container…",
|
|
2137
|
+
"Container startup completed.",
|
|
2138
|
+
_start_container_step,
|
|
2081
2139
|
)
|
|
2140
|
+
_update_container_record(vmid, {"status": "running"})
|
|
2141
|
+
|
|
2142
|
+
def _bootstrap_progress_callback(
|
|
2143
|
+
step_index: int,
|
|
2144
|
+
total: int,
|
|
2145
|
+
step: Dict[str, Any],
|
|
2146
|
+
status: str,
|
|
2147
|
+
result: Optional[Dict[str, Any]],
|
|
2148
|
+
):
|
|
2149
|
+
label = step.get("display_name") or _friendly_step_label(step.get("name", "bootstrap"))
|
|
2150
|
+
error_summary = (result or {}).get("error_summary") or (result or {}).get("error")
|
|
2151
|
+
attempt = (result or {}).get("attempt")
|
|
2152
|
+
if status == "in_progress":
|
|
2153
|
+
message_text = f"{label} is running…"
|
|
2154
|
+
elif status == "completed":
|
|
2155
|
+
message_text = f"{label} completed."
|
|
2156
|
+
elif status == "retrying":
|
|
2157
|
+
attempt_desc = f" (attempt {attempt})" if attempt else ""
|
|
2158
|
+
message_text = f"{label} failed{attempt_desc}; retrying…"
|
|
2159
|
+
else:
|
|
2160
|
+
message_text = f"{label} failed"
|
|
2161
|
+
if error_summary:
|
|
2162
|
+
message_text += f": {error_summary}"
|
|
2163
|
+
details: Dict[str, Any] = {}
|
|
2164
|
+
if attempt:
|
|
2165
|
+
details["attempt"] = attempt
|
|
2166
|
+
if error_summary:
|
|
2167
|
+
details["error_summary"] = error_summary
|
|
2168
|
+
_emit_progress_event(
|
|
2169
|
+
self,
|
|
2170
|
+
step_index=step_index,
|
|
2171
|
+
total_steps=total,
|
|
2172
|
+
step_name=step.get("name", "bootstrap"),
|
|
2173
|
+
step_label=label,
|
|
2174
|
+
status=status,
|
|
2175
|
+
message=message_text,
|
|
2176
|
+
phase="bootstrap",
|
|
2177
|
+
request_id=request_id,
|
|
2178
|
+
details=details or None,
|
|
2179
|
+
on_behalf_of_device=device_id,
|
|
2180
|
+
)
|
|
2082
2181
|
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2182
|
+
public_key, steps = _bootstrap_portacode(
|
|
2183
|
+
vmid,
|
|
2184
|
+
payload["username"],
|
|
2185
|
+
payload["password"],
|
|
2186
|
+
payload["ssh_public_key"],
|
|
2187
|
+
steps=bootstrap_steps,
|
|
2188
|
+
progress_callback=_bootstrap_progress_callback,
|
|
2189
|
+
start_index=current_step_index,
|
|
2088
2190
|
total_steps=total_steps,
|
|
2089
|
-
|
|
2090
|
-
step_label=install_label,
|
|
2091
|
-
status="in_progress",
|
|
2092
|
-
message="Running sudo portacode service install…",
|
|
2093
|
-
phase="service",
|
|
2094
|
-
request_id=request_id,
|
|
2095
|
-
on_behalf_of_device=device_id,
|
|
2191
|
+
default_public_key=device_public_key if has_device_keypair else None,
|
|
2096
2192
|
)
|
|
2193
|
+
current_step_index += len(bootstrap_steps)
|
|
2194
|
+
|
|
2195
|
+
service_installed = False
|
|
2196
|
+
if has_device_keypair:
|
|
2197
|
+
logger.info(
|
|
2198
|
+
"deploying dashboard-provided Portacode keypair (device_id=%s) into container %s",
|
|
2199
|
+
device_id,
|
|
2200
|
+
vmid,
|
|
2201
|
+
)
|
|
2202
|
+
_deploy_device_keypair(
|
|
2203
|
+
vmid,
|
|
2204
|
+
payload["username"],
|
|
2205
|
+
device_private_key,
|
|
2206
|
+
device_public_key,
|
|
2207
|
+
)
|
|
2208
|
+
service_installed = True
|
|
2209
|
+
service_start_index = current_step_index
|
|
2097
2210
|
|
|
2098
|
-
|
|
2099
|
-
|
|
2211
|
+
auth_step_name = "setup_device_authentication"
|
|
2212
|
+
auth_label = "Setting up device authentication"
|
|
2213
|
+
_emit_progress_event(
|
|
2214
|
+
self,
|
|
2215
|
+
step_index=service_start_index,
|
|
2216
|
+
total_steps=total_steps,
|
|
2217
|
+
step_name=auth_step_name,
|
|
2218
|
+
step_label=auth_label,
|
|
2219
|
+
status="in_progress",
|
|
2220
|
+
message="Notifying the server of the new device…",
|
|
2221
|
+
phase="service",
|
|
2222
|
+
request_id=request_id,
|
|
2223
|
+
on_behalf_of_device=device_id,
|
|
2224
|
+
)
|
|
2225
|
+
_emit_progress_event(
|
|
2226
|
+
self,
|
|
2227
|
+
step_index=service_start_index,
|
|
2228
|
+
total_steps=total_steps,
|
|
2229
|
+
step_name=auth_step_name,
|
|
2230
|
+
step_label=auth_label,
|
|
2231
|
+
status="completed",
|
|
2232
|
+
message="Authentication metadata recorded.",
|
|
2233
|
+
phase="service",
|
|
2234
|
+
request_id=request_id,
|
|
2235
|
+
on_behalf_of_device=device_id,
|
|
2236
|
+
)
|
|
2100
2237
|
|
|
2101
|
-
|
|
2238
|
+
install_step = service_start_index + 1
|
|
2239
|
+
install_label = "Launching Portacode service"
|
|
2102
2240
|
_emit_progress_event(
|
|
2103
2241
|
self,
|
|
2104
2242
|
step_index=install_step,
|
|
2105
2243
|
total_steps=total_steps,
|
|
2106
2244
|
step_name="launch_portacode_service",
|
|
2107
2245
|
step_label=install_label,
|
|
2108
|
-
status="
|
|
2109
|
-
message=
|
|
2246
|
+
status="in_progress",
|
|
2247
|
+
message="Running sudo portacode service install…",
|
|
2110
2248
|
phase="service",
|
|
2111
2249
|
request_id=request_id,
|
|
2112
|
-
details={
|
|
2113
|
-
"stderr": res.get("stderr"),
|
|
2114
|
-
"stdout": res.get("stdout"),
|
|
2115
|
-
},
|
|
2116
2250
|
on_behalf_of_device=device_id,
|
|
2117
2251
|
)
|
|
2118
|
-
raise RuntimeError(res.get("stderr") or res.get("stdout") or "Service install failed")
|
|
2119
2252
|
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2253
|
+
cmd = _su_command(payload["username"], "sudo -S portacode service install")
|
|
2254
|
+
res = _run_pct(vmid, cmd, input_text=payload["password"] + "\n")
|
|
2255
|
+
|
|
2256
|
+
if res["returncode"] != 0:
|
|
2257
|
+
_emit_progress_event(
|
|
2258
|
+
self,
|
|
2259
|
+
step_index=install_step,
|
|
2260
|
+
total_steps=total_steps,
|
|
2261
|
+
step_name="launch_portacode_service",
|
|
2262
|
+
step_label=install_label,
|
|
2263
|
+
status="failed",
|
|
2264
|
+
message=f"{install_label} failed: {res.get('stderr') or res.get('stdout')}",
|
|
2265
|
+
phase="service",
|
|
2266
|
+
request_id=request_id,
|
|
2267
|
+
details={
|
|
2268
|
+
"stderr": res.get("stderr"),
|
|
2269
|
+
"stdout": res.get("stdout"),
|
|
2270
|
+
},
|
|
2271
|
+
on_behalf_of_device=device_id,
|
|
2272
|
+
)
|
|
2273
|
+
raise RuntimeError(res.get("stderr") or res.get("stdout") or "Service install failed")
|
|
2132
2274
|
|
|
2133
|
-
|
|
2275
|
+
_emit_progress_event(
|
|
2276
|
+
self,
|
|
2277
|
+
step_index=install_step,
|
|
2278
|
+
total_steps=total_steps,
|
|
2279
|
+
step_name="launch_portacode_service",
|
|
2280
|
+
step_label=install_label,
|
|
2281
|
+
status="completed",
|
|
2282
|
+
message="Portacode service install finished.",
|
|
2283
|
+
phase="service",
|
|
2284
|
+
request_id=request_id,
|
|
2285
|
+
on_behalf_of_device=device_id,
|
|
2286
|
+
)
|
|
2134
2287
|
|
|
2135
|
-
|
|
2288
|
+
logger.info(
|
|
2289
|
+
"create_proxmox_container: portacode service install completed inside ct %s", vmid
|
|
2290
|
+
)
|
|
2136
2291
|
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
"
|
|
2145
|
-
"
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2292
|
+
current_step_index += 2
|
|
2293
|
+
|
|
2294
|
+
return {
|
|
2295
|
+
"event": "proxmox_container_created",
|
|
2296
|
+
"success": True,
|
|
2297
|
+
"message": f"Container {vmid} is ready and Portacode key captured.",
|
|
2298
|
+
"ctid": str(vmid),
|
|
2299
|
+
"public_key": public_key,
|
|
2300
|
+
"container": {
|
|
2301
|
+
"vmid": vmid,
|
|
2302
|
+
"hostname": payload["hostname"],
|
|
2303
|
+
"template": payload["template"],
|
|
2304
|
+
"storage": payload["storage"],
|
|
2305
|
+
"disk_gib": payload["disk_gib"],
|
|
2306
|
+
"ram_mib": payload["ram_mib"],
|
|
2307
|
+
"cpus": payload["cpus"],
|
|
2308
|
+
},
|
|
2309
|
+
"setup_steps": steps,
|
|
2310
|
+
"device_id": device_id,
|
|
2311
|
+
"on_behalf_of_device": device_id,
|
|
2312
|
+
"service_installed": service_installed,
|
|
2313
|
+
}
|
|
2314
|
+
except Exception:
|
|
2315
|
+
if reservation_id and not created_record:
|
|
2316
|
+
_release_container_reservation(reservation_id)
|
|
2317
|
+
if vmid is not None and proxmox and node:
|
|
2318
|
+
_cleanup_failed_container(proxmox, node, vmid, provisioning_id)
|
|
2319
|
+
_remove_container_record(vmid)
|
|
2320
|
+
raise
|
|
2160
2321
|
|
|
2161
2322
|
|
|
2162
2323
|
class StartPortacodeServiceHandler(SyncHandler):
|
|
@@ -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=Inn3jAXtKmJcxP1Ht9pcGtSS3m4h3OsmOpgsOB9feYM,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
|
|
@@ -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=hsEsulBU55bLF5j5ACDLo-51hZe8rMKj5bdB5n5jv9Y,97125
|
|
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=fr12QpOr_Z8KYGUU-AYrTQwRPAcrLK85hvj3SEq1Kw8,14757
|
|
@@ -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.17.
|
|
68
|
+
portacode-1.4.17.dev7.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.17.
|
|
95
|
-
portacode-1.4.17.
|
|
96
|
-
portacode-1.4.17.
|
|
97
|
-
portacode-1.4.17.
|
|
98
|
-
portacode-1.4.17.
|
|
94
|
+
portacode-1.4.17.dev7.dist-info/METADATA,sha256=WSr8txw3OXCRLK9nD39AW20JtMkJUq_OjJ0xWv410f0,13051
|
|
95
|
+
portacode-1.4.17.dev7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
96
|
+
portacode-1.4.17.dev7.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
97
|
+
portacode-1.4.17.dev7.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
98
|
+
portacode-1.4.17.dev7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|