About Articles Projects Software love Feed

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:

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:

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

Comments

Date: 2019-09-13

Made with Emacs 26.3 (Org mode 9.1.9)

Creative Commons License