#!/bin/bash
#
# Resource script for haproxy daemon with namespace support
#
# Description:  Manages haproxy daemon as an OCF resource in
#               an High Availability setup inside a namespace
#
# HAProxy OCF script's Author: Mirantis
# License: GNU General Public License (GPL)
#
#	usage: $0 {start|stop|restart|status|monitor|validate-all|meta-data}
#
#	The "start" arg starts haproxy.
#
#	The "stop" arg stops it.
#
# OCF parameters:
# OCF_RESKEY_ns
# OCF_RESKEY_conffile
# OCF_RESKEY_pidfile
# OCF_RESKEY_binpath
# OCF_RESKEY_extraconf
#
# OCF_RESKEY_host_interface
# OCF_RESKEY_namespace_interface
# OCF_RESKEY_host_ip
# OCF_RESKEY_namespace_ip
# OCF_RESKEY_network_mask
# OCF_RESKEY_route_metric
#
# Note: This RA requires that the haproxy config files has a "pidfile"
# entry so that it is able to act on the correct process
##########################################################################
# Initialization:

OCF_ROOT_default="/usr/lib/ocf"
: ${OCF_ROOT=${OCF_ROOT_default}}

: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat}
. ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs
: ${OCF_FUEL_FUNCTIONS_DIR=${OCF_ROOT}/resource.d/fuel}
. ${OCF_FUEL_FUNCTIONS_DIR}/ocf-fuel-funcs

###########################################################################

OCF_RESKEY_ns_default="haproxy"
OCF_RESKEY_conffile_default="/etc/haproxy/haproxy.cfg"
OCF_RESKEY_pidfile_default="${HA_RSCTMP}/${__SCRIPT_NAME}/${__SCRIPT_NAME}.pid"
OCF_RESKEY_binpath_default="/usr/sbin/haproxy"
OCF_RESKEY_extraconf_default=""

OCF_RESKEY_other_networks_default=""
OCF_RESKEY_host_interface_default="hapr-host"
OCF_RESKEY_namespace_interface_default="hapr-ns"
OCF_RESKEY_host_ip_default="240.0.0.1"
OCF_RESKEY_namespace_ip_default="240.0.0.2"
OCF_RESKEY_network_mask_default="30"
OCF_RESKEY_route_metric_default="10000"
OCF_RESKEY_debug_default=false

: ${HA_LOGTAG="ocf-ns_haproxy"}
: ${HA_LOGFACILITY="daemon"}

: ${OCF_RESKEY_ns=${OCF_RESKEY_ns_default}}
: ${OCF_RESKEY_conffile=${OCF_RESKEY_conffile_default}}
: ${OCF_RESKEY_pidfile=${OCF_RESKEY_pidfile_default}}
: ${OCF_RESKEY_binpath=${OCF_RESKEY_binpath_default}}
: ${OCF_RESKEY_extraconf=${OCF_RESKEY_extraconf_default}}

: ${OCF_RESKEY_other_networks=${OCF_RESKEY_other_networks_default}}
: ${OCF_RESKEY_host_interface=${OCF_RESKEY_host_interface_default}}
: ${OCF_RESKEY_namespace_interface=${OCF_RESKEY_namespace_interface_default}}
: ${OCF_RESKEY_host_ip=${OCF_RESKEY_host_ip_default}}
: ${OCF_RESKEY_namespace_ip=${OCF_RESKEY_namespace_ip_default}}
: ${OCF_RESKEY_network_mask=${OCF_RESKEY_network_mask_default}}
: ${OCF_RESKEY_route_metric=${OCF_RESKEY_route_metric_default}}
: ${OCF_RESKEY_debug=${OCF_RESKEY_debug_default}}

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

RUN_IN_NS="ip netns exec $OCF_RESKEY_ns "
if [ -z "${OCF_RESKEY_ns}" ] ; then
	RUN=''
else
	RUN="$RUN_IN_NS "
fi

##########################################################################

usage()
{
	echo $USAGE >&2
}

meta_data()
{
cat <<END
<?xml version="1.0"?>
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
<resource-agent name="haproxy">
<version>1.0</version>
<longdesc lang="en">
This script manages haproxy daemon with namespace support
</longdesc>
<shortdesc lang="en">Manages an haproxy daemon inside an namespace</shortdesc>

<parameters>

<parameter name="dummy" unique="1">
<longdesc lang="en">
This is a dummy parameter.
Pacemaker needs it to enable reload operation for the resource
</longdesc>
<shortdesc lang="en">Dummy parameter</shortdesc>
<content type="boolean" default="false" />
</parameter>

<parameter name="ns">
<longdesc lang="en">
Name of network namespace.
Should be present.
</longdesc>
<shortdesc lang="en">Name of network namespace.</shortdesc>
<content type="string" default="${OCF_RESKEY_ns_default}"/>
</parameter>

<parameter name="conffile">
<longdesc lang="en">
The haproxy daemon configuration file name with full path.
For example, "/etc/haproxy/haproxy.cfg"
</longdesc>
<shortdesc lang="en">Configuration file name with full path</shortdesc>
<content type="string" default="${OCF_RESKEY_conffile_default}" />
</parameter>

<parameter name="pidfile">
<longdesc lang="en">
The haproxy pid file path.
For example, "/var/run/haproxy.pid"
</longdesc>
<shortdesc lang="en">Full path to the haproxy pid file</shortdesc>
<content type="string" default="${OCF_RESKEY_pidfile_default}"/>
</parameter>

<parameter name="binpath">
<longdesc lang="en">
The haproxy binary path.
For example, "/usr/sbin/haproxy"
</longdesc>
<shortdesc lang="en">Full path to the haproxy binary</shortdesc>
<content type="string" default="${OCF_RESKEY_binpath_default}"/>
</parameter>

<parameter name="extraconf">
<longdesc lang="en">
Extra command line arguments to pass to haproxy.
For example, "-f /etc/haproxy/shared.cfg"
</longdesc>
<shortdesc lang="en">Extra command line arguments for haproxy</shortdesc>
<content type="string" default="${OCF_RESKEY_extraconf_default}" />
</parameter>

<parameter name="other_networks">
<longdesc lang="en">
Additional routes that should be added to this resource. Routes will be added via value namespace_interface.
</longdesc>
<shortdesc lang="en">List of addtional routes to add routes for.</shortdesc>
<content type="string" default="$OCF_RESKEY_other_networks_default"/>
</parameter>

<parameter name="host_interface">
<longdesc lang="en">
The host part of the interface pair used to connect the namespace to the network
For example, "hapr-host"
</longdesc>
<shortdesc lang="en">The name of the host interface used for namespace</shortdesc>
<content type="string" default="${OCF_RESKEY_host_interface_default}" />
</parameter>

<parameter name="namespace_interface">
<longdesc lang="en">
The namespace part of the interface pair used to connect the namespace to the network
For example, "hapr-ns"
</longdesc>
<shortdesc lang="en">The name of the namespace interface used for namespace</shortdesc>
<content type="string" default="${OCF_RESKEY_namespace_interface_default}" />
</parameter>

<parameter name="host_ip">
<longdesc lang="en">
The IP address used by the host interface. Must be from the same subnet as namesapce IP
and uses network_mask to determine subnet.
Should not collide with any IP addresses already used in your network.
For example, "240.0.0.1"
</longdesc>
<shortdesc lang="en">Host interface IP address</shortdesc>
<content type="string" default="${OCF_RESKEY_host_ip_default}" />
</parameter>

<parameter name="namespace_ip">
<longdesc lang="en">
The IP address used by the namespace interface. Must be from the same subnet as host IP
and uses network_mask to determine subnet.
Should not collide with any IP addresses already used in your network.
For example, "240.0.0.2"
</longdesc>
<shortdesc lang="en">Namespace interface IP address</shortdesc>
<content type="string" default="${OCF_RESKEY_namespace_ip_default}" />
</parameter>

<parameter name="network_mask">
<longdesc lang="en">
The network mask length used to determine subnet of the host
and the namspace interfaces.
For example, "30"
</longdesc>
<shortdesc lang="en">Network mask length</shortdesc>
<content type="string" default="${OCF_RESKEY_network_mask_default}" />
</parameter>

<parameter name="route_metric">
<longdesc lang="en">
The metric value of the default route set for the pipe
link connecting namespace and host. It should be set to
a large number to be higher then other default route metrics
that could be set to override this default route.
If other routes are set eithin the namespace thir metric should
be smaller then this number if you want them to be used istead of
this route.
For example, "1000"
</longdesc>
<shortdesc lang="en">Namespace default route metric</shortdesc>
<content type="string" default="${OCF_RESKEY_route_metric_default}" />
</parameter>

<parameter name="debug" unique="0" required="0">
<longdesc lang="en">
The debug flag for haproxy.
</longdesc>
<shortdesc lang="en">HAProxy RA debug flag</shortdesc>
<content type="boolean" default="${OCF_RESKEY_debug_default}" />
</parameter>

</parameters>

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

# Only match TCP packets with the SYN bit set and the ACK,RST and FIN bits cleared
# and block them. Such packets are used to request TCP connection initiation.
# Limit the block rule scope to the haproxy namespace to no affect other connections
block_client_access()
{
    # do not add temporary SYN blocking rule, if it is already exist
    # otherwise, try to add a blocking rule with max of 5 retries
    local tries=5
    until $($RUN_IN_NS iptables -t filter -nvL | grep -q 'temporary SYN block') || [ $tries -eq 0 ]; do
      tries=$((tries-1))
      ocf_run $RUN_IN_NS iptables -t filter -I INPUT -p tcp \
        -m comment --comment 'temporary SYN block' --syn -j DROP
      sleep 1
    done
    if [ $tries -eq 0 ]; then
        return $OCF_ERR_GENERIC
    else
        return $OCF_SUCCESS
    fi
}

# Unblock the blocking rule
unblock_client_access()
{
    # remove all temporary SYN blocking rules, if there are more than one exist
    for i in $($RUN_IN_NS iptables -t filter -nvL --line-numbers | awk '/temporary SYN block/ {print $1}'); do
      ocf_run $RUN_IN_NS iptables -t filter -D INPUT -p tcp \
        -m comment --comment 'temporary SYN block' --syn -j DROP
    done
}

check_ns() {
  local LH="${LL} check_ns():"
  local ns=`ip netns list | grep "$OCF_RESKEY_ns"`
  ocf_log debug "${LH} recieved netns list: ${ns}"
  [ "${ns}" != "${OCF_RESKEY_ns}" ] && return $OCF_ERR_GENERIC
  return $OCF_SUCCESS
}

get_ns() {
  local rc
  local LH="${LL} get_ns():"
  check_ns && return $OCF_SUCCESS

  ocf_run ip netns add $OCF_RESKEY_ns
  rc=$?
  ocf_run $RUN_IN_NS /sbin/sysctl -w net.ipv4.ip_nonlocal_bind=1
  ocf_run $RUN_IN_NS ip link set up dev lo
  ocf_log debug "${LH} added netns ${OCF_RESKEY_ns} and set up lo"

  return $rc
}

get_variables() {
        local LH="${LL} get_variables():"
        get_ns
        CONF_FILE="${OCF_RESKEY_conffile}"
        COMMAND="$RUN ${OCF_RESKEY_binpath}"
        if [ -n "${OCF_RESKEY_pidfile}" ]; then
                PIDFILE="${OCF_RESKEY_pidfile}"
        else
                PIDFILE=$(grep -v "#" ${CONF_FILE} | grep "pidfile" | sed 's/^[ \t]*pidfile[ \t]*//')
        fi
        ocf_log debug "${LH} set up variables and PIDFILE name"
}

set_ns_routing() {

  nsip() {
    ip netns exec "${OCF_RESKEY_ns}" ip ${@}
  }

  # create host-ns veth pair unless it's present
  ip link | grep -q -w "${OCF_RESKEY_host_interface}:"
  if [ $? -gt 0 ]; then
    ocf_log debug "Creating host interface: ${OCF_RESKEY_host_interface} and namespace interface: ${OCF_RESKEY_namespace_interface}"
    ocf_run ip link add "${OCF_RESKEY_host_interface}" type veth peer name "${OCF_RESKEY_namespace_interface}"
  fi

  # move the ns part to the namespace
  ip link | grep -q -w "${OCF_RESKEY_namespace_interface}:"
  if [ $? -eq 0 ]; then
    ocf_log debug "Moving interface: ${OCF_RESKEY_namespace_interface} to namespace: ${OCF_RESKEY_ns}"
    ocf_run ip link set dev "${OCF_RESKEY_namespace_interface}" netns "${OCF_RESKEY_ns}"
  fi

  # up the host part
  ocf_log debug "Bringing up host interface: ${OCF_RESKEY_host_interface}"
  ocf_run ip link set "${OCF_RESKEY_host_interface}" up

  # set host part's ip
  ip addr show dev "${OCF_RESKEY_host_interface}" | grep -q "inet ${OCF_RESKEY_host_ip}/${OCF_RESKEY_network_mask}"
  if [ $? -gt 0 ]; then
    ocf_log debug "Setting host interface: ${OCF_RESKEY_host_interface} IP to: ${OCF_RESKEY_host_ip}/${OCF_RESKEY_network_mask}"
    ocf_run ip addr add "${OCF_RESKEY_host_ip}/${OCF_RESKEY_network_mask}" dev "${OCF_RESKEY_host_interface}"
  fi

  # up the ns part
  ocf_log debug "Bringing up the namespace interface: ${OCF_RESKEY_namespace_interface}"
  ocf_run nsip link set "${OCF_RESKEY_namespace_interface}" up

  # set ns part's ip
  nsip addr show dev "${OCF_RESKEY_namespace_interface}" | grep -q "inet ${OCF_RESKEY_namespace_ip}/${OCF_RESKEY_network_mask}"
  if [ $? -gt 0 ]; then
    ocf_log debug "Setting namespace interface: ${OCF_RESKEY_namespace_interface} IP to: ${OCF_RESKEY_namespace_ip}/${OCF_RESKEY_network_mask}"
    ocf_run nsip addr add "${OCF_RESKEY_namespace_ip}/${OCF_RESKEY_network_mask}" dev "${OCF_RESKEY_namespace_interface}"
  fi

  # set default gateway inside ns
  nsip route list | grep -q "default via ${OCF_RESKEY_host_ip}"
  if [ $? -gt 0 ]; then
    ocf_log debug "Creating default route inside the namespace to ${OCF_RESKEY_host_ip} with metric ${OCF_RESKEY_route_metric}"
    ocf_run nsip route add default via "${OCF_RESKEY_host_ip}" metric "${OCF_RESKEY_route_metric}"
  fi

  # set masquerade on host node
  iptables -t nat -L | grep -q masquerade-for-haproxy-namespace
  if [ $? -gt 0 ]; then
    ocf_log debug "Creating NAT rule on the host system for traffic from IP: ${OCF_RESKEY_namespace_ip}"
    ocf_run iptables -t nat -A POSTROUTING -s "${OCF_RESKEY_namespace_ip}" -j MASQUERADE -m comment --comment "masquerade-for-haproxy-namespace"
  fi

  ### Needed for ML2 routing ###
  ocf_run sysctl -w net.ipv4.conf.${OCF_RESKEY_host_interface}.rp_filter=2
  ocf_run $RUN_IN_NS sysctl -w net.ipv4.conf.all.rp_filter=2
  ##############################

  if [ "${OCF_RESKEY_other_networks}" != "false" ] ; then
    for network in ${OCF_RESKEY_other_networks}
    do
      ocf_log debug "Adding route on the host system to ${network}: ${OCF_RESKEY_namespace_ip}"
      ocf_run $RUN_IN_NS ip route replace ${network} via ${OCF_RESKEY_host_ip} metric 10000
    done
  fi
}

haproxy_status() {
	get_variables

        # check and make PID file dir
        local PID_DIR="$( dirname ${PIDFILE} )"
        if [ ! -d "${PID_DIR}" ] ; then
                ocf_log debug "Create pid file dir: ${PID_DIR}"
                mkdir -p "${PID_DIR}"
                # no need to chown, root is user for haproxy
                chmod 755 "${PID_DIR}"
        fi

	if [ -n "${PIDFILE}" -a -f "${PIDFILE}" ]; then
		# haproxy is probably running
		# get pid from pidfile
		PID="`cat ${PIDFILE}`"
		if [ -n "${PID}" ]; then
			# check if process exists
			if $RUN ps -p "${PID}" | grep -q haproxy; then
				ocf_log info "haproxy daemon running"
				return $OCF_SUCCESS
			else
				ocf_log warn "haproxy daemon is not running but pid file exists"
				return $OCF_NOT_RUNNING
			fi
		else
			ocf_log err "PID file empty!"
			return $OCF_ERR_GENERIC
		fi
	fi
	# haproxy is not running
	ocf_log info "haproxy daemon is not running"
	return $OCF_NOT_RUNNING
}

haproxy_start()
{
	get_variables

	# if haproxy is running return success
	haproxy_status
	retVal=$?
	if [ $retVal -eq $OCF_SUCCESS ]; then
		return $OCF_SUCCESS
	elif [ $retVal -ne $OCF_NOT_RUNNING ]; then
		ocf_log err "Error. Unknown status."
		return $OCF_ERR_GENERIC
	fi

	# run the haproxy binary
	ocf_run_as_root ${COMMAND} ${OCF_RESKEY_extraconf} -f "${CONF_FILE}" -p "${PIDFILE}"
	if [ $? -ne 0 ]; then
		ocf_log err "Error. haproxy daemon returned error $?."
		return $OCF_ERR_GENERIC
	fi

	if [ "${OCF_RESKEY_ns}" != '' ]; then
		set_ns_routing
	fi

	ocf_log info "Started haproxy daemon."
	return $OCF_SUCCESS
}

haproxy_reload()
{
  local rc
  get_variables
  if haproxy_status; then
      # get pid from pidfile
      PID="`cat ${PIDFILE}`"
      # Safe-unblock the rules, if there are any
      unblock_client_access
      # Apply the blocking rule
      block_client_access
      rc=$?
      if [ $rc -eq $OCF_SUCCESS ]; then
          ocf_log info "Blocked all SYN for the Haproxy reload operation"
      else
          ocf_log warn "Cannot block all SYN for the Haproxy reload operation!"
      fi
      # reload haproxy binary replacing the old process
      ocf_run_as_root ${COMMAND} ${OCF_RESKEY_extraconf} -f "${CONF_FILE}" -p "${PIDFILE}" -sf "${PID}"
      rc=$?
      unblock_client_access
      ocf_log info "Unblocked all SYN for the Haproxy reload operation"
      if [ $rc -ne 0 ]; then
          ocf_log err "Error. haproxy daemon returned error $?."
          return $OCF_ERR_GENERIC
      fi
  else
      ocf_log info "Haproxy daemon is not running. Starting it."
      haproxy_start
  fi
}

haproxy_stop()
{
    local rc
    local LH="${LL} haproxy_stop():"
    local shutdown_timeout=15
    if [ -n "$OCF_RESKEY_CRM_meta_timeout" ]; then
        shutdown_timeout=$(( ($OCF_RESKEY_CRM_meta_timeout/1000) ))
    fi

    get_variables
    haproxy_status
    rc="${?}"
    if [ "${rc}" -eq "${OCF_NOT_RUNNING}" ]; then
        ocf_log info "${LH} haproxy already stopped."
        return "${OCF_SUCCESS}"
    fi

    proc_stop "${PIDFILE}" "${OCF_RESKEY_binpath}" $shutdown_timeout
    return "${?}"
}

haproxy_monitor()
{
	haproxy_status
}

haproxy_validate_all()
{
	get_variables
	if [ -n "$OCF_RESKEY_binpath" -a ! -x "$OCF_RESKEY_binpath" ]; then
		ocf_log err "Binary path $OCF_RESKEY_binpath does not exist."
		return $OCF_ERR_ARGS
	fi
	if [ -n "$OCF_RESKEY_conffile" -a ! -f "$OCF_RESKEY_conffile" ]; then
		ocf_log err "Config file $OCF_RESKEY_conffile does not exist."
		return $OCF_ERR_ARGS
	fi

	if  grep -v "^#" "$CONF_FILE" | grep "pidfile" > /dev/null ; then
		:
	else
		ocf_log err "Error. \"pidfile\" entry required in the haproxy config file by haproxy OCF RA."
		return $OCF_ERR_GENERIC
	fi

	return $OCF_SUCCESS
}

haproxy_restart()
{
	haproxy_stop
	haproxy_start
}

#
# Main
#

if [ $# -ne 1 ]; then
	usage
	exit $OCF_ERR_ARGS
fi
umask 0022
export LL="${OCF_RESOURCE_INSTANCE}:"

case $1 in
	start) haproxy_start
	;;

	stop) haproxy_stop
	;;

	reload) haproxy_reload
	;;

	restart) haproxy_restart
	;;

	status)	haproxy_status
	;;

	monitor) haproxy_monitor
	;;

	validate-all) haproxy_validate_all
	;;

	meta-data) meta_data
	;;

	usage) usage; exit $OCF_SUCCESS
	;;

	*) usage; exit $OCF_ERR_UNIMPLEMENTED
	;;
esac
