diff options
Diffstat (limited to 'agents/autodetect/fencing.py')
-rw-r--r-- | agents/autodetect/fencing.py | 1393 |
1 files changed, 1393 insertions, 0 deletions
diff --git a/agents/autodetect/fencing.py b/agents/autodetect/fencing.py new file mode 100644 index 0000000..ea21ace --- /dev/null +++ b/agents/autodetect/fencing.py @@ -0,0 +1,1393 @@ +#!/usr/bin/python -tt + +import sys, getopt, time, os, uuid, pycurl, stat +import pexpect, re, syslog +import logging +import subprocess +import threading +import shlex +import exceptions +import socket +import textwrap +import __main__ + +## do not add code here. +#BEGIN_VERSION_GENERATION +RELEASE_VERSION="4.0.21.23-eaa13" +BUILD_DATE="(built Wed Nov 4 13:28:43 CET 2015)" +REDHAT_COPYRIGHT="Copyright (C) Red Hat, Inc. 2004-2010 All rights reserved." +#END_VERSION_GENERATION + +__all__ = ['atexit_handler', 'check_input', 'process_input', 'all_opt', 'show_docs', + 'fence_login', 'fence_action', 'fence_logout'] + +EC_OK = 0 +EC_GENERIC_ERROR = 1 +EC_BAD_ARGS = 2 +EC_LOGIN_DENIED = 3 +EC_CONNECTION_LOST = 4 +EC_TIMED_OUT = 5 +EC_WAITING_ON = 6 +EC_WAITING_OFF = 7 +EC_STATUS = 8 +EC_STATUS_HMC = 9 +EC_PASSWORD_MISSING = 10 +EC_INVALID_PRIVILEGES = 11 + +all_opt = { + "help" : { + "getopt" : "h", + "longopt" : "help", + "help" : "-h, --help Display this help and exit", + "required" : "0", + "shortdesc" : "Display help and exit", + "order" : 54}, + "version" : { + "getopt" : "V", + "longopt" : "version", + "help" : "-V, --version Display version information and exit", + "required" : "0", + "shortdesc" : "Display version information and exit", + "order" : 53}, + "verbose" : { + "getopt" : "v", + "longopt" : "verbose", + "help" : "-v, --verbose Verbose mode", + "required" : "0", + "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}, + "delay" : { + "getopt" : ":", + "longopt" : "delay", + "help" : "--delay=[seconds] Wait X seconds before fencing is started", + "required" : "0", + "default" : "0", + "order" : 200}, + "agent" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "web" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "force_on" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "action" : { + "getopt" : "o:", + "longopt" : "action", + "help" : "-o, --action=[action] Action: status, reboot (default), off or on", + "required" : "1", + "shortdesc" : "Fencing action", + "default" : "reboot", + "order" : 1}, + "fabric_fencing" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "ipaddr" : { + "getopt" : "a:", + "longopt" : "ip", + "help" : "-a, --ip=[ip] IP address or hostname of fencing device", + "required" : "1", + "order" : 1}, + "ipport" : { + "getopt" : "u:", + "longopt" : "ipport", + "help" : "-u, --ipport=[port] TCP/UDP port to use for connection", + "required" : "0", + "shortdesc" : "TCP/UDP port to use for connection with device", + "order" : 1}, + "login" : { + "getopt" : "l:", + "longopt" : "username", + "help" : "-l, --username=[name] Login name", + "required" : "?", + "order" : 1}, + "no_login" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "no_password" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "no_port" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "no_status" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "no_on" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "no_off" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "telnet" : { + "getopt" : "", + "help" : "", + "order" : ""}, + "passwd" : { + "getopt" : "p:", + "longopt" : "password", + "help" : "-p, --password=[password] Login password or passphrase", + "required" : "0", + "order" : 1}, + "passwd_script" : { + "getopt" : "S:", + "longopt" : "password-script", + "help" : "-S, --password-script=[script] Script to run to retrieve password", + "required" : "0", + "order" : 1}, + "identity_file" : { + "getopt" : "k:", + "longopt" : "identity-file", + "help" : "-k, --identity-file=[filename] Identity file (private key) for SSH", + "required" : "0", + "order" : 1}, + "cmd_prompt" : { + "getopt" : "c:", + "longopt" : "command-prompt", + "help" : "-c, --command-prompt=[prompt] Force Python regex for command prompt", + "required" : "0", + "order" : 1}, + "secure" : { + "getopt" : "x", + "longopt" : "ssh", + "help" : "-x, --ssh Use SSH connection", + "required" : "0", + "order" : 1}, + "ssh_options" : { + "getopt" : ":", + "longopt" : "ssh-options", + "help" : "--ssh-options=[options] SSH options to use", + "required" : "0", + "order" : 1}, + "ssl" : { + "getopt" : "z", + "longopt" : "ssl", + "help" : "-z, --ssl Use SSL connection with verifying certificate", + "required" : "0", + "order" : 1}, + "ssl_insecure" : { + "getopt" : "", + "longopt" : "ssl-insecure", + "help" : "--ssl-insecure Use SSL connection without verifying certificate", + "required" : "0", + "order" : 1}, + "ssl_secure" : { + "getopt" : "", + "longopt" : "ssl-secure", + "help" : "--ssl-secure Use SSL connection with verifying certificate", + "required" : "0", + "order" : 1}, + "notls" : { + "getopt" : "t", + "longopt" : "notls", + "help" : "-t, --notls " + "Disable TLS negotiation and force SSL3.0. " + "This should only be used for devices that do not support TLS1.0 and up.", + "required" : "0", + "order" : 1}, + "tls1.0" : { + "getopt" : "", + "longopt" : "tls1.0", + "help" : "--tls1.0 " + "Disable TLS negotiation and force TLS1.0. " + "This should only be used for devices that do not support TLS1.1 and up.", + "required" : "0", + "order" : 1}, + "port" : { + "getopt" : "n:", + "longopt" : "plug", + "help" : "-n, --plug=[id] " + "Physical plug number on device, UUID or identification of machine", + "required" : "1", + "order" : 1}, + "switch" : { + "getopt" : "s:", + "longopt" : "switch", + "help" : "-s, --switch=[id] Physical switch number on device", + "required" : "0", + "order" : 1}, + "exec" : { + "getopt" : "e:", + "longopt" : "exec", + "help" : "-e, --exec=[command] Command to execute", + "required" : "0", + "order" : 1}, + "vmware_type" : { + "getopt" : "d:", + "longopt" : "vmware_type", + "help" : "-d, --vmware_type=[type] Type of VMware to connect", + "required" : "0", + "order" : 1}, + "vmware_datacenter" : { + "getopt" : "s:", + "longopt" : "vmware-datacenter", + "help" : "-s, --vmware-datacenter=[dc] VMWare datacenter filter", + "required" : "0", + "order" : 2}, + "snmp_version" : { + "getopt" : "d:", + "longopt" : "snmp-version", + "help" : "-d, --snmp-version=[version] Specifies SNMP version to use (1|2c|3)", + "required" : "0", + "shortdesc" : "Specifies SNMP version to use", + "choices" : ["1", "2c", "3"], + "order" : 1}, + "community" : { + "getopt" : "c:", + "longopt" : "community", + "help" : "-c, --community=[community] Set the community string", + "required" : "0", + "order" : 1}, + "snmp_auth_prot" : { + "getopt" : "b:", + "longopt" : "snmp-auth-prot", + "help" : "-b, --snmp-auth-prot=[prot] Set authentication protocol (MD5|SHA)", + "required" : "0", + "shortdesc" : "Set authentication protocol", + "choices" : ["MD5", "SHA"], + "order" : 1}, + "snmp_sec_level" : { + "getopt" : "E:", + "longopt" : "snmp-sec-level", + "help" : "-E, --snmp-sec-level=[level] " + "Set security level (noAuthNoPriv|authNoPriv|authPriv)", + "required" : "0", + "shortdesc" : "Set security level", + "choices" : ["noAuthNoPriv", "authNoPriv", "authPriv"], + "order" : 1}, + "snmp_priv_prot" : { + "getopt" : "B:", + "longopt" : "snmp-priv-prot", + "help" : "-B, --snmp-priv-prot=[prot] Set privacy protocol (DES|AES)", + "required" : "0", + "shortdesc" : "Set privacy protocol", + "choices" : ["DES", "AES"], + "order" : 1}, + "snmp_priv_passwd" : { + "getopt" : "P:", + "longopt" : "snmp-priv-passwd", + "help" : "-P, --snmp-priv-passwd=[pass] Set privacy protocol password", + "required" : "0", + "order" : 1}, + "snmp_priv_passwd_script" : { + "getopt" : "R:", + "longopt" : "snmp-priv-passwd-script", + "help" : "-R, --snmp-priv-passwd-script Script to run to retrieve privacy password", + "required" : "0", + "order" : 1}, + "inet4_only" : { + "getopt" : "4", + "longopt" : "inet4-only", + "help" : "-4, --inet4-only Forces agent to use IPv4 addresses only", + "required" : "0", + "order" : 1}, + "inet6_only" : { + "getopt" : "6", + "longopt" : "inet6-only", + "help" : "-6, --inet6-only Forces agent to use IPv6 addresses only", + "required" : "0", + "order" : 1}, + "separator" : { + "getopt" : "C:", + "longopt" : "separator", + "help" : "-C, --separator=[char] Separator for CSV created by 'list' operation", + "default" : ",", + "required" : "0", + "order" : 100}, + "login_timeout" : { + "getopt" : ":", + "longopt" : "login-timeout", + "help" : "--login-timeout=[seconds] Wait X seconds for cmd prompt after login", + "default" : "5", + "required" : "0", + "order" : 200}, + "shell_timeout" : { + "getopt" : ":", + "longopt" : "shell-timeout", + "help" : "--shell-timeout=[seconds] Wait X seconds for cmd prompt after issuing command", + "default" : "3", + "required" : "0", + "order" : 200}, + "power_timeout" : { + "getopt" : ":", + "longopt" : "power-timeout", + "help" : "--power-timeout=[seconds] Test X seconds for status change after ON/OFF", + "default" : "20", + "required" : "0", + "order" : 200}, + "power_wait" : { + "getopt" : ":", + "longopt" : "power-wait", + "help" : "--power-wait=[seconds] Wait X seconds after issuing ON/OFF", + "default" : "0", + "required" : "0", + "order" : 200}, + "missing_as_off" : { + "getopt" : "", + "longopt" : "missing-as-off", + "help" : "--missing-as-off Missing port returns OFF instead of failure", + "required" : "0", + "order" : 200}, + "retry_on" : { + "getopt" : ":", + "longopt" : "retry-on", + "help" : "--retry-on=[attempts] Count of attempts to retry power on", + "default" : "1", + "required" : "0", + "order" : 201}, + "session_url" : { + "getopt" : "s:", + "longopt" : "session-url", + "help" : "-s, --session-url URL to connect to XenServer on", + "required" : "1", + "order" : 1}, + "sudo" : { + "getopt" : "", + "longopt" : "use-sudo", + "help" : "--use-sudo Use sudo (without password) when calling 3rd party software", + "required" : "0", + "order" : 205}, + "method" : { + "getopt" : "m:", + "longopt" : "method", + "help" : "-m, --method=[method] Method to fence (onoff|cycle) (Default: onoff)", + "required" : "0", + "shortdesc" : "Method to fence", + "default" : "onoff", + "choices" : ["onoff", "cycle"], + "order" : 1}, + "telnet_path" : { + "getopt" : ":", + "longopt" : "telnet-path", + "help" : "--telnet-path=[path] Path to telnet binary", + "required" : "0", + "default" : "/usr/bin/telnet", + "order": 300}, + "ssh_path" : { + "getopt" : ":", + "longopt" : "ssh-path", + "help" : "--ssh-path=[path] Path to ssh binary", + "required" : "0", + "default" : "/usr/bin/ssh", + "order": 300}, + "gnutlscli_path" : { + "getopt" : ":", + "longopt" : "gnutlscli-path", + "help" : "--gnutlscli-path=[path] Path to gnutls-cli binary", + "required" : "0", + "default" : "/usr/bin/gnutls-cli", + "order": 300}, + "sudo_path" : { + "getopt" : ":", + "longopt" : "sudo-path", + "help" : "--sudo-path=[path] Path to sudo binary", + "required" : "0", + "default" : "/usr/bin/sudo", + "order": 300}, + "snmpwalk_path" : { + "getopt" : ":", + "longopt" : "snmpwalk-path", + "help" : "--snmpwalk-path=[path] Path to snmpwalk binary", + "required" : "0", + "default" : "/usr/bin/snmpwalk", + "order" : 300}, + "snmpset_path" : { + "getopt" : ":", + "longopt" : "snmpset-path", + "help" : "--snmpset-path=[path] Path to snmpset binary", + "required" : "0", + "default" : "/usr/bin/snmpset", + "order" : 300}, + "snmpget_path" : { + "getopt" : ":", + "longopt" : "snmpget-path", + "help" : "--snmpget-path=[path] Path to snmpget binary", + "required" : "0", + "default" : "/usr/bin/snmpget", + "order" : 300}, + "snmp": { + "getopt" : "", + "help" : "", + "order" : 1}, + "port_as_ip": { + "getopt" : "", + "longopt" : "port-as-ip", + "help" : "--port-as-ip Make \"port/plug\" to be an alias to IP address", + "required" : "0", + "order" : 200}, + "on_target": { + "getopt" : "", + "help" : "", + "order" : 1}, + "quiet": { + "getopt" : "q", + "longopt": "quiet", + "help" : "-q, --quiet Disable logging to stderr. Does not affect --verbose or --debug-file or logging to syslog.", + "required" : "0", + "order" : 50} +} + +# options which are added automatically if 'key' is encountered ("default" is always added) +DEPENDENCY_OPT = { + "default" : ["help", "debug", "verbose", "version", "action", "agent", \ + "power_timeout", "shell_timeout", "login_timeout", "power_wait", "retry_on", \ + "delay", "quiet"], + "passwd" : ["passwd_script"], + "sudo" : ["sudo_path"], + "secure" : ["identity_file", "ssh_options", "ssh_path"], + "telnet" : ["telnet_path"], + "ipaddr" : ["ipport", "inet4_only", "inet6_only"], + "port" : ["separator"], + "ssl" : ["ssl_secure", "ssl_insecure", "gnutlscli_path"], + "snmp" : ["snmp_auth_prot", "snmp_sec_level", "snmp_priv_prot", \ + "snmp_priv_passwd", "snmp_priv_passwd_script", "community", \ + "snmpset_path", "snmpget_path", "snmpwalk_path"] + } + +class fspawn(pexpect.spawn): + def __init__(self, options, command): + logging.info("Running command: %s", command) + pexpect.spawn.__init__(self, command) + self.opt = options + + def log_expect(self, pattern, timeout): + result = self.expect(pattern, timeout) + logging.debug("Received: %s", str(self.before) + str(self.after)) + return result + + def send(self, message): + logging.debug("Sent: %s", message) + return pexpect.spawn.send(self, message) + + # send EOL according to what was detected in login process (telnet) + def send_eol(self, message): + return self.send(message + self.opt["eol"]) + +def atexit_handler(): + try: + sys.stdout.close() + os.close(1) + except IOError: + logging.error("%s failed to close standard output\n", sys.argv[0]) + sys.exit(EC_GENERIC_ERROR) + +def _add_dependency_options(options): + ## Add also options which are available for every fence agent + added_opt = [] + for opt in options + ["default"]: + if DEPENDENCY_OPT.has_key(opt): + added_opt.extend([y for y in DEPENDENCY_OPT[opt] if options.count(y) == 0]) + + if not "port" in (options + added_opt) and \ + not "nodename" in (options + added_opt) and \ + "ipaddr" in (options + added_opt): + added_opt.append("port_as_ip") + all_opt["port"]["help"] = "-n, --plug=[ip] IP address or hostname of fencing device " \ + "(together with --port-as-ip)" + + return added_opt + +def fail_usage(message="", stop=True): + if len(message) > 0: + logging.error("%s\n", message) + if stop: + logging.error("Please use '-h' for usage\n") + sys.exit(EC_GENERIC_ERROR) + +def fail(error_code): + message = { + EC_LOGIN_DENIED : "Unable to connect/login to fencing device", + EC_CONNECTION_LOST : "Connection lost", + EC_TIMED_OUT : "Connection timed out", + EC_WAITING_ON : "Failed: Timed out waiting to power ON", + EC_WAITING_OFF : "Failed: Timed out waiting to power OFF", + EC_STATUS : "Failed: Unable to obtain correct plug status or plug is not available", + EC_STATUS_HMC : "Failed: Either unable to obtain correct plug status, " + "partition is not available or incorrect HMC version used", + EC_PASSWORD_MISSING : "Failed: You have to set login password", + EC_INVALID_PRIVILEGES : "Failed: The user does not have the correct privileges to do the requested action." + }[error_code] + "\n" + logging.error("%s\n", message) + sys.exit(EC_GENERIC_ERROR) + +def usage(avail_opt): + print "Usage:" + print "\t" + os.path.basename(sys.argv[0]) + " [options]" + print "Options:" + + sorted_list = [(key, all_opt[key]) for key in avail_opt] + sorted_list.sort(lambda x, y: cmp(x[1]["order"], y[1]["order"])) + + for key, value in sorted_list: + if len(value["help"]) != 0: + print " " + _join_wrap([value["help"]], first_indent=3) + +def metadata(avail_opt, docs): + # avail_opt has to be unique, if there are duplicities then they should be removed + sorted_list = [(key, all_opt[key]) for key in list(set(avail_opt))] + sorted_list.sort(lambda x, y: cmp(x[0], y[0])) + sorted_list.sort(lambda x, y: cmp(x[1]["order"], y[1]["order"])) + + print "<?xml version=\"1.0\" ?>" + print "<resource-agent name=\"" + os.path.basename(sys.argv[0]) + \ + "\" shortdesc=\"" + docs["shortdesc"] + "\" >" + for (symlink, desc) in docs.get("symlink", []): + print "<symlink name=\"" + symlink + "\" shortdesc=\"" + desc + "\"/>" + print "<longdesc>" + docs["longdesc"] + "</longdesc>" + print "<vendor-url>" + docs["vendorurl"] + "</vendor-url>" + print "<parameters>" + for option, _ in sorted_list: + if all_opt[option].has_key("help") and len(all_opt[option]["help"]) > 0: + print "\t<parameter name=\"" + option + "\" unique=\"0\" required=\"" + all_opt[option]["required"] + "\">" + + default = "" + if all_opt[option].has_key("default"): + default = "default=\"" + _encode_html_entities(str(all_opt[option]["default"])) + "\" " + + 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 = _encode_html_entities(mixed) + + if not "shortdesc" in all_opt[option]: + shortdesc = re.sub("\s\s+", " ", all_opt[option]["help"][31:]) + else: + shortdesc = all_opt[option]["shortdesc"] + + print "\t\t<getopt mixed=\"" + mixed + "\" />" + if all_opt[option].has_key("choices"): + print "\t\t<content type=\"select\" "+default+" >" + for choice in all_opt[option]["choices"]: + print "\t\t\t<option value=\"%s\" />" % (choice) + print "\t\t</content>" + elif 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\">" + shortdesc + "</shortdesc>" + print "\t</parameter>" + print "</parameters>" + print "<actions>" + + (available_actions, _) = _get_available_actions(avail_opt) + + if "on" in available_actions: + available_actions.remove("on") + on_target = ' on_target="1"' if avail_opt.count("on_target") else '' + print "\t<action name=\"on\"%s automatic=\"%d\"/>" % (on_target, avail_opt.count("fabric_fencing")) + + for action in available_actions: + print "\t<action name=\"%s\" />" % (action) + print "</actions>" + print "</resource-agent>" + +def process_input(avail_opt): + avail_opt.extend(_add_dependency_options(avail_opt)) + + # @todo: this should be put elsewhere? + os.putenv("LANG", "C") + os.putenv("LC_ALL", "C") + + if "port_as_ip" in avail_opt: + avail_opt.append("port") + + if len(sys.argv) > 1: + opt = _parse_input_cmdline(avail_opt) + else: + opt = _parse_input_stdin(avail_opt) + + if "--port-as-ip" in opt and "--plug" in opt: + opt["--ip"] = opt["--plug"] + + return opt + +## +## This function checks input and answers if we want to have same answers +## in each of the fencing agents. It looks for possible errors and run +## password script to set a correct password +###### +def check_input(device_opt, opt, other_conditions = False): + device_opt.extend(_add_dependency_options(device_opt)) + + options = dict(opt) + options["device_opt"] = device_opt + + _update_metadata(options) + options = _set_default_values(options) + options["--action"] = options["--action"].lower() + + ## In special cases (show help, metadata or version) we don't need to check anything + ##### + # OCF compatibility + if options["--action"] == "meta-data": + options["--action"] = "metadata" + + if options["--action"] == "metadata" or any(options.has_key(k) for k in ("--help", "--version")): + return options + + if options.has_key("--verbose"): + logging.getLogger().setLevel(logging.DEBUG) + + ## add logging to syslog + logging.getLogger().addHandler(SyslogLibHandler()) + if not options.has_key("--quiet"): + ## add logging to stderr + logging.getLogger().addHandler(logging.StreamHandler(sys.stderr)) + + (acceptable_actions, _) = _get_available_actions(device_opt) + + if 1 == device_opt.count("fabric_fencing"): + acceptable_actions.extend(["enable", "disable"]) + + if 0 == acceptable_actions.count(options["--action"]): + fail_usage("Failed: Unrecognised action '" + options["--action"] + "'") + + ## Compatibility layer + ##### + if options["--action"] == "enable": + options["--action"] = "on" + if options["--action"] == "disable": + options["--action"] = "off" + + + if options["--action"] == "validate-all" and not other_conditions: + _validate_input(options, False) + sys.exit(EC_OK) + else: + _validate_input(options, True) + + if options.has_key("--debug-file"): + try: + debug_file = logging.FileHandler(options["--debug-file"]) + debug_file.setLevel(logging.DEBUG) + logging.getLogger().addHandler(debug_file) + except IOError: + logging.error("Unable to create file %s", options["--debug-file"]) + fail_usage("Failed: Unable to create file " + options["--debug-file"]) + + if options.has_key("--snmp-priv-passwd-script"): + options["--snmp-priv-passwd"] = os.popen(options["--snmp-priv-passwd-script"]).read().rstrip() + + if options.has_key("--password-script"): + options["--password"] = os.popen(options["--password-script"]).read().rstrip() + + return options + +## Obtain a power status from possibly more than one plug +## "on" is returned if at least one plug is ON +###### +def get_multi_power_fn(connection, options, get_power_fn): + status = "off" + plugs = options["--plugs"] if options.has_key("--plugs") else [""] + + for plug in plugs: + try: + options["--uuid"] = str(uuid.UUID(plug)) + except ValueError: + pass + except KeyError: + pass + + options["--plug"] = plug + plug_status = get_power_fn(connection, options) + if plug_status != "off": + status = plug_status + + return status + +def set_multi_power_fn(connection, options, set_power_fn, get_power_fn, retry_attempts=1): + plugs = options["--plugs"] if options.has_key("--plugs") else [""] + + for _ in range(retry_attempts): + for plug in plugs: + try: + options["--uuid"] = str(uuid.UUID(plug)) + except ValueError: + pass + except KeyError: + pass + + options["--plug"] = plug + set_power_fn(connection, options) + time.sleep(int(options["--power-wait"])) + + for _ in xrange(int(options["--power-timeout"])): + if get_multi_power_fn(connection, options, get_power_fn) != options["--action"]: + time.sleep(1) + else: + return True + return False + +def show_docs(options, docs=None): + device_opt = options["device_opt"] + + if docs == None: + docs = {} + docs["shortdesc"] = "Fence agent" + docs["longdesc"] = "" + + if options.has_key("--help"): + usage(device_opt) + sys.exit(0) + + if options.get("--action", "") == "metadata": + if "port_as_ip" in device_opt: + device_opt.remove("separator") + metadata(device_opt, docs) + sys.exit(0) + + if options.has_key("--version"): + print __main__.RELEASE_VERSION, __main__.BUILD_DATE + print __main__.REDHAT_COPYRIGHT + sys.exit(0) + +def fence_action(connection, options, set_power_fn, get_power_fn, get_outlet_list=None, reboot_cycle_fn=None): + result = 0 + + try: + if options.has_key("--plug"): + options["--plugs"] = options["--plug"].split(",") + + ## Process options that manipulate fencing device + ##### + if (options["--action"] in ["list", "list-status"]) or \ + ((options["--action"] == "monitor") and 1 == options["device_opt"].count("port") and \ + 0 == options["device_opt"].count("port_as_ip")): + + if 0 == options["device_opt"].count("port"): + print "N/A" + elif get_outlet_list == None: + ## @todo: exception? + ## This is just temporal solution, we will remove default value + ## None as soon as all existing agent will support this operation + print "NOTICE: List option is not working on this device yet" + else: + options["--original-action"] = options["--action"] + options["--action"] = "list" + outlets = get_outlet_list(connection, options) + options["--action"] = options["--original-action"] + del options["--original-action"] + + ## keys can be numbers (port numbers) or strings (names of VM, UUID) + for outlet_id in outlets.keys(): + (alias, status) = outlets[outlet_id] + if status is None or (not status.upper() in ["ON", "OFF"]): + status = "UNKNOWN" + status = status.upper() + + if options["--action"] == "list": + print outlet_id + options["--separator"] + alias + elif options["--action"] == "list-status": + print outlet_id + options["--separator"] + alias + options["--separator"] + status + + return + + if options["--action"] == "monitor" and not "port" in options["device_opt"] and "no_status" in options["device_opt"]: + # Unable to do standard monitoring because 'status' action is not available + return 0 + + status = None + if not "no_status" in options["device_opt"]: + status = get_multi_power_fn(connection, options, get_power_fn) + if status != "on" and status != "off": + fail(EC_STATUS) + + if options["--action"] == status: + if not (status == "on" and "force_on" in options["device_opt"]): + print "Success: Already %s" % (status.upper()) + return 0 + + if options["--action"] == "on": + if set_multi_power_fn(connection, options, set_power_fn, get_power_fn, 1 + int(options["--retry-on"])): + print "Success: Powered ON" + else: + fail(EC_WAITING_ON) + elif options["--action"] == "off": + if set_multi_power_fn(connection, options, set_power_fn, get_power_fn): + print "Success: Powered OFF" + else: + fail(EC_WAITING_OFF) + elif options["--action"] == "reboot": + power_on = False + if options.get("--method", "").lower() == "cycle" and reboot_cycle_fn is not None: + for _ in range(1, 1 + int(options["--retry-on"])): + if reboot_cycle_fn(connection, options): + power_on = True + break + + if not power_on: + fail(EC_TIMED_OUT) + + else: + if status != "off": + options["--action"] = "off" + if not set_multi_power_fn(connection, options, set_power_fn, get_power_fn): + fail(EC_WAITING_OFF) + + options["--action"] = "on" + + try: + power_on = set_multi_power_fn(connection, options, set_power_fn, get_power_fn, int(options["--retry-on"])) + except Exception, ex: + # an error occured during power ON phase in reboot + # fence action was completed succesfully even in that case + logging.warning("%s", str(ex)) + + if power_on == False: + # this should not fail as node was fenced succesfully + logging.error('Timed out waiting to power ON\n') + + print "Success: Rebooted" + elif options["--action"] == "status": + print "Status: " + status.upper() + if status.upper() == "OFF": + result = 2 + elif options["--action"] == "monitor": + pass + except pexpect.EOF: + fail(EC_CONNECTION_LOST) + except pexpect.TIMEOUT: + fail(EC_TIMED_OUT) + except pycurl.error, ex: + logging.error("%s\n", str(ex)) + fail(EC_TIMED_OUT) + except socket.timeout, ex: + logging.error("%s\n", str(ex)) + fail(EC_TIMED_OUT) + + return result + +def fence_login(options, re_login_string=r"(login\s*: )|((?!Last )Login Name: )|(username: )|(User Name :)"): + run_delay(options) + + if not options.has_key("eol"): + options["eol"] = "\r\n" + + if options.has_key("--command-prompt") and type(options["--command-prompt"]) is not list: + options["--command-prompt"] = [options["--command-prompt"]] + + try: + if options.has_key("--ssl"): + conn = _open_ssl_connection(options) + elif options.has_key("--ssh") and not options.has_key("--identity-file"): + conn = _login_ssh_with_password(options, re_login_string) + elif options.has_key("--ssh") and options.has_key("--identity-file"): + conn = _login_ssh_with_identity_file(options) + else: + conn = _login_telnet(options, re_login_string) + except pexpect.EOF, exception: + logging.debug("%s", str(exception)) + fail(EC_LOGIN_DENIED) + except pexpect.TIMEOUT, exception: + logging.debug("%s", str(exception)) + fail(EC_LOGIN_DENIED) + return conn + +def is_executable(path): + if os.path.exists(path): + stats = os.stat(path) + if stat.S_ISREG(stats.st_mode) and os.access(path, os.X_OK): + return True + return False + +def run_command(options, command, timeout=None, env=None, log_command=None): + if timeout is None and "--power-timeout" in options: + timeout = options["--power-timeout"] + if timeout is not None: + timeout = float(timeout) + + logging.info("Executing: %s\n", log_command or command) + + try: + process = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) + except OSError: + fail_usage("Unable to run %s\n" % command) + + thread = threading.Thread(target=process.wait) + thread.start() + thread.join(timeout) + if thread.is_alive(): + process.kill() + fail(EC_TIMED_OUT) + + status = process.wait() + + (pipe_stdout, pipe_stderr) = process.communicate() + process.stdout.close() + process.stderr.close() + + logging.debug("%s %s %s\n", str(status), str(pipe_stdout), str(pipe_stderr)) + + return (status, pipe_stdout, pipe_stderr) + +def run_delay(options): + ## Delay is important for two-node clusters fencing but we do not need to delay 'status' operations + if options["--action"] in ["off", "reboot"]: + logging.info("Delay %s second(s) before logging in to the fence device", options["--delay"]) + time.sleep(int(options["--delay"])) + +def fence_logout(conn, logout_string, sleep=0): + # Logout is not required part of fencing but we should attempt to do it properly + # In some cases our 'exit' command is faster and we can not close connection as it + # was already closed by fencing device + try: + conn.send_eol(logout_string) + time.sleep(sleep) + conn.close() + except exceptions.OSError: + pass + except pexpect.ExceptionPexpect: + pass + +# Convert array of format [[key1, value1], [key2, value2], ... [keyN, valueN]] to dict, where key is +# in format a.b.c.d...z and returned dict has key only z +def array_to_dict(array): + return dict([[x[0].split(".")[-1], x[1]] for x in array]) + +## Own logger handler that uses old-style syslog handler as otherwise everything is sourced +## from /dev/syslog +class SyslogLibHandler(logging.StreamHandler): + """ + A handler class that correctly push messages into syslog + """ + def emit(self, record): + syslog_level = { + logging.CRITICAL:syslog.LOG_CRIT, + logging.ERROR:syslog.LOG_ERR, + logging.WARNING:syslog.LOG_WARNING, + logging.INFO:syslog.LOG_INFO, + logging.DEBUG:syslog.LOG_DEBUG, + logging.NOTSET:syslog.LOG_DEBUG, + }[record.levelno] + + msg = self.format(record) + + # syslos.syslog can not have 0x00 character inside or exception is thrown + syslog.syslog(syslog_level, msg.replace("\x00", "\n")) + return + +def _open_ssl_connection(options): + gnutls_opts = "" + ssl_opts = "" + + if options.has_key("--notls"): + gnutls_opts = "--priority \"NORMAL:-VERS-TLS1.2:-VERS-TLS1.1:-VERS-TLS1.0:+VERS-SSL3.0\"" + elif options.has_key("--tls1.0"): + gnutls_opts = "--priority \"NORMAL:-VERS-TLS1.2:-VERS-TLS1.1:+VERS-TLS1.0:%LATEST_RECORD_VERSION\"" + + # --ssl is same as the --ssl-secure; it means we want to verify certificate in these cases + if options.has_key("--ssl-insecure"): + ssl_opts = "--insecure" + + command = '%s %s %s --crlf -p %s %s' % \ + (options["--gnutlscli-path"], gnutls_opts, ssl_opts, options["--ipport"], options["--ip"]) + try: + conn = fspawn(options, command) + except pexpect.ExceptionPexpect, ex: + logging.error("%s\n", str(ex)) + sys.exit(EC_GENERIC_ERROR) + + return conn + +def _login_ssh_with_identity_file(options): + if options.has_key("--inet6-only"): + force_ipvx = "-6 " + elif options.has_key("--inet4-only"): + force_ipvx = "-4 " + else: + force_ipvx = "" + + command = '%s %s %s@%s -i %s -p %s' % \ + (options["--ssh-path"], force_ipvx, options["--username"], options["--ip"], \ + options["--identity-file"], options["--ipport"]) + if options.has_key("--ssh-options"): + command += ' ' + options["--ssh-options"] + + conn = fspawn(options, command) + + result = conn.log_expect(["Enter passphrase for key '" + options["--identity-file"] + "':", \ + "Are you sure you want to continue connecting (yes/no)?"] + \ + options["--command-prompt"], int(options["--login-timeout"])) + if result == 1: + conn.sendline("yes") + result = conn.log_expect( + ["Enter passphrase for key '" + options["--identity-file"]+"':"] + \ + options["--command-prompt"], int(options["--login-timeout"])) + if result == 0: + if options.has_key("--password"): + conn.sendline(options["--password"]) + conn.log_expect(options["--command-prompt"], int(options["--login-timeout"])) + else: + fail_usage("Failed: You have to enter passphrase (-p) for identity file") + + return conn + +def _login_telnet(options, re_login_string): + re_login = re.compile(re_login_string, re.IGNORECASE) + re_pass = re.compile("(password)|(pass phrase)", re.IGNORECASE) + + conn = fspawn(options, options["--telnet-path"]) + conn.send("set binary\n") + conn.send("open %s -%s\n"%(options["--ip"], options["--ipport"])) + + conn.log_expect(re_login, int(options["--login-timeout"])) + conn.send_eol(options["--username"]) + + ## automatically change end of line separator + screen = conn.read_nonblocking(size=100, timeout=int(options["--shell-timeout"])) + if re_login.search(screen) != None: + options["eol"] = "\n" + conn.send_eol(options["--username"]) + conn.log_expect(re_pass, int(options["--login-timeout"])) + elif re_pass.search(screen) == None: + conn.log_expect(re_pass, int(options["--shell-timeout"])) + + try: + conn.send_eol(options["--password"]) + valid_password = conn.log_expect([re_login] + \ + options["--command-prompt"], int(options["--shell-timeout"])) + if valid_password == 0: + ## password is invalid or we have to change EOL separator + options["eol"] = "\r" + conn.send_eol("") + screen = conn.read_nonblocking(size=100, timeout=int(options["--shell-timeout"])) + ## after sending EOL the fence device can either show 'Login' or 'Password' + if re_login.search(conn.after + screen) != None: + conn.send_eol("") + conn.send_eol(options["--username"]) + conn.log_expect(re_pass, int(options["--login-timeout"])) + conn.send_eol(options["--password"]) + conn.log_expect(options["--command-prompt"], int(options["--login-timeout"])) + except KeyError: + fail(EC_PASSWORD_MISSING) + + return conn + +def _login_ssh_with_password(options, re_login_string): + re_login = re.compile(re_login_string, re.IGNORECASE) + re_pass = re.compile("(password)|(pass phrase)", re.IGNORECASE) + + if options.has_key("--inet6-only"): + force_ipvx = "-6 " + elif options.has_key("--inet4-only"): + force_ipvx = "-4 " + else: + force_ipvx = "" + + command = '%s %s %s@%s -p %s -o PubkeyAuthentication=no' % \ + (options["--ssh-path"], force_ipvx, options["--username"], options["--ip"], options["--ipport"]) + if options.has_key("--ssh-options"): + command += ' ' + options["--ssh-options"] + + conn = fspawn(options, command) + + if options.has_key("telnet_over_ssh"): + # This is for stupid ssh servers (like ALOM) which behave more like telnet + # (ignore name and display login prompt) + result = conn.log_expect( \ + [re_login, "Are you sure you want to continue connecting (yes/no)?"], + int(options["--login-timeout"])) + if result == 1: + conn.sendline("yes") # Host identity confirm + conn.log_expect(re_login, int(options["--login-timeout"])) + + conn.sendline(options["--username"]) + conn.log_expect(re_pass, int(options["--login-timeout"])) + else: + result = conn.log_expect( \ + ["ssword:", "Are you sure you want to continue connecting (yes/no)?"], + int(options["--login-timeout"])) + if result == 1: + conn.sendline("yes") + conn.log_expect("ssword:", int(options["--login-timeout"])) + + conn.sendline(options["--password"]) + conn.log_expect(options["--command-prompt"], int(options["--login-timeout"])) + + return conn + +# +# To update metadata, we change values in all_opt +def _update_metadata(options): + device_opt = options["device_opt"] + + if device_opt.count("login") and device_opt.count("no_login") == 0: + all_opt["login"]["required"] = "1" + else: + all_opt["login"]["required"] = "0" + + if device_opt.count("port_as_ip"): + all_opt["ipaddr"]["required"] = "0" + all_opt["port"]["required"] = "0" + + (available_actions, default_value) = _get_available_actions(device_opt) + all_opt["action"]["default"] = default_value + + actions_with_default = \ + [x if not x == all_opt["action"]["default"] else x + " (default)" for x in available_actions] + all_opt["action"]["help"] = \ + "-o, --action=[action] Action: %s" % (_join_wrap(actions_with_default, last_separator=" or ")) + + if device_opt.count("ipport"): + default_value = None + default_string = None + + if all_opt["ipport"].has_key("default"): + default_value = all_opt["ipport"]["default"] + elif device_opt.count("web") and device_opt.count("ssl"): + default_value = "80" + default_string = "(default 80, 443 if --ssl option is used)" + elif device_opt.count("telnet") and device_opt.count("secure"): + default_value = "23" + default_string = "(default 23, 22 if --ssh option is used)" + else: + tcp_ports = {"community" : "161", "secure" : "22", "telnet" : "23", "web" : "80", "ssl" : "443"} + # all cases where next command returns multiple results are covered by previous blocks + protocol = [x for x in ["community", "secure", "ssl", "web", "telnet"] if device_opt.count(x)][0] + default_value = tcp_ports[protocol] + + if default_string is None: + all_opt["ipport"]["help"] = "-u, --ipport=[port] TCP/UDP port to use (default %s)" % \ + (default_value) + else: + all_opt["ipport"]["help"] = "-u, --ipport=[port] TCP/UDP port to use\n" + " "*40 + default_string + +def _set_default_values(options): + if "ipport" in options["device_opt"]: + if not "--ipport" in options: + if "default" in all_opt["ipport"]: + options["--ipport"] = all_opt["ipport"]["default"] + elif "community" in options["device_opt"]: + options["--ipport"] = "161" + elif "--ssh" in options or all_opt["secure"].get("default", "0") == "1": + options["--ipport"] = "22" + elif "--ssl" in options or all_opt["ssl"].get("default", "0") == "1": + options["--ipport"] = "443" + elif "--ssl-secure" in options or all_opt["ssl_secure"].get("default", "0") == "1": + options["--ipport"] = "443" + elif "--ssl-insecure" in options or all_opt["ssl_insecure"].get("default", "0") == "1": + options["--ipport"] = "443" + elif "web" in options["device_opt"]: + options["--ipport"] = "80" + elif "telnet" in options["device_opt"]: + options["--ipport"] = "23" + + if "--ipport" in options: + all_opt["ipport"]["default"] = options["--ipport"] + + for opt in options["device_opt"]: + if all_opt[opt].has_key("default") and not opt == "ipport": + getopt_long = "--" + all_opt[opt]["longopt"] + if not options.has_key(getopt_long): + options[getopt_long] = all_opt[opt]["default"] + + return options + +# stop = True/False : exit fence agent when problem is encountered +def _validate_input(options, stop = True): + device_opt = options["device_opt"] + valid_input = True + + if not options.has_key("--username") and \ + device_opt.count("login") and (device_opt.count("no_login") == 0): + valid_input = False + fail_usage("Failed: You have to set login name", stop) + + if device_opt.count("ipaddr") and not options.has_key("--ip") and not options.has_key("--managed"): + valid_input = False + fail_usage("Failed: You have to enter fence address", stop) + + if device_opt.count("no_password") == 0: + if 0 == device_opt.count("identity_file"): + if not (options.has_key("--password") or options.has_key("--password-script")): + valid_input = False + fail_usage("Failed: You have to enter password or password script", stop) + else: + if not (options.has_key("--password") or \ + options.has_key("--password-script") or options.has_key("--identity-file")): + valid_input = False + fail_usage("Failed: You have to enter password, password script or identity file", stop) + + if not options.has_key("--ssh") and options.has_key("--identity-file"): + valid_input = False + fail_usage("Failed: You have to use identity file together with ssh connection (-x)", stop) + + if options.has_key("--identity-file") and not os.path.isfile(options["--identity-file"]): + valid_input = False + fail_usage("Failed: Identity file " + options["--identity-file"] + " does not exist", stop) + + if (0 == ["list", "list-status", "monitor"].count(options["--action"])) and \ + not options.has_key("--plug") and device_opt.count("port") and \ + device_opt.count("no_port") == 0 and not device_opt.count("port_as_ip"): + valid_input = False + fail_usage("Failed: You have to enter plug number or machine identification", stop) + + if options.has_key("--plug") and len(options["--plug"].split(",")) > 1 and \ + options.has_key("--method") and options["--method"] == "cycle": + valid_input = False + fail_usage("Failed: Cannot use --method cycle for more than 1 plug", stop) + + for failed_opt in _get_opts_with_invalid_choices(options): + valid_input = False + fail_usage("Failed: You have to enter a valid choice for %s from the valid values: %s" % \ + ("--" + all_opt[failed_opt]["longopt"], str(all_opt[failed_opt]["choices"])), stop) + + return valid_input + +def _encode_html_entities(text): + return text.replace("&", "&").replace('"', """).replace('<', "<"). \ + replace('>', ">").replace("'", "'") + +def _prepare_getopt_args(options): + getopt_string = "" + longopt_list = [] + for k in options: + if all_opt.has_key(k) and all_opt[k]["getopt"] != ":": + # getopt == ":" means that opt is without short getopt, but has value + getopt_string += all_opt[k]["getopt"] + elif not all_opt.has_key(k): + fail_usage("Parse error: unknown option '"+k+"'") + + if all_opt.has_key(k) and all_opt[k].has_key("longopt"): + if all_opt[k]["getopt"].endswith(":"): + longopt_list.append(all_opt[k]["longopt"] + "=") + else: + longopt_list.append(all_opt[k]["longopt"]) + + return (getopt_string, longopt_list) + +def _parse_input_stdin(avail_opt): + 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 avail_opt.count(name) == 0 and name in ["nodename"]: + continue + elif avail_opt.count(name) == 0: + logging.warning("Parse error: Ignoring unknown option '%s'\n", line) + continue + + if all_opt[name]["getopt"].endswith(":"): + opt["--"+all_opt[name]["longopt"].rstrip(":")] = value + elif value.lower() in ["1", "yes", "on", "true"]: + opt["--"+all_opt[name]["longopt"]] = "1" + else: + logging.warning("Parse error: Ignoring option '%s' because it does not have value\n", name) + return opt + +def _parse_input_cmdline(avail_opt): + filtered_opts = {} + _verify_unique_getopt(avail_opt) + (getopt_string, longopt_list) = _prepare_getopt_args(avail_opt) + + try: + (entered_opt, left_arg) = getopt.gnu_getopt(sys.argv[1:], getopt_string, longopt_list) + if len(left_arg) > 0: + logging.warning("Unused arguments on command line: %s" % (str(left_arg))) + except getopt.GetoptError, error: + fail_usage("Parse error: " + error.msg) + + for opt in avail_opt: + filtered_opts.update({opt : all_opt[opt]}) + + # Short and long getopt names are changed to consistent "--" + long name (e.g. --username) + long_opts = {} + for arg_name in dict(entered_opt).keys(): + all_key = [key for (key, value) in filtered_opts.items() \ + if "--" + value.get("longopt", "") == arg_name or "-" + value.get("getopt", "").rstrip(":") == arg_name][0] + long_opts["--" + filtered_opts[all_key]["longopt"]] = dict(entered_opt)[arg_name] + + # This test is specific because it does not apply to input on stdin + if "port_as_ip" in avail_opt and not "--port-as-ip" in long_opts and "--plug" in long_opts: + fail_usage("Parser error: option -n/--plug is not recognized") + + return long_opts + +# for ["John", "Mary", "Eli"] returns "John, Mary and Eli" +def _join2(words, normal_separator=", ", last_separator=" and "): + if len(words) <= 1: + return "".join(words) + else: + return last_separator.join([normal_separator.join(words[:-1]), words[-1]]) + +def _join_wrap(words, normal_separator=", ", last_separator=" and ", first_indent=42): + x = _join2(words, normal_separator, last_separator) + wrapper = textwrap.TextWrapper() + wrapper.initial_indent = " "*first_indent + wrapper.subsequent_indent = " "*40 + wrapper.width = 85 + wrapper.break_on_hyphens = False + wrapper.break_long_words = False + wrapped_text = "" + for line in wrapper.wrap(x): + wrapped_text += line + "\n" + return wrapped_text.lstrip().rstrip("\n") + +def _get_opts_with_invalid_choices(options): + options_failed = [] + device_opt = options["device_opt"] + + for opt in device_opt: + if all_opt[opt].has_key("choices"): + longopt = "--" + all_opt[opt]["longopt"] + possible_values_upper = [y.upper() for y in all_opt[opt]["choices"]] + if options.has_key(longopt): + options[longopt] = options[longopt].upper() + if not options["--" + all_opt[opt]["longopt"]] in possible_values_upper: + options_failed.append(opt) + return options_failed + +def _verify_unique_getopt(avail_opt): + used_getopt = set() + + for opt in avail_opt: + getopt_value = all_opt[opt].get("getopt", "").rstrip(":") + if getopt_value and getopt_value in used_getopt: + fail_usage("Short getopt for %s (-%s) is not unique" % (opt, getopt_value)) + else: + used_getopt.add(getopt_value) + +def _get_available_actions(device_opt): + available_actions = ["on", "off", "reboot", "status", "list", "list-status", \ + "monitor", "metadata", "validate-all"] + default_value = "reboot" + + if device_opt.count("fabric_fencing"): + available_actions.remove("reboot") + default_value = "off" + if device_opt.count("no_status"): + available_actions.remove("status") + if device_opt.count("no_on"): + available_actions.remove("on") + if device_opt.count("no_off"): + available_actions.remove("off") + if not device_opt.count("separator"): + available_actions.remove("list") + available_actions.remove("list-status") + + return (available_actions, default_value) |