Retour d'expérience sur Buildbot 1/3

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.