portacode 1.4.15.dev7__py3-none-any.whl → 1.4.15.dev9__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.15.dev7'
32
- __version_tuple__ = version_tuple = (1, 4, 15, 'dev7')
31
+ __version__ = version = '1.4.15.dev9'
32
+ __version_tuple__ = version_tuple = (1, 4, 15, 'dev9')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -47,6 +47,9 @@ UNIT_DIR = Path("/etc/systemd/system")
47
47
  _MANAGED_CONTAINERS_CACHE_TTL_S = 30.0
48
48
  _MANAGED_CONTAINERS_CACHE: Dict[str, Any] = {"timestamp": 0.0, "summary": None}
49
49
  _MANAGED_CONTAINERS_CACHE_LOCK = threading.Lock()
50
+ _STARTUP_TEMPLATES: List[str] | None = None
51
+ _STARTUP_TEMPLATE_KEY: tuple[Any, ...] | None = None
52
+ _STARTUP_TEMPLATES_LOCK = threading.Lock()
50
53
 
51
54
  ProgressCallback = Callable[[int, int, Dict[str, Any], str, Optional[Dict[str, Any]]], None]
52
55
 
@@ -179,6 +182,70 @@ def _list_templates(client: Any, node: str, storages: Iterable[Dict[str, Any]])
179
182
  return templates
180
183
 
181
184
 
185
+ def _template_key(config: Dict[str, Any]) -> tuple[Any, ...]:
186
+ return (
187
+ config.get("host"),
188
+ config.get("node"),
189
+ config.get("user"),
190
+ config.get("token_name"),
191
+ config.get("verify_ssl"),
192
+ )
193
+
194
+
195
+ def _build_proxmox_client_from_config(config: Dict[str, Any]):
196
+ user = config.get("user")
197
+ token_name = config.get("token_name")
198
+ token_value = config.get("token_value")
199
+ if not user or not token_name or not token_value:
200
+ raise RuntimeError("Proxmox API credentials are missing")
201
+ ProxmoxAPI = _ensure_proxmoxer()
202
+ return ProxmoxAPI(
203
+ config.get("host", DEFAULT_HOST),
204
+ user=user,
205
+ token_name=token_name,
206
+ token_value=token_value,
207
+ verify_ssl=config.get("verify_ssl", False),
208
+ timeout=30,
209
+ )
210
+
211
+
212
+ def _ensure_startup_templates(config: Dict[str, Any]) -> None:
213
+ global _STARTUP_TEMPLATES, _STARTUP_TEMPLATE_KEY
214
+ if not config:
215
+ with _STARTUP_TEMPLATES_LOCK:
216
+ _STARTUP_TEMPLATES = []
217
+ _STARTUP_TEMPLATE_KEY = None
218
+ return
219
+ with _STARTUP_TEMPLATES_LOCK:
220
+ if _STARTUP_TEMPLATE_KEY == _template_key(config) and _STARTUP_TEMPLATES is not None:
221
+ return
222
+ key = _template_key(config)
223
+ templates: List[str] = []
224
+ try:
225
+ client = _build_proxmox_client_from_config(config)
226
+ node = config.get("node") or _pick_node(client)
227
+ storages = client.nodes(node).storage.get()
228
+ templates = _list_templates(client, node, storages)
229
+ except Exception as exc:
230
+ logger.warning("Unable to refresh Proxmox templates: %s", exc)
231
+ with _STARTUP_TEMPLATES_LOCK:
232
+ _STARTUP_TEMPLATES = list(templates)
233
+ _STARTUP_TEMPLATE_KEY = key
234
+
235
+
236
+ def _get_startup_templates(config: Dict[str, Any]) -> List[str]:
237
+ _ensure_startup_templates(config)
238
+ with _STARTUP_TEMPLATES_LOCK:
239
+ return list(_STARTUP_TEMPLATES or [])
240
+
241
+
242
+ def _clear_startup_templates() -> None:
243
+ global _STARTUP_TEMPLATES, _STARTUP_TEMPLATE_KEY
244
+ with _STARTUP_TEMPLATES_LOCK:
245
+ _STARTUP_TEMPLATES = None
246
+ _STARTUP_TEMPLATE_KEY = None
247
+
248
+
182
249
  def _pick_storage(storages: Iterable[Dict[str, Any]]) -> str:
183
250
  candidates = [s for s in storages if "rootdir" in s.get("content", "") and s.get("avail", 0) > 0]
184
251
  if not candidates:
@@ -561,21 +628,24 @@ def _build_bootstrap_steps(
561
628
  return steps
562
629
 
563
630
 
564
- def _detect_package_manager(vmid: int) -> str:
565
- candidates = [
566
- ("apt", "apt-get"),
567
- ("dnf", "dnf"),
568
- ("yum", "yum"),
569
- ("apk", "apk"),
570
- ("pacman", "pacman"),
571
- ("zypper", "zypper"),
572
- ]
573
- for name, binary in candidates:
574
- res = _run_pct(vmid, f"command -v {binary} >/dev/null 2>&1")
575
- if res.get("returncode") == 0:
576
- logger.debug("Detected package manager %s inside container %s", name, vmid)
577
- return name
578
- logger.warning("Unable to detect package manager inside container %s; defaulting to apt", vmid)
631
+ def _guess_package_manager_from_template(template: str) -> str:
632
+ normalized = (template or "").lower()
633
+ if "alpine" in normalized:
634
+ return "apk"
635
+ if "archlinux" in normalized:
636
+ return "pacman"
637
+ if "centos-7" in normalized:
638
+ return "yum"
639
+ if any(keyword in normalized for keyword in ("centos-8", "centos-9", "centos-9-stream", "centos-8-stream")):
640
+ return "dnf"
641
+ if any(keyword in normalized for keyword in ("rockylinux", "almalinux", "fedora")):
642
+ return "dnf"
643
+ if "opensuse" in normalized or "suse" in normalized:
644
+ return "zypper"
645
+ if any(keyword in normalized for keyword in ("debian", "ubuntu", "devuan", "turnkeylinux")):
646
+ return "apt"
647
+ if normalized.startswith("system/") and "linux" in normalized:
648
+ return "apt"
579
649
  return "apt"
580
650
 
581
651
 
@@ -686,7 +756,7 @@ def _remove_container_record(vmid: int) -> None:
686
756
 
687
757
 
688
758
  def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) -> Dict[str, Any]:
689
- templates = config.get("templates") or []
759
+ templates = _get_startup_templates(config)
690
760
  default_template = templates[0] if templates else ""
691
761
  template = message.get("template") or default_template
692
762
  if not template:
@@ -1056,16 +1126,7 @@ def _bootstrap_portacode(
1056
1126
  total_steps: Optional[int] = None,
1057
1127
  default_public_key: Optional[str] = None,
1058
1128
  ) -> Tuple[str, List[Dict[str, Any]]]:
1059
- if steps is not None:
1060
- actual_steps = steps
1061
- else:
1062
- package_manager = _detect_package_manager(vmid)
1063
- actual_steps = _build_bootstrap_steps(
1064
- user,
1065
- password,
1066
- ssh_key,
1067
- package_manager=package_manager,
1068
- )
1129
+ actual_steps = steps if steps is not None else _build_bootstrap_steps(user, password, ssh_key)
1069
1130
  results, ok = _run_setup_steps(
1070
1131
  vmid,
1071
1132
  actual_steps,
@@ -1123,13 +1184,14 @@ def build_snapshot(config: Dict[str, Any]) -> Dict[str, Any]:
1123
1184
  "user": config.get("user"),
1124
1185
  "token_name": config.get("token_name"),
1125
1186
  "default_storage": config.get("default_storage"),
1126
- "templates": config.get("templates") or [],
1187
+ "templates": _get_startup_templates(config),
1127
1188
  "last_verified": config.get("last_verified"),
1128
1189
  "network": base_network,
1129
1190
  }
1130
1191
 
1131
1192
 
1132
1193
  def configure_infrastructure(token_identifier: str, token_value: str, verify_ssl: bool = False) -> Dict[str, Any]:
1194
+ _clear_startup_templates()
1133
1195
  ProxmoxAPI = _ensure_proxmoxer()
1134
1196
  user, token_name = _parse_token(token_identifier)
1135
1197
  client = ProxmoxAPI(
@@ -1165,12 +1227,12 @@ def configure_infrastructure(token_identifier: str, token_value: str, verify_ssl
1165
1227
  "token_value": token_value,
1166
1228
  "verify_ssl": verify_ssl,
1167
1229
  "default_storage": default_storage,
1168
- "templates": templates,
1169
1230
  "last_verified": datetime.utcnow().isoformat() + "Z",
1170
1231
  "network": network,
1171
1232
  "node_status": status,
1172
1233
  }
1173
1234
  _save_config(config)
1235
+ _ensure_startup_templates(config)
1174
1236
  snapshot = build_snapshot(config)
1175
1237
  snapshot["node_status"] = status
1176
1238
  snapshot["managed_containers"] = _get_managed_containers_summary(force=True)
@@ -1190,6 +1252,7 @@ def revert_infrastructure() -> Dict[str, Any]:
1190
1252
  _revert_bridge()
1191
1253
  if CONFIG_PATH.exists():
1192
1254
  CONFIG_PATH.unlink()
1255
+ _clear_startup_templates()
1193
1256
  snapshot = build_snapshot({})
1194
1257
  snapshot["network"] = snapshot.get("network", {})
1195
1258
  snapshot["network"]["applied"] = False
@@ -1250,12 +1313,17 @@ class CreateProxmoxContainerHandler(SyncHandler):
1250
1313
  device_public_key = (message.get("device_public_key") or "").strip()
1251
1314
  device_private_key = (message.get("device_private_key") or "").strip()
1252
1315
  has_device_keypair = bool(device_public_key and device_private_key)
1316
+ config_guess = _load_config()
1317
+ template_candidates = _get_startup_templates(config_guess)
1318
+ template_hint = (message.get("template") or (template_candidates[0] if template_candidates else "")).strip()
1319
+ package_manager = _guess_package_manager_from_template(template_hint)
1253
1320
  bootstrap_user, bootstrap_password, bootstrap_ssh_key = _get_provisioning_user_info(message)
1254
1321
  bootstrap_steps = _build_bootstrap_steps(
1255
1322
  bootstrap_user,
1256
1323
  bootstrap_password,
1257
1324
  bootstrap_ssh_key,
1258
1325
  include_portacode_connect=not has_device_keypair,
1326
+ package_manager=package_manager,
1259
1327
  )
1260
1328
  total_steps = 3 + len(bootstrap_steps) + 2
1261
1329
  current_step_index = 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.15.dev7
3
+ Version: 1.4.15.dev9
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=Xx0Po1xBg9CoeugBu19tIDntOjkrKAyq9NOKq8YEY_w,719
4
+ portacode/_version.py,sha256=YbH9Im7cDkYWkWFiH-JGJ5BzJGjRVMg1TyCC8DAh1fE,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=GXPyDcdwgtTfQyw7msAARAmHGun-p8GJOdtV0LL5iuQ,65513
25
+ portacode/connection/handlers/proxmox_infra.py,sha256=g3AXQ_SQ5-WDH0ixDMtowqPtod6ruTrPd7_6FkMp1ts,68206
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.15.dev7.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
68
+ portacode-1.4.15.dev9.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.15.dev7.dist-info/METADATA,sha256=YHefdl_WhByeUv5ELxOcmpCdhID0lj04wJTl57e3O8A,13051
95
- portacode-1.4.15.dev7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
- portacode-1.4.15.dev7.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
- portacode-1.4.15.dev7.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
- portacode-1.4.15.dev7.dist-info/RECORD,,
94
+ portacode-1.4.15.dev9.dist-info/METADATA,sha256=6lnCia50lWblHLbiGKsxzaOVohX8yzayZgrAB8Hz-CY,13051
95
+ portacode-1.4.15.dev9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
+ portacode-1.4.15.dev9.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
+ portacode-1.4.15.dev9.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
+ portacode-1.4.15.dev9.dist-info/RECORD,,