Passend zum Titelthema: Mike
Diesmal entstand die Software gewissermaßen als Auftragsarbeit: Die Redaktion brauchte für den Schwerpunkt dieses Magazins ein Tool, das die Update-Server der großen Linux-Distributionen nach Releasedaten durchforstet. Da Perl in dem Ruf steht, gut in komplexen Abfragen und Stringmanipulationen zu sein, widmet sich der folgende Artikel ganz der werkzeuglichen Unterstützung des Schwerpunkt-Themas. Ziel soll ein Skript sein, das die per Kommandozeile übergebene Anfrage entgegennimmt, die verschiedenen Lokalitäten aufsucht, passende Paketnamen ausfiltert, die Datumsangaben einsammelt und zum Schluss die Ergebnisse tabelliert ausgibt.
Die Schwierigkeit besteht nun freilich darin, dass die Distributionen ihre Release-Informationen in unterschiedlichen Formaten anbieten. So liegen die Pakete bei Ubuntu und Debian auf einem FTP-Server – allerdings nicht alle in einem Verzeichnis, sondern aufgespaltet in Unterverzeichnisse, deren Namen aus dem ersten Buchstaben des Paketnamens bestehen (Abbildung 1).
So schlägt das Paket »pulseaudio« bei Debian im Verzeichnis »p« seine Zelte auf, während ein Server in Esslingen alle Fedora-Pakete im Verzeichnis zur Prozessor-Architektur auflistet (Abbildung 2). Open Suse verfährt ähnlich, zeigt aber die Datei-Modifikationsdaten nicht wie ein FTP-Server an, sondern in einem wohl selbst erfundenen Format à la »19-Nov-2012 16:14« (Abbildung 3).

Abbildung 1: Debian und Ubuntu stellen die Releasepakete in zweistufiger Hierarchie per FTP zur Schau.

Abbildung 3: Open Suse wirft alle RPMs in ein Verzeichnis und serviert sie in einem eigenen Datumsformat.
Online PLUS
In einem Screencast demonstriert Michael Schilli das Beispiel: https://www.linux-magazin.de/plus/2013/03
Plugins überwinden Unterschiede
Ein Skript, das diese Informationen von den einzelnen Servern abholt, steht nun vor der Schwierigkeit, dass es einerseits Gemeinsamkeiten zwischen den Repositories gibt, zum Beispiel die FTP-Server-artige Darstellung, aber andererseits auch Unterschiede wie die verschiedenen URLs oder das Datumsformat. Die unterschiedlichen Paketnamen zwischen den Distributionen stellen ein weiteres Problem dar, das sich durch unscharfe Suchen und manuelle Auswahl aber mildern lässt (Abbildung 4).

Abbildung 4: Das Skript »dist« in Aktion: Verschiedene Distributions-Server berichten ihre Releasedaten des Pulseaudio-Pakets.
Duplizieren verpönt
Code duplizieren gilt in Entwicklerkreisen als missliebig. Grund: Wer in einem der Copy-Paste-Abschnitte eine Kleinigkeit ändert, muss alle Duplikate aufspüren und nachbearbeiten. Die objektorientierte Programmierung bietet deshalb den Mechanismus der Vererbung, und das heute vorgestellte Perl-Skript »dist« macht davon Gebrauch. Es bedient sich nämlich zum Einholen der Informationen mehrerer Plugins, die ihrerseits von anderen funktionsähnlichen Plugins oder Utility-Modulen erben.
Das Skript in Listing 1 definiert eine Klasse »Distro« . Deren Methode »list()« klappert auf einen Suchstring hin die Paketserver von Distributionen ab. Das Skript nimmt die Abfrage auf der Kommandozeile entgegen, etwa »distro pulseaudio« , findet zur Laufzeit heraus, wie viele »Distro« -Plugins der User installiert hat, und ruft der Reihe nach die »list()« -Methode jedes einzelnen auf.
Alle Plugins halten sich an eine vorgegebene Schnittstelle, akzeptieren einen Aufruf der Methode »list()« mit einem Suchstring und geben eine Referenz auf einen Array mit den Treffern zurück. Jeder Treffer besteht aus einer Referenz auf einen Hash mit den Einträgen »pkg« und »mtime« , die den Namen gefundener Pakete und deren letztes Modifikationsdatum in Sekunden seit 1970 enthalten.
Listing 1
dist
01 #!/usr/local/bin/perl -w
02 use strict;
03 use Log::Log4perl qw(:easy);
04 # Log::Log4perl->easy_init($DEBUG);
05
06 my( $query ) = @ARGV;
07 die "usage: $0 query" if !defined $query;
08
09 my $distro = Distro->new();
10 $distro->list( $query );
11
12 ###########################################
13 package Distro;
14 ###########################################
15 use Mouse;
16 use Module::Pluggable
17 require => 1,
18 search_dirs => ['.'];
19
20 sub list {
21 my( $self, $query ) = @_;
22
23 for my $plugin ( $self->plugins() ) {
24 next unless $plugin->can( "list" );
25
26 print "[$plugin]\n";
27 my $data = $plugin->list( $query );
28
29 for ( @$data ) {
30 print "$_->{ pkg }\t",
31 $plugin->dateformat(
32 $_->{ mtime } ), "\n";
33 }
34 }
35 }
Code sparen mit Mouse
Wer viel objektorientiert in Perl programmiert, dem geht die weitschweifige Syntax für oft genutzte Bausteine wie Konstruktoren oder Parameterabfragen relativ schnell auf die Nerven. Vor einiger Zeit hatten es sich deswegen die Entwickler des CPAN-Moduls Moose [2] zur Aufgabe gemacht, Perl mittels syntaktischer Zauberei zu einer objektorientierten Sprache erster Wahl zu mausern (“A Postmodern Object System for Perl”).
Moose ist sehr umfangreich und bietet viele Funktionen – so viele, dass es Ressourcen frisst und für Entwickler einfacher objektorientierter Programme mehr Last als Hilfe bringt. Darum pflegen einige CPAN-Programmierer abgespeckte Moose-Derivate wie Mouse [3], Moo und einige mehr. Listing 1 verwendet Mouse, könnte aber auch gut mit Moose arbeiten, da die bei Moose üblichen zusätzlichen Sekunden beim Laden eines Einmal-Skripts nicht ins Gewicht fallen.
Beim Listing 1 fällt auf, dass die ab Zeile 13 definierte Klasse »Distro« keinen Konstruktor »new()« definiert, das Hauptprogramm aber einen solchen in Zeile 9 aufruft. Das Geheimnis: »use Mouse« im Code der Klasse schmuggelt einen Konstruktor ein. Dieser ruft nicht nur ein »Distro« -Objekt ins Leben, sondern könnte bei Bedarf sogar benamte Parameter verarbeiten und intern im Objekt und mit sauberen Accessors nach außen verwalten.
Plugins ohne Ballast
Eine weitere Besonderheit in Listing 1 ist die mit Module::Pluggable aus dem CPAN eingeschleuste Plugin-Verwaltung. Der Parameter »search_dirs« in Zeile 18 gibt mit ».« an, dass das Modul später im aktuellen Verzeichnis nach Dateien der Form »Distro/Plugin/xxx.pm« suchen wird. Die Option »require« ist auf einen wahren Wert gesetzt, also lädt das Modul mittels »plugins()« gefundene Plugins automatisch, instanziert ihre Klasse und gibt eine Referenz auf das entstandene Objekt zurück. Jedes Plugin bietet – direkt oder ererbt – eine »list()« -Methode zum Einholen der Informationen an sowie eine »dateformat()« -Methode, die das Sekundendatum in ein anwenderfreundliches Stringformat verwandelt.
Listing 2 zeigt ein Plugin, das den Ubuntu-Server abfragt. Das Debian-Derivat nutzt das gleiche Format wie der Stammvater. Das Debian-Plugin erbt daher in Zeile 5 mit dem Mouse-Schlüsselwort »extends« vom Basisplugin »Distro::Plugin::Debian« und definiert nur eine Funktion »base_url()« mit der Basis-URL zum FTP-Server des Ubuntu-Projekts. Die »list()« -Methode bietet dem Ubuntu-Plugin dies über die gleichnamige Methode der »Debian« -Basisklasse an.
Auch wenn Perl später die ererbte Methode in einem anderen Modul ausführt, weiß es doch, dass es sich bei dem gerade aktiven Objekt um ein Ubuntu- und nicht um ein Debian-Plugin handelt. Ruft dann der Code im Debian-Plugin »base_url()« auf, springt Perl die Methode im Ubuntu-Plugin an. So reicht eine einzige Zeile im Ubuntu-Plugin, um die Debian-Funktionen mit einer anderen URL zu offerieren.
Listing 2
Ubuntu.pm
01 ###########################################
02 package Distro::Plugin::Ubuntu;
03 ###########################################
04 use Mouse;
05 extends 'Distro::Plugin::Debian';
06
07 sub base_url {
08 return
09 "ftp://ftp.ubuntu.com/ubuntu/pool/main";
10 }
11
12 1;
Debian – etwas kompliziert
Das Debian-Modul in Listing 3 definiert ebenfalls eine Funktion »base_url()« , die auf den Debian-FTP-Server zeigt. Die Methode »list()« ab Zeile 12 nimmt vereinbarungsgemäß einen Suchstring entgegen, extrahiert mit der Perl-Funktion »substr()« dessen ersten Buchstaben und baut eine URL zur zweistufigen Verzeichnisstruktur auf, in der das gesuchte Paket liegt. Die ererbte Methode »dirlist« aus dem Modul Distro::Plugin::FTP interpretiert das Directory-Listing eines FTP-Servers, extrahiert Paket- und Datumsangaben und gibt die vereinbarte Datenstruktur als Referenz auf einen Hash zurück.
Das FTP-Modul in Listing 4 nutzt zum Einholen der FTP-URL den Tausendsassa LWP::UserAgent. Die Zeitstempel in den Abbildungen 1 und 2 versteht das CPAN-Modul File::Listing einzulesen und zu interpretieren. Seine Methode »parse_dir()« nimmt die Ausgabezeilen des FTP-Servers entgegen und fieselt Dateinamen, Dateityp, Größe in Bytes, das Datum der letzten Modifikation und Berechtigungsmodus heraus. Zurück kommt eine Liste mit Werten, von denen sich Zeile 28 nur Name und Datum schnappt und als Eintrag an die später ans Hauptprogramm zurückgereichte Datenstruktur anhängt.
Damit das Hauptprogramm den Sekundenwert des Zeitstempels ohne Mühe in ein »DateTime« -Objekt umwandeln kann, ruft die Methode »dateformat()« ab Zeile 36 in Distro::Plugin::FTP den alternativen »DateTime« -Konstruktor »from_epoch()« auf, der ein »DateTime« -Objekt des Zeitstempels zurückgibt. Beim FTP-Modul handelt es sich nicht um ein Distributions-Plugin, sondern nur um ein Utility-Paket. Es definiert deshalb auch keine »list()« -Methode. Das Hauptprogramm in Listing 1 prüft dies mit der allen Perl-Objekten eigenen Methode »can()« in Zeile 24 und überspringt das Plugin in der Distributionen-Liste.
Listing 3
Debian.pm
01 ###########################################
02 package Distro::Plugin::Debian;
03 ###########################################
04 use Mouse;
05 extends 'Distro::Plugin::FTP';
06
07 sub base_url {
08 return
09 "ftp://ftp.debian.org/debian/pool/main";
10 }
11
12 sub list {
13 my( $self, $query ) = @_;
14
15 my $first = substr( $query, 0, 1 );
16 my $url = $self->base_url() .
17 "/$first/$query";
18
19 return $self->dirlist( $url );
20 }
21
22 1;
Listing 4
FTP.pm
01 ###########################################
02 package Distro::Plugin::FTP;
03 ###########################################
04 use Mouse;
05 use LWP::UserAgent;
06 use Log::Log4perl qw(:easy);
07 use File::Listing;
08
09 sub dirlist {
10 my( $self, $url ) = @_;
11
12 DEBUG "Listing $url";
13
14 my $ua = LWP::UserAgent->new();
15 my $resp = $ua->get( $url );
16
17 my $listing = $resp->content();
18 my @lines = split /\n/, $listing;
19 pop @lines;
20
21 my @data = ();
22
23 for (File::Listing::parse_dir(
24 \@lines, 'GMT')) {
25 my($name, $type, $size,
26 $mtime, $mode) = @$_;
27 push @data,
28 { pkg => $name, mtime => $mtime };
29 }
30
31 DEBUG "Found ", scalar @data, " results";
32 return \@data;
33 }
34
35 ###########################################
36 sub dateformat {
37 ###########################################
38 my( $self, $time ) = @_;
39
40 my $dt = DateTime->from_epoch(
41 epoch => $time );
42 return "$dt";
43 }
44
45 1;
Fedora und Open Suse
Fedora-Pakete liegen ebenfalls auf einem FTP-Server, allerdings ohne die zweistufige Schichtung der Debian- und Ubuntu-Server. Folgerichtig erbt das Plugin in Listing 5 vom FTP-Plugin und stellt selbst nur die Methode »list()« bereit. Die holt mit »dirlist()« die Daten ein und beschränkt sie mit einem einfachen Pattern-Match auf jene, die auf den eingereichten Suchstring passen. Den Rest einschließlich der Datumskonvertierung mit »dateformat()« erbt das Fedora-Modul.
Open Suse präsentiert seine Pakete unter der in Zeile 14 von Listing 6 angegebenen URL. Auf der Seite wuchern eingebettete Images und Links zu den RPM-Dateien (Abbildung 5). Was ein Link zu einem Paket ist und was nur der Navigation dient, ist schwer herauszufinden, da die Seite keine HTML-Tags mit »class« -Attributen transportiert.
Deshalb macht Web::Scraper in Listing 6 das Beste draus: Er extrahiert in Zeile 18 den Textsalat, der sich zwischen den »<pre>« -Tags befindet, um dann mit dem regulären Ausdruck in den Zeilen 31 bis 33 den strukturierten Text mit den RPM-Paketen und deren Releasedaten zu erfassen. Suses kreatives Datumsformat muss ein spezieller Date-Time-Formatter interpretieren. Als Zeitzone übergibt er in Zeile 28 den String »UTC« , also die Standardzeit am Längengrad Null.
Listing 5
Fedora.pm
01 ###########################################
02 package Distro::Plugin::Fedora;
03 ###########################################
04 use Mouse;
05 extends 'Distro::Plugin::FTP';
06
07 sub list {
08 my( $self, $query ) = @_;
09
10 my $listing = $self->dirlist(
11 "ftp://ftp-stud.hs-esslingen.de/pub/" .
12 "fedora/linux//updates/17/x86_64" );
13
14 my @result = grep {
15 # match anywhere, not only front
16 $_->{ pkg } =~ /$query/
17 } @$listing;
18
19 return \@result;
20 }
21
22 1;
Listing 6
Suse.pm
01 ###########################################
02 package Distro::Plugin::Suse;
03 ###########################################
04 use Mouse;
05 extends 'Distro::Plugin::FTP';
06 use DateTime::Format::Strptime;
07 use Web::Scraper;
08 use URI;
09
10 sub list {
11 my( $self, $query ) = @_;
12
13 my @result = ();
14 my $url = "http://download.opensuse.org".
15 "/update/openSUSE-current/x86_64/";
16
17 my $rpms = scraper {
18 process "pre", "text" => 'TEXT';
19 };
20
21 my $html =
22 $rpms->scrape( URI->new( $url ) );
23 my $text = $html->{ text };
24
25 # b=28-Nov-2012 c=17:02
26 my $f = DateTime::Format::Strptime->new(
27 pattern => "%d-%b-%Y %H:%M",
28 time_zone => "UTC",
29 );
30
31 while( $text =~ /^\s*(\S+\.rpm)
32 \s+(\d\S+)
33 \s+(\d\S+)
34 /msgx ) {
35 my $pkg = $1;
36 my $date = "$2 $3";
37
38 next if $pkg !~ /$query/;
39
40 my $dt = $f->parse_datetime( $date );
41 push @result, {
42 pkg => $pkg, mtime => $dt->epoch() };
43 }
44
45 return \@result;
46 }
47
48 1;
Tipps zum Weitermachen
Die Plugins ließen sich natürlich für andere Distributionen erweitern. So zeigt sich Red Hat noch Scraper-feindlicher und bietet Informationen nur an, wenn sich das Skript durch einige Webformulare klickt. Ähnlich kompliziert gibt sich SLES. Mit einem Scraper wie WWW::Mechanize vom CPAN bekäme der geübte Perl-Mensch Analyse-Automaten auch dieses Kalibers aufgestellt.
Infos
- Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2013/03/Perl
- Moose: http://moose.iinteractive.com/about.html
- Mouse: http://search.cpan.org/dist/Mouse/lib/Mouse.pm








