Miałem dość ręcznego wyłączania laba przy awarii zasilania
Mieszkam w miejscu, gdzie przerwy w zasilaniu zdarzają się kilka razy w roku. Dla homelaba to taki nieplanowany stress test. UPS przejmuje zasilanie, wszystko działa jeszcze przez chwilę i jeśli jestem w domu, mogę spokojnie zamknąć maszyny. Gorzej, jeśli akurat jestem poza domem – w sklepie, w pracy albo w podróży. UPS w końcu się rozładowuje, a maszyny VM dostają twarde odcięcie. Uszkodzone VMDK, niespójne bazy danych, ogólnie taki wtorek, którego wolę nie mieć.
Mam UPS APC z kartą zarządzającą AP9631. Ta karta mówi po SNMP. Skoro i tak wie, co się dzieje – czy jest zasilanie z sieci, jaki jest poziom baterii i ile czasu pracy zostało – to czemu nie miałaby zrobić części roboty za mnie?
Tak powstał esxi-ups-monitor: aplikacja w Go, która odpytuje UPS po SNMP, śledzi stan baterii, etapami wyłącza maszyny VM na ESXi, a po powrocie zasilania uruchamia je z powrotem. Działa w kontenerze Docker, wysyła powiadomienia na Telegramie na każdym etapie i od jakiegoś czasu obsługuje mój lab bez większych dramatów.
Sprzęt
AP9631 to karta zarządzająca APC, często spotykana w modelach Smart-UPS. Udostępnia pełne SNMP MIB z rzeczami, które są tu potrzebne: stan wyjścia, czyli online albo praca na baterii, procent naładowania baterii, szacowany czas pracy, obciążenie wyjścia i kilka innych parametrów.
W jednym zapytaniu SNMP GET odpytuję pięć OID-ów:
| OID | Co zwraca |
|---|---|
.1.3.6.1.4.1.318.1.1.1.4.1.1.0 |
Status wyjścia: 2 = online, 3 = na baterii |
.1.3.6.1.4.1.318.1.1.1.2.2.1.0 |
Poziom baterii, 0-100% |
.1.3.6.1.4.1.318.1.1.1.2.2.3.0 |
Pozostały czas pracy (TimeTicks – trzeba podzielić przez 100, żeby dostać sekundy) |
.1.3.6.1.4.1.318.1.1.1.2.1.1.0 |
Status baterii |
.1.3.6.1.4.1.318.1.1.1.4.2.3.0 |
Procent obciążenia wyjścia |
Najprostszy test połączenia wygląda tak:
snmpwalk -v2c -c public 192.168.1.100 .1.3.6.1.4.1.318.1.1.1.2.1.1.0
Jeśli dostaniesz w odpowiedzi liczbę, jesteś w domu. Jeśli nie, trzeba wrócić do konfiguracji sieci albo SNMP.
Dlaczego Go, dlaczego SNMP, dlaczego SSH
Dlaczego Go? Chciałem mieć pojedynczą binarkę, którą można wrzucić do obrazu Dockera i o niej zapomnieć. Go pasuje tu dobrze: biblioteka standardowa ogarnia SSH, a gosnmp obsługuje część SNMP. Wynikiem jest statyczna binarka około 15 MB działająca w Alpine. Bez interpretera, bez runtime’u, bez zbędnych zależności.
Dlaczego SNMP zamiast panelu WWW? AP9631 ma web UI, które dałoby się scrapować, ale SNMP jest stabilniejsze i mniej inwazyjne. Jeden pakiet UDP, jedna odpowiedź i koniec. Bez parsowania HTML-a, bez sesji, bez logowania do panelu. Do tego zadania SNMP jest po prostu właściwym narzędziem.
Dlaczego SSH bezpośrednio do ESXi zamiast vCenter? Nie używam vCenter. To pojedynczy host ESXi w labie. Nawet gdybym miał vCenter, jego API byłoby tu armatą na muchę. SSH daje bezpośredni dostęp do esxcli vm process kill --type=soft i esxcli system shutdown poweroff. Aplikacja przy pierwszym uruchomieniu generuje klucz SSH i wypisuje klucz publiczny w logach, więc konfiguracja zajmuje około 30 sekund.
Jak to działa
Rdzeniem jest maszyna stanów z czterema stanami: ONLINE, ON_BATTERY, RECOVERING i ESXI_SHUTDOWN. Co 10 sekund aplikacja odpytuje UPS, przekazuje status do maszyny stanów i wykonuje akcję, która z tego wynika.
ONLINE → ON_BATTERY
Gdy UPS zgłasza status wyjścia 3, czyli pracę na baterii, przechodzimy z ONLINE do ON_BATTERY. Startuje licznik czasu. Na Telegram trafia wiadomość z aktualnym stanem baterii.
ON_BATTERY: wykonywanie etapów
Etapy definiujesz w config.yaml. Każdy etap może mieć after_seconds i/lub runtime_remaining_seconds. Wystarczy spełnienie jednego warunku – tego, który nastąpi pierwszy. Przykład:
stages:
- after_seconds: 300
runtime_remaining_seconds: 900
vms:
- plex
- forgejo-runner
- test-server
- after_seconds: 600
runtime_remaining_seconds: 600
vms:
- grafana
- influxdb
- homeassistant
- after_seconds: 900
runtime_remaining_seconds: 420
vms:
- docker-prod
Ostatni etap
Gdy bateria jest już nisko, wchodzi ostatni etap. Aplikacja wyłącza ostatnie maszyny VM, w tym sam monitor, a potem wysyła do ESXi polecenie zamknięcia hosta z opóźnieniem.
final:
after_seconds: 1200
runtime_remaining_seconds: 300
delay_seconds: 90
vms:
- esxi-ups-monitor
Opóźnienie daje kontenerowi czas na spokojne zakończenie pracy, zanim ESXi zacznie się wyłączać.
RECOVERING: zasilanie wraca
Gdy UPS znowu zgłasza zasilanie z sieci, aplikacja przechodzi do stanu RECOVERING. Odzyskiwanie czeka, aż bateria będzie miała wystarczający poziom naładowania i czas pracy:
recovery:
startup_delay_seconds: 120
startup_runtime_remaining_seconds: 1800
ESXI_SHUTDOWN: stan uśpienia
Po wyłączeniu hosta ESXi monitor zostaje w stanie ESXI_SHUTDOWN i czeka na powrót zasilania.
Wzorzec odzyskiwania
Uruchamianie działa według zasady LIFO, czyli last-in-first-out. Maszyny VM wyłączają się w kolejności etapów, a uruchamiają w odwrotnej kolejności. Ostatnia wyłączona maszyna startuje jako pierwsza. Dzięki temu podstawowa infrastruktura wraca dopiero wtedy, gdy zasilanie jest już stabilne.
Dlaczego nie po prostu NUT?
NUT, czyli Network UPS Tools, to solidny projekt. Problemem był u mnie staged shutdown z uwzględnieniem pozostałego czasu pracy. NUT potrafi uruchamiać skrypty po zdarzeniach zasilania, ale jeśli chcesz mieć kilka etapów, progi czasowe i logikę OR, i tak kończysz z własnym skryptem.
Integracja z Telegramem, odzyskiwanie w kolejności LIFO i łagodne wyłączenie hosta ESXi z opóźnieniem to logika dopisana nad surowymi danymi z UPS-a. Ten monitor zastępuje skrypt, który i tak musiałbym napisać nad NUT-em, tylko ma od razu wbudowane odpytywanie SNMP.
Deployment
Uruchomienie zajmuje około pięciu minut. Klonujesz repo, edytujesz config.yaml i odpalasz docker-compose up -d. Przy pierwszym starcie kontener generuje klucz SSH i wypisuje klucz publiczny w logach. Ten klucz trzeba dodać do authorized keys na ESXi, a potem zrestartować kontener.
docker-compose up -d
docker-compose logs | grep "Public key"
# Wklej do authorized keys na ESXi
docker-compose restart
docker-compose.yml montuje konfigurację tylko do odczytu i tworzy katalog ./secrets na klucz SSH. Zmienne Telegrama trafiają przez .env. Gotowe binarki są dostępne na stronie releases dla linux-amd64, arm64, armv7 i armv6.
Rzeczy, na których się potknąłem
Kontener jest jedną z maszyn VM, którymi zarządza. Rozwiązaniem jest opóźnienie w ostatnim etapie. Monitor wyłącza sam siebie, a dopiero potem ESXi dostaje polecenie zamknięcia z opóźnieniem. Dzięki temu kontener ma czas wysłać ostatnie powiadomienie na Telegramie i domknąć połączenia.
SNMP TimeTicks są w setnych częściach sekundy. APC MIB zwraca pozostały czas pracy jako TimeTicks, czyli 1/100 sekundy. 30000 TimeTicks to 300 sekund, czyli 5 minut. Kod dzieli tę wartość przez 100.
Walidacja klucza hosta SSH. Użycie ssh.InsecureIgnoreHostKey() w homelabie jest dla mnie akceptowalne, zwłaszcza gdy klucz hosta zmienia się po reinstalacji ESXi.
ESXi znajduje maszyny VM po world ID, nie po nazwie. Funkcja ListVMs() parsuje wynik esxcli vm process list, żeby znaleźć world ID pasujące do nazw maszyn.
Kod
Projekt jest napisany w Go i leży tutaj: repo.noop.re/drops/esxi-apc. Licencja MIT. Pull requesty mile widziane.
Jeśli masz podobny setup – UPS APC z SNMP, host ESXi i kilka maszyn VM – sklonuj projekt, dopasuj konfigurację i daj znać, czy działa. Albo czy się wywraca. Jedno i drugie jest przydatnym feedbackiem.
