Automatización de una Conexión sftp

Lo primero que hay que hacer antes de automatizar un proceso con Expect es hacerlo manualmente de manera que conozcamos los patrones de cadenas que intervienen en los ciclos acción-reacción de la interacción. El siguiente ejemplo muestra el guión que uso habitualmente para sincronizar los ficheros de estos apuntes con su copia en el servidor ftp del centro en el que trabajo. Veamos primero el patrón que sigue una conexión convencional. El servidor requiere una conexión sftp . Después de conectarnos y proporcionar la palabra clave cambiamos al directorio adecuado y volcamos los ficheros:

lhp@nereida:~/public_html/perlexamples$ sftp xxxxxxx@ftp.xxxxx.xxx.es
Connecting to ftp.xxxxx.xxx.es...
xxxxxxx@ftp.xxxxx.xxx.es's password:
sftp> cd asignas/asignas/XXX
sftp> put index.html
Uploading index.html to /var/ftp/pub/asignas/XXX/index.html
index.html          100%   23KB  22.8KB/s   00:00
sftp> quit
lhp@nereida:~/public_html/perlexamples$
Ahora pasamos a automatizarlo:
lhp@nereida:~/Lperl/src/expect/tutorial$ cat -n sftp_put
 1  #!/usr/bin/perl -w
 2  use strict;
 3  use Expect;
 4
 5  my $host           = shift || 'user@ftp.domain.ull.es';
 6  my $localpath      = shift || 'local_dir';
 7  my $remotepath     = shift || 'remote_dir';
 8  my $glob           = shift || '*';
 9  my $command         = "put $glob\n";
10  my $deadline       = shift || 8;
11  my $filespercolumn = shift || 4;
12  my $ftp_prompt  = 'sftp>\s*';
13  my $pass_prompt = qr'assword:\s*';
14  my $password    = get_password();
15  my $ftp;
16
17  $Expect::Log_Stdout = 0;
18
19  sub get_password {
20    my $password    = `cat .pass`;
21    chomp($password);
22    $password = reverse $password;
23    "$password\n";
24  }
25
26  {  # Clausura con $count
27    my $count = 1;
28
29    sub upload_handler {
30      my $self = shift;
31      my $a = $self->match();
32
33      $self->clear_accum();
34      $a =~ /g +(\S+)/;
35      print "$1\t";
36      print "\n" if !($count++ % $filespercolumn);
37      exp_continue;
38    }
39  }
40
41  chdir $localpath or die "Cant change to dir $localpath\n";
42  $ftp = Expect->spawn("sftp", $host);
43  $ftp->expect(
44    $deadline,
45    [$pass_prompt, sub { print $ftp $password; } ],
46    $ftp_prompt
47  );
48
49  my $err = $ftp->error();
50  die "Deadline $deadline passed. $err\n" if ($err);
51  print "Successfully connected to $host\n";
52
53  print $ftp "cd $remotepath\r";
54  $ftp->expect($deadline, '-re', $ftp_prompt)
55    or die "After <cd>. Never got prompt ".$ftp->exp_error()."\n";
56  print "Changing directory to $remotepath\n";
57
58  print "Executing $command ...\n";
59  print $ftp $command;
60  $ftp->expect(
61    $deadline,
62    [qr'Uploading +\S+', \&upload_handler],
63    $ftp_prompt
64  );
65  $err = $ftp->error();
66  die "Deadline $deadline passed. $err\n" if ($err);
67  print("\nAfter $command\n");
68
69  print $ftp "quit\r";
70  $ftp->hard_close();

Enviar entrada a un programa controlado con Expect es tan sencillo como hacer un print del comando. Un problema es que terminales, dispositivos y sockets suelen diferir en lo que esperan que sea un terminador de línea. En palabras de Christiansen y Torkington [3]

We've left the sanctuary of the C standard I/O library
y la conversión automática de \n a lo que corresponda no tiene lugar. Por eso verás que en algunas expresiones regulares que siguen se pone \r o cualquier combinación que se vea que funciona.

En la línea 42 se llama a spawn como método de la clase:

$ftp = Expect->spawn("sftp", $host)

esta llamada produce un fork y se ejecuta (exec) el comando sftp. Retorna un objeto Expect, esto es un objeto que representa la conexión con el programa lanzado y que contiene la información sobre la Pseudo terminal TTY (Véase la clase IO::Pty ). El truco en el que se basa Expect es este: hacerle creer al proceso al otro lado que está en una sesión interactiva con una terminal. De ahí que el objeto sea una terminal virtual. Si spawn falla, esto es, si el programa no puede arrancarse, se retorna undef. La sintáxis mas usada es:

Expect->spawn($command, @parameters)

Los parámetros se le pasan sin cambios a exec. Si el comando no se pudo ejecutar el objeto Expect sigue siendo válido y la siguiente llamada a expect verá un "Cannot exec". En este caso el objeto se ha creado usando spawn pero podría haberse creado primero con el constructor new. El método new admite dos formas de llamada:

$object = new Expect ()
$object = new Expect ($command, @parameters)
La segunda es un alias de spawn.

Es posible cambiar los parámetros del objeto antes de hacer spawn. Esto puede ser interesante si se quieren cambiar las características de la terminal cliente. Por ejemplo, antes de ejecutar el spawn podemos llamar al método $object->raw_pty(1) el cual desactivará el eco y la traducción de $ CR \rightarrow LF$ produciendo una conducta mas parecida a la de un pipe Unix.

El patrón del programa es una iteración de unidades de acción-reacción. Por ejemplo, en la línea 53 emitimos el comando para cambiar a un determinado directorio de la máquina remota mediante Expect. Los métodos print y send son sinónimos. Así la línea 53 puede ser reeescrita como:

ftp->send("cd $remotepath\r");

La respuesta se recibe mediante el método expect. El esquema de funcionamiento de expect es que tenemos un manejador de fichero/flujo abierto desde un proceso interactivo y le damos tiempo suficiente hasta que la cadena que se va recibiendo case con una expresión regular que proporcionamos como límite. Una vez que el buffer casa con la expresión regular se libera todo el texto hasta el final del emparejamiento. La forma mas simple de llamada a expect sigue el formato:

$exp->expect($timeout, '-re', $regexp);

o un poco mas general

$exp->expect($timeout, @match_patterns);

El parámetro $timeout dice cuantos segundos se esperará para que el objeto produzca una respuesta que case con una de los patrones. Si $timeout es undef se esperará un tiempo indefinido hasta que aparezca una respuesta que case con uno de los patrones en @match_patterns.

Es posible poner varias expresiones regulares en la llamada:

$which = $object->expect(15, 'respuesta', '-re', 'Salir.?\s+', 'boom');
if ($which) { 
  # Se encontró una de esos patrones ...
}

En un contexto escalar expect retorna el número del argumento con el que ha casado: 1 si fué respuesta, 2 si casó con Salir.?\s+, etc. Si ninguno casa se retorna falso.

Una segunda forma de llamada a expect puede verse en las líneas 43 y 60. Estas dos llamadas siguen el formato:

$exp->expect($timeout,
  [ qr/pattern1/i, sub { my $self = shift; .... exp_continue; }],
  [ qr/pattern2/i, sub { my $self = shift; .... exp_continue; }],
  ...
  $prompt
);
En este caso expect establece un bucle. Si se casa con pattern1 se ejecuta la subrutina apuntada por la referencia asociada. Si esta termina en exp_continue se sigue en expect. En ese caso el cronómetro se arranca de nuevo y el tiempo se empieza a contar desde cero. Si se casa con pattern2 se ejecuta la segunda subrutina, etc. Si se casa con $prompt o se sobrepasa el límite $timeout se sale del lazo.

Por ejemplo, en la línea 58:

58  $ftp->expect(
59    $deadline,
60    [qr'Uploading +\S+', \&upload_handler],
61    $ftp_prompt
62  );

Después del put, el mensaje Uploading file sale por cada fichero que se transfiere. Eso significa que el manejador upload_handler se estará ejecutando hasta que aparezca de nuevo el prompt o bien se supere el tiempo limite establecido.

El manejador upload_handler (líneas 29-38) obtiene mediante la llamada al método match la cadena que casó con la expresión regular que ha triunfado Uploading +\S+. El método clear_accum limpia el acumulador del objeto, de manera que la próxima llamada a expect sólo verá datos nuevos. Mediante la expresión regular de la línea 34 se detecta el nombre del fichero implicado y se muestra por pantalla.

Para cerrar la sesión (línea 70) llamamos al método hard_close . Una alternativa a este es usar soft_close o hacer una llamada a expect esperando por 'eof'. Sin embargo esta alternativa toma mas tiempo. En cualquier caso es importante asegurarnos que cerramos adecuadamente y no dejamos procesos consumiendo recursos del sistema.

Al ejecutar el programa anterior obtenemos:

lhp@nereida:~/Lperl/src/expect/tutorial$ sftp_put
Successfully connected to username@ftp.domain.ull.es
Changing directory to remote_dir
Executing put *
..........      ..........      ..........      ..........
images.aux.gz   images.bbl      images.bbl.gz   images.idx
images.idx.gz   images.log      images.log.gz   images.out
images.pl       images.pl.gz    images.tex      images.tex.gz
img1.old        img1.png        img10.old       img10.png
img10.png.gz    img100.old      img100.png      img101.old
img101.png      img102.old      img102.png      img103.old
..........      ..........      ..........      ..........
node95.html.gz  node96.html     node96.html.gz  node97.html
node97.html.gz  node98.html     node98.html.gz  node99.html
node99.html.gz  pause2.png      perl_errata_form.html   perlexamples.css
..........      ..........      ..........      ..........
After put *
Veamos una segunda ejecución a otra máquina que no solicita clave (conexión via ssh usando clave pública-clave privada):
lhp@nereida:~/Lperl/src/expect/tutorial$ sftp_put europa '.' '.' etsii
Successfully connected to europa
Changing directory to .
Executing put etsii
 ...
etsii
After put etsii

Ejercicio 8.9.1   ¿Cómo es que funciona si la máquina no solicita el password? ¿Qué esta ocurriendo en la llamada a expect de las líneas 43-47?

La variable $Expect::Log_Stdout (línea 17) controla la salida de los mensajes de log por STDOUT. Normalmente vale 1. Tiene asociado un método get/set $object->log_stdout(0|1|undef) (alias $object->log_user(0|1|undef)). Cuando el método se llama sin parámetros retorna los valores actuales. Al poner la variable a cero

$Expect::Log_Stdout = 0;
desactivamos globalmente la salida.

Casiano Rodríguez León
Licencia de Creative Commons
Programación Distribuida y Mejora del Rendimiento
por Casiano Rodríguez León is licensed under a Creative Commons Reconocimiento 3.0 Unported License.

Permissions beyond the scope of this license may be available at http://campusvirtual.ull.es/ocw/course/view.php?id=44.
2012-06-19