#!/usr/local/bin/bash
#
MUSSH_VERSION="0.5"
#
# Description:  This script is used to execute the same command(s) on
#               many hosts.
#
# by doughnut
#




########################
# INITAILIZE THIS CRAP #
########################
DEBUG=0
SSH_VERBOSE="-q"
QUIET=0
FORCE_AGENT=0
UNIQUE_HOSTS=1
PROXY_SSH_ARGS="-o PasswordAuthentication=no"
SSH_ARGS="-o BatchMode=yes"
AGENT_LOADED=0

############################
# GOTTA LOVE DOCUMENTATION #
############################
USAGE="Usage: mussh [OPTIONS] <-h host.. | -H hostfile> [-c cmd] [-C scriptfile]
mussh --help        for full help text"
HELPTEXT="
Send a command or list of commands to multiple hosts.

OPTIONS:
        --help          This text.
        -d[n]           Verbose debug.  Prints each action, all hosts
                        and commands to be executed to STDERR.  'n' can
                        be from 0 to 2.
        -v[n]           Ssh debug levels.  Can be from 0 to 3.
        -q              No output unless necessary. 
        -i <identity> [identity ..]
                        Load an identity file.  May be used
                        more than once.
        -o <ssh-args>   Args to pass to ssh with -o option.
        -a              Force loading ssh-agent.
        -A              Do NOT load ssh-agent.
        -u              Unique.  Eliminate duplicate hosts (default).
        -U              Do NOT make host list unique.
        -P              Do NOT fall back to passwords on any host.  This will
                        skip hosts where keys fail.
	-l <login>      Use 'login' when no other is specified with hostname.
	-L <login>      Force use of 'login' name on all hosts.
        -V              Print version info and exit.
PROXY ARGS:
        -p [user@]<host>
                        Host to use as proxy.  (Must have mussh installed)
        -po <ssh-args>        Args to pass to ssh on proxy with -o option.
HOST ARGS:
        -h [user@]<host> [[user@]<host> ..]
                        Add a host to list of hosts.  May be
                        used more than once.
        -H <file> [file ..]
                        Add contents of file(s) to list of hosts.
                        Files should have one host per line.  Use
                        \"#\" for comments.
COMMAND ARGS:
If neither is specified, commands will be read from standard input.
        -c <command>    Add a command or quoted list of commands and
                        args to list of commands to be executed on
                        each host.  May be used more than once.
        -C <file> [file ..]
                        Add file contents to list of commands to be
                        executed on each host.  May be used more
                        than once.

At least one host is required.  Arguments are in no particular order.

EXAMPLES:
mussh -H ./linuxhosts -C spfiles/testscript.sh
mussh -c \"cat /etc/hosts\" -h myhost.mydomain.com

Comments and Bug Reports: doughnut_@users.sourceforge.net
"

###########################
# FUNCTIONS FOR SSH-AGENT #
###########################
load_keys() {
        [ "$AGENT_LOADED" = "0" -a "$IDENT" = "" ] && return
        [ "$DEBUG" -ge 1 ] && echo "DEBUG: Adding Keys" 1>&2
        ssh-add $* 1>&2
}

start_agent() {
        [ "$DEBUG" -ge 1 ] && echo "DEBUG: Starting Agent" 1>&2
        if [ "$FORCE_AGENT" -ge 1 ] ; then
                # Load it anyway
                [ "$DEBUG" -ge 1 ] && echo "DEBUG: Forcing SSH Agent"
        elif [ -S "$SSH_AUTH_SOCK" ] ; then
                [ "$DEBUG" -ge 1 ] && echo "DEBUG: SSH Agent already loaded"
                return
        fi
        eval `ssh-agent` >/dev/null
        AGENT_LOADED=1
}
stop_agent() {
        # get rid of the keys that we added (if any)
        if [ "$IDENT" != "" ] ; then
                [ "$DEBUG" -ge 1 ] && echo "DEBUG: Removing keys from agent"
                ssh-add -d $IDENT > /dev/null 2>&1
        fi
        [ "$AGENT_LOADED" = "0" ] && exit
        [ "$DEBUG" -ge 1 ] && echo "DEBUG: Stopping Agent" 1>&2
	eval `ssh-agent -k` >/dev/null
        exit
}

########################################
# REMEMBER TO CLEAN UP BEFORE WE PANIC #
########################################
trap stop_agent SIGINT
trap stop_agent SIGTERM

#############################
# PARSE THE COMMAND OPTIONS #
#############################
while [ "$1" != "" ]; do
        case "$1" in
        ###########
        # OPTIONS #
        ###########
          -A)
                NO_AGENT=1
                shift
                  ;;
          -a)
                FORCE_AGENT=1
                shift
                ;;
          -q)
                QUIET=1
                DEBUG=0
                SSH_VERBOSE="-q"
                shift
                ;;
          -o)
                SSH_ARGS="$SSH_ARGS -o $2"
                shift 2
                ;;
          -u)
                UNIQUE_HOSTS=1
                shift
                ;;
          -U)
                UNIQUE_HOSTS=0
                shift
                ;;
          -l)
                DEFAULT_LOGIN=$2
                shift 2
                ;;
          -L)
                FORCE_LOGIN=$2
                shift 2
                ;;
          -d|-d[0-5])
                DEBUG="${1#-d}"
                DEBUG="${DEBUG:=1}"
                shift
                  ;;
          -v|-v[0-3])
                TMP_ARG="${1#-v}"
                TMP_ARG="${DEBUG:=1}"
                SSH_VERBOSE="-v"
                [ "$TMP_ARG" -ge 2 ] && SSH_VERBOSE="$SSH_VERBOSE -v"
                [ "$TMP_ARG" -ge 3 ] && SSH_VERBOSE="$SSH_VERBOSE -v"
                shift
                  ;;
          -V)
                echo "Version: $MUSSH_VERSION"
                exit
                  ;;
          -P)
                SSH_ARGS="$SSH_ARGS -o PasswordAuthentication=no"
                shift
                  ;;
          --help)
                # print help text
                echo "$USAGE"
                echo "$HELPTEXT"
                exit 
                ;;
          -i)
                # Load the identity file in ssh-agent 
                  while [ "$2" != "" -a "${2#-}" = "$2" ] ; do
                        IDENT="$IDENT $2"
                        shift
                done
                shift
                ;;
        ##############
        # PROXY ARGS #
        ##############
          -p)
                PROXY=$2
                SSH_ARGS="$SSH_ARGS -o ForwardAgent=yes"
                shift 2
                  ;;
          -po)
                PROXY_SSH_ARGS="$PROXY_SSH_ARGS -o $2"
                shift 2
                  ;;
        #############
        # HOST ARGS #
        #############
          -h)
                while [ "$2" != "" -a "${2#-?*}" = "$2" ] ; do
                          HOSTLIST="$2
$HOSTLIST"
                        shift
                done
                shift 
                  ;;
          -H)
                while [ "$2" != "" -a "${2#-?*}" = "$2" ] ; do
                        HOSTFILE="$2"
                        if [ ! -e "$HOSTFILE" -a "$HOSTFILE" != "-" ] ; then
                                echo "mussh: Host file '$HOSTFILE' does not exist!" 1>&2
                                exit 1
                        fi
                        HOSTLIST="`cat $HOSTFILE | sed -e 's/#.*//' | egrep -v "^ *$" `
$HOSTLIST"
                        shift
                done
                shift 
                ;;
        ################
        # COMMAND ARGS #
        ################
          -c)
                THESCRIPT="$THESCRIPT
$2"
                #set "" ; shift
                shift 2
                ;;
          -C)
                while [ "$2" != "" -a "${2#-?*}" = "$2" ] ; do
                        SCRIPTFILE="$2"
                          if [ ! -e "$SCRIPTFILE" -a "$SCRIPTFILE" != "-" ] ; then
                                echo "mussh: Script File '$SCRIPTFILE' does not exist!" 1>&2
                                exit 1
                        fi
                        THESCRIPT="$THESCRIPT
`cat $SCRIPTFILE `"
                        shift
                done
                shift
                ;;
          *)
                echo "mussh: invalid command - \"$1\"" 1>&2
                echo "$USAGE" 1>&2
                exit 1
                ;;
        esac
done

#####################
# EXPRESS VERBOSITY #
#####################
[ "$DEBUG" -ge 1 ] && echo "DEBUG LEVEL: $DEBUG" 1>&2
[ "$DEBUG" -ge 1 ] && echo "SSH DEBUG LEVEL: $SSH_VERBOSE" 1>&2

#####################
# CLEAN UP HOSTLIST #
#####################
HOSTLIST=`echo "$HOSTLIST" | sed -e 's/#.*//' | egrep -v "^ *$" `

if [ "$FORCE_LOGIN" != "" ] ; then
	[ "$DEBUG" -ge 1 ] && echo "DEBUG: FORCE_LOGIN: $FORCE_LOGIN"
	HOSTLIST=`echo "$HOSTLIST" | sed -e "s/^\(.*@\)\{0,1\}\([^@]*\)\$/$FORCE_LOGIN@\2/"`
elif [ "$DEFAULT_LOGIN" != "" ] ; then
	[ "$DEBUG" -ge 1 ] && echo "DEBUG: DEFUALT_LOGIN: $DEFAULT_LOGIN"
	HOSTLIST=`echo "$HOSTLIST" | sed -e "s/^\([^@]*\)\$/$DEFAULT_LOGIN@\1/"`
fi
[ $UNIQUE_HOSTS -ge 1 ] && HOSTLIST=`echo "$HOSTLIST" | sort -uf `


################
# CHECK SYNTAX #
################
if [ "$HOSTLIST" = "" ] ; then
        echo "mussh: ERROR: You must supply at least one host!" 1>&2
        echo "$USAGE" 1>&2
        exit 1
fi

###################################
# DEFAULT TO STDIN IF NO COMMANDS #
###################################
if [ "$THESCRIPT" = "" ] ; then
        echo "Enter your script here.  End with single \".\" or EOF." 1>&2
        while read THISLINE
        do 
                if [ "$THISLINE" = "." ] ; then
                        break
                fi
        THESCRIPT="$THESCRIPT
$THISLINE"
        done
fi

############################
# PRINT VERBOSE DEBUG INFO #
############################
if [ "$DEBUG" -ge 2 ] ; then
        echo "DEBUG: HOSTLIST: " $HOSTLIST 1>&2
        echo "DEBUG: THE SCRIPT: $THESCRIPT" 1>&2
        echo "DEBUG: SSH ARGS: $SSH_ARGS" 1>&2
fi

############################
# LOAD SSH-AGENT WITH KEYS #
############################
if [ "$NO_AGENT" = "1" ] ; then
        [ "$DEBUG" -ge 1 ] && echo "DEBUG: Not using ssh-agent"
elif [ "$IDENT" != "" ] ; then
        start_agent
        load_keys "$IDENT"
elif [ ! -f "$HOME/.ssh/identity" -a ! -f "$HOME/.ssh/id_dsa" ]  ; then
        [ "$DEBUG" -ge 1 ] && echo "DEBUG: No identity file found.  Skipping agent."
else
        start_agent 
        load_keys "$IDENT"
fi
#echo

########################
# EXECUTE THE COMMANDS #
########################
#set `echo $HOSTLIST | sed -e 's/#.*//' | egrep -v "^ *$" | sort -uf `
if [ "$PROXY" != "" ] ; then
        [ "$DEBUG" -ge 1 ] && echo "DEBUG: PROXY CONNECT $PROXY" 1>&2
        echo "$THESCRIPT" \
                | ssh -T $SSH_ARGS $PROXY \
                        "mussh -h $HOSTLIST \
                                -C - \
                                -D$DEBUG \
                                $PROXY_SSH_ARGS 2>&1 " \
				| while read SSH_LINE ; do
				    if [ "$QUIET" -lt 1 -a "$SSH_LINE" != "" ] ; then
					    echo "$SSH_LINE" | sed -e "s/^/$HOST: /"
				    fi
				done
                
else
        set $HOSTLIST
        while [ "$1" != "" ] ; do
                HOST="$1"
                #echo
                [ "$DEBUG" -ge 1 ] && echo "DEBUG: CONNECT $HOST" 1>&2
		shift
		echo "$THESCRIPT" \
		    | ssh -T $SSH_ARGS $HOST 2>&1 \
		    | while read SSH_LINE ; do
			if [ "$QUIET" -lt 1 -a "$SSH_LINE" != "" ] ; then
				echo "$SSH_LINE" | sed -e "s/^/$HOST: /"
			fi
		    done
        done
fi

############
# CLEAN UP #
############
stop_agent


