Erinnert sich noch jemand an expect
?
Zeichenketten zu senden, um auf Zeichenketten
zu warten, um wiederum ... ideal, um über telnet
Routinearbeiten auf
einem Dutzend Rechner automatisch durchzuführen.
expect
basierte auf Tcl
, und, wie soll ich es sagen, Tcl
und ich,
wir kamen uns nicht näher, etwas stand immer zwischen uns: Mal war's eine
geschweifte Klammer in der falschen Zeile, mal ein eval
zuviel oder
zuwenig - es wollte nicht klappen mit uns zwei'n. Wie froh war ich daher,
zu sehen, daß es das Perl-Modul Net::Telnet
gibt und alles viel
einfacher geht!
Heute stelle ich ein Skript vor, das sich der Reihe nach auf
jedem Rechner eines Maschinenparks einloggt, dort die Auslastung und
die Größe einer Log-Datei abfragt und die gesammelten Daten
schließlich schön formatiert anzeigt.
Und, zwecks Komfortsteigerung verpacken wir das Ganze in ein CGI-Skript,
das einen handelsüblichen Web-Server dazu bewegt, auf Knopfdruck den Zustand unserer
Rechner anzuzeigen. Abbildung 1 zeigt das Ergebnis des CGI-Skripts checkload.pl
(Listing 1) im Browser.
Zeile 3 bindet Lincoln Steins praktisches CGI
-Modul ein, mit
den Tags :standard
und :html
exportiert es die weiter unten
benötigten Funktionen ohne überflüssige Objekt-Huberei. Cool!
Zeile 4 zieht das Modul Net::Telnet
, dessen Distribution auf dem CPAN unter
CPAN/modules/by-module/Net/Net-Telnet-3.00.tar.gz
liegt und sich wie alle Perl-Module am schnellsten mit dem im Oktober
vorgestellten CPAN
-Saugrüssel installieren läßt.
Die Benutzerkennung und das Paßwort aus den Zeilen 7 und 8 gewähren Zugang
zu allen nachfolgend abgefragten Rechnern.
Jeder Eintrag der @hosts
-Liste ab Zeile 11 enthält eine Referenz auf
eine Liste, die als erstes Element den Host-Namen und als zweites den
'Prompt' enthält, jene Zeichenkette, die der jeweilige Rechner zur
Eingabeaufforderung anzeigt. So steht bei machine2.domain.com
das
für die Bourne-Shell typische $
, machine1.domain.com
bevorzugt
hingegen machine1>
- jedem das Seine.
Zeile 19 iteriert über diese LoL
(List of Lists).
Für jeden Schleifendurchgang liegt eine Referenz auf
die aktuelle Unterliste in $_
. Zeile 20
dereferenziert dies mittels @$_
und kopiert die Werte für den Rechnernamen und den Prompt
in die augenfreundlicheren Variablen $host
und $prompt
.
Damit die nachfolgende Telnet-Session auch wirklich nur auf den Prompt
reagiert und nicht etwa auf eine aufgelistete Datei gleichen Namens,
formt Zeile 21 aus der angegebenen Zeichenkette einen regulären Ausdruck,
der festlegt, daß der Chatter auf eine Zeile als Antwort wartet, die
mit dem Prompt beginnt und dann eventuell mit einer Reihe von Leerzeichen
endet. Die quotemeta
-Funktion aus der Perl-Standard-Bibliothek
übernimmt dabei die Maskierung gefährlicher Zeichen
wie *
, denen in regulären Ausdrücken eine Sonderbedeutung zukommt.
Zeile 29 versucht dann, mittels der login
-Methode des vorher erzeugten
Telnet
-Objekts Kontakt mit dem aktuellen Rechner aufzunehmen
und den Login-Prozeß durchzuführen. Da login
leider innerhalb
des Telnet
-Moduls mit einer die
-Anweisung abbricht, falls
der fremde Rechner sie zurückweist oder nicht erreichbar ist, fängt
Zeile 29 diesen Fehlerfall mit Hilfe eines eval
-Konstruktes ab.
Ging innerhalb des eval
-Blockes etwas schief, ist $@
gesetzt,
was Zeile 31 abprüft und gegebenenfalls die Ergebnis-Liste @hostinfo
um einen Fehlereintrag bereichert.
Andernfalls führt Zeile 35 ein uptime
-Kommando auf dem fremden Rechner aus.
Die cmd
-Methode aus dem Telnet-Modul setzt das angegebene Kommando
ab, wartet auf den Prompt und liefert die empfangenen Zeilen als
Liste zurück. Da uptime
nur eine Zeile zurückliefert,
enthält $lines[0]
einen String a la
11:31am up 2:28, 3 users, load average: 0.07, 0.11, 0.15
Der reguläre Ausdruck aus Zeile 36 filtert daraus 0.07
, den
Wert der Rechnerlast, gemittelt über die Zeitspanne einer Minute.
Entsprechend startet Zeile 39 ein ls
-Kommando, welches folgende
Informationen über die Logdatei zurückliefert:
-rw-r--r-- 1 nobody nogroup 43896 Nov 22 13:39 /log/error_log
Zeile 40 trennt die ausgebenen Felder an den Zwischenräumen und filtert
daraus das fünfte Element, die Größe der Datei, 43896
Bytes.
In Zeile 41 kommt der bewährte Trick zum Einsatz, der große Zahlen
durch Punkte in Dreier-Gruppen aufspaltet (Perl FAQ).
Zeile 43 schiebt eine Referenz der Liste gewonnener Informationen
ans Ende des Behälters (LoL) @hostinfo
.
Statt print
verwendet checkload.pl
, wie z.B. in Zeile 50,
cgiprint
. Diese ab Zeile 70 definierte Funktion unterhält eine
statische Variable, die ihr anzeigt, ob der vor allen anderen Ausgaben
notwendige CGI-Header schon ausgegeben wurde. So muß sich niemand
mehr um den lästigen Header kümmern. Egal welchen
Weg das Programm im Fehler- oder Gutfall nimmt - cgiprint
wird's
schon richten.
Zeile 50 gibt die HTML-Startsequenz aus und setzt dabei
den Titel des angezeigten Dokuments.
Nach der Ausgabe der Überschrift in Zeile 54 beginnt Zeile 56, den
Inhalt einer HTML-Tabelle im String $tablecontent
aufzubauen.
Die foreach
-Schleife
zwischen den Zeilen 60 und 62 transformiert die LoL
@hostinfo
in eine HTML-Tabelle - für jeden Schleifendurchlauf
erzeugt TR()
eine neue Tabellenzeile, die wiederum mittels
der map
-Funktion in <TD>
-Tags gepreßte
Unterlisten-Elemente als Spalten enthält. Uff! Ohne Abitur geht's
halt nicht.
Zeile 65 gibt das Monstrum schließlich aus, Zeile 66 schließt
alle HTML-Ausgaben mit </HTML>
ab.
Noch ein ernstes Wort zum Thema Sicherheit: Natürlich kann ein Skript, das
eine Benutzerkennung für mehrere Rechner samt gültigem Paßwort im Klartext
enthält,
ein gewaltiges Loch in ein sonst gut abgesichertes System reißen.
Ohne weitere Sicherungsmaßnahmen sollte man das Skript deswegen nur
innerhalb einer Firewall betreiben (Browser und Webserver!),
die rigoros alle von außen kommenden telnet
-Anfragen abblockt.
Weiter darf nur der Server selbst das Skript lesen. Im Falle
eines Apache-Servers, der nach der Initialisierung als nobody
laüft, muß
checkload.pl
also nobody
gehören und die Rechte -rwx------
gesetzt haben. Entdeckt allerdings ein pickliger 14-jähriger
ein Sicherheitsloch im Apache, bringt uns gerade das ins Grab.
Für mehr Sicherheit muß auf allen Rechnern, die checkload.pl
abklappert,
ein spezieller Account 'ran. Die Shell dort läuft in einem Sandkasten
und bietet nur ls
und uptime
als Funktionalität. Keinen vi
,
kein cat
, kein gar nichts! Das bringt zusätzlichen Schutz vor
ungebetenen Gästen, ist aber ein wenig aufwendig:
So wie man beim anonymen FTP-Zugriff nicht in den Top-Verzeichnissen
(z.B. /etc
) eines Servers herumwühlen darf, beschränkt ein speziell
eingerichteter watch
-Account den Zugriffsbereich des Benutzers
mit chroot
auf einen kleinen Unterbereich des
Dateisystems. chroot
legt ein Verzeichnis als neue Wurzel
des Dateisystems fest und unterbindet rigoros Zugriffe oberhalb
dieses ``Käfigs''.
Hierzu richtet man in /etc/passwd
einen neuen Benutzer ein:
watch::9999:9999::/home/watch:/home/watch/bin/cage
und erzeugt mit
mkdir /home/watch
dessen neues Verzeichnis. Das neue Paßwort wird als root
mit
passwd watch
gesetzt. Die Käfig-Shell /home/watch/bin/cage
stricken wir mit
#include <stdio.h> /* * Michael Schilli, 1998 (mschilli@perlmeister.com) */ main() { char *sandboxdir = "/home/watch"; putenv("LD_LIBRARY_PATH=/lib"); putenv("PATH=.:/bin"); if(chdir(sandboxdir)) { perror("Chdir failed"); exit(1); } if(chroot(sandboxdir)) { perror("Chroot failed"); exit(1); } setuid(9999); execl("/bin/bash", "-cs", NULL); perror("Shell did not start"); }
selber und kompilieren das Ergebnis nach /home/watch/bin/cage
.
Das Executable cage
muß dafür root
gehören und das setuid-Bit
(mit chmod 4755 cage
) gesetzt haben.
Die aus dem C-Programm aufgerufene bash
-Shell liegt aber
normalerweise in /bin
, ganz zu schweigen von den shared libraries,
die sie benötigt - alles völlig außer Reichweite, sind wir einmal
in /home/watch
gefangen, ohne Ausweg nach oben!
Da hilft nur Kopieren: Die Shell, ls
und uptime
, den
Linux-Loader und die Bibliotheken:
cd /home/watch mkdir lib bin usr usr/lib dev etc log cp /bin/ls bin cp /usr/bin/uptime bin cp /lib/ld-linux.so.1 lib cp /etc/ld.so.cache etc cp /lib/libtermcap.so.2 lib cp /lib/libc.so.5 lib cp /bin/bash bin
(Andere Linux-Versionen als 2.0.X erfordern unter Umständen andere
Dateien, einfach cage
als root
aufrufen und eventuell
ausgegebene Fehlermeldungen prüfen).
Und: Oh Jammer oh Not! Auch das zu untersuchende Logfile liegt außerhalb des Sicherheitsbereichs. Diese Schranke überwindet der 'harte' Link:
ln /log/error_log log/error_log
im Verzeichnis /home/watch
. Damit greift man auch
aus dem Käfig über /log/error_log
auf die Log-Datei zu.
Ein symbolischer Link genügt übrigens nicht, der darf aus Sicherheitsgründen
nicht über die gesetzte Grenze hinwegschauen.
Das uptime
-Kommando benötigt weiter einen
mount
auf /proc
, was manuell mit
mount /proc /home/watch/proc -t proc
geht, für automatisches Mounten beim Startup sorgt folgender
Eintrag in /etc/fstab
:
/proc /home/watch/proc proc defaults
Außerdem nutzt uptime
/var/run/utmp
, also spendieren wir auch
dafür einen Link. In /home/watch
geht das mit
mkdir var var/run ln /var/run/utmp var/run/utmp
Wer noch etwas Zeit und Muße hat: Das letzten Monat vorstellte
Chart
-Paket eignet sich hervorragend zur optisch ansprechenden
Aufbereitung der Daten.
Sonst: Ab mit dem Skript ins cgi-bin
-Verzeichnis des Webservers,
den URL http://my.server.com/cgi-bin/checkload.pl
in die
Bookmarks/Favourites-Liste
des Browsers aufgenommen und und bei Arbeitsbeginn einmal draufgeklickt.
Alle Server laufen wie die Nähmaschinen?
Na, da schmeckt der Morgenkaffee doch gleich viel besser.
Mmmmhh. Scheint ein erfolgreicher Tag zu werden ...
Abb.1: Alles läuft, einer schläft ... |
01 #!/usr/bin/perl -w 02 ###################################################################### 03 # Michael Schilli, 1998 (mschilli@perlmeister.com) 04 ###################################################################### 05 06 use CGI qw/:standard :html/; 07 use Net::Telnet; 08 09 # Konfiguration 10 $userid = "watch"; 11 $passwd = "topsecret!"; 12 13 # Server-Namen und deren Prompts 14 @hosts = (['machine1.domain.com', 'machine1>'], 15 ['machine2.domain.com', '$'], 16 ['machine3.domain.com', '>'], 17 ['murkshost', '$'], 18 ); 19 $logfilename = "/log/error_log"; 20 21 # Alle Rechner abklappern 22 for (@hosts) { 23 my ($host, $prompt) = @$_; 24 25 $prompt = sprintf('^%s\s*$', quotemeta($prompt)); 26 27 $telnet = new Net::Telnet (Host => $host, 28 Timeout => 60, 29 Prompt => "/$prompt/m"); 30 31 # Einloggen, 'die' abfangen 32 eval { $telnet->login($userid, $passwd); }; 33 34 if($@) { # Fehler aufgetreten? 35 push(@hostinfo, [$host, "Login FAILED"]); 36 } else { 37 # Last abfragen 38 @lines = $telnet->cmd("uptime"); 39 ($load) = ($lines[0] =~ /average:\s*([0-9.]+)/); 40 41 # Logfile-Länge abfragen 42 @lines = $telnet->cmd("ls -l $logfilename"); 43 $logsize = (split(' ', $lines[0]))[4]; 44 1 while ($logsize =~ s/^(\d+)(\d\d\d)/$1.$2/); 45 46 push(@hostinfo, [$host, "OK", $load, $logsize]); 47 } 48 49 $telnet->close; 50 } 51 52 # HTML ausgeben 53 cgiprint(start_html(-BGCOLOR => "bisque", 54 "title" => "Watch your Servers!")); 55 56 # Überschrift 57 cgiprint(h1("Watch your Servers!"), "\n"); 58 59 $tablecontent = TR( th("Host"), th("Status"), 60 th("Load"), th("Logfile Size"), "\n" ); 61 62 # @hostinfo LoL -> HTML Tabelle 63 foreach $lref (@hostinfo) { 64 $tablecontent .= TR( map { td($_) } @$lref ) . "\n"; 65 } 66 67 # Tabelle ausgeben 68 cgiprint(table({border => 1}, $tablecontent)); 69 cgiprint(end_html()); 70 71 72 ######################################################### 73 sub cgiprint { 74 ######################################################### 75 # Text ausgeben, Header falls notwendig 76 ######################################################### 77 print header() unless defined $header_printed; 78 print "@_"; 79 $header_printed = 1; 80 }
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |