Expect, la librería que automatiza scripts interactivos

La instalación silenciosa y despliegue automático de librerías o componentes es un aspecto fundamental a tener en cuenta a la hora de montar un entorno fiable y productivo.

Es habitual que en proyectos de desarrollo software o en gestión de sistemas se tengan que integrar procesos estandarizados. Si estos procesos requieren la interacción del usuario, el tiempo que se tarda en completar la tarea aumenta considerablemente, al igual que el riesgo debido al factor humano.

Por eso, herramientas como Expect facilitan el trabajo al hacer posible la automatización de algunas tareas como los scripts interactivos. En este post veremos los aspectos básicos de esta librería para Linux y un par de ejemplos que nos ayuden a crear nuestros propios ficheros ejecutables.

¿Qué es Expect?

Expect es un programa que habla a otros programas a través de un script. Siguiendo este script, Expect sabe qué salida esperar del programa que ejecuta y responder en consecuencia y, si procede, es posible devolver el control al usuario o revocarlo.

Resulta muy útil para automatizar tareas repetitivas en sistemas, tanto de forma local como remota, que requieren introducir información manualmente, más aún cuando trabajamos con instrucciones y protocolos como SSH, SCP, SFTP, TELNET o RLOGIN.  

De igual modo, los despliegues de aplicaciones o componentes en máquinas remotas pueden ser gestionados con scripts de usuarios (init.d), que controlan los permisos y el acceso externo. Con Expect, la secuencia de comandos a introducir es fácilmente automatizable y los scripts simples también son fáciles de integrar en nuestro workflow.

Para equipos que deban administrar sistemas, instalando siempre los mismos programas o modificando configuraciones a través de la terminal, también resulta una opción a tener en cuenta, ya que se puede desarrollar un script que lance todos esos procesos de una sola vez.

Instalando Expect

En el caso de las sistemas Linux esta herramienta puede venir instalada por defecto. Si no es nuestro caso y nuestra distribución es Debian o Ubuntu, podemos usar ‘apt-get’:

$ sudo apt-get install expect

Para entornos como Fedora o CentOs, la instalación se realiza a través de ‘yum’. En el caso de los sistemas Mac, se puede instalar del mismo modo a través de HomeBrew:

$ sudo yum install expect 

En Windows la instalación es un poco más laboriosa. Debemos descargarnos las librerías Expect desde sourceforge y los binarios tcl desde ActiveState. Tras instalar las librerías se deberá compilar el código y referenciar en el path los archivos generados para poder utilizar Expect con normalidad. En los archivos descargados se puede encontrar información detallada acerca de cómo instalar la herramienta.

En este punto ya tenemos instalada la nueva herramienta y lista para funcionar.

Ejecutando Expect

En el apartado anterior hemos comentado las formas de instalar Expect en función del sistema operativo. En este caso vamos a utilizar Ubuntu, ya que es de los más sencillos tanto para instalarlo como para crear nuestros scripts.

Podemos arrancar abriendo la consola de Expect con este comando:

$ expect 

Desde aquí podemos probar los comandos más comunes y hacer algunas pruebas base:

Para comprender mejor cómo se procesan las respuestas de la consola, debemos ver el comportamiento de las variables expect_out (0, string) y expect_out (buffer).

  • expect_out (0, string) contiene la cadena de texto que se corresponde (match) con lo que Expect espera.
  • expect_out (buffer) contiene toda la cadena recibida por Expect, incluida (o no)  expect_out (0, string).

Si tenemos este script:

 expect "Hola\n" 
 send "Has escrito <$expect_out (buffer)>"  
 send "Pero solo esperaba <$expect_out(O,string)>"

Al lanzarlo se nos permitirá introducir el texto que queramos. Supongamos que introducimos esto:

 Buenas! 
 Hola 
 Has escrito <Buenas! 
 Hola> 
 Pero solo esperaba <Hola>

Uso de Expect a través de scripts Ficheros .exp

Por convención, Expect utiliza la extensión .exp en sus scripts, pero la llamada que ejecuta los scripts le es indiferente la extensión. Se podría indicar .sh o directamente ninguna. Únicamente se debe indicar al inicio del fichero #!/usr/bin/expect para que se interprete correctamente.

Expect utiliza TCL (Tool Command Languaje). Esto significa que utiliza las instrucciones más comunes en scripting, como son if, for o break, evalúa expresiones y otras características como recursividad o declaración de procedimientos y subrutinas. Los comandos más importantes, sobre los que basamos los siguientes ejemplos, son spawn, expect  y send.

  • Spawn: Se trata del comando base de cualquier script de Expect. Esta instrucción inicia el programa o proceso externo con el que se va a interactuar. A cada proceso que se inicia se le asigna un id que se almacena en una variable llamada spawn_id que le sirve a Expect para diferenciar el proceso sobre el que actúa. El primer argumento de spawn es el proceso a ejecutar y los argumentos siguientes son los propios argumentos del proceso. Por ejemplo:
spawn ftp ftp.host.net 
spawn ssh  usuario@host 
spawn sh install_all.sh
  • Expect: Este comando analiza la salida del programa lanzado con spawn y actúa en consecuencia. Cada comando expect está ligado a un spawn_id que se asigna según el orden de aparición del spawn, por defecto se asocia al proceso lanzado más recientemente.
  • Send: Instrucción básica de respuesta de Expect. Cuando la salida del proceso concuerda con la indicada en el comando expect, send envía lo que le indiquemos al programa. También se vale de la variable spawn_id para asociarse al proceso que le corresponde. Por ejemplo:
    spawn /bin/sh 
    expect "\\$ " 
    send "ls -la\r"
    

Para ver la estructura básica de un script .exp, aquí tenemos un ejemplo de automatización de un comando scp:  

#/bin/sh 
scp /ruta/origen usuario@host:/ruta/destino/

Si ejecutamos el script anterior como sh convencional, el comando scp nos irá solicitando las contraseñas de la máquina host.

Con Expect hacemos la llamada lanzando scp de forma independiente y procesando la entrada estándar de consola. Cuando se detecte la cadena ‘password:’, se introducirá lo que le indiquemos de forma automática.

#!/usr/bin/expect -f 
 set filename [lindex $argv 0] 
 set timeout -1 
 spawn scp $filename user@host:/home/user/ 
 set pass "password" 
 
 expect { 
      password: {send "$pass\r" ; exp_continue} 
      eof exit 
 }

En la primera línea #!/usr/bin/expect -f indicamos que el script debe ejecutarse con Expect y que los comandos vendrán introducidos por fichero .exp_continue e indica al script que no evalúe más expresiones y continúe con la siguiente instrucción.

Cada vez que se llama a esta acción, el timeout de espera (por defecto cada 10 segundos) para cada respuesta de consola se reinicia. Esto se puede evitar indicando exp_continue –continue_timer.

Los ficheros de Expect (.exp) pueden ejecutarse de este modo siempre que tengan el permiso adecuado de ejecución:

$ chmod +x script01.exp 
$ expect script01.exp

La salida correspondiente al script anterior (sin intervención alguna del usuario) es la siguiente:

Login remoto con Expect

En el próximo ejemplo vamos a escribir y comentar un script más completo que actúe en una máquina remota. Levantaremos una sesión ssh incluyendo logs y control de errores.

#!/usr/bin/expect -f 
 
 # Uso sshlogin.exp <ssh user> <ssh host> <ssh password> 
 #Seteamos timeout personalizado y procesamos los argumentos. 
 set timeout 20 
 set ip [lindex $argv 0] 
 set user [lindex $argv 1] 
 set password [lindex $argv 2] 
 #regex con los caracteres mas comunes en las consolas (habitualmente '$') 
 set prompt "(%|#|I|\\$)" ; 
 
 proc logger {message} { 
#Método que imprime por pantalla a modo de log. 
set HEADER \[DEPLOY_LOG\]; 
#puts imprime por consola lo que indiquemos. 
puts "\n $HEADER $message $HEADER \n"; 
 }
proc exit_on_error {message} { 
#Ha habido un error. Traceamos y salimos. 
logger $message 
exit 1  } 
 #Login ssh 
 logger "Conectando a $ip como $user" 
 #Lanzamos la sesión ssh sin comprobar la existencia del host en la máquina 
 #Ver alternativa a "StrictHostKeyChecking no" 
 spawn ssh -o "StrictHostKeyChecking no" "$user\@$ip"; 
  
 expect { 
 #Control de las posibles entradas. Timeout, eof genérico o peticion de pass 
       "*assword" {send "$password\r";  
             logger "Login en $ip como $user correcto." 
#Con exp_continue, indicamos que vuelva a procesar la entrada con los inputs #de este bloque expect. Si la pass es incorrecta no se quedará colgado ya que #hay un numero de intentos máximos y saldrá por eof 
                   exp_continue 
            } 
  -re $prompt { 
#Si la conexión ha ido bien, recibiremos una línea con un #carácter (%|#|I|\\$)  
         logger "Login en $ip como $user correcto." 
  }  
        timeout {exit_on_error "Fallo en la conexión SSH en expect" } 
        eof {exit_on_error "Fallo en la conexión SSH en expect. Salimos."} 
 } 
 
 #Sin timeout 
 set timeout –1 
 #Finalmente, el script deja el control al usuario 
 interact

Si queremos evitar indicar StrictHostKeyChecking no podemos controlar la comprobación de las keys:

spawn ssh "$user\@$ip"; 
expect "yes/no" { 
           send "yes\r" 
           expect "*assword" { send "$password\r" }                 }  
"*assword" { send "$password" } 
}

Se habrá notado un agujero de seguridad bastante evidente en estos scripts. Como son ejemplos introductorios, no hemos tenido en cuenta el hecho de escribir en plano o por parámetro una contraseña para una conexión remota, pero cuando usemos Expect en otros entornos la forma correcta de proceder es mediante encriptaciones (por ejemplo, con openssl) en ficheros protegidos o SSH keys.

Autoexpect

Hemos visto varios scripts que muestran los aspectos básicos de Expect y las funciones de sus comandos principales.

Para llevar esta librería (o vaguear) al siguiente nivel, debemos comentar la herramienta Autoexpect que, al ejecutarse, observa las instrucciones que realiza el usuario y genera un script de Expect de forma autónoma.

Autoexpect viene unido a Expect, por lo que no sería necesario realizar ninguna instalación adicional. El código generado es algo tosco, pero puede afinarse fácilmente:

$ autoexpect 

A partir de este punto, Autoexpect irá incluyendo en el fichero script.exp los comandos que se introduzcan por consola en lenguaje expect. De este modo, cuando se complete el trabajo y se cierre la terminal (o el comando exit), el fichero resultante será completamente viable para ejecutarlo como un .exp normal.

Se debe tener en cuenta que también se guarda la salida completa de la consola, por lo que si esa salida contiene datos variables (como fechas en ssh o date), el script no va a funcionar dado que no coincide exactamente la salida de la consola.

Para evitar esto, Autoexpect puede ejecutar en modo ‘promt’, mediante el cual únicamente guarda la última línea de la salida de consola.

En la siguiente imagen se aprecia cómo crear un script sencillo con Autoexpect:

Una vez se ha creado, se ejecuta como .exp:

Conclusión: ¿en qué puntos de mi ciclo de desarrollo puede utilizarse Expect?

Expect es una herramienta poco conocida, pero potente. Hemos visto que es capaz de simplificar procesos que requieren la intervención del usuario para pasar a ejecutarlos de forma autónoma con un lenguaje muy intuitivo y aplicable, como suite de apoyo para administración de sistemas o como ayuda para mejorar el tiempo y la calidad del ciclo de software.

Recalcar que a la hora de crear nuestros propios scripts, debemos tener muy en cuenta lo relacionado con la seguridad y conocer muy bien el proceso que queremos automatizar para controlar los posibles fallos y crear un código robusto.

No debemos olvidar que Expect se ciñe exclusivamente a lo que aparezca en su fichero, por lo que es muy importante el control de errores, timeouts o salidas alternativas y procesar dichas salidas de cada comando de forma minuciosa e independiente para evitar sustos innecesarios. Nadie quiere que se ejecute un rm si el anterior cd ha fallado.

Es habitual que en la fase de despliegue de servicios o aplicaciones haya que instalar o ejecutar comandos sobre consola en sistemas externos. Expect podría ser la herramienta adecuada para ello si estas instrucciones deben ejecutarse de manera interactiva.

El hecho de que en nuestros entornos locales o de cliente los despliegues de la aplicación estén controlados por scripts estandarizados internos, no debe impedir que se complete el ciclo de desarrollo continuo, ya que utilizando Expect con herramientas de integración continua (como Jenkins o goCD…) se pueden automatizar estos procesos desde máquinas fijas, virtuales o dockers.

En mi experiencia en el uso de Expect, los resultados tras aplicar esta herramienta sobre despliegues en entornos externos han sido tremendamente positivos. Eliminando el factor humano y con una correcta configuración, el tiempo de despliegue de una aplicación web de reinicio del servidor y el envío de ficheros de configuración se ha visto reducido de forma muy significativa, al igual que el riesgo de fallo.

Para finalizar, decir que Expect también está disponible en otros lenguajes como Java, c#, Perl, Scala o Python, entre otros.

Ingeniero Informático desde el año 2014. Miembro del equipo de QAradigma. Automatizo cosas. Desde siempre me ha gustado el desarrollo software, el deporte en general y el baloncesto en particular. ¿Lo más importante en lo profesional y en lo deportivo? El equipo.

Ver toda la actividad de Eladio Mejuto

Escribe un comentario