Heb je al enige ervaring met shell scripting en begin je stilaan de beperkingen ervan te voelen? Dan is het misschien tijd om over te stappen op Perl. Dat is heus niet zo moeilijk als het lijkt! In deze workshop leggen we de basisprincipes uit aan de hand van een erg concreet voorbeeld: een script om het filesystem usage van een Raspberry Pi te monitoren.

Filip Vervloesem

Shell scripts zijn erg handig om allerlei taken snel te automatiseren. Maar vroeg of laat bots je toch tegen de technische limieten van bash scripting. De mogelijkheden zijn al bij al vrij beperkt. Je valt dus gauw terug op een breed scala aan externe tools. En juist doordat een bash-script voortdurend externe processen opstart, is het erg traag. Dat valt misschien niet op als je af en toe een klein script start. Maar als je bash-scripts gebruikt om massieve hoeveelheden tekst te manipuleren, wordt het al snel een issue. Zo hebben we ooit in een bedrijf een Linux-serverpark gezien waar elke nacht een aantal logfiles en configuratiefiles gescand werden op verdachte wijzigingen. Initieel was dit een erg kleine setup, en dus had men dit via (Korn) shell scripts geïmplementeerd. Maar door de jaren heen groeide zowel het aantal files als het aantal servers. Uiteindelijk had dit shell script al een ganse nacht nodig om zijn taak uit te voeren. Toen de sysadmins zelfs ‘s morgens nog op dit script moesten wachten alvorens ze het rapport konden nakijken, werd het echt problematisch. De oplossing? Het script herschrijven in Perl.

 

Waarom Perl?

Uiteraard is Perl niet de enige taal die je kan gebruiken om beheertaken te automatiseren. Vandaag de dag heeft Perl misschien een ietwat oubollig imago in vergelijking met talen zoals Python en Ruby. Toch zijn we ervan overtuigd dat Perl nog steeds zijn plaats heeft in de toolkit van een sysadmin. Perl wordt immers al decennia gebruikt door UNIX- en Linux-sysadmins. Op het internet vind je dus enorm veel praktijkvoorbeelden en er zijn ook talloze modules ontwikkeld voor de meest uiteenlopende taken. Tot slot zijn sommige populaire tools voor sysadmins in Perl geschreven, zoals bijvoorbeeld logwatch. Wil je dergelijke tools zelf uitbreiden of aanpassen, dan moet je wel Perl kennen.

 

In de rest van dit artikel werken we een kort script uit – ongeveer 70 regels – om  het filesystem usage op een Raspberry Pi te monitoren. We wilden daarvoor geen monitoring tool zoals Nagios of Monit gebruiken om de installatie zo eenvoudig mogelijk te houden. Onze Raspberry Pi is immers geen fileserver en dus groeit de hoeveelheid data erg traag. Het volstaat om pakweg éénmaal per uur het filesystem usage te controleren. Wordt er een vooraf bepaalde threshold overschreden, dan geeft het script een waarschuwing. Start je dit script op via cron en heb je een lokale mailserver met een correcte relay host geconfigureerd, dan krijg je de melding netjes in je mailbox. We zullen stap voor stap kleine blokken code uitleggen: een downloadlink naar het volledige script vind je onderaan het artikel.

 

Modules

Een Perl-script heeft meestal de extensie .pl, al is dat geen vereiste. Het is immers de eerste regel (#!/usr/bin/perl) die aangeeft met welk programma de code moet worden uitgevoerd. Daarna vind je vaak een aantal regels terug die beginnen met “use”. Met use schakel je optionele mogelijkheden in (die standaard in Perl zitten) of extra modules (die je nog moet installeren). In afbeelding 1 zie je de benodigde modules voor ons script: GetOpt::Long om opties op de commandline mee te geven en Filesys::Df voor een implementatie van het df-commando in Perl. Die eerste module wordt standaard bij Perl geleverd, terwijl je die tweede installeert via je package manager (bijvoorbeeld libfilesys-df-perl in Debian). Merk op dat we bij de eerste module expliciet opgeven welke mogelijkheden we willen gebruiken (GetOptions) en bij de tweede niet. In dat geval zijn alle features van de module beschikbaar. Sommige modules zijn nog verder te configureren. Zo zorgt de gnu_getopt-optie voor GetOpt::Long ervoor dat GetOpt::Long zich exact hetzelfde gedraagt als GNU’s getopt, dat je allicht kent van shell scripts. In de commandline-rubriek van dit nummer lees je waar je de documentatie van Perl-modules vindt.

 

Maar hoe weet je nu welke modules er beschikbaar zijn? Een volledig overzicht vind je op het Comprehensive Perl Archive Network (www.cpan.org). Een goed startpunt is ook een zoekopdracht in de repositories van jouw distributie. Het voordeel van die modules is immers dat ze gemakkelijker te installeren zijn. Dat is erg handig als je je script op meerdere systemen wilt installeren of wilt delen met andere mensen. Op Debian geeft de volgende zoekopdracht:

$ aptitude search lib.*-perl

 maar liefst 3000 resultaten terug!

Merk ook op dat elke regel eindigt met een puntkomma (;). Krijg je rare foutmeldingen bij het testen van je script? 9 kansen op 10 ben je ergens een puntkomma vergeten! Dat is immers de klassieke fout voor beginners die overstappen van shell scripting.

Variabelen

Perl onderscheidt drie types variabelen, elk aangeduid met een ander karakter: een $ voor een scalar, een @ voor een array en een % voor een hash:

– een scalar is een eenvoudige variabele met één waarde, bijvoorbeeld: $var = ‘value’.

– een array bevat een reeks waarden, bijvoorbeeld: @array = ( ‘1’, ‘2’, ‘3’ ). Met $array[0] vraag je de eerste waarde op, met $array[1] de tweede, enzovoorts

– een hash bevat een aantal keys met elk een eigen value, bijvoorbeeld:

%hash = (

        key1 => ‘value1’,

        key2 => ‘value2’,

        key3 => ‘value3’,

);

In plaats van een cijfer gebruik je nu een key om een bepaalde value op te vragen: $hash{key1}, $hash{key2}, enzovoorts. Bovendien is het mogelijk om weer een andere hash (in plaats van een scalar) als value te gebruiken. Dergelijke multi dimensional hashes zijn een erg krachtig hulpmiddel om complexe data in Perl te benaderen. Voor ons script gaat dit echter een stap te ver en houden we het bij eenvoudige hashes.

Opties

We willen voor verschillende filesystems een andere threshold instellen waarop het script een waarschuwing geeft. Zo mag /home misschien tot 95% vollopen, terwijl we voor / al bij 80% willen ingrijpen. We zouden dit in het script zelf kunnen definiëren, maar dat is niet erg netjes. Het is handiger om daarvoor een apart configuratiebestand te gebruiken. In afbeelding 2 voorzien we de commandline-optie –config voor het te gebruiken configuratiebestand. Verder in het script benaderen we dat bestand via de scalar $ConfigFile. Wordt de –config-optie niet meegegeven, dan neemt het script het bestand /opt/local/etc/dfmonitor.conf als standaardwaarde (het script zelf installeren we overigens als /opt/local/bin/dfmonitor.pl). De “or die” aan het einde van de regel is een handige functie van Perl om het script af te breken met een foutmelding indien er iets misloopt, in dit geval bij een ongeldige optie. Probeer het maar eens uit met een optie ‘–foo=bar’ en kijk hoe Perl reageert. Merk op dat je bij print-opdrachten in Perl de newline (\n) steeds expliciet moet opgeven. De regel met GetOptions en die erna, worden overigens niet afgesloten met een ;. De tweede regel hoort immers nog bij het GetOptions-commando (dat merk je aan de ronde haakjes ervoor en erachter).

Bestanden lezen

De kern van het script is erg eenvoudig: een lijst van filesystems en thresholds inlezen en die vergelijken met het huidige filesystem usage. We beginnen bij het eerste deel: de benodigde code daarvoor vind je in afbeelding 3. Om te beginnen maken we een (lege) hash aan genaamd %filesystems. Een hash is immers perfect voor dit soort informatie: als key gebruiken we het mountpoint en als value de threshold. Interactie met bestanden verloopt in Perl steeds via zogenaamde file handles. De inhoud van het bestand $ConfigFile wordt hier read-only (dit is het ‘<‘-teken) beschikbaar gesteld via de handle $fh_conf_r. Gebruiken we die file handle nu als input voor een while-loop (via de constructie <$fh_conf_r>), dan lezen we het configuratiebestand regel per regel in via de $line-variabele. De loop-code plaats je in Perl tussen accolades, terwijl de voorwaarde om die loop uit te voeren tussen ronde haakjes staat. De eerste en de laatste regel van de loop (met de accolades) hoef je ook niet af te sluiten met een ;. Voorwaardelijke expressies (zoals unless of if in afbeeldingen 4 en 5) kennen een gelijkaardige syntax.

Elke regel in het configuratiebestand heeft volgend formaat: filesystem,threshold. Die informatie bewaren we in de %filesystems-hash door:

– spaties en tabs te verwijderen uit de regel met een reguliere expressie ($line =~ s/<search>/<replace>/). De syntax lijkt erg op die van sed.

– de regel te splitsen op het komma-teken en beide elementen in een (tijdelijke) array te bewaren met de split-functie.

– de array te ontleden tot individuele variabelen met de shift-functie, die steeds het eerste element van de array wegneemt.

– een nieuwe key toe te voegen in de hash met de $fs-variabele als key en de $th-variabele als value.

Ben je klaar met het bestand te verwerken, vergeet dan niet om de file handle weer te sluiten.

 

Tests

Shell scripts bevatten vaak veel code om mogelijke fouten te detecteren en correct af te handelen. In Perl is dat niet anders, al heb je meestal minder code nodig. Onze while-loop uit de vorige paragraaf is nog niet echt robuust. Je hebt immers geen controle op het configuratiebestand: mogelijk bevat het dus ongeldige invoer. Met de code uit afbeelding 4 gaan we even na of:

– het opgegeven mountpoint leesbaar is (-r). We controleren niet of het effectief een directory is, aangezien df ook prima werkt voor bestanden (dan geeft het gewoon de output voor het onderliggende filesystem terug).

– de opgegeven threshold een getal tussen 0 en 100 is.

Is aan één van beide voorwaarden niet voldaan, dan tonen we een waarschuwing en gaan we meteen verder naar de volgende regel (unless, next). Op die manier negeert het script de ongeldige input en werkt het nog wél voor de geldige input.

Loops

Nu komen we bij het tweede deel van het script: de ingelezen filesystems en thresholds vergelijken met het huidige filesystem usage. Daarvoor gebruiken we de foreach-loop uit afbeelding 5. Een foreach-loop bevat steeds twee argumenten: de variabele waarin je het huidige element wilt plaatsen (my $fs) en de lijst van elementen die je wilt doorlopen (sort keys %filesystems). Van een hash kan je zowel de keys als de values gebruiken als input voor de loop. Daarom moet je nog expliciet opgeven welk deel van de hash je wilt doorlopen. In dit geval zijn we uiteraard geïnteresseerd in de keys, aangezien we voor elk filesystem in het configuratiebestand het df-commando willen uitvoeren. De sort-functie tot slot geeft de verschillende hash-keys in alfabetische volgorde terug. Zo worden de verschillende filesystems ook alfabetisch gesorteerd in de (eventuele) output van het script.

 

In de loop voeren we eerst de df()-functie uit en bewaren we de output daarvan in de $df-variabele. Met de optie ‘1048576’ zetten we die output meteen om in megabytes. Dat is iets gebruiksvriendelijker dan de standaardweergave in kilobytes. $df is overigens een verwijzing naar een hash waarvan je verschillende keys kan opvragen: gebruikte schijfruimte (used), beschikbare schijfruimte (bfree), percentage van gebruikte schijfruimte (per), enzovoorts. In ons script hebben we uiteraard die laatste key nodig. Indien het huidige filesystem usage ($df->{per}) hoger ligt dan de ingestelde threshold ($filesystems{$fs}) tonen we dus een waarschuwing. Ditmaal gebruiken we niet het print-commando, maar printf. Daarbij vervang je de verschillende variabelen in het print-commando door placeholders zoals %d (voor een geheel getal) of %s (voor tekst). De eigenlijke variabelen volgen na het printf-commando, gescheiden door komma’s. Dat is heel wat leesbaarder dan lange print-commando’s vol met variabelen. Om een %-teken weer te geven met printf gebruik je overigens %%.

 

Bestanden schrijven

De eerste versie van het script is nu klaar. Test dit door te spelen met verschillende instellingen in het configuratiebestand: een threshold hoger of lager dan het huidige filesystem usage, een niet-bestaand filesystem, een ongeldige threshold, enzovoorts. Er is echter nog één onverwacht probleem: het script geeft telkens opnieuw dezelfde output als een filesystem boven een threshold uitkomt. Dat is problematisch wanneer je het script via cron start. Je zou dan bijvoorbeeld elk uur dezelfde e-mail krijgen totdat het probleem is opgelost! Om dat te vermijden, laten we het script een tweede bestand aanmaken waarin eerdere alerts bewaard worden. Zo weet het script de volgende keer of een bepaalde alert nieuw is of niet. Voeg om te beginnen bij de code uit afbeelding 2 de cache-optie toe om een $CacheFile te definiëren (standaardwaarde: /opt/local/var/dfmonitor). Vervolgens lees je dat bestand in en maak je een hash %alerts aan (net zoals je dat in afbeelding 3 deed voor $ConfigFile en %filesystems). De foutdetectie uit afbeelding 4 mag je ditmaal achterwege laten. De cache file is aangemaakt door ons script, dus gaan we ervan uit dat het formaat steeds correct is.

 

Na het inlezen van de cache file openen we die met schrijftoegang (>) en maken we die leeg (zie afbeelding 6). Het is immers mogelijk dat een bepaalde threshold nu niet meer overschreden is. Mocht er weer een threshold overschreden zijn, dan schrijven we die in de foreach-loop weg naar de cache file. Net voor de foreach-loop openen we dat bestand dus een derde keer, ditmaal met append-toegang (>>). Zo kunnen we in de loop meermaals naar het bestand schrijven zonder de eerdere inhoud te overschrijven. Voeg de tweede regel uit afbeelding 7 toe binnen de if-constructie in de foreach-loop om een nieuwe alert weg te schrijven in de cache file. Plaats de laatste regel uit afbeelding 7 na de foreach-loop om de file handle weer te sluiten. Het script schrijft nu steeds de laatste alerts weg naar de cache file, maar doet er verder niets mee. Daarom voegen we nog een extra voorwaarde toe rond het printf-statement (zie afbeelding 8). Indien de cache file al een alert bevat voor het filesystem in kwestie met dezelfde waarde als het huidige filesystem usage, tonen we geen output. Zo krijgen we enkel nog e-mails als het filesystem usage verder blijft stijgen boven de ingestelde threshold. Kwestie van onze mailbox niet te spammen met nutteloze berichten!