portacode 1.4.15.dev10__py3-none-any.whl → 1.4.15.dev23__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 +2 -2
- portacode/connection/handlers/proxmox_infra.py +262 -53
- {portacode-1.4.15.dev10.dist-info → portacode-1.4.15.dev23.dist-info}/METADATA +1 -1
- {portacode-1.4.15.dev10.dist-info → portacode-1.4.15.dev23.dist-info}/RECORD +8 -8
- {portacode-1.4.15.dev10.dist-info → portacode-1.4.15.dev23.dist-info}/WHEEL +0 -0
- {portacode-1.4.15.dev10.dist-info → portacode-1.4.15.dev23.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.15.dev10.dist-info → portacode-1.4.15.dev23.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.15.dev10.dist-info → portacode-1.4.15.dev23.dist-info}/top_level.txt +0 -0
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.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 4, 15, '
|
|
31
|
+
__version__ = version = '1.4.15.dev23'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 15, 'dev23')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -16,7 +16,7 @@ import sys
|
|
|
16
16
|
import tempfile
|
|
17
17
|
import time
|
|
18
18
|
import threading
|
|
19
|
-
from datetime import datetime
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
22
22
|
|
|
@@ -47,7 +47,7 @@ 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
|
-
|
|
50
|
+
TEMPLATES_REFRESH_INTERVAL_S = 300
|
|
51
51
|
|
|
52
52
|
ProgressCallback = Callable[[int, int, Dict[str, Any], str, Optional[Dict[str, Any]]], None]
|
|
53
53
|
|
|
@@ -197,10 +197,33 @@ def _build_proxmox_client_from_config(config: Dict[str, Any]):
|
|
|
197
197
|
)
|
|
198
198
|
|
|
199
199
|
|
|
200
|
+
def _current_time_iso() -> str:
|
|
201
|
+
return datetime.now(timezone.utc).isoformat()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _parse_iso_timestamp(value: str) -> Optional[datetime]:
|
|
205
|
+
if not value:
|
|
206
|
+
return None
|
|
207
|
+
text = value
|
|
208
|
+
if text.endswith("Z"):
|
|
209
|
+
text = text[:-1] + "+00:00"
|
|
210
|
+
try:
|
|
211
|
+
return datetime.fromisoformat(text)
|
|
212
|
+
except ValueError:
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _templates_need_refresh(config: Dict[str, Any]) -> bool:
|
|
217
|
+
if not config or not config.get("token_value"):
|
|
218
|
+
return False
|
|
219
|
+
last = _parse_iso_timestamp(config.get("templates_last_refreshed") or "")
|
|
220
|
+
if not last:
|
|
221
|
+
return True
|
|
222
|
+
return (datetime.now(timezone.utc) - last).total_seconds() >= TEMPLATES_REFRESH_INTERVAL_S
|
|
223
|
+
|
|
224
|
+
|
|
200
225
|
def _ensure_templates_refreshed_on_startup(config: Dict[str, Any]) -> None:
|
|
201
|
-
|
|
202
|
-
if _STARTUP_TEMPLATES_REFRESHED or not config or not config.get("token_value"):
|
|
203
|
-
_STARTUP_TEMPLATES_REFRESHED = True
|
|
226
|
+
if not _templates_need_refresh(config):
|
|
204
227
|
return
|
|
205
228
|
try:
|
|
206
229
|
client = _build_proxmox_client_from_config(config)
|
|
@@ -209,10 +232,10 @@ def _ensure_templates_refreshed_on_startup(config: Dict[str, Any]) -> None:
|
|
|
209
232
|
templates = _list_templates(client, node, storages)
|
|
210
233
|
if templates:
|
|
211
234
|
config["templates"] = templates
|
|
235
|
+
config["templates_last_refreshed"] = _current_time_iso()
|
|
236
|
+
_save_config(config)
|
|
212
237
|
except Exception as exc:
|
|
213
238
|
logger.warning("Unable to refresh Proxmox templates on startup: %s", exc)
|
|
214
|
-
finally:
|
|
215
|
-
_STARTUP_TEMPLATES_REFRESHED = True
|
|
216
239
|
|
|
217
240
|
|
|
218
241
|
def _pick_storage(storages: Iterable[Dict[str, Any]]) -> str:
|
|
@@ -473,48 +496,160 @@ def _friendly_step_label(step_name: str) -> str:
|
|
|
473
496
|
return normalized.capitalize()
|
|
474
497
|
|
|
475
498
|
|
|
499
|
+
_NETWORK_WAIT_CMD = (
|
|
500
|
+
"count=0; "
|
|
501
|
+
"while [ \"$count\" -lt 20 ]; do "
|
|
502
|
+
" if command -v ip >/dev/null 2>&1 && ip route get 1.1.1.1 >/dev/null 2>&1; then break; fi; "
|
|
503
|
+
" if [ -f /proc/net/route ] && grep -q '^00000000' /proc/net/route >/dev/null 2>&1; then break; fi; "
|
|
504
|
+
" sleep 1; "
|
|
505
|
+
" count=$((count+1)); "
|
|
506
|
+
"done"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
_PACKAGE_MANAGER_PROFILES: Dict[str, Dict[str, Any]] = {
|
|
510
|
+
"apt": {
|
|
511
|
+
"update_cmd": "apt-get update -y",
|
|
512
|
+
"update_step_name": "apt_update",
|
|
513
|
+
"install_cmd": "apt-get install -y python3 python3-pip sudo --fix-missing",
|
|
514
|
+
"install_step_name": "install_deps",
|
|
515
|
+
"update_retries": 4,
|
|
516
|
+
"install_retries": 5,
|
|
517
|
+
},
|
|
518
|
+
"dnf": {
|
|
519
|
+
"update_cmd": "dnf check-update || true",
|
|
520
|
+
"update_step_name": "dnf_update",
|
|
521
|
+
"install_cmd": "dnf install -y python3 python3-pip sudo",
|
|
522
|
+
"install_step_name": "install_deps",
|
|
523
|
+
"update_retries": 3,
|
|
524
|
+
"install_retries": 5,
|
|
525
|
+
},
|
|
526
|
+
"yum": {
|
|
527
|
+
"update_cmd": "yum makecache",
|
|
528
|
+
"update_step_name": "yum_update",
|
|
529
|
+
"install_cmd": "yum install -y python3 python3-pip sudo",
|
|
530
|
+
"install_step_name": "install_deps",
|
|
531
|
+
"update_retries": 3,
|
|
532
|
+
"install_retries": 5,
|
|
533
|
+
},
|
|
534
|
+
"apk": {
|
|
535
|
+
"update_cmd": "apk update",
|
|
536
|
+
"update_step_name": "apk_update",
|
|
537
|
+
"install_cmd": "apk add --no-cache python3 py3-pip sudo shadow",
|
|
538
|
+
"install_step_name": "install_deps",
|
|
539
|
+
"update_retries": 3,
|
|
540
|
+
"install_retries": 5,
|
|
541
|
+
},
|
|
542
|
+
"pacman": {
|
|
543
|
+
"update_cmd": "pacman -Sy --noconfirm",
|
|
544
|
+
"update_step_name": "pacman_update",
|
|
545
|
+
"install_cmd": "pacman -S --noconfirm python python-pip sudo",
|
|
546
|
+
"install_step_name": "install_deps",
|
|
547
|
+
"update_retries": 3,
|
|
548
|
+
"install_retries": 5,
|
|
549
|
+
},
|
|
550
|
+
"zypper": {
|
|
551
|
+
"update_cmd": "zypper refresh",
|
|
552
|
+
"update_step_name": "zypper_update",
|
|
553
|
+
"install_cmd": "zypper install -y python3 python3-pip sudo",
|
|
554
|
+
"install_step_name": "install_deps",
|
|
555
|
+
"update_retries": 3,
|
|
556
|
+
"install_retries": 5,
|
|
557
|
+
},
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
_UPDATE_RETRY_ON = [
|
|
561
|
+
"Temporary failure resolving",
|
|
562
|
+
"Could not resolve",
|
|
563
|
+
"Failed to fetch",
|
|
564
|
+
]
|
|
565
|
+
|
|
566
|
+
_INSTALL_RETRY_ON = [
|
|
567
|
+
"lock-frontend",
|
|
568
|
+
"Unable to acquire the dpkg frontend lock",
|
|
569
|
+
"Temporary failure resolving",
|
|
570
|
+
"Could not resolve",
|
|
571
|
+
"Failed to fetch",
|
|
572
|
+
]
|
|
573
|
+
|
|
574
|
+
|
|
476
575
|
def _build_bootstrap_steps(
|
|
477
576
|
user: str,
|
|
478
577
|
password: str,
|
|
479
578
|
ssh_key: str,
|
|
480
579
|
include_portacode_connect: bool = True,
|
|
580
|
+
package_manager: str = "apt",
|
|
481
581
|
) -> List[Dict[str, Any]]:
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
"
|
|
491
|
-
"
|
|
492
|
-
|
|
493
|
-
|
|
582
|
+
profile = _PACKAGE_MANAGER_PROFILES.get(package_manager, _PACKAGE_MANAGER_PROFILES["apt"])
|
|
583
|
+
steps: List[Dict[str, Any]] = [
|
|
584
|
+
{"name": "wait_for_network", "cmd": _NETWORK_WAIT_CMD, "retries": 0},
|
|
585
|
+
]
|
|
586
|
+
update_cmd = profile.get("update_cmd")
|
|
587
|
+
if update_cmd:
|
|
588
|
+
steps.append(
|
|
589
|
+
{
|
|
590
|
+
"name": profile.get("update_step_name", "package_update"),
|
|
591
|
+
"cmd": update_cmd,
|
|
592
|
+
"retries": profile.get("update_retries", 3),
|
|
593
|
+
"retry_delay_s": 5,
|
|
594
|
+
"retry_on": _UPDATE_RETRY_ON,
|
|
595
|
+
}
|
|
596
|
+
)
|
|
597
|
+
install_cmd = profile.get("install_cmd")
|
|
598
|
+
if install_cmd:
|
|
599
|
+
steps.append(
|
|
600
|
+
{
|
|
601
|
+
"name": profile.get("install_step_name", "install_deps"),
|
|
602
|
+
"cmd": install_cmd,
|
|
603
|
+
"retries": profile.get("install_retries", 5),
|
|
604
|
+
"retry_delay_s": 5,
|
|
605
|
+
"retry_on": _INSTALL_RETRY_ON,
|
|
606
|
+
}
|
|
607
|
+
)
|
|
608
|
+
steps.extend(
|
|
609
|
+
[
|
|
610
|
+
{"name": "user_exists", "cmd": f"id -u {user} >/dev/null 2>&1 || adduser --disabled-password --gecos '' {user}", "retries": 0},
|
|
611
|
+
{
|
|
612
|
+
"name": "add_sudo",
|
|
613
|
+
"cmd": (
|
|
614
|
+
f"if command -v usermod >/dev/null 2>&1; then "
|
|
615
|
+
" if ! getent group sudo >/dev/null 2>&1; then "
|
|
616
|
+
" if command -v groupadd >/dev/null 2>&1; then "
|
|
617
|
+
" groupadd sudo >/dev/null 2>&1 || true; "
|
|
618
|
+
" fi; "
|
|
619
|
+
" fi; "
|
|
620
|
+
f" usermod -aG sudo {user}; "
|
|
621
|
+
"else "
|
|
622
|
+
" for grp in wheel sudo; do "
|
|
623
|
+
" if ! getent group \"$grp\" >/dev/null 2>&1 && command -v groupadd >/dev/null 2>&1; then "
|
|
624
|
+
" groupadd \"$grp\" >/dev/null 2>&1 || true; "
|
|
625
|
+
" fi; "
|
|
626
|
+
" addgroup \"$grp\" >/dev/null 2>&1 || true; "
|
|
627
|
+
f" addgroup {user} \"$grp\" >/dev/null 2>&1 || true; "
|
|
628
|
+
" done; "
|
|
629
|
+
"fi"
|
|
630
|
+
),
|
|
631
|
+
"retries": 0,
|
|
632
|
+
},
|
|
494
633
|
{
|
|
495
|
-
"name": "
|
|
496
|
-
"cmd":
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
"Unable to acquire the dpkg frontend lock",
|
|
502
|
-
"Temporary failure resolving",
|
|
503
|
-
"Could not resolve",
|
|
504
|
-
"Failed to fetch",
|
|
505
|
-
],
|
|
634
|
+
"name": "add_sudoers",
|
|
635
|
+
"cmd": (
|
|
636
|
+
f"printf '%s ALL=(ALL) NOPASSWD:ALL\n' {shlex.quote(user)} >/etc/sudoers.d/portacode && "
|
|
637
|
+
"chmod 0440 /etc/sudoers.d/portacode"
|
|
638
|
+
),
|
|
639
|
+
"retries": 0,
|
|
506
640
|
},
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
]
|
|
641
|
+
]
|
|
642
|
+
)
|
|
510
643
|
if password:
|
|
511
644
|
steps.append({"name": "set_password", "cmd": f"echo '{user}:{password}' | chpasswd", "retries": 0})
|
|
512
645
|
if ssh_key:
|
|
513
|
-
steps.append(
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
646
|
+
steps.append(
|
|
647
|
+
{
|
|
648
|
+
"name": "add_ssh_key",
|
|
649
|
+
"cmd": f"install -d -m 700 /home/{user}/.ssh && echo '{ssh_key}' >> /home/{user}/.ssh/authorized_keys && chown -R {user}:{user} /home/{user}/.ssh",
|
|
650
|
+
"retries": 0,
|
|
651
|
+
}
|
|
652
|
+
)
|
|
518
653
|
steps.extend(
|
|
519
654
|
[
|
|
520
655
|
{"name": "pip_upgrade", "cmd": "python3 -m pip install --upgrade pip", "retries": 0},
|
|
@@ -526,6 +661,45 @@ def _build_bootstrap_steps(
|
|
|
526
661
|
return steps
|
|
527
662
|
|
|
528
663
|
|
|
664
|
+
def _guess_package_manager_from_template(template: str) -> str:
|
|
665
|
+
normalized = (template or "").lower()
|
|
666
|
+
if "alpine" in normalized:
|
|
667
|
+
return "apk"
|
|
668
|
+
if "archlinux" in normalized:
|
|
669
|
+
return "pacman"
|
|
670
|
+
if "centos-7" in normalized:
|
|
671
|
+
return "yum"
|
|
672
|
+
if any(keyword in normalized for keyword in ("centos-8", "centos-9", "centos-9-stream", "centos-8-stream")):
|
|
673
|
+
return "dnf"
|
|
674
|
+
if any(keyword in normalized for keyword in ("rockylinux", "almalinux", "fedora")):
|
|
675
|
+
return "dnf"
|
|
676
|
+
if "opensuse" in normalized or "suse" in normalized:
|
|
677
|
+
return "zypper"
|
|
678
|
+
if any(keyword in normalized for keyword in ("debian", "ubuntu", "devuan", "turnkeylinux")):
|
|
679
|
+
return "apt"
|
|
680
|
+
if normalized.startswith("system/") and "linux" in normalized:
|
|
681
|
+
return "apt"
|
|
682
|
+
return "apt"
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _detect_package_manager(vmid: int) -> str:
|
|
686
|
+
candidates = [
|
|
687
|
+
("apt", "apt-get"),
|
|
688
|
+
("dnf", "dnf"),
|
|
689
|
+
("yum", "yum"),
|
|
690
|
+
("apk", "apk"),
|
|
691
|
+
("pacman", "pacman"),
|
|
692
|
+
("zypper", "zypper"),
|
|
693
|
+
]
|
|
694
|
+
for name, binary in candidates:
|
|
695
|
+
res = _run_pct(vmid, f"command -v {binary} >/dev/null 2>&1")
|
|
696
|
+
if res.get("returncode") == 0:
|
|
697
|
+
logger.debug("Detected package manager %s inside container %s", name, vmid)
|
|
698
|
+
return name
|
|
699
|
+
logger.warning("Unable to detect package manager inside container %s; defaulting to apt", vmid)
|
|
700
|
+
return "apt"
|
|
701
|
+
|
|
702
|
+
|
|
529
703
|
def _get_storage_type(storages: Iterable[Dict[str, Any]], storage_name: str) -> str:
|
|
530
704
|
for entry in storages:
|
|
531
705
|
if entry.get("storage") == storage_name:
|
|
@@ -713,7 +887,8 @@ def _connect_proxmox(config: Dict[str, Any]) -> Any:
|
|
|
713
887
|
|
|
714
888
|
|
|
715
889
|
def _run_pct(vmid: int, cmd: str, input_text: Optional[str] = None) -> Dict[str, Any]:
|
|
716
|
-
|
|
890
|
+
shell = "/bin/sh"
|
|
891
|
+
full = ["pct", "exec", str(vmid), "--", shell, "-c", cmd]
|
|
717
892
|
start = time.time()
|
|
718
893
|
proc = subprocess.run(full, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, input=input_text)
|
|
719
894
|
return {
|
|
@@ -725,6 +900,10 @@ def _run_pct(vmid: int, cmd: str, input_text: Optional[str] = None) -> Dict[str,
|
|
|
725
900
|
}
|
|
726
901
|
|
|
727
902
|
|
|
903
|
+
def _su_command(user: str, command: str) -> str:
|
|
904
|
+
return f"su - {user} -s /bin/sh -c {shlex.quote(command)}"
|
|
905
|
+
|
|
906
|
+
|
|
728
907
|
def _run_pct_check(vmid: int, cmd: str) -> Dict[str, Any]:
|
|
729
908
|
res = _run_pct(vmid, cmd)
|
|
730
909
|
if res["returncode"] != 0:
|
|
@@ -784,7 +963,7 @@ def _push_bytes_to_container(
|
|
|
784
963
|
|
|
785
964
|
|
|
786
965
|
def _resolve_portacode_key_dir(vmid: int, user: str) -> str:
|
|
787
|
-
data_dir_cmd =
|
|
966
|
+
data_dir_cmd = _su_command(user, "echo -n ${XDG_DATA_HOME:-$HOME/.local/share}")
|
|
788
967
|
data_home = _run_pct_check(vmid, data_dir_cmd)["stdout"].strip()
|
|
789
968
|
portacode_dir = f"{data_home}/portacode"
|
|
790
969
|
_run_pct_exec_check(vmid, ["mkdir", "-p", portacode_dir])
|
|
@@ -801,18 +980,19 @@ def _deploy_device_keypair(vmid: int, user: str, private_key: str, public_key: s
|
|
|
801
980
|
|
|
802
981
|
|
|
803
982
|
def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -> Dict[str, Any]:
|
|
804
|
-
|
|
983
|
+
su_connect_cmd = _su_command(user, "portacode connect")
|
|
984
|
+
cmd = ["pct", "exec", str(vmid), "--", "/bin/sh", "-c", su_connect_cmd]
|
|
805
985
|
proc = subprocess.Popen(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
806
986
|
start = time.time()
|
|
807
987
|
|
|
808
|
-
data_dir_cmd =
|
|
988
|
+
data_dir_cmd = _su_command(user, "echo -n ${XDG_DATA_HOME:-$HOME/.local/share}")
|
|
809
989
|
data_dir = _run_pct_check(vmid, data_dir_cmd)["stdout"].strip()
|
|
810
990
|
key_dir = f"{data_dir}/portacode/keys"
|
|
811
991
|
pub_path = f"{key_dir}/id_portacode.pub"
|
|
812
992
|
priv_path = f"{key_dir}/id_portacode"
|
|
813
993
|
|
|
814
994
|
def file_size(path: str) -> Optional[int]:
|
|
815
|
-
stat_cmd = f"
|
|
995
|
+
stat_cmd = _su_command(user, f"test -s {path} && stat -c %s {path}")
|
|
816
996
|
res = _run_pct(vmid, stat_cmd)
|
|
817
997
|
if res["returncode"] != 0:
|
|
818
998
|
return None
|
|
@@ -870,7 +1050,7 @@ def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -
|
|
|
870
1050
|
final_pub = file_size(pub_path)
|
|
871
1051
|
final_priv = file_size(priv_path)
|
|
872
1052
|
if final_pub and final_priv:
|
|
873
|
-
key_res = _run_pct(vmid, f"
|
|
1053
|
+
key_res = _run_pct(vmid, _su_command(user, f"cat {pub_path}"))
|
|
874
1054
|
if not process_exited:
|
|
875
1055
|
proc.terminate()
|
|
876
1056
|
try:
|
|
@@ -910,7 +1090,7 @@ def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -
|
|
|
910
1090
|
except subprocess.TimeoutExpired:
|
|
911
1091
|
proc.kill()
|
|
912
1092
|
|
|
913
|
-
key_res = _run_pct(vmid, f"
|
|
1093
|
+
key_res = _run_pct(vmid, _su_command(user, f"cat {pub_path}"))
|
|
914
1094
|
return {
|
|
915
1095
|
"ok": True,
|
|
916
1096
|
"public_key": key_res["stdout"].strip(),
|
|
@@ -1003,7 +1183,16 @@ def _bootstrap_portacode(
|
|
|
1003
1183
|
total_steps: Optional[int] = None,
|
|
1004
1184
|
default_public_key: Optional[str] = None,
|
|
1005
1185
|
) -> Tuple[str, List[Dict[str, Any]]]:
|
|
1006
|
-
|
|
1186
|
+
if steps is not None:
|
|
1187
|
+
actual_steps = steps
|
|
1188
|
+
else:
|
|
1189
|
+
detected_manager = _detect_package_manager(vmid)
|
|
1190
|
+
actual_steps = _build_bootstrap_steps(
|
|
1191
|
+
user,
|
|
1192
|
+
password,
|
|
1193
|
+
ssh_key,
|
|
1194
|
+
package_manager=detected_manager,
|
|
1195
|
+
)
|
|
1007
1196
|
results, ok = _run_setup_steps(
|
|
1008
1197
|
vmid,
|
|
1009
1198
|
actual_steps,
|
|
@@ -1027,6 +1216,15 @@ def _bootstrap_portacode(
|
|
|
1027
1216
|
else:
|
|
1028
1217
|
command_text = str(command)
|
|
1029
1218
|
command_suffix = f" command={command_text}" if command_text else ""
|
|
1219
|
+
stdout = details.get("stdout")
|
|
1220
|
+
stderr = details.get("stderr")
|
|
1221
|
+
if stdout or stderr:
|
|
1222
|
+
logger.debug(
|
|
1223
|
+
"Bootstrap command output%s%s%s",
|
|
1224
|
+
f" stdout={stdout!r}" if stdout else "",
|
|
1225
|
+
" " if stdout and stderr else "",
|
|
1226
|
+
f"stderr={stderr!r}" if stderr else "",
|
|
1227
|
+
)
|
|
1030
1228
|
if summary:
|
|
1031
1229
|
logger.warning(
|
|
1032
1230
|
"Portacode bootstrap failure summary=%s%s%s",
|
|
@@ -1034,10 +1232,15 @@ def _bootstrap_portacode(
|
|
|
1034
1232
|
f" history_len={len(history)}" if history else "",
|
|
1035
1233
|
f" command={command_text}" if command_text else "",
|
|
1036
1234
|
)
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1235
|
+
logger.error(
|
|
1236
|
+
"Portacode bootstrap command failed%s%s%s",
|
|
1237
|
+
f" command={command_text}" if command_text else "",
|
|
1238
|
+
f" stdout={stdout!r}" if stdout else "",
|
|
1239
|
+
f" stderr={stderr!r}" if stderr else "",
|
|
1240
|
+
)
|
|
1241
|
+
raise RuntimeError(
|
|
1242
|
+
f"Portacode bootstrap steps failed: {summary}{history_snippet}{command_suffix}"
|
|
1243
|
+
)
|
|
1041
1244
|
key_step = next((entry for entry in results if entry.get("name") == "portacode_connect"), None)
|
|
1042
1245
|
public_key = key_step.get("public_key") if key_step else default_public_key
|
|
1043
1246
|
if not public_key:
|
|
@@ -1104,8 +1307,9 @@ def configure_infrastructure(token_identifier: str, token_value: str, verify_ssl
|
|
|
1104
1307
|
"token_value": token_value,
|
|
1105
1308
|
"verify_ssl": verify_ssl,
|
|
1106
1309
|
"default_storage": default_storage,
|
|
1107
|
-
"templates": templates,
|
|
1108
1310
|
"last_verified": datetime.utcnow().isoformat() + "Z",
|
|
1311
|
+
"templates": templates,
|
|
1312
|
+
"templates_last_refreshed": _current_time_iso(),
|
|
1109
1313
|
"network": network,
|
|
1110
1314
|
"node_status": status,
|
|
1111
1315
|
}
|
|
@@ -1189,12 +1393,17 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1189
1393
|
device_public_key = (message.get("device_public_key") or "").strip()
|
|
1190
1394
|
device_private_key = (message.get("device_private_key") or "").strip()
|
|
1191
1395
|
has_device_keypair = bool(device_public_key and device_private_key)
|
|
1396
|
+
config_guess = _load_config()
|
|
1397
|
+
template_candidates = config_guess.get("templates") or []
|
|
1398
|
+
template_hint = (message.get("template") or (template_candidates[0] if template_candidates else "")).strip()
|
|
1399
|
+
package_manager = _guess_package_manager_from_template(template_hint)
|
|
1192
1400
|
bootstrap_user, bootstrap_password, bootstrap_ssh_key = _get_provisioning_user_info(message)
|
|
1193
1401
|
bootstrap_steps = _build_bootstrap_steps(
|
|
1194
1402
|
bootstrap_user,
|
|
1195
1403
|
bootstrap_password,
|
|
1196
1404
|
bootstrap_ssh_key,
|
|
1197
1405
|
include_portacode_connect=not has_device_keypair,
|
|
1406
|
+
package_manager=package_manager,
|
|
1198
1407
|
)
|
|
1199
1408
|
total_steps = 3 + len(bootstrap_steps) + 2
|
|
1200
1409
|
current_step_index = 1
|
|
@@ -1424,7 +1633,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1424
1633
|
on_behalf_of_device=device_id,
|
|
1425
1634
|
)
|
|
1426
1635
|
|
|
1427
|
-
cmd =
|
|
1636
|
+
cmd = _su_command(payload["username"], "sudo -S portacode service install")
|
|
1428
1637
|
res = _run_pct(vmid, cmd, input_text=payload["password"] + "\n")
|
|
1429
1638
|
|
|
1430
1639
|
if res["returncode"] != 0:
|
|
@@ -1556,7 +1765,7 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1556
1765
|
on_behalf_of_device=on_behalf_of_device,
|
|
1557
1766
|
)
|
|
1558
1767
|
|
|
1559
|
-
cmd =
|
|
1768
|
+
cmd = _su_command(user, "sudo -S portacode service install")
|
|
1560
1769
|
res = _run_pct(vmid, cmd, input_text=password + "\n")
|
|
1561
1770
|
|
|
1562
1771
|
if res["returncode"] != 0:
|
|
@@ -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=
|
|
4
|
+
portacode/_version.py,sha256=8YGMPC5_rqCpJnjJF3sLM5flS3H0d8do1siDivvmwVs,721
|
|
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=
|
|
25
|
+
portacode/connection/handlers/proxmox_infra.py,sha256=cAueQb_D3COmPI84LfI1bqHOcgt02BOgho_pCRGeLWE,71132
|
|
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.
|
|
68
|
+
portacode-1.4.15.dev23.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.
|
|
95
|
-
portacode-1.4.15.
|
|
96
|
-
portacode-1.4.15.
|
|
97
|
-
portacode-1.4.15.
|
|
98
|
-
portacode-1.4.15.
|
|
94
|
+
portacode-1.4.15.dev23.dist-info/METADATA,sha256=1pp2pJiX9YjuOBghWO4mYN7fkpK0dqJSv6CdFMgbwOQ,13052
|
|
95
|
+
portacode-1.4.15.dev23.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
96
|
+
portacode-1.4.15.dev23.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
97
|
+
portacode-1.4.15.dev23.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
98
|
+
portacode-1.4.15.dev23.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|