Problema
En infraestructuras caseras o de pequeña empresa es frecuente conectar varios servidores, NAS, contenedores y máquinas virtuales a una única UPS. Cuando la energía se corta, la mayoría de los sistemas solo ejecutan el script de apagado del propio host. El resto de los equipos sigue funcionando sin coordinación, lo que provoca pérdida de datos, corrupción de sistemas de archivos y, en el peor caso, hardware dañado por apagados bruscos. El patrón problemático es la falta de un mecanismo central que:
- Detecte la caída de la UPS a través de NUT (Network UPS Tools).
- Defina un orden de apagado que respete dependencias (por ejemplo, detener primero los servicios de aplicación, luego los contenedores, después las VMs y por último los dispositivos de almacenamiento).
- Ejecute la secuencia de forma fiable, incluso cuando el daemon que controla la UPS está corriendo en un contenedor sin privilegios de root.
Sin esa orquestación, los administradores pasan horas depurando fallos intermitentes después de cada corte de energía.
Causa
Los fallos aparecen por varias razones habituales:
- Scripts monolíticos: la mayoría de los tutoriales de NUT recomiendan colocar un único
upsmonque ejecutashutdown -h nowal detectar bajo voltaje. Ese enfoque asume que solo hay una máquina a apagar. - Falta de visibilidad entre hosts: cada nodo desconoce el estado de los demás, por lo que no se puede garantizar que los recursos compartidos (NFS, SMB) se sincronicen antes de que el NAS sea desconectado.
- Redundancia no considerada: en entornos con fuentes de alimentación duales (A+B) o varios UPS alimentando el mismo rack, un simple umbral de batería dispara el apagado aunque todavía haya energía suficiente en otra unidad.
- Despliegues containerizados: cuando el daemon de monitoreo corre dentro de Docker, el proceso no tiene acceso directo a
systemctlni aupsrw, lo que impide ejecutar comandos críticos sin una capa de delegación (SSH loopback, API interna, etc.). - Configuraciones estáticas: cambios en la topología (añadir una nueva VM o un nuevo contenedor) requieren reiniciar el daemon, lo que deja una ventana sin protección durante el reload.
Solución
Una arquitectura basada en un daemon centralizado que combine NUT, una API REST y un motor de orquestación de apagados resuelve los puntos anteriores. La solución se compone de tres bloques:
-
Monitor de UPS
- Usa
nut-monitoroupsdpara recibir eventos de cada UPS conectado. - Configura cada UPS como device independiente dentro del mismo daemon; así se pueden crear grupos de redundancia (p.ej., “grupo‑A” con dos UPS, umbral de 1/2 fallos).
- Al recibir un evento crítico (
LOWBATT,ONBATTprolongado), el daemon publica el estado en un bus interno (MQTT o canal de notificaciones) y dispara la cadena de apagado.
- Usa
-
Motor de orquestación
- Define fases (compute → storage → network). Cada fase contiene una lista de targets (hosts, contenedores, VMs).
- Dentro de una fase, los targets se ejecutan en paralelo; entre fases, la ejecución es secuencial.
- Cada target se describe con una acción (Docker‑compose down,
virsh shutdown,systemctl stop nfs-server, etc.) y una condición de éxito (código de retorno, verificación de socket). - El motor soporta dry‑run: simula la secuencia sin enviar los comandos reales, útil para validar el orden antes de un corte real.
-
API y UI ligera
- Un servidor HTTP embebido expone endpoints de solo‑lectura (
GET /status,GET /history) y de control (POST /shutdown,POST /reload). - La autenticación se maneja con usuarios locales o API‑keys; los tokens pueden ser revocados sin reiniciar el daemon.
- Opcionalmente, una UI estática (HTML/JS) se sirve directamente desde el proceso, evitando la necesidad de un reverse‑proxy externo.
- Un servidor HTTP embebido expone endpoints de solo‑lectura (
Implementación práctica
-
Instalación del daemon
- Usa la imagen oficial de Docker (
ghcr.io/m4r1k/eneru:latest) o compila el paquete Python en el host. - Monta los volúmenes de configuración y estado para que persistan entre reinicios.
- Usa la imagen oficial de Docker (
-
Configuración de NUT
- En
/etc/nut/ups.confdeclara cada UPS con su driver y credenciales. - En
/etc/nut/upsmon.confhabilitaMONITORpara cada dispositivo y apunta al daemon medianteNOTIFYCMD /usr/local/bin/eneru-notify.
- En
-
Definición de la cadena de apagado (ejemplo YAML simplificado):
shutdown: phases: - name: compute parallel: true targets: - type: docker-compose path: /srv/webapp/docker-compose.yml - type: systemd service: myapp.service - name: storage parallel: false targets: - type: ssh host: nas.local command: "systemctl stop nfs-server && umount -a" - name: network parallel: true targets: - type: vm name: router-vm -
Redundancia de UPS
- En la configuración del daemon, crea un group con los IDs de los UPS y define
min_healthy: 2. El motor solo inicia el apagado cuando menos de dos UPS del grupo siguen reportandoOL(On Line).
- En la configuración del daemon, crea un group con los IDs de los UPS y define
-
Persistencia de notificaciones
- Configura un queue en SQLite que almacene los mensajes pendientes. El daemon reintenta automáticamente después de una reconexión a Internet o a la API de notificaciones (Telegram, Slack, etc.).
Cuándo aplicar esta solución
- Entornos mixtos: servidores bare‑metal, VMs y contenedores alimentados por la misma UPS.
- Redundancia física: racks con dos fuentes de alimentación o varios UPS en paralelo.
- Requerimientos de integridad: bases de datos o sistemas de archivos distribuidos que no toleran apagados abruptos.
- Despliegues containerizados: cuando el daemon debe correr como contenedor sin privilegios de root.
No es necesario si:
- Solo hay un host conectado a la UPS y el script de apagado estándar cubre todas las dependencias.
- La infraestructura no tiene requisitos de ordenamiento (p.ej., todos los nodos son idénticos y pueden apagarse simultáneamente).
Código
# Ejecutar el daemon en Docker con configuración persistente
docker run -d --name eneru \
-p 9191:9191 \
-v /srv/eneru/config.yaml:/etc/ups-monitor/config.yaml:ro \
-v /srv/eneru/state:/var/lib/eneru \
ghcr.io/m4r1k/eneru:latest \
run --config /etc/ups-monitor/config.yaml \
--api --api-bind 0.0.0.0 --api-port 9191
# Recargar configuración sin reiniciar (systemd)
systemctl reload eneru.service
# O vía API
curl -X POST http://localhost:9191/api/v1/reload -H "Authorization: Bearer <API_KEY>"
# Simular la secuencia completa (dry‑run)
curl -X POST http://localhost:9191/api/v1/shutdown \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"dry_run": true}'
Verificación
- Estado de la UPS
upsc <UPS_NAME>@localhostdebe devolverbattery.charge,battery.runtimeyups.status.
- Endpoint de salud
curl http://localhost:9191/api/v1/healthdebe responder{"status":"ok"}.
- Prueba de fase
- Ejecuta el dry‑run y revisa el log (
/var/log/eneru.log). Cada fase debe listarse en orden y los comandos a ejecutar deben mostrarse sin errores.
- Ejecuta el dry‑run y revisa el log (
- Corte real
- Simula una caída de batería con
upsrw -u <UPS_NAME> beeper. Verifica que el daemon inicia la fase compute y que los contenedores se detienen antes de que el NAS sea desmontado.
- Simula una caída de batería con
Notas adicionales
- Sincronización de buffers: antes de apagar un host, siempre ejecuta
sync && echo 3 > /proc/sys/vm/drop_cachespara forzar la escritura de datos pendientes. - Tiempo de espera: ajusta
shutdown_timeoutpor fase (p.ej., 120 s para bases de datos, 30 s para contenedores) para evitar que procesos críticos se corten antes de terminar. - Monitoreo externo: expón métricas de Prometheus (
/metrics) para observar la frecuencia de eventosONBATTy el tiempo medio de recuperación. - Seguridad: mantén la contraseña de NUT fuera de la línea de comandos usando
upsrwcon--password-file. El daemon lee el archivo en tiempo de arranque y nunca lo expone en los procesos. - Escalado: en clústers grandes, despliega una instancia del daemon por zona de disponibilidad y usa un bus de mensajes (Kafka, NATS) para coordinar apagados globales.
Con esta arquitectura, los apagados por pérdida de energía dejan de ser un punto ciego y pasan a formar parte del plan de recuperación operativa. La clave está en centralizar la detección, definir fases explícitas y ofrecer una API que permita pruebas automáticas y ajustes en caliente.