From 8a0c5579816f3a43c3bd610f7e1fedd6091fefce Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Mon, 20 Apr 2026 12:40:49 +0000 Subject: [PATCH] feat(failover): add VM 201 manual failover + recovery scripts, watchdog alert VM 201 (Windows critical) stays out of HA by design. Added: - failover-vm201.sh: interactive failover pvemini -> pveelite with ZFS replication state - recover-vm201-to-pvemini.sh: interactive reverse migration with uptime + split-brain checks - pvemini-down-alert.sh: cron watchdog on pveelite, emails full runbook after 2min DOWN Replication RPO tightened: CT 108 + VM 201 to 5min, CT 171 to 15min. CT 171 added to HA (ha-group-main) for continuous Claude Code access. Co-Authored-By: Claude Opus 4.7 (1M context) --- proxmox/cluster/failover/README.md | 105 +++++++++++++ proxmox/cluster/failover/failover-vm201.sh | 79 ++++++++++ .../cluster/failover/pvemini-down-alert.sh | 83 +++++++++++ .../failover/recover-vm201-to-pvemini.sh | 139 ++++++++++++++++++ 4 files changed, 406 insertions(+) create mode 100644 proxmox/cluster/failover/README.md create mode 100755 proxmox/cluster/failover/failover-vm201.sh create mode 100755 proxmox/cluster/failover/pvemini-down-alert.sh create mode 100755 proxmox/cluster/failover/recover-vm201-to-pvemini.sh diff --git a/proxmox/cluster/failover/README.md b/proxmox/cluster/failover/README.md new file mode 100644 index 0000000..800e333 --- /dev/null +++ b/proxmox/cluster/failover/README.md @@ -0,0 +1,105 @@ +# Failover & High Availability + +Strategia HA pe cluster, separată pe nivele de criticitate. + +## Rezumat resurse critice + +| Resursă | Criticitate | HA automat | Replicare | RPO | +|---------|-------------|------------|-----------|-----| +| CT 108 Oracle central | Nivel 0 | Da | pveelite `*/5` min, pve1 `*/5` min | 5 min | +| VM 201 Windows roacentral | Nivel 0 | **NU** (failover manual) | pveelite `*/5` min, pve1 `*/5` min | 5 min | +| CT 171 claude-agent | Nivel 1 | Da | pveelite `*/15` min, pve1 `*/15` min | 15 min | +| CT 104 flowise | Nivel 2 | Da | zilnic 21:15 | 24h | +| CT 106 gitea | Nivel 2 | Da | `*/2` ore | 2h | +| CT 100 portainer | Nivel 2 | Da | zilnic | 24h | + +## VM 201 — failover manual + +VM 201 (Windows production) **nu este în HA deliberat**. Incident anterior: când pvemini a fost scos temporar din HA pentru mentenanță, VM 201 a migrat automat — comportament nedorit pentru Windows production care cere fereastră de mentenanță controlată. + +### Declanșator alertă + +Cron pe pveelite verifică pvemini la fiecare minut: +- Script: `/opt/scripts/pvemini-down-alert.sh` +- După 2 minute DOWN consecutive → mail `mmarius28@gmail.com` cu comanda failover ready-to-run + +### Execuție failover + +Din orice sesiune SSH: + +```bash +ssh root@10.0.20.202 /opt/scripts/failover-vm201.sh +``` + +Scriptul: +1. Verifică pvemini efectiv DOWN (ping + ssh); abort dacă răspunde +2. Verifică VM 201 nu rulează deja pe pveelite +3. Afișează ultima replicare ZFS disponibilă local +4. Cere confirmare interactivă (tastezi `DA`) +5. `qm start 201` pe pveelite +6. Trimite mail confirmare cu pași post-failover + +Pentru automatizare (fără prompt): +```bash +ssh root@10.0.20.202 /opt/scripts/failover-vm201.sh --yes +``` + +### După revenire pvemini — script interactiv + +**NU porni automat VM 201 pe pvemini.** Date posibil divergente. + +```bash +ssh root@10.0.20.202 /opt/scripts/recover-vm201-to-pvemini.sh +``` + +Scriptul verifică: +- pvemini uptime min. 30 min + erori recente +- pvemini reachable ssh + VM 201 nu rulează deja acolo (anti split-brain) +- VM 201 running local pe pveelite + +Pași executați automat (cu confirmare): +1. `qm shutdown 201` pe pveelite +2. `pvesr create-local-job 201-2 pvemini --source pveelite` + run forțat +3. `qm migrate 201 pvemini` offline +4. Cleanup job replicare inversă +5. `qm start 201` pe pvemini + +Non-interactiv: adaugă `--yes`. + +## CT 108 Oracle — HA automat + +- Group `ha-group-main` (pvemini:100, pveelite:50, pve1:33) +- `max_restart 3, max_relocate 2` +- Replicare 5 min → failover automat cu RPO 5 min +- **Constrângere RAM pveelite 16 GB**: dacă pvemini cade, pveelite trebuie să aibă spațiu pentru CT 108 (8 GB) + CT 101 existent + eventual alte CT-uri HA + +## CT 171 claude-agent — HA automat + +- Adăugat în HA pentru acces Claude Code continuu +- Dacă pvemini cade, CT 171 pornește pe pveelite cu ultima replicare (max 15 min în urmă) +- State = workspace files din `/workspace/` — replicate ZFS + +## Fișiere relevante + +| Fișier | Locație | Scop | +|--------|---------|------| +| `failover-vm201.sh` | `/opt/scripts/` pe pveelite | Failover manual VM 201 pvemini → pveelite | +| `recover-vm201-to-pvemini.sh` | `/opt/scripts/` pe pveelite | Recovery interactiv VM 201 pveelite → pvemini | +| `pvemini-down-alert.sh` | `/opt/scripts/` pe pveelite | Watchdog 1 min, mail dacă pvemini DOWN 2 min | +| `oom-alert.sh` | `/opt/scripts/` pe toate nodurile | Alertă OOM kills | + +## Comenzi utile + +```bash +# Stare replicare +ssh root@10.0.20.201 "pvesr status" + +# Stare HA +ssh root@10.0.20.201 "ha-manager status" + +# Forțează replicare acum (util înainte de mentenanță) +ssh root@10.0.20.201 "pvesr run --id 108-1; pvesr run --id 201-1; pvesr run --id 171-1" + +# Test script failover (dry — fără confirmare) +ssh root@10.0.20.202 "/opt/scripts/failover-vm201.sh" # va abort dacă pvemini răspunde +``` diff --git a/proxmox/cluster/failover/failover-vm201.sh b/proxmox/cluster/failover/failover-vm201.sh new file mode 100755 index 0000000..e432746 --- /dev/null +++ b/proxmox/cluster/failover/failover-vm201.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# Manual failover VM 201 (roacentral Windows) către pveelite +# Deployed la /opt/scripts/failover-vm201.sh pe pveelite +# Folosește ultima replicare ZFS locală +set -euo pipefail + +VMID=201 +PRIMARY=pvemini +PRIMARY_IP=10.0.20.201 +SECONDARY=$(hostname) +LOG=/var/log/failover-vm201.log +MAIL_TO=mmarius28@gmail.com + +log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG"; } +die() { log "ABORT: $*"; exit 1; } + +[[ "$SECONDARY" == "pveelite" ]] || die "Rulează pe pveelite, nu pe $SECONDARY" +[[ $EUID -eq 0 ]] || die "Trebuie root" + +FORCE=${1:-} + +log "=== Failover VM $VMID pornit ===" + +# Check 1: pvemini efectiv down +log "Verific $PRIMARY..." +if ping -c 2 -W 2 "$PRIMARY_IP" &>/dev/null; then + if ssh -o ConnectTimeout=5 -o BatchMode=yes "root@$PRIMARY_IP" 'pvecm status' &>/dev/null; then + die "$PRIMARY răspunde la ping + ssh. NU fac failover. Oprește manual VM 201 pe $PRIMARY înainte." + fi + log "$PRIMARY ping OK dar ssh fail — posibil degradat" +fi +log "$PRIMARY confirmat DOWN" + +# Check 2: VM nu rulează deja local +if qm status "$VMID" 2>/dev/null | grep -q running; then + die "VM $VMID deja running pe $SECONDARY" +fi + +# Check 3: discul există local (ultima replicare) +if ! zfs list "rpool/data/vm-$VMID-disk-3" &>/dev/null; then + die "Disc vm-$VMID-disk-3 lipsește local. Replicare nefuncțională?" +fi +LAST_SNAP=$(zfs list -t snapshot -o name,creation -s creation "rpool/data/vm-$VMID-disk-3" 2>/dev/null | tail -1) +log "Ultima replicare: $LAST_SNAP" + +# Confirmare interactivă +if [[ "$FORCE" != "--yes" ]]; then + echo + echo "========================================" + echo "ATENȚIE: VM $VMID va porni pe $SECONDARY" + echo "folosind ultima replicare ZFS." + echo "Date pierdute = ce s-a scris după $LAST_SNAP" + echo "========================================" + read -p "Continui? (tastează 'DA' pentru confirmare): " CONFIRM + [[ "$CONFIRM" == "DA" ]] || die "Anulat de utilizator" +fi + +# Start +log "Pornesc VM $VMID pe $SECONDARY..." +qm start "$VMID" +sleep 5 +STATUS=$(qm status "$VMID" | awk '{print $2}') +log "Status: $STATUS" + +# Mail +{ + echo "Failover VM $VMID executat pe $SECONDARY la $(date)" + echo + echo "Primary $PRIMARY era DOWN." + echo "Ultima replicare folosită: $LAST_SNAP" + echo "Status VM: $STATUS" + echo + echo "Pași următori:" + echo "1. Verifică aplicația Windows pe 10.0.20.201 (IIS, etc)" + echo "2. Când $PRIMARY revine, NU porni VM $VMID pe $PRIMARY" + echo "3. După stabilizare, migrare înapoi: qm migrate $VMID $PRIMARY --online 0" +} | mail -r 'ups@romfast.ro' -s "[CRITIC] Failover VM $VMID pornit pe $SECONDARY" "$MAIL_TO" + +log "=== Failover complet ===" diff --git a/proxmox/cluster/failover/pvemini-down-alert.sh b/proxmox/cluster/failover/pvemini-down-alert.sh new file mode 100755 index 0000000..34f49b3 --- /dev/null +++ b/proxmox/cluster/failover/pvemini-down-alert.sh @@ -0,0 +1,83 @@ +#!/bin/bash +# Alertă când pvemini e unreachable — rulează pe pveelite din cron 1 min +# Deployed la /opt/scripts/pvemini-down-alert.sh pe pveelite +# Stateful: alertează o dată după 2 minute consecutive DOWN, reset la UP +set -euo pipefail + +PRIMARY_IP=10.0.20.201 +STATE=/var/run/pvemini-down-alert.state +MAIL_TO=mmarius28@gmail.com +THRESHOLD=2 # min consecutive + +COUNT=0 +[[ -f "$STATE" ]] && COUNT=$(cat "$STATE") + +if ping -c 2 -W 2 "$PRIMARY_IP" &>/dev/null; then + if [[ "$COUNT" -ge "$THRESHOLD" ]]; then + # Recovery + echo "pvemini revenit online la $(date)" | \ + mail -r 'ups@romfast.ro' -s "[OK] pvemini UP" "$MAIL_TO" + fi + echo 0 > "$STATE" + exit 0 +fi + +COUNT=$((COUNT + 1)) +echo "$COUNT" > "$STATE" + +# Alertează o singură dată când atingem threshold +if [[ "$COUNT" -eq "$THRESHOLD" ]]; then + { + echo "pvemini (10.0.20.201) NU răspunde la ping de $THRESHOLD minute" + echo "Timp: $(date)" + echo + echo "Stare cluster:" + pvecm status 2>&1 | head -20 || true + echo + echo "HA status:" + ha-manager status 2>&1 || true + echo + echo "==========================================" + echo "CT-uri HA (104, 106, 108, 171): migrare AUTOMATĂ" + echo "pe pveelite în ~2 min dacă pveelite are RAM." + echo "==========================================" + echo + echo "### FAILOVER MANUAL VM 201 (Windows) ###" + echo + echo "Dacă pvemini rămâne DOWN, pentru a porni VM 201 pe pveelite:" + echo + echo " ssh root@10.0.20.202 /opt/scripts/failover-vm201.sh" + echo + echo "(tastezi 'DA' la prompt; scriptul folosește ultima replicare" + echo " ZFS — max 5 min în urmă. Verifică ping + ssh înainte)" + echo + echo "Variantă non-interactivă:" + echo " ssh root@10.0.20.202 /opt/scripts/failover-vm201.sh --yes" + echo + echo + echo "### REVENIRE PE PVEMINI (după ce pvemini e stabil) ###" + echo + echo "IMPORTANT: NU porni VM 201 pe pvemini automat — date divergente." + echo "Folosește scriptul interactiv care face toți pașii în siguranță:" + echo + echo " ssh root@10.0.20.202 /opt/scripts/recover-vm201-to-pvemini.sh" + echo + echo "Scriptul verifică:" + echo " - pvemini uptime min. 30 min + erori recente" + echo " - pvemini reachable ssh + VM 201 NU rulează deja acolo" + echo " - VM 201 running local pe pveelite" + echo + echo "Execuția face automat (cu confirmări la fiecare pas):" + echo " 1. qm shutdown 201 pe pveelite" + echo " 2. pvesr replicare inversă pveelite → pvemini + run forțat" + echo " 3. qm migrate 201 pvemini (offline)" + echo " 4. cleanup job replicare inversă" + echo " 5. qm start 201 pe pvemini" + echo + echo "Variantă non-interactivă: adaugă '--yes' la final." + echo + echo "CT-urile HA (104, 106, 108, 171) revin automat pe pvemini" + echo "conform priorității ha-group-main." + echo "==========================================" + } | mail -r 'ups@romfast.ro' -s "[CRITIC] pvemini DOWN $THRESHOLD min" "$MAIL_TO" +fi diff --git a/proxmox/cluster/failover/recover-vm201-to-pvemini.sh b/proxmox/cluster/failover/recover-vm201-to-pvemini.sh new file mode 100755 index 0000000..512106c --- /dev/null +++ b/proxmox/cluster/failover/recover-vm201-to-pvemini.sh @@ -0,0 +1,139 @@ +#!/bin/bash +# Revenire VM 201 pe pvemini după failover manual +# Deployed la /opt/scripts/recover-vm201-to-pvemini.sh pe pveelite +# Rulează DUPĂ ce pvemini e stabil din nou +set -euo pipefail + +VMID=201 +PRIMARY=pvemini +PRIMARY_IP=10.0.20.201 +SECONDARY=$(hostname) +LOG=/var/log/recover-vm201.log +MAIL_TO=mmarius28@gmail.com +MIN_UPTIME_MIN=30 # pvemini trebuie să fie UP min. 30 min + +log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG"; } +die() { log "ABORT: $*"; exit 1; } + +confirm() { + local msg="$1" + if [[ "${FORCE:-}" == "--yes" ]]; then + log "$msg → auto-confirmat (--yes)" + return 0 + fi + echo + read -p "$msg (tastează 'DA'): " ANS + [[ "$ANS" == "DA" ]] || die "Anulat la pasul: $msg" +} + +[[ "$SECONDARY" == "pveelite" ]] || die "Rulează pe pveelite, nu pe $SECONDARY" +[[ $EUID -eq 0 ]] || die "Trebuie root" + +FORCE=${1:-} + +log "=== Recovery VM $VMID → $PRIMARY pornit ===" + +# === CHECK-URI PRE-RECOVERY === + +# Check 1: VM rulează pe pveelite +if ! qm status "$VMID" 2>/dev/null | grep -q running; then + die "VM $VMID NU rulează pe $SECONDARY. Nu e nimic de recuperat." +fi +log "VM $VMID running pe $SECONDARY ✓" + +# Check 2: pvemini reachable + ssh OK +if ! ping -c 3 -W 2 "$PRIMARY_IP" &>/dev/null; then + die "$PRIMARY nu răspunde la ping. Abort." +fi +if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "root@$PRIMARY_IP" 'pvecm status' &>/dev/null; then + die "$PRIMARY nu răspunde la ssh sau nu e în cluster. Abort." +fi +log "$PRIMARY reachable ✓" + +# Check 3: uptime pvemini > threshold +UPTIME_SEC=$(ssh "root@$PRIMARY_IP" "cat /proc/uptime | awk '{print int(\$1)}'") +UPTIME_MIN=$((UPTIME_SEC / 60)) +log "$PRIMARY uptime: ${UPTIME_MIN} min" +if [[ "$UPTIME_MIN" -lt "$MIN_UPTIME_MIN" ]]; then + die "$PRIMARY uptime doar ${UPTIME_MIN} min (< ${MIN_UPTIME_MIN} min). Așteaptă stabilizare." +fi + +# Check 4: errors recente pe pvemini +ERR_COUNT=$(ssh "root@$PRIMARY_IP" "journalctl -p err -b --since '30 min ago' 2>/dev/null | grep -v '^-- ' | wc -l" || echo 0) +log "$PRIMARY erori în ultimele 30 min: $ERR_COUNT" +if [[ "$ERR_COUNT" -gt 50 ]]; then + log "ATENȚIE: $ERR_COUNT erori pe $PRIMARY" + confirm "Continui oricum?" +fi + +# Check 5: VM 201 NU există ca running pe pvemini +if ssh "root@$PRIMARY_IP" "qm status $VMID 2>/dev/null | grep -q running"; then + die "VM $VMID rulează deja pe $PRIMARY (split brain!). Oprește manual întâi." +fi + +# === EXECUȚIE === + +echo +echo "========================================" +echo "Plan recovery VM $VMID către $PRIMARY:" +echo " 1. Shutdown VM $VMID pe $SECONDARY" +echo " 2. Replicare inversă $SECONDARY → $PRIMARY" +echo " 3. Migrare offline $VMID către $PRIMARY" +echo " 4. Restore replicări normale" +echo " 5. Start VM $VMID pe $PRIMARY" +echo "========================================" +confirm "Pornesc recovery?" + +# Pas 1: Shutdown VM 201 pe pveelite +log "Pas 1/5: Shutdown VM $VMID pe $SECONDARY..." +qm shutdown "$VMID" --timeout 120 || die "Shutdown VM eșuat" +sleep 5 +qm status "$VMID" | grep -q stopped || die "VM $VMID nu s-a oprit" +log "VM $VMID stopped ✓" + +# Pas 2: Job replicare inversă pveelite → pvemini +log "Pas 2/5: Creez replicare inversă $SECONDARY → $PRIMARY..." +if pvesr status | grep -q "^${VMID}-2 "; then + log "Job ${VMID}-2 deja există, îl șterg" + pvesr delete "${VMID}-2" --force 1 || true + sleep 2 +fi +pvesr create-local-job "${VMID}-2" "$PRIMARY" --schedule '*/5' --source "$SECONDARY" \ + --comment "recovery reverse" +log "Rulez replicare inversă acum (poate dura, depinde de delta)..." +pvesr run --id "${VMID}-2" || die "Replicare inversă eșuată" +log "Replicare inversă OK ✓" + +# Pas 3: Migrare offline VM către pvemini +log "Pas 3/5: Migrare offline VM $VMID → $PRIMARY..." +qm migrate "$VMID" "$PRIMARY" || die "Migrare eșuată" +log "Migrare completă ✓" + +# Pas 4: Șterge job reverse + verifică replicările normale +log "Pas 4/5: Curăț jobul de replicare inversă..." +pvesr delete "${VMID}-2" --force 1 || true +sleep 2 +log "Replicări active:" +pvesr status | grep "^${VMID}-" | tee -a "$LOG" + +# Pas 5: Start VM pe pvemini +log "Pas 5/5: Start VM $VMID pe $PRIMARY..." +ssh "root@$PRIMARY_IP" "qm start $VMID" || die "Start pe $PRIMARY eșuat" +sleep 10 +PRIMARY_STATUS=$(ssh "root@$PRIMARY_IP" "qm status $VMID" | awk '{print $2}') +log "VM $VMID pe $PRIMARY: $PRIMARY_STATUS" + +# Mail confirmare +{ + echo "Recovery VM $VMID complet la $(date)" + echo + echo "$SECONDARY → $PRIMARY migrare reușită." + echo "Status VM pe $PRIMARY: $PRIMARY_STATUS" + echo + echo "Replicări active:" + pvesr status | grep "^${VMID}-" + echo + echo "Verifică aplicația Windows pe 10.0.20.201 (IIS etc)." +} | mail -r 'ups@romfast.ro' -s "[OK] Recovery VM $VMID pe $PRIMARY" "$MAIL_TO" + +log "=== Recovery complet ==="