Seu dia a dia automatizado com PHP e Expect.

17/04/2012 by faustovaz

Em 2010 tive a oportunidade de ir ao Latinoware e assitir a uma palestra do Rogério Ferreira sobre automatização de tarefas rotineiras, aquelas que sempre fazemos no nosso dia a dia como desenvolvedores (ou sys admins). Na palestra do Rogério ele apresentou uma extensão da linguagem de script TCL chamada Expect que permite que você faça interações com um shell de forma automática. Assim, você pode criar um script que fará automaticamente toda aquela interação com o shell que necessite de intervenção humana. Por exemplo, você pode criar um script para logar em um servidor qualquer, executar alguns comandos e enviar alguns arquivos. Tudo isso de forma automatica. Todo o trabalho que você faria para logar
no servidor e digitar os comandos necessários para cumprir uma tarefa como essa o script com Expect faz pra você.

A palestra foi muito interessante e o Rogério disponibiliza até hoje os slides da apresentação no site dele.

Nesses últimos meses estou trabalhando como freelancer nos horários vagos com um amigo que presta serviços fazendo consultoria em recursos humanos. Em um dos clientes nosso trabalho envolvia integrar o sistema de cartão ponto do cliente com o sistema que estavamos desenvolvendo. Esse cliente era grande e por consequência disso tinha máquinas de cartão ponto espalhadas em vários locais para facilitar o registro de horários dos funcionários. Devido a limitação dos aparelhos que esse cliente possuía, todos os horários eram registrados em arquivos texto e embora os computadores fossem ligados em rede eles não tinha uma estrutura que permitisse que esses arquivos fossem enviados ou centralizados em um único local, tornando dificil armazenar os registros no nosso sistema afim de calcular salários e horários extras. Como já conhecíamos o Expect, tínhamos uma carta na manga para solucionar esse problema. No entanto, precisávamos de uma solução rápida e não tínhamos disponível no time uma pessoa que entendesse de TCL. Foi aí que descobrimos, fuçando no site da pecl, a extensão do expect para PHP.

Essa extensão é bem bacana e apesar de estar na versão beta, funciona muito bem. Com a extensão instalada você programa em php e aproveita toda a funcionalidade que o expect oferece. Abaixo mostro a instalação e alguns scripts que dão uma idéia do que pode ser feito com o expect.

Instalação

Aqui usamos o pacote xampp para desenvolver nossos trabalhos e para instalar a extensão do expect basta baixar o pacote de desenvolvimento do xampp, que vem os fontes do PHP e outros arquivos mais. Uma vez com os arquivos devidamente instalados basta usar o aplicativo pecl para instalar a extensão. Se você não tem o expect-dev e o autoconf instalados siga os seguintes passos:

 apt-get install expect-dev;
 apt-get install autoconf;
 pecl install expect-0.3.1;
 

Se você já tiver o autoconf e o expect-dev instalados, basta executar a última instrução.
Logo apos a instalação do pacote basta incluir no seu arquivo php.ini a extensão do expect:

extension=expect.so;

Exemplos

A extensão expect possui somente duas funções, uma para abrir o stream do shell e outra que comparar a saída do shell após um comando dado com os patterns passados. Para exemplificar melhor, segue um script com expect:

#!/opt/lampp/bin/php
<?php

$servers = array(
        array("ip" => "192.168.1.22", "user" => "adminuser", "password" => "adminuser", "path" => "/home/adminuser/punto/punto_comercial.txt"),
        array("ip" => "192.168.1.45", "user" => "adminuser", "password" => "adminuser", "path" => "/home/adminuser/punto/punto_admin.txt"),
        array("ip" => "192.168.1.37", "user" => "adminuser", "password" => "adminuser", "path" => "/home/adminuser/punto/punto_vendas.txt"),
        array("ip" => "192.168.1.15", "user" => "adminuser", "password" => "adminuser", "path" => "/home/adminuser/punto/punto_info.txt"),
);
$localPath = "/home/fausto/files/";

ini_set("expect.loguser", "Off");
$options = array(
        array("password:", "PASSWORD"),
);

foreach ($servers as $server)
{
    $params = $server['user'] . '@' . $server['ip'] . ":" . $server['path'] . " " .$localPath;
    $stream = expect_popen("scp -r $params");
    switch(expect_expectl($stream, $options))
    {
        case "PASSWORD"        :    fwrite($stream, $server['password']."\n");
                                    while($line = fgets($stream));
                                    break;
    }
    fclose($stream);
}

echo "Done!\n";
?>

Esse é o script que usamos para resolver o problema que citei acima. Ele é bem simples e tudo que ele faz é copiar os arquivos textos de cada um dos servidores relacionados para um diretório local qualquer.

Para entender melhor, na linha 12, começamos setando uma opção de configuração do Expect, o expect.loguser, para off. Dessa forma não serão gerados log do que estiver acontecendo no shell. Para que você possa ver o output que é gerado ao executar o expect, basta retirar esse trecho de código, uma vez que a opção padrão dessa configuração é On. O expect tem outras três opções de configurações que podem ser consultadas aqui.

Logo abaixo, na linha 20, temos uma chamada a função expect_popen. Esse método executa o comando passado no shell e retorna o stream do terminal. Com esse stream você pode usar a função fwrite do php para escrever dados no terminal, como uma senha, uma resposta de yes / no ou outros comandos.

Mas como saber o momento de escrever um password ou uma resposta de yes / no para o shell? Para isso temos a função expect_expectl. Essa função compara a saida que o shell disponibiliza após a execução de um comando com um array de array de opções.

Explicando melhor, quando o script acima é executado ele chama a função expect_popen e passa para ela o comando scp -r que copia recursivamente, via ssh, arquivos de um local qualquer para um destino qualquer. Assim que esse comando é executado, o shell espera pelo password que autorizará a execução da instrução. Assim a função expect_expectl compara a saída disponibilizada pelo shell (que no caso será “password:”) com o array de array de opções. Ao encontrar o pattern, ele executa os métodos associados, que no caso está dentro de um bloco switch case. Ao executar, a  função fwrite escreve o password requerido no shell e executa a tarefa.

Segue abaixo um outro exemplo que uso no dia a dia para deployar rapidamente uma aplicação em um servidor de testes. Executo esse script sempre que preciso enviar para nosso servidor de testes a versão corrente da app que estou trabalhando e atualizar o banco de dados para os testes.

#!/opt/lampp/bin/php
<?php
//Configurações do servidor e aplicação.
$host = "192.168.1.5";
$user = "adminuser";
$password = "adminuser";
$destino = "/home/adminuser/bagual/";
$projeto = "/opt/lampp/htdocs/codeigniter/";
//Configurações do banco de dados
$mysqlUser = "root";
$mysqlPassword = "";
$mysqlDB = "dev1";
$arquivoSQL = "agritrad_extranet.sql";
//Parametros de configuração e comportamento do expect
ini_set("expect.loguser", "Off");
$shell = fopen("expect://scp -r $projeto $user@$host:$destino", "r");
$opcoes = array (
    array("password:","PASSWORD"),
    array(" $" ,"SHELL", EXP_EXACT)
);
switch (expect_expectl ($shell, $opcoes)){
    case 'PASSWORD': fwrite ($shell, "$password\n");
                     break;
    default        : die ("Eita, deu um erro.\n");
}
while($line = fgets($shell)){
echo $line; //Mostra os arquivos enviados.
}
fwrite($shell, "exit");
fwrite($shell, "\n");
fclose ($shell);

// Atualiza banco de dados
$done = false;
$stream = expect_popen("ssh $user@$host");
while(true){
    switch (expect_expectl ($stream, $opcoes)){
        case 'PASSWORD': fwrite($stream, "$password\n");
                         break;
        case 'SHELL'   : if($done == false){
                             fwrite($stream,"cd $destino");
                             fwrite($stream, "\n");
                             fwrite($stream, "mysql -u $mysqlUser -p $mysqlDB < $arquivoSQL");
                             fwrite($stream, "\n");
                             while(true){
                                 switch(expect_expectl($stream, $opcoes)){
                                     case 'PASSWORD': fwrite($stream, "$mysqlPassword\n");
                                                      $done = true;
                                                      break 2;
                                 }
                             }
                         }else{
                              break 2;
                         }
    }
}
echo "Concluido!\n";
fclose ($stream);
?>

O script é bem semelhante ao primeiro. Uma coisa interessante para notar é o uso da constante EXP_EXACT, que indica ao PHP que o pattern fornecido é uma string exata. Há outras constantes que você pode usar para facilitar na construção dos seus scripts.

Outro ponto, é o uso de uma chamado adicional ao método fwrite com o parametro “\n”, para simular o ENTER. Por algum motivo desconhecido se usarmos o “\n” concatenado ao final do comando, o script imprime exatamente a cadeia de caracteres “\n” ao invés de simular um ENTER. Deve ser algum problema com a extensão do expect. Por isso forcei a chamada do fwrite com somente o “\n” como parametro.

O Expect abre uma gama de possibilidades de automatização em casos onde há a necessidade de intervenção humana. Tentar automatizar sempre é bom. No final das contas, quando você automatiza tarefas sempre sobra tempo para coisas mais legais e produtivas.

Uncategorized