Monter un buildbot en 2mn avec collective.buildbot

 

Au Plone 3 Paris Sprint en début d'année j'avais bossé avec Kaï et JF sur collective.buildbot. Du coup j'ai profité du Paris Bobün Sprint pour continuer.

J'ai ajouté une template Paste à collective.buildbot:

buildbot@cecilia:~$ paster create --list-template
Available templates:
  basic_package:  A basic setuptools-enabled package
  buildbot:       A template for collective.buildbot
  ...

C'est assez pratique pour monter un buildbot rapidement et pas se prendre la tête en passant des heures à lire la doc.

C'est relativement simple. On utilise la template qui pose quelques questions:

buildbot@cecilia:~$ paster create -t buildbot gawel.org
Selected and implied templates:
  collective.buildbot#buildbot  A template for collective.buildbot

Variables:
  egg:      gawel.org
  package:  gawelorg
  project:  gawel.org
Enter port (the port to use for internal communication) ['9050']: 6050
Enter wport (the port to use for web interface) ['9080']: 6080
Enter vcs (the vcs type. hg, bzr and git are supported.) ['svn']:
Enter vcs_url (the url to checkout from) ['']: https://svn.gawel.org/gp.fileupload/trunk
Creating template buildbot
Creating directory ./gawel.org
blabla...

Et voilà... On lance 2/3 commandes histoire d'avoir quand même un truc à faire. Sinon c'est un coup à devenir feignant:

buildbot@cecilia:~$ cd gawel.org/
buildbot@cecilia:~/gawel.org$ python bootstrap.py
Downloading http://pypi.python.org/packages/2.4/s/setuptools/setuptools-0.6c9-py2.4.egg
blabla...

buildbot@cecilia:~/gawel.org$ ./bin/buildout -U
Creating directory '/home/buildbot/gawel.org/eggs'.
blabla...
Generated script '/home/buildbot/gawel.org/bin/master'.
blabla...
Generated script '/home/buildbot/gawel.org/bin/cecilia'.

On a maintenant deux script qui correspondent au master et au slave. Si vous ignorez ce qu'est un master et un slave, reporté vous à la documentation buildbot.

Y a plus qu'a les lancer:

buildbot@cecilia:~/gawel.org$ ./bin/master start
blabla...

buildbot@cecilia:~/gawel.org$ ./bin/cecilia start
blabla...

Le tout doit pas prendre plus de deux minutes. Et ça marche. Comme quoi des fois un titre à une signification.

Bien sur, on peut affiner la configuration, utiliser Mercurial ou git, etc. Mais il faut lire la doc.

Upload de fichiers et WSGI

 

Je viens de releaser gp.fileupload 0.5 qui fournis un ensemble de middlewares WSGI pour gérer l'upload de fichiers.

Première utilité: afficher une barre de progression

Et ceci de manière quasi transparente. On colle le gp.fileupload.FileUpload dans sa pile d'application et zou; les formulaire pourvu d'un enctype=multipart/form-data sont attraper au vol par du javascript et une barre de progression s'affiche à la soumission du formulaire. Il y a une petite démo (et une belle doc Sphinx) disponible pour les curieux.

Deuxième utilité: limiter le temps des transactions

En général, on ouvre une transaction, on attends 3 heures qu'un fichier de 300Mo arrive, on se choppe 40 conflits au vol, et avec un peu de chance, la transaction aboutit.

gp.fileupload.Storage catch les requêtes POST et attends d'avoir lu tout son contenu.

Le contenu de la requête est parsé pour en extraire les fichiers qui sont écrit sur le système de fichier dans un répertoire défini.

La requête originale est récrite en remplaçant le contenu original de chaque fichier trouvé par son chemin sur le système de fichier.

C'est seulement ensuite que l'application à la main, avec un POST qui ne dépassera pas le kilo octet. Ainsi le fichier est déjà stocké et la durée de la transaction minimale.

Une option encore pas trop testée permet de desservir toutes les requêtes non text/html depuis le middleware. Une fois que j'aurais un peu mieux testé ce machin, l'utilisation de ce middleware pourra devenir totalement transparente pour l'application.

Je n'ai rien inventé. C'est un système similaire à tramline. Peut être moins optimal car tramline utilise mod_python et est donc totalement indépendant du processus de l'application. Mais bon, je penses que c'est à la fois plus simple d'utilisation (car cela ne nécessite pas Apache et mod_python, justement) et plus transparent pour l'application.

Faire du virtualhosting avec zope façon wsgi

 

J'en avais ras le bol que Zope nécessite des url complètement tordues pour faire du virtual hosting. Ça m'empêchais entre autre d'utiliser Paste#urlmap pour dispatcher certaines url sur d'autres applis que Zope.

Du coup, j'ai tenté un truc tout con: plutôt que d'utiliser les RewriteRule d'Apache, récrire le PATH_INFO en englobant l'application Zope dans une autre. Et ça marche. Fiesta !

Voilà donc à quoi ça ressemble. J'utilise zopeproject. J'ai donc modifier le machin qui créer l'application Zope. A savoir le fichier startup.py comme ceci:

def application_factory(global_conf, conf='zope.conf', vhost='www.gawel.org'):
    vhost = '/++vh++http:%s:80/++' % vhost
    zopeapp = zope.app.wsgi.getWSGIApplication(zope_conf)
    def zopewrapper(environ, start_response):
        environ['PATH_INFO'] = vhost + environ['PATH_INFO']
        return zopeapp(environ, start_response)
    return zopewrapper

Et hop, ça roule. L'avantage, en plus d'avoir une url propre en entrée, c'est que vu que je développe aussi derrière Apache, j'ai juste eu à changer mon fichier debug.ini pour prendre en compte mon virtual host de développement.

En fait j'ai fais un peu mieux que tout ça, car comme dit au début, le but était d'utiliser Paste#urlmap. La source de la bidouille en question est ici.

Aller, pendant que j'y suis, j'en chiais aussi pas mal pour déterminer vers quel backend rediriger les requêtes dans varnish. Tester des ++ dans l'url, ça lui plaisait pas du tout. Vu que j'utilise Apache devant (surtout pour subversion), j'ai trouvé le truc. Il suffit d'activer le module headers:

# a2enmod headers

Puis rajouter un truc du genre dans votre virtualhost Apache:

RequestHeader set VARNISH_BACKEND gawel_org

Vous l'aurez compris, ceci ajoute un header à la requête. Ensuite, dans varnish, on test ce header:

if (req.http.VARNISH_BACKEND ~ "gawel_org") {
    set req.backend = gawel_org;
}

Et le tour est joué. Il faut bien sur que toutes les requêtes entrantes aient ce header. Pour moi ce n'est pas un problème vu que tout passe par apache.

nose doctest plugin sucks

 

En ce moment je bosse sur une application en Pylons. J'adore ce petit framework, mais y a un truc que je pouvais pas encadrer, c'est de faire des tests avec des TestCase. Je préfère de loin les doctests.

Me voilà donc partit à la recherche de docs pour pouvoir écrire mes tests comme j'aime les écrire. Pylons utilise nose comme framework de test. Je découvre alors avec joie que nose fournit un plugin pour parcourir les doctests. Chouet !

Le problème, c'est que ce plugin est carrément rudimentaire. En gros, il choppe vos doctest et les initialise ultra basiquement. Comprendre: impossible de passer des options telles que optionflag, setUp ou tearDown. En bref, ça pu. Comment je fais pour initialiser mon framework Pylons pour mes tests moi ? Hein ?

J'ai finalement trouvé une solution en surclassant la classe doctest.DocFileCase afin de faire ce que je veux. Voici le code en question. Il suffit de le placer dans le fichier tests/functional/test_docs.py de votre application Pylons:

# -*- coding: utf-8 -*-
import os
import doctest
import mypylonsapp
from mypylonsapp.tests import *

optionflags = (doctest.ELLIPSIS |
               doctest.NORMALIZE_WHITESPACE |
               doctest.REPORT_ONLY_FIRST_FAILURE)

dirname = os.path.join(os.path.dirname(mypylonsapp.__file__), 'docs')


def build_testcase(filename):
    name = os.path.splitext(filename)[0]
    path = os.path.join(dirname, filename)

    class Dummy(doctest.DocFileCase, TestController):
        def __init__(self, *args, **kwargs):
            # init pylons stuff
            TestController.__init__(self, *args, **kwargs)

            # get tests from file
            parser = doctest.DocTestParser()
            doc = open(self.path).read()
            test = parser.get_doctest(doc, globals(), name, self.path, 0)

            # init doc test case
            doctest.DocFileCase.__init__(self, test, optionflags=optionflags)

        def setUp(self):
            """init pylons stuff and make app available in doctest
            """
            TestController.setUp(self)
            test = self._dt_test
            test.globs['app'] = self.app

        def tearDown(self):
            """cleaning
            """
            TestController.tearDown(self)
            test = self._dt_test
            test.globs.clear()

    # generate a new class for the file
    return ("Test%s" % name.title(),
            type('Test%sClass' % name.title(), (Dummy,), dict(path=path)))

for filename in os.listdir(dirname):
    if filename == '.svn':
        continue
    name, klass = build_testcase(filename)
    exec "%s =  klass" % name

# clean namespace to avoid test duplication
del build_testcase, filename, name, klass

Vous admirerez la ruse qui est de générer une nouvelle classe pour chaque fichier trouvé dans le répertoire contenant les doctests.

On peut ensuite créer un fichier texte dans docs/ et y écrire des tests du genre:

>>> response = app.get(url_for(controller='main', action="index"))
>>> print response
Response: 200
...

Ce qui est tout de même vachement plus convi qu'un test classique.

Packager ses scripts Python

 

Bon nombre de gens utilise python pour faire de petits scripts. Le problème c'est que pour les distribuer, ensuite, c'est pas le top.

Heureusement il y a distutils !!

distutils est un paquet inclus dans les distributions python permettant de créer des paquet python.

Le principe est simple. On englobe un module python dans un paquet contenant un fichier setup.py

Le plus simple est d'utiliser paste pour créer son paquet. Renseignez bien les information demandées. Elles seront visible si vous décidez de distribuer votre paquet par la suite. Donc:

$ easy_install -U PasteScript
$ paster create monscript
$ cd monscript
$ ls
monscript/ monscript.egg-info/ setup.cfg setup.py

Ceci nous créer un répertoire monscript contenant un setup.py et un sous répertoire destiné à recevoir le code python.

Nous devons maintenant créer un point d'entrée pour notre script. Pour cela, nous allons modifier monscript/__init__.py pour qu'il ressemble à ça:

def main():
    print 'Yeah !'

Ensuite, en modifiant le fichier setup.py, nous pouvons associer ce point d'entrée à un véritable script qui sera installé à l'installation du paquet. Modifiez la section entry_points du setup.py pour qu'il ressemble à quelque chose du du genre:

entry_points="""
# -*- Entry points: -*-
[console_scripts]
mon_super_script = monscript:main
""",

Voilà, le tour est joué. Alors, pourquoi tout cela pour un simple script ? C'est simple. Vous pouvez maintenant aisément le distribuer.

Voici les principales commandes qui vous serons utiles:

  • créer un tarball:

    $ python setup.py sdist
    
  • créer un egg:

    $ python setup.py bdist_egg
    
  • rendre le paquet disponible sur pypi:

    $ python setup.py sdist bdist_egg register upload
    

Un utilisateur lambda pourra ensuite l'installer simplement:

  • via le tarball:

    $ wget http://exemple.com/monscript-0.1.tar.gz
    $ tar monscript-0.1.tar.gz
    $ cd monscript
    $ python setup.py install
    
  • via pypi:

    $ easy_install -U monscript
    

Moralité, distutils rends la vie plus facile.