Integración continua con Drupal 7 (I/V)

01/05/2013

Este post es el primero de una serie de cinco (que espero poder escribir) en los que trataremos la manera de pasar un proyecto desarrollado con Drupal 7 a integración continua.

¿Cuál es el objetivo final de todo esto?, ¿qué es lo que perseguimos en todo este proceso?

Lo que se pretendo conseguir con este proceso de integración continua no es ni más ni menos que mantener a nuestros clientes totalmente informados, es más sobreinformados, de los avances en el proyecto de la misma manera que cuando compramos cualquier producto en cualquier tienda en Internet (de mediana calidad).

Todo esto se puede realizar regularmente de manera manual, remitiéndole a nuestros clientes un informe de evolución del proyecto en el que se detalle el grado de evolución de cada una de las tareas que se han completado y sobre todo cuál es el aspecto que va tomando su proyecto desplegando en nuestro entorno de preproducción.

Evidentemente realizar este proceso de una manera manual es algo que consumiría mucho tiempo, y que por tanto es susceptible de ser automatizado, en mayor o menor medida, con el fin de conseguir una mayor productividad.

Para conseguir este objetivo necesitamos, en primer lugar, definir la infraestructura de desarrollo que vamos a utilizar. En el siguiente esquema se muestra una posible definición de la misma.

schemas_01

Como se puede observar tenemos varios programadores cada uno de los cuales cuenta con un entorno de desarrollo propio, conectado con una base de datos común, estos programadores sincronizan su código entre ellos a través de SVN.

Uno de los elementos que necesitaremos será un sistema de integración continua que se encargue de ejecutar tareas de manera automática, en este caso he optado por Jenkins por su rapidez de instalación (aunque no pierdo de vista CruiseControl). Las tareas a ejecutar serían:

  1. Ejecución de pruebas
  2. Despliegue automático del proyecto
  3. Notificación automática del resultado
  4. Envío de informe de evolución

Los puntos 1, 3 y 4 son los que aún están pendientes, pero el punto 2 sí lo tengo mucho más avanzado y es el que vamos a ver ahora … manos a la obra.

Lo primero un esquema de vamos a montar.

ci

Para llevar a cabo el despliegue automático vamos a desarrollar un script SH que haciendo uso de drush, se encargue de:

  1. Generar el fichero de despliegue
  2. Realizar el backup de la copia anterior en el entorno de preproducción
  3. Desplegar la nueva versión en el entorno de preproducción
#!/bin/bash

# VARIABLES
fecha=$(date +"%Y%m%d%H%M%S")

inicio=$(date +"%s")

project=P0001_proyecto_de_prueba
project_name=drupalci
project_repo=http://192.168.1.126/repo/$project
project_repo_trunk=/trunk

deploy_env_host=192.168.1.125
deploy_env_user=deploy
deploy_env_path=/var/www/$project

# Comienza la ejecución
echo '['$(date +"%d/%m/%Y-%H:%M:%S:%S")'] Comienza el proceso de despliegue del proyecto '$project

# Se hace un tag del trunk svn
echo '['$(date +"%d/%m/%Y-%H:%M:%S")'] Se genera la etiqueta '$fecha
svn copy $project_repo$project_repo_trunk $project_repo/tags/$fecha -m 'Etiqueta correspondiente con el despliegue '$fecha

# Se hace un export del tag
svn export $project_repo/tags/$fecha /tmp/$project/$fecha/
echo '['$(date +"%d/%m/%Y-%H:%M:%S")'] Se genera un export de la etiqueta '$fecha

# Se genera el fichero de release
cd /tmp/$project/$fecha/
drush archive-dump default --destination=/tmp/$project/$fecha/$fecha.tar
echo '['$(date +"%d/%m/%Y-%H:%M:%S")'] Se genera el fichero de release en '/tmp/$project/$fecha/$fecha.tar

# Se sube al entorno de despliegue
scp /tmp/$project/$fecha/$fecha.tar $deploy_env_user@$deploy_env_host:$deploy_env_path
echo '['$(date +"%d/%m/%Y-%H:%M:%S")'] Se sube al entorno de despliegue el fichero generado'

# Se hace un backup de la copia anterior y se elimina
ssh $deploy_env_user@$deploy_env_host 'rm -rf '$deploy_env_path'/old.tar'
echo '['$(date +"%d/%m/%Y-%H:%M:%S")'] Se elimina la copia del día anterior'
ssh $deploy_env_user@$deploy_env_host 'cd '$deploy_env_path'/'$project_name'; drush archive-dump --destination='$deploy_env_path'/old.tar'
echo '['$(date +"%d/%m/%Y-%H:%M:%S")'] Se genera una nueva copia del despliegue del día anterior'
ssh $deploy_env_user@$deploy_env_host 'rm -rf '$deploy_env_path'/'$project_name
echo '['$(date +"%d/%m/%Y-%H:%M:%S")'] Se elimina el despliegue anterior'

# Se despliega y se elimina el fichero de release
ssh $deploy_env_user@$deploy_env_host 'drush archive-restore '$deploy_env_path'/'$fecha'.tar --db-url=mysql://root:root@localhost/drupalci --destination='$deploy_env_path'/'$project_name
echo '['$(date +"%d/%m/%Y-%H:%M:%S")'] Se despliega la plataforma'
ssh $deploy_env_user@$deploy_env_host 'rm -rf '$deploy_env_path'/'$fecha'.tar'
echo '['$(date +"%d/%m/%Y-%H:%M:%S")'] Se elimina la copia de despliegue'

# Se modifica el fichero de settings por el de producción
ssh $deploy_env_user@$deploy_env_host 'rm -rf  '$deploy_env_path'/'$project_name'/sites/default/settings.php'
echo '['$(date +"%d/%m/%Y-%H:%M:%S")'] Se elimina el fichero de settings'
ssh $deploy_env_user@$deploy_env_host 'mv '$deploy_env_path'/'$project_name'/sites/default/settings_pro.php '$deploy_env_path'/'$project_name'/sites/default/settings.php'
echo '['$(date +"%d/%m/%Y-%H:%M:%S")'] Se activa el fichero de settings de producción'

# Se elimina el fichero generado en local
rm -rf /tmp/$project
echo '['$(date +"%d/%m/%Y-%H:%M:%S")'] Se elimina el fichero local generado'

# Finaliza el proceso
fin=$(date +"%s")
tiempo=$((fin - inicio))
echo '['$(date +"%d/%m/%Y-%H:%M:%S")'] El proceso ha concluido con éxito en '$tiempo' segundos'

Como podemos ver el script no es totalmente funcional, ya que no cuenta con gestión de errores, que en definitiva serán los que indicarán si el proceso ha terminado correctamente.

Como pre-requisitos para este script debemos contar con:

  • Cliente SVN por consola 
  • Instalación de DRUSH
  • Acceso por SSH por certificado

¿Qué os parece?, ¿alguna aportación?


Hacia portales OpenCms en integración continua

20/07/2010

Hace ya muchísimo tiempo que no escribo nada en el blog. La verdad que no ha sido por falta de ganas, si no como suele ocurrir por falta de tiempo. En todo este tiempo he seguido trabajando en el módulo OpenCms Free Balance pero, desde que los amigos de OpenCms Hispano me invitaran como ponente a la 2ª tarde tecnológica de OpenCms, he estado trabajando en algo que por aquellos días solo conseguí esbozar y que la verdad sea dicha me ha costado mucho más de lo que en un principio pensaba, pero creo que los resultado han merecido la pena: un plugin de maven que permite a los desarrolladores, tras compilar el módulo, desplegarlo en OpenCms automáticamente

En un principio esa fue realmente la idea original, para la versión beta, ya que después, dándole una vuelta de turca más en base a mi experiencia en el desarrollo de portales haciendo uso de tecnología OpenCms, se me ocurrió que sería mucho más interesante que el plugin, además permitira el despliegue del módulo en un servidor remoto de forma que hiciera factible la integración continua.

Aunque crear un nuevo plugin en Maven es realmente fácil, los mayores problemas han sido:

  1. Gestionar todas las dependencias necesarias para la obtención de un objeto CmsObject fuera del contexto de OpenCms.
  2. Obtención del propio objeto CmsObject, en función de la versión de OpenCms que se use el modo de obtención de dicho CmsObject será distinto.
  3. Importación y publicación del módulo dentro del contexto de OpenCms.

Solucionar el problema fue fácil aunque muy tedioso ya que el proceso consistió en compilar el módulo ejecutar el plugin y cuando fallaba se busca y declara la dependencia, el resultado final ha sido:

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>servlet-api</artifactId>
<version>6.0.26</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
</dependency>
<dependency>
<groupId>oro</groupId>
<artifactId>oro</artifactId>
<version>2.0.8</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>20040616</version>
</dependency>
<dependency>
<groupId>org.safehaus.uuid</groupId>
<artifactId>uuid</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons.digester</groupId>
<artifactId>digester</artifactId>
<version>1.8</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>20030211.134440</version>
</dependency>
<dependency>
<groupId>quartz</groupId>
<artifactId>quartz</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>org.htmlparser</groupId>
<artifactId>htmlparser</artifactId>
<version>1.6</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>jsp-api</artifactId>
<version>6.0.26</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queries</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-snowball</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>commons-email</groupId>
<artifactId>commons-email</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.0.7</version>
</dependency>
<dependency>
<groupId>com.alkacon</groupId>
<artifactId>simapi</artifactId>
<version>0.9.8</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.1</version>
</dependency>

El siguiente problema a que tuve que solucionar fue, como he comentado antes, la obtención de un objeto CmsObject, para ello tuve que apoyarme en el código fuente de varios plugins de eclipse que permiten sincronizar los ficheros físicos con los ficheros de OpenCms, a continuación os dejo algunas de las referencias que me han sido de utilidad:

  1. OpenCms Mod Dev
  2. OpenCms VFS

Y el último problema que tuve que solucionar fue la importación del módulo, importación que para nada es trivial y para solucionarlo una vez más tuve que adentrarme en el código fuente de OpenCms para finalmente encontrar la solución:

String importpath = OpenCms.getSystemInfo().getPackagesRfsPath();

importpath = OpenCms.getSystemInfo().getAbsoluteRfsPathRelativeToWebInf(
importpath + “modules/” + config.getModuleName());

// Se obtiene el módulo
module = CmsModuleImportExportHandler.readModuleFromImport(importpath);

// check the module dependencies
List dependencies = OpenCms.getModuleManager().checkDependencies(module,
CmsModuleManager.DEPENDENCY_MODE_IMPORT);
if (dependencies.size() > 0) {
// some dependencies not fulfilled
StringBuffer missingModules = new StringBuffer();
Iterator it = dependencies.iterator();
while (it.hasNext()) {
CmsModuleDependency dependency = (CmsModuleDependency) it.next();
missingModules.append(”  “).append(dependency.getName()).append(“, Version “).append(
dependency.getVersion()).append(“\r\n”);
}
System.out.println(“[ERROR] ” + module.getName() + “, Version ” + module.getVersion() + missingModules);
}

// check the imported resource types for name / id conflicts
List checkedTypes = new ArrayList();
Iterator i = module.getResourceTypes().iterator();
while (i.hasNext()) {
I_CmsResourceType type = (I_CmsResourceType) i.next();
// first check against the already configured resource types
int externalConflictIndex = OpenCms.getResourceManager().getResourceTypes().indexOf(type);
if (externalConflictIndex >= 0) {
I_CmsResourceType conflictingType = (I_CmsResourceType) OpenCms.getResourceManager()
.getResourceTypes().get(externalConflictIndex);
if (!type.isIdentical(conflictingType)) {
// if name and id are identical, we assume this is a module replace operation
throw new CmsConfigurationException(org.opencms.loader.Messages.get().container(
org.opencms.loader.Messages.ERR_CONFLICTING_MODULE_RESOURCE_TYPES_5,
new Object[] {type.getTypeName(), new Integer(type.getTypeId()), module.getName(),
conflictingType.getTypeName(), new Integer(conflictingType.getTypeId())}));
}
}
// now check against the other resource types of the imported module
int internalConflictIndex = checkedTypes.indexOf(type);
if (internalConflictIndex >= 0) {
I_CmsResourceType conflictingType = (I_CmsResourceType) checkedTypes.get(internalConflictIndex);
throw new CmsConfigurationException(org.opencms.loader.Messages.get().container(
org.opencms.loader.Messages.ERR_CONFLICTING_RESTYPES_IN_MODULE_5,
new Object[] {module.getName(), type.getTypeName(), new Integer(type.getTypeId()),
conflictingType.getTypeName(), new Integer(conflictingType.getTypeId())}));
}
// add the resource type for the next check
checkedTypes.add(type);
}

// Se comprueba si el módulo esta instalado
if(!OpenCms.getModuleManager().hasModule(module.getName())){
OpenCms.getModuleManager().addModule(cmsObject, module);
} else {
OpenCms.getModuleManager().updateModule(cmsObject, module);
}

// reinitialize the resource manager with additional module resource types if necessary
if (module.getResourceTypes() != Collections.EMPTY_LIST) {
OpenCms.getResourceManager().initialize(cmsObject);
}
// reinitialize the workplace manager with additional module explorer types if necessary
if (module.getExplorerTypes() != Collections.EMPTY_LIST) {
OpenCms.getWorkplaceManager().addExplorerTypeSettings(module);
}

// import the module resources
CmsImport cmsImport = new CmsImport(cmsObject,
new CmsShellReport(cmsObject.getRequestContext().getLocale()));
CmsImportParameters parameters = new CmsImportParameters(importpath, “/”, true);
cmsImport.importData(parameters);

cmsObject.publishResource(“/”);

Para la primera versión del módulo, una vez que compruebe el correcto funcionamiento en distintas plataformas el objetivo de esta primera versión estará cumplido, el siguiente paso será que el plugin permita el despliegue remoto de módulos de forma que se permita la integración continua.


Integración continua con PHP (II). Pruebas unitarias.

27/12/2008

En el proceso de construcción del software la implementación y ejecución de pruebas unitarias es, entre otros, uno de los pilares fundamentales. La creación de tests unitarios y su ejecución automatizada permiten dotar a dicho proceso de construcción del software de confianza y calidad, permitiendo éstos parámetros, a su vez, la puesta en marcha de metodologías de trabajo ágiles.

En la actualidad existen en el mercado del software libre multitud de frameworks que facilitan la tarea de implementación y ejecución de pruebas  unitarias, concretamente, para entornos de desarrollo basados en tecnología PHP nos encontramos con frameworks como PHPUnit o SimpleTest, aunque también existen frameworks de desarrollo, como Symfony, que proporcionan su propia suite de testing.

En el presente post nos centraremos en PHPUnit y su integración con Phing para un proyecto basado en Symfony 1.0 (instrucciones de instalacion).

Para instalar PHPUnit ejecutaremos las siguientes instrucciones (se supone que tenemos instalado pear):

  • pear channel-discover pear.phpunit.de
  • pear install -a phpunit/PHPUnit

Una vez instalado PHPUnit, crearemos un proyecto symfony (con la correspondiente aplicación y módulo), para este post vamos a crear una clase Contact con su correspondiente test ContactTest. Para ejecutar los tests nos situaremos en la carpeta “/lib/classes/” del proyecto y ejecutaremos la siguiente instrucción:

  • phpunit *Test.class.php

Pero, cómo podemos automatizar la ejecución de las pruebas, es en este punto donde entra en juego la herramienta de la que hablamos en el anterior post, phing, con ella podemos declarar una nueva tarea que ejecutará todas las pruebas unitarias que hayamos implementado para la aplicación que estamos desarrollando. veámoslo.

En primer lugar tendremos que crear un nuevo fichero build.xml con el que construiremos nuestra aplicación, este fichero contará, entre otras, una tarea de ejecución de pruebas que será algo como lo siguiente:

<target name=”test”>
<phpunit2 haltonfailure=”false” printsummary=”true”>
<batchtest>
<fileset dir=”./lib/classes/”>
<include name=”*Test.class.php”/>
</fileset>
</batchtest>
</phpunit2>
</target>

Con esta tarea le estamos indicando a phing que queremos ejecutar todas las pruebas que se encuentran en el directorio “lib/classes/”, que no queremos que se pare si encuentra alguna prueba no satisfactoria y que queremos que se muestre por pantalla el resumen, a su vez existen otras tareas ligadas a phpunit que permiten obtener reportes más completos como gráficas estadísticas, niveles de covertura, etc.

Como podemos ver la integración de phpunit en phing nos proporciona una forma ágil de ejecutar tests unitarios obteniendo los reportes oportunos.

Aquí dejo el proyecto symfony con el fichero build.xml

En esta ocasión voy a optar por no escribir una frase célebre que más o menos resuma la idea que se escondía detrás de este post, como es mi costumbre, sino que voy a aprovechar para felicitaros a todos estas fiestas y desearos un feliz comienzo de año 2009.


Integración continua con PHP (I)

27/09/2008

Para una empresa que tiene en el Desarrollo de Software una parte importante del núcleo de su negocio disponer de todas las herramientas necesarias para facilitar el desarrollo (IDE´s, licencias de software, entornos de preproducción, etc) de su actividad supone una ventaja respecto a sus competidores.

Hoy vamos a comenzar con el primero de una serie de posts cuyo objetivo final es conseguir configurar un sistema de integración continua para proyectos ejecutados con tecnología PHP. Un entorno de integración continua es una de esas herramientas que facilitan y mejoran todo el ciclo de vida de un producto software.

Un sistema de integración continua está compuesto por una serie de piezas que dispuestas de una cierta forma permitirán realizar integraciones automáticas frecuentes del proyecto, con todas las ventajas que ello supone. Estas piezas que necesitamos son, como mínimo:

  • Sistema de control de versiones (SVN).
  • Sistema de construcción de proyectos (Phing).
  • Servidor de integración continua (Xinc).

En este primer post solo vamos a cubrir las dos primeras partes del sistema de integración continua: sistema de control de versiones y sistema de construcción de proyectos. Para el sistema de control de versiones vamos a utilizar Subversion y como sistema de construcción de proyectos vamos a usar Phing.

Como supongo que todos conocemos lo que es un sistema de control de versiones como Subversion hablemos de Phing. Phing es un sistema de construcción de proyectos basado en Apache Ant que permite automatizar, a través de la descripción en un fichero XML, tareas como: ejecución de pruebas de unitarias, generación de documentación, comprobación de estándares de codificación, generación de “distribuibles”, etc.

Lo primero que haremos será llevar a cabo la instalación del sistema de control de versiones para ello podemos seguir los pasos de pasos de la siguiente guía.

Una vez instalado el sistema de control de versiones pasaremos a instalar Phing, (suponiendo que tenemos ya tenemos instalado PHP) para ello seguiremos los siguientes pasos:

  • apt-get install php5-dev, con este paquete podremos instalar phpize que nos facilitará la instalación de Xdebug.
  • apt-get install pear, PEAR es un sistema de distribución de componentes PHP.
  • pear channel-discover pear.phing.info
  • pear install -a phing/phing, con la opción -a haremos que se descarguen todas las dependencias de Phing.
  • pear install VersionControl_SVN-0.3.1, debemos instalar este paquete porque la configuración por defecto de Pear siempre instala la versión “estable” y para trabajar con phing necesitamos esta versión alpha.

Bien, ya tenemos instalado nuestro sistema de construcción de proyectos, ahora veamos cómo funciona. Para comenzar, una prueba simple (más adelante lo complicaremos más). Supongamos, para no ser demasiado simplista, que estamos desarrollando un proyecto con Drupal y lo tenemos versionado, un nuevo recurso se incorpora al proyecto para poder comenzar a desarrollar lo único que tendrá que hacer es instalar Phing y tras descargar el fichero build.xml

<?xml version="1.0"?>
<project name="drupal" default="dev" basedir=".">
    <target name="dev" depends="checkout"/>
    <svncheckout
       svnpath="/usr/bin/svn"
       username="usuario"
       password="contraseña"
       repositoryurl="http://localhost/svn/portal-drupal/trunk/"
       todir="/home/user/workspace/Test"/>
</project>

Tras hacer esto ya tendrá disponible el entorno de desarrollo listo para comenzar con actividad.

Aunque se presentado un ejemplo muy simple las posibilidades de Phing son, como hemos comentado antes, muy ampllias y las iremos descubriendo en sucesivos posts.

La función de un buen software es hacer que lo complejo aparente ser simple. Grady Booch