Rulare detasata din dashboard/Discord: nssh local fara ssh la sine, CT 171 ramane pornit si moare odata cu pvemini (HA freeze il reporneste), email cu instructiunile de pornire, pauza si pentru testul DR si fereastra de patch. Garda CT 171 nu mai depinde de /proc/1/environ (era sarita fara root). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KddsXCqEbKMhdFJDYbAsx8
494 lines
20 KiB
Bash
Executable File
494 lines
20 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.
|
|
# Excepție: cu --allow-on-node poate rula chiar pe pvemini (lansat din
|
|
# dashboard/Discord, detașat prin systemd-run) — nssh către pvemini devine
|
|
# execuție locală, iar pvemini se oprește ultimul, singur, la pasul 8.
|
|
#
|
|
# 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
|
|
# ./cluster-shutdown.sh --allow-on-node # permite rularea chiar pe pvemini
|
|
#
|
|
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 — cu --allow-on-node rămâne pornit, vezi KEEP_RUNNING
|
|
109 # oracle-dr-windows
|
|
301 # docker-portainer-template
|
|
108 # central-oracle — ULTIMUL
|
|
)
|
|
|
|
# Cu --allow-on-node, acest guest NU e oprit explicit: ha-manager set --state
|
|
# stopped ar marca resursa ca oprită și HA n-ar mai reporni-o la boot. Moare
|
|
# oricum odată cu pvemini, iar shutdown_policy=freeze îl repornește după.
|
|
KEEP_RUNNING=171
|
|
|
|
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|weekly-dr-test-proxmox|vm109-patch-window'
|
|
|
|
# Destinatarul instrucțiunilor de pornire, trimise prin email după ce starea
|
|
# clusterului e salvată.
|
|
ALERT_RECIPIENT="${ALERT_RECIPIENT:-mmarius28@gmail.com}"
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
LOCAL_NODE=$(hostname)
|
|
|
|
ASSUME_YES=0
|
|
DRY_RUN=0
|
|
SKIP_NODES=0
|
|
ALLOW_ON_NODE=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
|
|
if (( ALLOW_ON_NODE )) && [[ "$node" == "$LOCAL_NODE" ]]; then
|
|
bash -c "$*"
|
|
return
|
|
fi
|
|
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 ;;
|
|
--allow-on-node) ALLOW_ON_NODE=1 ;;
|
|
-h|--help) sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
*) die "Argument necunoscut: $1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
STATE_FILE="${CLUSTER_STATE_FILE:-}"
|
|
if [[ -z "$STATE_FILE" ]]; then
|
|
if (( ALLOW_ON_NODE )); then
|
|
STATE_FILE=/opt/scripts/.cluster-state.txt
|
|
else
|
|
STATE_FILE="$SCRIPT_DIR/.cluster-state.txt"
|
|
fi
|
|
fi
|
|
|
|
# --------------------------------------------------------- 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).
|
|
# --allow-on-node ridică prima interdicție: rulat din dashboard/Discord, chiar
|
|
# pe pvemini, prin systemd-run.
|
|
if command -v pvecm >/dev/null 2>&1 && (( ! ALLOW_ON_NODE )); then
|
|
die "Rulezi pe un nod Proxmox. Scriptul trebuie rulat de pe stația de admin (sau cu --allow-on-node)."
|
|
fi
|
|
# Hostname-ul se verifica independent: /proc/1/environ nu e lizibil fara root,
|
|
# iar garda era sarita in tacere pentru userul claude.
|
|
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
|
|
if [[ -r /proc/1/environ ]] && grep -qa 'container=lxc' /proc/1/environ 2>/dev/null; then
|
|
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
|
|
|
|
# Instrucțiuni de pornire, trimise acum cât timp mai există cine să le citească —
|
|
# clusterul urmează să se oprească.
|
|
if (( DRY_RUN )); then
|
|
printf '%s [dry-run] email: instrucțiuni de pornire către %s%s\n' "$c_dim" "$ALERT_RECIPIENT" "$c_off"
|
|
else
|
|
MAIL_BODY=$(cat <<EOF
|
|
Clusterul romfast intră în oprire planificată.
|
|
|
|
Pentru pornire:
|
|
1. JuiceSSH (prin Tailscale) → conectare la 10.0.20.36, port 22122.
|
|
2. Rulează: C:\wolcluster.bat
|
|
3. Așteaptă mesajul „Cluster sus" în Discord.
|
|
|
|
Rezervă, dacă WoL nu pornește toate nodurile:
|
|
ssh root@10.0.20.201 /opt/scripts/cluster-startup.sh --allow-on-node --yes
|
|
EOF
|
|
)
|
|
printf '%s\n' "$MAIL_BODY" | mail -s "[INFO] Cluster romfast: oprire în curs — cum îl pornești" "$ALERT_RECIPIENT" \
|
|
&& ok "instrucțiuni de pornire trimise către $ALERT_RECIPIENT" \
|
|
|| warn "emailul cu instrucțiuni de pornire NU a putut fi trimis"
|
|
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
|
|
if (( ALLOW_ON_NODE )) && [[ "$id" == "$KEEP_RUNNING" ]]; then
|
|
warn "$id rămâne pornit; se oprește odată cu pvemini, HA freeze îl repornește"
|
|
continue
|
|
fi
|
|
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
|
|
if (( ALLOW_ON_NODE )); then
|
|
# ct:$KEEP_RUNNING rămâne intenționat „started" — nu e o scăpare.
|
|
BAD=$(nssh pvemini "ha-manager status | grep -E 'started|migrate|relocate|fence'" | grep -v "ct:$KEEP_RUNNING" || true)
|
|
else
|
|
BAD=$(nssh pvemini "ha-manager status | grep -E 'started|migrate|relocate|fence'" || true)
|
|
fi
|
|
[[ -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
|
|
if (( ALLOW_ON_NODE )); then
|
|
n=$(nssh "$node" "{ pct list | awk '\$2==\"running\" && \$1!=$KEEP_RUNNING'; qm list; } | grep -c running || true")
|
|
else
|
|
n=$(nssh "$node" "{ pct list; qm list; } | grep -c running || true")
|
|
fi
|
|
(( 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]})..."
|
|
|
|
if (( ALLOW_ON_NODE )) && [[ "$node" == "$LOCAL_NODE" ]] && (( ! DRY_RUN )); then
|
|
# Nodul local (pvemini) e ultimul din listă — nu se poate ping-a pe sine
|
|
# după poweroff, deci nu intră în bucla de așteptare de mai jos. Trimite
|
|
# raportul final și se oprește direct.
|
|
REPORT="Cluster romfast oprit. Noduri oprite: ${NODE_SHUTDOWN_ORDER[*]}."
|
|
if [[ -n "${INFRA_LOG:-}" && -r "$INFRA_LOG" ]]; then
|
|
REPORT+=$'\n\nUltimele linii din jurnal:\n'"$(tail -n 30 "$INFRA_LOG")"
|
|
fi
|
|
printf '%s\n' "$REPORT" | mail -s "[INFO] Cluster romfast: oprit" "$ALERT_RECIPIENT" \
|
|
|| warn "raportul final nu a putut fi trimis"
|
|
postqueue -f 2>/dev/null || true
|
|
sleep 15
|
|
systemctl poweroff --no-block
|
|
exit 0
|
|
fi
|
|
|
|
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
|