From 9f249f5d797e723ec87fab181d797cf6c36e36ac Mon Sep 17 00:00:00 2001 From: Peter Wells Date: Mon, 18 Sep 2023 09:03:57 +1000 Subject: [PATCH 1/2] Update fence_xenapi.py Fix to make --uuid work (so that this is actually usable). Added missing --plug-separator option (to prevent error message). Added ability to bypass SSL Certificate verification using --ssl-insecure --- agents/xenapi/fence_xenapi.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/agents/xenapi/fence_xenapi.py b/agents/xenapi/fence_xenapi.py index 10c8ee03e..8896cc741 100644 --- a/agents/xenapi/fence_xenapi.py +++ b/agents/xenapi/fence_xenapi.py @@ -127,9 +127,14 @@ def connect_and_login(options): username = options["--username"] password = options["--password"] + # Allow bypassing SSL verification for hosts without SSL certificates + skipVerification = False + if "--ssl-insecure" in options: + skipVerification = True + try: # Create the XML RPC session to the specified URL. - session = XenAPI.Session(url) + session = XenAPI.Session(url, None, None, 0, 1, skipVerification) # Login using the supplied credentials. session.xenapi.login_with_password(username, password) except Exception as exn: @@ -191,23 +196,42 @@ def return_vm_reference(session, options): # to the VM). raise Exception("VM_LOGIC_ERROR") +def define_new_opts(): + all_opt["uuid"] = { + "getopt" : ":", + "longopt" : "uuid", + "help" : "--uuid=[VM UUID] The VM UUID.", + "default" : "", + "required" : "0", + "shortdesc" : "The VM UUID", + "order" : 2} + all_opt["plug-separator"] = { + "getopt" : ":", + "longopt" : "plug-separator", + "help" : "--plug-separator=separator The saparator for multiple plugs.", + "default" : ":", + "required" : "0", + "shortdesc" : "The saparator for multiple plugs", + "order" : 3} + def main(): - device_opt = ["login", "passwd", "port", "no_login", "no_password", "session_url", "web"] + device_opt = ["login", "passwd", "port", "no_login", "no_password", "session_url", "web", "ssl", "uuid", "plug_separator"] atexit.register(atexit_handler) + define_new_opts() options = check_input(device_opt, process_input(device_opt)) docs = {} docs["shortdesc"] = "Fence agent for Citrix XenServer over XenAPI" docs["longdesc"] = "\ -fence_cxs is an I/O Fencing agent used on Citrix XenServer hosts. \ +fence_xenapi is an I/O Fencing agent used on Citrix XenServer and XCP-ng hosts. \ It uses the XenAPI, supplied by Citrix, to establish an XML-RPC session \ 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." - docs["vendorurl"] = "http://www.xenproject.org" + docs["vendorurl"] = "https://xenproject.org" show_docs(options, docs) run_delay(options) From fcaf33e29b0a483da6ee45a3e7f74f8cc421d7f7 Mon Sep 17 00:00:00 2001 From: Peter Wells Date: Tue, 19 Sep 2023 08:38:50 +1000 Subject: [PATCH 2/2] Update XenAPI.py.py Update XenAPI to latest version from xapi-project/xen-api repo --- lib/XenAPI.py.py | 366 +++++++++++++++++++++++++++-------------------- 1 file changed, 207 insertions(+), 159 deletions(-) diff --git a/lib/XenAPI.py.py b/lib/XenAPI.py.py index 6fe11ef6f..ab868e921 100644 --- a/lib/XenAPI.py.py +++ b/lib/XenAPI.py.py @@ -1,20 +1,30 @@ -#============================================================================ -# 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. +# Copyright (c) Citrix Systems, Inc. +# All rights reserved. # -# 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. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: # -# 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. -#============================================================================ +# 1) Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. # +# 2) Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# -------------------------------------------------------------------- # Parts of this file are based upon xmlrpclib.py, the XML-RPC client # interface included in the Python distribution. # @@ -44,169 +54,207 @@ # OF THIS SOFTWARE. # -------------------------------------------------------------------- -import sys import gettext import socket -import logging +import sys -if sys.version_info[0] > 2: - import xmlrpc.client as xmlrpclib - import http.client as httplib +if sys.version_info[0] == 2: + import httplib as httplib + import xmlrpclib as xmlrpclib else: - import xmlrpclib - import httplib + import http.client as httplib + import xmlrpc.client as xmlrpclib + + +translation = gettext.translation('xen-xm', fallback = True) -translation = gettext.translation('xen-xm', fallback=True) +API_VERSION_1_1 = '1.1' +API_VERSION_1_2 = '1.2' 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 as exn: - self.details = ['INTERNAL_ERROR', 'Client-side: ' + str(exn)] - - def __str__(self): - try: - return translation.ugettext(self.details[0]) % self._details_map() - except TypeError as exn: - return "Message database broken: %s.\nXen-API failure: %s" % \ - (exn, str(self.details)) - except Exception as exn: - logging.error("%s\n", str(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 _: ()) + def __init__(self, details): + self.details = details + + def __str__(self): + try: + return str(self.details) + except Exception as exn: + msg = "Xen-API failure: %s" % exn + sys.stderr.write(msg) + return msg + + def _details_map(self): + return dict([(str(i), self.details[i]) + for i in range(len(self.details))]) + + +# Just a "constant" that we use to decide whether to retry the RPC +_RECONNECT_AND_RETRY = object() 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) + """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 UDSTransport(xmlrpclib.Transport): - def make_connection(self, host): - return httplib.HTTPConnection(host) + def add_extra_header(self, key, value): + self._extra_headers += [ (key,value) ] + def make_connection(self, host): + return UDSHTTPConnection(host) + +def notimplemented(name, *args, **kwargs): + raise NotImplementedError("XMLRPC proxies do not support python magic methods", name, *args, **kwargs) 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) + """A server proxy and session manager for communicating with xapi using + the Xen-API. + + Example: + + session = Session('http://localhost/') + session.login_with_password('me', 'mypassword', '1.0', 'xen-api-scripts-xenapi.py') + session.xenapi.VM.start(vm_uuid) + session.xenapi.session.logout() + """ + + def __init__(self, uri, transport=None, encoding=None, verbose=0, + allow_none=1, ignore_ssl=False): + + # Fix for CA-172901 (+ Python 2.4 compatibility) + # Fix for context=ctx ( < Python 2.7.9 compatibility) + if not (sys.version_info[0] <= 2 and sys.version_info[1] <= 7 and sys.version_info[2] <= 9 ) \ + and ignore_ssl: + import ssl + ctx = ssl._create_unverified_context() + xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding, + verbose, allow_none, context=ctx) + else: + xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding, + verbose, allow_none) + self.transport = transport + self._session = None + self.last_login_method = None + self.last_login_params = None + self.API_version = API_VERSION_1_1 + + + def xenapi_request(self, methodname, params): + if methodname.startswith('login'): + self._login(methodname, params) + return None + elif methodname == 'logout' or methodname == 'session.logout': + self._logout() + 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 is _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): + try: + result = _parse_result( + getattr(self, 'session.%s' % method)(*params)) + if result is _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 + self.API_version = self._get_api_version() + except socket.error as e: + if e.errno == socket.errno.ETIMEDOUT: + raise xmlrpclib.Fault(504, 'The connection timed out') + else: + raise e + + def _logout(self): + try: + if self.last_login_method.startswith("slave_local"): + return _parse_result(self.session.local_logout(self._session)) + else: + return _parse_result(self.session.logout(self._session)) + finally: + self._session = None + self.last_login_method = None + self.last_login_params = None + self.API_version = API_VERSION_1_1 + + def _get_api_version(self): + pool = self.xenapi.pool.get_all()[0] + host = self.xenapi.pool.get_master(pool) + major = self.xenapi.host.get_API_version_major(host) + minor = self.xenapi.host.get_API_version_minor(host) + return "%s.%s"%(major,minor) + + def __getattr__(self, name): + if name == 'handle': + return self._session + elif name == 'xenapi': + return _Dispatcher(self.API_version, self.xenapi_request, None) + elif name.startswith('login') or name.startswith('slave_local'): + return lambda *params: self._login(name, params) + elif name == 'logout': + return _Dispatcher(self.API_version, self.xenapi_request, "logout") + elif name.startswith('__') and name.endswith('__'): + return lambda *args, **kwargs: notimplemented(name, args, kwargs) + else: + return xmlrpclib.ServerProxy.__getattr__(self, name) def xapi_local(): - return Session("http://_var_xapi_xapi/", transport=UDSTransport()) + return Session("http://_var_lib_xcp_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') + 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 '' % self.__name - else: - return '' - - 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) + def __init__(self, API_version, send, name): + self.__API_version = API_version + self.__send = send + self.__name = name + + def __repr__(self): + if self.__name: + return '' % self.__name + else: + return '' + + def __getattr__(self, name): + if self.__name is None: + return _Dispatcher(self.__API_version, self.__send, name) + else: + return _Dispatcher(self.__API_version, self.__send, "%s.%s" % (self.__name, name)) + + def __call__(self, *args): + return self.__send(self.__name, args)