Problema

En infraestructuras virtualizadas es frecuente que los servidores dependan de un UPS para evitar apagados bruscos. Cuando el UPS se comunica por USB, la mayoría de las soluciones de gestión de energía en Proxmox requieren scripts personalizados o acceso directo al hardware, lo que complica la automatización y la portabilidad. El patrón típico es:

  1. Un UPS conectado vía USB a un nodo que no ejecuta directamente el software de monitorización.
  2. Necesidad de exponer los valores de batería a varios hosts sin instalar agentes complejos.
  3. Riesgo de que el driver del UPS falle y siga enviando datos “stale”, provocando decisiones de apagado erróneas.

El objetivo es disponer de un punto único que lea los datos del UPS, los sirva a través de una API estándar y permita que cualquier host (incluidos contenedores Docker) tome decisiones de apagado basadas en información fiable.

Causa

Los fallos más habituales provienen de tres áreas:

  • Drivers NUT inestables – Cuando el proceso upsd pierde la conexión con el hardware, sigue respondiendo con los últimos valores. Si el cliente interpreta esos valores como válidos, puede creer que la energía sigue disponible.
  • Falta de abstracción de lectura – Muchos scripts acceden directamente a /dev/usb/hiddev* o a /proc del host, lo que impide que otros nodos o contenedores consuman la información sin montar el dispositivo.
  • Configuración rígida de umbrales – Los umbrales de batería y tiempo de ejecución se definen en archivos locales. Si el UPS no reporta una métrica (por ejemplo, battery.runtime), el umbral nunca se dispara y el servidor permanece encendido aunque la energía se agote.

Solución

Una arquitectura basada en NUT como driver + cliente NUT read‑only en Docker resuelve los puntos anteriores:

  1. Servidor NUT dedicado – Puede ejecutarse en cualquier máquina con acceso físico al UPS (Raspberry Pi, NAS, OPNsense, o incluso el propio nodo Proxmox). El servidor expone los datos por TCP (puerto 3493) y gestiona reconexiones automáticas.
  2. Cliente Docker read‑only – Un contenedor ligero se conecta al servidor NUT, solicita LIST VAR y expone los valores a través de una API REST interna o los escribe en volúmenes compartidos. No envía comandos de apagado ni ejecuta upsmon, por lo que sigue siendo estrictamente de lectura.
  3. Manejo de datos obsoletos – El cliente interpreta respuestas ERR DATA-STALE, ERR DRIVER‑NOT‑CONNECTED o la ausencia de ups.status como “UPS no disponible”. En esos casos se genera una alarma pero nunca se inicia un apagado.
  4. Umbrales configurables – Los umbrales (porcentaje de carga, minutos restantes) se definen en variables de entorno del contenedor. Si el UPS no reporta una métrica, el cliente muestra un mensaje de “trigger no disponible” y evita crear un umbral imposible.
  5. Despliegue con Docker Compose – La solución se entrega como una imagen versionada (ghcr.io/ffind-dev/pve-ups:<tag>). El contenedor se ejecuta con los volúmenes config y logs persistentes, lo que permite conservar la configuración entre actualizaciones sin tocar el host.

Paso a paso

  1. Instalar NUT en el nodo con acceso al UPS

    apt-get install nut-server nut-client
    

    Configura ups.conf con el driver usbhid-ups y habilita MODE=standalone. Reinicia upsd.

  2. Crear archivo docker-compose.yml

    version: "3.8"
    services:
      pve-ups:
        image: ghcr.io/ffind-dev/pve-ups:3.2.0
        restart: unless-stopped
        network_mode: host
        environment:
          NUT_HOST: "192.168.1.10"
          NUT_PORT: "3493"
          UPS_NAME: "myups"
          UPS_USER: "monitor"
          UPS_PASSWORD: "secret"
          THRESHOLD_RUNTIME: "10"   # minutos
          THRESHOLD_BATTERY: "20"   # porcentaje
        volumes:
          - ./pve-ups-config:/app/config
          - ./pve-ups-logs:/app/logs
    
  3. Iniciar el contenedor

    docker compose up -d
    
  4. Configurar la acción de apagado en Proxmox
    Usa un API token con permiso Sys.PowerMgmt y crea una tarea programada que invoque pve-ups vía su API interna cuando la alarma de “low battery” sea emitida.

Cuándo aplicar esta solución

  • Entornos con varios nodos Proxmox que comparten un único UPS físico.
  • Infraestructuras donde el UPS está conectado a un dispositivo distinto al host principal (NAS, Raspberry Pi, firewall).
  • Necesidad de evitar scripts de apagado distribuidos; centralizar la lógica de decisión en un contenedor simplifica el mantenimiento.
  • Escenarios donde el driver NUT puede fallar y se requiere una política de “fail‑safe” que no apague el host por datos obsoletos.

No es la solución adecuada cuando:

  • El UPS solo necesita ser leído por un único host y ya existe una integración nativa (por ejemplo, apcupsd en el mismo nodo).
  • Se requiere control activo del UPS (ejecutar upsdrvctl shutdown), ya que el cliente Docker es estrictamente de solo lectura.

Código

# Instalar NUT server
apt-get update && apt-get install -y nut-server nut-client

# Editar /etc/nut/ups.conf (ejemplo)
cat > /etc/nut/ups.conf <<EOF
[myups]
    driver = usbhid-ups
    port = auto
    desc = "USB UPS en Raspberry Pi"
EOF

# Habilitar modo standalone y reiniciar
sed -i 's/^MODE=.*/MODE=standalone/' /etc/nut/nut.conf
systemctl restart nut-server

# Docker compose (ya mostrado en la sección anterior)
docker compose up -d

Verificación

  1. Comprobar conexión NUT

    Debería listar variables como battery.charge, battery.runtime y ups.status.

  2. Revisar logs del contenedor

    docker logs pve-ups
    

    Busca líneas que indiquen “UPS reachable” o “Data stale – alarm”.

  3. Simular pérdida de driver
    Desconecta el UPS físicamente y ejecuta upsc. La respuesta debe ser ERR DRIVER-NOT-CONNECTED. El contenedor debe registrar una alarma sin iniciar el apagado.

  4. Provocar bajo nivel de batería
    Reduce la carga del UPS (por ejemplo, desconectando la carga) hasta que battery.charge caiga bajo el umbral configurado. Verifica que el contenedor envíe la alerta y que la tarea API de Proxmox reciba la señal.

Notas adicionales

  • SNMPv3 – Si tu UPS solo expone datos vía SNMP, asegúrate de que la librería pysnmp tenga la dependencia cryptography instalada; de lo contrario la autenticación authPriv fallará silenciosamente.
  • Self‑test – Configura SELF_TEST_INTERVAL (15 min – 24 h) en una variable de entorno. El cliente omitirá la prueba mientras el UPS está en batería, evitando falsos positivos.
  • Persistencia de configuración – Mantén los volúmenes config y logs fuera del contenedor para que las actualizaciones de la imagen no sobrescriban la configuración personalizada.
  • Escalado en clúster – En un clúster Proxmox, despliega una única instancia del cliente Docker en el nodo de gestión. Usa la API de Proxmox para que todos los nodos reciban la misma señal de apagado, garantizando que el nodo que ejecuta el cliente sea el último en apagarse.