#!/bin/bash

#######################################################################
# Initialization:

: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat}
. ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs

# Defaults

OCF_RESKEY_cidr_netmask_default="32"
OCF_RESKEY_base_veth_default=""       # may be omited if OVS used
OCF_RESKEY_gateway_default="none"     # can be "none", "link", IPaddr
OCF_RESKEY_gateway_metric_default=0   # can be "", or metric value
OCF_RESKEY_also_check_interfaces_default="" # can be "", or list of interfaces
OCF_RESKEY_other_networks_default=""  #  can be "", or list of networks in CIDR format

: ${HA_LOGTAG="ocf-ns_IPaddr2"}
: ${HA_LOGFACILITY="daemon"}
: ${OCF_RESKEY_cidr_netmask=${OCF_RESKEY_cidr_netmask_default}}
: ${OCF_RESKEY_base_veth=${OCF_RESKEY_base_veth_default}}
: ${OCF_RESKEY_gateway=${OCF_RESKEY_gateway_default}}
: ${OCF_RESKEY_gateway_metric=${OCF_RESKEY_gateway_metric_default}}
: ${OCF_RESKEY_also_check_interfaces=${OCF_RESKEY_also_check_interfaces_default}}
: ${OCF_RESKEY_other_networks=${OCF_RESKEY_other_networks_default}}

FAMILY='inet'
RUN_IN_NS="ip netns exec $OCF_RESKEY_ns "
SH="/bin/bash"

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

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

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

<longdesc lang="en">
This Linux-specific resource manages IP address inside network namespace.
</longdesc>

<shortdesc lang="en">This Linux-specific resource manages IP address inside network namespace.</shortdesc>

<parameters>

<parameter name="bridge" required="1">
<longdesc lang="en">
Name of the bridge that has network namespace with VIP connected to it.
</longdesc>
<shortdesc lang="en">Name of the bridge.</shortdesc>
<content type="string" />
</parameter>

<parameter name="ip" unique="1" required="1">
<longdesc lang="en">
The IPv4 address to be configured in dotted quad notation, for example
"192.168.1.1".
</longdesc>
<shortdesc lang="en">IPv4 address</shortdesc>
<content type="string" />
</parameter>

<parameter name="cidr_netmask">
<longdesc lang="en">
The netmask for the interface in CIDR format
(e.g., 24 and not 255.255.255.0)

If unspecified, the script will also try to determine this from the
routing table.
</longdesc>
<shortdesc lang="en">CIDR netmask</shortdesc>
<content type="string" default=""/>
</parameter>

<parameter name="iflabel">
<longdesc lang="en">
You can specify an additional label for your IP address here.
This label is appended to your interface name.

A label can be specified in nic parameter but it is deprecated.
If a label is specified in nic name, this parameter has no effect.
</longdesc>
<shortdesc lang="en">Interface label</shortdesc>
<content type="string" default=""/>
</parameter>

<parameter name="ns" required="1">
<longdesc lang="en">
Name of network namespace.\n
Should be present.
</longdesc>
<shortdesc lang="en">Name of network namespace.</shortdesc>
<content type="string" />
</parameter>

<parameter name="base_veth">
<longdesc lang="en">
Name of base system side veth pair tail.\n
Should be present.
</longdesc>
<shortdesc lang="en">Name of base system side veth pair tail.</shortdesc>
<content type="string" default="$OCF_RESKEY_base_veth_default"/>
</parameter>

<parameter name="ns_veth" required="1">
<longdesc lang="en">
Name of net.namespace side veth pair tail.\n
Should be present.
</longdesc>
<shortdesc lang="en">Name of net.namespace side veth pair tail.</shortdesc>
<content type="string"/>
</parameter>

<parameter name="gateway">
<longdesc lang="en">
Default route address.\n
Can be "", "link" or IP address.
</longdesc>
<shortdesc lang="en">Default route address.</shortdesc>
<content type="string" default="$OCF_RESKEY_gateway_default"/>
</parameter>

<parameter name="gateway_metric">
<longdesc lang="en">
Default route address.\n
Can be "", "link" or IP address.
</longdesc>
<shortdesc lang="en">Default route address.</shortdesc>
<content type="string" default="$OCF_RESKEY_gateway_metric_default"/>
</parameter>

<parameter name="ns_iptables_start_rules">
<longdesc lang="en">
Iptables rules that should be started along with IP in the namespace.\n
</longdesc>
<shortdesc lang="en">Iptables rules associated with IP start in ns.</shortdesc>
<content type="string" default=""/>
</parameter>

<parameter name="ns_iptables_stop_rules">
<longdesc lang="en">
Iptables rules that should be stopped along with IP in the namespace.\n
</longdesc>
<shortdesc lang="en">Iptables rules associated with IP stop in ns.</shortdesc>
<content type="string" default=""/>
</parameter>

<parameter name="iptables_comment">
<longdesc lang="en">
Iptables comment to associate with rules.\n
</longdesc>
<shortdesc lang="en">Iptables comment to associate with rules.</shortdesc>
<content type="string" default="something_rule_for_VIP"/>
</parameter>

<parameter name="also_check_interfaces">
<longdesc lang="en">
Network interfaces list (ex. NIC), that should be in UP state for monitor action returns succesful.\n
</longdesc>
<shortdesc lang="en">Network interfaces list (ex. NIC), that should be in UP state for monitor action returns succesful.</shortdesc>
<content type="string" default="$OCF_RESKEY_also_check_interfaces_default"/>
</parameter>

<parameter name="other_networks">
<longdesc lang="en">
Additional routes that should be added to this resource. Routes will be added via value ns_veth. Should be space separated list of networks in CIDR format.
</longdesc>
<shortdesc lang="en">List of addtional routes to add routes for.</shortdesc>
<content type="string" default="$OCF_RESKEY_other_networks_default"/>
</parameter>

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

  exit $OCF_SUCCESS
}


ip_validate() {

  if [[ X`uname -s` != "XLinux" ]] ; then
      ocf_log err "ns_IPaddr2 only supported Linux."
      exit $OCF_ERR_INSTALLED
  fi

  if [ -z "$OCF_RESKEY_ip" ] ; then
    ocf_log err "IP address not given"
    exit $OCF_ERR_CONFIGURED
  fi

  if [ -z "$OCF_RESKEY_ns" ] ; then
    ocf_log err "Network namespace not given"
    exit $OCF_ERR_CONFIGURED
  fi

  if [ -z "$OCF_RESKEY_cidr_netmask" ] ; then
    ocf_log err "CIDR Netmask not given"
    exit $OCF_ERR_CONFIGURED
  fi

  if [ -z "$OCF_RESKEY_ns_veth" ] ; then
    ocf_log err "NS veth tail name not given"
    exit $OCF_ERR_CONFIGURED
  fi

  if ! ocf_is_decimal "$OCF_RESKEY_gateway_metric"; then
    ocf_log err "Gateway_metric should be a positive digital value"
    exit $OCF_ERR_CONFIGURED
  fi

  return $OCF_SUCCESS
}


#
#   Find out which interfaces serve the given IP address and netmask.
#   The arguments are an IP address and a netmask.
#   Its output are interface names devided by spaces (e.g., "eth0 eth1").
#
find_interface() {
    local ipaddr="$1"
    local netmask="$2"
    [ -z "$netmask" ] || ipaddr="$ipaddr/$netmask"

    #
    # List interfaces but exclude FreeS/WAN ipsecN virtual interfaces
    local iface="`ip -o -f inet addr show \
        | grep "\ $ipaddr" \
        | cut -d ' ' -f2 \
        | grep -v '^ipsec[[0-9]][[0-9]]*$'`"
    local rc=$?
    echo "$iface"
    return $rc
}

find_interface_in_ns() {
    local ns="$1"
    local ipaddr="$2"
    local netmask="$3"
    [ -z "$netmask" ] || ipaddr="$ipaddr/$netmask"

    #
    # List interfaces but exclude FreeS/WAN ipsecN virtual interfaces
    local iface=`ip netns exec $ns ip -o -f inet addr show \
        | grep "\ $ipaddr" \
        | cut -d ' ' -f2 \
        | grep -v '^ipsec[[0-9]][[0-9]]*$'`
    local rc=$?
    echo "$iface"
    return $rc
}

setup_routes() {
  local network
  if [ ! -z "$OCF_RESKEY_other_networks" ] ; 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 add ${network} dev ${OCF_RESKEY_ns_veth}
    done
  fi
}

# add veth to bridge if not already added
add_to_bridge() {
  local br="$1"
  local veth="$2"
  local ns_veth="$3"
  local bridge_mtu=`cat /sys/class/net/${br}/mtu`
  # check which bridge (OVS or LNX) used
  if [[ -d /sys/class/net/${br}/brif ]] ; then
    # LNX
    if [[ ! -d /sys/class/net/${br}/brif/${veth} ]] ; then
      # attach 1-st jack to an LNX bridge
      ocf_run brctl addif $br $veth || return $OCF_ERR_GENERIC
    fi
  else
    # OVS
    ovs-vsctl list port $veth > /dev/null 2>&1
    if [[ $? != 0 ]] ; then
      # attach 1-st jack to an OVS bridge
      ocf_run ovs-vsctl --may-exist add-port $br $veth || return $OCF_ERR_GENERIC
    fi
  fi
  # adjust MTU
  ocf_run            ip link set mtu $bridge_mtu dev $veth
  ocf_run $RUN_IN_NS ip link set mtu $bridge_mtu dev $ns_veth
  return $OCF_SUCCESS
}

remove_from_bridge() {
  if [[ -d /sys/class/net/${OCF_RESKEY_bridge}/brif ]] ; then
    # native linux bridges
    if [[ -d /sys/class/net/${OCF_RESKEY_bridge}/brif/${OCF_RESKEY_base_veth} ]] ; then
      ocf_run brctl delif $OCF_RESKEY_bridge $OCF_RESKEY_base_veth || return $OCF_ERR_GENERIC
    else
      ocf_log warn "Jack ${OCF_RESKEY_base_veth} not a member of ${OCF_RESKEY_bridge} yet."
    fi
  else
    # OVS bridge
    ocf_run ovs-vsctl del-port $OCF_RESKEY_bridge $OCF_RESKEY_base_veth || return $OCF_ERR_GENERIC
  fi
  return $OCF_SUCCESS
}


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


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

get_ns() {
  local rc
  check_ns || ocf_run ip netns add $OCF_RESKEY_ns
  ocf_run $RUN_IN_NS ip link set up dev lo || return $OCF_ERR_GENERIC
  return $OCF_SUCCESS
}

get_or_create_veth_pair() {
  local rc
  local rc1

  # check tail of veth-pair in base system
  ip link show $OCF_RESKEY_base_veth 2>&1 > /dev/null
  rc=$?
  if [[ $rc != 0 ]] ; then
      # 1st jack not found, need to create pair and attach 2nd jack to the net.namespace
      # create veth pair and put 2nd jack to the net.ns
      ocf_run ip link add $OCF_RESKEY_base_veth type veth peer name $OCF_RESKEY_ns_veth
      ocf_run ip link set dev $OCF_RESKEY_ns_veth netns $OCF_RESKEY_ns
      ocf_run            ip link set up dev $OCF_RESKEY_base_veth
      ocf_run $RUN_IN_NS ip link set up dev $OCF_RESKEY_ns_veth
      sleep 1
      # connect veth-pair to the bridge and adjust MTU
      add_to_bridge $OCF_RESKEY_bridge $OCF_RESKEY_base_veth $OCF_RESKEY_ns_veth
  fi
  return $OCF_SUCCESS
}

check_interfaces_for_up_state() {
  local interfaces=$(echo "$1" | tr " ,:;" "\n")
  local rc=$OCF_SUCCESS

  for i in $interfaces ; do
    rv=$(cat /sys/class/net/$i/carrier)  # can return non-zero error-code for administrative-downed interface
    if [[ $? != 0 || $rv != "1" ]] ; then
      rc=$OCF_NOT_RUNNING
      break
    fi
  done

  return $rc
}

ip_prepare() {
  local rc
  ip_validate
  [[ $? != 0 ]] && return $OCF_ERR_GENERIC

  # create or get existing network namespace
  get_ns || return $OCF_ERR_GENERIC

  # create or get existing pair of veth interfaces
  get_or_create_veth_pair || return $OCF_ERR_GENERIC

  # attach IP address inside network namespace
  ocf_run $RUN_IN_NS ip addr replace "$OCF_RESKEY_ip/$OCF_RESKEY_cidr_netmask" dev $OCF_RESKEY_ns_veth
  [[ $? != 0 ]] && return $OCF_ERR_GENERIC

  # setup default routing in namespace if gateway given
  if [[ "$OCF_RESKEY_gateway" == 'link' ]] ; then
    ocf_run $RUN_IN_NS ip route replace default dev $OCF_RESKEY_ns_veth metric $OCF_RESKEY_gateway_metric
  elif  [[ "$OCF_RESKEY_gateway" == 'none' ]] ; then
    echo "Setup default gateway -- do nothing."
  else
    ocf_run $RUN_IN_NS ip route replace default via $OCF_RESKEY_gateway metric $OCF_RESKEY_gateway_metric
  fi

  # Send Gratuitous ARP REQUEST packets to update all neighbours in a detached background process
  ARGS="-U -c 32 -w 10 -I $OCF_RESKEY_ns_veth -q $OCF_RESKEY_ip"
  $RUN_IN_NS arping $ARGS 2>&1 > /dev/null &

  # Send Gratuitous ARP REPLY packets to update all neighbours in a detached background process
  ARGS="-A -c 32 -w 10 -I $OCF_RESKEY_ns_veth -q $OCF_RESKEY_ip"
  $RUN_IN_NS arping $ARGS 2>&1 > /dev/null &

  return $OCF_SUCCESS
}

iptables_start() {
  local ns_iptables_rules
  local rule
  # setup iptables rules if given

  if [[ "$OCF_RESKEY_ns_iptables_start_rules" != "false" ]] ; then
    IFS=';' read -a ns_iptables_rules <<< "$OCF_RESKEY_ns_iptables_start_rules"
    for rule in "${ns_iptables_rules[@]}" ; do
      ocf_run $RUN_IN_NS $rule
    done
  fi

  setup_routes
  return $OCF_SUCCESS
}

iptables_stop() {
  local ns_iptables_rules
  local rule
  # remove iptables rules if given
  if [[ $OCF_RESKEY_ns_iptables_stop_rules != "false" ]] ; then
    IFS=';' read -a ns_iptables_rules <<< "$OCF_RESKEY_ns_iptables_stop_rules"
    for rule in "${ns_iptables_rules[@]}" ; do
      ocf_run $RUN_IN_NS $rule
    done
  fi

  return $OCF_SUCCESS
}

ip_start() {
  check_interfaces_for_up_state "$OCF_RESKEY_bridge:$OCF_RESKEY_also_check_interfaces" || return $OCF_ERR_GENERIC
  ip_prepare

  rc=$?
  if [[ $rc != $OCF_SUCCESS ]] ; then
    # cleanun ns
    ip_stop
    rc=$OCF_ERR_GENERIC
  else
    iptables_start
  fi
  return $rc
}

ip_stop() {
  local rc
  ip_validate
  if [ -n "$OCF_RESKEY_bridge" ] ; then
    remove_from_bridge
  fi
  # destroy veth-pair in base system
  ocf_run ip link show $OCF_RESKEY_base_veth
  rc=$?
  if [[ $rc == 0 ]] ; then
    ocf_run ip link set down dev $OCF_RESKEY_base_veth &&
    sleep 1 &&  # prevent race
    ocf_run ip link del dev $OCF_RESKEY_base_veth
    rc=$?
  else
    rc=0
  fi

  if [[ $rc == 0 ]] ; then
    rc=$OCF_SUCCESS  # it means stop was success
    iptables_stop
  else
    rc=$OCF_ERR_GENERIC
  fi
  return $rc
}

ip_monitor() {
  local rc
  ip_validate
  check_ns || return $OCF_NOT_RUNNING
  local iface=$(find_interface_in_ns $OCF_RESKEY_ns $OCF_RESKEY_ip $OCF_RESKEY_cidr_netmask)

  [ -z "$iface" ] && return $OCF_NOT_RUNNING

  check_interfaces_for_up_state "$OCF_RESKEY_bridge:$OCF_RESKEY_also_check_interfaces" || return $OCF_NOT_RUNNING
  # use arping here, because no IP from VIP network allowed on host system
  ocf_run arping -c 10 -w 2 -I $OCF_RESKEY_bridge $OCF_RESKEY_ip || return $OCF_NOT_RUNNING

  # Send Gratuitous ARP REQUEST packets to update all neighbours in a detached background process
  ARGS="-U -c 5 -w 2 -I $OCF_RESKEY_ns_veth -q $OCF_RESKEY_ip"
  $RUN_IN_NS arping $ARGS 2>&1 > /dev/null &

  # Send Gratuitous ARP REPLY packets to update all neighbours in a detached background process
  ARGS="-A -c 5 -w 2 -I $OCF_RESKEY_ns_veth -q $OCF_RESKEY_ip"
  $RUN_IN_NS arping $ARGS 2>&1 > /dev/null &

  return $OCF_SUCCESS
}


ip_usage() {
    cat <<END
usage: $0 {start|stop|status|monitor|validate-all|meta-data}

Expects to have a fully populated OCF RA-compliant environment set.
END
}

## main

rc=$OCF_SUCCESS
case $__OCF_ACTION in
  meta-data)
    meta_data
    exit $OCF_SUCCESS
    ;;
  usage|help)
    ip_usage
    exit $OCF_SUCCESS
    ;;
  start)
    ip_start
    rc=$?
    ;;
  stop)
    ip_stop
    rc=$?
    ;;
  monitor)
    ip_monitor
    rc=$?
    ;;
  validate-all)
    ;;
  *)
    ip_usage
    exit $OCF_ERR_UNIMPLEMENTED
    ;;
esac
exit $rc
# vi:sw=4:ts=8:
