12 octubre 2021

Cómo procesar los comandos de consola de tus aplicaciones Python usando Argparse

Todas las aplicaciones de consola tienen algo en común: tienen que procesar los argumentos que introduce el usuario al teclear el comando que lanza la aplicación. Pocas aplicaciones de consola carecewn de argumentos, la mayoría los necesitan para funcionar adecuadamente. Piensa por ejemplo en el comando ping: necesita al menos un argumento, la dirección IP o la URL a la que se le hace ping.

dante@Camelot:~/$ ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=113 time=3.73 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=113 time=3.83 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=113 time=3.92 ms
^C
--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2005ms
rtt min/avg/max/mdev = 3.733/3.828/3.920/0.076 ms

dante@Camelot:~$

Cuando ejecutas una aplicación, te llegan en la lista sys.argv todos los argumentos que usuario introdujo cuando llamó a tu aplicación desde la consola. Ese comando y sus argumentos se divide  en tokens, usando los espacios en blanco como separadores, siendo cada token un elemento de la lista sys.argv. El primer elemento de esa lista es el nombre de tu aplicación (el comando en si). Por eso, si por ejemplo hemos implementado nuestra propia versión del comando ping, el elemento 0 de sys.argv será "ping" y el elemento en la posición 1 será la IP, por ejemplo "8.8.8.8".

Podrías hacer tu propio análisis de los argumentos revisando por ti mismo el contenido de sys.argv. La mayor parte de los desarrolladores lo hacen así... al principio. Si lo haces así, pronto acabas dándote cuenta de que no es tan fácil gestionar de manera limpia los argumentos de usuario y encima acabas repitiendo muchísimo código en tus diferentes aplicaciones, por eso hay librerías pensadas para analizar por ti los argumentos de usuario. Una que me encanta es Argparse.

Argparse es un módulo incluido de serie en la distribución estándar de Python, así que no hay que descargarlo de Pypi. Una vez que se comprenden sus conceptos resulta fácil, muy flexible y gestiona por ti muchas casuísticas nada evidentes a priori. Es uno de esos módulos que echas de menos cuando programas en otros lenguajes.

El primer concepto a comprender de Argparser es precisamente el de Argument Parser. Para este módulo, un Argument Parser es una palabra fija que está seguida por argumentos de usuario posicionales u opcionales. El Argument Parser por defecto es precisamente el nombre de la aplicación que ejerce de comando. En nuestro ejemplo, el comando "ping" es un Argument Parser. Cada Argument Parser puede verse acompañado de Arguments, los cuales pueden ser de dos tipos:

  • Argumentos posicionales: Son aquellos obligatorios para el usuario. El usuario tiene que introducirlos o el comando asume que la orden es incorrecta y aborta la ejecución. Este tipo de argumentos tiene que introducirse en un orden específico. En nuestro ejemplo del comando ping "8.8.8.8" es un argumento posicional. Podemos encontrar otros ejemplos, el comando "cp" por ejemplo necesita dos argumentos posicionales: el fichero origen de la copia y el fichero de destino.
  • Argumentos opcionales: Pueden ser introducidos o no. Se marcan con etiquetas. Las etiqueta sabreviadas usan un guión y un carácter, mientras que las etiquetas largas usan un guión doble y una palabra. Algunos argumentos opcionales admiten un valor y otros no (son booleanos, true si se usan o false si no).
Aquí se pueden ver algunos de los argumentos que acepta el comando "cat":

dante@Camelot:~/$ cat --help
Usage: cat [OPTION]... [FILE]...
Concatenate FILE(s) to standard output.

With no FILE, or when FILE is -, read standard input.

-A, --show-all equivalent to -vET
-b, --number-nonblank number nonempty output lines, overrides -n
-e equivalent to -vE
-E, --show-ends display $ at end of each line
-n, --number number all output lines
-s, --squeeze-blank suppress repeated empty output lines
-t equivalent to -vT
-T, --show-tabs display TAB characters as ^I
-u (ignored)
-v, --show-nonprinting use ^ and M- notation, except for LFD and TAB
--help display this help and exit
--version output version information and exit

Examples:
cat f - g Output f's contents, then standard input, then g's contents.
cat Copy standard input to standard output.

GNU coreutils online help: <https://www.gnu.org/software/coreutils/>
Full documentation at: <https://www.gnu.org/software/coreutils/cat>
or available locally via: info '(coreutils) cat invocation'

dante@Camelot:~$

Hay comandos más complejos que incluyen lo que se denomina Subparsers. Los subaparsers aparecen cuando nuestro comando incluye otras palabras "fijas", como verbos. Por ejemplo, el comando "docker" tiene muchos subparser: "docker build", "docker run" o "docker ps". Cada subparser puede aceptar su propio conjunto de argumentos. Un subparser puede incluir a otros subparsers, formando subparser anidados y dando lugar a árboles de comandos.

Para mostrar como usar el módulo argparse, voy a usar como ejemplo my propia configuración para la aplicación Cifra. En concreto usaremos su fichero cifra_launcher.py file en GitHub.

En su sección __main__ se puede ver cómo se procesan los argumentos a alto nivel:

Como se puede ver esatamos usando sys.argv para obtener los argumentos facilitados por el usuario, descartando el primero ("[1:]") porque es el comando raiz mismo: "cifra".

Me gusta usar sys.argv como parámetro por defecto de main() porque así puedo llamar a main() desde mis test funcionales, usando una lista específica de argumentos.

Los argumentos se pasan a la función parse_arguments() que es la función que hace toda la magia de argparse. Allí se puede ver la configuración base de argparse:

Ahí se puede configurar el parser base, el que se enlaza al comando raiz, definiendo la descripción del comando y una nota final ("epilog") para el comando. Esos textos aparecerán cuando el usuario llame al comando con el argumento "--help".

Mi aplicación cifra tiene algunos subparsers, al igual que hace docker. Para permitir que un parser albergue subparsers, hay que hacer:

Una vez que has permitido que tu parser tenga subparsers, ya se puede empezar a crear esos subparser. Cifra tiene 4 subparsers en ese nivel: 

 

 

Luego veremos que argparse devuelve un objeto similar a un diccionario tras realizar su labor de parseo. Con el código anterior, si por ejemplo el usuario introdujo "cifra dictionary" entonces el objeto similar a un diccionario tendrá una clave "mode" con un valor "dictionary".

Cada subparser puede tener a su vez subparsers, en realidad en nuestro código de ejemplo el subparser "dictionary" tiene más subparsers que añaden más ramas al árbol del comando. De esa manera se puede llamar a "cifra dictionary create", "cifra dictionary update", "cifra ditionary delete", etc.

Una vez que llegas a la conclusión de que una rama ha llegado a su máxima profundidad y no necesita más subparsers, se pueden añadir sus respectivos argumentos. Para el subparser de "cipher" tenemos los siguientes argumentos:


En estos argumentos, "algorithm" y "key" son posicionales y por lo tanto obligatorios. Los argumentos que se sitúen en esas posiciones serán los valores de las claves "algorithm" y "key" del objeto devuelto por argparse.

El parámetro metavar es la cadena que queremos que se use para representar este parámetro cuando se llame a la ayuda con el argumento --help. Como te puedes imaginar, el parámetro help es la cadena de ayuda que sale para explicar este argumento cuando se llama a la ayuda con --help.

El parámetro type se usa para preprocesar el argumento facilitado por el susuario. Por defecto, los argumentos se interpretan como strings, pero si se usa type=int entonces se convertirán a int (o se lanzaría un error si la conversión resultase imposible). En realidad str o int son funciones, así que se pueden pasar los nombres de tus propias funciones de conversión a type. Por ejemplo, para el parámetro file_to_cipher yo he usado mi propia función _check_is_file().

Como se puede ver, esta función comprueba si los argumentos facilitados se corresponde con una ruta de ficheros correcta y de ser así devuelve la cadena de la ruta o un error si la ruta fuese incorrecta.

Recuerda que los parámetros opcionales siempre vienen precedidos de guiones. Si esos parámetros están seguidos de un argumento dado por el usuario, ese argumento será el valor para la clave, denominada como el nombre largo del parámetro, en el objeto tipo diccionario devuelto por argparser. En nuestro ejemplo, la línea 175 significa que cuando el usuario teclea "--ciphered_file myfile.txt", argparser devolverá una clave llamada "ciphered_file" con el valor  "myfile.txt".

A veces los parámetros opcionales no necesitarán valores. Pr ejemplo, podrían ser simples parámetros booleanos para activar una salida detallada:

parser.add_argument("--verbose", help="increase output verbosity", action="store_true", default=False)

Con action="store_true" el objeto tipo diccionario tendrá la clave "verbose" asociada con un valor booleano: verdadero si se usó --verbose o falso en caso contrario.

Antes de devolver de salir de parse_arguments() me gusta filtrar el contenido del diccionario a devolver:


En esta sección de código parsed_arguments() es el objeto tipo diccionario que devuelve argparse tras procesar los argumentos del usuario con arg_parser.parse_args(args). Este objeto incluye también, con valor None, todos los argumentos opcionales que se podrían haber introducido pero que al final no se metieron. Podría devolver directamente este objeto y luego ir comprobando parámetro a parámetro si es None o no, pero me parece más limpio quitar todas las claves que sean None y luego limitarme a comprbar si cada clave está presente o no. Por eso filtro el objeto diccionario en la línea 235.

De vuelta a la función main(), tras procesar los argumentos del usuario se puede desarrollar el resto de la lógica de la aplicación dependiendo del contenido del diccionario de argumentos. Por ejemplo, para cifra:

 De esta manera, argparser te permite procesar los argumentos del usuario de una manera limpia y que te ofrece también por el mismo "precio" (gratis) mensajes de ayuda y de error, dependiendo de si el usuario llamó a --help o introdujo un comando erroneo o equivocado.En realidad, los mensajes generados son tan limpios que los uso para el redme.md de mi repositorio y conforma el punto de partida para generar mis páginas de man (aunque eso ya es material para otro artículo).

04 septiembre 2021

Cómo usar contenedores de Docker

 
La virtualización se basa en el engaño.

Con la virtualización pesada (es decir, la de VMware, VirtualBox o Xen) a un sistema operativo "invitado" se le engaña para que crea que está funcionando en un hardware dedicado, cuando en realidad este es compartido.

Con virtualización ligera (es decir, la de Docker) a una aplicación se la engaña para que crea que está utilizando un kernel propio de sistema operativo, cuando en realidad este está compartido. La clave es que en Linux todo lo que no sea el kernel se considera una aplicación por lo que Docker puede hacer que varias destribuiones de linux compartan el mismo kernel (el del sistema anfitrión). Dado que el nivel de abstracción del engaño es mucho mayor que el practicado en la virtualización pesada el consumo de recursos es mucho menor, de ahí el nombre de virtualización ligera. Es tan ligera que muchas aplicaciones se distribuyen en paquetes de docker (denominados contenedores) en loas que la aplicación se empaqueta junto al sistema operativo y las dependencias que necesita para ser ejecutado de una vez en otro sistema, sin enredar con las dependencias de este último.

Es cierto que hay cosas que no se pueden hacer con virtualización ligera, pero no son muchas. Para desarrollar y ejecutar una aplicación estándar "de nivel 7" no encontraremos ninguna limitación usando virtualización de Docker.


Instalación

Antiguamente se podía instalar docker desde el repositorio de paquetes de tu distribución. Actualmente, aunque puedan encontrarse paquetes de docker en los repositorios estándar, lo normal es que ya no funcionen. Para hacer funcionar Docker lo suyo es ignorar los paquetes del repositorio estándar de tu distribución y tirar de los repositorios oficiales de Docker. 

Docker facilita repositorios de paquetes para muchas distribuciones. Por ejemplo, aquí se pueden leer las instrucciones para instalar docker en un equipo Ubuntu. Lo que sí hay que tener en cuenta es que en caso de que estemos usando algún derivado de Ubuntu, como Linux Mint, vamos a necesitar personalizar esas instrucciones para configurar la versión en las listas de fuentes de apt en nuestro sistema.

Una vez instalado docker en nuestro ordenador, podemos ejecutar la aplicación de Hello World, incluido en un contenedor de ejemplo, para comprobar que todo funcione bien:

dante@Camelot:~/$ sudo docker run hello-world
[sudo] password for dante:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:7d91b69e04a9029b99f3585aaaccae2baa80bcf318f4a5d2165a9898cd2dc0a1
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/

For more examples and ideas, visit:
https://docs.docker.com/get-started/

dante@Camelot:~$

Ten en cuenta que aunque puedas ejecutar contenedores de docker puede ser que no puedas descargar nuevos e instalarlos sin ser usar sudo. Si no te resulta cómodo trabajar así, siempre puedes añadirte a tí mismos al grupo de docker:

dante@Camelot:~/$ sudo usermod -aG docker $USER
[sudo] password for dante:

dante@Camelot:~$

Puede ser que tengas que cerrar la sesión y volver a hacer login para que los cambios se activen. De esta manera podrás ejecutar comandos de docker como un usuario normal sin privilegios de administrador.


Uso

Podemos crear nuestros propios contenedores, pero eso es un tema para otro artículo. En este artículo vamos a usar contenedores personalizados por otros.

Para encontrar contenedores que sirvan de punto de partida, dirígete a  Docker Hub y teclea cualquier aplicación o distribución de Linux en su campo de búsqueda. La búsqueda devolverá muchas opciones. Si buscas una distribución monda y lironda lo mejor es usar una con la etiqueta "Official image". 

Supongamos que quieres probar una aplicación en Ubuntu Xenial, entonces elige Ubuntu en el listado de salida de la búsqueda y échale un ojo a la sección de "Supported tags...". Ahí se puede ver como se llaman las distintas versiones para ser descargadas. En nuestro caso tomaremos nota de las etiquetas "xenial" y "16.04".

Ahora que ya sabemos qué bajar, vamos a hacerlo con docker pull:

dante@Camelot:~/$ docker pull ubuntu:xenial
xenial: Pulling from library/ubuntu
528184910841: Pull complete
8a9df81d603d: Pull complete
636d9303bf66: Pull complete
672b5bdcef61: Pull complete
Digest: sha256:6a3ac136b6ca623d6a6fa20a7622f098b2fae1ac05f0114386ef439d8ca89a4a
Status: Downloaded newer image for ubuntu:xenial
docker.io/library/ubuntu:xenial


dante@Camelot:~$

Lo que hemos hecho ha sido bajar lo que se denomina una imagen. Una imagen es un paquete base con una distribución de linux específica con una serie de aplicaciones instaladas. Desde ese paquete base se pueden derivar tus propios paquetes personalizados o ejecutar instancias de esos paquetes base, esas instancias es lo que denominamos contenedores.

Si quieres comprobar cuantas imágenes tenemos disponibles sólo hay que usar docker images:

dante@Camelot:~/$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu xenial 38b3fa4640d4 4 weeks ago 135MB
hello-world latest d1165f221234 5 months ago 13.3kB



dante@Camelot:~$

Puedes iniciar instancias (es decir contenedores) desde esas imñagenes usando  docker run:

dante@Camelot:~/$ docker run --name ubuntu_container_1 ubuntu:xenial
dante@Camelot:~$

Usando el parámetro --name puedes asignar un nombre específico a un contenedor para identificarlo entre otros contenedores iniciados desde la misma imagen.

El problema de iniciar contenedores de esta manera es que se cierran inmediatamente. Si miramos el estatus de nuestros contenedores usando docker ps:

dante@Camelot:~/$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c7baad2f7e56 ubuntu:xenial "/bin/bash" 12 seconds ago Exited (0) 11 seconds ago ubuntu_container_1
3ba89f1f37c6 hello-world "/hello" 9 hours ago Exited (0) 9 hours ago focused_zhukovsky

dante@Camelot:~$

He usado el parámetro -a para mostrar todos los contenedores, no sólo los activos. De esa manera se puede ver que el contenedor ubuntu_container_1 finalizó su actividad casi inmediatamene tras suzomienzo. Eso ocurre porque los contenedores de docker están diseñados para ejecutar una applicación específica y cerrarse en cuanto esa applicación acaba. Como nosotros no dijimos qué aplicación ejecutar, el contenedor se limitó a cerrarse en cuanto se inició.

Antes de intentar nada más, vamos a borrar el contenedor anterior usandodocker rm. De esa manera empezaremos desde cero:

dante@Camelot:~/$ docker rm ubuntu_container_1
ubuntu_container_1

dante@Camelot:~$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3ba89f1f37c6 hello-world "/hello" 10 hours ago Exited (0) 10 hours ago focused_zhukovsky
dante@Camelot:~$

Ahora lo que queremos es mantener el contenedor vivo para poder acceder a su consola. Una manera de hacerlo es:

dante@Camelot:~/$ docker run -d -ti --name ubuntu_container_1 ubuntu:xenial
a14e6bcac57dd04cc777a4eac787a8465acd5b8d379591976c56de1d0acc2798

dante@Camelot:~$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a14e6bcac57d ubuntu:xenial "/bin/bash" 8 seconds ago Up 7 seconds ubuntu_container_1
3ba89f1f37c6 hello-world "/hello" 10 hours ago Exited (0) 10 hours ago focused_zhukovsky
dante@Camelot:~$

En el comando anterior hemos usado el parámetro -d para ejecutar el contenedor en segundo plano y los parámetros -ti para iniciar una consola interactiva y mantenerla abierta. Haciéndolo de esa manera, podemos ver que esta vez el contenedor se mantiene arriba. Pero seguimos sin acceder a la consola del contenedor, para acceder a ella debemos hacer docker attach:

dante@Camelot:~/$ dante@Camelot:~$ docker attach ubuntu_container_1
root@a14e6bcac57d:/#

Se puede ver que la consola ha cambiado su identificador del lado izquierdo. Se puede salir del contenedor tecleando exit, pero eso para el contenedor. Para dejar el contenedor manteniéndolo activo debemos usar Ctrl+p seguido de Ctrl+q en su lugar. 

Se puede para un contenedor inactivo para ahorrar recursos y reanudarlo después usando docker stop y docker start:

dante@Camelot:~/$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9727742a40bf ubuntu:xenial "/bin/bash" 4 minutes ago Up 4 minutes ubuntu_container_1
3ba89f1f37c6 hello-world "/hello" 10 hours ago Exited (0) 10 hours ago focused_zhukovsky
dante@Camelot:~$ docker stop ubuntu_container_1
ubuntu_container_1
dante@Camelot:~$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9727742a40bf ubuntu:xenial "/bin/bash" 7 minutes ago Exited (127) 6 seconds ago ubuntu_container_1
3ba89f1f37c6 hello-world "/hello" 10 hours ago Exited (0) 10 hours ago focused_zhukovsky
dante@Camelot:~$ docker start ubuntu_container_1
ubuntu_container_1
dante@Camelot:~$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9727742a40bf ubuntu:xenial "/bin/bash" 8 minutes ago Up 4 seconds ubuntu_container_1
3ba89f1f37c6 hello-world "/hello" 10 hours ago Exited (0) 10 hours ago focused_zhukovsky
dante@Camelot:~$


Salvar los cambios

Será habitual que quieras exportar los cambios realizados a un contenedor para poder iniciar uno nuevo con esos cambios aplicados.

La mejor manera es usar dockerfiles, pero eso lo dejaré para un artículo posterior. Una manera rápida (y chapucera) es registrar los cambios en una nueva imagen haciendo docker commit:

dante@Camelot:~/$ docker commit ubuntu_container_1 custom_ubuntu
sha256:e2005a0ec8302f8948958a90e2abc1e8957a15155c6e6bbf7300eb1709d4ae70
dante@Camelot:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
custom_ubuntu latest e2005a0ec830 5 seconds ago 331MB
ubuntu xenial 38b3fa4640d4 4 weeks ago 135MB
ubuntu latest 1318b700e415 4 weeks ago 72.8MB
hello-world latest d1165f221234 5 months ago 13.3kB

dante@Camelot:~$

En este ejemplo hemos creado una nueva imagen llamada custom_ubuntu. Usando esa nueva imagen podemos crear nuevas instancias con los cambios hechos hasta el momento a ubuntu_container_1.

Ten en cuenta que al registrar los cambios de un contenedor en funcionamiento este se para con el fin de evitar la corrrupción de datos. Al finalizar el registro el contenedor reanuda su funcionamiento.


Ejecutar servicios desde contenedores

Hasta ahora hemos levantado una máquina virtual ligera y hemos accedido a su consola, pero eso no es suficiente si queremos ofrecer servicios desde ese contenedor.

Supongamos que hemos configurado un servidor de SSH en el contenedos y que queremos accedes a él a través de la LAN. En ese caso necesitaremos que determinados puertos del contenedor sean mapeados con otros del anfitrión que puedan ser accedidos por la LAN.

Una trampa importante a tener en cuenta aquí es que ese mapeo de puertos debe ser configurado cuando el contenedor se ejecuta por primera vez con docker run, usando el parámetro -p:
dante@Camelot:~/$ docker run -d -ti -p 8888:22 --name custom_ubuntu_container custom_ubuntu
5ea2604da8c696af397cd1b1f98d7426dd68182e5edd81a56a6b739849ba1e84

dante@Camelot:~$

Hecho eso se podrá acceder al servicio SSH del contenedor usando el puerto 8888 del anfitrión.


Compaartir ficheros con los contenedores 

Nuestros contenedores no serán islas aisladas. En ocasiones necesitarán que le facilitemos ficheros externos o nos devolverán ficheros.

Una manera de hacerlo es arrancar los contenedores montando una carpeta del anfitrión a modo de carpeta compartida:

dante@Camelot:~/$ docker run -d -ti -v $(pwd)/docker_share:/root/shared ubuntu:focal
ee64e7392a77b4d0b35adadad467fe5d6a105b84290945e346f82199116b7c56

dante@Camelot:~$

Aquí, la carpeta del anfitrión docker_share será accesible en la ruta del contenedor /root/shared. Ojo que las rutas deben ser absolutas. Utilizao $(pwd) como un atajo para poner el directorio actual.

Una vez que el contenedor esté arrancado, cualquier fichero que el contenedor sitúe en la carpeta /root/shared será visible desde el anfitrión, incluso después de que se pare al contenedor. En el otro sentido, es decir dejar un fichero del anfitrión para que lo vea el contenedor, es posible pero habrá que hacer sudo:

dante@Camelot:~/$ cp docker.png docker_share/.
cp: cannot create regular file 'docker_share/./docker.png': Permission denied

dante@Camelot:~/$ sudo cp docker.png docker_share/.
[sudo] password for dante:

dante@Camelot:~$

Otra manera es compartir los ficheros usando el comando de copia que tiene docker:

dante@Camelot:~/$ docker cp docker.png vigorous_kowalevski:/root/shared 
dante@Camelot:~$

Aquí, hemos copiado el fichero docker.png del anfitrión a la carpeta /root/shared del contenedor vigorous_kowalevski. Fíjate que no hemos necesitado hacer sudo para ejecutar el comando docker cp

En el otro sentido también es posible, sólo hay que cambiar el orden de los argumentos:

dante@Camelot:~/$ docker cp vigorous_kowalevski:/etc/apt/sources.list sources.list 
dante@Camelot:~$

Aquí, hemos copiado el fichero sources.list del contenedor al anfitrión.


A partir de aquí

Hasta ahora hemos visto como manejar contenedores de docker. El siguiente paso es crear tus propias imágenes usando dockerfiles y compartiendolos a través de Docker Hub. Explicaré todo eso en un artículo posterior.

 

Usando JFrog Artifactory para distribuir nuestros paquetes

 
En un artículo anterior revisé algunas maneras de generar paquetes debian o rpm para las aplicaciones que desarrollásemos. Pero después de empaquetar tus aplicaciones hay que encontrar una manera para que tus usuarios puedan descargarse e instalarse esos paquetes.

Siempre se pueden dejar los paquetes en la sección de releases de tu repositorio en Github y que tus usuarios lo descarguen de allí, pero la pega de eso será que no habrá una manera fácil de anunciar las actualizaciones y distribuirlas. Es mucho mejor usar un repositorio de paquetes y dejar que el usuario instale el paquete mediante el gestor de paquete de su sistema (por ejemplo apt o yum).

Puedes intentar meter tu paquete en los repositorios oficiale de tu distribución pero probablemente no cumplas los requisitos para ello, por eso un repositorio personal es la vía más factible.

Durante bastante tiempo utilicé Bintray para alojar mis paquetes deb y rpm, pero Bintray finalizó su servicio en marzo de 2021, por lo que tuve que buscar una alternativa. Finalmente encontré JFrog Artifactory, el heredero oficial de Bintray.

JFrog Artifactory tiene una modalidad gratuita para proyectos open source. Si tu proyecto no es tan popular como para superar 50 GB de descarga mensual, esta modalidad debería ser más que suficiente para tus proyectos personales.

La única pega es que Artifactory es más complejo (y completo) que Bintray, por eso es más complejo ponerlo en marcha si lo usas para tus proyectos personales. En este artículo voy a explicar lo que he aprendido hasta ahora para que te resulte más fácil empezar con Artifactory de lo que me ha resultado a mí.

Una vez registrado en la plataforma, se accede al menú de configuración rápida:

Ahí se puede crear un repositorio de cualquiera de los paquetes soportados. Para este artículo voy a usar Debian. Haz click en el icono de Debian y selecciona crear un nuevo repositorio.

En la siguiente ventana se te pide dar un nombre (un prefix) para el repositorio:


En la captura de pantalla he llamado vdist a mi repositorio. Artifactory crea repositorios virtuales, remotos y locales. El único repositorio que me ha resultado útil hasta el momento es el local, por eso cuando sigas este artículo asegurate de seleccionar siempre la opción "debian-local".

La siguiente ventana es engañosamente simple ya que te puede hacer pensar que ya estás listo para subir paquetes siguiendo las instrucciones de las pestañas "Deploy" y "Resolve":

 
El problema es que necesitas configurar algunas cosas antes de que tu repositorio esté completamente funcional, como he tenido que aprender a las duras.
 
Lo primero es que necesitas permitir el acceso anónimo al repositorio para que la gente pueda descargar tus paquetes. Lo que confunde aquí es que el acceso anónimo está configurado (se puede ver el permiso en Administration > Identity y Access > Permissions) pero aparentemente no parece funcionar en absoluto, por lo que cuando intentas acceder a tu repositorio usando apt lo único que consigues es un unauthorized error. La trampa es que premiero necesitas permitir globalmente el acceso anónimo en Administration > Security > Settings:

 


Sólo después de chequear esa opción dejarás de recibir el error al usar apt.

Para que los usuarios configuren sus linux para usar el repositorio, sólo hay que incluir en el fichero /etc/sources.list lo siguiente:

 deb https://dlabninja.jfrog.io/artifactory/<REPOSITORY_NAME>-debian-local <DISTRIBUTION> <COMPONENT>

En mi ejemplo REPOSITORY_NAME es vdist, DISTRIBUTION es el nombre de la distribución a la que nos estamos enfocando (por ejemplo, en Ubuntu, podría ser trusty) y para COMPONENT uso main. Por cierto, dlabninja es el nombre que le dí a mi cuenta cuando me registré en Artifactory, el nombre que le des a la tuya será diferente.

Se podría pensar que ya estás listo para subir paquetes, pero me temo que no es así todavía. Si intentas usar apt para acceder al repositorio en este punto te saldrá un mensaje diciendo que el repositorio no está firmado y que el acceso a él está prohibido. Para solucionarlo hay que crear un par de claves GPG (pública y privada) para firmar los paquetes y subirlos a Artifactory.

Para crear un certificado GPG puedes usar el siguiente comando:


Hay que meter el nombre identificativo, un correo y un password cuando te lo pida. Como nombre suelo usar el del repositorio. Toma nota del password que uses, si lo olvidas no hay manera de recuperarlo. La cadena que empieza en "F4F316" y acaba en "010E55" es el id del certificado. Tu id será similar. Te resultará útil para identificar tu certificado con los comandos gpg.

Puedes sacar una lista de los certificados:


Para subir las claves generadas, primero hay que exportarlas a un fichero. Ese proceso de exportación necesita generar dos ficheros: uno para tu clave pública y otro para tu clave privada:

 

Con el primer comando he exportado la clave pública y con el segundo la privada. Fíjate que le he puesto una extensión para poder identificarlas. Este es un buen momento para guardar esas claves en un lugar seguro.

Para subir esos ficheros a Artifactory necesitas ir a Administration > Artifactory > Security > Keys management:

 

Ahí, selecciona "+ Add keys" en la pestaña de "Signing keys". En la ventana que se abra mete el nombre para la clave (en este caso "vdist") y arrastra sobre la ventana los ficheros de las claves exportadas y mete la contraseña de la clave privada. Cuando lo hagas tendrás tu certificado correctamente importado en Artifactory y listo para ser usado.

Para configurar el certificado GPG en un repositorio ve a Administration > Repositories, selecciona tu repositorio y la pestaña "Advanced". Allí hay un combo "Primary key name" donde puedes seleccionar tu certificado. No olvides pulsar "Save & Finish" antes de salir o se perderán los cambios que hayas hecho:

 

Hecho eso, ddejará de salirte el error de repositorio sin firmar al usar apt, pero te seguirá saliendo el siguiente error:

(Click to enlarge)

En este caso el gestor de paquetes protesta porque aunque el repositorio está firmado con GPG no es capaz de reconocer su clave pública. Para resolverlo hay que subir la clave pública a uno de los servidores de claves PGP gratuitos de manera que nuestros usuarios puedan desscargárselos e importarlos. En estos casos yo suelo mandar mis claves públicas a ubuntu.keyserver.com:


Una vez que un registro público tiene una clave pública se sincroniza con los demás servidores PGP para compartirla. Nuestro usuario debe importar la clave pública y decirle a su gestor de paquetes que la clave pública es confiable. Para hacerlo en nuestro sistema debemos hacer sudo:

Si queremos instalar nuestro propio paquete, tendremos que hacer lo mismo.

Despues de eso, sudo apt update debería funcionar perfectamente:


Por fin estamos ya preparados para subir nuestro primer paquete a nuestro repositorio. Hay dos manera de hacerlo: manualmente y de manera automatizada.

Se pueden subir paquetes manualmente a través del interfaz web de Artifactory yendo a Artifactory > Artifacts > Selecting repository (in my example vdist-debian-local) > Deploy (el botón de la parte superior derecha). Eso abre una ventana emergente donde se puede arrastrar el fichero del paquete. Revisa que el campo de "Target repository" está correctamente fijado al tuyo (es fácil equivocarse y mandar el paquete al repositorio equivocado).

Además de lo anterior, Artifactory te deja subir paquetes desde la línea de comandos, lo que lo hace perfecto para automatizarlo en el marco de fujos de integración continua. Puedes ver el comando necesario en Artifactory > Artifacts > Set me up (el botón de la esquina superior derecha). Eso abre una ventana emergente con una pestaña denominada "Deploy" donde puedes ver el comando necesario para subir paquetes a un repositorio determinado:

Como se puede ver, los comandos tienen "place holders" en muchos campos. Si no estás seguro de qué poner en los campos USERNAME y PASSWORD, ve a la pestaña "Configure" y mete tu contraseña de tu usuario de Artifactory. Luego vuelve a las pestaña "Deploy" para ver cómo los campos de USERNAME y PASSWORD han sido rellenados por tí.

30 agosto 2021

Cómo usar Travis CI con proyectos Python

 

Cuando tu proyecto empiece a crecer, testearlo, compilarlo y desplegarlo se volverá complejo.

Lo normal es empezar haciéndolo todo manualmente pero pasado un tiempo empezar a automatizar todo lo posible mediante scripts. Usar scripts propios para compilar y empaquetar puede ser suficiente para muchos proyectos personales pero muchas veces los test funcionales tardan más de lo que estamos dispuestos a esperar delante del ordenados. En esos casos, delegar en otros la ejecución de los test funcionales es la solución y es ahí donde Travis-CI aparece.

Travis-CI ofrece un entorno de automatización para ejecutar tus test funcionales, así como cualquier script que deseemos ejecutar en función del éxito o fracaso de los tests. Puede que ya conozcas  Jenkins, en realidad Travis-CI es similar pero más simple y fácil. En un entorno empresarial lo normal es usar Jenkins para la integración continua pero para proyectos personales Travis-CI puede ser más que suficiente. Actualmente, Travis-CI es gratuito para proyectos open-source.

Cuando uno se conecta a Travis-CI se le pide conectar su cuenta de GitHub, así que esta resulta imprescindible. A partir de ese momento se usará la cuenta de Github para identificarse al acceder a Travis.

Una vez dentro de la cuenta de Travis, se puede pulsar el icono "+" para enlazar cualquiera de nuestros repositorios de Travis. Despuñes de enlazar un repositorio con Travis, hay que incluir en su raiz un fichero llamado .travis.yml (ojo al punto del principio), su contenido sirve para decirle a Travis qué hacer cuando detecte una actualización del repositorio.

Para tener algo que no sirva como ejemplo, vamos a analizar la configuración de travis usada para vdist. Hay otros ejemplos en la documentación de Travis pero creo que el proceso de empaqutado de Travis cubre suficientes características de Travis para dar una buena idea de lo que se puede conseguir. Vamos a centrarnos en examinar una foto fija de la configuración del fichero de .travis.yml de vdist. Por eso, lo mejor es mantener abierta una ventana con el contenido de .travis.yml mientras se lea este artículo. Voy a explicar el flujo de trabajo a alto nivel y luego iremos paso por paso viendo cada uno con más detalle.

El flujo de trabajo que describe ese fichero .travis.yml es el siguiente:


Flujo de trabajo de Travis con vdist (hacer click para verlo más grande)

Cuando actualizo la rama de staging de mi repositorio de Github, este visa a Travis mediante un webhook y luego descarga la última versión del código fuente y busca un fichero .travis.yml en su raiz. Con ese fichero Travis puede averiguar qué flujo de trabajo seguir. 

En concreto con vdist, Travis busca mis tests funcionales y los ejecuta usando un intérprete de python 3.6, así como uno marcado como nightly en los repositorios de Python (las versiones beta del intérprete). De esa manera puedo comprobar que mi código se ejecuta bien en mi versión objetivo de Python (3.6) y además puedo anticipar cualquier problema que pueda tener en la futura versión estale de Python. Si cualquier test falla al ejecutarse con Python 3.6 entonces el proceso de trabajo finaliza y se me manda un correo con un aviso. Sin embargo, si lo que falla son los tests que se ejecuten con la versión nightly se me manda un aviso por correo pero el flujo de trabajo continua, dado que sólo doy soporte a versiones estables de Python. De esa manera puedo saber si tengo que solucionar cualquier futuro problema con la próxima versión de python pero no interrumpe el proceso de empaquetado con la versión actual.

Si los tests resultan exitosos, la rama de staging se funde con la rama master en Github. En ese momento, Github activa dos webhooks a los siguientes servicios:

  • Docker Hub: Para reconstruir algunas imágenes optimizadas de docker que vdist usa al empaquetar una aplicación.
  • Read the Docs: Para reconstruir la documentación del sitio de la aplicación usando los documentos en markdown de la carpeta docs/ del repositorio de vdist.
Esos webhooks son una de las características más interesantes de Github ya que permiten integrar muchos servicios de otros proveedores en los flujos de trabajo de Github. 

Mientras Github mezcla ramas y activa webhooks, Travis comienza el proceso de empaquetado y de despliegue de los paquetes generados a algunos repositorios públicos. Se generan paquetes de tres tipos: paquetes wheel, paquetes deb  y paquetes rpm. Los paquetes wheel se despliegan a Pypi, mientras que los deb y rpm de despliegan en mi repositorio de Bintray and y en el de release del repositorio de vdist en Github.

Ese es el proceso a grandes rasgos. Vamos a ver como se implementa todo esto en Travis usando el fihero .travis.yml.


Modos de Travis


Cuando Travis se activa por una actualización en el repositorio, da comienzo lo que se denomina build.

Un build genera uno o más jobs. Cada job clona el repositorio en un entorno virtual y ejecuta una serie de fases. 

Cada job copia tu repositorio en un entorno virtual y lleva a cabo una serie de fases. Un job finaliza cuando ejecuta todas sus fases. Un build acaba cuando termina todos sus jobs.

Por defecto, Travis ejecuta un coclo de vida cuyas fases principales para sus jobs son:

  1. before_install
  2. install
  3. before-script
  4. script
  5. after_sucess or after_failure
  6. before_deploy
  7. deploy
  8. after_deploy
  9. after_script
En realidad, hay más fases opcionales y nisiruiera hay que implementarlas todas.  De hecho, sólo la fase de script es realmente obligatoria. La fase de script es donde normalmente se ejecutan los tests funcionales. Si estos son exitosos, se ejecutan las fases desde after_sucess a after_script, pero si no lo son sólo se ejecuta after_failure.  La fase install es dondo se suelen instalar las dependencias necesarias para ejecutar tus tests. La fase deploy es donde se suben los paquetes generados a tus diferentes repositorios, por eso lo normal es usar la fase de before_deploy para ejecutar los comandos necesarios para generar esos paquetes.

 

 

¿Por qué decimos que un build puede tener uno o más jobs? porque se puede configurar lo que denomina una build matrix. Se genera una build matrix al definir que queremos probar el codigo con múltiples intérpretes o múltiples variables de entorno. Por ejemplo, se puede definir que queremos probar el código contra python 2.7, 3.6 y una versión de desarrollo de 3.7, en cuyo caso se genera una build matrix de 3 jobs.
 
El problema con este modo es que la build matrix genera trabajos completos, por lo que cada uno ejecuta los tests y la fase deploy. Lo que pasa es que a veces queremos ejecutar varios tipos de tests pero sólo queremos ejecutar una única fase de despliegue. Por ejemplo, supopngamos que estamos construyendo un proyecto de python cuyo código está preparado para ejecutarse tanto en python 2.7 como en 3.6, en ese caso nos gustaría probar el código tanto  con 2.7 como con 3.6, pero en caso de éxito sólo queremos generar un paquete para subirlo a Pypi. Extrañamente, esa clase de flujo de trabajo no ha estado soportada por Travis hasta hace poco tiempo. Hasta hace poco, al intentar el modo por defecto uno se podía encontrar que al probar el proyecto tanto para python 2.7 como 3.6 se generaba y se desplegaba el paquete resultante 2 veces.
 
 

 
Afortunadamente, Travis ha evolucionado a lo que llaman el modo stage, el cual funciona realmente y soluciona el problema descrito en el párrafo anterior.  
 
Este modo introduce el concepto de etapa (stage). Una etapa es formalmente un grupo de jobs que se ejecutan en paralelo como parte de un proceso de construcción secuencial compuesto por varias etapas. Mientras que el modo antiguo ejecutaba los jobs en paralelo desde el comienzo hasta el final, la etapa organiza el trabajo mediante etapas secuenciales y dentro de esas etapas es donde pueden ejecutarse los jobs en paralelo.
 
 

 

En nustro ejemplo, se puede crear una etapa para ejecutar en paralelo dos jobs para probar la aplicación tanto con python 2.7 como 3.6 y después, en caso de éxito, se puede lanzar otra etapa para crear y desplegar un único paquete.
 
Como esto era exactamente lo que necesitaba para vdist, este modo precisamente (el stage mode) será el que explique en este artículo.


Configuración inicial

 
Echa un vistazo a nuestro .travis.yaml de ejemplo. De las líneas 1 a 11 tenemos:


En esas líneas definimos la configuración general de Travis.

Primero se fija el lenguaje nativo usando la etiqueta "language" de tal manera que Travis pueda facilitar un entorno virtual con las dependencias necesarias instaladas.

Travis facilita dos tipos de entornos virtuales: el entorno virtual por defecto es un contenedor de docker con linux, el cual es muy ligero y rápido de arrancarse; el segundo entorno virtual es una máquina linux virtualizada (mediante virtualización pesada, tipo Hiper-V o VMware), lo cual supones tiempos de arranque más largo pero a veces permiten hacer cosas que no son posibles con contenedores. Por ejemplo, vdist usa docker al ejecutarse (por eso incluyo en valos docker con la etiqueta "services" del fichero .travis.yaml) lo que me obliga a usar un entorno de virtualización pesado. De otra manera, si intentas ejecutar un entono de docker dentro de un contenedor de docker te vas a dar cuenta de que no se puede. Por eso, para lanzar un entorno virtual pesado hay que incluir la etiqueta "sudo: enabled".

Por defecto, Travis usa una versión bastante antigua de linux (Ubuntu Trusty). Cuando tengan nuevas versiones del sistema operativo, habrá que cambiar la etiqueta "dist: trusty" por aquella versión más moderna que hayan incluido.

A veces, puede ser que al usar un entorno virtual tan antiguo no se disponga de las dependencias que necesitamos. Para ayudar con eso, el equipo de Travis intenta mantener una imagen de Trusty especialmente actualizada. Para utilizar esa versión especialmente actualizada habría que usar la etiqueta "group: travis_latest".

 

Matrix de pruebas

Desde las líneas 14 a 33 tenemos:

 


Bajo la etiqueta "python: " se define con qué versiones de python queremos ejecutar nuestros tests.

Podríamos necesitar ejecutar los tests no sólo depediendo de las versiones del intérprete de python sino también dependiendo de diversas variables de entorno. La manera de hacerlo es fijando esas variables de entorno debajo de la etiqueta "matrix:".

Puedes fijar algunas condiciones a ser probadas y que te avisen si fallan sin que eso suponga el final de todo el job. Para ello se usa la etiqueta "allow_failures". En este caso dejo que mi build continue si el fallo se produce cuando ejecuto las pruebas con la versión de desarrollo de python (versión nightly), de esa manera me avisan de que mi aplicación va a fallar en la versión de python que se está preparando para ser lanzada, pero dejo que mi aplicación sea empaquetada si al menos funciona con la versión de python estable.

Se pueden definit las variables de entorno a utilizar en los scripts de construcción usando la etiqueta "global:".

Si cualquiera de esas variables tiene valores peligrosos para ser mostrados en un repositorio público, se pueden cifrar usando las herramientas de Travis. Para ello hay que asegurar que las herramientas de Travis estén instaladas. Dado que las herramientas de Travis están desarrolladas con Ruby, lo primero es instalar su intérprete:

dante@Camelot:~/$ sudo apt-get install python-software-properties dante@Camelot:~/$ sudo apt-add-repository ppa:brightbox/ruby-ng dante@Camelot:~/$ sudo apt-get update dante@Camelot:~/$ sudo apt-get install ruby2.1 ruby-switch dante@Camelot:~/$ sudo ruby-switch --set ruby2.1

 

A partir de ahí se podrá usar el gestor de paquetes de Ruby para instalar la herramienta de Travis:

dante@Camelot:~/$ sudo gem install travis



Con la herramienta de Travis instalada ya podemos usarla para cifrar cualquier valor que queramos:

dante@Camelot:~/$ sudo travis encrypt MY_PASSWORD=my_super_secret_password

 

Como resultado, la herramienta sacará una etiqueta "secure:" seguida por una cadena aparentemente aleatoria. Esa etiqueta, con la cadena aleatoria, se puede incluir en el fichero .travis.yaml. Lo que hemos hecho aquí es usar la clave pública de Travis para cifrar nuestra cadena. Cuando Travis lea nuestro fichero .travis.yaml usará su fichero privado para descifrar cada etiqueta "secure:" que encuentre. 


Filtrado de ramas


Este fragmento viene de las líneas 36 a 41 de nuestro fichero .travis.yaml de ejemplo. Por defecto, Travis se activa con cada push que dé en cualquier rama de nuestro repositorio pero lo normal es restringir la activación a ramas específicas. En mi caso, los builds se activan cuando se producen pushes sobre la rama de "staging" (desarrollo). 

 

Notificaciones

 

Como se puede ver en las líneas 44 a 51, se puede configurar a qué dirección de correo se le notificará tanto el éxito como el fracaso de los tests. 


Testing

De las líneas 54 a 67 configuramos las pruebas, el verdadero meollo de nuestro trabajo:




Como se puede ver, en realidad abarca 3 fases: "before_install", "install" and "script".

Puedes usar esas fases de la manera que te resulte más cómoda. Yo he usado "before_install" para instalar todos los paquetes de sistema necesarios para mis tests, mientras que en "install" he instalado todas las dependencias de python.

En la fase de "script" es donde se lanzan las pruebas. En mi caso he usado pytest para ejecutar mis pruebas. Hay que ser consciente de que Travis espera durante 10 minutos a recibir alguna salida de tus tests por pantalla. Si no se recibe alguna salida en ese plazo, Travis interpreta que el test se ha quedado colgado y lo cancela. Este comportamiento puede ser un problema si resulta ser normal que un test en concreto dure más de 10 minutos sin dar señales de vida. En ese caso, hay que lanzar el comando de test usando el comando "travis_wait N", donde N es el número de minutos que queremos que dure la espera. En mi caso, los tests de vdist son realmente largos, así que le pido a Travis que espere 30 minutos antes de dar por perdido el test.

 

Etapas

En nuestro ejemplo, las etapas (stages) se definen entre las líneas 71 a 130.

En realidad, hasta este punto la configuración no ha sido diferente de los que habría sido si hubiéramos usado el modo por defecto de Travis. Donde empiezan realmente las diferencias es cuando encontramos la etiqueta "jobs:" dado que esta marca dónde comienza la definición de las etapas. A partir de ahí, cada etiqueta de "stage:" marca el comienzo de la definición de una fase.

Tal y como dijimos, las etapas se ejecutan secuencialmente siguiendo el mismo orden en el que se encuentren en el fichero .travis.yaml. Si una de la etapas falla el job acaba en ese mismo punto.

Es lógico preguntarse por qué no se definen las pruebas como si fueran una etapa. En realidad puede hacerse y de hecho así se podría alterar el orden de las pruebas si no quisiéramos que se realizasen al principio. En todo caso, si las pruebas no se definen explícitamente como una etapa entonces se ejecutarán al principio, como si fueran una fase 0.

Analicemos las etapas de vdist.


Etapa para fundir la rama de staging en la master

Desde la línea 77 a la 81:
 


Si los tests han sido exitosos podemos estar seguros de que el código está lo suficientemente limpio como para incluir dicho código en la rama master. En el caso del ejemplo, fundir (merge) con la rama master activa los webhooks de RedTheDocs y Docker Hob.

Para mantener el fichero .travis.yaml limpio, he sacado todo el código necesario para la fusión a un script externo. Ese script ejecuta automáticamente los mismos comandos de consola que usaríamos para realizar la fusión manualmente.


Etapa para construir y desplegar un paquete a un servicio de alojamiento de paquetes

Hasta ahora hemos comprobado que nuestro code está OK, ahora es el momento de empaquetarlo en un paquete y subirlo allá de donde nuestros vayan a descargarlo.

Hay muchas manera de construir y empaqueta nuestro código (tantas como lenguajes de programación) y muchos servicios para alojar los paquetes. Para los servicios de alojamiento más habituales Travis ya cuenta con jobs de despliegue predefinidos que automatizan gran parte del trabajo. Podemos ver un ejemplo en las líneas 83 a 94:

 



Ahí se puede ver un trabajo de despliegue predefinido para paquetes de python consistente en dos pasos: empaquetado en un paquete wheel y subida al servicio de Pypi.

Los parámetros a incluir con cada job de despliegue predefinido varían de uno a otro. Lo mejor es consultar las instrucciones específicas de Travis para el job predefinido concreto que nos interese.

Pero habrá veces que el servicio de alojamiento que usemos no esté incluido en la lista de jobs predefinidos de Travis. En ese caso las cosas se vuelven más manuales, ya que siempre se puede optar por unsar una etiqueta script para ejecutar comando de shell, tal y como se puede ver en las líneas 95 a 102:



En esas líneas se puede ver cómo llamo a algunos scripts definidos en carpetas de mi codigo fuente. Esos scripts utilizan herramientas propias para empaquetar mi código en paquetes rpm y deb. Aunque en las siguientes líneas he usado los jobs predefinidos de despliegue para subir los paquetes generados a los servicios de alojamiento, podría haberlo hecho mediante scripts manuales. Todos los servicios de alojamiento de paquetes facilitan una manera de subir paquetes usando comandos de consola, de manera que siempre contamos con esa posibilidad si Travis no nos facilita un job predefinido para hacerlo.


Depuración


A menudo, nuestra configuración de Travis no nos funcionará a la primera así que tendremos que depurarla.

Para depurar un build de Travis lo primero es leer el log de salida. Si el log es tan grande como para que Travis no lo muestre por completo en el explorador entonces podremos descargar el log en crudo y examinarlo en nuestro editor de texto favorito. Hay que buscar en el log cualquier error inesperado.

Otra opción es ejecutar el script en una máuina virtual con la mismas versiones que use Travis. Si el error mostrado en el log se repite en la máquina virtual entonces tendremos todo lo necesario para averiguar qué es lo que falla.

La cosa se complica cuando el eror no ocurre en nuestra máquina virtual local. En ese caso el error se encuentra en cualquier peculiaridad del entorno de Travis que no pueda se replicado en una máuina virtual propia, por lo que no qeda otra que entrar en el entorno mismo de Travis para averiguar que es lo que está pasando.

Travis activa por defecto la depuración para los repositorios privados, en caso de que el tuyo lo sea encontrarás el botón de depuraciñin justo debajo del de "restart job":



Si se pulsa ese botón el build dará comienzo, pero pasados los primeros pasos se parará y abrirá un puerto de ssh para que nos conectemos. Aquí hay un mensaje de salida de ejemplo de lo que se verá tras pulsar ese botón:


Debug build initiated by BanzaiMan
Setting up debug tools.
Preparing debug sessions.
Use the following SSH command to access the interactive debugging environment:
ssh DwBhYvwgoBQ2dr7iQ5ZH34wGt@ny2.tmate.io
This build is running in quiet mode. No session output will be displayed.
This debug build will stay alive for 30 minutes.



En este ejemplo conectaríamos por SSH a través de DwBhYvwgoBQ2dr7iQ5ZH34wGt@ny2.tmate.io, lo que nos daría acceso a la consola de la máquina virtual utilizada por Travis para el build. Una vez dentro de la máquina virtual puedes ejecutar cada paso del proceso de construcción haciendo uso de los siguientes comandos:

travis_run_before_install
travis_run_install
travis_run_before_script
travis_run_script
travis_run_after_success
travis_run_after_failure
travis_run_after_script


Esos comandos activarán los respectivos pasos del proceso de construcción. Cuando finalmente aparezca el error, se puede examinar el entorno para averiguar quñe es lo que está fallando.

El problema es que el botón de debug no está disponible para repositorios publicos, aunque afortunadamente se puede seguir haciendo uso de esa característica aunque para ello serán necesarios algunos pasos extra. Para activar esa funcionalidad de depuración hay que solicitarlo al soporte de Travis a través del correo support@travis.ci.com. Pueden tardar unas cuantas horas en responder y activarlo.

Una vez que recibamos confirmación del soporte de Travis acerca de que la depuración está habilitada, seguiremos sin ver el botón de debug pero podremos hacer uso de ella mediante una llamada a la API de Travis. Se puede hacer esa llamada con el siguiente comando de consola:

dante@Camelot:~/project-directory$ curl -s -X POST \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -H "Travis-API-Version: 3" \ -H "Authorization: token ********************" \ -d "{\"quiet\": true}" \ https://api.travis-ci.org/job/{id}/debug


Para conseguir to token necesitarás ejecutar este comando antes:

dante@Camelot:~/project-directory$ travis login We need your GitHub login to identify you. This information will not be sent to Travis CI, only to api.github.com. The password will not be displayed. Try running with --github-token or --auto if you don't want to enter your password anyway. Username: dante-signal31 Password for dante-signal31: ***************************************** Successfully logged in as dante-signal31! dante@Camelot:~/project-directory$ travis token Your access token is **********************



Para conseguir un nñumero válido de {id} hay que entrar en el log del job que se quiera repetir y expandir la sección "Build system information". Ahí se puede encontrar el número que necesitamos en la línea "Job id:".

Justo después de lanzar la llamada a la api podremos hacer ssh a la máquina virtual para comenzar la depuración.

Para acabar la sesión de depuración podemos o cerrar todas las ventanas de ssh o cancelar el build desde el interfaz web.

Aunque hay que ser consciente de un peligro potencial. ¿Por qué esta funcionalidad no está disponible para repositorios públicos? porque el servidor de ssh no tiene autentificación por lo que cualquiera que sepa a donde conectar podrá obtener acceso a la consola de la máuina virtual. ¿Cómo podría averiguar un atacante a quñe URL conectar? mirando los logs del build, si tu repositorio es público los logs aparecen en abierto para cualquiera que quiera verlos en el portal de Travis. Si un atacante estuviese vigilando el log del build y en ese mismo momento iniciasemos una sesión de depuración, elatacante vería la misma cadena de conexión que nosotros y podrá conectarse a la misma máquina. Dentro de la máuina virtual de Travis, el atacante puede revisar las variables de entornos y sacar cualquier contraseña o token que tengamos en ellas. Lo bueno es que cualquier conexión de ssh nueva se conectará a la misma sesión de consola que nosotros, por lo que veríamos sus comandos aparecer en la sesión, dándonos la oportunidad de cancelar la sesión de depuración desde el interfaz web antes de que el atacante pueda alcanzar nada crítico.