Files
FabledSteward/roundtable/nut_setup.py
T
2026-04-13 17:10:31 -04:00

122 lines
5.0 KiB
Python

"""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)