# Copyright (c) 2010 Citrix Systems, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # # Helper functions for the Nova xapi plugins. In time, this will merge # with the pluginlib.py shipped with xapi, but for now, that file is not # very stable, so it's easiest just to have a copy of all the functions # that we need. # import httplib import logging import logging.handlers import re import time ##### Logging setup def configure_logging(name): log = logging.getLogger() log.setLevel(logging.DEBUG) sysh = logging.handlers.SysLogHandler('/dev/log') sysh.setLevel(logging.DEBUG) formatter = logging.Formatter('%s: %%(levelname)-8s %%(message)s' % name) sysh.setFormatter(formatter) log.addHandler(sysh) ##### Exceptions class PluginError(Exception): """Base Exception class for all plugin errors.""" def __init__(self, *args): Exception.__init__(self, *args) class ArgumentError(PluginError): """Raised when required arguments are missing, argument values are invalid, or incompatible arguments are given. """ def __init__(self, *args): PluginError.__init__(self, *args) ##### Helpers def ignore_failure(func, *args, **kwargs): try: return func(*args, **kwargs) except XenAPI.Failure, e: logging.error('Ignoring XenAPI.Failure %s', e) return None ##### Argument validation ARGUMENT_PATTERN = re.compile(r'^[a-zA-Z0-9_:\.\-,]+$') def validate_exists(args, key, default=None): """Validates that a string argument to a RPC method call is given, and matches the shell-safe regex, with an optional default value in case it does not exist. Returns the string. """ if key in args: if len(args[key]) == 0: raise ArgumentError('Argument %r value %r is too short.' % (key, args[key])) if not ARGUMENT_PATTERN.match(args[key]): raise ArgumentError('Argument %r value %r contains invalid characters.' % (key, args[key])) if args[key][0] == '-': raise ArgumentError('Argument %r value %r starts with a hyphen.' % (key, args[key])) return args[key] elif default is not None: return default else: raise ArgumentError('Argument %s is required.' % key) def validate_bool(args, key, default=None): """Validates that a string argument to a RPC method call is a boolean string, with an optional default value in case it does not exist. Returns the python boolean value. """ value = validate_exists(args, key, default) if value.lower() == 'true': return True elif value.lower() == 'false': return False else: raise ArgumentError("Argument %s may not take value %r. Valid values are ['true', 'false']." % (key, value)) def exists(args, key): """Validates that a freeform string argument to a RPC method call is given. Returns the string. """ if key in args: return args[key] else: raise ArgumentError('Argument %s is required.' % key) def optional(args, key): """If the given key is in args, return the corresponding value, otherwise return None""" return key in args and args[key] or None def get_this_host(session): return session.xenapi.session.get_this_host(session.handle) def get_domain_0(session): this_host_ref = get_this_host(session) expr = 'field "is_control_domain" = "true" and field "resident_on" = "%s"' % this_host_ref return session.xenapi.VM.get_all_records_where(expr).keys()[0] def create_vdi(session, sr_ref, name_label, virtual_size, read_only): vdi_ref = session.xenapi.VDI.create( { 'name_label': name_label, 'name_description': '', 'SR': sr_ref, 'virtual_size': str(virtual_size), 'type': 'User', 'sharable': False, 'read_only': read_only, 'xenstore_data': {}, 'other_config': {}, 'sm_config': {}, 'tags': [] }) logging.debug('Created VDI %s (%s, %s, %s) on %s.', vdi_ref, name_label, virtual_size, read_only, sr_ref) return vdi_ref def with_vdi_in_dom0(session, vdi, read_only, f): dom0 = get_domain_0(session) vbd_rec = {} vbd_rec['VM'] = dom0 vbd_rec['VDI'] = vdi vbd_rec['userdevice'] = 'autodetect' vbd_rec['bootable'] = False vbd_rec['mode'] = read_only and 'RO' or 'RW' vbd_rec['type'] = 'disk' vbd_rec['unpluggable'] = True vbd_rec['empty'] = False vbd_rec['other_config'] = {} vbd_rec['qos_algorithm_type'] = '' vbd_rec['qos_algorithm_params'] = {} vbd_rec['qos_supported_algorithms'] = [] logging.debug('Creating VBD for VDI %s ... ', vdi) vbd = session.xenapi.VBD.create(vbd_rec) logging.debug('Creating VBD for VDI %s done.', vdi) try: logging.debug('Plugging VBD %s ... ', vbd) session.xenapi.VBD.plug(vbd) logging.debug('Plugging VBD %s done.', vbd) return f(session.xenapi.VBD.get_device(vbd)) finally: logging.debug('Destroying VBD for VDI %s ... ', vdi) vbd_unplug_with_retry(session, vbd) ignore_failure(session.xenapi.VBD.destroy, vbd) logging.debug('Destroying VBD for VDI %s done.', vdi) def vbd_unplug_with_retry(session, vbd): """Call VBD.unplug on the given VBD, with a retry if we get DEVICE_DETACH_REJECTED. For reasons which I don't understand, we're seeing the device still in use, even when all processes using the device should be dead.""" while True: try: session.xenapi.VBD.unplug(vbd) logging.debug('VBD.unplug successful first time.') return except XenAPI.Failure, e: if (len(e.details) > 0 and e.details[0] == 'DEVICE_DETACH_REJECTED'): logging.debug('VBD.unplug rejected: retrying...') time.sleep(1) elif (len(e.details) > 0 and e.details[0] == 'DEVICE_ALREADY_DETACHED'): logging.debug('VBD.unplug successful eventually.') return else: logging.error('Ignoring XenAPI.Failure in VBD.unplug: %s', e) return def with_http_connection(proto, netloc, f): conn = (proto == 'https' and httplib.HTTPSConnection(netloc) or httplib.HTTPConnection(netloc)) try: return f(conn) finally: conn.close() def with_file(dest_path, mode, f): dest = open(dest_path, mode) try: return f(dest) finally: dest.close()