Posts mit dem Label Skript werden angezeigt. Alle Posts anzeigen
Posts mit dem Label Skript 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();

07.04.2017

Selbstmodifizierender Code

Heute zeige ich mal ein kleines Stück Skriptcode, das eine eigentlich verpönte Programmiertechnik zeigt, nämlich "selbstmodifizierenden Code". Das bedeutet, dass zur Laufzeit erst Programmcode erzeugt und dann direkt ausgeführt wird.

Am besten funktioniert das natürlich in Skriptsprachen, und es muss auch eine Möglichkeit geben, das ausführende Organ, also den Skript-Interpreter, darauf hinzuweisen, dass es neuen Code gibt.

Das Beispiel, das ich vorstellen möchte, ist eher harmlos. Generell sollte man aber bedenken, dass es gefährlich sein kann, wenn man einem Programm die Möglichkeit gibt, sich selbst zu verändern. Diese Technik darf man also insbesondere nicht verwenden, wenn man keine oder wenig Kontrolle über die "Zulieferung" hat, also z.B. bloß nicht in Software für's Web wie CGI-Skripte oder so was.

Mein Artikel beschreibt die Abfrage des Tintenstands eines Druckers. Auf dem Display bzw. im Webinterface zeigt der Drucker sehr schön an, zu wieviel Prozent die Patronen gefüllt sind. Es gibt noch eine weitere Technik: nämlich die Abfrage mit SNMP ("simple network management protocol"). SNMP an sich ist erst mal ziemlich dämlich: es beschreibt nur, wie Geräte Informationen liefern. Welcher Art die gelieferten Informationen sind, kann aber total unterschiedlich sein. SNMP wird von Routern, Switches, Servern, PCs, Druckern, Firewalls, NAS, SAN, Monitoring und vielen anderen Geräten gesprochen, und jedes davon hat ein anderes Spektrum an Parametern.

Deshalb gibt es zu Geräten der diversen Kategorien Beschreibungsdateien, in denen niedergelegt ist, welche Daten ein Gerät liefern kann. Diese Beschreibungen werden gesammelt in MIBs ("Management Information Base"). Es gibt standardisierte MIBs, und manchmal liefern Hersteller, wenn sie nett sind, auch eigene MIBs mit den spezifischen Erweiterungen, z.B. für ein bestimmtes Druckermodell. MIBs werden in einer syntaktisch festgelegten Grammatik geschrieben, in ASN.1.

Zur Abfrage eines Geräts benötigt man ein Hilfsprogramm, das SNMP spricht und eine Netzwerkverbindung zum Ziel aufbauen kann. Für Linux installiert man dazu "net-snmp", das es als fertiges Paket (sourceforge binaries download) oder zum Selbstbauen gibt.

Für die Abfrage muss man ein paar merkwürdige, sehr lange Zahlen wissen. Manchmal kann es trickreich sein, genau herauszufinden, was man braucht. Man kann die MIB-Datei lesen (braucht etwas Übung) oder man versucht es mit einer Suchmaschine (so wie ich, wenn ich faul bin).

Weiter unten stelle ich das komplette Skript vor, aber ich zerlege einzelne Zeilen in ihre Einzelteile und zeige, wie sie funktionieren.

Das Skript zum Abfragen der Tintenstände verwendet sogar zwei verschiedene Techniken aus der Kategorie "selbstmodifizierender Code". Zum Einen verwende ich den "eval"-Befehl der Linux-Shell (die /bin/sh ist bei mir bash), und zum anderen baue ich ein winziges Perl-Skript mit Inhalten zusammen, die ich mir gerade vorher erst im Shellskript aus einem snmp-Befehl zusammengesammelt habe.

Die eigentliche Abfrage des Tintenstands funktioniert so wie im folgenden Schnipsel gezeigt. Zuerst der Befehl, dann die Ergebnisse mit meinem speziellen Drucker. Der Befehl "snmpwalk" liefert mir mehrere Ergebnisse mit einer Abfrage; dazu "wandert" der Befehl durch alle Unter-Ergebnisse der angegebenen Nummer 1.3.6.1.2.1.43.11.1.1.6.0, also .6.0.1, .6.0.2 usw.
Diese spezielle Abfrage liefert mir die Zuordnung, welche Nummer zu welcher Farbpatrone gehört.

# snmpwalk -v1 -c public 192.168.100.173 1.3.6.1.2.1.43.11.1.1.6.0
SNMPv2-SMI::mib-2.43.11.1.1.6.0.1 = STRING: "black ink"
SNMPv2-SMI::mib-2.43.11.1.1.6.0.2 = STRING: "yellow ink"
SNMPv2-SMI::mib-2.43.11.1.1.6.0.3 = STRING: "cyan ink"
SNMPv2-SMI::mib-2.43.11.1.1.6.0.4 = STRING: "magenta ink"

Als nächstes beschaffe ich mir die Tintenstände und den Maximalwert, damit ich prozentual berechnen kann, wie voll jede Patrone noch ist.

# snmpwalk -v1 -c public 192.168.100.173 1.3.6.1.2.1.43.11.1.1.9.0
SNMPv2-SMI::mib-2.43.11.1.1.9.0.1 = INTEGER: 231
SNMPv2-SMI::mib-2.43.11.1.1.9.0.2 = INTEGER: 94
SNMPv2-SMI::mib-2.43.11.1.1.9.0.3 = INTEGER: 210
SNMPv2-SMI::mib-2.43.11.1.1.9.0.4 = INTEGER: 174

# snmpwalk -v1 -c praxis 192.168.100.173 1.3.6.1.2.1.43.11.1.1.8.0
SNMPv2-SMI::mib-2.43.11.1.1.8.0.1 = INTEGER: 674
SNMPv2-SMI::mib-2.43.11.1.1.8.0.2 = INTEGER: 240
SNMPv2-SMI::mib-2.43.11.1.1.8.0.3 = INTEGER: 226
SNMPv2-SMI::mib-2.43.11.1.1.8.0.4 = INTEGER: 241
"black" ist also noch zu 231/674 voll, umgerechnet ca. 34 %.

So, damit haben wir das Werkzeug, um Dinge zu tun. Wäre schön, wenn das noch mit etwas mehr Bequemlichkeit verbunden wäre. Also packen wir all das in ein Skript.

Dieses Skript muss also mehrere SNMP-Abfragen durchführen, sich die Werte merken, Prozente ausrechnen und den ganzen Spaß dann wieder ausgeben.

Die Technik der folgenden Zeile verwendet den bash-Befehl "eval", um Shell-Befehle in diesem Moment auszuführen.
eval $(snmpwalk -v1 -c praxis 192.168.100.173 \
  1.3.6.1.2.1.43.11.1.1.6.0 | perl -ne 'print "c[$1]=$2\n" \
if(m!SNMPv2-SMI::mib-2.43.11.1.1.6.0.(\d) = STRING:\s+"(\w+) ink"!i);')

Der Shell-Befehl, den das perl-Skript zusammenbaut, nimmt die Zahlenwerte aus der SNMP-Abfrage und erzeugt daraus die Zuweisung der Abfragewerte zu Shellvariablen in einem Array. Dazu nimmt der perl-Interpreter die Ausgabe des snmpwalk-Befehls zeilenweise und untersucht jede dieser Zeilen, ob sie einem vorgegebenen Textmuster entsprechen (regular expression). perl baut also ungefähr so etwas

c[1]=black
c[2]=yellow
c[3]=cyan
c[4]=magenta
Der "eval" in der Shell sorgt nun dafür, dass diese Zuweisung im bash-Skript zu diesem Zeitpunkt ausgeführt wird, als hätte ich es vorher in den Code geschrieben. Hier greift also der Begriff "selbstmodifizierend", weil im Skript etwas passiert, was das Skript selbst vorher zusammengebaut hat (mit Hilfe von perl).

Auf dieselbe Weise werden die Maximalwerte jeder Patrone vom Drucker geholt; durch den "eval"-Befehl erhält die Shell dann noch das Array "max" mit folgendem Inhalt:

max[1]=674
max[2]=240
max[3]=226
max[4]=241
Und im nächsten Schritt wird es noch trickreicher: mit den eben gerade gelernten Arrays wird ein perl-Skript dynamisch zusammengebaut, in dem diese Zahlenwerte von einem bash-Array in ein perl-Array umgebaut werden, und ich lasse dann die Prozentwerte in perl ausrechnen. Da die Arrays in perl bei 0 anfangen zu zählen, ist in jedem Array das nullte Element ein leerer Text, also "". Das perl-Skript erhält die Tintennamen und die Maximalwerte als fest einprogrammierte Werte, und die aktuellen Zahlenwerte liest es direkt aus der Standardausgabe von snmpwalk.

Wie funktioniert dieser Trick nun genau?

Üblicherweise ruft man den Skriptinterpreter (perl, awk, sed oder irgendwas anderes) so auf: perl -e 'bla mein programm usw.'. Die Apostrophe teilen der Shell mit, dass alles dazwischen unverändert an perl weitergegeben werden soll. Man kann allerdings diese '...' unterbrechen, und an dieser Unterbrechung setzt die Shell ihre Arbeit fort. Wenn ich also perl -e '...bla(' ${variable} ')...' schreibe, besteht das Programm für perl also an dieser Stelle aus dem aktuellen Inhalt der Shell-Variablen ${variable} (z.B. 5) und das ganze wird effektiv zu perl -e '...bla(5)...'! Das ganze muss natürlich syntaktisch korrekter perl-Code werden, also muss man auf Anführungszeichen für Strings usw. achten.
#!/bin/sh

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

# get current ink levels
eval $(snmpwalk -v1 -c praxis 192.168.100.173 \
  1.3.6.1.2.1.43.11.1.1.6.0 |
perl -ne 'print "c[$1]=$2\n" \
  if(m!SNMPv2-SMI::mib-2.43.11.1.1.6.0.(\d) = STRING:\s+"(\w+) ink"!i);')

# get max ink level per cartridge
eval $(snmpwalk -v1 -c praxis 192.168.100.173 \
  1.3.6.1.2.1.43.11.1.1.8.0 |
perl -ne 'print "max[$1]=$2\n" \
  if(m!SNMPv2-SMI::mib-2.43.11.1.1.8.0.(\d) = INTEGER:\s+(\d+)!i);')

snmpwalk -v1 -c praxis 192.168.100.173 1.3.6.1.2.1.43.11.1.1.9.0 |
perl -ne '
    my @c=("","'${c[1]}'","'${c[2]}'","'${c[3]}'","'${c[4]}'");
    my @max=("","'${max[1]}'","'${max[2]}'","'${max[3]}'","'${max[4]}'");
    printf"# $c[$1]=$2 (%.0f)\n",$2/$max[$1]*100
        if(m!SNMPv2-SMI::mib-2.43.11.1.1.9.0.(\d) = INTEGER:\s+(\d+)!i);'
 Das Ergebnis sieht dann ungefähr so aus:
$ sh ./ink.sh
# black=247 (38)
# yellow=218 (95)
# cyan=127 (59)
# magenta=195 (85)

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);

22.04.2015

OpenOffice-Dokumente mit perl-Skript bearbeiten

Offene Dateiformate sind eine tolle Sache!

Das merke ich immer wieder bei lustigen Problemen, wenn mir z.B. jemand ein Office-Dokument schickt und ich damit irgendetwas total langweiliges erledigen soll.

Es macht viel mehr Spaß, dafür ein Programm oder ein Skript zu schreiben und den Computer die Arbeit machen zu lassen, statt selbst dröge Arbeiten zu erledigen, die nur aus Wiederholungen von Tastendrücken und Mausbewegungen bestehen!

Letzte Woche hatte ich so einen Auftrag: ein Kollege schickte mir eine Excel-Datei mit mehreren Tabellenblättern und bat mich, daraus mehrere Tables einer Datenbank zu befüllen.

Mein erster Ansatz war, jedes Blatt als Textdatei (CSV) abzuspeichern und mit ein wenig Skripting die SQL-Befehle pro CSV-Datei zu generieren.

Der nächste Gedanke war einiges cooler und hat mir deshalb noch viel besser gefallen: ich speichere die Excel-Datei im OpenOffice-Format (ODF=OpenDocumentFormat) ab und lasse ein Skript die ganze Arbeit machen! Die Datei enthält um die 15 Tabellenblätter - allein vor dem Abspeichern von 15 CSV-Dateien in Handarbeit grauste es mir schon!

Hinzu kommt, dass mein Arbeitgeber OpenOffice als Standardwerkzeug und Standardformat festgelegt hat und es somit vollkommen natürlich ist, mit dem OpenDocument-Format zu arbeiten. Erwähnte ich schon, dass mein Arbeitsplatz mit Linux ausgestattet ist?

Ein ODF-Dokument ist eigentlich ganz einfach aufgebaut: es ist im Wesentlichen eine zip-Datei und der Inhalt ist immer in der Datei content.xml zu finden. Alles andere ist Beiwerk (Stylesheets, Bilder, ...), das ich für meinen Zweck ignorieren kann.

Natürlich verwende ich perl zum Programmieren meiner Umwandlungsskripte, und natürlich gibt es ein Modul für jeden Zweck. Für die Verbindung zu OpenOffice gibt es sogar mehrere, und ich habe mir OpenOffice::OODoc ausgesucht. Mir ist nämlich in der Beschreibung aufgefallen, dass es eine ganz bestimmte Funktion gibt, die haargenau das erledigt, was ich mir als Lösungsweg überlegt hatte: den Tabelleninhalt als CSV-Text auszuliefern.

Damit war mein perl-Skript plötzlich fertig, kaum dass ich 20 Zeilen geschrieben hatte, wobei das meiste davon sogar ein fast fertiges Beispiel aus der man-page war! Ich musste nur ein wenig die Daten massieren, als ich mit einem Testskript anfing und die ersten Daten probeweise auf dem Bildschirm ausgeben ließ.

Das folgende Skript erledigt nun den ersten Teil meiner Aufgabe: es zerlegt ein OpenOffice-Spreadsheet in mehrere CSV-Dateien, eine pro Tabellenblatt.
Als Ziel schwebt mir vor, dass die temporären CSV-Dateien gar nicht mehr gebraucht werden und ich direkt den SQL-Code erzeuge, aber ich denke, die Umwandlung von .ods in .csv kann man immer mal brauchen.

Einzige Bedingung: der Name des Tabellenblatts sollte sinnvoll gesetzt sein, weil daraus nämlich der Name der CSV-Datei gebildet wird.  Wenn man das nicht macht, heißen die Dateien einfach "Sheet1.csv", "Sheet2.csv" usw.

#!/usr/bin/perl -w
use OpenOffice::OODoc;
sub out {
  my ($table,$data)=@_;
  my $f=$table->{'att'}->{'table:name'}.".csv";
  return unless (open(OUT,">",$f));
  print STDERR "# $f\n";
  print OUT $data;
  close(OUT);
}
my $file=shift||"file.ods";
my $doc = odfText(file => $file);
foreach my $table ($doc->getTableList()) {
  $doc->normalizeSheet($table);
  my $data = $doc->getTableText($table);
  $data=~s!(<<|>>)!!msgo;
  $data=~s!^[\s;]*$!!msgo;
  $data=~s!;$!!msgo;
  chomp($data);
  out($table,$data);
}
Die Ausgabe-Funktion bildet den Dateinamen aus dem Attribut table:name des Blatts. Die eigentliche Umwandlung findet in der Methode getTableText statt: hier wird ein gesamtes Tabellenblatt mit einstellbaren Trennzeichen für Spalten und Zeilen in CSV-Text zurückgeliefert. Manche Blattnamen werden von "<<" und ">>" eingerahmt, was ich natürlich in Dateinamen nicht brauchen kann, deshalb werden die Daten noch etwas gesäubert. Welche Blattnamen es gibt, kann man mit getTableList erfragen.

Natürlich kann man auch andere Methoden einsetzen, um einzelne Zeilen, Spalten oder Zellen zu adressieren, und man kann mit diesem perl-Modul auch Daten ändern und die Änderungen abspeichern. Ein geniales Modul! Wie gesagt, die bequemste Funktion war für mich die Umwandlung in CSV mit einem einzigen Aufruf.

[Update: OpenOffice optimiert die Tabellenstruktur in der internen Verwaltung (content.xml), wenn mehrere Zellen denselben Inhalt haben; deshalb muss man vor der Benutzung der Methode getTableText das XML wieder "entzerren". Dazu setze ich vor das Auslesen der Tabelle die Methode normalizeSheet ein. ]

Und zack!, schon ist der erste Teil der Aufgabe erledigt - das Auslesen einer ODF-Datei.

Den zweiten Teil - die Umwandlung von Text in SQL - gibt es bei der nächsten Märchenstunde.

Und jetzt am Schluss verrate ich noch ein Geheimnis: ich erzähle, wie man dieses Modul in sein eigenes perl integriert (Punkte zwischen [...] optional).
  1. Modul herunterladen
  2. Auspacken
  3. In das Directory wechseln
  4. perl Makefile.PL
  5. make
  6. [make test]
  7. make install
oder noch viel bequemer mit dem CPAN-Modul:
  1. perl -MCPAN -e 'install OpenOffice::OODoc'
[Update 20150521: Umbruch im perl-Skript repariert]
[Update 20180810: dito]

14.10.2014

Flash-Update auf Version 15

Ausnahmsweise mal ein normales Update des Flash-Players für Windows, Mac OS und Linux. Nur verwunderlich, wie schnell die Major-Versionen purzeln. Grade erst Version 14, jetzt schon 15.

[Update der Versionsnummern 26.11.2014]

Wie üblich in ihrem freundlichen Service-Blog die passende Automation zum Herunterladen und Installieren.
Falls ein Proxy verwendet wird, das "rem" bzw. "#" entfernen.

Das Tool wget wird bei Windows noch benötigt wie hier beschrieben. Bei Linux sollte es schon vorhanden sein, da es von vielen anderen Programmen intern verwendet wird.

Für Windows wie üblich beide Varianten, ActiveX und Netscape Plugin.
@echo off

rem set http_proxy=http://192.168.100.100:3128/
set VNP=
15.0.0.239
set VAX=15.0.0.239
set H=fpdownload.macromedia.com
set P=/get/flashplayer/current/licensing/win
set AX=install_flash_player_15_active_x.exe
set NP=install_flash_player_15_plugin.exe

wget http://%H%%P%/%AX% -O flash-%VAX%_ax.exe
.\flash-%VAX%_ax -install
wget http://%H%%P%/%NP% -O flash-%VNP%_np.exe
.\flash-%VNP%_np -install

Für Linux 64 bit rpm (als root ausführen oder "sudo rpm" schreiben):
#!/bin/sh

# http_proxy=http://192.168.100.100:3128/

VL=11.2.202.424
H=fpdownload.macromedia.com
PL=/get/flashplayer/current/licensing/linux

DL() { wget -N "$1/$2"; mv "$2" "$3"; }

echo Linux 64 bit rpm ...
DL http://${H}${PL} \
   flash-plugin-${VL}-release.x86_64.rpm \
   flash-${VL}.x86_64.rpm
rpm -F --force
flash-${VL}.x86_64.rpm

[Hier ist noch der Verweis auf die Anleitung für Version 14 und Version 13]

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
 

05.02.2014

Notfall-Update für Adobe Flash player - mal wieder

Und schon wieder ein außerplanmäßiges Update des Flash-Players für Windows, Mac OS und Linux. heise empfiehlt aufgrund der Dringlichkeit die sofortige manuelle Installation. Das Problem wird schon von Bösewichten ausgenutzt. Man sollte sich nicht auf das automatischen Update-Verfahren verlassen, das in den Flash-Player eingebaut ist.

Wie üblich in ihrem freundlichen Service-Blog die passende Automation zum Herunterladen und Installieren.
Falls ein Proxy verwendet wird, das "rem" bzw. "#" entfernen.

Das Tool wget wird bei Windows noch benötigt wie hier beschrieben. Bei Linux sollte es schon vorhanden sein, da es von vielen anderen Programmen intern verwendet wird.

Für Windows wie üblich beide Varianten, ActiveX und Netscape Plugin.
@echo off

rem set http_proxy=http://192.168.100.100:3128/
set VNP=12.0.0.70
set VAX=12.0.0.70

set H=fpdownload.macromedia.com
set P=/get/flashplayer/current/licensing/win
set AX=install_flash_player_12_active_x.exe
set NP=install_flash_player_12_plugin.exe

wget http://%H%%P%/%AX% -O flash-%VAX%_ax.exe
.\flash-%VAX%_ax -install
wget http://%H%%P%/%NP% -O flash-%VNP%_np.exe
.\flash-%VNP%_np -install

Für Linux 64 bit rpm (als root ausführen oder "sudo rpm" schreiben):
#!/bin/sh

# http_proxy=http://192.168.100.100:3128/

VL=11.2.202.341

H=fpdownload.macromedia.com
PL=/get/flashplayer/current/licensing/linux

DL() { wget -N "$1/$2"; mv "$2" "$3"; }

echo Linux 64 bit rpm ...
DL http://${H}${PL} \
   flash-plugin-${VL}-release.x86_64.rpm \
   flash-${VL}.x86_64.rpm
rpm -F --force
flash-${VL}.x86_64.rpm
[Update 20140205: Tippfehler in der Windows-Versionsnummer, Hinweis auf wget für Windows]
[Update 20140221: Neue Flashversionsnummern gemäß 2. heise-Meldung]

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.

09.01.2014

Erbsen zählen mit Linux - IP Traffic accounting

Manche unter uns wurden geplagt von langsamem DSL.

Ein paar von denen sind mutig auf LTE umgestiegen, das die Telekom und andere Anbieter in unterversorgten Gebieten anbieten müssen (zwar nur im 800 MHz-Band, aber immerhin).

Und ein paar von denen sind neugierig, welcher Mitbewohner denn nun den Traffic verursacht und schon am 20. die Quote für den ganzen Monat aufgebraucht hat ;)

Das alles und noch viel mehr kann man mit einem Linux-Server machen, wenn man seine PCs und Smartphones im Haushalt nicht direkt ins Internet lässt, sondern den Linux-Rechner als Gateway dazwischenklemmt.

Dieser Ansatz hat nebenher noch den Vorteil, dass auf dem Linux-Server ein Caching Proxy laufen kann und damit potentiell ein bißchen vom Traffic nur einmal über die externe Internetverbindung abgerufen werden muss.

Hauptsächlich ist aber das interessanteste, dass nun wirklich jede Internetverbindung am Linux sichtbar ist.

Mit dem Modul "iptables", das bei jeder Linux-Distribution dabei sein sollte, kann man eine Statistik führen, welcher Rechner oder welches Smartphone wieviel Traffic verursacht hat.

Normalerweise wird "iptables" dazu verwendet, mit Regeln festzulegen, was erlaubt ist und was nicht. Nebenbei wird aber auch mitgezählt, wieviele Bytes durch die Regeln geflossen sind. Man kann diesen Mechanismus also für den Nebeneffekt des Zählens verwenden, wenn man schlicht "keine Regel" festlegt.

Nebenbei könnte man aber natürlich auch am Proxy datenintensive Sites wie spotify usw. sperren, die besonders viel vom Datenvolumen verbrauchen.

Grundsätzlich muss man dazu einen Linux-Server mit zwei Ethernet-Schnittstellen aufbauen. Manche Mainboards haben schon zwei davon; falls nicht, kann man für sehr wenig Geld eine zweite Karte in den PC einbauen. Es bietet sich an, den Linux-Server dann auch gleich zum NAS auszubauen mit einer zweiten Festplatte (für ein gespiegeltes RAID1) oder mit mehreren (wenn man ein RAID5 haben will).

Das wichtigste kommt nun: der Router (DSL oder LTE) kommt an die zweite Schnittstelle (oBdA ab jetzt eth1 genannt), und niemand sonst darf direkt mit dem Router verbunden sein. Es ist also sinnvoll, das WLAN am Router abzuschalten oder das Passwort so zu ändern, dass sich niemand mehr anmelden kann. Außerdem muss am Linux-Server das IP-Forwarding eingeschaltet sein, damit die anderen Systeme im Haushalt die Chance haben, trotzdem ins Internet zu kommen. Wenn man das WLAN am Router abschaltet, handelt man sich noch ein zu lösendes Problem ein: man benötigt einen separaten WLAN-Access Point für das interne Netz, der für Smartphones die Basisstation wird.

Alle anderen PCs und der Linux-Server mit seiner ersten Schnittstelle (eth0) müssen an einen "internen" Switch angeschlossen werden. Das kann der interne, neue WLAN-Access Point sein, oder eine eigene kleine Kiste, ganz nach Geschmack und örtlichen Gegebenheiten.

Am Linux muss nun prinzipiell das IP-Forwarding eingeschaltet werden. Dies bedeutet, dass aller Traffic, der an eth0 ankommt und nicht für eine andere interne Station gedacht ist, automatisch an eth1 weitergeleitet wird (und von dort natürlich weiter ins Internet).

Das deute ich hier nur an, für Details mal ein bißchen im Internet nach "linux ip forwarding" suchen.
    iptables -A FORWARD -j ACCEPT -m state --state ESTABLISHED
    echo 1 > /proc/sys/net/ipv4/ip_forward
    net=192.168.1.0/24
    iptables        -A FORWARD     -j ACCEPT     -i eth0 -s "$net"
    iptables -t nat -A POSTROUTING -j MASQUERADE -o eth1 -s "$net"

    iptables -A FORWARD -j REJECT
Statt des gesamten Netzes (in der Variablen $net) kann man auch gezielt einzelne Hostnamen oder IPs angeben, aber das Setup des IP-Forwardings soll hier nicht das Thema sein.


Stattdessen will ich ein bißchen genauer auf das eigentliche Thema eingehen: das Zählen des Traffics.

Dazu richtet man mit iptables zwei zusätzliche "Chains" ein, und klinkt beide in die immer vorhandene FORWARD-Chain mit ein. Das bedeutet so etwas wie ein Verdoppeln der Daten, wobei die Duplikate in den neuen Chains (eine für Input, eine für Output) nur für das Zählen verwendet und dann weggeworfen werden.
    iptables -N INET_OUT
    iptables -N INET_IN 
    iptables -I FORWARD -j INET_IN
    iptables -I FORWARD -j INET_OUT
Diese beiden neuen Chains stattet man als nächstes mit der Information aus, für welche IP-Adresse überhaupt gezählt werden soll. Dabei muss zum Zählen pro IP-Adresse natürlich dieselbige angegeben werden als "Selektor". Einmal muss der Selektor als "source" (ausgehender Traffic) und einmal als "destination" (eingehender Traffic) verwendet werden (die Variable $ip geeignet setzen).
    iptables -I INET_OUT -s "$ip"
    iptables -I INET_IN  -d "$ip"
Danach kann man schon mal ein bißchen mit dem Gerät spielen (am Smartphone den App Store/Play Store aufrufen oder eine andere App mit Internetabrufen) und beobachten, ob sich bei den Zählern etwas bewegt.
iptables -L INET_IN -v
Um das regelmäßig anzuschauen, kann man den Befehl "watch" verwenden, der ein anderes Kommando regelmäßig aufruft. Meinem Smartphone habe ich im DNS den Namen "nexus" gegeben, und nach diesem Namen suche ich nun:

# watch -n 60 "iptables -L INET_IN -v|grep nexus"

Every 60.0s: iptables -L INET_IN -v|grep nexus
1567 1198K all -- any any anywhere nexus.moeller-seeling.local
Das Interessanteste dürfte der "inbound" sein, der hereinkommt. Typischerweise ist das Downloadvolumen eher groß, und der "outbound" besteht hauptsächlich darin, die Anfragen zu verschicken. Außer natürlich, man lädt gerade große Bilder zu Flickr oder Videos zu Youtube hoch ;)

Als nächstes richtet man dann einen cron-job ein, der diese Daten in regelmäßigen Abständen protokolliert und daraus lustige Reports erstellt. Davon erzähle ich im nächsten Beitrag, wenn ich mit Programmieren fertig bin ;)


[Update: Idee von Robert Dahlem von der LUG Frankfurt, außerdem diese FAQ]

21.11.2013

So baut man ein aktuelles Komplettpaket Acrobat Reader X

Hilfsvariablen setzen


set D=C:\tmp\AR10
set S=C:\tmp\download
set MSI=AdbeRdr1000_de_DE.msi
set AR=ftp://ftp.adobe.com/pub/adobe/reader/win/10.x

Ordner erstellen

if exist %S%\nul rmdir /S /Q %S%
if exist %D%\nul rmdir /S /Q %D%
mkdir %S%
mkdir %D%
cd /D %S%

wget beschaffen

Bei sourceforge.net herunterladen und das ZIP auspacken. wget ist ein cooles Kommandozeilenwerkzeug zum Abrufen von URLs, sei es http, https oder ftp.

Dateien herunterladen

wget -nv -nc -N %AR%/10.0.0/de_DE/%MSI%
wget -nv -nc -N %AR%/10.1.8/misc/AdbeRdrUpd1018.msp

Patches in das Grundpaket integrieren

msiexec /a %S%\%MSI% /qb- TARGETDIR=%D%
msiexec /p %S%\AdbeRdrUpd1018.msp /a %D%\%MSI% /qb- TARGETDIR=%D%
Die Angabe /a führt eine reine "Administrator"-Installation durch, d.h. die Software wird nicht auf dem PC installiert, auf dem der Befehl aufgerufen wird, sondern es wird nur vorbereitet, dass die Software auf anderen PCs installiert werden kann. Mit /p werden die heruntergeladenen Patches in die Basissoftware integriert. Die Reihenfolge von /p und /a im letzten Befehl oben muss man einhalten, sonst klappt es nicht.

Recht angenehm: man muss nur den neuesten Patch für den Acrobat Reader herunterladen und nicht sukzessiv alle. Derzeit aktuell ist 10.1.8.

Der Ordner C:\tmp\download wird danach nicht mehr benötigt und kann gelöscht werden. Den vollständigen Ordner C:\tmp\AR10 kopiert man nun auf ein Netzlaufwerk, z.B. ein NAS oder eine Festplatte am DSL-/LTE-Router.

Installation

msiexec /i I:\win\adobe\AR10\AdbeRdr1000_de_DE.msi /qb-!
Natürlich muss man hier das Beispiel I:\win\adobe\AR10 entsprechend an die eigenen Verhältnisse anpassen.

Credits: das Prinzip und die Downloadlinks hab ich mir hier im heise-Forum abgeguckt.