diff options
Diffstat (limited to '')
-rw-r--r-- | agents/stonith/Makefile.am | 19 | ||||
-rwxr-xr-x | agents/stonith/fence_legacy.in | 277 | ||||
-rwxr-xr-x | agents/stonith/fence_watchdog.in | 284 |
3 files changed, 580 insertions, 0 deletions
diff --git a/agents/stonith/Makefile.am b/agents/stonith/Makefile.am new file mode 100644 index 0000000..e231775 --- /dev/null +++ b/agents/stonith/Makefile.am @@ -0,0 +1,19 @@ +# +# Copyright 2003-2023 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/common.mk +include $(top_srcdir)/mk/man.mk + +sbin_SCRIPTS = fence_watchdog + +if BUILD_LHA_SUPPORT +sbin_SCRIPTS += fence_legacy +endif + +CLEANFILES = $(man7_MANS) $(man8_MANS) diff --git a/agents/stonith/fence_legacy.in b/agents/stonith/fence_legacy.in new file mode 100755 index 0000000..c0eeca1 --- /dev/null +++ b/agents/stonith/fence_legacy.in @@ -0,0 +1,277 @@ +#!@PYTHON@ + +__copyright__ = "Copyright 2018-2023 the Pacemaker project contributors" +__license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY" + +import os +import sys +import argparse +import subprocess + +# These imports allow running from a source checkout after running `make`. +# Note that while this doesn't necessarily mean it will successfully run tests, +# but being able to see --help output can be useful. +if os.path.exists("@abs_top_srcdir@/python"): + sys.path.insert(0, "@abs_top_srcdir@/python") + +if os.path.exists("@abs_top_builddir@/python") and "@abs_top_builddir@" != "@abs_top_srcdir@": + sys.path.insert(0, "@abs_top_builddir@/python") + +from pacemaker.exitstatus import ExitStatus + +VERSION = "1.1.0" + +USAGE = """Helper that presents a Pacemaker-style interface for Linux-HA stonith plugins + +Should never be invoked by the user directly + + +Usage: fence_legacy [options] + +Options: + -h usage + -t <sub agent> sub agent + -n <name> nodename + -o <string> Action: on | off | reset (default) | stat | hostlist + -s <stonith> stonith command + -q quiet mode + -V version""" + +META_DATA = """<?xml version="1.0" ?> +<resource-agent name="fence_pcmk" shortdesc="Helper that presents a Pacemaker-style interface for Linux-HA stonith plugins"> +<longdesc> +This agent should never be invoked by the user directly. +</longdesc> +<vendor-url>https://www.clusterlabs.org/</vendor-url> +<parameters> + <parameter name="action" unique="1" required="1"> + <getopt mixed="-o <action>" /> + <content type="string" default="disable" /> + <shortdesc lang="en">Fencing Action</shortdesc> + </parameter> + <parameter name="port" unique="1" required="1"> + <getopt mixed="-n <id>" /> + <content type="string" /> + <shortdesc lang="en">Physical plug number or name of virtual machine</shortdesc> + </parameter> + <parameter name="help" unique="1" required="0"> + <getopt mixed="-h" /> + <content type="string" /> + <shortdesc lang="en">Display help and exit</shortdesc> + </parameter> +</parameters> +<actions> + <action name="enable" /> + <action name="disable" /> + <action name="reboot" /> + <action name="off" /> + <action name="on" /> + <action name="status" /> + <action name="list" /> + <action name="metadata" /> +</actions> +</resource-agent>""" + +ACTIONS = [ + "on", + "off", + "reset", + "reboot", + "stat", + "status", + "metadata", + "monitor", + "list", + "hostlist", + "poweroff", + "poweron" +] + + +def parse_cli_options(): + """ Return parsed command-line options (as argparse namespace) """ + + + # Don't add standard help option, so we can format it how we want + parser = argparse.ArgumentParser(add_help=False) + + parser.add_argument("-t", metavar="SUBAGENT", dest="subagent", + nargs=1, default="none", help="sub-agent") + + parser.add_argument("-n", metavar="NODE", dest="node", + nargs=1, default="", help="name of target node") + + # The help text here is consistent with the original version, though + # perhaps all actions should be listed. + parser.add_argument("-o", metavar="ACTION", dest="action", + nargs=1, choices=ACTIONS, default="reset", + help="action: on | off | reset (default) | stat | hostlist") + + parser.add_argument("-s", metavar="COMMAND", dest="command", + nargs=1, default="stonith", help="stonith command") + + parser.add_argument("-q", dest="quiet", action="store_true", + help="quiet mode") + + parser.add_argument("-h", "--help", action="store_true", + help="show usage and exit") + + # Don't use action="version", because that printed to stderr before + # Python 3.4, and help2man doesn't like that. + parser.add_argument("-V", "--version", action="store_true", + help="show version and exit") + + return parser.parse_args() + + +def parse_stdin_options(options): + """ Update options namespace with options parsed from stdin """ + + nlines = 0 + for line in sys.stdin: + # Remove leading and trailing whitespace + line = line.strip() + + # Skip blank lines and comments + if line == "" or line[0] == "#": + continue + + nlines = nlines + 1 + + # Parse option name and value (allow whitespace around equals sign) + try: + (name, value) = line.split("=", 1) + name = name.rstrip() + if name == "": + raise ValueError + except ValueError: + print("parse error: illegal name in option %d" % nlines, + file=sys.stderr) + sys.exit(ExitStatus.INVALID_PARAM) + value = value.lstrip() + + if name == "plugin": + options.subagent = value + + elif name in [ "option", "action" ]: + options.action = value + + elif name == "nodename": + options.node = value + os.environ[name] = value + + elif name == "stonith": + options.command = value + + elif name != "agent": # agent is used by fenced + os.environ[name] = value + + +def normalize_options(options): + """ Use string rather than list of one string """ + + if not hasattr(options.subagent, "strip"): + options.subagent = options.subagent[0] + + if not hasattr(options.node, "strip"): + options.node = options.node[0] + + if not hasattr(options.action, "strip"): + options.action = options.action[0] + + if not hasattr(options.command, "strip"): + options.command = options.command[0] + + +def build_command(options): + """ Return command to execute (as list of arguments) """ + + if options.action in [ "hostlist", "list" ]: + extra_args = [ "-l" ] + + elif options.action in [ "monitor", "stat", "status" ]: + extra_args = [ "-S" ] + + else: + if options.node == "": + if not options.quiet: + print("failed: no plug number") + sys.exit(ExitStatus.ERROR) + extra_args = [ "-T", options.action, options.node ] + + return [ options.command, "-t", options.subagent, "-E" ] + extra_args + + +def handle_local_options(options): + """ Handle options that don't require the fence agent """ + + if options.help: + print(USAGE) + sys.exit(ExitStatus.OK) + + if options.version: + print(VERSION) + sys.exit(ExitStatus.OK) + + +def remap_action(options): + """ Pre-process requested action """ + + options.action = options.action.lower() + + if options.action == "metadata": + print(META_DATA) + sys.exit(ExitStatus.OK) + + elif options.action in [ "hostlist", "list" ]: + options.quiet = True + + # Remap accepted aliases to their actual commands + + elif options.action == "reboot": + options.action = "reset" + + elif options.action == "poweron": + options.action = "on" + + elif options.action == "poweroff": + options.action = "off" + + +def execute_command(options, cmd): + """ Execute command and return its exit status """ + + if not options.quiet: + print("Performing: " + " ".join(cmd)) + return subprocess.call(cmd) + + +def handle_result(options, status): + """ Process fence agent result """ + + if status == 0: + message = "success" + exitcode = ExitStatus.OK + else: + message = "failed" + exitcode = ExitStatus.ERROR + if not options.quiet: + print("%s: %s %d" % (message, options.node, status)) + sys.exit(exitcode) + + +def main(): + """ Execute an LHA-style fence agent """ + + options = parse_cli_options() + handle_local_options(options) + normalize_options(options) + parse_stdin_options(options) + remap_action(options) + cmd = build_command(options) + status = execute_command(options, cmd) + handle_result(options, status) + + +if __name__ == "__main__": + main() diff --git a/agents/stonith/fence_watchdog.in b/agents/stonith/fence_watchdog.in new file mode 100755 index 0000000..f43ab87 --- /dev/null +++ b/agents/stonith/fence_watchdog.in @@ -0,0 +1,284 @@ +#!@PYTHON@ +"""Dummy watchdog fence agent for providing meta-data for the pacemaker internal agent +""" + +__copyright__ = "Copyright 2012-2022 the Pacemaker project contributors" +__license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY" + +import io +import os +import re +import sys +import atexit +import getopt + +AGENT_VERSION = "1.0.0" +SHORT_DESC = "Dummy watchdog fence agent" +LONG_DESC = """fence_watchdog just provides +meta-data - actual fencing is done by the pacemaker internal watchdog agent.""" + +ALL_OPT = { + "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: metadata", + "required" : "1", + "shortdesc" : "Fencing Action", + "default" : "metadata", + "order" : 1 + }, + "nodename" : { + "getopt" : "N:", + "longopt" : "nodename", + "help" : "-N, --nodename Node name of fence target (ignored)", + "required" : "0", + "shortdesc" : "Ignored", + "order" : 2 + }, + "plug" : { + "getopt" : "n:", + "longopt" : "plug", + "help" : "-n, --plug=[id] Physical plug number on device (ignored)", + "required" : "1", + "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(LONG_DESC) + print() + 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. """ + + print("""<?xml version="1.0" ?> +<resource-agent name="%s" shortdesc="%s"> +<longdesc>%s</longdesc> +<parameters>""" % (agent(), SHORT_DESC, LONG_DESC)) + + for option, dummy in sorted_options(avail_opt): + if "shortdesc" in ALL_OPT[option]: + print(' <parameter name="' + option + + '" 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(' <getopt mixed="' + mixed + '" />') + + if ALL_OPT[option]["getopt"].count(":") > 0: + print(' <content type="string" ' + default + ' />') + else: + print(' <content type="boolean" ' + default + ' />') + + print(' <shortdesc lang="en">' + ALL_OPT[option]["shortdesc"] + '</shortdesc>') + print(' </parameter>') + + print(' </parameters>\n <actions>') + print(' <action name="on" />') + print(' <action name="off" />') + print(' <action name="reboot" />') + print(' <action name="monitor" />') + print(' <action name="list" />') + print(' <action name="metadata" />') + print(' </actions>') + print('</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] + + 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] + + 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 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) + + print("Watchdog fencing may be initiated only by the cluster, not this agent.", + file=sys.stderr) + + sys.exit(1) + + +if __name__ == "__main__": + main() |