|
|
d42a06 |
--- a/heartbeat/ocf.py 2020-04-08 13:03:20.543477544 +0200
|
|
|
d42a06 |
+++ b/heartbeat/ocf.py 2020-04-06 10:23:45.950913519 +0200
|
|
|
d42a06 |
@@ -88,6 +88,10 @@
|
|
|
d42a06 |
|
|
|
d42a06 |
OCF_RESOURCE_INSTANCE = env.get("OCF_RESOURCE_INSTANCE")
|
|
|
d42a06 |
|
|
|
d42a06 |
+OCF_ACTION = env.get("__OCF_ACTION")
|
|
|
d42a06 |
+if OCF_ACTION is None and len(argv) == 2:
|
|
|
d42a06 |
+ OCF_ACTION = argv[1]
|
|
|
d42a06 |
+
|
|
|
d42a06 |
HA_DEBUG = env.get("HA_debug", 0)
|
|
|
d42a06 |
HA_DATEFMT = env.get("HA_DATEFMT", "%b %d %T ")
|
|
|
d42a06 |
HA_LOGFACILITY = env.get("HA_LOGFACILITY")
|
|
|
d42a06 |
@@ -135,3 +139,343 @@
|
|
|
d42a06 |
log.addHandler(dfh)
|
|
|
d42a06 |
|
|
|
d42a06 |
logger = logging.LoggerAdapter(log, {'OCF_RESOURCE_INSTANCE': OCF_RESOURCE_INSTANCE})
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+_exit_reason_set = False
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+def ocf_exit_reason(msg):
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+ Print exit error string to stderr.
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ Allows the OCF agent to provide a string describing
|
|
|
d42a06 |
+ why the exit code was returned.
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+ global _exit_reason_set
|
|
|
d42a06 |
+ cookie = env.get("OCF_EXIT_REASON_PREFIX", "ocf-exit-reason:")
|
|
|
d42a06 |
+ sys.stderr.write("{}{}\n".format(cookie, msg))
|
|
|
d42a06 |
+ sys.stderr.flush()
|
|
|
d42a06 |
+ logger.error(msg)
|
|
|
d42a06 |
+ _exit_reason_set = True
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+def have_binary(name):
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+ True if binary exists, False otherwise.
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+ def _access_check(fn):
|
|
|
d42a06 |
+ return (os.path.exists(fn) and
|
|
|
d42a06 |
+ os.access(fn, os.F_OK | os.X_OK) and
|
|
|
d42a06 |
+ not os.path.isdir(fn))
|
|
|
d42a06 |
+ if _access_check(name):
|
|
|
d42a06 |
+ return True
|
|
|
d42a06 |
+ path = env.get("PATH", os.defpath).split(os.pathsep)
|
|
|
d42a06 |
+ seen = set()
|
|
|
d42a06 |
+ for dir in path:
|
|
|
d42a06 |
+ dir = os.path.normcase(dir)
|
|
|
d42a06 |
+ if dir not in seen:
|
|
|
d42a06 |
+ seen.add(dir)
|
|
|
d42a06 |
+ name2 = os.path.join(dir, name)
|
|
|
d42a06 |
+ if _access_check(name2):
|
|
|
d42a06 |
+ return True
|
|
|
d42a06 |
+ return False
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+def is_true(val):
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+ Convert an OCF truth value to a
|
|
|
d42a06 |
+ Python boolean.
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+ return val in ("yes", "true", "1", 1, "YES", "TRUE", "ja", "on", "ON", True)
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+def is_probe():
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+ A probe is defined as a monitor operation
|
|
|
d42a06 |
+ with an interval of zero. This is called
|
|
|
d42a06 |
+ by Pacemaker to check the status of a possibly
|
|
|
d42a06 |
+ not running resource.
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+ return (OCF_ACTION == "monitor" and
|
|
|
d42a06 |
+ env.get("OCF_RESKEY_CRM_meta_interval", "") == "0")
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+def get_parameter(name, default=None):
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+ Extract the parameter value from the environment
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+ return env.get("OCF_RESKEY_{}".format(name), default)
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+def distro():
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+ Return name of distribution/platform.
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ If possible, returns "name/version", else
|
|
|
d42a06 |
+ just "name".
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+ import subprocess
|
|
|
d42a06 |
+ import platform
|
|
|
d42a06 |
+ try:
|
|
|
d42a06 |
+ ret = subprocess.check_output(["lsb_release", "-si"])
|
|
|
d42a06 |
+ if type(ret) != str:
|
|
|
d42a06 |
+ ret = ret.decode()
|
|
|
d42a06 |
+ distro = ret.strip()
|
|
|
d42a06 |
+ ret = subprocess.check_output(["lsb_release", "-sr"])
|
|
|
d42a06 |
+ if type(ret) != str:
|
|
|
d42a06 |
+ ret = ret.decode()
|
|
|
d42a06 |
+ version = ret.strip()
|
|
|
d42a06 |
+ return "{}/{}".format(distro, version)
|
|
|
d42a06 |
+ except Exception:
|
|
|
d42a06 |
+ if os.path.exists("/etc/debian_version"):
|
|
|
d42a06 |
+ return "Debian"
|
|
|
d42a06 |
+ if os.path.exists("/etc/SuSE-release"):
|
|
|
d42a06 |
+ return "SUSE"
|
|
|
d42a06 |
+ if os.path.exists("/etc/redhat-release"):
|
|
|
d42a06 |
+ return "Redhat"
|
|
|
d42a06 |
+ return platform.system()
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+class Parameter(object):
|
|
|
d42a06 |
+ def __init__(self, name, shortdesc, longdesc, content_type, unique, required, default):
|
|
|
d42a06 |
+ self.name = name
|
|
|
d42a06 |
+ self.shortdesc = shortdesc
|
|
|
d42a06 |
+ self.longdesc = longdesc
|
|
|
d42a06 |
+ self.content_type = content_type
|
|
|
d42a06 |
+ self.unique = unique
|
|
|
d42a06 |
+ self.required = required
|
|
|
d42a06 |
+ self.default = default
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ def __str__(self):
|
|
|
d42a06 |
+ return self.to_xml()
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ def to_xml(self):
|
|
|
d42a06 |
+ ret = '
|
|
|
d42a06 |
+ if self.unique:
|
|
|
d42a06 |
+ ret += ' unique="1"'
|
|
|
d42a06 |
+ if self.required:
|
|
|
d42a06 |
+ ret += ' required="1"'
|
|
|
d42a06 |
+ ret += ">\n"
|
|
|
d42a06 |
+ ret += '<longdesc lang="en">' + self.longdesc + '</longdesc>' + "\n"
|
|
|
d42a06 |
+ ret += '<shortdesc lang="en">' + self.shortdesc + '</shortdesc>' + "\n"
|
|
|
d42a06 |
+ ret += '
|
|
|
d42a06 |
+ if self.default is not None:
|
|
|
d42a06 |
+ ret += ' default="{}"'.format(self.default)
|
|
|
d42a06 |
+ ret += " />\n"
|
|
|
d42a06 |
+ ret += "</parameter>\n"
|
|
|
d42a06 |
+ return ret
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+class Action(object):
|
|
|
d42a06 |
+ def __init__(self, name, timeout, interval, depth, role):
|
|
|
d42a06 |
+ self.name = name
|
|
|
d42a06 |
+ self.timeout = timeout
|
|
|
d42a06 |
+ self.interval = interval
|
|
|
d42a06 |
+ self.depth = depth
|
|
|
d42a06 |
+ self.role = role
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ def __str__(self):
|
|
|
d42a06 |
+ return self.to_xml()
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ def to_xml(self):
|
|
|
d42a06 |
+ def opt(s, name, var):
|
|
|
d42a06 |
+ if var is not None:
|
|
|
d42a06 |
+ if type(var) == int and name in ("timeout", "interval"):
|
|
|
d42a06 |
+ var = "{}s".format(var)
|
|
|
d42a06 |
+ return s + ' {}="{}"'.format(name, var)
|
|
|
d42a06 |
+ return s
|
|
|
d42a06 |
+ ret = '
|
|
|
d42a06 |
+ ret = opt(ret, "timeout", self.timeout)
|
|
|
d42a06 |
+ ret = opt(ret, "interval", self.interval)
|
|
|
d42a06 |
+ ret = opt(ret, "depth", self.depth)
|
|
|
d42a06 |
+ ret = opt(ret, "role", self.role)
|
|
|
d42a06 |
+ ret += " />\n"
|
|
|
d42a06 |
+ return ret
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+class Agent(object):
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+ OCF Resource Agent metadata XML generator helper.
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ Use add_parameter/add_action to define parameters
|
|
|
d42a06 |
+ and actions for the agent. Then call run() to
|
|
|
d42a06 |
+ start the agent main loop.
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ See doc/dev-guides/writing-python-agents.md for an example
|
|
|
d42a06 |
+ of how to use it.
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ def __init__(self, name, shortdesc, longdesc):
|
|
|
d42a06 |
+ self.name = name
|
|
|
d42a06 |
+ self.shortdesc = shortdesc
|
|
|
d42a06 |
+ self.longdesc = longdesc
|
|
|
d42a06 |
+ self.parameters = []
|
|
|
d42a06 |
+ self.actions = []
|
|
|
d42a06 |
+ self._handlers = {}
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ def add_parameter(self, name, shortdesc="", longdesc="", content_type="string", unique=False, required=False, default=None):
|
|
|
d42a06 |
+ for param in self.parameters:
|
|
|
d42a06 |
+ if param.name == name:
|
|
|
d42a06 |
+ raise ValueError("Parameter {} defined twice in metadata".format(name))
|
|
|
d42a06 |
+ self.parameters.append(Parameter(name=name,
|
|
|
d42a06 |
+ shortdesc=shortdesc,
|
|
|
d42a06 |
+ longdesc=longdesc,
|
|
|
d42a06 |
+ content_type=content_type,
|
|
|
d42a06 |
+ unique=unique,
|
|
|
d42a06 |
+ required=required,
|
|
|
d42a06 |
+ default=default))
|
|
|
d42a06 |
+ return self
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ def add_action(self, name, timeout=None, interval=None, depth=None, role=None, handler=None):
|
|
|
d42a06 |
+ self.actions.append(Action(name=name,
|
|
|
d42a06 |
+ timeout=timeout,
|
|
|
d42a06 |
+ interval=interval,
|
|
|
d42a06 |
+ depth=depth,
|
|
|
d42a06 |
+ role=role))
|
|
|
d42a06 |
+ if handler is not None:
|
|
|
d42a06 |
+ self._handlers[name] = handler
|
|
|
d42a06 |
+ return self
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ def __str__(self):
|
|
|
d42a06 |
+ return self.to_xml()
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ def to_xml(self):
|
|
|
d42a06 |
+ return """
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+<resource-agent name="{name}">
|
|
|
d42a06 |
+<version>1.0</version>
|
|
|
d42a06 |
+<longdesc lang="en">
|
|
|
d42a06 |
+{longdesc}
|
|
|
d42a06 |
+</longdesc>
|
|
|
d42a06 |
+<shortdesc lang="en">{shortdesc}</shortdesc>
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+<parameters>
|
|
|
d42a06 |
+{parameters}
|
|
|
d42a06 |
+</parameters>
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+<actions>
|
|
|
d42a06 |
+{actions}
|
|
|
d42a06 |
+</actions>
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+</resource-agent>
|
|
|
d42a06 |
+""".format(name=self.name,
|
|
|
d42a06 |
+ longdesc=self.longdesc,
|
|
|
d42a06 |
+ shortdesc=self.shortdesc,
|
|
|
d42a06 |
+ parameters="".join(p.to_xml() for p in self.parameters),
|
|
|
d42a06 |
+ actions="".join(a.to_xml() for a in self.actions))
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ def run(self):
|
|
|
d42a06 |
+ run(self)
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+def run(agent, handlers=None):
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+ Main loop implementation for resource agents.
|
|
|
d42a06 |
+ Does not return.
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ Arguments:
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ agent: Agent object.
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ handlers: Dict of action name to handler function.
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ Handler functions can take parameters as arguments,
|
|
|
d42a06 |
+ the run loop will read parameter values from the
|
|
|
d42a06 |
+ environment and pass to the handler.
|
|
|
d42a06 |
+ """
|
|
|
d42a06 |
+ import inspect
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ agent._handlers.update(handlers or {})
|
|
|
d42a06 |
+ handlers = agent._handlers
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ def check_required_params():
|
|
|
d42a06 |
+ for p in agent.parameters:
|
|
|
d42a06 |
+ if p.required and get_parameter(p.name) is None:
|
|
|
d42a06 |
+ ocf_exit_reason("{}: Required parameter not set".format(p.name))
|
|
|
d42a06 |
+ sys.exit(OCF_ERR_CONFIGURED)
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ def call_handler(func):
|
|
|
d42a06 |
+ if hasattr(inspect, 'signature'):
|
|
|
d42a06 |
+ params = inspect.signature(func).parameters.keys()
|
|
|
d42a06 |
+ else:
|
|
|
d42a06 |
+ params = inspect.getargspec(func).args
|
|
|
d42a06 |
+ def value_for_parameter(param):
|
|
|
d42a06 |
+ val = get_parameter(param)
|
|
|
d42a06 |
+ if val is not None:
|
|
|
d42a06 |
+ return val
|
|
|
d42a06 |
+ for p in agent.parameters:
|
|
|
d42a06 |
+ if p.name == param:
|
|
|
d42a06 |
+ return p.default
|
|
|
d42a06 |
+ arglist = [value_for_parameter(p) for p in params]
|
|
|
d42a06 |
+ try:
|
|
|
d42a06 |
+ rc = func(*arglist)
|
|
|
d42a06 |
+ if rc is None:
|
|
|
d42a06 |
+ rc = OCF_SUCCESS
|
|
|
d42a06 |
+ return rc
|
|
|
d42a06 |
+ except Exception as err:
|
|
|
d42a06 |
+ if not _exit_reason_set:
|
|
|
d42a06 |
+ ocf_exit_reason(str(err))
|
|
|
d42a06 |
+ else:
|
|
|
d42a06 |
+ logger.error(str(err))
|
|
|
d42a06 |
+ return OCF_ERR_GENERIC
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ meta_data_action = False
|
|
|
d42a06 |
+ for action in agent.actions:
|
|
|
d42a06 |
+ if action.name == "meta-data":
|
|
|
d42a06 |
+ meta_data_action = True
|
|
|
d42a06 |
+ break
|
|
|
d42a06 |
+ if not meta_data_action:
|
|
|
d42a06 |
+ agent.add_action("meta-data", timeout=10)
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ if len(sys.argv) == 2 and sys.argv[1] in ("-h", "--help"):
|
|
|
d42a06 |
+ sys.stdout.write("usage: %s {%s}\n\n" % (sys.argv[0], "|".join(sorted(handlers.keys()))) +
|
|
|
d42a06 |
+ "Expects to have a fully populated OCF RA compliant environment set.\n")
|
|
|
d42a06 |
+ sys.exit(OCF_SUCCESS)
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ if OCF_ACTION is None:
|
|
|
d42a06 |
+ ocf_exit_reason("No action argument set")
|
|
|
d42a06 |
+ sys.exit(OCF_ERR_UNIMPLEMENTED)
|
|
|
d42a06 |
+ if OCF_ACTION in ('meta-data', 'usage', 'methods'):
|
|
|
d42a06 |
+ sys.stdout.write(agent.to_xml() + "\n")
|
|
|
d42a06 |
+ sys.exit(OCF_SUCCESS)
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ check_required_params()
|
|
|
d42a06 |
+ if OCF_ACTION in handlers:
|
|
|
d42a06 |
+ rc = call_handler(handlers[OCF_ACTION])
|
|
|
d42a06 |
+ sys.exit(rc)
|
|
|
d42a06 |
+ sys.exit(OCF_ERR_UNIMPLEMENTED)
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+if __name__ == "__main__":
|
|
|
d42a06 |
+ import unittest
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ class TestMetadata(unittest.TestCase):
|
|
|
d42a06 |
+ def test_noparams_noactions(self):
|
|
|
d42a06 |
+ m = Agent("foo", shortdesc="shortdesc", longdesc="longdesc")
|
|
|
d42a06 |
+ self.assertEqual("""
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+<resource-agent name="foo">
|
|
|
d42a06 |
+<version>1.0</version>
|
|
|
d42a06 |
+<longdesc lang="en">
|
|
|
d42a06 |
+longdesc
|
|
|
d42a06 |
+</longdesc>
|
|
|
d42a06 |
+<shortdesc lang="en">shortdesc</shortdesc>
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+<parameters>
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+</parameters>
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+<actions>
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+</actions>
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+</resource-agent>
|
|
|
d42a06 |
+""", str(m))
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ def test_params_actions(self):
|
|
|
d42a06 |
+ m = Agent("foo", shortdesc="shortdesc", longdesc="longdesc")
|
|
|
d42a06 |
+ m.add_parameter("testparam")
|
|
|
d42a06 |
+ m.add_action("start")
|
|
|
d42a06 |
+ self.assertEqual(str(m.actions[0]), '<action name="start" />\n')
|
|
|
d42a06 |
+
|
|
|
d42a06 |
+ unittest.main()
|