Executando um mesmo comando remotamente em vários servidores via SSH

Acredito que não exista nada mais ingrato, na vida de um sysadmin, do que repetir um determinado comando em vários servidores. O SSH nos poupa ir máquina a máquina para digitar estes comandos, mas só permite que o façamos em um servidor por vez. Com um pequeno truque no Bash podemos repetí-lo em vários servidores de uma vez.

Para os veterenos o que vou mostrar aqui não é nada de novo, mas para os aspirantes ao posto de “capitão do navio” este truque vai ser legal. Esta dica inclusive deveria estar no post anterior sobre SSH mas acabei decidindo deixá-lo em um post a parte.

Teoria e conceitos básicos

Sem rodeios e indo direto a solução, devemos criar uma lista (arquivo) contendo login@servidor, com todos os servidores que desejamos executar o mesmo comando e então enviar cada uma destas linhas sequencialmente para o comando ssh, o que pode ser feito com uma estrutura de controle de laço via shellscript.

Vejamos através de um exemplo prático. Suponhamos que eu administre 4 servidores nomeados como segue e ainda que nos dois primeiros servidores eu tenha um login “welington”, enquanto nos dois últimos meu login é “braga”:

  • america.meudominio
  • asia.meudominio
  • africa.meudominio
  • europa.meudominio

O primeiro passo então é criar um arquivo chamado servidores.txt no meu computador com o conteúdo a seguir:

welington@america.meudominio
welington@asia.meudominio
braga@africa.meudominio
braga@europa.meudominio

A leitura de arquivos sequencialmente pelo Bash é feita diretamente como no exemplo abaixo onde, apenas como exemplo, vamos exibir o conteúdo do arquivo:

for server in $(cat servidores.txt); do
  echo "Servidor: $server"
done

Se você colar ou digitar as linhas acima em uma janela de seu terminal você deverá ter como resposta a seguinte saída:

Servidor: welington@america.meudominio
Servidor: welington@asia.meudominio
Servidor: braga@africa.meudominio
Servidor: braga@europa.meudominio

Uma vez que já sabemos como processar cada linha individualmente, a grosso modo poderíamos simplesmente substituir o comando “echo” pelo comando “ssh”, quem efetivamente conectará ao servidor remoto e juntamente passará o comando a ser executado.

O resultado fica assim:

for server in $(cat servidores.txt); do
  ssh "$server" "$1"
done

Observe que o “$1” é um parâmetro posicional do Bash e corresponde ao primeiro parâmetro passado a um script que tenha aquele conteúdo, portanto salve este conteúdo em um arquivo chamado por exemplo de “multissh”, no mesmo diretório onde salvamos o arquivo “servidores.txt” e o atribua permissão de execução (chmod +x multissh).

Com isso o nosso script básico já funciona muito bem. Digamos que eu queira verificar o uso da memória (comando “free -m”) nestes servidores. A partir do mesmo diretório onde salvei os arquivos criados bastaria digitar o comando a seguir:

./multissh "free -m"

Observe que o comando a ser passado deverá sempre vir entre aspas, pois o nosso script só aceita o primeiro parâmetro posicional, desta forma, ls -lh /var deveria ser passado como:

./multissh "ls -lh /var"

E os demais exemplos ficam por conta da sua imaginação. O resultado será a saída do comando executado remotamente sendo exibida na minha tela, e isso para cada servidor que estiver na lista criada (arquivo servidores.txt) e com isso cumprimos a missão de executar um mesmo comando em vários servidores 😉

Melhorando o trabalho

Não é porque estamos fazendo um script que ele tem que ser simplório beirando a gambiarra. É fato que o script apresentado funciona, mas claro que ele também deixa a desejar em alguns aspectos, então vamos resolver estes “probleminhas” em uma espécie de FAQ.

1. Cada vez que o script tenta conectar a um servidor eu tenho que digitar a minha senha naquele servidor. Tem como evitar isso?

Sim. Crie um certificado RSA ou DSA e o envie para todos os servidores usando o comando ssh-copy-id. Verifique no artigo anterior sobre ssh, como proceder.

2. Quando eu executo um comando de saída muito extensa o resultado fica confuso e não dá pra saber onde termina a listagem de um servidor e onde começa a do outro. Como resolver isso?

“Isso é elementar, meu caro Watson”. Inclua uma linha com o comando “echo” antes do comando “ssh”, no nosso script. Onde era:

  ssh "$server" "$1"

substitua por:

  echo "-----------------------------"
  echo "Servidor: $server"
  ssh "$server" "$1"

A linha com “——-” é meramente cosmética e ajuda a melhorar a visibilidade das saídas (Esta ideia foi descaradamente copiada do “gnt-cluster command” da ferramenta Ganeti.

3. Quando eu executo um determinado comando nada é retornado na tela, porque ele informa o resultado da sua tarefa a partir de códigos de erro de saída. É possível implementar isso?

Sim. Logo após o comando “ssh” você pode usar o comando “echo”, novamente, mas desta vez para exibir o o conteúdo do parâmetro “$?” que retorna o código de erro de saída do último comando executado no script. Onde era:

  ssh "$server" "$1"

substitua por:

  ssh "$server" "$1"
  echo
  echo "Código de retorno: $?"

4. Como eu poderia fazer o “ssh” pular linhas de comentários dentro do arquivo “servidores.txt”?

Substituindo o comando cat, pelo comando grep na linha que contém o laço “for” é possível permitir este requinte. Assim, substitua o comando:

cat servidores.txt

por

grep -v "^#" servidores.txt

Isso fará com que linhas iniciadas pelo caracter “#” sejam ignoradas e portanto poderão ser usadas como comentários.

5. Eu criei os arquivos dentro do meu home e algumas vezes, mesmo estando neste diretório eu invoco o comando e ele diz não encontrar o arquivo. O que pode estar acontecendo?

Bom, considerando que as permissões estejam OK (chmod +x /home/seuhome/multissh) é possível que você esteja esquecendo do “./” antes do nome do arquivo. Isso é obrigatório, pois o script não está em um diretório referenciado pelo PATH do sistema. Para resolver isso de forma elegante é preciso fazer duas simples modificações no script.

Modificação 1: Mover o arquivo servidores.txt para um local fixo e conhecido como por exemplo o /etc e depois mudar a referência a este arquivo para que ela aponte para o caminho correto.

Execute o comando no seu terminal:

sudo mv servidores.txt /etc/servidores.txt

Mude a linha:

for server in $(cat servidores.txt); do

para

for server in $(cat /etc/servidores.txt); do

Modificação 2: Mover o script multissh para um diretório que esteja no PATH. A recomendação é que você mova para o diretório /usr/local/bin , mas se você quiser outro local fique a vontade. Dentre as possibilidades temos /usr/local/sbin, /usr/bin, /usr/sbin, /bin, /sbin etc  (Ao mover para qualquer caminho que termine com Sbin somente o root encontrará o script para execução).

mv multissh /usr/local/sbin

6. Gostei de todas as melhorias. Pode embrulhar todas elas pra viagem?

Claro, senhor(a). É só levar ao caixa 😉

#!/bin/bash
for server in $(grep -v "^#" /etc/servidores.txt); do
  echo "-----------------------------"
  echo "Servidor: $server"
  ssh "$server" "$1"
  echo
  echo "Código de retorno: $?"
done

7. Gostei do trabalho, mas há algo mais que se se possa ser feito para melhorar ainda mais o script?

Sim. Muitas coisas podem ser implementadas ainda. Particularmente o script que eu uso tem uma estrutura “if” dentro do laço “for”, onde eu verifico três “subcomandos” de forma que eu posso não só executar comandos remotos, mas também posso copiar a minha chave rsa/dsa ou mesmo qualquer outro arquivo para os servidores. Além de um simpático texto de ajuda que é exibido caso eu execute o script sem parâmetro algum. O Download desta versão pode ser feita clicando aqui. E obviamente além disso lembre-se que o céu é o limite.

8. Isso não é trabalho redundante não? Certamente alguém já criou algo que faça o mesmo, não!?

Que eu tenha procurado só achei duas soluções. Uma é o “Clusterssh” que é uma interface em TCL/Tk onde ao digitar um dado comando ele é executado em várias janelas de terminal que são exibidas lado a lado na tela, cada uma conectada por SSH.

A outra solução é o gnt-cluster command (do Ganeti) que já citei antes, mas que só funciona em servidores configurados em Cluster e gerenciados pela ferramenta Ganneti. Este script funciona de maneira bem similar a este comando, mas funciona em qualquer servidor mesmo que não sejam parte de um cluster.

9. Então a sua motivação para fazer isso foi o Ganeti?

Não. Muito antes de eu ouvir falar em Ganeti ou mesmo trabalhar com virtualização eu já usava esta solução. Alguns anos atrás eu precisava desligar ou rebootar, com certa frequência, os 10 servidores que gerenciava na época. Digitar shutdown em cada um deles me deixava irritado então criei os scripts “ShutdownAll” e “RebootAll” que podemos dizer que foram os embriões do que eu acabei generalizando a adaptando ao longo do tempo até que ele virou o “multissh”.

6 comentários em “Executando um mesmo comando remotamente em vários servidores via SSH”

  1. Sou amador em shell script e por isso gostaria de sua ajuda.
    Pelo terminal do meu ubuntu eu acesso meu raspberry da seguinte forma:

    SSH pi@”ip do raspberry ”
    password: raspberry
    omxplayer teste.mp3

    Uso esse comando para controlar uma música remotamente. Tentei fazer um shell script pra automatizar isso, mas meu shell para na hr que é me pedido o password. Como posso resolver esse problema?

    1. Olá Odail,

      Crie um par de chaves DSA ou RSA e copie sua chave pública para o Raspberry, a partir daí os próximos login serão feitos sem pedido de senha.

      Basicamente seriam estes comandos a partir do seu computador:
      $ sshkeygen
      (Se perguntado alguma coisa apenas tecle ENTER e não forneça senha alguma)
      $ ssh-copy-id pi@ip-do-raspberry
      (Deve ser pedida a sua senha do raspberry, informe-a)

      A partir daqui todo comando ssh para o raspberry deverá ser feito sem questionar a senha remota:
      $ ssh pi@ip-raspberry um-comando-qualquer

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.