From 37a3e956de538fc75d608c50e366fe7557de9fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 24 Oct 2025 21:13:19 +0200 Subject: [PATCH] poprawki --- README.md | 4 +- install.py | 1323 ------------------------------------------------ npm_install.py | 8 +- 3 files changed, 6 insertions(+), 1329 deletions(-) delete mode 100644 install.py diff --git a/README.md b/README.md index d2fd3d9..8651ed9 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ Base URL: **https://gitea.linuxiarz.pl/gru/npm-angie-auto-install** --- ## Requirements -- Debian 13 (root privileges). -- `curl`, `python3`, `systemd`. +- Debian 13 / Ubuntu 24.04 (root privileges). +- `curl`, `python3` - Network access to fetch packages and Node/Yarn artifacts. - ~2 GB RAM (recommended) or add 2 GB swap for safer frontend builds. diff --git a/install.py b/install.py deleted file mode 100644 index 60643e5..0000000 --- a/install.py +++ /dev/null @@ -1,1323 +0,0 @@ -#!/usr/bin/env python3 - -import argparse, os, sys, json, shutil, subprocess, tarfile, tempfile, urllib.request, re, time, threading, signal -from pathlib import Path -from glob import glob - -DEBUG = False - -# ========== UI / Spinner ========== - -class Spinner: - FRAMES = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"] - def __init__(self, text): - self.text = text - self._stop = threading.Event() - self._th = threading.Thread(target=self._spin, daemon=True) - - def _spin(self): - i = 0 - while not self._stop.is_set(): - frame = self.FRAMES[i % len(self.FRAMES)] - print(f"\r{frame} {self.text} ", end="", flush=True) - time.sleep(0.12) - i += 1 - - def start(self): - if not DEBUG: - self._th.start() - else: - print(f"• {self.text} ...") - - def stop_ok(self): - if not DEBUG: - self._stop.set() - self._th.join(timeout=0.2) - print(f"\r✔ {self.text}{' ' * 20}") - else: - print(f"✔ {self.text}") - - def stop_fail(self): - if not DEBUG: - self._stop.set() - self._th.join(timeout=0.2) - print(f"\r✖ {self.text}{' ' * 20}") - else: - print(f"✖ {self.text}") - -def step(text): - class _Ctx: - def __enter__(self_inner): - self_inner.spinner = Spinner(text) - self_inner.spinner.start() - return self_inner - def __exit__(self_inner, exc_type, exc, tb): - if exc is None: - self_inner.spinner.stop_ok() - else: - self_inner.spinner.stop_fail() - return _Ctx() - -def _devnull(): - return subprocess.DEVNULL if not DEBUG else None - -def run(cmd, check=True, env=None): - if DEBUG: - print("+", " ".join(cmd)) - return subprocess.run(cmd, check=check, env=env, - stdout=None if DEBUG else subprocess.DEVNULL, - stderr=None if DEBUG else subprocess.DEVNULL) - -def run_out(cmd, check=True): - if DEBUG: - print("+", " ".join(cmd)) - result = subprocess.run(cmd, check=check, capture_output=True, text=True) - return result.stdout - -# ========== Utils ========== - -def ensure_root(): - if os.geteuid() != 0: - 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"]) - run(["apt-get", "-y", "upgrade"]) - -def apt_install(pkgs): - if not pkgs: return - 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(["apt-cache","show", p], 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)}"): - run(["apt-get", "purge", "-y"] + pkgs, check=False) - run(["apt-get", "autoremove", "-y"], check=False) - -def write_file(path: Path, content: str, mode=0o644): - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding="utf-8") - os.chmod(path, mode) - -def append_unique(path: Path, lines: str): - path.parent.mkdir(parents=True, exist_ok=True) - existing = path.read_text(encoding="utf-8") if path.exists() else "" - out = existing - for line in lines.splitlines(): - if line.strip() and line not in existing: - out += ("" if out.endswith("\n") else "\n") + line + "\n" - path.write_text(out, encoding="utf-8") - -def github_latest_release_tag(repo: str, override: str | None) -> str: - if override: - return override.lstrip("v") - url = f"https://api.github.com/repos/{repo}/releases/latest" - with step(f"Downloading from GitGub: {repo}"): - with urllib.request.urlopen(url) as r: - data = json.load(r) - tag = data["tag_name"] - return tag.lstrip("v") - -def write_resolvers_conf(ipv6_enabled: bool): - ns_v4, ns_v6 = [], [] - try: - for line in Path("/etc/resolv.conf").read_text().splitlines(): - line = line.strip() - if not line.startswith("nameserver"): - continue - ip = line.split()[1].split("%")[0] - (ns_v6 if ":" in ip else ns_v4).append(ip) - except Exception: - pass - - ips = ns_v4 + (ns_v6 if ipv6_enabled else []) - if not ips: - ips = ["1.1.1.1", "8.8.8.8"] + (["2606:4700:4700::1111", "2001:4860:4860::8888"] if ipv6_enabled else []) - - ipv6_flag = " ipv6=on" if ipv6_enabled and any(":" in x for x in ips) else "" - content = f"resolver {' '.join(ips)} valid=10s{ipv6_flag};\n" - write_file(Path("/etc/angie/conf.d/include/resolvers.conf"), content, 0o644) - -def download_extract_tar_gz(url: str, dest_dir: Path) -> Path: - dest_dir.mkdir(parents=True, exist_ok=True) - with step("Downloading and untaring"): - with urllib.request.urlopen(url) as r, tempfile.NamedTemporaryFile(delete=False) as tf: - shutil.copyfileobj(r, tf) - tf.flush() - tf_path = Path(tf.name) - with tarfile.open(tf_path, "r:gz") as t: - try: - t.extractall(dest_dir, filter="data") - except TypeError: - t.extractall(dest_dir) - top = t.getmembers()[0].name.split("/")[0] - os.unlink(tf_path) - return dest_dir / top - -# Distro info (used in banners & repo setup) -OSREL = os_release() - -# === extra sync === -def sync_backup_nginx_conf(): - from pathlib import Path - import shutil, filecmp - src = Path("/etc/nginx.bak/conf.d") - dst = Path("/etc/angie/conf.d") - if not src.exists(): - return - with step("Sync /etc/nginx.bak/conf.d -> /etc/angie/conf.d"): - for p in src.rglob("*"): - if p.is_dir(): - continue - rel = p.relative_to(src) - target = dst / rel - target.parent.mkdir(parents=True, exist_ok=True) - try: - if not target.exists() or not filecmp.cmp(p, target, shallow=False): - shutil.copy2(p, target) - except Exception as e: - print(f"Warning: sync failed for {p} -> {target}: {e}") - - -def setup_certbot_venv(venv_dir: Path = Path("/opt/certbot")): - 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"] - - 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) - - 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) - - 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 - ]) - - - 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 -fi -""" - write_file(Path("/etc/profile.d/npm-pyenv.sh"), profile_snippet, 0o644) - - 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/.") - - venv_bin = venv_dir / "bin" - pip_path = venv_bin / "pip" - certbot_path = venv_bin / "certbot" - - 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)]) - - 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) - - 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) - - - -def configure_letsencrypt(): - with step("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) - ini = """text = True -non-interactive = True -webroot-path = /data/letsencrypt-acme-challenge -key-type = ecdsa -elliptic-curve = secp384r1 -preferred-chain = ISRG Root X1 -""" - write_file(Path("/etc/letsencrypt.ini"), ini, 0o644) - run(["chown", "-R", "npm:npm", "/etc/letsencrypt"], check=False) - - -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("✔ Created symlink /etc/nginx -> /etc/angie") - return - - if link.exists() and not link.is_symlink(): - backup = Path("/etc/nginx.bak") - try: - if backup.exists(): - if backup.is_symlink() or backup.is_file(): - backup.unlink() - link.rename(backup) - print("✔ Backed up /etc/nginx to /etc/nginx.bak") - except Exception as e: - print(f"Warning: could not backup /etc/nginx: {e}") - - try: - if link.exists() or link.is_symlink(): - link.unlink() - except Exception: - pass - - try: - link.symlink_to(target) - 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 -#daemon off; - -load_module /etc/angie/modules/ngx_http_headers_more_filter_module.so; -load_module /etc/angie/modules/ngx_http_brotli_filter_module.so; -load_module /etc/angie/modules/ngx_http_brotli_static_module.so; -load_module /etc/angie/modules/ngx_http_zstd_filter_module.so; -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; - -worker_processes auto; -pcre_jit on; - -error_log /data/logs/fallback_error.log warn; - -# Custom -include /data/nginx/custom/root_top[.]conf; - -events { - include /data/nginx/custom/events[.]conf; -} - -http { - include /etc/angie/mime.types; - default_type application/octet-stream; - sendfile on; - server_tokens off; - tcp_nopush on; - tcp_nodelay on; - client_body_temp_path /tmp/angie/body 1 2; - keepalive_timeout 90s; - proxy_connect_timeout 90s; - proxy_send_timeout 90s; - proxy_read_timeout 90s; - ssl_prefer_server_ciphers on; - gzip on; - proxy_ignore_client_abort off; - client_max_body_size 2000m; - server_names_hash_bucket_size 1024; - proxy_http_version 1.1; - proxy_set_header X-Forwarded-Scheme $scheme; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Accept-Encoding ""; - proxy_cache off; - proxy_cache_path /var/lib/angie/cache/public levels=1:2 keys_zone=public-cache:30m max_size=192m; - proxy_cache_path /var/lib/angie/cache/private levels=1:2 keys_zone=private-cache:5m max_size=1024m; - - include /etc/angie/conf.d/include/log.conf; - include /etc/angie/conf.d/include/resolvers.conf; - - map $host $forward_scheme { default http; } - - # Real IP Determination (IPv4 only by default) - set_real_ip_from 10.0.0.0/8; - set_real_ip_from 172.16.0.0/12; - set_real_ip_from 192.168.0.0/16; - include /etc/angie/conf.d/include/ip_ranges.conf; - real_ip_header X-Real-IP; - real_ip_recursive on; - - # custom - brotli off; - brotli_comp_level 6; - brotli_static on; - brotli_types *; - zstd on; - zstd_min_length 256; - zstd_comp_level 3; - more_clear_headers "Server"; - more_set_headers 'X-by: linuxiarz.pl'; - - # npm - include /data/nginx/custom/http_top[.]conf; - include /etc/nginx/conf.d/*.conf; - include /data/nginx/default_host/*.conf; - include /data/nginx/proxy_host/*.conf; - include /data/nginx/redirection_host/*.conf; - 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 { - # npm - include /data/nginx/stream/*.conf; - include /data/nginx/custom/stream[.]conf; -} -# npm -include /data/nginx/custom/root[.]conf; -""" - -ANGIE_UNIT = """[Unit] -Description=Angie - high performance web server -Documentation=https://en.angie.software/angie/docs/ -After=network-online.target remote-fs.target nss-lookup.target -Wants=network-online.target - -[Service] -Type=forking -PIDFile=/run/angie/angie.pid -ExecStartPre=/bin/mkdir -p /run/angie -ExecStartPre=/bin/mkdir -p /tmp/angie/body -ExecStart=/usr/sbin/angie -c /etc/angie/angie.conf -ExecReload=/bin/sh -c "/bin/kill -s HUP $(/bin/cat /run/angie/angie.pid)" -ExecStop=/bin/sh -c "/bin/kill -s TERM $(/bin/cat /run/angie/angie.pid)" -Restart=on-failure -RestartSec=3s - -[Install] -WantedBy=multi-user.target -""" - -def lsb_info(): - try: - apt_try_install(["lsb-release"]) - dist = run_out(["bash","-lc","lsb_release -si"]).strip().lower().replace(" ", "") - rel = run_out(["bash","-lc","lsb_release -sr"]).strip() - code = run_out(["bash","-lc","lsb_release -sc"]).strip() - return {"ID": dist, "VERSION_ID": rel, "CODENAME": code, "PRETTY": f"{dist} {rel} ({code})"} - except Exception: - return os_release() - - -# ========== Angie ========== -def setup_angie(ipv6_enabled: bool): - def _norm(s: str, allow_dot: bool = False) -> str: - pat = r"[^a-z0-9+\-\.]" if allow_dot else r"[^a-z0-9+\-]" - return re.sub(pat, "", s.strip().lower()) - - with step("Adding Angie repo and installing Angie packages"): - apt_try_install([ - "ca-certificates", "curl", "gnupg", "apt-transport-https", - "software-properties-common", "lsb-release" - ]) - - run([ - "curl", "-fsSL", "-o", "/etc/apt/trusted.gpg.d/angie-signing.gpg", - "https://angie.software/keys/angie-signing.gpg" - ]) - - try: - dist = run_out(["lsb_release", "-si"]) - rel = run_out(["lsb_release", "-sr"]) - code = run_out(["lsb_release", "-sc"]) - except Exception: - dist = run_out(["bash","-c",". /etc/os-release && printf %s \"$ID\""]) - rel = run_out(["bash","-c",". /etc/os-release && printf %s \"$VERSION_ID\""]) - code = run_out(["bash","-c",". /etc/os-release && printf %s \"$VERSION_CODENAME\""]) - - dist = _norm(dist) - rel = _norm(rel, allow_dot=True) - code = _norm(code) - - os_id = f"{dist}/{rel}" if rel else dist - if code: - line = f"deb https://download.angie.software/angie/{os_id} {code} main\n" - else: - line = f"deb https://download.angie.software/angie/{os_id} main\n" - - write_file(Path("/etc/apt/sources.list.d/angie.list"), line) - run(["apt-get", "update"]) - - 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) - - WRAP = """#!/bin/sh -exec sudo -n /usr/sbin/angie "$@" -""" - write_file(Path("/usr/sbin/nginx"), WRAP, 0o755) - Path("/etc/nginx/conf.d/include").mkdir(parents=True, exist_ok=True) - - with step("Setting resolver(s) and cache directories"): - write_resolvers_conf(ipv6_enabled) - for p in ["/var/lib/angie/cache/public", "/var/lib/angie/cache/private"]: - Path(p).mkdir(parents=True, exist_ok=True) - os.chmod(p, 0o755) - - 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/metrics.conf (port 82 with console & status).""" - with step("Adding Angie metrics & console on :82"): - 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 ensure_angie_runtime_perms(): - run_path = Path("/run/angie") - pid_file = run_path / "angie.pid" - run_path.mkdir(parents=True, exist_ok=True) - os.chmod(run_path, 0o2775) - try: - import grp - gid = grp.getgrnam("angie").gr_gid - os.chown(run_path, -1, gid) - except Exception: - pass - if not pid_file.exists(): - pid_file.touch() - os.chmod(pid_file, 0o664) - try: - import grp, pwd - gid = grp.getgrnam("angie").gr_gid - uid = pwd.getpwnam("root").pw_uid - os.chown(pid_file, uid, gid) - except Exception: - pass - -def ensure_user_and_dirs(): - with step("Creating npm user and app/log directories"): - try: - run(["id", "-u", "npm"]) - except subprocess.CalledProcessError: - run(["useradd", "--system", "--home", "/opt/npm", "--create-home", "--shell", "/usr/sbin/nologin", "npm"]) - rc = subprocess.run(["getent","group","angie"], stdout=_devnull(), stderr=_devnull()).returncode - if rc != 0: - run(["groupadd","angie"]) - run(["usermod","-aG","angie","npm"], check=False) - - dirs = [ - "/data","/data/nginx","/data/custom_ssl","/data/logs","/data/access", - "/data/nginx/default_host","/data/nginx/default_www","/data/nginx/proxy_host", - "/data/nginx/redirection_host","/data/nginx/stream","/data/nginx/dead_host","/data/nginx/temp", - "/data/letsencrypt-acme-challenge","/opt/npm","/opt/npm/frontend","/opt/npm/global", - "/run/nginx","/run/angie","/tmp/angie/body" - ] - for d in dirs: - Path(d).mkdir(parents=True, exist_ok=True) - run(["chgrp","-h","angie","/run/angie"], check=False) - os.chmod("/run/angie", 0o2775) - Path("/var/log/angie").mkdir(parents=True, exist_ok=True) - for f in ["access.log","error.log"]: - (Path("/var/log/angie")/f).touch(exist_ok=True) - paths = ["/var/log/angie"] + glob("/var/log/angie/*.log") - for pth in paths: - run(["chgrp","-h","angie", pth], check=False) - run(["chmod","775","/var/log/angie"], check=False) - for pth in glob("/var/log/angie/*.log"): - run(["chmod","664", pth], check=False) - Path("/var/log/nginx").mkdir(parents=True, exist_ok=True) - Path("/var/log/nginx/error.log").touch(exist_ok=True) - os.chmod("/var/log/nginx/error.log", 0o666) - run(["chown","-R","npm:npm","/opt/npm","/data"]) - ensure_angie_runtime_perms() - -def create_sudoers_for_npm(): - with step("Configuring sudoers for npm -> angie"): - content = """User_Alias NPMUSERS = npm -NPMUSERS ALL=(root) NOPASSWD: /usr/sbin/angie -""" - path = Path("/etc/sudoers.d/npm") - write_file(path, content, 0o440) - if shutil.which("visudo"): - run(["visudo","-cf", str(path)], check=False) - -def adjust_nginx_like_paths_in_tree(root: Path): - for p in root.rglob("*.conf"): - try: - txt = p.read_text(encoding="utf-8") - except Exception: - continue - txt2 = txt.replace("include conf.d", "include /etc/nginx/conf.d") \ - .replace("include /etc/angie/conf.d", "include /etc/nginx/conf.d") - if txt2 != txt: - p.write_text(txt2, encoding="utf-8") - for cand in root.rglob("nginx.conf"): - try: - txt = cand.read_text(encoding="utf-8") - except Exception: - continue - txt = re.sub(r"^user\s+\S+.*", "user root;", txt, flags=re.M) - txt = re.sub(r"^pid\s+.*", "pid /run/angie/angie.pid;", txt, flags=re.M) - txt = txt.replace("daemon on;", "#daemon on;") - cand.write_text(txt, encoding="utf-8") - -def install_node_and_yarn(node_pkg: str): - apt_install([node_pkg]) - if shutil.which("yarn") or shutil.which("yarnpkg"): - return - 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") - -def _is_ubuntu_wo_distutils() -> bool: - try: - dist = (OSREL.get("ID","") or "").lower() - ver = (OSREL.get("VERSION_ID","") or "").strip() - def _vers(t): - parts = (ver.split(".") + ["0","0"])[:2] - return (int(parts[0]), int(parts[1])) - return dist == "ubuntu" and _vers(ver) >= (24, 4) - except Exception: - return False - -def _prepare_sass(frontend_dir: Path): - pj = frontend_dir / "package.json" - if not pj.exists(): - return - - import json, re, os - try: - data = json.loads(pj.read_text(encoding="utf-8")) - except Exception: - return - - deps = data.get("dependencies", {}) or {} - dev = data.get("devDependencies", {}) or {} - has_node_sass = ("node-sass" in deps) or ("node-sass" in dev) - if not has_node_sass: - return - - env_flag = (os.environ.get("USE_DART_SASS","").strip()) - use_dart = (env_flag == "1") or (env_flag == "" and _is_ubuntu_wo_distutils()) - - data.setdefault("dependencies", {}) - data.setdefault("devDependencies", {}) - - if use_dart: - data["dependencies"].pop("node-sass", None) - data["devDependencies"].pop("node-sass", None) - if "sass" not in data["dependencies"] and "sass" not in data["devDependencies"]: - data["devDependencies"]["sass"] = "^1.77.0" - - scripts = (data.get("scripts") or {}) - data["scripts"] = {k: re.sub(r"\bnode-sass\b", "sass", v or "") for k, v in scripts.items()} - - if env_flag == "": - os.environ["USE_DART_SASS"] = "1" - else: - target = "^9.0.0" - if "node-sass" in data["dependencies"]: - data["dependencies"]["node-sass"] = target - else: - data["devDependencies"]["node-sass"] = target - - res = (data.get("resolutions") or {}) - res["node-gyp"] = "^10.0.0" - res["node-sass"] = "^9.0.0" - data["resolutions"] = res - - os.environ["npm_config_node_sass_binary_site"] = "https://github.com/sass/node-sass/releases/download" - - pj.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - - -def _build_frontend(src_frontend: Path, dest_frontend: Path): - - - def _semver(s: str) -> bool: - return bool(re.match(r"^\d+(?:\.\d+){1,3}$", (s or "").strip())) - - def _good_yarn(argv: list[str]) -> bool: - v = (run_out(argv + ["--version"], check=False) or "").strip() - return _semver(v) - - def _pick_yarn_cmd() -> list[str] | None: - for c in (["yarn"], ["yarnpkg"]): - if shutil.which(c[0]) and _good_yarn(c): - return c - if shutil.which("npm") and (run_out(["npm", "--version"], check=False) or "").strip(): - if _good_yarn(["npm", "exec", "--yes", "yarn@stable"]): - return ["npm", "exec", "--yes", "yarn@stable"] - if shutil.which("npx") and (run_out(["npx", "--version"], check=False) or "").strip(): - if _good_yarn(["npx", "-y", "yarn@stable"]): - return ["npx", "-y", "yarn@stable"] - return None - - def _ensure_yarn_installed(): - if not shutil.which("npm"): - try: - apt_try_install(["npm"]) - except Exception: - run(["apt-get", "update"], check=False) - run(["apt-get", "install", "-y", "npm"]) - run(["npm", "install", "-g", "yarn"], check=False) - - yarn_cmd = _pick_yarn_cmd() - if not yarn_cmd: - _ensure_yarn_installed() - yarn_cmd = _pick_yarn_cmd() - if not yarn_cmd: - raise RuntimeError("Unable to detect or install a valid Yarn. Try: apt-get install -y npm && npm i -g yarn.") - - with step("Installing frontend dependencies (yarn)"): - os.environ["NODE_ENV"] = "development" - os.chdir(src_frontend) - _prepare_sass(src_frontend) - - cache_dir = (run_out(yarn_cmd + ["cache", "dir"], check=False) or "").strip() - if cache_dir and not Path(cache_dir).exists(): - Path(cache_dir).mkdir(parents=True, exist_ok=True) - - run(yarn_cmd + ["cache", "clean"], check=False) - run(yarn_cmd + ["install"]) - - with step("Building frontend (yarn build)"): - env = os.environ.copy() - env["NODE_OPTIONS"] = "--openssl-legacy-provider" - run(yarn_cmd + ["build"], env=env) - - with step("Copying frontend artifacts"): - shutil.copytree(src_frontend / "dist", dest_frontend, dirs_exist_ok=True) - if (src_frontend / "app-images").exists(): - shutil.copytree(src_frontend / "app-images", dest_frontend / "images", dirs_exist_ok=True) - - -def patch_npm_backend_commands(): - candidates = [ - Path("/opt/npm/lib/utils.js"), - Path("/opt/npm/utils.js"), - Path("/opt/npm/lib/commands.js"), - ] - for p in candidates: - if not p.exists(): - continue - try: - txt = p.read_text(encoding="utf-8") - except Exception: - continue - new = re.sub(r'\blogrotate\b', '/usr/local/bin/logrotate-npm', txt) - new = re.sub(r'(? /etc/nginx, reload: angie -s reload) -Node.js: v{node_v} Yarn: v{yarn_v} -NPM app: v{npm_v} -Paths: app=/opt/npm data=/data cache=/var/lib/angie/cache -{ipv6_line} -{creds} -########################################################################### -""" - motd_d = Path("/etc/motd.d") - if motd_d.exists(): - write_file(motd_d / "10-npm-angie", text.strip() + "\n", 0o644) - else: - motd = Path("/etc/motd") - existing = motd.read_text(encoding="utf-8") if motd.exists() else "" - pattern = re.compile(r"################################ NPM / ANGIE ################################.*?###########################################################################\n", re.S) - if pattern.search(existing): - content = pattern.sub(text.strip()+"\n", existing) - else: - content = (existing.rstrip()+"\n\n"+text.strip()+"\n") if existing else (text.strip()+"\n") - write_file(motd, content, 0o644) - -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 & 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 (app): v{npm_v}") - 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") - print("Default login: Email: admin@example.com Password: changeme") - print("Test config: /usr/sbin/angie -t") - print("==========================================================\n") - -# ========== UPDATE-ONLY ========== - -def update_only(node_pkg: str, npm_version_override: str | None, apply_dark: bool, dark_env: dict, ipv6_enabled: bool): - apt_update_upgrade() - install_node_and_yarn(node_pkg) - - version = github_latest_release_tag("NginxProxyManager/nginx-proxy-manager", npm_version_override) - url = f"https://codeload.github.com/NginxProxyManager/nginx-proxy-manager/tar.gz/refs/tags/v{version}" - tmp = Path(tempfile.mkdtemp(prefix="npm-update-")) - src = download_extract_tar_gz(url, tmp) - - with step("Setting version in package.json (update)"): - for pkg in ["backend/package.json", "frontend/package.json"]: - pj = src / pkg - txt = pj.read_text(encoding="utf-8") - txt = re.sub(r'"version":\s*"0\.0\.0"', f'"version": "{version}"', txt) - pj.write_text(txt, encoding="utf-8") - - _build_frontend(src / "frontend", Path("/opt/npm/frontend")) - - with step("Updating backend without overwriting config/"): - backup_cfg = Path("/tmp/npm-config-backup") - if backup_cfg.exists(): - shutil.rmtree(backup_cfg) - if Path("/opt/npm/config").exists(): - shutil.copytree("/opt/npm/config", backup_cfg, dirs_exist_ok=True) - for item in Path("/opt/npm").glob("*"): - if item.name in ("frontend","config"): - continue - if item.is_dir(): - shutil.rmtree(item) - else: - item.unlink() - shutil.copytree(src / "backend", "/opt/npm", dirs_exist_ok=True) - Path("/opt/npm/config").mkdir(parents=True, exist_ok=True) - if backup_cfg.exists(): - shutil.copytree(backup_cfg, "/opt/npm/config", dirs_exist_ok=True) - shutil.rmtree(backup_cfg, ignore_errors=True) - - with step("Installing backend dependencies after update"): - os.chdir("/opt/npm") - run(["yarn", "install"]) - - patch_npm_backend_commands() - create_systemd_units(ipv6_enabled=ipv6_enabled) - - with step("Setting owners"): - run(["chown","-R","npm:npm","/opt/npm"]) - - if apply_dark: - apply_dark_mode(**dark_env) - - with step("Restarting services after update"): - run(["systemctl","restart","angie.service"], check=False) - run(["systemctl","restart","npm.service"], check=False) - - return version - -# ========== DARK MODE ========== - -def apply_dark_mode(APP_FILEPATH="/opt/npm/frontend", - TP_DOMAIN=None, TP_COMMUNITY_THEME=None, TP_SCHEME=None, TP_THEME=None): - 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" - elif Path("/opt/nginx-proxy-manager/frontend").exists(): - APP_FILEPATH = "/opt/nginx-proxy-manager/frontend" - - if not TP_DOMAIN or TP_DOMAIN.strip() == "": - if DEBUG: print("No domain set, defaulting to theme-park.dev") - TP_DOMAIN = "theme-park.dev" - if not TP_SCHEME or TP_SCHEME.strip() == "": - 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() == "": - TP_THEME = "organizr" - - if "github.io" in TP_DOMAIN: - TP_DOMAIN = f"{TP_DOMAIN}/theme.park" - - 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("Injecting Theme.Park CSS into HTML"): - htmls = list(Path(APP_FILEPATH).rglob("*.html")) - for path in htmls: - html = path.read_text(encoding="utf-8") - if base_href not in html: - html = re.sub(r"", f" ", html, flags=re.I) - html = re.sub(r"", f" ", html, flags=re.I) - path.write_text(html, encoding="utf-8") - if DEBUG: - print(f"Patched: {path}") - -# ========== MAIN ========== -def main(): - global DEBUG - ensure_root() - parser = argparse.ArgumentParser( - 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).") - parser.add_argument("--npm-version", default=None, help="Force NPM app version (e.g. 2.12.6). Default: latest release.") - parser.add_argument("--motd", choices=["yes","no"], default="yes", help="Update MOTD after completion.") - parser.add_argument("--enable-ipv6", action="store_true", - help="Do not strip IPv6 from configs/resolvers (keep IPv6).") - parser.add_argument("--update", action="store_true", - help="Update mode: upgrade packages + rebuild frontend/backend without reconfiguring Angie.") - parser.add_argument("--dark-mode", action="store_true", - help="Inject Theme.Park CSS into frontend (see TP_* vars).") - parser.add_argument("--tp-domain", default=os.environ.get("TP_DOMAIN", ""), - help="Theme.Park domain (e.g. theme-park.dev or *.github.io).") - parser.add_argument("--tp-community-theme", default=os.environ.get("TP_COMMUNITY_THEME", "false"), - help="true = community-theme-options; false = theme-options.") - parser.add_argument("--tp-scheme", default=os.environ.get("TP_SCHEME", "https"), - help="URL scheme (http/https).") - parser.add_argument("--tp-theme", default=os.environ.get("TP_THEME", "organizr"), - help="Theme.Park theme name (e.g. organizr, catppuccin).") - parser.add_argument("--debug", action="store_true", - help="Show detailed logs and progress.") - - args = parser.parse_args() - DEBUG = args.debug - - 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: - install_logrotate_for_data_logs() - fix_logrotate_permissions_and_wrapper() - version = update_only( - node_pkg=args.nodejs_pkg, - npm_version_override=args.npm_version, - apply_dark=args.dark_mode, - dark_env=dict( - APP_FILEPATH="/opt/npm/frontend", - TP_DOMAIN=args.tp_domain, - TP_COMMUNITY_THEME=args.tp_community_theme, - TP_SCHEME=args.tp_scheme, - TP_THEME=args.tp_theme, - ), - ipv6_enabled=args.enable_ipv6, - ) - info = gather_versions(version) - update_motd(args.motd == "yes", info, ipv6_enabled=args.enable_ipv6) - print_summary(info, args.enable_ipv6, args.dark_mode, update_mode=True) - return - - apt_update_upgrade() - apt_purge(["nginx","openresty","nodejs","npm","yarn","certbot","rustc","cargo"]) - apt_install(["ca-certificates","curl","gnupg","openssl","apache2-utils","logrotate","sudo","acl", - "python3","sqlite3", "git", "lsb-release", "build-essential"]) - - setup_angie(ipv6_enabled=args.enable_ipv6) - write_metrics_files() - install_node_and_yarn(args.nodejs_pkg) - ensure_user_and_dirs() - create_sudoers_for_npm() - setup_certbot_venv() - configure_letsencrypt() - - npm_app_version = deploy_npm_app(args.npm_version) - - if not args.enable_ipv6: - strip_ipv6_listens([Path("/etc/angie"), Path("/etc/nginx")]) - else: - print("IPv6: leaving entries (skipped IPv6 cleanup).") - - if args.dark_mode: - apply_dark_mode(APP_FILEPATH="/opt/npm/frontend", - TP_DOMAIN=args.tp_domain, - TP_COMMUNITY_THEME=args.tp_community_theme, - TP_SCHEME=args.tp_scheme, - TP_THEME=args.tp_theme) - - create_systemd_units(ipv6_enabled=args.enable_ipv6) - - ensure_nginx_symlink() - install_logrotate_for_data_logs() - fix_logrotate_permissions_and_wrapper() - sync_backup_nginx_conf() - with step("Restarting services after installation"): - run(["systemctl","restart","angie.service"], check=False) - run(["systemctl","restart","npm.service"], check=False) - - info = gather_versions(npm_app_version) - update_motd(args.motd == "yes", info, ipv6_enabled=args.enable_ipv6) - print_summary(info, args.enable_ipv6, args.dark_mode, update_mode=False) - -if __name__ == "__main__": - signal.signal(signal.SIGINT, lambda s, f: sys.exit(130)) - main() diff --git a/npm_install.py b/npm_install.py index 016083e..f61de42 100644 --- a/npm_install.py +++ b/npm_install.py @@ -188,7 +188,7 @@ def comment_x_served_by_step(path="/etc/angie/conf.d/include/proxy.conf"): Path(tmp).write_text(out) shutil.copymode(p, tmp) os.replace(tmp, p) - print(f"✔ Commented {count} line(s) | backup: {backup}") + print(f"✔ Hide X-Server-by header | backup: {backup}") return count @@ -240,7 +240,7 @@ def setup_certbot_venv(venv_dir: Path = Path("/opt/certbot")): # --- Debian pyenv PYENV_ROOT = Path("/opt/npm/.pyenv") PYENV_OWNER = "npm" - PYTHON_VERSION = "3.11.11" + PYTHON_VERSION = "3.11.X" PYENV_BIN_CANDIDATES = ["pyenv", "/usr/bin/pyenv", "/usr/lib/pyenv/bin/pyenv"] # --- Ubuntu: PPA deadsnakes + venv --- @@ -359,7 +359,7 @@ fi pip_path = venv_bin / "pip" certbot_path = venv_bin / "certbot" - with step(f"Preparing Certbot venv at {venv_dir} (Python {PYTHON_VERSION})"): + 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)]) @@ -379,7 +379,7 @@ fi 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()}") + #print(f"Certbot: {cb_ver.strip()} | Pip: {pip_ver.strip()}") run(["chown", "-R", f"{PYENV_OWNER}:{PYENV_OWNER}", str(PYENV_ROOT)], check=False)