Files
ROMFASTSQL/proxmox/cluster/scripts/deploy-usb-lan-hotplug.ps1
Marius 8bdcb48291 feat(cluster): reatasare automata a adaptoarelor USB LAN dupa reset USB
Adaptoarele Realtek RTL8156 (r8152) sunt placa de retea principala pe DOUA
noduri, nu doar pe pveelite: pve1 are enx6c1ff759e2cb ca bridge-port, exact
aceeasi configuratie si aceeasi vulnerabilitate. pvemini e in regula, are Intel
igc pe PCIe.

Defectul nu e resetul USB in sine, ci ca interfata recreata nu mai ajunge inapoi
in vmbr0: e declarata "inet manual", fara auto si fara allow-hotplug, deci se
ridica doar ca efect secundar la boot. Nodul ramane pornit si invizibil, la
nesfarsit - 16 ore pe pveelite in noaptea asta, plus inca un reset azi la 10:31,
in timp ce lucram la asta.

Regula udev prinde reaparitia interfetei si porneste o unitate systemd care da
ifreload -a. Am ales ifreload, nu ifup, din doua motive: interfata e port de
punte si doar ifreload reface legatura cu vmbr0, si e comanda verificata in teren
- exact ea a readus pveelite in retea de doua ori azi.

Doua capcane platite, amandoua consemnate in fisiere ca sa nu se reia:

1. Prefixul 70- NU functioneaza. ID_NET_DRIVER e populat abia de
   80-net-setup-link.rules, deci la 70- variabila e goala si regula nu se
   potriveste - tacut, fara nicio eroare. udevadm test citea fisierul, dar RUN-ul
   nu aparea in lista finala. Prima rulare a testului a "reusit" doar pentru ca
   a lucrat plasa de siguranta la 90 s, nu regula. Cu 99-, revenirea e in 8
   secunde.

2. PowerShell inghite ghilimelele cand paseaza argumente catre ssh, iar nodul
   primea [ = r8152 ] in loc de [ "$d" = r8152 ]. Scripturile remote se trimit
   acum codificate base64, nu prin niveluri de citare.

Testat cu reset USB real pe pveelite (deautorizare/reautorizare pe magistrala,
adica exact ce face controllerul cand cedeaza singur): revenire in 8 secunde,
unitatea usb-lan-hotplug@eth0.service pornita si terminata cu succes. Instanta e
eth0 pentru ca evenimentul add precede redenumirea in enxMAC - unitatea ignora
%I si reaplica toata configuratia, deci nu conteaza.

8 secunde e sub tokenul corosync de 10 s, deci un reset USB nu ar mai trebui nici
macar sa scoata nodul din cluster.

Testul isi armeaza singur o plasa de siguranta - ifreload -a programat la 90 s -
ca o regula gresita sa nu ceara drum pana la nod. A si folosit, la prima rulare.
Pe pve1 am instalat fara testul distructiv, acolo ruleaza CT 101 si CT 110;
verificat neinvaziv cu udevadm test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAEUqrv26jCM5KwAPtkHb8
2026-08-28 11:01:37 +03:00

245 lines
9.6 KiB
PowerShell

<#
.SYNOPSIS
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.
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.
Context: ../incidents/2026-08-27-pveelite-down.md
RULEAZA DE PE STATIA DE ADMIN. SSH intre nodurile Proxmox trece prin
Tailscale si cere autentificare interactiva.
.PARAMETER Node
Instaleaza doar pe nodul dat (pve1 / pvemini / pveelite). Implicit: toate.
.PARAMETER DryRun
Arata ce ar face, fara sa modifice nimic.
.PARAMETER Test
Dupa instalare, forteaza un reset USB real ca sa verifice ca regula chiar
functioneaza. PERICULOS FARA PLASA - vezi nota de mai jos. Scriptul isi pune
singur o plasa de siguranta: un "ifreload -a" programat peste 90 de secunde,
care repara nodul chiar daca regula nu s-a declansat. Nu folosi pe pvemini.
.EXAMPLE
.\deploy-usb-lan-hotplug.ps1 -DryRun
.EXAMPLE
.\deploy-usb-lan-hotplug.ps1
.EXAMPLE
.\deploy-usb-lan-hotplug.ps1 -Node pveelite -Test
#>
[CmdletBinding()]
param(
[ValidateSet('pve1','pvemini','pveelite')]
[string]$Node,
[switch]$DryRun,
[switch]$Test
)
$ErrorActionPreference = 'Stop'
$NodeIp = [ordered]@{
pve1 = '10.0.20.200'
pvemini = '10.0.20.201'
pveelite = '10.0.20.202'
}
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$CfgDir = Join-Path (Split-Path -Parent $ScriptDir) 'config\usb-lan-hotplug'
$RuleSrc = Join-Path $CfgDir '99-usb-lan-hotplug.rules'
$UnitSrc = Join-Path $CfgDir 'usb-lan-hotplug@.service'
$RuleDst = '/etc/udev/rules.d/99-usb-lan-hotplug.rules'
$UnitDst = '/etc/systemd/system/usb-lan-hotplug@.service'
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-Bad { param([string]$Text) Write-Host " [X] $Text" -ForegroundColor Red }
function Write-Info { param([string]$Text) Write-Host " $Text" -ForegroundColor DarkGray }
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]$Ip, [string]$Command, [int]$Timeout = 15)
return ssh -o BatchMode=yes -o ConnectTimeout=$Timeout `
-o StrictHostKeyChecking=accept-new -o LogLevel=ERROR `
"root@$Ip" $Command
}
function Invoke-NodeScript {
# PowerShell inghite ghilimelele cand paseaza argumente catre ssh, iar shell-ul
# de pe nod primeste [ = r8152 ] in loc de [ "$d" = r8152 ]. In loc sa ne luptam
# cu nivelurile de citare, trimitem scriptul codificat si il decodam acolo.
param([string]$Ip, [string]$Script, [int]$Timeout = 20)
$lf = $Script -replace "`r`n", "`n"
$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($lf))
return Invoke-Node -Ip $Ip -Command "echo $b64 | base64 -d | sh" -Timeout $Timeout
}
foreach ($f in @($RuleSrc, $UnitSrc)) {
if (-not (Test-Path $f)) { Write-Bad "lipseste $f"; exit 1 }
# Un CRLF intr-o regula udev o face sa nu se potriveasca niciodata, tacut.
# .gitattributes forteaza LF pe directorul asta, dar verificam oricum.
if ([IO.File]::ReadAllText($f) -match "`r") {
Write-Bad "$f are CRLF - trebuie LF. Verifica .gitattributes."
exit 1
}
}
Write-Host ""
Write-Host "Hotplug pentru adaptoarele USB LAN (r8152)" -ForegroundColor Cyan
Write-Info "sursa: $CfgDir"
$tinte = if ($Node) { @($Node) } else { $NodeIp.Keys }
$instalate = @()
foreach ($n in $tinte) {
$ip = $NodeIp[$n]
Write-Step "$n ($ip)"
if (-not (Test-Alive $ip)) {
Write-Warn "nu raspunde la ping - sarit. Reia cu -Node $n cand revine."
continue
}
# Nodurile fara adaptor USB nu au nevoie de regula. pvemini are Intel igc.
$ifs = Invoke-NodeScript -Ip $ip -Script @'
for n in /sys/class/net/*; do
i=${n##*/}
d=$(readlink -f "$n/device/driver" 2>/dev/null)
case "$d" in *r8152) echo "$i" ;; esac
done
'@
if (-not $ifs) {
Write-Ok "nu are adaptor USB r8152 - nu are nevoie de regula, sarit"
continue
}
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
'@
Write-Info "bridge-ports vmbr0: $port"
if ($ifs -notcontains $port) {
Write-Warn "adaptorul USB nu e portul puntii - instalez oricum, dar verifica manual"
}
if ($DryRun) {
Write-Info "[dry-run] scp $RuleSrc -> ${ip}:$RuleDst"
Write-Info "[dry-run] scp $UnitSrc -> ${ip}:$UnitDst"
Write-Info "[dry-run] udevadm control --reload && systemctl daemon-reload"
continue
}
scp -o BatchMode=yes -o LogLevel=ERROR $RuleSrc "root@${ip}:$RuleDst" | Out-Null
scp -o BatchMode=yes -o LogLevel=ERROR $UnitSrc "root@${ip}:$UnitDst" | Out-Null
Invoke-Node -Ip $ip -Command "udevadm control --reload-rules; systemctl daemon-reload" | Out-Null
# Verificare reala: fisierele exista, unitatea e vazuta de systemd si regula
# e sintactic valida. udevadm test nu modifica nimic.
# Backtick-ul escapeaza variabilele de shell, ca sa nu le interpoleze PowerShell.
$ver = Invoke-NodeScript -Ip $ip -Script @"
if [ -f $RuleDst ]; then echo 'regula: prezenta'; else echo 'regula: LIPSA'; fi
if [ -f $UnitDst ]; then echo 'unitate: prezenta'; else echo 'unitate: LIPSA'; fi
cr=`$(tr -d '\n' < $RuleDst | tr -d '[:print:]' | wc -c)
echo "neprintabile in regula: `$cr"
if systemctl cat usb-lan-hotplug@test.service >/dev/null 2>&1; then echo 'systemd: vede unitatea'; else echo 'systemd: NU vede unitatea'; fi
"@
$ver | ForEach-Object { Write-Info $_ }
if ($ver -match 'LIPSA' -or $ver -match 'neprintabile in regula: [1-9]' -or $ver -match 'NU vede') {
Write-Bad "instalare incompleta pe $n"
} else {
Write-Ok "instalat"
$instalate += $n
}
}
# ------------------------------------------------------- testul cu reset real
if ($Test -and -not $DryRun) {
if (-not $Node) { Write-Bad "-Test cere -Node explicit"; exit 1 }
if ($Node -eq 'pvemini') { Write-Bad "refuz -Test pe pvemini: acolo ruleaza productia"; exit 1 }
if ($instalate -notcontains $Node) { Write-Bad "$Node nu a fost instalat cu succes - nu testez"; exit 1 }
$ip = $NodeIp[$Node]
Write-Step "Test cu reset USB real pe $Node"
Write-Warn "Nodul isi pierde reteaua pentru cateva secunde."
Write-Info "Plasa de siguranta: 'ifreload -a' programat peste 90 s, care repara"
Write-Info "nodul si daca regula nu se declanseaza - deci fara drum pana la el."
$usbPath = Invoke-NodeScript -Ip $ip -Script @'
for d in /sys/bus/usb/devices/*; do
[ -e "$d/idVendor" ] || continue
[ "$(cat "$d/idVendor")" = "0bda" ] || continue
ls "$d"/*/net >/dev/null 2>&1 || continue
echo "${d##*/}"
break
done
'@
if (-not $usbPath) { Write-Bad "nu gasesc dispozitivul USB Realtek"; exit 1 }
Write-Info "dispozitiv USB: $usbPath"
# Plasa se armeaza INAINTE de reset, cu nohup, ca sa supravietuiasca inchiderii
# sesiunii SSH odata cu caderea retelei.
Invoke-NodeScript -Ip $ip -Script @'
nohup sh -c 'sleep 90; /usr/sbin/ifreload -a' >/dev/null 2>&1 &
'@ | Out-Null
Write-Ok "plasa de siguranta armata (90 s)"
# Reset real: dezautorizeaza si reautorizeaza dispozitivul, exact ce face
# controllerul xHCI cand cedeaza singur.
Invoke-NodeScript -Ip $ip -Script @"
nohup sh -c 'echo 0 > /sys/bus/usb/devices/$usbPath/authorized; sleep 2; echo 1 > /sys/bus/usb/devices/$usbPath/authorized' >/dev/null 2>&1 &
"@ | Out-Null
Write-Info "reset trimis, astept revenirea..."
$revenit = $false
foreach ($i in 1..30) {
Start-Sleep -Seconds 4
if (Test-Alive $ip) { $revenit = $true; break }
}
if ($revenit) {
Write-Ok "nodul a revenit dupa ~$($i * 4) s"
if ($i * 4 -lt 60) {
Write-Ok "sub 60 s => regula udev a lucrat, nu plasa de siguranta"
} else {
Write-Warn "peste 60 s => posibil sa fi lucrat plasa. Verifica jurnalul:"
Write-Info " ssh root@$ip 'journalctl -u usb-lan-hotplug@* --since \"-5 min\"'"
}
$j = Invoke-Node -Ip $ip -Command "journalctl -u 'usb-lan-hotplug@*' --since '-5 min' --no-pager | tail -12"
$j | ForEach-Object { Write-Info $_ }
} else {
Write-Bad "nodul NU a revenit in 2 minute - verifica la consola fizica"
exit 1
}
}
Write-Host ""
if ($instalate) { Write-Ok "instalat pe: $($instalate -join ', ')" }
else { Write-Warn "niciun nod modificat" }