From 35c6faa1f43fd0751677a94573d5f5a2e76d73be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= <krig@koru.se>
Date: Thu, 4 Oct 2018 17:01:14 +0200
Subject: [PATCH 1/6] dev: ocf.py: Add missing imports to example

---
 doc/writing-python-agents.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/doc/writing-python-agents.md b/doc/writing-python-agents.md
index aeb4acbd6..584b1c997 100644
--- a/doc/writing-python-agents.md
+++ b/doc/writing-python-agents.md
@@ -60,6 +60,9 @@ logger.error("Something went terribly wrong.")
 ## Run loop and metadata example
 
 ``` python
+import os
+import sys
+
 OCF_FUNCTIONS_DIR="%s/lib/heartbeat" % os.environ.get("OCF_ROOT")
 sys.path.append(OCF_FUNCTIONS_DIR)
 import ocf

From 4b5d63a4faf54f91431795fb6c705dba679b0cc7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= <krig@koru.se>
Date: Thu, 4 Oct 2018 17:01:43 +0200
Subject: [PATCH 2/6] dev: ocf.py: Catch and log exceptions in a controlled way

---
 heartbeat/ocf.py | 19 +++++++++++++++----
 1 file changed, 15 insertions(+), 4 deletions(-)

diff --git a/heartbeat/ocf.py b/heartbeat/ocf.py
index 451a41dec..b16b5d4dd 100644
--- a/heartbeat/ocf.py
+++ b/heartbeat/ocf.py
@@ -141,6 +141,8 @@ def emit(self, record):
 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.
@@ -148,10 +150,12 @@ def ocf_exit_reason(msg):
 	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):
@@ -343,10 +347,17 @@ def default_for_parameter(paramname):
 					return meta.default
 			return None
 		arglist = [get_parameter(p, default_for_parameter(p)) for p in params]
-		rc = func(*arglist)
-		if rc is None:
-			rc = OCF_SUCCESS
-		return rc
+		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
 
 	if OCF_ACTION is None:
 		ocf_exit_reason("No action argument set")

From 655ffe758f3b19ec2667eb6720c77d77e07f415f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= <krig@koru.se>
Date: Thu, 4 Oct 2018 17:33:06 +0200
Subject: [PATCH 3/6] dev: ocf.py: Print usage help when passed -h/--help as
 argument

---
 heartbeat/ocf.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/heartbeat/ocf.py b/heartbeat/ocf.py
index b16b5d4dd..ff4c67c6e 100644
--- a/heartbeat/ocf.py
+++ b/heartbeat/ocf.py
@@ -359,6 +359,11 @@ def default_for_parameter(paramname):
 				logger.error(str(err))
 			return OCF_ERR_GENERIC
 
+	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)

From c95b3cdab92dc1ea2924c1ece302f6871f4f63e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= <krig@koru.se>
Date: Thu, 4 Oct 2018 17:43:21 +0200
Subject: [PATCH 4/6] dev: ocf.py: Enable passing handler as argument to
 add_action, and add Metadata.run() helper

---
 heartbeat/ocf.py | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/heartbeat/ocf.py b/heartbeat/ocf.py
index ff4c67c6e..a95fa1c81 100644
--- a/heartbeat/ocf.py
+++ b/heartbeat/ocf.py
@@ -266,6 +266,7 @@ def __init__(self, name, shortdesc, longdesc):
 		self.longdesc = longdesc
 		self.parameters = []
 		self.actions = []
+		self._handlers = {}
 
 	def add_parameter(self, name, shortdesc="", longdesc="", content_type="string", unique=False, required=False, default=None):
 		for param in self.parameters:
@@ -280,11 +281,13 @@ def add_parameter(self, name, shortdesc="", longdesc="", content_type="string",
 										 default=default))
 		return self
 
-	def add_action(self, name, timeout=None, interval=None, depth=None):
+	def add_action(self, name, timeout=None, interval=None, depth=None, handler=None):
 		self.actions.append(Action(name=name,
 								   timeout=timeout,
 								   interval=interval,
 								   depth=depth))
+		if handler is not None:
+			self._handlers[name] = handler
 		return self
 
 	def __str__(self):
@@ -312,8 +315,11 @@ def __str__(self):
 		   parameters="".join(str(p) for p in self.parameters),
 		   actions="".join(str(a) for a in self.actions))
 
+	def run(self):
+		run(self)
 
-def run(metadata, handlers):
+
+def run(metadata, handlers=None):
 	"""
 	Main loop implementation for resource agents.
 	Does not return.
@@ -330,6 +336,9 @@ def run(metadata, handlers):
 	"""
 	import inspect
 
+	metadata._handlers.update(handlers or {})
+	handlers = metadata._handlers
+
 	def check_required_params():
 		for p in metadata.parameters:
 			if p.required and get_parameter(p.name) is None:

From f9f067f670cea824544c61d5471865cc5cf54ecd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= <krig@koru.se>
Date: Thu, 4 Oct 2018 17:59:03 +0200
Subject: [PATCH 5/6] dev: ocf.py: Rename Metadata to Agent

---
 doc/writing-python-agents.md | 29 +++++++++++-------------
 heartbeat/ocf.py             | 43 +++++++++++++++++++++++++-----------
 2 files changed, 43 insertions(+), 29 deletions(-)

diff --git a/doc/writing-python-agents.md b/doc/writing-python-agents.md
index 584b1c997..1b87e753a 100644
--- a/doc/writing-python-agents.md
+++ b/doc/writing-python-agents.md
@@ -54,7 +54,7 @@ logger.error("Something went terribly wrong.")
 * `have_binary`: Returns True if the given binary is available.
 * `is_true`: Converts an OCF truth value to a Python boolean.
 * `get_parameter`: Looks up the matching `OCF_RESKEY_` environment variable.
-* `Metadata`: Class which helps to generate the XML metadata.
+* `Agent`: Class which helps to generate the XML metadata.
 * `run`: OCF run loop implementation.
 
 ## Run loop and metadata example
@@ -72,21 +72,18 @@ def start_action(argument):
 
 
 def main():
-    metadata = ocf.Metadata("example-agent",
-                            shortdesc="This is an example agent",
-                            longdesc="An example of how to " +
-                            "write an agent in Python using the ocf " +
-                            "Python library.")
-    metadata.add_parameter("argument",
-                           shortdesc="Example argument",
-                           longdesc="This argument is just an example.",
-                           content_type="string",
-                           default="foobar")
-    metadata.add_action("start", timeout=60)
-    ocf.run(metadata,
-            handlers={
-                "start": start_action
-            })
+    agent = ocf.Agent("example-agent",
+                      shortdesc="This is an example agent",
+                      longdesc="An example of how to " +
+                      "write an agent in Python using the ocf " +
+                      "Python library.")
+    agent.add_parameter("argument",
+                        shortdesc="Example argument",
+                        longdesc="This argument is just an example.",
+                        content_type="string",
+                        default="foobar")
+    agent.add_action("start", timeout=60, handler=start_action)
+    agent.run()
 
 if __name__ == "__main__":
     main()
diff --git a/heartbeat/ocf.py b/heartbeat/ocf.py
index a95fa1c81..e34066617 100644
--- a/heartbeat/ocf.py
+++ b/heartbeat/ocf.py
@@ -217,6 +217,9 @@ def __init__(self, name, shortdesc, longdesc, content_type, unique, required, de
 		self.default = default
 
 	def __str__(self):
+		return self.to_xml()
+
+	def to_xml(self):
 		ret = '<parameter name="' + self.name + '"'
 		if self.unique:
 			ret += ' unique="1"'
@@ -233,6 +236,7 @@ def __str__(self):
 		return ret
 
 
+
 class Action(object):
 	def __init__(self, name, timeout, interval, depth):
 		self.name = name
@@ -241,6 +245,9 @@ def __init__(self, name, timeout, interval, depth):
 		self.depth = depth
 
 	def __str__(self):
+		return self.to_xml()
+
+	def to_xml(self):
 		def opt(s, name, var):
 			if var is not None:
 				if type(var) == int:
@@ -255,9 +262,16 @@ def opt(s, name, var):
 		return ret
 
 
-class Metadata(object):
+class Agent(object):
 	"""
-	Metadata XML generator helper.
+	OCF Resource Agent metadata XML generator helper.
+
+	Use add_parameter/add_action to define parameters
+	and actions for the agent. Then call run() to
+	start the agent main loop.
+
+	See doc/writing-python-agents.md for an example
+	of how to use it.
 	"""
 
 	def __init__(self, name, shortdesc, longdesc):
@@ -291,6 +305,9 @@ def add_action(self, name, timeout=None, interval=None, depth=None, handler=None
 		return self
 
 	def __str__(self):
+		return self.to_xml()
+
+	def to_xml(self):
 		return """<?xml version="1.0"?>
 <!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
 <resource-agent name="{name}">
@@ -312,21 +329,21 @@ def __str__(self):
 """.format(name=self.name,
 		   longdesc=self.longdesc,
 		   shortdesc=self.shortdesc,
-		   parameters="".join(str(p) for p in self.parameters),
-		   actions="".join(str(a) for a in self.actions))
+		   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(metadata, handlers=None):
+def run(agent, handlers=None):
 	"""
 	Main loop implementation for resource agents.
 	Does not return.
 
 	Arguments:
 
-	metadata: Metadata structure generated by ocf.Metadata
+	agent: Agent object.
 
 	handlers: Dict of action name to handler function.
 
@@ -336,11 +353,11 @@ def run(metadata, handlers=None):
 	"""
 	import inspect
 
-	metadata._handlers.update(handlers or {})
-	handlers = metadata._handlers
+	agent._handlers.update(handlers or {})
+	handlers = agent._handlers
 
 	def check_required_params():
-		for p in metadata.parameters:
+		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)
@@ -351,7 +368,7 @@ def call_handler(func):
 		else:
 			params = inspect.getargspec(func).args
 		def default_for_parameter(paramname):
-			for meta in metadata.parameters:
+			for meta in agent.parameters:
 				if meta.name == paramname:
 					return meta.default
 			return None
@@ -377,7 +394,7 @@ def default_for_parameter(paramname):
 		ocf_exit_reason("No action argument set")
 		sys.exit(OCF_ERR_UNIMPLEMENTED)
 	if OCF_ACTION in ('meta-data', 'usage', 'methods'):
-		sys.stdout.write(str(metadata) + "\n")
+		sys.stdout.write(agent.to_xml() + "\n")
 		sys.exit(OCF_SUCCESS)
 
 	check_required_params()
@@ -392,7 +409,7 @@ def default_for_parameter(paramname):
 
 	class TestMetadata(unittest.TestCase):
 		def test_noparams_noactions(self):
-			m = Metadata("foo", shortdesc="shortdesc", longdesc="longdesc")
+			m = Agent("foo", shortdesc="shortdesc", longdesc="longdesc")
 			self.assertEqual("""<?xml version="1.0"?>
 <!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
 <resource-agent name="foo">
@@ -414,7 +431,7 @@ def test_noparams_noactions(self):
 """, str(m))
 
 		def test_params_actions(self):
-			m = Metadata("foo", shortdesc="shortdesc", longdesc="longdesc")
+			m = Agent("foo", shortdesc="shortdesc", longdesc="longdesc")
 			m.add_parameter("testparam")
 			m.add_action("start")
 			self.assertEqual(str(m.actions[0]), '<action name="start" />\n')

From ee1e4551eeb85af89815ef136c42e944bb50e0dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= <krig@koru.se>
Date: Mon, 8 Oct 2018 14:53:24 +0200
Subject: [PATCH 6/6] dev: ocf.py: Only lookup default if parameter is not set

---
 heartbeat/ocf.py | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/heartbeat/ocf.py b/heartbeat/ocf.py
index e34066617..72ec90820 100644
--- a/heartbeat/ocf.py
+++ b/heartbeat/ocf.py
@@ -367,12 +367,14 @@ def call_handler(func):
 			params = inspect.signature(func).parameters.keys()
 		else:
 			params = inspect.getargspec(func).args
-		def default_for_parameter(paramname):
-			for meta in agent.parameters:
-				if meta.name == paramname:
-					return meta.default
-			return None
-		arglist = [get_parameter(p, default_for_parameter(p)) for p in params]
+		def value_for_parameter(param):
+			val = get_parameter(val)
+			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:
