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:
@@ -52,12 +52,56 @@ Restart servicii
|
|||||||
### Cluster Proxmox
|
### Cluster Proxmox
|
||||||
- **pvemini (10.0.20.201)** - Nod PRIMARY
|
- **pvemini (10.0.20.201)** - Nod PRIMARY
|
||||||
- Are UPS-ul conectat fizic
|
- Are UPS-ul conectat fizic
|
||||||
- Rulează NUT server și driver
|
- Rulează NUT server și driver (`MODE=standalone`)
|
||||||
|
- Orchestrează oprirea celorlalte noduri prin SSH
|
||||||
- Ultimul nod care se oprește
|
- Ultimul nod care se oprește
|
||||||
- **pve1 (10.0.20.200)** - Nod SECONDARY
|
- **pve1 (10.0.20.200)** - Nod SECONDARY
|
||||||
- Se oprește primul în caz de baterie critică
|
- **NU rulează NUT deloc** — e oprit prin SSH de pe pvemini
|
||||||
- **pve2 (10.0.20.202)** - Nod SECONDARY
|
- **pveelite (10.0.20.202)** - Nod SECONDARY
|
||||||
- Se oprește primul în caz de baterie critică
|
- **NU rulează NUT deloc** — e oprit prin SSH de pe pvemini
|
||||||
|
|
||||||
|
### Cum ajung secundarele să se oprească
|
||||||
|
|
||||||
|
NUT nu e instalat pe pve1 și pveelite. Nu sunt clienți `netclient` ai lui pvemini.
|
||||||
|
Singurul mecanism care le oprește e scriptul `ups-shutdown-cluster.sh`, care
|
||||||
|
rulează pe pvemini și face SSH în ele.
|
||||||
|
|
||||||
|
**Consecință:** dacă scriptul nu rulează sau eșuează, secundarele NU se opresc —
|
||||||
|
merg până se termină bateria și apoi sunt tăiate hard. NUT-ul de pe pvemini își
|
||||||
|
oprește curat doar propriul nod, prin `SHUTDOWNCMD`.
|
||||||
|
|
||||||
|
> ### ⚠️ De verificat fizic: sunt toate trei nodurile în UPS?
|
||||||
|
>
|
||||||
|
> `ups.load` era **8%** la ultima verificare, ceea ce e puțin pentru trei
|
||||||
|
> servere. Dacă pve1 și pveelite nu sunt alimentate din UPS, ele mor instantaneu
|
||||||
|
> la pană, SSH-ul din script eșuează, iar toată orchestrarea e decorativă.
|
||||||
|
> **Niciun script nu poate verifica asta — trebuie urmărite cablurile.**
|
||||||
|
|
||||||
|
### Lanțul de permisiuni (esențial)
|
||||||
|
|
||||||
|
`upsmon` face fork în două procese: unul root și unul neprivilegiat, ca user
|
||||||
|
`nut`. **`NOTIFYCMD` (deci `upssched`, deci scriptul de shutdown) rulează ca
|
||||||
|
`nut`**, care are shell `nologin`, home `/var/lib/nut` și **nicio cheie SSH**.
|
||||||
|
|
||||||
|
De aceea `upssched-cmd` invocă scriptul prin `sudo`, iar
|
||||||
|
`/etc/sudoers.d/nut-shutdown` îi dă userului `nut` dreptul de a-l rula ca root.
|
||||||
|
|
||||||
|
Fără asta, scriptul nu poate face nimic util: `pct`, `qm`, `ha-manager` și
|
||||||
|
`ssh` eșuează toate. **Exact asta s-a întâmplat între 2025-10-06 și 2026-08-27** —
|
||||||
|
scriptul trimitea emailuri (singurul lucru pentru care `nut` avea sudo) și atât.
|
||||||
|
`/var/log/ups-shutdown.log` nu conținea nicio oprire reușită.
|
||||||
|
|
||||||
|
### Testare
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# rulare în gol a procedurii reale, ca root
|
||||||
|
ssh root@10.0.20.201 /usr/local/bin/ups-shutdown-test.sh
|
||||||
|
|
||||||
|
# testarea lanțului complet de permisiuni, exact ca la o pană reală
|
||||||
|
ssh root@10.0.20.201 "sudo -u nut sudo /usr/local/bin/ups-shutdown-cluster.sh --dry-run --force"
|
||||||
|
```
|
||||||
|
|
||||||
|
A doua comandă e cea care contează: dacă ea merge, merge și la o pană adevărată.
|
||||||
|
|
||||||
### Monitorizare
|
### Monitorizare
|
||||||
- **VM 201 (Windows 11)** - Monitorizare vizuală via WinNUT
|
- **VM 201 (Windows 11)** - Monitorizare vizuală via WinNUT
|
||||||
|
|||||||
25
proxmox/cluster/ups/config/sudoers.d-nut-shutdown
Normal file
25
proxmox/cluster/ups/config/sudoers.d-nut-shutdown
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# /etc/sudoers.d/nut-shutdown
|
||||||
|
#
|
||||||
|
# upssched (NOTIFYCMD din upsmon.conf) rulează ca user `nut`, nu ca root:
|
||||||
|
# upsmon face fork în două procese, iar cel care execută NOTIFYCMD e cel
|
||||||
|
# neprivilegiat. Userul `nut` are shell nologin, home /var/lib/nut și NICIO
|
||||||
|
# cheie SSH.
|
||||||
|
#
|
||||||
|
# Fără regulile de aici, shutdown-ul orchestrat NU poate face nimic:
|
||||||
|
# - `pct` / `qm` / `ha-manager` -> permission denied
|
||||||
|
# - `ssh root@pve1 ...` -> nut nu are chei
|
||||||
|
# - `shutdown` / `systemctl poweroff` -> permission denied
|
||||||
|
# Singurul lucru care funcționa era trimiterea emailurilor, pentru care exista
|
||||||
|
# deja regula `nut ALL=(root) NOPASSWD: /usr/bin/perl` din /etc/sudoers.d/nut.
|
||||||
|
# De asta /var/log/ups-shutdown.log nu conținea nicio oprire reușită.
|
||||||
|
#
|
||||||
|
# Instalare:
|
||||||
|
# cp sudoers.d-nut-shutdown /etc/sudoers.d/nut-shutdown
|
||||||
|
# chmod 440 /etc/sudoers.d/nut-shutdown
|
||||||
|
# visudo -c # verificare sintaxă, obligatorie
|
||||||
|
#
|
||||||
|
# Un path fără argumente în sudoers permite orice argumente pentru acel
|
||||||
|
# executabil — deci `--dry-run` și `--force` funcționează cu aceeași regulă.
|
||||||
|
|
||||||
|
nut ALL=(root) NOPASSWD: /usr/local/bin/ups-shutdown-cluster.sh
|
||||||
|
nut ALL=(root) NOPASSWD: /usr/local/bin/ups-shutdown-test.sh
|
||||||
583
proxmox/cluster/ups/scripts/ups-shutdown-cluster.sh
Normal file → Executable file
583
proxmox/cluster/ups/scripts/ups-shutdown-cluster.sh
Normal file → Executable file
@@ -1,230 +1,443 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
#
|
||||||
# Script de shutdown orchestrat pentru cluster Proxmox cand UPS este pe baterie critica
|
# ups-shutdown-cluster.sh — oprire orchestrată a clusterului Proxmox `romfast`
|
||||||
# Trimite notificari email via PVE::Notify pentru fiecare pas
|
# când UPS-ul e pe baterie.
|
||||||
#
|
#
|
||||||
# Creat: 2025-10-06
|
# Rulează PE pvemini (nodul cu UPS-ul pe USB), declanșat de upssched:
|
||||||
# Actualizat: 2026-01-14 - Fix permisiuni log file, adaugat sudo pentru PVE::Notify
|
# ONBATT + 3 min → shutdown orchestrat
|
||||||
|
# LOWBATT → shutdown imediat
|
||||||
|
#
|
||||||
|
# TREBUIE SĂ RULEZE CA ROOT. upssched rulează ca user `nut`, deci upssched-cmd
|
||||||
|
# îl invocă prin `sudo` (vezi /etc/sudoers.d/nut-shutdown). Fără asta, scriptul
|
||||||
|
# nu poate face nici pct/qm/ha-manager, nici ssh (nut nu are chei), nici
|
||||||
|
# shutdown — exact motivul pentru care versiunea veche nu a funcționat
|
||||||
|
# niciodată, deși trimitea emailuri.
|
||||||
|
#
|
||||||
|
# Utilizare manuală:
|
||||||
|
# ups-shutdown-cluster.sh --dry-run # arată ce ar face, nu atinge nimic
|
||||||
|
# ups-shutdown-cluster.sh --force # rulează chiar dacă UPS-ul e pe rețea
|
||||||
|
#
|
||||||
|
# Rescris: 2026-08-27
|
||||||
|
#
|
||||||
|
|
||||||
|
# NU folosim `set -e`: într-o oprire de urgență, un ssh eșuat către un nod deja
|
||||||
|
# mort nu trebuie să oprească restul procedurii.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- configurare
|
||||||
|
|
||||||
LOGFILE=/var/log/ups-shutdown.log
|
LOGFILE=/var/log/ups-shutdown.log
|
||||||
NODES=("10.0.20.200" "10.0.20.202") # pve1, pveelite (pvemini va fi ultimul)
|
LOCKFILE=/run/ups-shutdown.lock
|
||||||
NODE_NAMES=("pve1" "pveelite") # Nume pentru notificari
|
|
||||||
|
SECONDARY_IPS=(10.0.20.200 10.0.20.202)
|
||||||
|
SECONDARY_NAMES=(pve1 pveelite)
|
||||||
|
|
||||||
|
ORACLE_CT=108
|
||||||
|
ORACLE_DOCKER=oracle-xe
|
||||||
|
|
||||||
UPS_NAME="nutdev1"
|
UPS_NAME="nutdev1"
|
||||||
UPS_USER="admin"
|
UPS_USER="admin"
|
||||||
UPS_PASS="parola99"
|
UPS_PASS="parola99" # suprascris de /etc/nut/ups-shutdown.conf, dacă există
|
||||||
|
|
||||||
|
# Bugete de timp — sunt secunde de baterie, nu le mări fără să știi autonomia.
|
||||||
|
GUEST_STOP_WAIT=150 # cât așteptăm guest-urile obișnuite
|
||||||
|
ORACLE_SQL_WAIT=60 # cât așteptăm `shutdown immediate`
|
||||||
|
ORACLE_CT_WAIT=60 # cât așteptăm oprirea CT 108 după aceea
|
||||||
|
NODE_DOWN_WAIT=90 # cât așteptăm nodurile secundare
|
||||||
|
SSH_TIMEOUT=5
|
||||||
|
NOTIFY_TIMEOUT=20 # emailurile nu au voie să consume bateria
|
||||||
|
|
||||||
TEMPLATE_DIR="/etc/pve/notification-templates/default"
|
TEMPLATE_DIR="/etc/pve/notification-templates/default"
|
||||||
HOSTNAME=$(hostname)
|
HOSTNAME=$(hostname)
|
||||||
FQDN=$(hostname -f 2>/dev/null || hostname)
|
FQDN=$(hostname -f 2>/dev/null || hostname)
|
||||||
|
|
||||||
# Asigura ca fisierul de log exista si are permisiunile corecte
|
# Credențiale în afara scriptului, dacă fișierul există.
|
||||||
# Scriptul poate rula ca user 'nut', deci fisierul trebuie sa fie writable
|
[[ -r /etc/nut/ups-shutdown.conf ]] && source /etc/nut/ups-shutdown.conf
|
||||||
ensure_logfile() {
|
|
||||||
if [ ! -f "$LOGFILE" ]; then
|
|
||||||
touch "$LOGFILE" 2>/dev/null || sudo touch "$LOGFILE"
|
|
||||||
fi
|
|
||||||
if [ ! -w "$LOGFILE" ]; then
|
|
||||||
sudo chown nut:nut "$LOGFILE" 2>/dev/null
|
|
||||||
sudo chmod 664 "$LOGFILE" 2>/dev/null
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_logfile
|
DRY_RUN=0
|
||||||
|
FORCE=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--dry-run) DRY_RUN=1 ;;
|
||||||
|
--force) FORCE=1 ;;
|
||||||
|
-h|--help) sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
log_message() {
|
# ------------------------------------------------------------------- utilitare
|
||||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a $LOGFILE 2>/dev/null
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOGFILE" 2>/dev/null
|
||||||
logger -t ups-shutdown "$1"
|
logger -t ups-shutdown "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Obtine status UPS
|
do_run() { # execută, sau doar loghează dacă --dry-run
|
||||||
get_ups_info() {
|
if (( DRY_RUN )); then
|
||||||
echo "Status: $(upsc $UPS_NAME ups.status 2>/dev/null || echo 'UNKNOWN')"
|
log " [dry-run] $*"
|
||||||
echo "Battery: $(upsc $UPS_NAME battery.charge 2>/dev/null || echo '?')%"
|
return 0
|
||||||
echo "Input: $(upsc $UPS_NAME input.voltage 2>/dev/null || echo '?')V"
|
fi
|
||||||
|
"$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Trimite notificare email via PVE::Notify
|
ssh_node() {
|
||||||
|
local ip=$1; shift
|
||||||
|
ssh -o BatchMode=yes -o ConnectTimeout=$SSH_TIMEOUT -o StrictHostKeyChecking=no \
|
||||||
|
-o LogLevel=ERROR "root@$ip" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
node_alive() { ping -c 1 -W 2 "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
get_ups_info() {
|
||||||
|
echo "Status: $(upsc $UPS_NAME ups.status 2>/dev/null || echo UNKNOWN)" \
|
||||||
|
"| Baterie: $(upsc $UPS_NAME battery.charge 2>/dev/null || echo '?')%" \
|
||||||
|
"| Input: $(upsc $UPS_NAME input.voltage 2>/dev/null || echo '?')V"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Notificare email via PVE::Notify. Mărginită în timp — nu consumăm baterie pe mail.
|
||||||
send_notification() {
|
send_notification() {
|
||||||
local EVENT_TYPE="$1"
|
local EVENT_TYPE="$1" EVENT_TITLE="$2" EVENT_DESC="$3" SEVERITY="$4"
|
||||||
local EVENT_TITLE="$2"
|
|
||||||
local EVENT_DESC="$3"
|
|
||||||
local SEVERITY="$4"
|
|
||||||
|
|
||||||
local UPS_STATUS=$(upsc $UPS_NAME ups.status 2>/dev/null || echo "UNKNOWN")
|
(( DRY_RUN )) && { log " [dry-run] email: $EVENT_TITLE"; return 0; }
|
||||||
local BATTERY_CHARGE=$(upsc $UPS_NAME battery.charge 2>/dev/null || echo "0")
|
|
||||||
local INPUT_VOLTAGE=$(upsc $UPS_NAME input.voltage 2>/dev/null || echo "0")
|
|
||||||
local EVENT_DATE=$(date '+%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
log_message "Sending notification: $EVENT_TITLE"
|
local UPS_STATUS BATTERY_CHARGE INPUT_VOLTAGE EVENT_DATE
|
||||||
|
UPS_STATUS=$(upsc $UPS_NAME ups.status 2>/dev/null || echo UNKNOWN)
|
||||||
|
BATTERY_CHARGE=$(upsc $UPS_NAME battery.charge 2>/dev/null || echo 0)
|
||||||
|
INPUT_VOLTAGE=$(upsc $UPS_NAME input.voltage 2>/dev/null || echo 0)
|
||||||
|
EVENT_DATE=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
/usr/bin/sudo /usr/bin/perl -I/usr/share/perl5 << EOFPERL 2>&1 | tee -a $LOGFILE
|
log "Trimit notificare: $EVENT_TITLE"
|
||||||
|
|
||||||
|
timeout $NOTIFY_TIMEOUT /usr/bin/perl -I/usr/share/perl5 <<EOFPERL >>"$LOGFILE" 2>&1
|
||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings;
|
||||||
use PVE::Notify;
|
use PVE::Notify;
|
||||||
|
|
||||||
my \$template_data = {
|
my \$template_data = {
|
||||||
'hostname' => '$FQDN',
|
'hostname' => '$FQDN',
|
||||||
'event_date' => '$EVENT_DATE',
|
'event_date' => '$EVENT_DATE',
|
||||||
'event_type' => '$EVENT_TYPE',
|
'event_type' => '$EVENT_TYPE',
|
||||||
'event_title' => '$EVENT_TITLE',
|
'event_title' => '$EVENT_TITLE',
|
||||||
'event_description' => '$EVENT_DESC',
|
'event_description' => '$EVENT_DESC',
|
||||||
'event_class' => 'shutdown',
|
'event_class' => 'shutdown',
|
||||||
'alert_type' => 'danger',
|
'alert_type' => 'danger',
|
||||||
'ups_status' => '$UPS_STATUS',
|
'ups_status' => '$UPS_STATUS',
|
||||||
'battery_charge' => '$BATTERY_CHARGE',
|
'battery_charge' => '$BATTERY_CHARGE',
|
||||||
'input_voltage' => '$INPUT_VOLTAGE',
|
'input_voltage' => '$INPUT_VOLTAGE',
|
||||||
'action_taken' => 'Shutdown in curs',
|
'action_taken' => 'Shutdown in curs',
|
||||||
'next_steps' => ''
|
'next_steps' => ''
|
||||||
};
|
};
|
||||||
|
my \$fields = { 'hostname' => '$HOSTNAME', 'type' => 'ups-power-event' };
|
||||||
|
|
||||||
my \$fields = {
|
eval { PVE::Notify::notify('$SEVERITY', 'ups-power-event', \$template_data, \$fields); print "Notification sent\\n"; };
|
||||||
'hostname' => '$HOSTNAME',
|
if (\$@) { print STDERR "Notification failed: \$@\\n"; }
|
||||||
'type' => 'ups-power-event'
|
|
||||||
};
|
|
||||||
|
|
||||||
eval {
|
|
||||||
PVE::Notify::notify('$SEVERITY', 'ups-power-event', \$template_data, \$fields);
|
|
||||||
print "Notification sent\\n";
|
|
||||||
};
|
|
||||||
if (\$@) {
|
|
||||||
print STDERR "Notification failed: \$@\\n";
|
|
||||||
}
|
|
||||||
EOFPERL
|
EOFPERL
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
log_message "========================================"
|
# ------------------------------------------------------------------- preflight
|
||||||
log_message "UPS SHUTDOWN ORCHESTRATION STARTED"
|
|
||||||
log_message "$(get_ups_info)"
|
|
||||||
log_message "========================================"
|
|
||||||
|
|
||||||
# Verifica daca UPS este intr-adevar pe baterie critica
|
# Log-ul trebuie să existe și să fie scriabil.
|
||||||
UPS_STATUS=$(upsc $UPS_NAME ups.status 2>/dev/null)
|
[[ -f "$LOGFILE" ]] || touch "$LOGFILE" 2>/dev/null
|
||||||
if [[ ! $UPS_STATUS =~ (OB|LB) ]]; then
|
chmod 664 "$LOGFILE" 2>/dev/null
|
||||||
log_message "WARNING: UPS status is $UPS_STATUS - not critical. Aborting shutdown."
|
|
||||||
|
if [[ "$(id -u)" != "0" ]]; then
|
||||||
|
log "FATAL: scriptul rulează ca $(id -un), nu ca root."
|
||||||
|
log " upssched rulează ca 'nut'; upssched-cmd trebuie să-l invoce prin sudo."
|
||||||
|
log " Vezi /etc/sudoers.d/nut-shutdown."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# O singură instanță: ONBATT și LOWBATT pot declanșa amândouă scriptul.
|
||||||
|
exec 9>"$LOCKFILE"
|
||||||
|
if ! flock -n 9; then
|
||||||
|
log "O instanță rulează deja (lock $LOCKFILE). Ies."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Email: START SHUTDOWN
|
log "========================================"
|
||||||
send_notification \
|
log "UPS SHUTDOWN ORCHESTRAT — START$( ((DRY_RUN)) && echo ' (DRY-RUN)' )"
|
||||||
"SHUTDOWN_START" \
|
log "$(get_ups_info)"
|
||||||
"Shutdown cluster PORNIT" \
|
log "========================================"
|
||||||
"UPS pe baterie critica. Se initiaza oprirea ordonata a cluster-ului Proxmox." \
|
|
||||||
"error"
|
|
||||||
|
|
||||||
log_message "Step 1: Oprire VM-uri pe toate nodurile..."
|
UPS_STATUS=$(upsc $UPS_NAME ups.status 2>/dev/null)
|
||||||
|
if [[ ! $UPS_STATUS =~ (OB|LB) ]]; then
|
||||||
# Opreste VM-uri pe toate nodurile (inclusiv local)
|
if (( FORCE || DRY_RUN )); then
|
||||||
for node in ${NODES[@]} localhost; do
|
log "UPS status '$UPS_STATUS' nu e critic, dar continui (--force/--dry-run)."
|
||||||
if [ "$node" == "localhost" ]; then
|
|
||||||
NODE_NAME="pvemini (local)"
|
|
||||||
else
|
else
|
||||||
NODE_NAME=$node
|
log "UPS status '$UPS_STATUS' nu e critic. Abandonez."
|
||||||
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_message " - Oprire VM-uri pe $NODE_NAME..."
|
|
||||||
|
|
||||||
if [ "$node" == "localhost" ]; then
|
|
||||||
for vmid in $(qm list | awk 'NR>1 {print $1}'); do
|
|
||||||
vm_status=$(qm status $vmid | awk '{print $2}')
|
|
||||||
if [ "$vm_status" == "running" ]; then
|
|
||||||
log_message " * Oprire VM $vmid pe pvemini..."
|
|
||||||
qm shutdown $vmid --timeout 60 &
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
else
|
|
||||||
ssh -o ConnectTimeout=5 root@$node "
|
|
||||||
for vmid in \$(qm list | awk 'NR>1 {print \$1}'); do
|
|
||||||
vm_status=\$(qm status \$vmid | awk '{print \$2}')
|
|
||||||
if [ \"\$vm_status\" == \"running\" ]; then
|
|
||||||
echo ' * Oprire VM '\$vmid' pe $node...'
|
|
||||||
qm shutdown \$vmid --timeout 60 &
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
" 2>&1 | tee -a $LOGFILE
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
log_message "Step 2: Oprire containere LXC pe toate nodurile..."
|
|
||||||
|
|
||||||
# Opreste containere LXC pe toate nodurile
|
|
||||||
for node in ${NODES[@]} localhost; do
|
|
||||||
if [ "$node" == "localhost" ]; then
|
|
||||||
NODE_NAME="pvemini (local)"
|
|
||||||
else
|
|
||||||
NODE_NAME=$node
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_message " - Oprire LXC pe $NODE_NAME..."
|
|
||||||
|
|
||||||
if [ "$node" == "localhost" ]; then
|
|
||||||
pct list 2>/dev/null | awk 'NR>1 && $2=="running" {print $1}' | while read ctid; do
|
|
||||||
log_message " * Oprire container $ctid pe pvemini..."
|
|
||||||
pct shutdown $ctid --timeout 60 &
|
|
||||||
done
|
|
||||||
else
|
|
||||||
ssh -o ConnectTimeout=5 root@$node "
|
|
||||||
pct list 2>/dev/null | awk 'NR>1 && \$2==\"running\" {print \$1}' | while read ctid; do
|
|
||||||
echo ' * Oprire container '\$ctid' pe $node...'
|
|
||||||
pct shutdown \$ctid --timeout 60 &
|
|
||||||
done
|
|
||||||
" 2>&1 | tee -a $LOGFILE
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
log_message "Step 3: Asteptare 90 secunde pentru oprirea VM-urilor si LXC..."
|
|
||||||
sleep 90
|
|
||||||
|
|
||||||
log_message "Step 4: Oprire noduri secundare..."
|
|
||||||
|
|
||||||
# Opreste nodurile secundare si trimite notificare pentru fiecare
|
|
||||||
for i in "${!NODES[@]}"; do
|
|
||||||
node="${NODES[$i]}"
|
|
||||||
node_name="${NODE_NAMES[$i]}"
|
|
||||||
|
|
||||||
log_message " - Shutdown nod $node_name ($node)..."
|
|
||||||
|
|
||||||
# Email: SHUTDOWN NOD SECUNDAR
|
|
||||||
send_notification \
|
|
||||||
"SHUTDOWN_NODE" \
|
|
||||||
"Shutdown $node_name trimis" \
|
|
||||||
"Comanda shutdown a fost trimisa catre nodul $node_name ($node)." \
|
|
||||||
"error"
|
|
||||||
|
|
||||||
ssh -o ConnectTimeout=5 root@$node "shutdown -h +1 'UPS battery critical - shutting down'" 2>&1 | tee -a $LOGFILE &
|
|
||||||
done
|
|
||||||
|
|
||||||
log_message "Step 5: Asteptare 60 secunde pentru shutdown noduri secundare..."
|
|
||||||
sleep 60
|
|
||||||
|
|
||||||
log_message "Step 6: Oprire nod local (pvemini - primary)..."
|
|
||||||
|
|
||||||
# Email: SHUTDOWN NOD PRIMARY (ultimul email inainte de shutdown)
|
|
||||||
send_notification \
|
|
||||||
"SHUTDOWN_PRIMARY" \
|
|
||||||
"Shutdown pvemini (ULTIMUL NOD)" \
|
|
||||||
"Se opreste nodul primary pvemini. Acesta este ultimul nod din cluster. UPS-ul se va opri dupa shutdown." \
|
|
||||||
"error"
|
|
||||||
|
|
||||||
log_message "Step 7: Oprire UPS dupa shutdown..."
|
|
||||||
|
|
||||||
# Comanda UPS sa se opreasca dupa un delay (permite shutdown-ul sa se finalizeze)
|
|
||||||
# Verifica daca comanda este disponibila
|
|
||||||
if upscmd -l $UPS_NAME 2>/dev/null | grep -q "shutdown.stayoff"; then
|
|
||||||
log_message " - Comanda UPS shutdown.stayoff (oprire completa)..."
|
|
||||||
upscmd -u $UPS_USER -p $UPS_PASS $UPS_NAME shutdown.stayoff 2>&1 | tee -a $LOGFILE
|
|
||||||
elif upscmd -l $UPS_NAME 2>/dev/null | grep -q "shutdown.return"; then
|
|
||||||
log_message " - Comanda UPS shutdown.return (oprire cu restart la revenire curent)..."
|
|
||||||
upscmd -u $UPS_USER -p $UPS_PASS $UPS_NAME shutdown.return 2>&1 | tee -a $LOGFILE
|
|
||||||
else
|
|
||||||
log_message " - WARNING: Nu s-a gasit comanda UPS shutdown disponibila"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_message "========================================"
|
# ---------------------------------------------- pas 1: quorum + freeze
|
||||||
log_message "UPS SHUTDOWN ORCHESTRATION COMPLETED"
|
|
||||||
log_message "$(get_ups_info)"
|
|
||||||
log_message "Local node will shutdown in 1 minute"
|
|
||||||
log_message "========================================"
|
|
||||||
|
|
||||||
# Opreste nodul local (ultimul)
|
HAVE_QUORUM=0
|
||||||
shutdown -h +1 "UPS battery critical - primary node shutting down"
|
if pvecm status 2>/dev/null | grep -q 'Quorate: *Yes'; then
|
||||||
|
HAVE_QUORUM=1
|
||||||
|
log "Pas 1: quorum OK."
|
||||||
|
else
|
||||||
|
log "Pas 1: ATENȚIE — clusterul NU are quorum."
|
||||||
|
log " Nodurile secundare sunt probabil deja moarte (nu sunt pe UPS?)."
|
||||||
|
log " /etc/pve e read-only: nu pot seta freeze și nu pot folosi ha-manager."
|
||||||
|
log " Trec pe oprire directă a guest-urilor, local."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# `freeze` împiedică HA să relocheze resursele când nodurile se opresc. Fără el,
|
||||||
|
# politica implicită `conditional` transformă fiecare poweroff într-un failover
|
||||||
|
# către un nod care oricum se stinge în secunda următoare — exact incidentul
|
||||||
|
# 2026-01-11 (VM 201 migrat în timpul unei pene de curent).
|
||||||
|
if (( HAVE_QUORUM )); then
|
||||||
|
CURRENT_POLICY=$(grep -oP 'shutdown_policy=\K\w+' /etc/pve/datacenter.cfg 2>/dev/null)
|
||||||
|
if [[ "$CURRENT_POLICY" == "freeze" ]]; then
|
||||||
|
log " shutdown_policy deja pe freeze."
|
||||||
|
else
|
||||||
|
log " setez shutdown_policy=freeze (era: ${CURRENT_POLICY:-nesetat})"
|
||||||
|
if (( ! DRY_RUN )); then
|
||||||
|
if [[ -f /etc/pve/datacenter.cfg ]]; then
|
||||||
|
sed -i '/^ha:/d' /etc/pve/datacenter.cfg
|
||||||
|
printf 'ha: shutdown_policy=freeze\n' >> /etc/pve/datacenter.cfg
|
||||||
|
else
|
||||||
|
printf 'ha: shutdown_policy=freeze\n' > /etc/pve/datacenter.cfg
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
send_notification "SHUTDOWN_START" "Shutdown cluster PORNIT" \
|
||||||
|
"UPS pe baterie. Se inițiază oprirea ordonată a clusterului Proxmox." "error"
|
||||||
|
|
||||||
|
# ------------------------------------------------- pas 2: inventar guest-uri
|
||||||
|
|
||||||
|
log "Pas 2: inventar guest-uri..."
|
||||||
|
|
||||||
|
declare -A HA_SID=()
|
||||||
|
if (( HAVE_QUORUM )); then
|
||||||
|
while read -r sid; do
|
||||||
|
[[ -n "$sid" ]] && HA_SID["$sid"]=1
|
||||||
|
done < <(ha-manager config 2>/dev/null | grep -oE '^(ct|vm):[0-9]+')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Linii: "<type> <ip> <nodename> <vmid>"
|
||||||
|
GUESTS=()
|
||||||
|
collect_from() {
|
||||||
|
local ip=$1 name=$2 line id st
|
||||||
|
if [[ "$ip" == "local" ]]; then
|
||||||
|
while read -r id st; do [[ "$st" == running ]] && GUESTS+=("ct local $name $id"); done \
|
||||||
|
< <(pct list 2>/dev/null | awk 'NR>1 {print $1, $2}')
|
||||||
|
while read -r id st; do [[ "$st" == running ]] && GUESTS+=("vm local $name $id"); done \
|
||||||
|
< <(qm list 2>/dev/null | awk 'NR>1 {print $1, $3}')
|
||||||
|
else
|
||||||
|
node_alive "$ip" || { log " $name ($ip) nu răspunde — îl sar (probabil deja oprit)."; return; }
|
||||||
|
while read -r id st; do [[ "$st" == running ]] && GUESTS+=("ct $ip $name $id"); done \
|
||||||
|
< <(ssh_node "$ip" "pct list 2>/dev/null | awk 'NR>1 {print \$1, \$2}'")
|
||||||
|
while read -r id st; do [[ "$st" == running ]] && GUESTS+=("vm $ip $name $id"); done \
|
||||||
|
< <(ssh_node "$ip" "qm list 2>/dev/null | awk 'NR>1 {print \$1, \$3}'")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_from local "$HOSTNAME"
|
||||||
|
for i in "${!SECONDARY_IPS[@]}"; do
|
||||||
|
collect_from "${SECONDARY_IPS[$i]}" "${SECONDARY_NAMES[$i]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
log " ${#GUESTS[@]} guest-uri pornite: $(printf '%s ' "${GUESTS[@]##* }")"
|
||||||
|
|
||||||
|
# ------------------------------------------------- funcții de oprire guest
|
||||||
|
|
||||||
|
guest_is_stopped() {
|
||||||
|
local type=$1 ip=$2 id=$3 cmd out
|
||||||
|
[[ "$type" == ct ]] && cmd=pct || cmd=qm
|
||||||
|
if [[ "$ip" == "local" ]]; then
|
||||||
|
out=$($cmd status "$id" 2>/dev/null)
|
||||||
|
else
|
||||||
|
out=$(ssh_node "$ip" "$cmd status $id 2>/dev/null")
|
||||||
|
fi
|
||||||
|
[[ "$out" == *stopped* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Trimite comanda de oprire, fără să aștepte.
|
||||||
|
issue_stop() {
|
||||||
|
local type=$1 ip=$2 id=$3 sid="$1:$3" cmd
|
||||||
|
[[ "$type" == ct ]] && cmd=pct || cmd=qm
|
||||||
|
|
||||||
|
# Pentru resursele HA folosim ha-manager: din CLI, pct/qm shutdown NU
|
||||||
|
# actualizează state-ul HA, iar CRM-ul poate reporni guest-ul în mijlocul
|
||||||
|
# opririi. Fără quorum ha-manager nu merge, deci cădem pe oprire directă.
|
||||||
|
if (( HAVE_QUORUM )) && [[ -n "${HA_SID[$sid]:-}" ]]; then
|
||||||
|
do_run ha-manager set "$sid" --state stopped >/dev/null 2>&1 &
|
||||||
|
elif [[ "$ip" == "local" ]]; then
|
||||||
|
do_run $cmd shutdown "$id" --timeout $GUEST_STOP_WAIT >/dev/null 2>&1 &
|
||||||
|
else
|
||||||
|
do_run ssh_node "$ip" "$cmd shutdown $id --timeout $GUEST_STOP_WAIT" >/dev/null 2>&1 &
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
force_stop() {
|
||||||
|
local type=$1 ip=$2 id=$3 cmd
|
||||||
|
[[ "$type" == ct ]] && cmd=pct || cmd=qm
|
||||||
|
log " FORȚEZ oprirea $type:$id"
|
||||||
|
if [[ "$ip" == "local" ]]; then
|
||||||
|
do_run $cmd stop "$id" >/dev/null 2>&1
|
||||||
|
else
|
||||||
|
do_run ssh_node "$ip" "$cmd stop $id" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Așteaptă ca o listă de guest-uri să se oprească; forțează ce rămâne.
|
||||||
|
wait_for_stopped() {
|
||||||
|
local -n _list=$1
|
||||||
|
local budget=$2 waited=0 remaining
|
||||||
|
|
||||||
|
(( DRY_RUN )) && { log " [dry-run] aș aștepta oprirea a ${#_list[@]} guest-uri"; return 0; }
|
||||||
|
|
||||||
|
while (( waited < budget )); do
|
||||||
|
remaining=()
|
||||||
|
for g in "${_list[@]}"; do
|
||||||
|
read -r type ip name id <<<"$g"
|
||||||
|
guest_is_stopped "$type" "$ip" "$id" || remaining+=("$g")
|
||||||
|
done
|
||||||
|
(( ${#remaining[@]} == 0 )) && { log " toate oprite (${waited}s)"; return 0; }
|
||||||
|
sleep 5; waited=$(( waited + 5 ))
|
||||||
|
done
|
||||||
|
|
||||||
|
log " ${#remaining[@]} guest-uri nu s-au oprit în ${budget}s"
|
||||||
|
for g in "${remaining[@]}"; do
|
||||||
|
read -r type ip name id <<<"$g"
|
||||||
|
force_stop "$type" "$ip" "$id"
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------- pas 3: oprire consumatori (tot, mai puțin Oracle)
|
||||||
|
|
||||||
|
log "Pas 3: opresc consumatorii (Oracle rămâne ultimul)..."
|
||||||
|
|
||||||
|
CONSUMERS=()
|
||||||
|
ORACLE_ENTRY=""
|
||||||
|
for g in "${GUESTS[@]}"; do
|
||||||
|
read -r type ip name id <<<"$g"
|
||||||
|
if [[ "$type" == ct && "$id" == "$ORACLE_CT" ]]; then
|
||||||
|
ORACLE_ENTRY="$g"
|
||||||
|
else
|
||||||
|
CONSUMERS+=("$g")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# În paralel: nu depind unul de altul, doar de Oracle. Bateria nu așteaptă.
|
||||||
|
for g in "${CONSUMERS[@]}"; do
|
||||||
|
read -r type ip name id <<<"$g"
|
||||||
|
log " opresc $type:$id pe $name"
|
||||||
|
issue_stop "$type" "$ip" "$id"
|
||||||
|
done
|
||||||
|
wait
|
||||||
|
|
||||||
|
if (( ${#CONSUMERS[@]} > 0 )); then
|
||||||
|
wait_for_stopped CONSUMERS $GUEST_STOP_WAIT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------- pas 4: Oracle, curat, la final
|
||||||
|
|
||||||
|
if [[ -z "$ORACLE_ENTRY" ]]; then
|
||||||
|
log "Pas 4: CT $ORACLE_CT nu rulează — sar peste."
|
||||||
|
else
|
||||||
|
read -r otype oip oname oid <<<"$ORACLE_ENTRY"
|
||||||
|
log "Pas 4: opresc instanța Oracle din CT $oid ($oname)..."
|
||||||
|
|
||||||
|
if (( DRY_RUN )); then
|
||||||
|
log " [dry-run] shutdown immediate în $ORACLE_DOCKER"
|
||||||
|
else
|
||||||
|
SQL="pct exec $oid -- docker exec $ORACLE_DOCKER bash -c \"printf 'shutdown immediate\nexit\n' | sqlplus -s / as sysdba\""
|
||||||
|
if [[ "$oip" == "local" ]]; then
|
||||||
|
timeout $ORACLE_SQL_WAIT bash -c "$SQL" >>"$LOGFILE" 2>&1
|
||||||
|
else
|
||||||
|
timeout $ORACLE_SQL_WAIT ssh_node "$oip" "$SQL" >>"$LOGFILE" 2>&1
|
||||||
|
fi
|
||||||
|
if (( $? == 0 )); then
|
||||||
|
log " instanța Oracle oprită curat"
|
||||||
|
else
|
||||||
|
log " ATENȚIE: shutdown immediate a eșuat sau a depășit ${ORACLE_SQL_WAIT}s"
|
||||||
|
log " Baza va porni cu instance recovery. Continui — bateria nu așteaptă."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
log " opresc containerul CT $oid"
|
||||||
|
issue_stop "$otype" "$oip" "$oid"
|
||||||
|
wait
|
||||||
|
ORACLE_LIST=("$ORACLE_ENTRY")
|
||||||
|
wait_for_stopped ORACLE_LIST $ORACLE_CT_WAIT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------------- pas 5: verificare
|
||||||
|
|
||||||
|
if (( HAVE_QUORUM && ! DRY_RUN )); then
|
||||||
|
ACTIVE=$(ha-manager status 2>/dev/null | grep -cE 'started|migrate|relocate|fence')
|
||||||
|
if (( ACTIVE > 0 )); then
|
||||||
|
log "Pas 5: ATENȚIE — $ACTIVE servicii HA încă active. Continui oricum (baterie)."
|
||||||
|
ha-manager status 2>/dev/null | grep -E 'started|migrate|relocate|fence' >>"$LOGFILE"
|
||||||
|
else
|
||||||
|
log "Pas 5: toate serviciile HA sunt oprite. Watchdog-ul nu mai e armat."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------- pas 6: oprire noduri secundare
|
||||||
|
|
||||||
|
log "Pas 6: opresc nodurile secundare..."
|
||||||
|
|
||||||
|
DOWN_TARGETS=()
|
||||||
|
for i in "${!SECONDARY_IPS[@]}"; do
|
||||||
|
ip="${SECONDARY_IPS[$i]}"; name="${SECONDARY_NAMES[$i]}"
|
||||||
|
if ! node_alive "$ip"; then
|
||||||
|
log " $name ($ip) nu răspunde — deja oprit."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
log " trimit poweroff către $name ($ip)"
|
||||||
|
send_notification "SHUTDOWN_NODE" "Shutdown $name trimis" \
|
||||||
|
"Comanda de oprire a fost trimisă către nodul $name ($ip)." "error"
|
||||||
|
do_run ssh_node "$ip" "systemctl poweroff --no-block" >>"$LOGFILE" 2>&1
|
||||||
|
DOWN_TARGETS+=("$ip:$name")
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( ${#DOWN_TARGETS[@]} > 0 )) && (( ! DRY_RUN )); then
|
||||||
|
log " aștept oprirea lor (max ${NODE_DOWN_WAIT}s)..."
|
||||||
|
waited=0
|
||||||
|
while (( waited < NODE_DOWN_WAIT )); do
|
||||||
|
still=()
|
||||||
|
for t in "${DOWN_TARGETS[@]}"; do
|
||||||
|
node_alive "${t%%:*}" && still+=("${t##*:}")
|
||||||
|
done
|
||||||
|
(( ${#still[@]} == 0 )) && { log " nodurile secundare sunt jos (${waited}s)"; break; }
|
||||||
|
sleep 10; waited=$(( waited + 10 ))
|
||||||
|
done
|
||||||
|
(( ${#still[@]} > 0 )) && log " ${still[*]} încă răspund după ${NODE_DOWN_WAIT}s — continui oricum."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------ pas 7: oprire UPS + local
|
||||||
|
|
||||||
|
send_notification "SHUTDOWN_PRIMARY" "Shutdown $HOSTNAME (ULTIMUL NOD)" \
|
||||||
|
"Se oprește nodul primary $HOSTNAME. UPS-ul se va opri după." "error"
|
||||||
|
|
||||||
|
log "Pas 7: comand oprirea UPS-ului..."
|
||||||
|
UPS_CMDS=$(upscmd -l $UPS_NAME 2>/dev/null)
|
||||||
|
if grep -q 'shutdown.stayoff' <<<"$UPS_CMDS"; then
|
||||||
|
log " shutdown.stayoff (oprire completă)"
|
||||||
|
do_run upscmd -u "$UPS_USER" -p "$UPS_PASS" $UPS_NAME shutdown.stayoff >>"$LOGFILE" 2>&1
|
||||||
|
elif grep -q 'shutdown.return' <<<"$UPS_CMDS"; then
|
||||||
|
log " shutdown.return (repornire la revenirea curentului)"
|
||||||
|
do_run upscmd -u "$UPS_USER" -p "$UPS_PASS" $UPS_NAME shutdown.return >>"$LOGFILE" 2>&1
|
||||||
|
else
|
||||||
|
log " ATENȚIE: nicio comandă de oprire disponibilă pe UPS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "========================================"
|
||||||
|
log "UPS SHUTDOWN ORCHESTRAT — TERMINAT"
|
||||||
|
log "$(get_ups_info)"
|
||||||
|
log "========================================"
|
||||||
|
|
||||||
|
if (( DRY_RUN )); then
|
||||||
|
log "DRY-RUN: nodul local NU se oprește."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Opresc nodul local ($HOSTNAME) — ultimul."
|
||||||
|
systemctl poweroff --no-block
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
80
proxmox/cluster/ups/scripts/ups-shutdown-test.sh
Normal file → Executable file
80
proxmox/cluster/ups/scripts/ups-shutdown-test.sh
Normal file → Executable file
@@ -1,63 +1,33 @@
|
|||||||
#!/bin/bash
|
#!/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
|
SCRIPT=/usr/local/bin/ups-shutdown-cluster.sh
|
||||||
NODES=(10.0.20.200 10.0.20.202)
|
|
||||||
|
|
||||||
log_message() {
|
if [[ ! -x "$SCRIPT" ]]; then
|
||||||
echo "[2025-10-06 20:03:03] $1" | tee -a $LOGFILE
|
echo "EROARE: $SCRIPT lipsește sau nu e executabil." >&2
|
||||||
}
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
log_message "========================================"
|
if [[ "$(id -u)" != "0" ]]; then
|
||||||
log_message "UPS SHUTDOWN TEST STARTED (DRY RUN)"
|
echo "EROARE: rulează ca $(id -un), nu ca root." >&2
|
||||||
log_message "UPS Status: $(upsc nutdev1 ups.status 2>/dev/null || echo 'UNKNOWN')"
|
echo " La o pană reală, upssched rulează ca 'nut' și invocă scriptul prin sudo." >&2
|
||||||
log_message "Battery Charge: $(upsc nutdev1 battery.charge 2>/dev/null || echo 'UNKNOWN')%"
|
echo " Testează lanțul complet cu: sudo -u nut sudo $SCRIPT --dry-run --force" >&2
|
||||||
log_message "Input Voltage: $(upsc nutdev1 input.voltage 2>/dev/null || echo 'UNKNOWN')V"
|
exit 1
|
||||||
log_message "Output Voltage: $(upsc nutdev1 output.voltage 2>/dev/null || echo 'UNKNOWN')V"
|
fi
|
||||||
log_message "========================================"
|
|
||||||
|
|
||||||
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
|
exec "$SCRIPT" --dry-run --force "$@"
|
||||||
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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user