diff --git a/npm_install.py b/npm_install.py index 60643e5..c3bd5b8 100644 --- a/npm_install.py +++ b/npm_install.py @@ -211,133 +211,138 @@ def sync_backup_nginx_conf(): print(f"Warning: sync failed for {p} -> {target}: {e}") +from pathlib import Path +import shutil + def setup_certbot_venv(venv_dir: Path = Path("/opt/certbot")): + """ + Tworzy środowisko venv dla Certbota, korzystając z pyenv. + Debian: instaluje pyenv z APT. + Ubuntu: instaluje zależności build i klonuje pyenv z Git (do /opt/npm/.pyenv). + Wymaga pomocniczych funkcji: os_release(), apt_try_install(pkgs: list[str]), run(cmd: list[str], check=True). + """ + OSREL = os_release() + + def _is_ubuntu() -> bool: + return (OSREL.get("ID", "") or "").lower() == "ubuntu" + + def ensure_pyenv_installed(pyenv_root: Path, owner: str): + """Na Ubuntu brak pakietu pyenv w APT — instalacja z Git, jeśli brak.""" + if shutil.which("pyenv") or (pyenv_root / "bin" / "pyenv").exists(): + return + # minimalne narzędzia do klonowania + apt_try_install(["git", "ca-certificates", "curl"]) + run([ + "sudo", "-u", owner, "bash", "-lc", + f'git clone --depth=1 https://github.com/pyenv/pyenv.git "{pyenv_root}"' + ], check=False) + PYENV_ROOT = Path("/opt/npm/.pyenv") PYENV_OWNER = "npm" PYTHON_VERSION = "3.11.11" - PYENV_BIN_CANDIDATES = ["pyenv", "/usr/bin/pyenv", "/usr/lib/pyenv/bin/pyenv"] + PYENV_BIN_CANDIDATES = [ + "pyenv", + "/usr/bin/pyenv", + "/usr/lib/pyenv/bin/pyenv", + str(PYENV_ROOT / "bin" / "pyenv"), + ] - try: - apt_try_install([ - "pyenv", "build-essential", "gcc", "make", "pkg-config", - "libssl-dev", "zlib1g-dev", "libbz2-dev", "libreadline-dev", - "libsqlite3-dev", "tk-dev", "libncursesw5-dev", "libgdbm-dev", - "libffi-dev", "uuid-dev", "liblzma-dev", "ca-certificates", "curl" - ]) - except Exception: - run(["apt-get", "update"], check=False) - run(["apt-get", "install", "-y", - "pyenv", "build-essential", "gcc", "make", "pkg-config", - "libssl-dev", "zlib1g-dev", "libbz2-dev", "libreadline-dev", - "libsqlite3-dev", "tk-dev", "libncursesw5-dev", "libgdbm-dev", - "libffi-dev", "uuid-dev", "liblzma-dev", "ca-certificates", "curl" - ], check=False) + # Zależności kompilacji Pythona + build_deps = [ + "build-essential", "gcc", "make", "pkg-config", + "libssl-dev", "zlib1g-dev", "libbz2-dev", "libreadline-dev", + "libsqlite3-dev", "tk-dev", "libncursesw5-dev", "libgdbm-dev", + "libffi-dev", "uuid-dev", "liblzma-dev", "ca-certificates", "curl", "git", + ] + # Debian: pyenv z APT; Ubuntu: tylko zależności build (pyenv z Git niżej) + if _is_ubuntu(): + apt_try_install(build_deps) + else: + apt_try_install(["pyenv"] + build_deps) + + # Przygotuj katalogi/owner Path("/opt/npm").mkdir(parents=True, exist_ok=True) PYENV_ROOT.mkdir(parents=True, exist_ok=True) run(["chown", "-R", f"{PYENV_OWNER}:{PYENV_OWNER}", "/opt/npm"], check=False) + # Ubuntu: klon pyenv z Git, jeśli nie ma binarki + ensure_pyenv_installed(PYENV_ROOT, PYENV_OWNER) + + # Znajdź pyenv pyenv_bin = next((c for c in PYENV_BIN_CANDIDATES if shutil.which(c)), None) if not pyenv_bin: - raise RuntimeError("Nie znaleziono 'pyenv' (spróbuj /usr/bin/pyenv lub /usr/lib/pyenv/bin/pyenv).") - - env_pyenv = os.environ.copy() - env_pyenv.update({ - "HOME": "/opt/npm", - "PYENV_ROOT": str(PYENV_ROOT), - "PATH": "/usr/lib/pyenv/bin:/usr/bin:/bin" - }) - with step(f"Installing Python {PYTHON_VERSION} via pyenv into {PYENV_ROOT}"): - # 1) Upewnij się, że PYENV_ROOT istnieje i należy do 'npm' - run(["mkdir", "-p", str(PYENV_ROOT)]) - run(["chown", "-R", f"{PYENV_OWNER}:{PYENV_OWNER}", "/opt/npm"], check=False) - - # 2) Jeżeli lokalny pyenv nie istnieje – sklonuj go (pomija wrappera Debiana) - run([ - "sudo", "-u", PYENV_OWNER, "bash", "-lc", - 'if [ ! -x "/opt/npm/.pyenv/bin/pyenv" ]; then ' - ' command -v git >/dev/null 2>&1 || sudo apt-get install -y git; ' - ' git clone --depth=1 https://github.com/pyenv/pyenv.git /opt/npm/.pyenv; ' - "fi" - ]) - - # 3) Z bardzo czystym środowiskiem (env -i) instalujemy CPython - # – żadnych /etc/profile, żadnych wrapperów. - install_cmd = ( - 'export HOME=/opt/npm; ' - 'export PYENV_ROOT=/opt/npm/.pyenv; ' - 'export PATH="$PYENV_ROOT/bin:/usr/bin:/bin"; ' - 'mkdir -p "$PYENV_ROOT"; cd "$HOME"; ' - f'pyenv install -s {PYTHON_VERSION}' - ) - run([ - "sudo", "-u", PYENV_OWNER, "env", "-i", - "HOME=/opt/npm", - f"PYENV_ROOT={PYENV_ROOT}", - f"PATH={PYENV_ROOT}/bin:/usr/bin:/bin", - "bash", "-lc", install_cmd - ]) - + if (PYENV_ROOT / "bin" / "pyenv").exists(): + pyenv_bin = str(PYENV_ROOT / "bin" / "pyenv") + else: + raise RuntimeError("Nie znaleziono 'pyenv' (ani z APT, ani z Git).") + # Idempotentny snippet inicjujący pyenv dla użytkownika 'npm' profile_snippet = f"""# Auto-generated by setup_certbot_venv # Ustawienia pyenv dla uzytkownika '{PYENV_OWNER}' if [ -d "{PYENV_ROOT}" ]; then export PYENV_ROOT="{PYENV_ROOT}" - # Dopnij lokalne binarki pyenv (git-install) idempotentnie - case ":$PATH:" in *":$PYENV_ROOT/bin:"*) ;; *) PATH="$PYENV_ROOT/bin:$PATH";; esac - # Dopnij systemowe binarki pyenv z pakietu Debiana idempotentnie - case ":$PATH:" in *":/usr/lib/pyenv/bin:"*) ;; *) PATH="/usr/lib/pyenv/bin:$PATH";; esac - export PATH - # Inicjalizacja tylko dla interaktywnych powlok uzytkownika '{PYENV_OWNER}' - case "$-" in *i*) _interactive=1 ;; *) _interactive=0 ;; esac - if [ "$_interactive" = 1 ] && {{ [ "${{USER:-}}" = "{PYENV_OWNER}" ] || [ "${{SUDO_USER:-}}" = "{PYENV_OWNER}" ]; }}; then - if command -v pyenv >/dev/null 2>&1; then - eval "$(pyenv init -)" - elif [ -x "{PYENV_ROOT}/bin/pyenv" ]; then - eval "$("{PYENV_ROOT}/bin/pyenv" init -)" - fi - fi + case ":$PATH:" in + *":{PYENV_ROOT}/bin:"*) ;; + *) export PATH="{PYENV_ROOT}/bin:$PATH" ;; + esac + eval "$({pyenv_bin} init -)" fi """ - write_file(Path("/etc/profile.d/npm-pyenv.sh"), profile_snippet, 0o644) + # Zapisz snippet do ~/.profile użytkownika 'npm' (idempotentnie) + run([ + "sudo", "-u", PYENV_OWNER, "bash", "-lc", + r'PROFILE="$HOME/.profile"; ' + r'grep -q "Auto-generated by setup_certbot_venv" "$PROFILE" || ' + f'printf "%s\n" {profile_snippet!r} >> "$PROFILE"' + ], check=False) - python311 = PYENV_ROOT / "versions" / PYTHON_VERSION / "bin" / "python3.11" - if not python311.exists(): - python311 = PYENV_ROOT / "versions" / PYTHON_VERSION / "bin" / "python3" - if not python311.exists(): - raise RuntimeError(f"Nie znaleziono interpretera Pythona {PYTHON_VERSION} w {PYENV_ROOT}/versions/.") + # Zainstaluj żądaną wersję Pythona (idempotentnie, -s = skip jeśli istnieje) + run([ + "sudo", "-u", PYENV_OWNER, "bash", "-lc", + f'export PYENV_ROOT="{PYENV_ROOT}"; export PATH="{{PYENV_ROOT}}/bin:$PATH"; ' + f'"{pyenv_bin}" install -s {PYTHON_VERSION}; ' + f'"{pyenv_bin}" versions' + ]) - venv_bin = venv_dir / "bin" - pip_path = venv_bin / "pip" - certbot_path = venv_bin / "certbot" + # Znajdź ścieżkę do zainstalowanego Pythona + py_cmd = run([ + "sudo", "-u", PYENV_OWNER, "bash", "-lc", + f'export PYENV_ROOT="{PYENV_ROOT}"; export PATH="{{PYENV_ROOT}}/bin:$PATH"; ' + f'PY=$( "{pyenv_bin}" prefix {PYTHON_VERSION} )/bin/python3; ' + r'echo "$PY"' + ]) + python_path = py_cmd.stdout.decode().strip() + if not python_path: + raise RuntimeError("Nie udało się ustalić ścieżki do python3 z pyenv.") - with step(f"Preparing Certbot venv at {venv_dir} (Python {PYTHON_VERSION})"): - venv_dir.mkdir(parents=True, exist_ok=True) - if not venv_dir.exists() or not pip_path.exists(): - run([str(python311), "-m", "venv", str(venv_dir)]) + # Utwórz venv dla Certbota (idempotentnie) + venv_dir = Path(venv_dir) + venv_dir.mkdir(parents=True, exist_ok=True) + run([python_path, "-m", "venv", str(venv_dir)], check=True) - env_build = os.environ.copy() - env_build["SETUPTOOLS_USE_DISTUTILS"] = "local" - - run([str(pip_path), "install", "-U", "pip", "setuptools", "wheel"], env=env_build) - run([str(pip_path), "install", "-U", - "cryptography", "cffi", "certbot", "tldextract"], env=env_build) + # Zainstaluj/aktualizuj pip + certbot + run([ + str(venv_dir / "bin" / "python"), "-m", "pip", "install", "--upgrade", "pip", "wheel", "setuptools" + ], check=True) + run([ + str(venv_dir / "bin" / "pip"), "install", "--upgrade", "certbot" + ], check=True) + # Symlink (opcjonalnie) /usr/local/bin/certbot -> venv + certbot_bin = venv_dir / "bin" / "certbot" + if certbot_bin.exists(): Path("/usr/local/bin").mkdir(parents=True, exist_ok=True) - target = Path("/usr/local/bin/certbot") - if target.exists() or target.is_symlink(): - try: - target.unlink() - except Exception: - pass - target.symlink_to(certbot_path) - - cb_ver = run_out([str(certbot_path), "--version"], check=False) or "" - pip_ver = run_out([str(pip_path), "--version"], check=False) or "" - print(f"Certbot: {cb_ver.strip()} | Pip: {pip_ver.strip()}") - - run(["chown", "-R", f"{PYENV_OWNER}:{PYENV_OWNER}", str(PYENV_ROOT)], check=False) + run(["ln", "-sf", str(certbot_bin), "/usr/local/bin/certbot"], check=False) + return { + "pyenv_bin": pyenv_bin, + "python": python_path, + "venv": str(venv_dir), + "certbot": str(certbot_bin) if certbot_bin.exists() else None, + "distro": OSREL.get("ID", ""), + "version": OSREL.get("VERSION_ID", ""), + } def configure_letsencrypt():