Schon springt der Butler (Linux-Magazin, Oktober 2014)

Statt den CI-Server Jenkins im Browser mittels Mausklicks und Texteingaben für Builds zu konfigurieren, legen faule Tipper die Daten im Source-Control-System ab und lassen ein Perlskript die Handlangerarbeit tun.

Continuous Integration (CI) und die damit einhergehende Produktivitätssteigerung kann man sich heute kaum noch wegdenken. Rein mit dem Code ins Source-Control-System, einen Pull-Request abgesetzt, schnell mal einen Mitstreiter drüberschauen lassen, und schon erfasst das Räderwerk der CI-Pipeline die Änderung, unterwirft sie der stetig wachsendenden Testsuite und schwupps ist alles auf einmal live.

CI-Server wie Jenkins oder Teamcity, die sich die neuesten Source-Versionen eines Projekts schnappen und einen Build mit Testsuite anwerfen, erfreuen sich wachsender Beliebtheit. Doch jedes Mal 2 Minuten beim Aufsetzen eines neuen Build-Projekts in der bräsigen Jenkins-Oberfläche zuzubringen zermürbt auch noch den motiviertesten Entwickler. Wie Abbildung 2 zeigt, gilt es, die notwendigen Kästchen zu finden und zu aktivieren und einige Textfelder auszufüllen. Und wenn jedes Perl-Projekt die gleiche Kommandosequenz zum Starten des Build-Prozesses verlangt, ist es überflüssig, sie bei jedem neuen Projekt unverändert einzutippen. Auch bleibt die Frage, was passiert, falls ein paar Dutzend Projekte auf einen neuen CI-Server umziehen, geht dann der Tipp- und Klickwahnsinn von vorne los?

Abbildung 1: Drei verschiedene Projekte laufen auf einer Jenkins-Installation, allesamt per Perlskript eingespielt.

Lieber Minimal

Eigentlich schwebt mir eher ein minimalistischer, konventionsgetriebener Ansatz vor, wie ihn etwa Travis-CI [3] seit langem praktiziert. Im vorliegenden Falls gibt eine kleine Datei im Sourcecode des Projekts an, von welchem Git-Repository Jenkins den Code abholen kann und unter welchem Namen das Projekt auf dem Jenkins-Server laufen soll. Ein heute vorgestelltes Perl-Skript baut dann die Jenkins-Konfiguration als XML-Dokument zusammen, und setzt sie als POST-Request über die Jenkins-API an den Server ab. Schon ist das Projekt dort eingebaut, ohne jegliche Tipparbeit.

Abbildung 2: Jenkins führt gerade Build und Test des auf Github abgelegten CPAN-Projekts Log4perl aus.

Woher nehmen und nicht stehlen

Doch wie kommt man an das richtige Format der XML-Daten, ohne die Dokumentation oder gar den Source-Code des Servers zu lesen? Ein einmal von Hand auf der Jenkins-Oberfläche eingetipptes Projekt wie in Abbildung 2 gibt die Geheimnisse ohne Fisematenten preis. Der Entwickler hat "Git" als Option für das Source-Code-Repository aktiviert und ins dann erscheinende Textfeld den URL zu Github eingetragen. Unter dem Namen "log4perl" ist das Projekt auf dem Jenkins-Server aktiv und das Skript in Listing 2 kann die dazugehörigen XML-Daten einfach über die API auslesen. Das CPAN-Modul Jenkins::API bietet dazu die Methode project_config() an, die den Server auf Port 8080 des lokalen Hosts kontaktiert und unter Angabe des Projektnamens ("log4perl") die Konfiguration erfragt.

Zunächst sollte ein Skript wie in Listing 1 allerdings erst einmal prüfen, ob der Jenkins-Server überhaupt unter dem voreingestellten Port auf Anfragen reagiert. Klappt dies, kann Listing 2 anfragen, wie denn die Konfiguration eines bestimmten Projektes aussieht.

Listing 1: jenkins-ok

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Jenkins::API;
    04 
    05 my $jenkins = Jenkins::API->new(
    06   { base_url => 'http://localhost:8080' });
    07 
    08 if( $jenkins->check_jenkins_url() ) {
    09   print "ok\n";
    10 } else {
    11   print "not ok\n";
    12 }

Konfigurationssalat

Von der Methode project_config() kommt im Erfolgsfall ein XML-Salat zurück, der unter anderem die von Hand in die Web-UI eingetragenen Werte der URL zum Git-Repository des Projekts offenbart (Abbildung 3).

Listing 2: jenkins-project-xml

    1 #!/usr/local/bin/perl -w
    2 use strict;
    3 use Jenkins::API;
    4 
    5 my $jenkins = Jenkins::API->new( 
    6   { base_url => 'http://localhost:8080' });
    7 
    8 print $jenkins->project_config( 
    9     "log4perl" ), "\n";

Abbildung 3: Im XML-Salat der Jenkins-Konfiguration findet sich die auf der UI eingestellte URL zum Github-Repository des Projekts [% $git_url %].

Mittels dieser Vorlage kann nun ein Skript wie in Listing 3 ein Template mit XML-Struktur erstellen und mittels Variablen (zum Beispiel [% git_url %]) aus dem Modul Template-Toolkit vom CPAN an vorgegebene Projekte anpassen (Abbildung 4). Das Skript liest die Minimalkonfiguration jedes Projekts aus den YAML-Daten in Listing 4, bestehend aus dem Kürzel des Projekts (zum Beispiel "log4perl") und der URL zu den Sourcen auf Github.

Abbildung 4: In dem aus dem XML generierten Template ersetzt der Template-Prozessor Variablen wie hier die URL zum Git-Repository des Projekts.

Listing 3: jenkins-projects.yml

    01 ---
    02 -
    03     name: log4perl
    04     git_url: https://github.com/mschilli/log4perl.git
    05 -
    06     name: libwww-perl
    07     git_url: https://github.com/libwww-perl/libwww-perl.git
    08 -
    09     name: algorithm-bucketizer
    10     git_url: https://github.com/mschilli/algorithm-bucketizer-perl.git

Nur das unbedingt Notwendige

Stellt das Skript fest, dass die bereits auf dem Jenkins-Server gespeicherten Konfigurationsdaten identisch mit den lokal aus dem Template generierten sind, lässt es das aktuelle Projekt in Ruhe und schreitet zum nächsten. Weichen die Konfigurationen voneinander ab, überspielt es die Server-Konfiguration mit der neu lokal genierten. Sieht es, dass das Projekt noch nicht auf dem Jenkins-Server existiert, ruft es die Methode create_job() auf, um dort einen neuen Build-Prozess einzurichten. Der Ablauf des Skripts erzeugt also unter Umständen folgende Ausgabe:

    $ ./jenkins-projects-sync
    2014/08/05 21:20:18 Job for log4perl already exists.
    2014/08/05 21:20:18 Updating job log4perl
    2014/08/05 21:20:18 Creating new job for libwww-perl.
    2014/08/05 21:20:18 Creating new job for algorithm-bucketizer.

Das erste Projekt musste aufgefrischt werden, während die letzten zwei sich noch gar nicht auf dem Server befanden und neu eingerichtet werden mussten. Das Endresultat zeigt Abbildung 1 am Anfang des Artikels. Alle drei Perl-Projekte wurden identisch eingerichtet und unterscheiden sich nur durch ihre individuellen Github-URLs sowie die Projektnamen. Alle drei kann der User regelmäßig per Cronjob, mittels eines Webhooks bei eintrudelnden Commits auf Github, oder durch einen manuellen Mausklick auf "Build Now" loslaufen lassen.

Listing 4: jenkins-projects-sync

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use YAML qw( LoadFile );
    04 use Jenkins::API;
    05 use Template;
    06 use Log::Log4perl qw(:easy);
    07 
    08 my $projects_data_file = 
    09   "jenkins-projects.yml";
    10 my $jenkins_url = "http://localhost:8080";
    11 my $projects_tmpl_file =
    12   "project-jenkins.tmpl";
    13 
    14 Log::Log4perl->easy_init($DEBUG);
    15 
    16 my $tt = Template->new();
    17 
    18 my $jenkins = Jenkins::API->new(
    19   { base_url => $jenkins_url });
    20 
    21 my $data = LoadFile( $projects_data_file );
    22 
    23 for my $project ( @$data ) {
    24   my $xml = $jenkins->project_config(
    25     $project->{ name } );
    26 
    27   $tt->process( $projects_tmpl_file, 
    28     $project, \my $new_xml ) or 
    29       $tt->error;
    30 
    31   if( $xml =~ /^<\?xml/ ) {
    32     INFO "Job for $project->{ name } ",
    33          "already exists.";
    34 
    35     if( $new_xml ne $xml ) {
    36       INFO 
    37         "Updating job $project->{ name }";
    38       $jenkins->set_project_config( 
    39         $project->{ name }, 
    40         $new_xml ) or die;
    41     }
    42     next;
    43   }
    44 
    45   INFO "Creating new job for ",
    46     "$project->{ name }.";
    47 
    48   $jenkins->create_job(
    49     $project->{ name }, $new_xml ) or die;
    50 }

Virtuell installieren

Die Installation des Jenkins-Servers auf meinem Desktop zuhause fand in einer mittels Vagrant erstellten VirtualBox-VM statt, um den Rechner sauber zu halten und zu Testzwecken bequem über vorher eingefangene Snapshots auf einen leeren Jenkins-Server zurücksetzen zu können. Die Vagrant-Datei in Listing 5 nutzt das von vagrantbox.es heruntergeladene Ubunut-12.04-Image und leitet den von Jenkins genutzten Port 8080 aus der VM an den Host weiter, damit der Anwender von dort aus den Jenkins-Server ansteuern kann.

Listing 5: Vagrantfile

    1 Vagrant::Config.run do |config|
    2   config.vm.box = "ubuntu-1204"
    3   config.vm.forward_port 8080, 8080
    4 end

Der nach den offiziellen Anweisungen ([4]) installierte CI-Server ist allerdings kurioserweise im letzten Jahrhundert steckengeblieben und kann von Haus aus nur mit CVS- und SVN-Repositories umgehen. Nach einiger Wartezeit zeigt der Server im Menü "Manage Jenkins"->"Manage Plugins"->"Available" einen "Git Plugin" an, den der Anwender per Mausklick im Browser herunterladen und installieren kann.

Abbildung 5: Der Git Plugin lässt sich über das "Manage Jenkins"-Menü herunterladen und installieren.

Abbildung 6: Die Installation des Git-Plugins für Jenkins dauert nur eine Minute und läuft direkt im Web-Browser ab.

Die Ubuntu-VM kommt allerdings ohne das Kommandozeilentool git daher, sodass der geneigte Leser auch noch mit vagrant ssh in die VM einsteigen muss, um das Tool mit sudo apt-get install git zu installieren. Die Anwendungen des CI-Servers beschränken sich nicht nur auf den Build und das Ausführen der Testsuite, sondern es lassen sich auch weitere "Build Steps" definieren, die den eingecheckten Code zur Freude des Entwicklers ohne weiteren menschliche Eingriffe oder Verzögerungen in die Produktion schubsen.

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2014/10/Perl

[2]

"The Docker Book: Containerization is the new virtualization", Kindle Edition, 2014, James Turnbull

[3]

"Erweiterte Testansicht", Michael Schilli, 2012, http://www.linux-magazin.de/Ausgaben/2012/06/Perl-Snapshot

[4]

Jenkins-Installation auf Ubuntu: https://wiki.jenkins-ci.org/display/JENKINS/Installing+Jenkins+on+Ubuntu

Michael Schilli

arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen der Skriptsprache Perl. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.