From 6d6726839763127aaa9d8fddafe36dc8f5cd6dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 24 Oct 2025 07:58:11 +0200 Subject: [PATCH] ubuntu support and more --- npm_install.py | 158 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 121 insertions(+), 37 deletions(-) diff --git a/npm_install.py b/npm_install.py index a9b95fc..111656f 100644 --- a/npm_install.py +++ b/npm_install.py @@ -80,9 +80,26 @@ def ensure_root(): print("Run as root.", file=sys.stderr) sys.exit(1) +def os_release(): + data = {} + try: + for line in Path("/etc/os-release").read_text().splitlines(): + if "=" in line: + k,v = line.split("=",1) + data[k] = v.strip().strip('"') + except Exception: + pass + pretty = data.get("PRETTY_NAME") or f"{data.get('ID','linux')} {data.get('VERSION_ID','')}".strip() + return { + "ID": data.get("ID",""), + "VERSION_ID": data.get("VERSION_ID",""), + "CODENAME": data.get("VERSION_CODENAME",""), + "PRETTY": pretty + } + def apt_update_upgrade(): with step("Updating package lists and system"): - run(["apt-get", "update", "-y"] if DEBUG else ["apt-get", "update","-y"]) + run(["apt-get", "update", "-y"]) run(["apt-get", "-y", "upgrade"]) def apt_install(pkgs): @@ -90,6 +107,18 @@ def apt_install(pkgs): with step(f"Installing packages: {', '.join(pkgs)}"): run(["apt-get", "install", "-y"] + pkgs) +def apt_try_install(pkgs): + if not pkgs: return + avail = [] + for p in pkgs: + ok = subprocess.run(["bash","-lc", f"apt-cache show {p} >/dev/null 2>&1"], stdout=_devnull(), stderr=_devnull()) + if ok.returncode == 0: + avail.append(p) + elif DEBUG: + print(f"skip missing pkg: {p}") + if avail: + apt_install(avail) + def apt_purge(pkgs): if not pkgs: return with step(f"Removing conflicting packages: {', '.join(pkgs)}"): @@ -136,7 +165,10 @@ def download_extract_tar_gz(url: str, dest_dir: Path) -> Path: os.unlink(tf_path) return dest_dir / top -# === NA GÓRZE pliku, obok innych utili === +# Distro info (used in banners & repo setup) +OSREL = os_release() + +# === extra sync === def sync_backup_nginx_conf(): from pathlib import Path import shutil, filecmp @@ -157,14 +189,13 @@ def sync_backup_nginx_conf(): except Exception as e: print(f"Warning: sync failed for {p} -> {target}: {e}") - def ensure_nginx_symlink(): from pathlib import Path target = Path("/etc/angie") link = Path("/etc/nginx") try: if link.is_symlink() and link.resolve() == target: - print("Symlink /etc/nginx -> /etc/angie already in place") + print("✔ Created symlink /etc/nginx -> /etc/angie") return if link.exists() and not link.is_symlink(): @@ -174,7 +205,7 @@ def ensure_nginx_symlink(): if backup.is_symlink() or backup.is_file(): backup.unlink() link.rename(backup) - print("Backed up /etc/nginx to /etc/nginx.bak") + print("✔ Backed up /etc/nginx to /etc/nginx.bak") except Exception as e: print(f"Warning: could not backup /etc/nginx: {e}") @@ -186,13 +217,12 @@ def ensure_nginx_symlink(): try: link.symlink_to(target) - print("Created symlink /etc/nginx -> /etc/angie") + print("✔ Created symlink /etc/nginx -> /etc/angie") except Exception as e: print(f"Warning: could not create /etc/nginx symlink: {e}") except Exception as e: print(f"Warning: symlink check failed: {e}") - # ========== Angie / NPM template ========== ANGIE_CONF_TEMPLATE = """# run nginx in foreground @@ -207,7 +237,6 @@ load_module /etc/angie/modules/ngx_http_zstd_static_module.so; # other modules include /data/nginx/custom/modules[.]conf; - pid /run/angie/angie.pid; user root; @@ -281,6 +310,9 @@ http { include /data/nginx/dead_host/*.conf; include /data/nginx/temp/*.conf; include /data/nginx/custom/http[.]conf; + + # metrics & console + include /etc/angie/metrics.conf; } stream { @@ -314,31 +346,32 @@ WantedBy=multi-user.target # ========== Angie ========== def setup_angie(): with step("Adding Angie repo and installing Angie packages"): - run(["apt-get", "install", "-y", "ca-certificates", "curl", "gnupg"]) + # Ubuntu/Debian: common base + apt_try_install(["ca-certificates", "curl", "gnupg", "apt-transport-https", "software-properties-common"]) run(["curl", "-fsSL", "-o", "/etc/apt/trusted.gpg.d/angie-signing.gpg", "https://angie.software/keys/angie-signing.gpg"]) os_id = run_out(["bash","-lc",". /etc/os-release && echo \"$ID/$VERSION_ID $VERSION_CODENAME\""]).strip() write_file(Path("/etc/apt/sources.list.d/angie.list"), f"deb https://download.angie.software/angie/{os_id} main\n") run(["apt-get", "update"]) - run(["apt-get", "install", "-y", "angie", "angie-module-headers-more", "angie-module-brotli", "angie-module-zstd"]) + + # Angie core + optional modules if available (prometheus + console for your metrics block) + base = ["angie", "angie-module-headers-more", "angie-module-brotli", "angie-module-zstd"] + optional = ["angie-module-prometheus", "angie-console-light"] + apt_install(base) + apt_try_install(optional) with step("Configuring modules and main Angie config"): modules_dir = Path("/etc/nginx/modules") modules_dir.mkdir(parents=True, exist_ok=True) write_file(Path("/etc/angie/angie.conf"), ANGIE_CONF_TEMPLATE, 0o644) - if not Path("/usr/sbin/nginx").exists(): - write_file(Path("/usr/sbin/nginx"), "#!/bin/sh\nexec /usr/sbin/angie \"$@\"\n", 0o755) - + # wrapper forcing sudo (NOPASSWD is set for npm user) WRAP = """#!/bin/sh -# /usr/sbin/nginx wrapper -> run angie as root via sudo (non-interactive) exec sudo -n /usr/sbin/angie "$@" """ write_file(Path("/usr/sbin/nginx"), WRAP, 0o755) - if not Path("/etc/nginx").exists(): - os.symlink("/etc/angie", "/etc/nginx") Path("/etc/nginx/conf.d/include").mkdir(parents=True, exist_ok=True) with step("Setting resolver (IPv4 only) and cache directories"): @@ -358,12 +391,50 @@ exec sudo -n /usr/sbin/angie "$@" with step("Installing corrected systemd unit for Angie"): write_file(Path("/etc/systemd/system/angie.service"), ANGIE_UNIT, 0o644) +def write_metrics_files(): + """Create /etc/angie/prometheus_all.conf and /etc/angie/metrics.conf (port 82 with console & status).""" + with step("Adding Angie metrics & console on :82"): + # global prometheus config that can be reused + write_file(Path("/etc/angie/prometheus_all.conf"), "prometheus all;\n", 0o644) + metrics = """include /etc/angie/prometheus_all.conf; +server { + listen 82; + + location /nginx_status { + stub_status on; + access_log off; + allow all; + } + + auto_redirect on; + + location /status/ { + api /status/; + api_config_files on; + } + + location /console/ { + alias /usr/share/angie-console-light/html/; + index index.html; + } + + location /console/api/ { + api /status/; + } + + location =/p8s { + prometheus all; + } +} +""" + write_file(Path("/etc/angie/metrics.conf"), metrics, 0o644) + def install_certbot_with_dns_plugins(): with step("Installing certbot + DNS plugins"): base = ["certbot"] out = run_out(["bash","-lc","apt-cache search '^python3-certbot-dns-' | awk '{print $1}'"], check=False) or "" dns_pkgs = [p for p in out.splitlines() if p.strip()] - run(["apt-get", "install", "-y"] + base + dns_pkgs) + apt_install(base + dns_pkgs) def ensure_angie_runtime_perms(): run_path = Path("/run/angie") @@ -392,7 +463,7 @@ def ensure_user_and_dirs(): try: run(["id", "-u", "npm"]) except subprocess.CalledProcessError: - run(["useradd", "--system", "--home", "/opt/npm", "--create-home", "--shell", "/usr/sbin/nologsn", "npm"]) + run(["useradd", "--system", "--home", "/opt/npm", "--create-home", "--shell", "/usr/sbin/nologin", "npm"]) run(["bash","-lc","getent group angie >/dev/null 2>&1 || groupadd angie"]) run(["bash","-lc","usermod -aG angie npm || true"]) @@ -448,7 +519,15 @@ def adjust_nginx_like_paths_in_tree(root: Path): cand.write_text(txt, encoding="utf-8") def install_node_and_yarn(node_pkg: str): - apt_install([node_pkg, "yarnpkg"]) + # Node + apt_install([node_pkg]) + # Yarn (Ubuntu/Debian variance) + if shutil.which("yarn") or shutil.which("yarnpkg"): + return + # try install 'yarn' then 'yarnpkg' + apt_try_install(["yarn"]) + if not shutil.which("yarn") and not shutil.which("yarnpkg"): + apt_try_install(["yarnpkg"]) if not Path("/usr/bin/yarn").exists() and Path("/usr/bin/yarnpkg").exists(): os.symlink("/usr/bin/yarnpkg","/usr/bin/yarn") @@ -580,7 +659,6 @@ def strip_ipv6_listens(paths): if new != txt: f.write_text(new, encoding="utf-8") - def install_logrotate_for_data_logs(): with step("Installing logrotate policy for /data/logs (*.log), keep 7 rotations"): conf_path = Path("/etc/logrotate.d/npm-data-logs") @@ -640,7 +718,6 @@ def create_systemd_units(ipv6_enabled: bool): "Group=npm", "WorkingDirectory=/opt/npm", "Environment=NODE_ENV=production", - # Environment=DISABLE_IPV6=true -> dodawane tylko gdy IPv6 NIE jest włączony flagą ] if not ipv6_enabled: unit_lines.append("Environment=DISABLE_IPV6=true") @@ -678,7 +755,9 @@ def update_motd(enabled: bool, info, ipv6_enabled: bool): creds = "Default login: admin@example.com / changeme" text = f""" ################################ NPM / ANGIE ################################ +OS: {OSREL['PRETTY']} ({OSREL['ID']} {OSREL['VERSION_ID']}) Nginx Proxy Manager: http://{ip}:81 +Angie stats & Prometheus: http://{ip}:82/console | http://{ip}:82/p8s Angie: v{angie_v} (conf: /etc/angie -> /etc/nginx, reload: angie -s reload) Node.js: v{node_v} Yarn: v{yarn_v} NPM app: v{npm_v} @@ -703,13 +782,15 @@ Paths: app=/opt/npm data=/data cache=/var/lib/angie/cache def print_summary(info, ipv6_enabled, dark_enabled, update_mode): ip, angie_v, node_v, yarn_v, npm_v = info print("\n====================== SUMMARY ======================") + print(f"OS: {OSREL['PRETTY']} ({OSREL['ID']} {OSREL['VERSION_ID']})") print(f"Mode: {'UPDATE' if update_mode else 'INSTALL'}") print(f"NPM panel address: http://{ip}:81") + print(f"angie stats & prometheus stats: http://{ip}:82/console | http://{ip}:82/p8s ") print(f"Angie: v{angie_v} (unit: angie.service, PID: /run/angie/angie.pid)") print(f"Node.js: v{node_v}") print(f"Yarn: v{yarn_v}") print(f"NPM (aplikacja): v{npm_v}") - print(f"IPv6: {'ENABLED' if ipv6_enabled else 'DISABLED (in confogs too)'}") + print(f"IPv6: {'ENABLED' if ipv6_enabled else 'DISABLED (in configs too)'}") print(f"Dark mode (TP): {'YES' if dark_enabled else 'NO'}") print("Paths: /opt/npm (app), /data (data), /etc/angie (conf), /var/log/angie (logs)") print("Services: systemctl status angie.service / npm.service") @@ -779,10 +860,12 @@ def update_only(node_pkg: str, npm_version_override: str | None, apply_dark: boo def apply_dark_mode(APP_FILEPATH="/opt/npm/frontend", TP_DOMAIN=None, TP_COMMUNITY_THEME=None, TP_SCHEME=None, TP_THEME=None): - print('--------------------------------------') - print('| Nginx Proxy Manager theme.park Mod |') - print('--------------------------------------') + if DEBUG: + print('--------------------------------------') + print('| Nginx Proxy Manager theme.park Mod |') + print('--------------------------------------') + # locate frontend if not Path(APP_FILEPATH).exists(): if Path("/app/frontend").exists(): APP_FILEPATH = "/app/frontend" @@ -790,30 +873,29 @@ def apply_dark_mode(APP_FILEPATH="/opt/npm/frontend", APP_FILEPATH = "/opt/nginx-proxy-manager/frontend" if not TP_DOMAIN or TP_DOMAIN.strip() == "": - print("No domain set, defaulting to theme-park.dev") + if DEBUG: print("No domain set, defaulting to theme-park.dev") TP_DOMAIN = "theme-park.dev" if not TP_SCHEME or TP_SCHEME.strip() == "": - print("No scheme set, defaulting to https") TP_SCHEME = "https" THEME_TYPE = "community-theme-options" if (str(TP_COMMUNITY_THEME).lower() == "true") else "theme-options" if not TP_THEME or TP_THEME.strip() == "": - print("No theme set, defaulting to organizr") TP_THEME = "organizr" if "github.io" in TP_DOMAIN: TP_DOMAIN = f"{TP_DOMAIN}/theme.park" - print("Variables set:\n" - f"'APP_FILEPATH'={APP_FILEPATH}\n" - f"'TP_DOMAIN'={TP_DOMAIN}\n" - f"'TP_COMMUNITY_THEME'={TP_COMMUNITY_THEME}\n" - f"'TP_SCHEME'={TP_SCHEME}\n" - f"'TP_THEME'={TP_THEME}\n") + if DEBUG: + print("Variables set:\n" + f"'APP_FILEPATH'={APP_FILEPATH}\n" + f"'TP_DOMAIN'={TP_DOMAIN}\n" + f"'TP_COMMUNITY_THEME'={TP_COMMUNITY_THEME}\n" + f"'TP_SCHEME'={TP_SCHEME}\n" + f"'TP_THEME'={TP_THEME}\n") base_href = f"{TP_SCHEME}://{TP_DOMAIN}/css/base/nginx-proxy-manager/nginx-proxy-manager-base.css" theme_href = f"{TP_SCHEME}://{TP_DOMAIN}/css/{THEME_TYPE}/{TP_THEME}.css" - with step("Wstrzykuję arkusze stylów Theme.Park do HTML"): + with step("Injecting Theme.Park CSS into HTML"): htmls = list(Path(APP_FILEPATH).rglob("*.html")) for path in htmls: html = path.read_text(encoding="utf-8") @@ -829,7 +911,7 @@ def main(): global DEBUG ensure_root() parser = argparse.ArgumentParser( - description="Install/upgrade NPM on Angie (Debian 13) with step animation.", + description="Install/upgrade NPM on Angie (Debian/Ubuntu) with step animation.", formatter_class=argparse.ArgumentDefaultsHelpFormatter ) parser.add_argument("--nodejs-pkg", default="nodejs", help="APT Node.js package name (e.g. nodejs, nodejs-18).") @@ -857,6 +939,7 @@ def main(): print("\n================== NPM + ANGIE installer ( https://gitea.linuxiarz.pl/gru/npm-angie-auto-install ) ==================") print("Log mode:", "DEBUG" if DEBUG else "SIMPLE") + print(f"Detected OS: {OSREL['PRETTY']} ({OSREL['ID']} {OSREL['VERSION_ID']})") print("\n@linuxiarz.pl\n") if args.update: @@ -882,10 +965,11 @@ def main(): apt_update_upgrade() apt_purge(["nginx","openresty","nodejs","npm","yarn","certbot","rustc","cargo"]) - apt_install(["ca-certificates","curl","gnupg","openssl","apache2-utils","logrotate","sudo", + apt_install(["ca-certificates","curl","gnupg","openssl","apache2-utils","logrotate","sudo","acl", "python3","python3-venv","sqlite3","build-essential"]) setup_angie() + write_metrics_files() install_certbot_with_dns_plugins() install_node_and_yarn(args.nodejs_pkg) ensure_user_and_dirs()