From f9aa0428e01bd67d0ea4f3c2a364c8fcf6d5454e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 5 Jun 2026 23:53:15 +0200 Subject: [PATCH] fix install v2.15.X --- npm_install.py | 231 ++++++++++++++++++++++++++++++------------------- 1 file changed, 141 insertions(+), 90 deletions(-) diff --git a/npm_install.py b/npm_install.py index 583c6bd..1930111 100644 --- a/npm_install.py +++ b/npm_install.py @@ -972,6 +972,125 @@ def sync_backup_nginx_conf(): print(f"Warning: sync failed for {p} -> {target}: {e}") + +CERTBOT_REQUIRED_PACKAGES = [ + "certbot", + "acme", + "certbot-dns-cloudflare", + "certbot-dns-rfc2136", +] + + +def _ensure_certbot_symlink(certbot_path: Path): + 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) + + +def _detect_certbot_version(certbot_path: Path) -> str: + cb_ver = run_out([str(certbot_path), "--version"], check=False).strip() + m = re.search(r"(\d+\.\d+\.\d+)", cb_ver) + if not m: + raise RuntimeError(f"Cannot detect certbot version from: {cb_ver!r}") + return m.group(1) + + +def _install_certbot_stack(pip_path: Path, certbot_path: Path, env_build: dict): + """Install Certbot and DNS plugins in one venv with matching versions. + + NPM installs DNS plugins on startup when they are missing. On non-docker + installs this can fail with 'acme==undefined'. We prevent that by making + the venv complete before npm.service starts. + """ + 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, + ) + + certbot_ver = _detect_certbot_version(certbot_path) + run( + [ + str(pip_path), + "install", + "-U", + f"acme=={certbot_ver}", + f"certbot-dns-cloudflare=={certbot_ver}", + f"certbot-dns-rfc2136=={certbot_ver}", + ], + env=env_build, + ) + + missing = [] + for pkg in CERTBOT_REQUIRED_PACKAGES: + result = subprocess.run( + [str(pip_path), "show", pkg], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode != 0: + missing.append(pkg) + if missing: + raise RuntimeError(f"Certbot venv incomplete, missing: {', '.join(missing)}") + + _ensure_certbot_symlink(certbot_path) + + +def ensure_certbot_venv_ready(venv_dir: Path = Path("/opt/certbot"), force_rebuild: bool = False): + """Fresh install: build full venv. Update: keep a complete existing venv. + + Rebuild only when forced or when certbot/plugin packages are missing. + """ + certbot_path = venv_dir / "bin" / "certbot" + pip_path = venv_dir / "bin" / "pip" + + if force_rebuild and venv_dir.exists(): + with step("Removing certbot venv for forced rebuild"): + shutil.rmtree(venv_dir, ignore_errors=True) + + needs_rebuild = force_rebuild or not certbot_path.exists() or not pip_path.exists() + + if not needs_rebuild: + for pkg in CERTBOT_REQUIRED_PACKAGES: + result = subprocess.run( + [str(pip_path), "show", pkg], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode != 0: + needs_rebuild = True + if DEBUG: + print(f" Certbot venv missing package: {pkg}") + break + + if needs_rebuild: + setup_certbot_venv(venv_dir) + else: + _ensure_certbot_symlink(certbot_path) + cb_ver = run_out([str(certbot_path), "--version"], check=False).strip() + print(f"✓ Existing certbot venv is complete: {cb_ver}") + + run(["chown", "-R", "npm:npm", str(venv_dir)], check=False) + return True + def setup_certbot_venv(venv_dir: Path = Path("/opt/certbot")): info = os_release() distro_id = (info.get("ID") or "").lower() @@ -1010,31 +1129,7 @@ def setup_certbot_venv(venv_dir: Path = Path("/opt/certbot")): 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, - ) - - 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) + _install_certbot_stack(pip_path, certbot_path, env_build) cb_ver = run_out([str(certbot_path), "--version"], check=False) or "" pip_ver = run_out([str(pip_path), "--version"], check=False) or "" @@ -1073,31 +1168,7 @@ def setup_certbot_venv(venv_dir: Path = Path("/opt/certbot")): 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, - ) - - 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) + _install_certbot_stack(pip_path, certbot_path, env_build) cb_ver = run_out([str(certbot_path), "--version"], check=False) or "" pip_ver = run_out([str(pip_path), "--version"], check=False) or "" @@ -1249,31 +1320,7 @@ fi 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, - ) - - 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) + _install_certbot_stack(pip_path, certbot_path, env_build) cb_ver = run_out([str(certbot_path), "--version"], check=False) or "" pip_ver = run_out([str(pip_path), "--version"], check=False) or "" @@ -1289,10 +1336,7 @@ def configure_letsencrypt(): run(["chown", "-R", "npm:npm", "/opt/certbot"], check=False) Path("/etc/letsencrypt").mkdir(parents=True, exist_ok=True) run(["chown", "-R", "npm:npm", "/etc/letsencrypt"], check=False) - run( - ["apt-get", "install", "-y", "--no-install-recommends", "certbot"], - check=False, - ) + # Do not install distro certbot here; use /opt/certbot venv only. ini = """text = True non-interactive = True webroot-path = /data/letsencrypt-acme-challenge @@ -1625,6 +1669,9 @@ def write_metrics_files(): """Create /etc/angie/metrics.conf (port 82/8282 with console & status).""" with step("Adding Angie metrics & console on :82 / :8282 (https)"): + if NPM_ADMIN_ENABLE_SSL: + generate_selfsigned_cert() + metrics = f"""include /etc/angie/prometheus_all.conf; server {{ listen 8282 ssl; @@ -2697,12 +2744,12 @@ def deploy_npm_app_from_release(version: str | None) -> str: version = github_latest_release_tag(repo, override=None) print(f"✓ Latest stable version: {version}") + version_parsed = parse_version(version) if version_parsed < (2, 13, 0): error(f"Version {version} is not supported. Minimum version: 2.13.0") sys.exit(1) # Check if version >= 2.13.0 - if so, use git instead (releases missing /global) - version_parsed = parse_version(version) if version_parsed >= (2, 13, 0): print( f" Version {version} >= 2.13.0: using git source (release archive incomplete)" @@ -2944,6 +2991,11 @@ exec /usr/sbin/logrotate -s {state_file} "$@" def create_systemd_units(ipv6_enabled: bool): with step("Creating and starting systemd services (angie, npm)"): + # Some configs may already reference the admin/metrics SSL certificate. + # Ensure it exists before the first Angie config test/restart. + if NPM_ADMIN_ENABLE_SSL: + generate_selfsigned_cert() + unit_lines = [ "[Unit]", "Description=Nginx Proxy Manager (backend)", @@ -2955,6 +3007,7 @@ def create_systemd_units(ipv6_enabled: bool): "Group=npm", "WorkingDirectory=/opt/npm", "Environment=NODE_ENV=production", + "Environment=PATH=/opt/certbot/bin:/usr/local/bin:/usr/bin:/bin", ] if not ipv6_enabled: unit_lines.append("Environment=DISABLE_IPV6=true") @@ -2971,9 +3024,12 @@ def create_systemd_units(ipv6_enabled: bool): write_file(Path("/etc/systemd/system/npm.service"), "\n".join(unit_lines), 0o644) write_file(Path("/etc/systemd/system/angie.service"), ANGIE_UNIT, 0o644) subprocess.run(["systemctl", "daemon-reload"], check=False) + + # Validate configuration before touching the running service. + run(["/usr/sbin/angie", "-t"], check=True) + subprocess.run(["systemctl", "restart", "angie.service"], check=False) subprocess.run(["systemctl", "enable", "angie.service"], check=False) - run(["/usr/sbin/angie", "-t"], check=False) subprocess.run(["systemctl", "restart", "npm.service"], check=False) subprocess.run(["systemctl", "enable", "npm.service"], check=False) @@ -3892,6 +3948,8 @@ def update_only( run(["yarn", "install"]) patch_npm_backend_commands() + ensure_certbot_venv_ready() + configure_letsencrypt() create_systemd_units(ipv6_enabled=ipv6_enabled) with step("Setting owners"): @@ -3907,14 +3965,6 @@ def update_only( except Exception as e: print(f" ⚠ Warning: Could not remove dev.conf: {e}") - certbot_venv = Path("/opt/certbot") - if certbot_venv.exists: - print(f"♻ Removing stale certbot venv for rebuild...") - shutil.rmtree(certbot_venv, ignore_errors=True) - - setup_certbot_venv() - configure_letsencrypt() - with step("Restarting services after update"): run(["systemctl", "restart", "angie.service"], check=False) run(["systemctl", "restart", "npm.service"], check=False) @@ -4127,7 +4177,7 @@ def main(): ensure_minimum_nodejs(user_requested_version=args.node_version) ensure_user_and_dirs() create_sudoers_for_npm() - setup_certbot_venv() + ensure_certbot_venv_ready() configure_letsencrypt() # ========== INSTALLATION ========== @@ -4262,3 +4312,4 @@ def main(): if __name__ == "__main__": signal.signal(signal.SIGINT, lambda s, f: sys.exit(130)) main() + \ No newline at end of file