For loop through servers with custom ports (for i in "user1@server1 -p 12345" "user2@server2 -p 54321" …; do)
I’m trying to run a command on a series of servers via ssh. I have recently changed ssh ports to avoid Internet scanners, but it broke my script. Does anyone know "an easy way" to specify different ports per user@server entry in a for loop?
for i in 'user1@server1.domain1.com -p 12345' 'user2@server2.domain2.com -p 54321'
do printf "e[%sm%se[00mn" 32 $i
ssh $i crontab -l
done
The second line is just to print the server name before the output (for readability).
Instead of happiness it gave me: ssh: Could not resolve hostname server1.domain1.com -p 12345: Name or service not known
It worked before when all ports were the default 22 and didn’t need to be specified.
For a for
loop that can loop over more than one variable, switch to zsh
. You’re already using zsh
syntax by not quoting your variables:
# zsh
for colour host port (
green user1@server1.domain1.com 12345
blue user2@server2.domain2.com 54321
) {
print -rP "%F{$colour}%B$host:$port%b%f"
ssh -p $port $host 'crontab -l'
}
To loop over lists of arguments, you could use a shell with multidimensional arrays such as ksh93
:
# ksh93
ssh_args=(
(-p 12345 user1@server1.domain1.com)
(-p 54321 user2@server2.domain2.com)
)
for i in "${!ssh_args[@]}"}; do
printf 'e[1;32m%se[mn' "${ssh_args[i][2]}"
ssh "${ssh_args[i][@]}" 'crontab -l'
done
Or have the elements of the array containing strings with the arguments concatenated using a character that you know doesn’t occur in the strings and split them. zsh
does have a ${(s[x])var}
splitting character.
ksh and bash also do but it’s only via the clunky split+glob one that is invoked implicitly when you forget to quote an expansion there. You could use it as:
# ksh93 / bash / yash / mksh / zsh --emulate ksh
ssh_args=(
-p:12345:user1@server1.domain1.com
-p:54321:user2@server2.domain2.com
)
IFS=:; set -o noglob # tune your split+glob operator by specifying
# the separator and disabling globbing
for joined_args in "${ssh_args[@]}"; do
args=( $joined_args ) # invoke split+glob
printf 'e[1;32m%se[mn' "${args[2]}"
ssh "${args[@]}" 'crontab -l'
done
That’s basically what you did in your approach except you used space instead of :
to separate the elements, but didn’t set $IFS
and didn’t disable glob.
The fact that your $i
wasn’t split could be explained either by $IFS
being set to the empty string, or the code being run by zsh instead of bash, where neither splitting nor globbing is done implicitly upon parameter expansion.
In zsh, if you want IFS-splitting, use $=var
instead of $var
, and if you want globbing: $~var
. bash’s $var
is like zsh’s $=~var
. So in zsh, you’d need:
# zsh
ssh_args=(
-p:12345:user1@server1.domain1.com
-p:54321:user2@server2.domain2.com
)
IFS=:
for joined_args in $ssh_args; do
args=( $=joined_args ) # split on $IFS or better:
args=( ${(s[:])joined_args} ) # split explicitly on : without
# having to touch $IFS
print -rP "%F{green}%B$host[-1]%b%f"
ssh $args 'crontab -l'
done
A portable approach that would work in sh
would be to write it:
# sh / bash / dash / ksh / yash / zsh...
while IFS=' ' read<&3 host port; do
{
printf '33[1;32m%se[mn' "$host:$port"
ssh -p "$port" "$host" 'crontab -l'
} 3<&-
done 3<< 'EOF'
user1@server1.domain1.com 12345
user2@server2.domain2.com 54321
EOF
Omitting the -r
option to read
allows the values to contain the separator (here space) by prefixing them with a backslash like in:
john doe@server1 1234
But contrary to the other approaches, it doesn’t allow those values to contain newline characters (likely not a problem for user, host, service names or port numbers).
Another portable one to loop over lists all of which have the same $n
number of elements and that doesn’t have that limitation is to store all the elements in the positional parameters and loop over them in a while [ "$#" -ge "$n" ]; do something with "$1" "$2"...; shift "$n"; done
loop. So here:
# sh / bash / dash / ksh / yash / zsh...
eval "$(
printf "fg_%s='33[3%sm' "
black 0 red 1 green 2 yellow 3
blue 4 magenta 5 cyan 6 white 7
printf "bg_%s='33[4%sm' "
black 0 red 1 green 2 yellow 3
blue 4 magenta 5 cyan 6 white 7
printf "attr_%s='33[%sm' "
reset '' bold 1 dim 2 italics 3 underline 4
blink 5 standout 7 reverse 7 secure 8
)"
set --
"$fg_green$attr_bold" user1@server1.domain1.com 12345
"$bg_blue$fg_white" user2@server2.domain2.com 54321
while [ "$#" -ge 3 ]; do
colour=$1 host=$2 port=$3
printf '%sn' "$colour$host:$port$attr_reset"
ssh -p "$port" "$host" 'crontab -l'
shift 3
done
Since version 7.7, OpenSSH’s ssh
(along with its scp
and sftp
) will accept the destination in the form of a URI. You can specify the port number as part of the URI if you like, for example "ssh://someuser@somehost:42" to connect to port 42. A URI has the advantage of being a single string, as far as the shell is concerned. So you can do this:
for i in 'ssh://user1@server1.domain1.com:12345' 'ssh://user2@server2.domain2.com:54321'
do printf ...
ssh "$i" ...
done
The general form of an SSH URI is "ssh://user@host:port". The "user" and "port" parts are both optional and can be left out (along with the "@" or ":" punctuation) if you don’t need to specify values for those.
The right way would be to store that on config, rather than specifying everything on every connection.
On ~/.ssh/config
you could have:
Host server1.domain1.com # When connecting to this
User user1 # Use this user
Port 12345 # with this port
Host serv2 # We can also use a different, internal, name
Hostname server1.domain1.com # The actual hostname or IP it will use to connect
User user2
Port 54321
Then ssh server1.domain1.com
and ssh serv2
would do the Right Thing™ No need to remember the usernames or port you used for each machine. Benefiting both scripts and humans.
And should you later change the ports, there’s a single place to modify.
It is also possible to use wildcards, so if all machines on domain1.com
had its sshd listening on port 1234, you could match all of them with Host *.domain1.com
, without having to list them individually.
A general solution for looping through two lists, using a pair of elements (one from either list) in each iteration of the loop, in a POSIX shell:
#!/bin/sh
# The list of port numbers
set -- 123245 54321
# Loop over the list of user@host strings
for remote in
user1@server1.domain1.com
user2@server2.domain2.com
do
# Use "$remote" together with the port number in "$1", then shift
printf 'Connecting to %s, port %sn' "$remote" "$1"
ssh -p "$1" "$remote" crontab -l
shift
done
Use pdsh for multiple hosts.. It is widely used in data centers with large number of clustered hosts..
From pdsh documentation ..
Run with user ‘foo’ on hosts h0,h1,h2, and user ‘bar’ on hosts h3,h5:
pdsh -w foo@h[0-2],bar@h[3,5]
https://code.google.com/archive/p/pdsh/wikis/UsingPDSH.wiki
This tool along side with ssh config file as Ángel mentioned.
Full example below ..
~/.ssh/config
Host server1
Hostname server1
User user1
Port 12345
Host server2
Hostname server2
User user2
Port 54321
Now simple run
pdsh -w server1,server2 crontab -l