#!/bin/sh

# $Id: tentakel,v 1.57 2003/05/19 17:27:32 cran Exp $

# Copyright (c) 2002 Sebastian Stark
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR SEBASTIAN STARK
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

set -e

RSH="/usr/bin/ssh"
SEPARATOR_STRING="### Output from"
MYVER="0.3.1"
MYNAME=`IFS=/; set $0; eval echo \\$$#`

# output warnings
warn () {
#        assert warn "$# -eq 1"
        echo "$MYNAME: $1" >&2
}

# error handler
err () {
#        assert err "$# -eq 1"
        warn "$1"
        exit 1
}

# Create random file and return filename
tempfile () {
        assert tempfile "$# -eq 0"
        mktemp "/tmp/tentakel.$$.XXXXXXXXX"
}

make_counter () {
        assert make_counter "$# -eq 2"
        eval "$1__count=0
              $1__prefix=$2
              $1_inc () {
                      assert $1_inc \"\$# -eq 0\"
                      $1__count=\`echo \$$1__count + 1 | bc\`
              }
              $1_get () {
                      assert $1_get \"\$# -eq 0\"
                      echo \$$1__prefix\$$1__count
              }
        "
}

make_host_container () {
        assert make_host_container "$# -eq 1"
        eval "$1__host_ids=
              $1_append () {
                      assert $1_append \"\$# -eq 1\"
                      $1__host_ids=\"\$$1__host_ids \$1\"
              }
              $1_call () {
                      assert $1_call \"\$# -ge 1\"
                      method=\$1
                      shift
                      for id in \$$1__host_ids
                      do
                              \${id}_\${method} \"\$@\"
                      done
              }
              $1_call_parallel () {
                      assert $1_call_parallel \"\$# -ge 1\"
                      method=\$1
                      shift
                      processes=
                      for id in \$$1__host_ids
                      do
                              \${id}_\${method} \"\$@\" &
                              processes=\"\$processes \$!:\$id\"
                      done
                      for process in \$processes
                      do
                              id=\`echo \$process | sed 's/.*://'\`
                              pid=\`echo \$process | sed 's/:.*//'\`
                              if wait \$pid
                              then
                                      eval \${id}__exitcode_local=0
                              else
                                      eval \${id}__exitcode_local=\$?
                              fi
                      done
              }
              $1_destroy () {
                      :
              }"
}

# creates an object named $1 for host $2
make_host () {
        assert make_host "$# -eq 2"
        eval "$1__hostname=$2
              $1__exitcode_local=undefined
              $1__exitcode_remote=undefined
              $1__remotecommand=$RSH
              $1__stdout=`tempfile` || err \"Error in tempfile creation.\"
              $1__stderr=`tempfile` || err \"Error in tempfile creation.\"
              $1_exitcode_local () {
                      assert $1_exitcode_local \"\$# -eq 0\"
                      if test \$$1__exitcode_local = undefined
                      then
                              return 1
                      else
                              echo \$$1__exitcode_local
                      fi
              }
              ${1}_exitcode_remote () {
                      assert $1_exitcode_remote \"\$# -eq 0\"
                      if test \$${1}__exitcode_remote = undefined
                      then
                              return 1
                      else
                              echo \$$1__exitcode_remote
                      fi
              }
              $1_hostname () {
                      assert $1_hostname \"\$# -eq 0\"
                      echo \"\$$1__hostname\"
              }
              $1_output () {
                      assert $1_output \"\$# -eq 0\"
                      test \$FLAG_NO_SEPARATOR -ne 1 \
                      && echo \"\$SEPARATOR_STRING \`$1_hostname\` (\`$1_exitcode_local\`):\"
                      cat \"\$$1__stderr\"
                      cat \"\$$1__stdout\"
              }
              $1_destroy () {
                      assert $1_destroy \"\$# -eq 0\"
                      rm \"\$$1__stderr\" \"\$$1__stdout\"
              }
              $1_run () {
                      assert $1_run \"\$# -ne 0\"
                      \$$1__remotecommand \
                              \$$1__hostname \"\$@\" \
                               > \"\$$1__stdout\" 2> \"\$$1__stderr\" \
                      || return \$?
              }"
}

# execute $2 $3...$n for all hosts in $1 via ${RSH}
distribute_command () {
        assert distribute_command "$# -ge 2"
        HOSTS="$1"
        shift

        make_host_container container
        make_counter objid hostid

        for host in $HOSTS
        do
                make_host `objid_get` $host
                container_append `objid_get`
                objid_inc
        done

        container_call_parallel run "$@"
        container_call output
        container_call destroy
        container_destroy
}

# Show version
version () {
        assert version "$# -eq 0" "-n '$MYNAME'" "-n '$MYVER'"
        echo "$MYNAME $MYVER"
}

# Show help
usage () {
        assert usage "$# -eq 0" "-n '$MYNAME'"
        echo "Usage: $MYNAME [ options ] command [ options ]
 -l filename    Use the file \$HOME/.tentakel/filename as the hostlist.
 -n             Do not print separators.
 -q             Be quiet (alias for -n).
 -d             Enable debug information on stderr.
 --help         Display this help.
 --version      Display version information.
 command        Commmand to be executed on all hosts."
}

# Flags
parse_options () {
        FLAG_HOSTLIST=0
        FLAG_DEBUG=0
        FLAG_HELP=0
        FLAG_NO_SEPARATOR=0
        FLAG_VERSION=0
        FLAG_NO_COMMAND=0
        LOCAL_ERROR="Use \"$MYNAME --help\" for more information."
        SHIFT_HACK=

        test $# -eq 0 && err "No parameters given. $LOCAL_ERROR"

        while :
        do
                case "$1" in
                --help )        FLAG_HELP=1 ;;
                --version )     FLAG_VERSION=1 ;;
                -n )            FLAG_NO_SEPARATOR=1 ;;
                -l )            shift
                                SHIFT_HACK="shift; $SHIFT_HACK"
                                test $# -eq 0 && err "Filename expected. $LOCAL_ERROR"
                                CF="$1"
                                FLAG_HOSTLIST=1 ;;
                -q )            FLAG_NO_SEPARATOR=1 ;;
                -d )            FLAG_DEBUG=1 ;;
                -* )            err "Parameter \"$1\" unknown. $LOCAL_ERROR" ;;
                * )             if test $# -eq 0
                                then
                                        FLAG_NO_COMMAND=1
                                fi
                                break ;;
                esac
                shift
                SHIFT_HACK="shift; $SHIFT_HACK"
        done
}

# Setup
setup () {
#       assert setup "'$FLAG_HELP' -eq 0 -o '$FLAG_HELP' -eq 1" \
#              "'$FLAG_VERSION' -eq 0 -o '$FLAG_VERSION' -eq 1" \
#              "'$FLAG_NO_SEPARATOR' -eq 0 -o '$FLAG_NO_SEPARATOR' -eq 1" \
#              "'$FLAG_HOSTLIST' -eq 0 -o '$FLAG_HOSTLIST' -eq 1" \
#              "'$FLAG_DEBUG' -eq 0 -o '$FLAG_DEBUG' -eq 1"

        FLAG_EXIT=0

        if test $FLAG_DEBUG -eq 1
        then
                dprint () {
                        assert dprint "$# -eq 1"
                        echo "$MYNAME- $1" >&2
                }

                assert () {
                        function_name="$1"
                        shift
                        for assertion
                        do
                                if eval "test $assertion"
                                then
					:
                                else
                                        dprint "Assertion failed in $function_name: \"$assertion\"."
                                fi
                        done
                }
        else
                dprint () {
                        :
                }

                assert () {
                        :
                }
        fi

        if test $FLAG_VERSION -eq 1
        then
                version
                FLAG_EXIT=1
        fi

        if test $FLAG_HELP -eq 1
        then
                usage
                FLAG_EXIT=1
        fi

        if test $FLAG_EXIT -eq 1
        then
                exit 0
        fi

        if test $FLAG_NO_COMMAND -eq 1
        then
                err "No command given for execution. Use \"tentakel --help\" for more information."
        fi

        if test $FLAG_HOSTLIST -eq 1
        then
                HOSTLIST_FILE="$HOME/.tentakel/$CF"
        else
                HOSTLIST_FILE="$HOME/.tentakel/default"
        fi

        if test ! -r "$HOSTLIST_FILE"
        then
                err "Cannot read from file \"$HOSTLIST_FILE\"."
        fi
}

# Set up host list
parse_hostlist () {
        assert parse_hostlist "$# -eq 0" "-n '$HOSTLIST_FILE'"
        HOSTS=
        while read line
        do
                case "$line" in
                        "#"* | "" ) ;;
                        * ) HOSTS="$HOSTS $line" ;;
                esac
        done < "$HOSTLIST_FILE"
        dprint "Using hostlist \"$HOSTLIST_FILE\" (`set $HOSTS; echo $#` hosts)"
}

# Execute remote command if not empty
execute_command () {
        assert execute_command "$# -ge 1"
        dprint "Executing: \"$*\""
        distribute_command "$HOSTS" "$@"
}

parse_options "$@"
setup
parse_hostlist
eval "$SHIFT_HACK"
execute_command "$@"

exit 0
