#!/bin/sh

# ClonePanel - Manages duplicate accounts on two or more webservers,
# including snapshot backups, monitoring and failover dns.
# Copyright (C)2006 Chris Cheers, Internet Lynx.
# Contact chris[at]clonepanel[dot]com.
# Internet Lynx, PO Box 7117, Mannering Park, NSW 2259, Australia

# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

Version=0.33
# failover -h for built-in documentation

set -u
# Be strict about variable declaration

unset PATH
#avoid use of $PATH - limit script to system commands we choose

PROGRAM_DIR=$(/bin/echo $0 |/bin/sed -e "s/\/[a-zA-Z0-9_]*$//")
# Extract the working directory from command line
# NB - if your system doesn't have echo and sed in these locations
# then this line will need to be changed in all shell scripts

# Standard include files:
. $PROGRAM_DIR/includes -q
# quiet mode to suppress multiple copyleft messages on auto-failover

# Built-in documentation:
function showdocs {
	$CAT <<-EODOC
		Called manually or by set_status script, failover:
		Modifies zone files for specified account on status change.
		Attempts to update dns servers on hosts in working state 
		(GREEN / AMBER / CYAN by default), with fall-back to try again
		repeatedly using set_dns delayed task
		Updates on hosts in non-working state (RED / DISABLED) go
		straight to delayed task list
		Inputs may be given as command line parameters or interactively.
		
		Usage:
		./failover
		Command line options: 
		-h		Print these instructions and exit
		-u user		CPanel username of the account
		-H HOST		Host name (nickname)
		-z domain1 -z domain2	Domain / zone names (multiple permitted)
				(optional - if omitted do all domains)
		-a action	Allowable options are $FAILOVER_ACTIONS (optional -
				if omitted use default action for host's current status)
		-v 		verify failover allowed for this user / zone
				(normally when called automatically by set_status)
		-d 		Dummy run - don't make changes or set delayed tasks

EODOC
	exit $DOC_REQUEST
}

function add_task {
# Add a new job to the pending_tasks directory, unless there already
# add_task command
	task_exists=''
	if [ "`$LS $PENDING_TASKS_DIR`" ]; then
		task_exists=`$GREP "$2" $PENDING_TASKS_DIR/*`
	fi
	if [ ! "$task_exists" ]; then
		highest=`$FIND $PENDING_TASKS_DIR -maxdepth 1 -type f -printf "%f\n" | $SORT -n | $TAIL -n 1`
# highest numeric existing entry
		if [ ! "$highest" ]; then
			highest=1000
		fi
		taskfile=$($EXPR $highest + 1 )
		$ECHO "host='$1'" >$PENDING_TASKS_DIR/$taskfile
		$ECHO "command='$2'" >>$PENDING_TASKS_DIR/$taskfile
                checkerror $? $E_WRITE_TASK_FAILED
	fi
}

# Initialise own input variables:
user=''
host=''
zone=''
mzones=()
action=''
verify='n'
dummy='n'

# Start real script

# Check for help request and input variables in the command line options:
while getopts ":hH:u:z:a:vd" opt
do
	case $opt in
		h)	showdocs
			;;
		u)	user=$OPTARG
			;;
		H)	host=$OPTARG
			;;
		z)	mzones[${#mzones[@]}]=$OPTARG
			;;
		a)	action=$OPTARG
			;;
		v)	verify='y'
			;;
		d)	dummy='y'
			;;
		*)	$ECHO "Unknown option. Use -h for instructions"; exit $E_UNKNOWN_OPT
			;;
	esac
done
shift $(($OPTIND - 1))

OPTIND=1
# reset getopts for next time

if [ ! $user ]; then
	cecho -n -c $prompt "Enter account name: "
	read user
fi

if [ ! $host ]; then
	cecho -n -c $prompt "Enter host name (nickname): "
	read host
fi

if [ ! $user -o ! $host ]; then
	cecho -c $error "User and host names must be specified"
	exit $E_BLANK_INPUTS
fi

config -u $user -H $host
# Config specific to this user and host
 
host_ip=$SERVER
# Get remote server ip address from hosts file
if [ ! $host_ip ]; then
	cecho -c $error "Host name not found - check hosts/$host/config for SERVER"
	exit $E_NO_HOST
fi

current_status=`$GREP "^$host=" $STATUS_FILE |$CUT -d= -f2`
if [ ! $current_status ]; then
	cecho -c $error "No match for $host found in $STATUS_FILE"
	exit $E_NO_STATUS
fi

m_status=`$GREP "^$MY_NAME=" $STATUS_FILE |$CUT -d= -f2`
if [ ! $m_status ]; then
	cecho -c $error "No match for $MY_NAME found in $STATUS_FILE"
	exit $E_NO_STATUS
fi

if [ ! "$action" ]; then
# Use default action for current_status
	eval action=\$"FAILOVER_ACTION_$current_status"
fi

# Now check that the monitor server itself is ok. If not, don't let it trigger
# a failover, but don't interfere with prepare / restore
if [ "`$ECHO $FAILOVER_TRUSTED_STATUS |$GREP -v $m_status`" ]; then
        if [ "$action" == "FAILOVER" ]; then
		cecho -c $error "Monitor server status is $m_status - not trusted to failover (aborting)"
		exit $E_NOT_TRUSTED
	fi
fi

if [ ${#mzones[*]} -eq 0 ]; then
# Search the zones file for each domain hosted in this account to see if affected
	allzones=`$FIND $ZONES_DIR -maxdepth 1 -mindepth 1 -type d -printf "%f "`
	for zone in $allzones
	do
		WEB_SLAVE=''
		WEB_MASTER=''
# Initialize these in case not defined by the account config
		config -u $user -H $host -z $zone
# First check if failover enabled for this user / zone
		if [ $FAILOVER_ENABLE != "y" ]; then
# config settings - auto failover not enabled for this user / zone
			if [ "$verify" == "y" ]; then
#				cecho -c $warning "Auto-failover not enabled for $zone."
				continue
			else
				cecho -c $info "Note: auto-failover not enabled for $zone. Manual run ok"
			fi
		fi
# This domain will be affected by the status change if this host is its web_master
		if [ "$host" != "$WEB_MASTER" ]; then
			cecho -c $info "Zone $zone not affected by this change"
			continue
		fi
# Failover possible only if this host has a web_slave
		if [ ! "$WEB_SLAVE" ]; then
			cecho -c $warning "Zone $zone has no slave - failover not possible"
			continue
		fi
# Failover not useful if the web_slave isn't currently in an "alive" state
		slave_status=`$GREP "^$WEB_SLAVE=" $STATUS_FILE |$CUT -d= -f2`
		if [ ! $slave_status ]; then
			cecho -c $error "No match for $WEB_SLAVE found in $STATUS_FILE"
			continue
		fi
		if [ ! "`$ECHO $STATUS_CODES_ALIVE |$GREP $slave_status`" ]; then
# Slave isn't ready - don't allow failover (prepare / restore ok)
			if [ $action == "FAILOVER" ]; then
				cecho -c $warning "Slave $WEB_SLAVE for Zone $zone is $slave_status - failover not possible"
				continue
			fi
		fi

# If we get this far the zone will be affected and can usefully be updated
		mzones[${#mzones[@]}]=$zone
	done
fi
if [ ${#mzones[*]} -eq 0 ]; then
	if [ "$verify" != "y" ]; then
#suppress repeated warnings on automatic status change
		cecho -c $error "No zones in $user account can failover from $host"
	fi
	exit $E_NO_ZONE
fi

if [ ! $action -o ! "`$ECHO $FAILOVER_ACTIONS |$GREP $action`" ]; then
	cecho -c $error "Unknown action keyword - use $FAILOVER_ACTIONS"
	exit $E_UNKNOWN_ACTION
fi

# (Finally) finished setup and checking - now do something...

if [ "$dummy" == "y" ]; then
# Dummy run - display modified zone file then stop
	for zone in ${mzones[*]}
	do
		config -u $user -H $host -z $zone
		$CP $ZONE_DIR/$ZONE_FILENAME $TEMP_DIR/$ZONE_FILENAME
# Use perl script to make the changes to temporary copy of zone.db file:
		$PERL -I $PROGRAM_DIR $PROGRAM_DIR/$DNS_MODIFY_PROGRAM $TEMP_DIR/$ZONE_FILENAME $ZONE_DIR/$action.ov $ROLES_FILE $HOSTS_DIR $HOSTS_CONFIG_FILENAME $zone
		cecho -c $info "Dummy run - modified zone file would be:"
		$CAT $TEMP_DIR/$ZONE_FILENAME
		$RM $TEMP_DIR/$ZONE_FILENAME
	done
else
# Not a dummy run - modify zone file and send to dns servers
	for zone in ${mzones[*]}
	do
		config -u $user -H $host -z $zone
# Use perl script to make the changes to zone.db files:
		$PERL -I $PROGRAM_DIR $PROGRAM_DIR/$DNS_MODIFY_PROGRAM $ZONE_DIR/$ZONE_FILENAME $ZONE_DIR/$action.ov $ROLES_FILE $HOSTS_DIR $HOSTS_CONFIG_FILENAME $zone
		if [ $? -ne 0 ]; then
			cecho -c $error "Failed to modify dns zone"
			exit $E_MOD_DNS_FAILED
		fi
# Then send updated zone file to each dns server...
		dnshost=''
		for dnshost in $DNS_MASTER $DNS_SLAVE
# todo - allow for multiple slaves
		do
			dnshost_status=`$GREP "^$dnshost=" $STATUS_FILE |$CUT -d= -f2`
#Both dns hosts and their current status from status file
			update_done=n
			if [ "`$ECHO $STATUS_CODES_ALIVE |$GREP $dnshost_status`" ]; then
				$PROGRAM_DIR/$DNS_UPDATE_PROGRAM -u $user -z $zone -H $dnshost
# Immediate update if dns host is available
				if [ $? -eq 0 ]; then
					cecho -c $info "Immediate update of $zone complete on $dnshost"
					update_done=y
				fi
			fi
			if [ $update_done != "y" ]; then
# Status doesn't permit immediate update, or immediate update failed
				add_task $dnshost "$PROGRAM_DIR/$DNS_UPDATE_PROGRAM -u $user -z $zone -H $dnshost"
				cecho -c $info "Scheduled update of $zone on $dnshost"
# If status is down, run it as a pending task through error_manager until successful
			fi
		done
		if [ ! $dnshost ]; then
			cecho -c $error "No DNS_ records found in $roles"
			exit $E_NO_HOST
		fi
	done
fi
cecho -c $ok "Done action $action" ;
 
