<# .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" }