| #!/usr/bin/python | |
| """Dummy fence agent for testing | |
| """ | |
| # Pacemaker targets compatibility with Python 2.6+ and 3.2+ | |
| from __future__ import print_function, unicode_literals, absolute_import, division | |
| __copyright__ = "Copyright (C) 2012-2016 Andrew Beekhof <andrew@beekhof.net>" | |
| __license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY" | |
| import io | |
| import os | |
| import re | |
| import sys | |
| import time | |
| import random | |
| import atexit | |
| import getopt | |
| AGENT_VERSION = "4.0.0" | |
| OCF_VERSION = "1.0" | |
| SHORT_DESC = "Dummy fence agent" | |
| LONG_DESC = """fence_dummy is a fake fencing agent which reports success | |
| based on its mode (pass|fail|random) without doing anything.""" | |
| # Short options used: ifhmnoqsvBDHMRUV | |
| ALL_OPT = { | |
| "quiet" : { | |
| "getopt" : "q", | |
| "help" : "", | |
| "order" : 50 | |
| }, | |
| "verbose" : { | |
| "getopt" : "v", | |
| "longopt" : "verbose", | |
| "help" : "-v, --verbose Verbose mode", | |
| "required" : "0", | |
| "shortdesc" : "Verbose mode", | |
| "order" : 51 | |
| }, | |
| "debug" : { | |
| "getopt" : "D:", | |
| "longopt" : "debug-file", | |
| "help" : "-D, --debug-file=[debugfile] Debugging to output file", | |
| "required" : "0", | |
| "shortdesc" : "Write debug information to given file", | |
| "order" : 52 | |
| }, | |
| "version" : { | |
| "getopt" : "V", | |
| "longopt" : "version", | |
| "help" : "-V, --version Display version information and exit", | |
| "required" : "0", | |
| "shortdesc" : "Display version information and exit", | |
| "order" : 53 | |
| }, | |
| "help" : { | |
| "getopt" : "h", | |
| "longopt" : "help", | |
| "help" : "-h, --help Display this help and exit", | |
| "required" : "0", | |
| "shortdesc" : "Display help and exit", | |
| "order" : 54 | |
| }, | |
| "action" : { | |
| "getopt" : "o:", | |
| "longopt" : "action", | |
| "help" : "-o, --action=[action] Action: status, list, reboot (default), off or on", | |
| "required" : "1", | |
| "shortdesc" : "Fencing Action", | |
| "default" : "reboot", | |
| "order" : 1 | |
| }, | |
| "nodename" : { | |
| "getopt" : "N:", | |
| "longopt" : "nodename", | |
| "help" : "-N, --nodename Node name of fence victim (ignored)", | |
| "required" : "0", | |
| "shortdesc" : "The node name of fence victim (ignored)", | |
| "order" : 2 | |
| }, | |
| "mode": { | |
| "getopt" : "M:", | |
| "longopt" : "mode", | |
| "required" : "0", | |
| "help" : "-M, --mode=(pass|fail|random) Exit status to return for non-monitor operations", | |
| "shortdesc" : "Whether fence operations should always pass, always fail, or fail at random", | |
| "order" : 3 | |
| }, | |
| "monitor_mode" : { | |
| "getopt" : "m:", | |
| "longopt" : "monitor_mode", | |
| "help" : "-m, --monitor_mode=(pass|fail|random) Exit status to return for monitor operations", | |
| "required" : "0", | |
| "shortdesc" : "Whether monitor operations should always pass, always fail, or fail at random", | |
| "order" : 3 | |
| }, | |
| "random_sleep_range": { | |
| "getopt" : "R:", | |
| "required" : "0", | |
| "longopt" : "random_sleep_range", | |
| "help" : "-R, --random_sleep_range=[seconds] Sleep between 1 and [seconds] before returning", | |
| "shortdesc" : "Wait randomly between 1 and [seconds]", | |
| "order" : 3 | |
| }, | |
| "mock_dynamic_hosts" : { | |
| "getopt" : "H:", | |
| "longopt" : "mock_dynamic_hosts", | |
| "help" : "-H, --mock_dynamic_hosts=[list] What to return when dynamically queried for possible targets", | |
| "required" : "0", | |
| "shortdesc" : "A list of hosts we can fence", | |
| "order" : 3 | |
| }, | |
| "delay" : { | |
| "getopt" : "f:", | |
| "longopt" : "delay", | |
| "help" : "-f, --delay [seconds] Wait X seconds before fencing is started", | |
| "required" : "0", | |
| "shortdesc" : "Wait X seconds before fencing is started", | |
| "default" : "0", | |
| "order" : 3 | |
| }, | |
| "plug" : { | |
| "getopt" : "n:", | |
| "longopt" : "plug", | |
| "help" : "-n, --plug=[id] Physical plug number on device (ignored)", | |
| "required" : "1", | |
| "shortdesc" : "Ignored", | |
| "order" : 4 | |
| }, | |
| "port" : { | |
| "getopt" : "n:", | |
| "longopt" : "plug", | |
| "help" : "-n, --plug=[id] Physical plug number on device (ignored)", | |
| "required" : "1", | |
| "shortdesc" : "Ignored", | |
| "order" : 4 | |
| }, | |
| "switch" : { | |
| "getopt" : "s:", | |
| "longopt" : "switch", | |
| "help" : "-s, --switch=[id] Physical switch number on device (ignored)", | |
| "required" : "0", | |
| "shortdesc" : "Ignored", | |
| "order" : 4 | |
| }, | |
| "nodeid" : { | |
| "getopt" : "i:", | |
| "longopt" : "nodeid", | |
| "help" : "-i, --nodeid Corosync id of fence victim (ignored)", | |
| "required" : "0", | |
| "shortdesc" : "Ignored", | |
| "order" : 4 | |
| }, | |
| "uuid" : { | |
| "getopt" : "U:", | |
| "longopt" : "uuid", | |
| "help" : "-U, --uuid UUID of the VM to fence (ignored)", | |
| "required" : "0", | |
| "shortdesc" : "Ignored", | |
| "order" : 4 | |
| } | |
| } | |
| def agent(): | |
| """ Return name this file was run as. """ | |
| return os.path.basename(sys.argv[0]) | |
| def fail_usage(message): | |
| """ Print a usage message and exit. """ | |
| sys.exit("%s\nPlease use '-h' for usage" % message) | |
| def show_docs(options): | |
| """ Handle informational options (display info and exit). """ | |
| device_opt = options["device_opt"] | |
| if "-h" in options: | |
| usage(device_opt) | |
| sys.exit(0) | |
| if "-o" in options and options["-o"].lower() == "metadata": | |
| metadata(device_opt, options) | |
| sys.exit(0) | |
| if "-V" in options: | |
| print(AGENT_VERSION) | |
| sys.exit(0) | |
| def sorted_options(avail_opt): | |
| """ Return a list of all options, in their internally specified order. """ | |
| sorted_list = [(key, ALL_OPT[key]) for key in avail_opt] | |
| sorted_list.sort(key=lambda x: x[1]["order"]) | |
| return sorted_list | |
| def usage(avail_opt): | |
| """ Print a usage message. """ | |
| print("Usage:") | |
| print("\t" + agent() + " [options]") | |
| print("Options:") | |
| for dummy, value in sorted_options(avail_opt): | |
| if len(value["help"]) != 0: | |
| print(" " + value["help"]) | |
| def metadata(avail_opt, options): | |
| """ Print agent metadata. """ | |
| # This log is just for testing handling of stderr output | |
| print("asked for fence_dummy metadata", file=sys.stderr) | |
| print("""<?xml version="1.0" ?> | |
| <resource-agent name="%s" shortdesc="%s" version="%s"> | |
| <version>%s</version> | |
| <longdesc>%s</longdesc> | |
| <parameters>""" % (agent(), SHORT_DESC, AGENT_VERSION, OCF_VERSION, LONG_DESC)) | |
| for option, dummy in sorted_options(avail_opt): | |
| if "shortdesc" in ALL_OPT[option]: | |
| print("\t<parameter name=\"" + option + "\" unique=\"0\" required=\"" + ALL_OPT[option]["required"] + "\">") | |
| default = "" | |
| default_name_arg = "-" + ALL_OPT[option]["getopt"][:-1] | |
| default_name_no_arg = "-" + ALL_OPT[option]["getopt"] | |
| if "default" in ALL_OPT[option]: | |
| default = 'default="%s"' % str(ALL_OPT[option]["default"]) | |
| elif default_name_arg in options: | |
| if options[default_name_arg]: | |
| try: | |
| default = 'default="%s"' % options[default_name_arg] | |
| except TypeError: | |
| ## @todo/@note: Currently there is no clean way how to handle lists | |
| ## we can create a string from it but we can't set it on command line | |
| default = 'default="%s"' % str(options[default_name_arg]) | |
| elif default_name_no_arg in options: | |
| default = 'default="true"' | |
| mixed = ALL_OPT[option]["help"] | |
| ## split it between option and help text | |
| res = re.compile(r"^(.*--\S+)\s+", re.IGNORECASE | re.S).search(mixed) | |
| if None != res: | |
| mixed = res.group(1) | |
| mixed = mixed.replace("<", "<").replace(">", ">") | |
| print("\t\t<getopt mixed=\"" + mixed + "\" />") | |
| if ALL_OPT[option]["getopt"].count(":") > 0: | |
| print("\t\t<content type=\"string\" "+default+" />") | |
| else: | |
| print("\t\t<content type=\"boolean\" "+default+" />") | |
| print("\t\t<shortdesc lang=\"en\">" + ALL_OPT[option]["shortdesc"] + "</shortdesc>") | |
| print("\t</parameter>") | |
| print("""</parameters> | |
| <actions> | |
| \t<action name="on" on_target="1" /> | |
| \t<action name="off" /> | |
| \t<action name="reboot" /> | |
| \t<action name="status" /> | |
| \t<action name="monitor" /> | |
| \t<action name="metadata" /> | |
| \t<action name="list" /> | |
| </actions> | |
| </resource-agent>""") | |
| def option_longopt(option): | |
| """ Return the getopt-compatible long-option name of the given option. """ | |
| if ALL_OPT[option]["getopt"].endswith(":"): | |
| return ALL_OPT[option]["longopt"] + "=" | |
| else: | |
| return ALL_OPT[option]["longopt"] | |
| def opts_from_command_line(argv, avail_opt): | |
| """ Read options from command-line arguments. """ | |
| # Prepare list of options for getopt | |
| getopt_string = "" | |
| longopt_list = [] | |
| for k in avail_opt: | |
| if k in ALL_OPT: | |
| getopt_string += ALL_OPT[k]["getopt"] | |
| else: | |
| fail_usage("Parse error: unknown option '"+k+"'") | |
| if k in ALL_OPT and "longopt" in ALL_OPT[k]: | |
| longopt_list.append(option_longopt(k)) | |
| try: | |
| opt, dummy = getopt.gnu_getopt(argv, getopt_string, longopt_list) | |
| except getopt.GetoptError as error: | |
| fail_usage("Parse error: " + error.msg) | |
| # Transform longopt to short one which are used in fencing agents | |
| old_opt = opt | |
| opt = {} | |
| for old_option in dict(old_opt).keys(): | |
| if old_option.startswith("--"): | |
| for option in ALL_OPT.keys(): | |
| if "longopt" in ALL_OPT[option] and "--" + ALL_OPT[option]["longopt"] == old_option: | |
| opt["-" + ALL_OPT[option]["getopt"].rstrip(":")] = dict(old_opt)[old_option] | |
| else: | |
| opt[old_option] = dict(old_opt)[old_option] | |
| # Compatibility Layer (with what? probably not needed for fence_dummy) | |
| new_opt = dict(opt) | |
| if "-T" in new_opt: | |
| new_opt["-o"] = "status" | |
| if "-n" in new_opt: | |
| new_opt["-m"] = new_opt["-n"] | |
| opt = new_opt | |
| return opt | |
| def opts_from_stdin(avail_opt): | |
| """ Read options from standard input. """ | |
| opt = {} | |
| name = "" | |
| for line in sys.stdin.readlines(): | |
| line = line.strip() | |
| if line.startswith("#") or (len(line) == 0): | |
| continue | |
| (name, value) = (line + "=").split("=", 1) | |
| value = value[:-1] | |
| # Compatibility Layer (with what? probably not needed for fence_dummy) | |
| if name == "option": | |
| name = "action" | |
| if name not in avail_opt: | |
| print("Parse error: Ignoring unknown option '%s'" % line, | |
| file=sys.stderr) | |
| continue | |
| if ALL_OPT[name]["getopt"].endswith(":"): | |
| opt["-"+ALL_OPT[name]["getopt"].rstrip(":")] = value | |
| elif value.lower() in ["1", "yes", "on", "true"]: | |
| opt["-"+ALL_OPT[name]["getopt"]] = "1" | |
| return opt | |
| def process_input(avail_opt): | |
| """ Set standard environment variables, and parse all options. """ | |
| # Set standard environment | |
| os.putenv("LANG", "C") | |
| os.putenv("LC_ALL", "C") | |
| # Read options from command line or standard input | |
| if len(sys.argv) > 1: | |
| return opts_from_command_line(sys.argv[1:], avail_opt) | |
| else: | |
| return opts_from_stdin(avail_opt) | |
| def atexit_handler(): | |
| """ Close stdout on exit. """ | |
| try: | |
| sys.stdout.close() | |
| os.close(1) | |
| except IOError: | |
| sys.exit("%s failed to close standard output" % agent()) | |
| def success_mode(options, option, default_value): | |
| """ Return exit code specified by option. """ | |
| if option in options: | |
| test_value = options[option] | |
| else: | |
| test_value = default_value | |
| if test_value == "pass": | |
| exitcode = 0 | |
| elif test_value == "fail": | |
| exitcode = 1 | |
| else: | |
| exitcode = random.randint(0, 1) | |
| return exitcode | |
| def write_options(options): | |
| """ Write out all options to debug file. """ | |
| try: | |
| debugfile = io.open(options["-D"], 'at') | |
| debugfile.write("### %s ###\n" % (time.strftime("%Y-%m-%d %H:%M:%S"))) | |
| for option in sorted(options): | |
| debugfile.write("%s=%s\n" % (option, options[option])) | |
| debugfile.write("###\n") | |
| debugfile.close() | |
| except IOError: | |
| pass | |
| def main(): | |
| """ Make it so! """ | |
| device_opt = ALL_OPT.keys() | |
| ## Defaults for fence agent | |
| atexit.register(atexit_handler) | |
| options = process_input(device_opt) | |
| options["device_opt"] = device_opt | |
| show_docs(options) | |
| # dump input to file | |
| if "-D" in options: | |
| write_options(options) | |
| if "-f" in options: | |
| val = int(options["-f"]) | |
| print("delay sleep for %d seconds" % val, file=sys.stderr) | |
| time.sleep(val) | |
| # random sleep for testing | |
| if "-R" in options: | |
| val = int(options["-R"]) | |
| ran = random.randint(1, val) | |
| print("random sleep for %d seconds" % ran, file=sys.stderr) | |
| time.sleep(ran) | |
| if "-o" in options: | |
| action = options["-o"] | |
| else: | |
| action = "action" | |
| if action == "monitor": | |
| exitcode = success_mode(options, "-m", "pass") | |
| elif action == "list": | |
| print("fence_dummy action (list) called", file=sys.stderr) | |
| if "-H" in options: | |
| print(options["-H"]) | |
| exitcode = 0 | |
| else: | |
| print("dynamic hostlist requires mock_dynamic_hosts to be set", | |
| file=sys.stderr) | |
| exitcode = 1 | |
| else: | |
| exitcode = success_mode(options, "-M", "random") | |
| # Ensure we generate some error output on failure exit. | |
| if exitcode == 1: | |
| print("simulated %s failure" % action, file=sys.stderr) | |
| sys.exit(exitcode) | |
| if __name__ == "__main__": | |
| main() |