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 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.dev6'
32
- __version_tuple__ = version_tuple = (1, 4, 17, 'dev6')
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
- _MANAGED_CONTAINERS_CACHE_TTL_S = 30.0
49
- _MANAGED_CONTAINERS_CACHE: Dict[str, Any] = {"timestamp": 0.0, "summary": None}
50
- _MANAGED_CONTAINERS_CACHE_LOCK = threading.Lock()
51
- _CAPACITY_LOCK = threading.Lock()
52
- _PENDING_ALLOCATIONS = {"ram_mib": 0.0, "disk_gib": 0.0, "cpu_share": 0.0}
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 _invalidate_managed_containers_cache() -> None:
587
- with _MANAGED_CONTAINERS_CACHE_LOCK:
588
- _MANAGED_CONTAINERS_CACHE["timestamp"] = 0.0
589
- _MANAGED_CONTAINERS_CACHE["summary"] = None
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 _compute_free_resources(summary: Dict[str, Any]) -> Dict[str, float]:
797
- """Return free resources using the same math as the dashboard."""
798
- free_ram = None
799
- free_disk = None
800
- free_cpu = None
801
- with _CAPACITY_LOCK:
802
- pending_ram = float(_PENDING_ALLOCATIONS["ram_mib"])
803
- pending_disk = float(_PENDING_ALLOCATIONS["disk_gib"])
804
- pending_cpu = float(_PENDING_ALLOCATIONS["cpu_share"])
805
- host_ram = summary.get("host_total_ram_mib")
806
- alloc_ram = summary.get("allocated_ram_mib")
807
- if host_ram is not None and alloc_ram is not None:
808
- free_ram = max(float(host_ram) - float(alloc_ram) - pending_ram, 0.0)
809
-
810
- host_disk = summary.get("host_total_disk_gib")
811
- alloc_disk = summary.get("allocated_disk_gib")
812
- if host_disk is not None and alloc_disk is not None:
813
- free_disk = max(float(host_disk) - float(alloc_disk) - pending_disk, 0.0)
814
-
815
- host_cpu = summary.get("host_total_cpu_cores")
816
- alloc_cpu = summary.get("allocated_cpu_share")
817
- if host_cpu is not None and alloc_cpu is not None:
818
- free_cpu = max(float(host_cpu) - float(alloc_cpu) - pending_cpu, 0.0)
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
- if shortages:
848
- raise RuntimeError(f"Insufficient resources: {', '.join(shortages)}")
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 _get_managed_containers_summary(force: bool = False) -> Dict[str, Any]:
852
- def _refresh_container_statuses(records: List[Dict[str, Any]], config: Dict[str, Any] | None) -> None:
853
- if not records or not config:
854
- return
855
- try:
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
- # Validate capacity using the same math as the dashboard and serialize allocation.
1177
- summary = _get_managed_containers_summary(force=True)
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 _write_container_record(vmid: int, payload: Dict[str, Any]) -> None:
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
- _invalidate_managed_containers_cache()
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
- _invalidate_managed_containers_cache()
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(proxmox: Any, node: str, payload: Dict[str, Any]) -> Tuple[int, float]:
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
- def _create_container():
1928
- proxmox = _connect_proxmox(config)
1929
- node = config.get("node") or DEFAULT_NODE_NAME
1930
- payload = _build_container_payload(message, config)
1931
- payload["cpulimit"] = float(payload["cpus"])
1932
- payload["cores"] = int(max(math.ceil(payload["cpus"]), 1))
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
- public_key, steps = _bootstrap_portacode(
2028
- vmid,
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
- 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,
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
- auth_step_name = "setup_device_authentication"
2057
- auth_label = "Setting up device authentication"
2058
- _emit_progress_event(
2059
- self,
2060
- step_index=service_start_index,
2061
- total_steps=total_steps,
2062
- step_name=auth_step_name,
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,
2069
- )
2070
- _emit_progress_event(
2071
- self,
2072
- step_index=service_start_index,
2073
- total_steps=total_steps,
2074
- step_name=auth_step_name,
2075
- step_label=auth_label,
2076
- status="completed",
2077
- message="Authentication metadata recorded.",
2078
- phase="service",
2079
- request_id=request_id,
2080
- on_behalf_of_device=device_id,
2081
- )
2082
-
2083
- install_step = service_start_index + 1
2084
- install_label = "Launching Portacode service"
2085
- _emit_progress_event(
2086
- self,
2087
- step_index=install_step,
2088
- total_steps=total_steps,
2089
- step_name="launch_portacode_service",
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,
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
- cmd = _su_command(payload["username"], "sudo -S portacode service install")
2099
- res = _run_pct(vmid, cmd, input_text=payload["password"] + "\n")
2120
+ def _start_container_step():
2121
+ _start_container(proxmox, node, vmid)
2100
2122
 
2101
- if res["returncode"] != 0:
2102
- _emit_progress_event(
2103
- self,
2104
- step_index=install_step,
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
- step_name="launch_portacode_service",
2107
- step_label=install_label,
2108
- status="failed",
2109
- message=f"{install_label} failed: {res.get('stderr') or res.get('stdout')}",
2110
- phase="service",
2111
- request_id=request_id,
2112
- details={
2113
- "stderr": res.get("stderr"),
2114
- "stdout": res.get("stdout"),
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
- current_step_index += 2
2324
+ threading.Thread(target=_provision_background, daemon=True).start()
2136
2325
 
2137
- response = {
2138
- "event": "proxmox_container_created",
2326
+ return {
2327
+ "event": "proxmox_container_accepted",
2139
2328
  "success": True,
2140
- "message": f"Container {vmid} is ready and Portacode key captured.",
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
- "on_behalf_of_device": device_id,
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.17.dev6
3
+ Version: 1.4.17.dev8
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -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=BieguAdHtQb8jxJDq07EaNCiTUct5ZU-e1HrKSeSOHQ,719
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=nOKhFs7JgpYVIfZs6xSaIf7qFVCX3B4BACrrXBXO8nQ,89884
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.dev6.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
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.dev6.dist-info/METADATA,sha256=Q2yCzBl8-toCB8tJtzryZ878HCCyl6FaMqrRmvw9SNI,13051
95
- portacode-1.4.17.dev6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
- portacode-1.4.17.dev6.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
- portacode-1.4.17.dev6.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
- portacode-1.4.17.dev6.dist-info/RECORD,,
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,,