multx (MULTiple eXecution) is a shell script that invokes the same command(s) on multiple "target" hosts.
multx can execute these command(s) either sequentially (foreground) or in parallel (background).
multx has only a few command line arguments, so
∫knowledge dt is tiny.
It's useful when you must execute commands on multiple *NIX hosts and you don't have access
to a working configuration management tool that you know how to use.
In order to use multx without having to (interactively) type passphrases or passwords
(except for the initial setup, of course!), the use of two other shell scripts is strongly recommended.
The three scripts are:
Download these
shell_scripts from the github repository.
Note: once ssh-agent is running with the correct configuration, all other commands that are based on ssh
will work without being prompted for authentication as well. For example, scp and rsync.
The Default multx Usage Message
$ multx
usage (version 1.0.1):
multx [option(s)] remote_command
Where options are: 123abch:f:o:qs:x:
-1 :+ hostnames: fred wilma
-2 :+ hostnames: barney betty
-3 :+ hostnames: pebbles bamm-bamm
-a :+ hostnames: fred wilma barney betty pebbles bamm-bamm
-b : background mode
-c : confirmation mode
-h host_list :+ add a hostname or 'hostname_1 ... hostname_n'
-f file :+ read a file containing hostname(s)
-o outfilepat : output file pattern ("'${HOSTNAME}.info'" is a useful choice)
-q : quiet mode
-s ssh_arg(s) :+ arguments to be passed to ssh
-x ERE :+ eXclude all/part of hostname(s) containing this ERE (pattern)
'+' options may be used more than once and/or combined with others
multx Copyright (C) 2016-2019 James S. Crook
This program comes with ABSOLUTELY NO WARRANTY and is distributed under the FreeBSD license.
Preparation: Configuring ssh and ssh-agent
Before one can use multx without typing passphrases or passwords every time (strongly recommended),
ssh must be configured on all the target systems and ssh-agent on at least one initiating system.
See
manage_ssh_agent for details on how to do this.
Simple Examples of Running multx
Note that typing passwords or passphrases is not required in any of the examples below.
Also, it would be just as easy to run remote commands on 100 target hosts as on a few.
- Running hostname on set 1 of hosts (foreground):
$ multx -1 hostname
Sat Feb 13 15:37:08 AEDT 2016 : multx starting
fred === <hostname> ===
fred
wilma === <hostname> ===
wilma
Sat Feb 13 15:37:17 AEDT 2016 : multx finished
- Running hostname on set 1 of hosts (background):
$ multx -1b hostname
Sat Feb 13 15:38:09 AEDT 2016 : multx starting
fred === <hostname> ===
wilma === <hostname> ===
wilma
fred
Sat Feb 13 15:38:09 AEDT 2016 : multx finished
Note that the order of the output is non-deterministic when using the background option!
- Creating a file with date on all hosts (background):
$ multx -ab 'date > /tmp/junk'
Sat Feb 13 15:42:46 AEDT 2016 : multx starting
fred === <date > /tmp/junk> ===
wilma === <date > /tmp/junk> ===
barney === <date > /tmp/junk> ===
betty === <date > /tmp/junk> ===
pebbles === <date > /tmp/junk> ===
bamm-bamm === <date > /tmp/junk> ===
Sat Feb 13 15:42:48 AEDT 2016 : multx finished
- Using sed to "edit" a file [create a modified new file] (background)"
$ multx -ab 'sed s/2016/1492/ /tmp/junk > /tmp/rubbish'
Sat Feb 13 15:45:41 AEDT 2016 : multx starting
fred === <sed s/2016/1492/ /tmp/junk > /tmp/rubbish> ===
wilma === <sed s/2016/1492/ /tmp/junk > /tmp/rubbish> ===
barney === <sed s/2016/1492/ /tmp/junk > /tmp/rubbish> ===
betty === <sed s/2016/1492/ /tmp/junk > /tmp/rubbish> ===
pebbles === <sed s/2016/1492/ /tmp/junk > /tmp/rubbish> ===
bamm-bamm === <sed s/2016/1492/ /tmp/junk > /tmp/rubbish> ===
Sat Feb 13 15:45:42 AEDT 2016 : multx finished
- Listing the contents of a file with cat (foreground):
$ multx -a 'cat /tmp/rubbish'
Sat Feb 13 15:45:59 AEDT 2016 : multx starting
fred === <cat /tmp/rubbish> ===
Sat Feb 13 12:42:47 AWST 1492
wilma === <cat /tmp/rubbish> ===
Sat Feb 13 12:42:47 AWST 1492
barney === <cat /tmp/rubbish> ===
Sat Feb 13 12:42:47 AWST 1492
betty === <cat /tmp/rubbish> ===
Sat Feb 13 12:42:47 AWST 1492
pebbles === <cat /tmp/rubbish> ===
Sat Feb 13 12:42:47 AWST 1492
bamm-bamm === <cat /tmp/rubbish> ===
Sat Feb 13 00:46:21 AWST 1492
Sat Feb 13 15:46:04 AEDT 2016 : multx finished
- Checking if all hosts have the same version of a file (background, quiet mode):
$ multx -abq 'echo $(cksum /etc/sysconfig/postfix) $(hostname) $(uname)' | sort
409984396 9662 /etc/sysconfig/postfix barney Linux
409984396 9662 /etc/sysconfig/postfix betty Linux
409984396 9662 /etc/sysconfig/postfix fred Linux
409984396 9662 /etc/sysconfig/postfix wilma Linux
452371448 14776 /etc/sysconfig/postfix bamm-bamm Linux
452371448 14776 /etc/sysconfig/postfix pebbles Linux
Sat Feb 13 15:43:06 AEDT 2016 : multx starting
Sat Feb 13 15:43:08 AEDT 2016 : multx finished
- Checking if all hosts have the correct date (background, quiet mode):
$ multx -abq 'echo $(date) $(hostname)' | sort
Sat Feb 13 17:00:16 AWST 2016 bamm-bamm
Sun Feb 14 04:56:41 AWST 2016 barney
Sun Feb 14 04:56:41 AWST 2016 betty
Sun Feb 14 04:56:41 AWST 2016 fred
Sun Feb 14 04:56:41 AWST 2016 pebbles
Sun Feb 14 04:56:41 AWST 2016 wilma
Sun Feb 14 07:56:40 AEDT 2016 : multx starting
Sun Feb 14 07:56:42 AEDT 2016 : multx finished
- Displaying a particular line of interest from a particular file (background, quiet mode):
$ multx -abq 'echo $(hostname) : $(grep ^users: /etc/group)' | sort
bamm-bamm : users:x:100:mux
barney : users:x:100:lux,mux
betty : users:x:100:mux
fred : users:x:100:tux,lux,mux
pebbles : users:x:100:lux,mux
wilma : users:x:100:lux,mux
Sun Feb 14 07:55:06 AEDT 2016 : multx starting
Sun Feb 14 07:55:08 AEDT 2016 : multx finished
- Running commands as root (with sudo) on the target host(s). In this example,
'hostname' is run as your user ID and calling sudo runs 'id' as root (background, quiet mode).
Double quotes are particularly useful here:
$ multx -1bq 'printf "%-10s: %s\n" $(hostname) "$(sudo id)"' | sort
fred : uid=0(root) gid=0(root) groups=0(root),2000(nfsuser)
wilma : uid=0(root) gid=0(root) groups=0(root),2000(nfsuser),2001(foouser)
Sun Feb 14 21:35:18 AEDT 2016 : multx starting
Sun Feb 14 21:35:19 AEDT 2016 : multx finished
If sudo on the target(s) fails with a sudo tty error, for example:
$ multx -1q 'sudo id'
Tue Apr 4 10:46:05 AEST 2017 : multx starting
sudo: sorry, you must have a tty to run sudo
sudo: sorry, you must have a tty to run sudo
Tue Apr 4 10:46:07 AEST 2017 : multx finished
Tell multx to pass ssh the option(s) it needs to allow this access using '-s option(s)'.
Typically, "-s -t" or "-s -tt"):
$ multx -s -t -1q 'sudo id'
Tue Apr 4 10:46:16 AEST 2017 : multx starting
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
Connection to fred closed.
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
Connection to wilma closed.
Tue Apr 4 10:46:18 AEST 2017 : multx finished
Of course one can do much more interesting things than just 'id'. One can do literally
anything. For example, stop/start services, or even stop/reboot the remote host(s)!
Specifying the multx background option ("-b") may require "-s -tt":
$ multx -s -tt -1bq sudo systemctl stop dse.service
Tue Apr 4 10:46:16 AEST 2017 : multx starting
Connection to fred closed.
Connection to wilma closed.
Tue Apr 4 10:46:18 AEST 2017 : multx finished
- Adding and/or deleting particular host(s):
$ multx -12bq -h pebbles -x barney -x wilma hostname
- or, equivalently:
$ multx -12bq -h pebbles -x 'barney|wilma' hostname
Mon Mar 7 08:04:25 AEDT 2016 : multx starting
fred
betty
pebbles
Mon Mar 7 08:04:25 AEDT 2016 : multx starting
Hopefully, the examples above convey the basic idea. Of course, multx can do many more
things than the simple examples above!
Rolling Out a New File
- Copy the new file (newfile) to all "target" hosts. This can be done in two ways:
- If all the hosts are configured to be both "initiators" and "targets",
(recommended, if practical) use this method. Call multx on the "original initiator"
to execute scp on all the "target" hosts. This will copy newfile from the "original
initiator" to newfile.tmp on all the target hosts (including to itself):
$ multx -abq '. .ssh/ssh-agent; scp -p initiator:newfile newfile.tmp
- If there is only one "initiator", one would have to copy to each "target" host
in turn:
$ scp -p newfile target_1:newfile.tmp
...
$ scp -p newfile target_N:newfile.tmp
One quick and easy way to do this would be with a script like this:
INITIATORNAME=$1; TARGETNAME=$2; shift; shift
for HOST in $*; do
scp -p $INITIATORNAME $HOST:$TARGETNAME & # Launch in background for asynchronous copies
done
wait
And then execute that script:
$ script newfile newfile.tmp target_1 target_2 ... target_N
Launching each scp in the background means all the copies happen asynchronously.
And, of course, no passwords or passphrases would be required.
- Make a backup of the original file (optional):
$ multx -abq 'cp -p /usr/local/bin/file /usr/local/bin/file.save'
-
Install the new file:
$ multx -abq 'cp newfile.tmp /usr/local/bin/file'
-
After a suitable period of time... Cleanup (optional):
$ multx -abq 'rm newfile.tmp /usr/local/bin/file.save'
Change the Same File on Multiple Hosts
- Create a new version of the file in every host:
On set 1 of hosts:
$ multx -1bq 'sed "/^ *- seeds: *".*"$/s/\".*\"/\"1.1.1.1,2.2.2.2,3.3.3.3\"/"
/etc/dse/cassandra/cassandra.yaml > /tmp/new.yaml'
On set 2 of hosts:
$ multx -2bq 'sed "/^ *- seeds: *".*"$/s/\".*\"/\"4.4.4.4,5.5.5.5,6.6.6.6\"/"
/etc/dse/cassandra/cassandra.yaml > /tmp/new.yaml'
- Check the new version on both sets hosts (optional, but strongly recommended):
$ multx -12 'diff /etc/dse/cassandra/cassandra.yaml /tmp/new.yaml'
or, alternately:
$ multx -12qb 'echo $(diff /etc/dse/cassandra/cassandra.yaml /tmp/new.yaml) $(hostname)'
| sort
- Update the configuration file with the modified version on both sets of hosts:
$ multx -12bq 'cp /tmp/new.yaml /etc/dse/cassandra/cassandra.yaml'
The only limit is your imagination!
Store Some Info from Each Target Host in a Named File on the Initiating Host
There are (as of version 0.1.0) at least two ways to do this. First, the easy way, using the
'-o outfilepat' method. Note that one must prevent the initiating system's shell from attempting to
evaluate ${HOSTNAME} by 'escaping' the $. This can be done by preceding the $ with a \
(\${HOSTNAME}) or by surrounding it with
single quotes ('${HOSTNAME}').
-
$ multx -1qb -o '${HOSTNAME}.info' 'rpm -qa | sort'
-
If all the hosts are configured to be both "initiators" and "targets" and ssh-agent is running on
all systems with a valid passphrase (see above), one could do the same thing 'old school' (the
hard way):
$ multx -1bq 'rpm -qa | sort > tmp/junk; . .ssh/ssh-agent.env; scp /tmp/junk fred:$(hostname).info; rm /tmp/junk'
Both of these commands should produce files like the ones below on the "initiating" host
(which is fred in the second example)
- each of which contains the information requested. In this example, the information requested is
a sorted list of all the packages installed on each of the target hosts.
3ddiag-0.742-32.25
a2ps-4.13-1326.37.1
aaa_base-11-6.99.100.1
acl-2.2.47-30.36.1
...
Customizing multx to Your Environment(s)
Remember:
multx is a template to be edited as required.
If, on the off chance, your hostnames are not Flintstones character names, but (say) Star Trek,
edit multx accordingly.
The required changes are obvious, and must be made in 4 places. In the Flintstones to Star Trek example:
- In the HOST_GROUP section:
- Change '-1' to '-C' (for Captains).
- Change 'HOST_GROUP_1="..."' to 'HOST_GROUP_C="kirk picard sisko janeway archer"'.
- Change '-2' to '-F' (for First officers).
- Change 'HOST_GROUP_2="..."' to 'HOST_GROUP_F="spock riker nerys chakotay tpol worf"'.
- Delete '-3' if it's not required.
- Change 'OPTIONS=123abch:f:o:qx:' to 'OPTIONS='CFabch:f:o:qx:'
- In the getopts case statement, change '1)' to 'C)', '2)' to 'F)' and delete the line with '3)'
- In the usage message, change '-1' to '-C', '-2' to '-C' and delete the '-3' line.
Using multx with -f hostnames_file
If you can't be bothered changing multx, it's possible to use it with the '-f file'
option (here specified twice). These must be files containing the relevant hostnames.
This example collects the packages installed on all the hosts in DC_1 and DC_2 (assuming these
two files exist and have valid contents, of course):
$
multx -f DC_1_hostnames -f DC_2_hostnames -bq -o '$HOSTNAME.packages' 'rpm -qa | sort'
Notes
- multx takes your commands and runs them on multiple hosts. If your commands
do bad things, multx just causes those bad things to happen on multiple systems.
I take no responsibility whatsoever for any damage you cause
when running your commands using multx!!!
- As with any parallel tool, be careful. If you make a mistake, you make it N times!
- Everything on this web page assumes one is running bash, sh, or ksh. If you're still running
csh after all these years, there's no hope for you!
- With the '-o outfilepat' option, '$H' works exactly like '$HOSTNAME'. If curly braces would be
required by a shell, multx also requires them. E.g., '$HOSTNAME.info' does not require them,
but '${HOSTNAME}_info' does.
- I've never had more than 20 hosts to test them all logging in to the "original initiator"
simultaneously. I recall that I had to increase MaxStartups above the (then) default of 10 in
/etc/ssh/sshd_config.
- To permit root to login using ssh, set PermitRoot to yes in /etc/ssh/sshd_config.
- Other tools such as pssh or clusterssh may also be useful. multx has always done
everything I needed to do.
- In background (asynchronous) mode - where commands are running on lots of hosts at the same
time - and the commands are generating output, it's possible that the output from some
host(s) could become interspersed with the output from other(s). The only time this
has ever been observed in the wild is when multiple output-producing commands are issued in
the same multx call. For example, multx -abq 'command_1; command_2'. If this is a
small problem, remove the '-b' option. If it's a big problem, let me know and I'll investigate.
- multx (& co) also runs on Cygwin. It has not been tested on *BSD.