Posts mit dem Label programmieren werden angezeigt. Alle Posts anzeigen
Posts mit dem Label programmieren werden angezeigt. Alle Posts anzeigen

16.08.2018

Formeln in LibreOffice Calc mit einem perl-Skript erzeugen

Hier kommt mal wieder eine Kleinigkeit, die mich etwas Mühe gekostet hat: ein perl-Skript, das mir ein XLSX-Spreadsheet erzeugt, in dem funktionierende Formeln eingebettet sind. XLSX ist das XML-Dokumentenformat von Microsoft Office, funktioniert aber auch in LibreOffice. Technisch ist es weit umständlicher als das OpenDocument Format und nicht wirklich frei, aber das soll hier nicht das Thema sein.

Ich bin an und für sich kein großer Office-Benutzer und weiß eigentlich auch nur das Nötigste - aber beruflich hab ich halt doch immer mal wieder den Auftrag, irgendwelche Office-Dateien zu verwenden oder zu liefern. In solchen Fällen versuche ich immer, mir mit Perl-Skripten die Handarbeit vom Hals zu schaffen und stattdessen eine schöne Automation zu verwenden ;-)

Kürzlich wollte nun ein Kollege in ein Spreadsheet, das ich für ihn generiere, in mehreren Zellen eine Verknüpfung zu einem anderen Tabellenblatt ("sheet") eingebaut haben.

Ich hatte größere Schwierigkeiten, die richtige Schreibweise für Formelbefehle und die Zellenkoordinaten zu finden - LibreOffice ist hier zu hilfreich und verbirgt über automatische Korrekturen im Calc-Programm viele Eingabefehler vor dem Benutzer - die Schreibweise aus dem GUI ins Skript übernommen hat nicht funktioniert und beim Öffnen der neuen Datei erhielt ich immer eine Fehlermeldung in allen Zellen mit Formel. Interessant daran: wenn ich im Office-Programm die Formel bearbeitete und abspeicherte, fing sie an zu funktionieren, obwohl ich an der Schreibweise nichts geändert hatte (z.B. einfach ein Zeichen gelöscht und dasselbe Zeichen wieder eingetippt). Das war extrem frustrierend, zeigt es doch, dass hier im Sinne der Bequemlichkeit Dinge in der Software unter der Haube passieren, die man als Benutzer nicht erkennen kann.

Außerdem ist die Syntax zwischen Microsoft Office und LibreOffice leicht unterschiedlich. Auch bei /r/LibreOffice und /r/perl waren die Lösungsvorschläge für meine Fragen breit gestreut, was die mögliche Schreibweise der Formeln angeht. Letztendlich hat sich dann eine Schreibweise gefunden, die bei LibreOffice und MS Office funktioniert - hurra!

Für das Experimentieren hatte ich mir ein winziges Perl-Skript geschrieben, das eine XLSX-Datei mit zwei Sheets erzeugt und in einer Zelle des ersten Blatts einen Verweis auf das zweite Blatt einfügt. Beim Öffnen der Datei mit Office wird dann der Verweis ausgeführt und der referenzierte Wert (im Skript unten grün markiert) erscheint dynamisch in der Formelzelle. Der rot markierte Text wird durch die Zuweisung "$u[4]=..." durch die Formel ersetzt. Die Formel enthält noch Platzhalter %d und %s, um mit printf bestimmte Zeilennummern und Spaltennamen einzusetzen, während die XLSX-Datei erzeugt wird. Die write-Methode für eine Zeile des Spreadsheets erwartet eine Referenz auf ein Array mit den Zelleninhalten, deshalb muss ich als Parameter \@l angeben. Es gibt auch speziellere write-Methoden, mit denen man jede Zelle einzeln befüllen kann, aber write ist meistens gut darin, die Datentypen im Array zu erkennen und die Arbeit intern passend zu delegieren.

Das Spreadsheet sieht dann so aus:

Der erste Tab "unix users" enthält in Zelle E1 die Formel =IFERROR( VLOOKUP( A1; $Lookup.$A$1:$B$10000; 2; 0 );"" ). Statt der Platzhalters %d sieht man die Zeilennummer "1" bzw. die Spaltennummer "2" und statt der %s werden die Spaltennamen "A" und "B" eingesetzt, d.h. der Bereich, in dem nachgeschlagen wird, geht von A1 bis B1000 und es wird bei einem Treffer in A1 der Inhalt der 2. Spalte des Bereichs (hier dann B1) übernommen.
Den Formeltext in kursiv im vorigen Absatz habe ich übrigens aus dem Eingabefeld in LibreOffice kopiert. Man sieht, dass die Schreibweise nicht dem entspricht, was im Skript als Formelschreibweise verwendet wird - hier steht plötzlich $Lookup. und nicht Lookup!. Wenn ich mit unzip in die XLSX-Datei hineinschaue (trotz der Endung ist es eine schlichte Zip-Datei!), sehe ich die Schreibweise zwischen den XML-Tags für die Formel <f> ... </f> so wie im Skript: <f>IFERROR(VLOOKUP(A1,Lookup!$A$1:$B$10000,2,0),"")</f>


Und der zweite Tab "Lookup" enthält für jeden Unixuser die Beschreibung für das GECOS-Feld.

Für eine komplexe Geschichte wie eine XLSX-Datei ist das mit knapp 20 Zeilen perl zu bewerkstelligen. Ich liebe dieses Perl-Modul.
#!/usr/bin/perl -w
use strict 'refs';
use strict 'vars';
use utf8;
use Excel::Writer::XLSX;
 
my $excelout = 'formel.xlsx';
my $workbook = Excel::Writer::XLSX->new($excelout);
my $lookupfrm=qq{=IFERROR(VLOOKUP(A%d,Lookup!\$%s\$1:\$%s\$1000,%d,0),"")};
my $worksheet;
my @u=("root","*",0,0,"no gecos","/root","/bin/sh");
my $row=0;
 
print STDERR "# write Excel unix users\n";
$worksheet=$workbook->add_worksheet('unix users');
$worksheet->keep_leading_zeros();
$u[4]=sprintf($lookupfrm,1+$row,"A","B",2),
$worksheet->write($row, 0, \@u);
print STDERR "# unix user @u\n";
 
$worksheet=$workbook->add_worksheet('Lookup');
my @l=("root","root user");
$worksheet->write($row, 0, \@l);
print STDERR "# lookup @l\n";
 
$workbook->close();

09.03.2017

Excel-Datei mit perl erzeugen

Hach, ich liebe perl! Hab ich das schon mal erwähnt? Bestimmt nicht.

Eine ganze Zeit lang habe ich ein etwas älteres perl-Modul für Excel-Spreadsheets verwendet. Beim Lesen der Anleitung fand ich kürzlich den Hinweis, dass es einen Nachfolger gibt, mit dem das neuere XML-Dokumentenformat erzeugt werden kann. Dieses Modul kann nahezu den kompletten Umfang von Excel-Spreadsheets ausreizen, inklusive Formatierung, Charts und Formeln.

Hier ist ein kurzes Beispiel für die Verwendung. Es liest die /etc/passwd eines Unix-Systems und erzeugt daraus eine kleine Exceltabelle. Als Schmankerl setzt es die Spaltenbreite und Autofilter für alle Spalten, so dass man z.B. alle Benutzer mit "nologin" filtern kann.

Vor der ersten Benutzung muss man das perl-Modul noch mit dem Befehl

# cpan install Excel::Writer::XLSX

(als root) installieren. In blau habe ich ein paar zusätzliche Kommentare an interessanten Stellen eingefügt.

#!/usr/bin/perl -w

use strict 'refs';
use strict 'vars';

use Excel::Writer::XLSX;

my %userlist;

# Spaltentitel
my @t_passwd =qw( USER PASSWD UID GID GECOS HOME SHELL );
# Spaltenbreiten, <0 heißt Spalte verbergen
my @f_passwd =qw(   20     -3  10  10    30   20    20 );

sub setfilter {
    my ($worksheet,$row,@colwidth)=@_;
    my $numcol=scalar(@colwidth);


# Autofilter für alle Spalten
    $worksheet->autofilter(0,0,$row,$numcol-1);
    for (my $i=0; $i<$numcol; ++$i) {
        my $w=$colwidth[$i];

        if ($w>0) {
            $worksheet->set_column( $i,$i, $w );
        }
        else {

# Breite setzen, kein Format, aber verstecken
            $worksheet->set_column( $i,$i, -$w, undef, 1);
        }
    }
}

sub collect_linux_passwd {
    my ($file)=@_;

    if (open(F,"<",$file)) {
        while (my $u=<F>) {
            chomp($u);
            my @user=split(/:/,$u);
            my $uname=$user[0];

# split löscht leere Felder am Ende

            $user[6]||="";
            $userlist{$uname}=[ @user ];
        }
        close(F);
    }
}

sub exceloutpasswd {
    my ($worksheet,$title,$list)=@_;
    my $row=0;


# eine ganze Zeile schreiben ($title ist ein Arrayref)
    $worksheet->write(0,0, $title);
    foreach my $i (sort(keys(%{$list}))) {
        ++$row;

# $list->{$i} ist ein Arrayref
        $worksheet->write($row, 0, $list->{$i});
    }
    setfilter($worksheet,$row,@f_passwd);
}

collect_linux_passwd("/etc/passwd");
my $workbook = Excel::Writer::XLSX->new('passwd.xlsx');
exceloutpasswd( $workbook->add_worksheet('unix users'), \@t_passwd, \%userlist );

$workbook->close();

20.02.2017

Soziale Netzwerke vor 30 Jahren oder: Opa erzählt vom Krieg

Vor langer Zeit, als die "sozialen Netzwerke" noch "Mailboxnetze" hießen, war ich Mitglied in mehreren solcher Netze.

Die Technik damals war im Vergleich zu heute gar nicht so viel anders. Das heutige Internet ähnelt zwar dem Entwurf des DARPA-Netzes auch nicht mehr so, aber das Grundprinzip ist immer noch sichtbar: ein dezentrales Netzwerk mit store-and-forward-Prinzip an den Relaispunkten.

Die Mailboxnetze von damals gab es in mehreren Kategorien, wenn man die Software betrachtet. Auf der einen Seite gab es die Netze mit Fido-Technik, daneben gab es das Maus-Netz, und ein paar wackere Kämpen hatten damals schon Zugang zum Usenet mit Unixtechniken wie UUCP und kleinere Netze mit noch eigener Software wie Zerberus usw.

Wir reden hier vom Zeitraum von ungefähr 1988 bis 2000. Damals kostete das Telefonieren noch richtig viel Geld, und das Internet gab es nur für große Firmen und Unis als "Standleitung" für mehrere Hundert DM Gebühren pro Monat. Die Uni Gießen hatte damals eine 2-MBit/s-Anbindung, und es war ein unglaublicher Fortschritt, als ca. 1995 die 34 MBit/s.-Leitung geschaltet wurde; aber ich schweife ab. Privatleute verwendeten ein sogenanntes Telefonmodem, und die gab es damals mit und ohne Segen der Deutschen Bundespost und heftigen Strafandrohungen, falls man denn erwischt werden würde. Aus heutiger Sicht betrachtet vollkommen lächerlich, aber so selbstherrlich gebärdete sich damals die staatliche Bundespost.

Die Entscheidung für oder gegen die Teilnahme an einem Mailboxnetz wurde hauptsächlich durch die Erreichbarkeit der Partner und damit die Telefonkosten bestimmt. Die Verbindung zum "Uplink", also dem Lieferanten, wurde mit Hilfe von Telefonmodems gehalten, die üblicherweise Geschwindigkeiten von 2.400 bis 14.400 Bit/s. hatten (da sind wirklich keine Vorsilben vor der Maßeinheit!).

Bei den schnelleren Modems gab es zwei Standards, weil die Standardisierung nur bis 9.600 gekommen war, die Norm hieß damals V.32.

Bei der Weiterentwicklung auf 14.400 Bit/s gab es zwei Konkurrenten: US Robotics mit einem proprietären Standard, und die wurden später von ZyXEL ziemlich heftig vom Markt gefegt, soweit ich mich noch erinnere. ZyXEL und später andere Hersteller lieferten Modems mit V.32bis-Standard für 14.400 Bit/s., und obwohl ZyXEL eigentlich aus der Geschichte mit dem proprietären US-Robotics-Protokoll hätte lernen können, machten sie es dann genauso: für die "normalen" ZyXEL-Modems wurde mit einem ROM-Update eine mit keinem anderen Hersteller kompatible Geschwindigkeit von 16.800 Bit/s. geliefert (damals musste man die EPROMs noch mit einem Spezialgerät brennen, Typ 27512). Im FidoNet gab es dann unglaublich hitzige Diskussionen, ob man in der Nodeliste diese Angabe einpflegen soll und unter welcher Kennung. Wenn man sonst keine Probleme hat ...
ZyXEL hat dann das Plus-Modell mit mehr Speicher und etwas schnellerem Prozessor gebaut, das dann ebenfalls proprietäre 19.200 Bit/s. beherrschte. So ein Modell werkelt bei uns in der Praxis immer noch, aber wird nur noch als reine Faxmaschine mißbraucht. Außerdem habe ich bei ebay billig noch ein Ersatzgerät gekauft, falls das erste kaputt geht (Cold standby).

Nebenbei gab es noch eine proprietäre Technik, nämlich das Telebit Trailblazer, die damals schon 19.200 Bit/s. beherrschten, und davon gab es auch eine postzugelassene Variante in Deutschland zu kaufen oder von der Post zu mieten (!). Ich kann mich aber nicht mehr an Preise erinnern.

Diese Modems haben in der Größenordnung 500 bis 800 DM gekostet, eine ganz schöne Stange Geld (inflationsbereinigt würde ich sagen, das ist heute dasselbe in Euro). Der Höhepunkt der analogen Telefonmodems waren die Geräte mit standardisierten 28.800 und 56.000 Bit/s, die aber nicht mehr allzuviel Verbreitung fanden, weil viele Betreiber neben dem analogen auch einen digitalen ISDN-Zugang anboten (ich übrigens auch - meine TeX-Box hatte zwischendurch 2x analog und 2x ISDN für bis zu vier gleichzeitige Anrufer).

Nun ja, wie auch immer - ich hatte im Ortsbereich einen Lieferanten für Fido in Lich, das war für mich als Schüler bzw. angehender Student gerade noch bezahlbar. Die eigene Telefonleitung war schon Luxus, nur das Umstecken von Telefon zu Modem war ziemlich schnell ziemlich lästig. Kurze Zeit später hatte ich also eine separate Telefonleitung für das Modem, und von da zum Mailboxbetreiber statt nur -nutzer war es nur noch ein kleiner Schritt ;-), aber bis zur TeX-Box war es zeitlich noch ein ganz schöner Weg. Wer noch alte c't-Hefte aufgehoben hat: dort war ich ziemlich lang mit der "TeX-Box" in der Mailboxliste enthalten; eines der Netze, an denen ich teilnahm, war das GerNet mit der Zonennummer 21:, das vom Heise-Verlag betrieben wurde und kurz vor der Jahrtausendwende abgeschaltet wurde, weil deren Software nicht y2k-tauglich war (soweit ich mich noch erinnere).

Zunächst hatte ich also ein relativ preisgünstiges Modem als interne Steckkarte für den PC mit 2.400 Bit/s. Übrigens haben damals ziemlich viele Leute die Maßeinheiten Baud und Bit ziemlich konfus durcheinander verwendet, obwohl sie ganz unterschiedliche technische Sachverhalte beschreiben. "Baud" misst, wieviele "Symbole" pro Sekunde übertragen werden. Mit einem "Symbol" können aber je nach Kodierung mehrere Bits übertragen werden. Bis 2.400 war Baud und Bit noch dasselbe, aber mit zunehmender Geschwindigkeit konnten pro Baud immer mehr Bit übertragen werden, mit Kodierungen wie Trellis, QAM usw.

Die richtigen Profis hatten einen eigenen PC, der nur für die Verwaltung des bzw. der Modems zuständig war, und haben auf einem anderen PC die Mails gelesen. Soweit war ich noch lange nicht, bei mir lief alles auf einem PC, und dazu gab es damals (vor Windows-Zeiten) Programme, mit denen auf einem 286 oder 386 schon richtig gutes Multitasking möglich war. Die Software, die ich zunächst verwendete, damit die Mailbox funktionierte, hieß QEMM-386 und war im großen und ganzen der Vorläufer der heutigen Betriebssysteme, die mehrere Dinge (fast) gleichzeitig tun konnten.

Nach kurzer Zeit war ich damit aber eher unzufrieden, und weil ich schon länger in Fachzeitschriften wie mc und c't von "großen" Betriebssystemen gelesen hatte, wollte ich unbedingt OS/2 ausprobieren. Damals war die Version 2.0 im Entstehen, und für kleines Geld gab es die ersten Beta-Versionen zum Testen zu kaufen, geliefert auf ca. 50 Disketten (mit C-Compiler, Druckertreibern, Bildschirmtreibern und zahllosen anderen Dingen wie 3270, die ich damals überhaupt nicht verstand).

OS/2 war richtig klasse und ich bin mit fliegenden Fahnen von MS-Dos weg hin zu OS/2 gewechselt und bis zu einem doofen Festplattendefekt Mitte 2000 war ich ein begeisterter Anhänger. Als IBM 2005 dann den Tod von OS/2 bekanntgab, war ich ziemlich niedergeschmettert; obwohl ich privat schon lang kein OS/2 mehr verwendete, sondern beruflich bedingt Linux und Windows, finde ich bis heute, dass OS/2 ein trauriges und unrühmliches Ende nahm. Es wird bis heute als "eComStation" weiter von einer Drittfirma vermarktet und gepflegt, aber eigentlich ist das nur für Firmenkunden interessant; für Privatleute hingegen überhaupt nicht.

Leider gab es für OS/2 relativ wenig gute Software, insbesondere für die Fido-Technik, und die Dos-Emulation ließ am Anfang doch arg zu wünschen übrig. Also fing ich an, selbst Fido-Software zu programmieren. Dabei half mir Open Source ganz gewaltig: es gab nämlich den genialen gcc-Compiler in einer speziellen OS/2-Variante, die den größten Teil der richtig guten Features nutzen konnte, auch ohne von IBM das Software Development Kit zu kaufen. Das einzige, was ich nie hinbekommen habe, war, wie man in einem 32-Bit-C-Programm eine Callback-Funktion definiert, die von einer 16-Bit-DLL aufgerufen werden kann. Das hätte ich für die Benutzung der OS/2-ISDN-CAPI brauchen können, die gab es nämlich nur als 16-Bit-DLL.

Nach einiger Zeit hatte ich eine komplette Suite von Programmen und Hilfsprogrammen, um eine Mailbox mit allen Features zu betreiben. Nachdem das letzte Dos-Programm also einen Ersatz gefunden hatte, konnte ich feierlich in der config.sys von OS/2 die Umstellung auf "ProtectOnly=Yes" durchführen und nach dem nächsten Booten hatte ich 1 MB Speicher mehr, weil es keine Dos-Emulation mehr gab.

Einer meiner Bekannten berichtete dann bei einem Treffen davon, dass er jetzt auch Zugang zum Usenet hat, und rein zufällig war sein Lieferant auch für mich über Ortsnetz erreichbar, also halbwegs preisgünstig. Flugs den C-Compiler  geschwungen und mir selbst ein Gateway zwischen UUCP und Fidonetz gebastelt, für Mail (mit batch-smtp) und News.  Auch im Usenet war ich dann unter texbox erreichbar, zuerst unter der Domain cpr.sub.org, und als die dann eingestellt wurde, war ich noch eine Zeitlang Mitglied bei lahn.de. Der Verein hatte durch gewisse Kontakte die Möglichkeit erhalten, einen eigenen Rechner im Rechenzentrum der Uni Gießen aufzustellen, und bot damit über Modems die Möglichkeit, ebenfalls Mail und News zu beziehen. Mit der zunehmenden Verbreitung von Zugangsmöglichkeiten über die normalen Telefonanbieter wie Telekom ist das alles dann irgendwie entschlafen, und heute habe ich über TGnet eine 50-MBit/s-Anbindung. Wahnsinn ;)

Das Windows in OS/2 habe ich übrigens nie verwendet (in OS/2 2.0 war Windows 3.0 enthalten und in Warp 3 aus dem Jahr 1994 war es dann Windows 3.1). IBM konnte das mitliefern, weil Microsoft und IBM noch gute Partner waren, bevor sie sich beim Streit über Windows NT gegen OS/2 2.x entzweiten. OS/2 3.0 "Warp" gab es dann in zwei Varianten, eine "rote" Edition ohne Windows für deutlich weniger Geld, und ein "blaues" Warp 3, bei dem schon Windows 3.1 enthalten war. Und selbst beim "roten" OS/2 konnte man nachträglich mit den eigenen Windows-Disketten die Emulation installieren. Windows 95 als 32-Bit-Aufsatz auf einen Dos-Unterbau ließ sich unter OS/2 nicht zum Laufen bringen, das wäre mit dem Protected Mode kollidiert. Es gab aber die abgespeckte win32s-Variante, aber das weiß ich nur aus Zeitungsartikeln, ich lebte da schon länger "ProtectOnly". Das Spiel "FreeCell" gibt es heute noch in Windows. Damals war es ein erster Lackmus-Test, ob win32s korrekt installiert wurde und funktioniert; dieses Spiel war das erste, das die neue 32-Bit-API von Windows benutzte, und win32s war eine Erweiterung für die älteren Windows-Versionen, um wenigstens einen Teil der 32-Bit-API in die alte Welt zu bringen.

Zuerst hieß der Artikel noch "Jugendsünden" - ich wollte nämlich darüber schreiben, wie ich in einem meiner Fido-Programme einen ganz dämlichen Fehler eingebaut hatte und ihn monatelang nicht finden konnte. Eines der Blogs, das ich ganz gern lese, hat gelegentlich auch Artikel über Fehler beim Programmieren. Aber irgendwie hat sich der Artikel dann ganz anders entwickelt, als ich dachte. Trotzdem will ich diese Anekdote noch aufschreiben, nach diesen ganzen Abschweifungen und historischen Andeutungen.

Eines der Programme, an dem ich ziemlich lang und ziemlich intensiv gearbeitet hatte (es hatte am Ende knapp 150.000 Zeilen Code), war eines, mit dem man Dateien in einem Mailboxnetzwerk an Abonnenten verteilen konnte. Dieses Programm konnte auch Mails erzeugen, um die Interessenten zu benachrichtigen. Das Aussehen der Mails war mit Textschablonen ziemlich frei konfigurierbar. Und im Programmcode für das Erzeugen der Mails war ein ziemlich dummer Fehler drin.

Üblicherweise deklariert man in C-Funktionen die Variablen, die man benötigt. Als Anfänger habe ich zuerst alle meine Strings als statische char-Arrays definiert. Irgendwann in meiner Lernphase dachte ich dann, ich müsste den Speicher effizienter ausnutzen und statt char origin[66] besser char *origin=malloc(length(irgendwas)) schreiben. Heute lacht man natürlich darüber, ob es sich lohnt, den Code zum zweiten Mal anzufassen, um 60 Byte für einen String einzusparen. Naja.

Dummerweise hatte ich an einer ganz anderen Stelle im Code (das ist der erste blöde Fehler) ein Sicherheitsnetz einbauen wollen, damit die von mir erzeugten Mails einen bestimmten Fido-Standard nicht verletzen, und deshalb beim Abspeichern der Mail origin[65]='\0' verwendet, um eine maximale Länge nicht zu überschreiten. Das waren dann gleich zwei blöde Fehler: (1) ein hartkodierter Wert 65, der (2) weit entfernt von der Deklaration der Variablen origin verwendet wird.

Das ist so eine Programmiertechnik, die meistens funktioniert. Außer, wenn der Parameter, der beim weit entfernten malloc einfließt, kleiner als 65 wird. Der String ist dann natürlich kürzer als 65 Byte, und in diesem Fall schieße ich mir irgendwo in den Speicher in eine ganz andere Variable ein Nullbyte hinein, das da nicht hingehört.

Ich habe ungelogen Monate gebraucht, bis mir der Zusammenhang klar war und ich eines meiner ersten Kopf->Tisch-Erlebnisse hatte. Natürlich ist das nie bei mir passiert, weil ich ausgerechnet so eine Situation bei mir nie hatte, aber ich habe die von mir programmierte OS/2-Software auch an andere OS/2-Mailboxbetreiber weiter gegeben, und von denen kamen dann ab und zu Meldungen über unerklärliche Abstürze oder abgeschnittene Texte in Mails. Das wurde mir dann klar: ein Nullbyte an der falschen Stelle kann viel Schaden anrichten.

Gefunden habe ich den Fehler dann, indem ich nahezu einen gesamten Programmdurchlauf im Debugger im Single-Step-Modus ausgeführt habe, während ich ein paar der "verdächtigen" Variablen mit einem dauerhaften Watch-Ausdruck beobachtete. Ich wusste gar nicht mehr, dass ich an einer weit entfernten Stelle im Code dieses "Sicherheitsnetz" eingebaut hatte, das war schon Monate her. Aber in dem Moment, als der Debugger mir die Zeile Code anzeigte und wie oben schon erwähnt eine ganz andere Variable plötzlich auf die Hälfte gekürzt wurde, war ich total begeistert. Und einen Moment später kam dann auch das schon erwähnte Kopf->Tisch-Erlebnis ;-)

29.09.2016

Häufigkeiten von Würfelsummen - kleines Rätsel

Ich bin ein typischer Informatiker - wenn irgendwo ein Rätsel, Denksportaufgabe o.ä. abgedruckt ist, muss ich es probieren. Zuerst denke ich kurz darüber nach, ob es eine offensichtliche Lösung gibt, und da ich sehr ungeduldig bin, geht das nicht lange gut ;)

Danach denke ich darüber nach, wie ich die Aufgabe mit dem Rechner lösen kann - auch das Nachdenken über eine algorithmische Lösung ist schließlich Denksport ;)

Kürzlich fragte die "Spektrum der Wissenschaft" nach der Häufigkeitsverteilung beim Würfeln mit drei Würfeln. Das ist ein nettes Beispiel für eine Anfängeraufgabe, und natürlich versuchte ich mich damit, das "Problem" in perl zu lösen. Ich liebe perl, perl ist cool, genauso wie Fliegen cool sind ;)

Also zurück zur Aufgabe: welche Würfelsumme ist beim Würfeln mit drei Würfeln am häufigsten?

Der offensichtliche Ansatz ist eine dreifach geschachtelte Schleife, d.h. dreimal von 1 bis 6 zählen, Summe bilden und diese Summe (die zwischen 3 und 18 liegen kann) als Index verwenden, um in einem Array einen Zähler für diese Summe zu erhöhen.

Bei dieser Aufgabe kann man einige coole Features von perl gut zum Einsatz bringen, nämlich Arrays und einige sehr bequeme Funktionen und Operatoren, um mit Arrays umzugehen.

Natürlich will ich das kleine perl-Programm nicht einfach so hier in's Blog werfen, sondern ein paar der Besonderheiten von perl  ausführlicher kommentieren.

Zunächst verwende ich für das Zählen der Häufigkeiten ein assoziatives Array %sum, bei dem die Indizes beliebige Strings sein können, und kein Array mit numerischen Indizes. Das mag zunächst unnötig umständlich wirken, aber es erspart mir später einige Fallunterscheidungen, weil ich mir vorgenommen habe, Funktionen zu verwenden, die wenig bekannt sind. Aber natürlich gilt wie immer der perl-Leitsatz "there's more than one way to do it", und man kann die Aufgabe auch lösen, wenn man ein Array @sum verwendet.

Das Progrämmchen besteht aus drei Teilen: Variablendeklaration, Berechnung und Ausgabe. Die Kommentare unten markieren diese drei Teile. Ganz unten ist der vollständige Code zu finden, und hier im Text jeder Teil einzeln.

# Variablendeklaration
my %sum;


Wie schon beschrieben, wird bei der Berechnung einfach die Summe der drei ineinander geschachtelten Laufvariablen (i1, i2, i3) berechnet und als Index in das Array verwendet. Der auch aus C bekannte "++"-Operator addiert eins zum adressierten Array-Element. Die Schreibweise für die Schleifen finde ich besonders elegant: der perl-Operator ".." erzeugt eine Liste (oder auch Array genannt) von .. bis. Das funktioniert übrigens nur sauber in der Aufwärts-Richtung, es gibt grenzwertige Situationen, in denen man überrascht wird, wenn das bis kleiner ist als das von, also obacht hier!

# Berechnung
foreach my $i1 (1..6) {
    foreach my $i2 (1..6) {
        foreach my $i3 (1..6) {
            ++$sum{$i1+$i2+$i3};
        }
    }
}


Bei der Lösung solcher Aufgaben sollte man sich vorher überlegen, ob man den "brute force"-Ansatz wählt und einfach alle Varianten durchprobiert oder ob es Optimierungsmöglichkeiten gibt, mit denen man die Komplexität reduzieren kann. Bei drei Schleifen von 1 bis 6, mithin also 216 Schleifendurchläufen, ist das Programm selbst auf einem C-64 noch zu meinen Lebzeiten beendet ;)

# Ausgabe
print join("\n",
           map { sprintf("% 3d",$_) . " => $sum{$_}" }

               sort { $sum{$b} <=> $sum{$a} }
                    keys(%sum)
      ),"\n";


Bei der Ausgabe der Ergebnisse habe ich tief in die Trickkiste gegriffen und alles in einen einzigen "print"-Befehl gepackt. Dieser Befehl bekommt als Parameter das Ergebnis des "join"-Kommandos und hintendran wird noch ein einzelnes "\n", also ein Zeilenumbruch gehängt. Die Einrückungen habe ich so gewählt, dass man genau sieht, was zusammengehört.

print join("\n",
           map { sprintf("% 3d",$_) . " => $sum{$_}" }
               sort { $sum{$b} <=> $sum{$a} }
                    keys(%sum)
      ),"\n";


Der "join"-Befehl selbst nimmt zwei Parameter: das Verbindungszeichen "\n" und ein Array von Strings (man kann in perl alles als String verwenden, selbst Zahlen - es gibt keine strikte Unterscheidung, wie man ein "skalares Element" verwenden darf).

map { sprintf("% 3d",$_) . " => $sum{$_}" }
   
sort { $sum{$b} <=> $sum{$a} }
         keys(%sum)


Das "Array von Strings", das "join" bekommt, wird von einem "map"-Befehl dynamisch erzeugt. "map" ist eigentlich ein versteckter Schleifenoperator und erzeugt aus einer Funktion und einem Array als Parameter wiederum ein Array. Dies ist eines der faszinierenden Features von perl, die ich oben erwähnt hatte: "map" nimmt als Parameter eine namenlose Funktion, die man in {} schreibt.

In dieser Funktion, genau wie im folgenden Befehl "sort" gibt es Pseudo-Variablen, die nur innerhalb dieser Funktion benutzbar sind. Bei "map" ist es die Variable "$_", die das aktuell bearbeitete Array-Element beinhaltet. Dieses Element ist der Index des Arrays und bedeutet hier eine spezielle Würfelsumme. Mit "sprintf" formatiere ich diese Zahl rechtsbündig, dahinter hänge ich mit dem Stringoperator "." noch ein bißchen Schmuck als Trennzeichen an und danach noch die tatsächliche Anzahl an Möglichkeiten, diese Würfelsumme zu werfen.

sort { $sum{$b} <=> $sum{$a} } keys(%sum)

Jetzt kommt noch ein weiteres cooles Feature von perl: es gibt einen eingebauten Befehl zum Sortieren von Arrays, und diesem Befehl kann ich wiederum in einer namenlosen Funktion mitgeben, wie ich die Sortierung wünsche. Da ich beim Sortieren immer zwei Elemente vergleichen muss, liefert mir perl bequemerweise zwei Pseudovariablen, die mit "$a" und "$b" bezeichnet werden. Diese Variablen sind nur innerhalb dieses "sort"-Befehls bekannt und beeinflussen eventuell "außerhalb" deklarierte Variablen "$a" und "$b" nicht!

Sortiert wird absteigend nach der Häufigkeit, mit der dieser Würfelwurf vorkommen kann, also nach dem Inhalt des Arrays an dieser Stelle, und der "<=>"-Operator erzählt mir, welcher der beiden Werte kleiner bzw. größer ist. Dies ist ein sogenannter "ternärer" Operator, der mir je nach Vergleichsergebnis -1, 0 oder 1 liefert (also drei mögliche Ergebnisse, deshalb "ternär").

Solche verketteten Ausdrücke in perl muss man also von hinten nach vorne lesen, damit man den Sinn versteht.

Nochmal aus dieser Sichtweise betrachtet: ich sortiere das Array nach der Häufigkeit und übergebe das sortierte Array an "map", um dort die einzelnen Array-Elemente hübsch lesbar zu machen. Mit "join" verknote ich alle diese aufgehübschten Array-Elemente, so dass "print" einen einzelnen String ausgeben kann.

Und dies ist das gesamte Skript ohne Erklärungen dazwischen:

#!/usr/bin/perl -w


# Variablendeklaration

my %sum;

# Berechnung

foreach my $i1 (1..6) {
    foreach my $i2 (1..6) {
        foreach my $i3 (1..6) {
            ++$sum{$i1+$i2+$i3};
        }
    }
}
 

# Ausgabe
print join("\n",
           map { sprintf("% 3d",$_) . " => $sum{$_}" }
               sort { $sum{$b} <=> $sum{$a} }
                    keys(%sum)
      ),"\n";



[Update 20160930: Schleife von for auf foreach geändert, Einrückungen bei print zur Verdeutlichung]

16.09.2016

Linux kernel patch update skript

Ich bin ein großer Freund von "Selber machen" - zumindest am Computer. In der echten Welt hab ich eher so zwei linke Hände ;)

Das resultiert darin, dass mein Server zuhause mit einem selbst gelöteten Linux arbeitet, nämlich mit "Linux from Scratch", bei dem man alle Pakete selbst als Quelltext herunterlädt und installiert.

Zuerst hatte ich mal SuSE verwendet, aber die Paketverwaltung und die Abhängigkeiten waren mir dann zu viel geworden. Also dann: alles selbst kompilieren und genau sehen, welche Abhängigkeiten unbedingt nötig sind und welche nicht. Bei vorgefertigten Linux-Distributionen wird eigentlich immer das maximale Kunstwerk installiert, und genau das wollte ich nicht.

Diese DIY-Methode hat auch den Vorteil, dass man immer genau die gewünschte Version von Software hat, z.B. beim Kernel auch immer die neueste, die dann z.B. aktuelle Hardware unterstützt.

Ich will hier ein kleines Skript vorstellen, das ich mir vor längerer Zeit als Hilfsmittel gebastelt habe, um den Linux-Kernel immer auf dem aktuellen Stand zu halten.

Das Skript lädt eine neue Version in Form der Differenzdatei herunter, wendet dann diesen einen oder auch mehrere Patches an und speichert das Ergebnis wieder als neuestes Kernelpaket. Das kann man dann auch gleich kompilieren und dem Bootload unterjubeln (dazu hab ich ein separates Skript).

In blau hab ich ein paar Bemerkungen dazugeschrieben; das Skript ist aber eigentlich selbsterklärend - finde ich ;)

#!/bin/sh -e

msg() { echo "$*" 1>&2; }
err() { msg "$*"; exit 1; }


# dies ist die Basisversion des Kernels
base="${1:-4.7}"
# "von" Version
old="${2:-0}"

# "nach" Version
new="${3:-4}"
v="${base}.${old}"
latest="linux-${base}.${new}"

# die Files heißen 4.7 und nicht 4.7.0
test "${old}" = "0" && v="${base}"
test "${new}" = "0" && latest="linux-${base}"



url="https://cdn.kernel.org/pub/linux/kernel/v4.x"
wget="wget -q --no-check-certificate"

kl="linux-${base}.${new}"
ext="xz"
source="/sources"

# nix mehr zu tun, schon da
test -f "${kl}" && err "# Kernel ${kl} exists"

msg "unpack linux-${v}"
work=$(mktemp -d -p "${source}")
cd "${work}"
tar xf "${source}/linux-${v}.tar.${ext}"
mv "linux-${v}" "${latest}"

# wenn der alte Kernel 0 ist, reicht der neueste Patch allein
if [ "${old}" = "0" ]
then
    patch="patch-${base}.${new}.${ext}"
    test -f "${patch}" || ${wget} "${url}/${patch}"
    test -f "${patch}" || err "! download ${patch} failed"
    msg "apply ${patch}"
    xz -cd "${patch}" | patch -d "${latest}" -Nstp1
else

# ansonsten jeden patch von x nach x+1 holen
    while [ "${old}" -lt "${new}" ]
    do
        next=$(expr "${old}" + 1)
        ipatch="patch-${base}.${old}-${next}.${ext}"
        test -f "${ipatch}" || ${wget} "${url}/incr/${ipatch}"
        test -f "${ipatch}" || err "! download ${ipatch} failed"
        msg "apply ${ipatch}"
        xz -cd "${ipatch}" | patch -d "${latest}" -Nstp1
        rm -f "${ipatch}"
        old="${next}"
    done
fi
rc="${?}"

if [ ${rc} -eq 0 ]
then
    msg "pack ${latest}"
    egrep "^(VERSION|PATCHLEVEL|SUBLEVEL) = " "${latest}"/Makefile 2>/dev/null
    tar cf - "${latest}" | xz -9 > ../"${latest}.tar.${ext}"
    msg "cleanup tmp"
    cd ..
    rm -fr "${work}"
else
    err "! patch failed, rc=${rc}"
fi

25.06.2016

Zahlensysteme umwandeln in perl

Angeregt durch eine Diskussion mit einem Kollegen vor einiger Zeit, und jetzt nochmal in einem Facebook-Kommentar, habe ich ein kleines Perl-Skript geschrieben, mit dem man Zahlen von einem Zahlensystem in ein anderes umrechnen kann.

Dabei können sowohl Ausgangs- als auch Zielsystem beliebige Basis haben, man muss beim Aufruf angeben, von wo nach wo umgewandelt werden soll. Im Moment funktioniert es nur bis Basis 16. Wer größere Werte braucht, findet leicht die eine Stelle im Code, die geändert werden muss ;). In Vorlesungsskripten steht dann üblicherweise " ... left as an exercise for the reader".

Als kleines Bonbon ist es auch möglich, nicht nur die Zahlen auszugeben, sondern alternativ auch die ASCII-Zeichen, die diesen Werten entsprechen.

Damit kann man dann lustige, leicht unverständliche Kommentare schreiben wie

> 1000010 1101001 1101110 1100001 1100101 1110010 1100011 1101111 1100100 1100101 1101001 1110011 1110100 1100110 1110101 1100101 1110010 1010111 1100101 1101001 1100011 1101000 1100101 1101001 1100101 1110010 100001
>  01001110 01101001 01111000 00100000 01100100 01100001 00101100 00100000 01001000 01100101 01111000 00100000 01101001 01110011 01110100 00100000 01100110 11000011 10111100 01110010 00100000 01000110 01100001 01110101 01101100 01100101 01101110 01111010 01100101 01110010 00100001
 >  53 65 6c 62 73 74 76 65 72 73 74 61 65 6e 64 6c 69 63 68 2c 50 72 6f 67 72 61 6d 6d 69 65 72 65 72 73 69 6e 64 66 61 75 6c 3b 2d 29

So wird es benutzt:

$ ./code.pl -i16 -o2 48 61 6c 6c 6f
# out: 1001000 1100001 1101100 1101100 1101111

$ ./code.pl -a -i16 -o2 48 61 6c 6c 6f
# asc: Hallo

$ ./code.pl -a -i2 -o16 01101000 01100001 00100000 01101000 01100001
# asc: ha ha

$ ./code.pl -i10 -o2 1 2 3 4 5
# out: 1 10 11 100 101


#!/usr/bin/perl -w

use strict 'vars';
use strict 'refs';

use Getopt::Std;

use vars qw($opt_i $opt_o $opt_a $opt_v);

my ($in,$out,$inbase,$outbase,$ok);
my $chars="0123456789abcdef";

sub from {
    my $v=0;
    if ($inbase==1) {
        $v=ord($_[0]);
    }
    else {
        print "# c: " if ($opt_v);
        for my $c (split(//,$_[0])) {
            print ".$c" if ($opt_v);
            my $t=index($chars,lc($c));
            $v=($v*$inbase)+$t;
        }
        print " ($v)\n" if ($opt_v);
    }
    return $v;
}

sub to {
    my $c="";
    if ($outbase==1) {
        $c=chr($_[0]);
        print ".$c" if ($opt_v);
    }
    else {
        my $v=$_[0];
        while ($v>0) {
            my $t=$v%$outbase;
            $c=substr($chars,$t,1).$c;
            $v=int($v/$outbase);
            print ".$v%$t" if ($opt_v);
        }
        print " ($c)\n" if ($opt_v);
        $c=" ".$c;
    }
    return $c;
}

getopts("avi:o:");
$ok=0;
my $asc="";

if ($opt_i) { $inbase =$opt_i; $ok|=1; }
if ($opt_o) { $outbase=$opt_o; $ok|=2; }
if ($ok!=3) {
    print STDERR "# unknown base!\n";
    exit(1);
}

if ($#ARGV>=0) { $in=join(" ",@ARGV); }
else { $in=<>; }

print "# from: $inbase\n"  if ($opt_v);
print "# to  : $outbase\n" if ($opt_v);

if ($inbase==1) {
    $in=join(" ",split(//,$in));
}
foreach my $word (split(/\s+/,$in)) {
    my ($c,$a);

    print "# w: $word\n" if ($opt_v);
    $c=from($word);
    print "# c: $c\n" if ($opt_v);
    $a=to($c);
    $asc.=chr($c);
    $out.=$a;
}

print "# out:$out\n" unless ($opt_a);
print "# asc: $asc\n" if ($opt_a);

09.09.2014

Signierten Code mit Java erstellen

Man hat's nicht leicht, mit den ganzen Updates Schritt zu halten ... Dauernd Sicherheitslücken in Windows, Flash, Java, sogar in Android findet sich die eine oder andere Lücke.
Um den Erfolg der Updates zu überprüfen, insbesondere für Flash und Java, hatte ich mir vor langer Zeit eine ganz primitive Webseite erstellt, die mir die aktuellen Versionsnummern der installierten Plugins anzeigen kann.

Seit Version 1.7 der Java Runtime werden aber lästige Warnungen ausgegeben, dass demnächst nur noch signierter Code ausgeführt wird, dass Code, der von http- und nicht https-Schema nachgeladen wird, als unsicher betrachtet wird, und überhaupt lassen die Sicherheitseinstellungen der JRE demnächst gar nix mehr zu.

Also hab ich mich mal drangesetzt und aus meiner .class-Datei eine .jar-Datei gemacht, die ich mit meinen eigenen, selbst erstellten Key signiert habe. Das ist aber natürlich nur die halbe Miete, weil man auch der JRE im Browser klarmachen muss, dass der self-signed Code auch wirklich zulässig ist. Man muss also in der policy-Datei auch noch Änderungen vornehmen. Hier ist der ganze Ablauf zusammengestellt, den ich durchlaufen musste, bis meine simple JRE-Versionsabfrage mit Firefox 31, Chrome 36 und JRE 1.8.0.11 wieder funktioniert hat.

Hier ist zunächst der Code für die Ausgabe der JRE-Versionsnummer:
import java.applet.Applet;
import java.awt.Color;
import java.awt.Label;
public class JavaVersionDisplayApplet extends Applet
{
  private Label V;
  public JavaVersionDisplayApplet() {
    this.setBackground(Color.pink);
    V = new Label(" Java Version: " + System.getProperty("java.version") + " from " + System.getProperty("java.vendor"));
    this.add(V);
  }
}
Um diesen Code zu kompilieren, muss das Java Development Kit mit dem javac-Compiler installiert sein. Da Java einigermaßen kompatibel zu alten Versionen ist, kann man auch mit einer aktuellen JRE wie 1.8 noch Code ausführen, der mit Version 1.4 kompiliert wurde. Eine Krankheit von Java ist, dass der Dateiname exakt dem Klassennamen in der Datei entsprechen muss, hier also "JavaVersionDisplayApplet.java".
javac JavaVersionDisplayApplet.java
Als nächstes müssen noch zwei Dinge vorbereitet werden: ein Schlüsselpaar für das Signieren und eine Manifest-Datei, in der der Java-Code etwas genauer beschrieben wird, also etwas poetischer gesagt: Meta-Daten.

Zunächst also das Schlüsselpaar mit dem Programm keytool erzeugen, dass bei der JRE und beim JDK mitgeliefert wird.
keytool -genkeypair -alias thomas -keystore seeling.jks -dname "CN=Thomas Seeling, OU=System Administration, O=Kleintierpraxis Berstadt, L=Berstadt, ST=Hessen, C=DE" -keypass Password1 -storepass Password2
Im weiteren verwende ich immer den Aliasnamen "thomas" für den Schlüssel. Je nach Zusammenhang ist damit entweder der private oder der öffentliche Schlüssel gemeint.

Als nächstes benötige ich noch den öffentlichen Schlüssel separat, damit ich ihn in meinem Browser importieren kann. Dieser Schritt würde entfallen, wenn ich den Schlüssel von einer öffentlichen Root CA signieren lasse oder wenn ich mir selbst eine Root CA gebaut habe und den öffentlichen Schlüssel dieser Root CA schon in meinem Browser hätte.
keytool -export -keystore seeling.jks -alias thomas -storepass Password2 -file tseeling.cer
Ich mag meine Schlüssel lieber in base64-kodiert, also wandle ich die binäre .cer-Datei noch in eine PEM-Datei um:
openssl x509 -in tseeling.cer -inform DER -out tseeling.pem -outform PEM
Weil dies "nur" ein öffentlicher Schlüssel ist, benötige ich nach dem Export aus dem Java-Keyring kein Passwort mehr.
Diese PEM-Datei kann ich nun bequem über ein Transportmittel wie Email, ftp etc. zu meinem Browser-PC übertragen und dort importieren.

Desweiteren benötige ich die schon erwähnte Manifest-Datei, die mit in die .jar-Datei gepackt und signiert werden muss. Diese Datei sieht bei mir so aus:
Permissions: sandbox
Codebase: https://admin.moeller-seeling.local/*
Application-Name: JavaVersion
Der "Application-Name" ist hier nur "Schmuck am Nachthemd" und wird für eine Infobox benötigt, in der die JRE dem Benutzer anzeigt, wer da gerade ausgeführt werden soll.

Wenn das alles zusammengestellt ist, wird nun in zwei Schritten aus der kompilierten Java-Datei und dem Manifest zunächst eine .jar-Datei erzeugt und dann diese Datei mit dem zuvor erstellten privaten Schlüssel signiert.
jar -cfm JavaVersion.jar JavaVersion.txt JavaVersionDisplayApplet.class
jarsigner -keystore /opt/jdk/jre/lib/security/seeling.jks -keypass Password1 -storepass Password2 -verbose JavaVersion.jar thomas 

 Puh, fast geschafft! Jetzt noch diese .jar-Datei in einem kleinen HTML benutzen und vom Webserver ausliefern lassen. Darauf gehe ich nur ganz kurz ein, hier ist ein Beispiel für den HTML-Code:
<table border="1">
<tr><th> The version and vendor from the JRE</th>
<td align="center">
<applet height="60" alt="Browser has Java disabled"
hspace="22" width="440"
archive="JavaVersion.jar"
code="JavaVersionDisplayApplet.class">
</applet>
</td></tr>
</table>
Soweit, so gut. Das war der erste Schritt auf dem Webserver.

Auf dem Browser-PC sind auch noch kleine Schritte nötig, damit man ein self-signed .jar ausführen darf:
man muss in die Security-Policy einen Eintrag machen, dass signierte Dateien von bestimmten Programmierern erlaubt sind, und man muss den öffentlichen Schlüssel dieses Programmierers in den Standard-Keystore der JRE importieren, die der Browser im Java-Plugin verwendet, oder alternativ einen anderen Keystore angeben, in dem dieses Zertifikat enthalten ist.

Auf Windows findet sich das unter "%ProgramFiles(x86)%\java\jre8\lib\security\java.policy" (für die Java-Version 1.8).

Zu dieser (i.a. schon vorhandenen Datei) im Klartextformat habe ich folgende Einträge hinzugeführt:

keystore "file:${java.home}/lib/security/seeling.jks", "jks";
grant signedBy "thomas" {
  permission java.security.AllPermission, signedBy "thomas";
};
Zu der Liste der vertrauenswürdigen Zertifikate habe ich meinen öffentlichen Schlüssel hinzugefügt, den ich weiter oben als PEM-Format gespeichert hatte.
keytool -importcert -noprompt -trustcacerts -alias thomas -file tseeling.pem -keystore "%ProgramFiles(x86)%\java\jre8\lib\security\cacerts" -storepass changeit
Das Passwort der Schlüsseldatei "cacerts" ist üblicherweise "changeit". Natürlich ändert es nie jemand ;)

So, nachdem also nun ganz viele kleine Gemeinheiten geschafft sind, müsste im Browser ein Applet funktionieren, das die JRE-Versionsnummer ausgibt. Wenn man auch nur einen dieser Schritte weglässt, klappt es nicht und entweder der Browser oder die JRE beschimpfen mich, dass alles ganz schröcklich unsicher ist.

28.02.2014

LTE-Empfangsqualität abfragen (Diagnose-Logfile)

Neulich hab ich mich beschwert, dass man beim alten Modell des LTE-Routers (Telekom LTE 1 bzw. Huawei B390s) nicht die Empfangsqualität abfragen kann.

Und es geht doch! Zwar ziemlich versteckt in den Tiefen der "Erweiterten Einstellungen", aber man kann es doch abfragen.

Wie üblich in Ihrem freundlichen Service-Blog auch gleich wieder die Möglichkeit, diese Daten automatisiert per Skript abzufragen und damit ggfs. regelmäßig zu protokollieren.

Der übliche Trick: mit dem ersten wget-Aufruf geschieht der Login, das Session-Cookie wird gespeichert, und mit dem zweiten Aufruf wird dann die interessante Seite abgerufen.
#!/bin/sh

PATH=/opt/bin:/opt/sbin${PATH:+:$PATH}

H=vodafonemobile.cpe
C=/tmp/cookies.txt
U1="http://"$H"/login.cgi"
U2="http://"$H"/diagnosis_export.cgi?configId=$(date +%s)&FileName=diagnosis.txt"

wget --save-cookies "$C" \
     --keep-session-cookies \
     -q --tries=1 \
     -O /dev/null \
     --post-data 'Username=admin&Password=yourpw' \
     "$U1"

wget --load-cookies "$C" \
     -q --tries=1 \
     -O - \
     "$U2" | \
awk '
{if (NR>=5&&NR<=42){print}}
'

rm -f "$C"
Der Output sieht dann in etwa so aus wie da unten.

Besonders interessant sind die Zeilen bei "RSRP" und "RSRQ". Mit den Werten -90dBm und -8 dB zeigt mir der Router 4 von 5 Balken an. Laut Telekom-Auskunft sind Werte bis -60/-2 möglich, das wäre super-super-optimal. Wenn man das Gerät in der Ausrichtung verändert, darf  RSRP ruhig etwas schlechter werden, falls man dadurch RSRQ verbessern kann. Optimal wäre, wenn beide Werte größer werden (-90 --> -60, -8 --> -2).

Auch sehr hübsch: man erfährt den Funkmast, mit dem der Router verbunden ist, und zwar mit dem Parameter "CellId".

~~~~~~Product Information~~~~~~
003    Model:               B390s-2                    PASS
004    Software Version:    V200R001C35SP12            PASS
005    Hardware Version:    B390-B390RW2A Ver.C        PASS
006    SN:                  4UA5TC1110400163           PASS
007    IMEI:                354637040077555            PASS
008    LAN MAC Address:     F4:C7:14:**:**:**           PASS
009    WLAN MAC Address:    F4:C7:14:**:**:**           PASS
010    Check AT-Port:        Available                  PASS

~~~~~~LTE State~~~~~~
011    Dialing Mode:                                   PASS
012    APN:                  internet.home              PASS
013    DNS:                  8.8.8.8                    PASS
014    PDN Type:             IPv4                       PASS
015    Service Status:       Normal Service            PASS
016    Connection Status:    LTE STATE CONNECTED       PASS
017    Frequency:            816000 kHz               PASS
018    Bandwidth:            10MHz                    PASS
019    CellId:               392                       PASS

020    IP Address:           2.162.**.***             PASS
021    RSRP:                 -90dBm                    PASS

022    RSSI:                 -66dBm                    PASS
023    RSRQ:                 -7dB                      PASS 

024    Roam:                  no                       PASS
025    Antenna State:        Built-In                  PASS

~~~~~~DHCP~~~~~~
026    Gateway IP Address:    192.168.42.1           PASS
027    DHCP Enabled:          false                  PASS
028    Subnet Mask:          255.255.255.0          PASS
029    DHCP_ip_pool_start:    N/A                    PASS
030    DHCP_ip_pool_end:     N/A                     PASS
031    DHCP_lease_time(s):    N/A                     PASS

~~~~~~SIM/PIN State~~~~~~
032    sim_state:             SIM Card detected       PASS
033    PIN code:             READY                    PASS
 

18.01.2014

Erbsen zählen - Perl und CGI - Dritter Teil

Im dritten Teil beschreibe ich nun das Interessanteste: die Auswertung der Daten in Text- und Grafikform, deren Prinzip ich im ersten Teil und Sammlung im zweiten Teil beschrieben habe.

Genau wie schon früher beschrieben, verwende ich das Grafikmodul GD für Perl. Dort kann man ein paar Daten hineinstecken und dann mit einem Aufruf unterschiedliche Schaubilder erzeugen lassen, als "line"-Graph, "bar"-Graph und viele andere. Der Output ist dann HTTP-konform eine Grafikdatei im gewünschten Format, z.B. PNG oder GIF, zusammen mit einer Headerzeile und dem passenden MIME-Typ.

Beim Design gibt es zwei grundsätzliche Überlegungen:
  1. Die Logdateien sammeln Daten pro überwachtem Hostname, ich muss also noch eine Summe bilden, um den Verbrauch pro Host im Verhältnis zum Rest beurteilen zu können.
  2. Ich würde gern sowohl eine Tabelle mit den aktuellen Daten sehen als auch einen Graphen, aus dem man Trends ablesen kann. Ich brauche also im CGI-Skript eine Fallunterscheidung - Text oder Grafik.
Hier kommt das komplette CGI-Skript. An ein paar Stellen füge ich Kommentare in rot oder blau ein, wenn es etwas zu erklären gibt.

Eine grundsätzliche Bemerkung vorneweg: die Datenübergabe erfolgt immer in Form von Referenzen (in C wären das Zeiger auf Datenstrukturen), deshalb verwende ich im Skript i.a. auch gleich Variablen, Arrays und Hashes, die Referenzen enthalten, damit die Übergabe an GD nicht noch unnötig Datenformate umwandeln muss.

Um das CGI-Skript bequem testen zu können, habe ich einen "Testmodus" eingebaut. Man kann generell Skripte, die das CGI-Modul verwenden, auch auf der Kommandozeile aufrufen. Die Argumente, die normalerweise in der Request-URL nach dem "?" folgen, schreibt man einfach als Pärchen mit "name=wert" hinter den Skriptnamen in die Kommandozeile, wie man hier am Beispiel sieht.
# ./accounting.pl scale=1000 testmode=99 xmax=400 ymax=300
Bei diesem Testmodus ist noch zu bedenken, dass natürlich die Umgebungsvariablen des Webservers nicht gesetzt  sind (QUERY_STRING, PATH_INFO, die SSL_*-Variablen, wenn das Skript mit https aufgerufen wurde, usw.). Ggfs. müsste man diese Variablen manuell mit passenden gefälschten Inhalten setzen, damit das Skript an den entsprechenden Stellen sinnvolle Werte bekommt.

Dem Skript kann man einen Parameter "filter" übergeben, um nur einen ganz bestimmten Hostnamen auszufiltern. Deshalb gibt es eine etwas unübersichtliche Fallunterscheidung, ob ein Filter gesetzt ist oder nicht.

Mit "mode=1" wird eine Grafik in voller Größe mit mehreren Graphen und der Gesamtsumme angezeigt; "mode=16" erzeugt eine Tabelle in Textform mit einer Zeile pro Hostname in der Logdatei und hinter jedem Hostnamen in einer Extraspalte eine wönzig kleine Grafik mit der Gesamtsumme und dem Verbrauch dieses Hosts.

#!/usr/bin/perl -w

use strict 'vars';
use strict 'refs';

use GD::Graph;
use GD::Graph::lines;
use GD::Graph::bars;
use GD::Graph::hbars;

# Die GD-Module zeichnen die verschiedenen Typen von Graphen
use CGI qw(:standard);

#use lib "/usr/local/bin";

my @data;

# Array mit Beschriftung, x-Skala und ein oder mehreren y-Datenpunkten
my %hosts;

# Array zum Speichern der Daten pro Hostname
my $graph;
my $format;
my ($xmax,$ymax);

# Größe des auszugebenden Bilds
my $query;
my $range;
my $filter;

# Regexfilter für Hostnamen
# mode=0 full graph mode all hosts
# mode=9 sum only graph
# mode=16 text mode
# mode=99 debug text mode
my $mode;
my @x;
my @y1;

# y1=summierte Daten IN
my @y2;

# y2=summierte Daten OUT
my $scale;
#my $offset;
my $title="LTE volume statistics";

# X-Beschriftung
# werte1
# werte2
# ...
@data=(
);

%hosts=(
# "dummy.moeller-seeling.local" => { "IN " => [ 0 ], "OUT" => [ 0 ] }
);

my $max=0;
my $lines=0;

# logfile inbound, outbound
my ($login,$logout);


$query=new CGI();
$mode=$query->param( 'mode' ) || 0;
$range=$query->param( 'range' ) || "60";
#$offset=$query->param( 'offset' ) || "0";
$scale=$query->param( 'scale' ) || "2500";
$filter=$query->param( 'filter' ) || "";
$xmax=$query->param( 'xmax' ) || "800";
$ymax=$query->param( 'ymax' ) || "600";
$login=$query->param( 'infile' ) || "/var/log/lte-acct.in";
$logout=$query->param( 'outfile' ) || "/var/log/lte-acct.out";
$graph = GD::Graph::lines->new($xmax, $ymax);
$format = $graph->export_format;

# 20140108-110202 IN  i9100.moeller-seeling.local              91M
# 20140108-110202 IN  lifetab.moeller-seeling.local            42M
# 20140108-110202 IN  nexus.moeller-seeling.local              8748K
# 20140108-110202 IN  vettie68.moeller-seeling.local           143M

#my $d;

sub readfile {
  my ($file,$tag,$y)=@_;

  if (open(F,"<",$file)) {
    my ($dt,$old)=("","");

      while (<F>) {
      my ($name,$num,$unit,$hm,$m);

      chomp;
# 20140107-184506 IN  lifetab.moeller-seeling.local            0
# dieser Regex zerlegt eine Zeile, $tag ist IN oder OUT      if ($filter) {
        ($name,$num,$unit,$dt,$hm,$m)=($filter,$7,$8,"$1$2$3-$4$5$6","$4$5",$5)
          if (/^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2}) $tag\s+$filter.*?\s*(\d+)([KMG]?$)/);
      }
      else {
        ($name,$num,$unit,$dt,$hm,$m)=($7,$8,$9,"$1$2$3-$4$5$6","$4$5",$5)
          if (/^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2}) $tag\s+([a-z.-][0-9a-z.-]+)\s*(\d+)([KMG]?)/);
      }
      if ($name && defined($num) && defined($unit)) {

        ++$lines;
        if ($unit eq "")  { $num>>=10; }
#       if ($unit eq "K") { $num<<=10; }
        if ($unit eq "M") { $num<<=10; }
        if ($unit eq "G") { $num<<=20; }
# alle Zahlenwerte sind in KB        if ($num>1000) {
          if ($ymax<100) { push(@x,$m); }
          else { push(@x,$hm); }
          $num>>=10;
          push(@{$hosts{$name}->{$tag}},$num);
if ($mode==99) { print STDERR "# <$_>\n# $hm $tag $filter $num $unit | $max\n"; }

# in den Logfiles stehen kumulierte Daten pro Host
# alle Werte vom selben Timestamp addieren für Gesamtsumme
          if ($dt eq $old) { $num+=pop(@{$y}); }
#         else { $num+=$offset; }
          push(@{$y},$num);
          $old=$dt;

# Timestamp aufheben für Vergleich mit nächster Zeile
          if ($max<$num) { $max=$num; }

# Maximum merken für y-Skalierung
if ($mode==99) { print STDERR "# @{$y}\n"; }
        }
      }

    }
    close(F);
  }
}

readfile($login, "IN ",\@y1);
readfile($logout,"OUT",\@y2);

if ($mode==99) {
  local($,=", ");
  foreach my $h (keys(%hosts)) {
    print STDERR "# h $h\n";
    print STDERR "# i @{$hosts{$h}->{'IN '}}\n";
    print STDERR "# o @{$hosts{$h}->{'OUT'}}\n";
  }
  print STDERR "# Sum\n";
  print STDERR "# i @y1\n";
  print STDERR "# o @y2\n";
}


# maximal die letzten $range werte aus der Datei darstellen


if ($range>0) {
  splice(@x,0,-$range);
  splice(@y1,0,-$range);
  splice(@y2,0,-$range);
  $lines=$range;
}
$data[0]=\@x;

# mode=9 nur die Summe
# mode !=9 alle Hostnamen auch einzeln zeigen
if ($mode!=9) {
  foreach my $h (keys(%hosts)) {

# inbound ist interessanter
# man könnte aber in und out anzeigen
    push(@data,$hosts{$h}->{'IN '});
#   push(@data,$hosts{$h}->{'OUT'});
  }
}
# sum of all hosts IN
push(@data,\@y1);
# sum of all hosts OUT
#push(@data,\@y2);

$max=$scale*int($max/$scale+1);

# sinnvolle obere y-Grenze ausrechnen

if ($mode==99) { print STDERR "# max=$max\n"; }

# Daten übergeben$graph->set(
  x_label       => 'Volume Date/Time, ' . $lines . " samples",
  y_label       => 'max ' . $max . ' GB',
  title         => 'LTE volume (GB)',
  y_max_value   => $max,
  y_tick_number => 20,
  y_label_skip  => ($ymax<200) ? 5: 2,
  x_labels_vertical => 1,
  x_label_skip  => $lines / 12,
) or die $graph->error;

# optional die Farben selbst festlegen
#$graph->set( dclrs => [ qw(green lred blue cyan) ] );

# gibt es überhaupt daten?

if ($lines>0) {
  if ($mode<16) {

# bilddatei erzeugen
# binmode ist nur mit windows-webserver wichtig
    print header("image/$format");
    binmode STDOUT;
    print $graph->plot(\@data)->$format();
  }

# textmodus output als tabelle
  elsif ($mode==16) {
    print CGI::header();

    print <<__HEADER__;
<HTML>
<HEAD>
<TITLE>$title</TITLE>
<META http-equiv="refresh" content="300" />
<style type="text/css">
th {
  padding : 3px;
  text-align : left;
}
td {
  padding : 3px;
  text-align : right;
}
</style>
</HEAD>
__HEADER__

print "<!-- ";
print join( " -->\n<!-- ",
           map( "$_ => $ENV{$_}", sort(keys(%ENV)))
          );
print "-->\n";

    print <<__BODY__;
<BODY>
<H1>$title</H1>
<table border="1">
<tr>
<th> Hostname </th>
<th> Download </th>
<th> Upload </th>
</tr>
__BODY__

    foreach my $h (keys(%hosts)) {
      print "<tr>\n";
      print "<th> $h</th>\n";
      print "<td>",pop(@{$hosts{$h}->{'IN '}})," MB </td>\n";
      print "<td>",pop(@{$hosts{$h}->{'OUT'}})," MB </td>\n";

# hinter jeden hostnamen einen wönzigen Detailgraphen anzeigen
if (defined($ENV{'HTTP_HOST'})) {
      print "<td><IMG SRC=http://",$ENV{'HTTP_HOST'},$ENV{'SCRIPT_NAME'},"?filter=$h&xmax=300&ymax=100&scale=",int($max/10),"</IMG></td>\n";
}
      print "</tr>\n";
    }
    print "<tr>\n";
    print "<th style='text-align:right'> &Sigma; </th>\n";
    print "<td>",pop(@y1)," MB </td>\n";
    print "<td>",pop(@y2)," MB </td>\n";
if (defined($ENV{'HTTP_HOST'})) {
      print "<td><IMG SRC=http://",$ENV{'HTTP_HOST'},$ENV{'SCRIPT_NAME'},"?mode=9&xmax=300&ymax=100&scale=",int($max/10),"</IMG></td>\n";
}
    print "</tr>\n";

    print <<__BODY__;
</table>
<DIV>
<HR/>
<ADDRESS>
ths, last changed 09.01.2014
</ADDRESS>
</DIV>
</BODY>
</HTML>
__BODY__

  }
}


[Update 20140122: Links zum 1. und 2. Teil eingefügt]

10.01.2014

Erbsen zählen - zweiter Teil

Nachdem ich also im 1. Teil das Grundprinzip des Erbsenzählens beschrieben habe, kommt hier ein bißchen mehr Skripting zum Einsatz, damit sich der Tag "Automation" auch lohnt ;)

Das Datensammeln an sich ist eine triviale Sache. Man braucht ein kleines Skript, das mit Hilfe von cron regelmäßig ausgeführt wird.

Ich habe einfach mal willkürlich festgelegt, dass es einmal stündlich laufen soll:

2 * * * * /usr/local/sbin/ip-accounting.sh -I /var/log/lte-acct.in -O /var/log/lte-acct.out >/dev/null 2>&1

Der Inhalt des Skripts sieht ein bißchen aufgebläht aus, weil ein paar Kommandozeilenargumente ausgewertet werden wollen. Die wichtigste Zeile im Skript habe ich nochmal extra rot markiert, das ist die eigentliche Datensammlung.
#!/bin/sh

PATH=/opt/sbin:/opt/bin:/usr/bin:/usr/sbin${PATH:+:$PATH}

bytesin=0
bytesout=0
while getopts "iI:oO:" opt
do
  case "$opt" in
  i) bytesin=1;;
  I) filein="$OPTARG";;
  o) bytesout=1;;
  O) fileout="$OPTARG";;
  esac
done
shift $(expr $OPTIND - 1)
if [ "$bytesin$bytesout$filein$fileout" = "00" ]
then
  bytesin=1
  bytesout=1
fi

dt=$(date +%Y%m%d-%H%M%S)

out=$(iptables -L INET_OUT -v | awk -v D="$dt" '{if(NR>2){printf"%s OUT %-40s %s\n",D,$7,$2}}'|sort)
if [ "$bytesout" -eq 1 ]
then
  echo "$out"
fi
if [ -n "$fileout" ]
then
  echo "$out" >> "$fileout"
fi

out=$(iptables -L INET_IN  -v | awk -v D="$dt" '{if(NR>2){printf"%s IN  %-40s %s\n",D,$8,$2}}'|sort)
if [ "$bytesin" -eq 1 ]
then
  echo "$out"
fi
if [ -n "$filein" ]
then
  echo "$out" >> "$filein"
fi
Und was genau macht diese rot markierte Zeile? Schauen wir mal genauer hin. Es sind mehrere Befehle in einer Zeile, verbunden mit dem Weiterleitungszeichen | (pipe). Diese Pipe verbindet den Output des Programms vorne mit dem Input des Programms dahinter.
iptables -L INET_IN  -v |

Der erste Befehl zeigt die bis dahin aufgelaufene Datenmenge für alle überwachten Systeme an. Wichtig ist der Switch -v, sonst bekomme ich die Datenmenge nicht zu sehen.
awk -v D="$dt" '{if(NR>2){printf"%s IN  %-40s %s\n",D,$8,$2}}' |

Die Ausgabe enthält allerdings ziemlich viel Kram, der mich gar nicht interessiert, deswegen verwende ich awk, um mir gezielt nur das zu extrahieren, was ich für meine Neugier brauche. Natürlich könnte ich normalerweise auch grep verwenden, um etwas zu suchen, aber weil ich gleichzeitig ein wenig programmieren will und einzelne Felder aus jeder Zeile des Outputs ausschneiden will, verwende ich ganz gern awk, statt grep mit cut o.ä. zu kombinieren. Spart ein wenig CPU ;)

Bei diesem Befehl benutze ich zwei eingebaute Fähigkeiten von awk: es zählt automatisch die Zeilennummern in der Variablen NR (number of records) mit, was ich verwende, um die ersten zwei Zeilen mit der Überschrift des iptables-Outputs wegzuwerfen, und es zerlegt automatisch die Zeile in "Felder" gemäß einem vorgegebenen Trennzeichen (üblicherweise Whitespace). awk arbeitet also den printf-Befehl nur ab der dritten Zeile des Inputs ab und zeigt davon nur das achte und das zweite Feld an (Hostname und Datenmenge)

Bei der Chain INET_OUT ist  übrigens  der Hostname im siebten Feld enthalten, und bei der Chain INET_IN im achten Feld, weil ich ja einmal nach "source" und einmal nach "destination" suche.

Randbemerkung: damit awk einen Timestamp mit ausgeben kann, gebe ich dem Aufruf von awk einen solchen vorgefertigten Timestamp auf der Kommandozeile mit dem Switch -v NAME=WERT mit. Das können nicht alle awk unter allen Unix-Varianten, m.W. wurde das mit dem "new awk" (nawk) unter SunOS eingeführt, GNU awk unter Linux kann es auf jeden Fall.
sort
Der letzte Befehl sortiert das ganze schlicht und einfach alphabetisch. Ich könnte auch iptables mit dem Switch -n verwenden, um die IP-Adressen zu sehen anstatt der Hostnamen. Dann wäre es angebracht, bei sort ebenfalls -n zu verwenden, um numerische Sortierung zu erzwingen.

Dieses Skript sammelt also stündlich Daten pro überwachtem Hostname. Die gesammelten Daten sehen dann so aus:
server:/share/www/cgi-bin # tail -n2 /var/log/lte-acct.in /var/log/lte-acct.out
==> /var/log/lte-acct.in <==
20140109-150202 IN  nexus.moeller-seeling.local              23M
20140109-150202 IN  vettie68.moeller-seeling.local           286M


==> /var/log/lte-acct.out <==
20140109-150202 OUT nexus.moeller-seeling.local              6752K
20140109-150202 OUT vettie68.moeller-seeling.local           13M

Wie man sieht, ist der Output menschenfreundlich und es wird immer eine halbwegs lesbare Größenordnung bei der Datenmenge angezeigt: KB, MB oder GB. Ohne Einheit sind "Byte" gemeint. Diese Kleinigkeit ist beim Programmieren einer kleiner Stolperstein gewesen ;)

Das Auswerte-Skript habe ich als CGI-Skript in Perl gelöst. Es kann sowohl eine Tabelle als auch eine Grafik erzeugen (mit dem Perl-Modul GD). Das folgt dann im nächsten Teil.

Am Ersten des Monats sollte man übrigens die Zähler wieder auf Null setzen:
iptables -L INET_IN -Z
iptables -L INET_OUT -Z
Ich habe schon ein Skript, das mir meinen Verbrauchszähler auf Null setzt, dort werde ich das noch mit einbauen.