Usando while loop para ssh a múltiples servidores

75

Tengo un archivo servers.txt, con una lista de servidores:

server1.mydomain.com
server2.mydomain.com
server3.mydomain.com

cuando leo el archivo línea por línea con whiley hago eco de cada línea, todo funciona como se esperaba. Todas las líneas están impresas.

$ while read HOST ; do echo $HOST ; done < servers.txt
server1.mydomain.com
server2.mydomain.com
server3.mydomain.com

Sin embargo, cuando quiero ssh a todos los servidores y ejecutar un comando, de repente mi whileciclo deja de funcionar:

$ while read HOST ; do ssh $HOST "uname -a" ; done < servers.txt
Linux server1 2.6.30.4-1 #1 SMP Wed Aug 12 19:55:12 EDT 2009 i686 GNU/Linux

Esto solo se conecta al primer servidor de la lista, no a todos. No entiendo lo que está pasando aquí. ¿Alguien puede explicar?

Esto es aún más extraño, ya que usar el forbucle funciona bien:

$ for HOST in $(cat servers.txt ) ; do ssh $HOST "uname -a" ; done
Linux server1 2.6.30.4-1 #1 SMP Wed Aug 12 19:55:12 EDT 2009 i686 GNU/Linux
Linux server2 2.6.30.4-1 #1 SMP Wed Aug 12 19:55:12 EDT 2009 i686 GNU/Linux
Linux server3 2.6.30.4-1 #1 SMP Wed Aug 12 19:55:12 EDT 2009 i686 GNU/Linux

Debe ser algo específico para ssh, porque otros comandos funcionan bien, como ping:

$ while read HOST ; do ping -c 1 $HOST ; done < servers.txt
Martin Vegter
fuente
44
usoansible
Matt

Respuestas:

98

ssh está leyendo el resto de su entrada estándar.

while read HOST ; do … ; done < servers.txt

readlee de stdin. El <redireccionamiento stdin desde un archivo.

Desafortunadamente, el comando que está intentando ejecutar también lee stdin, por lo que termina comiendo el resto de su archivo. Puedes verlo claramente con:

$ while read HOST ; do echo start $HOST end; cat; done < servers.txt 
start server1.mydomain.com end
server2.mydomain.com
server3.mydomain.com

Observe cómo catcomió (y repitió) las dos líneas restantes. (Si lo hubiera leído como se esperaba, cada línea tendría el "inicio" y el "final" alrededor del host).

¿Por qué forfunciona?

Su forlínea no redirige a stdin. (De hecho, lee todo el contenido del servers.txtarchivo en la memoria antes de la primera iteración). Entonces sshcontinúa leyendo su stdin desde la terminal (o posiblemente nada, dependiendo de cómo se llame su script).

Solución

Al menos en bash, puede haber readusado un descriptor de archivo diferente.

while read -u10 HOST ; do ssh $HOST "uname -a" ; done 10< servers.txt
#          ^^^^                                       ^^

Debería funcionar. 10es solo un número de archivo arbitrario que elegí. 0, 1 y 2 tienen significados definidos y, por lo general, la apertura de archivos comenzará desde el primer número disponible (por lo tanto, 3 será el siguiente). Por lo tanto, 10 es lo suficientemente alto como para mantenerse alejado, pero lo suficientemente bajo como para estar por debajo del límite en algunos proyectiles. Además es un buen número redondo ...

Solución alternativa 1: -n

Como McNisse señala en su respuesta , el cliente OpenSSH tiene una -nopción que le impedirá leer stdin. Esto funciona bien en el caso particular de ssh, pero, por supuesto, otros comandos pueden carecer de esto; las otras soluciones funcionan independientemente de qué comando se está comiendo su stdin.

Solución alternativa 2: segunda redirección

Aparentemente puede (como en, lo probé, funciona en mi versión de Bash al menos ...) hacer una segunda redirección, que se parece a esto:

while read HOST ; do ssh $HOST "uname -a" < /dev/null; done < servers.txt

Puede usar esto con cualquier comando, pero será difícil si realmente desea que la entrada del terminal vaya al comando.

derobert
fuente
1
¿De dónde 10viene el número ?
Martin Vegter
2
@ MartinVegter lo inventé. 0/1/2 son stdin, stdout y stderr. Puede elegir cualquier número que desee: bash le permite ir bastante alto, probablemente hasta el límite del sistema operativo. Otros proyectiles pueden limitarlo a menos ...
derobert
@ MartinVegter que he editado para responder a sus dos comentarios.
derobert
Otra alternativa: exec 3<&0; while read HOST; do ssh $HOST "uname -a" <&3; done <servers.txt; exec 3<&- Esto hace que el descriptor de archivos 3 sea una copia de seguridad del stdin original, luego lo usa para el stdin de ssh, luego cierra el FD de respaldo cuando haya terminado. Esto funciona en cualquier shell compatible con POSIX.
Richard Hansen
8
La -uopción no es compatible con POSIX, y por lo tanto no debe ser utilizado para #!/bin/shlas secuencias de comandos; utilizar read HOST <&10en su lugar. Además, POSIX solo requiere shells para admitir los descriptores de archivo 0 a 9, por 10<servers.txtlo que no se puede usar si el script debe cumplir estrictamente.
Richard Hansen el
28

Como describe derobert sshlee tu stdin.

Para cambiar este comportamiento, puede agregar -n no ssh para evitar que lea stdin.

ssh -n $HOST "uname -a"
McNisse
fuente
10

Probablemente sea mejor usar pssh del proyecto parallel-ssh .

pssh -h $hostfile -t $timeout -i $commands

-isignifica interactivo. pssh también viene con un scp paralelo y rsync paralelo. Lo bueno es que se ejecuta de forma asíncrona y ejecutará tantos subprocesos como usted lo solicite. El valor predeterminado (no i / interactivo) es enviar a directorios separados para stdout / stderr, lo que hace por $ outputdir / $ hostname.

infowolfe
fuente
3

Si te encuentras haciendo este tipo de tarea con bastante frecuencia, deberías probar Fabric

Instale el Fabric siguiendo las instrucciones , la mayoría de los casos solo necesitasudo apt-get install fabric

Cree un archivo fabfile.pycon el siguiente código:

from fabric.api import env, run

env.hosts = ['server1.mydomain.com',
             'server1.mydomain.com',
             'server1.mydomain.com']

def mytask():
    run('uname -a')

Luego ejecutar fab mytaskle dará el resultado que desea.

número 5
fuente
1

Es más fácil usar un comando como este:

for f in `cat servers.txt`; do ssh $f uname -a; done

Por lo general, me gusta esto:

for f in `cat servers.txt`; do echo "### $f ###"; ssh $f uname -a; done

El echoes ver qué servidor está atascado, o no puede conectarse a él.

Ionut Ciucanu
fuente
1

Debido a que un comando ssh y toma todo el flujo de la entrada estándar, alimentado por la instrucción while,

Puede usar una tubería para cambiar el stdin de ssh a otra fuente:

echo "" | ssh ...

ejemplo:

while read HOST ; do echo "" | ssh $HOST "uname -a" ; done < servers.txt

La entrada estándar de todos los comandos ssh en un ciclo while debe cambiarse a otra fuente.

Ali ISSA
fuente