diff --git a/.gitignore b/.gitignore index 5175038..a31cf26 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ SOURCES/ClusterLabs-resource-agents-e711383f.tar.gz -SOURCES/SAPHanaSR-2067519.tar.gz SOURCES/SAPHanaSR-ScaleOut-a77e8c6.tar.gz +SOURCES/SAPHanaSR-d1dde99.tar.gz SOURCES/aliyun-cli-2.1.10.tar.gz SOURCES/aliyun-python-sdk-core-2.8.5.tar.gz SOURCES/aliyun-python-sdk-ecs-4.9.3.tar.gz diff --git a/.resource-agents.metadata b/.resource-agents.metadata index 943736b..ba66161 100644 --- a/.resource-agents.metadata +++ b/.resource-agents.metadata @@ -1,6 +1,6 @@ 0358e1cb7fe86b2105bd2646cbe86f3c0273844a SOURCES/ClusterLabs-resource-agents-e711383f.tar.gz -92409ca65e8f4e63d5c308368861fa67ced470f1 SOURCES/SAPHanaSR-2067519.tar.gz e5624e3028720d0d55ab3d598655a1938042e26c SOURCES/SAPHanaSR-ScaleOut-a77e8c6.tar.gz +442679403d1287835841d70c5bef208a330c2c11 SOURCES/SAPHanaSR-d1dde99.tar.gz 306e131d8908ca794276bfe3a0b55ccc3bbd482f SOURCES/aliyun-cli-2.1.10.tar.gz b68122ef53a0857d020529ff85d14f0e905c327f SOURCES/aliyun-python-sdk-core-2.8.5.tar.gz c2a98b9a1562d223a76514f05028488ca000c395 SOURCES/aliyun-python-sdk-ecs-4.9.3.tar.gz diff --git a/SOURCES/bz1582685-IPsrcaddr-1-add-destination-and-table-parameters.patch b/SOURCES/bz1582685-IPsrcaddr-1-add-destination-and-table-parameters.patch new file mode 100644 index 0000000..cb4fde4 --- /dev/null +++ b/SOURCES/bz1582685-IPsrcaddr-1-add-destination-and-table-parameters.patch @@ -0,0 +1,202 @@ +--- ClusterLabs-resource-agents-e711383f/heartbeat/IPsrcaddr 2019-08-15 16:02:10.055827624 +0200 ++++ /home/oalbrigt/src/resource-agents/heartbeat/IPsrcaddr 2019-08-15 15:45:50.690757838 +0200 +@@ -1,6 +1,6 @@ + #!/bin/sh + # +-# Description: IPsrcaddr - Preferred source address modification ++# Description: IPsrcaddr - Preferred source(/dest) address modification + # + # Author: John Sutton + # Support: users@clusterlabs.org +@@ -11,7 +11,7 @@ + # + # This script manages the preferred source address associated with + # packets which originate on the localhost and are routed through the +-# default route. By default, i.e. without the use of this script or ++# matching route. By default, i.e. without the use of this script or + # similar, these packets will carry the IP of the primary i.e. the + # non-aliased interface. This can be a nuisance if you need to ensure + # that such packets carry the same IP irrespective of which host in +@@ -27,7 +27,7 @@ + # + # NOTES: + # +-# 1) There must be one and not more than 1 default route! Mainly because ++# 1) There must be one and not more than 1 matching route! Mainly because + # I can't see why you should have more than one. And if there is more + # than one, we would have to box clever to find out which one is to be + # modified, or we would have to pass its identity as an argument. +@@ -54,16 +54,25 @@ + . ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs + + # Defaults ++OCF_RESKEY_ipaddress_default="" ++OCF_RESKEY_cidr_netmask_default="" ++OCF_RESKEY_destination_default="0.0.0.0/0" + OCF_RESKEY_proto_default="" ++OCF_RESKEY_table_default="" + ++: ${OCF_RESKEY_ipaddress=${OCF_RESKEY_ipaddress_default}} ++: ${OCF_RESKEY_cidr_netmask=${OCF_RESKEY_cidr_netmask_default}} ++: ${OCF_RESKEY_destination=${OCF_RESKEY_destination_default}} + : ${OCF_RESKEY_proto=${OCF_RESKEY_proto_default}} ++: ${OCF_RESKEY_table=${OCF_RESKEY_table_default}} + ####################################################################### + + [ -z "$OCF_RESKEY_proto" ] && PROTO="" || PROTO="proto $OCF_RESKEY_proto" ++[ -z "$OCF_RESKEY_table" ] && TABLE="" || TABLE="table $OCF_RESKEY_table" + + USAGE="usage: $0 {start|stop|status|monitor|validate-all|meta-data}"; + +- CMDSHOW="$IP2UTIL route show to exact 0.0.0.0/0" ++ CMDSHOW="$IP2UTIL route show $TABLE to exact $OCF_RESKEY_destination" + CMDCHANGE="$IP2UTIL route change to " + + SYSTYPE="`uname -s`" +@@ -91,7 +100,7 @@ + The IP address. + + IP address +- ++ + + + +@@ -100,7 +109,15 @@ + dotted quad notation 255.255.255.0). + + Netmask +- ++ ++ ++ ++ ++ ++The destination IP/subnet for the route (default: $OCF_RESKEY_destination_default) ++ ++Destination IP/subnet ++ + + + +@@ -108,7 +125,17 @@ + Proto to match when finding network. E.g. "kernel". + + Proto +- ++ ++ ++ ++ ++ ++Table to modify. E.g. "local". ++ ++The table has to have a route matching the "destination" parameter. ++ ++Table ++ + + + +@@ -151,21 +178,22 @@ + export OCF_RESKEY_ip=$OCF_RESKEY_ipaddress + + srca_read() { +- # Capture the default route - doublequotes prevent word splitting... +- DEFROUTE="`$CMDSHOW`" || errorexit "command '$CMDSHOW' failed" +- +- # ... so we can make sure there is only 1 default route +- [ 1 -eq `echo "$DEFROUTE" | wc -l` ] || \ +- errorexit "more than 1 default route exists" ++ # Capture matching route - doublequotes prevent word splitting... ++ ROUTE="`$CMDSHOW`" || errorexit "command '$CMDSHOW' failed" + +- # But there might still be no default route +- [ -z "$DEFROUTE" ] && errorexit "no default route exists" ++ # ... so we can make sure there is only 1 matching route ++ [ 1 -eq `echo "$ROUTE" | wc -l` ] || \ ++ errorexit "more than 1 matching route exists" ++ ++ # But there might still be no matching route ++ [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] && [ -z "$ROUTE" ] && \ ++ ! ocf_is_probe && errorexit "no matching route exists" + + # Sed out the source ip address if it exists +- SRCIP=`echo $DEFROUTE | sed -n "s/$MATCHROUTE/\3/p"` ++ SRCIP=`echo $ROUTE | sed -n "s/$MATCHROUTE/\3/p"` + + # and what remains after stripping out the source ip address clause +- ROUTE_WO_SRC=`echo $DEFROUTE | sed "s/$MATCHROUTE/\1\5/"` ++ ROUTE_WO_SRC=`echo $ROUTE | sed "s/$MATCHROUTE/\1\5/"` + + [ -z "$SRCIP" ] && return 1 + [ $SRCIP = $1 ] && return 0 +@@ -185,11 +213,13 @@ + rc=$OCF_SUCCESS + ocf_log info "The ip route has been already set.($NETWORK, $INTERFACE, $ROUTE_WO_SRC)" + else +- $IP2UTIL route replace $NETWORK dev $INTERFACE src $1 || \ +- errorexit "command 'ip route replace $NETWORK dev $INTERFACE src $1' failed" ++ $IP2UTIL route replace $TABLE $NETWORK dev $INTERFACE src $1 || \ ++ errorexit "command 'ip route replace $TABLE $NETWORK dev $INTERFACE src $1' failed" + +- $CMDCHANGE $ROUTE_WO_SRC src $1 || \ +- errorexit "command '$CMDCHANGE $ROUTE_WO_SRC src $1' failed" ++ if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] ;then ++ $CMDCHANGE $ROUTE_WO_SRC src $1 || \ ++ errorexit "command '$CMDCHANGE $ROUTE_WO_SRC src $1' failed" ++ fi + rc=$? + fi + +@@ -201,7 +231,7 @@ + # If one exists but it's not the same as the one specified, that's + # an error. Maybe that's the wrong behaviour because if this fails + # then when IPaddr releases the associated interface (if there is one) +-# your default route will also get dropped ;-( ++# your matching route will also get dropped ;-( + # The exit code should conform to LSB exit codes. + # + +@@ -217,11 +247,13 @@ + + [ $rc = 2 ] && errorexit "The address you specified to stop does not match the preferred source address" + +- $IP2UTIL route replace $NETWORK dev $INTERFACE || \ +- errorexit "command 'ip route replace $NETWORK dev $INTERFACE' failed" ++ $IP2UTIL route replace $TABLE $NETWORK dev $INTERFACE || \ ++ errorexit "command 'ip route replace $TABLE $NETWORK dev $INTERFACE' failed" + +- $CMDCHANGE $ROUTE_WO_SRC || \ +- errorexit "command '$CMDCHANGE $ROUTE_WO_SRC' failed" ++ if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] ;then ++ $CMDCHANGE $ROUTE_WO_SRC || \ ++ errorexit "command '$CMDCHANGE $ROUTE_WO_SRC' failed" ++ fi + + return $? + } +@@ -406,6 +438,10 @@ + return $OCF_ERR_CONFIGURED + fi + ++ if ! echo "$OCF_RESKEY_destination" | grep -q "/"; then ++ return $OCF_ERR_CONFIGURED ++ fi ++ + + if ! [ "x$SYSTYPE" = "xLinux" ]; then + # checks after this point are only relevant for linux. +@@ -486,7 +522,11 @@ + } + + INTERFACE=`echo $findif_out | awk '{print $1}'` +-NETWORK=`$IP2UTIL route list dev $INTERFACE scope link $PROTO match $ipaddress|grep -m 1 -o '^[^ ]*'` ++if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] ;then ++ NETWORK=`$IP2UTIL route list dev $INTERFACE scope link $PROTO match $ipaddress|grep -m 1 -o '^[^ ]*'` ++else ++ NETWORK="$OCF_RESKEY_destination" ++fi + + case $1 in + start) srca_start $ipaddress diff --git a/SOURCES/bz1582685-IPsrcaddr-2-local-rule-destination-fixes.patch b/SOURCES/bz1582685-IPsrcaddr-2-local-rule-destination-fixes.patch new file mode 100644 index 0000000..cca64cb --- /dev/null +++ b/SOURCES/bz1582685-IPsrcaddr-2-local-rule-destination-fixes.patch @@ -0,0 +1,42 @@ +From 0e73d3f474d08779b64ed99fb3f80c1e806ff1b7 Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Thu, 28 Nov 2019 16:11:51 +0100 +Subject: [PATCH] IPsrcaddr: fixes to replace local rule if using local table, + and set src back to primary for device on stop + +--- + heartbeat/IPsrcaddr | 14 ++++++++++++-- + 1 file changed, 12 insertions(+), 2 deletions(-) + +diff --git a/heartbeat/IPsrcaddr b/heartbeat/IPsrcaddr +index d80b72165..f9085f082 100755 +--- a/heartbeat/IPsrcaddr ++++ b/heartbeat/IPsrcaddr +@@ -75,6 +75,10 @@ USAGE="usage: $0 {start|stop|status|monitor|validate-all|meta-data}"; + CMDSHOW="$IP2UTIL route show $TABLE to exact $OCF_RESKEY_destination" + CMDCHANGE="$IP2UTIL route change to " + ++if [ "$OCF_RESKEY_table" = "local" ]; then ++ TABLE="$TABLE local" ++fi ++ + SYSTYPE="`uname -s`" + + usage() { +@@ -247,8 +251,14 @@ srca_stop() { + + [ $rc = 2 ] && errorexit "The address you specified to stop does not match the preferred source address" + +- $IP2UTIL route replace $TABLE $NETWORK dev $INTERFACE || \ +- errorexit "command 'ip route replace $TABLE $NETWORK dev $INTERFACE' failed" ++ OPTS="" ++ if [ "$OCF_RESKEY_destination" != "0.0.0.0/0" ] ;then ++ PRIMARY_IP="$($IP2UTIL -4 -o addr show dev eth0 primary | awk '{split($4,a,"/");print a[1]}')" ++ OPTS="proto kernel scope host src $PRIMARY_IP" ++ fi ++ ++ $IP2UTIL route replace $TABLE $NETWORK dev $INTERFACE $OPTS || \ ++ errorexit "command 'ip route replace $TABLE $NETWORK dev $INTERFACE $OPTS' failed" + + if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] ;then + $CMDCHANGE $ROUTE_WO_SRC || \ diff --git a/SOURCES/bz1582685-IPsrcaddr-3-fix-probe-issues.patch b/SOURCES/bz1582685-IPsrcaddr-3-fix-probe-issues.patch new file mode 100644 index 0000000..b9f8e7e --- /dev/null +++ b/SOURCES/bz1582685-IPsrcaddr-3-fix-probe-issues.patch @@ -0,0 +1,45 @@ +From 7afc581f6cd8fc37c3e14ece12fb16d31f1886f9 Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Fri, 10 Jan 2020 14:35:56 +0100 +Subject: [PATCH] IPsrcaddr: fixes to avoid failing during probe + +--- + heartbeat/IPsrcaddr | 11 ++++++++++- + 1 file changed, 10 insertions(+), 1 deletion(-) + +diff --git a/heartbeat/IPsrcaddr b/heartbeat/IPsrcaddr +index f9085f082..0ef8b391f 100755 +--- a/heartbeat/IPsrcaddr ++++ b/heartbeat/IPsrcaddr +@@ -75,6 +75,10 @@ USAGE="usage: $0 {start|stop|status|monitor|validate-all|meta-data}"; + CMDSHOW="$IP2UTIL route show $TABLE to exact $OCF_RESKEY_destination" + CMDCHANGE="$IP2UTIL route change to " + ++if [ "$OCF_RESKEY_destination" != "0.0.0.0/0" ]; then ++ CMDSHOW="$CMDSHOW src $OCF_RESKEY_ipaddress" ++fi ++ + if [ "$OCF_RESKEY_table" = "local" ]; then + TABLE="$TABLE local" + fi +@@ -183,7 +187,7 @@ export OCF_RESKEY_ip=$OCF_RESKEY_ipaddress + + srca_read() { + # Capture matching route - doublequotes prevent word splitting... +- ROUTE="`$CMDSHOW`" || errorexit "command '$CMDSHOW' failed" ++ ROUTE="`$CMDSHOW 2> /dev/null`" || errorexit "command '$CMDSHOW' failed" + + # ... so we can make sure there is only 1 matching route + [ 1 -eq `echo "$ROUTE" | wc -l` ] || \ +@@ -199,6 +203,11 @@ srca_read() { + # and what remains after stripping out the source ip address clause + ROUTE_WO_SRC=`echo $ROUTE | sed "s/$MATCHROUTE/\1\5/"` + ++ # using "src " only returns output if there's a match ++ if [ "$OCF_RESKEY_destination" != "0.0.0.0/0" ]; then ++ [ -z "$ROUTE" ] && return 1 || return 0 ++ fi ++ + [ -z "$SRCIP" ] && return 1 + [ $SRCIP = $1 ] && return 0 + return 2 diff --git a/SOURCES/bz1582685-IPsrcaddr-4-fix-hardcoded-device.patch b/SOURCES/bz1582685-IPsrcaddr-4-fix-hardcoded-device.patch new file mode 100644 index 0000000..e0e1d04 --- /dev/null +++ b/SOURCES/bz1582685-IPsrcaddr-4-fix-hardcoded-device.patch @@ -0,0 +1,23 @@ +From 5f0d15ad70098510a3782d6fd18d6eacfb51b0cf Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Thu, 16 Jan 2020 14:59:26 +0100 +Subject: [PATCH] IPsrcaddr: remove hardcoded device when using destination + parameter + +--- + heartbeat/IPsrcaddr | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/heartbeat/IPsrcaddr b/heartbeat/IPsrcaddr +index 0ef8b391f..7cdc3a9fe 100755 +--- a/heartbeat/IPsrcaddr ++++ b/heartbeat/IPsrcaddr +@@ -262,7 +262,7 @@ srca_stop() { + + OPTS="" + if [ "$OCF_RESKEY_destination" != "0.0.0.0/0" ] ;then +- PRIMARY_IP="$($IP2UTIL -4 -o addr show dev eth0 primary | awk '{split($4,a,"/");print a[1]}')" ++ PRIMARY_IP="$($IP2UTIL -4 -o addr show dev $INTERFACE primary | awk '{split($4,a,"/");print a[1]}')" + OPTS="proto kernel scope host src $PRIMARY_IP" + fi + diff --git a/SOURCES/bz1582685-IPsrcaddr-add-destination-and-table-parameters.patch b/SOURCES/bz1582685-IPsrcaddr-add-destination-and-table-parameters.patch deleted file mode 100644 index cb4fde4..0000000 --- a/SOURCES/bz1582685-IPsrcaddr-add-destination-and-table-parameters.patch +++ /dev/null @@ -1,202 +0,0 @@ ---- ClusterLabs-resource-agents-e711383f/heartbeat/IPsrcaddr 2019-08-15 16:02:10.055827624 +0200 -+++ /home/oalbrigt/src/resource-agents/heartbeat/IPsrcaddr 2019-08-15 15:45:50.690757838 +0200 -@@ -1,6 +1,6 @@ - #!/bin/sh - # --# Description: IPsrcaddr - Preferred source address modification -+# Description: IPsrcaddr - Preferred source(/dest) address modification - # - # Author: John Sutton - # Support: users@clusterlabs.org -@@ -11,7 +11,7 @@ - # - # This script manages the preferred source address associated with - # packets which originate on the localhost and are routed through the --# default route. By default, i.e. without the use of this script or -+# matching route. By default, i.e. without the use of this script or - # similar, these packets will carry the IP of the primary i.e. the - # non-aliased interface. This can be a nuisance if you need to ensure - # that such packets carry the same IP irrespective of which host in -@@ -27,7 +27,7 @@ - # - # NOTES: - # --# 1) There must be one and not more than 1 default route! Mainly because -+# 1) There must be one and not more than 1 matching route! Mainly because - # I can't see why you should have more than one. And if there is more - # than one, we would have to box clever to find out which one is to be - # modified, or we would have to pass its identity as an argument. -@@ -54,16 +54,25 @@ - . ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs - - # Defaults -+OCF_RESKEY_ipaddress_default="" -+OCF_RESKEY_cidr_netmask_default="" -+OCF_RESKEY_destination_default="0.0.0.0/0" - OCF_RESKEY_proto_default="" -+OCF_RESKEY_table_default="" - -+: ${OCF_RESKEY_ipaddress=${OCF_RESKEY_ipaddress_default}} -+: ${OCF_RESKEY_cidr_netmask=${OCF_RESKEY_cidr_netmask_default}} -+: ${OCF_RESKEY_destination=${OCF_RESKEY_destination_default}} - : ${OCF_RESKEY_proto=${OCF_RESKEY_proto_default}} -+: ${OCF_RESKEY_table=${OCF_RESKEY_table_default}} - ####################################################################### - - [ -z "$OCF_RESKEY_proto" ] && PROTO="" || PROTO="proto $OCF_RESKEY_proto" -+[ -z "$OCF_RESKEY_table" ] && TABLE="" || TABLE="table $OCF_RESKEY_table" - - USAGE="usage: $0 {start|stop|status|monitor|validate-all|meta-data}"; - -- CMDSHOW="$IP2UTIL route show to exact 0.0.0.0/0" -+ CMDSHOW="$IP2UTIL route show $TABLE to exact $OCF_RESKEY_destination" - CMDCHANGE="$IP2UTIL route change to " - - SYSTYPE="`uname -s`" -@@ -91,7 +100,7 @@ - The IP address. - - IP address -- -+ - - - -@@ -100,7 +109,15 @@ - dotted quad notation 255.255.255.0). - - Netmask -- -+ -+ -+ -+ -+ -+The destination IP/subnet for the route (default: $OCF_RESKEY_destination_default) -+ -+Destination IP/subnet -+ - - - -@@ -108,7 +125,17 @@ - Proto to match when finding network. E.g. "kernel". - - Proto -- -+ -+ -+ -+ -+ -+Table to modify. E.g. "local". -+ -+The table has to have a route matching the "destination" parameter. -+ -+Table -+ - - - -@@ -151,21 +178,22 @@ - export OCF_RESKEY_ip=$OCF_RESKEY_ipaddress - - srca_read() { -- # Capture the default route - doublequotes prevent word splitting... -- DEFROUTE="`$CMDSHOW`" || errorexit "command '$CMDSHOW' failed" -- -- # ... so we can make sure there is only 1 default route -- [ 1 -eq `echo "$DEFROUTE" | wc -l` ] || \ -- errorexit "more than 1 default route exists" -+ # Capture matching route - doublequotes prevent word splitting... -+ ROUTE="`$CMDSHOW`" || errorexit "command '$CMDSHOW' failed" - -- # But there might still be no default route -- [ -z "$DEFROUTE" ] && errorexit "no default route exists" -+ # ... so we can make sure there is only 1 matching route -+ [ 1 -eq `echo "$ROUTE" | wc -l` ] || \ -+ errorexit "more than 1 matching route exists" -+ -+ # But there might still be no matching route -+ [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] && [ -z "$ROUTE" ] && \ -+ ! ocf_is_probe && errorexit "no matching route exists" - - # Sed out the source ip address if it exists -- SRCIP=`echo $DEFROUTE | sed -n "s/$MATCHROUTE/\3/p"` -+ SRCIP=`echo $ROUTE | sed -n "s/$MATCHROUTE/\3/p"` - - # and what remains after stripping out the source ip address clause -- ROUTE_WO_SRC=`echo $DEFROUTE | sed "s/$MATCHROUTE/\1\5/"` -+ ROUTE_WO_SRC=`echo $ROUTE | sed "s/$MATCHROUTE/\1\5/"` - - [ -z "$SRCIP" ] && return 1 - [ $SRCIP = $1 ] && return 0 -@@ -185,11 +213,13 @@ - rc=$OCF_SUCCESS - ocf_log info "The ip route has been already set.($NETWORK, $INTERFACE, $ROUTE_WO_SRC)" - else -- $IP2UTIL route replace $NETWORK dev $INTERFACE src $1 || \ -- errorexit "command 'ip route replace $NETWORK dev $INTERFACE src $1' failed" -+ $IP2UTIL route replace $TABLE $NETWORK dev $INTERFACE src $1 || \ -+ errorexit "command 'ip route replace $TABLE $NETWORK dev $INTERFACE src $1' failed" - -- $CMDCHANGE $ROUTE_WO_SRC src $1 || \ -- errorexit "command '$CMDCHANGE $ROUTE_WO_SRC src $1' failed" -+ if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] ;then -+ $CMDCHANGE $ROUTE_WO_SRC src $1 || \ -+ errorexit "command '$CMDCHANGE $ROUTE_WO_SRC src $1' failed" -+ fi - rc=$? - fi - -@@ -201,7 +231,7 @@ - # If one exists but it's not the same as the one specified, that's - # an error. Maybe that's the wrong behaviour because if this fails - # then when IPaddr releases the associated interface (if there is one) --# your default route will also get dropped ;-( -+# your matching route will also get dropped ;-( - # The exit code should conform to LSB exit codes. - # - -@@ -217,11 +247,13 @@ - - [ $rc = 2 ] && errorexit "The address you specified to stop does not match the preferred source address" - -- $IP2UTIL route replace $NETWORK dev $INTERFACE || \ -- errorexit "command 'ip route replace $NETWORK dev $INTERFACE' failed" -+ $IP2UTIL route replace $TABLE $NETWORK dev $INTERFACE || \ -+ errorexit "command 'ip route replace $TABLE $NETWORK dev $INTERFACE' failed" - -- $CMDCHANGE $ROUTE_WO_SRC || \ -- errorexit "command '$CMDCHANGE $ROUTE_WO_SRC' failed" -+ if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] ;then -+ $CMDCHANGE $ROUTE_WO_SRC || \ -+ errorexit "command '$CMDCHANGE $ROUTE_WO_SRC' failed" -+ fi - - return $? - } -@@ -406,6 +438,10 @@ - return $OCF_ERR_CONFIGURED - fi - -+ if ! echo "$OCF_RESKEY_destination" | grep -q "/"; then -+ return $OCF_ERR_CONFIGURED -+ fi -+ - - if ! [ "x$SYSTYPE" = "xLinux" ]; then - # checks after this point are only relevant for linux. -@@ -486,7 +522,11 @@ - } - - INTERFACE=`echo $findif_out | awk '{print $1}'` --NETWORK=`$IP2UTIL route list dev $INTERFACE scope link $PROTO match $ipaddress|grep -m 1 -o '^[^ ]*'` -+if [ "$OCF_RESKEY_destination" = "0.0.0.0/0" ] ;then -+ NETWORK=`$IP2UTIL route list dev $INTERFACE scope link $PROTO match $ipaddress|grep -m 1 -o '^[^ ]*'` -+else -+ NETWORK="$OCF_RESKEY_destination" -+fi - - case $1 in - start) srca_start $ipaddress diff --git a/SOURCES/bz1624203-Filesystem-1-monitor-symlink-support.patch b/SOURCES/bz1624203-Filesystem-1-monitor-symlink-support.patch new file mode 100644 index 0000000..a57e5b0 --- /dev/null +++ b/SOURCES/bz1624203-Filesystem-1-monitor-symlink-support.patch @@ -0,0 +1,39 @@ +From 2aa8015bc4ff0bd61eca13eceb59aaa672335b76 Mon Sep 17 00:00:00 2001 +From: Reid Wahl +Date: Thu, 30 Aug 2018 18:36:11 -0700 +Subject: [PATCH] Filesystem: Support symlink as mountpoint directory + +Filesystem monitor operation fails when the `directory` attribute is a +symlink. + +The monitor operation calls the `list_mounts` function, which cats +`/proc/mounts` if it exists, else cats `/etc/mtab` if it exists, else +runs the `mount` command. It then greps for `" $MOUNTPOINT "` in the +output, where `$MOUNTPOINT` is the value of the `directory` attribute. + +`/proc/mounts`, `/etc/mtab`, and the `mount` command resolve symlinks +to their canonical targets. So while the monitor operation greps for +the symlink path (surrounded by spaces) as defined in the directory +attribute, the symlink will not be present in the `list_mounts` output. +Only the symlink's target will be present. + +This patch uses `readlink -f $MOUNTPOINT` to resolve the symlink to its +canonical name before using it as a grep pattern in the +`Filesystem_status` function. +--- + heartbeat/Filesystem | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/heartbeat/Filesystem b/heartbeat/Filesystem +index 7c73b0b97..fc4b8fcd5 100755 +--- a/heartbeat/Filesystem ++++ b/heartbeat/Filesystem +@@ -580,7 +580,7 @@ Filesystem_stop() + # + Filesystem_status() + { +- if list_mounts | grep -q " $MOUNTPOINT " >/dev/null 2>&1; then ++ if list_mounts | grep -q " $(readlink -f $MOUNTPOINT) " >/dev/null 2>&1; then + rc=$OCF_SUCCESS + msg="$MOUNTPOINT is mounted (running)" + else diff --git a/SOURCES/bz1624203-Filesystem-2-add-symlink-support.patch b/SOURCES/bz1624203-Filesystem-2-add-symlink-support.patch new file mode 100644 index 0000000..d5cf49f --- /dev/null +++ b/SOURCES/bz1624203-Filesystem-2-add-symlink-support.patch @@ -0,0 +1,43 @@ +From e2c3ec91cdd123b8afc6010f45ecd22ee6d8ecf7 Mon Sep 17 00:00:00 2001 +From: Reid Wahl +Date: Mon, 3 Sep 2018 00:30:01 -0700 +Subject: [PATCH] Filesystem: Canonicalize mountpoint symlinks + +Commit 2aa8015 added support to `Filesystem_status()` for mountpoints +that are symlinks. However, it missed two other places where `readlink` +calls should have been added to canonicalize symlinks. +--- + heartbeat/Filesystem | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/heartbeat/Filesystem b/heartbeat/Filesystem +index fc4b8fcd5..2a43d1daa 100755 +--- a/heartbeat/Filesystem ++++ b/heartbeat/Filesystem +@@ -278,7 +278,7 @@ determine_blockdevice() { + nfs4|nfs|smbfs|cifs|glusterfs|ceph|tmpfs|overlay|overlayfs|rozofs|zfs|cvfs|none) + : ;; + *) +- DEVICE=`list_mounts | grep " $MOUNTPOINT " | cut -d' ' -f1` ++ DEVICE=`list_mounts | grep " $(readlink -f "$MOUNTPOINT" ) " | cut -d' ' -f1` + if [ -b "$DEVICE" ]; then + blockdevice=yes + fi +@@ -396,7 +396,7 @@ fstype_supported() + Filesystem_start() + { + # Check if there are any mounts mounted under the mountpoint +- if list_mounts | grep -q -E " $MOUNTPOINT/\w+" >/dev/null 2>&1; then ++ if list_mounts | grep -q -E " $(readlink -f "$MOUNTPOINT" )/\w+" >/dev/null 2>&1; then + ocf_log err "There is one or more mounts mounted under $MOUNTPOINT." + return $OCF_ERR_CONFIGURED + fi +@@ -580,7 +580,7 @@ Filesystem_stop() + # + Filesystem_status() + { +- if list_mounts | grep -q " $(readlink -f $MOUNTPOINT) " >/dev/null 2>&1; then ++ if list_mounts | grep -q " $(readlink -f "$MOUNTPOINT" ) " >/dev/null 2>&1; then + rc=$OCF_SUCCESS + msg="$MOUNTPOINT is mounted (running)" + else diff --git a/SOURCES/bz1624203-Filesystem-3-fix-umount-disk-failure.patch b/SOURCES/bz1624203-Filesystem-3-fix-umount-disk-failure.patch new file mode 100644 index 0000000..1a53120 --- /dev/null +++ b/SOURCES/bz1624203-Filesystem-3-fix-umount-disk-failure.patch @@ -0,0 +1,53 @@ +From 69d607dc7568168e874f99d5a8b6bdb66b579d8b Mon Sep 17 00:00:00 2001 +From: "yusk.iida" +Date: Tue, 7 May 2019 19:37:26 +0900 +Subject: [PATCH] Low: Filesystem: Fix a problem umount is not executed in the + event of a disk failure + +--- + heartbeat/Filesystem | 11 ++++++++--- + 1 file changed, 8 insertions(+), 3 deletions(-) + +diff --git a/heartbeat/Filesystem b/heartbeat/Filesystem +index 2a43d1daa..bd974f8f3 100755 +--- a/heartbeat/Filesystem ++++ b/heartbeat/Filesystem +@@ -278,7 +278,7 @@ determine_blockdevice() { + nfs4|nfs|smbfs|cifs|glusterfs|ceph|tmpfs|overlay|overlayfs|rozofs|zfs|cvfs|none) + : ;; + *) +- DEVICE=`list_mounts | grep " $(readlink -f "$MOUNTPOINT" ) " | cut -d' ' -f1` ++ DEVICE=`list_mounts | grep " $CANONICALIZED_MOUNTPOINT " | cut -d' ' -f1` + if [ -b "$DEVICE" ]; then + blockdevice=yes + fi +@@ -396,7 +396,7 @@ fstype_supported() + Filesystem_start() + { + # Check if there are any mounts mounted under the mountpoint +- if list_mounts | grep -q -E " $(readlink -f "$MOUNTPOINT" )/\w+" >/dev/null 2>&1; then ++ if list_mounts | grep -q -E " $CANONICALIZED_MOUNTPOINT/\w+" >/dev/null 2>&1; then + ocf_log err "There is one or more mounts mounted under $MOUNTPOINT." + return $OCF_ERR_CONFIGURED + fi +@@ -580,7 +580,7 @@ Filesystem_stop() + # + Filesystem_status() + { +- if list_mounts | grep -q " $(readlink -f "$MOUNTPOINT" ) " >/dev/null 2>&1; then ++ if list_mounts | grep -q " $CANONICALIZED_MOUNTPOINT " >/dev/null 2>&1; then + rc=$OCF_SUCCESS + msg="$MOUNTPOINT is mounted (running)" + else +@@ -804,6 +804,11 @@ if [ -z "$OCF_RESKEY_directory" ]; then + else + MOUNTPOINT=$(echo $OCF_RESKEY_directory | sed 's/\/*$//') + : ${MOUNTPOINT:=/} ++ CANONICALIZED_MOUNTPOINT=$(readlink -f "$MOUNTPOINT") ++ if [ $? -ne 0 ]; then ++ ocf_exit_reason "Could not canonicalize $MOUNTPOINT because readlink failed" ++ exit $OCF_ERR_GENERIC ++ fi + # At this stage, $MOUNTPOINT does not contain trailing "/" unless it is "/" + # TODO: / mounted via Filesystem sounds dangerous. On stop, we'll + # kill the whole system. Is that a good idea? diff --git a/SOURCES/bz1624203-Filesystem-4-fix-readlink-issue.patch b/SOURCES/bz1624203-Filesystem-4-fix-readlink-issue.patch new file mode 100644 index 0000000..68b9313 --- /dev/null +++ b/SOURCES/bz1624203-Filesystem-4-fix-readlink-issue.patch @@ -0,0 +1,32 @@ +From 48a7ebcea5ce0522021cf3079b62107a06b530b9 Mon Sep 17 00:00:00 2001 +From: James Oakley +Date: Thu, 8 Aug 2019 05:56:14 -0700 +Subject: [PATCH] Don't call readlink on path if it does not exist + +--- + heartbeat/Filesystem | 12 ++++++++---- + 1 file changed, 8 insertions(+), 4 deletions(-) + +diff --git a/heartbeat/Filesystem b/heartbeat/Filesystem +index 4bbbc06d3..738e3c08e 100755 +--- a/heartbeat/Filesystem ++++ b/heartbeat/Filesystem +@@ -858,10 +858,14 @@ if [ -z "$OCF_RESKEY_directory" ]; then + else + MOUNTPOINT=$(echo $OCF_RESKEY_directory | sed 's/\/*$//') + : ${MOUNTPOINT:=/} +- CANONICALIZED_MOUNTPOINT=$(readlink -f "$MOUNTPOINT") +- if [ $? -ne 0 ]; then +- ocf_exit_reason "Could not canonicalize $MOUNTPOINT because readlink failed" +- exit $OCF_ERR_GENERIC ++ if [ -e "$MOUNTPOINT" ] ; then ++ CANONICALIZED_MOUNTPOINT=$(readlink -f "$MOUNTPOINT") ++ if [ $? -ne 0 ]; then ++ ocf_exit_reason "Could not canonicalize $MOUNTPOINT because readlink failed" ++ exit $OCF_ERR_GENERIC ++ fi ++ else ++ CANONICALIZED_MOUNTPOINT="$MOUNTPOINT" + fi + # At this stage, $MOUNTPOINT does not contain trailing "/" unless it is "/" + # TODO: / mounted via Filesystem sounds dangerous. On stop, we'll diff --git a/SOURCES/bz1633249-gcp-pd-move-1.patch b/SOURCES/bz1633249-gcp-pd-move-1.patch new file mode 100644 index 0000000..c7cbe8e --- /dev/null +++ b/SOURCES/bz1633249-gcp-pd-move-1.patch @@ -0,0 +1,425 @@ +From dedf420b8aa7e7e64fa56eeda2d7aeb5b2a5fcd9 Mon Sep 17 00:00:00 2001 +From: Gustavo Serra Scalet +Date: Mon, 17 Sep 2018 12:29:51 -0300 +Subject: [PATCH] Add gcp-pd-move python script + +--- + configure.ac | 1 + + doc/man/Makefile.am | 1 + + heartbeat/Makefile.am | 1 + + heartbeat/gcp-pd-move.in | 370 +++++++++++++++++++++++++++++++++++++++ + 4 files changed, 373 insertions(+) + create mode 100755 heartbeat/gcp-pd-move.in + +diff --git a/configure.ac b/configure.ac +index 10f5314da..b7ffb99f3 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -958,6 +958,7 @@ AC_CONFIG_FILES([heartbeat/conntrackd], [chmod +x heartbeat/conntrackd]) + AC_CONFIG_FILES([heartbeat/dnsupdate], [chmod +x heartbeat/dnsupdate]) + AC_CONFIG_FILES([heartbeat/eDir88], [chmod +x heartbeat/eDir88]) + AC_CONFIG_FILES([heartbeat/fio], [chmod +x heartbeat/fio]) ++AC_CONFIG_FILES([heartbeat/gcp-pd-move], [chmod +x heartbeat/gcp-pd-move]) + AC_CONFIG_FILES([heartbeat/gcp-vpc-move-ip], [chmod +x heartbeat/gcp-vpc-move-ip]) + AC_CONFIG_FILES([heartbeat/gcp-vpc-move-vip], [chmod +x heartbeat/gcp-vpc-move-vip]) + AC_CONFIG_FILES([heartbeat/gcp-vpc-move-route], [chmod +x heartbeat/gcp-vpc-move-route]) +diff --git a/doc/man/Makefile.am b/doc/man/Makefile.am +index 0bef88740..0235c9af6 100644 +--- a/doc/man/Makefile.am ++++ b/doc/man/Makefile.am +@@ -115,6 +115,7 @@ man_MANS = ocf_heartbeat_AoEtarget.7 \ + ocf_heartbeat_fio.7 \ + ocf_heartbeat_galera.7 \ + ocf_heartbeat_garbd.7 \ ++ ocf_heartbeat_gcp-pd-move.7 \ + ocf_heartbeat_gcp-vpc-move-ip.7 \ + ocf_heartbeat_gcp-vpc-move-vip.7 \ + ocf_heartbeat_gcp-vpc-move-route.7 \ +diff --git a/heartbeat/Makefile.am b/heartbeat/Makefile.am +index 993bff042..843186c98 100644 +--- a/heartbeat/Makefile.am ++++ b/heartbeat/Makefile.am +@@ -111,6 +111,7 @@ ocf_SCRIPTS = AoEtarget \ + fio \ + galera \ + garbd \ ++ gcp-pd-move \ + gcp-vpc-move-ip \ + gcp-vpc-move-vip \ + gcp-vpc-move-route \ +diff --git a/heartbeat/gcp-pd-move.in b/heartbeat/gcp-pd-move.in +new file mode 100755 +index 000000000..f9f6c3163 +--- /dev/null ++++ b/heartbeat/gcp-pd-move.in +@@ -0,0 +1,370 @@ ++#!@PYTHON@ -tt ++# - *- coding: utf- 8 - *- ++# ++# --------------------------------------------------------------------- ++# Copyright 2018 Google Inc. ++# ++# Licensed under the Apache License, Version 2.0 (the "License"); ++# you may not use this file except in compliance with the License. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# --------------------------------------------------------------------- ++# Description: Google Cloud Platform - Disk attach ++# --------------------------------------------------------------------- ++ ++import json ++import logging ++import os ++import re ++import sys ++import time ++ ++OCF_FUNCTIONS_DIR = "%s/lib/heartbeat" % os.environ.get("OCF_ROOT") ++sys.path.append(OCF_FUNCTIONS_DIR) ++ ++import ocf ++ ++try: ++ import googleapiclient.discovery ++except ImportError: ++ pass ++ ++if sys.version_info >= (3, 0): ++ # Python 3 imports. ++ import urllib.parse as urlparse ++ import urllib.request as urlrequest ++else: ++ # Python 2 imports. ++ import urllib as urlparse ++ import urllib2 as urlrequest ++ ++ ++CONN = None ++PROJECT = None ++ZONE = None ++REGION = None ++LIST_DISK_ATTACHED_INSTANCES = None ++INSTANCE_NAME = None ++ ++PARAMETERS = { ++ 'disk_name': None, ++ 'disk_scope': None, ++ 'disk_csek_file': None, ++ 'mode': None, ++ 'device_name': None, ++} ++ ++MANDATORY_PARAMETERS = ['disk_name', 'disk_scope'] ++ ++METADATA_SERVER = 'http://metadata.google.internal/computeMetadata/v1/' ++METADATA_HEADERS = {'Metadata-Flavor': 'Google'} ++METADATA = ''' ++ ++ ++1.0 ++ ++Resource Agent that can attach or detach a regional/zonal disk on current GCP ++instance. ++Requirements : ++- Disk has to be properly created as regional/zonal in order to be used ++correctly. ++ ++Attach/Detach a persistent disk on current GCP instance ++ ++ ++The name of the GCP disk. ++Disk name ++ ++ ++ ++Disk scope ++Network name ++ ++ ++ ++Path to a Customer-Supplied Encryption Key (CSEK) key file ++Customer-Supplied Encryption Key file ++ ++ ++ ++Attachment mode (rw, ro) ++Attachment mode ++ ++ ++ ++An optional name that indicates the disk name the guest operating system will see. ++Optional device name ++ ++ ++ ++ ++ ++ ++ ++ ++ ++''' ++ ++ ++def get_metadata(metadata_key, params=None, timeout=None): ++ """Performs a GET request with the metadata headers. ++ ++ Args: ++ metadata_key: string, the metadata to perform a GET request on. ++ params: dictionary, the query parameters in the GET request. ++ timeout: int, timeout in seconds for metadata requests. ++ ++ Returns: ++ HTTP response from the GET request. ++ ++ Raises: ++ urlerror.HTTPError: raises when the GET request fails. ++ """ ++ timeout = timeout or 60 ++ metadata_url = os.path.join(METADATA_SERVER, metadata_key) ++ params = urlparse.urlencode(params or {}) ++ url = '%s?%s' % (metadata_url, params) ++ request = urlrequest.Request(url, headers=METADATA_HEADERS) ++ request_opener = urlrequest.build_opener(urlrequest.ProxyHandler({})) ++ return request_opener.open(request, timeout=timeout * 1.1).read() ++ ++ ++def populate_vars(): ++ global CONN ++ global INSTANCE_NAME ++ global PROJECT ++ global ZONE ++ global REGION ++ global LIST_DISK_ATTACHED_INSTANCES ++ ++ global PARAMETERS ++ ++ # Populate global vars ++ try: ++ CONN = googleapiclient.discovery.build('compute', 'v1') ++ except Exception as e: ++ logger.error('Couldn\'t connect with google api: ' + str(e)) ++ sys.exit(ocf.OCF_ERR_CONFIGURED) ++ ++ for param in PARAMETERS: ++ value = os.environ.get('OCF_RESKEY_%s' % param, None) ++ if not value and param in MANDATORY_PARAMETERS: ++ logger.error('Missing %s mandatory parameter' % param) ++ sys.exit(ocf.OCF_ERR_CONFIGURED) ++ PARAMETERS[param] = value ++ ++ try: ++ INSTANCE_NAME = get_metadata('instance/name') ++ except Exception as e: ++ logger.error( ++ 'Couldn\'t get instance name, is this running inside GCE?: ' + str(e)) ++ sys.exit(ocf.OCF_ERR_CONFIGURED) ++ ++ PROJECT = get_metadata('project/project-id') ++ ZONE = get_metadata('instance/zone').split('/')[-1] ++ REGION = ZONE[:-2] ++ LIST_DISK_ATTACHED_INSTANCES = get_disk_attached_instances( ++ PARAMETERS['disk_name']) ++ ++ ++def configure_logs(): ++ # Prepare logging ++ global logger ++ logging.getLogger('googleapiclient').setLevel(logging.WARN) ++ logging_env = os.environ.get('OCF_RESKEY_stackdriver_logging') ++ if logging_env: ++ logging_env = logging_env.lower() ++ if any(x in logging_env for x in ['yes', 'true', 'enabled']): ++ try: ++ import google.cloud.logging.handlers ++ client = google.cloud.logging.Client() ++ handler = google.cloud.logging.handlers.CloudLoggingHandler( ++ client, name=INSTANCE_NAME) ++ handler.setLevel(logging.INFO) ++ formatter = logging.Formatter('gcp:alias "%(message)s"') ++ handler.setFormatter(formatter) ++ ocf.log.addHandler(handler) ++ logger = logging.LoggerAdapter( ++ ocf.log, {'OCF_RESOURCE_INSTANCE': ocf.OCF_RESOURCE_INSTANCE}) ++ except ImportError: ++ logger.error('Couldn\'t import google.cloud.logging, ' ++ 'disabling Stackdriver-logging support') ++ ++ ++def wait_for_operation(operation): ++ while True: ++ result = CONN.zoneOperations().get( ++ project=PROJECT, ++ zone=ZONE, ++ operation=operation['name']).execute() ++ ++ if result['status'] == 'DONE': ++ if 'error' in result: ++ raise Exception(result['error']) ++ return ++ time.sleep(1) ++ ++ ++def get_disk_attached_instances(disk): ++ def get_users_list(): ++ fl = 'name="%s"' % disk ++ request = CONN.disks().aggregatedList(project=PROJECT, filter=fl) ++ while request is not None: ++ response = request.execute() ++ locations = response.get('items', {}) ++ for location in locations.values(): ++ for d in location.get('disks', []): ++ if d['name'] == disk: ++ return d.get('users', []) ++ request = CONN.instances().aggregatedList_next( ++ previous_request=request, previous_response=response) ++ raise Exception("Unable to find disk %s" % disk) ++ ++ def get_only_instance_name(user): ++ return re.sub('.*/instances/', '', user) ++ ++ return map(get_only_instance_name, get_users_list()) ++ ++ ++def is_disk_attached(instance): ++ return instance in LIST_DISK_ATTACHED_INSTANCES ++ ++ ++def detach_disk(instance, disk_name): ++ # Python API misses disk-scope argument. ++ ++ # Detaching a disk is only possible by using deviceName, which is retrieved ++ # as a disk parameter when listing the instance information ++ request = CONN.instances().get( ++ project=PROJECT, zone=ZONE, instance=instance) ++ response = request.execute() ++ ++ device_name = None ++ for disk in response['disks']: ++ if disk_name in disk['source']: ++ device_name = disk['deviceName'] ++ break ++ ++ if not device_name: ++ logger.error("Didn't find %(d)s deviceName attached to %(i)s" % { ++ 'd': disk_name, ++ 'i': instance, ++ }) ++ return ++ ++ request = CONN.instances().detachDisk( ++ project=PROJECT, zone=ZONE, instance=instance, deviceName=device_name) ++ wait_for_operation(request.execute()) ++ ++ ++def attach_disk(instance, disk_name): ++ location = 'zones/%s' % ZONE ++ if PARAMETERS['disk_scope'] == 'regional': ++ location = 'regions/%s' % REGION ++ prefix = 'https://www.googleapis.com/compute/v1' ++ body = { ++ 'source': '%(prefix)s/projects/%(project)s/%(location)s/disks/%(disk)s' % { ++ 'prefix': prefix, ++ 'project': PROJECT, ++ 'location': location, ++ 'disk': disk_name, ++ }, ++ } ++ ++ # Customer-Supplied Encryption Key (CSEK) ++ if PARAMETERS['disk_csek_file']: ++ with open(PARAMETERS['disk_csek_file']) as csek_file: ++ body['diskEncryptionKey'] = { ++ 'rawKey': csek_file.read(), ++ } ++ ++ if PARAMETERS['device_name']: ++ body['deviceName'] = PARAMETERS['device_name'] ++ ++ if PARAMETERS['mode']: ++ body['mode'] = PARAMETERS['mode'] ++ ++ force_attach = None ++ if PARAMETERS['disk_scope'] == 'regional': ++ # Python API misses disk-scope argument. ++ force_attach = True ++ else: ++ # If this disk is attached to some instance, detach it first. ++ for other_instance in LIST_DISK_ATTACHED_INSTANCES: ++ logger.info("Detaching disk %(disk_name)s from other instance %(i)s" % { ++ 'disk_name': PARAMETERS['disk_name'], ++ 'i': other_instance, ++ }) ++ detach_disk(other_instance, PARAMETERS['disk_name']) ++ ++ request = CONN.instances().attachDisk( ++ project=PROJECT, zone=ZONE, instance=instance, body=body, ++ forceAttach=force_attach) ++ wait_for_operation(request.execute()) ++ ++ ++def fetch_data(): ++ configure_logs() ++ populate_vars() ++ ++ ++def gcp_pd_move_start(): ++ fetch_data() ++ if not is_disk_attached(INSTANCE_NAME): ++ logger.info("Attaching disk %(disk_name)s to %(instance)s" % { ++ 'disk_name': PARAMETERS['disk_name'], ++ 'instance': INSTANCE_NAME, ++ }) ++ attach_disk(INSTANCE_NAME, PARAMETERS['disk_name']) ++ ++ ++def gcp_pd_move_stop(): ++ fetch_data() ++ if is_disk_attached(INSTANCE_NAME): ++ logger.info("Detaching disk %(disk_name)s to %(instance)s" % { ++ 'disk_name': PARAMETERS['disk_name'], ++ 'instance': INSTANCE_NAME, ++ }) ++ detach_disk(INSTANCE_NAME, PARAMETERS['disk_name']) ++ ++ ++def gcp_pd_move_status(): ++ fetch_data() ++ if is_disk_attached(INSTANCE_NAME): ++ logger.info("Disk %(disk_name)s is correctly attached to %(instance)s" % { ++ 'disk_name': PARAMETERS['disk_name'], ++ 'instance': INSTANCE_NAME, ++ }) ++ else: ++ sys.exit(ocf.OCF_NOT_RUNNING) ++ ++ ++def main(): ++ if len(sys.argv) < 2: ++ logger.error('Missing argument') ++ return ++ ++ command = sys.argv[1] ++ if 'meta-data' in command: ++ print(METADATA) ++ return ++ ++ if command in 'start': ++ gcp_pd_move_start() ++ elif command in 'stop': ++ gcp_pd_move_stop() ++ elif command in ('monitor', 'status'): ++ gcp_pd_move_status() ++ else: ++ configure_logs() ++ logger.error('no such function %s' % str(command)) ++ ++ ++if __name__ == "__main__": ++ main() diff --git a/SOURCES/bz1633249-gcp-pd-move-2-use-OCF_FUNCTIONS_DIR.patch b/SOURCES/bz1633249-gcp-pd-move-2-use-OCF_FUNCTIONS_DIR.patch new file mode 100644 index 0000000..9a9681c --- /dev/null +++ b/SOURCES/bz1633249-gcp-pd-move-2-use-OCF_FUNCTIONS_DIR.patch @@ -0,0 +1,18 @@ +commit cbe0e6507992b50afbaebc46dfaf8955cc02e5ec +Author: Oyvind Albrigtsen + + Python agents: use OCF_FUNCTIONS_DIR env variable when available + +diff --git a/heartbeat/gcp-pd-move.in b/heartbeat/gcp-pd-move.in +index f9f6c316..c5007a43 100755 +--- a/heartbeat/gcp-pd-move.in ++++ b/heartbeat/gcp-pd-move.in +@@ -25,7 +25,7 @@ import re + import sys + import time + +-OCF_FUNCTIONS_DIR = "%s/lib/heartbeat" % os.environ.get("OCF_ROOT") ++OCF_FUNCTIONS_DIR = os.environ.get("OCF_FUNCTIONS_DIR", "%s/lib/heartbeat" % os.environ.get("OCF_ROOT")) + sys.path.append(OCF_FUNCTIONS_DIR) + + import ocf diff --git a/SOURCES/bz1633249-gcp-pd-move-3-add-stackdriver_logging-to-metadata.patch b/SOURCES/bz1633249-gcp-pd-move-3-add-stackdriver_logging-to-metadata.patch new file mode 100644 index 0000000..5819b94 --- /dev/null +++ b/SOURCES/bz1633249-gcp-pd-move-3-add-stackdriver_logging-to-metadata.patch @@ -0,0 +1,48 @@ +From 4fa41a1d7b4bee31526649c40cc4c58bc6333917 Mon Sep 17 00:00:00 2001 +From: masaki-tamura +Date: Wed, 2 Oct 2019 17:12:42 +0900 +Subject: [PATCH 1/2] add parameter stackdriver_logging + +--- + heartbeat/gcp-pd-move.in | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/heartbeat/gcp-pd-move.in b/heartbeat/gcp-pd-move.in +index c5007a43c..fac5c9744 100755 +--- a/heartbeat/gcp-pd-move.in ++++ b/heartbeat/gcp-pd-move.in +@@ -102,6 +102,11 @@ correctly. + Optional device name + + ++ ++Use stackdriver_logging output to global resource (yes, true, enabled) ++Use stackdriver_logging ++ ++ + + + + +From f762ce3da00e1775587a04751a8828ba004fb534 Mon Sep 17 00:00:00 2001 +From: masaki-tamura +Date: Wed, 2 Oct 2019 17:44:30 +0900 +Subject: [PATCH 2/2] defautl no + +--- + heartbeat/gcp-pd-move.in | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/heartbeat/gcp-pd-move.in b/heartbeat/gcp-pd-move.in +index fac5c9744..7fabc80dc 100755 +--- a/heartbeat/gcp-pd-move.in ++++ b/heartbeat/gcp-pd-move.in +@@ -105,7 +105,7 @@ correctly. + + Use stackdriver_logging output to global resource (yes, true, enabled) + Use stackdriver_logging +- ++ + + + diff --git a/SOURCES/bz1640575-1-ocf.py-update.patch b/SOURCES/bz1640575-1-ocf.py-update.patch new file mode 100644 index 0000000..e94deb7 --- /dev/null +++ b/SOURCES/bz1640575-1-ocf.py-update.patch @@ -0,0 +1,357 @@ +--- a/heartbeat/ocf.py 2020-04-08 13:03:20.543477544 +0200 ++++ b/heartbeat/ocf.py 2020-04-06 10:23:45.950913519 +0200 +@@ -88,6 +88,10 @@ + + OCF_RESOURCE_INSTANCE = env.get("OCF_RESOURCE_INSTANCE") + ++OCF_ACTION = env.get("__OCF_ACTION") ++if OCF_ACTION is None and len(argv) == 2: ++ OCF_ACTION = argv[1] ++ + HA_DEBUG = env.get("HA_debug", 0) + HA_DATEFMT = env.get("HA_DATEFMT", "%b %d %T ") + HA_LOGFACILITY = env.get("HA_LOGFACILITY") +@@ -135,3 +139,343 @@ + log.addHandler(dfh) + + logger = logging.LoggerAdapter(log, {'OCF_RESOURCE_INSTANCE': OCF_RESOURCE_INSTANCE}) ++ ++ ++_exit_reason_set = False ++ ++def ocf_exit_reason(msg): ++ """ ++ Print exit error string to stderr. ++ ++ Allows the OCF agent to provide a string describing ++ why the exit code was returned. ++ """ ++ global _exit_reason_set ++ cookie = env.get("OCF_EXIT_REASON_PREFIX", "ocf-exit-reason:") ++ sys.stderr.write("{}{}\n".format(cookie, msg)) ++ sys.stderr.flush() ++ logger.error(msg) ++ _exit_reason_set = True ++ ++ ++def have_binary(name): ++ """ ++ True if binary exists, False otherwise. ++ """ ++ def _access_check(fn): ++ return (os.path.exists(fn) and ++ os.access(fn, os.F_OK | os.X_OK) and ++ not os.path.isdir(fn)) ++ if _access_check(name): ++ return True ++ path = env.get("PATH", os.defpath).split(os.pathsep) ++ seen = set() ++ for dir in path: ++ dir = os.path.normcase(dir) ++ if dir not in seen: ++ seen.add(dir) ++ name2 = os.path.join(dir, name) ++ if _access_check(name2): ++ return True ++ return False ++ ++ ++def is_true(val): ++ """ ++ Convert an OCF truth value to a ++ Python boolean. ++ """ ++ return val in ("yes", "true", "1", 1, "YES", "TRUE", "ja", "on", "ON", True) ++ ++ ++def is_probe(): ++ """ ++ A probe is defined as a monitor operation ++ with an interval of zero. This is called ++ by Pacemaker to check the status of a possibly ++ not running resource. ++ """ ++ return (OCF_ACTION == "monitor" and ++ env.get("OCF_RESKEY_CRM_meta_interval", "") == "0") ++ ++ ++def get_parameter(name, default=None): ++ """ ++ Extract the parameter value from the environment ++ """ ++ return env.get("OCF_RESKEY_{}".format(name), default) ++ ++ ++def distro(): ++ """ ++ Return name of distribution/platform. ++ ++ If possible, returns "name/version", else ++ just "name". ++ """ ++ import subprocess ++ import platform ++ try: ++ ret = subprocess.check_output(["lsb_release", "-si"]) ++ if type(ret) != str: ++ ret = ret.decode() ++ distro = ret.strip() ++ ret = subprocess.check_output(["lsb_release", "-sr"]) ++ if type(ret) != str: ++ ret = ret.decode() ++ version = ret.strip() ++ return "{}/{}".format(distro, version) ++ except Exception: ++ if os.path.exists("/etc/debian_version"): ++ return "Debian" ++ if os.path.exists("/etc/SuSE-release"): ++ return "SUSE" ++ if os.path.exists("/etc/redhat-release"): ++ return "Redhat" ++ return platform.system() ++ ++ ++class Parameter(object): ++ def __init__(self, name, shortdesc, longdesc, content_type, unique, required, default): ++ self.name = name ++ self.shortdesc = shortdesc ++ self.longdesc = longdesc ++ self.content_type = content_type ++ self.unique = unique ++ self.required = required ++ self.default = default ++ ++ def __str__(self): ++ return self.to_xml() ++ ++ def to_xml(self): ++ ret = '' + "\n" ++ ret += '' + self.shortdesc + '' + "\n" ++ ret += ' ++ ++ ++1.0 ++ ++{longdesc} ++ ++{shortdesc} ++ ++ ++{parameters} ++ ++ ++ ++{actions} ++ ++ ++ ++""".format(name=self.name, ++ longdesc=self.longdesc, ++ shortdesc=self.shortdesc, ++ parameters="".join(p.to_xml() for p in self.parameters), ++ actions="".join(a.to_xml() for a in self.actions)) ++ ++ def run(self): ++ run(self) ++ ++ ++def run(agent, handlers=None): ++ """ ++ Main loop implementation for resource agents. ++ Does not return. ++ ++ Arguments: ++ ++ agent: Agent object. ++ ++ handlers: Dict of action name to handler function. ++ ++ Handler functions can take parameters as arguments, ++ the run loop will read parameter values from the ++ environment and pass to the handler. ++ """ ++ import inspect ++ ++ agent._handlers.update(handlers or {}) ++ handlers = agent._handlers ++ ++ def check_required_params(): ++ for p in agent.parameters: ++ if p.required and get_parameter(p.name) is None: ++ ocf_exit_reason("{}: Required parameter not set".format(p.name)) ++ sys.exit(OCF_ERR_CONFIGURED) ++ ++ def call_handler(func): ++ if hasattr(inspect, 'signature'): ++ params = inspect.signature(func).parameters.keys() ++ else: ++ params = inspect.getargspec(func).args ++ def value_for_parameter(param): ++ val = get_parameter(param) ++ if val is not None: ++ return val ++ for p in agent.parameters: ++ if p.name == param: ++ return p.default ++ arglist = [value_for_parameter(p) for p in params] ++ try: ++ rc = func(*arglist) ++ if rc is None: ++ rc = OCF_SUCCESS ++ return rc ++ except Exception as err: ++ if not _exit_reason_set: ++ ocf_exit_reason(str(err)) ++ else: ++ logger.error(str(err)) ++ return OCF_ERR_GENERIC ++ ++ meta_data_action = False ++ for action in agent.actions: ++ if action.name == "meta-data": ++ meta_data_action = True ++ break ++ if not meta_data_action: ++ agent.add_action("meta-data", timeout=10) ++ ++ if len(sys.argv) == 2 and sys.argv[1] in ("-h", "--help"): ++ sys.stdout.write("usage: %s {%s}\n\n" % (sys.argv[0], "|".join(sorted(handlers.keys()))) + ++ "Expects to have a fully populated OCF RA compliant environment set.\n") ++ sys.exit(OCF_SUCCESS) ++ ++ if OCF_ACTION is None: ++ ocf_exit_reason("No action argument set") ++ sys.exit(OCF_ERR_UNIMPLEMENTED) ++ if OCF_ACTION in ('meta-data', 'usage', 'methods'): ++ sys.stdout.write(agent.to_xml() + "\n") ++ sys.exit(OCF_SUCCESS) ++ ++ check_required_params() ++ if OCF_ACTION in handlers: ++ rc = call_handler(handlers[OCF_ACTION]) ++ sys.exit(rc) ++ sys.exit(OCF_ERR_UNIMPLEMENTED) ++ ++ ++if __name__ == "__main__": ++ import unittest ++ ++ class TestMetadata(unittest.TestCase): ++ def test_noparams_noactions(self): ++ m = Agent("foo", shortdesc="shortdesc", longdesc="longdesc") ++ self.assertEqual(""" ++ ++ ++1.0 ++ ++longdesc ++ ++shortdesc ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++""", str(m)) ++ ++ def test_params_actions(self): ++ m = Agent("foo", shortdesc="shortdesc", longdesc="longdesc") ++ m.add_parameter("testparam") ++ m.add_action("start") ++ self.assertEqual(str(m.actions[0]), '\n') ++ ++ unittest.main() diff --git a/SOURCES/bz1640575-2-azure-events.patch b/SOURCES/bz1640575-2-azure-events.patch new file mode 100644 index 0000000..a23c9f6 --- /dev/null +++ b/SOURCES/bz1640575-2-azure-events.patch @@ -0,0 +1,1087 @@ +diff -uNr a/configure.ac b/configure.ac +--- a/configure.ac 2020-04-08 13:20:19.895631628 +0200 ++++ b/configure.ac 2020-04-14 11:23:31.169755539 +0200 +@@ -30,6 +30,8 @@ + PKG_FEATURES="" + + AC_CONFIG_AUX_DIR(.) ++AC_CONFIG_MACRO_DIR([m4]) ++ + AC_CANONICAL_HOST + + dnl Where #defines go (e.g. `AC_CHECK_HEADERS' below) +@@ -72,6 +74,11 @@ + [AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])]) + with_systemdsystemunitdir=no], + [with_systemdsystemunitdir="$def_systemdsystemunitdir"])]) ++if test "x$with_systemdsystemunitdir" != "xno" && \ ++ test "x${prefix}" != "xNONE" && \ ++ test "x${prefix}" != "x/usr"; then ++ with_systemdsystemunitdir="${prefix}/$with_systemdsystemunitdir" ++fi + AS_IF([test "x$with_systemdsystemunitdir" != "xno"], + [AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir])]) + AM_CONDITIONAL([HAVE_SYSTEMD], [test "x$with_systemdsystemunitdir" != "xno"]) +@@ -79,6 +86,11 @@ + AC_ARG_WITH([systemdtmpfilesdir], + AS_HELP_STRING([--with-systemdtmpfilesdir=DIR], [Directory for systemd tmp files]), + [], [with_systemdtmpfilesdir=$($PKGCONFIG --variable=tmpfilesdir systemd)]) ++ if test "x$with_systemdtmpfilesdir" != xno && \ ++ test "x${prefix}" != "xNONE" && \ ++ test "x${prefix}" != "x/usr"; then ++ with_systemdtmpfilesdir="${prefix}/$with_systemdtmpfilesdir" ++ fi + if test "x$with_systemdtmpfilesdir" != xno; then + AC_SUBST([systemdtmpfilesdir], [$with_systemdtmpfilesdir]) + fi +@@ -431,7 +443,7 @@ + + # Expand $prefix + eval HA_RSCTMPDIR="`eval echo ${HA_RSCTMPDIR}`" +-AC_DEFINE_UNQUOTED(HA_RSCTMPDIR,"$HA_RSCTMPDIR", Where Resouce agents keep state files) ++AC_DEFINE_UNQUOTED(HA_RSCTMPDIR,"$HA_RSCTMPDIR", Where Resource agents keep state files) + AC_SUBST(HA_RSCTMPDIR) + + dnl Eventually move out of the heartbeat dir tree and create symlinks when needed +@@ -501,12 +513,35 @@ + AC_SUBST(RM) + AC_SUBST(TEST) + ++dnl Ensure PYTHON is an absolute path ++AC_PATH_PROG([PYTHON], [$PYTHON]) ++ + AM_PATH_PYTHON + if test -z "$PYTHON"; then + echo "*** Essential program python not found" 1>&2 +- exit 1 + fi + ++AC_PYTHON_MODULE(googleapiclient) ++AC_PYTHON_MODULE(pyroute2) ++ ++AS_VERSION_COMPARE([$PYTHON_VERSION], [2.7], [BUILD_OCF_PY=0], [BUILD_OCF_PY=1], [BUILD_OCF_PY=1]) ++ ++BUILD_AZURE_EVENTS=1 ++if test -z "$PYTHON" || test $BUILD_OCF_PY -eq 0; then ++ BUILD_AZURE_EVENTS=0 ++ AC_MSG_WARN("Not building azure-events") ++fi ++AM_CONDITIONAL(BUILD_AZURE_EVENTS, test $BUILD_AZURE_EVENTS -eq 1) ++ ++BUILD_GCP_PD_MOVE=1 ++AM_CONDITIONAL(BUILD_GCP_PD_MOVE, test $BUILD_GCP_PD_MOVE -eq 1) ++ ++BUILD_GCP_VPC_MOVE_ROUTE=1 ++AM_CONDITIONAL(BUILD_GCP_VPC_MOVE_ROUTE, test $BUILD_GCP_VPC_MOVE_ROUTE -eq 1) ++ ++BUILD_GCP_VPC_MOVE_VIP=1 ++AM_CONDITIONAL(BUILD_GCP_VPC_MOVE_VIP, test $BUILD_GCP_VPC_MOVE_VIP -eq 1) ++ + AC_PATH_PROGS(ROUTE, route) + AC_DEFINE_UNQUOTED(ROUTE, "$ROUTE", path to route command) + +@@ -541,6 +576,12 @@ + if test x"${STYLESHEET_PREFIX}" = x""; then + DIRS=$(find "${datadir}" -name $(basename $(dirname ${DOCBOOK_XSL_PATH})) \ + -type d | LC_ALL=C sort) ++ if test x"${DIRS}" = x""; then ++ # when datadir is not standard OS path, we cannot find docbook.xsl ++ # use standard OS path as backup ++ DIRS=$(find "/usr/share" -name $(basename $(dirname ${DOCBOOK_XSL_PATH})) \ ++ -type d | LC_ALL=C sort) ++ fi + XSLT=$(basename ${DOCBOOK_XSL_PATH}) + for d in ${DIRS}; do + if test -f "${d}/${XSLT}"; then +@@ -911,6 +952,7 @@ + heartbeat/ocf-shellfuncs \ + heartbeat/shellfuncs \ + systemd/Makefile \ ++ systemd/resource-agents.conf \ + tools/Makefile \ + tools/ocf-tester \ + tools/ocft/Makefile \ +@@ -947,6 +989,7 @@ + ) + + dnl Files we output that need to be executable ++AC_CONFIG_FILES([heartbeat/azure-events], [chmod +x heartbeat/azure-events]) + AC_CONFIG_FILES([heartbeat/AoEtarget], [chmod +x heartbeat/AoEtarget]) + AC_CONFIG_FILES([heartbeat/ManageRAID], [chmod +x heartbeat/ManageRAID]) + AC_CONFIG_FILES([heartbeat/ManageVE], [chmod +x heartbeat/ManageVE]) +@@ -1020,7 +1063,7 @@ + AC_MSG_RESULT([]) + AC_MSG_RESULT([$PACKAGE configuration:]) + AC_MSG_RESULT([ Version = ${VERSION}]) +-AC_MSG_RESULT([ Build Version = e711383fd5c7bef9c24ff6bc85465e59f91080f9]) ++AC_MSG_RESULT([ Build Version = $Format:%H$]) + AC_MSG_RESULT([ Features =${PKG_FEATURES}]) + AC_MSG_RESULT([]) + AC_MSG_RESULT([ Prefix = ${prefix}]) +diff -uNr a/doc/man/Makefile.am b/doc/man/Makefile.am +--- a/doc/man/Makefile.am 2020-04-08 13:20:19.914631297 +0200 ++++ b/doc/man/Makefile.am 2020-04-14 11:11:26.334806204 +0200 +@@ -55,7 +55,7 @@ + # 12126 on savannah.gnu.org. But, maybe it gets fixed soon, it was + # first reported in 1995 and added to Savannah in in 2005... + if BUILD_DOC +-man_MANS = ocf_heartbeat_AoEtarget.7 \ ++man_MANS = ocf_heartbeat_AoEtarget.7 \ + ocf_heartbeat_AudibleAlarm.7 \ + ocf_heartbeat_ClusterMon.7 \ + ocf_heartbeat_CTDB.7 \ +@@ -186,6 +186,22 @@ + man_MANS += ocf_heartbeat_IPv6addr.7 + endif + ++if BUILD_AZURE_EVENTS ++man_MANS += ocf_heartbeat_azure-events.7 ++endif ++ ++if BUILD_GCP_PD_MOVE ++man_MANS += ocf_heartbeat_gcp-pd-move.7 ++endif ++ ++if BUILD_GCP_VPC_MOVE_ROUTE ++man_MANS += ocf_heartbeat_gcp-vpc-move-route.7 ++endif ++ ++if BUILD_GCP_VPC_MOVE_VIP ++man_MANS += ocf_heartbeat_gcp-vpc-move-vip.7 ++endif ++ + xmlfiles = $(man_MANS:.7=.xml) + + %.1 %.5 %.7 %.8: %.xml +diff -uNr a/heartbeat/azure-events.in b/heartbeat/azure-events.in +--- a/heartbeat/azure-events.in 1970-01-01 01:00:00.000000000 +0100 ++++ b/heartbeat/azure-events.in 2020-04-14 11:11:26.325806378 +0200 +@@ -0,0 +1,824 @@ ++#!@PYTHON@ -tt ++# ++# Resource agent for monitoring Azure Scheduled Events ++# ++# License: GNU General Public License (GPL) ++# (c) 2018 Tobias Niekamp, Microsoft Corp. ++# and Linux-HA contributors ++ ++import os ++import sys ++import time ++import subprocess ++import json ++try: ++ import urllib2 ++except ImportError: ++ import urllib.request as urllib2 ++import socket ++from collections import defaultdict ++ ++OCF_FUNCTIONS_DIR = os.environ.get("OCF_FUNCTIONS_DIR", "%s/lib/heartbeat" % os.environ.get("OCF_ROOT")) ++sys.path.append(OCF_FUNCTIONS_DIR) ++import ocf ++ ++############################################################################## ++ ++ ++VERSION = "0.10" ++USER_AGENT = "Pacemaker-ResourceAgent/%s %s" % (VERSION, ocf.distro()) ++ ++attr_globalPullState = "azure-events_globalPullState" ++attr_lastDocVersion = "azure-events_lastDocVersion" ++attr_curNodeState = "azure-events_curNodeState" ++attr_pendingEventIDs = "azure-events_pendingEventIDs" ++ ++default_loglevel = ocf.logging.INFO ++default_relevantEventTypes = set(["Reboot", "Redeploy"]) ++ ++global_pullMaxAttempts = 3 ++global_pullDelaySecs = 1 ++ ++############################################################################## ++ ++class attrDict(defaultdict): ++ """ ++ A wrapper for accessing dict keys like an attribute ++ """ ++ def __init__(self, data): ++ super(attrDict, self).__init__(attrDict) ++ for d in data.keys(): ++ self.__setattr__(d, data[d]) ++ ++ def __getattr__(self, key): ++ try: ++ return self[key] ++ except KeyError: ++ raise AttributeError(key) ++ ++ def __setattr__(self, key, value): ++ self[key] = value ++ ++############################################################################## ++ ++class azHelper: ++ """ ++ Helper class for Azure's metadata API (including Scheduled Events) ++ """ ++ metadata_host = "http://169.254.169.254/metadata" ++ instance_api = "instance" ++ events_api = "scheduledevents" ++ api_version = "2017-08-01" ++ ++ @staticmethod ++ def _sendMetadataRequest(endpoint, postData=None): ++ """ ++ Send a request to Azure's Azure Metadata Service API ++ """ ++ url = "%s/%s?api-version=%s" % (azHelper.metadata_host, endpoint, azHelper.api_version) ++ ocf.logger.debug("_sendMetadataRequest: begin; endpoint = %s, postData = %s" % (endpoint, postData)) ++ ocf.logger.debug("_sendMetadataRequest: url = %s" % url) ++ ++ req = urllib2.Request(url, postData) ++ req.add_header("Metadata", "true") ++ req.add_header("User-Agent", USER_AGENT) ++ resp = urllib2.urlopen(req) ++ data = resp.read() ++ ocf.logger.debug("_sendMetadataRequest: response = %s" % data) ++ if data: ++ data = json.loads(data) ++ ++ ocf.logger.debug("_sendMetadataRequest: finished") ++ return data ++ ++ @staticmethod ++ def getInstanceInfo(): ++ """ ++ Fetch details about the current VM from Azure's Azure Metadata Service API ++ """ ++ ocf.logger.debug("getInstanceInfo: begin") ++ ++ jsondata = azHelper._sendMetadataRequest(azHelper.instance_api) ++ ocf.logger.debug("getInstanceInfo: json = %s" % jsondata) ++ ++ ocf.logger.debug("getInstanceInfo: finished, returning {}".format(jsondata["compute"])) ++ return attrDict(jsondata["compute"]) ++ ++ @staticmethod ++ def pullScheduledEvents(): ++ """ ++ Retrieve all currently scheduled events via Azure Metadata Service API ++ """ ++ ocf.logger.debug("pullScheduledEvents: begin") ++ ++ jsondata = azHelper._sendMetadataRequest(azHelper.events_api) ++ ocf.logger.debug("pullScheduledEvents: json = %s" % jsondata) ++ ++ ocf.logger.debug("pullScheduledEvents: finished") ++ return attrDict(jsondata) ++ ++ @staticmethod ++ def forceEvents(eventIDs): ++ """ ++ Force a set of events to start immediately ++ """ ++ ocf.logger.debug("forceEvents: begin") ++ ++ events = [] ++ for e in eventIDs: ++ events.append({ ++ "EventId": e, ++ }) ++ postData = { ++ "StartRequests" : events ++ } ++ ocf.logger.info("forceEvents: postData = %s" % postData) ++ resp = azHelper._sendMetadataRequest(azHelper.events_api, postData=json.dumps(postData)) ++ ++ ocf.logger.debug("forceEvents: finished") ++ return ++ ++############################################################################## ++ ++class clusterHelper: ++ """ ++ Helper functions for Pacemaker control via crm ++ """ ++ @staticmethod ++ def _getLocation(node): ++ """ ++ Helper function to retrieve local/global attributes ++ """ ++ if node: ++ return ["--node", node] ++ else: ++ return ["--type", "crm_config"] ++ ++ @staticmethod ++ def _exec(command, *args): ++ """ ++ Helper function to execute a UNIX command ++ """ ++ args = list(args) ++ ocf.logger.debug("_exec: begin; command = %s, args = %s" % (command, str(args))) ++ ++ def flatten(*n): ++ return (str(e) for a in n ++ for e in (flatten(*a) if isinstance(a, (tuple, list)) else (str(a),))) ++ command = list(flatten([command] + args)) ++ ocf.logger.debug("_exec: cmd = %s" % " ".join(command)) ++ try: ++ ret = subprocess.check_output(command) ++ ocf.logger.debug("_exec: return = %s" % ret) ++ return ret.rstrip() ++ except Exception as err: ++ ocf.logger.exception(err) ++ return None ++ ++ @staticmethod ++ def setAttr(key, value, node=None): ++ """ ++ Set the value of a specific global/local attribute in the Pacemaker cluster ++ """ ++ ocf.logger.debug("setAttr: begin; key = %s, value = %s, node = %s" % (key, value, node)) ++ ++ if value: ++ ret = clusterHelper._exec("crm_attribute", ++ "--name", key, ++ "--update", value, ++ clusterHelper._getLocation(node)) ++ else: ++ ret = clusterHelper._exec("crm_attribute", ++ "--name", key, ++ "--delete", ++ clusterHelper._getLocation(node)) ++ ++ ocf.logger.debug("setAttr: finished") ++ return len(ret) == 0 ++ ++ @staticmethod ++ def getAttr(key, node=None): ++ """ ++ Retrieve a global/local attribute from the Pacemaker cluster ++ """ ++ ocf.logger.debug("getAttr: begin; key = %s, node = %s" % (key, node)) ++ ++ val = clusterHelper._exec("crm_attribute", ++ "--name", key, ++ "--query", "--quiet", ++ "--default", "", ++ clusterHelper._getLocation(node)) ++ ocf.logger.debug("getAttr: finished") ++ if not val: ++ return None ++ return val if not val.isdigit() else int(val) ++ ++ @staticmethod ++ def getAllNodes(): ++ """ ++ Get a list of hostnames for all nodes in the Pacemaker cluster ++ """ ++ ocf.logger.debug("getAllNodes: begin") ++ ++ nodes = [] ++ nodeList = clusterHelper._exec("crm_node", "--list") ++ for n in nodeList.decode().split("\n"): ++ nodes.append(n.split()[1]) ++ ocf.logger.debug("getAllNodes: finished; return %s" % str(nodes)) ++ ++ return nodes ++ ++ @staticmethod ++ def getHostNameFromAzName(azName): ++ """ ++ Helper function to get the actual host name from an Azure node name ++ """ ++ return clusterHelper.getAttr("hostName_%s" % azName) ++ ++ @staticmethod ++ def removeHoldFromNodes(): ++ """ ++ Remove the ON_HOLD state from all nodes in the Pacemaker cluster ++ """ ++ ocf.logger.debug("removeHoldFromNodes: begin") ++ ++ for n in clusterHelper.getAllNodes(): ++ if clusterHelper.getAttr(attr_curNodeState, node=n) == "ON_HOLD": ++ clusterHelper.setAttr(attr_curNodeState, "AVAILABLE", node=n) ++ ocf.logger.info("removeHoldFromNodes: removed ON_HOLD from node %s" % n) ++ ++ ocf.logger.debug("removeHoldFromNodes: finished") ++ return False ++ ++ @staticmethod ++ def otherNodesAvailable(exceptNode): ++ """ ++ Check if there are any nodes (except a given node) in the Pacemaker cluster that have state AVAILABLE ++ """ ++ ocf.logger.debug("otherNodesAvailable: begin; exceptNode = %s" % exceptNode) ++ ++ for n in clusterHelper.getAllNodes(): ++ state = clusterHelper.getAttr(attr_curNodeState, node=n) ++ state = stringToNodeState(state) if state else AVAILABLE ++ if state == AVAILABLE and n != exceptNode.hostName: ++ ocf.logger.info("otherNodesAvailable: at least %s is available" % n) ++ ocf.logger.debug("otherNodesAvailable: finished") ++ return True ++ ocf.logger.info("otherNodesAvailable: no other nodes are available") ++ ocf.logger.debug("otherNodesAvailable: finished") ++ ++ return False ++ ++ @staticmethod ++ def transitionSummary(): ++ """ ++ Get the current Pacemaker transition summary (used to check if all resources are stopped when putting a node standby) ++ """ ++ # Is a global crm_simulate "too much"? Or would it be sufficient it there are no planned transitions for a particular node? ++ # # crm_simulate -Ls ++ # Transition Summary: ++ # * Promote rsc_SAPHana_HN1_HDB03:0 (Slave -> Master hsr3-db1) ++ # * Stop rsc_SAPHana_HN1_HDB03:1 (hsr3-db0) ++ # * Move rsc_ip_HN1_HDB03 (Started hsr3-db0 -> hsr3-db1) ++ # * Start rsc_nc_HN1_HDB03 (hsr3-db1) ++ # # Excepted result when there are no pending actions: ++ # Transition Summary: ++ ocf.logger.debug("transitionSummary: begin") ++ ++ summary = clusterHelper._exec("crm_simulate", "-Ls") ++ if not summary: ++ ocf.logger.warning("transitionSummary: could not load transition summary") ++ return False ++ if summary.find("Transition Summary:") < 0: ++ ocf.logger.warning("transitionSummary: received unexpected transition summary: %s" % summary) ++ return False ++ summary = summary.split("Transition Summary:")[1] ++ ret = summary.decode().split("\n").pop(0) ++ ++ ocf.logger.debug("transitionSummary: finished; return = %s" % str(ret)) ++ return ret ++ ++ @staticmethod ++ def listOperationsOnNode(node): ++ """ ++ Get a list of all current operations for a given node (used to check if any resources are pending) ++ """ ++ # hsr3-db1:/home/tniek # crm_resource --list-operations -N hsr3-db0 ++ # rsc_azure-events (ocf::heartbeat:azure-events): Started: rsc_azure-events_start_0 (node=hsr3-db0, call=91, rc=0, last-rc-change=Fri Jun 8 22:37:46 2018, exec=115ms): complete ++ # rsc_azure-events (ocf::heartbeat:azure-events): Started: rsc_azure-events_monitor_10000 (node=hsr3-db0, call=93, rc=0, last-rc-change=Fri Jun 8 22:37:47 2018, exec=197ms): complete ++ # rsc_SAPHana_HN1_HDB03 (ocf::suse:SAPHana): Master: rsc_SAPHana_HN1_HDB03_start_0 (node=hsr3-db0, call=-1, rc=193, last-rc-change=Fri Jun 8 22:37:46 2018, exec=0ms): pending ++ # rsc_SAPHanaTopology_HN1_HDB03 (ocf::suse:SAPHanaTopology): Started: rsc_SAPHanaTopology_HN1_HDB03_start_0 (node=hsr3-db0, call=90, rc=0, last-rc-change=Fri Jun 8 22:37:46 2018, exec=3214ms): complete ++ ocf.logger.debug("listOperationsOnNode: begin; node = %s" % node) ++ ++ resources = clusterHelper._exec("crm_resource", "--list-operations", "-N", node) ++ if len(resources) == 0: ++ ret = [] ++ else: ++ ret = resources.decode().split("\n") ++ ++ ocf.logger.debug("listOperationsOnNode: finished; return = %s" % str(ret)) ++ return ret ++ ++ @staticmethod ++ def noPendingResourcesOnNode(node): ++ """ ++ Check that there are no pending resources on a given node ++ """ ++ ocf.logger.debug("noPendingResourcesOnNode: begin; node = %s" % node) ++ ++ for r in clusterHelper.listOperationsOnNode(node): ++ ocf.logger.debug("noPendingResourcesOnNode: * %s" % r) ++ resource = r.split()[-1] ++ if resource == "pending": ++ ocf.logger.info("noPendingResourcesOnNode: found resource %s that is still pending" % resource) ++ ocf.logger.debug("noPendingResourcesOnNode: finished; return = False") ++ return False ++ ocf.logger.info("noPendingResourcesOnNode: no pending resources on node %s" % node) ++ ocf.logger.debug("noPendingResourcesOnNode: finished; return = True") ++ ++ return True ++ ++ @staticmethod ++ def allResourcesStoppedOnNode(node): ++ """ ++ Check that all resources on a given node are stopped ++ """ ++ ocf.logger.debug("allResourcesStoppedOnNode: begin; node = %s" % node) ++ ++ if clusterHelper.noPendingResourcesOnNode(node): ++ if len(clusterHelper.transitionSummary()) == 0: ++ ocf.logger.info("allResourcesStoppedOnNode: no pending resources on node %s and empty transition summary" % node) ++ ocf.logger.debug("allResourcesStoppedOnNode: finished; return = True") ++ return True ++ ocf.logger.info("allResourcesStoppedOnNode: transition summary is not empty") ++ ocf.logger.debug("allResourcesStoppedOnNode: finished; return = False") ++ return False ++ ++ ocf.logger.info("allResourcesStoppedOnNode: still pending resources on node %s" % node) ++ ocf.logger.debug("allResourcesStoppedOnNode: finished; return = False") ++ return False ++ ++############################################################################## ++ ++AVAILABLE = 0 # Node is online and ready to handle events ++STOPPING = 1 # Standby has been triggered, but some resources are still running ++IN_EVENT = 2 # All resources are stopped, and event has been initiated via Azure Metadata Service ++ON_HOLD = 3 # Node has a pending event that cannot be started there are no other nodes available ++ ++def stringToNodeState(name): ++ if type(name) == int: return name ++ if name == "STOPPING": return STOPPING ++ if name == "IN_EVENT": return IN_EVENT ++ if name == "ON_HOLD": return ON_HOLD ++ return AVAILABLE ++ ++def nodeStateToString(state): ++ if state == STOPPING: return "STOPPING" ++ if state == IN_EVENT: return "IN_EVENT" ++ if state == ON_HOLD: return "ON_HOLD" ++ return "AVAILABLE" ++ ++############################################################################## ++ ++class Node: ++ """ ++ Core class implementing logic for a cluster node ++ """ ++ def __init__(self, ra): ++ self.raOwner = ra ++ self.azInfo = azHelper.getInstanceInfo() ++ self.azName = self.azInfo.name ++ self.hostName = socket.gethostname() ++ self.setAttr("azName", self.azName) ++ clusterHelper.setAttr("hostName_%s" % self.azName, self.hostName) ++ ++ def getAttr(self, key): ++ """ ++ Get a local attribute ++ """ ++ return clusterHelper.getAttr(key, node=self.hostName) ++ ++ def setAttr(self, key, value): ++ """ ++ Set a local attribute ++ """ ++ return clusterHelper.setAttr(key, value, node=self.hostName) ++ ++ def selfOrOtherNode(self, node): ++ """ ++ Helper function to distinguish self/other node ++ """ ++ return node if node else self.hostName ++ ++ def setState(self, state, node=None): ++ """ ++ Set the state for a given node (or self) ++ """ ++ node = self.selfOrOtherNode(node) ++ ocf.logger.debug("setState: begin; node = %s, state = %s" % (node, nodeStateToString(state))) ++ ++ clusterHelper.setAttr(attr_curNodeState, nodeStateToString(state), node=node) ++ ++ ocf.logger.debug("setState: finished") ++ ++ def getState(self, node=None): ++ """ ++ Get the state for a given node (or self) ++ """ ++ node = self.selfOrOtherNode(node) ++ ocf.logger.debug("getState: begin; node = %s" % node) ++ ++ state = clusterHelper.getAttr(attr_curNodeState, node=node) ++ ocf.logger.debug("getState: state = %s" % state) ++ ocf.logger.debug("getState: finished") ++ if not state: ++ return AVAILABLE ++ return stringToNodeState(state) ++ ++ def setEventIDs(self, eventIDs, node=None): ++ """ ++ Set pending EventIDs for a given node (or self) ++ """ ++ node = self.selfOrOtherNode(node) ++ ocf.logger.debug("setEventIDs: begin; node = %s, eventIDs = %s" % (node, str(eventIDs))) ++ ++ if eventIDs: ++ eventIDStr = ",".join(eventIDs) ++ else: ++ eventIDStr = None ++ clusterHelper.setAttr(attr_pendingEventIDs, eventIDStr, node=node) ++ ++ ocf.logger.debug("setEventIDs: finished") ++ return ++ ++ def getEventIDs(self, node=None): ++ """ ++ Get pending EventIDs for a given node (or self) ++ """ ++ node = self.selfOrOtherNode(node) ++ ocf.logger.debug("getEventIDs: begin; node = %s" % node) ++ ++ eventIDStr = clusterHelper.getAttr(attr_pendingEventIDs, node=node) ++ if eventIDStr: ++ eventIDs = eventIDStr.decode().split(",") ++ else: ++ eventIDs = None ++ ++ ocf.logger.debug("getEventIDs: finished; eventIDs = %s" % str(eventIDs)) ++ return eventIDs ++ ++ def updateNodeStateAndEvents(self, state, eventIDs, node=None): ++ """ ++ Set the state and pending EventIDs for a given node (or self) ++ """ ++ ocf.logger.debug("updateNodeStateAndEvents: begin; node = %s, state = %s, eventIDs = %s" % (node, nodeStateToString(state), str(eventIDs))) ++ ++ self.setState(state, node=node) ++ self.setEventIDs(eventIDs, node=node) ++ ++ ocf.logger.debug("updateNodeStateAndEvents: finished") ++ return state ++ ++ def putNodeStandby(self, node=None): ++ """ ++ Put self to standby ++ """ ++ node = self.selfOrOtherNode(node) ++ ocf.logger.debug("putNodeStandby: begin; node = %s" % node) ++ ++ clusterHelper._exec("crm_attribute", ++ "-t", "nodes", ++ "-N", node, ++ "-n", "standby", ++ "-v", "on", ++ "--lifetime=forever") ++ ++ ocf.logger.debug("putNodeStandby: finished") ++ ++ def putNodeOnline(self, node=None): ++ """ ++ Put self back online ++ """ ++ node = self.selfOrOtherNode(node) ++ ocf.logger.debug("putNodeOnline: begin; node = %s" % node) ++ ++ clusterHelper._exec("crm_attribute", ++ "-t", "nodes", ++ "-N", node, ++ "-n", "standby", ++ "-v", "off", ++ "--lifetime=forever") ++ ++ ocf.logger.debug("putNodeOnline: finished") ++ ++ def separateEvents(self, events): ++ """ ++ Split own/other nodes' events ++ """ ++ ocf.logger.debug("separateEvents: begin; events = %s" % str(events)) ++ ++ localEvents = [] ++ remoteEvents = [] ++ for e in events: ++ e = attrDict(e) ++ if e.EventType not in self.raOwner.relevantEventTypes: ++ continue ++ if self.azName in e.Resources: ++ localEvents.append(e) ++ else: ++ remoteEvents.append(e) ++ ocf.logger.debug("separateEvents: finished; localEvents = %s, remoteEvents = %s" % (str(localEvents), str(remoteEvents))) ++ return (localEvents, remoteEvents) ++ ++ def removeOrphanedEvents(self, azEvents): ++ """ ++ Remove remote events that are already finished ++ """ ++ ocf.logger.debug("removeOrphanedEvents: begin; azEvents = %s" % str(azEvents)) ++ ++ azEventIDs = set() ++ for e in azEvents: ++ azEventIDs.add(e.EventId) ++ # for all nodes except self ... ++ for n in clusterHelper.getAllNodes(): ++ if n == self.hostName: ++ continue ++ curState = self.getState(node=n) ++ # ... that still show in an event or shutting down resources ... ++ if curState in (STOPPING, IN_EVENT): ++ ocf.logger.info("removeOrphanedEvents: node %s has state %s" % (n, curState)) ++ clusterEventIDs = self.getEventIDs(node=n) ++ stillActive = False ++ # ... but don't have any more events running according to Azure, ... ++ for p in clusterEventIDs: ++ if p in azEventIDs: ++ ocf.logger.info("removeOrphanedEvents: (at least) event %s on node %s has not yet finished" % (str(p), n)) ++ stillActive = True ++ break ++ if not stillActive: ++ # ... put them back online. ++ ocf.logger.info("removeOrphanedEvents: clusterEvents %s on node %s are not in azEvents %s -> bring node back online" % (str(clusterEventIDs), n, str(azEventIDs))) ++ self.putNodeOnline(node=n) ++ ++ ocf.logger.debug("removeOrphanedEvents: finished") ++ ++ def handleRemoteEvents(self, azEvents): ++ """ ++ Handle a list of events (as provided by Azure Metadata Service) for other nodes ++ """ ++ ocf.logger.debug("handleRemoteEvents: begin; hostName = %s, events = %s" % (self.hostName, str(azEvents))) ++ ++ if len(azEvents) == 0: ++ ocf.logger.debug("handleRemoteEvents: no remote events to handle") ++ ocf.logger.debug("handleRemoteEvents: finished") ++ return ++ eventIDsForNode = {} ++ ++ # iterate through all current events as per Azure ++ for e in azEvents: ++ ocf.logger.info("handleRemoteEvents: handling remote event %s (%s; nodes = %s)" % (e.EventId, e.EventType, str(e.Resources))) ++ # before we can force an event to start, we need to ensure all nodes involved have stopped their resources ++ if e.EventStatus == "Scheduled": ++ allNodesStopped = True ++ for azName in e.Resources: ++ hostName = clusterHelper.getHostNameFromAzName(azName) ++ state = self.getState(node=hostName) ++ if state == STOPPING: ++ # the only way we can continue is when node state is STOPPING, but all resources have been stopped ++ if not clusterHelper.allResourcesStoppedOnNode(hostName): ++ ocf.logger.info("handleRemoteEvents: (at least) node %s has still resources running -> wait" % hostName) ++ allNodesStopped = False ++ break ++ elif state in (AVAILABLE, IN_EVENT, ON_HOLD): ++ ocf.logger.info("handleRemoteEvents: node %s is still %s -> remote event needs to be picked up locally" % (hostName, nodeStateToString(state))) ++ allNodesStopped = False ++ break ++ if allNodesStopped: ++ ocf.logger.info("handleRemoteEvents: nodes %s are stopped -> add remote event %s to force list" % (str(e.Resources), e.EventId)) ++ for n in e.Resources: ++ hostName = clusterHelper.getHostNameFromAzName(n) ++ if hostName in eventIDsForNode: ++ eventIDsForNode[hostName].append(e.EventId) ++ else: ++ eventIDsForNode[hostName] = [e.EventId] ++ elif e.EventStatus == "Started": ++ ocf.logger.info("handleRemoteEvents: remote event already started") ++ ++ # force the start of all events whose nodes are ready (i.e. have no more resources running) ++ if len(eventIDsForNode.keys()) > 0: ++ eventIDsToForce = set([item for sublist in eventIDsForNode.values() for item in sublist]) ++ ocf.logger.info("handleRemoteEvents: set nodes %s to IN_EVENT; force remote events %s" % (str(eventIDsForNode.keys()), str(eventIDsToForce))) ++ for node, eventId in eventIDsForNode.items(): ++ self.updateNodeStateAndEvents(IN_EVENT, eventId, node=node) ++ azHelper.forceEvents(eventIDsToForce) ++ ++ ocf.logger.debug("handleRemoteEvents: finished") ++ ++ def handleLocalEvents(self, azEvents): ++ """ ++ Handle a list of own events (as provided by Azure Metadata Service) ++ """ ++ ocf.logger.debug("handleLocalEvents: begin; hostName = %s, azEvents = %s" % (self.hostName, str(azEvents))) ++ ++ azEventIDs = set() ++ for e in azEvents: ++ azEventIDs.add(e.EventId) ++ ++ curState = self.getState() ++ clusterEventIDs = self.getEventIDs() ++ mayUpdateDocVersion = False ++ ocf.logger.info("handleLocalEvents: current state = %s; pending local clusterEvents = %s" % (nodeStateToString(curState), str(clusterEventIDs))) ++ ++ # check if there are currently/still events set for the node ++ if clusterEventIDs: ++ # there are pending events set, so our state must be STOPPING or IN_EVENT ++ i = 0; touchedEventIDs = False ++ while i < len(clusterEventIDs): ++ # clean up pending events that are already finished according to AZ ++ if clusterEventIDs[i] not in azEventIDs: ++ ocf.logger.info("handleLocalEvents: remove finished local clusterEvent %s" % (clusterEventIDs[i])) ++ clusterEventIDs.pop(i) ++ touchedEventIDs = True ++ else: ++ i += 1 ++ if len(clusterEventIDs) > 0: ++ # there are still pending events (either because we're still stopping, or because the event is still in place) ++ # either way, we need to wait ++ if touchedEventIDs: ++ ocf.logger.info("handleLocalEvents: added new local clusterEvent %s" % str(clusterEventIDs)) ++ self.setEventIDs(clusterEventIDs) ++ else: ++ ocf.logger.info("handleLocalEvents: no local clusterEvents were updated") ++ else: ++ # there are no more pending events left after cleanup ++ if clusterHelper.noPendingResourcesOnNode(self.hostName): ++ # and no pending resources on the node -> set it back online ++ ocf.logger.info("handleLocalEvents: all local events finished -> clean up, put node online and AVAILABLE") ++ curState = self.updateNodeStateAndEvents(AVAILABLE, None) ++ self.putNodeOnline() ++ clusterHelper.removeHoldFromNodes() ++ # repeat handleLocalEvents() since we changed status to AVAILABLE ++ else: ++ ocf.logger.info("handleLocalEvents: all local events finished, but some resources have not completed startup yet -> wait") ++ else: ++ # there are no pending events set for us (yet) ++ if curState == AVAILABLE: ++ if len(azEventIDs) > 0: ++ if clusterHelper.otherNodesAvailable(self): ++ ocf.logger.info("handleLocalEvents: can handle local events %s -> set state STOPPING" % (str(azEventIDs))) ++ # this will also set mayUpdateDocVersion = True ++ curState = self.updateNodeStateAndEvents(STOPPING, azEventIDs) ++ else: ++ ocf.logger.info("handleLocalEvents: cannot handle azEvents %s (only node available) -> set state ON_HOLD" % str(azEventIDs)) ++ self.setState(ON_HOLD) ++ else: ++ ocf.logger.debug("handleLocalEvents: no local azEvents to handle") ++ if curState == STOPPING: ++ if clusterHelper.noPendingResourcesOnNode(self.hostName): ++ ocf.logger.info("handleLocalEvents: all local resources are started properly -> put node standby") ++ self.putNodeStandby() ++ mayUpdateDocVersion = True ++ else: ++ ocf.logger.info("handleLocalEvents: some local resources are not clean yet -> wait") ++ ++ ocf.logger.debug("handleLocalEvents: finished; mayUpdateDocVersion = %s" % str(mayUpdateDocVersion)) ++ return mayUpdateDocVersion ++ ++############################################################################## ++ ++class raAzEvents: ++ """ ++ Main class for resource agent ++ """ ++ def __init__(self, relevantEventTypes): ++ self.node = Node(self) ++ self.relevantEventTypes = relevantEventTypes ++ ++ def monitor(self): ++ ocf.logger.debug("monitor: begin") ++ ++ pullFailedAttemps = 0 ++ while True: ++ # check if another node is pulling at the same time; ++ # this should only be a concern for the first pull, as setting up Scheduled Events may take up to 2 minutes. ++ if clusterHelper.getAttr(attr_globalPullState) == "PULLING": ++ pullFailedAttemps += 1 ++ if pullFailedAttemps == global_pullMaxAttempts: ++ ocf.logger.warning("monitor: exceeded maximum number of attempts (%d) to pull events" % global_pullMaxAttempts) ++ ocf.logger.debug("monitor: finished") ++ return ocf.OCF_SUCCESS ++ else: ++ ocf.logger.info("monitor: another node is pulling; retry in %d seconds" % global_pullDelaySecs) ++ time.sleep(global_pullDelaySecs) ++ continue ++ ++ # we can pull safely from Azure Metadata Service ++ clusterHelper.setAttr(attr_globalPullState, "PULLING") ++ events = azHelper.pullScheduledEvents() ++ clusterHelper.setAttr(attr_globalPullState, "IDLE") ++ ++ # get current document version ++ curDocVersion = events.DocumentIncarnation ++ lastDocVersion = self.node.getAttr(attr_lastDocVersion) ++ ocf.logger.debug("monitor: lastDocVersion = %s; curDocVersion = %s" % (lastDocVersion, curDocVersion)) ++ ++ # split events local/remote ++ (localEvents, remoteEvents) = self.node.separateEvents(events.Events) ++ ++ # ensure local events are only executing once ++ if curDocVersion != lastDocVersion: ++ ocf.logger.debug("monitor: curDocVersion has not been handled yet") ++ # handleLocalEvents() returns True if mayUpdateDocVersion is True; ++ # this is only the case if we can ensure there are no pending events ++ if self.node.handleLocalEvents(localEvents): ++ ocf.logger.info("monitor: handleLocalEvents completed successfully -> update curDocVersion") ++ self.node.setAttr(attr_lastDocVersion, curDocVersion) ++ else: ++ ocf.logger.debug("monitor: handleLocalEvents still waiting -> keep curDocVersion") ++ else: ++ ocf.logger.info("monitor: already handled curDocVersion, skip") ++ ++ # remove orphaned remote events and then handle the remaining remote events ++ self.node.removeOrphanedEvents(remoteEvents) ++ self.node.handleRemoteEvents(remoteEvents) ++ break ++ ++ ocf.logger.debug("monitor: finished") ++ return ocf.OCF_SUCCESS ++ ++############################################################################## ++ ++def setLoglevel(verbose): ++ # set up writing into syslog ++ loglevel = default_loglevel ++ if verbose: ++ opener = urllib2.build_opener(urllib2.HTTPHandler(debuglevel=1)) ++ urllib2.install_opener(opener) ++ loglevel = ocf.logging.DEBUG ++ ocf.log.setLevel(loglevel) ++ ++description = ( ++ "Microsoft Azure Scheduled Events monitoring agent", ++ """This resource agent implements a monitor for scheduled ++(maintenance) events for a Microsoft Azure VM. ++ ++If any relevant events are found, it moves all Pacemaker resources ++away from the affected node to allow for a graceful shutdown. ++ ++ Usage: ++ [OCF_RESKEY_eventTypes=VAL] [OCF_RESKEY_verbose=VAL] azure-events ACTION ++ ++ action (required): Supported values: monitor, help, meta-data ++ eventTypes (optional): List of event types to be considered ++ relevant by the resource agent (comma-separated). ++ Supported values: Freeze,Reboot,Redeploy ++ Default = Reboot,Redeploy ++/ verbose (optional): If set to true, displays debug info. ++ Default = false ++ ++ Deployment: ++ crm configure primitive rsc_azure-events ocf:heartbeat:azure-events \ ++ op monitor interval=10s ++ crm configure clone cln_azure-events rsc_azure-events ++ ++For further information on Microsoft Azure Scheduled Events, please ++refer to the following documentation: ++https://docs.microsoft.com/en-us/azure/virtual-machines/linux/scheduled-events ++""") ++ ++def monitor_action(eventTypes): ++ relevantEventTypes = set(eventTypes.split(",") if eventTypes else []) ++ ra = raAzEvents(relevantEventTypes) ++ return ra.monitor() ++ ++def validate_action(eventTypes): ++ if eventTypes: ++ for event in eventTypes.split(","): ++ if event not in ("Freeze", "Reboot", "Redeploy"): ++ ocf.ocf_exit_reason("Event type not one of Freeze, Reboot, Redeploy: " + eventTypes) ++ return ocf.OCF_ERR_CONFIGURED ++ return ocf.OCF_SUCCESS ++ ++def main(): ++ agent = ocf.Agent("azure-events", shortdesc=description[0], longdesc=description[1]) ++ agent.add_parameter( ++ "eventTypes", ++ shortdesc="List of resources to be considered", ++ longdesc="A comma-separated list of event types that will be handled by this resource agent. (Possible values: Freeze,Reboot,Redeploy)", ++ content_type="string", ++ default="Reboot,Redeploy") ++ agent.add_parameter( ++ "verbose", ++ shortdesc="Enable verbose agent logging", ++ longdesc="Set to true to enable verbose logging", ++ content_type="boolean", ++ default="false") ++ agent.add_action("start", timeout=10, handler=lambda: ocf.OCF_SUCCESS) ++ agent.add_action("stop", timeout=10, handler=lambda: ocf.OCF_SUCCESS) ++ agent.add_action("validate-all", timeout=20, handler=validate_action) ++ agent.add_action("monitor", timeout=240, interval=10, handler=monitor_action) ++ setLoglevel(ocf.is_true(ocf.get_parameter("verbose", "false"))) ++ agent.run() ++ ++if __name__ == '__main__': ++ main() +diff -uNr a/heartbeat/Makefile.am b/heartbeat/Makefile.am +--- a/heartbeat/Makefile.am 2020-04-08 13:20:19.912631331 +0200 ++++ b/heartbeat/Makefile.am 2020-04-14 11:11:26.333806223 +0200 +@@ -55,7 +55,7 @@ + osp_SCRIPTS = nova-compute-wait \ + NovaEvacuate + +-ocf_SCRIPTS = AoEtarget \ ++ocf_SCRIPTS = AoEtarget \ + AudibleAlarm \ + ClusterMon \ + CTDB \ +@@ -120,10 +120,7 @@ + fio \ + galera \ + garbd \ +- gcp-pd-move \ + gcp-vpc-move-ip \ +- gcp-vpc-move-vip \ +- gcp-vpc-move-route \ + iSCSILogicalUnit \ + iSCSITarget \ + ids \ +@@ -180,6 +177,22 @@ + vsftpd \ + zabbixserver + ++if BUILD_AZURE_EVENTS ++ocf_SCRIPTS += azure-events ++endif ++ ++if BUILD_GCP_PD_MOVE ++ocf_SCRIPTS += gcp-pd-move ++endif ++ ++if BUILD_GCP_VPC_MOVE_ROUTE ++ocf_SCRIPTS += gcp-vpc-move-route ++endif ++ ++if BUILD_GCP_VPC_MOVE_VIP ++ocf_SCRIPTS += gcp-vpc-move-vip ++endif ++ + ocfcommondir = $(OCF_LIB_DIR_PREFIX)/heartbeat + ocfcommon_DATA = ocf-shellfuncs \ + ocf-binaries \ +@@ -208,3 +221,13 @@ + + %.check: % + OCF_ROOT=$(abs_srcdir) OCF_FUNCTIONS_DIR=$(abs_srcdir) ./$< meta-data | xmllint --path $(abs_srcdir) --noout --relaxng $(abs_srcdir)/metadata.rng - ++ ++do_spellcheck = printf '[%s]\n' "$(agent)"; \ ++ OCF_ROOT=$(abs_srcdir) OCF_FUNCTIONS_DIR=$(abs_srcdir) \ ++ ./$(agent) meta-data 2>/dev/null \ ++ | xsltproc $(top_srcdir)/make/extract_text.xsl - \ ++ | aspell pipe list -d en_US --ignore-case \ ++ --home-dir=$(top_srcdir)/make -p spellcheck-ignore \ ++ | sed -n 's|^&\([^:]*\):.*|\1|p'; ++spellcheck: ++ @$(foreach agent,$(ocf_SCRIPTS), $(do_spellcheck)) +diff -uNr a/m4/ac_python_module.m4 b/m4/ac_python_module.m4 +--- a/m4/ac_python_module.m4 1970-01-01 01:00:00.000000000 +0100 ++++ b/m4/ac_python_module.m4 2020-04-14 11:11:26.325806378 +0200 +@@ -0,0 +1,30 @@ ++dnl @synopsis AC_PYTHON_MODULE(modname[, fatal]) ++dnl ++dnl Checks for Python module. ++dnl ++dnl If fatal is non-empty then absence of a module will trigger an ++dnl error. ++dnl ++dnl @category InstalledPackages ++dnl @author Andrew Collier . ++dnl @version 2004-07-14 ++dnl @license AllPermissive ++ ++AC_DEFUN([AC_PYTHON_MODULE],[ ++ AC_MSG_CHECKING(python module: $1) ++ $PYTHON -c "import $1" 2>/dev/null ++ if test $? -eq 0; ++ then ++ AC_MSG_RESULT(yes) ++ eval AS_TR_CPP(HAVE_PYMOD_$1)=yes ++ else ++ AC_MSG_RESULT(no) ++ eval AS_TR_CPP(HAVE_PYMOD_$1)=no ++ # ++ if test -n "$2" ++ then ++ AC_MSG_ERROR(failed to find required module $1) ++ exit 1 ++ fi ++ fi ++]) +diff -uNr a/systemd/resource-agents.conf b/systemd/resource-agents.conf +--- a/systemd/resource-agents.conf 2018-06-29 14:05:02.000000000 +0200 ++++ b/systemd/resource-agents.conf 1970-01-01 01:00:00.000000000 +0100 +@@ -1 +0,0 @@ +-d /var/run/resource-agents/ 1755 root root +diff -uNr a/systemd/resource-agents.conf.in b/systemd/resource-agents.conf.in +--- a/systemd/resource-agents.conf.in 1970-01-01 01:00:00.000000000 +0100 ++++ b/systemd/resource-agents.conf.in 2020-04-14 11:11:26.325806378 +0200 +@@ -0,0 +1 @@ ++d @HA_RSCTMPDIR@ 1755 root root diff --git a/SOURCES/bz1642067-SAPHanaTopology-make-multi-instance-aware.patch b/SOURCES/bz1642067-SAPHanaTopology-make-multi-instance-aware.patch deleted file mode 100644 index c4fbd7c..0000000 --- a/SOURCES/bz1642067-SAPHanaTopology-make-multi-instance-aware.patch +++ /dev/null @@ -1,49 +0,0 @@ -From d43d8af650ee9ed175ebd4fdfc15ac2767734a8d Mon Sep 17 00:00:00 2001 -From: Fabian Herschel -Date: Wed, 25 Jul 2018 12:30:32 +0200 -Subject: [PATCH] ra/SAPHanaTopology: bsc#1016691 "SAPHanaSR: Status file of - SAPHanaTopology is not multi instance aware (MCOS)" - ---- - SAPHana/ra/SAPHanaTopology | 20 +++++++++++--------- - 1 file changed, 11 insertions(+), 9 deletions(-) - -diff --git a/SAPHanaSR-2067519/SAPHana/ra/SAPHanaTopology b/SAPHanaSR-2067519/SAPHana/ra/SAPHanaTopology -index 1e2f301..20d2593 100755 ---- a/SAPHanaSR-2067519/SAPHana/ra/SAPHanaTopology -+++ b/SAPHanaSR-2067519/SAPHana/ra/SAPHanaTopology -@@ -28,7 +28,7 @@ - ####################################################################### - # - # Initialization: --SAPHanaVersion="0.152.21" -+SAPHanaVersion="0.152.22" - timeB=$(date '+%s') - - : ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat} -@@ -736,15 +736,17 @@ function sht_start() { - # sht_stop: Stop the SAP HANA Topology Resource - # - function sht_stop() { -- super_ocf_log info "FLOW $FUNCNAME ($*)" -- local output="" -- local rc=0 -- -- rm $HA_RSCTMP/SAPHana/SAPTopologyON.${SID} -- rc=$OCF_SUCCESS -+ super_ocf_log info "FLOW $FUNCNAME ($*)" -+ local output="" -+ local rc=0 -+ if [ -f $HA_RSCTMP/SAPHana/SAPTopologyON ]; then -+ rm $HA_RSCTMP/SAPHana/SAPTopologyON -+ fi -+ rm $HA_RSCTMP/SAPHana/SAPTopologyON.${SID} -+ rc=$OCF_SUCCESS - -- super_ocf_log info "FLOW $FUNCNAME rc=$rc" -- return $rc -+ super_ocf_log info "FLOW $FUNCNAME rc=$rc" -+ return $rc - } - - diff --git a/SOURCES/bz1726736-Route-1-dont-fence-when-parameters-not-set.patch b/SOURCES/bz1726736-Route-1-dont-fence-when-parameters-not-set.patch new file mode 100644 index 0000000..6c39248 --- /dev/null +++ b/SOURCES/bz1726736-Route-1-dont-fence-when-parameters-not-set.patch @@ -0,0 +1,35 @@ +From 1286636b768bb635e9a6b1f1fbf6267c9c3f4b03 Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Mon, 19 Aug 2019 13:31:06 +0200 +Subject: [PATCH] Route: dont fence node when parameters arent set + +--- + heartbeat/Route | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/heartbeat/Route b/heartbeat/Route +index b4011e37d..9f92eff3a 100755 +--- a/heartbeat/Route ++++ b/heartbeat/Route +@@ -249,18 +249,18 @@ route_validate() { + if [ "${OCF_RESKEY_CRM_meta_clone}" ]; then + if [ "${OCF_RESKEY_CRM_meta_clone_node_max}" != 1 ]; then + ocf_exit_reason "Misconfigured clone parameters. Must set meta attribute \"clone_node_max\" to 1, got ${OCF_RESKEY_CRM_meta_clone_node_max}." +- return $OCF_ERR_ARGS ++ return $OCF_ERR_CONFIGURED + fi + fi + # Did we get a destination? + if [ -z "${OCF_RESKEY_destination}" ]; then + ocf_exit_reason "Missing required parameter \"destination\"." +- return $OCF_ERR_ARGS ++ return $OCF_ERR_CONFIGURED + fi + # Did we get either a device or a gateway address? + if [ -z "${OCF_RESKEY_device}" -a -z "${OCF_RESKEY_gateway}" ]; then + ocf_exit_reason "Must specify either \"device\", or \"gateway\", or both." +- return $OCF_ERR_ARGS ++ return $OCF_ERR_CONFIGURED + fi + # If a device has been configured, is it available on this system? + if [ -n "${OCF_RESKEY_device}" ]; then diff --git a/SOURCES/bz1726736-Route-2-validate-start-validate-all.patch b/SOURCES/bz1726736-Route-2-validate-start-validate-all.patch new file mode 100644 index 0000000..e2d012e --- /dev/null +++ b/SOURCES/bz1726736-Route-2-validate-start-validate-all.patch @@ -0,0 +1,40 @@ +From 444bdc44fc47c65f848efc0c39c1e8e6620ce10d Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Fri, 11 Oct 2019 12:12:52 +0200 +Subject: [PATCH] Route: only validate for start and validate-all actions + +--- + heartbeat/Route | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/heartbeat/Route b/heartbeat/Route +index 9f92eff3a..8898e1afd 100755 +--- a/heartbeat/Route ++++ b/heartbeat/Route +@@ -187,6 +187,8 @@ END + } + + route_start() { ++ route_validate || exit $? ++ + route_status + status=$? + if [ $status -eq $OCF_SUCCESS ]; then +@@ -313,8 +315,6 @@ for binary in ip grep; do + check_binary $binary + done + +-route_validate || exit $? +- + case $OCF_RESKEY_family in + ip4) addr_family="-4" ;; + ip6) addr_family="-6" ;; +@@ -334,7 +334,7 @@ status|monitor) route_status;; + reload) ocf_log info "Reloading..." + route_start + ;; +-validate-all) ;; ++validate-all) route_validate;; + *) route_usage + exit $OCF_ERR_UNIMPLEMENTED + ;; diff --git a/SOURCES/bz1726736-Route-dont-fence-when-parameters-not-set.patch b/SOURCES/bz1726736-Route-dont-fence-when-parameters-not-set.patch deleted file mode 100644 index 6c39248..0000000 --- a/SOURCES/bz1726736-Route-dont-fence-when-parameters-not-set.patch +++ /dev/null @@ -1,35 +0,0 @@ -From 1286636b768bb635e9a6b1f1fbf6267c9c3f4b03 Mon Sep 17 00:00:00 2001 -From: Oyvind Albrigtsen -Date: Mon, 19 Aug 2019 13:31:06 +0200 -Subject: [PATCH] Route: dont fence node when parameters arent set - ---- - heartbeat/Route | 6 +++--- - 1 file changed, 3 insertions(+), 3 deletions(-) - -diff --git a/heartbeat/Route b/heartbeat/Route -index b4011e37d..9f92eff3a 100755 ---- a/heartbeat/Route -+++ b/heartbeat/Route -@@ -249,18 +249,18 @@ route_validate() { - if [ "${OCF_RESKEY_CRM_meta_clone}" ]; then - if [ "${OCF_RESKEY_CRM_meta_clone_node_max}" != 1 ]; then - ocf_exit_reason "Misconfigured clone parameters. Must set meta attribute \"clone_node_max\" to 1, got ${OCF_RESKEY_CRM_meta_clone_node_max}." -- return $OCF_ERR_ARGS -+ return $OCF_ERR_CONFIGURED - fi - fi - # Did we get a destination? - if [ -z "${OCF_RESKEY_destination}" ]; then - ocf_exit_reason "Missing required parameter \"destination\"." -- return $OCF_ERR_ARGS -+ return $OCF_ERR_CONFIGURED - fi - # Did we get either a device or a gateway address? - if [ -z "${OCF_RESKEY_device}" -a -z "${OCF_RESKEY_gateway}" ]; then - ocf_exit_reason "Must specify either \"device\", or \"gateway\", or both." -- return $OCF_ERR_ARGS -+ return $OCF_ERR_CONFIGURED - fi - # If a device has been configured, is it available on this system? - if [ -n "${OCF_RESKEY_device}" ]; then diff --git a/SOURCES/bz1744145-Filesystem-1-avoid-corrupt-mount-list.patch b/SOURCES/bz1744145-Filesystem-1-avoid-corrupt-mount-list.patch new file mode 100644 index 0000000..1184817 --- /dev/null +++ b/SOURCES/bz1744145-Filesystem-1-avoid-corrupt-mount-list.patch @@ -0,0 +1,46 @@ +From b67278bc92cfb0b9947ff5fff65f46f420a42c2c Mon Sep 17 00:00:00 2001 +From: Kazutomo Nakahira +Date: Fri, 10 May 2019 14:30:51 +0900 +Subject: [PATCH] Low: Filesystem: Fix missing mount point due to corrupted + mount list + +--- + heartbeat/Filesystem | 20 +++++++++++++++----- + 1 file changed, 15 insertions(+), 5 deletions(-) + +diff --git a/heartbeat/Filesystem b/heartbeat/Filesystem +index 2a43d1daa..c38ae12d4 100755 +--- a/heartbeat/Filesystem ++++ b/heartbeat/Filesystem +@@ -255,16 +255,26 @@ is_bind_mount() { + } + list_mounts() { + local inpf="" ++ local mount_list="" ++ local check_list="x" ++ + if [ -e "/proc/mounts" ] && ! is_bind_mount; then + inpf=/proc/mounts + elif [ -f "/etc/mtab" -a -r "/etc/mtab" ]; then + inpf=/etc/mtab + fi +- if [ "$inpf" ]; then +- cut -d' ' -f1,2,3 < $inpf +- else +- $MOUNT | cut -d' ' -f1,3,5 +- fi ++ ++ # Make sure that the mount list has not been changed while reading. ++ while [ "$mount_list" != "$check_list" ]; do ++ check_list=$mount_list ++ if [ "$inpf" ]; then ++ mount_list=$(cut -d' ' -f1,2,3 < $inpf) ++ else ++ mount_list=$($MOUNT | cut -d' ' -f1,3,5) ++ fi ++ done ++ ++ echo "$mount_list" + } + + determine_blockdevice() { diff --git a/SOURCES/bz1744145-Filesystem-2-prevent-killing-bind-mount.patch b/SOURCES/bz1744145-Filesystem-2-prevent-killing-bind-mount.patch new file mode 100644 index 0000000..f642548 --- /dev/null +++ b/SOURCES/bz1744145-Filesystem-2-prevent-killing-bind-mount.patch @@ -0,0 +1,52 @@ +From bfbc99003ebd96d79bbf8ad50be0b5e714a92fd7 Mon Sep 17 00:00:00 2001 +From: ytakeshita +Date: Fri, 7 Jun 2019 15:20:52 +0900 +Subject: [PATCH] Medium: Filesystem: Prevents to all root user processes are + killed when bind mounting a directory on rootfs. + +if a directory is bound mounting on rootfs and "force_umount" is not set "safe", change "force_umount" to "safe". +--- + heartbeat/Filesystem | 23 +++++++++++++++++++++++ + 1 file changed, 23 insertions(+) + +diff --git a/heartbeat/Filesystem b/heartbeat/Filesystem +index c46ec3cca..1b29a08b3 100755 +--- a/heartbeat/Filesystem ++++ b/heartbeat/Filesystem +@@ -314,6 +314,24 @@ bind_kernel_check() { + [ $? -ne 0 ] && + ocf_log warn "kernel `uname -r` cannot handle read only bind mounts" + } ++ ++bind_rootfs_check() { ++ local SOURCE ++ local TARGET ++ local ROOTFS ++ ++ SOURCE=$1 ++ TARGET=$(df --output=target $SOURCE | tail -n 1) ++ ++ ROOTFS=$(list_mounts | grep -w rootfs | cut -d' ' -f 2) ++ ++ if [ "${TARGET}" = "${ROOTFS}" ]; then ++ return 1 ++ else ++ return 0 ++ fi ++} ++ + bind_mount() { + if is_bind_mount && [ "$options" != "-o bind" ] + then +@@ -476,6 +494,11 @@ get_pids() + local procs + local mmap_procs + ++ if is_bind_mount && ocf_is_true "$FORCE_UNMOUNT" && ! bind_rootfs_check "$DEVICE"; then ++ ocf_log debug "Change force_umount from '$FORCE_UNMOUNT' to 'safe'" ++ FORCE_UNMOUNT=safe ++ fi ++ + if ocf_is_true "$FORCE_UNMOUNT"; then + if [ "X${HOSTOS}" = "XOpenBSD" ];then + fstat | grep $dir | awk '{print $3}' diff --git a/SOURCES/bz1744145-Filesystem-3-improved-bind-mount-check.patch b/SOURCES/bz1744145-Filesystem-3-improved-bind-mount-check.patch new file mode 100644 index 0000000..9bb2f11 --- /dev/null +++ b/SOURCES/bz1744145-Filesystem-3-improved-bind-mount-check.patch @@ -0,0 +1,42 @@ +From f8e5d2afc5b9bbf676ac20894f0f26e6ec998557 Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Tue, 10 Sep 2019 15:40:12 +0200 +Subject: [PATCH] Filesystem: improve "/" check for bind mounts + +--- + heartbeat/Filesystem | 15 +++------------ + 1 file changed, 3 insertions(+), 12 deletions(-) + +diff --git a/heartbeat/Filesystem b/heartbeat/Filesystem +index 738e3c08e..e66ddc77f 100755 +--- a/heartbeat/Filesystem ++++ b/heartbeat/Filesystem +@@ -337,17 +337,8 @@ bind_kernel_check() { + ocf_log warn "kernel `uname -r` cannot handle read only bind mounts" + } + +-bind_rootfs_check() { +- local SOURCE +- local TARGET +- local ROOTFS +- +- SOURCE=$1 +- TARGET=$(df --output=target $SOURCE | tail -n 1) +- +- ROOTFS=$(list_mounts | grep -w rootfs | cut -d' ' -f 2) +- +- if [ "${TARGET}" = "${ROOTFS}" ]; then ++bind_root_mount_check() { ++ if [ "$(df -P "$1" | awk 'END{print $6}')" = "/" ]; then + return 1 + else + return 0 +@@ -516,7 +507,7 @@ get_pids() + local procs + local mmap_procs + +- if is_bind_mount && ocf_is_true "$FORCE_UNMOUNT" && ! bind_rootfs_check "$DEVICE"; then ++ if is_bind_mount && ocf_is_true "$FORCE_UNMOUNT" && ! bind_root_mount_check "$DEVICE"; then + ocf_log debug "Change force_umount from '$FORCE_UNMOUNT' to 'safe'" + FORCE_UNMOUNT=safe + fi diff --git a/SOURCES/bz1744431-pgsql-1-set-initial-score.patch b/SOURCES/bz1744431-pgsql-1-set-initial-score.patch new file mode 100644 index 0000000..d11f12d --- /dev/null +++ b/SOURCES/bz1744431-pgsql-1-set-initial-score.patch @@ -0,0 +1,34 @@ +From f8e1b1407b613657ebd90381d53e6a567b92b241 Mon Sep 17 00:00:00 2001 +From: Kazutomo Nakahira +Date: Mon, 17 Dec 2018 14:15:24 +0900 +Subject: [PATCH] Medium: pgsql: Set initial score for primary and hot standby + in the probe. + +--- + heartbeat/pgsql | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/heartbeat/pgsql b/heartbeat/pgsql +index 842dc0ac4..8ef84dd3e 100755 +--- a/heartbeat/pgsql ++++ b/heartbeat/pgsql +@@ -974,11 +974,19 @@ pgsql_real_monitor() { + case "$output" in + f) ocf_log debug "PostgreSQL is running as a primary." + if [ "$OCF_RESKEY_monitor_sql" = "$OCF_RESKEY_monitor_sql_default" ]; then ++ if ocf_is_probe; then ++ # Set initial score for primary. ++ exec_with_retry 0 $CRM_MASTER -v $PROMOTE_ME ++ fi + return $OCF_RUNNING_MASTER + fi + ;; + + t) ocf_log debug "PostgreSQL is running as a hot standby." ++ if ocf_is_probe; then ++ # Set initial score for hot standby. ++ exec_with_retry 0 $CRM_MASTER -v $CAN_NOT_PROMOTE ++ fi + return $OCF_SUCCESS;; + + *) ocf_exit_reason "$CHECK_MS_SQL output is $output" diff --git a/SOURCES/bz1744431-pgsql-2-prevent-incorrect-status-start.patch b/SOURCES/bz1744431-pgsql-2-prevent-incorrect-status-start.patch new file mode 100644 index 0000000..daca241 --- /dev/null +++ b/SOURCES/bz1744431-pgsql-2-prevent-incorrect-status-start.patch @@ -0,0 +1,34 @@ +From ac430f79c333d73e6cd59ae59178c7040e7dbfda Mon Sep 17 00:00:00 2001 +From: Kazunori INOUE +Date: Wed, 8 May 2019 18:23:59 +0900 +Subject: [PATCH] pgsql: enhance checks in pgsql_real_start to prevent + incorrect status gets + +--- + heartbeat/pgsql | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/heartbeat/pgsql b/heartbeat/pgsql +index 842dc0ac4..5d04618e6 100755 +--- a/heartbeat/pgsql ++++ b/heartbeat/pgsql +@@ -483,7 +483,7 @@ runasowner() { + "-q") + quietrun="-q" + shift 1;; +- "warn"|"err") ++ "info"|"warn"|"err") + loglevel="-$1" + shift 1;; + *) +@@ -544,7 +544,9 @@ pgsql_real_start() { + local postgres_options + local rc + +- if pgsql_status; then ++ pgsql_real_monitor info ++ rc=$? ++ if [ $rc -eq $OCF_SUCCESS -o $rc -eq $OCF_RUNNING_MASTER ]; then + ocf_log info "PostgreSQL is already running. PID=`cat $PIDFILE`" + if is_replication; then + return $OCF_ERR_GENERIC diff --git a/SOURCES/bz1759067-LVM-activate-detect-volume-without-reboot.patch b/SOURCES/bz1759067-LVM-activate-detect-volume-without-reboot.patch new file mode 100644 index 0000000..4725d8e --- /dev/null +++ b/SOURCES/bz1759067-LVM-activate-detect-volume-without-reboot.patch @@ -0,0 +1,48 @@ +From 6c24147ebe0e979c48db93a5f8ec6094b8707591 Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Thu, 26 Sep 2019 12:52:39 +0200 +Subject: [PATCH] LVM-activate: move pvscan --cache to validate + +It needs to be called before validate attempts to look at the VG. +--- + configure.ac | 2 +- + heartbeat/LVM-activate | 6 +++++- + 2 files changed, 6 insertions(+), 2 deletions(-) + +diff --git a/configure.ac b/configure.ac +index 97dac7cf8..1eb65cf34 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -21,7 +21,7 @@ dnl checks for system services + + AC_INIT([resource-agents], + m4_esyscmd([make/git-version-gen .tarball-version]), +- [to_be_defined@foobar.org]) ++ [developers@clusterlabs.org]) + + AC_USE_SYSTEM_EXTENSIONS + +diff --git a/heartbeat/LVM-activate b/heartbeat/LVM-activate +index 3df40c894..9b7c0aa7f 100755 +--- a/heartbeat/LVM-activate ++++ b/heartbeat/LVM-activate +@@ -489,6 +489,11 @@ lvm_validate() { + check_binary lvm + check_binary dmsetup + ++ # This is necessary when using system ID to update lvm hints, ++ # or in older versions of lvm, this is necessary to update the ++ # lvmetad cache. ++ pvscan --cache ++ + if ! vgs --foreign ${VG} >/dev/null 2>&1 ; then + # stop action exits successfully if the VG cannot be accessed... + if [ $__OCF_ACTION = "stop" ]; then +@@ -627,7 +632,6 @@ clvmd_activate() { + systemid_activate() { + local cur_systemid + +- pvscan --cache + cur_systemid=$(vgs --foreign --noheadings -o systemid ${VG} | tr -d '[:blank:]') + + # Put our system ID on the VG diff --git a/SOURCES/bz1759068-LVM-activate-partial-activation.patch b/SOURCES/bz1759068-LVM-activate-partial-activation.patch new file mode 100644 index 0000000..1eec112 --- /dev/null +++ b/SOURCES/bz1759068-LVM-activate-partial-activation.patch @@ -0,0 +1,69 @@ +diff -uNr a/heartbeat/LVM-activate b/heartbeat/LVM-activate +--- a/heartbeat/LVM-activate 2019-10-08 12:10:11.755991580 +0200 ++++ b/heartbeat/LVM-activate 2019-10-08 12:14:38.388288176 +0200 +@@ -42,6 +42,11 @@ + : ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat} + . ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs + ++# Parameter defaults ++OCF_RESKEY_partial_activation_default="false" ++ ++: ${OCF_RESKEY_partial_activation=${OCF_RESKEY_partial_activation_default}} ++ + # If LV is given, only activate this named LV; otherwise, activate all + # LVs in the named VG. + VG=${OCF_RESKEY_vgname} +@@ -150,6 +155,16 @@ + + + ++ ++ ++If set, the volume group will be activated partially even with some ++physical volumes missing. It helps to set to true when using mirrored ++logical volumes. ++ ++Activate VG partially when missing PVs ++ ++ ++ + + + +@@ -486,6 +501,25 @@ + exit $OCF_ERR_CONFIGURED + fi + ++ # Inconsistency might be due to missing physical volumes, which doesn't ++ # automatically mean we should fail. If partial_activation=true then ++ # we should let start try to handle it, or if no PVs are listed as ++ # "unknown device" then another node may have marked a device missing ++ # where we have access to all of them and can start without issue. ++ case $(vgs -o attr --noheadings $VG | tr -d ' ') in ++ ???p??*) ++ if ! ocf_is_true "$OCF_RESKEY_partial_activation" ; then ++ # We are missing devices and cannot activate partially ++ ocf_exit_reason "Volume group [$VG] has devices missing. Consider partial_activation=true to attempt to activate partially" ++ exit $OCF_ERR_GENERIC ++ else ++ # We are missing devices but are allowed to activate partially. ++ # Assume that caused the vgck failure and carry on ++ ocf_log warn "Volume group inconsistency detected with missing device(s) and partial_activation enabled. Proceeding with requested action." ++ fi ++ ;; ++ esac ++ + # Get the access mode from VG metadata and check if it matches the input + # value. Skip to check "tagging" mode because there's no reliable way to + # automatically check if "tagging" mode is being used. +@@ -545,6 +579,10 @@ + do_activate() { + local activate_opt=$1 + ++ if ocf_is_true "$OCF_RESKEY_partial_activation" ; then ++ activate_opt="${activate_opt} --partial" ++ fi ++ + # Only activate the specific LV if it's given + if [ -n "$LV" ]; then + ocf_run lvchange $activate_opt ${VG}/${LV} diff --git a/SOURCES/bz1759113-aws-vpc-route53-1-update.patch b/SOURCES/bz1759113-aws-vpc-route53-1-update.patch new file mode 100644 index 0000000..9c689b1 --- /dev/null +++ b/SOURCES/bz1759113-aws-vpc-route53-1-update.patch @@ -0,0 +1,273 @@ +--- ClusterLabs-resource-agents-e711383f/heartbeat/aws-vpc-route53.in 2018-06-29 14:05:02.000000000 +0200 ++++ /home/oalbrigt/src/resource-agents/heartbeat/aws-vpc-route53.in 2019-11-07 12:24:18.822111495 +0100 +@@ -152,9 +152,15 @@ + END + } + +-ec2ip_validate() { ++r53_validate() { + ocf_log debug "function: validate" + ++ # Check for required binaries ++ ocf_log debug "Checking for required binaries" ++ for command in curl dig; do ++ check_binary "$command" ++ done ++ + # Full name + [[ -z "$OCF_RESKEY_fullname" ]] && ocf_log error "Full name parameter not set $OCF_RESKEY_fullname!" && exit $OCF_ERR_CONFIGURED + +@@ -175,32 +181,111 @@ + ocf_log debug "ok" + + if [ -n "$OCF_RESKEY_profile" ]; then +- AWS_PROFILE_OPT="--profile $OCF_RESKEY_profile" ++ AWS_PROFILE_OPT="--profile $OCF_RESKEY_profile --cli-connect-timeout 10" + else +- AWS_PROFILE_OPT="--profile default" ++ AWS_PROFILE_OPT="--profile default --cli-connect-timeout 10" + fi + + return $OCF_SUCCESS + } + +-ec2ip_monitor() { +- ec2ip_validate ++r53_monitor() { ++ # ++ # For every start action the agent will call Route53 API to check for DNS record ++ # otherwise it will try to get results directly bu querying the DNS using "dig". ++ # Due to complexity in some DNS architectures "dig" can fail, and if this happens ++ # the monitor will fallback to the Route53 API call. ++ # ++ # There will be no failure, failover or restart of the agent if the monitor operation fails ++ # hence we only return $OCF_SUCESS in this function ++ # ++ # In case of the monitor operation detects a wrong or non-existent Route53 DNS entry ++ # it will try to fix the existing one, or create it again ++ # ++ # ++ ARECORD="" ++ IPREGEX="^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$" ++ r53_validate + ocf_log debug "Checking Route53 record sets" +- IPADDRESS="$(ec2metadata aws ip | grep local-ipv4 | /usr/bin/awk '{ print $2 }')" +- ARECORD="$(aws $AWS_PROFILE_OPT route53 list-resource-record-sets --hosted-zone-id $OCF_RESKEY_hostedzoneid --query "ResourceRecordSets[?Name=='$OCF_RESKEY_fullname']" | grep RESOURCERECORDS | /usr/bin/awk '{ print $2 }' )" +- ocf_log debug "Found IP address: $ARECORD ." +- if [ "${ARECORD}" == "${IPADDRESS}" ]; then +- ocf_log debug "ARECORD $ARECORD found" ++ # ++ IPADDRESS="$(curl -s http://169.254.169.254/latest/meta-data/local-ipv4)" ++ # ++ if [ "$__OCF_ACTION" = "start" ] || ocf_is_probe ; then ++ # ++ cmd="aws $AWS_PROFILE_OPT route53 list-resource-record-sets --hosted-zone-id $OCF_RESKEY_hostedzoneid --query ResourceRecordSets[?Name=='$OCF_RESKEY_fullname']" ++ ocf_log info "Route53 Agent Starting or probing - executing monitoring API call: $cmd" ++ CLIRES="$($cmd 2>&1)" ++ rc=$? ++ ocf_log debug "awscli returned code: $rc" ++ if [ $rc -ne 0 ]; then ++ CLIRES=$(echo $CLIRES | grep -v '^$') ++ ocf_log warn "Route53 API returned an error: $CLIRES" ++ ocf_log warn "Skipping cluster action due to API call error" ++ return $OCF_ERR_GENERIC ++ fi ++ ARECORD=$(echo $CLIRES | grep RESOURCERECORDS | awk '{ print $5 }') ++ # ++ if ocf_is_probe; then ++ # ++ # Prevent R53 record change during probe ++ # ++ if [[ $ARECORD =~ $IPREGEX ]] && [ "$ARECORD" != "$IPADDRESS" ]; then ++ ocf_log debug "Route53 DNS record $ARECORD found at probing, disregarding" ++ return $OCF_NOT_RUNNING ++ fi ++ fi ++ else ++ # ++ cmd="dig +retries=3 +time=5 +short $OCF_RESKEY_fullname 2>/dev/null" ++ ocf_log info "executing monitoring command : $cmd" ++ ARECORD="$($cmd)" ++ rc=$? ++ ocf_log debug "dig return code: $rc" ++ # ++ if [[ ! $ARECORD =~ $IPREGEX ]] || [ $rc -ne 0 ]; then ++ ocf_log info "Fallback to Route53 API query due to DNS resolution failure" ++ cmd="aws $AWS_PROFILE_OPT route53 list-resource-record-sets --hosted-zone-id $OCF_RESKEY_hostedzoneid --query ResourceRecordSets[?Name=='$OCF_RESKEY_fullname']" ++ ocf_log debug "executing monitoring API call: $cmd" ++ CLIRES="$($cmd 2>&1)" ++ rc=$? ++ ocf_log debug "awscli return code: $rc" ++ if [ $rc -ne 0 ]; then ++ CLIRES=$(echo $CLIRES | grep -v '^$') ++ ocf_log warn "Route53 API returned an error: $CLIRES" ++ ocf_log warn "Monitor skipping cluster action due to API call error" ++ return $OCF_SUCCESS ++ fi ++ ARECORD=$(echo $CLIRES | grep RESOURCERECORDS | awk '{ print $5 }') ++ fi ++ # ++ fi ++ ocf_log info "Route53 DNS record pointing $OCF_RESKEY_fullname to IP address $ARECORD" ++ # ++ if [ "$ARECORD" == "$IPADDRESS" ]; then ++ ocf_log info "Route53 DNS record $ARECORD found" ++ return $OCF_SUCCESS ++ elif [[ $ARECORD =~ $IPREGEX ]] && [ "$ARECORD" != "$IPADDRESS" ]; then ++ ocf_log info "Route53 DNS record points to a different host, setting DNS record on Route53 to this host" ++ _update_record "UPSERT" "$IPADDRESS" + return $OCF_SUCCESS + else +- ocf_log debug "No ARECORD found" +- return $OCF_NOT_RUNNING ++ ocf_log info "No Route53 DNS record found, setting DNS record on Route53 to this host" ++ _update_record "UPSERT" "$IPADDRESS" ++ return $OCF_SUCCESS + fi + + return $OCF_SUCCESS + } + + _update_record() { ++ # ++ # This function is the one that will actually execute Route53's API call ++ # and configure the DNS record using the correct API calls and parameters ++ # ++ # It creates a temporary JSON file under /tmp with the required API payload ++ # ++ # Failures in this function are critical and will cause the agent to fail ++ # + update_action="$1" + IPADDRESS="$2" + ocf_log info "Updating Route53 $OCF_RESKEY_hostedzoneid with $IPADDRESS for $OCF_RESKEY_fullname" +@@ -209,19 +294,19 @@ + ocf_exit_reason "Failed to create temporary file for record update" + exit $OCF_ERR_GENERIC + fi +- cat >>"${ROUTE53RECORD}" <<-EOF ++ cat >>"$ROUTE53RECORD" <<-EOF + { + "Comment": "Update record to reflect new IP address for a system ", + "Changes": [ + { +- "Action": "${update_action}", ++ "Action": "$update_action", + "ResourceRecordSet": { +- "Name": "${OCF_RESKEY_fullname}", ++ "Name": "$OCF_RESKEY_fullname", + "Type": "A", +- "TTL": ${OCF_RESKEY_ttl}, ++ "TTL": $OCF_RESKEY_ttl, + "ResourceRecords": [ + { +- "Value": "${IPADDRESS}" ++ "Value": "$IPADDRESS" + } + ] + } +@@ -229,46 +314,53 @@ + ] + } + EOF +- cmd="aws --profile ${OCF_RESKEY_profile} route53 change-resource-record-sets --hosted-zone-id ${OCF_RESKEY_hostedzoneid} \ +- --change-batch file://${ROUTE53RECORD} " ++ cmd="aws --profile $OCF_RESKEY_profile route53 change-resource-record-sets --hosted-zone-id $OCF_RESKEY_hostedzoneid --change-batch file://$ROUTE53RECORD " + ocf_log debug "Executing command: $cmd" +- CHANGEID=$($cmd | grep CHANGEINFO | /usr/bin/awk -F'\t' '{ print $3 }' ) +- ocf_log debug "Change id: ${CHANGEID}" +- rmtempfile ${ROUTE53RECORD} +- CHANGEID=$(echo $CHANGEID |cut -d'/' -f 3 |cut -d'"' -f 1 ) +- ocf_log debug "Change id: ${CHANGEID}" ++ CLIRES="$($cmd 2>&1)" ++ rc=$? ++ ocf_log debug "awscli returned code: $rc" ++ if [ $rc -ne 0 ]; then ++ CLIRES=$(echo $CLIRES | grep -v '^$') ++ ocf_log warn "Route53 API returned an error: $CLIRES" ++ ocf_log warn "Skipping cluster action due to API call error" ++ return $OCF_ERR_GENERIC ++ fi ++ CHANGEID=$(echo $CLIRES | awk '{ print $12 }') ++ ocf_log debug "Change id: $CHANGEID" ++ rmtempfile $ROUTE53RECORD ++ CHANGEID=$(echo $CHANGEID | cut -d'/' -f 3 | cut -d'"' -f 1 ) ++ ocf_log debug "Change id: $CHANGEID" + STATUS="PENDING" +- MYSECONDS=2 ++ MYSECONDS=20 + while [ "$STATUS" = 'PENDING' ]; do +- sleep ${MYSECONDS} +- STATUS="$(aws --profile ${OCF_RESKEY_profile} route53 get-change --id $CHANGEID | grep CHANGEINFO | /usr/bin/awk -F'\t' '{ print $4 }' |cut -d'"' -f 2 )" +- ocf_log debug "Waited for ${MYSECONDS} seconds and checked execution of Route 53 update status: ${STATUS} " ++ sleep $MYSECONDS ++ STATUS="$(aws --profile $OCF_RESKEY_profile route53 get-change --id $CHANGEID | grep CHANGEINFO | awk -F'\t' '{ print $4 }' |cut -d'"' -f 2 )" ++ ocf_log debug "Waited for $MYSECONDS seconds and checked execution of Route 53 update status: $STATUS " + done + } + +-ec2ip_stop() { +- ocf_log info "Bringing down Route53 agent. (Will remove ARECORD)" +- IPADDRESS="$(ec2metadata aws ip | grep local-ipv4 | /usr/bin/awk '{ print $2 }')" +- ARECORD="$(aws $AWS_PROFILE_OPT route53 list-resource-record-sets --hosted-zone-id $OCF_RESKEY_hostedzoneid --query "ResourceRecordSets[?Name=='$OCF_RESKEY_fullname']" | grep RESOURCERECORDS | /usr/bin/awk '{ print $2 }' )" +- ocf_log debug "Found IP address: $ARECORD ." +- if [ ${ARECORD} != ${IPADDRESS} ]; then +- ocf_log debug "No ARECORD found" +- return $OCF_SUCCESS +- else +- # determine IP address +- IPADDRESS="$(ec2metadata aws ip | grep local-ipv4 | /usr/bin/awk '{ print $2 }')" +- # Patch file +- ocf_log debug "Deleting IP address to ${IPADDRESS}" +- return $OCF_SUCCESS +- fi +- +- _update_record "DELETE" "$IPADDRESS" ++r53_stop() { ++ # ++ # Stop operation doesn't perform any API call or try to remove the DNS record ++ # this mostly because this is not necessarily mandatory or desired ++ # the start and monitor functions will take care of changing the DNS record ++ # if the agent starts in a different cluster node ++ # ++ ocf_log info "Bringing down Route53 agent. (Will NOT remove Route53 DNS record)" + return $OCF_SUCCESS + } + +-ec2ip_start() { +- IPADDRESS="$(ec2metadata aws ip | grep local-ipv4 | /usr/bin/awk '{ print $2 }')" +- _update_record "UPSERT" "$IPADDRESS" ++r53_start() { ++ # ++ # Start agent and config DNS in Route53 ++ # ++ ocf_log info "Starting Route53 DNS update...." ++ IPADDRESS="$(curl -s http://169.254.169.254/latest/meta-data/local-ipv4)" ++ r53_monitor ++ if [ $? != $OCF_SUCCESS ]; then ++ ocf_log info "Could not start agent - check configurations" ++ return $OCF_ERR_GENERIC ++ fi + return $OCF_SUCCESS + } + +@@ -284,16 +376,16 @@ + exit $OCF_SUCCESS + ;; + monitor) +- ec2ip_monitor ++ r53_monitor + ;; + stop) +- ec2ip_stop ++ r53_stop + ;; + validate-all) +- ec2ip_validate ++ r53_validate + ;; + start) +- ec2ip_start ++ r53_start + ;; + *) + usage diff --git a/SOURCES/bz1759113-aws-vpc-route53-2-add-public-and-secondary-ip-support.patch b/SOURCES/bz1759113-aws-vpc-route53-2-add-public-and-secondary-ip-support.patch new file mode 100644 index 0000000..afb8bb6 --- /dev/null +++ b/SOURCES/bz1759113-aws-vpc-route53-2-add-public-and-secondary-ip-support.patch @@ -0,0 +1,220 @@ +From 9b77d06bfe3308692946b8ac08bc7ec3399a762b Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Thu, 2 Apr 2020 13:38:30 +0200 +Subject: [PATCH 1/2] aws-vpc-route53: cleanup and improvements + +--- + heartbeat/aws-vpc-route53.in | 73 ++++++++++++++++++++---------------- + 1 file changed, 41 insertions(+), 32 deletions(-) + +diff --git a/heartbeat/aws-vpc-route53.in b/heartbeat/aws-vpc-route53.in +index b276dfb3c..1cfc2b01f 100644 +--- a/heartbeat/aws-vpc-route53.in ++++ b/heartbeat/aws-vpc-route53.in +@@ -43,8 +43,14 @@ + : ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat} + . ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs + ++OCF_RESKEY_hostedzoneid_default="" ++OCF_RESKEY_fullname_default="" ++OCF_RESKEY_ip_default="local" + OCF_RESKEY_ttl_default=10 + ++: ${OCF_RESKEY_hostedzoneid:=${OCF_RESKEY_hostedzoneid_default}} ++: ${OCF_RESKEY_fullname:=${OCF_RESKEY_fullname_default}} ++: ${OCF_RESKEY_ip:=${OCF_RESKEY_ip_default}} + : ${OCF_RESKEY_ttl:=${OCF_RESKEY_ttl_default}} + + ####################################################################### +@@ -104,7 +110,7 @@ Hosted zone ID of Route 53. This is the table of + the Route 53 record. + + AWS hosted zone ID +- ++ + + + +@@ -113,7 +119,7 @@ Example: service.cloud.example.corp. + Note: The trailing dot is important to Route53! + + Full service name +- ++ + + + +@@ -189,6 +195,31 @@ r53_validate() { + return $OCF_SUCCESS + } + ++r53_start() { ++ # ++ # Start agent and config DNS in Route53 ++ # ++ ocf_log info "Starting Route53 DNS update...." ++ IPADDRESS="$(curl -s http://169.254.169.254/latest/meta-data/local-ipv4)" ++ r53_monitor ++ if [ $? != $OCF_SUCCESS ]; then ++ ocf_log info "Could not start agent - check configurations" ++ return $OCF_ERR_GENERIC ++ fi ++ return $OCF_SUCCESS ++} ++ ++r53_stop() { ++ # ++ # Stop operation doesn't perform any API call or try to remove the DNS record ++ # this mostly because this is not necessarily mandatory or desired ++ # the start and monitor functions will take care of changing the DNS record ++ # if the agent starts in a different cluster node ++ # ++ ocf_log info "Bringing down Route53 agent. (Will NOT remove Route53 DNS record)" ++ return $OCF_SUCCESS ++} ++ + r53_monitor() { + # + # For every start action the agent will call Route53 API to check for DNS record +@@ -339,31 +370,6 @@ _update_record() { + done + } + +-r53_stop() { +- # +- # Stop operation doesn't perform any API call or try to remove the DNS record +- # this mostly because this is not necessarily mandatory or desired +- # the start and monitor functions will take care of changing the DNS record +- # if the agent starts in a different cluster node +- # +- ocf_log info "Bringing down Route53 agent. (Will NOT remove Route53 DNS record)" +- return $OCF_SUCCESS +-} +- +-r53_start() { +- # +- # Start agent and config DNS in Route53 +- # +- ocf_log info "Starting Route53 DNS update...." +- IPADDRESS="$(curl -s http://169.254.169.254/latest/meta-data/local-ipv4)" +- r53_monitor +- if [ $? != $OCF_SUCCESS ]; then +- ocf_log info "Could not start agent - check configurations" +- return $OCF_ERR_GENERIC +- fi +- return $OCF_SUCCESS +-} +- + ############################################################################### + + case $__OCF_ACTION in +@@ -375,20 +381,23 @@ case $__OCF_ACTION in + metadata + exit $OCF_SUCCESS + ;; +- monitor) +- r53_monitor ++ start) ++ r53_validate || exit $? ++ r53_start + ;; + stop) + r53_stop + ;; ++ monitor) ++ r53_monitor ++ ;; + validate-all) + r53_validate + ;; +- start) +- r53_start +- ;; + *) + usage + exit $OCF_ERR_UNIMPLEMENTED + ;; + esac ++ ++exit $? + +From 745c6b9b3e331ed3705a641f1ec03a2604de3a1d Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Thu, 2 Apr 2020 13:40:33 +0200 +Subject: [PATCH 2/2] aws-vpc-route53: add support for public and secondary + private IPs + +--- + heartbeat/aws-vpc-route53.in | 31 +++++++++++++++++++++++++++++-- + 1 file changed, 29 insertions(+), 2 deletions(-) + +diff --git a/heartbeat/aws-vpc-route53.in b/heartbeat/aws-vpc-route53.in +index 1cfc2b01f..ca6556951 100644 +--- a/heartbeat/aws-vpc-route53.in ++++ b/heartbeat/aws-vpc-route53.in +@@ -121,6 +121,15 @@ Note: The trailing dot is important to Route53! + Full service name + + ++ ++ ++IP (local (default), public or secondary private IP address (e.g. 10.0.0.1). ++ ++A secondary private IP can be setup with the awsvip agent. ++ ++Type of IP or secondary private IP address (local, public or e.g. 10.0.0.1) ++ ++ + + + Time to live for Route53 ARECORD +@@ -173,6 +182,15 @@ r53_validate() { + # Hosted Zone ID + [[ -z "$OCF_RESKEY_hostedzoneid" ]] && ocf_log error "Hosted Zone ID parameter not set $OCF_RESKEY_hostedzoneid!" && exit $OCF_ERR_CONFIGURED + ++ # Type of IP/secondary IP address ++ case $OCF_RESKEY_ip in ++ local|public|*.*.*.*) ++ ;; ++ *) ++ ocf_exit_reason "Invalid value for ip: ${OCF_RESKEY_ip}" ++ exit $OCF_ERR_CONFIGURED ++ esac ++ + # profile + [[ -z "$OCF_RESKEY_profile" ]] && ocf_log error "AWS CLI profile not set $OCF_RESKEY_profile!" && exit $OCF_ERR_CONFIGURED + +@@ -200,7 +218,7 @@ r53_start() { + # Start agent and config DNS in Route53 + # + ocf_log info "Starting Route53 DNS update...." +- IPADDRESS="$(curl -s http://169.254.169.254/latest/meta-data/local-ipv4)" ++ _get_ip + r53_monitor + if [ $? != $OCF_SUCCESS ]; then + ocf_log info "Could not start agent - check configurations" +@@ -239,7 +257,7 @@ r53_monitor() { + r53_validate + ocf_log debug "Checking Route53 record sets" + # +- IPADDRESS="$(curl -s http://169.254.169.254/latest/meta-data/local-ipv4)" ++ _get_ip + # + if [ "$__OCF_ACTION" = "start" ] || ocf_is_probe ; then + # +@@ -308,6 +326,15 @@ r53_monitor() { + return $OCF_SUCCESS + } + ++_get_ip() { ++ case $OCF_RESKEY_ip in ++ local|public) ++ IPADDRESS="$(curl -s http://169.254.169.254/latest/meta-data/${OCF_RESKEY_ip}-ipv4)";; ++ *.*.*.*) ++ IPADDRESS="${OCF_RESKEY_ip}";; ++ esac ++} ++ + _update_record() { + # + # This function is the one that will actually execute Route53's API call diff --git a/SOURCES/bz1764886-exportfs-allow-same-fsid.patch b/SOURCES/bz1764886-exportfs-allow-same-fsid.patch new file mode 100644 index 0000000..93d47af --- /dev/null +++ b/SOURCES/bz1764886-exportfs-allow-same-fsid.patch @@ -0,0 +1,22 @@ +From 9cea030ba6d5c759971873b80d6d97b545ecac39 Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Thu, 7 Nov 2019 13:03:30 +0100 +Subject: [PATCH] exportfs: allow multiple exports of same directory + +--- + heartbeat/exportfs | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/heartbeat/exportfs b/heartbeat/exportfs +index d79aced88..1cabdee70 100755 +--- a/heartbeat/exportfs ++++ b/heartbeat/exportfs +@@ -82,7 +82,7 @@ The directory or directories to export. + + + +- ++ + + The fsid option to pass to exportfs. This can be a unique positive + integer, a UUID (assuredly sans comma characters), or the special string diff --git a/SOURCES/bz1772337-IPaddr2-add-noprefixroute-parameter.patch b/SOURCES/bz1772337-IPaddr2-add-noprefixroute-parameter.patch new file mode 100644 index 0000000..f713613 --- /dev/null +++ b/SOURCES/bz1772337-IPaddr2-add-noprefixroute-parameter.patch @@ -0,0 +1,66 @@ +From 34b46b172857babbb2bca5e012c7827ed6a26b01 Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Wed, 6 Nov 2019 10:00:31 +0100 +Subject: [PATCH] IPaddr2: add noprefixroute parameter + +--- + heartbeat/IPaddr2 | 17 ++++++++++++++++- + 1 file changed, 16 insertions(+), 1 deletion(-) + +diff --git a/heartbeat/IPaddr2 b/heartbeat/IPaddr2 +index 1d39ae514..6f8e8c734 100755 +--- a/heartbeat/IPaddr2 ++++ b/heartbeat/IPaddr2 +@@ -88,6 +88,7 @@ OCF_RESKEY_arp_sender_default="" + OCF_RESKEY_send_arp_opts_default="" + OCF_RESKEY_flush_routes_default="false" + OCF_RESKEY_run_arping_default=false ++OCF_RESKEY_noprefixroute_default="false" + OCF_RESKEY_preferred_lft_default="forever" + OCF_RESKEY_network_namespace_default="" + +@@ -109,6 +110,7 @@ OCF_RESKEY_network_namespace_default="" + : ${OCF_RESKEY_send_arp_opts=${OCF_RESKEY_send_arp_opts_default}} + : ${OCF_RESKEY_flush_routes=${OCF_RESKEY_flush_routes_default}} + : ${OCF_RESKEY_run_arping=${OCF_RESKEY_run_arping_default}} ++: ${OCF_RESKEY_noprefixroute=${OCF_RESKEY_noprefixroute_default}} + : ${OCF_RESKEY_preferred_lft=${OCF_RESKEY_preferred_lft_default}} + : ${OCF_RESKEY_network_namespace=${OCF_RESKEY_network_namespace_default}} + +@@ -377,6 +379,14 @@ Whether or not to run arping for IPv4 collision detection check. + + + ++ ++ ++Use noprefixroute flag (see 'man ip-address'). ++ ++Use noprefixroute flag ++ ++ ++ + + + For IPv6, set the preferred lifetime of the IP address. +@@ -397,8 +407,8 @@ the namespace. + Network namespace to use + + +- + ++ + + + +@@ -640,6 +650,11 @@ add_interface () { + msg="Adding $FAMILY address $ipaddr/$netmask with broadcast address $broadcast to device $iface" + fi + ++ if ocf_is_true "${OCF_RESKEY_noprefixroute}"; then ++ cmd="$cmd noprefixroute" ++ msg="${msg} (with noprefixroute)" ++ fi ++ + if [ ! -z "$label" ]; then + cmd="$cmd label $label" + msg="${msg} (with label $label)" diff --git a/SOURCES/bz1781427-rabbitmq-cluster-delete-nodename-when-stop-fails.patch b/SOURCES/bz1781427-rabbitmq-cluster-delete-nodename-when-stop-fails.patch new file mode 100644 index 0000000..fc95598 --- /dev/null +++ b/SOURCES/bz1781427-rabbitmq-cluster-delete-nodename-when-stop-fails.patch @@ -0,0 +1,38 @@ +From 47d75f8de9dc912da035805f141c674885ce432f Mon Sep 17 00:00:00 2001 +From: John Eckersberg +Date: Thu, 16 Jan 2020 10:20:59 -0500 +Subject: [PATCH] rabbitmq-cluster: ensure we delete nodename if stop action + fails + +If the stop action fails, we want to remove the nodename from the crm +attribute. Currently it is possible for the stop action to fail but +the rabbitmq server does actually stop. This leaves the attribute +still present. This means if the entire rabbitmq cluster is stopped, +it is not possible to start the cluster again because the first node +to start will think there is at least one other node running. Then +the node tries to join an existing cluster instead of rebootstrapping +the cluster from a single node. +--- + heartbeat/rabbitmq-cluster | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/heartbeat/rabbitmq-cluster b/heartbeat/rabbitmq-cluster +index 7837e9e3c..a9ebd37ad 100755 +--- a/heartbeat/rabbitmq-cluster ++++ b/heartbeat/rabbitmq-cluster +@@ -552,6 +552,7 @@ rmq_stop() { + + if [ $rc -ne 0 ]; then + ocf_log err "rabbitmq-server stop command failed: $RMQ_CTL stop, $rc" ++ rmq_delete_nodename + return $rc + fi + +@@ -565,6 +566,7 @@ rmq_stop() { + break + elif [ "$rc" -ne $OCF_SUCCESS ]; then + ocf_log info "rabbitmq-server stop failed: $rc" ++ rmq_delete_nodename + exit $OCF_ERR_GENERIC + fi + sleep 1 diff --git a/SOURCES/bz1792241-redis-1-fix-validate-all.patch b/SOURCES/bz1792241-redis-1-fix-validate-all.patch new file mode 100644 index 0000000..1d94852 --- /dev/null +++ b/SOURCES/bz1792241-redis-1-fix-validate-all.patch @@ -0,0 +1,32 @@ +From 617adbf651e9e1767f8f52218beb0a572be7bc50 Mon Sep 17 00:00:00 2001 +From: zaenk +Date: Fri, 17 Jan 2020 09:23:28 +0100 +Subject: [PATCH] redis: validate_all: fixes file status tests + +--- + heartbeat/redis.in | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/heartbeat/redis.in b/heartbeat/redis.in +index 1486e48b0..b030a8223 100755 +--- a/heartbeat/redis.in ++++ b/heartbeat/redis.in +@@ -698,15 +698,15 @@ redis_notify() { + } + + redis_validate() { +- if [[ -x "$REDIS_SERVER" ]]; then ++ if [[ ! -x "$REDIS_SERVER" ]]; then + ocf_log err "validate: $REDIS_SERVER does not exist or is not executable" + return $OCF_ERR_INSTALLED + fi +- if [[ -x "$REDIS_CLIENT" ]]; then ++ if [[ ! -x "$REDIS_CLIENT" ]]; then + ocf_log err "validate: $REDIS_CLIENT does not exist or is not executable" + return $OCF_ERR_INSTALLED + fi +- if [[ -f "$REDIS_CONFIG" ]]; then ++ if [[ ! -f "$REDIS_CONFIG" ]]; then + ocf_log err "validate: $REDIS_CONFIG does not exist" + return $OCF_ERR_CONFIGURED + fi diff --git a/SOURCES/bz1792241-redis-2-run-validate-during-start.patch b/SOURCES/bz1792241-redis-2-run-validate-during-start.patch new file mode 100644 index 0000000..c5d3107 --- /dev/null +++ b/SOURCES/bz1792241-redis-2-run-validate-during-start.patch @@ -0,0 +1,24 @@ +From 10b94d052f7f0fd710af7da8a46270b7548efbdf Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Thu, 19 Mar 2020 10:55:35 +0100 +Subject: [PATCH] redis: run validate-action during start + +--- + heartbeat/redis.in | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/heartbeat/redis.in b/heartbeat/redis.in +index b030a8223..da7230a49 100755 +--- a/heartbeat/redis.in ++++ b/heartbeat/redis.in +@@ -673,6 +673,10 @@ + clientpasswd="$(sed -n -e 's/^\s*requirepass\s*\(.*\)\s*$/\1/p' < $REDIS_CONFIG | tail -n 1)" + fi + ++if [ "$__OCF_ACTION" = "start" ]; then ++ redis_validate || exit $? ++fi ++ + ocf_log debug "action=${1:-$__OCF_ACTION} notify_type=${OCF_RESKEY_CRM_meta_notify_type} notify_operation=${OCF_RESKEY_CRM_meta_notify_operation} master_host=${OCF_RESKEY_CRM_meta_notify_master_uname} slave_host=${OCF_RESKEY_CRM_meta_notify_slave_uname} promote_host=${OCF_RESKEY_CRM_meta_notify_promote_uname} demote_host=${OCF_RESKEY_CRM_meta_notify_demote_uname}; params: bin=${OCF_RESKEY_bin} client_bin=${OCF_RESKEY_client_bin} config=${OCF_RESKEY_config} user=${OCF_RESKEY_user} rundir=${OCF_RESKEY_rundir} port=${OCF_RESKEY_port}" + + case "${1:-$__OCF_ACTION}" in diff --git a/SOURCES/bz1793431-oralsnr-allow-using-same-tns_admin.patch b/SOURCES/bz1793431-oralsnr-allow-using-same-tns_admin.patch new file mode 100644 index 0000000..5963259 --- /dev/null +++ b/SOURCES/bz1793431-oralsnr-allow-using-same-tns_admin.patch @@ -0,0 +1,23 @@ +From 2e288c233dc3b67d1845d5fdb8f857e0d7377820 Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Wed, 12 Feb 2020 10:32:17 +0100 +Subject: [PATCH] oralsnr: allow using the tns_admin directory for different + listeners + +--- + heartbeat/oralsnr | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/heartbeat/oralsnr b/heartbeat/oralsnr +index 08fb8ae11..1ecf8fca8 100755 +--- a/heartbeat/oralsnr ++++ b/heartbeat/oralsnr +@@ -116,7 +116,7 @@ Defaults to LISTENER. + + + +- ++ + + Full path to the directory that contains the Oracle + listener tnsnames.ora configuration file. The shell diff --git a/SOURCES/bz1818035-ocf_is_clone-1-fix-clone-max-can-be-0.patch b/SOURCES/bz1818035-ocf_is_clone-1-fix-clone-max-can-be-0.patch new file mode 100644 index 0000000..2b025c5 --- /dev/null +++ b/SOURCES/bz1818035-ocf_is_clone-1-fix-clone-max-can-be-0.patch @@ -0,0 +1,23 @@ +From bb9e54cdac71a1f26aa626d234e38c8ae8417e9f Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Thu, 26 Mar 2020 16:26:14 +0100 +Subject: [PATCH] ocf-shellfuncs: fix ocf_is_clone() (clone_max can be 0 with + cloned resources) + +--- + heartbeat/ocf-shellfuncs.in | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/heartbeat/ocf-shellfuncs.in b/heartbeat/ocf-shellfuncs.in +index 7a97558a5..e0eaae1d5 100644 +--- a/heartbeat/ocf-shellfuncs.in ++++ b/heartbeat/ocf-shellfuncs.in +@@ -557,7 +557,7 @@ ocf_is_probe() { + # defined as a resource where the clone-max meta attribute is present, + # and set to greater than zero. + ocf_is_clone() { +- [ ! -z "${OCF_RESKEY_CRM_meta_clone_max}" ] && [ "${OCF_RESKEY_CRM_meta_clone_max}" -gt 0 ] ++ [ ! -z "${OCF_RESKEY_CRM_meta_clone_max}" ] + } + + # returns true if the resource is configured as a multistate diff --git a/SOURCES/bz1818035-ocf_is_clone-2-update-comment.patch b/SOURCES/bz1818035-ocf_is_clone-2-update-comment.patch new file mode 100644 index 0000000..4b9be99 --- /dev/null +++ b/SOURCES/bz1818035-ocf_is_clone-2-update-comment.patch @@ -0,0 +1,24 @@ +From 420e55da2eb542b35fe8af5d05496b129cd190d5 Mon Sep 17 00:00:00 2001 +From: Oyvind Albrigtsen +Date: Fri, 27 Mar 2020 08:44:12 +0100 +Subject: [PATCH] ocf-shellfuncs: ocf_is_clone: update comment based on + clone-max fix in previous commit + +--- + heartbeat/ocf-shellfuncs.in | 3 +-- + 1 file changed, 1 insertion(+), 2 deletions(-) + +diff --git a/heartbeat/ocf-shellfuncs.in b/heartbeat/ocf-shellfuncs.in +index e0eaae1d5..c4d40e382 100644 +--- a/heartbeat/ocf-shellfuncs.in ++++ b/heartbeat/ocf-shellfuncs.in +@@ -554,8 +554,7 @@ ocf_is_probe() { + } + + # returns true if the resource is configured as a clone. This is +-# defined as a resource where the clone-max meta attribute is present, +-# and set to greater than zero. ++# defined as a resource where the clone-max meta attribute is present. + ocf_is_clone() { + [ ! -z "${OCF_RESKEY_CRM_meta_clone_max}" ] + } diff --git a/SPECS/resource-agents.spec b/SPECS/resource-agents.spec index ec27d51..9fac9bc 100644 --- a/SPECS/resource-agents.spec +++ b/SPECS/resource-agents.spec @@ -45,7 +45,7 @@ %global sap_script_package_hash f3644f5 %global saphana_prefix SAPHanaSR -%global saphana_hash 2067519 +%global saphana_version d1dde99 %global saphana_scaleout_prefix SAPHanaSR-ScaleOut %global saphana_scaleout_hash a77e8c6 @@ -95,7 +95,7 @@ Name: resource-agents Summary: Open Source HA Reusable Cluster Resource Scripts Version: 4.1.1 -Release: 42%{?dist} +Release: 53%{?dist} License: GPLv2+ and LGPLv2+ and ASL 2.0 URL: https://github.com/ClusterLabs/resource-agents %if 0%{?fedora} || 0%{?centos_version} || 0%{?rhel} @@ -106,7 +106,7 @@ Group: Productivity/Clustering/HA Source0: %{upstream_prefix}-%{upstream_version}.tar.gz Source1: %{sap_script_prefix}-%{sap_script_hash}.tar.gz Source2: %{sap_script_package_prefix}-%{sap_script_package_hash}.tar.gz -Source3: %{saphana_prefix}-%{saphana_hash}.tar.gz +Source3: %{saphana_prefix}-%{saphana_version}.tar.gz Source4: %{saphana_scaleout_prefix}-%{saphana_scaleout_hash}.tar.gz Source5: %{googlecloudsdk}-%{googlecloudsdk_version}-linux-x86_64.tar.gz Source6: %{pyroute2}-%{pyroute2_version}.tar.gz @@ -178,19 +178,48 @@ Patch57: bz1719443-CTDB-1-fixes.patch Patch58: bz1719443-CTDB-2-add-v4.9-support.patch Patch59: bz1732873-LVM-activate-fix-monitor-hang.patch Patch60: bz1598969-2-iSCSILogicalUnit-create-acls-fix.patch -Patch61: bz1642067-SAPHanaTopology-make-multi-instance-aware.patch -Patch62: bz1582685-IPsrcaddr-add-destination-and-table-parameters.patch -Patch63: bz1726736-Route-dont-fence-when-parameters-not-set.patch -Patch64: bz1746266-docker-1-fail-gracefully-when-not-running.patch -Patch65: bz1746266-docker-2-improve-daemon-check.patch -Patch66: bz1746266-docker-3-fix-stop-return-code.patch -Patch67: bz1744467-rabbitmq-cluster-restore-users-single-node-mode.patch -Patch68: bz1528365-IPaddr2-1-sanitize-IPv6-IPs.patch -Patch69: bz1528365-IPaddr2-2-return-empty-when-sanitation-fails.patch -Patch70: bz1740069-NovaEvacuate-evacuate_delay.patch -Patch71: bz1757753-nfsserver-1-systemd-perf-improvements.patch -Patch72: bz1757753-nfsserver-2-systemd-use-no-legend.patch -Patch73: bz1695041-LVM-activate-return-NOT_RUNNING-rejoin-cluster.patch +Patch61: bz1582685-IPsrcaddr-1-add-destination-and-table-parameters.patch +Patch62: bz1726736-Route-1-dont-fence-when-parameters-not-set.patch +Patch63: bz1746266-docker-1-fail-gracefully-when-not-running.patch +Patch64: bz1746266-docker-2-improve-daemon-check.patch +Patch65: bz1746266-docker-3-fix-stop-return-code.patch +Patch66: bz1744467-rabbitmq-cluster-restore-users-single-node-mode.patch +Patch67: bz1528365-IPaddr2-1-sanitize-IPv6-IPs.patch +Patch68: bz1528365-IPaddr2-2-return-empty-when-sanitation-fails.patch +Patch69: bz1740069-NovaEvacuate-evacuate_delay.patch +Patch70: bz1757753-nfsserver-1-systemd-perf-improvements.patch +Patch71: bz1757753-nfsserver-2-systemd-use-no-legend.patch +Patch72: bz1695041-LVM-activate-return-NOT_RUNNING-rejoin-cluster.patch +Patch73: bz1726736-Route-2-validate-start-validate-all.patch +Patch74: bz1582685-IPsrcaddr-2-local-rule-destination-fixes.patch +Patch75: bz1582685-IPsrcaddr-3-fix-probe-issues.patch +Patch76: bz1582685-IPsrcaddr-4-fix-hardcoded-device.patch +Patch77: bz1759067-LVM-activate-detect-volume-without-reboot.patch +Patch78: bz1744145-Filesystem-1-avoid-corrupt-mount-list.patch +Patch79: bz1744145-Filesystem-2-prevent-killing-bind-mount.patch +Patch80: bz1744145-Filesystem-3-improved-bind-mount-check.patch +Patch81: bz1759068-LVM-activate-partial-activation.patch +Patch82: bz1764886-exportfs-allow-same-fsid.patch +Patch83: bz1772337-IPaddr2-add-noprefixroute-parameter.patch +Patch84: bz1781427-rabbitmq-cluster-delete-nodename-when-stop-fails.patch +Patch85: bz1793431-oralsnr-allow-using-same-tns_admin.patch +Patch86: bz1759113-aws-vpc-route53-1-update.patch +Patch87: bz1759113-aws-vpc-route53-2-add-public-and-secondary-ip-support.patch +Patch88: bz1792241-redis-1-fix-validate-all.patch +Patch89: bz1792241-redis-2-run-validate-during-start.patch +Patch90: bz1818035-ocf_is_clone-1-fix-clone-max-can-be-0.patch +Patch91: bz1818035-ocf_is_clone-2-update-comment.patch +Patch92: bz1624203-Filesystem-1-monitor-symlink-support.patch +Patch93: bz1624203-Filesystem-2-add-symlink-support.patch +Patch94: bz1624203-Filesystem-3-fix-umount-disk-failure.patch +Patch95: bz1624203-Filesystem-4-fix-readlink-issue.patch +Patch96: bz1633249-gcp-pd-move-1.patch +Patch97: bz1633249-gcp-pd-move-2-use-OCF_FUNCTIONS_DIR.patch +Patch98: bz1633249-gcp-pd-move-3-add-stackdriver_logging-to-metadata.patch +Patch99: bz1640575-1-ocf.py-update.patch +Patch100: bz1640575-2-azure-events.patch +Patch101: bz1744431-pgsql-1-set-initial-score.patch +Patch102: bz1744431-pgsql-2-prevent-incorrect-status-start.patch # bundle patches Patch1000: bz1568588-7-gcp-bundled.patch @@ -385,7 +414,7 @@ SAP instances to be managed in a cluster environment. License: GPLv2+ Summary: SAP HANA Scale-Out cluster resource agents Version: 0.163.2 -Release: 18%{?rcver:%{rcver}}%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty}}%{?dist} +Release: 29%{?rcver:%{rcver}}%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty}}%{?dist} %if 0%{?fedora} || 0%{?centos_version} || 0%{?rhel} Group: System Environment/Base %else @@ -405,7 +434,7 @@ environment. License: GPLv2+ Summary: SAP cluster connector script Version: 3.0.1 -Release: 18%{?rcver:%{rcver}}%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty}}%{?dist} +Release: 29%{?rcver:%{rcver}}%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty}}%{?dist} %if 0%{?fedora} || 0%{?centos_version} || 0%{?rhel} Group: System Environment/Base %else @@ -425,10 +454,10 @@ instances to be managed in a cluster environment. exit 1 %endif %setup -q -n %{upstream_prefix}-%{upstream_version} -%setup -T -D -a 1 -n %{upstream_prefix}-%{upstream_version} -%setup -T -D -a 2 -n %{upstream_prefix}-%{upstream_version} -%setup -T -D -a 3 -n %{upstream_prefix}-%{upstream_version} -%setup -T -D -a 4 -n %{upstream_prefix}-%{upstream_version} +%setup -q -T -D -a 1 -n %{upstream_prefix}-%{upstream_version} +%setup -q -T -D -a 2 -n %{upstream_prefix}-%{upstream_version} +%setup -q -T -D -a 3 -n %{upstream_prefix}-%{upstream_version} +%setup -q -T -D -a 4 -n %{upstream_prefix}-%{upstream_version} %patch0 -p1 %patch1 -p1 %patch2 -p1 @@ -503,15 +532,47 @@ exit 1 %patch71 -p1 %patch72 -p1 %patch73 -p1 +%patch74 -p1 +%patch75 -p1 +%patch76 -p1 +%patch77 -p1 +%patch78 -p1 +%patch79 -p1 +%patch80 -p1 +%patch81 -p1 +%patch82 -p1 +%patch83 -p1 -F2 +%patch84 -p1 +%patch85 -p1 -F1 +%patch86 -p1 +%patch87 -p1 +%patch88 -p1 +%patch89 -p1 +%patch90 -p1 +%patch91 -p1 +%patch92 -p1 +%patch93 -p1 +%patch94 -p1 +%patch95 -p1 +%patch96 -p1 +%patch97 -p1 +%patch98 -p1 +%patch99 -p1 +%patch100 -p1 +%patch101 -p1 +%patch102 -p1 # add SAPHana agents to Makefile.am -mv %{saphana_prefix}-%{saphana_hash}/SAPHana/ra/SAPHana* heartbeat +mv %{saphana_prefix}-%{saphana_version}/ra/SAPHana* heartbeat mv %{saphana_scaleout_prefix}-%{saphana_scaleout_hash}/SAPHana/ra/SAPHanaController heartbeat mv %{saphana_scaleout_prefix}-%{saphana_scaleout_hash}/SAPHana/ra/SAPHanaTopology heartbeat/SAPHanaTopologyScaleOut sed -i -e 's/\( /dev/null 2>&1 ||: /usr/lib/ocf/resource.d/heartbeat/SAPHanaTopology %{_mandir}/man7/*SAPHana.* %{_mandir}/man7/*SAPHanaTopology.* +%{_datadir}/SAPHanaSR +%exclude %{_datadir}/SAPHanaSR/srHook/SAPHanaSR.py? %endif %ifarch x86_64 ppc64le @@ -1134,6 +1199,63 @@ ccs_update_schema > /dev/null 2>&1 ||: %endif %changelog +* Tue Apr 14 2020 Oyvind Albrigtsen - 4.1.1-53 +- aws-vpc-route53: new resource agent for AWS +- redis: fix validate-all action and run it during start-action +- ocf-shellfuncs: ocf_is_clone(): fix to return true when clone-max + is set to 0 +- Filesystem: add symlink support +- gcp-pd-move: new resource agent for Google Cloud +- azure-events: new resource agent for Azure +- pgsql: get correct status during start-action, and set initial + score for primary and hot standby + + Resolves: rhbz#1759113 + Resolves: rhbz#1792241 + Resolves: rhbz#1818035 + Resolves: rhbz#1624203 + Resolves: rhbz#1633249 + Resolves: rhbz#1640575 + Resolves: rhbz#1744431 + +* Tue Feb 18 2020 Oyvind Albrigtsen - 4.1.1-50 +- Rebase resource-agents-sap-hana to SAPHanaSR 0.154.0 upstream release. + + Resolves: rhbz#1794879 + +* Wed Feb 12 2020 Oyvind Albrigtsen - 4.1.1-49 +- oralsnr: allow using same tns_admin directory for listeners + + Resolves: rhbz#1793431 + +* Thu Jan 30 2020 Oyvind Albrigtsen - 4.1.1-48 +- rabbitmq-cluster: delete nodename when stop fails + + Resolves: rhbz#1781427 + +* Tue Jan 21 2020 Oyvind Albrigtsen - 4.1.1-47 +- LVM-activate: detect systemid volume without reboot +- Filesystem: avoid corrupt mount-list and dont kill incorrect processes +- LVM-activate: add partial-activation support +- exportfs: allow multiple exports with same fsid +- IPaddr2: add noprefixroute parameter + + Resolves: rhbz#1759067 + Resolves: rhbz#1744145 + Resolves: rhbz#1759068 + Resolves: rhbz#1764886 + Resolves: rhbz#1772337 + +* Thu Jan 16 2020 Oyvind Albrigtsen - 4.1.1-46 +- IPsrcaddr: add destination and table parameters + + Resolves: rhbz#1582685 + +* Fri Oct 11 2019 Oyvind Albrigtsen - 4.1.1-43 +- Route: dont fence when parameters not set + + Resolves: rhbz#1726736 + * Wed Oct 2 2019 Oyvind Albrigtsen - 4.1.1-42 - LVM-activate: return NOT_RUNNING when node rejoins cluster @@ -1160,17 +1282,10 @@ ccs_update_schema > /dev/null 2>&1 ||: Resolves: rhbz#1744467 * Mon Sep 9 2019 Oyvind Albrigtsen - 4.1.1-36 -- Route: dont fence when parameters not set - docker: improve daemon check to fence when docker is stopped manually - Resolves: rhbz#1726736 Resolves: rhbz#1746266 -* Thu Aug 15 2019 Oyvind Albrigtsen - 4.1.1-34 -- IPsrcaddr: add destination and table parameters - - Resolves: rhbz#1582685 - * Tue Aug 6 2019 Oyvind Albrigtsen - 4.1.1-33 - SAPHanaTopology: make multi instance aware (MCOS)