portacode 1.4.17.dev6__py3-none-any.whl → 1.4.17.dev8__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 +536 -363
- {portacode-1.4.17.dev6.dist-info → portacode-1.4.17.dev8.dist-info}/METADATA +1 -1
- {portacode-1.4.17.dev6.dist-info → portacode-1.4.17.dev8.dist-info}/RECORD +8 -8
- {portacode-1.4.17.dev6.dist-info → portacode-1.4.17.dev8.dist-info}/WHEEL +0 -0
- {portacode-1.4.17.dev6.dist-info → portacode-1.4.17.dev8.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.17.dev6.dist-info → portacode-1.4.17.dev8.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.17.dev6.dist-info → portacode-1.4.17.dev8.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.dev8'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 17, 'dev8')
|
|
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,278 @@ 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"})
|
|
1986
|
-
|
|
1987
|
-
def _bootstrap_progress_callback(
|
|
1988
|
-
step_index: int,
|
|
1989
|
-
total: int,
|
|
1990
|
-
step: Dict[str, Any],
|
|
1991
|
-
status: str,
|
|
1992
|
-
result: Optional[Dict[str, Any]],
|
|
1993
|
-
):
|
|
1994
|
-
label = step.get("display_name") or _friendly_step_label(step.get("name", "bootstrap"))
|
|
1995
|
-
error_summary = (result or {}).get("error_summary") or (result or {}).get("error")
|
|
1996
|
-
attempt = (result or {}).get("attempt")
|
|
1997
|
-
if status == "in_progress":
|
|
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,
|
|
2025
|
-
)
|
|
2061
|
+
node = config.get("node") or DEFAULT_NODE_NAME
|
|
2062
|
+
payload = _build_container_payload(message, config)
|
|
2063
|
+
payload["cpulimit"] = float(payload["cpus"])
|
|
2064
|
+
payload["cores"] = int(max(math.ceil(payload["cpus"]), 1))
|
|
2065
|
+
payload["memory"] = int(payload["ram_mib"])
|
|
2066
|
+
payload["node"] = node
|
|
2026
2067
|
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
payload["username"],
|
|
2030
|
-
payload["password"],
|
|
2031
|
-
payload["ssh_public_key"],
|
|
2032
|
-
steps=bootstrap_steps,
|
|
2033
|
-
progress_callback=_bootstrap_progress_callback,
|
|
2034
|
-
start_index=current_step_index,
|
|
2035
|
-
total_steps=total_steps,
|
|
2036
|
-
default_public_key=device_public_key if has_device_keypair else None,
|
|
2068
|
+
reservation_id = _reserve_container_resources(
|
|
2069
|
+
payload, device_id=device_id, request_id=request_id
|
|
2037
2070
|
)
|
|
2038
|
-
|
|
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,
|
|
2046
|
-
)
|
|
2047
|
-
_deploy_device_keypair(
|
|
2048
|
-
vmid,
|
|
2049
|
-
payload["username"],
|
|
2050
|
-
device_private_key,
|
|
2051
|
-
device_public_key,
|
|
2052
|
-
)
|
|
2053
|
-
service_installed = True
|
|
2054
|
-
service_start_index = current_step_index
|
|
2071
|
+
provisioning_id = secrets.token_hex(6)
|
|
2072
|
+
payload["description"] = f"{payload.get('description', MANAGED_MARKER)};provisioning_id={provisioning_id}"
|
|
2055
2073
|
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2074
|
+
def _provision_background() -> None:
|
|
2075
|
+
nonlocal current_step_index
|
|
2076
|
+
proxmox: Any = None
|
|
2077
|
+
vmid: Optional[int] = None
|
|
2078
|
+
created_record = False
|
|
2079
|
+
try:
|
|
2080
|
+
def _create_container():
|
|
2081
|
+
nonlocal proxmox, vmid, created_record
|
|
2082
|
+
proxmox = _connect_proxmox(config)
|
|
2083
|
+
logger.debug(
|
|
2084
|
+
"Provisioning container node=%s template=%s ram=%s cpu=%s storage=%s",
|
|
2085
|
+
node,
|
|
2086
|
+
payload["template"],
|
|
2087
|
+
payload["ram_mib"],
|
|
2088
|
+
payload["cpus"],
|
|
2089
|
+
payload["storage"],
|
|
2090
|
+
)
|
|
2091
|
+
try:
|
|
2092
|
+
vmid = _allocate_vmid(proxmox)
|
|
2093
|
+
vmid, _ = _instantiate_container(proxmox, node, payload, vmid=vmid)
|
|
2094
|
+
except Exception:
|
|
2095
|
+
_release_container_reservation(reservation_id)
|
|
2096
|
+
if vmid is not None:
|
|
2097
|
+
_cleanup_failed_container(proxmox, node, vmid, provisioning_id)
|
|
2098
|
+
raise
|
|
2099
|
+
payload["vmid"] = vmid
|
|
2100
|
+
payload["created_at"] = datetime.utcnow().isoformat() + "Z"
|
|
2101
|
+
payload["status"] = "creating"
|
|
2102
|
+
payload["device_id"] = device_id
|
|
2103
|
+
try:
|
|
2104
|
+
_write_container_record(vmid, payload, reservation_id=reservation_id)
|
|
2105
|
+
created_record = True
|
|
2106
|
+
except Exception:
|
|
2107
|
+
_release_container_reservation(reservation_id)
|
|
2108
|
+
_cleanup_failed_container(proxmox, node, vmid, provisioning_id)
|
|
2109
|
+
raise
|
|
2110
|
+
return proxmox, node, vmid, payload
|
|
2111
|
+
|
|
2112
|
+
proxmox, _, vmid, payload_local = _run_lifecycle_step(
|
|
2113
|
+
"create_container",
|
|
2114
|
+
"Creating container",
|
|
2115
|
+
"Provisioning the LXC container…",
|
|
2116
|
+
"Container created.",
|
|
2117
|
+
_create_container,
|
|
2118
|
+
)
|
|
2097
2119
|
|
|
2098
|
-
|
|
2099
|
-
|
|
2120
|
+
def _start_container_step():
|
|
2121
|
+
_start_container(proxmox, node, vmid)
|
|
2100
2122
|
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2123
|
+
_run_lifecycle_step(
|
|
2124
|
+
"start_container",
|
|
2125
|
+
"Starting container",
|
|
2126
|
+
"Booting the container…",
|
|
2127
|
+
"Container startup completed.",
|
|
2128
|
+
_start_container_step,
|
|
2129
|
+
)
|
|
2130
|
+
_update_container_record(vmid, {"status": "running"})
|
|
2131
|
+
|
|
2132
|
+
def _bootstrap_progress_callback(
|
|
2133
|
+
step_index: int,
|
|
2134
|
+
total: int,
|
|
2135
|
+
step: Dict[str, Any],
|
|
2136
|
+
status: str,
|
|
2137
|
+
result: Optional[Dict[str, Any]],
|
|
2138
|
+
):
|
|
2139
|
+
label = step.get("display_name") or _friendly_step_label(step.get("name", "bootstrap"))
|
|
2140
|
+
error_summary = (result or {}).get("error_summary") or (result or {}).get("error")
|
|
2141
|
+
attempt = (result or {}).get("attempt")
|
|
2142
|
+
if status == "in_progress":
|
|
2143
|
+
message_text = f"{label} is running…"
|
|
2144
|
+
elif status == "completed":
|
|
2145
|
+
message_text = f"{label} completed."
|
|
2146
|
+
elif status == "retrying":
|
|
2147
|
+
attempt_desc = f" (attempt {attempt})" if attempt else ""
|
|
2148
|
+
message_text = f"{label} failed{attempt_desc}; retrying…"
|
|
2149
|
+
else:
|
|
2150
|
+
message_text = f"{label} failed"
|
|
2151
|
+
if error_summary:
|
|
2152
|
+
message_text += f": {error_summary}"
|
|
2153
|
+
details: Dict[str, Any] = {}
|
|
2154
|
+
if attempt:
|
|
2155
|
+
details["attempt"] = attempt
|
|
2156
|
+
if error_summary:
|
|
2157
|
+
details["error_summary"] = error_summary
|
|
2158
|
+
_emit_progress_event(
|
|
2159
|
+
self,
|
|
2160
|
+
step_index=step_index,
|
|
2161
|
+
total_steps=total,
|
|
2162
|
+
step_name=step.get("name", "bootstrap"),
|
|
2163
|
+
step_label=label,
|
|
2164
|
+
status=status,
|
|
2165
|
+
message=message_text,
|
|
2166
|
+
phase="bootstrap",
|
|
2167
|
+
request_id=request_id,
|
|
2168
|
+
details=details or None,
|
|
2169
|
+
on_behalf_of_device=device_id,
|
|
2170
|
+
)
|
|
2171
|
+
|
|
2172
|
+
public_key, steps = _bootstrap_portacode(
|
|
2173
|
+
vmid,
|
|
2174
|
+
payload_local["username"],
|
|
2175
|
+
payload_local["password"],
|
|
2176
|
+
payload_local["ssh_public_key"],
|
|
2177
|
+
steps=bootstrap_steps,
|
|
2178
|
+
progress_callback=_bootstrap_progress_callback,
|
|
2179
|
+
start_index=current_step_index,
|
|
2105
2180
|
total_steps=total_steps,
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
"
|
|
2114
|
-
|
|
2181
|
+
default_public_key=device_public_key if has_device_keypair else None,
|
|
2182
|
+
)
|
|
2183
|
+
current_step_index += len(bootstrap_steps)
|
|
2184
|
+
|
|
2185
|
+
service_installed = False
|
|
2186
|
+
if has_device_keypair:
|
|
2187
|
+
logger.info(
|
|
2188
|
+
"deploying dashboard-provided Portacode keypair (device_id=%s) into container %s",
|
|
2189
|
+
device_id,
|
|
2190
|
+
vmid,
|
|
2191
|
+
)
|
|
2192
|
+
_deploy_device_keypair(
|
|
2193
|
+
vmid,
|
|
2194
|
+
payload_local["username"],
|
|
2195
|
+
device_private_key,
|
|
2196
|
+
device_public_key,
|
|
2197
|
+
)
|
|
2198
|
+
service_installed = True
|
|
2199
|
+
service_start_index = current_step_index
|
|
2200
|
+
|
|
2201
|
+
auth_step_name = "setup_device_authentication"
|
|
2202
|
+
auth_label = "Setting up device authentication"
|
|
2203
|
+
_emit_progress_event(
|
|
2204
|
+
self,
|
|
2205
|
+
step_index=service_start_index,
|
|
2206
|
+
total_steps=total_steps,
|
|
2207
|
+
step_name=auth_step_name,
|
|
2208
|
+
step_label=auth_label,
|
|
2209
|
+
status="in_progress",
|
|
2210
|
+
message="Notifying the server of the new device…",
|
|
2211
|
+
phase="service",
|
|
2212
|
+
request_id=request_id,
|
|
2213
|
+
on_behalf_of_device=device_id,
|
|
2214
|
+
)
|
|
2215
|
+
_emit_progress_event(
|
|
2216
|
+
self,
|
|
2217
|
+
step_index=service_start_index,
|
|
2218
|
+
total_steps=total_steps,
|
|
2219
|
+
step_name=auth_step_name,
|
|
2220
|
+
step_label=auth_label,
|
|
2221
|
+
status="completed",
|
|
2222
|
+
message="Authentication metadata recorded.",
|
|
2223
|
+
phase="service",
|
|
2224
|
+
request_id=request_id,
|
|
2225
|
+
on_behalf_of_device=device_id,
|
|
2226
|
+
)
|
|
2227
|
+
|
|
2228
|
+
install_step = service_start_index + 1
|
|
2229
|
+
install_label = "Launching Portacode service"
|
|
2230
|
+
_emit_progress_event(
|
|
2231
|
+
self,
|
|
2232
|
+
step_index=install_step,
|
|
2233
|
+
total_steps=total_steps,
|
|
2234
|
+
step_name="launch_portacode_service",
|
|
2235
|
+
step_label=install_label,
|
|
2236
|
+
status="in_progress",
|
|
2237
|
+
message="Running sudo portacode service install…",
|
|
2238
|
+
phase="service",
|
|
2239
|
+
request_id=request_id,
|
|
2240
|
+
on_behalf_of_device=device_id,
|
|
2241
|
+
)
|
|
2242
|
+
|
|
2243
|
+
cmd = _su_command(payload_local["username"], "sudo -S portacode service install")
|
|
2244
|
+
res = _run_pct(vmid, cmd, input_text=payload_local["password"] + "\n")
|
|
2245
|
+
|
|
2246
|
+
if res["returncode"] != 0:
|
|
2247
|
+
_emit_progress_event(
|
|
2248
|
+
self,
|
|
2249
|
+
step_index=install_step,
|
|
2250
|
+
total_steps=total_steps,
|
|
2251
|
+
step_name="launch_portacode_service",
|
|
2252
|
+
step_label=install_label,
|
|
2253
|
+
status="failed",
|
|
2254
|
+
message=f"{install_label} failed: {res.get('stderr') or res.get('stdout')}",
|
|
2255
|
+
phase="service",
|
|
2256
|
+
request_id=request_id,
|
|
2257
|
+
details={
|
|
2258
|
+
"stderr": res.get("stderr"),
|
|
2259
|
+
"stdout": res.get("stdout"),
|
|
2260
|
+
},
|
|
2261
|
+
on_behalf_of_device=device_id,
|
|
2262
|
+
)
|
|
2263
|
+
raise RuntimeError(res.get("stderr") or res.get("stdout") or "Service install failed")
|
|
2264
|
+
|
|
2265
|
+
_emit_progress_event(
|
|
2266
|
+
self,
|
|
2267
|
+
step_index=install_step,
|
|
2268
|
+
total_steps=total_steps,
|
|
2269
|
+
step_name="launch_portacode_service",
|
|
2270
|
+
step_label=install_label,
|
|
2271
|
+
status="completed",
|
|
2272
|
+
message="Portacode service install finished.",
|
|
2273
|
+
phase="service",
|
|
2274
|
+
request_id=request_id,
|
|
2275
|
+
on_behalf_of_device=device_id,
|
|
2276
|
+
)
|
|
2277
|
+
|
|
2278
|
+
logger.info(
|
|
2279
|
+
"create_proxmox_container: portacode service install completed inside ct %s", vmid
|
|
2280
|
+
)
|
|
2281
|
+
|
|
2282
|
+
current_step_index += 2
|
|
2283
|
+
|
|
2284
|
+
_emit_host_event(
|
|
2285
|
+
self,
|
|
2286
|
+
{
|
|
2287
|
+
"event": "proxmox_container_created",
|
|
2288
|
+
"success": True,
|
|
2289
|
+
"message": f"Container {vmid} is ready and Portacode key captured.",
|
|
2290
|
+
"ctid": str(vmid),
|
|
2291
|
+
"public_key": public_key,
|
|
2292
|
+
"container": {
|
|
2293
|
+
"vmid": vmid,
|
|
2294
|
+
"hostname": payload_local["hostname"],
|
|
2295
|
+
"template": payload_local["template"],
|
|
2296
|
+
"storage": payload_local["storage"],
|
|
2297
|
+
"disk_gib": payload_local["disk_gib"],
|
|
2298
|
+
"ram_mib": payload_local["ram_mib"],
|
|
2299
|
+
"cpus": payload_local["cpus"],
|
|
2300
|
+
},
|
|
2301
|
+
"setup_steps": steps,
|
|
2302
|
+
"device_id": device_id,
|
|
2303
|
+
"on_behalf_of_device": device_id,
|
|
2304
|
+
"service_installed": service_installed,
|
|
2305
|
+
"request_id": request_id,
|
|
2306
|
+
},
|
|
2307
|
+
)
|
|
2308
|
+
except Exception as exc:
|
|
2309
|
+
if reservation_id and not created_record:
|
|
2310
|
+
_release_container_reservation(reservation_id)
|
|
2311
|
+
if vmid is not None and proxmox and node:
|
|
2312
|
+
_cleanup_failed_container(proxmox, node, vmid, provisioning_id)
|
|
2313
|
+
_remove_container_record(vmid)
|
|
2314
|
+
_emit_host_event(
|
|
2315
|
+
self,
|
|
2316
|
+
{
|
|
2317
|
+
"event": "error",
|
|
2318
|
+
"message": str(exc),
|
|
2319
|
+
"device_id": device_id,
|
|
2320
|
+
"request_id": request_id,
|
|
2115
2321
|
},
|
|
2116
|
-
on_behalf_of_device=device_id,
|
|
2117
2322
|
)
|
|
2118
|
-
raise RuntimeError(res.get("stderr") or res.get("stdout") or "Service install failed")
|
|
2119
|
-
|
|
2120
|
-
_emit_progress_event(
|
|
2121
|
-
self,
|
|
2122
|
-
step_index=install_step,
|
|
2123
|
-
total_steps=total_steps,
|
|
2124
|
-
step_name="launch_portacode_service",
|
|
2125
|
-
step_label=install_label,
|
|
2126
|
-
status="completed",
|
|
2127
|
-
message="Portacode service install finished.",
|
|
2128
|
-
phase="service",
|
|
2129
|
-
request_id=request_id,
|
|
2130
|
-
on_behalf_of_device=device_id,
|
|
2131
|
-
)
|
|
2132
|
-
|
|
2133
|
-
logger.info("create_proxmox_container: portacode service install completed inside ct %s", vmid)
|
|
2134
2323
|
|
|
2135
|
-
|
|
2324
|
+
threading.Thread(target=_provision_background, daemon=True).start()
|
|
2136
2325
|
|
|
2137
|
-
|
|
2138
|
-
"event": "
|
|
2326
|
+
return {
|
|
2327
|
+
"event": "proxmox_container_accepted",
|
|
2139
2328
|
"success": True,
|
|
2140
|
-
"message":
|
|
2141
|
-
"ctid": str(vmid),
|
|
2142
|
-
"public_key": public_key,
|
|
2143
|
-
"container": {
|
|
2144
|
-
"vmid": vmid,
|
|
2145
|
-
"hostname": payload["hostname"],
|
|
2146
|
-
"template": payload["template"],
|
|
2147
|
-
"storage": payload["storage"],
|
|
2148
|
-
"disk_gib": payload["disk_gib"],
|
|
2149
|
-
"ram_mib": payload["ram_mib"],
|
|
2150
|
-
"cpus": payload["cpus"],
|
|
2151
|
-
},
|
|
2152
|
-
"setup_steps": steps,
|
|
2329
|
+
"message": "Provisioning accepted; resources reserved.",
|
|
2153
2330
|
"device_id": device_id,
|
|
2154
|
-
"
|
|
2155
|
-
"service_installed": service_installed,
|
|
2331
|
+
"request_id": request_id,
|
|
2156
2332
|
}
|
|
2157
|
-
if not response:
|
|
2158
|
-
raise RuntimeError("create_proxmox_container produced no response payload")
|
|
2159
|
-
return response
|
|
2160
2333
|
|
|
2161
2334
|
|
|
2162
2335
|
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=rwm62A3_CwJ5c5GjxdVZUVMzSoUb2-9NpXonNAkuT7Y,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=dxn6m7buw_GHbhkhWX6J7gDpqCpXmmBxfqSb-6aQiPg,98542
|
|
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.dev8.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.dev8.dist-info/METADATA,sha256=BtpSzPdxbKOViRvQWUArwKnLV0OdUBm6EGKTI_ZkiSk,13051
|
|
95
|
+
portacode-1.4.17.dev8.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
96
|
+
portacode-1.4.17.dev8.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
97
|
+
portacode-1.4.17.dev8.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
98
|
+
portacode-1.4.17.dev8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|