portacode 1.4.17.dev1__py3-none-any.whl → 1.4.17.dev2__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.dev1'
32
- __version_tuple__ = version_tuple = (1, 4, 17, 'dev1')
31
+ __version__ = version = '1.4.17.dev2'
32
+ __version_tuple__ = version_tuple = (1, 4, 17, 'dev2')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -49,6 +49,7 @@ _MANAGED_CONTAINERS_CACHE_TTL_S = 30.0
49
49
  _MANAGED_CONTAINERS_CACHE: Dict[str, Any] = {"timestamp": 0.0, "summary": None}
50
50
  _MANAGED_CONTAINERS_CACHE_LOCK = threading.Lock()
51
51
  _CAPACITY_LOCK = threading.Lock()
52
+ _PENDING_ALLOCATIONS = {"ram_mib": 0.0, "disk_gib": 0.0, "cpu_share": 0.0}
52
53
  TEMPLATES_REFRESH_INTERVAL_S = 300
53
54
 
54
55
  ProgressCallback = Callable[[int, int, Dict[str, Any], str, Optional[Dict[str, Any]]], None]
@@ -721,6 +722,13 @@ def _build_full_container_summary(records: List[Dict[str, Any]], config: Dict[st
721
722
  "template": record.get("template") if record else None,
722
723
  "created_at": record.get("created_at") if record else None,
723
724
  }
725
+ # Fallback to recorded specs if live probing returned zeros (e.g., just-created CT).
726
+ if not merged["ram_mib"] and record:
727
+ merged["ram_mib"] = record.get("ram_mib") or merged["ram_mib"]
728
+ if not merged["disk_gib"] and record:
729
+ merged["disk_gib"] = record.get("disk_gib") or merged["disk_gib"]
730
+ if not merged["cpu_share"] and record:
731
+ merged["cpu_share"] = record.get("cpus") or merged["cpu_share"]
724
732
  managed_entries.append(merged)
725
733
  else:
726
734
  unmanaged_entries.append(base_entry)
@@ -783,20 +791,24 @@ def _compute_free_resources(summary: Dict[str, Any]) -> Dict[str, float]:
783
791
  free_ram = None
784
792
  free_disk = None
785
793
  free_cpu = None
794
+ with _CAPACITY_LOCK:
795
+ pending_ram = float(_PENDING_ALLOCATIONS["ram_mib"])
796
+ pending_disk = float(_PENDING_ALLOCATIONS["disk_gib"])
797
+ pending_cpu = float(_PENDING_ALLOCATIONS["cpu_share"])
786
798
  host_ram = summary.get("host_total_ram_mib")
787
799
  alloc_ram = summary.get("allocated_ram_mib")
788
800
  if host_ram is not None and alloc_ram is not None:
789
- free_ram = max(float(host_ram) - float(alloc_ram), 0.0)
801
+ free_ram = max(float(host_ram) - float(alloc_ram) - pending_ram, 0.0)
790
802
 
791
803
  host_disk = summary.get("host_total_disk_gib")
792
804
  alloc_disk = summary.get("allocated_disk_gib")
793
805
  if host_disk is not None and alloc_disk is not None:
794
- free_disk = max(float(host_disk) - float(alloc_disk), 0.0)
806
+ free_disk = max(float(host_disk) - float(alloc_disk) - pending_disk, 0.0)
795
807
 
796
808
  host_cpu = summary.get("host_total_cpu_cores")
797
809
  alloc_cpu = summary.get("allocated_cpu_share")
798
810
  if host_cpu is not None and alloc_cpu is not None:
799
- free_cpu = max(float(host_cpu) - float(alloc_cpu), 0.0)
811
+ free_cpu = max(float(host_cpu) - float(alloc_cpu) - pending_cpu, 0.0)
800
812
 
801
813
  return {
802
814
  "ram_mib": free_ram,
@@ -1155,21 +1167,30 @@ def _start_container(proxmox: Any, node: str, vmid: int) -> Tuple[Dict[str, Any]
1155
1167
  return status, 0.0
1156
1168
 
1157
1169
  # Validate capacity using the same math as the dashboard and serialize allocation.
1170
+ summary = _get_managed_containers_summary(force=True)
1171
+ cfg = proxmox.nodes(node).lxc(vmid).config.get()
1172
+ payload = {
1173
+ "ram_mib": _to_mib(cfg.get("memory")),
1174
+ "disk_gib": _pick_container_disk_gib("lxc", cfg, {"rootfs": cfg.get("rootfs")}),
1175
+ "cpus": _pick_container_cpu_share("lxc", cfg, {}),
1176
+ }
1158
1177
  with _CAPACITY_LOCK:
1159
- summary = _get_managed_containers_summary(force=True)
1160
- cfg = proxmox.nodes(node).lxc(vmid).config.get()
1161
- payload = {
1162
- "ram_mib": _to_mib(cfg.get("memory")),
1163
- "disk_gib": _pick_container_disk_gib("lxc", cfg, {"rootfs": cfg.get("rootfs")}),
1164
- "cpus": _pick_container_cpu_share("lxc", cfg, {}),
1165
- }
1166
1178
  try:
1167
1179
  _assert_capacity_for_payload(payload, summary)
1168
1180
  except RuntimeError as exc:
1169
1181
  raise RuntimeError(f"Not enough resources to start container {vmid}: {exc}") from exc
1182
+ _PENDING_ALLOCATIONS["ram_mib"] += float(payload["ram_mib"] or 0)
1183
+ _PENDING_ALLOCATIONS["disk_gib"] += float(payload["disk_gib"] or 0)
1184
+ _PENDING_ALLOCATIONS["cpu_share"] += float(payload["cpus"] or 0)
1170
1185
 
1171
- upid = proxmox.nodes(node).lxc(vmid).status.start.post()
1172
- return _wait_for_task(proxmox, node, upid)
1186
+ try:
1187
+ upid = proxmox.nodes(node).lxc(vmid).status.start.post()
1188
+ return _wait_for_task(proxmox, node, upid)
1189
+ finally:
1190
+ with _CAPACITY_LOCK:
1191
+ _PENDING_ALLOCATIONS["ram_mib"] -= float(payload["ram_mib"] or 0)
1192
+ _PENDING_ALLOCATIONS["disk_gib"] -= float(payload["disk_gib"] or 0)
1193
+ _PENDING_ALLOCATIONS["cpu_share"] -= float(payload["cpus"] or 0)
1173
1194
 
1174
1195
 
1175
1196
  def _stop_container(proxmox: Any, node: str, vmid: int) -> Tuple[Dict[str, Any], float]:
@@ -1896,22 +1917,31 @@ class CreateProxmoxContainerHandler(SyncHandler):
1896
1917
  _validate_environment,
1897
1918
  )
1898
1919
 
1899
- def _create_container():
1900
- proxmox = _connect_proxmox(config)
1901
- node = config.get("node") or DEFAULT_NODE_NAME
1902
- payload = _build_container_payload(message, config)
1903
- payload["cpulimit"] = float(payload["cpus"])
1904
- payload["cores"] = int(max(math.ceil(payload["cpus"]), 1))
1905
- payload["memory"] = int(payload["ram_mib"])
1906
- payload["node"] = node
1907
- # Validate against current free resources (same math as dashboard charts) with serialization.
1908
- with _CAPACITY_LOCK:
1909
- summary = _get_managed_containers_summary(force=True)
1910
- try:
1911
- _assert_capacity_for_payload(payload, summary)
1912
- except RuntimeError as exc:
1913
- raise RuntimeError(f"Not enough resources to create the container safely: {exc}") from exc
1914
- vmid, _ = _instantiate_container(proxmox, node, payload)
1920
+ def _create_container():
1921
+ proxmox = _connect_proxmox(config)
1922
+ node = config.get("node") or DEFAULT_NODE_NAME
1923
+ payload = _build_container_payload(message, config)
1924
+ payload["cpulimit"] = float(payload["cpus"])
1925
+ payload["cores"] = int(max(math.ceil(payload["cpus"]), 1))
1926
+ payload["memory"] = int(payload["ram_mib"])
1927
+ payload["node"] = node
1928
+ # Validate against current free resources (same math as dashboard charts) and place a short-lived reservation.
1929
+ summary = _get_managed_containers_summary(force=True)
1930
+ with _CAPACITY_LOCK:
1931
+ try:
1932
+ _assert_capacity_for_payload(payload, summary)
1933
+ except RuntimeError as exc:
1934
+ raise RuntimeError(f"Not enough resources to create the container safely: {exc}") from exc
1935
+ _PENDING_ALLOCATIONS["ram_mib"] += float(payload["ram_mib"] or 0)
1936
+ _PENDING_ALLOCATIONS["disk_gib"] += float(payload["disk_gib"] or 0)
1937
+ _PENDING_ALLOCATIONS["cpu_share"] += float(payload["cpus"] or 0)
1938
+ try:
1939
+ vmid, _ = _instantiate_container(proxmox, node, payload)
1940
+ finally:
1941
+ with _CAPACITY_LOCK:
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)
1915
1945
  logger.debug(
1916
1946
  "Provisioning container node=%s template=%s ram=%s cpu=%s storage=%s",
1917
1947
  node,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.17.dev1
3
+ Version: 1.4.17.dev2
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=30JbXF45c9c1UEsvLScl1Hcz0g1qPk9RGMCBErXUK0c,719
4
+ portacode/_version.py,sha256=tY1iaGi0436VvMeNiABna_erno3lFewWo4mlsbvOmsg,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=afndfVsjcHWHsa8F2PkRnHmc67c39k5VMmJeUoSSnok,87601
25
+ portacode/connection/handlers/proxmox_infra.py,sha256=_v-qj-7zupk6egmed1AN_6iEhpL2k5hVa0q6-iZyK2I,89330
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.dev1.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
68
+ portacode-1.4.17.dev2.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.dev1.dist-info/METADATA,sha256=fiXLmOIToLe48UzncEAQCZnpib1ITZpl0gS06M5fmFA,13051
95
- portacode-1.4.17.dev1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
- portacode-1.4.17.dev1.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
- portacode-1.4.17.dev1.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
- portacode-1.4.17.dev1.dist-info/RECORD,,
94
+ portacode-1.4.17.dev2.dist-info/METADATA,sha256=dX2o0Oj7K-LojM-kwShhU7XEWqV-Lw30xCImcbfc-wU,13051
95
+ portacode-1.4.17.dev2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
+ portacode-1.4.17.dev2.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
+ portacode-1.4.17.dev2.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
+ portacode-1.4.17.dev2.dist-info/RECORD,,