#!/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}} ####################################################################### # for debugging this RA DEBUG_LOG_DIR=/tmp/vipfw.ocf.ra.debug DEBUG_LOG=$DEBUG_LOG_DIR/log USE_DEBUG_LOG=false ls_stat_is_dir_0700_root() { set -- $(command ls -ldn "$1" 2>/dev/null); [[ $1/$3 = drwx?-??-?/0 ]] } # try to avoid symlink vuln. if ls_stat_is_dir_0700_root $DEBUG_LOG_DIR && [[ -w "$DEBUG_LOG" && ! -L "$DEBUG_LOG" ]] then USE_DEBUG_LOG=true exec 9>>"$DEBUG_LOG" date >&9 echo "$*" >&9 env | grep OCF_ | sort >&9 else exec 9>/dev/null fi # end of debugging aid ####################################################################### meta_data() { cat < 0.1 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. Master/Slave OCF Resource Agent for Virtual IP address blocking by OS firewall Virtual IP address (CIDR prefixes are accepted too). ip addresses 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". iptables action on promote 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". iptables action on start/demote 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 "$OCF_RESOURCE_INSTANCE ($OCF_RESKEY_vip): Calling $cmd" cmd_out=$( "$@" ) ret=$? if [ $ret != 0 ]; then ocf_log err "$OCF_RESOURCE_INSTANCE ($OCF_RESKEY_vip): Called $cmd" ocf_log err "$OCF_RESOURCE_INSTANCE ($OCF_RESKEY_vip): Exit code $ret" ocf_log err "$OCF_RESOURCE_INSTANCE ($OCF_RESKEY_vip): Command output: $cmd_out" else ocf_log debug "$OCF_RESOURCE_INSTANCE ($OCF_RESKEY_vip): Exit code $ret" ocf_log debug "$OCF_RESOURCE_INSTANCE ($OCF_RESKEY_vip): Command output: $cmd_out" fi echo "$cmd_out" return $ret } vipfw_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 } vipfw_remove_master_score() { do_cmd ${HA_SBIN_DIR}/crm_master -l reboot -D } vipfw_update_master_score() { # We have no any preferences yet # FIXME: Shouldn't we check for presence of VIP locally? # VAL: not sure. vipfw_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 } ####################################################################### vipfw_usage() { echo "\ usage: $0 {start|stop|monitor|validate-all|promote|demote|notify|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 "INPUT" ${catch_regexp} ${regexp_all} ${regexp_vip} policy_input=$? vipfw_get_policy "OUTPUT" ${catch_regexp} ${regexp_vip} ${regexp_all} policy_output=$? if (( $policy_input != $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 } vipfw_monitor() { local status vipfw_status status=$? vipfw_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 # VAL: you are paranoid... until (( rc != 1 )) || (( count > 10 )) ; do # We are in bash, right? # First try to be quiet # Yes, no debug. But no flood in logs too. if [ $USE_DEBUG_LOG == true ] ; then do_cmd $IPTABLES "$@" else $IPTABLES "$@" >/dev/null 2>&1 fi 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 # Yes, no debug. But no flood in logs too. if [ $USE_DEBUG_LOG == true ] ; then do_cmd $IPTABLES "$@" else $IPTABLES "$@" >/dev/null 2>&1 fi 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 INPUT -d $OCF_RESKEY_vip $target rc_in=$? do_iptables $op OUTPUT -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 INPUT -d $OCF_RESKEY_vip $target do_iptables_all $op OUTPUT -s $OCF_RESKEY_vip $target ;; *) ;; esac return $rc } vipfw_start() { local rc local status local first_try=true rc=$OCF_ERR_GENERIC if ! is_firewall_enabled; then ocf_log err "Firewall is not started, unable to block VIPs."$'\n'; 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. vipfw_update_master_score return $rc } vipfw_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_log error "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 } vipfw_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_log error "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 } vipfw_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. vipfw_remove_master_score return $rc } vipfw_notify() { local n_type=$OCF_RESKEY_CRM_meta_notify_type local n_op=$OCF_RESKEY_CRM_meta_notify_operation # FIXME: Nothing to do? return $OCF_SUCCESS } # "macro" to be able to give useful error messages # on clone resource configuration error. meta_expect() { local what=$1 whatvar=OCF_RESKEY_CRM_meta_${1//-/_} op=$2 expect=$3 local val=${!whatvar} if [[ -n $val ]]; then # [, not [[, or it won't work ;) [ $val $op $expect ] && return fi ocf_log err "meta parameter misconfigured, expected $what $op $expect, but found ${val:-unset}." exit $OCF_ERR_CONFIGURED } ls_stat_is_block_maj_147() { set -- $(command ls -L -l "$1" 2>/dev/null) [[ $1 = b* ]] && [[ $5 == 147,* ]] } check_crm_feature_set() { set -- ${OCF_RESKEY_crm_feature_set//[!0-9]/ } local a=${1:-0} b=${2:-0} c=${3:-0} (( a > 3 )) || (( a == 3 && b > 0 )) || (( a == 3 && b == 0 && c > 0 )) || ocf_log warn "You may be disappointed: This RA is intended for pacemaker 1.0 or better!" } vipfw_validate_all() { check_binary $IPTABLES # XXX I really take cibadmin, sed, grep, etc. for granted. check_crm_feature_set # Check clone and M/S options. meta_expect clone-max -le 2 meta_expect clone-node-max = 1 meta_expect master-node-max = 1 meta_expect master-max = 1 case "$OCF_RESKEY_vip" in "") ocf_log err "No Virtual IP address specified!" return $OCF_ERR_CONFIGURED ;; *[!./0-9]*) ocf_log err "IP address must only contain [./0-9]" return $OCF_ERR_CONFIGURED esac case "${OCF_RESKEY_allow_action}" in "pass"|"accept") ;; *) ocf_log err "allow_action must be one of \"accept\" or \"pass\"" return $OCF_ERR_CONFIGURED ;; esac case "${OCF_RESKEY_deny_action}" in "drop"|"reject") ;; *) ocf_log err "deny_action must be one of \"drop\" or \"reject\"" return $OCF_ERR_CONFIGURED ;; esac return $OCF_SUCCESS } ####################################################################### if [ $# != 1 ]; then vipfw_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} case $__OCF_ACTION in meta-data) meta_data exit $OCF_SUCCESS ;; usage) vipfw_usage exit $OCF_SUCCESS esac if $USE_DEBUG_LOG ; then exec 2>&9 set -x fi # Everything except usage and meta-data must pass the validate test vipfw_validate_all || exit case $__OCF_ACTION in start) vipfw_start ;; stop) vipfw_stop ;; notify) vipfw_notify ;; promote) vipfw_promote ;; demote) vipfw_demote ;; status) vipfw_status ;; monitor) vipfw_monitor ;; validate-all) ;; *) vipfw_usage exit $OCF_ERR_UNIMPLEMENTED esac # exit code is the exit code (return code) of the last command (shell function)