#!/usr/bin/ksh
#
# BoKS plugin for resetting a root password
#
# Written by Thomas Sluyter (nagiosATkilalaDOTnl)
# By request of Unixerius, the Netherlands
# Last Modified: 10-01-2010
#
# Description:
#   This script can be used to generate, set and verify a new password
# for any root account within your BoKS domain. This script can be used
# as part of your monthly root password reset cycle, or for daily 
# maintenance purposes.
#
# Usage: 
# ./boks_new_rootpw [[-h HOST] [-H HG] [-i FILE] | -A] [-x HOST] [-X HG] [-f FILE] [-d -o FILE]
#
# -h HOST	Change the root password for HOST. Multiple -h entries allowed.
# -H HGROUP	Change the root passwords for HOSTGROUP. Multiple -H entries allowed.
# -i FILE	Change the root passwords for all hosts in FILE.
# -A 		Change the root passwords for ALL hosts.
# -x EXCLUDE	Hosts to exclude (when using -H or -A). Multiple -x entries allowed.
# -X EXCLUDEHG	Hostgroups to exclude (when using -A). Multiple -X entries allowed.
# -f FILE	Output file to store the new root passwords in. Default is stdout.
# -d 		Debug mode. Provides error logging. Does a dry-run, not doing any updates.
# -o FILE	Output file for debugging logs. Required when -d is passed.
#
# Example: 
# ./boks_new_rootpw -h HOST1 -h HOST2 -f $BOKS_var/root.txt
# ./boks_new_rootpw -A -d -o /tmp/foobar
#
# Limitations:
# * This script must be run as root from the BoKS shell.
# * This script was tested with BoKS 6.0.x.
# * If the script malfunctions, you may want to verify the platform
#   specific variables under "SETTING THINGS UP". 
#
# Output:
# * By default the script echoes all new root passwords (and errors) to
#   stdout. By providing an output file with the -f flag, no passwords
#   are shown on screen. The file is of course set to root:root 0600.
# * When running in debug/dry-run mode, the script outputs log messages
#   to the output file specified with the -o flag. This file will show 
#   detailed error reports for failing root updates. BEWARE: THE DEBUG
#   LOG WILL CONTAIN (UNUSED) ROOT PASSWORDS. It's also set to root:root
#   0600, to be on the safe side.
#
# Other notes:
# * If you ever run into problems with the script, run it in debug mode
#   using the -d flag. 
#
#


### SETTING THINGS UP ###

PROGNAME=$(basename $0)
PATH="/usr/bin:/usr/sbin:/bin:/sbin:$PATH"

TEMPDIR="/tmp/$PROGNAME.$(date +%Y%m%d)"; mkdir $TEMPDIR >/dev/null 2>&1; rm $TEMPDIR/* >/dev/null 2>&1; chmod 600 $TEMPDIR
HOSTLIST="$TEMPDIR/hostlist"
EXCLUDELIST="$TEMPDIR/excludelist"

HOSTNAMES="" 		# Default if -h parameter is not passed.
HOSTGROUPS=""		# Default if -H parameter is not passed.
HOSTFILE=""		# Default if -i parameter is not passed.
ALL="0"			# Default if -A parameter is not passed.
EXCLUDES=""             # Default if -x parameter is not passed.
EXCLUDEHGS=""           # Default if -X parameter is not passed.
KEEPPASSWORDLIST="0" 	# Default if -f parameter is not passed.
PASSWORDLIST=""		# Default if -f parameter is not passed.
DEBUG="0"		# Default if -d parameter is not passed.
DEBUGFILE=""		# Default if -o parameter is not passed.
PASSWORD=""		# Empty value before password generation.
ENCPASSWORD=""		# Empty value before password encryption.


# Setting up platform specific options

case $(uname) in
	AIX)    BOKSSBIN="/opt/boksm/sbin";
		BOKSBIN="/opt/boksm/bin";
		BOKSLIB="/opt/boksm/lib";
		BOKSTMP="/var/opt/boksm/tmp";
		BOKSETC="/etc/opt/boksm";
		AWK="/usr/bin/awk";
		GREP="/usr/bin/grep";
		PING="/bin/ping -c 1";
		PINGPARAM="";;
	Linux) BOKSSBIN="/opt/boksm/sbin";
		BOKSBIN="/opt/boksm/bin";
		BOKSLIB="/opt/boksm/lib";
		BOKSTMP="/var/opt/boksm/tmp";
		BOKSETC="/etc/opt/boksm";
		AWK="/bin/awk";
		GREP="/bin/grep";
		PING="/bin/ping -c 1";
		PINGPARAM="";;
	SunOS)  BOKSSBIN="/opt/boksm/sbin";
		BOKSBIN="/opt/boksm/bin";
		BOKSLIB="/opt/boksm/lib";
		BOKSETC="/etc/opt/boksm";
		BOKSTMP="/var/opt/boksm/tmp";
		AWK="/usr/xpg4/bin/awk";
		GREP="/usr/xpg4/bin/grep";
		PING="/usr/sbin/ping";
		PINGPARAM="1";;
	*)      echo "\nERROR: OS is not supported."; exit 1 ;;
esac


# Quick check to see if you're running as root with the BoKS shell

if [[ ( -z $BOKS_etc ) || ( -z $(id | $GREP "uid=0") ) ]]
then
        echo "ERROR: Script must be run as root, from the BoKS shell.\n"
        exit 1
fi


### REQUISITE COMMAND LINE STUFF ###

print_usage() {
	echo "\nUsage:"
	echo "$PROGNAME [[-h HOST] [-H HG] [-i FILE] | -A] [-x HOST] [-X HG] [-f FILE] [-d -o FILE]"
	echo "\n-h HOST\tChange the root password for HOST. Multiple -h entries allowed."
	echo "-H HGROUP\tChange the root passwords for HOSTGROUP. Multiple -H entries allowed."
	echo "-i FILE\tChange the root passwords for all hosts in FILE."
	echo "-A\t\tChange the root passwords for ALL hosts."
	echo "-x EXCLUDE\tHosts to exclude (when using -H or -A). Multiple -x entries allowed."
	echo "-X EXCLUDEHG\tHostgroups to exclude (when using -A). Multiple -X entries allowed."
	echo "-f FILE\tOutput file to store the new root passwords in. Default is stdout."
	echo "-d\t\tDebug mode. Provides error logging. Does a dry-run, not doing any updates."
	echo "-o FILE\tOutput file for debugging logs. Required when -d is passed."
	echo "\nExamples:"
	echo "$PROGNAME -h HOST1 -h HOST2 -f $BOKS_var/root.txt"
	echo "$PROGNAME -A -d -o /tmp/foobar\n\n"
	exit 1
}


[[ $# -eq 0 ]] && print_usage

while [[ $# -gt 0 ]]
do
    case "$1" in
	-h) if [[ (-z $(echo $2 | $GREP ^-)) && (! -z $2) ]]
	    then
		HOSTNAMES="$HOSTNAMES $2"; shift
	    else
		echo "\nERROR: Hostname incorrectly specified. Use -h.\n\n"
		print_usage
	    fi;;
	-H) if [[ (-z $(echo $2 | $GREP ^-)) && (! -z $2) ]]
	    then
		HOSTGROUPS="$HOSTGROUPS $2"; shift
	    else
		echo "\nERROR: Hostgroup incorrectly specified. Use -H.\n\n"
		print_usage
	    fi;;
	-i) if [[ (-z $(echo $2 | $GREP ^-)) && (! -z $2) ]]
	    then
		if [[ -f $2 ]]
		then
		    HOSTFILE="$2"; shift
		else
		    echo "\nERROR: Input file does not exist. Use -i.\n\n"
		    print_usage
		fi
	    else
		echo "\nERROR: Input file incorrectly specified. Use -i.\n\n"
		print_usage
	    fi;;
	-A) ALL="1";;
	-x) if [[ (-z $(echo $2 | $GREP ^-)) && (! -z $2) ]]
	    then
		EXCLUDES="$EXCLUDES $2"; shift
	    else
		echo "\nERROR: Hostname to exclude incorrectly specified. Use -x.\n\n"
		print_usage
	    fi;;
	-X) if [[ (-z $(echo $2 | $GREP ^-)) && (! -z $2) ]]
	    then
		EXCLUDEHGS="$EXCLUDEHGS $2"; shift
	    else
		echo "\nERROR: Hostgroup to exclude incorrectly specified. Use -X.\n\n"
		print_usage
	    fi;;
	-f) if [[ (-z $(echo $2 | $GREP ^-)) && (! -z $2) ]]
	    then
		if [[ -d $(dirname $2) ]]
		then
		    KEEPPASSWORDLIST="1"
		    PASSWORDLIST="$2"
		    echo > $PASSWORDLIST; chown root:root $PASSWORDLIST; chmod 0600 $PASSWORDLIST
		    shift
		else
		    echo "\nERROR: Output directory for password list does not exist. Use -f.\n\n"
		    print_usage
		fi
	    else
		echo "\nERROR: Password list file not specified. Use -f.\n\n"
		print_usage
	    fi;;
	-d) DEBUG="1";;
	-o) if [[ (-z $(echo $2 | $GREP ^-)) && (! -z $2) ]]
	    then
		if [[ -d $(dirname $2) ]]
		then
		    DEBUGFILE="$2"
		    echo > $DEBUGFILE; chown root:root $DEBUGFILE; chmod 0600 $DEBUGFILE
		    shift
		else
		    echo "\nERROR: Output directory for debug log does not exist. Use -o.\n\n"
		    print_usage
		fi
	    else
		echo "\nERROR: Debug log output file not specified. Use -o.\n\n"
		print_usage
	    fi;;
	-*) print_usage;;
	*)  print_usage;;	
    esac
    shift
done


# Some final checks and initialisation

if [[ $DEBUG -gt 0 ]]
then
	echo "=== SETUP ===" >> $DEBUGFILE
	echo "OS name is $(uname)" >> $DEBUGFILE
	echo "BOKSSBIN is $BOKSSBIN" >> $DEBUGFILE
	echo "BOKSBIN is $BOKSBIN" >> $DEBUGFILE
	echo "BOKSLIB is $BOKSLIB" >> $DEBUGFILE
	echo "BOKSETC is $BOKSETC \n" >> $DEBUGFILE
	echo "Parameters passed to script are:" >> $DEBUGFILE
	echo "HOSTNAMES = $HOSTNAMES" >> $DEBUGFILE
	echo "HOSTGROUPS = $HOSTGROUPS" >> $DEBUGFILE
	echo "HOSTFILE = $HOSTFILE" >> $DEBUGFILE
	echo "ALL = $ALL" >> $DEBUGFILE
	echo "EXCLUDES = $EXCLUDES" >> $DEBUGFILE
	echo "EXCLUDEHGS = $EXCLUDEHGS" >> $DEBUGFILE
	echo "KEEPPASSWORDLIST = $KEEPPASSWORDLIST" >> $DEBUGFILE
	echo "PASSWORDLIST = $PASSWORDLIST" >> $DEBUGFILE
	echo "DEBUG = $DEBUG" >> $DEBUGFILE
	echo "DEBUGFILE = $DEBUGFILE \n" >> $DEBUGFILE
fi



### DEFINING SUBROUTINES ###


GeneratePassword()
{
	[[ $DEBUG -gt 0 ]] && echo "\n=== STARTING GeneratePassword ===\n" >> $DEBUGFILE

	echo "$RANDOM $RANDOM $RANDOM $RANDOM $RANDOM $RANDOM $RANDOM $RANDOM" |
	awk '{split("a b c d e f g h i j k m n p q r s t u w x y z 1 2 3 4 5 6 7 8 9 A B C D E F G H J K L M N P Q R S T U V W X Y Z",m)
	    a=$1%50+1;b=$2%50+1;c=$3%50+1;d=$4%50+1;e=$5%50+1;f=$6%50+1;g=$7%50+1;h=$8%50+1
	    print m[a]m[b]m[c]m[d]m[e]m[f]m[g]m[h]
	}' | read PASSWORD

	[[ $DEBUG -gt 0 ]] && echo "Generated password is $PASSWORD.\n" >> $DEBUGFILE
}

EncryptPassword()
{
	[[ $DEBUG -gt 0 ]] && echo "\n=== STARTING EncryptPassword ===\n" >> $DEBUGFILE

	# Following inspired by http://www.cyberciti.biz/tips/howto-write-shell-script-to-add-user.html
	# However, this was unsafe because of the unchanging salt.
	# ENCPASS=$(perl -e 'print crypt($ARGV[0], "password")' $PASSWORD)

	# Following was designed and written by Hans Feringa. 
	# My utmost gratitude and a round of beer to him.

	ENCPASSWORD=$(echo $PASSWORD | perl -ne '
	BEGIN { @salt = ( ".", "/", 0 .. 9, "A" .. "Z", "a" .. "z" ); }; chomp;
	$pwd = $_;
	print crypt($pwd, join "", @salt[rand 64, rand 64]) . "\n"; 
	')

	[[ $DEBUG -gt 0 ]] && echo "Encrypted password is $ENCPASSWORD.\n" >> $DEBUGFILE
}

GenerateHostList()
{
	[[ $DEBUG -gt 0 ]] && echo "\n=== STARTING GenerateHostList ===\n" >> $DEBUGFILE

	rm $HOSTLIST 2>/dev/null; touch $HOSTLIST
	if [[ ! -f $HOSTLIST ]] 
	then
	    echo "ERROR: Failed to create list of hosts in $TEMPDIR."
	    exit 1
	fi
	
	if [[ $ALL -gt 0 ]] 
	then
            [[ $DEBUG -gt 0 ]] && echo "Parameter -A passed. Adding all hosts to hostlist." >> $DEBUGFILE
	    $BOKSSBIN/lsbks -l *:root | $GREP -v ^: | $GREP -v ^- | $AWK -F: '{print $1}' > $HOSTLIST 2>/dev/null
	else
	    if [[ $HOSTNAMES != "" ]]
	    then
        	[[ $DEBUG -gt 0 ]] && echo "Hostnames passed with -h parameter. Adding to hostlist." >> $DEBUGFILE
		for HOST in $(echo $HOSTNAMES); do echo $HOST >> $HOSTLIST; done
	    fi
	
	    if [[ $HOSTGROUPS != "" ]]
	    then
        	[[ $DEBUG -gt 0 ]] && echo "Hostgroups passed with -H parameter. Resolving and adding to hostlist." >> $DEBUGFILE
		for HG in $(echo $HOSTGROUPS); do hgrpadm -l -g $HG | $AWK '{print $2}' | $GREP -v ^->> $HOSTLIST; done
	    fi
	
	    if [[ $HOSTFILE != "" ]]
	    then
        	[[ $DEBUG -gt 0 ]] && echo "List of hosts passed with -i parameter. Adding to hostlist." >> $DEBUGFILE
		cat $HOSTFILE | $GREP -v ^- >> $HOSTLIST
	    fi
	fi

	cat $HOSTLIST | sort -u > $HOSTLIST.temp; mv $HOSTLIST.temp $HOSTLIST
	NUMHOSTS=$(wc -l $HOSTLIST | $AWK '{print $1}')

	[[ $DEBUG -gt 0 ]] && echo "Host list is $NUMHOSTS lines." >> $DEBUGFILE
}


GenerateExcludeList()
{
	[[ $DEBUG -gt 0 ]] && echo "\n=== STARTING GenerateExcludeList ===\n" >> $DEBUGFILE

	rm $EXCLUDELIST 2>/dev/null; touch $EXCLUDELIST
	if [[ ! -f $EXCLUDELIST ]] 
	then
	    echo "ERROR: Failed to create exclude list in $TEMPDIR."
	    exit 1
	fi
	
	if [[ $EXCLUDES != "" ]]
	then
            [[ $DEBUG -gt 0 ]] && echo "Hostnames passed with -x parameter. Adding to exclude list." >> $DEBUGFILE
	    for HOST in $(echo $EXCLUDES); do echo $HOST >> $EXCLUDELIST; done
	fi
	
	if [[ $EXCLUDEHGS != "" ]]
	then
            [[ $DEBUG -gt 0 ]] && echo "Hostgroups passed with -X parameter. Resolving and adding to exclude list." >> $DEBUGFILE
	    for HG in $(echo $EXCLUDEHGS); do hgrpadm -l -g $HG | $AWK '{print $2}' | $GREP -v ^- >> $EXCLUDELIST; done
	fi

	cat $EXCLUDELIST | sort -u > $EXCLUDELIST.temp; mv $EXCLUDELIST.temp $EXCLUDELIST
	NUMEXCLUDE=$(wc -l $EXCLUDELIST | $AWK '{print $1}')

	[[ $DEBUG -gt 0 ]] && echo "Exclude list is $NUMEXCLUDE lines." >> $DEBUGFILE
}


TestConnection()
{
	[[ $DEBUG -gt 0 ]] && echo "\n=== STARTING TestConnection ===\n" >> $DEBUGFILE

	if [[ -z $($BOKSSBIN/hostadm -l -h $HOST | $GREP UNIXBOKSHOST) ]]
	then
		echo "$HOST\t$PASSWORD\tERROR: Host is not a BoKS client."
		let FAIL=$FAIL+1
		[[ $DEBUG -gt 0 ]] && echo "ERROR: $HOST is not a BoKS client." >> $DEBUGFILE
		return 1
	fi

	IP=$($BOKSSBIN/hostadm -l -h $HOST | $GREP ^IP-address | $AWK '{print $2}')
        $PING $IP $PINGPARAM >/dev/null 2>&1
        PINGSTATUS=$?

        $BOKSLIB/cadm -l -f bcastaddr -h $HOST >/dev/null 2>&1
        CADMSTATUS=$?

        if [[ ($PINGSTATUS -gt 0) && ($CADMSTATUS -gt 0) ]]
	then
	    echo "$HOST\t$PASSWORD\tERROR: Host is not reachable."
	    let FAIL=$FAIL+1
	    [[ $DEBUG -gt 0 ]] && echo "ERROR: Host $HOST is unreachable." >> $DEBUGFILE
	    return 1
	fi

	return 0
}


TestPassword()
{
	#   This subroutine retrieves the root password for $HOST as its stored
	# in the three BoKS locations: the BoKS database, the local BRPF file and 
	# the local shadow file.
	#   It is assumed that the password is stored in the 18th field of table 1
	# of the BoKS database. Because this isn't highly reliable, instead we'll
	# use the lsbks -p command to get the password from the database.

	[[ $DEUBG -gt 0 ]] && echo "\n=== STARTING TestRemoteClient ===\n" >> $DEBUGFILE

	BOKSPASS=$($BOKSSBIN/lsbks -p -l $HOST:root | $AWK -F: '{print $3}')
	BRPFPASS=$($BOKSLIB/cadm -l -f brpf -h $HOST)
	SHDWPASS=$($BOKSLIB/cadm -l -f passwd -h $HOST | $GREP root | head -1 | $AWK -F: '{print $2}')

	if [[ $DEBUG -gt 0 ]]
	then
	    echo "INFO: re-setting ENCPASSWD to BOKSPASSWD" >> $DEBUGFILE
	    ENCPASSWORD="$BOKSPASS"
	fi

	if [[ ($ENCPASSWORD == $BOKSPASS) && ($ENCPASSWORD == $BRPFPASS) && ($ENCPASSWORD == $SHDWPASS) ]] 
	then
	    [[ $DEBUG -gt 0 ]] && echo "Password correctly verified for $HOST." >> $DEBUGFILE

	    if [[ $KEEPPASSWORDLIST -gt 0 ]] 
	    then
		echo "$HOST\t$PASSWORD\t$ENCPASSWORD" >> $PASSWORDLIST
	    else
		echo "$HOST\t$PASSWORD\t$ENCPASSWORD"
	    fi
	else
	    if [[ $DEBUG -gt 0 ]]
	    then
		echo "ERROR: Error in remote file." >> $DEBUGFILE
		echo "ENCPASSWORD is $ENCPASSWORD" >> $DEBUGFILE
		echo "BOKSPASS is $BOKSPASS" >> $DEBUGFILE
		echo "BRPFPASS is $BRPFPASS" >> $DEBUGFILE
		echo "SHDWPASS is $SHDWPASS" >> $DEBUGFILE
		echo "Fail count is now upped to $FAIL." >> $DEBUGFILE
	    fi

	    if [[ $KEEPPASSWORDLIST -gt 0 ]] 
	    then
	    echo "$HOST\t$PASSWORD\tERROR: discrepancy in remote file\n" >> $PASSWORDLIST
	    else
	    echo "$HOST\t$PASSWORD\tERROR: discrepancy in remote file\n"
	    fi

	    let FAIL=$FAIL+1
	fi
}



### FINALLY, THE MAIN ROUTINE ###

if [[ $DEBUG -gt 0 ]]
then
	echo "\n=== STARTING MAIN PHASE ===\n $(date) \n" >> $DEBUGFILE
	echo "\nINFO\tDebug mode. NOT changing any passwords.\n"
fi

GenerateHostList
GenerateExcludeList

COUNT="0"; FAIL="0"

for HOST in $(cat $HOSTLIST)
do
	[[ $DEBUG -gt 0 ]] && echo "\n=== STARTING FOR HOST $HOST ===\n" >> $DEBUGFILE

        if [[ (! -z $($GREP -x $HOST $EXCLUDELIST)) || (-z $(lsbks -l $HOST:root)) ]]
	then
		echo "$HOST\tEXCLUDED\tHost excluded by choice."
		[[ $DEBUG -gt 0 ]] && echo "INFO: Host $HOST is on exclude list." >> $DEBUGFILE
		continue
	fi

	TestConnection

	if [[ $? -gt 0 ]] 
	then
		# Needed to build in delay to prevent duplicate passwords.
		# Apparently the KSH random generator is not so random that it generates
		# different numbers every time it gets called.
		sleep 1 
		continue
	fi

	GeneratePassword
	# EncryptPassword (disabled, see below)

	if [[ $DEBUG -eq 0 ]]
	then
		# Replaced the following for three very good reasons:
		# 1. The password modification gets logged for auditing.
		# 2. The "password last changed" field gets updated.
		# 3. It removes the needs for my half-arsed crypt routine.
		# $BOKSSBIN/modbks -l $HOST:root -p "$ENCPASSWORD"

		$BOKSLIB/boksauth -c FUNC="change-psw" USER="$HOST:root" OLDPSW="" NEWPSW="$PASSWORD" UID="0"
		[[ $? -gt 0 ]] && echo "$HOST\t$PASSWORD\tERROR: boksauth password change failed."

		ENCPASSWORD=$($BOKSSBIN/lsbks -p -l $HOST:root | $AWK -F: '{print $3}')

	else

		echo "INFO: Not setting the new root password:" >> $DEBUGFILE
		echo "$BOKSLIB/boksauth -c FUNC=\"change-psw\" USER=\"$HOST:root\" OLDPSW=\"\" NEWPSW=\"$PASSWORD\" UID=\"0\"" >> $DEBUGFILE
	fi
	
	# Replaced a "boksdiag updpsw" followed by "sleep" with a "sleep 15".
	# The original occasionally led to overloading the BoKS master, which resulted
	# in dozens of stuck client queues which had to be manually cleared.
	sleep 15

	TestPassword

	let COUNT=$COUNT+1
done


if [[ $DEBUG -gt 0 ]] 
then
	echo "\n\n=== SCRIPT FINISHED ===\n\n" >> $DEBUGFILE
	echo "Ran for $COUNT root accounts ($NUMHOSTS total, $NUMEXCLUDE excluded, $FAIL failed).\n" >> $DEBUGFILE
	date >> $DEBUGFILE
fi

echo "\n\nDone with updating of root passwords.\n"
echo "Ran for $COUNT root accounts ($NUMHOSTS total, $NUMEXCLUDE excluded, $FAIL failed).\n"

rm -rf $TEMPDIR
