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
This commit is contained in:
Marius
2026-08-28 11:01:37 +03:00
parent 5e31d975df
commit 8bdcb48291
4 changed files with 297 additions and 0 deletions

5
.gitattributes vendored
View File

@@ -13,3 +13,8 @@ proxmox/cluster/ups/config/battery-install-date text eol=lf
*.ps1 text eol=crlf
*.bat text eol=crlf
*.cmd text eol=crlf
# Regula udev și unitatea systemd pentru hotplug-ul adaptoarelor USB LAN ajung în
# /etc/udev/rules.d și /etc/systemd/system pe noduri. Un CRLF într-o regulă udev
# o face să nu se mai potrivească niciodată, tăcut.
proxmox/cluster/config/usb-lan-hotplug/* text eol=lf

View File

@@ -0,0 +1,22 @@
# Reatasarea adaptoarelor USB LAN dupa un reset pe magistrala USB.
#
# De ce exista: adaptoarele Realtek RTL8156 (driver r8152) folosite ca placa de
# retea principala pe pve1 si pveelite se reseteaza singure pe xHCI. Kernelul
# sterge interfata si o recreeaza, dar nimeni NU o readauga in vmbr0 si nu o
# ridica - interfata e declarata "inet manual", fara "auto" si fara
# "allow-hotplug", deci se ridica doar ca efect secundar la boot.
#
# Rezultatul: nodul 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 - vezi ../../incidents/2026-08-27-pveelite-down.md
#
# Regula prinde momentul in care interfata reapare si reaplica configuratia.
# Se declanseaza si la boot; e inofensiv, ifreload -a e idempotent.
ACTION=="add", SUBSYSTEM=="net", ENV{ID_NET_DRIVER}=="r8152", RUN+="/usr/bin/systemctl --no-block start usb-lan-hotplug@$env{INTERFACE}.service"
# De ce prefixul 99 si nu 70: ID_NET_DRIVER e populat abia de
# /usr/lib/udev/rules.d/80-net-setup-link.rules. La 70- variabila e inca goala,
# regula nu se potriveste si nu se intampla nimic - tacut. Verificat pe pveelite
# la 2026-08-28: cu 70-, "udevadm test" citea fisierul dar RUN-ul nu aparea in
# lista finala de comenzi.

View File

@@ -0,0 +1,26 @@
[Unit]
Description=Reataseaza adaptorul USB LAN %I in punte dupa un reset USB
Documentation=file:///etc/udev/rules.d/70-usb-lan-hotplug.rules
After=network.target
[Service]
Type=oneshot
# Dispozitivul tocmai a fost re-enumerat: driverul r8152 se incarca, interfata e
# redenumita din ethN in enxMAC, iar linkul negociaza. Reaplicarea configuratiei
# inainte ca astea sa se aseze prinde interfata intr-o stare intermediara.
ExecStartPre=/bin/sleep 3
# ifreload -a, nu ifup: interfata e "inet manual" si e port de punte, iar doar
# ifreload reface legatura cu vmbr0. E si comanda verificata in teren - exact ea
# a readus pveelite in retea pe 28.08 la 09:49, de la consola fizica.
# Aplica numai diferentele fata de /etc/network/interfaces, deci nu atinge
# interfetele tap/veth ale guest-urilor.
ExecStart=/usr/sbin/ifreload -a
# Reteaua e deja rupta cand ajungem aici; o incercare esuata nu trebuie sa lase
# unitatea in "failed" si sa blocheze urmatoarea reaparitie a interfetei.
SuccessExitStatus=0 1
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,244 @@
<#
.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" }