portacode 1.4.15.dev7__py3-none-any.whl → 1.4.15.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.15.dev7'
32
- __version_tuple__ = version_tuple = (1, 4, 15, 'dev7')
31
+ __version__ = version = '1.4.15.dev8'
32
+ __version_tuple__ = version_tuple = (1, 4, 15, 'dev8')
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:
@@ -437,119 +504,48 @@ def _friendly_step_label(step_name: str) -> str:
437
504
  return normalized.capitalize()
438
505
 
439
506
 
440
- _PACKAGE_MANAGER_PROFILES: Dict[str, Dict[str, Any]] = {
441
- "apt": {
442
- "update_cmd": "apt-get update -y",
443
- "update_step_name": "apt_update",
444
- "install_cmd": "apt-get install -y python3 python3-pip sudo --fix-missing",
445
- "install_step_name": "install_deps",
446
- "update_retries": 4,
447
- "install_retries": 5,
448
- },
449
- "dnf": {
450
- "update_cmd": "dnf check-update || true",
451
- "update_step_name": "dnf_update",
452
- "install_cmd": "dnf install -y python3 python3-pip sudo",
453
- "install_step_name": "install_deps",
454
- "update_retries": 3,
455
- "install_retries": 5,
456
- },
457
- "yum": {
458
- "update_cmd": "yum makecache",
459
- "update_step_name": "yum_update",
460
- "install_cmd": "yum install -y python3 python3-pip sudo",
461
- "install_step_name": "install_deps",
462
- "update_retries": 3,
463
- "install_retries": 5,
464
- },
465
- "apk": {
466
- "update_cmd": "apk update",
467
- "update_step_name": "apk_update",
468
- "install_cmd": "apk add --no-cache python3 py3-pip sudo",
469
- "install_step_name": "install_deps",
470
- "update_retries": 3,
471
- "install_retries": 5,
472
- },
473
- "pacman": {
474
- "update_cmd": "pacman -Sy --noconfirm",
475
- "update_step_name": "pacman_update",
476
- "install_cmd": "pacman -S --noconfirm python python-pip sudo",
477
- "install_step_name": "install_deps",
478
- "update_retries": 3,
479
- "install_retries": 5,
480
- },
481
- "zypper": {
482
- "update_cmd": "zypper refresh",
483
- "update_step_name": "zypper_update",
484
- "install_cmd": "zypper install -y python3 python3-pip sudo",
485
- "install_step_name": "install_deps",
486
- "update_retries": 3,
487
- "install_retries": 5,
488
- },
489
- }
490
-
491
- _UPDATE_RETRY_ON = [
492
- "Temporary failure resolving",
493
- "Could not resolve",
494
- "Failed to fetch",
495
- ]
496
-
497
- _INSTALL_RETRY_ON = [
498
- "lock-frontend",
499
- "Unable to acquire the dpkg frontend lock",
500
- "Temporary failure resolving",
501
- "Could not resolve",
502
- "Failed to fetch",
503
- ]
504
-
505
-
506
507
  def _build_bootstrap_steps(
507
508
  user: str,
508
509
  password: str,
509
510
  ssh_key: str,
510
511
  include_portacode_connect: bool = True,
511
- package_manager: str = "apt",
512
512
  ) -> List[Dict[str, Any]]:
513
- profile = _PACKAGE_MANAGER_PROFILES.get(package_manager, _PACKAGE_MANAGER_PROFILES["apt"])
514
- steps: List[Dict[str, Any]] = []
515
- update_cmd = profile.get("update_cmd")
516
- if update_cmd:
517
- steps.append(
518
- {
519
- "name": profile.get("update_step_name", "package_update"),
520
- "cmd": update_cmd,
521
- "retries": profile.get("update_retries", 3),
522
- "retry_delay_s": 5,
523
- "retry_on": _UPDATE_RETRY_ON,
524
- }
525
- )
526
- install_cmd = profile.get("install_cmd")
527
- if install_cmd:
528
- steps.append(
529
- {
530
- "name": profile.get("install_step_name", "install_deps"),
531
- "cmd": install_cmd,
532
- "retries": profile.get("install_retries", 5),
533
- "retry_delay_s": 5,
534
- "retry_on": _INSTALL_RETRY_ON,
535
- }
536
- )
537
- steps.extend(
538
- [
539
- {"name": "user_exists", "cmd": f"id -u {user} >/dev/null 2>&1 || adduser --disabled-password --gecos '' {user}", "retries": 0},
540
- {"name": "add_sudo", "cmd": f"usermod -aG sudo {user}", "retries": 0},
541
- ]
542
- )
513
+ steps = [
514
+ {
515
+ "name": "apt_update",
516
+ "cmd": "apt-get update -y",
517
+ "retries": 4,
518
+ "retry_delay_s": 5,
519
+ "retry_on": [
520
+ "Temporary failure resolving",
521
+ "Could not resolve",
522
+ "Failed to fetch",
523
+ ],
524
+ },
525
+ {
526
+ "name": "install_deps",
527
+ "cmd": "apt-get install -y python3 python3-pip sudo --fix-missing",
528
+ "retries": 5,
529
+ "retry_delay_s": 5,
530
+ "retry_on": [
531
+ "lock-frontend",
532
+ "Unable to acquire the dpkg frontend lock",
533
+ "Temporary failure resolving",
534
+ "Could not resolve",
535
+ "Failed to fetch",
536
+ ],
537
+ },
538
+ {"name": "user_exists", "cmd": f"id -u {user} >/dev/null 2>&1 || adduser --disabled-password --gecos '' {user}", "retries": 0},
539
+ {"name": "add_sudo", "cmd": f"usermod -aG sudo {user}", "retries": 0},
540
+ ]
543
541
  if password:
544
542
  steps.append({"name": "set_password", "cmd": f"echo '{user}:{password}' | chpasswd", "retries": 0})
545
543
  if ssh_key:
546
- steps.append(
547
- {
548
- "name": "add_ssh_key",
549
- "cmd": f"install -d -m 700 /home/{user}/.ssh && echo '{ssh_key}' >> /home/{user}/.ssh/authorized_keys && chown -R {user}:{user} /home/{user}/.ssh",
550
- "retries": 0,
551
- }
552
- )
544
+ steps.append({
545
+ "name": "add_ssh_key",
546
+ "cmd": f"install -d -m 700 /home/{user}/.ssh && echo '{ssh_key}' >> /home/{user}/.ssh/authorized_keys && chown -R {user}:{user} /home/{user}/.ssh",
547
+ "retries": 0,
548
+ })
553
549
  steps.extend(
554
550
  [
555
551
  {"name": "pip_upgrade", "cmd": "python3 -m pip install --upgrade pip", "retries": 0},
@@ -561,24 +557,6 @@ def _build_bootstrap_steps(
561
557
  return steps
562
558
 
563
559
 
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)
579
- return "apt"
580
-
581
-
582
560
  def _get_storage_type(storages: Iterable[Dict[str, Any]], storage_name: str) -> str:
583
561
  for entry in storages:
584
562
  if entry.get("storage") == storage_name:
@@ -686,7 +664,7 @@ def _remove_container_record(vmid: int) -> None:
686
664
 
687
665
 
688
666
  def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) -> Dict[str, Any]:
689
- templates = config.get("templates") or []
667
+ templates = _get_startup_templates(config)
690
668
  default_template = templates[0] if templates else ""
691
669
  template = message.get("template") or default_template
692
670
  if not template:
@@ -1056,16 +1034,7 @@ def _bootstrap_portacode(
1056
1034
  total_steps: Optional[int] = None,
1057
1035
  default_public_key: Optional[str] = None,
1058
1036
  ) -> 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
- )
1037
+ actual_steps = steps if steps is not None else _build_bootstrap_steps(user, password, ssh_key)
1069
1038
  results, ok = _run_setup_steps(
1070
1039
  vmid,
1071
1040
  actual_steps,
@@ -1123,13 +1092,14 @@ def build_snapshot(config: Dict[str, Any]) -> Dict[str, Any]:
1123
1092
  "user": config.get("user"),
1124
1093
  "token_name": config.get("token_name"),
1125
1094
  "default_storage": config.get("default_storage"),
1126
- "templates": config.get("templates") or [],
1095
+ "templates": _get_startup_templates(config),
1127
1096
  "last_verified": config.get("last_verified"),
1128
1097
  "network": base_network,
1129
1098
  }
1130
1099
 
1131
1100
 
1132
1101
  def configure_infrastructure(token_identifier: str, token_value: str, verify_ssl: bool = False) -> Dict[str, Any]:
1102
+ _clear_startup_templates()
1133
1103
  ProxmoxAPI = _ensure_proxmoxer()
1134
1104
  user, token_name = _parse_token(token_identifier)
1135
1105
  client = ProxmoxAPI(
@@ -1165,12 +1135,12 @@ def configure_infrastructure(token_identifier: str, token_value: str, verify_ssl
1165
1135
  "token_value": token_value,
1166
1136
  "verify_ssl": verify_ssl,
1167
1137
  "default_storage": default_storage,
1168
- "templates": templates,
1169
1138
  "last_verified": datetime.utcnow().isoformat() + "Z",
1170
1139
  "network": network,
1171
1140
  "node_status": status,
1172
1141
  }
1173
1142
  _save_config(config)
1143
+ _ensure_startup_templates(config)
1174
1144
  snapshot = build_snapshot(config)
1175
1145
  snapshot["node_status"] = status
1176
1146
  snapshot["managed_containers"] = _get_managed_containers_summary(force=True)
@@ -1190,6 +1160,7 @@ def revert_infrastructure() -> Dict[str, Any]:
1190
1160
  _revert_bridge()
1191
1161
  if CONFIG_PATH.exists():
1192
1162
  CONFIG_PATH.unlink()
1163
+ _clear_startup_templates()
1193
1164
  snapshot = build_snapshot({})
1194
1165
  snapshot["network"] = snapshot.get("network", {})
1195
1166
  snapshot["network"]["applied"] = False
@@ -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.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=Xx0Po1xBg9CoeugBu19tIDntOjkrKAyq9NOKq8YEY_w,719
4
+ portacode/_version.py,sha256=wz7cM9xrg98SOlEfHeMTG7xaJCZ3Le5OiMq1M5iYGqc,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=QALeGkuvNdhpLidrEzZOVgowAXh3L2vk4YbdssGIHKk,64781
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.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.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.dev8.dist-info/METADATA,sha256=KL7PnPejNsSu4P3ATAsmYLVmFpw8kGml7RjJypiLvHQ,13051
95
+ portacode-1.4.15.dev8.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
+ portacode-1.4.15.dev8.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
+ portacode-1.4.15.dev8.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
+ portacode-1.4.15.dev8.dist-info/RECORD,,