Files
ROMFASTSQL/proxmox/cluster/scripts/cluster-startup.ps1
Marius d7b4007af8 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
2026-08-29 21:13:34 +03:00

536 lines
21 KiB
PowerShell

<#
.SYNOPSIS
Repornirea clusterului Proxmox `romfast` dupa o oprire planificata.
.DESCRIPTION
Asteapta nodurile, asteapta quorumul, porneste guest-urile in ordinea corecta
de dependente (Oracle primul), restaureaza cron-urile de monitorizare.
Porneste DOAR guest-urile care rulau inainte de oprire, citite din fisierul de
stare scris de cluster-shutdown. Fara fisier de stare, foloseste lista
implicita, minus guest-urile din NeverAutostart.
RULEAZA DE PE STATIA DE ADMIN, nu de pe un nod.
NODURILE TREBUIE PORNITE FIZIC INTAI. Scriptul le asteapta.
Porneste-le pe TOATE TREI aproximativ simultan: un singur nod pornit nu are
quorum, /etc/pve ramane read-only si nu se poate porni niciun guest.
.PARAMETER Yes
Nu cere confirmari.
.PARAMETER DryRun
Arata ce ar face, fara sa execute nimic care modifica starea.
.PARAMETER All
Ignora fisierul de stare, foloseste lista implicita.
.EXAMPLE
.\cluster-startup.ps1 -DryRun
.EXAMPLE
.\cluster-startup.ps1
#>
[CmdletBinding()]
param(
[switch]$Yes,
[switch]$DryRun,
[switch]$All
)
$ErrorActionPreference = 'Stop'
# ---------------------------------------------------------------- configurare
$NodeIp = [ordered]@{
pve1 = '10.0.20.200'
pvemini = '10.0.20.201'
pveelite = '10.0.20.202'
}
# Ordinea de PORNIRE: baza de date intai, consumatorii dupa.
# Inversul ordinii de oprire din cluster-shutdown.
$GuestStartOrder = @(
108, # central-oracle - PRIMUL, restul depind de el
201, # roacentral - IIS reverse proxy, consumator Oracle
104, # flowise - consumator Oracle
100, # portainer
102, # docker.romfast.ro
103, # dokploy
106, # gitea
171, # claude-agent
101, # minecraft
110, # moltbot
303, # Win11-Adina
304, # Win11-Marius
302, # oracle-test-302
109, # oracle-dr-windows
301, # docker-portainer-template
310 # Win11-Template
)
# Guest-uri care NU se pornesc automat cand LIPSESTE fisierul de stare.
# Daca fisierul de stare exista, el decide - daca erau pornite, se repornesc.
# 109 - VM DR. Pornirea lui nedorita a declansat bucla OOM din incidentul
# 2026-04-20; in mod normal sta oprit si e pornit doar de testul DR.
# 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'
# 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
$GuestWait = 300
$CronBackup = '/root/crontab.backup-mentenanta.txt'
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$StateFile = if ($env:CLUSTER_STATE_FILE) { $env:CLUSTER_STATE_FILE } else { Join-Path $ScriptDir '.cluster-state.txt' }
# ------------------------------------------------------------------- utilitare
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-Log { param([string]$Text) Write-Host "[$(Get-Date -Format HH:mm:ss)] $Text" -ForegroundColor DarkGray }
function Stop-WithError { param([string]$Text) Write-Host " [X] $Text" -ForegroundColor Red; exit 1 }
function Test-Alive {
param([string]$Address, [int]$TimeoutMs = 2000)
try {
$p = New-Object System.Net.NetworkInformation.Ping
return ($p.Send($Address, $TimeoutMs).Status -eq 'Success')
} catch {
return $false
}
}
function Invoke-Node {
param([string]$Node, [string]$Command)
$out = ssh -o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new -o LogLevel=ERROR "root@$($NodeIp[$Node])" $Command
return $out
}
function Invoke-NodeChange {
param([string]$Node, [string]$Command)
if ($DryRun) {
Write-Host " [dry-run] ${Node}: $Command" -ForegroundColor DarkGray
return @()
}
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 }
Write-Host ""
Write-Host $Message -ForegroundColor Yellow
$reply = Read-Host "Scrie DA ca sa continui"
if ($reply -ne 'DA') { Stop-WithError "Anulat de utilizator." }
}
function Get-NodeGuests {
param([string]$Node)
$result = @()
foreach ($line in ((Invoke-Node -Node $Node -Command 'pct list') | Select-Object -Skip 1)) {
$f = ($line -split '\s+') | Where-Object { $_ -ne '' }
if ($f.Count -ge 2) {
$result += [pscustomobject]@{ Type='ct'; Node=$Node; Id=[int]$f[0]; Status=$f[1] }
}
}
foreach ($line in ((Invoke-Node -Node $Node -Command 'qm list') | Select-Object -Skip 1)) {
$f = ($line -split '\s+') | Where-Object { $_ -ne '' }
if ($f.Count -ge 3) {
$result += [pscustomobject]@{ Type='vm'; Node=$Node; Id=[int]$f[0]; Status=$f[2] }
}
}
return $result
}
function Get-GuestStatus {
param([string]$Node, [string]$Type, [int]$Id)
$cmd = if ($Type -eq 'ct') { "pct status $Id" } else { "qm status $Id" }
$out = Invoke-Node -Node $Node -Command $cmd
if ($out -match 'status:\s*(\w+)') { return $Matches[1] }
return 'unknown'
}
# ------------------------------------------------- pas 1: asteapta nodurile
Write-Step "Astept nodurile"
Write-Warn "Nodurile trebuie pornite FIZIC. Porneste-le pe toate trei aproximativ simultan."
$waited = 0
$missing = @()
while ($waited -lt $NodeWait) {
$missing = @()
foreach ($node in $NodeIp.Keys) {
if (-not (Test-Alive $NodeIp[$node])) { $missing += $node }
}
if ($missing.Count -eq 0) { break }
Write-Host "`r astept: $($missing -join ', ') (${waited}s) " -NoNewline -ForegroundColor DarkGray
Start-Sleep -Seconds 10
$waited += 10
}
Write-Host "`r `r" -NoNewline
if ($missing.Count -ne 0) { Stop-WithError "Nodurile $($missing -join ', ') nu au aparut in ${NodeWait}s." }
foreach ($node in $NodeIp.Keys) {
$w = 0
while ($true) {
$h = Invoke-Node -Node $node -Command 'uptime -p'
if ($LASTEXITCODE -eq 0 -and $h) { break }
$w += 10
if ($w -gt 300) { Stop-WithError "$node raspunde la ping dar nu la SSH." }
Start-Sleep -Seconds 10
}
Write-Ok "$node - up, SSH OK ($h)"
}
# --------------------------------------------------- pas 2: asteapta quorum
Write-Step "Astept quorumul"
$waited = 0
while ($true) {
$q = Invoke-Node -Node 'pvemini' -Command 'pvecm status'
if (($q -join "`n") -match 'Quorate:\s*Yes') { break }
if ($waited -ge $QuorumWait) { Stop-WithError "Clusterul nu a atins quorumul in ${QuorumWait}s." }
Write-Host "`r astept quorum (${waited}s)... " -NoNewline -ForegroundColor DarkGray
Start-Sleep -Seconds 10
$waited += 10
}
Write-Host "`r `r" -NoNewline
$votes = 0
if (($q -join "`n") -match 'Total votes:\s*(\d+)') { $votes = [int]$Matches[1] }
if ($votes -ne 3) { Write-Warn "Quorum atins, dar doar $votes/3 voturi. Un nod lipseste din cluster." }
Write-Ok "quorum OK, $votes/3 voturi"
Invoke-Node -Node 'pvemini' -Command 'ha-manager status' | Select-Object -First 4 | Write-Host
# ------------------------------------ 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"
$toStart = @()
if ((-not $All) -and (Test-Path $StateFile)) {
foreach ($line in (Get-Content $StateFile)) {
if ($line -match '^\s*#' -or $line -match '^\s*$') { continue }
$f = ($line -split '\s+') | Where-Object { $_ -ne '' }
if ($f.Count -ge 5) {
$toStart += [pscustomobject]@{ Type=$f[0]; Node=$f[1]; Id=[int]$f[2]; Status=$f[3]; Ha=$f[4] }
}
}
Write-Ok "citit din $StateFile ($($toStart.Count) guest-uri)"
} else {
if (-not $All) { Write-Warn "Nu exista $StateFile - folosesc lista implicita." }
$haSids = @()
foreach ($line in (Invoke-Node -Node 'pvemini' -Command 'ha-manager config')) {
if ($line -match '^(ct|vm):(\d+)') { $haSids += "$($Matches[1]):$($Matches[2])" }
}
$allGuests = @()
foreach ($node in $NodeIp.Keys) {
foreach ($g in (Get-NodeGuests -Node $node)) {
$sid = "$($g.Type):$($g.Id)"
$g | Add-Member -NotePropertyName Ha -NotePropertyValue $(if ($haSids -contains $sid) { 'ha' } else { 'noha' })
$allGuests += $g
}
}
# Fara fisier de stare pornim doar ce e explicit in lista de ordine SI nu e
# in NeverAutostart. Altfel am porni DR-ul si template-urile, care erau
# oprite intentionat.
foreach ($id in $GuestStartOrder) {
if ($NeverAutostart -contains $id) {
Write-Warn "$id sarit (NeverAutostart) - porneste-l manual daca chiar il vrei"
continue
}
$g = $allGuests | Where-Object { $_.Id -eq $id } | Select-Object -First 1
if ($g) { $toStart += $g }
}
}
if ($toStart.Count -eq 0) { Stop-WithError "Nimic de pornit." }
foreach ($g in $toStart) {
" {0,-3} {1,-9} {2,-4} {3}" -f $g.Type, $g.Node, $g.Id, $g.Ha | Write-Host
}
Confirm-Step "Se pornesc guest-urile de mai sus, Oracle primul."
# ------------------------------------------------- pas 5: pornire guest-uri
Write-Step "Pornire guest-uri"
$failed = @()
function Start-Guest {
param([pscustomobject]$Guest)
$sid = "$($Guest.Type):$($Guest.Id)"
$cmd = if ($Guest.Type -eq 'ct') { 'pct' } else { 'qm' }
if ((Get-GuestStatus -Node $Guest.Node -Type $Guest.Type -Id $Guest.Id) -eq 'running') {
Write-Ok "$sid deja pornit"
return $true
}
if ($Guest.Ha -eq 'ha') {
# Pentru resursele HA, --state started repune resursa sub controlul CRM.
Invoke-NodeChange -Node $Guest.Node -Command "ha-manager set $sid --state started" | Out-Null
} else {
Invoke-NodeChange -Node $Guest.Node -Command "$cmd start $($Guest.Id)" | Out-Null
}
if ($DryRun) { return $true }
$waited = 0
while ($waited -lt $GuestWait) {
if ((Get-GuestStatus -Node $Guest.Node -Type $Guest.Type -Id $Guest.Id) -eq 'running') {
Write-Ok "$sid pornit (${waited}s)"
return $true
}
Start-Sleep -Seconds 5
$waited += 5
}
Write-Warn "$sid nu a pornit in ${GuestWait}s - verifica manual"
return $false
}
function Wait-Oracle {
param([string]$Node)
Write-Log "astept ca instanta Oracle sa accepte interogari..."
if ($DryRun) { return }
# '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-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
}
Write-Host "`r Oracle inca nu raspunde (${waited}s)... " -NoNewline -ForegroundColor DarkGray
Start-Sleep -Seconds 15
$waited += 15
}
Write-Host "`r `r" -NoNewline
Write-Warn "Oracle nu a raspuns in ${OracleWait}s. Continui, dar VERIFICA MANUAL."
Write-Warn " ssh root@$($NodeIp[$Node]) `"pct exec $OracleCt -- docker logs --tail 50 $OracleDocker`""
}
foreach ($id in $GuestStartOrder) {
$g = $toStart | Where-Object { $_.Id -eq $id } | Select-Object -First 1
if (-not $g) { continue }
Write-Log "pornesc $($g.Type):$($g.Id) pe $($g.Node) ($($g.Ha))"
if (-not (Start-Guest -Guest $g)) { $failed += "$($g.Type):$($g.Id)" }
# Oracle e blocant: nimic care depinde de el nu porneste pana nu e deschis.
if ($g.Id -eq $OracleCt) { Wait-Oracle -Node $g.Node }
}
# orice a mai ramas in lista si nu era in ordinea definita
foreach ($g in $toStart) {
if ($GuestStartOrder -contains $g.Id) { continue }
Write-Warn "guest neplanificat: $($g.Type):$($g.Id) pe $($g.Node) - il pornesc"
if (-not (Start-Guest -Guest $g)) { $failed += "$($g.Type):$($g.Id)" }
}
# ------------------------------------------------ pas 6: restaurare cron-uri
Write-Step "Restaurare cron-uri de monitorizare"
foreach ($node in $NodeIp.Keys) {
$exists = Invoke-Node -Node $node -Command "test -f $CronBackup; echo `$?"
if (($exists -join '') -notmatch '^0') {
Write-Warn "$node - nu exista $CronBackup, sar peste"
continue
}
Invoke-NodeChange -Node $node -Command "crontab $CronBackup" | Out-Null
if (-not $DryRun) {
$left = Invoke-Node -Node $node -Command "crontab -l | grep -c '^#MENTENANTA' || true"
if (($left -join '').Trim() -eq '0') {
Write-Ok "$node - cron-uri restaurate"
} else {
Write-Warn "$node - au ramas $left linii #MENTENANTA, verifica manual"
}
}
}
# ------------------------------------------------------- pas 7: verificare
Write-Step "Verificare finala"
if ($DryRun) {
Write-Warn "dry-run - sar peste verificare"
} else {
Invoke-Node -Node 'pvemini' -Command 'ha-manager status' | Write-Host
Write-Host ""
$rep = Invoke-Node -Node 'pvemini' -Command 'pvesr status'
$rep | Write-Host
Write-Host ""
$badRep = @($rep | Select-Object -Skip 1 | Where-Object { $_ -notmatch 'OK\s*$' -and $_.Trim() -ne '' })
if ($badRep.Count -eq 0) {
Write-Ok "replicare: toate job-urile OK"
} else {
Write-Warn "job-uri de replicare cu probleme:"
$badRep | Write-Host
}
}
Write-Step "Cluster pornit"
if ($failed.Count -gt 0) {
Write-Warn "Guest-uri care NU au pornit: $($failed -join ', ')"
Write-Warn "Verifica-le manual inainte de a considera repornirea terminata."
} else {
Write-Ok "toate guest-urile au pornit"
}
Write-Host ""
Write-Host " De verificat manual:"
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 ""