#!/bin/sh
#
#	Description:	IPsrcaddr - Preferred source(/dest) address modification
#
#	Author:			John Sutton <john@scl.co.uk>
#	Support:		users@clusterlabs.org
#	License:		GNU General Public License (GPL)
#	Copyright:		SCL Internet
#
#	Based on the IPaddr script.
#
#	This script manages the preferred source address associated with
#	packets which originate on the localhost and are routed through the
#	matching route.  By default, i.e. without the use of this script or
#	similar, these packets will carry the IP of the primary i.e. the
#	non-aliased interface.  This can be a nuisance if you need to ensure
#	that such packets carry the same IP irrespective of which host in
#	a redundant cluster they actually originate from.
#
#	It can add a preferred source address, or remove one.
#
#	usage: IPsrcaddr {start|stop|status|monitor|validate-all|meta-data}
#
#	The "start" arg adds a preferred source address.
#
#	Surprisingly, the "stop" arg removes it.	:-)
#
#	NOTES:
#
#	1) There must be one and not more than 1 matching route!  Mainly because
#	I can't see why you should have more than one.  And if there is more
#	than one, we would have to box clever to find out which one is to be
#	modified, or we would have to pass its identity as an argument.
#
#	2) The script depends on Alexey Kuznetsov's ip utility from the
#	iproute aka iproute2 package.
#
#	3) No checking is done to see if the passed in IP address can
#	reasonably be associated with the interface on which the default
#	route exists.  So unless you want to deliberately spoof your source IP,
#	check it!  Normally, I would expect that your haresources looks
#	something like:
#
#		nodename ip1 ip2 ... ipN IPsrcaddr::ipX
#
#	where ipX is one of the ip1 to ipN.
#
#	  OCF parameters are as below:
#		OCF_RESKEY_ipaddress

#######################################################################
# Initialization:
: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat}
. ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs
. ${OCF_FUNCTIONS_DIR}/findif.sh

# Defaults
OCF_RESKEY_ipaddress_default=""
OCF_RESKEY_cidr_netmask_default=""
OCF_RESKEY_destination_default="0.0.0.0/0"
OCF_RESKEY_proto_default=""
OCF_RESKEY_metric_default=""
OCF_RESKEY_pref_default=""
OCF_RESKEY_table_default=""

: ${OCF_RESKEY_ipaddress=${OCF_RESKEY_ipaddress_default}}
: ${OCF_RESKEY_cidr_netmask=${OCF_RESKEY_cidr_netmask_default}}
: ${OCF_RESKEY_destination=${OCF_RESKEY_destination_default}}
: ${OCF_RESKEY_proto=${OCF_RESKEY_proto_default}}
: ${OCF_RESKEY_metric=${OCF_RESKEY_metric_default}}
: ${OCF_RESKEY_pref=${OCF_RESKEY_pref_default}}
: ${OCF_RESKEY_table=${OCF_RESKEY_table_default}}
#######################################################################

[ -z "$OCF_RESKEY_proto" ] && PROTO="" || PROTO="proto $OCF_RESKEY_proto"
[ -z "$OCF_RESKEY_table" ] && TABLE="" || TABLE="table $OCF_RESKEY_table"

USAGE="usage: $0 {start|stop|status|monitor|validate-all|meta-data}";

echo "$OCF_RESKEY_ipaddress" | grep -q ":" && FAMILY="inet6" || FAMILY="inet"
[ "$FAMILY" = "inet6" ] && [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] && OCF_RESKEY_destination="::/0"

  CMDSHOW="$IP2UTIL -f $FAMILY route show   $TABLE to exact $OCF_RESKEY_destination"
CMDCHANGE="$IP2UTIL -f $FAMILY route change to "

if [ "$OCF_RESKEY_destination" != "0.0.0.0/0" ] && [ "$OCF_RESKEY_destination" != "::/0" ]; then
	CMDSHOW="$CMDSHOW src $OCF_RESKEY_ipaddress"
fi

if [ "$OCF_RESKEY_table" = "local" ]; then
	TABLE="$TABLE local"
fi

SYSTYPE="`uname -s`"

usage() {
	echo $USAGE >&2
}

meta_data() {
	cat <<END
<?xml version="1.0"?>
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
<resource-agent name="IPsrcaddr" version="1.0">
<version>1.0</version>

<longdesc lang="en">
Resource script for IPsrcaddr. It manages the preferred source address
modification. 

Note: DHCP should not be enabled for the interface serving the preferred
source address. Enabling DHCP may result in unexpected behavior, such as
the automatic addition of duplicate or conflicting routes. This may
cause the IPsrcaddr resource to fail, or it may produce undesired
behavior while the resource continues to run.
</longdesc>
<shortdesc lang="en">Manages the preferred source address for outgoing IP packets</shortdesc>

<parameters>
<parameter name="ipaddress" unique="0" required="1">
<longdesc lang="en">
The IP address. 
</longdesc>
<shortdesc lang="en">IP address</shortdesc>
<content type="string" default="${OCF_RESKEY_ipaddress_default}" />
</parameter>

<parameter name="cidr_netmask">
<longdesc lang="en">
The netmask for the interface in CIDR format. (ie, 24), or in
dotted quad notation  255.255.255.0).
</longdesc>
<shortdesc lang="en">Netmask</shortdesc>
<content type="string" default="${OCF_RESKEY_cidr_netmask_default}"/>
</parameter>

<parameter name="destination">
<longdesc lang="en">
The destination IP/subnet for the route (default: $OCF_RESKEY_destination_default)
</longdesc>
<shortdesc lang="en">Destination IP/subnet</shortdesc>
<content type="string" default="${OCF_RESKEY_destination_default}" />
</parameter>

<parameter name="proto">
<longdesc lang="en">
Proto to match when finding network. E.g. "kernel".
</longdesc>
<shortdesc lang="en">Proto</shortdesc>
<content type="string" default="${OCF_RESKEY_proto_default}" />
</parameter>

<parameter name="metric">
<longdesc lang="en">
Metric. Only needed if incorrect metric value is used.
</longdesc>
<shortdesc lang="en">Metric</shortdesc>
<content type="string" default="${OCF_RESKEY_metric_default}" />
</parameter>

<parameter name="pref">
<longdesc lang="en">
IPv6 route preference (low, medium or high). Only needed if incorrect pref value is used.
</longdesc>
<shortdesc lang="en">IPv6 route preference.</shortdesc>
<content type="string" default="${OCF_RESKEY_pref_default}" />
</parameter>

<parameter name="table">
<longdesc lang="en">
Table to modify and use for interface lookup. E.g. "local".

The table has to have a route matching the "destination" parameter.

This can be used for policy based routing. See man ip-rule(8).
</longdesc>
<shortdesc lang="en">Table</shortdesc>
<content type="string" default="${OCF_RESKEY_table_default}" />
</parameter>

</parameters>

<actions>
<action name="start" timeout="20s" />
<action name="stop" timeout="20s" />
<action name="monitor" depth="0" timeout="20s" interval="10s" />
<action name="validate-all" timeout="5s" />
<action name="meta-data" timeout="5s" />
</actions>
</resource-agent>
END
}

errorexit() {
	ocf_exit_reason "$*"
	exit $OCF_ERR_GENERIC
}

#
#	We can distinguish 3 cases: no preferred source address, a
#	preferred source address exists which matches that specified, and one
#	exists but doesn't match that specified.  srca_read() returns 1,0,2
#	respectively.
#
#	The output of route show is something along the lines of:
#
#		default via X.X.X.X dev eth1 src Y.Y.Y.Y
#
#	where the src clause "src Y.Y.Y.Y" may or may not be present

WS="[[:blank:]]"
case "$FAMILY" in
	inet)
		GROUP="[0-9]\{1,3\}"
		IPADDR="\($GROUP\.\)\{3\}$GROUP"
		;;
	inet6)
		GROUP="[0-9a-f]\{0,4\}"
		IPADDR="\($GROUP\:\)\{0,\}$GROUP"
		;;
esac
SRCCLAUSE="src$WS$WS*\($IPADDR\)"
MATCHROUTE="\(.*${WS}\)proto [^ ]\+\(.*${WS}\)\($SRCCLAUSE\)\($WS.*\|$\)"
METRICCLAUSE=".*\(metric$WS[^ ]\+\).*"
PROTOCLAUSE=".*\(proto$WS[^ ]\+\).*"
PREFCLAUSE=".*\(pref$WS[^ ]\+\).*"
FINDIF=findif

# findif needs that to be set
export OCF_RESKEY_ip=$OCF_RESKEY_ipaddress

srca_read() {
	# Capture matching route - doublequotes prevent word splitting...
	ROUTE="`$CMDSHOW dev $INTERFACE 2> /dev/null`" || errorexit "command '$CMDSHOW' failed"

	# ... so we can make sure there is only 1 matching route
	[ 1 -eq `echo "$ROUTE" | wc -l` ] || \
		errorexit "more than 1 matching route exists"

	# But there might still be no matching route
	([ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] || [ "$OCF_RESKEY_destination" = "::/0" ]) && [ -z "$ROUTE" ] && \
		 ! ocf_is_probe && [ "$__OCF_ACTION" != stop ] && errorexit "no matching route exists"

	# Sed out the source ip address if it exists
	SRCIP=`echo $ROUTE | sed -n "s/$MATCHROUTE/\4/p"`

	# and what remains after stripping out the source ip address clause
	ROUTE_WO_SRC=`echo $ROUTE | sed "s/$MATCHROUTE/\1\2\6/"`

	# using "src <ip>" only returns output if there's a match
	if [ "$OCF_RESKEY_destination" != "0.0.0.0/0" ] && [ "$OCF_RESKEY_destination" != "::/0" ]; then
		[ -z "$ROUTE" ] && return 1 || return 0
	fi

	[ -z "$SRCIP" ] && return 1
	[ $SRCIP = $1 ] && return 0
	[ "$__OCF_ACTION" = "monitor" ] || [ "$__OCF_ACTION" = "status" ] && [ "${ROUTE%% *}" = "default" ] && return 1
	return 2
}

#
#	Add (or change if it already exists) the preferred source address
#	The exit code should conform to LSB exit codes.
#

srca_start() {
	srca_read $1

	rc=$?
	if [ $rc = 0 ]; then 
		rc=$OCF_SUCCESS
		ocf_log info "The ip route has been already set.($NETWORK, $INTERFACE, $ROUTE_WO_SRC)"
	else
		# NetworkManager manages routes with proto static/kernel
		[ -z "$OCF_RESKEY_proto" ] && echo "$PROTO" | grep -q "proto \(kernel\|static\)" && PROTO="proto keepalived"

		$IP2UTIL route replace $TABLE $NETWORK dev $INTERFACE $PROTO src $1 $METRIC $PREF || \
			errorexit "command 'ip route replace $TABLE $NETWORK dev $INTERFACE $PROTO src $1 $METRIC $PREF' failed"

		if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] || [ "$OCF_RESKEY_destination" = "::/0" ]; then
			$CMDCHANGE $ROUTE_WO_SRC dev $INTERFACE $PROTO src $1 || \
				errorexit "command '$CMDCHANGE $ROUTE_WO_SRC dev $INTERFACE $PROTO src $1' failed"
		fi
		rc=$?
	fi

	return $rc
}

#
#	Remove (if it exists) the preferred source address.
#	If one exists but it's not the same as the one specified, that's
#	an error.  Maybe that's the wrong behaviour because if this fails
#	then when IPaddr releases the associated interface (if there is one)
#	your matching route will also get dropped ;-(
#	The exit code should conform to LSB exit codes.
#

srca_stop() {
	srca_read $1
	rc=$?

	if [ $rc = 1 ]; then
	# We do not have a preferred source address for now
	  ocf_log info "No preferred source address defined, nothing to stop"
	  exit $OCF_SUCCESS
	fi
	  
	[ $rc = 2 ] && errorexit "The address you specified to stop does not match the preferred source address"

	if [ -z "$TABLE" ] || [ "${TABLE#table }" = "main" ]; then
		SCOPE="link"
	else
		SCOPE="host"
	fi

	PRIMARY_IP="$($IP2UTIL -4 -o addr show dev $INTERFACE primary | awk '{split($4,a,"/");print a[1]}')"
	OPTS="proto kernel scope $SCOPE"
	[ "$FAMILY" = "inet" ] && OPTS="$OPTS src $PRIMARY_IP"

	$IP2UTIL route replace $TABLE $NETWORK dev $INTERFACE $OPTS $METRIC $PREF || \
		errorexit "command 'ip route replace $TABLE $NETWORK dev $INTERFACE $OPTS $METRIC $PREF' failed"

	if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] || [ "$OCF_RESKEY_destination" = "::/0" ]; then
		$CMDCHANGE $ROUTE_WO_SRC dev $INTERFACE proto static || \
			errorexit "command '$CMDCHANGE $ROUTE_WO_SRC dev $INTERFACE proto static' failed"
	fi

	return $?
}

srca_status() {
	srca_read $1

	case $? in
		0)	echo "OK"
			return $OCF_SUCCESS;;

		1)	echo "No preferred source address defined"
			return $OCF_NOT_RUNNING;;

		2)	echo "Preferred source address has incorrect value"
			return $OCF_ERR_GENERIC;;
	esac
}

# A not reliable IP address checking function, which only picks up those _obvious_ violations...
#
# It accepts IPv4 address in dotted quad notation, for example "192.168.1.1"
#
# 100% confidence whenever it reports "negative", 
# but may get false "positive" answer. 
# 
CheckIP() {
  ip="$1"
  case $ip in
    *[!0-9.]*) #got invalid char
	false;;
    .*|*.) #begin or end with ".", which is invalid
	false;;
    *..*) #consecutive ".", which is invalid
	false;;
    *.*.*.*.*) #four decimal dots, which is too many
	false;;
    *.*.*.*) #exactly three decimal dots, candidate, evaluate each field
	local IFS=.
	set -- $ip
	if
	    ( [ $1 -le 254 ] && [ $2 -le 254 ] && [ $3 -le 254 ] && [ $4 -le 254 ] )
	then
	    if [ $1 -eq 127 ]; then
		ocf_exit_reason "IP address [$ip] is a loopback address, thus can not be preferred source address"
		exit $OCF_ERR_CONFIGURED
	    fi
	else
	    true
	fi	   
	;;
    *) #less than three decimal dots
	false;;
  esac
  return $? # This return is unnecessary, this comment too :)
}

CheckIP6() {
  ip="$1"
  case $ip in
    *[!0-9a-f:]*) #got invalid char
	false;;
    *:::*) # more than 2 consecutive ":", which is invalid
	false;;
    *::*::*) # more than 1 "::", which is invalid
	false;;
  esac
}

#
#       Find out which interface or alias serves the given IP address
#       The argument is an IP address, and its output
#       is an (aliased) interface name (e.g., "eth0" and "eth0:0").
#
find_interface_solaris() {


  $IFCONFIG $IFCONFIG_A_OPT | $AWK '{if ($0 ~ /.*: / && NR > 1) {print "\n"$0} else {print}}' |
  while read ifname linkstuff
  do
    : ifname = $ifname
    read inet addr junk
    : inet = $inet addr = $addr
    while
      read line && [ "X$line" != "X" ]
    do
      : Nothing
    done

    #  This doesn't look right for a box with multiple NICs.
    #  It looks like it always selects the first interface on
    #  a machine.  Yet, we appear to use the results for this case too...
    ifname=`echo "$ifname" | sed s'%:*$%%'`

    case $addr in
      addr:$BASEIP)	echo $ifname; return $OCF_SUCCESS;;
      $BASEIP)	echo $ifname; return $OCF_SUCCESS;;
    esac
  done
  return $OCF_ERR_GENERIC
}


#
#       Find out which interface or alias serves the given IP address
#       The argument is an IP address, and its output
#       is an (aliased) interface name (e.g., "eth0" and "eth0:0").
#
find_interface_generic() {
	local iface=`$IP2UTIL -o -f $FAMILY addr show | grep "\ $BASEIP" \
            | cut -d ' ' -f2 | grep -v '^ipsec[0-9][0-9]*$'`
        if [ -z "$iface" ]; then
            return $OCF_ERR_GENERIC
        else 
            echo $iface
            return $OCF_SUCCESS
        fi
}


#
#       Find out which interface or alias serves the given IP address
#       The argument is an IP address, and its output
#       is an (aliased) interface name (e.g., "eth0" and "eth0:0").
#
find_interface() {
    case "$SYSTYPE" in
	SunOS)
	 	IF=`find_interface_solaris $BASEIP`
        ;;
      *)
	 	IF=`find_interface_generic $BASEIP`
       ;;
       esac

  echo $IF
  return $OCF_SUCCESS;
}


ip_status() {

  BASEIP="$1"
  case "$SYSTYPE" in
    Darwin)
	# Treat Darwin the same as the other BSD variants (matched as *BSD)
	SYSTYPE="${SYSTYPE}BSD"
	;;
    *)
        ;;
  esac


  case "$SYSTYPE" in
      *BSD)
	  $IFCONFIG $IFCONFIG_A_OPT | grep "inet.*[: ]$BASEIP " >/dev/null 2>&1
	  if [ $? = 0 ]; then
	      return $OCF_SUCCESS
	  else
	      return $OCF_NOT_RUNNING
	  fi;;
      
      Linux|SunOS)		
	  IF=`find_interface "$BASEIP"`
	  if [ -z "$IF" ]; then
	      return $OCF_NOT_RUNNING
	  fi

	  case $IF in
	  	lo*)  
		    ocf_exit_reason "IP address [$BASEIP] is served by loopback, thus can not be preferred source address"
		    exit $OCF_ERR_CONFIGURED
		    ;;
		*)return $OCF_SUCCESS;;
	  esac
	  ;;
	  
      *)		
	  if [ -z "$IF" ]; then
	      return $OCF_NOT_RUNNING
	  else
	      return $OCF_SUCCESS
	  fi;;
  esac
}


srca_validate_all() {

	if [ -z "$OCF_RESKEY_ipaddress" ]; then
		#  usage
		ocf_exit_reason "Please set OCF_RESKEY_ipaddress to the preferred source IP address!"
		return $OCF_ERR_CONFIGURED
	fi

	if ! echo "$OCF_RESKEY_destination" | grep -q "/"; then
		return $OCF_ERR_CONFIGURED
	fi


	if ! [ "x$SYSTYPE" = "xLinux" ]; then
		# checks after this point are only relevant for linux.
		return $OCF_SUCCESS
	fi

	check_binary $AWK
	case "$SYSTYPE" in
		*BSD|SunOS)
			check_binary $IFCONFIG
			;;
	esac

#	The IP address should be in good shape
	if CheckIP "$ipaddress"; then
	  :
	elif CheckIP6 "$ipaddress"; then
	  :
	else
	  ocf_exit_reason "Invalid IP address [$ipaddress]"
	  return $OCF_ERR_CONFIGURED
	fi

	if ocf_is_probe; then
	  return $OCF_SUCCESS
	fi

#	We should serve this IP address of course
	if [ "$OCF_CHECK_LEVEL" -eq 10 ]; then
		if ip_status "$ipaddress"; then
			:
		else
			ocf_exit_reason "We are not serving [$ipaddress], hence can not make it a preferred source address"
			return $OCF_ERR_INSTALLED
		fi
	fi
	return $OCF_SUCCESS
}

if
  ( [ $# -ne 1 ] )
then
  usage
  exit $OCF_ERR_ARGS
fi

# These operations do not require the OCF instance parameters to be set
case $1 in
	meta-data)	meta_data 
			exit $OCF_SUCCESS
			;;
	usage)		usage
			exit $OCF_SUCCESS
			;;
	*)	
			;;
esac

ipaddress="$OCF_RESKEY_ipaddress"

[ "$__OCF_ACTION" != "validate-all" ] && OCF_CHECK_LEVEL=10
srca_validate_all
rc=$?
if [ $rc -ne $OCF_SUCCESS ]; then
	case $1 in
		# if we can't validate the configuration during a stop, that
		# means the resources isn't configured correctly. There's no way
		# to actually stop the resource in this situation because there's
		# no way it could have even started. Return success here
		# to indicate that the resource is not running, otherwise the
		# stop action will fail causing the node to be fenced just because
		# of a mis configuration.
		stop) exit $OCF_SUCCESS;;
		*)    exit $rc;;
	esac
fi

findif_out=`$FINDIF`
rc=$?
[ $rc -ne 0 ] && {
	ocf_exit_reason "[$FINDIF] failed"
	exit $rc
}

INTERFACE=`echo $findif_out | awk '{print $1}'`
case "$FAMILY" in
	inet)
		LISTCMD="$IP2UTIL -f $FAMILY route list dev $INTERFACE scope link $PROTO match $ipaddress"
		;;
	inet6)
		LISTCMD="$IP2UTIL -f $FAMILY route list dev $INTERFACE $PROTO match $ipaddress"
	;;
esac
LISTROUTE=`$LISTCMD`

[ -z "$PROTO" ] && PROTO=`echo $LISTROUTE | sed -n "s/$PROTOCLAUSE/\1/p"`
if [ -n "$OCF_RESKEY_metric" ]; then
	METRIC="metric $OCF_RESKEY_metric"
elif [ -z "$TABLE" ] || [ "${TABLE#table }" = "main" ] || [ "$FAMILY" = "inet6" ]; then
	METRIC=`echo $LISTROUTE | sed -n "s/$METRICCLAUSE/\1/p"`
else
	METRIC=""
fi
if [ "$FAMILY" = "inet6" ]; then
	if [ -z "$OCF_RESKEY_pref" ]; then
		PREF=`echo $LISTROUTE | sed -n "s/$PREFCLAUSE/\1/p"`
	else
		PREF="pref $OCF_RESKEY_pref"
	fi
fi
if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] || [ "$OCF_RESKEY_destination" = "::/0" ] ;then
	NETWORK=`echo $LISTROUTE | grep -m 1 -o '^[^ ]*'`

	if [ -z "$NETWORK" ]; then
		err_str="command '$LISTCMD' failed to find a matching route"

		if [ "$__OCF_ACTION" = "start" ]; then
			ocf_exit_reason "$err_str"
			exit $OCF_ERR_ARGS
		elif ! ocf_is_probe; then
			ocf_log warn "$err_str"
		else
			ocf_log debug "$err_str"
		fi
	fi
else
	NETWORK="$OCF_RESKEY_destination"
fi

case $1 in
	start)		srca_start $ipaddress
			;;
	stop)		srca_stop $ipaddress
			;;
	status)		srca_status $ipaddress
			;;
	monitor)	srca_status $ipaddress
			;;
	validate-all)	srca_validate_all
			;;
	*)		usage
			exit $OCF_ERR_UNIMPLEMENTED
			;;
esac

exit $?

#
# Version 0.3  2002/11/04 17:00:00 John Sutton <john@scl.co.uk>
# Name changed from IPsrcroute to IPsrcaddr and now reports errors
# using ha_log rather than on stderr.
#
# Version 0.2  2002/11/02 17:00:00 John Sutton <john@scl.co.uk>
# Changed status output to "OK" to satisfy ResourceManager's
# we_own_resource() function.
#
# Version 0.1  2002/11/01 17:00:00 John Sutton <john@scl.co.uk>
# First effort but does the job?
#
