Everyday I connect to a number of servers, and everyday I send the same command to a list of servers.
Sometimes it is Ubuntu servers, sometimes Debian servers; I needed a way to slice and dice my list of servers, and start a clustered ssh session with the resulting sliced and diced list of servers.
Enter tmux, tmux-cssh, and some custom bash scripting.

So, a little while a go I switched to Windows as my daily desktop OS.
I figured out how to tune Windows Terminal to my liking, but was missing clusterssh from my OSX days.

A friend of mine (Shane Howearth), pointed me (I think he unknowingly pointed me) to tmux.
And in turn I was pointed to tmux-cssh


Very cool that this is written by a Kiwi (New Zealander).

So, tmux-cssh allows me to cluster my ssh sessions. Now I need to figure a way to slice and dice my list of servers.

The Problem

I have some Ubuntu servers, and some Debian servers.
Some of those servers are bare metal, some are not.
Some of those servers are Hyper Visors, some are not.

Sometimes I want to connect to only Debian servers, sometimes I want to connect to only real machines (bare metal installs).
I want the ability to organise my list of servers into some kind of searchable list.

My Solution

Firstly, I don’t know whether this is the most elegant way, in fact, I’m pretty sure it’s not, and I would love to hear people’s suggestions.
Comment below !

For this to work, you must have your ssh sorted.
i.e. you must be able to do:

ssh host1

Here is what I came up with:

The Data

I pictured my list of hosts as a number of rows in a table (or spreadsheet), and my “categories” as row headings.

Kind of like this:

hostname debian_hosts real_hosts hv_hosts ubuntu_hosts mikrotik_hosts blank
host1 y          
host2 y          
host3 y          
host4 y          
host5 y          
host6 y          
host7 y          
host8 y          
host9 y y        
host10 y y        
host11 y y y      
host12   y   y    
host13   y   y    
host14       y    
host15   y   y    
host16         y  
host17         y  
host18         y  
host19         y  


Weirdly, I had to create a blank column at the end.
Otherwise, bash would drop the last column.
So, now bash drops the blank column.

I saved this info into a csv file (input.csv).
The data is now “predictable”. 👍

Selecting from the data

awk to the rescue:

awk -v c1=3 -F , '$c1 == "y" {print "   "$1}' input.csv

Let’s do a quick run down on awk:

  • -v allows me to pass variables to awk.
    • In this case I’m using it to name my columns.
  • -F means fields are separated by the following character.
    • in this case “,” as this is a csv (comma separated values) file.
  • {print $1 } means, print the first column. i.e. the hostname.

Some Bash things I’ll use

So, I love bash.
I think anyone who reads this blog knows that ❗

I’m going to create an array I call “categories” using bash’s internal IFS special shell variable.

IFS=',' read -a categories <<< `head -n 1 input.csv`

I’m also going to use getopt to get variables passed from the command line.
Basically, I want to use pretty short AND long variable names 😊

One interesting thing I’ll note here:
I wanted a “list” function that would list all my hosts, or list the hosts in a specific category.
My getopt options expect a passed argument.
i.e. options=$(getopt -o hdrvumal: --long help,debian,real,virtual,ubuntu,mikrotik,all,list: -- "$@")

So I wrote a wee bit in the script that checks to see if $arg2 is set, and if it’s not, then sets it, and also sets “default” values.

if [ -z "$2" ]; then
    arg1=${1:-"h"}
    arg2=${2:-"all"}
    set -- "${arg1}" "${arg2}"
    unset arg1 arg2
fi

The Script

that’s enough chatter … here’s the script 😌

I’ve stored tmux-cssh in my ~/bin/ directory, and made sure it is executable.

#!/bin/bash

DATA_SRC=input.csv

IFS=',' read -a categories <<< `head -n 1 $DATA_SRC`

ShowHelp() {
    SCRIPT_NAME="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")"
    echo "
$SCRIPT_NAME is a small shell script that opens a tmux window pane to a bunch of hosts.
Then runs a tmux session to simulate ClusterSSH

It can only take one argument at a time.
Usage:
$SCRIPT_NAME [single option]
Options
 -h, --help             Show this help screen
 -d, --debian           Open all Debian hosts
 -r, --real             Open all 'real' bare-metal hosts
 -v, --virtualhost      Open all hypervisors (i.e. virtual servers)
 -u, --ubuntu           Open all Ubuntu hosts
 -m, --mikrotik         Open all Mikrotik hosts
 -a, --all              Open all hosts (excluding Mikrotik Hosts)
 -l, --list             List all hosts (optionally pass a category name)

All hosts are defined in the file $DATA_SRC and are categorised as:
* Debian hosts (debian_hosts).
* Real hosts (real_hosts).
* Hyper Visor hosts (hv_hosts).
* Ubunutu hosts (ubuntu_hosts).
* and Mikrotik hosts (mikrotik_hosts).

A valid csv file with the following headers is required:
  hostname,debian_hosts,real_hosts,hv_hosts,ubuntu_hosts,mikrotik_hosts,blank

Note: a blank column is required at the end
"
    exit 1
}

connect_hosts() {
    COLNUM=$1
    if [[ $COLNUM == 1 ]]; then
        # Column 6 is Mikrotik, let's connect to all NON-mikrotik hosts
        ARRAY=(`awk -F , '$6 != "y" {print $1}' <(tail -n +2 $DATA_SRC)`)
    else
        ARRAY=(`awk -v c1=$COLNUM -F , '$c1 == "y" {print $1}' $DATA_SRC`)
    fi

    # echo -e ${ARRAY[@]}
    clusterSSH ${ARRAY[@]}
}

clusterSSH() {
    h=("$@")
    ~/bin/tmux-cssh ${h[@]}
}

listHosts() {
    echo Categories
	case "$1" in
			all)
    			localCOUNT=1
				;;
			debian_hosts)
    			localCOUNT=2
				;;
			real_hosts)
    			localCOUNT=3
				;;
			hv_hosts)
    			localCOUNT=4
				;;
			ubuntu_hosts)
    			localCOUNT=5
				;;
			mikrotik_hosts)
    			localCOUNT=6
				;;
			*)
	esac
    localARRCOUNT=${#categories[@]}
    if [ $localCOUNT -gt 1 ]; then
        echo " "$1
        awk -v c1=$localCOUNT -F , '$c1 == "y" {print "   "$1}' $DATA_SRC
    else
        for cat in "${categories[@]}"; do
            # Column 7 ( -lt  $localARRCOUNT ) is "blank", let's show all NON-blank categories
            if [[ $localCOUNT -gt 1 && $localCOUNT -lt $localARRCOUNT ]]; then
                echo "  "$cat
                awk -v c1=$localCOUNT -F , '$c1 == "y" {print "   "$1}' $DATA_SRC
            fi
            ((localCOUNT++))
        done
    fi
    #
    # echo All Hosts:
	# awk -F , '$1 != "hostname" { print " "$1 }' $DATA_SRC
    #
}

if [ -z "$2" ]; then
    arg1=${1:-"h"}
    arg2=${2:-"all"}
    set -- "${arg1}" "${arg2}"
    unset arg1 arg2
fi

options=$(getopt -o hdrvumal: --long help,debian,real,virtual,ubuntu,mikrotik,all,list: -- "$@")


eval set -- "$options"

while :
do
	case "$1" in
		-h | --help ) ShowHelp; shift; break ;;
		-d | --debian ) connect_hosts "2"; shift; break ;;
		-r | --real ) connect_hosts "3"; shift; break ;;
		-v | --virtual ) connect_hosts "4"; shift; break ;;
		-u | --ubuntu ) connect_hosts "5"; shift; break ;;
		-m | --mikrotik ) connect_hosts "6"; shift; break ;;
		-a | --all ) connect_hosts "1"; shift; break ;;
		-l | --list ) listHosts "$2"; shift 2; break ;;
		* ) ShowHelp; shift; break;;
        -- ) shift; break ;;
	esac
done

Conclusion

Well, it works 👍

I’m not sure this is the “best” way of achieving what I wanted? I’d love to hear what you think.

1 Comment

Danish Khan

Now that is something man, awesome, I got a new easy insight for sshing into my orgs hosts in this clean manner. I will explore more and let you know if any improvements or enhancment done.

Leave a Comment

Liked what you've read?

  1. Leave me a message, or
  2. Drop me an email

It only takes a second of your time, but it means the world to me.
Don't comment on Facebook, comment here !

I don't keep any of your information, I only ask for a name, and an email to stop spammers!
Pretty please !! :)

Your email address will not be published. Required fields are marked *

You won't be notified of replies, so come back and visit again!

☝️ Top
🏠 Home