rebjak.com
RSS English
← Blog

PPPoE sessions z Cisco ASR1000 v LibreNMS: vlastný v2 plugin

LibreNMS o PPPoE sessions na Cisco ASR1000 nevie nič. Postavil som v2 plugin nad CISCO-PPPOE-MIB: prehľad všetkých BRASov, rozpad PTA/FWDED/TRANS a zoznam subscriberov.

Cieľ bol jednoduchý: vidieť všetkých päť BRASov a počet PPPoE sessions na jednom mieste.

LibreNMS mi z ASR1000 dá CPU, pamäť, prevádzku na portoch, teploty. O sessions ani slovo. Pritom je to presne to číslo, ktoré ako admin potrebujem: koľko sessions visí na ktorom BRASe. Dovtedy to bolo SSH a show pppoe summary na piatich zariadeniach:

bras6#show pppoe summary
    PTA  : Locally terminated sessions
    FWDED: Forwarded sessions
    TRANS: All other sessions (in transient state)

                                TOTAL     PTA   FWDED   TRANS
TOTAL                             240     240       0       0
TenGigabitEthernet1/0/0            90      90       0       0
TenGigabitEthernet1/1/0           150     150       0       0

Fajn raz za čas, nie na trend a už vôbec nie na alert.

Nakoniec z toho bol vlastný v2 plugin pre LibreNMS. Takto to vyzerá dnes:

Prehľad všetkých BRASov s počtom PPPoE sessions v LibreNMS
Všetky BRASy na jednej stránke: aktívne sessions, rozpad PTA/FWDED/TRANS a využitie voči limitu

Ako som sa k tomu dostal a kde som po ceste narazil:

Najprv Custom OID, lebo to je päť minút

LibreNMS má Custom OID priamo v editácii zariadenia: zadáš OID, dátový typ, jednotku, a poller ti to začne písať do RRD a grafovať. Ak ti stačí jedno číslo, nemusíš písať nič.

Pre celkový počet sessions to je:

PoleHodnota
OID.1.3.6.1.4.1.9.9.194.1.1.1.0
Data TypeGAUGE
Unitsessions

To je cPppoeSystemCurrSessions z CISCO-PPPOE-MIB. Tá .0 na konci je povinná, ide o skalár. Bez nej ti Test OID zlyhá.

Toto som spravil ako prvé a hneď to fungovalo. Mal som graf počtu sessions na BRAS. Lenže:

  • rozpad PTA / FWDED / TRANS je v MIBe len per interface, čo by znamenalo jeden Custom OID na každý ifIndex, a tých mám na jednom ASR cez 4 000,
  • nevidím, kto je pripojený, keď hľadám konkrétneho zákazníka, číslo mi nepomôže,
  • a hlavne to nie je prehľad, musím preklikať päť zariadení, aby som vedel, ako na tom sieť je celkovo.

Takže Custom OID áno, na históriu je ideálny. Ale na “čo sa deje teraz” som potreboval niečo iné.

Fáza nula: nehádať OIDs

Toto považujem za najdôležitejšiu časť a zároveň za tú, ktorú je najväčšie pokušenie preskočiť.

LibreNMS nedodáva ani CISCO-PPPOE-MIB, ani CISCO-SUBSCRIBER-SESSION-MIB, v mibs/cisco/ ich jednoducho nemá. Takže som si ich stiahol z oficiálneho Cisco repozitára na GitHube a nechal si numerické OIDs vygenerovať:

curl -O https://raw.githubusercontent.com/cisco/cisco-mibs/main/v2/CISCO-PPPOE-MIB.my
snmptranslate -M +mibs -m CISCO-PPPOE-MIB -On CISCO-PPPOE-MIB::cPppoeSystemCurrSessions
# .1.3.6.1.4.1.9.9.194.1.1.1

A potom to isté overiť snmpwalk-om proti živému ASR. Až keď mi zariadenie odpovedalo, začal som písať kód.

Prečo takto: plugin sa zariadenia pýta výhradne numerickými OIDmi. Keby sa spoliehal na MIB súbory v runtime, musel by som ich distribuovať do kontajnera a riešiť mibDir. Takto sú MIB súbory v repozitári len referencia pre človeka, ktorý sa pýta „a čo to číslo vlastne znamená”.

Čo v tých MIBoch je

CISCO-PPPOE-MIB (1.3.6.1.4.1.9.9.194) dáva na úrovni celého zariadenia:

OIDObjektČo vracia
...194.1.1.1.0cPppoeSystemCurrSessionsaktívne sessions
...194.1.1.2.0cPppoeSystemHighWaterSessionshistorické maximum
...194.1.1.3.0cPppoeSystemMaxAllowedSessionslimit
...194.1.1.5.0cPppoeSystemExceededSessionErrorsodmietnuté kvôli limitu

A tabuľku cPppoeSessionsPerInterfaceTable indexovanú cez ifIndex, kde je pre každý interface total, PTA, FWDED, TRANS a prahy pre trapy.

Tie tri skratky stoja za vysvetlenie, lebo v MIBe sú bez kontextu:

  • PTA: PPP Termination Aggregation, session sa terminuje na tomto BRASe a prevádzka ide lokálne do IP
  • FWDED: session sa tu neterminuje, posiela sa ďalej, typicky L2TP tunelom na LNS
  • TRANS: transient, session sa ešte dojednáva

CISCO-SUBSCRIBER-SESSION-MIB (...786) ide o úroveň nižšie: jeden riadok na subscribera, s username, stavom, IP adresou. Tam je háčik, ku ktorému sa dostanem.

Prečo plugin a nie polling modul

Zvažoval som pridať polling modul priamo do LibreNMS core (LibreNMS/OS/Iosxe.php). Nakoniec plugin, z dvoch dôvodov: nechcem si udržiavať fork LibreNMS a pri každom upgrade riešiť konflikty, a rozpad per interface aj tak nemá kam ukladať bez vlastnej schémy.

LibreNMS v2 plugin je prekvapivo jednoduchá vec. Naklonuješ adresár do app/Plugins/NazovPluginu/ a implementuješ hooky:

DeviceOverview.php      panel na stránke zariadenia
Menu.php                položka v menu
Page.php                samostatná stránka
Settings.php            konfigurácia
resources/views/*.blade.php

Štruktúra sa kontroluje pred inštaláciou a je case-sensitive. V každej hook triede prepisuješ len data() a authorize(), handle() je final.

Znie to priamočiaro a aj je. Len treba vedieť o štyroch veciach, ktoré v dokumentácii nie sú.

Štyri pasce, ktoré ma stáli najviac času

1. authorize() nesmie typovať App\Models\User

Toto ma stálo najviac. Panel na device overview mi fungoval, ale stránka aj nastavenia hádzali “Missing view”. Žiadna chyba v logu, nič.

Hooky sa volajú cez app()->call(), takže argumenty sa resolvujú z Laravel containera. A App\Models\User tam nie je nabindovaný, v AppServiceProvider je z modelov nabindovaný len Device. Laravel teda poslušne vyrobí čerstvú prázdnu User inštanciu, $user->can('global-read') vráti false, hook sa odfiltruje a controller spadne na fallback view plugins.missing.

Panel fungoval jednoducho preto, že ako jediný $user vôbec nepoužíval.

Riešenie je typovať kontrakt, ktorý Laravel bindne na prihláseného používateľa:

use Illuminate\Contracts\Auth\Authenticatable;

public function authorize(Authenticatable $user): bool
{
    return $user->can('global-read');
}

Mimochodom, presne takto to má ExamplePlugin, ktorý je súčasťou LibreNMS, hoci abstraktná trieda hooku deklaruje User. Keby som si ho pozrel skôr, ušetril by som si hodinu.

2. SettingsHook volá data() dvakrát

Toto je v zdrojáku vidno na prvý pohľad a aj tak sa na to dá naletieť:

final public function handle(string $pluginName, array $settings, Application $app): array
{
    return array_merge([
        'content_view' => Str::start($this->view, "$pluginName::"),
    ], $this->data($app->call($this->data(...), [
        'settings' => $settings,
    ])));
}

Vnútorné volanie dostane skutočné nastavenia. Vonkajšie dostane výsledok toho vnútorného ako argument $settings. A vracia sa to vonkajšie.

Ak teda data() vracia ['settings' => $settings, ...], druhý priechod dostane zabalený array a v šablóne máš $settings['settings']. Riešim to rozbalením hneď na vstupe:

public static function unwrap(array $settings): array
{
    if (isset($settings['settings']) && is_array($settings['settings'])) {
        return $settings['settings'];
    }

    return $settings;
}

Horšie je, keď má data() vedľajší efekt. Mám tam tlačidlo, ktoré zakladá Custom OIDy: prvý priechod ich založil, druhý ich už videl existovať a nahlásil “už existuje”. Výsledok si preto pamätám na inštancii hooku.

3. numeric() a hideMib() sa navzájom vylučujú

V rôznych návodoch sa reťazí SnmpQuery::device($device)->numeric()->hideMib()->walk($oid). Lenže obe metódy nastavujú tú istú vlastnosť oidFormat, a hideMib() je druhá v poradí, takže vyhrá:

public function numeric(bool $numeric = true): SnmpQueryInterface
{
    $this->options->oidFormat = $numeric ? SnmpOidOutput::Numeric : SnmpOidOutput::Module;
    return $this;
}

public function hideMib(): SnmpQueryInterface
{
    $this->options->oidFormat = SnmpOidOutput::Suffix;
    return $this;
}

Namiesto numerických OIDov teda dostaneš holý suffix a parser si na tom vyláme zuby. Používam len ->numeric().

Bonus: v LibreNMS 26.8 je SNMP vrstva v LibreNMS/Data/Source/NetSnmpQuery.php, v aktuálnom masteri sa presunula do Data/Source/Snmp/. Preto v kóde používam globálny alias SnmpQuery, nie priamy namespace. Pri upgrade sa tak nič nerozbije.

4. net-snmp háda formát OCTET STRING a občas háda zle

Toto je najzákernejšie, lebo to vyzerá, že funguje.

IP adresa subscribera je InetAddress, teda OCTET STRING so štyrmi bajtmi. Bez načítanej MIB nemá net-snmp display hint, tak háda: ak sú bajty „tlačiteľné”, vypíše ich ako text, inak ako hex. Takže 100.64.16.41 príde ako 64 40 10 29 (v pohode, rozparsujem), ale 100.64.13.53 ako d@ a riadiaci znak, ktorý mi rozsypal celý výstup v termináli.

Pri ladení som to videl doslova takto, jeden riadok uprostred výpisu bol prepísaný sám sebou:

100030     user05@example.net     3      "64 40 08 C7 "
y"0032     user06@example.net     3      "d@
100035     user07@example.net     3      "64 40 0B 92 "

Riešenie je nehádať a vypýtať si hex explicitne. LibreNMS to dovolí cez options():

SnmpQuery::device($device)->options(['-OQXUtenx'])->walk($oid);

Tie flagy nie sú náhodné, je to presne to, čo LibreNMS posiela štandardne (-OQXUte), plus n pre numerické OIDs a x pre hex. Pozor na to, že options() v 26.8 ostatné voľby nahradí, nie doplní.

Ešte zopár prekvapení od zariadenia

cPppoeSystemMaxAllowedSessions nevracia nulu, keď nie je limit. MIB tvrdí, že nenastavený limit je 0. Moje ASR-ká vracajú 4294967295, teda maximum Unsigned32. V UI to vyzeralo ako strop 4,3 miliardy sessions a všetky utilizačné ukazovatele mi vyšli na 0 %. Teraz beriem obe hodnoty ako „bez limitu”.

csubSessionMacAddress je prázdny. Na IOS-XE 15.5(3)S nevracia nič, na každej session. Stĺpec som z pluginu vyhodil úplne, ušetrí to jeden celý walk cez tabuľku, ktorá má riadok na každého subscribera.

Per-interface tabuľka pokrýva úplne všetko. Na jednom BRASe vrátila vyše 4 000 riadkov, z ktorých sessions niesli dva. Zvyšok sú sub-interfacy s nulami. Default je teraz zobraziť len tie, čo niečo nesú, s prepínačom na zvyšok.

Detail BRASu: sessions po interfacoch a zoznam subscriberov
Detail zariadenia. Všimni si „4292 idle interface(s) hidden": bez toho filtra by tabuľka mala 4 294 riadkov a dva užitočné

Zber dát: plugin nemá poller

Toto je principiálne obmedzenie, s ktorým sa treba zmieriť. V2 plugin systém má len UI hooky: DeviceOverviewHook, MenuEntryHook, PageHook, PortTabHook, SettingsHook. Žiadny z nich nebeží mimo HTTP requestu a lokálny plugin nevie zaregistrovať ani artisan command, ani scheduler. PluginProvider z neho načíta výhradne hook triedy.

Prakticky to znamená, že všetko sa deje počas renderovania stránky. Počty sessions sú lacné, jeden snmpget a jeden walk fyzických interfacov. Tie si pokojne vypýtam on-demand a nacachujem.

Zoznam subscriberov je iný príbeh: štyri walky, riadok na každého subscribera. Na BRASe s 2 200 sessions to je cez 8 000 varbindov a stránka by na to čakala. Riešenie:

  • zoznam subscriberov sa pri renderovaní nikdy nesťahuje, číta sa výhradne z cache,
  • keď tam nič nie je, stránka ponúkne tlačidlo Poll the BRAS now,
  • kto chce mať dáta pripravené, pustí si priložený skript z cronu.

Ten skript si bootstrapne Laravel mimo requestu a naplní cache:

require $installPath . '/vendor/autoload.php';
$app = require $installPath . '/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();

Je to najbližšie k polleru, ako sa lokálny plugin dostane. Sám ho nepoužívam. Keď stránku otváram občas, cron by dopytoval ASR-ká pre nikoho.

Grafy: nech to robí LibreNMS

Plugin si nič neukladá, takže sám o sebe nemá čo grafovať. Ale poller_modules.customoid je v LibreNMS zapnutý defaultne, čiže čokoľvek v tabuľke customoids poller sám zbiera do RRD, kreslí z toho grafy a dá sa na to alertovať.

Tak som sa vrátil na začiatok. V nastaveniach pluginu je tlačidlo, ktoré zaregistruje cPppoeSystemCurrSessions ako Custom OID na všetkých BRASoch, ktoré sedia na filter. Pred zápisom si hodnotu zo zariadenia sám prečíta, aby poller nezdedil OID, na ktoré zariadenie neodpovedá. Poller totiž berie len riadky s customoid_passed = 1.

Zámerne len pridáva. Nikdy nič neupraví ani nezmaže, aby sa nedala stratiť RRD história.

Nastavenia pluginu: filter zariadení, cache TTL a tlačidlo na založenie Custom OIDov
Nastavenia: automatický filter na BRASy, cache TTL, voliteľný walk subscriberov a tlačidlo Create custom OIDs

Jedna drobnosť, na ktorej som sa pomýlil aj tu: odkaz na graf. LibreNMS parsuje z cesty ako parameter len tie segmenty, ktoré obsahujú = (Url::parseLegacyPath). Takže device/633/graphs/customoid nastaví záložku, ale skupinu grafov nie a skončíš na prvej v poradí. Správne je:

device/device=633/tab=graphs/group=customoid/

Čo z toho je

Na jednej stránke vidím všetkých päť BRASov, súčet sessions, rozpad PTA/FWDED/TRANS a využitie voči limitu. Kliknutím sa prepadnem na konkrétne zariadenie, kde mám sessions po interfacoch, a ak chcem, aj zoznam subscriberov s username, stavom a pridelenou IP. Na stránke zariadenia mám ten istý prehľad v malom paneli a odkaz na graf.

Panel PPPoE Sessions na stránke zariadenia v LibreNMS
Panel priamo na device overview, s odkazom na graf a na detail

Odpoveď na „koľko sessions mám na ktorom BRASe” je teraz jeden pohľad namiesto piatich SSH relácií.

Testoval som ho proti LibreNMS 26.8.2 a Cisco ASR1000 s IOS-XE 15.5(3)S. Iné platformy môžu tie MIBy napĺňať inak, hlavne ten prázdny MAC a hodnotu limitu by som na inom hardvéri overil.

jozefrebjak/librenms-plugin-cisco-pppoe

PPPoE sessions z Cisco BRASov priamo v LibreNMS: prehľad všetkých zariadení, rozpad PTA/FWDED/TRANS, sessions po interfacoch a zoznam subscriberov.

MIT PHP / Blade LibreNMS v2 plugin
Pozri na GitHube →

Ak monitoruješ PPPoE alebo IPoE termináciu inak (cez RADIUS accounting, streaming telemetry, alebo niečím, čo mi uniklo), napíš mi na LinkedIn alebo GitHub. SNMP je tu evidentne najmenej elegantná cesta, len zhodou okolností tá, ktorú už mám všade zapnutú.