En esta segunda entrega correspondiente a la serie Mini Aplicaciones Web con Python y Juno vamos a utilizar el framework juno y la potencia de python para construir una pequeña aplicación web, un servidor de integración continua. ¡No os lo perdáis!

Autor:

Pythonista de corazón, programa en cualquier cosa que se mueva, puedes seguirle en twitter o conocer un poco más acerca de javier santana.
Descarga Demostración

Este tutorial forma parte de una serie

Descripción de la aplicación

Te preguntarás, ¿qué diablos es eso de un servidor de integración continua? Tiene un nombre espectacular, pero no es más que un servicio web que ejecuta un comando y almacena si la ejecución de ese comando fue bien o mal. Lo normal es que ese comando se encargue de compilar, pasar los test y otras tareas de una aplicación cada vez que hacemos un commit. Puedes leer un poco más sobre integración continua en la wikipedia.

En este tutorial vamos a usar bastantes cosas, por una parte vamos a dar un interfaz web donde se puedan ver si la aplicación ha pasado los test en cada commit, además llamaremos a procesos externos, lanzaremos threads y todo en poco más de 100 líneas de código.

El modo de funcionamiento de la aplicación es simple. Cuando se le indique a través del intefaz web, se va a bajar el último código disponible del repositorio que le indiquemos al arrancar y va a lanzar el proceso de compilación o test y mostrará los resultados en la web.

Base de datos

Primero es configurar juno y la base de datos. En este caso usamos sqlite (una base de datos embebida) y el fichero de base de datos es cipy.db. No habría problema es especificar mysql u otra.
El puerto lo cambiamos a 8000 para evitar que se pegue con un servidor web que podamos tener en el 80.

Un build no es más que una serie pasos que se ejecutan para obtener el producto final. Suele estar asociado a un binario final. Dentro de esos pasos normalmente está la compilación, ejecución de test, creación de paquetes instalables, etc, etc. En web lo habitual es hablar de deploy.

Además preparamos la tabla de builds, es decir, la tabla donde quedan registrados todos los builds de la aplicación. Los campos son:

  • date: fecha cuando se lanzó el build
  • result: resultado. 0 si falla 1 si todo fue bien
  • output: en el proceso de compilación/test puede que se genere un log por la salida standard, quedará registrado en esta variable
  • finished: indica si el proceso ha terminado
  • rev: revisión que a la que corresponde ese build (sha1 si es git, un número natural si es subversion)
init({'db_location': 'cipy.db', 'dev_port':8000})
Build = model('Build', date='str', result='int', output='str', finished='boolean', rev='str');

Vayamos a por la interfaz web.

Interfaz web

El interfaz web tiene dos partes, la primera que es donde se muestra al usuario los builds y los resultados de cada uno y luego tenemos dos servicios que sirven para lanzar los builds e indicar el resultado final de un build.

interfaz de usuario

Vamos a mostrar los últimos 10 builds, por tanto, usando el API de base de datos se recogen los 10 últimos ordenados en orden descendente y se meten en una lista. Es fácil ver la equivalencia a una consulta SQL. Esos datos se le pasan al template html para que los represente. Además le pasamos el path donde está el repositorio alojado en la máquina.

@route('/')
def index(web):
  builds = find(Build).order_by(Build.id.desc()).limit(10).all();
  template("index.html", { 'builds': builds, 'project_path': repo_path })

En la parte HTML tenemos dos partes interesantes. Por una parte como renderizar los datos que nos pasa el controlador (index), usando un bucle en el cual, en función de si el build ha finalizado o no o si el build fue bien o no muestra los datos. El sistema de templates es ni más ni menos que python, así que si sabes python no tendrás problema en usarlo.

{% for b in builds%} 
<li><span class="date">{{b.date}}</span>
          {% if b.finished %}
             rev: {{ b.rev }}           
             {% if b.result == 0 %}
                <span class="ok">PASS</span>
             {% else %}            
              <span class="fail">FAIL</span>           
             {% endif %}            
             <a id="{{b.id}}" class="show_output" href="#"> show output </a></li>
             <div class="output">
                {{b.output}}</div>      
          {% else %}
            <span class="building">building...</span>
          {% endif %}
   {% endfor %}

En la parte javascript, se usa jquery para el manejo del DOM y hacer peticiones AJAX.

La primera parte se encarga de ocultar o mostrar lo que mostró por pantalla el comando que ejecutó el servidor de CI.

$(document).ready(function() {
          $(".output").hide();
          $(".show_output").click(function() {
            out = $(this).attr('id');
            $("#"+out+"_output").toggle();
          });

La segunda parte lanza una petición al servicio web de la aplicación (más adelante se explica en detalle) cuando se pincha en el enlace de build.

$("#build").click(function(){
            $("#build_msg").html("starting...");
            $.post("/build",callback=function(data) {
                $("#build_msg").html(data);
                location.reload();
            });
          });

Por último si hay algún build activo se recarga la página cada 30 segundos para mostrar el resultado en caso de que haya acabado.

if($('.building').length != 0)
{
  setInterval(function(){
    location.reload();
  },30*1000);
}

servicios

El servicio de lanzamiento del build es muy simple, lanza el un comando :) . Cuando se le llama recoge la versión actual del repositorio, crea un objeto Build (añade un registro en la base de datos) indicando que hay un trabajo lanzado y por último lanza un hilo donde se baja actualiza la copia local del repositorio y ejecuta el proceso de compilación. Cuando el proceso termine llamará al servicio explicado más abajo para indicar que ha terminado.

@route('/build')
def build(web):
  #coge la última revisión
  data, ret = cmd(scm_cmds[repo_type]['rev'], repo_path);

  # crea el objeto build que indica que el proceso ha sido creado
  b = Build(date=datetime.datetime.now().strftime("%b%d %H:%M"), finished=False, rev=data[:6])
  b.save(); # lo almacena en la BBDD

  # lanza el thread para ejecutar el build
  # la función que ejecutará el thread es build_work y se le pasan los argumentos en la variable args
  threading.Thread(target=build_work, args=(b.id,)).start();
  return "scheduled!"

Y como sabe este servidor qué comando debe lanzar cuando le ordenamos que haga un build. Algunos servidores de IC se configuran en el interfaz web el comando a lanzar, nosotros vamos a usar otro sistema, ejecutaremos un fichero predefinido. El fichero REPO/.ci/build será el que lance la aplicación cada vez que se lance un proceso de build y en función del valor de retorno la aplicación dará por válidos los test o no.

Por último tenemos el servicio que indica a la aplicación web que se ha terminado de ejecutar el comando REPO/.ci/build, para que almacene el resultado y se pueda ver en el interfaz web. Lo interesante de este servicio es que muestra como recoge los datos pasados como parámetros de una petición POST, usando el diccionario web.input(). Sólamente se limita a recoger los datos y actualizar la base de datos.

@route('/finish')
def build_finished(web):
    b = find(Build).filter(Build.id == web.input()['id']).one()
    b.finished = True;
    b.output = web.input()['output'];
    b.result = web.input()['ret'];
    b.save();
    return "ok";

lógica

Por último la aplicación usa el API de python para ejecutar los comandos de sistema, recoger los resultados, lanzar hilos y hacer peticiones http. Si tienes curiosidad puedes ver las funciones cmd, exec_ci_cmd y build_work. La más interesante es esta última, en la cual después de lanzar el comando, llama al servicio “finished” que hemos visto antes para indicar el estado del proceso una vez terminado.

def build_work(build_id):
    cmd(scm_cmds[repo_type]['reset'], repo_path);
    data, ret = exec_ci_cmd("build");
    if ret != None:
      data = data.replace("\n", "
");
    else:
      data = "%s file not found, i don't know how to build" % join(CIPY_FOLDER, "build");

    # llamar al servicio web (POST)
    params = urllib.urlencode({'id': build_id,  'output': data, 'ret': ret})
    urllib.urlopen("http://127.0.0.1:8000/finish", params).read();

    # hooks, comandos que se lanzan después de ejecutar el comando
    if ret == 0:
      exec_ci_cmd("build_pass");
    else:
      exec_ci_cmd("build_failed");

Conclusión

Con este tuto terminamos la serie sobre juno. En resumen podemos decir que no es un framework válido para grandes aplicaciones web, sin embargo sí es útil para pequeños servicios como este.

Como curiosidad decir que esta pequeña aplicación web la uso en mi trabajo en un linux sobre un embebido y se dedica a compilar una aplicación con cada commit. Para enlazar son los commits de subversion símplemente tenemos que usar wget en el post-commit-hook:

#!/bin/sh
wget http://ciserver/build -O/dev/null

¡Nos vemos en próximas publicaciones!

¿Necesitas desarrollar un proyecto web o para móviles? ¡Estamos disponibles!

Visitar Cokidoo

Cokidoo, los creadores de Ontuts, desarrollamos proyectos tecnológicos centrados en redes sociales y aplicaciones web, aplicaciones móviles y consultoría web y bases de datos.

Somos jóvenes, inquietos, versátiles, apasionados por la innovación y enfocados en las nuevas tecnologías. Con Ontuts tratamos de compartir nuestro conocimiento adquirido en los distintos proyectos, ayudando a la comunidad y mostrando nuestra capacidad tecnológica.

Si necesitas un presupuesto sin compromiso, estamos disponibles, no dudes en contactar con nosotros.

Comentarios en esta publicación (5 comentarios)

¿Te ha gustado esta publicación? ¡Puedes compartir tu opinión con todos nosotros! Simplemente pincha aquí mismo.

Cada vez que leo tutoriales o artículos sobre python se me ponen los dientes larrrgos >_<

Juan Jose Alonso (KarlsBerg)

No encuentro yo la pagina web de Juno… esta super desconocido no? Yo soy pythonista y use y oí otros como Django, webpy, turbogears, pyjama, pero nunca oi hablar de el. ¿Cual es su web?

:S
http://www.google.es/search?hl=es&rlz=1C1CHMI_esES358ES360&q=python+juno+framework&btnG=Buscar&meta=&aq=f&oq=

Muy bueno el tuto, me parece super interesante lo de ejecutar tests u otras acciones en cada commit

Gracias!

lucas

buenaso recien me entero de eese de python