feat(cluster): cluster-shutdown/startup --allow-on-node pentru rulare pe pvemini

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
This commit is contained in:
Claude Agent
2026-09-13 10:18:12 +00:00
parent 0ac1cdd45f
commit 0104c255a5
4 changed files with 126 additions and 23 deletions

View File

@@ -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 ==="

View File

@@ -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 ==="