Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE
resource-agents.10370
0020-1229.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File 0020-1229.patch of Package resource-agents.10370
From 7d128146b3a6a860b6f6fc0d7e58669216a3ae19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= <krig@koru.se> Date: Mon, 1 Oct 2018 15:25:24 +0200 Subject: [PATCH 1/5] Dev: Add document describing how to write an agent in Python --- doc/writing-python-agents.md | 89 ++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 doc/writing-python-agents.md diff --git a/doc/writing-python-agents.md b/doc/writing-python-agents.md new file mode 100644 index 000000000..01c65d1c4 --- /dev/null +++ b/doc/writing-python-agents.md @@ -0,0 +1,89 @@ +# Resource Agent guide for Python + +## Introduction + +A simple library for authoring resource agents in Python is +provided in the `ocf.py` library. + +Agents written in Python should be ideally compatible both with Python +2.7+ and Python 3.3+. + +The library provides various helper constants and functions, a logging +implementation as well as a run loop and metadata generation facility. + +## Constants + +The following OCF constants are provided: + +* `OCF_SUCCESS` +* `OCF_ERR_GENERIC` +* `OCF_ERR_ARGS` +* `OCF_ERR_UNIMPLEMENTED` +* `OCF_ERR_PERM` +* `OCF_ERR_INSTALLED` +* `OCF_ERR_CONFIGURED` +* `OCF_NOT_RUNNING` +* `OCF_RUNNING_MASTER` +* `OCF_FAILED_MASTER` +* `OCF_RESOURCE_INSTANCE` +* `HA_DEBUG` +* `HA_DATEFMT` +* `HA_LOGFACILITY` +* `HA_LOGFILE` +* `HA_DEBUGLOG` + +## Logger + +The `logger` variable holds a Python standard log object with its +formatter set to follow the OCF standard logging format. + +Example: + +``` python + +from ocf import logger + +logger.error("Something went terribly wrong.") + +``` + +## Helper functions + +* `ocf_exit_reason`: Prints the exit error string to stderr. +* `have_binary`: Returns True if the given binary is available. +* `is_true`: Converts an OCF truth value to a Python boolean. +* `parameter`: Looks up the matching `OCF_RESKEY_` environment variable. +* `Metadata`: Class which helps to generate the XML metadata. +* `run`: OCF run loop implementation. + +## Run loop and metadata example + +``` python +OCF_FUNCTIONS_DIR="%s/lib/heartbeat" % os.environ.get("OCF_ROOT") +sys.path.append(OCF_FUNCTIONS_DIR) +import ocf + +def start_action(argument): + print("The start action receives the argument as a parameter: {}".format(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.parameter("argument", + shortdesc="Example argument", + longdesc="This argument is just an example.", + content_type="string", + default="foobar") + metadata.action("start", timeout=60) + ocf.run(metadata, + handlers={ + "start": start_action + }) + +if __name__ == "__main__": + main() +``` From b0bd2c058b322ceea3c78f533740f29f17b1c94b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= <krig@koru.se> Date: Mon, 1 Oct 2018 15:27:58 +0200 Subject: [PATCH 2/5] Dev: ocf.py: Add helper functions, metadata generation, run loop --- heartbeat/ocf.py | 257 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) diff --git a/heartbeat/ocf.py b/heartbeat/ocf.py index 36e7ccccd..e6f1daa92 100644 --- a/heartbeat/ocf.py +++ b/heartbeat/ocf.py @@ -135,3 +135,260 @@ def emit(self, record): log.addHandler(dfh) logger = logging.LoggerAdapter(log, {'OCF_RESOURCE_INSTANCE': OCF_RESOURCE_INSTANCE}) + + +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. + """ + cookie = os.environ.get("OCF_EXIT_REASON_PREFIX", "ocf-exit-reason:") + sys.stderr.write("{}{}\n".format(cookie, msg)) + sys.stderr.flush() + logger.error(msg) + + +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 = os.environ.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 (os.environ.get("__OCF_ACTION", "") == "monitor" and + os.environ.get("OCF_RESKEY_CRM_meta_interval", "") == "0") + + +def parameter(name, default=None): + """ + Extract the parameter value from the environment + """ + return os.environ.get("OCF_RESKEY_{}".format(name), default) + + +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): + ret = '<parameter name="' + self.name + '"' + if self.unique: + ret += ' unique="1"' + if self.required: + ret += ' required="1"' + ret += ">\n" + ret += '<longdesc lang="en">' + self.longdesc + '</longdesc>' + "\n" + ret += '<shortdesc lang="en">' + self.shortdesc + '</shortdesc>' + "\n" + ret += '<content type="' + self.content_type + '"' + if self.default is not None: + ret += ' default="{}"'.format(self.default) + ret += " />\n" + ret += "</parameter>\n" + return ret + + +class Action(object): + def __init__(self, name, timeout, interval, depth): + self.name = name + self.timeout = timeout + self.interval = interval + self.depth = depth + + def __str__(self): + def opt(s, name, var): + if var is not None: + if type(var) == int: + var = "{}s".format(var) + return s + ' {}="{}"'.format(name, var) + return s + ret = '<action name="{}"'.format(self.name) + ret = opt(ret, "timeout", self.timeout) + ret = opt(ret, "interval", self.interval) + ret = opt(ret, "depth", self.depth) + ret += " />\n" + return ret + + +class Metadata(object): + """ + Metadata XML generator helper. + """ + + def __init__(self, name, shortdesc, longdesc): + self.name = name + self.shortdesc = shortdesc + self.longdesc = longdesc + self.parameters = [] + self.actions = [] + + def parameter(self, name, shortdesc="", longdesc="", content_type="string", unique=False, required=False, default=None): + self.parameters.append(Parameter(name=name, + shortdesc=shortdesc, + longdesc=longdesc, + content_type=content_type, + unique=unique, + required=required, + default=default)) + return self + + def action(self, name, timeout=None, interval=None, depth=None): + self.actions.append(Action(name=name, + timeout=timeout, + interval=interval, + depth=depth)) + return self + + def __str__(self): + return """<?xml version="1.0"?> +<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd"> +<resource-agent name="{name}"> +<version>1.0</version> +<longdesc lang="en"> +{longdesc} +</longdesc> +<shortdesc lang="en">{shortdesc}</shortdesc> + +<parameters> +{parameters} +</parameters> + +<actions> +{actions} +</actions> + +</resource-agent> +""".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)) + + +def run(metadata, handlers): + """ + Main loop implementation for resource agents. + Does not return. + + Arguments: + + metadata: Metadata structure generated by ocf.Metadata + + 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 + + def check_required_params(): + for p in metadata.parameters: + if p.required and 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 default_for_parameter(paramname): + for meta in metadata.parameters: + if meta.name == paramname: + return meta.default + return None + arglist = [parameter(p, default_for_parameter(p)) for p in params] + rc = func(*arglist) + if rc is None: + rc = OCF_SUCCESS + return rc + + if len(sys.argv) == 2: + action = sys.argv[1] + else: + action = os.environ.get("__OCF_ACTION") + if action is None: + ocf_exit_reason("No action argument set") + sys.exit(OCF_ERR_UNIMPLEMENTED) + if action in ('meta-data', 'usage', 'methods'): + sys.stdout.write(str(metadata) + "\n") + sys.exit(OCF_SUCCESS) + + check_required_params() + if action in handlers: + rc = call_handler(handlers[action]) + sys.exit(rc) + sys.exit(OCF_ERR_UNIMPLEMENTED) + + +if __name__ == "__main__": + import unittest + + class TestMetadata(unittest.TestCase): + def test_noparams_noactions(self): + m = Metadata("foo", shortdesc="shortdesc", longdesc="longdesc") + self.assertEqual("""<?xml version="1.0"?> +<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd"> +<resource-agent name="foo"> +<version>1.0</version> +<longdesc lang="en"> +longdesc +</longdesc> +<shortdesc lang="en">shortdesc</shortdesc> + +<parameters> + +</parameters> + +<actions> + +</actions> + +</resource-agent> +""", str(m)) + + def test_params_actions(self): + m = Metadata("foo", shortdesc="shortdesc", longdesc="longdesc") + m.parameter("testparam") + m.action("start") + self.assertEqual(str(m.actions[0]), '<action name="start" />\n') + + unittest.main() From 370761418a28aaff91efec7a61b0fbd629703228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= <krig@koru.se> Date: Mon, 1 Oct 2018 17:39:33 +0200 Subject: [PATCH 3/5] Dev: ocf.py: Add OCF_ACTION variable --- doc/writing-python-agents.md | 1 + heartbeat/ocf.py | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/doc/writing-python-agents.md b/doc/writing-python-agents.md index 01c65d1c4..178c3eed2 100644 --- a/doc/writing-python-agents.md +++ b/doc/writing-python-agents.md @@ -31,6 +31,7 @@ The following OCF constants are provided: * `HA_LOGFACILITY` * `HA_LOGFILE` * `HA_DEBUGLOG` +* `OCF_ACTION` -- Set to `$__OCF_ACTION` if set, or to the first command line argument. ## Logger diff --git a/heartbeat/ocf.py b/heartbeat/ocf.py index e6f1daa92..582cef6e8 100644 --- a/heartbeat/ocf.py +++ b/heartbeat/ocf.py @@ -88,6 +88,10 @@ def emit(self, record): 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") @@ -144,7 +148,7 @@ def ocf_exit_reason(msg): Allows the OCF agent to provide a string describing why the exit code was returned. """ - cookie = os.environ.get("OCF_EXIT_REASON_PREFIX", "ocf-exit-reason:") + cookie = env.get("OCF_EXIT_REASON_PREFIX", "ocf-exit-reason:") sys.stderr.write("{}{}\n".format(cookie, msg)) sys.stderr.flush() logger.error(msg) @@ -160,7 +164,7 @@ def _access_check(fn): not os.path.isdir(fn)) if _access_check(name): return True - path = os.environ.get("PATH", os.defpath).split(os.pathsep) + path = env.get("PATH", os.defpath).split(os.pathsep) seen = set() for dir in path: dir = os.path.normcase(dir) @@ -187,15 +191,15 @@ def is_probe(): by Pacemaker to check the status of a possibly not running resource. """ - return (os.environ.get("__OCF_ACTION", "") == "monitor" and - os.environ.get("OCF_RESKEY_CRM_meta_interval", "") == "0") + return (OCF_ACTION == "monitor" and + env.get("OCF_RESKEY_CRM_meta_interval", "") == "0") def parameter(name, default=None): """ Extract the parameter value from the environment """ - return os.environ.get("OCF_RESKEY_{}".format(name), default) + return env.get("OCF_RESKEY_{}".format(name), default) class Parameter(object): @@ -341,20 +345,16 @@ def default_for_parameter(paramname): rc = OCF_SUCCESS return rc - if len(sys.argv) == 2: - action = sys.argv[1] - else: - action = os.environ.get("__OCF_ACTION") - if action is None: + if OCF_ACTION is None: ocf_exit_reason("No action argument set") sys.exit(OCF_ERR_UNIMPLEMENTED) - if action in ('meta-data', 'usage', 'methods'): + if OCF_ACTION in ('meta-data', 'usage', 'methods'): sys.stdout.write(str(metadata) + "\n") sys.exit(OCF_SUCCESS) check_required_params() - if action in handlers: - rc = call_handler(handlers[action]) + if OCF_ACTION in handlers: + rc = call_handler(handlers[OCF_ACTION]) sys.exit(rc) sys.exit(OCF_ERR_UNIMPLEMENTED) From d59b42c0f72809376b67e0797365babdf99df837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= <krig@koru.se> Date: Thu, 4 Oct 2018 14:41:25 +0200 Subject: [PATCH 4/5] dev: ocf.py: add_parameter() / add_action() name change --- doc/writing-python-agents.md | 12 ++++++------ heartbeat/ocf.py | 11 +++++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/doc/writing-python-agents.md b/doc/writing-python-agents.md index 178c3eed2..6c5d5c2ac 100644 --- a/doc/writing-python-agents.md +++ b/doc/writing-python-agents.md @@ -74,12 +74,12 @@ def main(): longdesc="An example of how to " + "write an agent in Python using the ocf " + "Python library.") - metadata.parameter("argument", - shortdesc="Example argument", - longdesc="This argument is just an example.", - content_type="string", - default="foobar") - metadata.action("start", timeout=60) + 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 diff --git a/heartbeat/ocf.py b/heartbeat/ocf.py index 582cef6e8..b640dfc6e 100644 --- a/heartbeat/ocf.py +++ b/heartbeat/ocf.py @@ -263,7 +263,10 @@ def __init__(self, name, shortdesc, longdesc): self.parameters = [] self.actions = [] - def parameter(self, name, shortdesc="", longdesc="", content_type="string", unique=False, required=False, default=None): + def add_parameter(self, name, shortdesc="", longdesc="", content_type="string", unique=False, required=False, default=None): + for param in self.parameters: + if param.name == name: + raise ValueError("Parameter {} defined twice in metadata".format(name)) self.parameters.append(Parameter(name=name, shortdesc=shortdesc, longdesc=longdesc, @@ -273,7 +276,7 @@ def parameter(self, name, shortdesc="", longdesc="", content_type="string", uniq default=default)) return self - def action(self, name, timeout=None, interval=None, depth=None): + def add_action(self, name, timeout=None, interval=None, depth=None): self.actions.append(Action(name=name, timeout=timeout, interval=interval, @@ -387,8 +390,8 @@ def test_noparams_noactions(self): def test_params_actions(self): m = Metadata("foo", shortdesc="shortdesc", longdesc="longdesc") - m.parameter("testparam") - m.action("start") + m.add_parameter("testparam") + m.add_action("start") self.assertEqual(str(m.actions[0]), '<action name="start" />\n') unittest.main() From ab2de7018055cb99d99d6bebf68758bf755b4d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= <krig@koru.se> Date: Thu, 4 Oct 2018 14:43:53 +0200 Subject: [PATCH 5/5] dev: ocf.py: Rename parameter() to get_parameter() --- doc/writing-python-agents.md | 2 +- heartbeat/ocf.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/writing-python-agents.md b/doc/writing-python-agents.md index 6c5d5c2ac..aeb4acbd6 100644 --- a/doc/writing-python-agents.md +++ b/doc/writing-python-agents.md @@ -53,7 +53,7 @@ logger.error("Something went terribly wrong.") * `ocf_exit_reason`: Prints the exit error string to stderr. * `have_binary`: Returns True if the given binary is available. * `is_true`: Converts an OCF truth value to a Python boolean. -* `parameter`: Looks up the matching `OCF_RESKEY_` environment variable. +* `get_parameter`: Looks up the matching `OCF_RESKEY_` environment variable. * `Metadata`: Class which helps to generate the XML metadata. * `run`: OCF run loop implementation. diff --git a/heartbeat/ocf.py b/heartbeat/ocf.py index b640dfc6e..451a41dec 100644 --- a/heartbeat/ocf.py +++ b/heartbeat/ocf.py @@ -195,7 +195,7 @@ def is_probe(): env.get("OCF_RESKEY_CRM_meta_interval", "") == "0") -def parameter(name, default=None): +def get_parameter(name, default=None): """ Extract the parameter value from the environment """ @@ -328,7 +328,7 @@ def run(metadata, handlers): def check_required_params(): for p in metadata.parameters: - if p.required and parameter(p.name) is None: + 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) @@ -342,7 +342,7 @@ def default_for_parameter(paramname): if meta.name == paramname: return meta.default return None - arglist = [parameter(p, default_for_parameter(p)) for p in params] + arglist = [get_parameter(p, default_for_parameter(p)) for p in params] rc = func(*arglist) if rc is None: rc = OCF_SUCCESS
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor