portacode 1.4.16.dev11__py3-none-any.whl → 1.4.17.dev0__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.16.dev11'
32
- __version_tuple__ = version_tuple = (1, 4, 16, 'dev11')
31
+ __version__ = version = '1.4.17.dev0'
32
+ __version_tuple__ = version_tuple = (1, 4, 17, 'dev0')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -777,6 +777,57 @@ def _build_full_container_summary(records: List[Dict[str, Any]], config: Dict[st
777
777
  return summary
778
778
 
779
779
 
780
+ def _compute_free_resources(summary: Dict[str, Any]) -> Dict[str, float]:
781
+ """Return free resources using the same math as the dashboard."""
782
+ free_ram = None
783
+ free_disk = None
784
+ free_cpu = None
785
+ host_ram = summary.get("host_total_ram_mib")
786
+ alloc_ram = summary.get("allocated_ram_mib")
787
+ if host_ram is not None and alloc_ram is not None:
788
+ free_ram = max(float(host_ram) - float(alloc_ram), 0.0)
789
+
790
+ host_disk = summary.get("host_total_disk_gib")
791
+ alloc_disk = summary.get("allocated_disk_gib")
792
+ if host_disk is not None and alloc_disk is not None:
793
+ free_disk = max(float(host_disk) - float(alloc_disk), 0.0)
794
+
795
+ host_cpu = summary.get("host_total_cpu_cores")
796
+ alloc_cpu = summary.get("allocated_cpu_share")
797
+ if host_cpu is not None and alloc_cpu is not None:
798
+ free_cpu = max(float(host_cpu) - float(alloc_cpu), 0.0)
799
+
800
+ return {
801
+ "ram_mib": free_ram,
802
+ "disk_gib": free_disk,
803
+ "cpu_share": free_cpu,
804
+ }
805
+
806
+
807
+ def _assert_capacity_for_payload(payload: Dict[str, Any], summary: Dict[str, Any]) -> None:
808
+ """Validate requested container resources against current free capacity."""
809
+ free = _compute_free_resources(summary)
810
+ shortages: List[str] = []
811
+
812
+ req_ram = float(payload.get("ram_mib", 0))
813
+ free_ram = free.get("ram_mib")
814
+ if free_ram is not None and req_ram > free_ram:
815
+ shortages.append(f"RAM (need {int(req_ram)} MiB, free {int(free_ram)} MiB)")
816
+
817
+ req_disk = float(payload.get("disk_gib", 0))
818
+ free_disk = free.get("disk_gib")
819
+ if free_disk is not None and req_disk > free_disk:
820
+ shortages.append(f"Disk (need {req_disk:.1f} GiB, free {free_disk:.1f} GiB)")
821
+
822
+ req_cpu = float(payload.get("cpus", 0))
823
+ free_cpu = free.get("cpu_share")
824
+ if free_cpu is not None and req_cpu > free_cpu:
825
+ shortages.append(f"CPU (need {req_cpu:.2f} vCPU, free {free_cpu:.2f} vCPU)")
826
+
827
+ if shortages:
828
+ raise RuntimeError(f"Insufficient resources: {', '.join(shortages)}")
829
+
830
+
780
831
  def _get_managed_containers_summary(force: bool = False) -> Dict[str, Any]:
781
832
  def _refresh_container_statuses(records: List[Dict[str, Any]], config: Dict[str, Any] | None) -> None:
782
833
  if not records or not config:
@@ -1102,22 +1153,18 @@ def _start_container(proxmox: Any, node: str, vmid: int) -> Tuple[Dict[str, Any]
1102
1153
  logger.info("Container %s already running (%ss)", vmid, uptime)
1103
1154
  return status, 0.0
1104
1155
 
1105
- node_status = proxmox.nodes(node).status.get()
1106
- mem_total_mb = int(node_status.get("memory", {}).get("total", 0) // (1024**2))
1107
- cores_total = int(node_status.get("cpuinfo", {}).get("cores", 0))
1108
-
1109
- running = _list_running_managed(proxmox, node)
1110
- used_mem_mb = sum(int(cfg.get("memory", 0)) for _, cfg in running)
1111
- used_cores = sum(int(cfg.get("cores", 0)) for _, cfg in running)
1112
-
1113
- target_cfg = proxmox.nodes(node).lxc(vmid).config.get()
1114
- target_mem_mb = int(target_cfg.get("memory", 0))
1115
- target_cores = int(target_cfg.get("cores", 0))
1116
-
1117
- if mem_total_mb and used_mem_mb + target_mem_mb > mem_total_mb:
1118
- raise RuntimeError("Not enough RAM to start this container safely.")
1119
- if cores_total and used_cores + target_cores > cores_total:
1120
- raise RuntimeError("Not enough CPU cores to start this container safely.")
1156
+ # Validate capacity using the same math as the dashboard.
1157
+ summary = _get_managed_containers_summary(force=True)
1158
+ cfg = proxmox.nodes(node).lxc(vmid).config.get()
1159
+ payload = {
1160
+ "ram_mib": _to_mib(cfg.get("memory")),
1161
+ "disk_gib": _pick_container_disk_gib("lxc", cfg, {"rootfs": cfg.get("rootfs")}),
1162
+ "cpus": _pick_container_cpu_share("lxc", cfg, {}),
1163
+ }
1164
+ try:
1165
+ _assert_capacity_for_payload(payload, summary)
1166
+ except RuntimeError as exc:
1167
+ raise RuntimeError(f"Not enough resources to start container {vmid}: {exc}") from exc
1121
1168
 
1122
1169
  upid = proxmox.nodes(node).lxc(vmid).status.start.post()
1123
1170
  return _wait_for_task(proxmox, node, upid)
@@ -1855,6 +1902,12 @@ class CreateProxmoxContainerHandler(SyncHandler):
1855
1902
  payload["cores"] = int(max(math.ceil(payload["cpus"]), 1))
1856
1903
  payload["memory"] = int(payload["ram_mib"])
1857
1904
  payload["node"] = node
1905
+ # Validate against current free resources (same math as dashboard charts).
1906
+ summary = _get_managed_containers_summary(force=True)
1907
+ try:
1908
+ _assert_capacity_for_payload(payload, summary)
1909
+ except RuntimeError as exc:
1910
+ raise RuntimeError(f"Not enough resources to create the container safely: {exc}") from exc
1858
1911
  logger.debug(
1859
1912
  "Provisioning container node=%s template=%s ram=%s cpu=%s storage=%s",
1860
1913
  node,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.16.dev11
3
+ Version: 1.4.17.dev0
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=ZsUlJUljVebwmjOdkxbUPcwi8iyVjIuyBU9g68awYpA,721
4
+ portacode/_version.py,sha256=bQ-FAL2ble5i7ZHjLXfiacqJWiDcpjxBVD73svWzbjg,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=-zAmaOTCTJaic8sclb5Z0DVCl_pU7XePo86NKohi3gc,85295
25
+ portacode/connection/handlers/proxmox_infra.py,sha256=GjGB0BICYR5eI-tUYwg7p2qnBWvLu_UcMshP5XTTLr0,87397
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.16.dev11.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
68
+ portacode-1.4.17.dev0.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.16.dev11.dist-info/METADATA,sha256=nEvtzLLbq9J8jG-kubMsNMRQ07YdtEczInIarfWpGhY,13052
95
- portacode-1.4.16.dev11.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
- portacode-1.4.16.dev11.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
- portacode-1.4.16.dev11.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
- portacode-1.4.16.dev11.dist-info/RECORD,,
94
+ portacode-1.4.17.dev0.dist-info/METADATA,sha256=F-YQ9c2OK7Ck8xlpNpfdypteq_TEfZMKpB-HGLVhllg,13051
95
+ portacode-1.4.17.dev0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
+ portacode-1.4.17.dev0.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
+ portacode-1.4.17.dev0.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
+ portacode-1.4.17.dev0.dist-info/RECORD,,