Jenkins & Python

2012-05-06 13:53

Ursprünglich wollte ich versuchen Continous Integration (CI) mit Python durchzuführen. Einerseits aus dem Interesse, was in dieser Richtung alles möglich ist, andererseits um regelmäßig durchgeführte Aufgaben zu automatisieren. Mittlerweile nutze ich CI zur Ermittlung von Kennzahlen, zum Verfolgen der Entwicklung und der automatischen Testausführung.

CI-Tool meiner Wahl ist Jenkins. Ich bevorzuge Jenkins gegenüber Hudson, weil Jenkins hinter sich eine aktive Community hat und mir Hudson relativ tot aussieht. Nach der Übernahme Suns durch Oracle wurde das Hudson-Projekt geforkt und als Jenkins durch die Community weiterentwickelt. Hudson wurde irgendwann zum Sterben in die Hände der Eclipse Foundation gelegt - ungefähr so, wie es Oracle bereits mit OpenOffice getan hat.

Die aktive Jenkins Community sorgt für die Weiterentwicklung und es gibt regelmäßig, alle 2 Wochen, Releases.

Ich will hier zeigen, wie wir uns mit wenig Aufwand eine eigene Jenkins-Instanz mit unsere Entwicklung unterstützenden Tools aufbauen können.

Installation

Das System habe ich auf einem Ubuntu Server aufgebaut. Die Installation erfolgte mittels den auf der Jenkins-Seite beschriebenen Schritten.

Nach der Installation wurde Jenkins automatisch gestartet und war fortan unter localhost:8080 zu erreichen.

Plugins

Jenkins lässt sich durch eine große Menge an Plugins erweitern. Aber viele davon brauchen wir nicht, der Zugriff auf die Versionverwaltung unserer Wahl und das Violations-Plugin reichen für den Anfang vollkommen aus.

Violations

Ich wähle das Violations-Plugin für die spätere Verwendung von pylint und pep8.

Da uns die über die Ubuntu-Repos installierte Version das Violations-Plugin nicht anzeigt, installieren ich es von Hand. Dazu laden wir es über diesen Link herunter und wählen in der Jenkins Verwaltung bei Plugins die Erweiterten Einstellungen. Dort habe ich dann die Möglichkeit eines manuellen Uploads.

Jobs einrichten

Nun können wir mit der Einrichtung unseres Projekts in Jenkins beginnen. Wir rufen natürlich Jenkins auf und clicken links auf Neuen Job anlegen. Wir wählen einen aussagekräfitgen Namen und wählen "Free Style"-Softwareprojekt bauen. Der Jobname sollte keine Underscores enthalten, da die Jenkins Shell damit Probleme haben kann.

Jenkins: Job erstellen

Auf der folgenden Seite haben wir dann eine Menge an Einstellungsmöglichkeiten. Unter den erweiterten Projekteinstellungen kann auch ein neuer Anzeigename gewählt werden - diesmal auch mit Leerzeichen.

Versionskontrolle

Hier können wir auch den Zugriff auf unser Versionskontrollsystem einstellen. Ich nutze hier ein git-Repository. Damit nicht alle Dateien im Jenkins-Workspace rumliegen, packen wir das Repository in einen Unterordner. Dies gehört bei git zu den erweiterten Einstellungen und ist als Local subdirectory for repo (optional) zu finden. Wir tragen dort den Wert repo ein.

Das automatische Durchführen unserer Build-Schritte ist auch möglich. Entweder wir geben von vornherein Zeiten fest, zu welchen gebaut werden soll, oder aber wir geben an, wann unsere zuvor eingetragenes Repository auf Aktualisierungen überprüft werden soll. Die Angaben folgen der cron-Syntax. Wir wollen alle 15 Minuten eine Überprüfung des Repositories, was anschließend ein neuen Build ausgeführt, wenn sich der Inhalt geändert hat. Das erreichen wir mit folgender Zeile:

*/15 * * * *

Man kann seine Jobs auch so konfigurieren, dass nach einem Commit in unserem Repo automatisch ein Build ausgeführt wird, aber wir bleiben erstmal bei dieser einfachen Variante.

Bevor es mit der weiteren Einrichtung weitergeht übernehmen wir die Änderungen und starten den ersten Build. So kann festgestellt werden, ob der Zugriff auf die Softwarequelle gelingt oder ob die Konfiguration noch angepasst werden muss. Den Rest der Konfiguration werden wir ähnlich inkrementell fortführen, um die Sicherheit zu haben, dass unsere neuen Einstellungen funktionieren.

Jenkins & Github

Wer mit Jenkins Github-Projekte auschecken will, der sollte unter Jenkins Verwalten -> Jenkins Konfigurieren Benutzername und E-Mail für git konfigurieren, da Github keinen anonymen Checkout erlaubt.

Build-Schritte

Nach dem erfolgreichen Checkout können wir anfangen unsere Build-Schritte zu erstellen. Hierzu fügen wir in der Job-Konfiguration einen Shell-Build-Schritt hinzu, welchen wir nun nach und nach mit Leben füllen.

1. Virtualenv einrichten

Wir beginnen mit der Einrichtung einer isolierten Python-Umgebung mit virtualenv. Das sollte dazu auf dem Hauptsystem bereits installiert sein.

virtualenv pyenv

. pyenv/bin/activate

2. Pakete installieren

Anschließend installieren wir die für die Automatisierung mit Jenkins ein paar Pakete in unserer Python-Umgebung.

pip install pylint

pip install pep8

pip install nose

3. Requirements installieren

Bringt unser Repository eine Datei mit Abhängigkeiten mit, so installieren wir auch diese Abhängigkeiten:

pip install -r repo/requirements.txt

4. Pythonpath richtig setzen

Damit unsere Module importiert werden können, erweitern wir den PYTHONPATH:

PYTHONPATH=$WORKSPACE/repo:$PYTHONPATH

export PYTHONPATH

5. pylint

Die Ausgabe von pylint lässt sich mittels Violations-Plugin einbetten. Dieses wird in der Job-Konfiguration aktiviert und anschließend tragen wir dort als XML filename pattern das folgende ein: **pylint.txt

Damit das einen Effekt hat, müssen wir natürlich auch pylint im Build-Schritt aufrufen und die Ausgabe in die Datei pylint.txt umleiten. Und da pylint bei der Ausgabe von Meldungen nicht mit dem exit code 0 beendet wird, bauen wir uns dafür einen kleinen Workaround. Ohne den Workaround bricht der Build nach diesem Schritt ab, ohne die restlichen Schritte durchgeführt zu haben.

(pylint --output-format=parseable repo/ > pylint.txt) || echo 'pylint did not finish with return code 0'

Jenkins: Erste Ergebnisse von pylint

6. pep8

Auch für pep8 nutzen wir das Violations-Plugin und einen kleinen Workaround - aus dem gleichen Grund wie bei pylint. Als Dateinamens-Muster geben wir **pep8.txt ein und erweitern den Build-Schritt wie folgt:

pep8 repo/ > pep8.txt || echo 'pep8 did not finish with return code 0'

7. Tests durchführen

Zuletzt wollen wir Jenkins dafür nutzen, dass er automatisch unsere Tests ausführt. Dazu wird nose genutzt. In der Job-Konfiguration aktivieren wir die Veröffentlichung der JUnit-Testergebnisse und tragen dort die Standard-Ausgabedatei **nosetests.xml ein. An den Build-Schritt hängen wir folgendes an:

nosetests --with-xunit repo/tests/

Jenkins: Die Tests laufen auch

8. Zusammenfassung

Wir haben mit wenigen Schritten unser Projekt so konfiguriert, dass wir einige Aussagen über den Code-Stil bekommen und automatisch unsere Tests ausgeführt bekommen.

Der Build-Schritt sieht zusammengefasst so aus:

virtualenv pyenv

. pyenv/bin/activate



pip install pep8

pip install pylint

pip install nose



pip install -r repo/requirements.txt



PYTHONPATH=$WORKSPACE/repo:$PYTHONPATH

export PYTHONPATH



pylint --output-format=parseable repo/ > pylint.txt || echo 'pylint did not finish with return code 0'

pep8 repo/ > pep8.txt || echo 'pep8 did not finish with return code 0'

nosetests --with-xunit repo/tests/

Wer eine Möglichkeit sucht das Setup um Code Coverage-Aussagen zu erweitern, der schaut bitte hier.

Kleine Verbesserungen

Der Stand, den wir nun haben, ist erstmal lauffähig. Aber wir können uns mit ein paar Kleinigkeiten noch ein angenehmeres Leben bescheren.

Build-Anzahl beschränken

Da ich zu selten arg alte Ergebnisse anschauen möchte, beschränke ich die Aufbewahrung der Builds mit einem Haken bei Alte Builds Verwerfen und trage dort die Anzahl an aufzuhebenden Builds ein, z.B. 25. Auch kann man dort bestimmen, dass alle Builds älter als n Tage gelöscht werden. Kleiner Nebeneffekt ist, dass der Festplattenplatz nicht unendlich belegt wird.

Build-Schritte in Shellscripte packen

Wenn man zu dem Punkt gekommen ist, an dem man daran denkt einen Job zu kopieren, sollte man kurz innehalten und sich folgende Frage stellen: Wieviel Prozent meiner Build-Schritte werde ich einfach kopieren?

Meine Erfahrung ist, dass es oftmals mehr als 75% sind. Hier und da wird PYTHONPATH angepasst oder ein Schritt weggelassen, weil er keinen Sinn macht (z.B. weil es keine Tests gibt). Das sind alles Fälle, die man wunderbar durch ein Shell-Script abdecken kann. Der einfachste Weg ist die Build-Schritte in ein Shellscript zu kopieren und dann in Jenkins die vielen einzelnen Schritte durch den Aufruf des Shellscripts zu ersetzen.

Nachdem ich einmal die Erfahrung gemacht habe, welcher Aufwand es ist bei einer kleinen Änderung im Ablauf eine Reihe von Jobs anpassen zu müssen, bin ich mittlerweile froh, dass ich (fast) alles durch ein Shell-Script abdecken kann. Hier wird anhand diverser Bedingungen entschieden, welche Schritte nötig sind und mit welchen Optionen sie gestartet werden müssen.

Python-Pakete lokal laden

Für die eigenen Unabhängigkeit vom PyPI sollte man sich überlegen, dass man die zu installierenden Pakete lokal vorhält. Auch kann man nicht überall uneingeschränkt auf das Internet zugreifen, um sich von dort seine Pakete zu installieren. Andererseits kann es eine Zeit dauern, bis alle Pakete heruntergeladen und installiert sind (insbesondere, wenn virtualenv mit der --clean Option gestartet wird).

Möglichkeiten so etwas umzusetzen reichen von der einfachen Alternative alle Files lokal zu lagern und pip den kompletten Pfad mitzugeben (bspw. pip install /home/niko/nose-1.1.2.tar.gz) bis zum Aufsetzen eines eigenen, lokalen PyPI-Spiegels. Die Angabe des kompletten Pfads hat den Nachteil, dass man damit natürlich noch nicht die Requirements-Datei abdeckt.

Aufwändiger ist das Aufstellen eines eigenen PyPI-Mirrors. Ich denke aber, dass er mit der Anzahl der Nutzer immer nützlicher wird. Möglichkeiten gibt es viele, dazu kann man z.B. hier lesen, einen Blick auf die von pip  dargebotenen Möglichkeiten oder bereits fertige Module wie collective.eggproxy werfen. $Suchmaschine ist dein Freund ;)

Und das wars auch schon. Ich bin auf Anregungen und Meinungen gespannt :)