#!/bin/bash
#
#
#        OCF Resource Agent compliant script
#
# Copyright (c) 2010 Vitalativ S.R.O.,
# Copyright (c) 2010 Vladislav Bogdanov
#
#                     All Rights Reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it would be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# Further, this software is distributed without any warranty that it is
# free of the rightful claim of any third person regarding infringement
# or the like.    Any license provided herein, whether implied or
# otherwise, applies only to this software file.  Patent licenses, if
# any, provided herein do not apply to combinations of this program with
# other software, or any other product whatsoever.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write the Free Software Foundation,
# Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA.
#
#

# OCF instance parameters
#    OCF_RESKEY_vip
#    OCF_RESKEY_CRM_meta_clone_max
#    OCF_RESKEY_CRM_meta_clone_node_max
#    OCF_RESKEY_CRM_meta_master_max
#    OCF_RESKEY_CRM_meta_master_node_max

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

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

export LANG=C LANGUAGE=C LC_ALL=C

# Defaults according to "Configuration 1.0 Explained",
# "Multi-state resource configuration options"
: ${OCF_RESKEY_CRM_meta_clone_node_max=1}
: ${OCF_RESKEY_CRM_meta_master_max=1}
: ${OCF_RESKEY_CRM_meta_master_node_max=1}

OCF_RESKEY_allow_action_default="accept"
: ${OCF_RESKEY_allow_action=${OCF_RESKEY_allow_action_default}}

OCF_RESKEY_deny_action_default="drop"
: ${OCF_RESKEY_deny_action=${OCF_RESKEY_deny_action_default}}

OCF_RESKEY_input_chain_default="INPUT"
: ${OCF_RESKEY_input_chain=${OCF_RESKEY_input_chain_default}}

OCF_RESKEY_output_chain_default="OUTPUT"
: ${OCF_RESKEY_output_chain=${OCF_RESKEY_output_chain_default}}

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

<longdesc lang="en">
This resource agent manages firewall rules for Virtual IP addresses
as a master/slave resource, blocking all traffic to/from VIP in Slave state and allowing
it in a Master state. Main purpose of this to be written is support for seamless IET iSCSI
target resource (backed by DRBD) migration.
</longdesc>

<shortdesc lang="en">Master/Slave OCF Resource Agent for Virtual IP address blocking by OS firewall</shortdesc>

<parameters>

<parameter name="vip" unique="1" required="1">
<longdesc lang="en">
Virtual IP address (CIDR prefixes are accepted too).
</longdesc>
<shortdesc lang="en">ip addresses</shortdesc>
<content type="string"/>
</parameter>

<parameter name="allow_action" required="0" unique="0">
<longdesc lang="en">
How should iptables be configured on promote. Must be one of "accept" or "pass",
first one will explicitely permit traffic to/from VIP, second will pass it further to chains.
Default is "accept".
</longdesc>
<shortdesc lang="en">iptables action on promote</shortdesc>
<content type="string" default="${OCF_RESKEY_allow_action_default}"/>
</parameter>

<parameter name="deny_action" required="0" unique="0">
<longdesc lang="en">
How should iptables be configured on start/demote. Must be one of "drop" or "reject",
first one will silently drop traffic to/from VIP, second will result in ICMP "port-unreachable" messages.
Default is "drop".
</longdesc>
<shortdesc lang="en">iptables action on start/demote</shortdesc>
<content type="string" default="${OCF_RESKEY_deny_action_default}"/>
</parameter>

<parameter name="input_chain" required="0" unique="0">
<longdesc lang="en">
Name of chain used to filter incoming packets.
</longdesc>
<shortdesc lang="en">Input chain name</shortdesc>
<content type="string" default="${OCF_RESKEY_input_chain_default}"/>
</parameter>

<parameter name="output_chain" required="0" unique="0">
<longdesc lang="en">
Name of chain used to filter outgoing packets.
</longdesc>
<shortdesc lang="en">Output chain name</shortdesc>
<content type="string" default="${OCF_RESKEY_output_chain_default}"/>
</parameter>

</parameters>

<actions>
<action name="start"     timeout="10" />
<action name="promote"   timeout="10" />
<action name="demote"    timeout="10" />
<action name="stop"      timeout="10" />
<action name="monitor"   timeout="5" interval="5" role="Slave" />
<action name="monitor"   timeout="5" interval="10" role="Master" />
<action name="meta-data" timeout="5" />
<action name="validate-all"  timeout="5" />
</actions>
</resource-agent>
END
}

do_cmd() {
    # Run a command, return its exit code, capture any output, and log
    # everything if appropriate.
    local cmd="$*" cmd_out ret
    ocf_log debug "Calling ${cmd}"
    cmd_out=$( eval "$@" )
    ret=$?

    if [ ${ret} -ne 0 ] ; then
        ocf_log err "Called ${cmd}"
        ocf_log err "Exit code ${ret}"
        ocf_log err "Command output: ${cmd_out}"
    else
        ocf_log debug "Exit code ${ret}"
        ocf_log debug "Command output: ${cmd_out}"
    fi

    echo "${cmd_out}"

    return ${ret}
}

set_master_score() {
    # Use quiet mode (-Q) to quench logging. Actual score updates
    # will get logged by attrd anyway
    do_cmd "${HA_SBIN_DIR}/crm_master" -Q -l reboot -v "$1"
}

remove_master_score() {
    do_cmd "${HA_SBIN_DIR}/crm_master" -l reboot -D
}

update_master_score() {
    # We have no any preferences yet
    # FIXME: Shouldn't we check for presence of VIP locally?
    # VAL: not sure.
    set_master_score 1000

    return $OCF_SUCCESS
}

is_firewall_enabled() {
    # VAL: pros and cons with using dedicated TABLE?
    test -f /proc/net/ip_tables_names
}

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


usage() {
    echo "\
usage: $0 {start|stop|monitor|validate-all|promote|demote|meta-data}

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

VIPFW_POLICY_UNKNOWN=0
VIPFW_POLICY_ACCEPT=1
VIPFW_POLICY_PASS=2
VIPFW_POLICY_DROP=3
VIPFW_POLICY_REJECT=4

vipfw_get_policy() {
    local policy

    set -- $( iptables -nL "$1" | \
        grep -E "$2[[:space:]]+all[[:space:]]+--[[:space:]]+$3[[:space:]]+$4[[:space:]]+$"| \
        head -n1 )
    case $1 in
        ACCEPT)
            policy=$VIPFW_POLICY_ACCEPT
            ;;
        DROP)
            policy=$VIPFW_POLICY_DROP
            ;;
        REJECT)
            policy=$VIPFW_POLICY_REJECT
            ;;
        all) # No target specified and second field is catched instead of first one
            policy=$VIPFW_POLICY_PASS
            ;;
        *)
            policy=$VIPFW_POLICY_UNKNOWN
            ;;
    esac
    return ${policy}
}

vipfw_status() {
    local rc policy_input policy_output
    local catch_regexp="^(ACCEPT|DROP|REJECT|[[:space:]]+)"

    rc=$OCF_NOT_RUNNING

    if ! is_firewall_enabled ; then
        return ${rc}
    fi

    case "$1" in
        non_pass)
            catch_regexp="^(ACCEPT|DROP|REJECT)"
            ;;
        *)
            ;;
    esac
    regexp_vip=${OCF_RESKEY_vip//./\\.} # Prepare dots for regexp
    regexp_vip=${regexp_vip/\/32/} # Strip /32 if any to comply with iptables output
    regexp_all="0\.0\.0\.0/0"

    vipfw_get_policy "${OCF_RESKEY_input_chain}" "${catch_regexp}" "${regexp_all}" "${regexp_vip}"
    policy_input=$?

    vipfw_get_policy "${OCF_RESKEY_output_chain}" "${catch_regexp}" "${regexp_vip}" "${regexp_all}"
    policy_output=$?

    if [ ${policy_input} -ne ${policy_output} ] ; then
        return $OCF_ERR_GENERIC # ??? FIXME!!!
    fi

    case "${policy_input}" in
        $VIPFW_POLICY_ACCEPT)
            if [ "${OCF_RESKEY_allow_action}" = "accept" ] ; then
                rc=$OCF_RUNNING_MASTER
            fi
            ;;
        $VIPFW_POLICY_PASS)
            if [ "${OCF_RESKEY_allow_action}" = "pass" ] ; then
                # Recursion, check next rule after 'pass' one
                vipfw_status "non_pass"
                case $? in
                    $OCF_SUCCESS)
                        # We have 'drop' or' reject' rule right after 'pass' rule and
                        # that rule is configured by ourselves.
                        # That means that traffic will be blocked.
                        # Fail for now.
                        rc=$OCF_ERR_GENERIC
                        ;;
                    $OCF_NOT_RUNNING)
                        # We didn't find any more rules except our 'pass' rule.
                        # This means that we are configured correctly, independently
                        # of correct or broken default iptables setup.
                        # If traffic is blocked by later rules then that's not our fault.
                        rc=$OCF_RUNNING_MASTER
                        ;;
                    $OCF_ERR_GENERIC)
                        # Hmmm. Something is really broken.
                        rc=$OCF_ERR_GENERIC
                        ;;
                esac
            fi
            ;;
        $VIPFW_POLICY_DROP)
            if [ "${OCF_RESKEY_deny_action}" = "drop" ] ; then
                rc=$OCF_SUCCESS
            fi
            ;;
        $VIPFW_POLICY_REJECT)
            if [ "${OCF_RESKEY_deny_action}" = "reject" ] ; then
                rc=$OCF_SUCCESS
            fi
            ;;
        *)
            ;;
    esac

    return ${rc}
}

monitor() {
    local status

    vipfw_status
    status=$?

    update_master_score

    return ${status}
}

# This will return after first successful operation
do_iptables() {
    local rc=1
    local count=0

    # Call iptables from within a loop, because iptables is known to fail
    # on concurrent operations
    # iptables returns 2 on command line params error, stop in that case too

    until (( rc != 1 )) || (( count > 10 )) ; do # We are in bash, right?
        # First try to be quiet
        eval $IPTABLES "$@" >/dev/null 2>&1
        rc=$?
        (( count++ ))
    done
    if (( count > 10 )) ; then
        # Try once more and log error
        do_cmd $IPTABLES "$@"
        rc=$?
    fi
    return ${rc}
}

# This will run command in loop until first error
do_iptables_all() {
    local rc=0
    local count=0

    # Call iptables from within a loop, because iptables is known to fail
    # on concurrent operations
    # iptables returns 2 on command line params error
    until (( rc != 0 )) ; do
        # First try to be quiet
        eval $IPTABLES "$@" >/dev/null 2>&1
        rc=$?
        (( count++ ))
    done
    if (( count > 1 )) ; then # We succeded at least once
        return 0
    else
        return ${rc}
    fi
}

vipfw_op() {
    local op op1
    local target target1
    local rc_in
    local rc_out
    local rc

    case "$1" in
        # It is not safe to use "-R" because the only way to replace rule is by its rule number
        # But, that number can change between read and replace operations. So I use insert/delete.
        add|block|unblock)
            # We should add our rules very early in list to avoid default setup with stateful firewall
            # break things
            op="-I"
            target="-j DROP"
            case $1 in
                unblock)
                    if [ "${OCF_RESKEY_allow_action}" = "pass" ] ; then
                        target=""
                    else
                        target="-j ACCEPT"
                    fi
                    ;;
                *)
                    ;;
            esac

            do_iptables "${op}" "${OCF_RESKEY_input_chain}" -d "${OCF_RESKEY_vip}" "${target}"
            rc_in=$?
            do_iptables "${op}" "${OCF_RESKEY_output_chain}" -s "${OCF_RESKEY_vip}" "${target}"
            rc_out=$?

            if (( rc_in )) || (( rc_out )) ; then
                rc=1
            else
                rc=0
            fi
            ;;
        *)
            ;;
    esac

    case "$1" in
        block|unblock|delete)
            # be safe on deletion: delete by rule spec, not by rule number
            op="-D"
            target="-j DROP"
            case "$1" in
                block)
                    # It is not safe to use "-R" because the only way to replace rule is by its rule number
                    # But, that number can change between read and replace operations
                    if [ "${OCF_RESKEY_allow_action}" = "pass" ] ; then
                        target=""
                    else
                        target="-j ACCEPT"
                    fi
                    ;;
                *)
                    ;;
            esac
            # Delete all matching rules (they can remain from previous runs)
            # We are not interested in result, because this is only a cleanup.
            # FIXME: Nope, that's wrong. We MUST ensure that all DROP rules are deleted if
            # we use 'pass' mode'
            do_iptables_all "${op}" "${OCF_RESKEY_input_chain}" -d "${OCF_RESKEY_vip}" "${target}"
            do_iptables_all "${op}" "${OCF_RESKEY_output_chain}" -s "${OCF_RESKEY_vip}" "${target}"
            ;;
        *)
            ;;
    esac

    return ${rc}
}

start() {
    local rc
    local status
    local first_try=true

    rc=$OCF_ERR_GENERIC

    if ! is_firewall_enabled ; then
        ocf_exit_reason "Firewall is not started, unable to block VIPs."
        return $OCF_ERR_INSTALLED
    fi

    # Keep trying to bring up the resource;
    # wait for the CRM to time us out if this fails
    while : ; do
        vipfw_status
        status=$?
        case ${status} in
            $OCF_SUCCESS)
                rc=$OCF_SUCCESS
                break
                ;;
            $OCF_NOT_RUNNING)
                vipfw_op add
                ;;
            $OCF_RUNNING_MASTER)
                ocf_log warn "${OCF_RESKEY_vip} already promoted, demoting."
                vipfw_op block
                ;;
            $OCF_ERR_GENERIC)
                vipfw_op block
                ;;
        esac
        ${first_try} || sleep 1
        first_try=false
    done
    # in case someone does not configure monitor,
    # we must at least call it once after start.
    update_master_score

    return ${rc}
}

promote() {
    local rc
    local status
    local first_try=true

    rc=$OCF_ERR_GENERIC

    # Keep trying to promote the resource;
    # wait for the CRM to time us out if this fails
    while : ; do
        vipfw_status
        status=$?
        case ${status} in
            $OCF_SUCCESS)
                vipfw_op unblock
                ;;
            $OCF_NOT_RUNNING)
                ocf_exit_reason "Trying to promote a resource that was not started"
                break
                ;;
            $OCF_RUNNING_MASTER)
                rc=$OCF_SUCCESS
                break
                ;;
            $OCF_ERR_GENERIC)
                vipfw_op unblock
                ;;
        esac
        ${first_try} || sleep 1
        first_try=false
    done

    return ${rc}
}

demote() {
    local rc
    local status
    local first_try=true

    rc=$OCF_ERR_GENERIC

    # Keep trying to demote the resource;
    # wait for the CRM to time us out if this fails
    while : ; do
        vipfw_status
        status=$?
        case ${status} in
            $OCF_SUCCESS)
                rc=$OCF_SUCCESS
                break
                ;;
            $OCF_NOT_RUNNING)
                ocf_exit_reason "Trying to demote a resource that was not started"
                break
                ;;
            $OCF_RUNNING_MASTER)
                vipfw_op block
                ;;
            $OCF_ERR_GENERIC)
                vipfw_op block
                ;;
        esac
        ${first_try} || sleep 1
        first_try=false
    done

    return ${rc}
}

stop() {
    local rc=$OCF_ERR_GENERIC
    local first_try=true

    # Keep trying to bring down the resource;
    # wait for the CRM to time us out if this fails
    while : ; do
        vipfw_status
        status=$?
        case ${status} in
            $OCF_SUCCESS)
                vipfw_op delete
                ;;
            $OCF_NOT_RUNNING)
                rc=$OCF_SUCCESS
                break
                ;;
            $OCF_RUNNING_MASTER)
                ocf_log warn "${OCF_RESKEY_vip} still Primary, demoting."
                vipfw_op block
                ;;
            $OCF_ERR_GENERIC)
                vipfw_op block
                vipfw_op delete
                ;;
        esac
        ${first_try} || sleep 1
        first_try=false
    done

    # do not let old master scores laying around.
    # they may confuse crm if this node was set to standby.
    remove_master_score

    return ${rc}
}


# "macro" to be able to give useful error messages
# on clone resource configuration error.
meta_expect() {
    local what=$1
    local whatvar=OCF_RESKEY_CRM_meta_${1//-/_}
    local op=$2
    local expect=$3
    local val=${!whatvar}
    if [ -n "${val}" ] ; then
        # [, not [[, or it won't work ;)
        [ ${val} ${op} ${expect} ] && return $OCF_SUCCESS
    fi
    ocf_exit_reason "meta parameter misconfigured, expected ${what} ${op} ${expect}, but found ${val:-unset}."
    return $OCF_ERR_CONFIGURED
}

validate() {
    ( check_binary "$IPTABLES" ) || return $OCF_ERR_INSTALLED

    # Check clone and M/S options.
    meta_expect clone-max -le 2 || return $?
    meta_expect clone-node-max -eq 1 || return $?
    meta_expect master-node-max -eq 1 || return $?
    meta_expect master-max -eq 1 || return $?

    case "${OCF_RESKEY_vip}" in
        "")
            ocf_exit_reason "No Virtual IP address specified!"
            return $OCF_ERR_CONFIGURED
            ;;
        *[!./0-9]*)
            ocf_exit_reason "IP address must only contain [./0-9]"
            return $OCF_ERR_CONFIGURED
    esac

    case "${OCF_RESKEY_allow_action}" in
        "pass"|"accept")
            ;;
        *)
            ocf_exit_reason "allow_action must be one of \"accept\" or \"pass\""
            return $OCF_ERR_CONFIGURED
            ;;
    esac

    case "${OCF_RESKEY_deny_action}" in
        "drop"|"reject")
            ;;
        *)
            ocf_exit_reason "deny_action must be one of \"drop\" or \"reject\""
            return $OCF_ERR_CONFIGURED
            ;;
    esac

    return $OCF_SUCCESS
}

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

if [ $# != 1 ] ; then
    usage
    exit $OCF_ERR_ARGS
fi

# if ${__OCF_ACTION} = monitor, but meta_interval not set,
# this is a "probe". we could change behaviour.
: ${OCF_RESKEY_CRM_meta_interval=0}

VALIDATION_FAILURE_FLAG="${HA_RSCTMP}/${OCF_RESOURCE_INSTANCE}.invalid"

case "${__OCF_ACTION}" in
    meta-data)
        meta_data
        exit $OCF_SUCCESS
        ;;
    usage|help)
        usage
        exit $OCF_SUCCESS
        ;;
    start)
        validate
        ret=$?
        if [ ${ret} -ne $OCF_SUCCESS ] ; then
            touch "${VALIDATION_FAILURE_FLAG}"
            exit ${ret}
        else
            rm -f "${VALIDATION_FAILURE_FLAG}"
        fi
        ;;
    stop)
        validate
        ret=$?
        if [ ${ret} -ne $OCF_SUCCESS ] ; then
            if [ -f "${VALIDATION_FAILURE_FLAG}" ] ; then
                rm -f "${VALIDATION_FAILURE_FLAG}"
                exit $OCF_SUCCESS
            else
                exit ${ret}
            fi
        fi
        ;;
    *) # monitor | notify | reload | etc
        validate
        ret=$?
        if [ ${ret} -ne $OCF_SUCCESS ] ; then
            if ocf_is_probe ; then
                # What if cluster is exiting the maintenance mode?
                exit $OCF_NOT_RUNNING
            fi
            exit ${ret}
        fi
        ;;
esac

case "${__OCF_ACTION}" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    promote)
        promote
        ;;
    demote)
        demote
        ;;
    monitor)
        monitor
        ;;
    validate-all)
        exit $OCF_SUCCESS
        ;;
    *)
        usage
        exit $OCF_ERR_UNIMPLEMENTED
esac

exit $?
