tmux cssh and ansible
Starting a tmux CSSH session based on Ansible inventory.
Ansible is a great automation and orchestration tool.
There are times though, where one needs to cluster ssh into multiple systems to action something.
clusterssh and multi-ssh are two common projects which work well.
I much prefer to use tmux though. Mainly for these reasons:
- I can customise it to work with my key bindings.
- I can easily copy and paste in and out of tmux using the keyboard without any mouse, ever.
Often when one uses your “cssh” choice of tool, one maintains a separate clusterssh list of systems to connect to. This is duplication and is especially annoying when many software engineers each have their own list of servers to connect to and don’t collaborate the list.
In the interest of fixing this I came up with the following solution.
I hacked together the below improved tmux cssh bash script. It basically allows one to start a tmux “CSSH” session based on an Ansible inventory.
Note: This is a modification of Pierre Guinoiseau tmux-cssh project.
It also allows for:
- Host selection via Ansible host pattern matching.
- Host selection by environment, dev, int, preprod, production.
- Further limiting the hosts using regular expressions, again by
using ansible’s
--limitoption.
It solves duplicating your CSSH inventory groups. Now ansible’s inventory group is the source of truth. One place to update the information.
A few examples to demonstrate it’s usefulness.
Launch a cssh session using tmux for all dev webservers:
tssh -e dev webservers
The same but for prod:
tssh -e prod webservers
Now limit it to one datacenter but using regex:
tssh -e prod -l '.*-dc1-' webservers
The above filters all hostnames with -dc1- which is assumed to be
hosts in datacenter 1.
Since all the intelligence is done in ansible you can really use any fancy pattern matching that you can with Ansible. That is well documented.
Here’s the script it it’s current form:
#!/usr/bin/env bash
# by Divan Santana <divan@santanas.co.za>
# Starts a tmux CSSH session based on Ansible inventory.
# TODO: Add support for -e 'all' so can cssh across environments.
########
# docs #
########
# You need the following to make this work:
# - Your search domain must include .x.example.com, as we use DNS to
# resolve names.
# - ansible, tmux installed and in your $PATH
# - Clone ansible code repository[1] somewhere locally
# [1]: https://code.example.com:7990/projects/DEVOPS/repos/ansible
# - Set src_ansible to path of local git ansible repository
# Ideally symlink this script into your $PATH like so:
# ln -s ~/src/work/digital-ansible/extras/tssh ~/.local/bin/tssh
# - run it, like so:
# tssh -e int webserver_hosts
# or
# tssh -e prod -l '~.*-dc1- webserver_hosts
#############
# debugging #
#############
# export DEBUG='true'
# to enable debugging before running this.
[ "$DEBUG" == 'true' ] && set -x
##################
# config options #
##################
# Set this to where you clone the ansible site project.
src_ansible="${HOME}/src/work/digital-ansible"
##############
# help usage #
##############
usage() {
echo "Usage: $0 [options] hostpattern" >&2
echo "" >&2
echo "Starts a tmux CSSH session based on Ansible inventory." >&2
echo "" >&2
echo "Options:" >&2
echo " -h Show help" >&2
echo " -e <environment> Ansible environment" >&2
echo " -l <limit> Further limit selected hosts to an additional pattern" >&2
}
while [ $# -ne 0 ]; do
case $1 in
-e)
shift;
if [ $# -eq 0 ]; then
usage
exit 2
fi
env="$1"; shift
;;
-l)
shift;
if [ $# -eq 0 ]; then
limit=""; shift
fi
limit="$1"; shift
;;
-h)
usage
exit 0
;;
-*)
usage
exit 2
;;
*)
gname=$1; shift
;;
esac
done
if [ -z "${gname}" ]; then
usage
exit 2
fi
######################
# requirements check #
######################
if ! type tmux > /dev/null 2>&1 ; then
echo "tmux not found. Is it installed?" >&2
exit 2
fi
if ! type ansible > /dev/null 2>&1 ; then
echo "ansible not found. Is it installed?" >&2
exit 2
fi
if ! [ -d "${src_ansible}" ] ; then
echo "ansible code base '${src_ansible}' not found." >&2
echo "Set var 'src_ansible' to location of ansible site git checkout." >&2
exit 2
fi
cd "${src_ansible}"
if ! [ -f "inventories/manual_inventory_${env}.ini" ] ; then
echo "File inventories/manual_inventory_${env}.ini not found." >&2
echo "Set environment to either dev, int, pp, lt or prod. " >&2
exit 2
fi
########
# main #
########
_tmux_session_name="tmux-${env}-${gname}"
# trim tmux session name, cut _hosts
tmux_session_name=`echo ${_tmux_session_name}|awk -F'_hosts' '{print $1}'`
_hosts=`ansible -i inventories/manual_inventory_${env}.ini --list-hosts ${gname} -l "${limit}" | sed -e '1,1d'`
# trim whitespace.
hosts="$(echo ${_hosts}|xargs)"
if [ -z "${hosts}" ]; then
exit 1
fi
# Find a name for a new session
n=0; while tmux has-session -t "${tmux_session_name}-${n}" 2>/dev/null; do n=$(($n + 1)); done
tmux_session="${tmux_session_name}-${n}"
# Open a new session and split into new panes for each SSH session
for host in ${hosts}; do
if ! tmux has-session -t "${tmux_session}" 2>/dev/null; then
tmux new-session -s "${tmux_session}" -d "ssh ${ssh_options} ${host}"
else
tmux split-window -t "${tmux_session}" -d "ssh ${ssh_options} ${host}"
# We have to reset the layout after each new pane otherwise the panes
# quickly become too small to spawn any more
tmux select-layout -t "${tmux_session}" tiled
fi
done
# Synchronize panes by default
tmux set-window-option -t "${tmux_session}" synchronize-panes on
if [ -n "${TMUX}" ]; then
# We are in a tmux, just switch to the new session
tmux switch-client -t "${tmux_session}"
else
# We are NOT in a tmux, attach to the new session
tmux attach-session -t "${tmux_session}"
fi
exit 0
The inventory would look something along these lines, with
environments separated per ini file.
$ tree inventories inventories ├── manual_inventory_dev.ini ├── manual_inventory_int.ini ├── manual_inventory_lt.ini ├── manual_inventory_pp.ini └── manual_inventory_prod.ini
