--- 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()