diff --git a/hyperv/nova/driver.py b/hyperv/nova/driver.py index 220dbeb5..9d70f195 100644 --- a/hyperv/nova/driver.py +++ b/hyperv/nova/driver.py @@ -17,17 +17,21 @@ A Hyper-V Nova Compute driver. """ +import functools import platform +import sys from nova import exception from nova.virt import driver +from os_win import exceptions as os_win_exc +from os_win import utilsfactory from oslo_log import log as logging from oslo_utils import excutils +import six from hyperv.i18n import _, _LE from hyperv.nova import eventhandler from hyperv.nova import hostops -from hyperv.nova import hostutils from hyperv.nova import imagecache from hyperv.nova import livemigrationops from hyperv.nova import migrationops @@ -40,6 +44,55 @@ from hyperv.nova import volumeops LOG = logging.getLogger(__name__) +def convert_exceptions(function, exception_map): + expected_exceptions = tuple(exception_map.keys()) + + @functools.wraps(function) + def wrapper(*args, **kwargs): + try: + return function(*args, **kwargs) + except expected_exceptions as ex: + raised_exception = exception_map.get(type(ex)) + if not raised_exception: + # exception might be a subclass of an expected exception. + for expected in expected_exceptions: + if isinstance(ex, expected): + raised_exception = exception_map[expected] + break + + exc_info = sys.exc_info() + # NOTE(claudiub): Python 3 raises the exception object given as + # the second argument in six.reraise. + # The original message will be maintained by passing the original + # exception. + exc = raised_exception(exc_info[1]) + six.reraise(raised_exception, exc, exc_info[2]) + return wrapper + + +def decorate_all_methods(decorator, *args, **kwargs): + def decorate(cls): + for attr in cls.__dict__: + class_member = getattr(cls, attr) + if callable(class_member): + setattr(cls, attr, decorator(class_member, *args, **kwargs)) + return cls + + return decorate + + +exception_conversion_map = { + # expected_exception: converted_exception + os_win_exc.OSWinException: exception.NovaException, + os_win_exc.HyperVVMNotFoundException: exception.InstanceNotFound, +} + +# NOTE(claudiub): the purpose of the decorator below is to prevent any +# os_win exceptions (subclasses of OSWinException) to leak outside of the +# HyperVDriver. + + +@decorate_all_methods(convert_exceptions, exception_conversion_map) class HyperVDriver(driver.ComputeDriver): capabilities = { "has_imagecache": True, @@ -65,7 +118,7 @@ class HyperVDriver(driver.ComputeDriver): self._imagecache = imagecache.ImageCache() def _check_minimum_windows_version(self): - if not hostutils.HostUtils().check_min_windows_version(6, 2): + if not utilsfactory.get_hostutils().check_min_windows_version(6, 2): # the version is of Windows is older than Windows Server 2012 R2. # Log an error, lettingusers know that this version is not # supported any longer. diff --git a/hyperv/nova/eventhandler.py b/hyperv/nova/eventhandler.py index ffad2b40..d1851b1f 100644 --- a/hyperv/nova/eventhandler.py +++ b/hyperv/nova/eventhandler.py @@ -20,15 +20,15 @@ import sys if sys.platform == 'win32': import wmi -from nova import exception from nova.i18n import _LW from nova.virt import event as virtevent +from os_win import exceptions as os_win_exc +from os_win import utilsfactory from oslo_config import cfg from oslo_log import log as logging from hyperv.nova import constants from hyperv.nova import serialconsoleops -from hyperv.nova import utilsfactory LOG = logging.getLogger(__name__) @@ -122,7 +122,7 @@ class InstanceEventHandler(object): "will be ignored."), instance_name) return instance_uuid - except exception.InstanceNotFound: + except os_win_exc.HyperVVMNotFoundException: # The instance has been deleted. pass diff --git a/hyperv/nova/hostops.py b/hyperv/nova/hostops.py index 5aefa73b..0cab9a7c 100644 --- a/hyperv/nova/hostops.py +++ b/hyperv/nova/hostops.py @@ -38,6 +38,7 @@ from oslo_utils import units from hyperv.i18n import _, _LE, _LI from hyperv.nova import constants +from hyperv.nova import pathutils from hyperv.nova import utilsfactory from hyperv.nova import vmops @@ -58,7 +59,7 @@ LOG = logging.getLogger(__name__) class HostOps(object): def __init__(self): self._hostutils = utilsfactory.get_hostutils() - self._pathutils = utilsfactory.get_pathutils() + self._pathutils = pathutils.PathUtils() self._vmutils = utilsfactory.get_vmutils() self._vmops = vmops.VMOps() self._api = api.API() diff --git a/hyperv/nova/imagecache.py b/hyperv/nova/imagecache.py index 6876e9f8..07b1f493 100644 --- a/hyperv/nova/imagecache.py +++ b/hyperv/nova/imagecache.py @@ -22,6 +22,7 @@ from nova import exception from nova import utils from nova.virt import imagecache from nova.virt import images +from os_win import utilsfactory from oslo_config import cfg from oslo_log import log as logging from oslo_utils import excutils @@ -29,7 +30,7 @@ from oslo_utils import units from oslo_utils import uuidutils from hyperv.i18n import _ -from hyperv.nova import utilsfactory +from hyperv.nova import pathutils LOG = logging.getLogger(__name__) @@ -54,7 +55,7 @@ def synchronize_with_path(f): class ImageCache(imagecache.ImageCacheManager): def __init__(self): super(ImageCache, self).__init__() - self._pathutils = utilsfactory.get_pathutils() + self._pathutils = pathutils.PathUtils() self._vhdutils = utilsfactory.get_vhdutils() self.used_images = [] self.unexplained_images = [] @@ -67,8 +68,7 @@ class ImageCache(imagecache.ImageCacheManager): return instance.root_gb def _resize_and_cache_vhd(self, instance, vhd_path): - vhd_info = self._vhdutils.get_vhd_info(vhd_path) - vhd_size = vhd_info['MaxInternalSize'] + vhd_size = self._vhdutils.get_vhd_size(vhd_path)['VirtualSize'] root_vhd_size_gb = self._get_root_vhd_size_gb(instance) root_vhd_size = root_vhd_size_gb * units.Gi @@ -166,7 +166,7 @@ class ImageCache(imagecache.ImageCacheManager): def _verify_rescue_image(self, instance, rescue_image_id, rescue_image_path): rescue_image_info = self._vhdutils.get_vhd_info(rescue_image_path) - rescue_image_size = rescue_image_info['MaxInternalSize'] + rescue_image_size = rescue_image_info['VirtualSize'] flavor_disk_size = instance.root_gb * units.Gi if rescue_image_size > flavor_disk_size: diff --git a/hyperv/nova/livemigrationops.py b/hyperv/nova/livemigrationops.py index fc08d951..a6b36bda 100644 --- a/hyperv/nova/livemigrationops.py +++ b/hyperv/nova/livemigrationops.py @@ -18,6 +18,7 @@ Management class for live migration VM operations. """ import functools +from os_win import utilsfactory from oslo_config import cfg from oslo_log import log as logging from oslo_utils import excutils @@ -25,8 +26,8 @@ from oslo_utils import excutils from hyperv.i18n import _ from hyperv.nova import block_device_manager from hyperv.nova import imagecache +from hyperv.nova import pathutils from hyperv.nova import serialconsoleops -from hyperv.nova import utilsfactory from hyperv.nova import vmops from hyperv.nova import volumeops @@ -54,7 +55,7 @@ class LiveMigrationOps(object): else: self._livemigrutils = None - self._pathutils = utilsfactory.get_pathutils() + self._pathutils = pathutils.PathUtils() self._vmops = vmops.VMOps() self._volumeops = volumeops.VolumeOps() self._serial_console_ops = serialconsoleops.SerialConsoleOps() diff --git a/hyperv/nova/migrationops.py b/hyperv/nova/migrationops.py index 410fc640..9d3f8498 100644 --- a/hyperv/nova/migrationops.py +++ b/hyperv/nova/migrationops.py @@ -21,6 +21,7 @@ import os from nova import exception from nova.virt import configdrive from nova.virt import driver +from os_win import utilsfactory from oslo_log import log as logging from oslo_utils import excutils from oslo_utils import units @@ -29,7 +30,7 @@ from hyperv.i18n import _, _LE from hyperv.nova import block_device_manager from hyperv.nova import constants from hyperv.nova import imagecache -from hyperv.nova import utilsfactory +from hyperv.nova import pathutils from hyperv.nova import vmops from hyperv.nova import volumeops @@ -41,7 +42,7 @@ class MigrationOps(object): self._hostutils = utilsfactory.get_hostutils() self._vmutils = utilsfactory.get_vmutils() self._vhdutils = utilsfactory.get_vhdutils() - self._pathutils = utilsfactory.get_pathutils() + self._pathutils = pathutils.PathUtils() self._volumeops = volumeops.VolumeOps() self._vmops = vmops.VMOps() self._imagecache = imagecache.ImageCache() @@ -228,11 +229,9 @@ class MigrationOps(object): self._vhdutils.reconnect_parent_vhd(diff_vhd_path, base_vhd_copy_path) - LOG.debug("Merging base disk %(base_vhd_copy_path)s and " - "diff disk %(diff_vhd_path)s", - {'base_vhd_copy_path': base_vhd_copy_path, - 'diff_vhd_path': diff_vhd_path}) - self._vhdutils.merge_vhd(diff_vhd_path, base_vhd_copy_path) + LOG.debug("Merging differential disk %s into its parent.", + diff_vhd_path) + self._vhdutils.merge_vhd(diff_vhd_path) # Replace the differential VHD with the merged one self._pathutils.rename(base_vhd_copy_path, diff_vhd_path) @@ -242,7 +241,7 @@ class MigrationOps(object): self._pathutils.remove(base_vhd_copy_path) def _check_resize_vhd(self, vhd_path, vhd_info, new_size): - curr_size = vhd_info['MaxInternalSize'] + curr_size = vhd_info['VirtualSize'] if new_size < curr_size: raise exception.CannotResizeDisk( reason=_("Cannot resize the root disk to a smaller size. " diff --git a/hyperv/nova/pathutils.py b/hyperv/nova/pathutils.py index 3c3ce543..4dbf92fa 100644 --- a/hyperv/nova/pathutils.py +++ b/hyperv/nova/pathutils.py @@ -14,21 +14,15 @@ # under the License. import os -import shutil -import sys import time -if sys.platform == 'win32': - import wmi - from nova import exception -from nova import utils +from os_win.utils import pathutils from oslo_config import cfg from oslo_log import log as logging from hyperv.i18n import _ from hyperv.nova import constants -from hyperv.nova import vmutils LOG = logging.getLogger(__name__) @@ -47,89 +41,13 @@ CONF.register_opts(hyperv_opts, 'hyperv') CONF.import_opt('instances_path', 'nova.compute.manager') ERROR_INVALID_NAME = 123 -ERROR_DIR_IS_NOT_EMPTY = 145 -class PathUtils(object): - def __init__(self): - self._set_smb_conn() - - @property - def _smb_conn(self): - if self._smb_conn_attr: - return self._smb_conn_attr - raise vmutils.HyperVException(_("The SMB WMI namespace is not " - "available on this OS version.")) - - def _set_smb_conn(self): - # The following namespace is not available prior to Windows - # Server 2012. utilsfactory is not used in order to avoid a - # circular dependency. - try: - self._smb_conn_attr = wmi.WMI( - moniker=r"root\Microsoft\Windows\SMB") - except wmi.x_wmi: - self._smb_conn_attr = None - - def open(self, path, mode): - """Wrapper on __builtin__.open used to simplify unit testing.""" - from six.moves import builtins - return builtins.open(path, mode) - - def exists(self, path): - return os.path.exists(path) - - def makedirs(self, path): - os.makedirs(path) - - def remove(self, path): - os.remove(path) - - def rename(self, src, dest): - os.rename(src, dest) - - def copyfile(self, src, dest): - self.copy(src, dest) - - def copy(self, src, dest): - # With large files this is 2x-3x faster than shutil.copy(src, dest), - # especially when copying to a UNC target. - # shutil.copyfileobj(...) with a proper buffer is better than - # shutil.copy(...) but still 20% slower than a shell copy. - # It can be replaced with Win32 API calls to avoid the process - # spawning overhead. - LOG.debug('Copying file from %s to %s', src, dest) - output, ret = utils.execute('cmd.exe', '/C', 'copy', '/Y', src, dest) - if ret: - raise IOError(_('The file copy from %(src)s to %(dest)s failed') - % {'src': src, 'dest': dest}) - - def move_folder_files(self, src_dir, dest_dir): - """Moves the files of the given src_dir to dest_dir. - It will ignore any nested folders. - - :param src_dir: Given folder from which to move files. - :param dest_dir: Folder to which to move files. - """ - - for fname in os.listdir(src_dir): - src = os.path.join(src_dir, fname) - # ignore subdirs. - if os.path.isfile(src): - self.rename(src, os.path.join(dest_dir, fname)) - - def rmtree(self, path): - # This will be removed once support for Windows Server 2008R2 is - # stopped - for i in range(5): - try: - shutil.rmtree(path) - return - except WindowsError as e: - if e.winerror == ERROR_DIR_IS_NOT_EMPTY: - time.sleep(1) - else: - raise e +# NOTE(claudiub): part of the pre-existing PathUtils is nova-specific and +# it does not belong in the os-win library. In order to ensure the same +# functionality with the least amount of changes necessary, adding as a mixin +# the os_win.pathutils.PathUtils class into this PathUtils. +class PathUtils(pathutils.PathUtils): def get_instances_dir(self, remote_server=None): local_instance_path = os.path.normpath(CONF.instances_path) @@ -145,25 +63,15 @@ class PathUtils(object): else: return local_instance_path - def _check_create_dir(self, path): - if not self.exists(path): - LOG.debug('Creating directory: %s', path) - self.makedirs(path) - - def _check_remove_dir(self, path): - if self.exists(path): - LOG.debug('Removing directory: %s', path) - self.rmtree(path) - def _get_instances_sub_dir(self, dir_name, remote_server=None, create_dir=True, remove_dir=False): instances_path = self.get_instances_dir(remote_server) path = os.path.join(instances_path, dir_name) try: if remove_dir: - self._check_remove_dir(path) + self.check_remove_dir(path) if create_dir: - self._check_create_dir(path) + self.check_create_dir(path) return path except WindowsError as ex: if ex.winerror == ERROR_INVALID_NAME: @@ -261,55 +169,6 @@ class PathUtils(object): if self.exists(local_log_path): self.copy(local_log_path, remote_log_path) - def check_smb_mapping(self, smbfs_share): - mappings = self._smb_conn.Msft_SmbMapping(RemotePath=smbfs_share) - - if not mappings: - return False - - if os.path.exists(smbfs_share): - LOG.debug('Share already mounted: %s', smbfs_share) - return True - else: - LOG.debug('Share exists but is unavailable: %s ', smbfs_share) - self.unmount_smb_share(smbfs_share, force=True) - return False - - def mount_smb_share(self, smbfs_share, username=None, password=None): - try: - LOG.debug('Mounting share: %s', smbfs_share) - self._smb_conn.Msft_SmbMapping.Create(RemotePath=smbfs_share, - UserName=username, - Password=password) - except wmi.x_wmi as exc: - err_msg = (_( - 'Unable to mount SMBFS share: %(smbfs_share)s ' - 'WMI exception: %(wmi_exc)s') % {'smbfs_share': smbfs_share, - 'wmi_exc': exc}) - raise vmutils.HyperVException(err_msg) - - def unmount_smb_share(self, smbfs_share, force=False): - mappings = self._smb_conn.Msft_SmbMapping(RemotePath=smbfs_share) - if not mappings: - LOG.debug('Share %s is not mounted. Skipping unmount.', - smbfs_share) - - for mapping in mappings: - # Due to a bug in the WMI module, getting the output of - # methods returning None will raise an AttributeError - try: - mapping.Remove(Force=force) - except AttributeError: - pass - except wmi.x_wmi: - # If this fails, a 'Generic Failure' exception is raised. - # This happens even if we unforcefully unmount an in-use - # share, for which reason we'll simply ignore it in this - # case. - if force: - raise vmutils.HyperVException( - _("Could not unmount share: %s") % smbfs_share) - def lookup_image_basepath(self, image_name): # Note: it is possible that the path doesn't exist base_dir = self.get_base_vhd_dir() diff --git a/hyperv/nova/rdpconsoleops.py b/hyperv/nova/rdpconsoleops.py index 72a3abcf..675ae5dd 100644 --- a/hyperv/nova/rdpconsoleops.py +++ b/hyperv/nova/rdpconsoleops.py @@ -14,10 +14,10 @@ # under the License. from nova.console import type as ctype +from os_win import utilsfactory from oslo_log import log as logging from hyperv.nova import hostops -from hyperv.nova import utilsfactory LOG = logging.getLogger(__name__) diff --git a/hyperv/nova/serialconsolehandler.py b/hyperv/nova/serialconsolehandler.py index 61422b68..8ea39905 100644 --- a/hyperv/nova/serialconsolehandler.py +++ b/hyperv/nova/serialconsolehandler.py @@ -18,15 +18,15 @@ from nova.console import serial as serial_console from nova.console import type as ctype from nova import exception from nova.i18n import _, _LI # noqa +from os_win.utils.io import ioutils +from os_win import utilsfactory from oslo_config import cfg from oslo_log import log as logging import six from hyperv.nova import constants -from hyperv.nova import ioutils -from hyperv.nova import namedpipe +from hyperv.nova import pathutils from hyperv.nova import serialproxy -from hyperv.nova import utilsfactory CONF = cfg.CONF LOG = logging.getLogger(__name__) @@ -38,7 +38,7 @@ class SerialConsoleHandler(object): """Handles serial console ops related to a given instance.""" def __init__(self, instance_name): self._vmutils = utilsfactory.get_vmutils() - self._pathutils = utilsfactory.get_pathutils() + self._pathutils = pathutils.PathUtils() self._instance_name = instance_name self._log_path = self._pathutils.get_vm_console_log_paths( @@ -128,7 +128,7 @@ class SerialConsoleHandler(object): if enable_logging: kwargs['log_file'] = self._log_path - handler = namedpipe.NamedPipeHandler(pipe_path, **kwargs) + handler = utilsfactory.get_named_pipe_handler(pipe_path, **kwargs) return handler def _get_vm_serial_port_mapping(self): diff --git a/hyperv/nova/serialconsoleops.py b/hyperv/nova/serialconsoleops.py index c8796bff..2ed9a524 100644 --- a/hyperv/nova/serialconsoleops.py +++ b/hyperv/nova/serialconsoleops.py @@ -19,12 +19,13 @@ import os from nova import exception from nova.i18n import _LI, _LE # noqa from nova import utils +from os_win import utilsfactory from oslo_config import cfg from oslo_log import log as logging import six +from hyperv.nova import pathutils from hyperv.nova import serialconsolehandler -from hyperv.nova import utilsfactory CONF = cfg.CONF @@ -46,7 +47,7 @@ def instance_synchronized(func): class SerialConsoleOps(object): def __init__(self): self._vmutils = utilsfactory.get_vmutils() - self._pathutils = utilsfactory.get_pathutils() + self._pathutils = pathutils.PathUtils() @instance_synchronized def start_console_handler(self, instance_name): diff --git a/hyperv/nova/snapshotops.py b/hyperv/nova/snapshotops.py index ae2f7972..e2184739 100644 --- a/hyperv/nova/snapshotops.py +++ b/hyperv/nova/snapshotops.py @@ -19,13 +19,16 @@ Management class for VM snapshot operations. import os from nova.compute import task_states +from nova import exception from nova.image import glance from nova import utils +from os_win import exceptions as os_win_exc +from os_win import utilsfactory from oslo_config import cfg from oslo_log import log as logging from hyperv.i18n import _LW -from hyperv.nova import utilsfactory +from hyperv.nova import pathutils CONF = cfg.CONF LOG = logging.getLogger(__name__) @@ -33,7 +36,7 @@ LOG = logging.getLogger(__name__) class SnapshotOps(object): def __init__(self): - self._pathutils = utilsfactory.get_pathutils() + self._pathutils = pathutils.PathUtils() self._vmutils = utilsfactory.get_vmutils() self._vhdutils = utilsfactory.get_vhdutils() @@ -54,7 +57,11 @@ class SnapshotOps(object): def instance_synchronized_snapshot(): self._snapshot(context, instance, image_id, update_task_state) - instance_synchronized_snapshot() + try: + instance_synchronized_snapshot() + except os_win_exc.HyperVVMNotFoundException: + # the instance might dissapear before starting the operation. + raise exception.InstanceNotFound(instance_id=instance.uuid) def _snapshot(self, context, instance, image_id, update_task_state): """Create snapshot from a running VM instance.""" @@ -103,11 +110,9 @@ class SnapshotOps(object): self._vhdutils.reconnect_parent_vhd(dest_vhd_path, dest_base_disk_path) - LOG.debug("Merging base disk %(dest_base_disk_path)s and " - "diff disk %(dest_vhd_path)s", - {'dest_base_disk_path': dest_base_disk_path, - 'dest_vhd_path': dest_vhd_path}) - self._vhdutils.merge_vhd(dest_vhd_path, dest_base_disk_path) + LOG.debug("Merging diff disk %s into its parent.", + dest_vhd_path) + self._vhdutils.merge_vhd(dest_vhd_path) image_vhd_path = dest_base_disk_path LOG.debug("Updating Glance image %(image_id)s with content from " diff --git a/hyperv/nova/utilsfactory.py b/hyperv/nova/utilsfactory.py index b0607a53..c581814c 100644 --- a/hyperv/nova/utilsfactory.py +++ b/hyperv/nova/utilsfactory.py @@ -23,16 +23,10 @@ from hyperv.nova import hostutils from hyperv.nova import vmutils from hyperv.nova import volumeutils -hyper_opts = [ - cfg.BoolOpt('force_volumeutils_v1', - default=False, - help='Force V1 volume utility class'), -] - CONF = cfg.CONF -CONF.register_opts(hyper_opts, 'hyperv') LOG = logging.getLogger(__name__) +CONF.import_group('hyperv', 'os_win.utilsfactory') utils = hostutils.HostUtils() diff --git a/hyperv/nova/vif.py b/hyperv/nova/vif.py index bc7517bc..831a8a5b 100644 --- a/hyperv/nova/vif.py +++ b/hyperv/nova/vif.py @@ -18,11 +18,11 @@ import abc from nova.i18n import _ from nova.network import model as network_model +from os_win import utilsfactory from oslo_config import cfg from oslo_log import log as logging from hyperv.nova import ovsutils -from hyperv.nova import utilsfactory hyperv_opts = [ @@ -62,22 +62,11 @@ class HyperVNovaNetworkVIFDriver(HyperVBaseVIFDriver): """Nova network VIF driver.""" def __init__(self): - self._vmutils = utilsfactory.get_vmutils() self._netutils = utilsfactory.get_networkutils() def plug(self, instance, vif): - vswitch_path = self._netutils.get_external_vswitch( - CONF.hyperv.vswitch_name) - - vm_name = instance.name - LOG.debug('Creating vswitch port for instance: %s', vm_name) - if self._netutils.vswitch_port_needed(): - vswitch_data = self._netutils.create_vswitch_port(vswitch_path, - vm_name) - else: - vswitch_data = vswitch_path - - self._vmutils.set_nic_connection(vm_name, vif['id'], vswitch_data) + self._netutils.connect_vnic_to_vswitch(CONF.hyperv.vswitch_name, + vif['id']) class HyperVOVSVIFDriver(HyperVNovaNetworkVIFDriver): diff --git a/hyperv/nova/vmops.py b/hyperv/nova/vmops.py index 21f03403..0537fdf7 100644 --- a/hyperv/nova/vmops.py +++ b/hyperv/nova/vmops.py @@ -29,6 +29,8 @@ from nova import objects from nova import utils from nova.virt import configdrive from nova.virt import hardware +from os_win import exceptions as os_win_exc +from os_win import utilsfactory from oslo_concurrency import processutils from oslo_config import cfg from oslo_log import log as logging @@ -43,10 +45,10 @@ from hyperv.i18n import _, _LI, _LE, _LW from hyperv.nova import block_device_manager from hyperv.nova import constants from hyperv.nova import imagecache +from hyperv.nova import pathutils from hyperv.nova import serialconsoleops -from hyperv.nova import utilsfactory +from hyperv.nova import utilsfactory as old_utilsfactory from hyperv.nova import vif as vif_utils -from hyperv.nova import vmutils from hyperv.nova import volumeops LOG = logging.getLogger(__name__) @@ -126,10 +128,10 @@ class VMOps(object): _ROOT_DISK_CTRL_ADDR = 0 def __init__(self): - self._vmutils = utilsfactory.get_vmutils() + self._vmutils = old_utilsfactory.get_vmutils() self._vhdutils = utilsfactory.get_vhdutils() - self._pathutils = utilsfactory.get_pathutils() self._hostutils = utilsfactory.get_hostutils() + self._pathutils = pathutils.PathUtils() self._serial_console_ops = serialconsoleops.SerialConsoleOps() self._volumeops = volumeops.VolumeOps() self._imagecache = imagecache.ImageCache() @@ -191,7 +193,7 @@ class VMOps(object): base_vhd_path = self._imagecache.get_cached_image(context, instance, rescue_image_id) base_vhd_info = self._vhdutils.get_vhd_info(base_vhd_path) - base_vhd_size = base_vhd_info['MaxInternalSize'] + base_vhd_size = base_vhd_info['VirtualSize'] format_ext = base_vhd_path.split('.')[-1] root_vhd_path = self._pathutils.get_root_vhd_path(instance.name, @@ -270,8 +272,7 @@ class VMOps(object): def _create_ephemeral_disk(self, instance_name, eph_info): self._vhdutils.create_dynamic_vhd(eph_info['path'], - eph_info['size'] * units.Gi, - eph_info['format']) + eph_info['size'] * units.Gi) def set_boot_order(self, vm_gen, block_device_info, instance_name): boot_order = self._block_device_manager.get_boot_order( @@ -682,7 +683,7 @@ class VMOps(object): LOG.info(_LI("Soft shutdown succeeded."), instance=instance) return True - except vmutils.HyperVException as e: + except os_win_exc.HyperVException as e: # Exception is raised when trying to shutdown the instance # while it is still booting. LOG.debug("Soft shutdown failed: %s", e, instance=instance) @@ -737,7 +738,7 @@ class VMOps(object): self._set_vm_state(instance, constants.HYPERV_VM_STATE_DISABLED) - except exception.InstanceNotFound: + except os_win_exc.HyperVVMNotFoundException: # The manager can call the stop API after receiving instance # power off events. If this is triggered when the instance # is being deleted, it might attempt to power off an unexisting diff --git a/hyperv/nova/vmutils.py b/hyperv/nova/vmutils.py index 457b0f9f..f244ac52 100644 --- a/hyperv/nova/vmutils.py +++ b/hyperv/nova/vmutils.py @@ -26,6 +26,7 @@ if sys.platform == 'win32': import wmi from nova import exception +from os_win import exceptions as os_win_exc from oslo_config import cfg from oslo_log import log as logging from oslo_service import loopingcall @@ -40,28 +41,9 @@ from hyperv.nova import hostutils CONF = cfg.CONF LOG = logging.getLogger(__name__) - -# TODO(alexpilotti): Move the exceptions to a separate module -# TODO(alexpilotti): Add more domain exceptions -class HyperVException(exception.NovaException): - def __init__(self, message=None): - super(HyperVException, self).__init__(message) - - -# TODO(alexpilotti): Add a storage exception base class -class VHDResizeException(HyperVException): - def __init__(self, message=None): - super(HyperVException, self).__init__(message) - - -class HyperVAuthorizationException(HyperVException): - def __init__(self, message=None): - super(HyperVException, self).__init__(message) - - -class UnsupportedConfigDriveFormatException(HyperVException): - def __init__(self, message=None): - super(HyperVException, self).__init__(message) +# NOTE(claudiub): in order to make sure the same exceptions are being raised. +HyperVException = os_win_exc.HyperVException +HyperVAuthorizationException = os_win_exc.HyperVAuthorizationException class VMUtils(object): diff --git a/hyperv/nova/volumeops.py b/hyperv/nova/volumeops.py index c7726b8d..b9d7462e 100644 --- a/hyperv/nova/volumeops.py +++ b/hyperv/nova/volumeops.py @@ -22,9 +22,12 @@ import os import re import time +from nova import block_device from nova import exception from nova import utils from nova.virt import driver +from os_win import exceptions as os_win_exc +from os_win import utilsfactory from oslo_config import cfg from oslo_log import log as logging from oslo_utils import excutils @@ -34,8 +37,6 @@ from six.moves import range from hyperv.i18n import _, _LE, _LW from hyperv.nova import constants -from hyperv.nova import utilsfactory -from hyperv.nova import vmutils LOG = logging.getLogger(__name__) @@ -72,7 +73,7 @@ class VolumeOps(object): def __init__(self): self._vmutils = utilsfactory.get_vmutils() - self._volutils = utilsfactory.get_volumeutils() + self._volutils = utilsfactory.get_iscsi_initiator_utils() self._initiator = None self._default_root_device = 'vda' self.volume_drivers = {'smbfs': SMBFSVolumeDriver(), @@ -121,8 +122,8 @@ class VolumeOps(object): root_device = block_device_info.get('root_device_name') if not root_device: root_device = self._default_root_device - return self._volutils.volume_in_mapping(root_device, - block_device_info) + return block_device.volume_in_mapping(root_device, + block_device_info) def fix_instance_volume_disk_paths(self, instance_name, block_device_info): mapping = driver.block_device_info_get_mapping(block_device_info) @@ -212,7 +213,7 @@ class VolumeOps(object): class ISCSIVolumeDriver(object): def __init__(self): self._vmutils = utilsfactory.get_vmutils() - self._volutils = utilsfactory.get_volumeutils() + self._volutils = utilsfactory.get_iscsi_initiator_utils() def login_storage_target(self, connection_info): data = connection_info['data'] @@ -413,9 +414,8 @@ def export_path_synchronized(f): class SMBFSVolumeDriver(object): def __init__(self): - self._pathutils = utilsfactory.get_pathutils() + self._smbutils = utilsfactory.get_smbutils() self._vmutils = utilsfactory.get_vmutils() - self._volutils = utilsfactory.get_volumeutils() self._username_regex = re.compile(r'user(?:name)?=([^, ]+)') self._password_regex = re.compile(r'pass(?:word)?=([^, ]+)') @@ -443,7 +443,7 @@ class SMBFSVolumeDriver(object): disk_path, ctrller_path, slot) - except vmutils.HyperVException as exn: + except os_win_exc.HyperVException as exn: LOG.exception(_LE('Attach volume failed to %(instance_name)s: ' '%(exn)s'), {'instance_name': instance_name, 'exn': exn}) @@ -486,12 +486,12 @@ class SMBFSVolumeDriver(object): def ensure_share_mounted(self, connection_info): export_path = self._get_export_path(connection_info) - if not self._pathutils.check_smb_mapping(export_path): + if not self._smbutils.check_smb_mapping(export_path): opts_str = connection_info['data'].get('options', '') username, password = self._parse_credentials(opts_str) - self._pathutils.mount_smb_share(export_path, - username=username, - password=password) + self._smbutils.mount_smb_share(export_path, + username=username, + password=password) def _parse_credentials(self, opts_str): match = self._username_regex.findall(opts_str) @@ -516,7 +516,7 @@ class SMBFSVolumeDriver(object): # an instance. @utils.synchronized(export_path) def unmount_synchronized(): - self._pathutils.unmount_smb_share(export_path) + self._smbutils.unmount_smb_share(export_path) unmount_synchronized() def set_disk_qos_specs(self, connection_info, instance_name, diff --git a/hyperv/tests/unit/test_base.py b/hyperv/tests/unit/test_base.py index 9547ab3f..fcd6fa39 100644 --- a/hyperv/tests/unit/test_base.py +++ b/hyperv/tests/unit/test_base.py @@ -15,9 +15,10 @@ # under the License. import mock +from os_win import utilsfactory from six.moves import builtins -from hyperv.nova import utilsfactory +from hyperv.nova import utilsfactory as old_utilsfactory from hyperv.tests import test @@ -29,15 +30,16 @@ class HyperVBaseTestCase(test.NoDBTestCase): wmi_patcher = mock.patch.object(builtins, 'wmi', create=True, new=self._mock_wmi) platform_patcher = mock.patch('sys.platform', 'win32') - hostutils_patcher = mock.patch.object(utilsfactory, 'utils') - - hostutils_patcher = mock.patch.multiple(utilsfactory, - _get_class=mock.DEFAULT) + utilsfactory_patcher = mock.patch.object(utilsfactory, '_get_class') + old_utilsfactory_patcher = mock.patch.object(old_utilsfactory, + '_get_class') platform_patcher.start() wmi_patcher.start() - hostutils_patcher.start() + utilsfactory_patcher.start() + old_utilsfactory_patcher.start() self.addCleanup(wmi_patcher.stop) self.addCleanup(platform_patcher.stop) - self.addCleanup(hostutils_patcher.stop) + self.addCleanup(utilsfactory_patcher.stop) + self.addCleanup(old_utilsfactory_patcher.stop) diff --git a/hyperv/tests/unit/test_driver.py b/hyperv/tests/unit/test_driver.py index 5f3e360f..8266d920 100644 --- a/hyperv/tests/unit/test_driver.py +++ b/hyperv/tests/unit/test_driver.py @@ -21,7 +21,9 @@ import platform import mock from nova import exception +from nova import safe_utils from nova.virt import driver as base_driver +from os_win import exceptions as os_win_exc from hyperv.nova import driver from hyperv.tests.unit import test_base @@ -47,16 +49,45 @@ class HyperVDriverTestCase(test_base.HyperVBaseTestCase): self.driver._serialconsoleops = mock.MagicMock() self.driver._imagecache = mock.MagicMock() - @mock.patch.object(driver.hostutils.HostUtils, 'check_min_windows_version') - def test_check_minimum_windows_version(self, mock_check_min_win_version): - mock_check_min_win_version.return_value = False + @mock.patch.object(driver.utilsfactory, 'get_hostutils') + def test_check_minimum_windows_version(self, mock_get_hostutils): + mock_hostutils = mock_get_hostutils.return_value + mock_hostutils.check_min_windows_version.return_value = False self.assertRaises(exception.HypervisorTooOld, self.driver._check_minimum_windows_version) def test_public_api_signatures(self): - self.assertPublicAPISignatures(base_driver.ComputeDriver(None), - self.driver) + # NOTE(claudiub): wrapped functions do not keep the same signature in + # Python 2.7, which causes this test to fail. Instead, we should + # compare the public API signatures of the unwrapped methods. + + for attr in driver.HyperVDriver.__dict__: + class_member = getattr(driver.HyperVDriver, attr) + if callable(class_member): + mocked_method = mock.patch.object( + driver.HyperVDriver, attr, + safe_utils.get_wrapped_function(class_member)) + mocked_method.start() + self.addCleanup(mocked_method.stop) + + self.assertPublicAPISignatures(base_driver.ComputeDriver, + driver.HyperVDriver) + + def test_converted_exception(self): + self.driver._vmops.get_info.side_effect = ( + os_win_exc.OSWinException) + self.assertRaises(exception.NovaException, + self.driver.get_info, mock.sentinel.instance) + + self.driver._vmops.get_info.side_effect = os_win_exc.HyperVException + self.assertRaises(exception.NovaException, + self.driver.get_info, mock.sentinel.instance) + + self.driver._vmops.get_info.side_effect = ( + os_win_exc.HyperVVMNotFoundException(vm_name='foofoo')) + self.assertRaises(exception.InstanceNotFound, + self.driver.get_info, mock.sentinel.instance) def test_need_legacy_block_device_info(self): self.assertFalse(self.driver.need_legacy_block_device_info) diff --git a/hyperv/tests/unit/test_eventhandler.py b/hyperv/tests/unit/test_eventhandler.py index bafc1520..1ae4d6b8 100644 --- a/hyperv/tests/unit/test_eventhandler.py +++ b/hyperv/tests/unit/test_eventhandler.py @@ -16,11 +16,11 @@ import eventlet import mock -from nova import exception +from os_win import exceptions as os_win_exc +from os_win import utilsfactory from hyperv.nova import constants from hyperv.nova import eventhandler -from hyperv.nova import utilsfactory from hyperv.tests.unit import test_base @@ -139,7 +139,8 @@ class EventHandlerTestCase(test_base.HyperVBaseTestCase): side_effect = (mock.sentinel.instance_uuid if not missing_uuid else None, ) else: - side_effect = exception.InstanceNotFound('fake_instance_uuid') + side_effect = os_win_exc.HyperVVMNotFoundException( + vm_name=mock.sentinel.instance_name) mock_get_uuid = self._event_handler._vmutils.get_instance_uuid mock_get_uuid.side_effect = side_effect diff --git a/hyperv/tests/unit/test_hostops.py b/hyperv/tests/unit/test_hostops.py index 11e16891..57a33c31 100644 --- a/hyperv/tests/unit/test_hostops.py +++ b/hyperv/tests/unit/test_hostops.py @@ -47,6 +47,7 @@ class HostOpsTestCase(test_base.HyperVBaseTestCase): self._hostops = hostops.HostOps() self._hostops._api = mock.MagicMock() self._hostops._vmops = mock.MagicMock() + self._hostops._pathutils = mock.MagicMock() def test_get_cpu_info(self): mock_processors = mock.MagicMock() diff --git a/hyperv/tests/unit/test_imagecache.py b/hyperv/tests/unit/test_imagecache.py index eeb8de3a..2ebc9be4 100644 --- a/hyperv/tests/unit/test_imagecache.py +++ b/hyperv/tests/unit/test_imagecache.py @@ -50,8 +50,8 @@ class ImageCacheTestCase(test_base.HyperVBaseTestCase): @mock.patch.object(imagecache.ImageCache, '_get_root_vhd_size_gb') def test_resize_and_cache_vhd_smaller(self, mock_get_vhd_size_gb): - self.imagecache._vhdutils.get_vhd_info.return_value = { - 'MaxInternalSize': (self.FAKE_VHD_SIZE_GB + 1) * units.Gi + self.imagecache._vhdutils.get_vhd_size.return_value = { + 'VirtualSize': (self.FAKE_VHD_SIZE_GB + 1) * units.Gi } mock_get_vhd_size_gb.return_value = self.FAKE_VHD_SIZE_GB mock_internal_vhd_size = ( @@ -63,7 +63,7 @@ class ImageCacheTestCase(test_base.HyperVBaseTestCase): mock.sentinel.instance, mock.sentinel.vhd_path) - self.imagecache._vhdutils.get_vhd_info.assert_called_once_with( + self.imagecache._vhdutils.get_vhd_size.assert_called_once_with( mock.sentinel.vhd_path) mock_get_vhd_size_gb.assert_called_once_with(mock.sentinel.instance) mock_internal_vhd_size.assert_called_once_with( @@ -162,7 +162,7 @@ class ImageCacheTestCase(test_base.HyperVBaseTestCase): fake_rescue_image_id = 'fake_rescue_image_id' self.imagecache._vhdutils.get_vhd_info.return_value = { - 'MaxInternalSize': self.instance.root_gb + 1} + 'VirtualSize': self.instance.root_gb + 1} (expected_path, expected_vhd_path) = self._prepare_get_cached_image( rescue_image_id=fake_rescue_image_id, diff --git a/hyperv/tests/unit/test_livemigrationops.py b/hyperv/tests/unit/test_livemigrationops.py index 51258f55..48240c5a 100644 --- a/hyperv/tests/unit/test_livemigrationops.py +++ b/hyperv/tests/unit/test_livemigrationops.py @@ -14,10 +14,10 @@ # under the License. import mock +from os_win import exceptions as os_win_exc from oslo_config import cfg from hyperv.nova import livemigrationops -from hyperv.nova import vmutils from hyperv.tests import fake_instance from hyperv.tests.unit import test_base @@ -32,6 +32,7 @@ class LiveMigrationOpsTestCase(test_base.HyperVBaseTestCase): self.context = 'fake_context' self._livemigrops = livemigrationops.LiveMigrationOps() self._livemigrops._block_dev_man = mock.MagicMock() + self._livemigrops._pathutils = mock.MagicMock() @mock.patch('hyperv.nova.serialconsoleops.SerialConsoleOps.' 'stop_console_handler') @@ -44,8 +45,8 @@ class LiveMigrationOpsTestCase(test_base.HyperVBaseTestCase): fake_dest = mock.sentinel.DESTINATION self._livemigrops._livemigrutils.live_migrate_vm.side_effect = [ side_effect] - if side_effect is vmutils.HyperVException: - self.assertRaises(vmutils.HyperVException, + if side_effect is os_win_exc.HyperVException: + self.assertRaises(os_win_exc.HyperVException, self._livemigrops.live_migration, self.context, mock_instance, fake_dest, mock_post, mock_recover, False, None) @@ -73,7 +74,7 @@ class LiveMigrationOpsTestCase(test_base.HyperVBaseTestCase): self._test_live_migration(side_effect=None) def test_live_migration_exception(self): - self._test_live_migration(side_effect=vmutils.HyperVException) + self._test_live_migration(side_effect=os_win_exc.HyperVException) def test_live_migration_wrong_os_version(self): self._livemigrops._livemigrutils = None diff --git a/hyperv/tests/unit/test_migrationops.py b/hyperv/tests/unit/test_migrationops.py index 29f4a1f4..8c42ac11 100644 --- a/hyperv/tests/unit/test_migrationops.py +++ b/hyperv/tests/unit/test_migrationops.py @@ -16,11 +16,11 @@ import os import mock from nova import exception +from os_win import exceptions as os_win_exc from oslo_utils import units from hyperv.nova import constants from hyperv.nova import migrationops -from hyperv.nova import vmutils from hyperv.tests import fake_instance from hyperv.tests.unit import test_base @@ -315,7 +315,7 @@ class MigrationOpsTestCase(test_base.HyperVBaseTestCase): recon_parent_vhd.assert_called_once_with(fake_diff_vhd_path, base_vhd_copy_path) self._migrationops._vhdutils.merge_vhd.assert_called_once_with( - fake_diff_vhd_path, base_vhd_copy_path) + fake_diff_vhd_path) self._migrationops._pathutils.rename.assert_called_once_with( base_vhd_copy_path, fake_diff_vhd_path) @@ -327,10 +327,10 @@ class MigrationOpsTestCase(test_base.HyperVBaseTestCase): os.path.basename(fake_base_vhd_path)) self._migrationops._vhdutils.reconnect_parent_vhd.side_effect = ( - vmutils.HyperVException) + os_win_exc.HyperVException) self._migrationops._pathutils.exists.return_value = True - self.assertRaises(vmutils.HyperVException, + self.assertRaises(os_win_exc.HyperVException, self._migrationops._merge_base_vhd, fake_diff_vhd_path, fake_base_vhd_path) self._migrationops._pathutils.exists.assert_called_once_with( @@ -341,7 +341,7 @@ class MigrationOpsTestCase(test_base.HyperVBaseTestCase): @mock.patch.object(migrationops.MigrationOps, '_resize_vhd') def test_check_resize_vhd(self, mock_resize_vhd): self._migrationops._check_resize_vhd( - vhd_path=mock.sentinel.vhd_path, vhd_info={'MaxInternalSize': 1}, + vhd_path=mock.sentinel.vhd_path, vhd_info={'VirtualSize': 1}, new_size=2) mock_resize_vhd.assert_called_once_with(mock.sentinel.vhd_path, 2) @@ -349,7 +349,7 @@ class MigrationOpsTestCase(test_base.HyperVBaseTestCase): self.assertRaises(exception.CannotResizeDisk, self._migrationops._check_resize_vhd, mock.sentinel.vhd_path, - {'MaxInternalSize': 1}, 0) + {'VirtualSize': 1}, 0) @mock.patch.object(migrationops.MigrationOps, '_merge_base_vhd') def test_resize_vhd(self, mock_merge_base_vhd): diff --git a/hyperv/tests/unit/test_pathutils.py b/hyperv/tests/unit/test_pathutils.py index 7049e0c2..94797c41 100644 --- a/hyperv/tests/unit/test_pathutils.py +++ b/hyperv/tests/unit/test_pathutils.py @@ -21,7 +21,6 @@ from six.moves import builtins from hyperv.nova import constants from hyperv.nova import pathutils -from hyperv.nova import vmutils from hyperv.tests.unit import test_base @@ -36,45 +35,6 @@ class PathUtilsTestCase(test_base.HyperVBaseTestCase): self._pathutils = pathutils.PathUtils() self._pathutils._smb_conn_attr = mock.MagicMock() - @mock.patch.object(pathutils, 'wmi', create=True) - def _test_smb_conn(self, mock_wmi, smb_available=True): - mock_wmi.x_wmi = Exception - mock_wmi.WMI.side_effect = None if smb_available else Exception - - self._pathutils._set_smb_conn() - - if smb_available: - expected_conn = mock_wmi.WMI.return_value - self.assertEqual(expected_conn, self._pathutils._smb_conn) - else: - self.assertRaises(vmutils.HyperVException, - getattr, - self._pathutils, '_smb_conn') - - def test_smb_conn_available(self): - self._test_smb_conn() - - def test_smb_conn_unavailable(self): - self._test_smb_conn(smb_available=False) - - @mock.patch.object(pathutils.PathUtils, 'rename') - @mock.patch.object(os.path, 'isfile') - @mock.patch.object(os, 'listdir') - def test_move_folder_files(self, mock_listdir, mock_isfile, mock_rename): - src_dir = 'src' - dest_dir = 'dest' - fname = 'tmp_file.txt' - subdir = 'tmp_folder' - src_fname = os.path.join(src_dir, fname) - dest_fname = os.path.join(dest_dir, fname) - - # making sure src_subdir is not moved. - mock_listdir.return_value = [fname, subdir] - mock_isfile.side_effect = [True, False] - - self._pathutils.move_folder_files(src_dir, dest_dir) - mock_rename.assert_called_once_with(src_fname, dest_fname) - def _mock_lookup_configdrive_path(self, ext, rescue=False): self._pathutils.get_instance_dir = mock.MagicMock( return_value=self.fake_instance_dir) @@ -113,81 +73,6 @@ class PathUtilsTestCase(test_base.HyperVBaseTestCase): self.fake_instance_name) self.assertIsNone(configdrive_path) - @mock.patch.object(pathutils.PathUtils, 'unmount_smb_share') - @mock.patch('os.path.exists') - def _test_check_smb_mapping(self, mock_exists, mock_unmount_smb_share, - existing_mappings=True, share_available=False): - mock_exists.return_value = share_available - - fake_mappings = ( - [mock.sentinel.smb_mapping] if existing_mappings else []) - - self._pathutils._smb_conn.Msft_SmbMapping.return_value = ( - fake_mappings) - - ret_val = self._pathutils.check_smb_mapping( - mock.sentinel.share_path) - - self.assertEqual(existing_mappings and share_available, ret_val) - if existing_mappings and not share_available: - mock_unmount_smb_share.assert_called_once_with( - mock.sentinel.share_path, force=True) - - def test_check_mapping(self): - self._test_check_smb_mapping() - - def test_remake_unavailable_mapping(self): - self._test_check_smb_mapping(existing_mappings=True, - share_available=False) - - def test_available_mapping(self): - self._test_check_smb_mapping(existing_mappings=True, - share_available=True) - - def test_mount_smb_share(self): - fake_create = self._pathutils._smb_conn.Msft_SmbMapping.Create - self._pathutils.mount_smb_share(mock.sentinel.share_path, - mock.sentinel.username, - mock.sentinel.password) - fake_create.assert_called_once_with( - RemotePath=mock.sentinel.share_path, - UserName=mock.sentinel.username, - Password=mock.sentinel.password) - - def _test_unmount_smb_share(self, force=False): - fake_mapping = mock.Mock() - smb_mapping_class = self._pathutils._smb_conn.Msft_SmbMapping - smb_mapping_class.return_value = [fake_mapping] - - self._pathutils.unmount_smb_share(mock.sentinel.share_path, - force) - - smb_mapping_class.assert_called_once_with( - RemotePath=mock.sentinel.share_path) - fake_mapping.Remove.assert_called_once_with(Force=force) - - def test_soft_unmount_smb_share(self): - self._test_unmount_smb_share() - - def test_force_unmount_smb_share(self): - self._test_unmount_smb_share(force=True) - - @mock.patch('shutil.rmtree') - def test_rmtree(self, mock_rmtree): - class WindowsError(Exception): - def __init__(self, winerror=None): - self.winerror = winerror - - mock_rmtree.side_effect = [WindowsError( - pathutils.ERROR_DIR_IS_NOT_EMPTY), True] - fake_windows_error = WindowsError - with mock.patch.object(builtins, 'WindowsError', - fake_windows_error, create=True): - self._pathutils.rmtree(mock.sentinel.FAKE_PATH) - - mock_rmtree.assert_has_calls([mock.call(mock.sentinel.FAKE_PATH), - mock.call(mock.sentinel.FAKE_PATH)]) - @mock.patch('os.path.join') def test_get_instances_sub_dir(self, fake_path_join): @@ -197,7 +82,7 @@ class PathUtilsTestCase(test_base.HyperVBaseTestCase): fake_dir_name = "fake_dir_name" fake_windows_error = WindowsError - self._pathutils._check_create_dir = mock.MagicMock( + self._pathutils.check_create_dir = mock.MagicMock( side_effect=WindowsError(pathutils.ERROR_INVALID_NAME)) with mock.patch.object(builtins, 'WindowsError', fake_windows_error, create=True): diff --git a/hyperv/tests/unit/test_serialconsolehandler.py b/hyperv/tests/unit/test_serialconsolehandler.py index 6a3be84c..957313cc 100644 --- a/hyperv/tests/unit/test_serialconsolehandler.py +++ b/hyperv/tests/unit/test_serialconsolehandler.py @@ -18,20 +18,19 @@ import mock from nova import exception from hyperv.nova import constants -from hyperv.nova import ioutils -from hyperv.nova import namedpipe from hyperv.nova import serialconsolehandler from hyperv.nova import serialproxy -from hyperv.nova import utilsfactory from hyperv.tests.unit import test_base class SerialConsoleHandlerTestCase(test_base.HyperVBaseTestCase): - @mock.patch.object(utilsfactory, 'get_pathutils') - def setUp(self, mock_get_pathutils): + + _FAKE_INSTANCE_NAME = 'fake_instance_name' + + def setUp(self): super(SerialConsoleHandlerTestCase, self).setUp() self._consolehandler = serialconsolehandler.SerialConsoleHandler( - mock.sentinel.instance_name) + self._FAKE_INSTANCE_NAME) self._consolehandler._log_path = mock.sentinel.log_path self._consolehandler._pathutils = mock.Mock() @@ -88,7 +87,7 @@ class SerialConsoleHandlerTestCase(test_base.HyperVBaseTestCase): @mock.patch.object(serialproxy, 'SerialProxy') @mock.patch('nova.console.serial.acquire_port') @mock.patch.object(serialconsolehandler.threading, 'Event') - @mock.patch.object(ioutils, 'IOQueue') + @mock.patch.object(serialconsolehandler.ioutils, 'IOQueue') def test_setup_serial_proxy_handler(self, mock_io_queue, mock_event, mock_acquire_port, mock_serial_proxy_class): @@ -105,7 +104,7 @@ class SerialConsoleHandlerTestCase(test_base.HyperVBaseTestCase): self._consolehandler._setup_serial_proxy_handler() mock_serial_proxy_class.assert_called_once_with( - mock.sentinel.instance_name, + self._FAKE_INSTANCE_NAME, mock.sentinel.host, mock.sentinel.port, mock_input_queue, mock_output_queue, @@ -161,8 +160,9 @@ class SerialConsoleHandlerTestCase(test_base.HyperVBaseTestCase): enable_logging=False)] mock_get_handler.assert_has_calls(expected_calls, any_order=True) - @mock.patch.object(namedpipe, 'NamedPipeHandler') - def _test_get_named_pipe_handler(self, mock_pipe_handler_class, + @mock.patch.object(serialconsolehandler.utilsfactory, + 'get_named_pipe_handler') + def _test_get_named_pipe_handler(self, mock_get_pipe_handler, pipe_type=None, enable_logging=False): expected_args = {} @@ -182,8 +182,8 @@ class SerialConsoleHandlerTestCase(test_base.HyperVBaseTestCase): ret_val = self._consolehandler._get_named_pipe_handler( mock.sentinel.pipe_path, pipe_type, enable_logging) - self.assertEqual(mock_pipe_handler_class.return_value, ret_val) - mock_pipe_handler_class.assert_called_once_with( + self.assertEqual(mock_get_pipe_handler.return_value, ret_val) + mock_get_pipe_handler.assert_called_once_with( mock.sentinel.pipe_path, **expected_args) diff --git a/hyperv/tests/unit/test_serialconsoleops.py b/hyperv/tests/unit/test_serialconsoleops.py index a7a140ff..db8d5bc8 100644 --- a/hyperv/tests/unit/test_serialconsoleops.py +++ b/hyperv/tests/unit/test_serialconsoleops.py @@ -28,6 +28,7 @@ class SerialConsoleOpsTestCase(test_base.HyperVBaseTestCase): super(SerialConsoleOpsTestCase, self).setUp() serialconsoleops._console_handlers = {} self._serialops = serialconsoleops.SerialConsoleOps() + self._serialops._pathutils = mock.MagicMock() def _setup_console_handler_mock(self): mock_console_handler = mock.Mock() diff --git a/hyperv/tests/unit/test_snapshotops.py b/hyperv/tests/unit/test_snapshotops.py index 940c07ab..28009470 100644 --- a/hyperv/tests/unit/test_snapshotops.py +++ b/hyperv/tests/unit/test_snapshotops.py @@ -17,6 +17,8 @@ import os import mock from nova.compute import task_states +from nova import exception +from os_win import exceptions as os_win_exc from hyperv.nova import snapshotops from hyperv.tests import fake_instance @@ -96,7 +98,7 @@ class SnapshotOpsTestCase(test_base.HyperVBaseTestCase): mock_reconnect.assert_called_once_with(dest_vhd_path, base_dest_disk_path) self._snapshotops._vhdutils.merge_vhd.assert_called_once_with( - dest_vhd_path, base_dest_disk_path) + dest_vhd_path) mock_save_glance_image.assert_called_once_with( self.context, mock.sentinel.IMAGE_ID, base_dest_disk_path) else: @@ -119,3 +121,18 @@ class SnapshotOpsTestCase(test_base.HyperVBaseTestCase): def test_snapshot_no_base_disk(self): self._test_snapshot(base_disk_path=None) + + @mock.patch.object(snapshotops.SnapshotOps, '_snapshot') + def test_snapshot_instance_not_found(self, mock_snapshot): + mock_instance = fake_instance.fake_instance_obj(self.context) + mock_snapshot.side_effect = os_win_exc.HyperVVMNotFoundException( + vm_name=mock_instance.name) + + self.assertRaises(exception.InstanceNotFound, + self._snapshotops.snapshot, + self.context, mock_instance, mock.sentinel.image_id, + mock.sentinel.update_task_state) + + mock_snapshot.assert_called_once_with(self.context, mock_instance, + mock.sentinel.image_id, + mock.sentinel.update_task_state) diff --git a/hyperv/tests/unit/test_utilsfactory.py b/hyperv/tests/unit/test_utilsfactory.py index c070f06f..33e88260 100644 --- a/hyperv/tests/unit/test_utilsfactory.py +++ b/hyperv/tests/unit/test_utilsfactory.py @@ -21,21 +21,21 @@ import mock from hyperv.nova import utilsfactory from hyperv.nova import vmutils -from hyperv.nova import volumeutilsv2 +from hyperv.nova import vmutilsv2 from hyperv.tests import test class TestHyperVUtilsFactory(test.NoDBTestCase): - def test_get_class(self): - expected_instance = volumeutilsv2.VolumeUtilsV2() - utilsfactory.utils = mock.MagicMock() - utilsfactory.utils.get_windows_version.return_value = '6.2' - instance = utilsfactory._get_class('volumeutils') + @mock.patch.object(utilsfactory.utils, 'get_windows_version') + def test_get_class(self, mock_get_windows_version): + expected_instance = vmutilsv2.VMUtilsV2() + mock_get_windows_version.return_value = '6.2' + instance = utilsfactory._get_class('vmutils') self.assertEqual(type(expected_instance), type(instance)) - def test_get_class_not_found(self): - utilsfactory.utils = mock.MagicMock() - utilsfactory.utils.get_windows_version.return_value = '5.2' + @mock.patch.object(utilsfactory.utils, 'get_windows_version') + def test_get_class_not_found(self, mock_get_windows_version): + mock_get_windows_version.return_value = '5.2' self.assertRaises(vmutils.HyperVException, utilsfactory._get_class, - 'hostutils') + 'vmutils') diff --git a/hyperv/tests/unit/test_vif.py b/hyperv/tests/unit/test_vif.py index a4f961fe..fa78e50b 100644 --- a/hyperv/tests/unit/test_vif.py +++ b/hyperv/tests/unit/test_vif.py @@ -1,4 +1,5 @@ -# Copyright 2014 Cloudbase Solutions SRL +# Copyright 2015 Cloudbase Solutions Srl +# # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -13,123 +14,22 @@ # License for the specific language governing permissions and limitations # under the License. -""" -Unit tests for the Hyper-V vif module. -""" - import mock -from nova.network import model as network_model -from nova.tests.unit.objects import test_virtual_interface -from oslo_config import cfg - from hyperv.nova import vif -from hyperv.tests import fake_instance from hyperv.tests.unit import test_base -CONF = cfg.CONF - - -class GetVIFDriverTestCase(test_base.HyperVBaseTestCase): - - def _test_get_vif_driver(self, expected_driver, vif_type, - network_class='nova.network.api.API', - expected_exception=None): - self.flags(network_api_class=network_class) - if expected_exception: - self.assertRaises(expected_exception, - vif.get_vif_driver, - vif_type) - else: - actual_class = type(vif.get_vif_driver(vif_type)) - self.assertEqual(expected_driver, actual_class) - - def test_get_vif_driver_neutron(self): - self._test_get_vif_driver( - expected_driver=vif.HyperVNeutronVIFDriver, - vif_type=network_model.VIF_TYPE_OTHER, - network_class='nova.network.neutronv2.api.API') - - def test_get_vif_driver_nova(self): - self._test_get_vif_driver( - expected_driver=vif.HyperVNovaNetworkVIFDriver, - vif_type=network_model.VIF_TYPE_OTHER, - network_class='nova.network.api.API') - - def test_get_vif_driver_ovs(self): - self._test_get_vif_driver(expected_driver=vif.HyperVOVSVIFDriver, - vif_type=network_model.VIF_TYPE_OVS) - - def test_get_vif_driver_invalid_class(self): - self._test_get_vif_driver( - expected_driver=None, - vif_type=network_model.VIF_TYPE_OTHER, - network_class='fake.driver', - expected_exception=TypeError) - - -class HyperVOVSVIFDriverTestCase(test_base.HyperVBaseTestCase): +class HyperVNovaNetworkVIFDriverTestCase(test_base.HyperVBaseTestCase): def setUp(self): - super(HyperVOVSVIFDriverTestCase, self).setUp() - - self.context = 'fake-context' - self.instance = fake_instance.fake_instance_obj(self.context) - self._vif = vif.HyperVOVSVIFDriver() - self._vif._vmutils = mock.MagicMock() - self._vif._netutils = mock.MagicMock() - - self._fake_vif = dict(test_virtual_interface.fake_vif, - network={'bridge': 'fake_bridge'}) + super(HyperVNovaNetworkVIFDriverTestCase, self).setUp() + self.vif_driver = vif.HyperVNovaNetworkVIFDriver() def test_plug(self): - mock_get_external_vswitch = self._vif._netutils.get_external_vswitch - mock_set_nic_connection = self._vif._vmutils.set_nic_connection - self._vif._netutils.vswitch_port_needed.return_value = False + self.flags(vswitch_name=mock.sentinel.vswitch_name, group='hyperv') + fake_vif = {'id': mock.sentinel.fake_id} - self._vif.plug(self.instance, self._fake_vif) - - mock_set_nic_connection.assert_called_once_with( - self.instance.name, - self._fake_vif['id'], - mock_get_external_vswitch()) - - @mock.patch('nova.utils.execute') - @mock.patch('hyperv.nova.ovsutils.check_bridge_has_dev') - def _test_post_start(self, mock_check_bridge_has_dev, mock_execute, calls, - bridge_has_dev=True): - mock_check_bridge_has_dev.return_value = bridge_has_dev - self._vif.post_start(self.instance, self._fake_vif) - mock_execute.assert_has_calls(calls) - - def test_post_start_no_dev(self, bridge_has_dev=False): - calls = [ - mock.call('ovs-vsctl', '--timeout=120', '--', '--if-exists', - 'del-port', self._fake_vif['id'], - '--', 'add-port', - self._fake_vif['network']['bridge'], - self._fake_vif['id'], '--', 'set', 'Interface', - self._fake_vif['id'], - 'external-ids:iface-id=%s' % self._fake_vif['id'], - 'external-ids:iface-status=active', - 'external-ids:attached-mac=%s' % - self._fake_vif['address'], - 'external-ids:vm-uuid=%s' % self.instance.uuid) - ] - self._test_post_start(calls=calls, bridge_has_dev=bridge_has_dev) - - def test_post_start(self): - self._test_post_start(calls=[]) - - @mock.patch('nova.utils.execute') - def test_unplug(self, mock_execute): - calls = [ - mock.call( - 'ovs-vsctl', '--timeout=120', '--', - '--if-exists', 'del-port', - self._fake_vif['network']['bridge'], - self._fake_vif['id']) - ] - - self._vif.unplug(self.instance, self._fake_vif) - mock_execute.assert_has_calls(calls) + self.vif_driver.plug(mock.sentinel.instance, fake_vif) + netutils = self.vif_driver._netutils + netutils.connect_vnic_to_vswitch.assert_called_once_with( + mock.sentinel.vswitch_name, mock.sentinel.fake_id) diff --git a/hyperv/tests/unit/test_vmops.py b/hyperv/tests/unit/test_vmops.py index 49277a94..e1d21242 100644 --- a/hyperv/tests/unit/test_vmops.py +++ b/hyperv/tests/unit/test_vmops.py @@ -22,6 +22,7 @@ from nova.objects import flavor as flavor_obj from nova.tests.unit.objects import test_flavor from nova.tests.unit.objects import test_virtual_interface from nova.virt import hardware +from os_win import exceptions as os_win_exc from oslo_concurrency import processutils from oslo_config import cfg from oslo_utils import fileutils @@ -31,7 +32,6 @@ import six from hyperv.nova import block_device_manager from hyperv.nova import constants from hyperv.nova import vmops -from hyperv.nova import vmutils from hyperv.nova import volumeops from hyperv.tests import fake_instance from hyperv.tests.unit import test_base @@ -134,7 +134,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): mock_instance = fake_instance.fake_instance_obj(self.context) mock_instance.root_gb = self.FAKE_SIZE self.flags(use_cow_images=use_cow_images) - self._vmops._vhdutils.get_vhd_info.return_value = {'MaxInternalSize': + self._vmops._vhdutils.get_vhd_info.return_value = {'VirtualSize': vhd_size * units.Gi} self._vmops._vhdutils.get_vhd_format.return_value = vhd_format root_vhd_internal_size = mock_instance.root_gb * units.Gi @@ -348,7 +348,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): mock_ephemeral_info) mock_create_dynamic_vhd.assert_called_once_with('fake_eph_path', - 10 * units.Gi, 'vhd') + 10 * units.Gi) @mock.patch.object(block_device_manager.BlockDeviceInfoManager, 'get_boot_order') @@ -406,8 +406,8 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): self.context, mock_instance, mock_image_meta, [mock.sentinel.FILE], mock.sentinel.PASSWORD, mock.sentinel.INFO, block_device_info) - elif fail is vmutils.HyperVException: - self.assertRaises(vmutils.HyperVException, self._vmops.spawn, + elif fail is os_win_exc.HyperVException: + self.assertRaises(os_win_exc.HyperVException, self._vmops.spawn, self.context, mock_instance, mock_image_meta, [mock.sentinel.FILE], mock.sentinel.PASSWORD, mock.sentinel.INFO, block_device_info) @@ -464,7 +464,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): self._test_spawn(exists=False, root_device_info=root_device_info, block_device_info=block_device_info, configdrive_required=True, - fail=vmutils.HyperVException) + fail=os_win_exc.HyperVException) def test_spawn_not_required(self): root_device_info = mock.sentinel.ROOT_DEV_INFO @@ -475,8 +475,8 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): def test_spawn_no_admin_permissions(self): self._vmops._vmutils.check_admin_permissions.side_effect = ( - vmutils.HyperVException) - self.assertRaises(vmutils.HyperVException, + os_win_exc.HyperVException) + self.assertRaises(os_win_exc.HyperVException, self._vmops.spawn, self.context, mock.DEFAULT, mock.DEFAULT, [mock.sentinel.FILE], mock.sentinel.PASSWORD, @@ -532,7 +532,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): mock_instance.flavor = flavor if remotefx is True and vm_gen == constants.VM_GEN_2: - self.assertRaises(vmutils.HyperVException, + self.assertRaises(os_win_exc.HyperVException, self._vmops.create_instance, instance=mock_instance, network_info=[fake_network_info], @@ -923,10 +923,11 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): @mock.patch('hyperv.nova.vmops.VMOps.power_off') def test_destroy_exception(self, mock_power_off): mock_instance = fake_instance.fake_instance_obj(self.context) - self._vmops._vmutils.destroy_vm.side_effect = vmutils.HyperVException + self._vmops._vmutils.destroy_vm.side_effect = ( + os_win_exc.HyperVException) self._vmops._vmutils.vm_exists.return_value = True - self.assertRaises(vmutils.HyperVException, + self.assertRaises(os_win_exc.HyperVException, self._vmops.destroy, mock_instance) def test_reboot_hard(self): @@ -949,10 +950,11 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): @mock.patch("hyperv.nova.vmops.VMOps._soft_shutdown") def test_reboot_soft_exception(self, mock_soft_shutdown, mock_power_on): mock_soft_shutdown.return_value = True - mock_power_on.side_effect = vmutils.HyperVException("Expected failure") + mock_power_on.side_effect = os_win_exc.HyperVException( + "Expected failure") instance = fake_instance.fake_instance_obj(self.context) - self.assertRaises(vmutils.HyperVException, self._vmops.reboot, + self.assertRaises(os_win_exc.HyperVException, self._vmops.reboot, instance, {}, vmops.REBOOT_TYPE_SOFT) mock_soft_shutdown.assert_called_once_with(instance) @@ -983,7 +985,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): instance = fake_instance.fake_instance_obj(self.context) mock_shutdown_vm = self._vmops._vmutils.soft_shutdown_vm - mock_shutdown_vm.side_effect = vmutils.HyperVException( + mock_shutdown_vm.side_effect = os_win_exc.HyperVException( "Expected failure.") result = self._vmops._soft_shutdown(instance, self._FAKE_TIMEOUT) @@ -1071,8 +1073,8 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): @mock.patch("hyperv.nova.vmops.VMOps._soft_shutdown") def test_power_off_unexisting_instance(self, mock_soft_shutdown): - mock_soft_shutdown.side_effect = ( - exception.InstanceNotFound('fake_instance_uuid')) + mock_soft_shutdown.side_effect = os_win_exc.HyperVVMNotFoundException( + vm_name=mock.sentinel.vm_name) self._test_power_off(timeout=1, set_state_expected=False) @mock.patch("hyperv.nova.vmops.VMOps._set_vm_state") @@ -1144,8 +1146,10 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): def test_set_vm_state_exception(self): mock_instance = fake_instance.fake_instance_obj(self.context) - self._vmops._vmutils.set_vm_state.side_effect = vmutils.HyperVException - self.assertRaises(vmutils.HyperVException, self._vmops._set_vm_state, + self._vmops._vmutils.set_vm_state.side_effect = ( + os_win_exc.HyperVException) + self.assertRaises(os_win_exc.HyperVException, + self._vmops._set_vm_state, mock_instance, mock.sentinel.STATE) def test_get_vm_state(self): @@ -1682,7 +1686,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): else: expected_result = image_prop_secure_boot == 'required' if fake_vm_gen != constants.VM_GEN_2 and expected_result: - self.assertRaises(vmutils.HyperVException, + self.assertRaises(os_win_exc.HyperVException, self._vmops._requires_secure_boot, mock_instance, mock_image_meta) else: diff --git a/hyperv/tests/unit/test_volumeops.py b/hyperv/tests/unit/test_volumeops.py index 788ca6f6..2f990cf9 100644 --- a/hyperv/tests/unit/test_volumeops.py +++ b/hyperv/tests/unit/test_volumeops.py @@ -17,6 +17,7 @@ import os import mock +from os_win import exceptions as os_win_exc from oslo_config import cfg from nova import exception @@ -24,8 +25,6 @@ from nova.tests.unit import fake_block_device from oslo_utils import units from hyperv.nova import constants -from hyperv.nova import pathutils -from hyperv.nova import vmutils from hyperv.nova import volumeops from hyperv.tests.unit import test_base @@ -145,16 +144,15 @@ class VolumeOpsTestCase(test_base.HyperVBaseTestCase): fake_volume_driver.disconnect_volumes.assert_called_once_with( block_device_mapping) - def test_ebs_root_in_block_devices(self): + @mock.patch('nova.block_device.volume_in_mapping') + def test_ebs_root_in_block_devices(self, mock_vol_in_mapping): block_device_info = get_fake_block_dev_info() response = self._volumeops.ebs_root_in_block_devices(block_device_info) - self._volumeops._volutils.volume_in_mapping.assert_called_once_with( + mock_vol_in_mapping.assert_called_once_with( self._volumeops._default_root_device, block_device_info) - self.assertEqual( - self._volumeops._volutils.volume_in_mapping.return_value, - response) + self.assertEqual(mock_vol_in_mapping.return_value, response) def test_get_volume_connector(self): mock_instance = mock.DEFAULT @@ -316,9 +314,9 @@ class ISCSIVolumeDriverTestCase(test_base.HyperVBaseTestCase): mock_logout_storage_target, mock_get_mounted_disk): connection_info = get_fake_connection_info() - mock_get_mounted_disk.side_effect = vmutils.HyperVException + mock_get_mounted_disk.side_effect = os_win_exc.HyperVException - self.assertRaises(vmutils.HyperVException, + self.assertRaises(os_win_exc.HyperVException, self._volume_driver.attach_volume, connection_info, mock.sentinel.instance_name) mock_logout_storage_target.assert_called_with(mock.sentinel.fake_iqn) @@ -491,7 +489,8 @@ class SMBFSVolumeDriverTestCase(test_base.HyperVBaseTestCase): super(SMBFSVolumeDriverTestCase, self).setUp() self._volume_driver = volumeops.SMBFSVolumeDriver() self._volume_driver._vmutils = mock.MagicMock() - self._volume_driver._pathutils = mock.MagicMock() + self._volume_driver._smbutils = mock.MagicMock() + self._volume_driver._volutils = mock.MagicMock() @mock.patch.object(volumeops.SMBFSVolumeDriver, 'ensure_share_mounted') @mock.patch.object(volumeops.SMBFSVolumeDriver, '_get_disk_path') @@ -543,15 +542,14 @@ class SMBFSVolumeDriverTestCase(test_base.HyperVBaseTestCase): def test_attach_non_existing_image(self, mock_get_disk_path, mock_ensure_share_mounted): self._volume_driver._vmutils.attach_drive.side_effect = ( - vmutils.HyperVException()) + os_win_exc.HyperVException) self.assertRaises(exception.VolumeAttachFailed, self._volume_driver.attach_volume, self._FAKE_CONNECTION_INFO, mock.sentinel.instance_name) @mock.patch.object(volumeops.SMBFSVolumeDriver, '_get_disk_path') - @mock.patch.object(pathutils.PathUtils, 'unmount_smb_share') - def test_detach_volume(self, mock_unmount_smb_share, mock_get_disk_path): + def test_detach_volume(self, mock_get_disk_path): mock_get_disk_path.return_value = ( mock.sentinel.disk_path) @@ -586,9 +584,9 @@ class SMBFSVolumeDriverTestCase(test_base.HyperVBaseTestCase): @mock.patch.object(volumeops.SMBFSVolumeDriver, '_parse_credentials') def _test_ensure_mounted(self, mock_parse_credentials, is_mounted=False): - mock_mount_smb_share = self._volume_driver._pathutils.mount_smb_share + mock_mount_smb_share = self._volume_driver._smbutils.mount_smb_share mock_check_smb_mapping = ( - self._volume_driver._pathutils.check_smb_mapping) + self._volume_driver._smbutils.check_smb_mapping) mock_check_smb_mapping.return_value = is_mounted mock_parse_credentials.return_value = ( self._FAKE_USERNAME, self._FAKE_PASSWORD) @@ -613,7 +611,7 @@ class SMBFSVolumeDriverTestCase(test_base.HyperVBaseTestCase): def test_disconnect_volumes(self): mock_unmount_smb_share = ( - self._volume_driver._pathutils.unmount_smb_share) + self._volume_driver._smbutils.unmount_smb_share) block_device_mapping = [ {'connection_info': self._FAKE_CONNECTION_INFO}] self._volume_driver.disconnect_volumes(block_device_mapping) diff --git a/requirements.txt b/requirements.txt index c517b2bf..9a4426a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ pbr>=1.6 Babel>=1.3 +os-win>=0.0.6 # Apache-2.0 oslo.config>=2.3.0 # Apache-2.0 oslo.log>=1.8.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0