From 3ae491f8f2d70d527c6e1b9e4893255a747a692b Mon Sep 17 00:00:00 2001 From: Marius Date: Thu, 27 Aug 2026 13:45:11 +0300 Subject: [PATCH] feat(cluster): scripturi de oprire si repornire controlata a clusterului MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01RRyaDj39hPQ89SZS6URRpS --- .gitignore | 3 + .../docs/oprire-planificata-cluster.md | 65 ++- proxmox/cluster/scripts/cluster-shutdown.sh | 378 ++++++++++++++++++ proxmox/cluster/scripts/cluster-startup.sh | 360 +++++++++++++++++ 4 files changed, 789 insertions(+), 17 deletions(-) create mode 100755 proxmox/cluster/scripts/cluster-shutdown.sh create mode 100755 proxmox/cluster/scripts/cluster-startup.sh diff --git a/.gitignore b/.gitignore index 656aaaf..940bde1 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/proxmox/cluster/docs/oprire-planificata-cluster.md b/proxmox/cluster/docs/oprire-planificata-cluster.md index 274a7c5..14a9e49 100644 --- a/proxmox/cluster/docs/oprire-planificata-cluster.md +++ b/proxmox/cluster/docs/oprire-planificata-cluster.md @@ -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 --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 diff --git a/proxmox/cluster/scripts/cluster-shutdown.sh b/proxmox/cluster/scripts/cluster-shutdown.sh new file mode 100755 index 0000000..e62ec96 --- /dev/null +++ b/proxmox/cluster/scripts/cluster-shutdown.sh @@ -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: +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 <&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 <