feat(cluster): scripturi de oprire si repornire controlata a clusterului

cluster-shutdown.sh / cluster-startup.sh — automatizeaza procedura din
docs/oprire-planificata-cluster.md. Ruleaza de pe statia de admin prin SSH,
nu de pe un nod: nodul care se opreste ultimul nu poate raporta rezultatul,
iar la pornire scriptul trebuie sa astepte nodurile din afara.

Decizii importante:
- guest-urile din HA se opresc/pornesc cu `ha-manager set --state`, NU cu
  pct/qm shutdown; din CLI acestea nu actualizeaza state-ul HA si CRM-ul
  poate reporni guest-ul in mijlocul opririi
- apartenenta la HA se descopera dinamic din `ha-manager config`
- ordinea de oprire e pe dependente: consumatorii intai, CT 108 Oracle
  ultimul, cu `shutdown immediate` in baza inainte de a opri containerul
- shutdown-ul salveaza local ce rula; startup-ul porneste exact atat, ca sa
  nu reporneasca VM-uri oprite intentionat
- NEVER_AUTOSTART (109, 301, 310) protejeaza fallback-ul fara fisier de
  stare: VM 109 e DR-ul a carui pornire nedorita a declansat incidentul
  2026-04-20, restul sunt template-uri
- garda impotriva rularii de pe un nod sau din CT 171 (s-ar sinucide)
- ping_host() portabil Git Bash / Linux

Corectat in runbook: ordinea de oprire spunea "Oracle primul", gresit ca
ordine de dependente — VM 201 si CT 104 consuma baza.

Ambele testate cu --dry-run pe clusterul live.

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 13:45:11 +03:00
parent bd83bf809c
commit 3ae491f8f2
4 changed files with 789 additions and 17 deletions

3
.gitignore vendored
View File

@@ -22,3 +22,6 @@ cpanel-dns.config.json
# Handoff-uri: stare de lucru, nu documentatie de proiect - raman doar pe disc
docs/handoff_*.md
# Stare runtime salvata de cluster-shutdown.sh (nu e documentatie)
proxmox/cluster/scripts/.cluster-state.txt

View File

@@ -10,7 +10,31 @@ mutare rack, mentenanță UPS, intervenții la switch.
---
## TL;DR
## Varianta automată
Toată procedura de mai jos e implementată în două scripturi, de rulat **de pe
stația de admin** (nu de pe un nod):
```bash
cd proxmox/cluster/scripts
./cluster-shutdown.sh --dry-run # arată exact ce ar face, nu atinge nimic
./cluster-shutdown.sh # oprire completă, cu confirmări
# ... după revenirea curentului, cu nodurile pornite fizic ...
./cluster-startup.sh # repornire completă
```
`cluster-shutdown.sh` salvează local ce guest-uri rulau, iar `cluster-startup.sh`
pornește **exact** ce era pornit — nu repornește VM-uri oprite intenționat.
**Rulează întâi cu `--dry-run`.** Restul documentului explică de ce fiecare pas e
necesar și cum se face manual din GUI, dacă preferi.
---
## TL;DR (procedura manuală)
1. **Datacenter → Options → HA Settings → Shutdown Policy = `Freeze`** ← fără asta, HA mută resursele
2. Dezactivează cron-urile de alertă (`pvemini-down-alert`, `vm109-watchdog`, `pveelite-down-alert`, `oom-alert`)
@@ -149,23 +173,30 @@ singure la primul tick după revenire.
## Pas 2 — oprește guest-urile manual
Ordine importantă: **Oracle primul**, restul după.
Ordinea corectă e cea de **dependențe: consumatorii întâi, Oracle ULTIMUL**.
VM 201 (IIS) și CT 104 (flowise) folosesc baza din CT 108.
| Nod | Guest | În HA | Cum |
|-----|-------|-------|-----|
| pvemini | **CT 108** central-oracle | Da | `shutdown immediate` în DB, apoi Shutdown CT |
| pvemini | VM 201 roacentral | Da | Shutdown (guest agent) |
| pvemini | VM 302 oracle-test | Nu | Shutdown |
| pvemini | VM 303 Win11-Adina | Nu | Shutdown |
| pvemini | CT 100 portainer | Da | Shutdown |
| pvemini | CT 102 docker.romfast.ro | Nu | Shutdown |
| pvemini | CT 103 dokploy | Da | Shutdown |
| pvemini | CT 104 flowise | Da | Shutdown |
| pvemini | CT 106 gitea | Da | Shutdown |
| pvemini | CT 171 claude-agent | Da | Shutdown |
| pve1 | CT 101 minecraft | Da | Shutdown |
| pve1 | CT 110 moltbot | Da | Shutdown |
| pveelite | — | — | nimic pornit (CT 301 și VM 109 sunt `stopped`) |
| # | Nod | Guest | În HA | Cum |
|---|-----|-------|-------|-----|
| 1 | pvemini | VM 303 Win11-Adina | Nu | Shutdown |
| 2 | pvemini | VM 302 oracle-test | Nu | Shutdown |
| 3 | pvemini | VM 201 roacentral | Da | Shutdown (guest agent) |
| 4 | pve1 | CT 101 minecraft | Da | Shutdown |
| 5 | pve1 | CT 110 moltbot | Da | Shutdown |
| 6 | pvemini | CT 104 flowise | Da | Shutdown |
| 7 | pvemini | CT 106 gitea | Da | Shutdown |
| 8 | pvemini | CT 103 dokploy | Da | Shutdown |
| 9 | pvemini | CT 102 docker.romfast.ro | Nu | Shutdown |
| 10 | pvemini | CT 100 portainer | Da | Shutdown |
| 11 | pvemini | CT 171 claude-agent | Da | Shutdown |
| 12 | pvemini | **CT 108 central-oracle** | Da | `shutdown immediate` în DB, apoi Shutdown CT |
pveelite nu are nimic pornit în mod normal (CT 301 și VM 109 sunt `stopped`).
> **Din CLI, folosește `ha-manager set <sid> --state stopped` pentru guest-urile
> din HA**, nu `pct shutdown` / `qm shutdown`. Din linia de comandă acestea **nu**
> actualizează state-ul HA, iar CRM-ul poate reporni guest-ul în mijlocul opririi.
> Din GUI nu e o problemă — butonul Shutdown trece oricum prin HA.
### Oracle CT 108 — shutdown curat al bazei

View File

@@ -0,0 +1,378 @@
#!/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
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
{ 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
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
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
run "$node" "crontab -l > $CRON_BACKUP 2>/dev/null || true"
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
nssh "$ORACLE_NODE" "pct exec $ORACLE_CT -- docker exec $ORACLE_DOCKER bash -c \"printf 'shutdown immediate\nexit\n' | sqlplus -s / as sysdba\"" \
|| warn "Shutdown-ul bazei a raportat eroare. Verifică manual înainte de a continua!"
ok "instanța Oracle oprită"
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

View File

@@ -0,0 +1,360 @@
#!/usr/bin/env bash
#
# cluster-startup.sh — repornirea clusterului Proxmox `romfast` după o oprire
# planificată făcută cu cluster-shutdown.sh.
#
# Așteaptă nodurile, așteaptă quorumul, pornește guest-urile în ordinea corectă
# de dependențe (Oracle primul), restaurează cron-urile de monitorizare.
#
# Pornește DOAR guest-urile care rulau înainte de oprire, citite din fișierul de
# stare scris de cluster-shutdown.sh. Fără fișier de stare, folosește lista
# implicită de mai jos.
#
# RULEAZĂ DE PE STAȚIA DE ADMIN, nu de pe un nod.
#
# NODURILE TREBUIE PORNITE FIZIC ÎNTÂI (sau prin WoL). Scriptul le așteaptă.
# Pornește-le pe TOATE TREI aproximativ simultan: un singur nod pornit nu are
# quorum, /etc/pve rămâne read-only și nu se poate porni niciun guest.
#
# Utilizare:
# ./cluster-startup.sh # interactiv
# ./cluster-startup.sh --yes # fără confirmări
# ./cluster-startup.sh --dry-run # arată ce ar face
# ./cluster-startup.sh --all # ignoră fișierul de stare, pornește lista implicită
#
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 PORNIRE a guest-urilor: baza de date întâi, consumatorii după.
# Inversul ordinii de oprire din cluster-shutdown.sh.
GUEST_START_ORDER=(
108 # central-oracle — PRIMUL, restul depind de el
201 # roacentral — IIS reverse proxy, consumator Oracle
104 # flowise — consumator Oracle
100 # portainer
102 # docker.romfast.ro
103 # dokploy
106 # gitea
171 # claude-agent
101 # minecraft
110 # moltbot
303 # Win11-Adina
302 # oracle-test-302
109 # oracle-dr-windows — DR, în mod normal rămâne oprit
301 # docker-portainer-template
310 # Win11-Template
)
# Guest-uri care NU se pornesc automat când LIPSEȘTE fișierul de stare.
# Dacă fișierul de stare există, el decide — dacă erau pornite, se repornesc.
# 109 — VM DR. Pornirea lui nedorită a declanșat bucla OOM din incidentul
# 2026-04-20; în mod normal stă oprit și e pornit doar de testul DR.
# 301, 310 — template-uri, nu servicii.
NEVER_AUTOSTART=(109 301 310)
ORACLE_CT=108
ORACLE_DOCKER=oracle-xe
ORACLE_WAIT=600 # secunde așteptate până baza răspunde la interogări
NODE_WAIT=1800 # secunde așteptate până apar toate nodurile
QUORUM_WAIT=300
GUEST_WAIT=300
CRON_BACKUP=/root/crontab.backup-mentenanta.txt
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STATE_FILE="${CLUSTER_STATE_FILE:-$SCRIPT_DIR/.cluster-state.txt}"
ASSUME_YES=0
DRY_RUN=0
USE_ALL=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_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() {
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."
}
while (( $# )); do
case "$1" in
--yes|-y) ASSUME_YES=1 ;;
--dry-run) DRY_RUN=1 ;;
--all) USE_ALL=1 ;;
-h|--help) sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) die "Argument necunoscut: $1" ;;
esac
shift
done
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
# ------------------------------------------------- pas 1: așteaptă nodurile
step "Aștept nodurile"
warn "Nodurile trebuie pornite FIZIC. Pornește-le pe toate trei aproximativ simultan."
waited=0
while (( waited < NODE_WAIT )); do
missing=()
for node in "${!NODE_IP[@]}"; do
ping_host "${NODE_IP[$node]}" || missing+=("$node")
done
(( ${#missing[@]} == 0 )) && break
printf '\r%s aștept: %s (%ds) %s' "$c_dim" "${missing[*]}" "$waited" "$c_off"
sleep 10; waited=$(( waited + 10 ))
done
printf '\r%*s\r' 70 ''
(( waited < NODE_WAIT )) || die "Nodurile ${missing[*]} nu au apărut în ${NODE_WAIT}s."
for node in "${!NODE_IP[@]}"; do
w=0
until nssh "$node" true 2>/dev/null; do
(( w += 10 )); (( w > 300 )) && die "$node răspunde la ping dar nu la SSH."
sleep 10
done
ok "$node — up, SSH OK ($(nssh "$node" 'uptime -p'))"
done
# --------------------------------------------------- pas 2: așteaptă quorum
step "Aștept quorumul"
waited=0
until nssh pvemini "pvecm status 2>/dev/null | grep -q 'Quorate: *Yes'"; do
(( waited >= QUORUM_WAIT )) && die "Clusterul nu a atins quorumul în ${QUORUM_WAIT}s."
printf '\r%s aștept quorum (%ds)...%s' "$c_dim" "$waited" "$c_off"
sleep 10; waited=$(( waited + 10 ))
done
printf '\r%*s\r' 40 ''
VOTES=$(nssh pvemini "pvecm status | awk '/Total votes/ {print \$3}'")
[[ "$VOTES" == "3" ]] || warn "Quorum atins, dar doar $VOTES/3 voturi. Un nod lipsește din cluster."
ok "quorum OK, $VOTES/3 voturi"
nssh pvemini "ha-manager status | head -4"
# ------------------------------------------------- pas 3: ce trebuie pornit
step "Ce se pornește"
TO_START=""
if (( ! USE_ALL )) && [[ -f "$STATE_FILE" ]]; then
TO_START=$(grep -v '^#' "$STATE_FILE" | grep -v '^[[:space:]]*$' || true)
ok "citit din $STATE_FILE ($(grep -c . <<<"$TO_START") guest-uri)"
else
(( USE_ALL )) || warn "Nu există $STATE_FILE — folosesc lista implicită."
HA_SIDS=$(nssh pvemini "ha-manager config | grep -E '^(ct|vm):' | tr -d ' '" || true)
for node in "${!NODE_IP[@]}"; do
while read -r id _; do
[[ -n "$id" ]] || continue
h=noha; grep -qx "ct:$id" <<<"$HA_SIDS" && h=ha
TO_START+="ct $node $id stopped $h"$'\n'
done < <(nssh "$node" "pct list | awk 'NR>1 {print \$1, \$2}'")
while read -r id _; do
[[ -n "$id" ]] || continue
h=noha; grep -qx "vm:$id" <<<"$HA_SIDS" && h=ha
TO_START+="vm $node $id stopped $h"$'\n'
done < <(nssh "$node" "qm list | awk 'NR>1 {print \$1, \$3}'")
done
# Fără fișier de stare pornim doar ce e explicit în lista de ordine ȘI nu e
# în NEVER_AUTOSTART. Altfel am porni DR-ul și template-urile, care erau
# oprite intenționat.
FILTERED=""
for id in "${GUEST_START_ORDER[@]}"; do
if printf '%s\n' "${NEVER_AUTOSTART[@]}" | grep -qx "$id"; then
warn "$id sărit (NEVER_AUTOSTART) — pornește-l manual dacă chiar îl vrei"
continue
fi
line=$(awk -v id="$id" '$3==id' <<<"$TO_START") || true
[[ -n "$line" ]] && FILTERED+="$line"$'\n'
done
TO_START="$FILTERED"
fi
[[ -n "$TO_START" ]] || die "Nimic de pornit."
while read -r type node id status ha; do
[[ -n "${id:-}" ]] && printf ' %-3s %-9s %-4s %s\n' "$type" "$node" "$id" "$ha"
done <<<"$TO_START"
confirm "Se pornesc guest-urile de mai sus, în ordinea: ${GUEST_START_ORDER[*]}"
# ------------------------------------------------- pas 4: pornire guest-uri
step "Pornire 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
}
start_guest() {
local type=$1 node=$2 id=$3 ha=$4
local sid="$type:$id" cmd
[[ "$type" == ct ]] && cmd=pct || cmd=qm
if [[ "$(guest_status "$node" "$type" "$id")" == "running" ]]; then
ok "$sid deja pornit"; return 0
fi
if [[ "$ha" == ha ]]; then
# Pentru resursele HA, `--state started` repune resursa sub controlul CRM.
run "$node" "ha-manager set $sid --state started"
else
run "$node" "$cmd start $id"
fi
(( DRY_RUN )) && return 0
local waited=0
while (( waited < GUEST_WAIT )); do
[[ "$(guest_status "$node" "$type" "$id")" == "running" ]] && { ok "$sid pornit (${waited}s)"; return 0; }
sleep 5; waited=$(( waited + 5 ))
done
warn "$sid nu a pornit în ${GUEST_WAIT}s — verifică manual"
return 1
}
wait_oracle() {
local node=$1
log "aștept ca instanța Oracle să accepte interogări..."
(( DRY_RUN )) && return 0
local waited=0
while (( waited < ORACLE_WAIT )); do
if nssh "$node" "pct exec $ORACLE_CT -- docker exec $ORACLE_DOCKER bash -c \"printf 'set pagesize 0 feedback off\nselect global_name from global_name;\nexit\n' | sqlplus -s / as sysdba\" 2>/dev/null | grep -q '[A-Z]'"; then
ok "Oracle deschis și interogabil (${waited}s)"
return 0
fi
printf '\r%s Oracle încă nu răspunde (%ds)...%s' "$c_dim" "$waited" "$c_off"
sleep 15; waited=$(( waited + 15 ))
done
printf '\r%*s\r' 50 ''
warn "Oracle nu a răspuns în ${ORACLE_WAIT}s. Continui, dar VERIFICĂ MANUAL."
warn " ssh root@${NODE_IP[$node]} \"pct exec $ORACLE_CT -- docker logs --tail 50 $ORACLE_DOCKER\""
return 1
}
FAILED=()
for id in "${GUEST_START_ORDER[@]}"; do
line=$(awk -v id="$id" '$3==id' <<<"$TO_START") || true
[[ -n "$line" ]] || continue
read -r type node vmid status ha <<<"$line"
log "pornesc $type:$vmid pe $node ($ha)"
start_guest "$type" "$node" "$vmid" "$ha" || FAILED+=("$type:$vmid")
# Oracle e blocant: nimic care depinde de el nu pornește până nu e deschis.
if [[ "$vmid" == "$ORACLE_CT" ]]; then
wait_oracle "$node" || true
fi
done
# orice a mai rămas în lista de pornit și nu era în ordinea definită
while read -r type node id status ha; do
[[ -n "${id:-}" ]] || continue
printf '%s\n' "${GUEST_START_ORDER[@]}" | grep -qx "$id" && continue
warn "guest neplanificat: $type:$id pe $node — îl pornesc"
start_guest "$type" "$node" "$id" "$ha" || FAILED+=("$type:$id")
done <<<"$TO_START"
# ------------------------------------------------ pas 5: restaurare cron-uri
step "Restaurare cron-uri de monitorizare"
for node in "${!NODE_IP[@]}"; do
if ! nssh "$node" "test -f $CRON_BACKUP"; then
warn "$node — nu există $CRON_BACKUP, sar peste"
continue
fi
run "$node" "crontab $CRON_BACKUP"
if (( ! DRY_RUN )); then
left=$(nssh "$node" "crontab -l | grep -c '^#MENTENANTA' || true")
if [[ "$left" == "0" ]]; then
ok "$node — cron-uri restaurate"
else
warn "$node — au rămas $left linii #MENTENANTA, verifică manual"
fi
fi
done
# ------------------------------------------------------- pas 6: verificare
step "Verificare finală"
if (( DRY_RUN )); then
warn "dry-run — sar peste verificare"
else
nssh pvemini "ha-manager status"
echo
nssh pvemini "pvesr status"
echo
BADREP=$(nssh pvemini "pvesr status | awk 'NR>1 && \$NF!=\"OK\" {print}'" || true)
[[ -z "$BADREP" ]] && ok "replicare: toate job-urile OK" \
|| { warn "job-uri de replicare cu probleme:"; printf '%s\n' "$BADREP"; }
fi
step "Cluster pornit"
if (( ${#FAILED[@]} )); then
warn "Guest-uri care NU au pornit: ${FAILED[*]}"
warn "Verifică-le manual înainte de a considera repornirea terminată."
else
ok "toate guest-urile au pornit"
fi
cat <<EOF
De verificat manual:
- shutdown_policy e încă 'freeze' (recomandat să rămână așa)
- dacă lipsesc date recente, compară cu ultima replicare din pvesr status
EOF