fix(vm109-dr): trap cleanup to stop VM 109 on script exit

The DR test script used set -euo pipefail, so a failing SSH
shutdown command caused the script to exit before qm stop.
On 2026-04-20 this left VM 109 running for 2.5 days and
triggered an OOM cascade when pvemini HA-failed over to
pveelite.

Adds EXIT trap that force-stops VM 109 regardless of exit
path, and makes the Step 7 SSH shutdown tolerant of failure.
Incident details: proxmox/cluster/incidents/2026-04-20-cluster-outage.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-20 11:16:04 +00:00
parent 11001933f2
commit 60c27e7232
2 changed files with 321 additions and 1 deletions

View File

@@ -0,0 +1,306 @@
# Incident 2026-04-20 — Cluster outage pvemini + pveelite
**Severity:** Major (LXC/VM downtime pe 2 din 3 noduri, ~12h impact intermitent)
**Detected:** 2026-04-20 ~12:45 EEST (user)
**Resolved:** 2026-04-20 12:49 EEST (reboot manual toate nodurile)
**Author:** Claude Code (mmarius28@gmail.com)
---
## TL;DR — lanțul cauzal (versiune finală)
**Topologie reală:** pvemini (64 GB) = gazdă primară pentru TOATE: CT 100, 101, 104, 106, 108 + VM 109. pveelite (16 GB) = gazdă secundară de failover HA. pve1 (32 GB) = quorum-only, nu găzduiește servicii.
1. **Luni 2026-04-20 00:23:24**`pvemini` îngheață total, **fără niciun log kernel**. Cauză necunoscută (nu avem mcelog, kdump, IPMI SEL, UPS logs).
2. **00:39:47** — pvemini revine SINGUR (BIOS auto-power-on sau watchdog reset). **User nu a intervenit** — primul reboot manual de la user a fost abia la 12:49.
3. **00:40:17** — pveelite devine HA master, declanșează fence pe pvemini (pentru că pvemini era deja booted dar HA nu știa să-l recunoască imediat post-crash).
4. **00:41:28** — HA recovery masiv: CT 100 + CT 104 + CT 106 + CT 108 mutate de pe pvemini pe pveelite. VM 109 (HA state=`started` de la scriptul DR de sâmbătă, care n-a reușit să-l oprească) re-pornită pe pveelite (preferred în `ha-group-elite`).
5. **00:42:29** — primul OOM kill pe pveelite: VM 109 (kvm, 6.3 GB RSS) omorât. pveelite avea brusc CT 108 Oracle (8 GB) + VM 109 (6 GB) + CT 104 flowise + CT 100 portainer + CT 106 gitea + overhead. CT 101 minecraft NU era pe pveelite (era tot pe pvemini). Total cerere mult peste 16 GB.
6. **00:42 → 11:45****Buclă OOM infinită timp de ~3 ore**: HA LRM reporniseș VM 109 la fiecare 20 s, OOM killer îl omora. CT 104 și CT 108 erau de asemenea killed — amândouă **n-au `max_restart`/`max_relocate`** configurat → repornire infinită. CT 100 și CT 106 aveau limite, s-au oprit după 3 încercări.
7. **11:45:35** — pveelite devine nefuncțional, user intervine cu reboot la 11:46.
8. **12:49** — user reboot general manual pe ambele noduri → cluster stabil.
**Root cause dublu:**
- **Trigger:** crash misterios pvemini fără diagnostic + DR test script lasă VM 109 în HA state=`started` (bug `set -e`).
- **Amplificator:** pveelite (16 GB) nu are capacitate să preia load-ul pvemini (care rulează ~25 GB de servicii). Limite HA incoerente (CT 104, CT 108, VM 109 fără `max_restart`). pve1 (32 GB) nefolosit.
---
## Cronologie (EEST)
| Ora | Nod | Eveniment | Sursa |
|-----|-----|-----------|-------|
| 2026-04-18 06:00 | pveelite | DR test script pornește VM 109. SSH `10.0.20.37` nu răspunde în 180s. Script crashează la STEP 7 (`ssh shutdown` fail + `set -e`). **VM 109 rămâne RUNNING** | `/var/log/oracle-dr/dr_test_20260418_060001.log` |
| 2026-04-18 → 04-20 | pveelite | VM 109 stă pornit 2.5 zile (~6 GB). CT 101 (minecraft, 8 GB) rula deja → total ~14 GB pe 16 GB host | - |
| 2026-04-20 00:23:24 | pvemini | **Ultimul log** (`pmxcfs data verification successful`), apoi tăcere. Mașina îngheață total. | `journalctl -b -2` |
| 00:40:17 | pveelite | HA CRM detectează pvemini offline, devine master. pvemini trece în `unknown`. | `pve-ha-crm` |
| 00:41:17 | pveelite | pvemini trece în `fence`. Serviciile ct:100, ct:104, ct:106, ct:108 marcate `fence`. | `pve-ha-crm` |
| 00:41:27 | pveelite | HA obține lock-ul de fence, pvemini considerat oficial DOWN. | `pve-ha-crm` |
| **00:41:28** | pveelite | **Recovery masiv:** ct:100, ct:104, ct:106, ct:108 mutate de pe pvemini pe pveelite. VM 109 era deja pe pveelite. | `pve-ha-crm` |
| 00:42:29 | pveelite | **Primul OOM-kill VM 109** (kvm, 6.3 GB RSS). CT 108 (oracle 8 GB) tocmai pornea. | kernel oom-killer |
| 00:42 → 11:45 | pveelite | Buclă OOM infinită: HA repornește VM 109 și alte servicii fără limită (CT 104, CT 108 nu au `max_restart`). Zeci de OOM-kill per oră. | `pve-ha-lrm` + kernel |
| 00:39:47 | pvemini | Boot automat kernel 6.8.12-15-pve (BIOS auto-power-on, **NU** intervenție user). | kernel boot |
| 00:43 → 12:47 | pvemini | pvemini rulează normal ~12h (serviciile sale erau fence-uite pe pveelite). Chill. | - |
| 11:30 → 11:45 | pveelite | OOM începe să omoare și alte procese (containerd, tailscaled, pvefw-logger, ora_w003_xe, python3). | kernel |
| 11:45:35 | pveelite | Host nefuncțional, ultim log. | `journalctl --list-boots` |
| 11:46:41 | pveelite | Reboot (utilizator). | reboot history |
| 11:51:37 | pveelite | HA shutdown controlat VM 109 (user a schimbat HA state la `stopped`). | `pve-ha-lrm` |
| 11:53:37 | pveelite | HA shutdown controlat CT 105. | `pve-ha-lrm` |
| 12:02:09 | pveelite | Al doilea reboot (utilizator, cleanup). | reboot history |
| **12:47:30** | pvemini | Shutdown (**utilizator, prima intervenție pe pvemini azi**). | reboot history |
| 12:47:53 | pveelite | Shutdown. | reboot history |
| 12:49:50 | pvemini + pveelite | Boot simultan, cluster Quorate 3/3. | kernel boot |
---
## Analiză detaliată
### 1. Crash pvemini 00:23 — CAUZĂ NECUNOSCUTĂ
`pvemini` = HP ProDesk 600 G2 DM (i7-6700T, 32 GB RAM)... Actually corecție — sistemul DMI raportează "Venus Series" pentru pvemini (posibil Minisforum/Beelink mini PC). Verificare necesară.
Revenirea automată la 00:39 (fără power button) sugerează:
- **BIOS "AC Power Loss Recovery: Power On"** activ.
- **Kernel panic + automatic reboot** (dacă `kernel.panic=N` setat) — dar 16 min e prea lung pentru automatic reboot.
- **Hardware watchdog reset** cu delay — posibil.
- **Pană de curent scurtă** — neverificat (nu există `apcupsd` activ pe pvemini sau pveelite pentru jurnalizare evenimente UPS).
**Gol diagnostic total:** nu există `mcelog`, `kdump`, `netconsole`, IPMI SEL. Următoarea oară nu vom ști mai mult decât acum.
### 2. DR test script bug — CAUZĂ PRINCIPALĂ PREVENTIBILĂ
`proxmox/vm109-windows-dr/scripts/weekly-dr-test-proxmox.sh`, linia 507-511:
```bash
ssh -p "$DR_VM_PORT" "$DR_VM_USER@$DR_VM_IP" "shutdown /s /t 30" 2>/dev/null
sleep 60
qm stop "$DR_VM_ID" 2>/dev/null
track_step "VM Shutdown" true "VM stopped" "$step_start"
```
Scriptul are `set -euo pipefail`. Când VM 109 nu e SSH-reachable (cazul Apr 18, 03-21→04-18 toate), comanda ssh eșuează cu non-zero. **`set -e` termină scriptul imediat**, înainte de `qm stop`. VM 109 rămâne pornit.
Logurile tuturor testelor eșuate (21 martie → 18 aprilie, 5 săptămâni) au exact 1069 bytes — același punct de crash. VM 109 a fost pornit și nu a fost oprit niciodată după Apr 18.
### 3. HA recovery overload — CAUZĂ ARHITECTURALĂ
Când pvemini a căzut, HA a aplicat politica de recovery. `ha-group-main` are:
```
nodes pve1:33, pvemini:100, pveelite:50
```
pve1 = quorum-only (confirmat de user, dar HA nu știe asta — are pondere 33 în grup). HA a ales pveelite (pondere 50 > 33) pentru recovery.
Dar **pveelite (16 GB) nu poate găzdui serviciile pvemini** în stare normală, darămite peste ce rulează deja:
- CT 101 (minecraft, 8 GB) — deja pe pveelite
- CT 108 (central-oracle, 8 GB) — mutat de pe pvemini
- VM 109 (6 GB) — deja pe pveelite (din bug-ul DR)
- CT 100 + CT 104 + CT 106 — overhead ~3-4 GB
Total cerință ≥25 GB pe 16 GB host → OOM inevitabil.
În plus, CT 104 (flowise) și CT 108 (central-oracle) **nu aveau** `max_restart`/`max_relocate` definite. CT 100, CT 101, CT 106 aveau `max_restart=3 max_relocate=3`. Incoerență de configurare.
### 4. pve1 ca quorum-only — capacitate nefolosită
pve1 are i7-6700T (8 core) + 32 GB RAM. **Dar user confirmă**: este folosit exclusiv pentru quorum, nu pentru găzduire. Deci la failover HA nu are target adecvat pentru serviciile pvemini.
---
## Plan de prevenție
Ordinea reflectă **impactul real asupra incidentului**.
### P0 — Oprește bug-ul DR test (5 minute, OBLIGATORIU)
Scriptul trebuie să garanteze oprirea VM 109 chiar dacă SSH eșuează:
```bash
# Editare /opt/scripts/weekly-dr-test-proxmox.sh pe pveelite, linia ~505-511
# ÎNLOCUIEȘTE:
# ssh -p "$DR_VM_PORT" "$DR_VM_USER@$DR_VM_IP" "shutdown /s /t 30" 2>/dev/null
# sleep 60
# qm stop "$DR_VM_ID" 2>/dev/null
# CU:
# Încearcă shutdown soft; ignoră eșecul pentru a NU ieși din script
ssh -p "$DR_VM_PORT" -o ConnectTimeout=10 "$DR_VM_USER@$DR_VM_IP" \
"shutdown /s /t 30" 2>/dev/null || log_warning "SSH shutdown failed, forcing qm stop"
sleep 60
# Forțează oprire indiferent de starea SSH
qm stop "$DR_VM_ID" --skiplock 2>/dev/null || true
# Verificare finală — dacă încă pornit, forțează stop
if qm status "$DR_VM_ID" | grep -q "running"; then
qm stop "$DR_VM_ID" --skiplock --timeout 30 2>/dev/null || true
sleep 5
qm status "$DR_VM_ID" | grep -q "stopped" && log "VM $DR_VM_ID confirmed stopped" \
|| log_error "VM $DR_VM_ID still running after force stop"
fi
```
**Alternativ, cleanup global la sfârșitul scriptului** (pattern `trap`):
```bash
cleanup_vm() {
log "Cleanup: ensuring VM $DR_VM_ID is stopped"
qm stop "$DR_VM_ID" --skiplock 2>/dev/null || true
}
trap cleanup_vm EXIT # rulează la orice ieșire, inclusiv set -e
```
### P0 — Limite de restart pe toate resursele HA (1 minut)
CT 104 și CT 108 lipsesc limitele. Uniformizează:
```bash
ssh root@10.0.20.200 bash <<'EOF'
ha-manager set ct:104 --max_restart 3 --max_relocate 2
ha-manager set ct:108 --max_restart 3 --max_relocate 2
ha-manager set vm:109 --max_restart 0 --max_relocate 0
# VM 109 nu trebuie gestionat de HA deloc
ha-manager config
EOF
```
`max_restart=0 max_relocate=0` pentru VM 109 = HA nu încearcă să-l repornească. Combinat cu `state stopped` → VM 109 rămâne oprit până cineva îl pornește explicit (scriptul DR).
### P0 — Scoate VM 109 din HA (recomandare)
VM 109 e un DR test VM, nu un serviciu live. Nu ar trebui gestionat de HA.
```bash
# Șterge complet din HA
ha-manager remove vm:109
```
Scriptul DR va folosi `qm start 109` pe pveelite (unde rulează scriptul). HA nu va mai interveni. Eliminarea rezolvă și failover-ul nedorit către pveelite.
### P1 — Re-arhitecturare HA recovery pentru pvemini crash
**Problema:** dacă pvemini cade, serviciile sale (CT 108 Oracle 8 GB + CT 104 flowise + CT 100 portainer + CT 106 gitea) nu încap pe pveelite (16 GB).
**3 opțiuni, alege una:**
**Opțiunea A — Acceptă downtime pentru Oracle la crash pvemini (SIMPLU)**
```bash
# Scoate CT 108 din HA (acceptă manual restart dacă pvemini cade)
ha-manager remove ct:108
# CT 108 va porni doar cu onboot=1, pe pvemini, manual după crash
```
**Pro:** pveelite nu mai e sufocat. **Contra:** Oracle DB (serviciul critic) nu se recuperează automat.
**Opțiunea B — Permite pve1 să preia serviciile critice (SCALARE)**
User a spus că pve1 e quorum-only, dar are 32 GB RAM liberi. Dacă rolul ar putea fi extins:
```bash
# Modifică ha-group-main să prefere pve1 peste pveelite
cat > /etc/pve/ha/groups.cfg <<'EOF'
group: ha-group-main
nodes pvemini:100,pve1:75,pveelite:25
nofailback 0
restricted 0
EOF
```
**Pro:** CT 108 ar putea merge pe pve1 (32 GB) în loc de pveelite. **Contra:** pve1 devine parte activă din cluster, nu mai e doar quorum.
**Opțiunea C — Investește în redundanță pvemini (HARDWARE)**
Dacă pvemini e singurul punct de failure real, atunci: RAID, UPS dedicat, backup nod identical. Scump, dar evită problema.
**Recomandare:** Opțiunea A pentru CT 108 (Oracle DB acceptă downtime de 5-10 min pentru reintervenție manuală) + păstrează HA pentru CT 100/104/106 (lightweight, pot sta pe pveelite temporar).
### P1 — Swap pe pveelite (10 minute, insurance)
```bash
ssh root@10.0.20.202 bash <<'EOF'
zfs create -V 8G -b 4K -o compression=zle -o logbias=throughput \
-o sync=always -o primarycache=metadata -o secondarycache=none \
rpool/swap
mkswap -f /dev/zvol/rpool/swap
echo "/dev/zvol/rpool/swap none swap sw 0 0" >> /etc/fstab
swapon -a
echo "vm.swappiness=10" > /etc/sysctl.d/99-swap.conf
sysctl -p /etc/sysctl.d/99-swap.conf
EOF
```
Swap = scade OOM-killer trigger (dă timp pentru migrare/fallback) dar e bandage, nu fix.
### P2 — Diagnostic crash pvemini
```bash
ssh root@10.0.20.201 bash <<'EOF'
apt update && apt install -y mcelog kdump-tools
systemctl enable --now mcelog
# kdump: în /etc/default/grub adaugă crashkernel=256M
# netconsole către pve1
modprobe netconsole netconsole=@10.0.20.201/vmbr0,6666@10.0.20.200/
echo "options netconsole netconsole=@10.0.20.201/vmbr0,6666@10.0.20.200/" >> /etc/modprobe.d/netconsole.conf
# Watchdog NMI pentru hang detection
echo "kernel.nmi_watchdog=1" > /etc/sysctl.d/99-nmi.conf
sysctl -p /etc/sysctl.d/99-nmi.conf
EOF
# Verifică BIOS update la Minisforum/Beelink — ultimul versiune disponibilă
# memtest86+ bootabil la următoarea mentenanță
```
### P2 — Configurare UPS monitoring
Niciun nod nu are `apcupsd`/`nut` pornit → nu există log când pică curentul. Dacă pvemini e pe UPS, configurează monitorizare:
```bash
apt install apcupsd
# Configurare /etc/apcupsd/apcupsd.conf per setup UPS
# Jurnalizare evenimente în /var/log/apcupsd.events
```
### P3 — Alertare OOM + node-down
```bash
# /opt/scripts/oom-alert.sh — cron */1 * * * *
#!/bin/bash
COUNT=$(journalctl --since "1 minute ago" -k | grep -c "Killed process")
[ "$COUNT" -gt 0 ] && echo "OOM x$COUNT on $(hostname)" | \
mail -s "[ALERT] OOM $(hostname)" proxmox@romfast.ro
```
Plus: fix DKIM/SPF pe `romfast.ro` (Gmail a respins notificarea în dimineața incidentului — vezi log 00:00:02).
---
## Acțiuni — executate 2026-04-20 13:0014:00
- [x] **P0-1** Fix `weekly-dr-test-proxmox.sh` — trap `cleanup_vm EXIT` + SSH shutdown `|| log_warning` (pveelite:/opt/scripts/)
- [x] **P0-2** `ha-manager set ct:104 ct:108 --max_restart 3 --max_relocate 2`
- [x] **P0-3** `ha-manager remove vm:109` — VM 109 nu mai e gestionat de HA
- [x] **P1-1** Corosync token mărit la 10 s (`config_version: 16`) — tolerează glitch-uri USB pveelite
- [x] **P1-2** Swap 8 GB pe pveelite (ZFS zvol `rpool/swap`, `vm.swappiness=10`)
- [x] **P2-1** `rasdaemon` instalat + activ pe pvemini (MCE + PCIe AER monitoring)
- [x] **P2-2** `netconsole` pvemini → pve1 (log: `/var/log/netconsole-pvemini.log`, systemd unit `netconsole-receiver`)
- [x] **P2-3** `kernel.panic=10` pe pvemini (auto-reboot la panic după 10s)
## Rămas de făcut (opțional, necesită reboot sau decizie)
- [ ] **kdump-tools** pe pvemini — captură kernel crash dump (necesită `crashkernel=256M` în GRUB + reboot)
- [ ] **Fix DKIM/SPF `romfast.ro`** — Gmail respinge notificările (vezi log 2026-04-20 00:00:02)
- [ ] **apcupsd/nut** monitorizare UPS pe pvemini (cablul UPS Cypress USB-to-Serial e conectat, dar nu există jurnal evenimente power event)
- [ ] **Al 2-lea link corosync** pentru pveelite prin eno1 (necesită cablu fizic în NIC integrat)
- [ ] **Alertare OOM** — cron `oom-alert.sh` cu mail la fiecare `Killed process`
- [ ] **Cauza 00:23 crash pvemini****necunoscută**. Dacă se repetă, `netconsole` + `rasdaemon` ar trebui să dea indicii.
---
## Anexă — verificări
```bash
# Confirmare VM 109 oprit
ssh root@10.0.20.202 "qm status 109; ha-manager status | grep 109"
# Verificare limite HA după fix
ssh root@10.0.20.200 "ha-manager config"
# Confirmare pveelite stabil
ssh root@10.0.20.202 "free -h; journalctl -k --since today | grep -c 'Killed process'"
# Verificare DR script fix
ssh root@10.0.20.202 "head -520 /opt/scripts/weekly-dr-test-proxmox.sh | tail -25"
```