1# 2# Copyright (c) 2016 Red Hat, Inc, Oyvind Albrigtsen 3# All Rights Reserved. 4# 5# 6# This library is free software; you can redistribute it and/or 7# modify it under the terms of the GNU Lesser General Public 8# License as published by the Free Software Foundation; either 9# version 2.1 of the License, or (at your option) any later version. 10# 11# This library is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14# Lesser General Public License for more details. 15# 16# You should have received a copy of the GNU Lesser General Public 17# License along with this library; if not, write to the Free Software 18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19# 20 21import sys, os, logging, syslog 22 23argv=sys.argv 24env=os.environ 25 26# 27# Common variables for the OCF Resource Agents supplied by 28# heartbeat. 29# 30 31OCF_SUCCESS=0 32OCF_ERR_GENERIC=1 33OCF_ERR_ARGS=2 34OCF_ERR_UNIMPLEMENTED=3 35OCF_ERR_PERM=4 36OCF_ERR_INSTALLED=5 37OCF_ERR_CONFIGURED=6 38OCF_NOT_RUNNING=7 39 40# Non-standard values. 41# 42# OCF does not include the concept of master/slave resources so we 43# need to extend it so we can discover a resource's complete state. 44# 45# OCF_RUNNING_MASTER: 46# The resource is in "master" mode and fully operational 47# OCF_FAILED_MASTER: 48# The resource is in "master" mode but in a failed state 49# 50# The extra two values should only be used during a probe. 51# 52# Probes are used to discover resources that were started outside of 53# the CRM and/or left behind if the LRM fails. 54# 55# They can be identified in RA scripts by checking for: 56# [ "${__OCF_ACTION}" = "monitor" -a "${OCF_RESKEY_CRM_meta_interval}" = "0" ] 57# 58# Failed "slaves" should continue to use: OCF_ERR_GENERIC 59# Fully operational "slaves" should continue to use: OCF_SUCCESS 60# 61OCF_RUNNING_MASTER=8 62OCF_FAILED_MASTER=9 63 64 65## Own logger handler that uses old-style syslog handler as otherwise 66## everything is sourced from /dev/syslog 67class SyslogLibHandler(logging.StreamHandler): 68 """ 69 A handler class that correctly push messages into syslog 70 """ 71 def emit(self, record): 72 syslog_level = { 73 logging.CRITICAL:syslog.LOG_CRIT, 74 logging.ERROR:syslog.LOG_ERR, 75 logging.WARNING:syslog.LOG_WARNING, 76 logging.INFO:syslog.LOG_INFO, 77 logging.DEBUG:syslog.LOG_DEBUG, 78 logging.NOTSET:syslog.LOG_DEBUG, 79 }[record.levelno] 80 81 msg = self.format(record) 82 83 # syslog.syslog can not have 0x00 character inside or exception 84 # is thrown 85 syslog.syslog(syslog_level, msg.replace("\x00","\n")) 86 return 87 88 89OCF_RESOURCE_INSTANCE = env.get("OCF_RESOURCE_INSTANCE") 90 91OCF_ACTION = env.get("__OCF_ACTION") 92if OCF_ACTION is None and len(argv) == 2: 93 OCF_ACTION = argv[1] 94 95HA_DEBUG = env.get("HA_debug", 0) 96HA_DATEFMT = env.get("HA_DATEFMT", "%b %d %T ") 97HA_LOGFACILITY = env.get("HA_LOGFACILITY") 98HA_LOGFILE = env.get("HA_LOGFILE") 99HA_DEBUGLOG = env.get("HA_DEBUGLOG") 100 101log = logging.getLogger(os.path.basename(argv[0])) 102log.setLevel(logging.DEBUG) 103 104## add logging to stderr 105if sys.stdout.isatty(): 106 seh = logging.StreamHandler(stream=sys.stderr) 107 if HA_DEBUG == 0: 108 seh.setLevel(logging.WARNING) 109 sehformatter = logging.Formatter('%(filename)s(%(OCF_RESOURCE_INSTANCE)s)[%(process)s]:\t%(asctime)s%(levelname)s: %(message)s', datefmt=HA_DATEFMT) 110 seh.setFormatter(sehformatter) 111 log.addHandler(seh) 112 113## add logging to syslog 114if HA_LOGFACILITY: 115 slh = SyslogLibHandler() 116 if HA_DEBUG == 0: 117 slh.setLevel(logging.WARNING) 118 slhformatter = logging.Formatter('%(levelname)s: %(message)s') 119 slh.setFormatter(slhformatter) 120 log.addHandler(slh) 121 122## add logging to file 123if HA_LOGFILE: 124 lfh = logging.FileHandler(HA_LOGFILE) 125 if HA_DEBUG == 0: 126 lfh.setLevel(logging.WARNING) 127 lfhformatter = logging.Formatter('%(filename)s(%(OCF_RESOURCE_INSTANCE)s)[%(process)s]:\t%(asctime)s%(levelname)s: %(message)s', datefmt=HA_DATEFMT) 128 lfh.setFormatter(lfhformatter) 129 log.addHandler(lfh) 130 131## add debug logging to file 132if HA_DEBUGLOG and HA_LOGFILE != HA_DEBUGLOG: 133 dfh = logging.FileHandler(HA_DEBUGLOG) 134 if HA_DEBUG == 0: 135 dfh.setLevel(logging.WARNING) 136 dfhformatter = logging.Formatter('%(filename)s(%(OCF_RESOURCE_INSTANCE)s)[%(process)s]:\t%(asctime)s%(levelname)s: %(message)s', datefmt=HA_DATEFMT) 137 dfh.setFormatter(dfhformatter) 138 log.addHandler(dfh) 139 140logger = logging.LoggerAdapter(log, {'OCF_RESOURCE_INSTANCE': OCF_RESOURCE_INSTANCE}) 141 142 143_exit_reason_set = False 144 145def ocf_exit_reason(msg): 146 """ 147 Print exit error string to stderr. 148 149 Allows the OCF agent to provide a string describing 150 why the exit code was returned. 151 """ 152 global _exit_reason_set 153 cookie = env.get("OCF_EXIT_REASON_PREFIX", "ocf-exit-reason:") 154 sys.stderr.write("{}{}\n".format(cookie, msg)) 155 sys.stderr.flush() 156 logger.error(msg) 157 _exit_reason_set = True 158 159 160def have_binary(name): 161 """ 162 True if binary exists, False otherwise. 163 """ 164 def _access_check(fn): 165 return (os.path.exists(fn) and 166 os.access(fn, os.F_OK | os.X_OK) and 167 not os.path.isdir(fn)) 168 if _access_check(name): 169 return True 170 path = env.get("PATH", os.defpath).split(os.pathsep) 171 seen = set() 172 for dir in path: 173 dir = os.path.normcase(dir) 174 if dir not in seen: 175 seen.add(dir) 176 name2 = os.path.join(dir, name) 177 if _access_check(name2): 178 return True 179 return False 180 181 182def is_true(val): 183 """ 184 Convert an OCF truth value to a 185 Python boolean. 186 """ 187 return val in ("yes", "true", "1", 1, "YES", "TRUE", "ja", "on", "ON", True) 188 189 190def is_probe(): 191 """ 192 A probe is defined as a monitor operation 193 with an interval of zero. This is called 194 by Pacemaker to check the status of a possibly 195 not running resource. 196 """ 197 return (OCF_ACTION == "monitor" and 198 ( env.get("OCF_RESKEY_CRM_meta_interval", "") == "0" or 199 env.get("OCF_RESKEY_CRM_meta_interval", "") == "" )) 200 201 202def get_parameter(name, default=None): 203 """ 204 Extract the parameter value from the environment 205 """ 206 return env.get("OCF_RESKEY_{}".format(name), default) 207 208 209def distro(): 210 """ 211 Return name of distribution/platform. 212 213 If possible, returns "name/version", else 214 just "name". 215 """ 216 import subprocess 217 import platform 218 try: 219 ret = subprocess.check_output(["lsb_release", "-si"]) 220 if type(ret) != str: 221 ret = ret.decode() 222 distro = ret.strip() 223 ret = subprocess.check_output(["lsb_release", "-sr"]) 224 if type(ret) != str: 225 ret = ret.decode() 226 version = ret.strip() 227 return "{}/{}".format(distro, version) 228 except Exception: 229 if os.path.exists("/etc/debian_version"): 230 return "Debian" 231 if os.path.exists("/etc/SuSE-release"): 232 return "SUSE" 233 if os.path.exists("/etc/redhat-release"): 234 return "Redhat" 235 return platform.system() 236 237 238class Parameter(object): 239 def __init__(self, name, shortdesc, longdesc, content_type, unique, required, default): 240 self.name = name 241 self.shortdesc = shortdesc 242 self.longdesc = longdesc 243 self.content_type = content_type 244 self.unique = unique 245 self.required = required 246 self.default = default 247 248 def __str__(self): 249 return self.to_xml() 250 251 def to_xml(self): 252 ret = '<parameter name="' + self.name + '"' 253 if self.unique: 254 ret += ' unique="1"' 255 if self.required: 256 ret += ' required="1"' 257 ret += ">\n" 258 ret += '<longdesc lang="en">' + self.longdesc + '</longdesc>' + "\n" 259 ret += '<shortdesc lang="en">' + self.shortdesc + '</shortdesc>' + "\n" 260 ret += '<content type="' + self.content_type + '"' 261 if self.default is not None: 262 ret += ' default="{}"'.format(self.default) 263 ret += " />\n" 264 ret += "</parameter>\n" 265 return ret 266 267 268 269class Action(object): 270 def __init__(self, name, timeout, interval, depth, role): 271 self.name = name 272 self.timeout = timeout 273 self.interval = interval 274 self.depth = depth 275 self.role = role 276 277 def __str__(self): 278 return self.to_xml() 279 280 def to_xml(self): 281 def opt(s, name, var): 282 if var is not None: 283 if type(var) == int and name in ("timeout", "interval"): 284 var = "{}s".format(var) 285 return s + ' {}="{}"'.format(name, var) 286 return s 287 ret = '<action name="{}"'.format(self.name) 288 ret = opt(ret, "timeout", self.timeout) 289 ret = opt(ret, "interval", self.interval) 290 ret = opt(ret, "depth", self.depth) 291 ret = opt(ret, "role", self.role) 292 ret += " />\n" 293 return ret 294 295 296class Agent(object): 297 """ 298 OCF Resource Agent metadata XML generator helper. 299 300 Use add_parameter/add_action to define parameters 301 and actions for the agent. Then call run() to 302 start the agent main loop. 303 304 See doc/dev-guides/writing-python-agents.md for an example 305 of how to use it. 306 """ 307 308 def __init__(self, name, shortdesc, longdesc): 309 self.name = name 310 self.shortdesc = shortdesc 311 self.longdesc = longdesc 312 self.parameters = [] 313 self.actions = [] 314 self._handlers = {} 315 316 def add_parameter(self, name, shortdesc="", longdesc="", content_type="string", unique=False, required=False, default=None): 317 for param in self.parameters: 318 if param.name == name: 319 raise ValueError("Parameter {} defined twice in metadata".format(name)) 320 self.parameters.append(Parameter(name=name, 321 shortdesc=shortdesc, 322 longdesc=longdesc, 323 content_type=content_type, 324 unique=unique, 325 required=required, 326 default=default)) 327 return self 328 329 def add_action(self, name, timeout=None, interval=None, depth=None, role=None, handler=None): 330 self.actions.append(Action(name=name, 331 timeout=timeout, 332 interval=interval, 333 depth=depth, 334 role=role)) 335 if handler is not None: 336 self._handlers[name] = handler 337 return self 338 339 def __str__(self): 340 return self.to_xml() 341 342 def to_xml(self): 343 return """<?xml version="1.0"?> 344<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd"> 345<resource-agent name="{name}"> 346<version>1.0</version> 347<longdesc lang="en"> 348{longdesc} 349</longdesc> 350<shortdesc lang="en">{shortdesc}</shortdesc> 351 352<parameters> 353{parameters} 354</parameters> 355 356<actions> 357{actions} 358</actions> 359 360</resource-agent> 361""".format(name=self.name, 362 longdesc=self.longdesc, 363 shortdesc=self.shortdesc, 364 parameters="".join(p.to_xml() for p in self.parameters), 365 actions="".join(a.to_xml() for a in self.actions)) 366 367 def run(self): 368 run(self) 369 370 371def run(agent, handlers=None): 372 """ 373 Main loop implementation for resource agents. 374 Does not return. 375 376 Arguments: 377 378 agent: Agent object. 379 380 handlers: Dict of action name to handler function. 381 382 Handler functions can take parameters as arguments, 383 the run loop will read parameter values from the 384 environment and pass to the handler. 385 """ 386 import inspect 387 388 agent._handlers.update(handlers or {}) 389 handlers = agent._handlers 390 391 def check_required_params(): 392 for p in agent.parameters: 393 if p.required and get_parameter(p.name) is None: 394 ocf_exit_reason("{}: Required parameter not set".format(p.name)) 395 sys.exit(OCF_ERR_CONFIGURED) 396 397 def call_handler(func): 398 if hasattr(inspect, 'signature'): 399 params = inspect.signature(func).parameters.keys() 400 else: 401 params = inspect.getargspec(func).args 402 if 'self' in params: params.remove('self') 403 def value_for_parameter(param): 404 val = get_parameter(param) 405 if val is not None: 406 return val 407 for p in agent.parameters: 408 if p.name == param: 409 return p.default 410 arglist = [value_for_parameter(p) for p in params] 411 try: 412 rc = func(*arglist) 413 if rc is None: 414 rc = OCF_SUCCESS 415 return rc 416 except Exception as err: 417 if not _exit_reason_set: 418 ocf_exit_reason(str(err)) 419 else: 420 logger.error(str(err)) 421 return OCF_ERR_GENERIC 422 423 meta_data_action = False 424 for action in agent.actions: 425 if action.name == "meta-data": 426 meta_data_action = True 427 break 428 if not meta_data_action: 429 agent.add_action("meta-data", timeout=10) 430 431 if len(sys.argv) == 2 and sys.argv[1] in ("-h", "--help"): 432 sys.stdout.write("usage: %s {%s}\n\n" % (sys.argv[0], "|".join(sorted(handlers.keys()))) + 433 "Expects to have a fully populated OCF RA compliant environment set.\n") 434 sys.exit(OCF_SUCCESS) 435 436 if OCF_ACTION is None: 437 ocf_exit_reason("No action argument set") 438 sys.exit(OCF_ERR_UNIMPLEMENTED) 439 if OCF_ACTION in ('meta-data', 'usage', 'methods'): 440 sys.stdout.write(agent.to_xml() + "\n") 441 sys.exit(OCF_SUCCESS) 442 443 check_required_params() 444 if OCF_ACTION in handlers: 445 rc = call_handler(handlers[OCF_ACTION]) 446 sys.exit(rc) 447 sys.exit(OCF_ERR_UNIMPLEMENTED) 448 449 450if __name__ == "__main__": 451 import unittest 452 453 class TestMetadata(unittest.TestCase): 454 def test_noparams_noactions(self): 455 m = Agent("foo", shortdesc="shortdesc", longdesc="longdesc") 456 self.assertEqual("""<?xml version="1.0"?> 457<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd"> 458<resource-agent name="foo"> 459<version>1.0</version> 460<longdesc lang="en"> 461longdesc 462</longdesc> 463<shortdesc lang="en">shortdesc</shortdesc> 464 465<parameters> 466 467</parameters> 468 469<actions> 470 471</actions> 472 473</resource-agent> 474""", str(m)) 475 476 def test_params_actions(self): 477 m = Agent("foo", shortdesc="shortdesc", longdesc="longdesc") 478 m.add_parameter("testparam") 479 m.add_action("start") 480 self.assertEqual(str(m.actions[0]), '<action name="start" />\n') 481 482 unittest.main() 483