fix(ups): shutdown-ul orchestrat la pana de curent nu functiona deloc

upsmon face fork in doua procese, iar NOTIFYCMD ruleaza in cel neprivilegiat,
ca user `nut`: shell nologin, home /var/lib/nut, NICIO cheie SSH. Verificat pe
pvemini — pentru `nut`, `qm list`, `pct list` si `ssh root@pve1` esueaza toate.

Deci ups-shutdown-cluster.sh nu putea opri niciun guest si nu putea ajunge la
niciun nod. Singurul lucru care mergea erau emailurile, pentru care exista deja
`nut ALL=(root) NOPASSWD: /usr/bin/perl`. De asta /var/log/ups-shutdown.log nu
continea nicio oprire reusita din 2025-10-06 incoace: la fiecare pana de curent
se trimiteau emailuri si nu se oprea nimic, iar pve1/pveelite mergeau pana se
termina bateria.

Reparat:
- /etc/sudoers.d/nut-shutdown da userului nut dreptul sa ruleze scriptul ca root
- upssched-cmd il invoca prin sudo (liniile 355 si 413)
- scriptul refuza sa porneasca daca nu e root, cu mesaj explicit

Rescris scriptul:
- seteaza shutdown_policy=freeze INAINTE de orice. Fara asta, politica
  implicita `conditional` transforma fiecare poweroff de nod in failover catre
  un nod care se stinge si el imediat — exact incidentul 2026-01-11, cu VM 201
  migrat in timpul unei pene
- guest-urile HA se opresc cu `ha-manager set --state stopped`, nu cu
  pct/qm shutdown, care din CLI nu actualizeaza state-ul HA
- Oracle primeste `shutdown immediate` inainte de oprirea containerului
- consumatorii se opresc in paralel, Oracle ultimul
- detecteaza lipsa quorumului si cade pe oprire directa, in loc sa blocheze
- flock: ONBATT si LOWBATT pot declansa amandoua scriptul
- sare peste nodurile care nu raspund la ping in loc sa astepte timeout SSH
- timeout pe notificarile email: nu consumam baterie pe SMTP
- --dry-run si --force; credentialele UPS pot veni din
  /etc/nut/ups-shutdown.conf in loc sa fie in script

ups-shutdown-test.sh nu mai duplica logica: invoca scriptul real cu --dry-run.
Un test cu cod propriu testeaza altceva decat ce ruleaza in realitate.

Testat pe pvemini prin lantul complet:
  sudo -u nut sudo /usr/local/bin/ups-shutdown-cluster.sh --dry-run --force
Descopera corect toate cele 11 guest-uri, inclusiv cele de pe pve1 (deci SSH-ul
merge), si gaseste shutdown.stayoff pe UPS.

RAMANE DE VERIFICAT FIZIC: ups.load e 8%, putin pentru trei servere. Daca pve1
si pveelite nu sunt alimentate din UPS, mor instant la pana si toata
orchestrarea e decorativa. Nu se poate verifica din software.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRyaDj39hPQ89SZS6URRpS
This commit is contained in:
Marius
2026-08-27 14:13:42 +03:00
parent 730ce94fac
commit 393cc8c736
4 changed files with 496 additions and 244 deletions

80
proxmox/cluster/ups/scripts/ups-shutdown-test.sh Normal file → Executable file
View File

@@ -1,63 +1,33 @@
#!/bin/bash
#
# Script de TEST pentru shutdown orchestrat - NU oprește nimic
# ups-shutdown-test.sh — rulează procedura reală de shutdown UPS în gol.
#
# Nu duplică logica: invocă exact scriptul care rulează la o pană de curent,
# cu --dry-run. Asta e tot rostul lui — dacă testul ar avea cod propriu, ar
# testa altceva decât ce se execută în realitate.
#
# Utilizare:
# ups-shutdown-test.sh # ca root
# sudo -u nut sudo /usr/local/bin/ups-shutdown-test.sh # verifică și lanțul de permisiuni
#
set -uo pipefail
LOGFILE=/var/log/ups-shutdown-test.log
NODES=(10.0.20.200 10.0.20.202)
SCRIPT=/usr/local/bin/ups-shutdown-cluster.sh
log_message() {
echo "[2025-10-06 20:03:03] $1" | tee -a $LOGFILE
}
if [[ ! -x "$SCRIPT" ]]; then
echo "EROARE: $SCRIPT lipsește sau nu e executabil." >&2
exit 1
fi
log_message "========================================"
log_message "UPS SHUTDOWN TEST STARTED (DRY RUN)"
log_message "UPS Status: $(upsc nutdev1 ups.status 2>/dev/null || echo 'UNKNOWN')"
log_message "Battery Charge: $(upsc nutdev1 battery.charge 2>/dev/null || echo 'UNKNOWN')%"
log_message "Input Voltage: $(upsc nutdev1 input.voltage 2>/dev/null || echo 'UNKNOWN')V"
log_message "Output Voltage: $(upsc nutdev1 output.voltage 2>/dev/null || echo 'UNKNOWN')V"
log_message "========================================"
if [[ "$(id -u)" != "0" ]]; then
echo "EROARE: rulează ca $(id -un), nu ca root." >&2
echo " La o pană reală, upssched rulează ca 'nut' și invocă scriptul prin sudo." >&2
echo " Testează lanțul complet cu: sudo -u nut sudo $SCRIPT --dry-run --force" >&2
exit 1
fi
log_message "TEST: Ar opri VM-urile de pe toate nodurile..."
echo "=== Test în gol al procedurii de shutdown UPS ==="
echo "Nu se oprește nimic. --force ocolește verificarea că UPS-ul e pe baterie."
echo
for node in ${NODES[@]} localhost; do
if [ "$node" == "localhost" ]; then
NODE_NAME="pvemini (local)"
else
NODE_NAME=$node
fi
log_message " - VM-uri pe $NODE_NAME:"
if [ "$node" == "localhost" ]; then
for vmid in $(qm list | awk 'NR>1 {print $1}'); do
vm_name=$(qm config $vmid | grep '^name:' | cut -d' ' -f2)
vm_status=$(qm status $vmid | awk '{print $2}')
log_message " * VM $vmid ($vm_name): $vm_status"
done
else
ssh -o ConnectTimeout=5 root@$node "
for vmid in \$(qm list | awk 'NR>1 {print \$1}'); do
vm_name=\$(qm config \$vmid | grep '^name:' | cut -d' ' -f2)
vm_status=\$(qm status \$vmid | awk '{print \$2}')
echo ' * VM '\$vmid' ('\$vm_name'): '\$vm_status
done
" 2>&1 | tee -a $LOGFILE
fi
done
log_message ""
log_message "TEST: Ordinea de shutdown ar fi:"
log_message " 1. Toate VM-urile de pe toate nodurile (paralel)"
log_message " 2. Așteptare 90 secunde"
log_message " 3. Shutdown pve1 (10.0.20.200)"
log_message " 4. Shutdown pve2 (10.0.20.202)"
log_message " 5. Așteptare 30 secunde"
log_message " 6. Shutdown pvemini (10.0.20.201) - PRIMARY/LAST"
log_message ""
log_message "========================================"
log_message "UPS SHUTDOWN TEST COMPLETED (DRY RUN)"
log_message "NICIUN sistem nu a fost oprit - doar test"
log_message "========================================"
exit 0
exec "$SCRIPT" --dry-run --force "$@"