#!/bin/sh
# Do a command on all student PCs.  See usage message for more details.
#
# Before running this, you need to configure the student PCs
# for a flat network, by logging in as "flat".

myname=${0##*/}

default_mode=flat	# "flat" network
#default_mode=term	# "terminal room" network
#default_mode=ex1	# classroom exercise 1
#default_mode=ex1-nmap	# classroom exercise 1, with unknown host addresses
#default_mode=ex2	# classroom exercise 2
#default_mode=ex2-nmap	# classroom exercise 2, with unknown host addresses

noc=196.200.223.1
myprefix=196.200.220
pkgftpsite=ftp://${noc}/pub/FreeBSD/releases/i386/6.2-RELEASE/packages/All

ssh_opts="-i /home/inst/.ssh/id_dsa"
scp_opts="$ssh_opts"
rsync_opts="-e 'ssh $ssh_opts'"

parallel=

# Convert a tag to a name.
tag-to-name ()
{
    local tag="$1"
    local name
    case "${tag}" in
    a|A|1) name=A ;;
    b|B|2) name=B ;;
    c|C|3) name=C ;;
    d|D|4) name=D ;;
    e|E|5) name=E ;;
    f|F|6) name=F ;;
    g|G|7) name=G ;;
    h|H|8) name=H ;;
    i|I|9) name=I ;;
    j|J|10) name=J ;;
    esac
    echo "${name}"
}

# Convert a tag to a number.
tag-to-number ()
{
    local tag="$1"
    local number
    case "${tag}" in
    a|A|1) number=1 ;;
    b|B|2) number=2 ;;
    c|C|3) number=3 ;;
    d|D|4) number=4 ;;
    e|E|5) number=5 ;;
    f|F|6) number=6 ;;
    g|G|7) number=7 ;;
    h|H|8) number=8 ;;
    i|I|9) number=9 ;;
    j|J|10) number=10 ;;
    esac
    echo "${number}"
}

# Convert a tag to an address range or a single address
tag-to-range ()
{
    local tag="$1"
    # invoke tag-to-range-foo, where foo is derived
    # from $mode with any trailing "-nmap" removed
    eval 'tag-to-range-'${mode%-nmap}' "${tag}"'
}
tag-to-addr ()
{
    local tag="$1"
    local range
    local addr
    local scanflags
    case "${mode}" in
    *-nmap)
		# Use tag-to-range to find an address range, and then
		# use nmap to scan the range looking for open SSH ports,
		# and assume that the first open SSH port
		# belongs to the PC that we are looking for.
		range="$( tag-to-range "${tag}" )"
		case "$(id -u)" in
		0) scanflags="-PS22 -sS" ;; # root can use these flags
		*) scanflags="-sT" ;;     # non-root can use these
		esac
		addr="$( nmap -oG - ${scanflags} -pT:22 "${range}" \
			|  awk '/22.open.tcp/ { print $2 ; exit }' )"
		case "${addr}" in
		"")
			echo >&2 "# ${tag}: scanned ${range} but did not find PC"
			addr="${range%/*}" # wrong, but we can't do better
			;;
		*)
			echo >&2 "# ${tag}: found PC at ${addr} after scanning ${range}"
			;;
		esac
		echo "${addr}"
		;;
    *)		# invoke tag-to-addr-foo
		eval 'tag-to-addr-'${mode}' "${tag}"' ;;
    esac
}

# For mode=flat, tag-to-addr and tag-to-range both return
# the same thing, which is simply myprefix.1 for row A,
# myprefix.2 for row B, etc.
tag-to-range-flat ()
{
    tag-to-addr-flat "$1"
}
tag-to-addr-flat ()
{
    local tag="$1"
    echo "${myprefix}.$(( 100 + $(tag-to-number "${tag}") ))"
}

# For mode=term, tag-to-addr and tag-to-range both return
# the same thing, which is an address that was assigned by DHCP
# myprefix.2 for row B, etc.
tag-to-range-term ()
{
    tag-to-addr-term "$1"
}
tag-to-addr-term ()
{
    local tag="$1"
    local addr
    case "${tag}" in
    a|A|1) addr=196.200.xxx.yyy ;;
    b|B|2) addr=196.200.xxx.yyy ;;
    c|C|3) addr=196.200.xxx.yyy ;;
    d|D|4) addr=196.200.xxx.yyy ;;
    e|E|5) addr=196.200.xxx.yyy ;;
    f|F|6) addr=196.200.xxx.yyy ;;
    g|G|7) addr=196.200.xxx.yyy ;;
    h|H|8) addr=196.200.xxx.yyy ;;
    i|I|9) addr=196.200.xxx.yyy ;;
    j|J|10) addr=196.200.xxx.yyy ;;
    esac
    echo "${addr}"
}

# For mode=ex1 (classroom exercise configuration 1),
# tag-to-range returns a /28 range calculated by arithmetic,
# and tag-to-addr returns an IP address chosen by the students.
tag-to-range-ex1 ()
{
    # XXX may change from one exercise to another.
    # After they change, edit this source code, or create a new
    # set of functions for mode=ex<n>.
    local tag="$1"
    local n="$( tag-to-number "${tag}" )"
    local subnet="${myprefix}.$(( $n * 16 ))/28"
    echo "${subnet}"
}
tag-to-addr-ex1 ()
{
    # XXX may change from one exercise to another.
    # After they change, try "${myname} -m ex1-nmap all print-addr"
    # to guess the addresses, and then edit this source code.
    local addr
    case "${tag}" in
    a|A|1) addr=${myprefix}.18 ;;
    b|B|2) addr=${myprefix}.34 ;;
    c|C|3) addr=${myprefix}.50 ;;
    d|D|4) addr=${myprefix}.70 ;;
    e|E|5) addr=${myprefix}.82 ;;
    f|F|6) addr=${myprefix}.98 ;;
    g|G|7) addr=${myprefix}.114 ;;
    h|H|8) addr=${myprefix}.130 ;;
    i|I|9) addr=${myprefix}.146 ;;
    j|J|10) addr=${myprefix}.162 ;;
    esac
    echo "${addr}"
}

# For mode=ex2 (classroom exercise configuration 2),
# tag-to-range returns a /28 range calculated by arithmetic,
# and tag-to-addr returns an IP address chosen by the students.
tag-to-range-ex2 ()
{
    # XXX may change from one exercise to another.
    # After they change, edit this source code, or create a new
    # set of functions for mode=ex<n>.
    local tag="$1"
    local range
    case "${tag}" in
    a|A|1) range=${myprefix}.52/30 ;;
    b|B|2) range=${myprefix}.56/30 ;;
    c|C|3) range=${myprefix}.60/30 ;;
    d|D|4) range=${myprefix}.64/30 ;;
    e|E|5) range=${myprefix}.68/30 ;;
    f|F|6) range=${myprefix}.72/30 ;;
    g|G|7) range=${myprefix}.76/30 ;;
    h|H|8) range=${myprefix}.80/30 ;;
    i|I|9) range=${myprefix}.84/30 ;;
    j|J|10) range=${myprefix}.88/30 ;;
    esac
    echo "${range}"
}
tag-to-addr-ex2 ()
{
    # XXX may change from one exercise to another.
    # After they change, try "${myname} -m ex1-nmap all print-addr"
    # to guess the addresses, and then edit this source code.
    local addr
    case "${tag}" in
    a|A|1) addr=${myprefix}.18 ;;
    b|B|2) addr=${myprefix}.34 ;;
    c|C|3) addr=${myprefix}.50 ;;
    d|D|4) addr=${myprefix}.70 ;;
    e|E|5) addr=${myprefix}.82 ;;
    f|F|6) addr=${myprefix}.98 ;;
    g|G|7) addr=${myprefix}.114 ;;
    h|H|8) addr=${myprefix}.130 ;;
    i|I|9) addr=${myprefix}.146 ;;
    j|J|10) addr=${myprefix}.162 ;;
    esac
    echo "${addr}"
}

# print a heading
print-heading ()
{
    local tag="$1" ; shift
    local rest="$*"
    echo "# $(tag-to-name "${tag}") ${rest}"
}

# shell-quotify: insert backslashes where appropriate
# to protect a string from the shell.
shell-quotify ()
{
  printf "%s" "$1" | sed -e 's/\([^-=+:,./A-Za-z0-9]\)/\\\1/g'
}

# args-to-cmd:  convert a list of args suitable for local use
# into a stringified command suitable for use via ssh.  This
# required escaping special characters.  For example,
# $(args-to-cmd somecommand "foo bar") gives "somecommand foo\ bar".
#
# We need this because ssh does not handle commands with args
# sensibly.  The following all mean the same to ssh:
#	ssh hostname somecommand "foo bar"
#	ssh hostname somecommand foo bar
#	ssh hostname "somecommand foo bar"
#
# If you want the behaviour that you might expect to get from
#	ssh hostname somecommand "foo bar"
# then you actually have to say
#	ssh hostname "somecommand foo\ bar"
args-to-cmd ()
{
    local cmd=''
    local arg
    local quotedarg
    for arg in "$@"
    do
	quotedarg="$( shell-quotify "${arg}" )"
	cmd="${cmd}${cmd:+ }${quotedarg}"
    done
    echo "${cmd}"
}

# Do a command on specified student PCs.  Takes a host name or adddress
# and a command string command with special characters escaped.
# If cmd is empty, then does an interactive login.
do-cmd ()
{
    local user_at_host="$1"
    local cmd="$2"
    case "${cmd}" in
    '')
	    #echo "${myname}: connecting to ${user_at_host}"
	    ssh $ssh_opts -t -t ${user_at_host}
	    ;;
    *)	
	    #echo "${myname}: connecting to ${user_at_host}"
	    if ${parallel}; then
		ssh $ssh_opts ${user_at_host} "${cmd}" &
	    else
		ssh $ssh_opts -t -t ${user_at_host} "${cmd}"
	    fi
	    ;;
    esac
}

# Massage args into a form suitable for an scp-like command.
# We need to combine our first arg and our last arg to make
# something that will be passed as the last arg to scp or rsync.
# For example, the output from
# 	massage-args-for-scp user_at_host src1 src2 src3 dest
# is "src1 src2 src3 user_at_host:dest"
massage-args-for-scp ()
{
    local user_at_host="$1" ; shift
    local srclist=''
    local dest
    local i
    while [ $# -gt 1 ]
    do
	srclist="${srclist}${srclist:+ }$(shell-quotify "$1")"
	shift
    done
    dest="${user_at_host}:$(shell-quotify "$1")" ; shift
    printf "%s" "${srclist} ${dest}"
}

# Perform an scp-like command to a host.
do-scp-like-cmd ()
{
    local cmd="$1" ; shift
    local opts="$1" ; shift
    if ${parallel} ; then
	eval $cmd $opts $(massage-args-for-scp "$@") &
    else
	eval $cmd $opts $(massage-args-for-scp "$@")
    fi
}

# Perform an nmap command
do-nmap ()
{
    local tag="$1" ; shift
    local range="$(tag-to-range "${tag}")"
    print-heading "${tag}" "nmap ${range}"
    nmap ${1+"$@"} "${range}"
}

# expand abbreviations in list of pkgs.
# XXX: doesn't try to quotify special characters.
expand-pkg-list ()
{
    local shortname
    local fullname
    local pkglist=''
    for shortname in "$@"
    do
	case "$shortname" in
	-*)	fullname="${shortname}" ;;
	*/*)	fullname="${shortname}" ;;
	*.t[bg]z)	fullname="${pkgftpsite}/${shortname}" ;;
	*)	fullname="${pkgftpsite}/${shortname}.tbz" ;;
	esac
	pkglist="${pkglist}${pkglist:+ }${fullname}"
    done
    echo "${pkglist}"
}

do-pingn ()
{
    local name="$1"
    local addr="$2"
    local n="$3"
    ping -qQ -t$(( $n + 1 )) -c"${n}" "${addr}" 2>&1 \
    | sed -n -e '/packets/s/^/'"${name}: ${addr}: "'/p'
}

usage ()
{
    cat <<END
usage: ${myname} [opts] where op [args]
<opts> are:
  -m mode (mode is "flat" or something else)
  -p: parallel (all at once, default if stdin is not a terminal)
  -s: serial (one at a time, as opposed to parallel)

<where> is "all", or a single PC name [A-J],
      or a (quoted) list of space- or comma-separated PC names.

<op [args]> is one of the following:
  print-addr
	Print the IP address (locally, not on the remote PC)
  print-range
	Print the IP address range
  identify
	Print a message on the console identifying the machine
  ping
	Ping until interrupted
  ping1
	Ping once
  nmap
        Run nmap with specified args.
	e.g. ${myname} A nmap -oG - -sP
	e.g. ${myname} all nmap -pT:22,23
  beep
	Beep once
  beeep
	Beep several times
  reboot
	Reboot (shutdown -r now)
  powerdown
	Power down (shutdown -p now)
  login
	Login interactively.
	e.g. ${myname} A login
  do command [args]
	Perform the specified command with the specified args,
	using normal (stupid) ssh (lack of) quoting semantics.
	e.g.: ${myname} all do ls /
	e.g.: ${myname} all do 'ls /'
  qdo command [args]
	Perform the specified command with the specified args,
	carefully escaping troublesome parts of the command or args.
	e.g.: ${myname} all qdo touch '/file with spaces in the name'
  -c quoted-shell-command
	This is short for qdo /bin/sh -c quoted-shell-command.
	e.g.: ${myname} all -c 'xinit & sleep 5 ; DISPLAY=:0 opera'
  pkg_add [flags] pkg ...
	Add the specified pkg.  pkg names may be absolute file
	names on the student PC (e.g. /tmp/whatever.tgz), complete
	URLs (e.g. ftp://host/path/whatever.tgz), or short names.
	Short names (without any slashes) are relative to
	${pkgftpsite}.
	e.g.: ${myname} all pkg_add bash-2.05b.004 rsync-2.5.6_1
  scp [opts] src ... dest
	scp the specified sources to the specified destination.
	Sources are on local host.  destination is on remote host.
	e.g.: ${myname} all scp ./rc.local /etc/rc.local
  rsync [opts] src dest
	rsync the specified source to the specified destination.
	Source is on local host.  destination is on remote host.
	e.g.: ${myname} all rsync -av ./rc.local /etc/

  Unrecognised commands are handled as if they had been preceded by "do".
END
}

main ()
{
    local where
    local op
    local user=root
    local dest # user@host
    local i

    # parse options
    while : ; do
	case "$1" in
	-m) mode="$2" ; shift 2 ;;
	-m*) mode="${1#-?}" ; shift ;;
	-p) parallel=true ; shift ;;
	-s) parallel=false ; shift ;;
	-*) usage ; exit 1 ;;
	*)  break ;;
	esac
    done

    # set defaults for unspecified options
    : "${mode:=${default_mode}}"
    if [ -z "${parallel}" ]; then
	# default to parallel mode if stdin is not a terminal
	if tty >/dev/null 2>&1 ; then
	    parallel=false
	else
	    parallel=true
	fi
    fi

    # parse positional args
    where="$1" ; shift 2>/dev/null
    op="$1" ; shift 2>/dev/null

    case "${where}" in
    '')   usage ; exit 1 ;;
    all)  where="A B C D E F G H I J" ;;
    ?[,\ ]*)  where="$( echo "$where" | tr ',' ' ' )" ;;
    ?)	  : OK ;;
    *)    usage ; exit 1 ;;
    esac

    for tag in $where
    do
	addr="$(tag-to-addr "${tag}")"
	name="$(tag-to-name "${tag}")"
	dest="${user}@${addr}"
	case "${op}" in
	help|-h*)	usage ; exit 1 ;;
	print-addr) echo "${addr}" ;;
	print-range) echo "$(tag-to-range "${tag}")" ;;
	ping)	ping "${addr}" ;;
	ping1)	do-pingn "${name}" "${addr}" 1 ;;
	ping5)	do-pingn "${name}" "${addr}" 5 ;;
	beep)	do-cmd "${dest}" "printf >/dev/console '\7'" ;;
	beeep)	do-cmd "${dest}" "printf >/dev/console '\7' ; sleep 1 ; printf >/dev/console '\7' ; sleep 1 ; printf >/dev/console '\7' ; sleep 1 ; printf >/dev/console '\7' ; sleep 1 ; printf >/dev/console '\7' ; sleep 1 ; "
		;;
	identify) do-cmd "${dest}" "echo Your name is ${name}, address is ${addr} >/dev/console" ;;
	reboot)	do-cmd "${dest}" "/sbin/shutdown -r now ; sleep 1" ;;
	powerdown)	do-cmd "${dest}" "/sbin/shutdown -p now ; sleep 1" ;;
	login)	do-cmd "${dest}" "" ;; # interactive login
	do)	do-cmd "${dest}" "$*" ;;
	qdo)	do-cmd "${dest}" "$(args-to-cmd "$@")" ;;
	-c)     do-cmd "${dest}" "$(args-to-cmd /bin/sh -c "$@")" ;;
	pkg_add) do-cmd "${dest}" "pkg_add $(expand-pkg-list "$@")" ;;
	scp)	do-scp-like-cmd scp "$scp_opts" "${dest}" "$@" ;;
	rsync)	do-scp-like-cmd rsync "$rsync_opts" "${dest}" "$@" ;;
	nmap)	do-nmap "${tag}" ${1+"$@"} ;;
	*)	do-cmd "${dest}" "${op} $*" ;;
	esac
    done
}

main ${1+"$@"}

