Thanks to Frank Brendel (author of original perl fence_pve) for help with writing and testing this agent.
--- .gitignore | 1 + configure.ac | 1 + fence/agents/pve/Makefile.am | 18 ++++ fence/agents/pve/fence_pve.py | 180 ++++++++++++++++++++++++++++++++++++++ tests/data/metadata/fence_pve.xml | 121 +++++++++++++++++++++++++ 5 files changed, 321 insertions(+) create mode 100644 fence/agents/pve/Makefile.am create mode 100644 fence/agents/pve/fence_pve.py create mode 100644 tests/data/metadata/fence_pve.xml diff --git a/.gitignore b/.gitignore index 74198b2..75b9aa7 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ fence/agents/node_assassin/fence_na.conf fence/agents/node_assassin/fence_na.lib fence/agents/node_assassin/fence_na.pod fence/agents/nss_wrapper/fence_nss_wrapper +fence/agents/pve/fence_pve fence/agents/rackswitch/fence_rackswitch fence/agents/rhevm/fence_rhevm fence/agents/raritan/fence_raritan diff --git a/configure.ac b/configure.ac index c24198c..5e37ee5 100644 --- a/configure.ac +++ b/configure.ac @@ -283,6 +283,7 @@ AC_CONFIG_FILES([Makefile fence/agents/netio/Makefile fence/agents/rackswitch/Makefile fence/agents/ovh/Makefile + fence/agents/pve/Makefile fence/agents/raritan/Makefile fence/agents/rhevm/Makefile fence/agents/rsa/Makefile diff --git a/fence/agents/pve/Makefile.am b/fence/agents/pve/Makefile.am new file mode 100644 index 0000000..9ac184e --- /dev/null +++ b/fence/agents/pve/Makefile.am @@ -0,0 +1,18 @@ +MAINTAINERCLEANFILES = Makefile.in + +TARGET = fence_pve + +SRC = $(TARGET).py + +EXTRA_DIST = $(SRC) + +sbin_SCRIPTS = $(TARGET) + +man_MANS = $(TARGET).8 + +include $(top_srcdir)/make/fencebuild.mk +include $(top_srcdir)/make/fenceman.mk +include $(top_srcdir)/make/agentpycheck.mk + +clean-local: clean-man + rm -f $(TARGET) diff --git a/fence/agents/pve/fence_pve.py b/fence/agents/pve/fence_pve.py new file mode 100644 index 0000000..4b96347 --- /dev/null +++ b/fence/agents/pve/fence_pve.py @@ -0,0 +1,180 @@ +#!/usr/bin/python -tt + +# This agent uses Proxmox VE API +# Thanks to Frank Brendel (author of original perl fence_pve) +# for help with writing and testing this agent. + +import sys, json, pycurl, StringIO, urllib, atexit, logging +sys.path.append("@FENCEAGENTSLIBDIR@") +from fencing import * + +#BEGIN_VERSION_GENERATION +RELEASE_VERSION="" +BUILD_DATE="" +REDHAT_COPYRIGHT="" +#END_VERSION_GENERATION + +def get_power_status(conn, options): + del conn + state = {"running" : "on", "stopped" : "off"} + if options["node"] is None: + nodes = send_cmd(options, "nodes") + if type(nodes) is not dict or "data" not in nodes or type(nodes["data"]) is not list: + return None + for node in nodes["data"]: # lookup the node holding the vm + if type(node) is not dict or "node" not in node: + return None + options["node"] = node["node"] + status = get_power_status(None, options) + if status is not None: + logging.info("vm found on node: " + options["node"]) + break + else: + options["node"] = None + return status + else: + cmd = "nodes/" + options["node"] + "/qemu/" + options["--plug"] + "/status/current" + result = send_cmd(options, cmd) + if type(result) is dict and "data" in result: + if type(result["data"]) is dict and "status" in result["data"]: + if result["data"]["status"] in state: + return state[result["data"]["status"]] + return None + +def set_power_status(conn, options): + del conn + action = { + 'on' : "start", + 'off': "stop" + }[options["--action"]] + cmd = "nodes/" + options["node"] + "/qemu/" + options["--plug"] + "/status/" + action + send_cmd(options, cmd, post={"skiplock":1}) + +def pve_monitor(options): + pve_version = send_cmd(options, "version") + if type(pve_version) is not dict: + fail_usage("Failed: Cannot get version of Proxmox") + print "Proxmox version: " + pve_version["data"]["version"] + "-" +\ + pve_version["data"]["release"] + "/" + pve_version["data"]["repoid"] + "\n" + return 0 + +def get_outlet_list(conn, options): + del conn + nodes = send_cmd(options, "nodes") + outlets = dict() + if type(nodes) is not dict or "data" not in nodes or type(nodes["data"]) is not list: + return None + for node in nodes["data"]: + if type(node) is not dict or "node" not in node: + return None + vms = send_cmd(options, "nodes/" + node["node"] + "/qemu") + if type(vms) is not dict or "data" not in vms or type(vms["data"]) is not list: + return None + for vm in vms["data"]: + outlets[vm["vmid"]] = [vm["name"], vm["status"]] + return outlets + +def get_ticket(options): + post = {'username': options["--username"], 'password': options["--password"]} + result = send_cmd(options, "access/ticket", post=post) + if type(result) is dict and "data" in result: + if type(result["data"]) is dict and "ticket" in result["data"] and "CSRFPreventionToken" in result["data"]: + return { + "ticket" : str("PVEAuthCookie=" + result["data"]["ticket"] + "; " + \ + "version=0; path=/; domain=" + options["--ip"] + \ + "; port=" + str(options["--ipport"]) + "; path_spec=0; secure=1; " + \ + "expires=7200; discard=0"), + "CSRF_token" : str("CSRFPreventionToken: " + result["data"]["CSRFPreventionToken"]) + } + return None + +def send_cmd(options, cmd, post=None): + url = options["url"] + cmd + conn = pycurl.Curl() + output_buffer = StringIO.StringIO() + if logging.getLogger().getEffectiveLevel() < logging.WARNING: + conn.setopt(pycurl.VERBOSE, True) + conn.setopt(pycurl.HTTPGET, 1) + conn.setopt(pycurl.URL, str(url)) + if "auth" in options and options["auth"] is not None: + conn.setopt(pycurl.COOKIE, options["auth"]["ticket"]) + conn.setopt(pycurl.HTTPHEADER, [options["auth"]["CSRF_token"]]) + if post is not None: + conn.setopt(pycurl.POSTFIELDS, urllib.urlencode(post)) + conn.setopt(pycurl.WRITEFUNCTION, output_buffer.write) + conn.setopt(pycurl.TIMEOUT, int(options["--shell-timeout"])) + conn.setopt(pycurl.SSL_VERIFYPEER, 0) + conn.setopt(pycurl.SSL_VERIFYHOST, 0) + + logging.debug("URL: " + url) + + try: + conn.perform() + result = output_buffer.getvalue() + + logging.debug("RESULT [" + str(conn.getinfo(pycurl.RESPONSE_CODE)) + \ + "]: " + result) + conn.close() + + return json.loads(result) + except pycurl.error: + logging.error("Connection failed") + except: + logging.error("Cannot parse json") + return None + +def main(): + atexit.register(atexit_handler) + + all_opt["node_name"] = { + "getopt" : "N:", + "longopt" : "nodename", + "help" : "-N, --nodename " + "Node on which machine is located", + "required" : "0", + "shortdesc" : "Node on which machine is located. " + "(Optional, will be automatically determined)", + "order": 2 + } + + device_opt = ["ipaddr", "login", "passwd", "web", "port", "node_name"] + + all_opt["login"]["required"] = "0" + all_opt["login"]["default"] = "root@pam" + all_opt["ipport"]["default"] = "8006" + all_opt["port"]["shortdesc"] = "Id of the virtual machine." + all_opt["ipaddr"]["shortdesc"] = "IP Address or Hostname of a node " +\ + "within the Proxmox cluster." + + options = check_input(device_opt, process_input(device_opt)) + docs = {} + docs["shortdesc"] = "Fencing agent for the Proxmox Virtual Environment" + docs["longdesc"] = "The fence_pve agent can be used to fence virtual \ +machines acting as nodes in a virtualized cluster." + docs["vendorurl"] = "http://www.proxmox.com/" + + show_docs(options, docs) + + ## Do the delay of the fence device before logging in + if options["--action"] in ["on", "off", "reboot"]: + time.sleep(int(options["--delay"])) + + options["node"] = None + options["url"] = "https://" + options["--ip"] + ":" + str(options["--ipport"]) + "/api2/json/" + + options["auth"] = get_ticket(options) + if options["auth"] is None: + fail(EC_LOGIN_DENIED) + + if "--nodename" in options and options["--nodename"]: + options["node"] = options["--nodename"] + + if options["--action"] == "monitor": + result = pve_monitor(options) + else: + result = fence_action(None, options, set_power_status, get_power_status, get_outlet_list) + + sys.exit(result) + +if __name__ == "__main__": + main() diff --git a/tests/data/metadata/fence_pve.xml b/tests/data/metadata/fence_pve.xml new file mode 100644 index 0000000..86c3cd7 --- /dev/null +++ b/tests/data/metadata/fence_pve.xml @@ -0,0 +1,121 @@ +<?xml version="1.0" ?> +<resource-agent name="fence_pve" shortdesc="Fencing agent for the Proxmox Virtual Environment" > +<longdesc>The fence_pve agent can be used to fence virtual machines acting as nodes in a virtualized cluster.</longdesc> +<vendor-url>http://www.proxmox.com/</vendor-url> +<parameters> + <parameter name="ipport" unique="0" required="0"> + <getopt mixed="-u, --ipport=[port]" /> + <content type="string" default="8006" /> + <shortdesc lang="en">TCP/UDP port to use for connection with device</shortdesc> + </parameter> + <parameter name="port" unique="0" required="1"> + <getopt mixed="-n, --plug=[id]" /> + <content type="string" /> + <shortdesc lang="en">Id of the virtual machine.</shortdesc> + </parameter> + <parameter name="inet6_only" unique="0" required="0"> + <getopt mixed="-6, --inet6-only" /> + <content type="boolean" /> + <shortdesc lang="en">Forces agent to use IPv6 addresses only</shortdesc> + </parameter> + <parameter name="ipaddr" unique="0" required="1"> + <getopt mixed="-a, --ip=[ip]" /> + <content type="string" /> + <shortdesc lang="en">IP Address or Hostname of a node within the Proxmox cluster.</shortdesc> + </parameter> + <parameter name="inet4_only" unique="0" required="0"> + <getopt mixed="-4, --inet4-only" /> + <content type="boolean" /> + <shortdesc lang="en">Forces agent to use IPv4 addresses only</shortdesc> + </parameter> + <parameter name="passwd_script" unique="0" required="0"> + <getopt mixed="-S, --password-script=[script]" /> + <content type="string" /> + <shortdesc lang="en">Script to retrieve password</shortdesc> + </parameter> + <parameter name="passwd" unique="0" required="0"> + <getopt mixed="-p, --password=[password]" /> + <content type="string" /> + <shortdesc lang="en">Login password or passphrase</shortdesc> + </parameter> + <parameter name="action" unique="0" required="1"> + <getopt mixed="-o, --action=[action]" /> + <content type="string" default="reboot" /> + <shortdesc lang="en">Fencing Action</shortdesc> + </parameter> + <parameter name="login" unique="0" required="1"> + <getopt mixed="-l, --username=[name]" /> + <content type="string" default="root@pam" /> + <shortdesc lang="en">Login Name</shortdesc> + </parameter> + <parameter name="node_name" unique="0" required="0"> + <getopt mixed="-N, --nodename" /> + <content type="string" /> + <shortdesc lang="en">Node on which machine is located. (Optional, will be automatically determined)</shortdesc> + </parameter> + <parameter name="verbose" unique="0" required="0"> + <getopt mixed="-v, --verbose" /> + <content type="boolean" /> + <shortdesc lang="en">Verbose mode</shortdesc> + </parameter> + <parameter name="debug" unique="0" required="0"> + <getopt mixed="-D, --debug-file=[debugfile]" /> + <content type="string" /> + <shortdesc lang="en">Write debug information to given file</shortdesc> + </parameter> + <parameter name="version" unique="0" required="0"> + <getopt mixed="-V, --version" /> + <content type="boolean" /> + <shortdesc lang="en">Display version information and exit</shortdesc> + </parameter> + <parameter name="help" unique="0" required="0"> + <getopt mixed="-h, --help" /> + <content type="boolean" /> + <shortdesc lang="en">Display help and exit</shortdesc> + </parameter> + <parameter name="separator" unique="0" required="0"> + <getopt mixed="-C, --separator=[char]" /> + <content type="string" default="," /> + <shortdesc lang="en">Separator for CSV created by operation list</shortdesc> + </parameter> + <parameter name="power_wait" unique="0" required="0"> + <getopt mixed="--power-wait=[seconds]" /> + <content type="string" default="0" /> + <shortdesc lang="en">Wait X seconds after issuing ON/OFF</shortdesc> + </parameter> + <parameter name="login_timeout" unique="0" required="0"> + <getopt mixed="--login-timeout=[seconds]" /> + <content type="string" default="5" /> + <shortdesc lang="en">Wait X seconds for cmd prompt after login</shortdesc> + </parameter> + <parameter name="power_timeout" unique="0" required="0"> + <getopt mixed="--power-timeout=[seconds]" /> + <content type="string" default="20" /> + <shortdesc lang="en">Test X seconds for status change after ON/OFF</shortdesc> + </parameter> + <parameter name="delay" unique="0" required="0"> + <getopt mixed="--delay=[seconds]" /> + <content type="string" default="0" /> + <shortdesc lang="en">Wait X seconds before fencing is started</shortdesc> + </parameter> + <parameter name="shell_timeout" unique="0" required="0"> + <getopt mixed="--shell-timeout=[seconds]" /> + <content type="string" default="3" /> + <shortdesc lang="en">Wait X seconds for cmd prompt after issuing command</shortdesc> + </parameter> + <parameter name="retry_on" unique="0" required="0"> + <getopt mixed="--retry-on=[attempts]" /> + <content type="string" default="1" /> + <shortdesc lang="en">Count of attempts to retry power on</shortdesc> + </parameter> +</parameters> +<actions> + <action name="on" automatic="0"/> + <action name="off" /> + <action name="reboot" /> + <action name="status" /> + <action name="list" /> + <action name="monitor" /> + <action name="metadata" /> +</actions> +</resource-agent> -- 1.8.3.1