[Cluster-devel] [PATCH 1/3] New fencing script for Citrix XenServer and XCP.
Matt Clark
mattjclark0407 at hotmail.com
Sat Apr 9 04:54:33 UTC 2011
Fencing script that uses the XenAPI to allow remote switch, status and list of virtual machines running on Citrix XenServer and Xen Cloud Platform hosts.
---
fence/agents/xenapi/Makefile.am | 17 +++
fence/agents/xenapi/XenAPI.py | 209 ++++++++++++++++++++++++++++++++
fence/agents/xenapi/fence_xenapi.py | 227 +++++++++++++++++++++++++++++++++++
3 files changed, 453 insertions(+), 0 deletions(-)
create mode 100644 fence/agents/xenapi/Makefile.am
create mode 100755 fence/agents/xenapi/XenAPI.py
create mode 100644 fence/agents/xenapi/fence_xenapi.py
diff --git a/fence/agents/xenapi/Makefile.am b/fence/agents/xenapi/Makefile.am
new file mode 100644
index 0000000..781975e
--- /dev/null
+++ b/fence/agents/xenapi/Makefile.am
@@ -0,0 +1,17 @@
+MAINTAINERCLEANFILES = Makefile.in
+
+TARGET = fence_xenapi
+
+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
+
+clean-local: clean-man
+ rm -f $(TARGET)
diff --git a/fence/agents/xenapi/XenAPI.py b/fence/agents/xenapi/XenAPI.py
new file mode 100755
index 0000000..4f27ef5
--- /dev/null
+++ b/fence/agents/xenapi/XenAPI.py
@@ -0,0 +1,209 @@
+#============================================================================
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of version 2.1 of the GNU Lesser General Public
+# License as published by the Free Software Foundation.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#============================================================================
+# Copyright (C) 2006 XenSource Inc.
+#============================================================================
+#
+# Parts of this file are based upon xmlrpclib.py, the XML-RPC client
+# interface included in the Python distribution.
+#
+# Copyright (c) 1999-2002 by Secret Labs AB
+# Copyright (c) 1999-2002 by Fredrik Lundh
+#
+# By obtaining, using, and/or copying this software and/or its
+# associated documentation, you agree that you have read, understood,
+# and will comply with the following terms and conditions:
+#
+# Permission to use, copy, modify, and distribute this software and
+# its associated documentation for any purpose and without fee is
+# hereby granted, provided that the above copyright notice appears in
+# all copies, and that both that copyright notice and this permission
+# notice appear in supporting documentation, and that the name of
+# Secret Labs AB or the author not be used in advertising or publicity
+# pertaining to distribution of the software without specific, written
+# prior permission.
+#
+# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
+# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
+# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
+# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
+# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
+# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
+# OF THIS SOFTWARE.
+# --------------------------------------------------------------------
+
+import gettext
+import xmlrpclib
+import httplib
+import socket
+
+translation = gettext.translation('xen-xm', fallback = True)
+
+class Failure(Exception):
+ def __init__(self, details):
+ try:
+ # If this failure is MESSAGE_PARAMETER_COUNT_MISMATCH, then we
+ # correct the return values here, to account for the fact that we
+ # transparently add the session handle as the first argument.
+ if details[0] == 'MESSAGE_PARAMETER_COUNT_MISMATCH':
+ details[2] = str(int(details[2]) - 1)
+ details[3] = str(int(details[3]) - 1)
+
+ self.details = details
+ except Exception, exn:
+ self.details = ['INTERNAL_ERROR', 'Client-side: ' + str(exn)]
+
+ def __str__(self):
+ try:
+ return translation.ugettext(self.details[0]) % self._details_map()
+ except TypeError, exn:
+ return "Message database broken: %s.\nXen-API failure: %s" % \
+ (exn, str(self.details))
+ except Exception, exn:
+ import sys
+ print >>sys.stderr, exn
+ return "Xen-API failure: %s" % str(self.details)
+
+ def _details_map(self):
+ return dict([(str(i), self.details[i])
+ for i in range(len(self.details))])
+
+
+_RECONNECT_AND_RETRY = (lambda _ : ())
+
+class UDSHTTPConnection(httplib.HTTPConnection):
+ """ Stupid hacked up HTTPConnection subclass to allow HTTP over Unix domain
+ sockets. """
+ def connect(self):
+ path = self.host.replace("_", "/")
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.sock.connect(path)
+
+class UDSHTTP(httplib.HTTP):
+ _connection_class = UDSHTTPConnection
+
+class UDSTransport(xmlrpclib.Transport):
+ def make_connection(self, host):
+ return UDSHTTP(host)
+
+class Session(xmlrpclib.ServerProxy):
+ """A server proxy and session manager for communicating with Xend using
+ the Xen-API.
+
+ Example:
+
+ session = Session('http://localhost:9363/')
+ session.login_with_password('me', 'mypassword')
+ session.xenapi.VM.start(vm_uuid)
+ session.xenapi.session.logout()
+
+ For now, this class also supports the legacy XML-RPC API, using
+ session.xend.domain('Domain-0') and similar. This support will disappear
+ once there is a working Xen-API replacement for every call in the legacy
+ API.
+ """
+
+ def __init__(self, uri, transport=None, encoding=None, verbose=0,
+ allow_none=1):
+ xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding,
+ verbose, allow_none)
+ self._session = None
+ self.last_login_method = None
+ self.last_login_params = None
+
+
+ def xenapi_request(self, methodname, params):
+ if methodname.startswith('login'):
+ self._login(methodname, params)
+ return None
+ else:
+ retry_count = 0
+ while retry_count < 3:
+ full_params = (self._session,) + params
+ result = _parse_result(getattr(self, methodname)(*full_params))
+ if result == _RECONNECT_AND_RETRY:
+ retry_count += 1
+ if self.last_login_method:
+ self._login(self.last_login_method,
+ self.last_login_params)
+ else:
+ raise xmlrpclib.Fault(401, 'You must log in')
+ else:
+ return result
+ raise xmlrpclib.Fault(
+ 500, 'Tried 3 times to get a valid session, but failed')
+
+
+ def _login(self, method, params):
+ result = _parse_result(getattr(self, 'session.%s' % method)(*params))
+ if result == _RECONNECT_AND_RETRY:
+ raise xmlrpclib.Fault(
+ 500, 'Received SESSION_INVALID when logging in')
+ self._session = result
+ self.last_login_method = method
+ self.last_login_params = params
+
+
+ def __getattr__(self, name):
+ if name == 'xenapi':
+ return _Dispatcher(self.xenapi_request, None)
+ elif name.startswith('login'):
+ return lambda *params: self._login(name, params)
+ else:
+ return xmlrpclib.ServerProxy.__getattr__(self, name)
+
+def xapi_local():
+ return Session("http://_var_xapi_xapi/", transport=UDSTransport())
+
+def _parse_result(result):
+ if type(result) != dict or 'Status' not in result:
+ raise xmlrpclib.Fault(500, 'Missing Status in response from server' + result)
+ if result['Status'] == 'Success':
+ if 'Value' in result:
+ return result['Value']
+ else:
+ raise xmlrpclib.Fault(500,
+ 'Missing Value in response from server')
+ else:
+ if 'ErrorDescription' in result:
+ if result['ErrorDescription'][0] == 'SESSION_INVALID':
+ return _RECONNECT_AND_RETRY
+ else:
+ raise Failure(result['ErrorDescription'])
+ else:
+ raise xmlrpclib.Fault(
+ 500, 'Missing ErrorDescription in response from server')
+
+
+# Based upon _Method from xmlrpclib.
+class _Dispatcher:
+ def __init__(self, send, name):
+ self.__send = send
+ self.__name = name
+
+ def __repr__(self):
+ if self.__name:
+ return '<XenAPI._Dispatcher for %s>' % self.__name
+ else:
+ return '<XenAPI._Dispatcher>'
+
+ def __getattr__(self, name):
+ if self.__name is None:
+ return _Dispatcher(self.__send, name)
+ else:
+ return _Dispatcher(self.__send, "%s.%s" % (self.__name, name))
+
+ def __call__(self, *args):
+ return self.__send(self.__name, args)
diff --git a/fence/agents/xenapi/fence_xenapi.py b/fence/agents/xenapi/fence_xenapi.py
new file mode 100644
index 0000000..0657f4e
--- /dev/null
+++ b/fence/agents/xenapi/fence_xenapi.py
@@ -0,0 +1,227 @@
+#!/usr/bin/python
+#
+#############################################################################
+# Copyright 2011 Matt Clark
+# This file is part of fence-xenserver
+#
+# fence-xenserver is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# fence-xenserver is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Please let me know if you are using this script so that I can work out
+# whether I should continue support for it. mattjclark0407 at hotmail dot com
+#############################################################################
+
+#############################################################################
+# It's only just begun...
+# Current status: completely usable. This script is now working well and,
+# has a lot of functionality as a result of the fencing.py library and the
+# XenAPI libary.
+
+#############################################################################
+# Please let me know if you are using this script so that I can work out
+# whether I should continue support for it. mattjclark0407 at hotmail dot com
+
+import sys
+sys.path.append("/usr/lib/fence")
+from fencing import *
+import XenAPI
+
+EC_BAD_SESSION = 1
+# Find the status of the port given in the -U flag of options.
+def get_power_fn(session, options):
+ if options.has_key("-v"):
+ verbose = True
+ else:
+ verbose = False
+
+ try:
+ # Get a reference to the vm specified in the UUID or vm_name/port parameter
+ vm = return_vm_reference(session, options)
+ # Query the VM for its' associated parameters
+ record = session.xenapi.VM.get_record(vm);
+ # Check that we are not trying to manipulate a template or a control
+ # domain as they show up as VM's with specific properties.
+ if not(record["is_a_template"]) and not(record["is_control_domain"]):
+ status = record["power_state"]
+ if verbose: print "UUID:", record["uuid"], "NAME:", record["name_label"], "POWER STATUS:", record["power_state"]
+ # Note that the VM can be in the following states (from the XenAPI document)
+ # Halted: VM is offline and not using any resources.
+ # Paused: All resources have been allocated but the VM itself is paused and its vCPUs are not running
+ # Running: Running
+ # Paused: VM state has been saved to disk and it is nolonger running. Note that disks remain in-Use while
+ # We want to make sure that we only return the status "off" if the machine is actually halted as the status
+ # is checked before a fencing action. Only when the machine is Halted is it not consuming resources which
+ # may include whatever you are trying to protect with this fencing action.
+ return (status=="Halted" and "off" or "on")
+ except Exception, exn:
+ print str(exn)
+
+ return "Error"
+
+# Set the state of the port given in the -U flag of options.
+def set_power_fn(session, options):
+ action = options["-o"].lower()
+ if options.has_key("-v"):
+ verbose = True
+ else:
+ verbose = False
+
+ try:
+ # Get a reference to the vm specified in the UUID or vm_name/port parameter
+ vm = return_vm_reference(session, options)
+ # Query the VM for its' associated parameters
+ record = session.xenapi.VM.get_record(vm)
+ # Check that we are not trying to manipulate a template or a control
+ # domain as they show up as VM's with specific properties.
+ if not(record["is_a_template"]) and not(record["is_control_domain"]):
+ if( action == "on" ):
+ # Start the VM
+ session.xenapi.VM.start(vm, False, True)
+ elif( action == "off" ):
+ # Force shutdown the VM
+ session.xenapi.VM.hard_shutdown(vm)
+ elif( action == "reboot" ):
+ # Force reboot the VM
+ session.xenapi.VM.hard_reboot(vm)
+ except Exception, exn:
+ print str(exn);
+
+# Function to populate an array of virtual machines and their status
+def get_outlet_list(session, options):
+ result = {}
+ if options.has_key("-v"):
+ verbose = True
+ else:
+ verbose = False
+
+ try:
+ # Return an array of all the VM's on the host
+ vms = session.xenapi.VM.get_all()
+ for vm in vms:
+ # Query the VM for its' associated parameters
+ record = session.xenapi.VM.get_record(vm);
+ # Check that we are not trying to manipulate a template or a control
+ # domain as they show up as VM's with specific properties.
+ if not(record["is_a_template"]) and not(record["is_control_domain"]):
+ name = record["name_label"]
+ uuid = record["uuid"]
+ status = record["power_state"]
+ result[uuid] = (name, status)
+ if verbose: print "UUID:", record["uuid"], "NAME:", name, "POWER STATUS:", record["power_state"]
+ except Exception, exn:
+ print str(exn);
+
+ return result
+
+# Function to initiate the XenServer session via the XenAPI library.
+def connect_and_login(options):
+ url = options["-s"]
+ username = options["-l"]
+ password = options["-p"]
+
+ try:
+ # Create the XML RPC session to the specified URL.
+ session = XenAPI.Session(url);
+ # Login using the supplied credentials.
+ session.xenapi.login_with_password(username, password);
+ except Exception, exn:
+ print str(exn);
+ # http://sources.redhat.com/cluster/wiki/FenceAgentAPI says that for no connectivity
+ # the exit value should be 1. It doesn't say anything about failed logins, so
+ # until I hear otherwise it is best to keep this exit the same to make sure that
+ # anything calling this script (that uses the same information in the web page
+ # above) knows that this is an error condition, not a msg signifying a down port.
+ sys.exit(EC_BAD_SESSION);
+ return session;
+
+# return a reference to the VM by either using the UUID or the vm_name/port. If the UUID is set then
+# this is tried first as this is the only properly unique identifier.
+# Exceptions are not handled in this function, code that calls this must be ready to handle them.
+def return_vm_reference(session, options):
+ if options.has_key("-v"):
+ verbose = True
+ else:
+ verbose = False
+
+ # Case where the UUID has been specified
+ if options.has_key("-U"):
+ uuid = options["-U"].lower()
+ # When using the -n parameter for name, we get an error message (in verbose
+ # mode) that tells us that we didn't find a VM. To immitate that here we
+ # need to catch and re-raise the exception produced by get_by_uuid.
+ try:
+ return session.xenapi.VM.get_by_uuid(uuid)
+ except Exception,exn:
+ if verbose: print "No VM's found with a UUID of \"%s\"" %uuid
+ raise
+
+
+ # Case where the vm_name/port has been specified
+ if options.has_key("-n"):
+ vm_name = options["-n"]
+ vm_arr = session.xenapi.VM.get_by_name_label(vm_name)
+ # Need to make sure that we only have one result as the vm_name may
+ # not be unique. Average case, so do it first.
+ if len(vm_arr) == 1:
+ return vm_arr[0]
+ else:
+ if len(vm_arr) == 0:
+ if verbose: print "No VM's found with a name of \"%s\"" %vm_name
+ # NAME_INVALID used as the XenAPI throws a UUID_INVALID if it can't find
+ # a VM with the specified UUID. This should make the output look fairly
+ # consistent.
+ raise Exception("NAME_INVALID")
+ else:
+ if verbose: print "Multiple VM's have the name \"%s\", use UUID instead" %vm_name
+ raise Exception("MULTIPLE_VMS_FOUND")
+
+ # We should never get to this case as the input processing checks that either the UUID or
+ # the name parameter is set. Regardless of whether or not a VM is found the above if
+ # statements will return to the calling function (either by exception or by a reference
+ # to the VM).
+ raise Exception("VM_LOGIC_ERROR")
+
+def main():
+
+ device_opt = [ "help", "version", "agent", "quiet", "verbose", "debug", "action",
+ "login", "passwd", "passwd_script", "port", "test", "separator",
+ "no_login", "no_password", "power_timeout", "shell_timeout",
+ "login_timeout", "power_wait", "session_url", "uuid" ]
+
+ atexit.register(atexit_handler)
+
+ options=process_input(device_opt)
+
+ options = check_input(device_opt, options)
+
+ docs = { }
+ docs["shortdesc"] = "XenAPI based fencing for the Citrix XenServer virtual machines."
+ docs["longdesc"] = "\
+fence_cxs is an I/O Fencing agent used on Citrix XenServer hosts. \
+It uses the XenAPI, supplied by Citrix, to establish an XML-RPC sesssion \
+to a XenServer host. Once the session is established, further XML-RPC \
+commands are issued in order to switch on, switch off, restart and query \
+the status of virtual machines running on the host."
+ show_docs(options, docs)
+
+ xenSession = connect_and_login(options)
+
+ # Operate the fencing device
+ result = fence_action(xenSession, options, set_power_fn, get_power_fn, get_outlet_list)
+
+ sys.exit(result)
+
+if __name__ == "__main__":
+ main()
+RELEASE_VERSION="3.1.2.11-2b5b-dirty"
+BUILD_DATE="(built Fri Mar 25 22:57:28 EST 2011)"
--
1.7.0.4
More information about the Cluster-devel
mailing list