"""Generate NUT (Network UPS Tools) configuration files. Called by entrypoint.sh before the NUT daemons start. Reads config either from environment variables (legacy NUT_MANAGED=1 path) or from a JSON file written by the FabledScryer settings page (/data/nut_managed.json). Environment variables (used when no --from-file is given): NUT_DRIVER NUT driver (default: snmp-ups) snmp-ups — any UPS with SNMP management card (APC, Eaton, CyberPower, Tripp Lite, Liebert, ...) netxml-ups — Eaton Network Management Card (XML/HTTP) NUT_UPS_NAME Name exposed by upsd (default: ups) Must match 'ups_name' in the UPS plugin settings. NUT_UPS_HOST IP address or hostname of the UPS management card (required) NUT_SNMP_COMMUNITY SNMP community string (default: public) [snmp-ups only] NUT_SNMP_VERSION SNMP version: v1, v2c (default: v1) [snmp-ups only] NUT_USERNAME Optional upsd username for authenticated access NUT_PASSWORD Optional upsd password JSON file format (--from-file path): { "ups_host": "192.168.1.x", // required "driver": "snmp-ups", "ups_name": "ups", "snmp_community": "public", "snmp_version": "v1", "username": "", "password": "" } """ import json import os import sys from pathlib import Path NUT_CONF_DIR = Path("/etc/nut") NUT_MANAGED_JSON = Path("/data/nut_managed.json") def _env(key: str, default: str = "") -> str: return os.environ.get(key, default).strip() def write_configs(cfg: dict | None = None) -> None: """Write NUT config files. If cfg is given, use it; otherwise fall back to env vars.""" NUT_CONF_DIR.mkdir(parents=True, exist_ok=True) if cfg is not None: driver = cfg.get("driver", "snmp-ups").strip() ups_name = cfg.get("ups_name", "ups").strip() ups_host = cfg.get("ups_host", "").strip() community = cfg.get("snmp_community", "public").strip() snmp_ver = cfg.get("snmp_version", "v1").strip() username = cfg.get("username", "").strip() password = cfg.get("password", "").strip() else: driver = _env("NUT_DRIVER", "snmp-ups") ups_name = _env("NUT_UPS_NAME", "ups") ups_host = _env("NUT_UPS_HOST") community = _env("NUT_SNMP_COMMUNITY", "public") snmp_ver = _env("NUT_SNMP_VERSION", "v1") username = _env("NUT_USERNAME") password = _env("NUT_PASSWORD") if not ups_host: print("[nut_setup] ERROR: NUT_UPS_HOST is required", file=sys.stderr) sys.exit(1) # ── nut.conf ─────────────────────────────────────────────────────────────── (NUT_CONF_DIR / "nut.conf").write_text("MODE=standalone\n") # ── ups.conf ─────────────────────────────────────────────────────────────── ups_block = [f"[{ups_name}]"] ups_block.append(f" driver = {driver}") if driver == "netxml-ups": # Eaton: port is a URL ups_block.append(f" port = http://{ups_host}") else: # snmp-ups and others: port is the IP/hostname ups_block.append(f" port = {ups_host}") if driver == "snmp-ups": ups_block.append(f" community = {community}") ups_block.append(f" snmp_version = {snmp_ver}") ups_block.append(f' desc = "Network UPS at {ups_host}"') (NUT_CONF_DIR / "ups.conf").write_text("\n".join(ups_block) + "\n") # ── upsd.conf ────────────────────────────────────────────────────────────── (NUT_CONF_DIR / "upsd.conf").write_text("LISTEN 127.0.0.1 3493\n") # ── upsd.users ───────────────────────────────────────────────────────────── if username and password: users_content = ( f"[{username}]\n" f" password = {password}\n" f" upsmon primary\n" ) else: users_content = "# No users configured — unauthenticated access\n" users_path = NUT_CONF_DIR / "upsd.users" users_path.write_text(users_content) users_path.chmod(0o640) print( f"[nut_setup] Config written: driver={driver} host={ups_host} ups_name={ups_name}" ) if __name__ == "__main__": try: cfg = None if "--from-file" in sys.argv: idx = sys.argv.index("--from-file") json_path = Path(sys.argv[idx + 1]) with json_path.open() as f: cfg = json.load(f) write_configs(cfg) except Exception as exc: print(f"[nut_setup] ERROR: {exc}", file=sys.stderr) sys.exit(1)