jueves 18 de febrero de 2010

Automatizando el volcado de repositorios

Recapitulando

En el artículo anterior, establecí un repositorio en el servidor de desarrollo local, Goldberg, que se sincroniza con el repositorio principal situado en Dreamhost. Comenté los comandos del subversion necesarios para realizar dicha tarea; tales comandos, ejecutados manualmente y a diario, pueden consumir bastante tiempo, el cual es más rentable si lo invertimos en tareas creativas, complejas y divertidas, por lo que he desarrollado unos sencillos scripts que ejecutarán dichas tareas por mi.

Objetivos

  • Crear scripts para realizar la sincronización entre ambos repositorios. Serán ejecutados desde el crontab.
  • Crear la infraestructura de soporte para dichos scripts y los sucesivos que ire desarrollando en mis tareas de automatización.

Consideraciones

El lenguaje de implementación de los scripts es Ruby. ¿Por qué? Pues porque hace mucho tiempo que no me divertía programando, ya se me hacía pesadísimo, y este lenguaje devuelve la sonrisa a tu rostro a medida que te familiarizas con el y te enamoras del código que vas creando, mucho más compacto, más legible, más flexible, más.... que en los otros lenguajes que conozco. Actualmente estoy explorando otros, como el Clojure, para abordar otros paradigmas de programación de manera agradable también. Para aquellos que quieran aprender Ruby (lo cual recomiendo) se puede acceder de forma gratuita al libro (disponible online gratuitamente) Programming Ruby, del cual hay una versión (no gratuita) más reciente que incluye la versión 1.9 de este lenguaje. Los scripts están desarrolados con la 1.8.7. Más información sobre Ruby, en la página del lenguaje.

Inicialmente desarrollé las clases y scripts de manera "dispersa", por decirlo de alguna manera, mientras leía el libro de Brian Marick titulado Everyday Scripting with Ruby: for Teams, Testers, and You. Tras haber mejorado mis habilidades como scripter, decidí empaquetarlo todo en un módulo al que denomino Proyectos, que incluye los scripts que iré presentando en este blog así como las clases en las que se apoya.

He creado un proyecto en el RubyForge denominado autom-proyectos, donde puedes descargarte el paquete. En la página del proyecto verás como hacer checkout del repositorio y tener siempre la última versión del código que voy desarrollando en este blog.

s4t-utils

Para la estructuración del paquete me baso en librería mencionada en el libro de Brian Marick (comentado anteriormente). Esta utilidad no solo te genera automáticamente la estructura, sino que se incorpora a él para proporcionar una serie de facilidades de programación (ajustar paths, facilidades para el testing, etc...). Otra alternativa hubiera sido distribuirlo como gema, pero eso ya lo haré más adelante.

En la carpeta bin se sitúan los scripts que realizan las distintas acciones, y en la carpeta lib, los ficheros que forman el módulo Proyectos, que da soporte a dichas utilidades, con las clases Project y Repository (no estaba muy inspirado el día que elegí los nombres).

Así mismo tenemos la carpeta doc, con la documentación del módulo en formato html y que se puede regenerar en cualquier momento sitúandonos en la carpeta raíz del paquete y ejecutando:

rake rdoc

Por último, en la carpeta test encontramos los tests del módulo. A continuación voy a explicar los ficheros principales, pero no mostraré todo el código para no hacer esto eterno. Descárgate las fuentes en el RubyForge. Me limitaré a comentar algunos fragmentos y en bastantes de ellos incluyo los comentarios del código para explicarlo.

Puedes ejecutar los scripts desde la línea de comandos con Ruta hacia la carpeta del módulo/bin/volcar_repositorios.rb o ejecutar (como sudo) rake install desde la carpeta raíz del proyecto.

Módulo Proyectos (lib/proyectos.rb)

Aquí defino los valores predeterminados de configuración. Para personalizar no hay más que cambiar el valor de las variables. Así vale, aunque lo ideal a medida que se incluyan más rutas y se vaya generalizando es cambiar estas constantes por un fichero de configuración YAML.

Aquí se define también el método check_usage:


def check_usage(num_required_args, message)
unless ARGV.length >= num_required_args
puts message
exit
end
end

Este método es invocado por aquellos scripts que requieran argumentos obligatorios a la hora de ejecutarse. Si el número de argumentos presentes en la línea de comandos del script es inferior al de los que requiere, muestra por pantalla el mensaje especificado y termina la ejecución del script.

Project (lib/proyectos/project.rb)

Esta clase gestiona el acceso al listado de proyectos, de momento es muy simple pero con el tiempo evolucionará (y probablemente se renombrará, en realidad debería haberse llamado ProjectList o algo similar en su actual encarnación) para acomodar todo el modelado de la generación automática de proyectos.

Tiene tres atributos, que son las rutas hacia ficheros o carpetas relevantes. En la inicialización, si no se ha definido alguna de estas rutas se cogen los valores por defecto establecidos en la declaración del módulo Proyectos.


attr_accessor :list_path, :export_path, :dump_path

# No comprueba la existencia de las carpetas. Se manejarán las excepciones en cada caso.
# Los atributos tomaran el valor especificado por defecto en la definicion del modulo o los indicados en options.
# * :list_path => Fichero con el listado de proyectos existentes
# * :export_path => Carpeta para la exportacion de proyectos
# * :dump_path => Carpeta para el volcado de repositorios
def initialize(options = {})
@list_path = options[:list_path] || PROJECT_LIST_PATH
@export_path = options[:export_path] || EXPORTED_PROJECTS_PATH
@dump_path = options[:dump_path] || SVN_DUMP_PATH
end

Y define un único método, que accede al listado de proyectos para devolverlo en forma de array. El fichero de lista de proyectos es un fichero de texto donde en cada línea se encuentra el identificador del proyecto, seguido de : y el número de la última revisión existente en el repositorio local. Cuando se inicia un proyecto nuevo, se añade una entrada a la lista con el identificador seguido de :0. Se puede ver un ejemplo en /test/data/project_list.


# Devuelve un array de proyectos, cada uno especificado por un hash con las claves
# * :project_id, que es la cadena identificativa del proyecto.
# * :revision, ultima revision almacenada en el repositorio del servidor de desarrollo local.
def list
lista = []
File.open(@list_path).readlines.each do |linea|
proyecto = linea.chomp.split(':')
lista << {:project_id => proyecto[0], :revision => proyecto[1].to_i}
end
lista
end

Repository (lib/proyectos/repository.rb)

Esta clase gestiona el acceso al repositorio subversion. Todas las operaciones las realiza invocando comandos del shell.

Tiene un solo atributo, que indica la url del repositorio. Si no se especifica un valor por defecto en la inicialización, coge el establecido por defectos en la definición del módulo Proyectos.


attr_accessor :url

# Crea una instancia vinculada a un repositorio especificado en la construccion
# o el establecido por defecto para el modulo.
# * url = url del repositorio, incluyendo la parte del protocolo.
def initialize(url = nil)
@url = url || DEFAULT_REPOSITORY
end

El método que realiza el volcado de un repositorio especificado por un identificador, en la carpeta especificada entre dos revisiones. Lo único que hacemos es invocar el comando de sistema svnadmin dump


# Vuelca un conjunto de revisiones de un proyecto. El fichero se denominara svn. seguido del valor de :project_id
# options debe ser:
# * :project_id => Identificador del proyecto
# * :rstart => Revision de comienzo
# * :rend => Revision final
# * :folder => Carpeta donde se almacena el volcado
def dump(options = {})
`svnadmin dump #{@url}/#{options[:project_id]} --incremental --revision #{options[:rstart]}:#{options[:rend]} > #{options[:folder]}/svn.#{options[:project_id]}`
end

Este es el método para cargar un volcado en un repositorio, necesita tan solo conocer la carpeta destino y el identificador de proyecto. Tira del comando svnadmin load


# Carga un fichero de volcado de transacciones de un repositorio subversion.
# options debe ser:
# * :folder => carpeta en la que se encuentra el volcado.
# * :project_id => cadena identificadora valida de un proyecto.
# El nombre del fichero que espera encontrar el metodo es svn. seguido por el valor de :project_id.
def load(options = {})
`svnadmin load #{@url}/#{options[:project_id]} < #{options[:folder]}/svn.#{options[:project_id]}`
end

El siguiente método es el que permite obtener cual es la última versión en un repositorio. Se utiliza para comparar esta versión con la presente en el fichero de lista de proyectos y poder averiguar entre que revisiones debe hacerse el volcado.


# Devuelve entero indicando ultima revision del repositorio
# * project_id = cadena identificadora valida de un proyecto.
def last_revision(project_id)
`svnlook youngest #{@url}/#{project_id}`.chomp.to_i
end

Los scripts

volcar_repositorios.rb (bin/volcar_repositorios.rb)

Este es el script que realiza el volcado de los repositorios de Dreamhost. Se sube este paquete y se ejecuta, incluyéndolo en el crontab. No voy a explicar como se hace, puedes verlo en este tutorial. Los comentarios explican lo suficientemente bien el código:


# Obtener lista de proyectos y revisiones
proyectos = Project.new(:list_path => '/home/cuentait7/etc/project_list', :dump_path => '~/svndumps')
repositorio = Repository.new('/home/cuentait7/svn')

# Para cada proyecto realizar el volcado en la carpeta correspondiente
proyectos.list.each do |proyecto|
# Ver cual es la revisión actual, obteniendo la información del repositorio que se va a volcar
actual = repositorio.last_revision(proyecto[:project_id])
# Ver cual es la revisión de Goldberg, información obtenida de la lista de proyectos
previa = proyecto[:revision]

# Hacer el volcado entre la siguiente revisión a la que está en Goldberg y la actual
if previa != actual
puts "volcando #{proyecto[:project_id]}"
repositorio.dump(:project_id => proyecto[:project_id], :rstart => previa + 1, :rend => actual, :folder => proyectos.dump_path)
end
end

cargar_repositorios.rb (bin/cargar_repositorios.rb)

Primero establecemos la lista, el repositorio, la carpeta donde se realiza el volcado en Dreamhost....


proyectos = Project.new
repositorio = Repository.new('/var/subversion')
lista = proyectos.list
dh_folder = '/home/cuentait7/svndumps'

A continuación abro una conexión ssh, usando como identificador la clave que generé cuando instalé el servidor.


Net::SSH.start('greene.dreamhost.com', 'cuentait7', {:keys => ['/home/eddy/.ssh/id_rsa']}) do |ssh|
puts "Conexión realizada con éxito"
lista.each do |proyecto|

Para cada proyecto que hay en la lista, hay que:

  1. Establecer las rutas hacia el fichero remoto y hacia el local.

    dh_project_url = "#{dh_folder}/svn.#{proyecto[:project_id]}"
    local_project_url = "#{proyectos.dump_path}/svn.#{proyecto[:project_id]}"

  2. Si existe un volcado del proyecto en cuestión lo descargamos.

    if ssh.exec!("ls #{dh_project_url}") !~ /No such/
    # obtener el fichero de Dreamhost
    ssh.sftp.download!("#{dh_project_url}", local_project_url)

    Aquí utilicé sftp en lugar de scp porque para ficheros muy grandes se me cortaba.
  3. Eliminar el volcado arriba.
    ssh.exec!("rm #{dh_project_url}")
  4. Cargarlo en el servidor de desarrollo local y actualizar el número de versión del proyecto en la lista.

    repositorio.load(:project_id => proyecto[:project_id], :folder => proyectos.dump_path)
    # actualizar version
    proyecto[:revision] = repositorio.last_revision(proyecto[:project_id])

  5. La lista está actualizada está en un array, la volvemos a escribir en el fichero.

    File.open(proyectos.list_path, 'w') do |f|
    lista.each { |l| f << l[:project_id] << ':' << l[:revision] << "\n" }
    end

  6. Subir la nueva versión del proyecto actualizado a Dreamhost.
    ssh.scp.upload!(proyectos.list_path, '/home/cuentait7/etc')

Notas

En lugar de usar comandos de shell directamente, podría haber usado los subversion-bindings, disponible para varios lenguajes de programación entre ellos Ruby. El problema es que requiere que el usuario los tenga instalado en su sistema, por lo que opté por una solución autosuficiente. De hecho, las gemas de las que depende este módulo (net-ssh, net-scp, etc...) vienen también incluidas por la misma razón.

Ante todo soy pragmático, escribí estos scripts para usarlos en mi entorno de trabajo actual, por lo que hay muchas rutas a carpetas y ficheros en el código puestas a piñón fijo. A medida que necesite generalizarlo lo hiré haciendo, hasta entonces, ¿para qué preocuparse?

Tampoco hago manejo de excepciones. El usuario de los scripts se supone que es un programador o diseñador/maquetador que lanzará los comandos desde la consola de manera más o menos inteligente, y con la mínima capacidad de entender lo que pasa si se encuentra con algún error. Mi idea es que este conjunto de scripts evolucione hasta una interfaz web para que cualquiera de la empresa pueda lanzar los procesos de configuración de software necesarios para lanzar y desplegar un proyecto, será entonces cuando me preocupe de eso.

Conclusión

Ya tengo un par de scripts para automatizar la tarea de sincronización de repositorios, y la infraestructura base (módulo Proyectos) para seguir desarrollando más. Como paranoico del backup, y no satisfecho con la redundancia obtenida hasta ahora, la siguiente tarea es hacer una exportación (código en limpio, sin los ficheros de control .svn) de la última versión de los proyectos para que entre en la copia de seguridad diaria de la empresa, tarea que por supuesto será automatizada también. ¡Pero eso lo dejo para el próximo artículo!

0 comentarios:

Publicar un comentario en la entrada