lunes, 28 de diciembre de 2009

Tutorial: ¿Cómo migrar desde Subversion (SVN) hacia Mercurial con Hgsvn?

Mercurial SCM

Después de sumarme al desarrollo del plugin de Trac para RPC (gracias a Odd Simon Simonsen ;o) y trabajar en unas mejoras, he decidido migrar mis proyectos anteriores desde Subversion hacia Mercurial. Aquí describiré una alternativa para realizarla. Por el momento no soy un experto, por lo que el análisis de las ventajas y puntos débiles de esta variante quedará pospuesto hasta que pruebe otras alternativas (si es que eso ocurre ...). Sin embargo, Usted puede formar parte de la búsqueda de la solución más adecuada y opinar u ofrecer sugerencias. Si desea saber acerca de cómo compartir código y colaborar utilizando repositorios de Mercurial, le sugiero que consulte un artículo previo sobre el uso de MQ con Bitbucket. Es así cómo se desarrolla el plugin que les mencioné anteriormente, utilizando su repositorio de parches.

Actualizado 23/04/2010 13:43:09 con hgpullsvn

Estado inicial : Subversion

Subversion ha sido por mucho tiempo la opción más difundida para desarrollar proyectos de software libre. De hecho este es el sistema de control de versiones que fue ofrecido en primer lugar por Sourceforge, Google Code y casi todos los otros grandes patrocinadores del open source. Sin embargo el surgimiento primero de GitHub, luego de Bitbucket y otros sitios similares basados en Bazaar, han hecho que los sistemas distribuidos de control de versiones ganen una fuerza tremenda. En gran parte esto se debe a la inmensa gama de modelos de colaboración que son posibles cuando no se tiene un repositorio centralizado. De hecho, desde que comencé a utilizar SVN la sugerencia que nos hacia mi profesor, Alí, era que le echáramos un vistazo a SVK.

En este momento se gestó el proyecto FLiOOPS con una idea original de Kyrie (a.k.a. Luis Alberto Zarrabeitia) para concebir una implementación de la programación por contratos en Python. Lo que una vez fue una tarea de curso, con el tiempo fue creciendo y se incorporaron nuevas ideas. Entre todas ellas se destacó el módulo dutest, que se utilizó para escribir las pruebas de unidad. El aumento en la complejidad, implicó la existencia de varias ramas de desarrollo y otros elementos a los que Subversion no les da una solución. Por ejemplo acá les muestro la estructura actual del repositorio original :

/
    java
        trunk
        branches
        tags
    py
        trunk
        branches
            add_opts
            mdl_inv
            pep302
            test_pycontract
            guarded_synch
            ochk_arch
            pysimrec
        tags
            dutest
                0_1_0
                0_1_1
                0_1_2
                0_2_1
                0_2_2

La buena noticia es que Sourceforge permite también el uso de (múltiples) repositorios de Mercurial y Git. Inicialmente esto hace posible tener un repositorio para cada lenguaje de programación, o para cada librería que se desarrolle en el marco del proyecto. Sin embargo hay que trasladar todas las versiones ya existentes hacia su nuevo destino.

Opciones para migrar

Después de buscar información e indagar un poco, encontré varias herramientas, pero parece que tres se distinguen del resto:

  • HgSubversion es una librería externa cuyo objetivo es lograr una integración completa con Subversion.
  • El comando convert permite migrar repositorios de distintos tipos (incluso otros repositorios hechos con Mercurial) hacia un repositorio de Mercurial. Como permite filtrar y renombrar los contenidos que serán incorporados, también resulta útil para filtrar repositorios Mercurial completos y obtener un subconjunto de los cambios que resultan de interés. Entre los tipos de herramientas soportadas se encuentran :
  • Hgsvn es un projrcto que surgió a partir de la comunidad de desarollo de Python para dar una respuesta a la migración de los repositorios de la librería estándar. Varias de sus características son:
    • Los datos de las versiones se solicitan por lotes.
    • El proceso es incremental, por lo que es posible interrumpirlo y resumirlo posteriormente.
    • Preserva los meta-datos (e.g. nombres y fechas de los cambios, registros de copia y de cambio de nombres de ficheros).
    • Crea marcas (a.k.a. tags) locales para cada versión en el repositorio original.
    • Preserva los nombres y las relaciones entre las ramas (a.k.a. branches).

En todos los casos parece que el punto débil resulta ser la posibilidad de hacer commits en el repositorio SVN. Sin embargo, como ya les expliqué, este no era mi objetivo en este caso. Finalmente me decidí a utilizar Hgsvn.

Clonando el repositorio con Hgsvn

Hgsvn solo clona una rama a la vez. Considerando la estructura del repositorio original, el primer paso es clonar el trunk. Para ello se ejecuta el comando hgimportsvn de la siguiente manera:

$ pwd
/home/olemis/repos/
$ mkdir flioops
$ cd flioops
$ hgimportsvn https://flioops.svn.sourceforge.net/svnroot/flioops/py/trunk > ./trunk.log
$ cd trunk && hgpullsvn >> ../trunk.log

En el ejemplo se redirecciona la salida hacia el fichero ./trunk.log para su posterior análisis. Les recomiendo observarlo, puesto a que conocerán los cambios que se van incorporando y los datos que se van recuperando del repositorio original. La lista puede ser larga.

En este caso se crea un repositorio dentro de la carpeta trunk. En general el nombre se corresponde con el último tramo de la URL que se especifica como parámetro. Podemos comprobar que todo está en orden después de observar el log del nuevo repositorio.

$ pwd
/home/olemis/repos/flioops/trunk
$ hg log | more
changeset:   49:39cfb58b31b6
branch:      trunk
tag:         tip
tag:         svn.91
user:        olemis
date:        Wed Sep 17 10:34:32 2008 -0500
summary:     [svn r91] - Bug fixed... The class PackageTestLoader loaded the tests defined in

changeset:   48:5595c9f190ca
branch:      trunk
tag:         svn.88
user:        olemis
date:        Thu Sep 11 14:38:56 2008 -0500
summary:     [svn r88] Starting dutest release 0.2.1.

changeset:   47:ac470dd94852
branch:      trunk
tag:         svn.86
user:        olemis
date:        Thu Sep 11 12:10:25 2008 -0500
summary:     [svn r86] - PackageTestLoader class added to oop.utils.dutest module. It loads

$ hg branches
trunk                         49:39cfb58b31b6

$ hg tags -v | more
tip                               49:39cfb58b31b6
svn.91                            49:39cfb58b31b6 local
svn.88                            48:5595c9f190ca local
svn.86                            47:ac470dd94852 local
svn.83                            46:0f8df4313c98 local
svn.82                            45:287eef1166bf local
svn.80                            44:7accc993aad9 local
svn.78                            43:9ab9af3bbbfd local
svn.77                            42:4678ec09ef71 local

$ hg log -r svn.88 -v
changeset:   48:5595c9f190ca
branch:      trunk
tag:         svn.88
user:        olemis
date:        Thu Sep 11 14:38:56 2008 -0500
files:       utils/dutest.py
description:
[svn r88] Starting dutest release 0.2.1.

Como se puede observar solo obtenemos una sola rama llamada trunk. Su nombre se selecciona de manera análoga a partir de la URL. También se crea una marca local para cada revisión que se encuentra en el repositorio original. De esta forma es más fácil relacionar los cambios en un repositorio y otro. Por ejemplo la versión 88 en el repositorio central se corresponde con la versión 48 localmente, cuyo identificador global sería 5595c9f190ca. Además, si se nos olvidan estos detalles (de más está decir que yo me los sé de memoria :P ) pero conocemos la versión en el repositorio de Subversion, entonces es posible hacer referencia a su contra-parte en el repositorio de Mercurial utilizando el nombre (i.e. marca) svn.88.

Como se puede observar se mantiene el nombre del autor de los cambios y la fecha. Quizás le puedan quedar dudas ... pues ¡aclarémoslas!.

$ svn info
Path: .
URL: https://flioops.svn.sourceforge.net/svnroot/flioops/py/trunk
Repository Root: https://flioops.svn.sourceforge.net/svnroot/flioops
Repository UUID: 878b2ac5-cb47-0410-a52a-c42bf53788ce
Revision: 91
Node Kind: directory
Schedule: normal
Last Changed Author: olemis
Last Changed Rev: 91
Last Changed Date: 2008-09-17 10:34:32 -0500 (Wed, 17 Sep 2008)

Pues bien, en un mismo espacio coexisten ambos repositorios. Compare esta fecha con la que reporta Mercurial y verá que son idénticas. Después de satisfacer su curiosidad inicial, es hora entonces de clonar el resto de los cambios.

Clonando las ramas con Hgsvn

Como hemos visto antes, Hgsvn solo clona una rama a la vez. El proceso para recuperar los cambios restantes es análogo.

$ cd ..
$ pwd
/home/olemis/repos/flioops
$ hgimportsvn https://flioops.svn.sourceforge.net/svnroot/flioops/py/branches/add_opts > ./add_opts.log
$ cd add_opts && hgpullsvn >> ../add_opts.log && cd ..
$ hgimportsvn https://flioops.svn.sourceforge.net/svnroot/flioops/py/branches/mdl_inv > ./mdl_inv.log
$ cd mdl_inv && hgpullsvn >> ../mdl_inv.log && cd ..
$ hgimportsvn https://flioops.svn.sourceforge.net/svnroot/flioops/py/branches/pep302 > ./pep302.log
$ cd pep302 && hgpullsvn >> ../pep302.log && cd ..
$ hgimportsvn https://flioops.svn.sourceforge.net/svnroot/flioops/py/branches/test_pycontract > ./test_pycontract.log
$ cd test_pycontract && hgpullsvn >> ../test_pycontract.log && cd ..
$ hgimportsvn https://flioops.svn.sourceforge.net/svnroot/flioops/py/branches/guarded_synch > ./guarded_synch.log
$ cd guarded_synch && hgpullsvn >> ../guarded_synch.log && cd ..
$ hgimportsvn https://flioops.svn.sourceforge.net/svnroot/flioops/py/branches/ochk_arch > ./ochk_arch.log
$ cd ochk_arch && hgpullsvn >> ../ochk_arch.log && cd ..
$ hgimportsvn https://flioops.svn.sourceforge.net/svnroot/flioops/py/branches/pysimrec > ./pysimrec.log
$ cd pysimrec && hgpullsvn >> ../pysimrec.log && cd ..
$ ls
add_opts          mdl_inv          pep302           test_pycontract
guarded_synch     ochk_arch        pysimrec         trunk
add_opts.log      mdl_inv.log      pep302.log       test_pycontract.log
guarded_synch.log ochk_arch.log    pysimrec.log     trunk.log

Ahora todos esperamos efectos similares (i.e. marcas locales, una sola rama, fechas consistentes y dos repositorios en fraternal coexistencia).

$ cd add_opts
$ hg log | more
changeset:   36:660d2a6c5f05
branch:      add_opts
tag:         tip
tag:         svn.54
user:        olemis
date:        Thu Aug 07 09:42:48 2008 -0500
summary:     [svn r54] The error messages can be formatted now by specifying the templates

changeset:   35:0243dc200611
branch:      add_opts
tag:         svn.53
user:        olemis
date:        Thu Aug 07 09:36:52 2008 -0500
summary:     [svn r53] The whole assertion checking infrastructure is prepared for

changeset:   34:84a28bf08590
branch:      add_opts
tag:         svn.52
user:        olemis
date:        Thu Aug 07 09:22:20 2008 -0500
summary:     [svn r52] The function oop.utils.NormalizeException is prepared for assuming

$ svn info
Path: .
URL: https://flioops.svn.sourceforge.net/svnroot/flioops/py/branches/add_opts
Repository Root: https://flioops.svn.sourceforge.net/svnroot/flioops
Repository UUID: 878b2ac5-cb47-0410-a52a-c42bf53788ce
Revision: 54
Node Kind: directory
Schedule: normal
Last Changed Author: olemis
Last Changed Rev: 54
Last Changed Date: 2008-08-07 09:42:48 -0500 (Thu, 07 Aug 2008)

$ hg branches
add_opts                      36:660d2a6c5f05
trunk                         29:536184bf7e16 (inactive)

$ hg heads | grep changeset
changeset:   36:660d2a6c5f05

$ hg tags -v
tip                               36:660d2a6c5f05
svn.54                            36:660d2a6c5f05 local
svn.53                            35:0243dc200611 local
svn.52                            34:84a28bf08590 local
svn.50                            33:7e5b743efd37 local
svn.49                            32:901f61877293 local
svn.48                            31:51e7d78f26d8 local
svn.47                            30:336637e2a0f1 local

$ hg log -b trunk | grep changeset
changeset:   29:536184bf7e16
changeset:   28:ec176d410993
changeset:   27:d6ddd117926d
changeset:   26:1f89e8573172
changeset:   25:7b0847a67a80
changeset:   24:23571cc5686b
changeset:   23:cad71e6a4e34
changeset:   22:2d9e43109e87
changeset:   21:2a77c9c5e6ad
changeset:   20:c177d00b5a4e
changeset:   19:64eb1c7cdd91
changeset:   18:7c941992dbe5
changeset:   17:4dd286854034
changeset:   16:548c66a002c2
changeset:   15:7ad0521ce2b4
changeset:   14:f0087f96d466
changeset:   13:2540d610c198
changeset:   12:ee7e670e9b88
changeset:   11:8ba039171b0a
changeset:   10:af39f33a1776
changeset:   9:c95d926d676d
changeset:   8:28535deb9fc9
changeset:   7:1ddfa139931c
changeset:   6:84e23e55166e
changeset:   5:e1d36b91fb4a
changeset:   4:80257d1256a9
changeset:   3:6c81277c9392
changeset:   2:82714a5663d1
changeset:   1:89d45b024c67
changeset:   0:51750d40d056

$ cd ..\trunk
$ hg log -r 29:25 | grep changeset
changeset:   29:536184bf7e16
changeset:   28:ec176d410993
changeset:   27:d6ddd117926d
changeset:   26:1f89e8573172
changeset:   25:7b0847a67a80

Si bien hay varias semejanzas con respecto a los meta-datos, las diferencias son notables. Por lo visto hay menos revisiones y ... ¿dos ramas? ¿De dónde salió trunk si clonamos a add_opts? ¿Qué se puede deducir después de ejecutar los comandos anteriores? Primeramente, como las revisiones tienen el mismo identificador en ambos repositorios (trunk y add_opts) se puede deducir que se clonó el primer repositorio que teníamos localmente hasta la revisión 29:536184bf7e16. De ahi en adelante hgsvn detectó que en el repositorio SVN se había hecho una copia de los contenidos de la carpeta trunk y por tanto interpreta este hecho como la creación de una nueva rama y lo refleja localmente. En lo sucesivo solo se añaden las versiones en la rama add_opts. Es por esto que solo se crea una cabeza (a.k.a. head). La misma situación se repite para los otros repositorios.

Si Usted prefiere tener varias ramas en un solo repositorio (como yo ;o) solo resta unir todas las ramas.

$ pwd
/home/olemis/repos/flioops/trunk
$ find ../* -type d -print0  | xargs -0 -I repos hg pull repos
$ hg branches
ochk_arch                     76:6ac33679d5c3
guarded_synch                 75:386b8f866218
test_pycontract               73:d80a5ccc6153
mdl_inv                       69:a9b380084029
pep302                        62:00b7244f3642
add_opts                      60:660d2a6c5f05
pysimrec                      53:93e9562bfb41
trunk                         49:39cfb58b31b6

$ hg heads | grep changeset
changeset:   76:6ac33679d5c3
changeset:   75:386b8f866218
changeset:   73:d80a5ccc6153
changeset:   69:a9b380084029
changeset:   62:00b7244f3642
changeset:   60:660d2a6c5f05
changeset:   53:93e9562bfb41
changeset:   49:39cfb58b31b6

Ya tenemos el repositorio con todas las de la ley. Lo único que es de resaltar es que desaparecen las marcas que indicaban los números de revisión SVN para las ramas, pues son locales a los repositorios clonados.

Conclusiones

Hgsvn solo clona una rama a la vez. Subversion no tiene implementada una noción de ramas, por lo tanto el comando hgimportsvn considera como nombre el último tramo de la URL especificada. La herramienta se destaca por mantener la mayor fidelidad posible con respecto al repositorio SVN original y recuperar la mayor cantidad posible de meta-datos para incorporarlos en la copia local hecha con Mercurial. Hgsvn analiza las operaciones realizadas en el repositorio SVN original e infiere las relaciones entre las ramas para reflejarlas en el clon que se crea localmente.

viernes, 18 de diciembre de 2009

El servidor Apache pescó un resfriado

Malas noticias para todos aquellos que creían que el servidor web Apache estaba hecho a pruebas de balas. Hace pocos minutos consulté el sitio http://www.meteo.fr y en su página principal se mostraba el siguiente mensaje :

Compte tenu des conditions météorologiques, le site de Météo-France est temporairement saturé. Nous vous redirigeons vers l'information de Vigilance et vous prions de nous excuser pour la gêne occasionnée.

¿Querrá esto decir que no funciona si hay más de 3 cm de nieve? ¿Cómo es posible que los desarrolladores no hayan previsto esto? ¿Será un error en la especificación? ¿Por qué no han incluido un caso de prueba al respecto en su suite de pruebas de rendimiento y balance de carga? Sin embargo hay quienes dicen que este es un error conocido: no es nada nuevo que los indios no están habituados a la nieve .

:-D

Lo que sí parece es que se repite la historia de cazador cazado

Descubierto por Hervé Agnoux

miércoles, 16 de diciembre de 2009

Mejorando nuestro software libre con Bitbucket : Mercurial Queues

Bitbucket

Es un servicio de hospedaje de proyectos basado en Mercurial y enriquecido con características sociales. A continuación les describo una funcionalidad que puede ser muy útil en los proyectos de software libre para manejar los parches (patches) y propuestas de mejoras. Las potencialidades de Mercurial, de Python y la web 2.0, se combinan una vez más para salvarnos.

¿Qué es Bitbucket?

No está muy alejado de la verdad mencionar que Bitbucket está inspirado en Github, otro excelente sitio con características similares pero orientado al uso de Git (y este es nada más y nada menos que el sistema de control de versiones utilizado para desarrollar el kernel de Linux ;o). Entre las diferencias fundamentales entre ambos tenemos que Mercurial es más fácil de mantener, puede ser adaptado más fácilmente a entornos distribuidos, especialmente por estar hecho en Python (al igual que Bazaar) y posee un número de comandos más reducido y similar al de Subversion, por lo que la transición es menos traumática para los usuarios de este popular sistema. Sin embargo esto es solo una breve introducción. Hay comparaciones entre Mercurial y Bazaar y también otras entre Bazaar y Mercurial :P. Se pueden encontrar más detalles en el libro Distributed revision control with Mercurial escrito por Bryan O'Sullivan. Por cierto solo se utilizó software libre para confeccionarlo ;o).

Por su parte Bitbucket todavía está en su versión beta y están trabajando rápidamente para realizar majoras y eliminar defectos. Pero ya se han puesto en funcionamiento una buena cantidad de posibilidades maravillosas. Una de ellas es el soporte para la extensión Mercurial Queues.

Mercurial Queues

Mercurial Queues no es más que la evolución de quilt, un enfoque iniciado con Git. Muy brevemente MQ es una extensión que permite mantener una pila (i.e. LIFO) de parches que se aplican sobre un repositorio. Esto permite trabajar en unas cuantas mejoras que se construyen una sobre la otra en la forma de parches, en vez de mantener cambios más abultados y poco cohesionados. Por ejemplo si se trabaja en la interfaz de usuario y se descubre un error en capas inferiores de la aplicación entonces es posible congelar los cambios para la interfaz de usuarios, e ir liberando parches hasta llegar hasta el nivel dónde se encuentra el error. Luego se arregla el error y se vuelven a incluir en la pila los cambios que fueron extraídos anteriormente, para así proseguir con el desarrollo de la interfaz de usuarios. De este modo los parches pueden ser considerados como revisiones temporales o flotantes que no se reflejan en el repositorio hasta que no se confirma que todo marcha bien o que un experimento realmente dará resultados. Como ventaja resulta que la historia del repositorio se simplifica, y desaparecen versiones irrelevantes.

Similares beneficios se pueden obtener en proyectos en los que se mantienen parches sobre un código de base (por ejemplo, optimizaciones para dispositivos o arquitecturas específicas), al menos hasta que son revisados y aprobados por los mantenedores oficiales o hasta que se terminan. En estos casos esta extensión también permite mezclar los parches (rebasing) e incorporarlos en el repositorio.

Normalmente se trabaja localmente sobre un conjunto de parches, pero es posible establecer un control de versiones sobre los propios parches de forma tal que otros también puedan perfeccionar aspectos en pleno desarrollo.

¿Cómo funciona MQ con Bitbucket?

A continuación ilustro el camino más corto para configurar una pila de parches en Bitbucket:

  • Cree el repositorio principal. Este contiene los ficheros del proyecto.
  • Si ya se tiene parte del trabajo en un repositorio local, pues le hacemos push especificando la URL del proyecto (llamémosle project-repo).
  • Presione el botón patch queue en la página principal del repositorio.

Activando el repositorio de parches en Bitbucket

  • Teclee los datos que son solicitados y cree el nuevo repositorio (llamémosle por ejemplo project-repo-patches) para manejar versiones de los parches.
  • Asegúrese de marcar la opción Omit series si Usted desea añadir parches previos.
  • Active la extensión MQ.
  • Ejecute el comando hg qclone como se muestra a continuación (si el repositorio es de nuestra propiedad entonces owner=user)

$ hg qclone https://user@bitbucket.org/owner/project-repo-patches

Como resultado obtendrá una copia local del repositorio y la pila de parches preparada para incluir nuevas versiones. Eso es todo si no se van a incluir parches ya existentes. Luego se utiliza hg qnew para comenzar un nuevo parche y se continua con hg qref y hg qcommit para refrescarlo y para añadir una versión del parche. El repositorio de los parches se encuentra dentro de la carpeta `./.hg/patches. Para distribuir el nuevo parche a los demás se hace lo siguiente.

$ cd ./.hg/patches 
$ hg push 

Las nuevas versiones del parche deben aparecer en el repositorio de parches preparado por Bitbucket :o). La URL del repositorio remoto es configurada automáticamente.

Si se desea publicar varios parches que ya existen entonces solo hace falta copiarlos en ./.hg/patches junto con el fichero series, pero nunca copie el fichero status. Agréguelos al repositorio de parches. Todo sería más o menos así.

$ cp /path/to/old/repos/.hg/patches/*.diff ./.hg/patches 
$ cp /path/to/old/repos/.hg/patches/series ./.hg/patches 
$ cd ./.hg/patches 
$ hg add ./*.diff

¿A qué se deben las diferencias?

Lo primero que resulta diferente con respecto al comportamiento habitual es que hay que hacer hg qclone al repositorio de parches y como consecuencia se obtiene todo el repositorio del proyecto. Esto se debe a que Bitbucket brinda soporte para asociar varias pilas de parches con un único proyecto. Esto es algo muy útil. Es por esto que el servidor no almacena el repositorio de parches dentro del proyecto como ocurre normalmente. Sino, ¿cómo saber cuál pila de parches se desea clonar localmente?

Para descubrir los detalles de implementación observemos el fichero ./.hg/patches/.hg/hgrc. Allí encontraremos algo más o menos así

[paths]
default = https://bitbucket.org/owner/project-repo-patches/.hg/patches

Interesante ¿no es así? En vez de enviar los contenidos hacia el repositorio de parches (como se podría imaginar inicialmente ;o), estos son enviados hacia una pila dentro del repositorio de parches. ¿Una pila de parches dentro de una pila de parches? ¿Por qué? Si observamos el fichero ./.hg/hgrc veremos algo más o menos así.

[paths]
default = https://bitbucket.org/owner/project-repo-patches/

Por tanto los cambios deberían ser enviados a la raíz del repositorio de parches. Sin embargo si se modifican localmente los ficheros del proyecto y se publican los cambios, entonces estos se reflejan en el repositorio original (i.e. https://bitbucket.org/owner/project-repo). ¿Estarán redireccionando la raíz del repositorio de parches hacia el repositorio del proyecto? Todo parece indicar que sí.

¿Qué pasa con SSH?

Hasta ahora los ejemplos han utilizado el protocolo HTTP y esto es por una razón: la magia descrita anteriormente para clonar un repositorio de parches no está implementada para SSH. Este protocolo es útil puesto a que permite facilidades adicionales (e.g. compresión) que no permite el protocolo HTTP. Una posible solución sería :

$ hg clone ssh://hg@bitbucket.org/user/project-repo .
$ cd .hg
$ hg clone ssh://hg@bitbucket.org/user/project-repo-patches/.hg/patches .

Al menos de esta forma queda todo preparado de forma equivalente al caso de la versión HTTP, solo que se utiliza la URL del repositorio real para saltarse el paso de la redirección.

Conclusiones

Es mejor poner el parche antes que salga el hueco. Pero si hay demasiados o si queremos compartir nuestras mejoras con otras personas para colaborar y refinarlas o si se torna complicado tener todo bajo de control entonces Bitbucket está aquí para salvarnos.

:P

martes, 8 de diciembre de 2009

Depurando errores en aplicaciones web CGI con Python

Powered by Python

La tendencia de arrendar un servidor compartido para hospedar sitios web parece que va en ascenso. Las limitaciones (e.g. nada de acceso a los log de Apache, ni posibilidad de instalar módulos ni de modificar directamente el httpd.conf) resultan frecuentes en estos casos. ¿Cómo depurar entonces los errores que pueden aparecer al desplegar nuestra aplicación web? Les muestro cómo se haría con scripts de Python ejecutados como CGI. El ejemplo utilzado ilustra además la manera de convertir una aplicación WSGI en un script CGI. Este enfoque es tan sencillo que me he decidido a compartir mis descubrimientos con Ustedes. ¡Espero que les sea útil!

El problema

Sí señores, resulta que pueden ocurrir errores incluso aunque se haya ensayado todo localmente en un servidor de prueba. Tenga en cuenta que las medidas de control de acceso y protección tomadas por el proveedor más la configuración de los servicios en el ordenador remoto hacen que el contexto de ejecución pueda ser totalmente diferente.

En condiciones normales se estila revisar los logs del servidor web pues en ellos quedan registradas las fallas detectadas en tiempo de ejecución. De esta forma se pueden valorar los síntomas y (a veces con una cuota de suerte ;o) se logra tener una pista de las causas. Resulta ser que esto a veces no es posible en los contextos que mencionaba anteriormente. Por otra parte las posibles causas son muchísimas y pueden depender de las configuraciones de los diferentes servicios o de parches específicos aplicados a las aplicaciones subyacentes. Esto implica que a la hora de la verdad comenzamos con las ráfagas de parches a ciegas. ¡Por suerte tenemos a Python!

Una aplicación WSGI sencilla

Las razones anteriores pueden darnos un indicio de que la solución es tomar al toro por los cuernos y hacernos responsables de descubrir los defectos nosotros mismos (especialmente si estamos seguros de que el script se ejecuta pero no cómo esperábamos, e.g. al recibir un error HTTP 500 Internal Server Error en el navegador web). Pero ¿cómo hacer una aplicación sencilla?

La respuesta no es única. Por suerte hay muchos caminos para llegar a Roma ;o). La manera estándar para escribir aplicaciones web con Python es el modelo WSGI. ¡Echémosle un vistazo! Una aplicación WSGI muy sencilla luce más o menos así :

#! /usr/bin/env python

def application(environ, start_response):
    status = '200 OK'
    headers = [('Content-type', 'text/plain')]
    start_response(status, headers)
    return ['Hello world ! \n\n', 'The simplest web app ever seen']

Como se puede apreciar solo se necesita escribir una función. Se recomienda que su nombre sea application. Esto se debe a que es el nombre predeterminado que utiliza el módulo mod_wsgi (i.e. Apache) para identificar la aplicación web que se ejecutará al invocar un módulo. La función (o en general cualquier objeto invocable o callable en inglés) tiene que aceptar dos parámetros. El primero es un diccionario que contiene valores que describen el entorno de ejecución. Entre los más relevantes se encuentran el nombre del host, los parámetros de la URL solicitada, el método (e.g. POST, GET ...) y hay otros más.

El segundo argumento es una función que se encarga de iniciar el proceso de envío de la repuesta del servidor hacia el cliente. La razón para hacer esto es que cada servidor web realiza esta operación de una forma diferente, pero siempre es capaz de facilitar una implementación de dicha función para encapsular los detalles. Como se aprecia en el ejemplo, esta función acepta como parámetros el texto que describe el código HTTP y una lista de tuplas binarias. Los primeros están definidos en el estándar RFC 2616. Por otra parte cada tupla representa el nombre y el valor de un encabezamiento que será retornado hacia el cliente. El valor que retorna la función es utilizado para enviar una respuesta hacia el cliente. Todo el contenido no tiene que ser enviado de una sola vez. Si nos fijamos bien, lo que se devuelve es una secuencia con varios segmentos de la respuesta. Estas porciones de la respuesta se envían en el mismo orden y poco a poco hacia el cliente. Esto es importante desde el punto de vista de la eficiencia puesto a que, si se está generando el cuerpo de la respuesta, no hay que esperar a realizar todo el procesamiento para hacer llegar todos los datos al cliente. Es por esto que los servidores y frameworks hechos con Python tienen tiempos de respuesta bastante pequeños.

Para poner a funcionar esta aplicación con mod_wsgi solo hace falta la siguiente directiva en el fichero httpd.conf (o equivalente ;o)

WSGIScriptAlias /helloworld /path/to/script.py

Si se accede a http://example.com/helloworld entonces se obtendría el siguiente texto

Hello world ! 

The simplest web app ever seen

Reutilizando la aplicación WSGI en otros contextos

Sin embargo, una restricción frecuente es que no están instalados los módulos de Apache (que viene siendo el caso cada vez más frecuente :o) que son necesarios para ejecutar este tipo de aplicaciones y solo tenemos a mod_python. Otras muchas veces ni siquiera tenemos eso y solo queda la opción de CGI. En estos casos el estándar WSGI todavía resulta útil para hacer las cosas una sola vez y que todo funcione en varios contextos de ejecución. Solo hay que hacer las siguientes modificaciones

if __name__ == '__main__' :
  from wsgiref.handlers import CGIHandler
  CGIHandler().run(application)

Listo !

Depurando los scripts con el módulo cgitb

El módulo cgitb provee un tratamiento de errores muy simple y que funciona incluso bajo situaciones extremas. Lo único que hay que hacer es añadir las dos líneas siguientes

import cgitb
cgitb.enable()

Después de hacer esto, si ocurre un error en la aplicación entonces ya no obtenemos el lacónico mensaje 500 Internal Server Error, sino más bien un reporte HTML detallando el error (incluso más informativo que el que se mostraría en la consola ;o). La función cgitb.enable intercepta el mecanismo estándar de procesamiento de excepciones para hacer su trabajo. También acepta otros parámetros, por ejemplo :

Llamada Efecto
enable(display=0) No mostrar los tracebacks
enable(logdir="/path/to/dir") Los reportes se encriben en ficheros ubicados en /path/to/dir
enable(format="text") Imprime reportes en modo texto

Comprobando que el servidor ejecuta los scripts

Todo lo que hemos visto es estándar. Pero todavía hay más. A veces es necesario determinar si el error es del servidor o es provocado por la aplicación web. Con estos fines utilizo el script siguiente

#! /usr/bin/env python

from wsgiref.simple_server import demo_app as application

if __name__ == '__main__' :
  from wsgiref.handlers import CGIHandler
  CGIHandler().run(application)

La función wsgiref.simple_server.demo_app es una aplicación WSGI pequeña pero completa, que visualiza en el navegador una página en texto plano con el mensaje Hello world'' y los pares de valores que describen en el contexto de ejecución. Resulta muy útil para verificar que el servidor WSGI funciona correctamente.

Conclusiones

El modelo WSGI se ajusta muy bien a las necesidades de los servidores web y del protocolo HTTP, haciendo énfasis en la eficiencia. El mismo resulta ser muy completo y ofrece soporte para otras tecnologías web. Sin embargo no resuelve los problemas cuando se despliega una aplicación en un hosting compartido o en otros contextos con restricciones especiales. Es por esto que el módulo cgitb resulta tan importante cuando nos encontramos en un callejón sin salida al desarrollar nuestra aplicación web. Como por arte de magia podemos tener un análisis exhaustivo de las causas del error escribiendo solamente dos líneas de código adicionales. Es así de fácil. Espero que le sean útiles estos consejos. Les espero pronto aquí en el blog de Simelo.