Utilisation de while loop pour ssh sur plusieurs serveurs


75

J'ai un fichier servers.txt, avec une liste de serveurs:

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

Quand je lis le fichier ligne par ligne avec whilechaque ligne et que je les répète , tout fonctionne comme prévu. Toutes les lignes sont imprimées.

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

Cependant, lorsque je veux ssh sur tous les serveurs et exécuter une commande, ma whileboucle cesse de fonctionner:

$ 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

Cela se connecte uniquement au premier serveur de la liste, pas à tous. Je ne comprends pas ce qui se passe ici. Quelqu'un peut-il s'il vous plaît expliquer?

C'est encore plus étrange, car utiliser la forboucle fonctionne 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

Cela doit être spécifique ssh, car les autres commandes fonctionnent bien, telles que ping:

$ while read HOST ; do ping -c 1 $HOST ; done < servers.txt

4
utiliseransible
Matt

Réponses:


98

ssh lit le reste de votre entrée standard.

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

readlit de stdin. Les <redirections stdin à partir d'un fichier.

Malheureusement, la commande que vous essayez d'exécuter lit également stdin, elle finit donc par manger le reste de votre fichier. Vous pouvez le voir clairement avec:

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

Remarquez comment ont catmangé (et repris) les deux lignes restantes. (Si lu l'avait fait comme prévu, chaque ligne aurait le "début" et le "fin" autour de l'hôte.)

Pourquoi ça formarche?

Votre forligne ne redirige pas vers stdin. (En fait, il lit tout le contenu du servers.txtfichier en mémoire avant la première itération). Alors sshcontinue à lire son stdin à partir du terminal (ou éventuellement rien, en fonction de la façon dont votre script est appelé).

Solution

Au moins en bash, vous pouvez readutiliser un descripteur de fichier différent.

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

devrait fonctionner. 10est juste un numéro de fichier arbitraire que j'ai choisi. 0, 1 et 2 ont des significations définies et l'ouverture des fichiers commence généralement par le premier nombre disponible (donc 3 est le prochain à être utilisé). 10 est donc assez élevé pour rester à l'écart, mais suffisamment bas pour rester sous la limite dans certains obus. En plus, c'est un joli chiffre rond ...

Solution alternative 1: -n

Comme McNisse l'a souligné dans sa réponse , le client OpenSSH dispose d'une -noption qui l'empêche de lire stdin. Cela fonctionne bien dans le cas particulier de ssh, mais bien sûr, d'autres commandes peuvent ne pas en bénéficier - les autres solutions fonctionnent quelle que soit la commande qui consomme votre stdin.

Solution alternative 2: deuxième redirection

Vous pouvez apparemment (comme dans, je l'ai essayé, cela fonctionne dans ma version de Bash au moins ...) faire une deuxième redirection, qui ressemble à ceci:

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

Vous pouvez l'utiliser avec n'importe quelle commande, mais ce sera difficile si vous voulez réellement que le terminal entre dans la commande.


1
d'où vient le numéro 10?
Martin Vegter

2
@ MartinVegter Je l'ai inventé. 0/1/2 sont stdin, stdout et stderr. Vous pouvez choisir n'importe quel nombre - bash vous permet d'aller assez haut, probablement jusqu'à la limite du système d'exploitation. D'autres obus pourraient vous en limiter ...
derobert

@MartinVegter Je l'ai édité pour répondre à vos deux commentaires.
derobert

Une autre alternative: exec 3<&0; while read HOST; do ssh $HOST "uname -a" <&3; done <servers.txt; exec 3<&- cela fait du descripteur de fichier 3 une sauvegarde du stdin d'origine, puis l'utilise pour le stdin de ssh, puis ferme le FD de sauvegarde une fois l'opération terminée. Cela fonctionne sur n’importe quel shell compatible POSIX.
Richard Hansen

8
Cette -uoption n'est pas supportée par POSIX et ne doit donc pas être utilisée pour les #!/bin/shscripts. utiliser à la read HOST <&10place. De plus, POSIX exige seulement que les shells prennent en charge les descripteurs de fichier compris entre 0 et 9; ils 10<servers.txtne peuvent donc pas être utilisés si le script doit être strictement conforme.
Richard Hansen

28

Comme décrit derobert sshlit votre stdin.

Pour changer ce comportement, vous pouvez ajouter -n no ssh pour l'empêcher de lire stdin.

ssh -n $HOST "uname -a"

10

En fait, il vaudrait probablement mieux utiliser pssh à partir du projet parallel-ssh .

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

-isignifie interactif. pssh est également livré avec un scp parallèle et un rsync parallèle. Ce qui est bien, c’est qu’il fonctionne de manière asynchrone et exécutera autant de threads que vous le lui demandez. La valeur par défaut (non -i / interactive) est la sortie dans des répertoires séparés pour stdout / stderr, ce qu'il fait par $ outputdir / $ nom_hôte.


3

Si vous rencontrez souvent ce genre de tâche, essayez Fabric.

Installez le tissu en suivant les instructions , la plupart des cas dont vous avez besoinsudo apt-get install fabric

Créez un fichier nommé fabfile.pyavec le code suivant:

from fabric.api import env, run

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

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

Ensuite, exécutez fab mytaskvous donnera le résultat que vous voulez.


1

C'est plus facile en utilisant une commande comme celle-ci:

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

Je fais habituellement comme ça:

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

Le echoest de voir quel serveur est bloqué, ou ne peut pas s'y connecter.


1

Parce qu'un commnand ssh prend tout le flux de l'entrée standard, alimenté par l'instruction while

Vous pouvez utiliser un tube pour changer le stdin de ssh en une autre source:

echo "" | ssh ...

exemple :

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

L'entrée stdin de toutes les commandes ssh d'une boucle while doit être commutée sur une autre source.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.