#!/bin/bash
#
# OCF resource agent for Linux IPVS connection synchronization
#
# Promoted   = IPVS sync master
# Unpromoted = IPVS sync backup
#

OCF_SUCCESS=0
OCF_ERR_GENERIC=1
OCF_ERR_ARGS=2
OCF_ERR_UNIMPLEMENTED=3
OCF_ERR_PERM=4
OCF_ERR_INSTALLED=5
OCF_ERR_CONFIGURED=6
OCF_NOT_RUNNING=7
OCF_RUNNING_PROMOTED=8
OCF_FAILED_PROMOTED=9

IPVSADM="${IPVSADM:-/usr/sbin/ipvsadm}"
IP="${IP:-/usr/sbin/ip}"

# The IP is used only to discover the LAN interface.
# Different nodes may have different interface names.
OCF_RESKEY_probe_ip="${OCF_RESKEY_probe_ip:-192.168.1.1}"
OCF_RESKEY_interface="${OCF_RESKEY_interface:-}"

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

  <longdesc lang="en">
    Manages the Linux IPVS connection synchronization daemon as a
    Pacemaker promotable resource.

    Unpromoted instances run the IPVS backup synchronization daemon.
    The promoted instance runs the IPVS master synchronization daemon.
  </longdesc>

  <shortdesc lang="en">
    IPVS connection synchronization
  </shortdesc>

  <parameters>
    <parameter name="probe_ip" unique="0" required="0">
      <longdesc lang="en">
        An IPv4 address reachable through the interface that should
        carry IPVS synchronization traffic. Used only for automatic
        interface discovery.
      </longdesc>
      <shortdesc lang="en">Interface discovery IP</shortdesc>
      <content type="string" default="192.168.1.1"/>
    </parameter>

    <parameter name="interface" unique="0" required="0">
      <longdesc lang="en">
        Optional explicit synchronization interface. If omitted,
        the interface is discovered using the route to probe_ip.
      </longdesc>
      <shortdesc lang="en">Synchronization interface</shortdesc>
      <content type="string"/>
    </parameter>
  </parameters>

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

get_iface()
{
    if [ -n "$OCF_RESKEY_interface" ]; then
        printf '%s\n' "$OCF_RESKEY_interface"
        return 0
    fi

    "$IP" route get "$OCF_RESKEY_probe_ip" 2>/dev/null |
        awk '
        {
            for (i=1; i<=NF; i++) {
                if ($i == "dev" && (i+1) <= NF) {
                    print $(i+1)
                    exit
                }
            }
        }'
}

daemon_status()
{
    "$IPVSADM" -L --daemon 2>/dev/null
}

set_promotion_score()
{
    /usr/sbin/crm_attribute         --promotion         --update 100         >/dev/null 2>&1 || true
}

monitor_resource()
{
    local output
    local have_master=0
    local have_backup=0

    output="$(daemon_status)"

    printf '%s\n' "$output" |
        grep -q '^master sync daemon' &&
        have_master=1

    printf '%s\n' "$output" |
        grep -q '^backup sync daemon' &&
        have_backup=1

    # Both roles simultaneously is an invalid state.
    if [ "$have_master" -eq 1 ] &&
       [ "$have_backup" -eq 1 ]; then
        return "$OCF_ERR_GENERIC"
    fi

    if [ "$have_master" -eq 1 ]; then
        set_promotion_score
        return "$OCF_RUNNING_PROMOTED"
    fi

    if [ "$have_backup" -eq 1 ]; then
        set_promotion_score
        return "$OCF_SUCCESS"
    fi

    return "$OCF_NOT_RUNNING"
}

stop_master()
{
    "$IPVSADM" --stop-daemon master >/dev/null 2>&1 || true
}

stop_backup()
{
    "$IPVSADM" --stop-daemon backup >/dev/null 2>&1 || true
}

start_master()
{
    local iface

    iface="$(get_iface)"

    if [ -z "$iface" ]; then
        echo "ERROR: unable to determine synchronization interface" >&2
        return "$OCF_ERR_GENERIC"
    fi

    "$IPVSADM" \
        --start-daemon master \
        --mcast-interface "$iface"
}

start_backup()
{
    local iface

    iface="$(get_iface)"

    if [ -z "$iface" ]; then
        echo "ERROR: unable to determine synchronization interface" >&2
        return "$OCF_ERR_GENERIC"
    fi

    "$IPVSADM" \
        --start-daemon backup \
        --mcast-interface "$iface"
}

start_resource()
{
    monitor_resource
    rc=$?

    case "$rc" in
        "$OCF_SUCCESS")
            # Already running correctly as backup.
            return "$OCF_SUCCESS"
            ;;

        "$OCF_RUNNING_PROMOTED")
            # A start operation must leave a promotable resource
            # in its unpromoted state.
            stop_master
            start_backup || return "$OCF_ERR_GENERIC"
            ;;

        "$OCF_NOT_RUNNING")
            start_backup || return "$OCF_ERR_GENERIC"
            ;;

        *)
            return "$OCF_ERR_GENERIC"
            ;;
    esac

    monitor_resource
    [ $? -eq "$OCF_SUCCESS" ] &&
        return "$OCF_SUCCESS"

    return "$OCF_ERR_GENERIC"
}

stop_resource()
{
    stop_master
    stop_backup

    monitor_resource
    rc=$?

    [ "$rc" -eq "$OCF_NOT_RUNNING" ] &&
        return "$OCF_SUCCESS"

    return "$OCF_ERR_GENERIC"
}

promote_resource()
{
    monitor_resource
    rc=$?

    case "$rc" in
        "$OCF_RUNNING_PROMOTED")
            return "$OCF_SUCCESS"
            ;;

        "$OCF_SUCCESS")
            stop_backup

            if ! start_master; then
                # Try to return to a safe unpromoted state.
                start_backup >/dev/null 2>&1 || true
                return "$OCF_ERR_GENERIC"
            fi
            ;;

        *)
            echo "ERROR: cannot promote IPVS sync resource from state rc=$rc" >&2
            return "$OCF_ERR_GENERIC"
            ;;
    esac

    monitor_resource
    [ $? -eq "$OCF_RUNNING_PROMOTED" ] &&
        return "$OCF_SUCCESS"

    return "$OCF_ERR_GENERIC"
}

demote_resource()
{
    monitor_resource
    rc=$?

    case "$rc" in
        "$OCF_SUCCESS")
            return "$OCF_SUCCESS"
            ;;

        "$OCF_RUNNING_PROMOTED")
            stop_master

            if ! start_backup; then
                return "$OCF_ERR_GENERIC"
            fi
            ;;

        "$OCF_NOT_RUNNING")
            # Be defensive: demote should result in a running
            # unpromoted instance.
            start_backup || return "$OCF_ERR_GENERIC"
            ;;

        *)
            return "$OCF_ERR_GENERIC"
            ;;
    esac

    monitor_resource
    [ $? -eq "$OCF_SUCCESS" ] &&
        return "$OCF_SUCCESS"

    return "$OCF_ERR_GENERIC"
}

validate_all()
{
    if [ ! -x "$IPVSADM" ]; then
        echo "ERROR: ipvsadm not found at $IPVSADM" >&2
        return "$OCF_ERR_INSTALLED"
    fi

    if [ ! -x "$IP" ]; then
        echo "ERROR: ip command not found at $IP" >&2
        return "$OCF_ERR_INSTALLED"
    fi

    iface="$(get_iface)"

    if [ -z "$iface" ]; then
        echo "ERROR: unable to determine synchronization interface" >&2
        return "$OCF_ERR_ARGS"
    fi

    if ! "$IP" link show dev "$iface" >/dev/null 2>&1; then
        echo "ERROR: interface '$iface' does not exist" >&2
        return "$OCF_ERR_ARGS"
    fi

    return "$OCF_SUCCESS"
}

case "$1" in
    meta-data)
        meta_data
        exit "$OCF_SUCCESS"
        ;;

    validate-all)
        validate_all
        exit $?
        ;;

    start)
        start_resource
        exit $?
        ;;

    stop)
        stop_resource
        exit $?
        ;;

    monitor|status)
        monitor_resource
        exit $?
        ;;

    promote)
        promote_resource
        exit $?
        ;;

    demote)
        demote_resource
        exit $?
        ;;

    *)
        echo "Usage: $0 {start|stop|monitor|promote|demote|validate-all|meta-data}" >&2
        exit "$OCF_ERR_UNIMPLEMENTED"
        ;;
esac
