Hi Fabio, Looks like I had a few problems moving the code over to git. Your comments all sound perfectly valid. Do you want me to fix up and resubmit? Cheers, Matt.
> Date: Mon, 11 Apr 2011 07:01:29 +0200 > From: [email protected] > To: [email protected]; [email protected] > Subject: Re: [Cluster-devel] [PATCH 1/3] New fencing script for Citrix > XenServer and XCP. > > Hi Matt, > > I'll leave the comments to the code to Marek, but we will need to do a > few adjustments here and there to get this into the tree. > > XenAPI.py being a fence library needs to move into the libs/ section. It > is not parsed/built/installed in this patch set. > > You sent us pre-built sources and that's not really ok. See for example > the hardcoded path to the fence library and the addition of > RELEASE_VERSION and BUILD_DATE at the end of the code for fence_xenapi. > Those should be generated dynamically at build time to reflect the build > info. > > Because I really like to make sure that COPYRIGHT and licences > information are as exact and clear as possible, please also update > docs/COPYRIGHT in the "exception" area. While the information in the > file are absolutely fine, most people tend to look for a > COPYRIGHT/LICENCE file rather than editing the file itself. It's fair > that your contribution deserves the right credit and such. > > Cheers > Fabio > > On 04/09/2011 06:54 AM, Matt Clark wrote: > > 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)" >
