Ring1 aplicat si verificat pe cluster live: config_version 17, ring1_addr pe
10.10.10.20x la fiecare nod, interface{linknumber:1}. Validat cu `corosync -t`
inainte de instalare. Testat prin oprirea reala a lui ring0 pe pveelite: link0
disconnected, link1 connected, cvorum 3/3 neatins - exact scenariul care pe
27 august a lasat nodul mort 16 ore.
Test de repornire completa a clusterului, cu Wake-on-LAN (trezire in ~15s).
Insula a urcat singura pe toate trei nodurile; ipoteza enumerarii tarzii a
USB-ului nu s-a materializat. Unealta noua: wake-cluster.ps1.
Testul a scos la iveala o linie ramasa in /etc/fstab pe pve1 si pveelite, care
monta storage-ul NFS de pe IP-ul de productie inaintea lui pvestatd. Backup-ul
trecea tacut pe reteaua gresita, cu storage.cfg corect. cluster-startup verifica
acum asta automat, impreuna cu IP-urile de insula si conectivitatea reala.
Patru bug-uri gasite prin rulare pe cluster live:
- sonda Oracle nu avea timeout: un sqlplus agatat pe o instanta in pornire a
blocat cluster-startup 14 minute, fara mesaj, cu propriul prag de 600s
nefolosit, fiindca bucla n-a apucat o iteratie;
- `bash -c` in loc de `bash -lc`: sqlplus lipsea din PATH, deci baza nu se
oprea si containerul s-ar fi inchis peste ea;
- PowerShell 5.1 pierde ghilimelele duble catre exe-uri native, deci comanda
ajungea rupta pe nod - trecut pe trimitere codificata base64;
- backup-ul de crontab si fisierul de stare se rescriau la o a doua rulare,
lasand monitorizarea oprita permanent si lista de repornit goala.
Documentatia de oprire planificata pornea de la o afirmatie devenita falsa
("nu exista datacenter.cfg") si de la o comanda care ar fi adaugat o a doua
linie `ha:`. Actualizata, impreuna cu inventarul de guest-uri (VM 304 lipsea
din toate listele de ordine si cadea in maturarea de dupa Oracle).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RJFQZcA7uerXRaL1NrsXpS
410 lines
16 KiB
Bash
Executable File
410 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# cluster-shutdown.sh — oprire controlată a întregului cluster Proxmox `romfast`.
|
|
#
|
|
# Oprește guest-urile în ordinea corectă de dependențe, apoi nodurile, fără ca HA
|
|
# să relocheze resursele și fără ca watchdog-ul să reseteze hard vreun nod.
|
|
#
|
|
# Procedura completă și explicațiile: docs/oprire-planificata-cluster.md
|
|
#
|
|
# RULEAZĂ DE PE STAȚIA DE ADMIN, nu de pe un nod Proxmox. Motivul: nodul care se
|
|
# oprește ultimul nu poate raporta rezultatul propriei opriri.
|
|
#
|
|
# Utilizare:
|
|
# ./cluster-shutdown.sh # interactiv, cere confirmare
|
|
# ./cluster-shutdown.sh --yes # fără confirmare
|
|
# ./cluster-shutdown.sh --dry-run # arată ce ar face, nu execută nimic
|
|
# ./cluster-shutdown.sh --no-nodes # oprește doar guest-urile, lasă nodurile pornite
|
|
#
|
|
set -euo pipefail
|
|
|
|
# ---------------------------------------------------------------- configurare
|
|
|
|
declare -A NODE_IP=(
|
|
[pve1]=10.0.20.200
|
|
[pvemini]=10.0.20.201
|
|
[pveelite]=10.0.20.202
|
|
)
|
|
|
|
# Ordinea de OPRIRE a nodurilor. pvemini ultimul — e nodul principal și serverul NFS.
|
|
NODE_SHUTDOWN_ORDER=(pveelite pve1 pvemini)
|
|
|
|
# Ordinea de OPRIRE a guest-urilor: consumatorii întâi, baza de date ULTIMA.
|
|
# VM 201 (roacentral) și CT 104 (flowise) folosesc Oracle din CT 108.
|
|
# Orice guest pornit care nu apare aici e oprit la final, într-o măturare.
|
|
GUEST_SHUTDOWN_ORDER=(
|
|
303 # Win11-Adina — desktop, fără dependențe
|
|
304 # Win11-Marius — desktop, fără dependențe
|
|
302 # oracle-test-302 — VM de test
|
|
310 # Win11-Template — template
|
|
201 # roacentral — IIS reverse proxy, consumator Oracle
|
|
101 # minecraft
|
|
110 # moltbot
|
|
104 # flowise — consumator Oracle
|
|
106 # gitea
|
|
103 # dokploy
|
|
102 # docker.romfast.ro
|
|
100 # portainer
|
|
171 # claude-agent
|
|
109 # oracle-dr-windows
|
|
301 # docker-portainer-template
|
|
108 # central-oracle — ULTIMUL
|
|
)
|
|
|
|
ORACLE_CT=108
|
|
ORACLE_DOCKER=oracle-xe
|
|
|
|
GUEST_TIMEOUT=300 # secunde așteptate pentru shutdown-ul unui guest
|
|
NODE_TIMEOUT=600 # secunde așteptate până un nod nu mai răspunde la ping
|
|
CRON_BACKUP=/root/crontab.backup-mentenanta.txt
|
|
|
|
# Cron-uri de dezactivat cât timp clusterul e jos (altfel mail storm, iar
|
|
# vm109-watchdog.sh chiar pornește VM 109).
|
|
CRON_PATTERN='oom-alert|pvemini-down-alert|pveelite-down-alert|vm109-watchdog'
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
STATE_FILE="${CLUSTER_STATE_FILE:-$SCRIPT_DIR/.cluster-state.txt}"
|
|
|
|
ASSUME_YES=0
|
|
DRY_RUN=0
|
|
SKIP_NODES=0
|
|
|
|
# ------------------------------------------------------------------- utilitare
|
|
|
|
c_red=$'\033[31m'; c_grn=$'\033[32m'; c_yel=$'\033[33m'; c_dim=$'\033[2m'; c_off=$'\033[0m'
|
|
|
|
log() { printf '%s[%s]%s %s\n' "$c_dim" "$(date +%H:%M:%S)" "$c_off" "$*"; }
|
|
ok() { printf '%s ✓%s %s\n' "$c_grn" "$c_off" "$*"; }
|
|
warn() { printf '%s !%s %s\n' "$c_yel" "$c_off" "$*"; }
|
|
die() { printf '%s ✗ %s%s\n' "$c_red" "$*" "$c_off" >&2; exit 1; }
|
|
step() { printf '\n%s══ %s%s\n' "$c_grn" "$*" "$c_off"; }
|
|
|
|
# ping portabil: Git Bash pe Windows folosește -n/-w, Linux folosește -c/-W
|
|
ping_host() {
|
|
if [[ "${OSTYPE:-}" == msys* || "${OSTYPE:-}" == cygwin* || "${OS:-}" == Windows_NT ]]; then
|
|
ping -n 1 -w 2000 "$1" >/dev/null 2>&1
|
|
else
|
|
ping -c 1 -W 2 "$1" >/dev/null 2>&1
|
|
fi
|
|
}
|
|
|
|
nssh() {
|
|
local node=$1; shift
|
|
ssh -o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new \
|
|
"root@${NODE_IP[$node]}" "$@"
|
|
}
|
|
|
|
run() { # execută pe nod, sau doar afișează dacă --dry-run
|
|
local node=$1; shift
|
|
if (( DRY_RUN )); then
|
|
printf '%s [dry-run] %s: %s%s\n' "$c_dim" "$node" "$*" "$c_off"
|
|
return 0
|
|
fi
|
|
nssh "$node" "$@"
|
|
}
|
|
|
|
confirm() {
|
|
(( ASSUME_YES || DRY_RUN )) && return 0
|
|
local reply
|
|
printf '\n%s%s%s\n' "$c_yel" "$1" "$c_off"
|
|
read -r -p "Scrie DA ca să continui: " reply
|
|
[[ "$reply" == "DA" ]] || die "Anulat de utilizator."
|
|
}
|
|
|
|
# ------------------------------------------------------------- argumente
|
|
|
|
while (( $# )); do
|
|
case "$1" in
|
|
--yes|-y) ASSUME_YES=1 ;;
|
|
--dry-run) DRY_RUN=1 ;;
|
|
--no-nodes) SKIP_NODES=1 ;;
|
|
-h|--help) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
*) die "Argument necunoscut: $1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# --------------------------------------------------------- garda de rulare
|
|
|
|
# Scriptul NU trebuie rulat de pe un nod Proxmox (s-ar opri singur la mijloc) și
|
|
# nici din CT 171 claude-agent (e în lista de oprit — s-ar sinucide).
|
|
if command -v pvecm >/dev/null 2>&1; then
|
|
die "Rulezi pe un nod Proxmox. Scriptul trebuie rulat de pe stația de admin."
|
|
fi
|
|
if [[ -r /proc/1/environ ]] && grep -qa 'container=lxc' /proc/1/environ 2>/dev/null; then
|
|
if [[ "$(hostname)" == *claude-agent* ]]; then
|
|
die "Rulezi din CT 171 (claude-agent), care e în lista de oprit. Mută-te pe stația de admin."
|
|
fi
|
|
warn "Rulezi dintr-un container LXC. Asigură-te că nu e unul găzduit pe acest cluster."
|
|
fi
|
|
|
|
# ------------------------------------------------------ pas 1: preflight
|
|
|
|
step "Preflight"
|
|
|
|
for node in "${!NODE_IP[@]}"; do
|
|
ping_host "${NODE_IP[$node]}" || die "$node (${NODE_IP[$node]}) nu răspunde la ping."
|
|
nssh "$node" true 2>/dev/null || die "$node nu acceptă SSH cu cheie."
|
|
ok "$node (${NODE_IP[$node]}) — reachable"
|
|
done
|
|
|
|
MASTER=$(nssh pvemini "ha-manager status | awk '/^master/ {print \$2}'")
|
|
[[ -n "$MASTER" ]] || die "Nu pot citi HA master. Cluster instabil?"
|
|
nssh pvemini "pvecm status | grep -q 'Quorate: *Yes'" || die "Clusterul NU are quorum. Oprește-te și investighează."
|
|
ok "quorum OK, HA master: $MASTER"
|
|
|
|
RUNNING_TASKS=0
|
|
for node in "${!NODE_IP[@]}"; do
|
|
n=$(nssh "$node" "pvesh get /nodes/$node/tasks --limit 20 --output-format json 2>/dev/null | grep -o '\"status\":\"running\"' | wc -l" || echo 0)
|
|
RUNNING_TASKS=$(( RUNNING_TASKS + n ))
|
|
done
|
|
if (( RUNNING_TASKS > 0 )); then
|
|
warn "$RUNNING_TASKS task-uri în execuție pe cluster (backup? migrare?)."
|
|
confirm "Sunt task-uri active. Continui oricum?"
|
|
else
|
|
ok "niciun task în execuție"
|
|
fi
|
|
|
|
# ------------------------------------------- pas 2: inventar + salvare stare
|
|
|
|
step "Inventar guest-uri"
|
|
|
|
HA_SIDS=$(nssh pvemini "ha-manager config | grep -E '^(ct|vm):' | tr -d ' '" || true)
|
|
is_ha() { grep -qx "$1" <<<"$HA_SIDS"; }
|
|
|
|
# Format linie: <type> <node> <vmid> <status> <ha|noha>
|
|
INVENTORY=""
|
|
for node in "${!NODE_IP[@]}"; do
|
|
while read -r id status; do
|
|
[[ -n "$id" ]] || continue
|
|
local_ha=noha; is_ha "ct:$id" && local_ha=ha
|
|
INVENTORY+="ct $node $id $status $local_ha"$'\n'
|
|
done < <(nssh "$node" "pct list | awk 'NR>1 {print \$1, \$2}'")
|
|
|
|
while read -r id status; do
|
|
[[ -n "$id" ]] || continue
|
|
local_ha=noha; is_ha "vm:$id" && local_ha=ha
|
|
INVENTORY+="vm $node $id $status $local_ha"$'\n'
|
|
done < <(nssh "$node" "qm list | awk 'NR>1 {print \$1, \$3}'")
|
|
done
|
|
|
|
RUNNING=$(grep ' running ' <<<"$INVENTORY" || true)
|
|
RUNNING_COUNT=$(grep -c . <<<"${RUNNING:-}" || true)
|
|
|
|
if [[ -z "$RUNNING" ]]; then
|
|
warn "Niciun guest pornit. Trec direct la oprirea nodurilor."
|
|
else
|
|
printf '%s\n' "$RUNNING" | while read -r type node id status ha; do
|
|
printf ' %-3s %-9s %-4s %s\n' "$type" "$node" "$id" "$ha"
|
|
done
|
|
ok "$RUNNING_COUNT guest-uri pornite"
|
|
fi
|
|
|
|
# Salvează starea LOCAL — cluster-startup.sh pornește exact ce era pornit,
|
|
# ca să nu repornească VM-uri oprite intenționat (302, 310, 301...).
|
|
if (( ! DRY_RUN )); then
|
|
# Nu rescrie peste o stare existentă cu un inventar gol. Cazul real: scriptul
|
|
# e rulat a doua oară (după --no-nodes, sau după o eroare), când guest-urile
|
|
# sunt deja oprite — inventarul e gol și am pierde exact lista de repornit.
|
|
EXISTING=0
|
|
[[ -f "$STATE_FILE" ]] && EXISTING=$(grep -cv -e '^[[:space:]]*#' -e '^[[:space:]]*$' "$STATE_FILE" || true)
|
|
if [[ -z "$RUNNING" ]] && (( EXISTING > 0 )); then
|
|
warn "Niciun guest pornit, dar $STATE_FILE conține deja $EXISTING intrări — îl PĂSTREZ."
|
|
warn "Altfel cluster-startup.sh n-ar mai ști ce să pornească."
|
|
else
|
|
{ echo "# stare cluster salvată la $(date '+%Y-%m-%d %H:%M:%S')"
|
|
printf '%s\n' "${RUNNING:-}"; } > "$STATE_FILE"
|
|
ok "stare salvată în $STATE_FILE"
|
|
fi
|
|
fi
|
|
|
|
confirm "Se opresc $RUNNING_COUNT guest-uri, apoi nodurile: ${NODE_SHUTDOWN_ORDER[*]}."
|
|
|
|
# -------------------------------------------- pas 3: shutdown_policy=freeze
|
|
|
|
step "Shutdown policy → freeze"
|
|
|
|
CURRENT_POLICY=$(nssh pvemini "cat /etc/pve/datacenter.cfg 2>/dev/null | grep -oP 'shutdown_policy=\K\w+'" || true)
|
|
if [[ "$CURRENT_POLICY" == "freeze" ]]; then
|
|
ok "deja pe freeze"
|
|
else
|
|
if nssh pvemini "test -f /etc/pve/datacenter.cfg"; then
|
|
# sed șterge DOAR linia 'ha:', nu rescrie fișierul: din 2026-08-29 acolo
|
|
# stă și 'migration: network=10.10.10.0/24,type=insecure', fără de care
|
|
# replicarea se întoarce tăcut pe rețeaua de producție.
|
|
run pvemini "cp /etc/pve/datacenter.cfg /root/datacenter.cfg.backup-mentenanta"
|
|
run pvemini "sed -i '/^ha:/d' /etc/pve/datacenter.cfg; printf 'ha: shutdown_policy=freeze\n' >> /etc/pve/datacenter.cfg"
|
|
else
|
|
run pvemini "printf 'ha: shutdown_policy=freeze\n' > /etc/pve/datacenter.cfg"
|
|
fi
|
|
ok "setat freeze (era: ${CURRENT_POLICY:-nesetat})"
|
|
fi
|
|
|
|
# ---------------------------------------- pas 4: dezactivare cron-uri alertă
|
|
|
|
step "Dezactivare cron-uri de alertă"
|
|
|
|
for node in "${!NODE_IP[@]}"; do
|
|
# Backup DOAR dacă crontab-ul curent nu e deja comentat. Altfel o a doua rulare
|
|
# a scriptului (după o eroare, de exemplu) ar salva peste backup versiunea deja
|
|
# comentată, iar cluster-startup.sh ar „restaura" monitorizarea oprită — tăcut.
|
|
run "$node" "if crontab -l 2>/dev/null | grep -q '^#MENTENANTA'; then echo 'backup păstrat'; else crontab -l > $CRON_BACKUP 2>/dev/null || true; fi"
|
|
run "$node" "crontab -l | sed -E '/^[^#]/ s%^(.*($CRON_PATTERN)\.sh.*)\$%#MENTENANTA \1%' | crontab -"
|
|
if (( ! DRY_RUN )); then
|
|
n=$(nssh "$node" "crontab -l | grep -c '^#MENTENANTA' || true")
|
|
ok "$node — $n linii dezactivate, backup în $CRON_BACKUP"
|
|
fi
|
|
done
|
|
|
|
warn "Monitorizarea e acum OPRITĂ pe tot clusterul. cluster-startup.sh o restaurează."
|
|
|
|
# --------------------------------------------- pas 5: shutdown curat Oracle
|
|
|
|
step "Oracle — shutdown immediate"
|
|
|
|
ORACLE_NODE=$(awk -v id="$ORACLE_CT" '$1=="ct" && $3==id && $4=="running" {print $2}' <<<"$INVENTORY")
|
|
if [[ -z "$ORACLE_NODE" ]]; then
|
|
warn "CT $ORACLE_CT nu rulează — sar peste shutdown-ul bazei."
|
|
else
|
|
log "CT $ORACLE_CT pe $ORACLE_NODE — opresc instanța..."
|
|
if (( DRY_RUN )); then
|
|
printf '%s [dry-run] shutdown immediate în %s%s\n' "$c_dim" "$ORACLE_DOCKER" "$c_off"
|
|
else
|
|
# `bash -lc`, NU `bash -c`: fără shell de login, PATH-ul Oracle nu e încărcat
|
|
# și comanda eșuează cu „sqlplus: command not found" — adică baza rămâne
|
|
# DESCHISĂ, iar containerul ar fi oprit peste ea.
|
|
# `timeout 180`: un „shutdown immediate" poate dura, dar nu la infinit. Fără
|
|
# el, un sqlplus blocat ar ține scriptul agățat fără niciun mesaj (pățit pe
|
|
# 2026-08-29, la sonda echivalentă din cluster-startup.sh).
|
|
SQL_OUT=$(nssh "$ORACLE_NODE" "timeout 180 pct exec $ORACLE_CT -- docker exec $ORACLE_DOCKER bash -lc \"printf 'shutdown immediate\nexit\n' | sqlplus -s / as sysdba\" 2>&1" || true)
|
|
|
|
# Codul de ieșire nu e o dovadă: sqlplus întoarce 0 și când n-a închis nimic.
|
|
# Singura confirmare reală e mesajul instanței.
|
|
if grep -q 'ORACLE instance shut down' <<<"$SQL_OUT"; then
|
|
ok "instanța Oracle oprită curat"
|
|
else
|
|
warn "Baza NU a confirmat „ORACLE instance shut down\". Ultimele linii:"
|
|
tail -8 <<<"$SQL_OUT"
|
|
confirm "Oprirea CT $ORACLE_CT peste o bază posibil deschisă. Continui?"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ------------------------------------------------ pas 6: oprire guest-uri
|
|
|
|
step "Oprire guest-uri"
|
|
|
|
guest_status() {
|
|
local node=$1 type=$2 id=$3
|
|
if [[ "$type" == ct ]]; then
|
|
nssh "$node" "pct status $id 2>/dev/null | awk '{print \$2}'" || echo unknown
|
|
else
|
|
nssh "$node" "qm status $id 2>/dev/null | awk '{print \$2}'" || echo unknown
|
|
fi
|
|
}
|
|
|
|
stop_guest() {
|
|
local type=$1 node=$2 id=$3 ha=$4
|
|
local sid="$type:$id" cmd
|
|
[[ "$type" == ct ]] && cmd=pct || cmd=qm
|
|
|
|
if [[ "$ha" == ha ]]; then
|
|
# Pentru resursele HA se folosește ha-manager, NU pct/qm shutdown: din CLI
|
|
# acestea nu actualizează state-ul HA, iar CRM-ul ar reporni guest-ul.
|
|
run "$node" "ha-manager set $sid --state stopped"
|
|
else
|
|
run "$node" "$cmd shutdown $id --timeout $GUEST_TIMEOUT" || true
|
|
fi
|
|
|
|
(( DRY_RUN )) && return 0
|
|
|
|
local waited=0
|
|
while (( waited < GUEST_TIMEOUT )); do
|
|
[[ "$(guest_status "$node" "$type" "$id")" == "stopped" ]] && { ok "$sid oprit (${waited}s)"; return 0; }
|
|
sleep 5; waited=$(( waited + 5 ))
|
|
done
|
|
|
|
warn "$sid nu s-a oprit în ${GUEST_TIMEOUT}s — forțez stop"
|
|
nssh "$node" "$cmd stop $id" || true
|
|
sleep 5
|
|
[[ "$(guest_status "$node" "$type" "$id")" == "stopped" ]] \
|
|
|| die "$sid REFUZĂ să se oprească. Rezolvă manual înainte de a opri nodurile."
|
|
ok "$sid oprit forțat"
|
|
}
|
|
|
|
# întâi în ordinea definită...
|
|
for id in "${GUEST_SHUTDOWN_ORDER[@]}"; do
|
|
line=$(awk -v id="$id" '$3==id && $4=="running"' <<<"$INVENTORY") || true
|
|
[[ -n "$line" ]] || continue
|
|
read -r type node vmid status ha <<<"$line"
|
|
log "opresc $type:$vmid pe $node ($ha)"
|
|
stop_guest "$type" "$node" "$vmid" "$ha"
|
|
done
|
|
|
|
# ...apoi orice a mai rămas pornit și nu era în listă
|
|
while read -r type node id status ha; do
|
|
[[ -n "${id:-}" ]] || continue
|
|
printf '%s\n' "${GUEST_SHUTDOWN_ORDER[@]}" | grep -qx "$id" && continue
|
|
warn "guest neplanificat încă pornit: $type:$id pe $node — îl opresc"
|
|
stop_guest "$type" "$node" "$id" "$ha"
|
|
done <<<"${RUNNING:-}"
|
|
|
|
# ------------------------------------------- pas 7: verificare obligatorie
|
|
|
|
step "Verificare înainte de oprirea nodurilor"
|
|
|
|
if (( DRY_RUN )); then
|
|
warn "dry-run — sar peste verificare"
|
|
else
|
|
BAD=$(nssh pvemini "ha-manager status | grep -E 'started|migrate|relocate|fence'" || true)
|
|
[[ -z "$BAD" ]] || { printf '%s\n' "$BAD"; die "Există servicii HA încă active. NU opri nodurile."; }
|
|
ok "toate serviciile HA sunt în state stopped"
|
|
|
|
STILL=0
|
|
for node in "${!NODE_IP[@]}"; do
|
|
n=$(nssh "$node" "{ pct list; qm list; } | grep -c running || true")
|
|
(( STILL += n ))
|
|
done
|
|
(( STILL == 0 )) || die "$STILL guest-uri încă pornite. NU opri nodurile."
|
|
ok "0 guest-uri pornite pe cluster"
|
|
ok "watchdog-ul nu mai e armat — pierderea quorumului e inofensivă"
|
|
fi
|
|
|
|
if (( SKIP_NODES )); then
|
|
step "--no-nodes: nodurile rămân pornite. Gata."
|
|
exit 0
|
|
fi
|
|
|
|
confirm "Se opresc nodurile în ordinea: ${NODE_SHUTDOWN_ORDER[*]} (pvemini ultimul)."
|
|
|
|
# ------------------------------------------------- pas 8: oprire noduri
|
|
|
|
step "Oprire noduri"
|
|
|
|
for node in "${NODE_SHUTDOWN_ORDER[@]}"; do
|
|
log "opresc $node (${NODE_IP[$node]})..."
|
|
run "$node" "systemctl poweroff --no-block" || true
|
|
(( DRY_RUN )) && continue
|
|
|
|
waited=0
|
|
while (( waited < NODE_TIMEOUT )); do
|
|
ping_host "${NODE_IP[$node]}" || { ok "$node OPRIT (${waited}s)"; break; }
|
|
sleep 10; waited=$(( waited + 10 ))
|
|
done
|
|
if (( waited >= NODE_TIMEOUT )); then
|
|
warn "$node încă răspunde la ping după ${NODE_TIMEOUT}s."
|
|
warn "Verifică manual — sshd poate fi deja jos deși rețeaua e sus."
|
|
fi
|
|
done
|
|
|
|
step "Cluster oprit"
|
|
cat <<EOF
|
|
|
|
Stare salvată: $STATE_FILE
|
|
Backup crontab: $CRON_BACKUP (pe fiecare nod)
|
|
Shutdown policy: freeze
|
|
|
|
La revenirea curentului: ./cluster-startup.sh
|
|
|
|
EOF
|