feat(cluster): corosync ring1 pe insula, test de repornire cu WoL, fixuri in scripturi

Ring1 aplicat si verificat pe cluster live: config_version 17, ring1_addr pe
10.10.10.20x la fiecare nod, interface{linknumber:1}. Validat cu `corosync -t`
inainte de instalare. Testat prin oprirea reala a lui ring0 pe pveelite: link0
disconnected, link1 connected, cvorum 3/3 neatins - exact scenariul care pe
27 august a lasat nodul mort 16 ore.

Test de repornire completa a clusterului, cu Wake-on-LAN (trezire in ~15s).
Insula a urcat singura pe toate trei nodurile; ipoteza enumerarii tarzii a
USB-ului nu s-a materializat. Unealta noua: wake-cluster.ps1.

Testul a scos la iveala o linie ramasa in /etc/fstab pe pve1 si pveelite, care
monta storage-ul NFS de pe IP-ul de productie inaintea lui pvestatd. Backup-ul
trecea tacut pe reteaua gresita, cu storage.cfg corect. cluster-startup verifica
acum asta automat, impreuna cu IP-urile de insula si conectivitatea reala.

Patru bug-uri gasite prin rulare pe cluster live:

- sonda Oracle nu avea timeout: un sqlplus agatat pe o instanta in pornire a
  blocat cluster-startup 14 minute, fara mesaj, cu propriul prag de 600s
  nefolosit, fiindca bucla n-a apucat o iteratie;
- `bash -c` in loc de `bash -lc`: sqlplus lipsea din PATH, deci baza nu se
  oprea si containerul s-ar fi inchis peste ea;
- PowerShell 5.1 pierde ghilimelele duble catre exe-uri native, deci comanda
  ajungea rupta pe nod - trecut pe trimitere codificata base64;
- backup-ul de crontab si fisierul de stare se rescriau la o a doua rulare,
  lasand monitorizarea oprita permanent si lista de repornit goala.

Documentatia de oprire planificata pornea de la o afirmatie devenita falsa
("nu exista datacenter.cfg") si de la o comanda care ar fi adaugat o a doua
linie `ha:`. Actualizata, impreuna cu inventarul de guest-uri (VM 304 lipsea
din toate listele de ordine si cadea in maturarea de dupa Oracle).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RJFQZcA7uerXRaL1NrsXpS
This commit is contained in:
Marius
2026-08-29 21:13:34 +03:00
parent fd2105d1f9
commit d7b4007af8
10 changed files with 801 additions and 113 deletions

View File

@@ -53,6 +53,7 @@ $NodeShutdownOrder = @('pveelite', 'pve1', 'pvemini')
# Orice guest pornit care nu apare aici e oprit la final, intr-o maturare.
$GuestShutdownOrder = @(
303, # Win11-Adina - desktop, fara dependente
304, # Win11-Marius - desktop, fara dependente
302, # oracle-test-302 - VM de test
310, # Win11-Template - template
201, # roacentral - IIS reverse proxy, consumator Oracle
@@ -110,6 +111,21 @@ function Invoke-Node {
return $out
}
# Comenzi cu ghilimele imbricate NU se trimit prin Invoke-Node.
#
# PowerShell 5.1 pierde ghilimelele duble cand paseaza un argument catre un
# executabil nativ: `bash -lc "printf '...' | sqlplus"` ajunge pe nod ca
# `bash -lc printf '...' | sqlplus`, adica printf fara format, iar sqlplus rulat
# pe NOD, unde nu exista. Esecul e usor de citit gresit ca "lipseste sqlplus".
# In loc sa ne luptam cu nivelurile de citare, trimitem scriptul codificat.
# Verificat pe 2026-08-29, dupa ce varianta directa a esuat pe cluster live.
function Invoke-NodeScript {
param([string]$Node, [string]$Script)
$lf = $Script -replace "`r`n", "`n"
$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($lf))
return Invoke-Node -Node $Node -Command "echo $b64 | base64 -d | bash"
}
# Ca Invoke-Node, dar respecta -DryRun (pentru comenzi care modifica starea).
function Invoke-NodeChange {
param([string]$Node, [string]$Command)
@@ -226,10 +242,22 @@ if ($running.Count -eq 0) {
# reporneasca VM-uri oprite intentionat (302, 310, 301...).
# Format identic cu varianta bash, ca cele doua sa fie interschimbabile.
if (-not $DryRun) {
$lines = @("# stare cluster salvata la $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
foreach ($g in $running) { $lines += "$($g.Type) $($g.Node) $($g.Id) $($g.Status) $($g.Ha)" }
Set-Content -Path $StateFile -Value $lines -Encoding utf8
Write-Ok "stare salvata in $StateFile"
# Nu rescrie peste o stare existenta cu un inventar gol. Cazul real: scriptul
# e rulat a doua oara (dupa -NoNodes, sau dupa o eroare), cand guest-urile sunt
# deja oprite - inventarul e gol si am pierde exact lista de repornit.
$existing = @()
if (Test-Path $StateFile) {
$existing = @(Get-Content $StateFile | Where-Object { $_ -notmatch '^\s*#' -and $_.Trim() })
}
if ($running.Count -eq 0 -and $existing.Count -gt 0) {
Write-Warn "Niciun guest pornit, dar $StateFile contine deja $($existing.Count) intrari - il PASTREZ."
Write-Warn "Altfel cluster-startup n-ar mai sti ce sa porneasca."
} else {
$lines = @("# stare cluster salvata la $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
foreach ($g in $running) { $lines += "$($g.Type) $($g.Node) $($g.Id) $($g.Status) $($g.Ha)" }
Set-Content -Path $StateFile -Value $lines -Encoding utf8
Write-Ok "stare salvata in $StateFile"
}
}
Confirm-Step "Se opresc $($running.Count) guest-uri, apoi nodurile: $($NodeShutdownOrder -join ', ')."
@@ -246,6 +274,9 @@ if ($currentPolicy -eq 'freeze') {
Write-Ok "deja pe freeze"
} else {
if ($null -ne $currentPolicy -or ($dcCfg | Measure-Object).Count -gt 0) {
# sed sterge DOAR linia 'ha:', nu rescrie fisierul: din 2026-08-29 acolo
# sta si 'migration: network=10.10.10.0/24,type=insecure', fara de care
# replicarea se intoarce tacut pe reteaua de productie.
Invoke-NodeChange -Node 'pvemini' -Command 'cp /etc/pve/datacenter.cfg /root/datacenter.cfg.backup-mentenanta' | Out-Null
Invoke-NodeChange -Node 'pvemini' -Command "sed -i '/^ha:/d' /etc/pve/datacenter.cfg; printf 'ha: shutdown_policy=freeze\n' >> /etc/pve/datacenter.cfg" | Out-Null
} else {
@@ -262,7 +293,10 @@ Write-Step "Dezactivare cron-uri de alerta"
$sedCron = "crontab -l | sed -E '/^[^#]/ s%^(.*($CronPattern)\.sh.*)`$%#MENTENANTA \1%' | crontab -"
foreach ($node in $NodeIp.Keys) {
Invoke-NodeChange -Node $node -Command "crontab -l > $CronBackup 2>/dev/null || true" | Out-Null
# Backup DOAR daca crontab-ul curent nu e deja comentat. Altfel o a doua rulare
# a scriptului (dupa o eroare, de exemplu) ar salva peste backup versiunea deja
# comentata, iar cluster-startup ar "restaura" monitorizarea oprita - tacut.
Invoke-NodeChange -Node $node -Command "if crontab -l 2>/dev/null | grep -q '^#MENTENANTA'; then echo 'backup pastrat'; else crontab -l > $CronBackup 2>/dev/null || true; echo 'backup nou'; fi" | Out-Null
Invoke-NodeChange -Node $node -Command $sedCron | Out-Null
if (-not $DryRun) {
$n = Invoke-Node -Node $node -Command "crontab -l | grep -c '^#MENTENANTA' || true"
@@ -285,13 +319,27 @@ if (-not $oracleGuest) {
if ($DryRun) {
Write-Host " [dry-run] shutdown immediate in $OracleDocker" -ForegroundColor DarkGray
} else {
$sqlCmd = 'pct exec ' + $OracleCt + ' -- docker exec ' + $OracleDocker + ' bash -c "printf ''shutdown immediate\nexit\n'' | sqlplus -s / as sysdba"'
$out = Invoke-Node -Node $oracleGuest.Node -Command $sqlCmd
if ($LASTEXITCODE -ne 0) {
Write-Warn "Shutdown-ul bazei a raportat eroare. Verifica manual inainte de a continua!"
$out | Select-Object -Last 5 | Write-Host
# 'bash -lc', NU 'bash -c': fara shell de login, PATH-ul Oracle nu e incarcat
# si comanda esueaza cu "sqlplus: command not found" - adica baza ramane
# DESCHISA, iar containerul ar fi oprit peste ea.
# '2>&1' se face pe nod, nu in PowerShell: altfel stderr-ul lui ssh devine
# ErrorRecord in PS 5.1 si arunca exceptie in loc sa fie tratat aici.
# 'timeout 180': un 'shutdown immediate' poate dura, dar nu la infinit. Fara
# el, un sqlplus blocat ar tine scriptul agatat fara niciun mesaj (patit pe
# 2026-08-29, la sonda echivalenta din cluster-startup).
$sqlScript = @"
timeout 180 pct exec $OracleCt -- docker exec $OracleDocker bash -lc "printf 'shutdown immediate\nexit\n' | sqlplus -s / as sysdba" 2>&1
"@
$out = Invoke-NodeScript -Node $oracleGuest.Node -Script $sqlScript
# Codul de iesire nu e o dovada: sqlplus intoarce 0 si cand n-a inchis nimic.
# Singura confirmare reala e mesajul instantei.
if (($out -join "`n") -match 'ORACLE instance shut down') {
Write-Ok "instanta Oracle oprita curat"
} else {
Write-Ok "instanta Oracle oprita"
Write-Warn "Baza NU a confirmat 'ORACLE instance shut down'. Ultimele linii:"
$out | Select-Object -Last 8 | Write-Host
Confirm-Step "Oprirea CT $OracleCt peste o baza posibil deschisa. Continui?"
}
}
}

View File

@@ -34,6 +34,7 @@ NODE_SHUTDOWN_ORDER=(pveelite pve1 pvemini)
# 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
304 # Win11-Marius — desktop, fără dependențe
302 # oracle-test-302 — VM de test
310 # Win11-Template — template
201 # roacentral — IIS reverse proxy, consumator Oracle
@@ -202,9 +203,19 @@ 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"
# Nu rescrie peste o stare existentă cu un inventar gol. Cazul real: scriptul
# e rulat a doua oară (după --no-nodes, sau după o eroare), când guest-urile
# sunt deja oprite — inventarul e gol și am pierde exact lista de repornit.
EXISTING=0
[[ -f "$STATE_FILE" ]] && EXISTING=$(grep -cv -e '^[[:space:]]*#' -e '^[[:space:]]*$' "$STATE_FILE" || true)
if [[ -z "$RUNNING" ]] && (( EXISTING > 0 )); then
warn "Niciun guest pornit, dar $STATE_FILE conține deja $EXISTING intrări — îl PĂSTREZ."
warn "Altfel cluster-startup.sh n-ar mai ști ce să pornească."
else
{ 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
fi
confirm "Se opresc $RUNNING_COUNT guest-uri, apoi nodurile: ${NODE_SHUTDOWN_ORDER[*]}."
@@ -218,6 +229,9 @@ if [[ "$CURRENT_POLICY" == "freeze" ]]; then
ok "deja pe freeze"
else
if nssh pvemini "test -f /etc/pve/datacenter.cfg"; then
# sed șterge DOAR linia 'ha:', nu rescrie fișierul: din 2026-08-29 acolo
# stă și 'migration: network=10.10.10.0/24,type=insecure', fără de care
# replicarea se întoarce tăcut pe rețeaua de producție.
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
@@ -231,7 +245,10 @@ fi
step "Dezactivare cron-uri de alertă"
for node in "${!NODE_IP[@]}"; do
run "$node" "crontab -l > $CRON_BACKUP 2>/dev/null || true"
# Backup DOAR dacă crontab-ul curent nu e deja comentat. Altfel o a doua rulare
# a scriptului (după o eroare, de exemplu) ar salva peste backup versiunea deja
# comentată, iar cluster-startup.sh ar „restaura" monitorizarea oprită — tăcut.
run "$node" "if crontab -l 2>/dev/null | grep -q '^#MENTENANTA'; then echo 'backup păstrat'; else crontab -l > $CRON_BACKUP 2>/dev/null || true; fi"
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")
@@ -253,9 +270,23 @@ else
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ă"
# `bash -lc`, NU `bash -c`: fără shell de login, PATH-ul Oracle nu e încărcat
# și comanda eșuează cu „sqlplus: command not found" — adică baza rămâne
# DESCHISĂ, iar containerul ar fi oprit peste ea.
# `timeout 180`: un „shutdown immediate" poate dura, dar nu la infinit. Fără
# el, un sqlplus blocat ar ține scriptul agățat fără niciun mesaj (pățit pe
# 2026-08-29, la sonda echivalentă din cluster-startup.sh).
SQL_OUT=$(nssh "$ORACLE_NODE" "timeout 180 pct exec $ORACLE_CT -- docker exec $ORACLE_DOCKER bash -lc \"printf 'shutdown immediate\nexit\n' | sqlplus -s / as sysdba\" 2>&1" || true)
# Codul de ieșire nu e o dovadă: sqlplus întoarce 0 și când n-a închis nimic.
# Singura confirmare reală e mesajul instanței.
if grep -q 'ORACLE instance shut down' <<<"$SQL_OUT"; then
ok "instanța Oracle oprită curat"
else
warn "Baza NU a confirmat „ORACLE instance shut down\". Ultimele linii:"
tail -8 <<<"$SQL_OUT"
confirm "Oprirea CT $ORACLE_CT peste o bază posibil deschisă. Continui?"
fi
fi
fi

View File

@@ -61,6 +61,7 @@ $GuestStartOrder = @(
101, # minecraft
110, # moltbot
303, # Win11-Adina
304, # Win11-Marius
302, # oracle-test-302
109, # oracle-dr-windows
301, # docker-portainer-template
@@ -74,9 +75,26 @@ $GuestStartOrder = @(
# 301, 310 - template-uri, nu servicii.
$NeverAutostart = @(109, 301, 310)
# Reteaua dedicata de cluster ("insula"), existenta din 2026-08-29. De ea atarna
# replicarea ZFS (migration: network=10.10.10.0/24 in datacenter.cfg) si storage-ul
# NFS backup-pvemini. Pe pve1 si pveelite IP-ul sta pe dongle-ul USB, adus la boot
# de un "auto enx..." din /etc/network/interfaces: daca nucleul enumereaza USB-ul
# DUPA ce networking.service a terminat, interfata ramane fara IP, iar nodul pare
# perfect sanatos. Detalii: ../docs/switch-2.5g-dedicat-noduri.md
$IslandIp = [ordered]@{
pve1 = '10.10.10.200'
pvemini = '10.10.10.201'
pveelite = '10.10.10.202'
}
$IslandNfsMount = '/mnt/pve/backup-pvemini'
$IslandNfsServer = '10.10.10.201'
$IslandNfsNodes = @('pve1', 'pveelite')
$OracleCt = 108
$OracleDocker = 'oracle-xe'
$OracleWait = 600 # secunde asteptate pana baza raspunde la interogari
# Masurat pe 2026-08-29, la pornire de la rece dupa oprirea completa a clusterului:
# instanta a avut nevoie de ~10 minute ca sa ajunga OPEN. 600s erau prea strimti.
$OracleWait = 1200 # secunde asteptate pana baza raspunde la interogari
$NodeWait = 1800 # secunde asteptate pana apar toate nodurile
$QuorumWait = 300
@@ -119,6 +137,21 @@ function Invoke-NodeChange {
return Invoke-Node -Node $Node -Command $Command
}
# Comenzi cu ghilimele imbricate NU se trimit prin Invoke-Node.
#
# PowerShell 5.1 pierde ghilimelele duble cand paseaza un argument catre un
# executabil nativ: `bash -lc "printf '...' | sqlplus"` ajunge pe nod ca
# `bash -lc printf '...' | sqlplus`, adica printf fara format, iar sqlplus rulat
# pe NOD, unde nu exista. Esecul e usor de citit gresit ca "lipseste sqlplus".
# In loc sa ne luptam cu nivelurile de citare, trimitem scriptul codificat.
# Verificat pe 2026-08-29, dupa ce varianta directa a esuat pe cluster live.
function Invoke-NodeScript {
param([string]$Node, [string]$Script)
$lf = $Script -replace "`r`n", "`n"
$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($lf))
return Invoke-Node -Node $Node -Command "echo $b64 | base64 -d | bash"
}
function Confirm-Step {
param([string]$Message)
if ($Yes -or $DryRun) { return }
@@ -209,7 +242,92 @@ Write-Ok "quorum OK, $votes/3 voturi"
Invoke-Node -Node 'pvemini' -Command 'ha-manager status' | Select-Object -First 4 | Write-Host
# ------------------------------------------------- pas 3: ce trebuie pornit
# ------------------------------------ pas 3: reteaua dedicata de cluster (insula)
# Se verifica INAINTE de pornirea guest-urilor: daca insula e jos, replicarea si
# backup-ul NFS tac fara nicio alerta, iar simptomul apare abia peste ore.
# Nu blocheaza pornirea - la revenirea curentului, Oracle sus conteaza mai mult
# decat replicarea - dar nu te lasa sa treci pe langa problema fara sa o vezi.
Write-Step "Verific reteaua de cluster (10.10.10.0/24)"
if ($DryRun) {
Write-Warn "dry-run - sar peste verificarea insulei"
} else {
$islandBad = @()
foreach ($node in $NodeIp.Keys) {
$want = $IslandIp[$node]
$have = (Invoke-Node -Node $node -Command "ip -4 -o addr show | grep -c ' $want/' || true") -join ''
if ($have.Trim() -eq '0') {
# Cazul asteptat de esec: dongle-ul USB enumerat tarziu la boot.
# ifreload -a aplica doar diferentele fata de /etc/network/interfaces,
# deci nu atinge interfetele tap/veth ale guest-urilor.
Write-Warn "$node - lipseste $want, incerc 'ifreload -a'"
Invoke-Node -Node $node -Command 'PATH=/usr/sbin:/sbin:/usr/bin:/bin ifreload -a' | Out-Null
Start-Sleep -Seconds 8
$have = (Invoke-Node -Node $node -Command "ip -4 -o addr show | grep -c ' $want/' || true") -join ''
}
if ($have.Trim() -eq '0') {
$islandBad += $node
Write-Warn "$node - TOT fara $want dupa ifreload"
} else {
Write-Ok "$node - $want prezent"
}
}
# Un IP configurat local nu garanteaza si cablul/switch-ul. pvemini e sursa
# replicarii si serverul NFS, deci se verifica din el spre celelalte doua.
foreach ($node in @('pve1', 'pveelite')) {
$ip = $IslandIp[$node]
$p = (Invoke-Node -Node 'pvemini' -Command "ping -c1 -W2 $ip >/dev/null 2>&1 && echo OK || echo FAIL") -join ''
if ($p.Trim() -eq 'OK') {
Write-Ok "pvemini -> $node ($ip) - ping OK"
} else {
if ($islandBad -notcontains $node) { $islandBad += $node }
Write-Warn "pvemini -> $node ($ip) - NU raspunde (cablu? switch?)"
}
}
$nfsBad = @()
foreach ($node in $IslandNfsNodes) {
$src = (Invoke-Node -Node $node -Command "findmnt -no SOURCE $IslandNfsMount 2>/dev/null || true") -join ''
if ($src -match [regex]::Escape($IslandNfsServer)) {
Write-Ok "$node - NFS $IslandNfsMount montat de pe $IslandNfsServer"
} elseif ($src.Trim()) {
# Prins pe 2026-08-29 la primul boot la rece: o linie ramasa in /etc/fstab
# monta storage-ul de pe IP-ul de productie INAINTE ca pvestatd sa apuce,
# deci backup-ul mergea tacut pe reteaua gresita, cu storage.cfg corect.
$nfsBad += $node
Write-Warn "$node - NFS montat de pe ALT server: $src (asteptat $IslandNfsServer)"
Write-Warn " verifica: ssh root@$($NodeIp[$node]) 'grep nfs /etc/fstab'"
} else {
# pvestatd remonteaza singur in cateva zeci de secunde daca reteaua e sus.
Write-Warn "$node - NFS $IslandNfsMount NEMONTAT (pvestatd il reia daca insula urca)"
}
}
if ($nfsBad.Count -gt 0) {
Write-Warn "Backup-ul NFS trece pe alta retea decat cea proiectata pe: $($nfsBad -join ', ')"
}
if ($islandBad.Count -gt 0) {
Write-Warn ""
Write-Warn "INSULA E JOS pe: $($islandBad -join ', ')"
Write-Warn "Consecinta: replicarea ZFS si storage-ul NFS backup-pvemini NU vor functiona."
Write-Warn "Guest-urile pornesc si merg normal - problema e tacuta, se vede abia in pvesr status."
Write-Warn "Verifica: ssh root@<nod> `"ip -br a; dmesg | tail -20`" (dongle USB enumerat tarziu?)"
Confirm-Step "Continui cu pornirea guest-urilor, cu replicarea jos?"
} elseif ($nfsBad.Count -eq 0) {
Write-Ok "insula completa - replicarea si NFS-ul au calea libera"
} else {
Write-Warn "insula e sus, dar NFS-ul nu o foloseste - vezi mai sus"
}
}
# ------------------------------------------------- pas 4: ce trebuie pornit
Write-Step "Ce se porneste"
@@ -262,7 +380,7 @@ foreach ($g in $toStart) {
Confirm-Step "Se pornesc guest-urile de mai sus, Oracle primul."
# ------------------------------------------------- pas 4: pornire guest-uri
# ------------------------------------------------- pas 5: pornire guest-uri
Write-Step "Pornire guest-uri"
@@ -306,11 +424,24 @@ function Wait-Oracle {
Write-Log "astept ca instanta Oracle sa accepte interogari..."
if ($DryRun) { return }
$sql = 'pct exec ' + $OracleCt + ' -- docker exec ' + $OracleDocker + ' bash -c "printf ''set pagesize 0 feedback off\nselect global_name from global_name;\nexit\n'' | sqlplus -s / as sysdba"'
# 'bash -lc', NU 'bash -c': fara shell de login nu exista sqlplus in PATH, iar
# asteptarea ar expira dupa $OracleWait secunde chiar cu baza perfect deschisa.
#
# 'timeout 30' e OBLIGATORIU, nu o precautie. Pe 2026-08-29, la pornirea de la
# rece, sqlplus lansat catre o instanta inca in startup s-a blocat definitiv:
# docker exec a mostenit blocajul, ssh a asteptat, iar bucla de mai jos n-a
# apucat sa faca nici macar o iteratie. Scriptul a stat agatat 14 minute fara
# niciun mesaj, cu propriul prag de asteptare nefolosit. Sonda trebuie sa
# raspunda mereu, fie si cu esec.
$sql = @"
timeout 30 pct exec $OracleCt -- docker exec $OracleDocker bash -lc "printf 'set pagesize 0 feedback off\nselect status from v\`$instance;\nexit\n' | sqlplus -s / as sysdba" 2>&1
"@
$waited = 0
while ($waited -lt $OracleWait) {
$out = Invoke-Node -Node $Node -Command $sql
if ($LASTEXITCODE -eq 0 -and (($out -join '') -match '[A-Z]')) {
$out = Invoke-NodeScript -Node $Node -Script $sql
# 'OPEN' e raspunsul instantei deschise; orice altceva (ORA-, container
# inca in pornire) inseamna ca mai asteptam.
if (($out -join '') -match 'OPEN') {
Write-Host "`r `r" -NoNewline
Write-Ok "Oracle deschis si interogabil (${waited}s)"
return
@@ -342,7 +473,7 @@ foreach ($g in $toStart) {
if (-not (Start-Guest -Guest $g)) { $failed += "$($g.Type):$($g.Id)" }
}
# ------------------------------------------------ pas 5: restaurare cron-uri
# ------------------------------------------------ pas 6: restaurare cron-uri
Write-Step "Restaurare cron-uri de monitorizare"
@@ -363,7 +494,7 @@ foreach ($node in $NodeIp.Keys) {
}
}
# ------------------------------------------------------- pas 6: verificare
# ------------------------------------------------------- pas 7: verificare
Write-Step "Verificare finala"
@@ -396,6 +527,9 @@ if ($failed.Count -gt 0) {
Write-Host ""
Write-Host " De verificat manual:"
Write-Host " - shutdown_policy e inca 'freeze' (recomandat sa ramana asa)"
Write-Host " - datacenter.cfg are inca AMBELE linii: 'ha: shutdown_policy=freeze'"
Write-Host " si 'migration: network=10.10.10.0/24,type=insecure'"
Write-Host " - daca lipsesc date recente, compara cu ultima replicare din pvesr status"
Write-Host " - daca a fost prima repornire dupa 2026-08-29: noteaza in"
Write-Host " docs/switch-2.5g-dedicat-noduri.md daca insula a urcat singura la boot"
Write-Host ""

View File

@@ -46,6 +46,7 @@ GUEST_START_ORDER=(
101 # minecraft
110 # moltbot
303 # Win11-Adina
304 # Win11-Marius
302 # oracle-test-302
109 # oracle-dr-windows — DR, în mod normal rămâne oprit
301 # docker-portainer-template
@@ -59,9 +60,26 @@ GUEST_START_ORDER=(
# 301, 310 — template-uri, nu servicii.
NEVER_AUTOSTART=(109 301 310)
# Rețeaua dedicată de cluster („insula"), existentă din 2026-08-29. De ea atârnă
# replicarea ZFS (migration: network=10.10.10.0/24 în datacenter.cfg) și storage-ul
# NFS backup-pvemini. Pe pve1 și pveelite IP-ul stă pe dongle-ul USB, adus la boot
# de un „auto enx..." din /etc/network/interfaces: dacă nucleul enumerează USB-ul
# DUPĂ ce networking.service a terminat, interfața rămâne fără IP, iar nodul pare
# perfect sănătos. Detalii: ../docs/switch-2.5g-dedicat-noduri.md
declare -A ISLAND_IP=(
[pve1]=10.10.10.200
[pvemini]=10.10.10.201
[pveelite]=10.10.10.202
)
ISLAND_NFS_MOUNT=/mnt/pve/backup-pvemini
ISLAND_NFS_SERVER=10.10.10.201
ISLAND_NFS_NODES=(pve1 pveelite)
ORACLE_CT=108
ORACLE_DOCKER=oracle-xe
ORACLE_WAIT=600 # secunde așteptate până baza răspunde la interogări
# Măsurat pe 2026-08-29, la pornire de la rece după oprirea completă a clusterului:
# instanța a avut nevoie de ~10 minute ca să ajungă OPEN. 600s erau prea strâmți.
ORACLE_WAIT=1200 # 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
@@ -177,7 +195,91 @@ ok "quorum OK, $VOTES/3 voturi"
nssh pvemini "ha-manager status | head -4"
# ------------------------------------------------- pas 3: ce trebuie pornit
# ----------------------------------- pas 3: rețeaua dedicată de cluster (insula)
# Se verifică ÎNAINTE de pornirea guest-urilor: dacă insula e jos, replicarea și
# backup-ul NFS tac fără nicio alertă, iar simptomul apare abia peste ore.
# Nu blochează pornirea — la revenirea curentului, Oracle sus contează mai mult
# decât replicarea — dar nu te lasă să treci pe lângă problemă fără să o vezi.
step "Verific rețeaua de cluster (10.10.10.0/24)"
if (( DRY_RUN )); then
warn "dry-run — sar peste verificarea insulei"
else
ISLAND_BAD=()
for node in "${!NODE_IP[@]}"; do
want="${ISLAND_IP[$node]}"
have=$(nssh "$node" "ip -4 -o addr show | grep -c ' $want/' || true")
if [[ "$have" == "0" ]]; then
# Cazul așteptat de eșec: dongle-ul USB enumerat târziu la boot.
# ifreload -a aplică doar diferențele față de /etc/network/interfaces,
# deci nu atinge interfețele tap/veth ale guest-urilor.
warn "$node — lipsește $want, încerc 'ifreload -a'"
nssh "$node" "PATH=/usr/sbin:/sbin:/usr/bin:/bin ifreload -a" || true
sleep 8
have=$(nssh "$node" "ip -4 -o addr show | grep -c ' $want/' || true")
fi
if [[ "$have" == "0" ]]; then
ISLAND_BAD+=("$node")
warn "$node — TOT fără $want după ifreload"
else
ok "$node — $want prezent"
fi
done
# Un IP configurat local nu garantează și cablul/switch-ul. pvemini e sursa
# replicării și serverul NFS, deci se verifică din el spre celelalte două.
for node in pve1 pveelite; do
ip="${ISLAND_IP[$node]}"
if nssh pvemini "ping -c1 -W2 $ip >/dev/null 2>&1"; then
ok "pvemini → $node ($ip) — ping OK"
else
printf '%s\n' "${ISLAND_BAD[@]:-}" | grep -qx "$node" || ISLAND_BAD+=("$node")
warn "pvemini → $node ($ip) — NU răspunde (cablu? switch?)"
fi
done
NFS_BAD=()
for node in "${ISLAND_NFS_NODES[@]}"; do
src=$(nssh "$node" "findmnt -no SOURCE $ISLAND_NFS_MOUNT 2>/dev/null || true")
if [[ "$src" == *"$ISLAND_NFS_SERVER"* ]]; then
ok "$node — NFS $ISLAND_NFS_MOUNT montat de pe $ISLAND_NFS_SERVER"
elif [[ -n "${src// /}" ]]; then
# Prins pe 2026-08-29 la primul boot la rece: o linie rămasă în /etc/fstab
# monta storage-ul de pe IP-ul de producție ÎNAINTE ca pvestatd să apuce,
# deci backup-ul mergea tăcut pe rețeaua greșită, cu storage.cfg corect.
NFS_BAD+=("$node")
warn "$node — NFS montat de pe ALT server: $src (așteptat $ISLAND_NFS_SERVER)"
warn " verifică: ssh root@${NODE_IP[$node]} 'grep nfs /etc/fstab'"
else
# pvestatd remontează singur în câteva zeci de secunde dacă rețeaua e sus.
warn "$node — NFS $ISLAND_NFS_MOUNT NEMONTAT (pvestatd îl reia dacă insula urcă)"
fi
done
if (( ${#NFS_BAD[@]} )); then
warn "Backup-ul NFS trece pe altă rețea decât cea proiectată pe: ${NFS_BAD[*]}"
fi
if (( ${#ISLAND_BAD[@]} )); then
warn ""
warn "INSULA E JOS pe: ${ISLAND_BAD[*]}"
warn "Consecință: replicarea ZFS și storage-ul NFS backup-pvemini NU vor funcționa."
warn "Guest-urile pornesc și merg normal — problema e tăcută, se vede abia în pvesr status."
warn "Verifică: ssh root@<nod> \"ip -br a; dmesg | tail -20\" (dongle USB enumerat târziu?)"
confirm "Continui cu pornirea guest-urilor, cu replicarea jos?"
elif (( ${#NFS_BAD[@]} == 0 )); then
ok "insulă completă — replicarea și NFS-ul au calea liberă"
else
warn "insula e sus, dar NFS-ul nu o folosește — vezi mai sus"
fi
fi
# ------------------------------------------------- pas 4: ce trebuie pornit
step "Ce se pornește"
@@ -223,7 +325,7 @@ done <<<"$TO_START"
confirm "Se pornesc guest-urile de mai sus, în ordinea: ${GUEST_START_ORDER[*]}"
# ------------------------------------------------- pas 4: pornire guest-uri
# ------------------------------------------------- pas 5: pornire guest-uri
step "Pornire guest-uri"
@@ -269,7 +371,16 @@ wait_oracle() {
(( 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
# `bash -lc`, NU `bash -c`: fără shell de login nu există sqlplus în PATH, iar
# așteptarea ar expira după $ORACLE_WAIT secunde chiar cu baza perfect deschisă.
#
# `timeout 30` e OBLIGATORIU, nu o precauție. Pe 2026-08-29, la pornirea de la
# rece, sqlplus lansat către o instanță încă în startup s-a blocat definitiv:
# docker exec a moștenit blocajul, ssh a așteptat, iar bucla n-a apucat să
# facă nici măcar o iterație. Scriptul a stat agățat 14 minute fără niciun
# mesaj, cu propriul prag de așteptare nefolosit. Sonda trebuie să răspundă
# mereu, fie și cu eșec.
if nssh "$node" "timeout 30 pct exec $ORACLE_CT -- docker exec $ORACLE_DOCKER bash -lc \"printf 'set pagesize 0 feedback off\nselect status from v\\\$instance;\nexit\n' | sqlplus -s / as sysdba\" 2>/dev/null | grep -q OPEN"; then
ok "Oracle deschis și interogabil (${waited}s)"
return 0
fi
@@ -305,7 +416,7 @@ while read -r type node id status ha; do
start_guest "$type" "$node" "$id" "$ha" || FAILED+=("$type:$id")
done <<<"$TO_START"
# ------------------------------------------------ pas 5: restaurare cron-uri
# ------------------------------------------------ pas 6: restaurare cron-uri
step "Restaurare cron-uri de monitorizare"
@@ -325,7 +436,7 @@ for node in "${!NODE_IP[@]}"; do
fi
done
# ------------------------------------------------------- pas 6: verificare
# ------------------------------------------------------- pas 7: verificare
step "Verificare finală"
@@ -354,7 +465,10 @@ fi
cat <<EOF
De verificat manual:
- shutdown_policy e încă 'freeze' (recomandat să rămână așa)
- datacenter.cfg are încă AMBELE linii: 'ha: shutdown_policy=freeze'
și 'migration: network=10.10.10.0/24,type=insecure'
- dacă lipsesc date recente, compară cu ultima replicare din pvesr status
- dacă a fost prima repornire după 2026-08-29: notează în
docs/switch-2.5g-dedicat-noduri.md dacă insula a urcat singură la boot
EOF

View File

@@ -3,22 +3,31 @@
Instaleaza reatasarea automata a adaptoarelor USB LAN dupa un reset USB.
.DESCRIPTION
Adaptoarele Realtek RTL8156 (driver r8152) folosite ca placa de retea
principala pe pve1 si pveelite se reseteaza singure pe magistrala USB.
Kernelul sterge interfata si o recreeaza, dar nimeni nu o readauga in vmbr0:
in /etc/network/interfaces ea e declarata "inet manual", fara "auto" si fara
"allow-hotplug", deci se ridica doar ca efect secundar, la boot.
Consecinta e un nod care merge perfect si e invizibil in retea, pe termen
nelimitat. Pe pveelite asta a insemnat 16 ore de tacere in noaptea de 27 spre
28 august 2026, plus inca un reset a doua zi la 10:31.
Adaptoarele Realtek RTL8156 (driver r8152) de pe pve1 si pveelite se
reseteaza singure pe magistrala USB. Kernelul sterge interfata si o
recreeaza, dar nimeni nu o reconfigureaza: se ridica doar ca efect secundar,
la boot.
Scriptul instaleaza o regula udev care prinde reaparitia interfetei si o
unitate systemd care reaplica configuratia de retea. Detecteaza singur ce
noduri au adaptor r8152 si le sare pe celelalte - pvemini are placa Intel
igc pe PCIe si nu are nevoie.
unitate systemd care reaplica configuratia de retea (ifreload -a).
Detecteaza singur ce noduri au adaptor r8152 si le sare pe celelalte -
pvemini are placa Intel pe PCIe si nu are nevoie.
CE S-A SCHIMBAT PE 2026-08-29. Cand a fost scris scriptul, dongle-ul USB era
portul puntii vmbr0, adica placa de retea PRINCIPALA - de-aia un reset USB
lasa nodul complet invizibil (16 ore de tacere pe pveelite in noaptea de 27
spre 28 august 2026). Intre timp vmbr0 a fost mutat pe interfata onboard, iar
dongle-ul a ramas doar cu IP-ul retelei dedicate de cluster
(10.10.10.200 / 10.10.10.202), declarat acum "auto ... inet static".
Regula ramane la fel de utila, dar miza s-a schimbat: un reset USB nu mai ia
nodul offline, ci opreste tacut replicarea ZFS si storage-ul NFS
backup-pvemini. In plus, fiind pe ACTION=="add", regula acopera si cazul in
care USB-ul se enumereaza tarziu la boot, dupa networking.service - singurul
risc ramas pentru insula. Neconfirmat inca la o repornire reala.
Context: ../incidents/2026-08-27-pveelite-down.md
../docs/switch-2.5g-dedicat-noduri.md
RULEAZA DE PE STATIA DE ADMIN. SSH intre nodurile Proxmox trece prin
Tailscale si cere autentificare interactiva.
@@ -138,13 +147,28 @@ done
}
Write-Info "adaptoare r8152: $($ifs -join ', ')"
# Interfata conteaza doar daca e chiar portul puntii; altfel regula e inutila.
$port = Invoke-NodeScript -Ip $ip -Script @'
sed -n 's/^[[:space:]]*bridge-ports[[:space:]]*//p' /etc/network/interfaces
# Regula e utila doar daca interfata chiar are ce sa i se reaplice: fie e
# portul puntii (configuratia dinainte de 2026-08-29), fie are propria strofa
# in /etc/network/interfaces (azi: IP-ul retelei dedicate de cluster).
# Fara niciuna, ifreload -a n-ar avea ce configura si regula n-ar folosi la nimic.
$rol = Invoke-NodeScript -Ip $ip -Script @'
port=$(sed -n 's/^[[:space:]]*bridge-ports[[:space:]]*//p' /etc/network/interfaces)
for n in /sys/class/net/*; do
i=${n##*/}
d=$(readlink -f "$n/device/driver" 2>/dev/null)
case "$d" in *r8152) ;; *) continue ;; esac
case " $port " in *" $i "*) echo "$i: port al puntii vmbr0"; continue ;; esac
if grep -qE "^[[:space:]]*(auto|allow-hotplug)[[:space:]]+$i([[:space:]]|$)" /etc/network/interfaces; then
echo "$i: strofa proprie ($(sed -n "s/^[[:space:]]*iface[[:space:]]\+$i[[:space:]]\+//p" /etc/network/interfaces | head -1)), IP: $(ip -4 -o addr show "$i" | awk '{print $4}' | tr '\n' ' ')"
else
echo "$i: NECONFIGURAT in /etc/network/interfaces"
fi
done
'@
Write-Info "bridge-ports vmbr0: $port"
if ($ifs -notcontains $port) {
Write-Warn "adaptorul USB nu e portul puntii - instalez oricum, dar verifica manual"
$rol | ForEach-Object { Write-Info $_ }
if ($rol -match 'NECONFIGURAT') {
Write-Warn "un adaptor r8152 nu e nici port de punte, nici cu strofa proprie -"
Write-Warn "regula se instaleaza, dar pentru el ifreload -a n-are ce aplica"
}
if ($DryRun) {

View File

@@ -0,0 +1,164 @@
<#
.SYNOPSIS
Porneste nodurile Proxmox prin Wake-on-LAN.
.DESCRIPTION
Trimite magic packet catre placile de retea de PRODUCTIE ale nodurilor si
asteapta sa apara pe retea.
De ce merge abia din 2026-08-29: pana atunci pve1 si pveelite aveau IP-ul
principal pe dongle-uri USB Realtek, care nu suporta WoL si oricum pierd
alimentarea cand nodul e oprit. Mutarea lui vmbr0 pe interfetele onboard a
facut WoL posibil - vezi ../docs/switch-2.5g-dedicat-noduri.md.
NU trezi un singur nod si atat, daca ai oprit tot clusterul: un nod pornit
singur nu are quorum, /etc/pve ramane read-only si nu poti porni niciun guest.
Implicit scriptul le trezeste pe toate trei.
RULEAZA DE PE STATIA DE ADMIN, care trebuie sa fie pe acelasi segment L2
(10.0.20.0/24) - magic packet-ul e broadcast si nu trece prin router.
.PARAMETER Node
Trezeste doar nodul dat. Implicit: toate trei.
.PARAMETER Wait
Secunde de asteptat sa apara nodurile (implicit 300).
.PARAMETER NoWait
Trimite pachetele si iese, fara sa astepte.
.EXAMPLE
.\wake-cluster.ps1
.EXAMPLE
.\wake-cluster.ps1 -Node pveelite
#>
[CmdletBinding()]
param(
[ValidateSet('pve1','pvemini','pveelite')]
[string]$Node,
[int]$Wait = 300,
[switch]$NoWait
)
$ErrorActionPreference = 'Stop'
# MAC-ul interfetei de PRODUCTIE (portul puntii vmbr0), nu al dongle-ului de insula.
# Citite de pe noduri pe 2026-08-29 cu: ethtool <if>; cat /sys/class/net/<if>/address
$Nodes = [ordered]@{
pve1 = @{ Ip = '10.0.20.200'; Mac = 'fc:3f:db:0a:0d:d8'; If = 'eno1' }
pvemini = @{ Ip = '10.0.20.201'; Mac = '58:47:ca:7d:51:3c'; If = 'enp90s0' }
pveelite = @{ Ip = '10.0.20.202'; Mac = '84:69:93:57:b2:ea'; If = 'eno1' }
}
function Write-Step { param([string]$Text) Write-Host ""; Write-Host "== $Text" -ForegroundColor Green }
function Write-Ok { param([string]$Text) Write-Host " [ok] $Text" -ForegroundColor Green }
function Write-Warn { param([string]$Text) Write-Host " [!] $Text" -ForegroundColor Yellow }
function Write-Info { param([string]$Text) Write-Host " $Text" -ForegroundColor DarkGray }
function Test-Alive {
param([string]$Address, [int]$TimeoutMs = 1500)
try {
$p = New-Object System.Net.NetworkInformation.Ping
return ($p.Send($Address, $TimeoutMs).Status -eq 'Success')
} catch { return $false }
}
function Send-MagicPacket {
param([string]$Mac)
$bytes = ($Mac -split '[:-]') | ForEach-Object { [byte]([Convert]::ToInt32($_, 16)) }
if ($bytes.Count -ne 6) { throw "MAC invalid: $Mac" }
# Magic packet: 6 octeti 0xFF, apoi MAC-ul repetat de 16 ori.
$packet = New-Object byte[] 102
for ($i = 0; $i -lt 6; $i++) { $packet[$i] = 0xFF }
for ($r = 0; $r -lt 16; $r++) { [Array]::Copy($bytes, 0, $packet, 6 + $r * 6, 6) }
# Trimis si pe broadcast global, si pe cel de subretea: unele stive le trateaza
# diferit, iar pachetul e mic - nu costa nimic sa mearga pe ambele.
# Porturile 9 si 7 sunt cele uzuale; placile asculta oricum dupa continut.
$udp = New-Object System.Net.Sockets.UdpClient
try {
$udp.EnableBroadcast = $true
foreach ($dest in @('255.255.255.255', '10.0.20.255')) {
foreach ($port in @(9, 7)) {
[void]$udp.Send($packet, $packet.Length, $dest, $port)
}
}
} finally {
$udp.Close()
}
}
$targets = if ($Node) { @($Node) } else { $Nodes.Keys }
Write-Step "Trimit magic packet"
$deja = @()
foreach ($n in $targets) {
if (Test-Alive $Nodes[$n].Ip) {
Write-Ok "$n ($($Nodes[$n].Ip)) - deja pornit, nu trimit"
$deja += $n
continue
}
Send-MagicPacket -Mac $Nodes[$n].Mac
Write-Ok "$n -> $($Nodes[$n].Mac) ($($Nodes[$n].If))"
}
$asteptate = @($targets | Where-Object { $deja -notcontains $_ })
if ($asteptate.Count -eq 0) {
Write-Warn "Toate nodurile cerute erau deja pornite."
exit 0
}
if ($NoWait) {
Write-Info "-NoWait: nu astept. Verifica singur cu ping."
exit 0
}
Write-Step "Astept nodurile (max ${Wait}s)"
# Retrimitem periodic: daca placa nu era inca alimentata la primul pachet
# (alimentare tocmai revenita, de exemplu), al doilea o prinde.
$waited = 0
$up = @()
while ($waited -lt $Wait) {
foreach ($n in $asteptate) {
if ($up -contains $n) { continue }
if (Test-Alive $Nodes[$n].Ip) {
$up += $n
Write-Ok "$n a pornit (${waited}s)"
}
}
if ($up.Count -eq $asteptate.Count) { break }
Start-Sleep -Seconds 5
$waited += 5
if ($waited % 60 -eq 0) {
foreach ($n in $asteptate) {
if ($up -notcontains $n) { Send-MagicPacket -Mac $Nodes[$n].Mac }
}
Write-Info "am retrimis pachetele (${waited}s)"
}
}
$lipsa = @($asteptate | Where-Object { $up -notcontains $_ })
Write-Step "Rezultat"
if ($lipsa.Count -eq 0) {
Write-Ok "toate nodurile cerute au pornit"
Write-Host ""
Write-Host " Urmatorul pas: .\cluster-startup.ps1"
Write-Host ""
} else {
Write-Warn "NU au pornit: $($lipsa -join ', ')"
Write-Info "Cauza cea mai probabila e in BIOS, nu in retea:"
Write-Info " - 'Wake on LAN' / 'Power On by PCI-E' dezactivat"
Write-Info " - ErP Ready / Deep Sleep ACTIVAT (taie alimentarea placii in S5)"
Write-Info "Verifica si ca statia de admin e pe acelasi segment L2 (10.0.20.0/24)."
Write-Info "Pe nodurile pornite, starea WoL se vede cu: ethtool <if> | grep Wake-on"
exit 1
}