Buildbot: cómo se organiza


Buildbot es un servidor de integración contínua. Está implementado en Python y su configuración se realiza también en Python, aunque puede utilizarse para ejecutar cualquier tipo de tarea. Ya escribí un artículo introductorio, pero hoy vamos a entender lo que se hizo allí, volviendo a empezar, pero esta vez desde cero.

Hay muy poca documentación al respecto, por lo que he decido contar un poco más sobre este servidor. En esta ocasión, veremos la organización básica.

Desde muy arriba

La primera división es fácil: hay un maestro y uno o varios clientes. Los clientes se registran en el servidor y, a partir de este momento, el servidor gestiona su disponibilidad, asignándoles trabajos.

Pero... ¿qué es un trabajo? ¿Cómo se asocian trabajos y clientes? ¿Se puede ejecutar de cualquier manera? Para entender esto es necesario bajar un peldaño en la organización.

Desde dentro

Glosario

Antes de continuar, es necesario definir un glosario básico: - slave: cada uno de los clientes. Cuentan como programas, independientemente de la máquina en la que se encuentren. Dicho de otro modo, pueden encontrarse donde el maestro o varios en la misma máquina física. - change source: Eventos que indican que ha ocurrido un cambio. Por ejemplo, un cambio en una rama en un repositorio, en un fichero, etc. - builders: Definen el trabajo propiamente dicho. Están divididos en distintas etapas. - steps: Cada una de las etapas en las que está dividido un builder. - scheduler: Son planificaciones de trabajos. Son la chispa que desencadena una ejecución.

Relaciones

Ya conocemos nuestros componentes. Es hora de ver cómo se relacionan entre ellos. Veremos que resulta bastante sencillo:

Cada slave tiene un nombre único, por el que podemos referenciarlo en cualquier momento. Durante su definición no se relacionan con nadie.

Las change source definen eventos como tales. Desencadenan eventos, por lo que sólo dependen de repositorios o de cambios en el entorno.

Los builders son la parte más importante y compleja. Contienen cada uno de los steps a ejecutar. Éstos serán secuenciales, aunque hay maneras de hacer detach sobre ellos.

Aquí hay una cosa rara en BuildBot: Dado que los builders son tan complejos, se han dividido en dos etapas. Por una parte, hay que crear la lista de steps con un BuildFactory. Una vez tenemos las fábricas, emparejaremos slaves y factories dándoles un nombre y generando el builder propiamente dicho.

Dicho de otra manera, le damos un nombre a la lista de pasos creada y lo asociamos con los slaves que pueden ejecutar esta build.

Los schedulers pueden unir un change source con los builders, de manera que un cambio desencadene una build. Existe un tipo de scheduler especial que tiene un change source implícito: es el de "forzar build"; en este caso, el evento es un click de ratón en la propia página web. En el archivo de configuración no lo veremos porque, como digo, es implícito. En algunos casos, admiten además algún tipo de filtro.

Ejemplo mínimo

Vamos a ver un pequeño ejemplo: un slave, un scheduler y un builder con un único step. Voy a separar los imports y las inicializaciones para facilitar la lectura de lo que realmente importa:

.. code:: python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# imports
from buildbot.buildslave import BuildSlave
from buildbot.schedulers.forcesched import ForceScheduler
from buildbot.steps.shell import ShellCommand
from buildbot.process.factory import BuildFactory
from buildbot.config import BuilderConfig

# inicializaciones
c = BuildmasterConfig = {}
c['slaves'] = []
c['builders'] = []
c['schedulers'] = []

Y ahora el código que realmente añade valor:

.. code:: python

# creamos el slave
c['slaves'].append(BuildSlave("my slave", "shared secret"))

# creamos un step y con él una fábrica
my_step = ShellCommand(name="Dormir", command=["sleep", "60"])
my_factory = BuildFactory([my_step])

# Ahora creamos el builder y lo añadimos a la lista
my_builder = BuilderConfig(
    name="my builder",
        descriptionDone="built", # se muestra en el waterfall cuando ha terminado
    slavenames=['my slave'],
    factory=my_factory)
c['builders'].append(my_builder)

# Finalmente, creamos el scheduler (se lanzará con un click en la web)
c['schedulers'].append(ForceScheduler(
    name="ejecutar",
    builderNames=["my builder"]))

Y eso es todo. En el archivo de configuración por defecto se organiza de otra manera, pero nada más. La gente de buildbot prefiere retrasar los imports e inicializaciones hasta el comienzo del bloque en el que se va a utilizar; los bloques están definidos por comentarios explicativos. Pero a todos los efectos, es lo mismo que este ejemplo.

El resto de la configuración

Hay ciertos parámetros extra que también se indican aquí, como el puerto por defecto en el que se va a escuchar, la base de datos, etc. Me he centrado exclusivamente en la definición de un trabajo sencillo, con el objetivo de enseñar cómo funciona.

Esta información es la siguiente:

.. code:: python

# Configuración:
c['slavePortnum'] = 9989  # igual en los esclavos
c['title'] = "Titulo en la web"
c['titleURL'] = "http://magmax.org"
c['buildbotURL'] = "http://localhost:8010/"
c['db'] = {
    'db_url' : "sqlite:///state.sqlite",
}

# Añadiendo la web por defecto:
from buildbot.status import html
c['status'] = [ html.WebStatus(http_port=8010) ]

En el archivo de configuración autogenerado, esta información general se encuentra desperdigada por todas partes.

Y, a parte de lo que ya hemos contado, el archivo de configuración no tiene nada más.

Un poco de teoría

Lo único importante es definir el BuildMasterConfig (al que hemos hecho un punterito llamado "c" para abreviar). Como veréis, este objeto global es nuestro programa, y en el ejemplo hemos definido las siguientes claves: - slaves: es una lista, con los slaves - builders: lista con los builders, ya asociados a los slaves - schedulers: lista con los schedulers, ya asociados a los builders

Conclusiones

Como se puede apreciar, es una arquitectura muy simple. La dificultad de comprensión viene asociada a la porquería que rodea a las definiciones e inicializaciones y otros requisitos de python.

Además, en mi opinión, la estructura del archivo es errónea, ya que por defecto definen los schedulers antes que los builders, cuando es más natural hacerlo justo al revés.


Comentarios

Comments powered by Disqus