Wann frischen Fedora, Debian und Co. jeweils ihre Pakete auf? Ein Perl-Skript bemüht verschiedene Plugins, die auf den FTP- und HTTP-Servern der Distros herumschnüffeln, die Releasedaten heraussuchen und Vergleichslisten erstellen.
Alle großen Distros listen ihre Pakete mit dem zugehörigen Release-Datum irgendwo auf dem Netz auf. Ein Skript braucht also nur die Anfrage eines Users entgegenzunehmen, die verschiedenen Lokalitäten aufzusuchen, passende Paketnamen auszufiltern, die Datumsangaben einsammeln und die Ergebnisse dann tabelliert auszugeben.
Die Schwierigkeit besteht nun freilich darin, dass die Distros 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 aufgespalten in Unterverzeichnisse, deren
Namen aus dem ersten Buchstaben des Paketnamens bestehen (Abbildung [1]).
So liegt das Paket pulseaudio
bei Debian im Verzeichnis p
, während
ein Server in Esslingen alle Fedora-Pakete in Verzeichnis zur Prozessorarchitektur
auflistet (Abbildung 2).
SUSE verfährt ähnlich, zeigt aber
das Modifikationsdatum der einzelnen Dateien nicht wie ein FTP-Server
an sondern in einem wohl selbsterfundenen Format (etwa
28-Nov-2012 17:02
, Abbildung 3).
Abbildung 1: Debian und Ubuntu stellen die Release-Pakete in einer zweistufigen Hierarchie auf einem FTP-Server zur Schau. |
Abbildung 2: Der FTP-Server von Fedora mit den aktuellen Paketen und deren Releasedaten |
Abbildung 3: Suse wirft alle RPMs in ein Verzeichnis und serviert sie mit einem eigenen Datumsformat über HTTP |
Ein Skript, das diese Informationen von den unterschiedlichen Servern abholt, steht nun vor dem Problem, dass es einerseits Gemeinsamkeiten zwischen den Distros gibt, wie zum Beispiel die FTP-Server-artige Darstellung, aber andererseits auch Unterschiede wie die verschiedenen URLs oder das Datumsformat. Die unterschiedlichen Paketnamen zwischen den Distros stellen ein weiteres Problem dar, das sich teilweise durch unscharfe Suchen und manuelle Auswahl lösen lässt.
Abbildung 4: Das Skript dist in Aktion: Verschiedene Distro-Server berichten ihre Releasedaten zum Pulseaudio-Paket. |
Code zu duplizieren gilt in Entwicklerkreisen als schädlich, denn
ändert sich eine Kleinigkeit, müssen alle Duplikate aufgespürt
und nachbearbeitet werden. In der objektorientierten Programmierung existiert
zu diesem Zweck der Mechanismus der Vererbung und das heute vorgestellte
Perl-Skript dist
bedient sich darum zum Einholen der Informationen
verschiedner Plugins, die ihrerseits von anderen Plugins oder Utility-Modulen
erben. Das Skript in Listing 1 definiert eine Klasse Distro
, die eine
Methode list()
anbietet, die auf einen Suchstring hin die verschiedenen
Distro-Server abklappert. Das Skript nimmt die Abfrage auf der
Kommandozeile entgegen (z. B. distro pulseaudio
), findet dann
zur Laufzeit heraus, wieviele Distro-Plugins der User installiert hat,
und ruft die list()
-Methode jedes einzelnen der Reihe nach 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.
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. Deswegen hat es sich vor einiger Zeit das CPAN-Modul Moose zur Aufgabe gemacht, Perl mittels syntaktischer Zauberei zu einer objektorientierten Sprache erster Wahl zu mausern. Nun bietet Moose aber viele Funktionen, die einfache objekorientierte Programme nur selten nutzen, und deswegen haben einige CPAN-Programmierer abgespeckte Versionen ins Leben gerufen, die unter anderem Mouse und Moo heißen.
Listing 1 verwendet Mouse, könnte aber genauso gut mit Moose arbeiten, da die bei Moose übliche Verzögerung beim Laden im Sekundenbereich bei einem Einmal-Skript keine wesentliche Rolle spielt.
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 }
Beim Studium von Listing 1 fällt auf, dass die ab Zeile 13 definierte
Klasse Distro
keinen Konstruktor new()
definiert, dass aber
das Hauptprogramm eben diesen in Zeile 9 aufruft. Das Geheimnis ist
schnell gelüftet, denn use Mouse
im Code der Klasse schmuggelt heimlich
einen Konstruktor ein. Dieser ruft nicht nur ein Distro-Objekt ins Leben
sondern könnte bei Bedarf sogar noch benamte Parameter verarbeiten
und intern im Objekt und mit sauberen Accessors nach außen verwalten.
Eine weitere Besonderheit in Listing 1 ist die mit dem CPAN-Modul
Module::Pluggable
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,
instantiiert ihre Klasse und gibt eine Referenz auf das enstandene Objekt
zurück. Jeder Plugin bietet (entweder direkt oder ererbt)
eine Methode list()
zum Einholen der Informationen an, sowie
eine weitere Methode namens dateformat()
, die das Sekundendatum in ein
anwenderfreundliches Stringformat verwandelt.
Listing 2 zeigt einen simplen Plugin zum Abfragen des Ubuntu-Servers.
Das Debian-Derivat nutzt das gleiche Format zur Darstellung der
Releaseprodukte wie die väterliche Debian-Distro. Um sich Arbeit zu sparen
erbt der Debian-Plugin in Zeile 5 deswegen mit dem Mouse-Schlüsselwort
extends
vom Basisplugin Distro::Plugin::Debian
und definiert lediglich
eine Funktion base_url()
mit dem Basis-URL zum FTP-Server des
Ubuntu-Projekts. Die list()
-Methode bietet der Ubuntu-Plugin ohne
eine Zeile Code über die gleichnamige Methode in der Basisklasse Debian
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-Plugin und nicht um einen 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.
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;
Das Debian-Modul in Listing 3 muss schon etwas mehr schuften,
definiert aber ebenfalls eine Funktion base_url()
, die dort 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 einen
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.
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;
Das FTP-Modul in Listing 4 nutzt zum Einholen der FTP-URL das
CPAN-Modul und Tausendsassa LWP::UserAgent. Die in den Abbildungen 1 und
2 sichtbaren Zeitstempel 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, die Größe in Bytes, das
Datum der letzten Modifikation und Berechtigungs-Modus 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 39 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 einen Distro-Plugin, sondern lediglich 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 den Plugin in der
Distroliste.
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-Pakete liegen ebenfalls auf einem FTP-Server, allerdings
ohne die zweistufige Schichtung der Debian- und Ubuntu-Server.
Folgerichtig erbt der Plugin in Listing 5 vom FTP-Plugin und stellt
selbst nur die Methode list()
zur Verfügung, die mit dirlist()
die Daten einholt und sie mit einem einfachen Pattern Match auf
diejenigen beschränkt, die auf den hereingereichten Suchstring passen.
Der Rest, einschließlich der Datumskonvertierung mit dateformat()
,
wird ererbt.
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;
Abbildung 5: Der Suse-Server sträubt sich mit unstrukturiertem HTML gegen das Scraping. |
OpenSuse wiederum zeigt seine Pakete auf der in Listing 6 in Zeile 14 angegebenen URL
auf einer HTML-Seite, auf der es von eingebettete Images und Links zu
den .rpm-Dateien nur so wuchert (Abbildung 5). Was ein Link zu einem Paket ist
und was wiederum nur als Navigationselement dient ist nicht ganz trivial
herauszufinden, da auf der Seite keine HTML-Tags mit class
-Attributen stehen.
Deshalb macht der Scraper Web::Scraper
in Listing 6 das beste draus,
extrahiert zunächst in Zeile 18 den gesamten Textsalat, der sich zwischen
den <pre>
-Tags befindet, um dann mit dem regulären Ausdruck in
den Zeile 31-33 den strukturierten Text mit den RPM-Paketen und
deren Releasedaten zu erfassen. Suses kreatives Datumsformat muss ein
speziell eingerichteter DateTime-Formatter interpretieren. Als Zeitzone
übergibt er in Zeile 28 den String "UTC", also die Standardzeit am
Längengrad Null.
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;
Die Plugins lassen sich auch für andere Distros erweitern. So gibt sich Red Hat noch weniger Scraper-freundlich und bietet Paketinformationen nur an, wenn sich das Skript durch einige Webformulare klickt. Ähnlich kompliziert gibt sich SLES, die Suse Enterprise-Edition. Mit einem Scraper wie WWW::Mechanize vom CPAN sind mächtigere Plugins, die auch diese Klippen umschiffen, jedoch schnell implementiert.
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2013/03/Perl