Hey,
it’s not uncommon for me to have to execute a quick command against a set of machines. Naturally, the easiest way of performing that against a single machine is using ssh
:
readonly private_key='./key.rsa'
readonly command='echo test'
readonly ip='10.0.0.2'
readonly user='ubuntu'
readonly port='22'
echo "$command" | ssh \
-i ${private_key} \
-p ${port} \
${user}:${ip}
Having the command properly wrapped in terms of variables now it’s just a matter making it a method, looping through a list of machines and executing the command.
# Executes a given command via SSH
# $1: IP of the machine;
# $2: port of the machine to
# connect to.
# $USER: user to use when executing
# the commands.
# $SCRIPT: global variable with the
# full multiline script to
# execute.
# $PRIVATE_KEY: global variable with the
# full path to a private
# key that is authorized by
# the machines.
execute_script_in_machine () {
local ip=$1
local port=$2
echo "$SCRIPT" | ssh \
-o "StrictHostKeyChecking=no" \
-i "$PRIVATE_KEY" \
-p $port \
$USER@$ip
}
The list of machines might be an array that we just iterate through:
readonly TARGETS=(
"127.0.0.1:2200"
"127.0.0.1:2201"
"127.0.0.1:2202"
)
As we provide the port
argument separated from the ip
argument we must split the strings during the iteration. Thankfully to bash
we are covered by some internal operators. For instance:
#!/bin/bash
readonly TARGETS=(
"firstIP:firstPORT"
"secondIP:secondPORT"
)
# when iterating over an array we must
# get each element from the array accessor
# `VARIABLE[@]` and not only `VARIABLE`.
main () {
local ip
local port
for target in "${TARGETS[@]}"; do
ip=${target%:*}
port=${target#*:}
printf "ip=$ip\tport=$port\n"
done
}
main
leads to the following:
bash test.sh
ip=firstIP port=firstPORT
ip=secondIP port=secondPORT
which essentially means that now we need to plumb these two pieces (the parsing of the target list and the command execution) and have the full working script:
#!/bin/bash
readonly USER="ubuntu"
readonly PRIVATE_KEY="./key.rsa"
readonly SCRIPT='echo -----STARTING;
echo "WhoAmI? $(whoami)";
echo -----DONE;
'
readonly TARGETS=(
"127.0.0.1:2020"
"127.0.0.1:2021"
"127.0.0.1:2022"
)
main () {
local ip
local port
for target in "${TARGETS[@]}"; do
ip=${target%:*}
port=${target#*:}
done
}
execute_script_in_machine () {
local ip=$1
local port=$2
echo "$SCRIPT" | ssh \
-o StrictHostKeyChecking=no \
-i "$PRIVATE_KEY" \
-p $port \
$USER@$ip
}
main
That’s it!
I included an extra section after some feedback. It describes an alternative called mpssh
that does the job of executing commands over SSH across many machines. Make sure you check it out!
Closing thoughts
This is just a quick script that might be handy if you want to inspect the state of something across many machines. I really don’t advise to make use of this as a way of provisioning machines as there are much better solutions out there (like ansible
) which take a more declarative approach, are less distribution-specific and provides the great benefit of idempotence.
Having some bash skills are nice and can really save some time if need to get something done quickly using standard Unix tools. If you’re interested in knowing more about some bash/shell stuff, make sure you subscribe to the mailing list.
I don’t have many resources to share regarding this article but I always end up reading something here and there from the advanced bash-scripting guide on TLDP. I’d not stress too much on a guide as at least in my case most of what I know comes from solving stuff when needed. After a while, you get the gotchas and become good enough to get to the goal.
Have a good one!
finis
Update - mpssh
as an alternative
Hey, a reader pointed out that there are some other very lightweight solutions that do the job very well. The alternative I liked the most is mpssh
.
If you’re a MacOS user and uses brew: brew install mpssh
.
Under Linux, clone the repository ndenev/mpssh and then run make
from the root (assuming you have at least gcc
and make
).
SYNOPSIS
mpssh \
[-besvV] \
[-o directory] \
[-u username] \
[-f hosts] \
[-p procs] \
<command>
The mpssh utility executes multiple parallel ssh binary
instances in order to connect to a list of hosts (specified
in the hosts file) and execute the given <command> on each
of them.
So, first thing to do is create a hosts
file. There you can specify in each line the ip
that you want to connect to. For instance:
echo "10.0.0.1
10.0.0.2
10.0.0.3" > /tmp/hosts
Next step is configuring ~/.ssh/config
: mpssh
seems to now have an easy way of specifying a private key to use so you have to specify an IdentityFile
in the ~/.ssh/config
file. For instance:
Host *
IdentityFile /tmp/key.rsa
With that we can already execute a command across those machines:
mpssh --user ubuntu --nokeychk --file /tmp/hosts 'echo "test"'
MPSSH - Mass Parallel Ssh Ver.1.3.3
(c)2005-2013 Nikolay Denev <ndenev@gmail.com>
[*] read (3) hosts from the list
[*] executing "echo "test"" as user "ubuntu"
[*] strict host key check disabled
[*] spawning 3 parallel ssh sessions
10.0.0.1 -> test
10.0.0.2 -> test
10.0.0.3 -> test
Done. 3 hosts processed.
Aside from just executing the commands in all of them, it can have a ratelimit in terms of parallelization (e.g, given a list of 100 machines, execute in parallel 3 at a time). It’s also possible to output the result of the execution of each machine in a separate file. Pretty good!