Blame SOURCES/bz2111147-azure-events-az-new-ra.patch

2de1a8
From 5dcd5153f0318e4766f7f4d3e61dfdb4b352c39c Mon Sep 17 00:00:00 2001
2de1a8
From: MSSedusch <sedusch@microsoft.com>
2de1a8
Date: Mon, 30 May 2022 15:08:10 +0200
2de1a8
Subject: [PATCH 1/2] add new Azure Events AZ resource agent
2de1a8
2de1a8
---
2de1a8
 .gitignore                   |   1 +
2de1a8
 configure.ac                 |   8 +
2de1a8
 doc/man/Makefile.am          |   4 +
2de1a8
 heartbeat/Makefile.am        |   4 +
2de1a8
 heartbeat/azure-events-az.in | 782 +++++++++++++++++++++++++++++++++++
2de1a8
 5 files changed, 799 insertions(+)
2de1a8
 create mode 100644 heartbeat/azure-events-az.in
2de1a8
2de1a8
diff --git a/.gitignore b/.gitignore
2de1a8
index 0c259b5cf..e2b7c039c 100644
2de1a8
--- a/.gitignore
2de1a8
+++ b/.gitignore
2de1a8
@@ -54,6 +54,7 @@ heartbeat/Squid
2de1a8
 heartbeat/SysInfo
2de1a8
 heartbeat/aws-vpc-route53
2de1a8
 heartbeat/azure-events
2de1a8
+heartbeat/azure-events-az
2de1a8
 heartbeat/clvm
2de1a8
 heartbeat/conntrackd
2de1a8
 heartbeat/dnsupdate
2de1a8
diff --git a/configure.ac b/configure.ac
2de1a8
index eeecfad0e..5716a2be2 100644
2de1a8
--- a/configure.ac
2de1a8
+++ b/configure.ac
2de1a8
@@ -523,6 +523,13 @@ if test -z "$PYTHON" || test $BUILD_OCF_PY -eq 0; then
2de1a8
 fi
2de1a8
 AM_CONDITIONAL(BUILD_AZURE_EVENTS, test $BUILD_AZURE_EVENTS -eq 1)
2de1a8
 
2de1a8
+BUILD_AZURE_EVENTS_AZ=1
2de1a8
+if test -z "$PYTHON" || test $BUILD_OCF_PY -eq 0; then
2de1a8
+    BUILD_AZURE_EVENTS_AZ=0
2de1a8
+    AC_MSG_WARN("Not building azure-events-az")
2de1a8
+fi
2de1a8
+AM_CONDITIONAL(BUILD_AZURE_EVENTS_AZ, test $BUILD_AZURE_EVENTS_AZ -eq 1)
2de1a8
+
2de1a8
 BUILD_GCP_PD_MOVE=1
2de1a8
 if test -z "$PYTHON" || test "x${HAVE_PYMOD_GOOGLEAPICLIENT}" != xyes || test $BUILD_OCF_PY -eq 0; then
2de1a8
     BUILD_GCP_PD_MOVE=0
2de1a8
@@ -976,6 +983,7 @@ rgmanager/Makefile						\
2de1a8
 
2de1a8
 dnl Files we output that need to be executable
2de1a8
 AC_CONFIG_FILES([heartbeat/azure-events], [chmod +x heartbeat/azure-events])
2de1a8
+AC_CONFIG_FILES([heartbeat/azure-events-az], [chmod +x heartbeat/azure-events-az])
2de1a8
 AC_CONFIG_FILES([heartbeat/AoEtarget], [chmod +x heartbeat/AoEtarget])
2de1a8
 AC_CONFIG_FILES([heartbeat/ManageRAID], [chmod +x heartbeat/ManageRAID])
2de1a8
 AC_CONFIG_FILES([heartbeat/ManageVE], [chmod +x heartbeat/ManageVE])
2de1a8
diff --git a/doc/man/Makefile.am b/doc/man/Makefile.am
2de1a8
index cd8fd16bf..658c700ac 100644
2de1a8
--- a/doc/man/Makefile.am
2de1a8
+++ b/doc/man/Makefile.am
2de1a8
@@ -219,6 +219,10 @@ if BUILD_AZURE_EVENTS
2de1a8
 man_MANS           	+= ocf_heartbeat_azure-events.7
2de1a8
 endif
2de1a8
 
2de1a8
+if BUILD_AZURE_EVENTS_AZ
2de1a8
+man_MANS           	+= ocf_heartbeat_azure-events-az.7
2de1a8
+endif
2de1a8
+
2de1a8
 if BUILD_GCP_PD_MOVE
2de1a8
 man_MANS           	+= ocf_heartbeat_gcp-pd-move.7
2de1a8
 endif
2de1a8
diff --git a/heartbeat/Makefile.am b/heartbeat/Makefile.am
2de1a8
index 20d41e36a..1133dc13e 100644
2de1a8
--- a/heartbeat/Makefile.am
2de1a8
+++ b/heartbeat/Makefile.am
2de1a8
@@ -188,6 +188,10 @@ if BUILD_AZURE_EVENTS
2de1a8
 ocf_SCRIPTS	     += azure-events
2de1a8
 endif
2de1a8
 
2de1a8
+if BUILD_AZURE_EVENTS_AZ
2de1a8
+ocf_SCRIPTS	     += azure-events-az
2de1a8
+endif
2de1a8
+
2de1a8
 if BUILD_GCP_PD_MOVE
2de1a8
 ocf_SCRIPTS	     += gcp-pd-move
2de1a8
 endif
2de1a8
diff --git a/heartbeat/azure-events-az.in b/heartbeat/azure-events-az.in
2de1a8
new file mode 100644
2de1a8
index 000000000..616fc8d9e
2de1a8
--- /dev/null
2de1a8
+++ b/heartbeat/azure-events-az.in
2de1a8
@@ -0,0 +1,782 @@
2de1a8
+#!@PYTHON@ -tt
2de1a8
+#
2de1a8
+#	Resource agent for monitoring Azure Scheduled Events
2de1a8
+#
2de1a8
+# 	License:	GNU General Public License (GPL)
2de1a8
+#	(c) 2018 	Tobias Niekamp, Microsoft Corp.
2de1a8
+#				and Linux-HA contributors
2de1a8
+
2de1a8
+import os
2de1a8
+import sys
2de1a8
+import time
2de1a8
+import subprocess
2de1a8
+import json
2de1a8
+try:
2de1a8
+		import urllib2
2de1a8
+		from urllib2 import URLError
2de1a8
+except ImportError:
2de1a8
+		import urllib.request as urllib2
2de1a8
+		from urllib.error import URLError
2de1a8
+import socket
2de1a8
+from collections import defaultdict
2de1a8
+
2de1a8
+OCF_FUNCTIONS_DIR = os.environ.get("OCF_FUNCTIONS_DIR", "%s/lib/heartbeat" % os.environ.get("OCF_ROOT"))
2de1a8
+sys.path.append(OCF_FUNCTIONS_DIR)
2de1a8
+import ocf
2de1a8
+
2de1a8
+##############################################################################
2de1a8
+
2de1a8
+
2de1a8
+VERSION = "0.10"
2de1a8
+USER_AGENT = "Pacemaker-ResourceAgent/%s %s" % (VERSION, ocf.distro())
2de1a8
+
2de1a8
+attr_globalPullState = "azure-events-az_globalPullState"
2de1a8
+attr_lastDocVersion  = "azure-events-az_lastDocVersion"
2de1a8
+attr_curNodeState = "azure-events-az_curNodeState"
2de1a8
+attr_pendingEventIDs = "azure-events-az_pendingEventIDs"
2de1a8
+attr_healthstate = "#health-azure"
2de1a8
+
2de1a8
+default_loglevel = ocf.logging.INFO
2de1a8
+default_relevantEventTypes = set(["Reboot", "Redeploy"])
2de1a8
+
2de1a8
+global_pullMaxAttempts = 3
2de1a8
+global_pullDelaySecs = 1
2de1a8
+
2de1a8
+##############################################################################
2de1a8
+
2de1a8
+class attrDict(defaultdict):
2de1a8
+	"""
2de1a8
+	A wrapper for accessing dict keys like an attribute
2de1a8
+	"""
2de1a8
+	def __init__(self, data):
2de1a8
+		super(attrDict, self).__init__(attrDict)
2de1a8
+		for d in data.keys():
2de1a8
+			self.__setattr__(d, data[d])
2de1a8
+
2de1a8
+	def __getattr__(self, key):
2de1a8
+		try:
2de1a8
+			return self[key]
2de1a8
+		except KeyError:
2de1a8
+			raise AttributeError(key)
2de1a8
+
2de1a8
+	def __setattr__(self, key, value):
2de1a8
+		self[key] = value
2de1a8
+
2de1a8
+##############################################################################
2de1a8
+
2de1a8
+class azHelper:
2de1a8
+	"""
2de1a8
+	Helper class for Azure's metadata API (including Scheduled Events)
2de1a8
+	"""
2de1a8
+	metadata_host = "http://169.254.169.254/metadata"
2de1a8
+	instance_api  = "instance"
2de1a8
+	events_api    = "scheduledevents"
2de1a8
+	api_version   = "2019-08-01"
2de1a8
+
2de1a8
+	@staticmethod
2de1a8
+	def _sendMetadataRequest(endpoint, postData=None):
2de1a8
+		"""
2de1a8
+		Send a request to Azure's Azure Metadata Service API
2de1a8
+		"""
2de1a8
+		url = "%s/%s?api-version=%s" % (azHelper.metadata_host, endpoint, azHelper.api_version)
2de1a8
+		data = ""
2de1a8
+		ocf.logger.debug("_sendMetadataRequest: begin; endpoint = %s, postData = %s" % (endpoint, postData))
2de1a8
+		ocf.logger.debug("_sendMetadataRequest: url = %s" % url)
2de1a8
+
2de1a8
+		if postData and type(postData) != bytes:
2de1a8
+			postData = postData.encode()
2de1a8
+
2de1a8
+		req = urllib2.Request(url, postData)
2de1a8
+		req.add_header("Metadata", "true")
2de1a8
+		req.add_header("User-Agent", USER_AGENT)
2de1a8
+		try:
2de1a8
+			resp = urllib2.urlopen(req)
2de1a8
+		except URLError as e:
2de1a8
+			if hasattr(e, 'reason'):
2de1a8
+				ocf.logger.warning("Failed to reach the server: %s" % e.reason)
2de1a8
+				clusterHelper.setAttr(attr_globalPullState, "IDLE")
2de1a8
+			elif hasattr(e, 'code'):
2de1a8
+				ocf.logger.warning("The server couldn\'t fulfill the request. Error code: %s" % e.code)
2de1a8
+				clusterHelper.setAttr(attr_globalPullState, "IDLE")
2de1a8
+		else:
2de1a8
+			data = resp.read()
2de1a8
+			ocf.logger.debug("_sendMetadataRequest: response = %s" % data)
2de1a8
+
2de1a8
+		if data:
2de1a8
+			data = json.loads(data)
2de1a8
+
2de1a8
+		ocf.logger.debug("_sendMetadataRequest: finished")
2de1a8
+		return data
2de1a8
+
2de1a8
+	@staticmethod
2de1a8
+	def getInstanceInfo():
2de1a8
+		"""
2de1a8
+		Fetch details about the current VM from Azure's Azure Metadata Service API
2de1a8
+		"""
2de1a8
+		ocf.logger.debug("getInstanceInfo: begin")
2de1a8
+
2de1a8
+		jsondata = azHelper._sendMetadataRequest(azHelper.instance_api)
2de1a8
+		ocf.logger.debug("getInstanceInfo: json = %s" % jsondata)
2de1a8
+
2de1a8
+		if jsondata:
2de1a8
+			ocf.logger.debug("getInstanceInfo: finished, returning {}".format(jsondata["compute"]))
2de1a8
+			return attrDict(jsondata["compute"])
2de1a8
+		else:
2de1a8
+			ocf.ocf_exit_reason("getInstanceInfo: Unable to get instance info")
2de1a8
+			sys.exit(ocf.OCF_ERR_GENERIC)
2de1a8
+
2de1a8
+	@staticmethod
2de1a8
+	def pullScheduledEvents():
2de1a8
+		"""
2de1a8
+		Retrieve all currently scheduled events via Azure Metadata Service API
2de1a8
+		"""
2de1a8
+		ocf.logger.debug("pullScheduledEvents: begin")
2de1a8
+
2de1a8
+		jsondata = azHelper._sendMetadataRequest(azHelper.events_api)
2de1a8
+		ocf.logger.debug("pullScheduledEvents: json = %s" % jsondata)
2de1a8
+
2de1a8
+		ocf.logger.debug("pullScheduledEvents: finished")
2de1a8
+		return attrDict(jsondata)
2de1a8
+
2de1a8
+	@staticmethod
2de1a8
+	def forceEvents(eventIDs):
2de1a8
+		"""
2de1a8
+		Force a set of events to start immediately
2de1a8
+		"""
2de1a8
+		ocf.logger.debug("forceEvents: begin")
2de1a8
+
2de1a8
+		events = []
2de1a8
+		for e in eventIDs:
2de1a8
+			events.append({
2de1a8
+				"EventId": e,
2de1a8
+			})
2de1a8
+		postData = {
2de1a8
+			"StartRequests" : events
2de1a8
+		}
2de1a8
+		ocf.logger.info("forceEvents: postData = %s" % postData)
2de1a8
+		resp = azHelper._sendMetadataRequest(azHelper.events_api, postData=json.dumps(postData))
2de1a8
+
2de1a8
+		ocf.logger.debug("forceEvents: finished")
2de1a8
+		return
2de1a8
+
2de1a8
+##############################################################################
2de1a8
+
2de1a8
+class clusterHelper:
2de1a8
+	"""
2de1a8
+	Helper functions for Pacemaker control via crm
2de1a8
+	"""
2de1a8
+	@staticmethod
2de1a8
+	def _getLocation(node):
2de1a8
+		"""
2de1a8
+		Helper function to retrieve local/global attributes
2de1a8
+		"""
2de1a8
+		if node:
2de1a8
+			return ["--node", node]
2de1a8
+		else:
2de1a8
+			return ["--type", "crm_config"]
2de1a8
+
2de1a8
+	@staticmethod
2de1a8
+	def _exec(command, *args):
2de1a8
+		"""
2de1a8
+		Helper function to execute a UNIX command
2de1a8
+		"""
2de1a8
+		args = list(args)
2de1a8
+		ocf.logger.debug("_exec: begin; command = %s, args = %s" % (command, str(args)))
2de1a8
+
2de1a8
+		def flatten(*n):
2de1a8
+			return (str(e) for a in n
2de1a8
+				for e in (flatten(*a) if isinstance(a, (tuple, list)) else (str(a),)))
2de1a8
+		command = list(flatten([command] + args))
2de1a8
+		ocf.logger.debug("_exec: cmd = %s" % " ".join(command))
2de1a8
+		try:
2de1a8
+			ret = subprocess.check_output(command)
2de1a8
+			if type(ret) != str:
2de1a8
+				ret = ret.decode()
2de1a8
+			ocf.logger.debug("_exec: return = %s" % ret)
2de1a8
+			return ret.rstrip()
2de1a8
+		except Exception as err:
2de1a8
+			ocf.logger.exception(err)
2de1a8
+			return None
2de1a8
+
2de1a8
+	@staticmethod
2de1a8
+	def setAttr(key, value, node=None):
2de1a8
+		"""
2de1a8
+		Set the value of a specific global/local attribute in the Pacemaker cluster
2de1a8
+		"""
2de1a8
+		ocf.logger.debug("setAttr: begin; key = %s, value = %s, node = %s" % (key, value, node))
2de1a8
+
2de1a8
+		if value:
2de1a8
+			ret = clusterHelper._exec("crm_attribute",
2de1a8
+									  "--name", key,
2de1a8
+									  "--update", value,
2de1a8
+									  clusterHelper._getLocation(node))
2de1a8
+		else:
2de1a8
+			ret = clusterHelper._exec("crm_attribute",
2de1a8
+									  "--name", key,
2de1a8
+									  "--delete",
2de1a8
+									  clusterHelper._getLocation(node))
2de1a8
+
2de1a8
+		ocf.logger.debug("setAttr: finished")
2de1a8
+		return len(ret) == 0
2de1a8
+
2de1a8
+	@staticmethod
2de1a8
+	def getAttr(key, node=None):
2de1a8
+		"""
2de1a8
+		Retrieve a global/local attribute from the Pacemaker cluster
2de1a8
+		"""
2de1a8
+		ocf.logger.debug("getAttr: begin; key = %s, node = %s" % (key, node))
2de1a8
+
2de1a8
+		val = clusterHelper._exec("crm_attribute",
2de1a8
+								  "--name", key,
2de1a8
+								  "--query", "--quiet",
2de1a8
+								  "--default", "",
2de1a8
+								  clusterHelper._getLocation(node))
2de1a8
+		ocf.logger.debug("getAttr: finished")
2de1a8
+		if not val:
2de1a8
+			return None
2de1a8
+		return val if not val.isdigit() else int(val)
2de1a8
+
2de1a8
+	@staticmethod
2de1a8
+	def getAllNodes():
2de1a8
+		"""
2de1a8
+		Get a list of hostnames for all nodes in the Pacemaker cluster
2de1a8
+		"""
2de1a8
+		ocf.logger.debug("getAllNodes: begin")
2de1a8
+
2de1a8
+		nodes = []
2de1a8
+		nodeList = clusterHelper._exec("crm_node", "--list")
2de1a8
+		for n in nodeList.split("\n"):
2de1a8
+			nodes.append(n.split()[1])
2de1a8
+		ocf.logger.debug("getAllNodes: finished; return %s" % str(nodes))
2de1a8
+
2de1a8
+		return nodes
2de1a8
+
2de1a8
+	@staticmethod
2de1a8
+	def getHostNameFromAzName(azName):
2de1a8
+		"""
2de1a8
+		Helper function to get the actual host name from an Azure node name
2de1a8
+		"""
2de1a8
+		return clusterHelper.getAttr("hostName_%s" % azName)
2de1a8
+
2de1a8
+	@staticmethod
2de1a8
+	def removeHoldFromNodes():
2de1a8
+		"""
2de1a8
+		Remove the ON_HOLD state from all nodes in the Pacemaker cluster
2de1a8
+		"""
2de1a8
+		ocf.logger.debug("removeHoldFromNodes: begin")
2de1a8
+
2de1a8
+		for n in clusterHelper.getAllNodes():
2de1a8
+			if clusterHelper.getAttr(attr_curNodeState, node=n) == "ON_HOLD":
2de1a8
+				clusterHelper.setAttr(attr_curNodeState, "AVAILABLE", node=n)
2de1a8
+				ocf.logger.info("removeHoldFromNodes: removed ON_HOLD from node %s" % n)
2de1a8
+
2de1a8
+		ocf.logger.debug("removeHoldFromNodes: finished")
2de1a8
+		return False
2de1a8
+
2de1a8
+	@staticmethod
2de1a8
+	def otherNodesAvailable(exceptNode):
2de1a8
+		"""
2de1a8
+		Check if there are any nodes (except a given node) in the Pacemaker cluster that have state AVAILABLE
2de1a8
+		"""
2de1a8
+		ocf.logger.debug("otherNodesAvailable: begin; exceptNode = %s" % exceptNode)
2de1a8
+
2de1a8
+		for n in clusterHelper.getAllNodes():
2de1a8
+			state = clusterHelper.getAttr(attr_curNodeState, node=n)
2de1a8
+			state = stringToNodeState(state) if state else AVAILABLE
2de1a8
+			if state == AVAILABLE and n != exceptNode.hostName:
2de1a8
+				ocf.logger.info("otherNodesAvailable: at least %s is available" % n)
2de1a8
+				ocf.logger.debug("otherNodesAvailable: finished")
2de1a8
+				return True
2de1a8
+		ocf.logger.info("otherNodesAvailable: no other nodes are available")
2de1a8
+		ocf.logger.debug("otherNodesAvailable: finished")
2de1a8
+
2de1a8
+		return False
2de1a8
+
2de1a8
+	@staticmethod
2de1a8
+	def transitionSummary():
2de1a8
+		"""
2de1a8
+		Get the current Pacemaker transition summary (used to check if all resources are stopped when putting a node standby)
2de1a8
+		"""
2de1a8
+		# <tniek> Is a global crm_simulate "too much"? Or would it be sufficient it there are no planned transitions for a particular node?
2de1a8
+		# # crm_simulate -Ls
2de1a8
+		# 	Transition Summary:
2de1a8
+		# 	 * Promote rsc_SAPHana_HN1_HDB03:0      (Slave -> Master hsr3-db1)
2de1a8
+		# 	 * Stop    rsc_SAPHana_HN1_HDB03:1      (hsr3-db0)
2de1a8
+		# 	 * Move    rsc_ip_HN1_HDB03     (Started hsr3-db0 -> hsr3-db1)
2de1a8
+		# 	 * Start   rsc_nc_HN1_HDB03     (hsr3-db1)
2de1a8
+		# # Excepted result when there are no pending actions:
2de1a8
+		# 	Transition Summary:
2de1a8
+		ocf.logger.debug("transitionSummary: begin")
2de1a8
+
2de1a8
+		summary = clusterHelper._exec("crm_simulate", "-Ls")
2de1a8
+		if not summary:
2de1a8
+			ocf.logger.warning("transitionSummary: could not load transition summary")
2de1a8
+			return False
2de1a8
+		if summary.find("Transition Summary:") < 0:
2de1a8
+			ocf.logger.warning("transitionSummary: received unexpected transition summary: %s" % summary)
2de1a8
+			return False
2de1a8
+		summary = summary.split("Transition Summary:")[1]
2de1a8
+		ret = summary.split("\n").pop(0)
2de1a8
+
2de1a8
+		ocf.logger.debug("transitionSummary: finished; return = %s" % str(ret))
2de1a8
+		return ret
2de1a8
+
2de1a8
+	@staticmethod
2de1a8
+	def listOperationsOnNode(node):
2de1a8
+		"""
2de1a8
+		Get a list of all current operations for a given node (used to check if any resources are pending)
2de1a8
+		"""
2de1a8
+		# hsr3-db1:/home/tniek # crm_resource --list-operations -N hsr3-db0
2de1a8
+		# rsc_azure-events-az    (ocf::heartbeat:azure-events-az):      Started: rsc_azure-events-az_start_0 (node=hsr3-db0, call=91, rc=0, last-rc-change=Fri Jun  8 22:37:46 2018, exec=115ms): complete
2de1a8
+		# rsc_azure-events-az    (ocf::heartbeat:azure-events-az):      Started: rsc_azure-events-az_monitor_10000 (node=hsr3-db0, call=93, rc=0, last-rc-change=Fri Jun  8 22:37:47 2018, exec=197ms): complete
2de1a8
+		# 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
2de1a8
+		# 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
2de1a8
+		ocf.logger.debug("listOperationsOnNode: begin; node = %s" % node)
2de1a8
+
2de1a8
+		resources = clusterHelper._exec("crm_resource", "--list-operations", "-N", node)
2de1a8
+		if len(resources) == 0:
2de1a8
+			ret = []
2de1a8
+		else:
2de1a8
+			ret = resources.split("\n")
2de1a8
+
2de1a8
+		ocf.logger.debug("listOperationsOnNode: finished; return = %s" % str(ret))
2de1a8
+		return ret
2de1a8
+
2de1a8
+	@staticmethod
2de1a8
+	def noPendingResourcesOnNode(node):
2de1a8
+		"""
2de1a8
+		Check that there are no pending resources on a given node
2de1a8
+		"""
2de1a8
+		ocf.logger.debug("noPendingResourcesOnNode: begin; node = %s" % node)
2de1a8
+
2de1a8
+		for r in clusterHelper.listOperationsOnNode(node):
2de1a8
+			ocf.logger.debug("noPendingResourcesOnNode: * %s" % r)
2de1a8
+			resource = r.split()[-1]
2de1a8
+			if resource == "pending":
2de1a8
+				ocf.logger.info("noPendingResourcesOnNode: found resource %s that is still pending" % resource)
2de1a8
+				ocf.logger.debug("noPendingResourcesOnNode: finished; return = False")
2de1a8
+				return False
2de1a8
+		ocf.logger.info("noPendingResourcesOnNode: no pending resources on node %s" % node)
2de1a8
+		ocf.logger.debug("noPendingResourcesOnNode: finished; return = True")
2de1a8
+
2de1a8
+		return True
2de1a8
+
2de1a8
+	@staticmethod
2de1a8
+	def allResourcesStoppedOnNode(node):
2de1a8
+		"""
2de1a8
+		Check that all resources on a given node are stopped
2de1a8
+		"""
2de1a8
+		ocf.logger.debug("allResourcesStoppedOnNode: begin; node = %s" % node)
2de1a8
+
2de1a8
+		if clusterHelper.noPendingResourcesOnNode(node):
2de1a8
+			if len(clusterHelper.transitionSummary()) == 0:
2de1a8
+				ocf.logger.info("allResourcesStoppedOnNode: no pending resources on node %s and empty transition summary" % node)
2de1a8
+				ocf.logger.debug("allResourcesStoppedOnNode: finished; return = True")
2de1a8
+				return True
2de1a8
+			ocf.logger.info("allResourcesStoppedOnNode: transition summary is not empty")
2de1a8
+			ocf.logger.debug("allResourcesStoppedOnNode: finished; return = False")
2de1a8
+			return False
2de1a8
+
2de1a8
+		ocf.logger.info("allResourcesStoppedOnNode: still pending resources on node %s" % node)
2de1a8
+		ocf.logger.debug("allResourcesStoppedOnNode: finished; return = False")
2de1a8
+		return False
2de1a8
+
2de1a8
+##############################################################################
2de1a8
+
2de1a8
+AVAILABLE = 0	# Node is online and ready to handle events
2de1a8
+STOPPING = 1	# Standby has been triggered, but some resources are still running
2de1a8
+IN_EVENT = 2	# All resources are stopped, and event has been initiated via Azure Metadata Service
2de1a8
+ON_HOLD = 3		# Node has a pending event that cannot be started there are no other nodes available
2de1a8
+
2de1a8
+def stringToNodeState(name):
2de1a8
+	if type(name) == int: return name
2de1a8
+	if name == "STOPPING": return STOPPING
2de1a8
+	if name == "IN_EVENT": return IN_EVENT
2de1a8
+	if name == "ON_HOLD": return ON_HOLD
2de1a8
+	return AVAILABLE
2de1a8
+
2de1a8
+def nodeStateToString(state):
2de1a8
+	if state == STOPPING: return "STOPPING"
2de1a8
+	if state == IN_EVENT: return "IN_EVENT"
2de1a8
+	if state == ON_HOLD: return "ON_HOLD"
2de1a8
+	return "AVAILABLE"
2de1a8
+
2de1a8
+##############################################################################
2de1a8
+
2de1a8
+class Node:
2de1a8
+	"""
2de1a8
+	Core class implementing logic for a cluster node
2de1a8
+	"""
2de1a8
+	def __init__(self, ra):
2de1a8
+		self.raOwner  = ra
2de1a8
+		self.azInfo   = azHelper.getInstanceInfo()
2de1a8
+		self.azName   = self.azInfo.name
2de1a8
+		self.hostName = socket.gethostname()
2de1a8
+		self.setAttr("azName", self.azName)
2de1a8
+		clusterHelper.setAttr("hostName_%s" % self.azName, self.hostName)
2de1a8
+
2de1a8
+	def getAttr(self, key):
2de1a8
+		"""
2de1a8
+		Get a local attribute
2de1a8
+		"""
2de1a8
+		return clusterHelper.getAttr(key, node=self.hostName)
2de1a8
+
2de1a8
+	def setAttr(self, key, value):
2de1a8
+		"""
2de1a8
+		Set a local attribute
2de1a8
+		"""
2de1a8
+		return clusterHelper.setAttr(key, value, node=self.hostName)
2de1a8
+
2de1a8
+	def selfOrOtherNode(self, node):
2de1a8
+		"""
2de1a8
+		Helper function to distinguish self/other node
2de1a8
+		"""
2de1a8
+		return node if node else self.hostName
2de1a8
+
2de1a8
+	def setState(self, state, node=None):
2de1a8
+		"""
2de1a8
+		Set the state for a given node (or self)
2de1a8
+		"""
2de1a8
+		node = self.selfOrOtherNode(node)
2de1a8
+		ocf.logger.debug("setState: begin; node = %s, state = %s" % (node, nodeStateToString(state)))
2de1a8
+
2de1a8
+		clusterHelper.setAttr(attr_curNodeState, nodeStateToString(state), node=node)
2de1a8
+
2de1a8
+		ocf.logger.debug("setState: finished")
2de1a8
+
2de1a8
+	def getState(self, node=None):
2de1a8
+		"""
2de1a8
+		Get the state for a given node (or self)
2de1a8
+		"""
2de1a8
+		node = self.selfOrOtherNode(node)
2de1a8
+		ocf.logger.debug("getState: begin; node = %s" % node)
2de1a8
+
2de1a8
+		state = clusterHelper.getAttr(attr_curNodeState, node=node)
2de1a8
+		ocf.logger.debug("getState: state = %s" % state)
2de1a8
+		ocf.logger.debug("getState: finished")
2de1a8
+		if not state:
2de1a8
+			return AVAILABLE
2de1a8
+		return stringToNodeState(state)
2de1a8
+
2de1a8
+	def setEventIDs(self, eventIDs, node=None):
2de1a8
+		"""
2de1a8
+		Set pending EventIDs for a given node (or self)
2de1a8
+		"""
2de1a8
+		node = self.selfOrOtherNode(node)
2de1a8
+		ocf.logger.debug("setEventIDs: begin; node = %s, eventIDs = %s" % (node, str(eventIDs)))
2de1a8
+
2de1a8
+		if eventIDs:
2de1a8
+			eventIDStr = ",".join(eventIDs)
2de1a8
+		else:
2de1a8
+			eventIDStr = None
2de1a8
+		clusterHelper.setAttr(attr_pendingEventIDs, eventIDStr, node=node)
2de1a8
+
2de1a8
+		ocf.logger.debug("setEventIDs: finished")
2de1a8
+		return
2de1a8
+
2de1a8
+	def getEventIDs(self, node=None):
2de1a8
+		"""
2de1a8
+		Get pending EventIDs for a given node (or self)
2de1a8
+		"""
2de1a8
+		node = self.selfOrOtherNode(node)
2de1a8
+		ocf.logger.debug("getEventIDs: begin; node = %s" % node)
2de1a8
+
2de1a8
+		eventIDStr = clusterHelper.getAttr(attr_pendingEventIDs, node=node)
2de1a8
+		if eventIDStr:
2de1a8
+			eventIDs = eventIDStr.split(",")
2de1a8
+		else:
2de1a8
+			eventIDs = None
2de1a8
+
2de1a8
+		ocf.logger.debug("getEventIDs: finished; eventIDs = %s" % str(eventIDs))
2de1a8
+		return eventIDs
2de1a8
+
2de1a8
+	def updateNodeStateAndEvents(self, state, eventIDs, node=None):
2de1a8
+		"""
2de1a8
+		Set the state and pending EventIDs for a given node (or self)
2de1a8
+		"""
2de1a8
+		ocf.logger.debug("updateNodeStateAndEvents: begin; node = %s, state = %s, eventIDs = %s" % (node, nodeStateToString(state), str(eventIDs)))
2de1a8
+
2de1a8
+		self.setState(state, node=node)
2de1a8
+		self.setEventIDs(eventIDs, node=node)
2de1a8
+
2de1a8
+		ocf.logger.debug("updateNodeStateAndEvents: finished")
2de1a8
+		return state
2de1a8
+
2de1a8
+	def putNodeStandby(self, node=None):
2de1a8
+		"""
2de1a8
+		Put self to standby
2de1a8
+		"""
2de1a8
+		node = self.selfOrOtherNode(node)
2de1a8
+		ocf.logger.debug("putNodeStandby: begin; node = %s" % node)
2de1a8
+
2de1a8
+		clusterHelper._exec("crm_attribute",
2de1a8
+							"--node", node,
2de1a8
+							"--name", attr_healthstate,
2de1a8
+							"--update", "-1000000",
2de1a8
+							"--lifetime=forever")
2de1a8
+
2de1a8
+		ocf.logger.debug("putNodeStandby: finished")
2de1a8
+
2de1a8
+	def isNodeInStandby(self, node=None):
2de1a8
+		"""
2de1a8
+		check if node is in standby
2de1a8
+		"""
2de1a8
+		node = self.selfOrOtherNode(node)
2de1a8
+		ocf.logger.debug("isNodeInStandby: begin; node = %s" % node)
2de1a8
+		isInStandy = False
2de1a8
+
2de1a8
+		healthAttributeStr = clusterHelper.getAttr(attr_healthstate, node)
2de1a8
+		if healthAttributeStr is not None:
2de1a8
+			try:
2de1a8
+				healthAttribute = int(healthAttributeStr)
2de1a8
+				isInStandy = healthAttribute < 0
2de1a8
+			except ValueError:
2de1a8
+				# Handle the exception
2de1a8
+				ocf.logger.warn("Health attribute %s on node %s cannot be converted to an integer value" % (healthAttributeStr, node))
2de1a8
+		
2de1a8
+		ocf.logger.debug("isNodeInStandby: finished - result %s" % isInStandy)
2de1a8
+		return isInStandy
2de1a8
+
2de1a8
+	def putNodeOnline(self, node=None):
2de1a8
+		"""
2de1a8
+		Put self back online
2de1a8
+		"""
2de1a8
+		node = self.selfOrOtherNode(node)
2de1a8
+		ocf.logger.debug("putNodeOnline: begin; node = %s" % node)
2de1a8
+
2de1a8
+		clusterHelper._exec("crm_attribute",
2de1a8
+							"--node", node,
2de1a8
+							"--name", "#health-azure",
2de1a8
+							"--update", "0",
2de1a8
+							"--lifetime=forever")
2de1a8
+
2de1a8
+		ocf.logger.debug("putNodeOnline: finished")
2de1a8
+
2de1a8
+	def separateEvents(self, events):
2de1a8
+		"""
2de1a8
+		Split own/other nodes' events
2de1a8
+		"""
2de1a8
+		ocf.logger.debug("separateEvents: begin; events = %s" % str(events))
2de1a8
+
2de1a8
+		localEvents = []
2de1a8
+		remoteEvents = []
2de1a8
+		for e in events:
2de1a8
+			e = attrDict(e)
2de1a8
+			if e.EventType not in self.raOwner.relevantEventTypes:
2de1a8
+				continue
2de1a8
+			if self.azName in e.Resources:
2de1a8
+				localEvents.append(e)
2de1a8
+			else:
2de1a8
+				remoteEvents.append(e)
2de1a8
+		ocf.logger.debug("separateEvents: finished; localEvents = %s, remoteEvents = %s" % (str(localEvents), str(remoteEvents)))
2de1a8
+		return (localEvents, remoteEvents)
2de1a8
+
2de1a8
+##############################################################################
2de1a8
+
2de1a8
+class raAzEvents:
2de1a8
+	"""
2de1a8
+	Main class for resource agent
2de1a8
+	"""
2de1a8
+	def __init__(self, relevantEventTypes):
2de1a8
+		self.node = Node(self)
2de1a8
+		self.relevantEventTypes = relevantEventTypes
2de1a8
+
2de1a8
+	def monitor(self):
2de1a8
+		ocf.logger.debug("monitor: begin")
2de1a8
+		
2de1a8
+		events = azHelper.pullScheduledEvents()
2de1a8
+
2de1a8
+		# get current document version
2de1a8
+		curDocVersion  = events.DocumentIncarnation
2de1a8
+		lastDocVersion = self.node.getAttr(attr_lastDocVersion)
2de1a8
+		ocf.logger.debug("monitor: lastDocVersion = %s; curDocVersion = %s" % (lastDocVersion, curDocVersion))
2de1a8
+
2de1a8
+		# split events local/remote
2de1a8
+		(localEvents, remoteEvents) = self.node.separateEvents(events.Events)
2de1a8
+
2de1a8
+		# ensure local events are only executing once
2de1a8
+		if curDocVersion == lastDocVersion:
2de1a8
+			ocf.logger.info("monitor: already handled curDocVersion, skip")
2de1a8
+			return ocf.OCF_SUCCESS
2de1a8
+
2de1a8
+		localAzEventIDs = set()
2de1a8
+		for e in localEvents:
2de1a8
+			localAzEventIDs.add(e.EventId)
2de1a8
+
2de1a8
+		curState = self.node.getState()
2de1a8
+		clusterEventIDs = self.node.getEventIDs()
2de1a8
+
2de1a8
+		ocf.logger.debug("monitor: curDocVersion has not been handled yet")
2de1a8
+		
2de1a8
+		if clusterEventIDs:
2de1a8
+			# there are pending events set, so our state must be STOPPING or IN_EVENT
2de1a8
+			i = 0; touchedEventIDs = False
2de1a8
+			while i < len(clusterEventIDs):
2de1a8
+				# clean up pending events that are already finished according to AZ
2de1a8
+				if clusterEventIDs[i] not in localAzEventIDs:
2de1a8
+					ocf.logger.info("monitor: remove finished local clusterEvent %s" % (clusterEventIDs[i]))
2de1a8
+					clusterEventIDs.pop(i)
2de1a8
+					touchedEventIDs = True
2de1a8
+				else:
2de1a8
+					i += 1
2de1a8
+			if len(clusterEventIDs) > 0:
2de1a8
+				# there are still pending events (either because we're still stopping, or because the event is still in place)
2de1a8
+				# either way, we need to wait
2de1a8
+				if touchedEventIDs:
2de1a8
+					ocf.logger.info("monitor: added new local clusterEvent %s" % str(clusterEventIDs))
2de1a8
+					self.node.setEventIDs(clusterEventIDs)
2de1a8
+				else:
2de1a8
+					ocf.logger.info("monitor: no local clusterEvents were updated")
2de1a8
+			else:
2de1a8
+				# there are no more pending events left after cleanup
2de1a8
+				if clusterHelper.noPendingResourcesOnNode(self.node.hostName):
2de1a8
+					# and no pending resources on the node -> set it back online
2de1a8
+					ocf.logger.info("monitor: all local events finished -> clean up, put node online and AVAILABLE")
2de1a8
+					curState = self.node.updateNodeStateAndEvents(AVAILABLE, None)
2de1a8
+					self.node.putNodeOnline()
2de1a8
+					clusterHelper.removeHoldFromNodes()
2de1a8
+					# If Azure Scheduled Events are not used for 24 hours (e.g. because the cluster was asleep), it will be disabled for a VM.
2de1a8
+					# When the cluster wakes up and starts using it again, the DocumentIncarnation is reset.
2de1a8
+					# We need to remove it during cleanup, otherwise azure-events-az will not process the event after wakeup
2de1a8
+					self.node.setAttr(attr_lastDocVersion, None)
2de1a8
+				else:
2de1a8
+					ocf.logger.info("monitor: all local events finished, but some resources have not completed startup yet -> wait")
2de1a8
+		else:
2de1a8
+			if curState == AVAILABLE:
2de1a8
+				if len(localAzEventIDs) > 0:
2de1a8
+					if clusterHelper.otherNodesAvailable(self.node):
2de1a8
+						ocf.logger.info("monitor: can handle local events %s -> set state STOPPING" % (str(localAzEventIDs)))
2de1a8
+						curState = self.node.updateNodeStateAndEvents(STOPPING, localAzEventIDs)
2de1a8
+					else:
2de1a8
+						ocf.logger.info("monitor: cannot handle azEvents %s (only node available) -> set state ON_HOLD" % str(localAzEventIDs))
2de1a8
+						self.node.setState(ON_HOLD)
2de1a8
+				else:
2de1a8
+					ocf.logger.debug("monitor: no local azEvents to handle")
2de1a8
+
2de1a8
+		if curState == STOPPING:
2de1a8
+			eventIDsForNode = {}
2de1a8
+			if clusterHelper.noPendingResourcesOnNode(self.node.hostName):
2de1a8
+				if not self.node.isNodeInStandby():
2de1a8
+					ocf.logger.info("monitor: all local resources are started properly -> put node standby and exit")
2de1a8
+					self.node.putNodeStandby()
2de1a8
+					return ocf.OCF_SUCCESS
2de1a8
+
2de1a8
+				for e in localEvents:
2de1a8
+					ocf.logger.info("monitor: handling remote event %s (%s; nodes = %s)" % (e.EventId, e.EventType, str(e.Resources)))
2de1a8
+					# before we can force an event to start, we need to ensure all nodes involved have stopped their resources
2de1a8
+					if e.EventStatus == "Scheduled":
2de1a8
+						allNodesStopped = True
2de1a8
+						for azName in e.Resources:
2de1a8
+							hostName = clusterHelper.getHostNameFromAzName(azName)
2de1a8
+							state = self.node.getState(node=hostName)
2de1a8
+							if state == STOPPING:
2de1a8
+								# the only way we can continue is when node state is STOPPING, but all resources have been stopped
2de1a8
+								if not clusterHelper.allResourcesStoppedOnNode(hostName):
2de1a8
+									ocf.logger.info("monitor: (at least) node %s has still resources running -> wait" % hostName)
2de1a8
+									allNodesStopped = False
2de1a8
+									break
2de1a8
+							elif state in (AVAILABLE, IN_EVENT, ON_HOLD):
2de1a8
+								ocf.logger.info("monitor: node %s is still %s -> remote event needs to be picked up locally" % (hostName, nodeStateToString(state)))
2de1a8
+								allNodesStopped = False
2de1a8
+								break
2de1a8
+						if allNodesStopped:
2de1a8
+							ocf.logger.info("monitor: nodes %s are stopped -> add remote event %s to force list" % (str(e.Resources), e.EventId))
2de1a8
+							for n in e.Resources:
2de1a8
+								hostName = clusterHelper.getHostNameFromAzName(n)
2de1a8
+								if hostName in eventIDsForNode:
2de1a8
+									eventIDsForNode[hostName].append(e.EventId)
2de1a8
+								else:
2de1a8
+									eventIDsForNode[hostName] = [e.EventId]
2de1a8
+					elif e.EventStatus == "Started":
2de1a8
+						ocf.logger.info("monitor: remote event already started")
2de1a8
+
2de1a8
+				# force the start of all events whose nodes are ready (i.e. have no more resources running)
2de1a8
+				if len(eventIDsForNode.keys()) > 0:
2de1a8
+					eventIDsToForce = set([item for sublist in eventIDsForNode.values() for item in sublist])
2de1a8
+					ocf.logger.info("monitor: set nodes %s to IN_EVENT; force remote events %s" % (str(eventIDsForNode.keys()), str(eventIDsToForce)))
2de1a8
+					for node, eventId in eventIDsForNode.items():
2de1a8
+						self.node.updateNodeStateAndEvents(IN_EVENT, eventId, node=node)
2de1a8
+					azHelper.forceEvents(eventIDsToForce)
2de1a8
+					self.node.setAttr(attr_lastDocVersion, curDocVersion)
2de1a8
+			else:
2de1a8
+				ocf.logger.info("monitor: some local resources are not clean yet -> wait")
2de1a8
+
2de1a8
+		ocf.logger.debug("monitor: finished")
2de1a8
+		return ocf.OCF_SUCCESS
2de1a8
+
2de1a8
+##############################################################################
2de1a8
+
2de1a8
+def setLoglevel(verbose):
2de1a8
+	# set up writing into syslog
2de1a8
+	loglevel = default_loglevel
2de1a8
+	if verbose:
2de1a8
+		opener = urllib2.build_opener(urllib2.HTTPHandler(debuglevel=1))
2de1a8
+		urllib2.install_opener(opener)
2de1a8
+		loglevel = ocf.logging.DEBUG
2de1a8
+	ocf.log.setLevel(loglevel)
2de1a8
+
2de1a8
+description = (
2de1a8
+	"Microsoft Azure Scheduled Events monitoring agent",
2de1a8
+	"""This resource agent implements a monitor for scheduled
2de1a8
+(maintenance) events for a Microsoft Azure VM.
2de1a8
+
2de1a8
+If any relevant events are found, it moves all Pacemaker resources
2de1a8
+away from the affected node to allow for a graceful shutdown.
2de1a8
+
2de1a8
+	Usage:
2de1a8
+		[OCF_RESKEY_eventTypes=VAL] [OCF_RESKEY_verbose=VAL] azure-events-az ACTION
2de1a8
+
2de1a8
+		action (required): Supported values: monitor, help, meta-data
2de1a8
+		eventTypes (optional): List of event types to be considered
2de1a8
+				relevant by the resource agent (comma-separated).
2de1a8
+				Supported values: Freeze,Reboot,Redeploy
2de1a8
+				Default = Reboot,Redeploy
2de1a8
+/		verbose (optional): If set to true, displays debug info.
2de1a8
+				Default = false
2de1a8
+
2de1a8
+	Deployment:
2de1a8
+		crm configure primitive rsc_azure-events-az ocf:heartbeat:azure-events-az \
2de1a8
+			op monitor interval=10s
2de1a8
+		crm configure clone cln_azure-events-az rsc_azure-events-az
2de1a8
+
2de1a8
+For further information on Microsoft Azure Scheduled Events, please
2de1a8
+refer to the following documentation:
2de1a8
+https://docs.microsoft.com/en-us/azure/virtual-machines/linux/scheduled-events
2de1a8
+""")
2de1a8
+
2de1a8
+def monitor_action(eventTypes):
2de1a8
+	relevantEventTypes = set(eventTypes.split(",") if eventTypes else [])
2de1a8
+	ra = raAzEvents(relevantEventTypes)
2de1a8
+	return ra.monitor()
2de1a8
+
2de1a8
+def validate_action(eventTypes):
2de1a8
+	if eventTypes:
2de1a8
+		for event in eventTypes.split(","):
2de1a8
+			if event not in ("Freeze", "Reboot", "Redeploy"):
2de1a8
+				ocf.ocf_exit_reason("Event type not one of Freeze, Reboot, Redeploy: " + eventTypes)
2de1a8
+				return ocf.OCF_ERR_CONFIGURED
2de1a8
+	return ocf.OCF_SUCCESS
2de1a8
+
2de1a8
+def main():
2de1a8
+	agent = ocf.Agent("azure-events-az", shortdesc=description[0], longdesc=description[1])
2de1a8
+	agent.add_parameter(
2de1a8
+		"eventTypes",
2de1a8
+		shortdesc="List of resources to be considered",
2de1a8
+		longdesc="A comma-separated list of event types that will be handled by this resource agent. (Possible values: Freeze,Reboot,Redeploy)",
2de1a8
+		content_type="string",
2de1a8
+		default="Reboot,Redeploy")
2de1a8
+	agent.add_parameter(
2de1a8
+		"verbose",
2de1a8
+		shortdesc="Enable verbose agent logging",
2de1a8
+		longdesc="Set to true to enable verbose logging",
2de1a8
+		content_type="boolean",
2de1a8
+		default="false")
2de1a8
+	agent.add_action("start", timeout=10, handler=lambda: ocf.OCF_SUCCESS)
2de1a8
+	agent.add_action("stop", timeout=10, handler=lambda: ocf.OCF_SUCCESS)
2de1a8
+	agent.add_action("validate-all", timeout=20, handler=validate_action)
2de1a8
+	agent.add_action("monitor", timeout=240, interval=10, handler=monitor_action)
2de1a8
+	setLoglevel(ocf.is_true(ocf.get_parameter("verbose", "false")))
2de1a8
+	agent.run()
2de1a8
+
2de1a8
+if __name__ == '__main__':
2de1a8
+	main()
2de1a8
\ No newline at end of file
2de1a8
2de1a8
From a95337d882c7cc69d604b050159ad50b679f18be Mon Sep 17 00:00:00 2001
2de1a8
From: MSSedusch <sedusch@microsoft.com>
2de1a8
Date: Thu, 2 Jun 2022 14:10:33 +0200
2de1a8
Subject: [PATCH 2/2] Remove developer documentation
2de1a8
2de1a8
---
2de1a8
 heartbeat/azure-events-az.in | 11 -----------
2de1a8
 1 file changed, 11 deletions(-)
2de1a8
2de1a8
diff --git a/heartbeat/azure-events-az.in b/heartbeat/azure-events-az.in
2de1a8
index 616fc8d9e..59d095306 100644
2de1a8
--- a/heartbeat/azure-events-az.in
2de1a8
+++ b/heartbeat/azure-events-az.in
2de1a8
@@ -723,17 +723,6 @@ description = (
2de1a8
 If any relevant events are found, it moves all Pacemaker resources
2de1a8
 away from the affected node to allow for a graceful shutdown.
2de1a8
 
2de1a8
-	Usage:
2de1a8
-		[OCF_RESKEY_eventTypes=VAL] [OCF_RESKEY_verbose=VAL] azure-events-az ACTION
2de1a8
-
2de1a8
-		action (required): Supported values: monitor, help, meta-data
2de1a8
-		eventTypes (optional): List of event types to be considered
2de1a8
-				relevant by the resource agent (comma-separated).
2de1a8
-				Supported values: Freeze,Reboot,Redeploy
2de1a8
-				Default = Reboot,Redeploy
2de1a8
-/		verbose (optional): If set to true, displays debug info.
2de1a8
-				Default = false
2de1a8
-
2de1a8
 	Deployment:
2de1a8
 		crm configure primitive rsc_azure-events-az ocf:heartbeat:azure-events-az \
2de1a8
 			op monitor interval=10s