Retour d'expérience sur Buildbot 1/3

Buildbot est une plate-forme générique bien connue pour construire des systèmes d'intégration continue. Je pratique depuis le début de l'année. Présentation générale des concepts, et un peu de multi-repo.
par Georges Racinet, mis à jour le 09/09/2012

Depuis que j'avais pris en main le développement de CPS, je caressais l'idée de remettre en place un outil d'intégration continue comme Buildbot, qui était utilisé à l'époque Nuxeo.

Les implications de l'intégration continue vont bien au-delà du simple lancement périodique des compilations et tests unitaires ; il peut s'agir:

  • de lancer une masse de tests qui serait insupportable pour le développeur à chaque commit ou train de commits. Dans les grosses applications modulaires, les tests d'intégration sont probablement les plus importants, mais ils peuvent être très longs
  • de lancer les tests dans différents environnements (système d'exploitations, middlewares, bases de données), et pourquoi pas aller jusqu'à faire de la veille sur les montées de version autour du projet ?
  • de faire du processus de livraison une banalité exécutée tous les jours : non seulement on fournit très facilement des versions journalières à la communauté sous diverses formes, mais on produit les versions officielles de la même façon (jusqu'aux paquets pour distributions)
  • de lancer des tests de charge et repérer ainsi les chutes de performances
  • de mettre en ligne automatiquement les diverses formes de documentation

J'ai beaucoup entendu dire que Buildbot n'était pas adapté à un produit multi-versionné comme CPS, et on me conseillait plutôt d'aller vers Jenkins, mais le monde Java a tendance à me rebuter

Le tutoriel m'a très rapidement convaincu que la flexibilité était bien plus grande que l'a priori que j'avais par ouï-dire. Dans l'ensemble, à chaque fois que je me suis dit « ce serait bien si…», j'ai constaté que c'était prévu ! Il reste vrai que les cas multi-versionnés n'ont pas (encore) de solution standard prévue par Buildbot, mais c'est aussi parce qu'il est difficile de proposer une façon de faire qui s'adapte à tous les cas, comme le souligne la FAQ.

La documentation de Buildbot est très complète, et on peut la mettre à jour par le processus standard Github. Il n'est pas question de la répéter ici, mais je vais quand même expliquer très vite les concepts de base, avant de détailler comment j'ai pu les appliquer dans quelques cas de figure :

  • une petite application web en repoze.bfg (renommé maintenant en Pyramid) versionnée avec deux dépôts Mercurial (core et web) ;
  • un projet dans lequel une partie gestion en OpenERP et une partie grand public en Django dialoguent en XML-RPC et partagent la même base PostgreSQL. Celui-ci illustre bien les problématiques à résoudre lorsque les tests dépendent d'une configuration de ports interne et externe, une plaie pour les tests unitaires, un mal nécessaire pour les tests fonctionnels et de charge
  • CPS : une trentaine de dépôts Mercurial évoluant de concert, regoupés en trois distributions, sur deux versions de Zope, avec une branche stable et une instable (9 combinaisons à tester);

Je ne prétends pas que les solutions trouvées soient idéales, mais elles ont le mérite de fonctionner.

Par contre, c'est pour moi aussi une occasion de souligner la puissance de la configuration programmatique : il y a des choses qu'on ne peut faire efficacement qu'en ayant un langage de programmation sous le capot, et qu'un format purement déclaratif, que ce soit en format INI ou XML ne pourra jamais faire, ou alors au prix d'une duplication insoutenable à court terme.

Principes de base

Builds et esclaves

Buildbot sert à lancer automatiquement des builds, notion à prendre au sens large : par exemple compilation, exécution de tests, création d'archives à partir du code source, mise à jour de documentation. Un build est l'exécution d'un builder.

Buildbot est un système réparti, suivant le modèle maître et esclaves ; toute la configuration, dont la définition des builders, est faite au niveau du maître, les esclaves ne font qu'exécuter les commandes transmises par le maître et lui renvoyer les résultats.
La configuration des builders est organisée en étapes (steps), dont la mise à jour du code source.

Les builders ont chacun leur sous-répertoire réservé sur le système de fichiers de l'esclave. Il est important de faire le moins de présupposés possible dans la définition des builds sur le contexte système général de l'esclave, sous peine d'avoir du mal à ajouter des esclaves quand le réseau grossit. Quelques exemples :

  • Si l'on ne peut éviter de supposer que l'on dispose d'un serveur PostgreSQL en local (cela peut-être même l'objet du test : vérifier le comportement sur la version officielle d'une distribution donnée), il faut éviter d'être obligé de faire des présupposés sur le port sur lequel il répond.
  • Si l'on doit lancer une application qui écoute sur un port réseau, on doit éviter de coder en dur le numéro de port.

La prise de décision

La décision de lancer un build est prise par un objet scheduler, toujours défini dans la configuration du maître.

Les schedulers utilisent eux-mêmes des informations provenant des change sources, qui surveillent les dépôts de code.
Suivant le système de contrôle de version (VCS) utilisé, il y a des change sources par interrogation régulière (poll) ou par appel direct depuis le VCS lui-même (hook).

Dans le cas des tests automatisés, il est important de lancer les builds en temps quasi-réel en fonction des commits : cela permet de corriger les effets de bord à un moment où les développeurs concernés ont leurs modifications en tête.
Buildbot inclut dans son rapport une liste des commits suspects (blame list). Idéalement, celle-ci ne doit vraiment comporter que les commits vraiment concernés. On verra que dans les cas qui nous intéressent, ce sera forcément un compromis.
Il faut également éviter de lancer trop de builds inutiles, sous peine de ralentir tout le réseau, surtout si les machines esclaves ne sont pas entièrement dédiées en tant qu'esclaves.

Il y a également des schedulers chronlogiques, et la possibilité de lancer des builds depuis l'interface web (force-build)

Les cas qui m'intéressent sont versionnés avec Bazaar ou Mercurial, pour lesquels il faut fonctionner par hook. Cela impose d'avoir la main sur la machine qui tient la branche à tester (souvent une branche de référence), ou d'en faire un miroir local.

Flexibilité : les propriétés

Tous les objets de la chaîne de décision et traitement lisent et écrivent dans un dictionnaire (association clef/valeur) partagé, les propriétés (Properties).

Par exemple, c'est par ces propriétés que le numéro de révision des sources est récupéré par le build. On peut aussi attacher des propriétés à l'esclave sur lequel le build s'exécute (par exemple le port sur lequel le serveur de base de données ambiant écoute)
ou les spécifier dans l'interface web en cas d'exécution manuelle.

Toutes les étapes qui constituent la définition du builder peuvent utiliser les propriétés.

Exemple: une application en deux parties

C'est le plus simple cas possible de dépôts multiples.

Attention, ce qui suit ne fonctionne bien que dans le cas général où le build est causé par le passage d'un changeset, mais par exemple pas pour le premier build d'un slave. Il m'a fallu insister (et faire plus sale) pour traiter ces cas au bord. De plus, ne pas espérer grouper les changesets par trains avec le treeStableTimer. Le futur Buildbot 0.8.7 traite tout cela nativement, et bien mieux. J'espère trouver le temps d'en parler bientôt. [Ajouté le 28/08/2012]

Contexte

Anybox développe une application web spécifique, qui est organisée en deux parties : monappli.core, qui définit des algorithmes de base mais ne fournit aucune interface utilisateur, et monappli.web, qui est une interface utilisateur en repoze.bfg. C'est une séparation très classique.

La plupart du temps,  les deux parties sont en développement actif. La partie web appelle les APIs définies dans le core, il est donc important de tester la partie web s'il y a des modifications dans le core. Chaque partie est versionnée indépendamment, en Mercurial.

Fonctionnement

Lorsque le change source reçoit l'information par le hook du VCS sur la disponiibilité de nouveaux commits, il note dans les propriétés :

  • de quel dépôt il s'agit (en général, le chemin sur le système de fichiers du VCS)
  • la révision concernée ; celle-ci n'a de sens que pour le dépôt concerné

Pour commencer, on met un scheduler qui réagit sur les deux dépôts qui nous intéressent, par une expression régulière sur le nom de dépôt :

SCHEDULERS = [                                                                  
SingleBranchScheduler(
name="monappli",
change_filter=ChangeFilter(branch='default',
repository_re='.*/monappli/.*'),
builderNames=["monappli"]),
]


La configuration du builder commence par la mise à jour des deux dépôts, sous la forme de deux steps de type Mercurial.
Sur chacun, je mets une condition d'exécution en fonction du dépôt pour éviter de faire un update de monappli.core sur une révision qui n'existe que dans monappli.web :

from buildbot.process.factory import BuildFactory
from buildbot.steps.source.mercurial import Mercurial

monappli_factory = BuildFactory()

def check_repo_name(val):
"""Return a function that can performs a repository name check on given val"""
def check(step):
return step.getProperty('repository').rsplit('/', 1)[-1] == val
return check

monappli_factory.addStep(Mercurial(
repourl='http://hg.example/Monappli/core',
mode='full',
method='fresh',
branchType='inrepo',
description='hg:core',
workdir='build/core',
doStepIf=check_repo_name('core')
))

monappli_factory.addStep(Mercurial(
repourl='http://hg.example/Monappli/web',
mode='full',
method='fresh',
branchType='inrepo',
haltOnFailure=True,
workdir='build/web',
doStepIf=check_repo_name('web')
))

En résumé, on peut enregistrer une fonction python grâce auparamètre doStepIf, et celle-ci accède aux propriétés. Pour éviter de trop dupliquer, on a ici un modèle de fonctions (check_repo_name), mais ce n'est pas l'essentiel.

Le paramètre workdir permet de spécifier où ranger les sources, ce qui sera utile dans les steps suivants, comme celui-ci, qui appelle simplement

monappli_factory.addStep(ShellCommand(
command=['core/bin/python', 'core/setup.py', 'test'],
haltOnFailure=True,
description='develop:core'))

C'est simple, mais ce n'est pas facilement généralisable à un grand nombre de dépôts : même en créant les steps dans des boucles, se baser sur le nommage deviendrait trop aléatoire.
Cela dit, on peut espérer pour ceux qui travaillent avec beaucoup de dépôts vivants en parallèle qu'ils ont des outils pour le faire, et que l'on peut les utiliser dans le cadre de buildbot. Ce sera l'objet du troisième article de la série : le cas CPS.