Verwenden der while-Schleife zum Senden an mehrere Server

75

Ich habe eine Datei servers.txtmit einer Liste von Servern:

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

wenn ich die datei zeilenweise mit lese whileund jede zeile wiederhole, funktioniert alles wie erwartet. Alle Zeilen werden gedruckt.

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

Wenn ich jedoch zu allen Servern sshen und einen Befehl ausführen möchte, whilefunktioniert meine Schleife plötzlich nicht mehr:

$ 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

Dies stellt nur eine Verbindung zum ersten Server in der Liste her, nicht zu allen. Ich verstehe nicht, was hier passiert. Kann jemand bitte erklären?

Dies ist noch seltsamer, da die Verwendung der forSchleife gut funktioniert:

$ 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

Es muss etwas Besonderes sein ssh, da andere Befehle gut funktionieren, wie zum Beispiel ping:

$ while read HOST ; do ping -c 1 $HOST ; done < servers.txt
Martin Vegter
quelle
4
Verwenden Sieansible
Matt

Antworten:

98

ssh liest den Rest Ihrer Standardeingabe.

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

readliest aus stdin. Das <leitet stdin von einer Datei um.

Leider lautet der Befehl, den Sie ausführen möchten, auch "stdin", sodass der Rest Ihrer Datei verschlungen wird. Sie sehen es deutlich mit:

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

Beachten Sie, wie catdie restlichen zwei Zeilen gegessen (und wiedergegeben) wurden. (Hätte man es wie erwartet gelesen, würde jede Zeile das "Start" und "Ende" um den Host haben.)

Warum forarbeitet?

Ihre forLeitung leitet nicht zu stdin um. (Tatsächlich liest es den gesamten Inhalt der servers.txtDatei vor der ersten Iteration in den Speicher.) Liest also sshweiterhin seinen stdin vom Terminal (oder möglicherweise nichts, je nachdem, wie Ihr Skript aufgerufen wird).

Lösung

Zumindest in Bash können Sie readeinen anderen Dateideskriptor verwenden.

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

sollte funktionieren. 10ist nur eine beliebige Dateinummer, die ich ausgewählt habe. 0, 1 und 2 haben definierte Bedeutungen, und das Öffnen von Dateien beginnt normalerweise mit der ersten verfügbaren Nummer (daher wird als Nächstes 3 verwendet). 10 ist also hoch genug, um aus dem Weg zu gehen, aber niedrig genug, um in einigen Shells unter der Grenze zu sein. Plus es ist eine schöne runde Zahl ...

Alternative Lösung 1: -n

Wie McNisse in seiner Antwort betont , hat der OpenSSH-Client eine -nOption, die verhindert, dass er Standardinhalte liest. Dies funktioniert in bestimmten Fällen gut ssh, aber natürlich kann dies bei anderen Befehlen fehlen - die anderen Lösungen funktionieren, unabhängig davon, welcher Befehl Ihren Standardwert aufnimmt.

Alternative Lösung 2: Zweite Umleitung

Sie können anscheinend (wie in, ich habe es versucht, es funktioniert zumindest in meiner Version von Bash ...) eine zweite Umleitung durchführen, die ungefähr so ​​aussieht:

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

Sie können dies mit jedem Befehl verwenden, aber es wird schwierig, wenn Sie tatsächlich eine Terminal-Eingabe für den Befehl wünschen.

derobert
quelle
1
woher kommt die 10nummer
Martin Vegter
2
@MartinVegter Ich habe es erfunden. 0/1/2 sind stdin, stdout und stderr. Sie können eine beliebige Zahl auswählen - mit bash können Sie ziemlich hoch hinaus, wahrscheinlich bis zum OS-Limit. Andere Muscheln können Sie auf weniger beschränken ...
Derobert
@MartinVegter Ich habe bearbeitet, um Ihre beiden Kommentare zu beantworten.
Derobert
Eine weitere Alternative: exec 3<&0; while read HOST; do ssh $HOST "uname -a" <&3; done <servers.txt; exec 3<&- Dadurch wird aus dem Dateideskriptor 3 eine Sicherungskopie der ursprünglichen Standard-ID erstellt, die dann für die Standard-ID von ssh verwendet wird. Anschließend wird die FD-Sicherungskopie geschlossen, wenn Sie fertig sind. Dies funktioniert auf jeder POSIX-kompatiblen Shell.
Richard Hansen
8
Die -uOption wird von POSIX nicht unterstützt und sollte daher nicht für #!/bin/shSkripte verwendet werden. Verwenden Sie read HOST <&10stattdessen. Außerdem benötigt POSIX nur Shells, um die Dateideskriptoren 0 bis 9 zu unterstützen, und 10<servers.txtkann daher nicht verwendet werden, wenn das Skript streng konform sein soll.
Richard Hansen
28

Wie derobert beschreibt sshliest dein stdin.

Um dieses Verhalten zu ändern, können Sie -n no ssh hinzufügen, um zu verhindern, dass stdin gelesen wird.

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

Sie wären wahrscheinlich besser dran, wenn Sie pssh aus dem parallel-ssh- Projekt verwenden.

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

-ibedeutet interaktiv. pssh kommt auch mit einem parallelen scp und einem parallelen rsync. Das Schöne ist, dass es asynchron läuft und so viele Threads ausführt, wie Sie es verlangen. Die Standardeinstellung (nicht -i / interactive) ist die Ausgabe in separate Verzeichnisse für stdout / stderr. Dies erfolgt über $ outputdir / $ hostname.

infowolfe
quelle
3

Wenn Sie diese Art von Aufgabe häufig erledigen, sollten Sie Fabric ausprobieren

Installieren Sie den Stoff gemäß den Anweisungen , die Sie in den meisten Fällen nur benötigensudo apt-get install fabric

Erstellen Sie eine Datei fabfile.pymit folgendem Namen :

from fabric.api import env, run

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

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

Dann führen fab mytaskSie das gewünschte Ergebnis aus.

Nummer 5
quelle
1

Es ist einfacher, einen Befehl wie den folgenden zu verwenden:

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

Normalerweise mag ich das:

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

Das echoist, um zu sehen, welcher Server feststeckt oder keine Verbindung herstellen kann.

Ionut Ciucanu
quelle
1

Weil ein ssh-Befehl den gesamten Stream von der Standardeingabe nimmt, gespeist von der while-Anweisung,

Sie können eine Pipe verwenden, um das stdin von ssh auf eine andere Quelle umzuschalten:

echo "" | ssh ...

Beispiel:

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

Die Standardeingabe aller ssh-Befehle in einer while-Schleife muss auf eine andere Quelle umgeschaltet werden.

Ali IVSS
quelle