diff --git a/os_brick/initiator/__init__.py b/os_brick/initiator/__init__.py index 0f5c33a21..e25ae8d56 100644 --- a/os_brick/initiator/__init__.py +++ b/os_brick/initiator/__init__.py @@ -19,3 +19,39 @@ The initator module contains the capabilities for discovering the initiator information as well as discovering and removing volumes from a host. """ + +import re + + +DEVICE_SCAN_ATTEMPTS_DEFAULT = 3 +MULTIPATH_ERROR_REGEX = re.compile("\w{3} \d+ \d\d:\d\d:\d\d \|.*$") +MULTIPATH_DEV_CHECK_REGEX = re.compile("\s+dm-\d+\s+") +MULTIPATH_PATH_CHECK_REGEX = re.compile("\s+\d+:\d+:\d+:\d+\s+") + +PLATFORM_ALL = 'ALL' +PLATFORM_x86 = 'X86' +PLATFORM_S390 = 'S390' +OS_TYPE_ALL = 'ALL' +OS_TYPE_LINUX = 'LINUX' +OS_TYPE_WINDOWS = 'WIN' + +S390X = "s390x" +S390 = "s390" + +ISCSI = "ISCSI" +ISER = "ISER" +FIBRE_CHANNEL = "FIBRE_CHANNEL" +AOE = "AOE" +DRBD = "DRBD" +NFS = "NFS" +GLUSTERFS = "GLUSTERFS" +LOCAL = "LOCAL" +HUAWEISDSHYPERVISOR = "HUAWEISDSHYPERVISOR" +HGST = "HGST" +RBD = "RBD" +SCALEIO = "SCALEIO" +SCALITY = "SCALITY" +QUOBYTE = "QUOBYTE" +DISCO = "DISCO" +VZSTORAGE = "VZSTORAGE" +SHEEPDOG = "SHEEPDOG" diff --git a/os_brick/initiator/connector.py b/os_brick/initiator/connector.py index 76de3ae7f..823b9aac2 100644 --- a/os_brick/initiator/connector.py +++ b/os_brick/initiator/connector.py @@ -20,45 +20,26 @@ The connectors here are responsible for discovering and removing volumes for each of the supported transport protocols. """ -import abc -import copy -import glob -import json -import os import platform import re -import requests import socket -import struct import sys -import tempfile -import time from oslo_concurrency import lockutils -from oslo_concurrency import processutils as putils from oslo_log import log as logging -from oslo_service import loopingcall from oslo_utils import importutils -from oslo_utils import strutils -import six -from six.moves import urllib -from os_brick import exception -from os_brick import executor +from os_brick.i18n import _ +from os_brick import initiator from os_brick import utils -from os_brick.initiator import host_driver -from os_brick.initiator import linuxfc -from os_brick.initiator import linuxrbd -from os_brick.initiator import linuxscsi -from os_brick.initiator import linuxsheepdog -from os_brick.initiator import windows as windows_connector -from os_brick.remotefs import remotefs -from os_brick.i18n import _, _LE, _LI, _LW - LOG = logging.getLogger(__name__) synchronized = lockutils.synchronized_with_prefix('os-brick-') + +# These constants are being deprecated and moving to the init file. +# Please use the constants there instead. + DEVICE_SCAN_ATTEMPTS_DEFAULT = 3 MULTIPATH_ERROR_REGEX = re.compile("\w{3} \d+ \d\d:\d\d:\d\d \|.*$") MULTIPATH_DEV_CHECK_REGEX = re.compile("\s+dm-\d+\s+") @@ -92,24 +73,81 @@ DISCO = "DISCO" VZSTORAGE = "VZSTORAGE" SHEEPDOG = "SHEEPDOG" +# List of connectors to call when getting +# the connector properties for a host connector_list = [ - 'os_brick.initiator.connector.BaseLinuxConnector', - 'os_brick.initiator.connector.ISCSIConnector', - 'os_brick.initiator.connector.FibreChannelConnector', - 'os_brick.initiator.connector.FibreChannelConnectorS390X', - 'os_brick.initiator.connector.AoEConnector', - 'os_brick.initiator.connector.RemoteFsConnector', - 'os_brick.initiator.connector.RBDConnector', - 'os_brick.initiator.connector.LocalConnector', - 'os_brick.initiator.connector.DRBDConnector', - 'os_brick.initiator.connector.HuaweiStorHyperConnector', - 'os_brick.initiator.connector.HGSTConnector', - 'os_brick.initiator.connector.ScaleIOConnector', - 'os_brick.initiator.connector.DISCOConnector', + 'os_brick.initiator.connectors.base.BaseLinuxConnector', + 'os_brick.initiator.connectors.iscsi.ISCSIConnector', + 'os_brick.initiator.connectors.fibre_channel.FibreChannelConnector', + ('os_brick.initiator.connectors.fibre_channel_s390x.' + 'FibreChannelConnectorS390X'), + 'os_brick.initiator.connectors.aoe.AoEConnector', + 'os_brick.initiator.connectors.remotefs.RemoteFsConnector', + 'os_brick.initiator.connectors.rbd.RBDConnector', + 'os_brick.initiator.connectors.local.LocalConnector', + 'os_brick.initiator.connectors.drbd.DRBDConnector', + 'os_brick.initiator.connectors.huawei.HuaweiStorHyperConnector', + 'os_brick.initiator.connectors.hgst.HGSTConnector', + 'os_brick.initiator.connectors.scaleio.ScaleIOConnector', + 'os_brick.initiator.connectors.disco.DISCOConnector', 'os_brick.initiator.windows.base.BaseWindowsConnector', 'os_brick.initiator.windows.iscsi.WindowsISCSIConnector', ] +# Mappings used to determine who to contruct in the factory +_connector_mapping_linux = { + initiator.AOE: + 'os_brick.initiator.connectors.aoe.AoEConnector', + initiator.DRBD: + 'os_brick.initiator.connectors.drbd.DRBDConnector', + + initiator.GLUSTERFS: + 'os_brick.initiator.connectors.remotefs.RemoteFsConnector', + initiator.NFS: + 'os_brick.initiator.connectors.remotefs.RemoteFsConnector', + initiator.SCALITY: + 'os_brick.initiator.connectors.remotefs.RemoteFsConnector', + initiator.QUOBYTE: + 'os_brick.initiator.connectors.remotefs.RemoteFsConnector', + initiator.VZSTORAGE: + 'os_brick.initiator.connectors.remotefs.RemoteFsConnector', + + initiator.ISCSI: + 'os_brick.initiator.connectors.iscsi.ISCSIConnector', + initiator.ISER: + 'os_brick.initiator.connectors.iscsi.ISCSIConnector', + initiator.FIBRE_CHANNEL: + 'os_brick.initiator.connectors.fibre_channel.FibreChannelConnector', + + initiator.LOCAL: + 'os_brick.initiator.connectors.local.LocalConnector', + initiator.HUAWEISDSHYPERVISOR: + 'os_brick.initiator.connectors.huawei.HuaweiStorHyperConnector', + initiator.HGST: + 'os_brick.initiator.connectors.hgst.HGSTConnector', + initiator.RBD: + 'os_brick.initiator.connectors.rbd.RBDConnector', + initiator.SCALEIO: + 'os_brick.initiator.connectors.scaleio.ScaleIOConnector', + initiator.DISCO: + 'os_brick.initiator.connectors.disco.DISCOConnector', + initiator.SHEEPDOG: + 'os_brick.initiator.connectors.sheepdog.SheepdogConnector', +} + +# Mapping for the S390X platform +_connector_mapping_linux_s390x = { + initiator.FIBRE_CHANNEL: + ('os_brick.initiator.connectors.fibre_channel_s390x.' + 'FibreChannelConnectorS390X'), +} + +# Mapping for the windows connectors +_connector_mapping_windows = { + initiator.ISCSI: + 'os_brick.initiator.windows.iscsi.WindowsISCSIConnector', +} + @utils.trace def get_connector_properties(root_helper, my_ip, multipath, enforce_multipath, @@ -160,3156 +198,57 @@ def get_connector_properties(root_helper, my_ip, multipath, enforce_multipath, return props -@six.add_metaclass(abc.ABCMeta) -class InitiatorConnector(executor.Executor): - - # This object can be used on any platform (x86, S390) - platform = PLATFORM_ALL - - # This object can be used on any os type (linux, windows) - os_type = OS_TYPE_ALL - - def __init__(self, root_helper, driver=None, execute=None, - device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, - *args, **kwargs): - super(InitiatorConnector, self).__init__(root_helper, execute=execute, - *args, **kwargs) - self.device_scan_attempts = device_scan_attempts - - def set_driver(self, driver): - """The driver is used to find used LUNs.""" - self.driver = driver - - @abc.abstractmethod - def get_connector_properties(root_helper, *args, **kwargs): - """The generic connector properties.""" - pass +# TODO(walter-boring) We have to keep this class defined here +# so we don't break backwards compatibility +class InitiatorConnector(object): @staticmethod def factory(protocol, root_helper, driver=None, use_multipath=False, - device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, + device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT, arch=None, *args, **kwargs): """Build a Connector object based upon protocol and architecture.""" - if sys.platform == 'win32': - return windows_connector.factory( - protocol, - use_multipath=use_multipath, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - # We do this instead of assigning it in the definition # to help mocking for unit tests if arch is None: arch = platform.machine() + # Set the correct mapping for imports + if sys.platform == 'win32': + _mapping = _connector_mapping_windows + elif arch in (initiator.S390, initiator.S390X): + _mapping = _connector_mapping_linux_s390x + else: + _mapping = _connector_mapping_linux + LOG.debug("Factory for %(protocol)s on %(arch)s", {'protocol': protocol, 'arch': arch}) protocol = protocol.upper() - if protocol in [ISCSI, ISER]: - # override transport kwarg for requests not comming - # from the nova LibvirtISERVolumeDriver - if protocol == ISER: - kwargs.update({'transport': 'iser'}) - return ISCSIConnector(root_helper=root_helper, - driver=driver, - use_multipath=use_multipath, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - elif protocol == FIBRE_CHANNEL: - if arch in (S390, S390X): - return FibreChannelConnectorS390X( - root_helper=root_helper, - driver=driver, - use_multipath=use_multipath, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - else: - return FibreChannelConnector( - root_helper=root_helper, - driver=driver, - use_multipath=use_multipath, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - elif protocol == AOE: - return AoEConnector(root_helper=root_helper, - driver=driver, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - elif protocol in (NFS, GLUSTERFS, SCALITY, QUOBYTE, VZSTORAGE): - return RemoteFsConnector(mount_type=protocol.lower(), - root_helper=root_helper, - driver=driver, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - elif protocol == DRBD: - return DRBDConnector(root_helper=root_helper, - driver=driver, - *args, **kwargs) - elif protocol == LOCAL: - return LocalConnector(root_helper=root_helper, - driver=driver, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - elif protocol == HUAWEISDSHYPERVISOR: - return HuaweiStorHyperConnector( - root_helper=root_helper, - driver=driver, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - elif protocol == HGST: - return HGSTConnector(root_helper=root_helper, - driver=driver, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - elif protocol == RBD: - return RBDConnector(root_helper=root_helper, - driver=driver, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - elif protocol == SCALEIO: - return ScaleIOConnector( - root_helper=root_helper, - driver=driver, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - elif protocol == DISCO: - return DISCOConnector( - root_helper=root_helper, - driver=driver, - device_scan_attempts=device_scan_attempts, - *args, **kwargs - ) - elif protocol == SHEEPDOG: - return SheepdogConnector(root_helper=root_helper, - driver=driver, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - else: + + # set any special kwargs needed by connectors + if protocol in (initiator.NFS, initiator.GLUSTERFS, + initiator.SCALITY, initiator.QUOBYTE, + initiator.VZSTORAGE): + kwargs.update({'mount_type': protocol.lower()}) + elif protocol == initiator.ISER: + kwargs.update({'transport': 'iser'}) + + # now set all the default kwargs + kwargs.update( + {'root_helper': root_helper, + 'driver': driver, + 'use_multipath': use_multipath, + 'device_scan_attempts': device_scan_attempts, + }) + + connector = _mapping.get(protocol) + if not connector: msg = (_("Invalid InitiatorConnector protocol " "specified %(protocol)s") % dict(protocol=protocol)) raise ValueError(msg) - @abc.abstractmethod - def check_valid_device(self, path, run_as_root=True): - """Test to see if the device path is a real device. - - :param path: The file system path for the device. - :type path: str - :param run_as_root: run the tests as root user? - :type run_as_root: bool - :returns: bool - """ - pass - - @abc.abstractmethod - def connect_volume(self, connection_properties): - """Connect to a volume. - - The connection_properties describes the information needed by - the specific protocol to use to make the connection. - - The connection_properties is a dictionary that describes the target - volume. It varies slightly by protocol type (iscsi, fibre_channel), - but the structure is usually the same. - - - An example for iSCSI: - - {'driver_volume_type': 'iscsi', - 'data': { - 'target_luns': [0, 2], - 'target_iqns': ['iqn.2000-05.com.3pardata:20810002ac00383d', - 'iqn.2000-05.com.3pardata:21810002ac00383d'], - 'target_discovered': True, - 'encrypted': False, - 'qos_specs': None, - 'target_portals': ['10.52.1.11:3260', '10.52.2.11:3260'], - 'access_mode': 'rw', - }} - - An example for fibre_channel: - - {'driver_volume_type': 'fibre_channel', - 'data': { - 'initiator_target_map': {'100010604b010459': ['21230002AC00383D'], - '100010604b01045d': ['21230002AC00383D'] - }, - 'target_discovered': True, - 'encrypted': False, - 'qos_specs': None, - 'target_lun': 1, - 'access_mode': 'rw', - 'target_wwn': [ - '20210002AC00383D', - '20220002AC00383D', - ], - }} - - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :returns: dict - """ - pass - - @abc.abstractmethod - def disconnect_volume(self, connection_properties, device_info): - """Disconnect a volume from the local host. - - The connection_properties are the same as from connect_volume. - The device_info is returned from connect_volume. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :param device_info: historical difference, but same as connection_props - :type device_info: dict - """ - pass - - @abc.abstractmethod - def get_volume_paths(self, connection_properties): - """Return the list of existing paths for a volume. - - The job of this method is to find out what paths in - the system are associated with a volume as described - by the connection_properties. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - """ - pass - - @abc.abstractmethod - def get_search_path(self): - """Return the directory where a Connector looks for volumes. - - Some Connectors need the information in the - connection_properties to determine the search path. - """ - pass - - @abc.abstractmethod - def extend_volume(self, connection_properties): - """Update the attached volume's size. - - This method will attempt to update the local hosts's - volume after the volume has been extended on the remote - system. The new volume size in bytes will be returned. - If there is a failure to update, then None will be returned. - - :param connection_properties: The volume connection properties. - :returns: new size of the volume. - """ - pass - - @abc.abstractmethod - def get_all_available_volumes(self, connection_properties=None): - """Return all volumes that exist in the search directory. - - At connect_volume time, a Connector looks in a specific - directory to discover a volume's paths showing up. - This method's job is to return all paths in the directory - that connect_volume uses to find a volume. - - This method is used in coordination with get_volume_paths() - to verify that volumes have gone away after disconnect_volume - has been called. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - """ - pass - - def check_IO_handle_valid(self, handle, data_type, protocol): - """Check IO handle has correct data type.""" - if (handle and not isinstance(handle, data_type)): - raise exception.InvalidIOHandleObject( - protocol=protocol, - actual_type=type(handle)) - - -class BaseLinuxConnector(InitiatorConnector): - os_type = OS_TYPE_LINUX - - def __init__(self, root_helper, driver=None, execute=None, - *args, **kwargs): - self._linuxscsi = linuxscsi.LinuxSCSI(root_helper, execute=execute) - - if not driver: - driver = host_driver.HostDriver() - self.set_driver(driver) - - super(BaseLinuxConnector, self).__init__(root_helper, execute=execute, - *args, **kwargs) - - @staticmethod - def get_connector_properties(root_helper, *args, **kwargs): - """The generic connector properties.""" - multipath = kwargs['multipath'] - enforce_multipath = kwargs['enforce_multipath'] - props = {} - - props['multipath'] = (multipath and - linuxscsi.LinuxSCSI.is_multipath_running( - enforce_multipath, root_helper, - execute=kwargs.get('execute'))) - - return props - - def check_valid_device(self, path, run_as_root=True): - cmd = ('dd', 'if=%(path)s' % {"path": path}, - 'of=/dev/null', 'count=1') - out, info = None, None - try: - out, info = self._execute(*cmd, run_as_root=run_as_root, - root_helper=self._root_helper) - except putils.ProcessExecutionError as e: - LOG.error(_LE("Failed to access the device on the path " - "%(path)s: %(error)s."), - {"path": path, "error": e.stderr}) - return False - # If the info is none, the path does not exist. - if info is None: - return False - return True - - def get_all_available_volumes(self, connection_properties=None): - volumes = [] - path = self.get_search_path() - if path: - # now find all entries in the search path - if os.path.isdir(path): - path_items = [path, '/*'] - file_filter = ''.join(path_items) - volumes = glob.glob(file_filter) - - return volumes - - def _discover_mpath_device(self, device_wwn, connection_properties, - device_name): - """This method discovers a multipath device. - - Discover a multipath device based on a defined connection_property - and a device_wwn and return the multipath_id and path of the multipath - enabled device if there is one. - """ - - path = self._linuxscsi.find_multipath_device_path(device_wwn) - device_path = None - multipath_id = None - - if path is None: - # find_multipath_device only accept realpath not symbolic path - device_realpath = os.path.realpath(device_name) - mpath_info = self._linuxscsi.find_multipath_device( - device_realpath) - if mpath_info: - device_path = mpath_info['device'] - multipath_id = device_wwn - else: - # we didn't find a multipath device. - # so we assume the kernel only sees 1 device - device_path = device_name - LOG.debug("Unable to find multipath device name for " - "volume. Using path %(device)s for volume.", - {'device': device_path}) - else: - device_path = path - multipath_id = device_wwn - if connection_properties.get('access_mode', '') != 'ro': - try: - # Sometimes the multipath devices will show up as read only - # initially and need additional time/rescans to get to RW. - self._linuxscsi.wait_for_rw(device_wwn, device_path) - except exception.BlockDeviceReadOnly: - LOG.warning(_LW('Block device %s is still read-only. ' - 'Continuing anyway.'), device_path) - return device_path, multipath_id - - -class FakeConnector(BaseLinuxConnector): - - fake_path = '/dev/vdFAKE' - - def connect_volume(self, connection_properties): - fake_device_info = {'type': 'fake', - 'path': self.fake_path} - return fake_device_info - - def disconnect_volume(self, connection_properties, device_info): - pass - - def get_volume_paths(self, connection_properties): - return [self.fake_path] - - def get_search_path(self): - return '/dev/disk/by-path' - - def extend_volume(self, connection_properties): - return None - - def get_all_available_volumes(self, connection_properties=None): - return ['/dev/disk/by-path/fake-volume-1', - '/dev/disk/by-path/fake-volume-X'] - - -class BaseISCSIConnector(InitiatorConnector): - def _iterate_all_targets(self, connection_properties): - for portal, iqn, lun in self._get_all_targets(connection_properties): - props = copy.deepcopy(connection_properties) - props['target_portal'] = portal - props['target_iqn'] = iqn - props['target_lun'] = lun - for key in ('target_portals', 'target_iqns', 'target_luns'): - props.pop(key, None) - yield props - - def _get_all_targets(self, connection_properties): - if all([key in connection_properties for key in ('target_portals', - 'target_iqns', - 'target_luns')]): - return zip(connection_properties['target_portals'], - connection_properties['target_iqns'], - connection_properties['target_luns']) - - return [(connection_properties['target_portal'], - connection_properties['target_iqn'], - connection_properties.get('target_lun', 0))] - - -class FakeBaseISCSIConnector(FakeConnector, BaseISCSIConnector): - pass - - -class ISCSIConnector(BaseLinuxConnector, BaseISCSIConnector): - """Connector class to attach/detach iSCSI volumes.""" - supported_transports = ['be2iscsi', 'bnx2i', 'cxgb3i', 'default', - 'cxgb4i', 'qla4xxx', 'ocs', 'iser'] - - def __init__(self, root_helper, driver=None, - execute=None, use_multipath=False, - device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, - transport='default', *args, **kwargs): - super(ISCSIConnector, self).__init__( - root_helper, driver=driver, - execute=execute, - device_scan_attempts=device_scan_attempts, - transport=transport, *args, **kwargs) - self.use_multipath = use_multipath - self.transport = self._validate_iface_transport(transport) - - @staticmethod - def get_connector_properties(root_helper, *args, **kwargs): - """The iSCSI connector properties.""" - props = {} - iscsi = ISCSIConnector(root_helper=root_helper, - execute=kwargs.get('execute')) - initiator = iscsi.get_initiator() - if initiator: - props['initiator'] = initiator - - return props - - def get_search_path(self): - """Where do we look for iSCSI based volumes.""" - return '/dev/disk/by-path' - - @utils.trace - def get_volume_paths(self, connection_properties): - """Get the list of existing paths for a volume. - - This method's job is to simply report what might/should - already exist for a volume. We aren't trying to attach/discover - a new volume, but find any existing paths for a volume we - think is already attached. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - """ - volume_paths = [] - - # if there are no sessions, then target_portal won't exist - if (('target_portal' not in connection_properties) and - ('target_portals' not in connection_properties)): - return volume_paths - - # Don't try and connect to the portals in the list as - # this can create empty iSCSI sessions to hosts if they - # didn't exist previously. - # We are simply trying to find any existing volumes with - # already connected sessions. - host_devices, target_props = self._get_potential_volume_paths( - connection_properties, - connect_to_portal=False, - use_rescan=False) - - for path in host_devices: - if os.path.exists(path): - volume_paths.append(path) - - return volume_paths - - def _get_iscsi_sessions(self): - out, err = self._run_iscsi_session() - - iscsi_sessions = [] - - if err: - LOG.warning(_LW("Couldn't find iscsi sessions because " - "iscsiadm err: %s"), - err) - else: - # parse the output from iscsiadm - # lines are in the format of - # tcp: [1] 192.168.121.250:3260,1 iqn.2010-10.org.openstack:volume- - lines = out.split('\n') - for line in lines: - if line: - entries = line.split() - portal = entries[2].split(',') - iscsi_sessions.append(portal[0]) - - return iscsi_sessions - - def _get_potential_volume_paths(self, connection_properties, - connect_to_portal=True, - use_rescan=True): - """Build a list of potential volume paths that exist. - - Given a list of target_portals in the connection_properties, - a list of paths might exist on the system during discovery. - This method's job is to build that list of potential paths - for a volume that might show up. - - This is used during connect_volume time, in which case we want - to connect to the iSCSI target portal. - - During get_volume_paths time, we are looking to - find a list of existing volume paths for the connection_properties. - In this case, we don't want to connect to the portal. If we - blindly try and connect to a portal, it could create a new iSCSI - session that didn't exist previously, and then leave it stale. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :param connect_to_portal: Do we want to try a new connection to the - target portal(s)? Set this to False if you - want to search for existing volumes, not - discover new volumes. - :param connect_to_portal: bool - :param use_rescan: Issue iSCSI rescan during discovery? - :type use_rescan: bool - :returns: dict - """ - - target_props = None - connected_to_portal = False - if self.use_multipath: - LOG.info(_LI("Multipath discovery for iSCSI enabled")) - # Multipath installed, discovering other targets if available - try: - ips_iqns = self._discover_iscsi_portals(connection_properties) - except Exception: - if 'target_portals' in connection_properties: - raise exception.TargetPortalsNotFound( - target_portal=connection_properties['target_portals']) - elif 'target_portal' in connection_properties: - raise exception.TargetPortalNotFound( - target_portal=connection_properties['target_portal']) - else: - raise - - if not connection_properties.get('target_iqns'): - # There are two types of iSCSI multipath devices. One which - # shares the same iqn between multiple portals, and the other - # which use different iqns on different portals. - # Try to identify the type by checking the iscsiadm output - # if the iqn is used by multiple portals. If it is, it's - # the former, so use the supplied iqn. Otherwise, it's the - # latter, so try the ip,iqn combinations to find the targets - # which constitutes the multipath device. - main_iqn = connection_properties['target_iqn'] - all_portals = set([ip for ip, iqn in ips_iqns]) - match_portals = set([ip for ip, iqn in ips_iqns - if iqn == main_iqn]) - if len(all_portals) == len(match_portals): - ips_iqns = zip(all_portals, [main_iqn] * len(all_portals)) - - for ip, iqn in ips_iqns: - props = copy.deepcopy(connection_properties) - props['target_portal'] = ip - props['target_iqn'] = iqn - if connect_to_portal: - if self._connect_to_iscsi_portal(props): - connected_to_portal = True - - if use_rescan: - self._rescan_iscsi() - host_devices = self._get_device_path(connection_properties) - else: - LOG.info(_LI("Multipath discovery for iSCSI not enabled.")) - iscsi_sessions = [] - if not connect_to_portal: - iscsi_sessions = self._get_iscsi_sessions() - - host_devices = [] - target_props = connection_properties - for props in self._iterate_all_targets(connection_properties): - if connect_to_portal: - if self._connect_to_iscsi_portal(props): - target_props = props - connected_to_portal = True - host_devices = self._get_device_path(props) - break - else: - LOG.warning(_LW( - 'Failed to connect to iSCSI portal %(portal)s.'), - {'portal': props['target_portal']}) - else: - # If we aren't trying to connect to the portal, we - # want to find ALL possible paths from all of the - # alternate portals - if props['target_portal'] in iscsi_sessions: - paths = self._get_device_path(props) - host_devices = list(set(paths + host_devices)) - - if connect_to_portal and not connected_to_portal: - msg = _("Could not login to any iSCSI portal.") - LOG.error(msg) - raise exception.FailedISCSITargetPortalLogin(message=msg) - - return host_devices, target_props - - def set_execute(self, execute): - super(ISCSIConnector, self).set_execute(execute) - self._linuxscsi.set_execute(execute) - - def _validate_iface_transport(self, transport_iface): - """Check that given iscsi_iface uses only supported transports - - Accepted transport names for provided iface param are - be2iscsi, bnx2i, cxgb3i, cxgb4i, default, qla4xxx, ocs or iser. - Note the difference between transport and iface; - unlike default(iscsi_tcp)/iser, this is not one and the same for - offloaded transports, where the default format is - transport_name.hwaddress - - :param transport_iface: The iscsi transport type. - :type transport_iface: str - :returns: str - """ - # Note that default(iscsi_tcp) and iser do not require a separate - # iface file, just the transport is enough and do not need to be - # validated. This is not the case for the other entries in - # supported_transports array. - if transport_iface in ['default', 'iser']: - return transport_iface - # Will return (6) if iscsi_iface file was not found, or (2) if iscsid - # could not be contacted - out = self._run_iscsiadm_bare(['-m', - 'iface', - '-I', - transport_iface], - check_exit_code=[0, 2, 6])[0] or "" - LOG.debug("iscsiadm %(iface)s configuration: stdout=%(out)s.", - {'iface': transport_iface, 'out': out}) - for data in [line.split() for line in out.splitlines()]: - if data[0] == 'iface.transport_name': - if data[2] in self.supported_transports: - return transport_iface - - LOG.warning(_LW("No useable transport found for iscsi iface %s. " - "Falling back to default transport."), - transport_iface) - return 'default' - - def _get_transport(self): - return self.transport - - def _discover_iscsi_portals(self, connection_properties): - if all([key in connection_properties for key in ('target_portals', - 'target_iqns')]): - # Use targets specified by connection_properties - return zip(connection_properties['target_portals'], - connection_properties['target_iqns']) - - out = None - if connection_properties.get('discovery_auth_method'): - try: - self._run_iscsiadm_update_discoverydb(connection_properties) - except putils.ProcessExecutionError as exception: - # iscsiadm returns 6 for "db record not found" - if exception.exit_code == 6: - # Create a new record for this target and update the db - self._run_iscsiadm_bare( - ['-m', 'discoverydb', - '-t', 'sendtargets', - '-p', connection_properties['target_portal'], - '--op', 'new'], - check_exit_code=[0, 255]) - self._run_iscsiadm_update_discoverydb( - connection_properties - ) - else: - LOG.error(_LE("Unable to find target portal: " - "%(target_portal)s."), - {'target_portal': connection_properties[ - 'target_portal']}) - raise - out = self._run_iscsiadm_bare( - ['-m', 'discoverydb', - '-t', 'sendtargets', - '-p', connection_properties['target_portal'], - '--discover'], - check_exit_code=[0, 255])[0] or "" - else: - out = self._run_iscsiadm_bare( - ['-m', 'discovery', - '-t', 'sendtargets', - '-p', connection_properties['target_portal']], - check_exit_code=[0, 255])[0] or "" - - return self._get_target_portals_from_iscsiadm_output(out) - - def _run_iscsiadm_update_discoverydb(self, connection_properties): - return self._execute( - 'iscsiadm', - '-m', 'discoverydb', - '-t', 'sendtargets', - '-p', connection_properties['target_portal'], - '--op', 'update', - '-n', "discovery.sendtargets.auth.authmethod", - '-v', connection_properties['discovery_auth_method'], - '-n', "discovery.sendtargets.auth.username", - '-v', connection_properties['discovery_auth_username'], - '-n', "discovery.sendtargets.auth.password", - '-v', connection_properties['discovery_auth_password'], - run_as_root=True, - root_helper=self._root_helper) - - @utils.trace - @synchronized('extend_volume') - def extend_volume(self, connection_properties): - """Update the local kernel's size information. - - Try and update the local kernel's size information - for an iSCSI volume. - """ - LOG.info(_LI("Extend volume for %s"), connection_properties) - - volume_paths = self.get_volume_paths(connection_properties) - LOG.info(_LI("Found paths for volume %s"), volume_paths) - if volume_paths: - return self._linuxscsi.extend_volume(volume_paths[0]) - else: - LOG.warning(_LW("Couldn't find any volume paths on the host to " - "extend volume for %(props)s"), - {'props': connection_properties}) - raise exception.VolumePathsNotFound() - - @utils.trace - @synchronized('connect_volume') - def connect_volume(self, connection_properties): - """Attach the volume to instance_name. - - :param connection_properties: The valid dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :returns: dict - - connection_properties for iSCSI must include: - target_portal(s) - ip and optional port - target_iqn(s) - iSCSI Qualified Name - target_lun(s) - LUN id of the volume - Note that plural keys may be used when use_multipath=True - """ - - device_info = {'type': 'block'} - - host_devices, target_props = self._get_potential_volume_paths( - connection_properties) - - # The /dev/disk/by-path/... node is not always present immediately - # TODO(justinsb): This retry-with-delay is a pattern, move to utils? - tries = 0 - # Loop until at least 1 path becomes available - while all(map(lambda x: not os.path.exists(x), host_devices)): - if tries >= self.device_scan_attempts: - raise exception.VolumeDeviceNotFound(device=host_devices) - - LOG.warning(_LW("ISCSI volume not yet found at: %(host_devices)s. " - "Will rescan & retry. Try number: %(tries)s."), - {'host_devices': host_devices, - 'tries': tries}) - - # The rescan isn't documented as being necessary(?), but it helps - if self.use_multipath: - self._rescan_iscsi() - else: - if (tries): - host_devices = self._get_device_path(target_props) - self._run_iscsiadm(target_props, ("--rescan",)) - - tries = tries + 1 - if all(map(lambda x: not os.path.exists(x), host_devices)): - time.sleep(tries ** 2) - else: - break - - if tries != 0: - LOG.debug("Found iSCSI node %(host_devices)s " - "(after %(tries)s rescans)", - {'host_devices': host_devices, 'tries': tries}) - - # Choose an accessible host device - host_device = next(dev for dev in host_devices if os.path.exists(dev)) - - # find out the WWN of the device - device_wwn = self._linuxscsi.get_scsi_wwn(host_device) - LOG.debug("Device WWN = '%(wwn)s'", {'wwn': device_wwn}) - device_info['scsi_wwn'] = device_wwn - - if self.use_multipath: - (host_device, multipath_id) = (super( - ISCSIConnector, self)._discover_mpath_device( - device_wwn, connection_properties, host_device)) - if multipath_id: - device_info['multipath_id'] = multipath_id - - device_info['path'] = host_device - return device_info - - @utils.trace - @synchronized('connect_volume') - def disconnect_volume(self, connection_properties, device_info): - """Detach the volume from instance_name. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :param device_info: historical difference, but same as connection_props - :type device_info: dict - - connection_properties for iSCSI must include: - target_portal(s) - IP and optional port - target_iqn(s) - iSCSI Qualified Name - target_lun(s) - LUN id of the volume - """ - if self.use_multipath: - self._rescan_multipath() - host_device = multipath_device = None - host_devices = self._get_device_path(connection_properties) - # Choose an accessible host device - for dev in host_devices: - if os.path.exists(dev): - host_device = dev - device_wwn = self._linuxscsi.get_scsi_wwn(dev) - (multipath_device, multipath_id) = (super( - ISCSIConnector, self)._discover_mpath_device( - device_wwn, connection_properties, dev)) - if multipath_device: - break - if not host_device: - LOG.error(_LE("No accessible volume device: %(host_devices)s"), - {'host_devices': host_devices}) - raise exception.VolumeDeviceNotFound(device=host_devices) - - if multipath_device: - device_realpath = os.path.realpath(host_device) - self._linuxscsi.remove_multipath_device(device_realpath) - return self._disconnect_volume_multipath_iscsi( - connection_properties, multipath_device) - - # When multiple portals/iqns/luns are specified, we need to remove - # unused devices created by logging into other LUNs' session. - for props in self._iterate_all_targets(connection_properties): - self._disconnect_volume_iscsi(props) - - def _disconnect_volume_iscsi(self, connection_properties): - # remove the device from the scsi subsystem - # this eliminates any stale entries until logout - host_devices = self._get_device_path(connection_properties) - - if host_devices: - host_device = host_devices[0] - else: - return - - dev_name = self._linuxscsi.get_name_from_path(host_device) - if dev_name: - self._linuxscsi.remove_scsi_device(dev_name) - - # NOTE(jdg): On busy systems we can have a race here - # where remove_iscsi_device is called before the device file - # has actually been removed. The result is an orphaned - # iscsi session that never gets logged out. The following - # call to wait addresses that issue. - self._linuxscsi.wait_for_volume_removal(host_device) - - # NOTE(vish): Only disconnect from the target if no luns from the - # target are in use. - device_byname = ("ip-%(portal)s-iscsi-%(iqn)s-lun-" % - {'portal': connection_properties['target_portal'], - 'iqn': connection_properties['target_iqn']}) - devices = self.driver.get_all_block_devices() - devices = [dev for dev in devices if (device_byname in dev - and - dev.startswith( - '/dev/disk/by-path/')) - and os.path.exists(dev)] - if not devices: - self._disconnect_from_iscsi_portal(connection_properties) - - def _munge_portal(self, target): - """Remove brackets from portal. - - In case IPv6 address was used the udev path should not contain any - brackets. Udev code specifically forbids that. - """ - portal, iqn, lun = target - return (portal.replace('[', '').replace(']', ''), iqn, - self._linuxscsi.process_lun_id(lun)) - - def _get_device_path(self, connection_properties): - if self._get_transport() == "default": - return ["/dev/disk/by-path/ip-%s-iscsi-%s-lun-%s" % - self._munge_portal(x) for x in - self._get_all_targets(connection_properties)] - else: - # we are looking for paths in the format : - # /dev/disk/by-path/ - # pci-XXXX:XX:XX.X-ip-PORTAL:PORT-iscsi-IQN-lun-LUN_ID - device_list = [] - for x in self._get_all_targets(connection_properties): - look_for_device = glob.glob( - '/dev/disk/by-path/*ip-%s-iscsi-%s-lun-%s' % - self._munge_portal(x)) - if look_for_device: - device_list.extend(look_for_device) - return device_list - - def get_initiator(self): - """Secure helper to read file as root.""" - file_path = '/etc/iscsi/initiatorname.iscsi' - try: - lines, _err = self._execute('cat', file_path, run_as_root=True, - root_helper=self._root_helper) - - for l in lines.split('\n'): - if l.startswith('InitiatorName='): - return l[l.index('=') + 1:].strip() - except putils.ProcessExecutionError: - LOG.warning(_LW("Could not find the iSCSI Initiator File %s"), - file_path) - return None - - def _run_iscsiadm(self, connection_properties, iscsi_command, **kwargs): - check_exit_code = kwargs.pop('check_exit_code', 0) - attempts = kwargs.pop('attempts', 1) - delay_on_retry = kwargs.pop('delay_on_retry', True) - (out, err) = self._execute('iscsiadm', '-m', 'node', '-T', - connection_properties['target_iqn'], - '-p', - connection_properties['target_portal'], - *iscsi_command, run_as_root=True, - root_helper=self._root_helper, - check_exit_code=check_exit_code, - attempts=attempts, - delay_on_retry=delay_on_retry) - msg = ("iscsiadm %(iscsi_command)s: stdout=%(out)s stderr=%(err)s" % - {'iscsi_command': iscsi_command, 'out': out, 'err': err}) - # don't let passwords be shown in log output - LOG.debug(strutils.mask_password(msg)) - - return (out, err) - - def _iscsiadm_update(self, connection_properties, property_key, - property_value, **kwargs): - iscsi_command = ('--op', 'update', '-n', property_key, - '-v', property_value) - return self._run_iscsiadm(connection_properties, iscsi_command, - **kwargs) - - def _get_target_portals_from_iscsiadm_output(self, output): - # return both portals and iqns - # - # as we are parsing a command line utility, allow for the - # possibility that additional debug data is spewed in the - # stream, and only grab actual ip / iqn lines. - targets = [] - for data in [line.split() for line in output.splitlines()]: - if len(data) == 2 and data[1].startswith('iqn.'): - targets.append(data) - return targets - - def _disconnect_volume_multipath_iscsi(self, connection_properties, - multipath_name): - """This removes a multipath device and it's LUNs.""" - LOG.debug("Disconnect multipath device %s", multipath_name) - mpath_map = self._get_multipath_device_map() - block_devices = self.driver.get_all_block_devices() - devices = [] - for dev in block_devices: - if os.path.exists(dev): - if "/mapper/" in dev: - devices.append(dev) - else: - dev_name = self._linuxscsi.get_name_from_path(dev) - mpdev = mpath_map.get(dev_name) - if mpdev and mpdev not in devices: - devices.append(mpdev) - - # Do a discovery to find all targets. - # Targets for multiple paths for the same multipath device - # may not be the same. - all_ips_iqns = self._discover_iscsi_portals(connection_properties) - - # As discovery result may contain other targets' iqns, extract targets - # to be disconnected whose block devices are already deleted here. - ips_iqns = [] - entries = [device.lstrip('ip-').split('-lun-')[0] - for device in self._get_iscsi_devices()] - for ip, iqn in all_ips_iqns: - ip_iqn = "%s-iscsi-%s" % (ip.split(",")[0], iqn) - if ip_iqn not in entries: - ips_iqns.append([ip, iqn]) - - if not devices: - # disconnect if no other multipath devices - self._disconnect_mpath(connection_properties, ips_iqns) - return - - # Get a target for all other multipath devices - other_iqns = self._get_multipath_iqns(devices, mpath_map) - - # Get all the targets for the current multipath device - current_iqns = [iqn for ip, iqn in ips_iqns] - - in_use = False - for current in current_iqns: - if current in other_iqns: - in_use = True - break - - # If no other multipath device attached has the same iqn - # as the current device - if not in_use: - # disconnect if no other multipath devices with same iqn - self._disconnect_mpath(connection_properties, ips_iqns) - return - - # else do not disconnect iscsi portals, - # as they are used for other luns - return - - def _connect_to_iscsi_portal(self, connection_properties): - # NOTE(vish): If we are on the same host as nova volume, the - # discovery makes the target so we don't need to - # run --op new. Therefore, we check to see if the - # target exists, and if we get 255 (Not Found), then - # we run --op new. This will also happen if another - # volume is using the same target. - LOG.info(_LI("Trying to connect to iSCSI portal %(portal)s"), - {"portal": connection_properties['target_portal']}) - try: - self._run_iscsiadm(connection_properties, ()) - except putils.ProcessExecutionError as exc: - # iscsiadm returns 21 for "No records found" after version 2.0-871 - if exc.exit_code in [21, 255]: - self._run_iscsiadm(connection_properties, - ('--interface', self._get_transport(), - '--op', 'new')) - else: - raise - - if connection_properties.get('auth_method'): - self._iscsiadm_update(connection_properties, - "node.session.auth.authmethod", - connection_properties['auth_method']) - self._iscsiadm_update(connection_properties, - "node.session.auth.username", - connection_properties['auth_username']) - self._iscsiadm_update(connection_properties, - "node.session.auth.password", - connection_properties['auth_password']) - - # Duplicate logins crash iscsiadm after load, - # so we scan active sessions to see if the node is logged in. - out = self._run_iscsiadm_bare(["-m", "session"], - run_as_root=True, - check_exit_code=[0, 1, 21])[0] or "" - - portals = [{'portal': p.split(" ")[2], 'iqn': p.split(" ")[3]} - for p in out.splitlines() if p.startswith("tcp:")] - - stripped_portal = connection_properties['target_portal'].split(",")[0] - if len(portals) == 0 or len([s for s in portals - if stripped_portal == - s['portal'].split(",")[0] - and - s['iqn'] == - connection_properties['target_iqn']] - ) == 0: - try: - self._run_iscsiadm(connection_properties, - ("--login",), - check_exit_code=[0, 255]) - except putils.ProcessExecutionError as err: - # exit_code=15 means the session already exists, so it should - # be regarded as successful login. - if err.exit_code not in [15]: - LOG.warning(_LW('Failed to login iSCSI target %(iqn)s ' - 'on portal %(portal)s (exit code ' - '%(err)s).'), - {'iqn': connection_properties['target_iqn'], - 'portal': connection_properties[ - 'target_portal'], - 'err': err.exit_code}) - return False - - self._iscsiadm_update(connection_properties, - "node.startup", - "automatic") - return True - - def _disconnect_from_iscsi_portal(self, connection_properties): - self._iscsiadm_update(connection_properties, "node.startup", "manual", - check_exit_code=[0, 21, 255]) - self._run_iscsiadm(connection_properties, ("--logout",), - check_exit_code=[0, 21, 255]) - self._run_iscsiadm(connection_properties, ('--op', 'delete'), - check_exit_code=[0, 21, 255], - attempts=5, - delay_on_retry=True) - - def _get_iscsi_devices(self): - try: - devices = list(os.walk('/dev/disk/by-path'))[0][-1] - except IndexError: - return [] - # For iSCSI HBAs, look at an offset of len('pci-0000:00:00.0') - return [entry for entry in devices if (entry.startswith("ip-") - or (entry.startswith("pci-") - and - entry.find("ip-", 16, 21) - >= 16))] - - def _disconnect_mpath(self, connection_properties, ips_iqns): - for ip, iqn in ips_iqns: - props = copy.deepcopy(connection_properties) - props['target_portal'] = ip - props['target_iqn'] = iqn - self._disconnect_from_iscsi_portal(props) - - self._rescan_multipath() - - def _get_multipath_iqns(self, multipath_devices, mpath_map): - entries = self._get_iscsi_devices() - iqns = [] - for entry in entries: - entry_real_path = os.path.realpath("/dev/disk/by-path/%s" % entry) - entry_multipath = mpath_map.get(entry_real_path) - if entry_multipath and entry_multipath in multipath_devices: - iqns.append(entry.split("iscsi-")[1].split("-lun")[0]) - return iqns - - def _get_multipath_device_map(self): - out = self._run_multipath(['-ll'], check_exit_code=[0, 1])[0] - mpath_line = [line for line in out.splitlines() - if not re.match(MULTIPATH_ERROR_REGEX, line)] - mpath_dev = None - mpath_map = {} - for line in out.splitlines(): - m = MULTIPATH_DEV_CHECK_REGEX.split(line) - if len(m) >= 2: - mpath_dev = '/dev/mapper/' + m[0].split(" ")[0] - continue - m = MULTIPATH_PATH_CHECK_REGEX.split(line) - if len(m) >= 2: - mpath_map['/dev/' + m[1].split(" ")[0]] = mpath_dev - - if mpath_line and not mpath_map: - LOG.warning(_LW("Failed to parse the output of multipath -ll. " - "stdout: %s"), out) - return mpath_map - - def _run_iscsi_session(self): - (out, err) = self._run_iscsiadm_bare(('-m', 'session'), - check_exit_code=[0, 1, 21, 255]) - LOG.debug("iscsi session list stdout=%(out)s stderr=%(err)s", - {'out': out, 'err': err}) - return (out, err) - - def _run_iscsiadm_bare(self, iscsi_command, **kwargs): - check_exit_code = kwargs.pop('check_exit_code', 0) - (out, err) = self._execute('iscsiadm', - *iscsi_command, - run_as_root=True, - root_helper=self._root_helper, - check_exit_code=check_exit_code) - LOG.debug("iscsiadm %(iscsi_command)s: stdout=%(out)s stderr=%(err)s", - {'iscsi_command': iscsi_command, 'out': out, 'err': err}) - return (out, err) - - def _run_multipath(self, multipath_command, **kwargs): - check_exit_code = kwargs.pop('check_exit_code', 0) - (out, err) = self._execute('multipath', - *multipath_command, - run_as_root=True, - root_helper=self._root_helper, - check_exit_code=check_exit_code) - LOG.debug("multipath %(multipath_command)s: " - "stdout=%(out)s stderr=%(err)s", - {'multipath_command': multipath_command, - 'out': out, 'err': err}) - return (out, err) - - def _rescan_iscsi(self): - self._run_iscsiadm_bare(('-m', 'node', '--rescan'), - check_exit_code=[0, 1, 21, 255]) - self._run_iscsiadm_bare(('-m', 'session', '--rescan'), - check_exit_code=[0, 1, 21, 255]) - - def _rescan_multipath(self): - self._run_multipath(['-r'], check_exit_code=[0, 1, 21]) - - -class FibreChannelConnector(BaseLinuxConnector): - """Connector class to attach/detach Fibre Channel volumes.""" - - def __init__(self, root_helper, driver=None, - execute=None, use_multipath=False, - device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, - *args, **kwargs): - self._linuxfc = linuxfc.LinuxFibreChannel(root_helper, execute) - super(FibreChannelConnector, self).__init__( - root_helper, driver=driver, - execute=execute, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - self.use_multipath = use_multipath - - def set_execute(self, execute): - super(FibreChannelConnector, self).set_execute(execute) - self._linuxscsi.set_execute(execute) - self._linuxfc.set_execute(execute) - - @staticmethod - def get_connector_properties(root_helper, *args, **kwargs): - """The Fibre Channel connector properties.""" - props = {} - fc = linuxfc.LinuxFibreChannel(root_helper, - execute=kwargs.get('execute')) - - wwpns = fc.get_fc_wwpns() - if wwpns: - props['wwpns'] = wwpns - wwnns = fc.get_fc_wwnns() - if wwnns: - props['wwnns'] = wwnns - - return props - - def get_search_path(self): - """Where do we look for FC based volumes.""" - return '/dev/disk/by-path' - - def _get_possible_volume_paths(self, connection_properties, hbas): - ports = connection_properties['target_wwn'] - possible_devs = self._get_possible_devices(hbas, ports) - - lun = connection_properties.get('target_lun', 0) - host_paths = self._get_host_devices(possible_devs, lun) - return host_paths - - @utils.trace - def get_volume_paths(self, connection_properties): - volume_paths = [] - # first fetch all of the potential paths that might exist - # how the FC fabric is zoned may alter the actual list - # that shows up on the system. So, we verify each path. - hbas = self._linuxfc.get_fc_hbas_info() - device_paths = self._get_possible_volume_paths( - connection_properties, hbas) - for path in device_paths: - if os.path.exists(path): - volume_paths.append(path) - - return volume_paths - - @utils.trace - @synchronized('extend_volume') - def extend_volume(self, connection_properties): - """Update the local kernel's size information. - - Try and update the local kernel's size information - for an FC volume. - """ - volume_paths = self.get_volume_paths(connection_properties) - if volume_paths: - return self._linuxscsi.extend_volume(volume_paths[0]) - else: - LOG.warning(_LW("Couldn't find any volume paths on the host to " - "extend volume for %(props)s"), - {'props': connection_properties}) - raise exception.VolumePathsNotFound() - - @utils.trace - @synchronized('connect_volume') - def connect_volume(self, connection_properties): - """Attach the volume to instance_name. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :returns: dict - - connection_properties for Fibre Channel must include: - target_wwn - World Wide Name - target_lun - LUN id of the volume - """ - LOG.debug("execute = %s", self._execute) - device_info = {'type': 'block'} - - hbas = self._linuxfc.get_fc_hbas_info() - host_devices = self._get_possible_volume_paths( - connection_properties, hbas) - - if len(host_devices) == 0: - # this is empty because we don't have any FC HBAs - LOG.warning( - _LW("We are unable to locate any Fibre Channel devices")) - raise exception.NoFibreChannelHostsFound() - - # The /dev/disk/by-path/... node is not always present immediately - # We only need to find the first device. Once we see the first device - # multipath will have any others. - def _wait_for_device_discovery(host_devices): - tries = self.tries - for device in host_devices: - LOG.debug("Looking for Fibre Channel dev %(device)s", - {'device': device}) - if os.path.exists(device): - self.host_device = device - # get the /dev/sdX device. This is used - # to find the multipath device. - self.device_name = os.path.realpath(device) - raise loopingcall.LoopingCallDone() - - if self.tries >= self.device_scan_attempts: - LOG.error(_LE("Fibre Channel volume device not found.")) - raise exception.NoFibreChannelVolumeDeviceFound() - - LOG.warning(_LW("Fibre Channel volume device not yet found. " - "Will rescan & retry. Try number: %(tries)s."), - {'tries': tries}) - - self._linuxfc.rescan_hosts(hbas) - self.tries = self.tries + 1 - - self.host_device = None - self.device_name = None - self.tries = 0 - timer = loopingcall.FixedIntervalLoopingCall( - _wait_for_device_discovery, host_devices) - timer.start(interval=2).wait() - - tries = self.tries - if self.host_device is not None and self.device_name is not None: - LOG.debug("Found Fibre Channel volume %(name)s " - "(after %(tries)s rescans)", - {'name': self.device_name, 'tries': tries}) - - # find out the WWN of the device - device_wwn = self._linuxscsi.get_scsi_wwn(self.host_device) - LOG.debug("Device WWN = '%(wwn)s'", {'wwn': device_wwn}) - device_info['scsi_wwn'] = device_wwn - - # see if the new drive is part of a multipath - # device. If so, we'll use the multipath device. - if self.use_multipath: - (device_path, multipath_id) = (super( - FibreChannelConnector, self)._discover_mpath_device( - device_wwn, connection_properties, self.device_name)) - if multipath_id: - # only set the multipath_id if we found one - device_info['multipath_id'] = multipath_id - - else: - device_path = self.host_device - - device_info['path'] = device_path - return device_info - - def _get_host_devices(self, possible_devs, lun): - host_devices = [] - for pci_num, target_wwn in possible_devs: - host_device = "/dev/disk/by-path/pci-%s-fc-%s-lun-%s" % ( - pci_num, - target_wwn, - self._linuxscsi.process_lun_id(lun)) - host_devices.append(host_device) - return host_devices - - def _get_possible_devices(self, hbas, wwnports): - """Compute the possible fibre channel device options. - - :param hbas: available hba devices. - :param wwnports: possible wwn addresses. Can either be string - or list of strings. - - :returns: list of (pci_id, wwn) tuples - - Given one or more wwn (mac addresses for fibre channel) ports - do the matrix math to figure out a set of pci device, wwn - tuples that are potentially valid (they won't all be). This - provides a search space for the device connection. - - """ - # the wwn (think mac addresses for fiber channel devices) can - # either be a single value or a list. Normalize it to a list - # for further operations. - wwns = [] - if isinstance(wwnports, list): - for wwn in wwnports: - wwns.append(str(wwn)) - elif isinstance(wwnports, six.string_types): - wwns.append(str(wwnports)) - - raw_devices = [] - for hba in hbas: - pci_num = self._get_pci_num(hba) - if pci_num is not None: - for wwn in wwns: - target_wwn = "0x%s" % wwn.lower() - raw_devices.append((pci_num, target_wwn)) - return raw_devices - - @utils.trace - @synchronized('connect_volume') - def disconnect_volume(self, connection_properties, device_info): - """Detach the volume from instance_name. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :param device_info: historical difference, but same as connection_props - :type device_info: dict - - connection_properties for Fibre Channel must include: - target_wwn - World Wide Name - target_lun - LUN id of the volume - """ - - devices = [] - volume_paths = self.get_volume_paths(connection_properties) - wwn = None - for path in volume_paths: - real_path = self._linuxscsi.get_name_from_path(path) - if not wwn: - wwn = self._linuxscsi.get_scsi_wwn(path) - device_info = self._linuxscsi.get_device_info(real_path) - devices.append(device_info) - - LOG.debug("devices to remove = %s", devices) - self._remove_devices(connection_properties, devices) - - if self.use_multipath: - # There is a bug in multipath where the flushing - # doesn't remove the entry if friendly names are on - # we'll try anyway. - self._linuxscsi.flush_multipath_device(wwn) - - def _remove_devices(self, connection_properties, devices): - # There may have been more than 1 device mounted - # by the kernel for this volume. We have to remove - # all of them - for device in devices: - self._linuxscsi.remove_scsi_device(device["device"]) - - def _get_pci_num(self, hba): - # NOTE(walter-boring) - # device path is in format of (FC and FCoE) : - # /sys/devices/pci0000:00/0000:00:03.0/0000:05:00.3/host2/fc_host/host2 - # /sys/devices/pci0000:20/0000:20:03.0/0000:21:00.2/net/ens2f2/ctlr_2 - # /host3/fc_host/host3 - # we always want the value prior to the host or net value - if hba is not None: - if "device_path" in hba: - device_path = hba['device_path'].split('/') - for index, value in enumerate(device_path): - if value.startswith('net') or value.startswith('host'): - return device_path[index - 1] - return None - - -class FibreChannelConnectorS390X(FibreChannelConnector): - """Connector class to attach/detach Fibre Channel volumes on S390X arch.""" - platform = PLATFORM_S390 - - def __init__(self, root_helper, driver=None, - execute=None, use_multipath=False, - device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, - *args, **kwargs): - super(FibreChannelConnectorS390X, self).__init__( - root_helper, - driver=driver, - execute=execute, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - LOG.debug("Initializing Fibre Channel connector for S390") - self._linuxfc = linuxfc.LinuxFibreChannelS390X(root_helper, execute) - self.use_multipath = use_multipath - - def set_execute(self, execute): - super(FibreChannelConnectorS390X, self).set_execute(execute) - self._linuxscsi.set_execute(execute) - self._linuxfc.set_execute(execute) - - def _get_host_devices(self, possible_devs, lun): - host_devices = [] - for pci_num, target_wwn in possible_devs: - target_lun = self._get_lun_string(lun) - host_device = self._get_device_file_path( - pci_num, - target_wwn, - target_lun) - self._linuxfc.configure_scsi_device(pci_num, target_wwn, - target_lun) - host_devices.append(host_device) - return host_devices - - def _get_lun_string(self, lun): - target_lun = 0 - if lun <= 0xffff: - target_lun = "0x%04x000000000000" % lun - elif lun <= 0xffffffff: - target_lun = "0x%08x00000000" % lun - return target_lun - - def _get_device_file_path(self, pci_num, target_wwn, target_lun): - host_device = "/dev/disk/by-path/ccw-%s-zfcp-%s:%s" % ( - pci_num, - target_wwn, - target_lun) - return host_device - - def _remove_devices(self, connection_properties, devices): - hbas = self._linuxfc.get_fc_hbas_info() - ports = connection_properties['target_wwn'] - possible_devs = self._get_possible_devices(hbas, ports) - lun = connection_properties.get('target_lun', 0) - target_lun = self._get_lun_string(lun) - for pci_num, target_wwn in possible_devs: - self._linuxfc.deconfigure_scsi_device(pci_num, - target_wwn, - target_lun) - - -class AoEConnector(BaseLinuxConnector): - """Connector class to attach/detach AoE volumes.""" - - def __init__(self, root_helper, driver=None, - device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, - *args, **kwargs): - super(AoEConnector, self).__init__( - root_helper, - driver=driver, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - - @staticmethod - def get_connector_properties(root_helper, *args, **kwargs): - """The AoE connector properties.""" - return {} - - def get_search_path(self): - return '/dev/etherd' - - @utils.trace - def get_volume_paths(self, connection_properties): - aoe_device, aoe_path = self._get_aoe_info(connection_properties) - volume_paths = [] - if os.path.exists(aoe_path): - volume_paths.append(aoe_path) - - return volume_paths - - def _get_aoe_info(self, connection_properties): - shelf = connection_properties['target_shelf'] - lun = connection_properties['target_lun'] - aoe_device = 'e%(shelf)s.%(lun)s' % {'shelf': shelf, - 'lun': lun} - path = self.get_search_path() - aoe_path = '%(path)s/%(device)s' % {'path': path, - 'device': aoe_device} - return aoe_device, aoe_path - - @utils.trace - @lockutils.synchronized('aoe_control', 'aoe-') - def connect_volume(self, connection_properties): - """Discover and attach the volume. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :returns: dict - - connection_properties for AoE must include: - target_shelf - shelf id of volume - target_lun - lun id of volume - """ - aoe_device, aoe_path = self._get_aoe_info(connection_properties) - - device_info = { - 'type': 'block', - 'device': aoe_device, - 'path': aoe_path, - } - - if os.path.exists(aoe_path): - self._aoe_revalidate(aoe_device) - else: - self._aoe_discover() - - waiting_status = {'tries': 0} - - # NOTE(jbr_): Device path is not always present immediately - def _wait_for_discovery(aoe_path): - if os.path.exists(aoe_path): - raise loopingcall.LoopingCallDone - - if waiting_status['tries'] >= self.device_scan_attempts: - raise exception.VolumeDeviceNotFound(device=aoe_path) - - LOG.warning(_LW("AoE volume not yet found at: %(path)s. " - "Try number: %(tries)s"), - {'path': aoe_device, - 'tries': waiting_status['tries']}) - - self._aoe_discover() - waiting_status['tries'] += 1 - - timer = loopingcall.FixedIntervalLoopingCall(_wait_for_discovery, - aoe_path) - timer.start(interval=2).wait() - - if waiting_status['tries']: - LOG.debug("Found AoE device %(path)s " - "(after %(tries)s rediscover)", - {'path': aoe_path, - 'tries': waiting_status['tries']}) - - return device_info - - @utils.trace - @lockutils.synchronized('aoe_control', 'aoe-') - def disconnect_volume(self, connection_properties, device_info): - """Detach and flush the volume. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :param device_info: historical difference, but same as connection_props - :type device_info: dict - - connection_properties for AoE must include: - target_shelf - shelf id of volume - target_lun - lun id of volume - """ - aoe_device, aoe_path = self._get_aoe_info(connection_properties) - - if os.path.exists(aoe_path): - self._aoe_flush(aoe_device) - - def _aoe_discover(self): - (out, err) = self._execute('aoe-discover', - run_as_root=True, - root_helper=self._root_helper, - check_exit_code=0) - - LOG.debug('aoe-discover: stdout=%(out)s stderr%(err)s', - {'out': out, 'err': err}) - - def _aoe_revalidate(self, aoe_device): - (out, err) = self._execute('aoe-revalidate', - aoe_device, - run_as_root=True, - root_helper=self._root_helper, - check_exit_code=0) - - LOG.debug('aoe-revalidate %(dev)s: stdout=%(out)s stderr%(err)s', - {'dev': aoe_device, 'out': out, 'err': err}) - - def _aoe_flush(self, aoe_device): - (out, err) = self._execute('aoe-flush', - aoe_device, - run_as_root=True, - root_helper=self._root_helper, - check_exit_code=0) - LOG.debug('aoe-flush %(dev)s: stdout=%(out)s stderr%(err)s', - {'dev': aoe_device, 'out': out, 'err': err}) - - def extend_volume(self, connection_properties): - # TODO(walter-boring): is this possible? - raise NotImplementedError - - -class RemoteFsConnector(BaseLinuxConnector): - """Connector class to attach/detach NFS and GlusterFS volumes.""" - - def __init__(self, mount_type, root_helper, driver=None, - execute=None, - device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, - *args, **kwargs): - kwargs = kwargs or {} - conn = kwargs.get('conn') - mount_type_lower = mount_type.lower() - if conn: - mount_point_base = conn.get('mount_point_base') - if mount_type_lower in ('nfs', 'glusterfs', 'scality', - 'quobyte', 'vzstorage'): - kwargs[mount_type_lower + '_mount_point_base'] = ( - kwargs.get(mount_type_lower + '_mount_point_base') or - mount_point_base) - else: - LOG.warning(_LW("Connection details not present." - " RemoteFsClient may not initialize properly.")) - - if mount_type_lower == 'scality': - cls = remotefs.ScalityRemoteFsClient - else: - cls = remotefs.RemoteFsClient - self._remotefsclient = cls(mount_type, root_helper, execute=execute, - *args, **kwargs) - - super(RemoteFsConnector, self).__init__( - root_helper, driver=driver, - execute=execute, - device_scan_attempts=device_scan_attempts, - *args, **kwargs) - - @staticmethod - def get_connector_properties(root_helper, *args, **kwargs): - """The RemoteFS connector properties.""" - return {} - - def set_execute(self, execute): - super(RemoteFsConnector, self).set_execute(execute) - self._remotefsclient.set_execute(execute) - - def get_search_path(self): - return self._remotefsclient.get_mount_base() - - def _get_volume_path(self, connection_properties): - mnt_flags = [] - if connection_properties.get('options'): - mnt_flags = connection_properties['options'].split() - - nfs_share = connection_properties['export'] - self._remotefsclient.mount(nfs_share, mnt_flags) - mount_point = self._remotefsclient.get_mount_point(nfs_share) - path = mount_point + '/' + connection_properties['name'] - return path - - @utils.trace - def get_volume_paths(self, connection_properties): - path = self._get_volume_path(connection_properties) - return [path] - - @utils.trace - def connect_volume(self, connection_properties): - """Ensure that the filesystem containing the volume is mounted. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - connection_properties must include: - export - remote filesystem device (e.g. '172.18.194.100:/var/nfs') - name - file name within the filesystem - :type connection_properties: dict - :returns: dict - - - connection_properties may optionally include: - options - options to pass to mount - """ - path = self._get_volume_path(connection_properties) - return {'path': path} - - @utils.trace - def disconnect_volume(self, connection_properties, device_info): - """No need to do anything to disconnect a volume in a filesystem. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :param device_info: historical difference, but same as connection_props - :type device_info: dict - """ - - def extend_volume(self, connection_properties): - # TODO(walter-boring): is this possible? - raise NotImplementedError - - -class RBDConnector(BaseLinuxConnector): - """"Connector class to attach/detach RBD volumes.""" - - def __init__(self, root_helper, driver=None, use_multipath=False, - device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, - *args, **kwargs): - - super(RBDConnector, self).__init__(root_helper, driver=driver, - device_scan_attempts= - device_scan_attempts, - *args, **kwargs) - self.do_local_attach = kwargs.get('do_local_attach', False) - - @staticmethod - def get_connector_properties(root_helper, *args, **kwargs): - """The RBD connector properties.""" - return {'do_local_attach': kwargs.get('do_local_attach', False)} - - def get_volume_paths(self, connection_properties): - # TODO(e0ne): Implement this for local volume. - return [] - - def get_search_path(self): - # TODO(walter-boring): don't know where the connector - # looks for RBD volumes. - return None - - def get_all_available_volumes(self, connection_properties=None): - # TODO(e0ne): Implement this for local volumes. - return [] - - def _get_rbd_handle(self, connection_properties): - try: - user = connection_properties['auth_username'] - pool, volume = connection_properties['name'].split('/') - conf = connection_properties.get('conffile') - except IndexError: - msg = _("Connect volume failed, malformed connection properties") - raise exception.BrickException(msg=msg) - - rbd_client = linuxrbd.RBDClient(user, pool) - rbd_volume = linuxrbd.RBDVolume(rbd_client, volume) - rbd_handle = linuxrbd.RBDVolumeIOWrapper( - linuxrbd.RBDImageMetadata(rbd_volume, pool, user, conf)) - return rbd_handle - - @staticmethod - def get_rbd_device_name(pool, volume): - """Return device name which will be generated by RBD kernel module. - - :param pool: RBD pool name. - :type pool: string - :param volume: RBD image name. - :type volume: string - """ - return '/dev/rbd/{pool}/{volume}'.format(pool=pool, volume=volume) - - @utils.trace - def connect_volume(self, connection_properties): - """Connect to a volume. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :returns: dict - """ - - do_local_attach = connection_properties.get('do_local_attach', - self.do_local_attach) - - if do_local_attach: - # NOTE(e0ne): sanity check if ceph-common is installed. - cmd = ['which', 'rbd'] - try: - self._execute(*cmd) - except putils.ProcessExecutionError: - msg = _("ceph-common package is not installed.") - LOG.error(msg) - raise exception.BrickException(message=msg) - - # NOTE(e0ne): map volume to a block device - # via the rbd kernel module. - pool, volume = connection_properties['name'].split('/') - cmd = ['rbd', 'map', volume, '--pool', pool] - self._execute(*cmd, root_helper=self._root_helper, - run_as_root=True) - - return {'path': RBDConnector.get_rbd_device_name(pool, volume), - 'type': 'block'} - - rbd_handle = self._get_rbd_handle(connection_properties) - return {'path': rbd_handle} - - @utils.trace - def disconnect_volume(self, connection_properties, device_info): - """Disconnect a volume. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :param device_info: historical difference, but same as connection_props - :type device_info: dict - """ - do_local_attach = connection_properties.get('do_local_attach', - self.do_local_attach) - if do_local_attach: - pool, volume = connection_properties['name'].split('/') - dev_name = RBDConnector.get_rbd_device_name(pool, volume) - cmd = ['rbd', 'unmap', dev_name] - self._execute(*cmd, root_helper=self._root_helper, - run_as_root=True) - else: - if device_info: - rbd_handle = device_info.get('path', None) - if rbd_handle is not None: - rbd_handle.close() - - def check_valid_device(self, path, run_as_root=True): - """Verify an existing RBD handle is connected and valid.""" - rbd_handle = path - - if rbd_handle is None: - return False - - original_offset = rbd_handle.tell() - - try: - rbd_handle.read(4096) - except Exception as e: - LOG.error(_LE("Failed to access RBD device handle: %(error)s"), - {"error": e}) - return False - finally: - rbd_handle.seek(original_offset, 0) - - return True - - def extend_volume(self, connection_properties): - # TODO(walter-boring): is this possible? - raise NotImplementedError - - -class LocalConnector(BaseLinuxConnector): - """"Connector class to attach/detach File System backed volumes.""" - - def __init__(self, root_helper, driver=None, - *args, **kwargs): - super(LocalConnector, self).__init__(root_helper, driver=driver, - *args, **kwargs) - - @staticmethod - def get_connector_properties(root_helper, *args, **kwargs): - """The Local connector properties.""" - return {} - - def get_volume_paths(self, connection_properties): - path = connection_properties['device_path'] - return [path] - - def get_search_path(self): - return None - - def get_all_available_volumes(self, connection_properties=None): - # TODO(walter-boring): not sure what to return here. - return [] - - @utils.trace - def connect_volume(self, connection_properties): - """Connect to a volume. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - connection_properties must include: - device_path - path to the volume to be connected - :type connection_properties: dict - :returns: dict - """ - if 'device_path' not in connection_properties: - msg = (_("Invalid connection_properties specified " - "no device_path attribute")) - raise ValueError(msg) - - device_info = {'type': 'local', - 'path': connection_properties['device_path']} - return device_info - - @utils.trace - def disconnect_volume(self, connection_properties, device_info): - """Disconnect a volume from the local host. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :param device_info: historical difference, but same as connection_props - :type device_info: dict - """ - pass - - def extend_volume(self, connection_properties): - # TODO(walter-boring): is this possible? - raise NotImplementedError - - -class DRBDConnector(BaseLinuxConnector): - """"Connector class to attach/detach DRBD resources.""" - - def __init__(self, root_helper, driver=None, - execute=putils.execute, *args, **kwargs): - - super(DRBDConnector, self).__init__(root_helper, driver=driver, - execute=execute, *args, **kwargs) - - self._execute = execute - self._root_helper = root_helper - - @staticmethod - def get_connector_properties(root_helper, *args, **kwargs): - """The DRBD connector properties.""" - return {} - - def check_valid_device(self, path, run_as_root=True): - """Verify an existing volume.""" - # TODO(linbit): check via drbdsetup first, to avoid blocking/hanging - # in case of network problems? - - return super(DRBDConnector, self).check_valid_device(path, run_as_root) - - @utils.trace - def get_all_available_volumes(self, connection_properties=None): - - base = "/dev/" - blkdev_list = [] - - for e in os.listdir(base): - path = base + e - if os.path.isblk(path): - blkdev_list.append(path) - - return blkdev_list - - def _drbdadm_command(self, cmd, data_dict, sh_secret): - # TODO(linbit): Write that resource file to a permanent location? - tmp = tempfile.NamedTemporaryFile(suffix="res", delete=False, mode="w") - try: - kv = {'shared-secret': sh_secret} - tmp.write(data_dict['config'] % kv) - tmp.close() - - (out, err) = self._execute('drbdadm', cmd, - "-c", tmp.name, - data_dict['name'], - run_as_root=True, - root_helper=self._root_helper) - finally: - os.unlink(tmp.name) - - return (out, err) - - @utils.trace - def connect_volume(self, connection_properties): - """Attach the volume.""" - - self._drbdadm_command("adjust", connection_properties, - connection_properties['provider_auth']) - - device_info = { - 'type': 'block', - 'path': connection_properties['device'], - } - - return device_info - - @utils.trace - def disconnect_volume(self, connection_properties, device_info): - """Detach the volume.""" - - self._drbdadm_command("down", connection_properties, - connection_properties['provider_auth']) - - @utils.trace - def get_volume_paths(self, connection_properties): - path = connection_properties['device'] - return [path] - - def get_search_path(self): - # TODO(linbit): is it allowed to return "/dev", or is that too broad? - return None - - def extend_volume(self, connection_properties): - # TODO(walter-boring): is this possible? - raise NotImplementedError - - -class HuaweiStorHyperConnector(BaseLinuxConnector): - """"Connector class to attach/detach SDSHypervisor volumes.""" - - attached_success_code = 0 - has_been_attached_code = 50151401 - attach_mnid_done_code = 50151405 - vbs_unnormal_code = 50151209 - not_mount_node_code = 50155007 - iscliexist = True - - def __init__(self, root_helper, driver=None, - *args, **kwargs): - self.cli_path = os.getenv('HUAWEISDSHYPERVISORCLI_PATH') - if not self.cli_path: - self.cli_path = '/usr/local/bin/sds/sds_cli' - LOG.debug("CLI path is not configured, using default %s.", - self.cli_path) - if not os.path.isfile(self.cli_path): - self.iscliexist = False - LOG.error(_LE('SDS CLI file not found, ' - 'HuaweiStorHyperConnector init failed.')) - super(HuaweiStorHyperConnector, self).__init__(root_helper, - driver=driver, - *args, **kwargs) - - @staticmethod - def get_connector_properties(root_helper, *args, **kwargs): - """The HuaweiStor connector properties.""" - return {} - - def get_search_path(self): - # TODO(walter-boring): Where is the location on the filesystem to - # look for Huawei volumes to show up? - return None - - @utils.trace - def get_all_available_volumes(self, connection_properties=None): - # TODO(walter-boring): what to return here for all Huawei volumes ? - return [] - - @utils.trace - def get_volume_paths(self, connection_properties): - volume_path = None - try: - volume_path = self._get_volume_path(connection_properties) - except Exception: - msg = _("Couldn't find a volume.") - LOG.warning(msg) - raise exception.BrickException(message=msg) - return [volume_path] - - def _get_volume_path(self, connection_properties): - out = self._query_attached_volume( - connection_properties['volume_id']) - if not out or int(out['ret_code']) != 0: - msg = _("Couldn't find attached volume.") - LOG.error(msg) - raise exception.BrickException(message=msg) - return out['dev_addr'] - - @utils.trace - @synchronized('connect_volume') - def connect_volume(self, connection_properties): - """Connect to a volume. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :returns: dict - """ - LOG.debug("Connect_volume connection properties: %s.", - connection_properties) - out = self._attach_volume(connection_properties['volume_id']) - if not out or int(out['ret_code']) not in (self.attached_success_code, - self.has_been_attached_code, - self.attach_mnid_done_code): - msg = (_("Attach volume failed, " - "error code is %s") % out['ret_code']) - raise exception.BrickException(message=msg) - - try: - volume_path = self._get_volume_path(connection_properties) - except Exception: - msg = _("query attached volume failed or volume not attached.") - LOG.error(msg) - raise exception.BrickException(message=msg) - - device_info = {'type': 'block', - 'path': volume_path} - return device_info - - @utils.trace - @synchronized('connect_volume') - def disconnect_volume(self, connection_properties, device_info): - """Disconnect a volume from the local host. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :param device_info: historical difference, but same as connection_props - :type device_info: dict - """ - out = self._detach_volume(connection_properties['volume_id']) - if not out or int(out['ret_code']) not in (self.attached_success_code, - self.vbs_unnormal_code, - self.not_mount_node_code): - msg = (_("Disconnect_volume failed, " - "error code is %s") % out['ret_code']) - raise exception.BrickException(message=msg) - - def is_volume_connected(self, volume_name): - """Check if volume already connected to host""" - LOG.debug('Check if volume %s already connected to a host.', - volume_name) - out = self._query_attached_volume(volume_name) - if out: - return int(out['ret_code']) == 0 - return False - - def _attach_volume(self, volume_name): - return self._cli_cmd('attach', volume_name) - - def _detach_volume(self, volume_name): - return self._cli_cmd('detach', volume_name) - - def _query_attached_volume(self, volume_name): - return self._cli_cmd('querydev', volume_name) - - def _cli_cmd(self, method, volume_name): - LOG.debug("Enter into _cli_cmd.") - if not self.iscliexist: - msg = _("SDS command line doesn't exist, " - "can't execute SDS command.") - raise exception.BrickException(message=msg) - if not method or volume_name is None: - return - cmd = [self.cli_path, '-c', method, '-v', volume_name] - out, clilog = self._execute(*cmd, run_as_root=False, - root_helper=self._root_helper) - analyse_result = self._analyze_output(out) - LOG.debug('%(method)s volume returns %(analyse_result)s.', - {'method': method, 'analyse_result': analyse_result}) - if clilog: - LOG.error(_LE("SDS CLI output some log: %s."), clilog) - return analyse_result - - def _analyze_output(self, out): - LOG.debug("Enter into _analyze_output.") - if out: - analyse_result = {} - out_temp = out.split('\n') - for line in out_temp: - LOG.debug("Line is %s.", line) - if line.find('=') != -1: - key, val = line.split('=', 1) - LOG.debug("%(key)s = %(val)s", {'key': key, 'val': val}) - if key in ['ret_code', 'ret_desc', 'dev_addr']: - analyse_result[key] = val - return analyse_result - else: - return None - - def extend_volume(self, connection_properties): - # TODO(walter-boring): is this possible? - raise NotImplementedError - - -class HGSTConnector(BaseLinuxConnector): - """Connector class to attach/detach HGST volumes.""" - - VGCCLUSTER = 'vgc-cluster' - - def __init__(self, root_helper, driver=None, - device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, - *args, **kwargs): - super(HGSTConnector, self).__init__(root_helper, driver=driver, - device_scan_attempts= - device_scan_attempts, - *args, **kwargs) - self._vgc_host = None - - @staticmethod - def get_connector_properties(root_helper, *args, **kwargs): - """The HGST connector properties.""" - return {} - - def _log_cli_err(self, err): - """Dumps the full command output to a logfile in error cases.""" - LOG.error(_LE("CLI fail: '%(cmd)s' = %(code)s\nout: %(stdout)s\n" - "err: %(stderr)s"), - {'cmd': err.cmd, 'code': err.exit_code, - 'stdout': err.stdout, 'stderr': err.stderr}) - - def _find_vgc_host(self): - """Finds vgc-cluster hostname for this box.""" - params = [self.VGCCLUSTER, "domain-list", "-1"] - try: - out, unused = self._execute(*params, run_as_root=True, - root_helper=self._root_helper) - except putils.ProcessExecutionError as err: - self._log_cli_err(err) - msg = _("Unable to get list of domain members, check that " - "the cluster is running.") - raise exception.BrickException(message=msg) - domain = out.splitlines() - params = ["ip", "addr", "list"] - try: - out, unused = self._execute(*params, run_as_root=False) - except putils.ProcessExecutionError as err: - self._log_cli_err(err) - msg = _("Unable to get list of IP addresses on this host, " - "check permissions and networking.") - raise exception.BrickException(message=msg) - nets = out.splitlines() - for host in domain: - try: - ip = socket.gethostbyname(host) - for l in nets: - x = l.strip() - if x.startswith("inet %s/" % ip): - return host - except socket.error: - pass - msg = _("Current host isn't part of HGST domain.") - raise exception.BrickException(message=msg) - - def _hostname(self): - """Returns hostname to use for cluster operations on this box.""" - if self._vgc_host is None: - self._vgc_host = self._find_vgc_host() - return self._vgc_host - - def get_search_path(self): - return "/dev" - - @utils.trace - def get_volume_paths(self, connection_properties): - path = ("%(path)s/%(name)s" % - {'path': self.get_search_path(), - 'name': connection_properties['name']}) - volume_path = None - if os.path.exists(path): - volume_path = path - return [volume_path] - - @utils.trace - def connect_volume(self, connection_properties): - """Attach a Space volume to running host. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - connection_properties for HGST must include: - name - Name of space to attach - :type connection_properties: dict - :returns: dict - """ - if connection_properties is None: - msg = _("Connection properties passed in as None.") - raise exception.BrickException(message=msg) - if 'name' not in connection_properties: - msg = _("Connection properties missing 'name' field.") - raise exception.BrickException(message=msg) - device_info = { - 'type': 'block', - 'device': connection_properties['name'], - 'path': '/dev/' + connection_properties['name'] - } - volname = device_info['device'] - params = [self.VGCCLUSTER, 'space-set-apphosts'] - params += ['-n', volname] - params += ['-A', self._hostname()] - params += ['--action', 'ADD'] - try: - self._execute(*params, run_as_root=True, - root_helper=self._root_helper) - except putils.ProcessExecutionError as err: - self._log_cli_err(err) - msg = (_("Unable to set apphost for space %s") % volname) - raise exception.BrickException(message=msg) - - return device_info - - @utils.trace - def disconnect_volume(self, connection_properties, device_info): - """Detach and flush the volume. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - For HGST must include: - name - Name of space to detach - noremovehost - Host which should never be removed - :type connection_properties: dict - :param device_info: historical difference, but same as connection_props - :type device_info: dict - """ - if connection_properties is None: - msg = _("Connection properties passed in as None.") - raise exception.BrickException(message=msg) - if 'name' not in connection_properties: - msg = _("Connection properties missing 'name' field.") - raise exception.BrickException(message=msg) - if 'noremovehost' not in connection_properties: - msg = _("Connection properties missing 'noremovehost' field.") - raise exception.BrickException(message=msg) - if connection_properties['noremovehost'] != self._hostname(): - params = [self.VGCCLUSTER, 'space-set-apphosts'] - params += ['-n', connection_properties['name']] - params += ['-A', self._hostname()] - params += ['--action', 'DELETE'] - try: - self._execute(*params, run_as_root=True, - root_helper=self._root_helper) - except putils.ProcessExecutionError as err: - self._log_cli_err(err) - msg = (_("Unable to set apphost for space %s") % - connection_properties['name']) - raise exception.BrickException(message=msg) - - def extend_volume(self, connection_properties): - # TODO(walter-boring): is this possible? - raise NotImplementedError - - -class ScaleIOConnector(BaseLinuxConnector): - """Class implements the connector driver for ScaleIO.""" - - OK_STATUS_CODE = 200 - VOLUME_NOT_MAPPED_ERROR = 84 - VOLUME_ALREADY_MAPPED_ERROR = 81 - GET_GUID_CMD = ['/opt/emc/scaleio/sdc/bin/drv_cfg', '--query_guid'] - - def __init__(self, root_helper, driver=None, - device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, - *args, **kwargs): - super(ScaleIOConnector, self).__init__( - root_helper, - driver=driver, - device_scan_attempts=device_scan_attempts, - *args, **kwargs - ) - - self.local_sdc_ip = None - self.server_ip = None - self.server_port = None - self.server_username = None - self.server_password = None - self.server_token = None - self.volume_id = None - self.volume_name = None - self.volume_path = None - self.iops_limit = None - self.bandwidth_limit = None - - @staticmethod - def get_connector_properties(root_helper, *args, **kwargs): - """The ScaleIO connector properties.""" - return {} - - def get_search_path(self): - return "/dev/disk/by-id" - - @utils.trace - def get_volume_paths(self, connection_properties): - self.get_config(connection_properties) - volume_paths = [] - device_paths = [self._find_volume_path()] - for path in device_paths: - if os.path.exists(path): - volume_paths.append(path) - return volume_paths - - def _find_volume_path(self): - LOG.info(_LI( - "Looking for volume %(volume_id)s, maximum tries: %(tries)s"), - {'volume_id': self.volume_id, 'tries': self.device_scan_attempts} - ) - - # look for the volume in /dev/disk/by-id directory - by_id_path = self.get_search_path() - - disk_filename = self._wait_for_volume_path(by_id_path) - full_disk_name = ("%(path)s/%(filename)s" % - {'path': by_id_path, 'filename': disk_filename}) - LOG.info(_LI("Full disk name is %(full_path)s"), - {'full_path': full_disk_name}) - return full_disk_name - - # NOTE: Usually 3 retries is enough to find the volume. - # If there are network issues, it could take much longer. Set - # the max retries to 15 to make sure we can find the volume. - @utils.retry(exceptions=exception.BrickException, - retries=15, - backoff_rate=1) - def _wait_for_volume_path(self, path): - if not os.path.isdir(path): - msg = ( - _("ScaleIO volume %(volume_id)s not found at " - "expected path.") % {'volume_id': self.volume_id} - ) - - LOG.debug(msg) - raise exception.BrickException(message=msg) - - disk_filename = None - filenames = os.listdir(path) - LOG.info(_LI( - "Files found in %(path)s path: %(files)s "), - {'path': path, 'files': filenames} - ) - - for filename in filenames: - if (filename.startswith("emc-vol") and - filename.endswith(self.volume_id)): - disk_filename = filename - break - - if not disk_filename: - msg = (_("ScaleIO volume %(volume_id)s not found.") % - {'volume_id': self.volume_id}) - LOG.debug(msg) - raise exception.BrickException(message=msg) - - return disk_filename - - def _get_client_id(self): - request = ( - "https://%(server_ip)s:%(server_port)s/" - "api/types/Client/instances/getByIp::%(sdc_ip)s/" % - { - 'server_ip': self.server_ip, - 'server_port': self.server_port, - 'sdc_ip': self.local_sdc_ip - } - ) - - LOG.info(_LI("ScaleIO get client id by ip request: %(request)s"), - {'request': request}) - - r = requests.get( - request, - auth=(self.server_username, self.server_token), - verify=False - ) - - r = self._check_response(r, request) - sdc_id = r.json() - if not sdc_id: - msg = (_("Client with ip %(sdc_ip)s was not found.") % - {'sdc_ip': self.local_sdc_ip}) - raise exception.BrickException(message=msg) - - if r.status_code != 200 and "errorCode" in sdc_id: - msg = (_("Error getting sdc id from ip %(sdc_ip)s: %(err)s") % - {'sdc_ip': self.local_sdc_ip, 'err': sdc_id['message']}) - - LOG.error(msg) - raise exception.BrickException(message=msg) - - LOG.info(_LI("ScaleIO sdc id is %(sdc_id)s."), - {'sdc_id': sdc_id}) - return sdc_id - - def _get_volume_id(self): - volname_encoded = urllib.parse.quote(self.volume_name, '') - volname_double_encoded = urllib.parse.quote(volname_encoded, '') - LOG.debug(_( - "Volume name after double encoding is %(volume_name)s."), - {'volume_name': volname_double_encoded} - ) - - request = ( - "https://%(server_ip)s:%(server_port)s/api/types/Volume/instances" - "/getByName::%(encoded_volume_name)s" % - { - 'server_ip': self.server_ip, - 'server_port': self.server_port, - 'encoded_volume_name': volname_double_encoded - } - ) - - LOG.info( - _LI("ScaleIO get volume id by name request: %(request)s"), - {'request': request} - ) - - r = requests.get(request, - auth=(self.server_username, self.server_token), - verify=False) - - r = self._check_response(r, request) - - volume_id = r.json() - if not volume_id: - msg = (_("Volume with name %(volume_name)s wasn't found.") % - {'volume_name': self.volume_name}) - - LOG.error(msg) - raise exception.BrickException(message=msg) - - if r.status_code != self.OK_STATUS_CODE and "errorCode" in volume_id: - msg = ( - _("Error getting volume id from name %(volume_name)s: " - "%(err)s") % - {'volume_name': self.volume_name, 'err': volume_id['message']} - ) - - LOG.error(msg) - raise exception.BrickException(message=msg) - - LOG.info(_LI("ScaleIO volume id is %(volume_id)s."), - {'volume_id': volume_id}) - return volume_id - - def _check_response(self, response, request, is_get_request=True, - params=None): - if response.status_code == 401 or response.status_code == 403: - LOG.info(_LI("Token is invalid, " - "going to re-login to get a new one")) - - login_request = ( - "https://%(server_ip)s:%(server_port)s/api/login" % - {'server_ip': self.server_ip, 'server_port': self.server_port} - ) - - r = requests.get( - login_request, - auth=(self.server_username, self.server_password), - verify=False - ) - - token = r.json() - # repeat request with valid token - LOG.debug(_("Going to perform request %(request)s again " - "with valid token"), {'request': request}) - - if is_get_request: - res = requests.get(request, - auth=(self.server_username, token), - verify=False) - else: - headers = {'content-type': 'application/json'} - res = requests.post( - request, - data=json.dumps(params), - headers=headers, - auth=(self.server_username, token), - verify=False - ) - - self.server_token = token - return res - - return response - - def get_config(self, connection_properties): - self.local_sdc_ip = connection_properties['hostIP'] - self.volume_name = connection_properties['scaleIO_volname'] - self.volume_id = connection_properties['scaleIO_volume_id'] - self.server_ip = connection_properties['serverIP'] - self.server_port = connection_properties['serverPort'] - self.server_username = connection_properties['serverUsername'] - self.server_password = connection_properties['serverPassword'] - self.server_token = connection_properties['serverToken'] - self.iops_limit = connection_properties['iopsLimit'] - self.bandwidth_limit = connection_properties['bandwidthLimit'] - device_info = {'type': 'block', - 'path': self.volume_path} - return device_info - - @utils.trace - @lockutils.synchronized('scaleio', 'scaleio-') - def connect_volume(self, connection_properties): - """Connect the volume. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :returns: dict - """ - device_info = self.get_config(connection_properties) - LOG.debug( - _( - "scaleIO Volume name: %(volume_name)s, SDC IP: %(sdc_ip)s, " - "REST Server IP: %(server_ip)s, " - "REST Server username: %(username)s, " - "iops limit:%(iops_limit)s, " - "bandwidth limit: %(bandwidth_limit)s." - ), { - 'volume_name': self.volume_name, - 'volume_id': self.volume_id, - 'sdc_ip': self.local_sdc_ip, - 'server_ip': self.server_ip, - 'username': self.server_username, - 'iops_limit': self.iops_limit, - 'bandwidth_limit': self.bandwidth_limit - } - ) - - LOG.info(_LI("ScaleIO sdc query guid command: %(cmd)s"), - {'cmd': self.GET_GUID_CMD}) - - try: - (out, err) = self._execute(*self.GET_GUID_CMD, run_as_root=True, - root_helper=self._root_helper) - - LOG.info(_LI("Map volume %(cmd)s: stdout=%(out)s " - "stderr=%(err)s"), - {'cmd': self.GET_GUID_CMD, 'out': out, 'err': err}) - - except putils.ProcessExecutionError as e: - msg = (_("Error querying sdc guid: %(err)s") % {'err': e.stderr}) - LOG.error(msg) - raise exception.BrickException(message=msg) - - guid = out - LOG.info(_LI("Current sdc guid: %(guid)s"), {'guid': guid}) - params = {'guid': guid, 'allowMultipleMappings': 'TRUE'} - self.volume_id = self.volume_id or self._get_volume_id() - - headers = {'content-type': 'application/json'} - request = ( - "https://%(server_ip)s:%(server_port)s/api/instances/" - "Volume::%(volume_id)s/action/addMappedSdc" % - {'server_ip': self.server_ip, 'server_port': self.server_port, - 'volume_id': self.volume_id} - ) - - LOG.info(_LI("map volume request: %(request)s"), {'request': request}) - r = requests.post( - request, - data=json.dumps(params), - headers=headers, - auth=(self.server_username, self.server_token), - verify=False - ) - - r = self._check_response(r, request, False, params) - if r.status_code != self.OK_STATUS_CODE: - response = r.json() - error_code = response['errorCode'] - if error_code == self.VOLUME_ALREADY_MAPPED_ERROR: - LOG.warning(_LW( - "Ignoring error mapping volume %(volume_name)s: " - "volume already mapped."), - {'volume_name': self.volume_name} - ) - else: - msg = ( - _("Error mapping volume %(volume_name)s: %(err)s") % - {'volume_name': self.volume_name, - 'err': response['message']} - ) - - LOG.error(msg) - raise exception.BrickException(message=msg) - - self.volume_path = self._find_volume_path() - device_info['path'] = self.volume_path - - # Set QoS settings after map was performed - if self.iops_limit is not None or self.bandwidth_limit is not None: - params = {'guid': guid} - if self.bandwidth_limit is not None: - params['bandwidthLimitInKbps'] = self.bandwidth_limit - if self.iops_limit is not None: - params['iopsLimit'] = self.iops_limit - - request = ( - "https://%(server_ip)s:%(server_port)s/api/instances/" - "Volume::%(volume_id)s/action/setMappedSdcLimits" % - {'server_ip': self.server_ip, 'server_port': self.server_port, - 'volume_id': self.volume_id} - ) - - LOG.info(_LI("Set client limit request: %(request)s"), - {'request': request}) - - r = requests.post( - request, - data=json.dumps(params), - headers=headers, - auth=(self.server_username, self.server_token), - verify=False - ) - r = self._check_response(r, request, False, params) - if r.status_code != self.OK_STATUS_CODE: - response = r.json() - LOG.info(_LI("Set client limit response: %(response)s"), - {'response': response}) - msg = ( - _("Error setting client limits for volume " - "%(volume_name)s: %(err)s") % - {'volume_name': self.volume_name, - 'err': response['message']} - ) - - LOG.error(msg) - - return device_info - - @utils.trace - @lockutils.synchronized('scaleio', 'scaleio-') - def disconnect_volume(self, connection_properties, device_info): - """Disconnect the ScaleIO volume. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :param device_info: historical difference, but same as connection_props - :type device_info: dict - """ - self.get_config(connection_properties) - self.volume_id = self.volume_id or self._get_volume_id() - LOG.info(_LI( - "ScaleIO disconnect volume in ScaleIO brick volume driver." - )) - - LOG.debug( - _("ScaleIO Volume name: %(volume_name)s, SDC IP: %(sdc_ip)s, " - "REST Server IP: %(server_ip)s"), - {'volume_name': self.volume_name, 'sdc_ip': self.local_sdc_ip, - 'server_ip': self.server_ip} - ) - - LOG.info(_LI("ScaleIO sdc query guid command: %(cmd)s"), - {'cmd': self.GET_GUID_CMD}) - - try: - (out, err) = self._execute(*self.GET_GUID_CMD, run_as_root=True, - root_helper=self._root_helper) - LOG.info( - _LI("Unmap volume %(cmd)s: stdout=%(out)s stderr=%(err)s"), - {'cmd': self.GET_GUID_CMD, 'out': out, 'err': err} - ) - - except putils.ProcessExecutionError as e: - msg = _("Error querying sdc guid: %(err)s") % {'err': e.stderr} - LOG.error(msg) - raise exception.BrickException(message=msg) - - guid = out - LOG.info(_LI("Current sdc guid: %(guid)s"), {'guid': guid}) - - params = {'guid': guid} - headers = {'content-type': 'application/json'} - request = ( - "https://%(server_ip)s:%(server_port)s/api/instances/" - "Volume::%(volume_id)s/action/removeMappedSdc" % - {'server_ip': self.server_ip, 'server_port': self.server_port, - 'volume_id': self.volume_id} - ) - - LOG.info(_LI("Unmap volume request: %(request)s"), - {'request': request}) - r = requests.post( - request, - data=json.dumps(params), - headers=headers, - auth=(self.server_username, self.server_token), - verify=False - ) - - r = self._check_response(r, request, False, params) - if r.status_code != self.OK_STATUS_CODE: - response = r.json() - error_code = response['errorCode'] - if error_code == self.VOLUME_NOT_MAPPED_ERROR: - LOG.warning(_LW( - "Ignoring error unmapping volume %(volume_id)s: " - "volume not mapped."), {'volume_id': self.volume_name} - ) - else: - msg = (_("Error unmapping volume %(volume_id)s: %(err)s") % - {'volume_id': self.volume_name, - 'err': response['message']}) - LOG.error(msg) - raise exception.BrickException(message=msg) - - def extend_volume(self, connection_properties): - # TODO(walter-boring): is this possible? - raise NotImplementedError - - -class DISCOConnector(BaseLinuxConnector): - """Class implements the connector driver for DISCO.""" - - DISCO_PREFIX = 'dms' - - def __init__(self, root_helper, driver=None, - device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, - *args, **kwargs): - """Init DISCO connector.""" - super(DISCOConnector, self).__init__( - root_helper, - driver=driver, - device_scan_attempts=device_scan_attempts, - *args, **kwargs - ) - LOG.info(_LI("Init DISCO connector")) - - self.server_port = None - self.server_ip = None - - @staticmethod - def get_connector_properties(root_helper, *args, **kwargs): - """The DISCO connector properties.""" - return {} - - def get_search_path(self): - """Get directory path where to get DISCO volumes.""" - return "/dev" - - @utils.trace - def get_volume_paths(self, connection_properties): - """Get config for DISCO volume driver.""" - self.get_config(connection_properties) - volume_paths = [] - disco_id = connection_properties['disco_id'] - disco_dev = '/dev/dms%s' % (disco_id) - device_paths = [disco_dev] - for path in device_paths: - if os.path.exists(path): - volume_paths.append(path) - return volume_paths - - @utils.trace - def get_all_available_volumes(self, connection_properties=None): - """Return all DISCO volumes that exist in the search directory.""" - path = self.get_search_path() - - if os.path.isdir(path): - path_items = [path, '/', self.DISCO_PREFIX, '*'] - file_filter = ''.join(path_items) - return glob.glob(file_filter) - else: - return [] - - def get_config(self, connection_properties): - """Get config for DISCO volume driver.""" - self.server_port = ( - six.text_type(connection_properties['conf']['server_port'])) - self.server_ip = ( - six.text_type(connection_properties['conf']['server_ip'])) - - disco_id = connection_properties['disco_id'] - disco_dev = '/dev/dms%s' % (disco_id) - device_info = {'type': 'block', - 'path': disco_dev} - return device_info - - @utils.trace - @synchronized('connect_volume') - def connect_volume(self, connection_properties): - """Connect the volume. Returns xml for libvirt.""" - device_info = self.get_config(connection_properties) - LOG.debug("Device info : %s.", device_info) - disco_id = connection_properties['disco_id'] - disco_dev = '/dev/dms%s' % (disco_id) - LOG.debug("Attaching %s", disco_dev) - - self._mount_disco_volume(disco_dev, disco_id) - return device_info - - @utils.trace - @synchronized('connect_volume') - def disconnect_volume(self, connection_properties, device_info): - """Detach the volume from instance.""" - disco_id = connection_properties['disco_id'] - disco_dev = '/dev/dms%s' % (disco_id) - LOG.debug("detaching %s", disco_dev) - - if os.path.exists(disco_dev): - ret = self._send_disco_vol_cmd(self.server_ip, - self.server_port, - 2, - disco_id) - if ret is not None: - msg = _("Detach volume failed") - raise exception.BrickException(message=msg) - else: - LOG.info(_LI("Volume already detached from host")) - - def _mount_disco_volume(self, path, volume_id): - """Send request to mount volume on physical host.""" - LOG.debug("Enter in mount disco volume %(port)s " - "and %(ip)s.", - {'port': self.server_port, - 'ip': self.server_ip}) - - if not os.path.exists(path): - ret = self._send_disco_vol_cmd(self.server_ip, - self.server_port, - 1, - volume_id) - if ret is not None: - msg = _("Attach volume failed") - raise exception.BrickException(message=msg) - else: - LOG.info(_LI("Volume already attached to host")) - - def _connect_tcp_socket(self, client_ip, client_port): - """Connect to TCP socket.""" - sock = None - - for res in socket.getaddrinfo(client_ip, - client_port, - socket.AF_UNSPEC, - socket.SOCK_STREAM): - aff, socktype, proto, canonname, saa = res - try: - sock = socket.socket(aff, socktype, proto) - except socket.error: - sock = None - continue - try: - sock.connect(saa) - except socket.error: - sock.close() - sock = None - continue - break - - if sock is None: - LOG.error(_LE("Cannot connect TCP socket")) - return sock - - def _send_disco_vol_cmd(self, client_ip, client_port, op_code, vol_id): - """Send DISCO client socket command.""" - s = self._connect_tcp_socket(client_ip, int(client_port)) - - if s is not None: - inst_id = 'DEFAULT-INSTID' - pktlen = 2 + 8 + len(inst_id) - LOG.debug("pktlen=%(plen)s op=%(op)s " - "vol_id=%(vol_id)s, inst_id=%(inst_id)s", - {'plen': pktlen, 'op': op_code, - 'vol_id': vol_id, 'inst_id': inst_id}) - data = struct.pack("!HHQ14s", - pktlen, - op_code, - int(vol_id), - inst_id) - s.sendall(data) - ret = s.recv(4) - s.close() - - LOG.debug("Received ret len=%(lenR)d, ret=%(ret)s", - {'lenR': len(repr(ret)), 'ret': repr(ret)}) - - ret_val = "".join("%02x" % ord(c) for c in ret) - - if ret_val != '00000000': - return 'ERROR' - return None - - def extend_volume(self, connection_properties): - raise NotImplementedError - - -class SheepdogConnector(BaseLinuxConnector): - """"Connector class to attach/detach sheepdog volumes.""" - - def __init__(self, root_helper, driver=None, use_multipath=False, - device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, - *args, **kwargs): - - super(SheepdogConnector, self).__init__(root_helper, driver=driver, - device_scan_attempts= - device_scan_attempts, - *args, **kwargs) - - @staticmethod - def get_connector_properties(root_helper, *args, **kwargs): - """The Sheepdog connector properties.""" - return {} - - def get_volume_paths(self, connection_properties): - # TODO(lixiaoy1): don't know where the connector - # looks for sheepdog volumes. - return [] - - def get_search_path(self): - # TODO(lixiaoy1): don't know where the connector - # looks for sheepdog volumes. - return None - - def get_all_available_volumes(self, connection_properties=None): - # TODO(lixiaoy1): not sure what to return here for sheepdog - return [] - - def _get_sheepdog_handle(self, connection_properties): - try: - host = connection_properties['hosts'][0] - name = connection_properties['name'] - port = connection_properties['ports'][0] - except IndexError: - msg = _("Connect volume failed, malformed connection properties") - raise exception.BrickException(msg=msg) - - sheepdog_handle = linuxsheepdog.SheepdogVolumeIOWrapper( - host, port, name) - return sheepdog_handle - - @utils.trace - def connect_volume(self, connection_properties): - """Connect to a volume. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :returns: dict - """ - - sheepdog_handle = self._get_sheepdog_handle(connection_properties) - return {'path': sheepdog_handle} - - @utils.trace - def disconnect_volume(self, connection_properties, device_info): - """Disconnect a volume. - - :param connection_properties: The dictionary that describes all - of the target volume attributes. - :type connection_properties: dict - :param device_info: historical difference, but same as connection_props - :type device_info: dict - """ - if device_info: - sheepdog_handle = device_info.get('path', None) - self.check_IO_handle_valid(sheepdog_handle, - linuxsheepdog.SheepdogVolumeIOWrapper, - 'Sheepdog') - if sheepdog_handle is not None: - sheepdog_handle.close() - - def check_valid_device(self, path, run_as_root=True): - """Verify an existing sheepdog handle is connected and valid.""" - sheepdog_handle = path - - if sheepdog_handle is None: - return False - - original_offset = sheepdog_handle.tell() - - try: - sheepdog_handle.read(4096) - except Exception as e: - LOG.error(_LE("Failed to access sheepdog device " - "handle: %(error)s"), - {"error": e}) - return False - finally: - sheepdog_handle.seek(original_offset, 0) - - return True - - def extend_volume(self, connection_properties): - # TODO(lixiaoy1): is this possible? - raise NotImplementedError + conn_cls = importutils.import_class(connector) + return conn_cls(*args, **kwargs) diff --git a/os_brick/initiator/connectors/__init__.py b/os_brick/initiator/connectors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/os_brick/initiator/connectors/aoe.py b/os_brick/initiator/connectors/aoe.py new file mode 100644 index 000000000..f3eb01982 --- /dev/null +++ b/os_brick/initiator/connectors/aoe.py @@ -0,0 +1,177 @@ +# All Rights Reserved. +# +# 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. + +import os + +from oslo_concurrency import lockutils +from oslo_log import log as logging +from oslo_service import loopingcall + +from os_brick import exception +from os_brick import initiator + +from os_brick.i18n import _LW +from os_brick.initiator.connectors import base +from os_brick import utils + +DEVICE_SCAN_ATTEMPTS_DEFAULT = 3 +LOG = logging.getLogger(__name__) + + +class AoEConnector(base.BaseLinuxConnector): + """Connector class to attach/detach AoE volumes.""" + + def __init__(self, root_helper, driver=None, + device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT, + *args, **kwargs): + super(AoEConnector, self).__init__( + root_helper, + driver=driver, + device_scan_attempts=device_scan_attempts, + *args, **kwargs) + + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The AoE connector properties.""" + return {} + + def get_search_path(self): + return '/dev/etherd' + + def get_volume_paths(self, connection_properties): + aoe_device, aoe_path = self._get_aoe_info(connection_properties) + volume_paths = [] + if os.path.exists(aoe_path): + volume_paths.append(aoe_path) + + return volume_paths + + def _get_aoe_info(self, connection_properties): + shelf = connection_properties['target_shelf'] + lun = connection_properties['target_lun'] + aoe_device = 'e%(shelf)s.%(lun)s' % {'shelf': shelf, + 'lun': lun} + path = self.get_search_path() + aoe_path = '%(path)s/%(device)s' % {'path': path, + 'device': aoe_device} + return aoe_device, aoe_path + + @utils.trace + @lockutils.synchronized('aoe_control', 'aoe-') + def connect_volume(self, connection_properties): + """Discover and attach the volume. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :returns: dict + + connection_properties for AoE must include: + target_shelf - shelf id of volume + target_lun - lun id of volume + """ + aoe_device, aoe_path = self._get_aoe_info(connection_properties) + + device_info = { + 'type': 'block', + 'device': aoe_device, + 'path': aoe_path, + } + + if os.path.exists(aoe_path): + self._aoe_revalidate(aoe_device) + else: + self._aoe_discover() + + waiting_status = {'tries': 0} + + # NOTE(jbr_): Device path is not always present immediately + def _wait_for_discovery(aoe_path): + if os.path.exists(aoe_path): + raise loopingcall.LoopingCallDone + + if waiting_status['tries'] >= self.device_scan_attempts: + raise exception.VolumeDeviceNotFound(device=aoe_path) + + LOG.warning(_LW("AoE volume not yet found at: %(path)s. " + "Try number: %(tries)s"), + {'path': aoe_device, + 'tries': waiting_status['tries']}) + + self._aoe_discover() + waiting_status['tries'] += 1 + + timer = loopingcall.FixedIntervalLoopingCall(_wait_for_discovery, + aoe_path) + timer.start(interval=2).wait() + + if waiting_status['tries']: + LOG.debug("Found AoE device %(path)s " + "(after %(tries)s rediscover)", + {'path': aoe_path, + 'tries': waiting_status['tries']}) + + return device_info + + @utils.trace + @lockutils.synchronized('aoe_control', 'aoe-') + def disconnect_volume(self, connection_properties, device_info): + """Detach and flush the volume. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :param device_info: historical difference, but same as connection_props + :type device_info: dict + + connection_properties for AoE must include: + target_shelf - shelf id of volume + target_lun - lun id of volume + """ + aoe_device, aoe_path = self._get_aoe_info(connection_properties) + + if os.path.exists(aoe_path): + self._aoe_flush(aoe_device) + + def _aoe_discover(self): + (out, err) = self._execute('aoe-discover', + run_as_root=True, + root_helper=self._root_helper, + check_exit_code=0) + + LOG.debug('aoe-discover: stdout=%(out)s stderr%(err)s', + {'out': out, 'err': err}) + + def _aoe_revalidate(self, aoe_device): + (out, err) = self._execute('aoe-revalidate', + aoe_device, + run_as_root=True, + root_helper=self._root_helper, + check_exit_code=0) + + LOG.debug('aoe-revalidate %(dev)s: stdout=%(out)s stderr%(err)s', + {'dev': aoe_device, 'out': out, 'err': err}) + + def _aoe_flush(self, aoe_device): + (out, err) = self._execute('aoe-flush', + aoe_device, + run_as_root=True, + root_helper=self._root_helper, + check_exit_code=0) + LOG.debug('aoe-flush %(dev)s: stdout=%(out)s stderr%(err)s', + {'dev': aoe_device, 'out': out, 'err': err}) + + def extend_volume(self, connection_properties): + # TODO(walter-boring): is this possible? + raise NotImplementedError diff --git a/os_brick/initiator/connectors/base.py b/os_brick/initiator/connectors/base.py new file mode 100644 index 000000000..51c5bc112 --- /dev/null +++ b/os_brick/initiator/connectors/base.py @@ -0,0 +1,129 @@ +# All Rights Reserved. +# +# 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. + + +import glob +import os + +from oslo_concurrency import processutils as putils +from oslo_log import log as logging + +from os_brick import exception +from os_brick import initiator + +from os_brick.i18n import _LE, _LW +from os_brick.initiator import host_driver +from os_brick.initiator import initiator_connector +from os_brick.initiator import linuxscsi + +LOG = logging.getLogger(__name__) + + +class BaseLinuxConnector(initiator_connector.InitiatorConnector): + os_type = initiator.OS_TYPE_LINUX + + def __init__(self, root_helper, driver=None, execute=None, + *args, **kwargs): + self._linuxscsi = linuxscsi.LinuxSCSI(root_helper, execute=execute) + + if not driver: + driver = host_driver.HostDriver() + self.set_driver(driver) + + super(BaseLinuxConnector, self).__init__(root_helper, execute=execute, + *args, **kwargs) + + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The generic connector properties.""" + multipath = kwargs['multipath'] + enforce_multipath = kwargs['enforce_multipath'] + props = {} + + props['multipath'] = (multipath and + linuxscsi.LinuxSCSI.is_multipath_running( + enforce_multipath, root_helper, + execute=kwargs.get('execute'))) + + return props + + def check_valid_device(self, path, run_as_root=True): + cmd = ('dd', 'if=%(path)s' % {"path": path}, + 'of=/dev/null', 'count=1') + out, info = None, None + try: + out, info = self._execute(*cmd, run_as_root=run_as_root, + root_helper=self._root_helper) + except putils.ProcessExecutionError as e: + LOG.error(_LE("Failed to access the device on the path " + "%(path)s: %(error)s."), + {"path": path, "error": e.stderr}) + return False + # If the info is none, the path does not exist. + if info is None: + return False + return True + + def get_all_available_volumes(self, connection_properties=None): + volumes = [] + path = self.get_search_path() + if path: + # now find all entries in the search path + if os.path.isdir(path): + path_items = [path, '/*'] + file_filter = ''.join(path_items) + volumes = glob.glob(file_filter) + + return volumes + + def _discover_mpath_device(self, device_wwn, connection_properties, + device_name): + """This method discovers a multipath device. + + Discover a multipath device based on a defined connection_property + and a device_wwn and return the multipath_id and path of the multipath + enabled device if there is one. + """ + + path = self._linuxscsi.find_multipath_device_path(device_wwn) + device_path = None + multipath_id = None + + if path is None: + # find_multipath_device only accept realpath not symbolic path + device_realpath = os.path.realpath(device_name) + mpath_info = self._linuxscsi.find_multipath_device( + device_realpath) + if mpath_info: + device_path = mpath_info['device'] + multipath_id = device_wwn + else: + # we didn't find a multipath device. + # so we assume the kernel only sees 1 device + device_path = device_name + LOG.debug("Unable to find multipath device name for " + "volume. Using path %(device)s for volume.", + {'device': device_path}) + else: + device_path = path + multipath_id = device_wwn + if connection_properties.get('access_mode', '') != 'ro': + try: + # Sometimes the multipath devices will show up as read only + # initially and need additional time/rescans to get to RW. + self._linuxscsi.wait_for_rw(device_wwn, device_path) + except exception.BlockDeviceReadOnly: + LOG.warning(_LW('Block device %s is still read-only. ' + 'Continuing anyway.'), device_path) + return device_path, multipath_id diff --git a/os_brick/initiator/connectors/base_iscsi.py b/os_brick/initiator/connectors/base_iscsi.py new file mode 100644 index 000000000..b91a592a3 --- /dev/null +++ b/os_brick/initiator/connectors/base_iscsi.py @@ -0,0 +1,42 @@ +# All Rights Reserved. +# +# 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. + + +import copy + +from os_brick.initiator import initiator_connector + + +class BaseISCSIConnector(initiator_connector.InitiatorConnector): + def _iterate_all_targets(self, connection_properties): + for portal, iqn, lun in self._get_all_targets(connection_properties): + props = copy.deepcopy(connection_properties) + props['target_portal'] = portal + props['target_iqn'] = iqn + props['target_lun'] = lun + for key in ('target_portals', 'target_iqns', 'target_luns'): + props.pop(key, None) + yield props + + def _get_all_targets(self, connection_properties): + if all([key in connection_properties for key in ('target_portals', + 'target_iqns', + 'target_luns')]): + return zip(connection_properties['target_portals'], + connection_properties['target_iqns'], + connection_properties['target_luns']) + + return [(connection_properties['target_portal'], + connection_properties['target_iqn'], + connection_properties.get('target_lun', 0))] diff --git a/os_brick/initiator/connectors/disco.py b/os_brick/initiator/connectors/disco.py new file mode 100644 index 000000000..af3371302 --- /dev/null +++ b/os_brick/initiator/connectors/disco.py @@ -0,0 +1,207 @@ +# All Rights Reserved. +# +# 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. + +import glob +import os +import socket +import struct + +from oslo_concurrency import lockutils +from oslo_log import log as logging +import six + +from os_brick.i18n import _, _LI, _LE +from os_brick import exception +from os_brick import initiator +from os_brick.initiator.connectors import base +from os_brick import utils + +LOG = logging.getLogger(__name__) +DEVICE_SCAN_ATTEMPTS_DEFAULT = 3 +synchronized = lockutils.synchronized_with_prefix('os-brick-') + + +class DISCOConnector(base.BaseLinuxConnector): + """Class implements the connector driver for DISCO.""" + + DISCO_PREFIX = 'dms' + + def __init__(self, root_helper, driver=None, + device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT, + *args, **kwargs): + """Init DISCO connector.""" + super(DISCOConnector, self).__init__( + root_helper, + driver=driver, + device_scan_attempts=device_scan_attempts, + *args, **kwargs + ) + LOG.info(_LI("Init DISCO connector")) + + self.server_port = None + self.server_ip = None + + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The DISCO connector properties.""" + return {} + + def get_search_path(self): + """Get directory path where to get DISCO volumes.""" + return "/dev" + + def get_volume_paths(self, connection_properties): + """Get config for DISCO volume driver.""" + self.get_config(connection_properties) + volume_paths = [] + disco_id = connection_properties['disco_id'] + disco_dev = '/dev/dms%s' % (disco_id) + device_paths = [disco_dev] + for path in device_paths: + if os.path.exists(path): + volume_paths.append(path) + return volume_paths + + def get_all_available_volumes(self, connection_properties=None): + """Return all DISCO volumes that exist in the search directory.""" + path = self.get_search_path() + + if os.path.isdir(path): + path_items = [path, '/', self.DISCO_PREFIX, '*'] + file_filter = ''.join(path_items) + return glob.glob(file_filter) + else: + return [] + + def get_config(self, connection_properties): + """Get config for DISCO volume driver.""" + self.server_port = ( + six.text_type(connection_properties['conf']['server_port'])) + self.server_ip = ( + six.text_type(connection_properties['conf']['server_ip'])) + + disco_id = connection_properties['disco_id'] + disco_dev = '/dev/dms%s' % (disco_id) + device_info = {'type': 'block', + 'path': disco_dev} + return device_info + + @utils.trace + @synchronized('connect_volume') + def connect_volume(self, connection_properties): + """Connect the volume. Returns xml for libvirt.""" + LOG.debug("Enter in DISCO connect_volume") + device_info = self.get_config(connection_properties) + LOG.debug("Device info : %s.", device_info) + disco_id = connection_properties['disco_id'] + disco_dev = '/dev/dms%s' % (disco_id) + LOG.debug("Attaching %s", disco_dev) + + self._mount_disco_volume(disco_dev, disco_id) + return device_info + + @utils.trace + @synchronized('connect_volume') + def disconnect_volume(self, connection_properties, device_info): + """Detach the volume from instance.""" + disco_id = connection_properties['disco_id'] + disco_dev = '/dev/dms%s' % (disco_id) + LOG.debug("detaching %s", disco_dev) + + if os.path.exists(disco_dev): + ret = self._send_disco_vol_cmd(self.server_ip, + self.server_port, + 2, + disco_id) + if ret is not None: + msg = _("Detach volume failed") + raise exception.BrickException(message=msg) + else: + LOG.info(_LI("Volume already detached from host")) + + def _mount_disco_volume(self, path, volume_id): + """Send request to mount volume on physical host.""" + LOG.debug("Enter in mount disco volume %(port)s " + "and %(ip)s.", + {'port': self.server_port, + 'ip': self.server_ip}) + + if not os.path.exists(path): + ret = self._send_disco_vol_cmd(self.server_ip, + self.server_port, + 1, + volume_id) + if ret is not None: + msg = _("Attach volume failed") + raise exception.BrickException(message=msg) + else: + LOG.info(_LI("Volume already attached to host")) + + def _connect_tcp_socket(self, client_ip, client_port): + """Connect to TCP socket.""" + sock = None + + for res in socket.getaddrinfo(client_ip, + client_port, + socket.AF_UNSPEC, + socket.SOCK_STREAM): + aff, socktype, proto, canonname, saa = res + try: + sock = socket.socket(aff, socktype, proto) + except socket.error: + sock = None + continue + try: + sock.connect(saa) + except socket.error: + sock.close() + sock = None + continue + break + + if sock is None: + LOG.error(_LE("Cannot connect TCP socket")) + return sock + + def _send_disco_vol_cmd(self, client_ip, client_port, op_code, vol_id): + """Send DISCO client socket command.""" + s = self._connect_tcp_socket(client_ip, int(client_port)) + + if s is not None: + inst_id = 'DEFAULT-INSTID' + pktlen = 2 + 8 + len(inst_id) + LOG.debug("pktlen=%(plen)s op=%(op)s " + "vol_id=%(vol_id)s, inst_id=%(inst_id)s", + {'plen': pktlen, 'op': op_code, + 'vol_id': vol_id, 'inst_id': inst_id}) + data = struct.pack("!HHQ14s", + pktlen, + op_code, + int(vol_id), + inst_id) + s.sendall(data) + ret = s.recv(4) + s.close() + + LOG.debug("Received ret len=%(lenR)d, ret=%(ret)s", + {'lenR': len(repr(ret)), 'ret': repr(ret)}) + + ret_val = "".join("%02x" % ord(c) for c in ret) + + if ret_val != '00000000': + return 'ERROR' + return None + + def extend_volume(self, connection_properties): + raise NotImplementedError diff --git a/os_brick/initiator/connectors/drbd.py b/os_brick/initiator/connectors/drbd.py new file mode 100644 index 000000000..edd277633 --- /dev/null +++ b/os_brick/initiator/connectors/drbd.py @@ -0,0 +1,109 @@ +# All Rights Reserved. +# +# 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. + +import os +import tempfile + +from oslo_concurrency import processutils as putils + +from os_brick.initiator.connectors import base +from os_brick import utils + + +class DRBDConnector(base.BaseLinuxConnector): + """"Connector class to attach/detach DRBD resources.""" + + def __init__(self, root_helper, driver=None, + execute=putils.execute, *args, **kwargs): + + super(DRBDConnector, self).__init__(root_helper, driver=driver, + execute=execute, *args, **kwargs) + + self._execute = execute + self._root_helper = root_helper + + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The DRBD connector properties.""" + return {} + + def check_valid_device(self, path, run_as_root=True): + """Verify an existing volume.""" + # TODO(linbit): check via drbdsetup first, to avoid blocking/hanging + # in case of network problems? + + return super(DRBDConnector, self).check_valid_device(path, run_as_root) + + def get_all_available_volumes(self, connection_properties=None): + + base = "/dev/" + blkdev_list = [] + + for e in os.listdir(base): + path = base + e + if os.path.isblk(path): + blkdev_list.append(path) + + return blkdev_list + + def _drbdadm_command(self, cmd, data_dict, sh_secret): + # TODO(linbit): Write that resource file to a permanent location? + tmp = tempfile.NamedTemporaryFile(suffix="res", delete=False, mode="w") + try: + kv = {'shared-secret': sh_secret} + tmp.write(data_dict['config'] % kv) + tmp.close() + + (out, err) = self._execute('drbdadm', cmd, + "-c", tmp.name, + data_dict['name'], + run_as_root=True, + root_helper=self._root_helper) + finally: + os.unlink(tmp.name) + + return (out, err) + + @utils.trace + def connect_volume(self, connection_properties): + """Attach the volume.""" + + self._drbdadm_command("adjust", connection_properties, + connection_properties['provider_auth']) + + device_info = { + 'type': 'block', + 'path': connection_properties['device'], + } + + return device_info + + @utils.trace + def disconnect_volume(self, connection_properties, device_info): + """Detach the volume.""" + + self._drbdadm_command("down", connection_properties, + connection_properties['provider_auth']) + + def get_volume_paths(self, connection_properties): + path = connection_properties['device'] + return [path] + + def get_search_path(self): + # TODO(linbit): is it allowed to return "/dev", or is that too broad? + return None + + def extend_volume(self, connection_properties): + # TODO(walter-boring): is this possible? + raise NotImplementedError diff --git a/os_brick/initiator/connectors/fake.py b/os_brick/initiator/connectors/fake.py new file mode 100644 index 000000000..d564e2978 --- /dev/null +++ b/os_brick/initiator/connectors/fake.py @@ -0,0 +1,48 @@ +# Copyright 2013 OpenStack Foundation. +# All Rights Reserved. +# +# 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. + + +from os_brick.initiator.connectors import base +from os_brick.initiator.connectors import base_iscsi + + +class FakeConnector(base.BaseLinuxConnector): + + fake_path = '/dev/vdFAKE' + + def connect_volume(self, connection_properties): + fake_device_info = {'type': 'fake', + 'path': self.fake_path} + return fake_device_info + + def disconnect_volume(self, connection_properties, device_info): + pass + + def get_volume_paths(self, connection_properties): + return [self.fake_path] + + def get_search_path(self): + return '/dev/disk/by-path' + + def extend_volume(self, connection_properties): + return None + + def get_all_available_volumes(self, connection_properties=None): + return ['/dev/disk/by-path/fake-volume-1', + '/dev/disk/by-path/fake-volume-X'] + + +class FakeBaseISCSIConnector(FakeConnector, base_iscsi.BaseISCSIConnector): + pass diff --git a/os_brick/initiator/connectors/fibre_channel.py b/os_brick/initiator/connectors/fibre_channel.py new file mode 100644 index 000000000..4c9489a3c --- /dev/null +++ b/os_brick/initiator/connectors/fibre_channel.py @@ -0,0 +1,300 @@ +# All Rights Reserved. +# +# 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. + +import os + +from oslo_concurrency import lockutils +from oslo_log import log as logging +from oslo_service import loopingcall +import six + +from os_brick.i18n import _LE, _LW +from os_brick import exception +from os_brick import initiator +from os_brick.initiator.connectors import base +from os_brick.initiator import linuxfc +from os_brick import utils + +synchronized = lockutils.synchronized_with_prefix('os-brick-') + +LOG = logging.getLogger(__name__) + + +class FibreChannelConnector(base.BaseLinuxConnector): + """Connector class to attach/detach Fibre Channel volumes.""" + + def __init__(self, root_helper, driver=None, + execute=None, use_multipath=False, + device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT, + *args, **kwargs): + self._linuxfc = linuxfc.LinuxFibreChannel(root_helper, execute) + super(FibreChannelConnector, self).__init__( + root_helper, driver=driver, + execute=execute, + device_scan_attempts=device_scan_attempts, + *args, **kwargs) + self.use_multipath = use_multipath + + def set_execute(self, execute): + super(FibreChannelConnector, self).set_execute(execute) + self._linuxscsi.set_execute(execute) + self._linuxfc.set_execute(execute) + + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The Fibre Channel connector properties.""" + props = {} + fc = linuxfc.LinuxFibreChannel(root_helper, + execute=kwargs.get('execute')) + + wwpns = fc.get_fc_wwpns() + if wwpns: + props['wwpns'] = wwpns + wwnns = fc.get_fc_wwnns() + if wwnns: + props['wwnns'] = wwnns + + return props + + def get_search_path(self): + """Where do we look for FC based volumes.""" + return '/dev/disk/by-path' + + def _get_possible_volume_paths(self, connection_properties, hbas): + ports = connection_properties['target_wwn'] + possible_devs = self._get_possible_devices(hbas, ports) + + lun = connection_properties.get('target_lun', 0) + host_paths = self._get_host_devices(possible_devs, lun) + return host_paths + + def get_volume_paths(self, connection_properties): + volume_paths = [] + # first fetch all of the potential paths that might exist + # how the FC fabric is zoned may alter the actual list + # that shows up on the system. So, we verify each path. + hbas = self._linuxfc.get_fc_hbas_info() + device_paths = self._get_possible_volume_paths( + connection_properties, hbas) + for path in device_paths: + if os.path.exists(path): + volume_paths.append(path) + + return volume_paths + + @utils.trace + @synchronized('extend_volume') + def extend_volume(self, connection_properties): + """Update the local kernel's size information. + + Try and update the local kernel's size information + for an FC volume. + """ + volume_paths = self.get_volume_paths(connection_properties) + if volume_paths: + return self._linuxscsi.extend_volume(volume_paths[0]) + else: + LOG.warning(_LW("Couldn't find any volume paths on the host to " + "extend volume for %(props)s"), + {'props': connection_properties}) + raise exception.VolumePathsNotFound() + + @utils.trace + @synchronized('connect_volume') + def connect_volume(self, connection_properties): + """Attach the volume to instance_name. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :returns: dict + + connection_properties for Fibre Channel must include: + target_wwn - World Wide Name + target_lun - LUN id of the volume + """ + LOG.debug("execute = %s", self._execute) + device_info = {'type': 'block'} + + hbas = self._linuxfc.get_fc_hbas_info() + host_devices = self._get_possible_volume_paths( + connection_properties, hbas) + + if len(host_devices) == 0: + # this is empty because we don't have any FC HBAs + LOG.warning( + _LW("We are unable to locate any Fibre Channel devices")) + raise exception.NoFibreChannelHostsFound() + + # The /dev/disk/by-path/... node is not always present immediately + # We only need to find the first device. Once we see the first device + # multipath will have any others. + def _wait_for_device_discovery(host_devices): + tries = self.tries + for device in host_devices: + LOG.debug("Looking for Fibre Channel dev %(device)s", + {'device': device}) + if os.path.exists(device): + self.host_device = device + # get the /dev/sdX device. This is used + # to find the multipath device. + self.device_name = os.path.realpath(device) + raise loopingcall.LoopingCallDone() + + if self.tries >= self.device_scan_attempts: + LOG.error(_LE("Fibre Channel volume device not found.")) + raise exception.NoFibreChannelVolumeDeviceFound() + + LOG.warning(_LW("Fibre Channel volume device not yet found. " + "Will rescan & retry. Try number: %(tries)s."), + {'tries': tries}) + + self._linuxfc.rescan_hosts(hbas) + self.tries = self.tries + 1 + + self.host_device = None + self.device_name = None + self.tries = 0 + timer = loopingcall.FixedIntervalLoopingCall( + _wait_for_device_discovery, host_devices) + timer.start(interval=2).wait() + + tries = self.tries + if self.host_device is not None and self.device_name is not None: + LOG.debug("Found Fibre Channel volume %(name)s " + "(after %(tries)s rescans)", + {'name': self.device_name, 'tries': tries}) + + # find out the WWN of the device + device_wwn = self._linuxscsi.get_scsi_wwn(self.host_device) + LOG.debug("Device WWN = '%(wwn)s'", {'wwn': device_wwn}) + device_info['scsi_wwn'] = device_wwn + + # see if the new drive is part of a multipath + # device. If so, we'll use the multipath device. + if self.use_multipath: + (device_path, multipath_id) = (super( + FibreChannelConnector, self)._discover_mpath_device( + device_wwn, connection_properties, self.device_name)) + if multipath_id: + # only set the multipath_id if we found one + device_info['multipath_id'] = multipath_id + + else: + device_path = self.host_device + + device_info['path'] = device_path + LOG.debug("connect_volume returning %s", device_info) + return device_info + + def _get_host_devices(self, possible_devs, lun): + host_devices = [] + for pci_num, target_wwn in possible_devs: + host_device = "/dev/disk/by-path/pci-%s-fc-%s-lun-%s" % ( + pci_num, + target_wwn, + self._linuxscsi.process_lun_id(lun)) + host_devices.append(host_device) + return host_devices + + def _get_possible_devices(self, hbas, wwnports): + """Compute the possible fibre channel device options. + + :param hbas: available hba devices. + :param wwnports: possible wwn addresses. Can either be string + or list of strings. + + :returns: list of (pci_id, wwn) tuples + + Given one or more wwn (mac addresses for fibre channel) ports + do the matrix math to figure out a set of pci device, wwn + tuples that are potentially valid (they won't all be). This + provides a search space for the device connection. + + """ + # the wwn (think mac addresses for fiber channel devices) can + # either be a single value or a list. Normalize it to a list + # for further operations. + wwns = [] + if isinstance(wwnports, list): + for wwn in wwnports: + wwns.append(str(wwn)) + elif isinstance(wwnports, six.string_types): + wwns.append(str(wwnports)) + + raw_devices = [] + for hba in hbas: + pci_num = self._get_pci_num(hba) + if pci_num is not None: + for wwn in wwns: + target_wwn = "0x%s" % wwn.lower() + raw_devices.append((pci_num, target_wwn)) + return raw_devices + + @utils.trace + @synchronized('connect_volume') + def disconnect_volume(self, connection_properties, device_info): + """Detach the volume from instance_name. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :param device_info: historical difference, but same as connection_props + :type device_info: dict + + connection_properties for Fibre Channel must include: + target_wwn - World Wide Name + target_lun - LUN id of the volume + """ + + devices = [] + volume_paths = self.get_volume_paths(connection_properties) + wwn = None + for path in volume_paths: + real_path = self._linuxscsi.get_name_from_path(path) + if not wwn: + wwn = self._linuxscsi.get_scsi_wwn(path) + device_info = self._linuxscsi.get_device_info(real_path) + devices.append(device_info) + + LOG.debug("devices to remove = %s", devices) + self._remove_devices(connection_properties, devices) + + if self.use_multipath: + # There is a bug in multipath where the flushing + # doesn't remove the entry if friendly names are on + # we'll try anyway. + self._linuxscsi.flush_multipath_device(wwn) + + def _remove_devices(self, connection_properties, devices): + # There may have been more than 1 device mounted + # by the kernel for this volume. We have to remove + # all of them + for device in devices: + self._linuxscsi.remove_scsi_device(device["device"]) + + def _get_pci_num(self, hba): + # NOTE(walter-boring) + # device path is in format of (FC and FCoE) : + # /sys/devices/pci0000:00/0000:00:03.0/0000:05:00.3/host2/fc_host/host2 + # /sys/devices/pci0000:20/0000:20:03.0/0000:21:00.2/net/ens2f2/ctlr_2 + # /host3/fc_host/host3 + # we always want the value prior to the host or net value + if hba is not None: + if "device_path" in hba: + device_path = hba['device_path'].split('/') + for index, value in enumerate(device_path): + if value.startswith('net') or value.startswith('host'): + return device_path[index - 1] + return None diff --git a/os_brick/initiator/connectors/fibre_channel_s390x.py b/os_brick/initiator/connectors/fibre_channel_s390x.py new file mode 100644 index 000000000..4f1a7b766 --- /dev/null +++ b/os_brick/initiator/connectors/fibre_channel_s390x.py @@ -0,0 +1,86 @@ +# All Rights Reserved. +# +# 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. + +from oslo_log import log as logging + +from os_brick import initiator + +from os_brick.initiator.connectors import fibre_channel +from os_brick.initiator import linuxfc + +LOG = logging.getLogger(__name__) + + +class FibreChannelConnectorS390X(fibre_channel.FibreChannelConnector): + """Connector class to attach/detach Fibre Channel volumes on S390X arch.""" + + platform = initiator.PLATFORM_S390 + + def __init__(self, root_helper, driver=None, + execute=None, use_multipath=False, + device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT, + *args, **kwargs): + super(FibreChannelConnectorS390X, self).__init__( + root_helper, + driver=driver, + execute=execute, + device_scan_attempts=device_scan_attempts, + *args, **kwargs) + LOG.debug("Initializing Fibre Channel connector for S390") + self._linuxfc = linuxfc.LinuxFibreChannelS390X(root_helper, execute) + self.use_multipath = use_multipath + + def set_execute(self, execute): + super(FibreChannelConnectorS390X, self).set_execute(execute) + self._linuxscsi.set_execute(execute) + self._linuxfc.set_execute(execute) + + def _get_host_devices(self, possible_devs, lun): + host_devices = [] + for pci_num, target_wwn in possible_devs: + target_lun = self._get_lun_string(lun) + host_device = self._get_device_file_path( + pci_num, + target_wwn, + target_lun) + self._linuxfc.configure_scsi_device(pci_num, target_wwn, + target_lun) + host_devices.append(host_device) + return host_devices + + def _get_lun_string(self, lun): + target_lun = 0 + if lun <= 0xffff: + target_lun = "0x%04x000000000000" % lun + elif lun <= 0xffffffff: + target_lun = "0x%08x00000000" % lun + return target_lun + + def _get_device_file_path(self, pci_num, target_wwn, target_lun): + host_device = "/dev/disk/by-path/ccw-%s-zfcp-%s:%s" % ( + pci_num, + target_wwn, + target_lun) + return host_device + + def _remove_devices(self, connection_properties, devices): + hbas = self._linuxfc.get_fc_hbas_info() + ports = connection_properties['target_wwn'] + possible_devs = self._get_possible_devices(hbas, ports) + lun = connection_properties.get('target_lun', 0) + target_lun = self._get_lun_string(lun) + for pci_num, target_wwn in possible_devs: + self._linuxfc.deconfigure_scsi_device(pci_num, + target_wwn, + target_lun) diff --git a/os_brick/initiator/connectors/hgst.py b/os_brick/initiator/connectors/hgst.py new file mode 100644 index 000000000..54f990ffe --- /dev/null +++ b/os_brick/initiator/connectors/hgst.py @@ -0,0 +1,182 @@ +# All Rights Reserved. +# +# 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. + +import os +import socket + +from oslo_concurrency import processutils as putils +from oslo_log import log as logging + +from os_brick.i18n import _, _LE +from os_brick import exception +from os_brick import initiator +from os_brick.initiator.connectors import base +from os_brick import utils + +LOG = logging.getLogger(__name__) + + +class HGSTConnector(base.BaseLinuxConnector): + """Connector class to attach/detach HGST volumes.""" + + VGCCLUSTER = 'vgc-cluster' + + def __init__(self, root_helper, driver=None, + device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT, + *args, **kwargs): + super(HGSTConnector, self).__init__(root_helper, driver=driver, + device_scan_attempts= + device_scan_attempts, + *args, **kwargs) + self._vgc_host = None + + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The HGST connector properties.""" + return {} + + def _log_cli_err(self, err): + """Dumps the full command output to a logfile in error cases.""" + LOG.error(_LE("CLI fail: '%(cmd)s' = %(code)s\nout: %(stdout)s\n" + "err: %(stderr)s"), + {'cmd': err.cmd, 'code': err.exit_code, + 'stdout': err.stdout, 'stderr': err.stderr}) + + def _find_vgc_host(self): + """Finds vgc-cluster hostname for this box.""" + params = [self.VGCCLUSTER, "domain-list", "-1"] + try: + out, unused = self._execute(*params, run_as_root=True, + root_helper=self._root_helper) + except putils.ProcessExecutionError as err: + self._log_cli_err(err) + msg = _("Unable to get list of domain members, check that " + "the cluster is running.") + raise exception.BrickException(message=msg) + domain = out.splitlines() + params = ["ip", "addr", "list"] + try: + out, unused = self._execute(*params, run_as_root=False) + except putils.ProcessExecutionError as err: + self._log_cli_err(err) + msg = _("Unable to get list of IP addresses on this host, " + "check permissions and networking.") + raise exception.BrickException(message=msg) + nets = out.splitlines() + for host in domain: + try: + ip = socket.gethostbyname(host) + for l in nets: + x = l.strip() + if x.startswith("inet %s/" % ip): + return host + except socket.error: + pass + msg = _("Current host isn't part of HGST domain.") + raise exception.BrickException(message=msg) + + def _hostname(self): + """Returns hostname to use for cluster operations on this box.""" + if self._vgc_host is None: + self._vgc_host = self._find_vgc_host() + return self._vgc_host + + def get_search_path(self): + return "/dev" + + def get_volume_paths(self, connection_properties): + path = ("%(path)s/%(name)s" % + {'path': self.get_search_path(), + 'name': connection_properties['name']}) + volume_path = None + if os.path.exists(path): + volume_path = path + return [volume_path] + + @utils.trace + def connect_volume(self, connection_properties): + """Attach a Space volume to running host. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + connection_properties for HGST must include: + name - Name of space to attach + :type connection_properties: dict + :returns: dict + """ + if connection_properties is None: + msg = _("Connection properties passed in as None.") + raise exception.BrickException(message=msg) + if 'name' not in connection_properties: + msg = _("Connection properties missing 'name' field.") + raise exception.BrickException(message=msg) + device_info = { + 'type': 'block', + 'device': connection_properties['name'], + 'path': '/dev/' + connection_properties['name'] + } + volname = device_info['device'] + params = [self.VGCCLUSTER, 'space-set-apphosts'] + params += ['-n', volname] + params += ['-A', self._hostname()] + params += ['--action', 'ADD'] + try: + self._execute(*params, run_as_root=True, + root_helper=self._root_helper) + except putils.ProcessExecutionError as err: + self._log_cli_err(err) + msg = (_("Unable to set apphost for space %s") % volname) + raise exception.BrickException(message=msg) + + return device_info + + @utils.trace + def disconnect_volume(self, connection_properties, device_info): + """Detach and flush the volume. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + For HGST must include: + name - Name of space to detach + noremovehost - Host which should never be removed + :type connection_properties: dict + :param device_info: historical difference, but same as connection_props + :type device_info: dict + """ + if connection_properties is None: + msg = _("Connection properties passed in as None.") + raise exception.BrickException(message=msg) + if 'name' not in connection_properties: + msg = _("Connection properties missing 'name' field.") + raise exception.BrickException(message=msg) + if 'noremovehost' not in connection_properties: + msg = _("Connection properties missing 'noremovehost' field.") + raise exception.BrickException(message=msg) + if connection_properties['noremovehost'] != self._hostname(): + params = [self.VGCCLUSTER, 'space-set-apphosts'] + params += ['-n', connection_properties['name']] + params += ['-A', self._hostname()] + params += ['--action', 'DELETE'] + try: + self._execute(*params, run_as_root=True, + root_helper=self._root_helper) + except putils.ProcessExecutionError as err: + self._log_cli_err(err) + msg = (_("Unable to set apphost for space %s") % + connection_properties['name']) + raise exception.BrickException(message=msg) + + def extend_volume(self, connection_properties): + # TODO(walter-boring): is this possible? + raise NotImplementedError diff --git a/os_brick/initiator/connectors/huawei.py b/os_brick/initiator/connectors/huawei.py new file mode 100644 index 000000000..8866dd18d --- /dev/null +++ b/os_brick/initiator/connectors/huawei.py @@ -0,0 +1,192 @@ +# All Rights Reserved. +# +# 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. + +import os + +from oslo_concurrency import lockutils +from oslo_log import log as logging + +from os_brick.i18n import _, _LE +from os_brick import exception +from os_brick.initiator.connectors import base +from os_brick import utils + +LOG = logging.getLogger(__name__) +synchronized = lockutils.synchronized_with_prefix('os-brick-') + + +class HuaweiStorHyperConnector(base.BaseLinuxConnector): + """"Connector class to attach/detach SDSHypervisor volumes.""" + + attached_success_code = 0 + has_been_attached_code = 50151401 + attach_mnid_done_code = 50151405 + vbs_unnormal_code = 50151209 + not_mount_node_code = 50155007 + iscliexist = True + + def __init__(self, root_helper, driver=None, + *args, **kwargs): + self.cli_path = os.getenv('HUAWEISDSHYPERVISORCLI_PATH') + if not self.cli_path: + self.cli_path = '/usr/local/bin/sds/sds_cli' + LOG.debug("CLI path is not configured, using default %s.", + self.cli_path) + if not os.path.isfile(self.cli_path): + self.iscliexist = False + LOG.error(_LE('SDS CLI file not found, ' + 'HuaweiStorHyperConnector init failed.')) + super(HuaweiStorHyperConnector, self).__init__(root_helper, + driver=driver, + *args, **kwargs) + + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The HuaweiStor connector properties.""" + return {} + + def get_search_path(self): + # TODO(walter-boring): Where is the location on the filesystem to + # look for Huawei volumes to show up? + return None + + def get_all_available_volumes(self, connection_properties=None): + # TODO(walter-boring): what to return here for all Huawei volumes ? + return [] + + def get_volume_paths(self, connection_properties): + volume_path = None + try: + volume_path = self._get_volume_path(connection_properties) + except Exception: + msg = _("Couldn't find a volume.") + LOG.warning(msg) + raise exception.BrickException(message=msg) + return [volume_path] + + def _get_volume_path(self, connection_properties): + out = self._query_attached_volume( + connection_properties['volume_id']) + if not out or int(out['ret_code']) != 0: + msg = _("Couldn't find attached volume.") + LOG.error(msg) + raise exception.BrickException(message=msg) + return out['dev_addr'] + + @utils.trace + @synchronized('connect_volume') + def connect_volume(self, connection_properties): + """Connect to a volume. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :returns: dict + """ + LOG.debug("Connect_volume connection properties: %s.", + connection_properties) + out = self._attach_volume(connection_properties['volume_id']) + if not out or int(out['ret_code']) not in (self.attached_success_code, + self.has_been_attached_code, + self.attach_mnid_done_code): + msg = (_("Attach volume failed, " + "error code is %s") % out['ret_code']) + raise exception.BrickException(message=msg) + + try: + volume_path = self._get_volume_path(connection_properties) + except Exception: + msg = _("query attached volume failed or volume not attached.") + LOG.error(msg) + raise exception.BrickException(message=msg) + + device_info = {'type': 'block', + 'path': volume_path} + return device_info + + @utils.trace + @synchronized('connect_volume') + def disconnect_volume(self, connection_properties, device_info): + """Disconnect a volume from the local host. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :param device_info: historical difference, but same as connection_props + :type device_info: dict + """ + LOG.debug("Disconnect_volume: %s.", connection_properties) + out = self._detach_volume(connection_properties['volume_id']) + if not out or int(out['ret_code']) not in (self.attached_success_code, + self.vbs_unnormal_code, + self.not_mount_node_code): + msg = (_("Disconnect_volume failed, " + "error code is %s") % out['ret_code']) + raise exception.BrickException(message=msg) + + def is_volume_connected(self, volume_name): + """Check if volume already connected to host""" + LOG.debug('Check if volume %s already connected to a host.', + volume_name) + out = self._query_attached_volume(volume_name) + if out: + return int(out['ret_code']) == 0 + return False + + def _attach_volume(self, volume_name): + return self._cli_cmd('attach', volume_name) + + def _detach_volume(self, volume_name): + return self._cli_cmd('detach', volume_name) + + def _query_attached_volume(self, volume_name): + return self._cli_cmd('querydev', volume_name) + + def _cli_cmd(self, method, volume_name): + LOG.debug("Enter into _cli_cmd.") + if not self.iscliexist: + msg = _("SDS command line doesn't exist, " + "can't execute SDS command.") + raise exception.BrickException(message=msg) + if not method or volume_name is None: + return + cmd = [self.cli_path, '-c', method, '-v', volume_name] + out, clilog = self._execute(*cmd, run_as_root=False, + root_helper=self._root_helper) + analyse_result = self._analyze_output(out) + LOG.debug('%(method)s volume returns %(analyse_result)s.', + {'method': method, 'analyse_result': analyse_result}) + if clilog: + LOG.error(_LE("SDS CLI output some log: %s."), clilog) + return analyse_result + + def _analyze_output(self, out): + LOG.debug("Enter into _analyze_output.") + if out: + analyse_result = {} + out_temp = out.split('\n') + for line in out_temp: + LOG.debug("Line is %s.", line) + if line.find('=') != -1: + key, val = line.split('=', 1) + LOG.debug("%(key)s = %(val)s", {'key': key, 'val': val}) + if key in ['ret_code', 'ret_desc', 'dev_addr']: + analyse_result[key] = val + return analyse_result + else: + return None + + def extend_volume(self, connection_properties): + # TODO(walter-boring): is this possible? + raise NotImplementedError diff --git a/os_brick/initiator/connectors/iscsi.py b/os_brick/initiator/connectors/iscsi.py new file mode 100644 index 000000000..4245c3b1e --- /dev/null +++ b/os_brick/initiator/connectors/iscsi.py @@ -0,0 +1,832 @@ +# All Rights Reserved. +# +# 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. + + +import copy +import glob +import os +import re +import time + +from oslo_concurrency import lockutils +from oslo_concurrency import processutils as putils +from oslo_log import log as logging +from oslo_utils import strutils + +from os_brick.i18n import _, _LE, _LI, _LW +from os_brick import exception +from os_brick import initiator +from os_brick.initiator.connectors import base +from os_brick.initiator.connectors import base_iscsi +from os_brick import utils + +synchronized = lockutils.synchronized_with_prefix('os-brick-') + +LOG = logging.getLogger(__name__) + + +class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector): + """Connector class to attach/detach iSCSI volumes.""" + + supported_transports = ['be2iscsi', 'bnx2i', 'cxgb3i', 'default', + 'cxgb4i', 'qla4xxx', 'ocs', 'iser'] + + def __init__(self, root_helper, driver=None, + execute=None, use_multipath=False, + device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT, + transport='default', *args, **kwargs): + super(ISCSIConnector, self).__init__( + root_helper, driver=driver, + execute=execute, + device_scan_attempts=device_scan_attempts, + transport=transport, *args, **kwargs) + self.use_multipath = use_multipath + self.transport = self._validate_iface_transport(transport) + + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The iSCSI connector properties.""" + props = {} + iscsi = ISCSIConnector(root_helper=root_helper, + execute=kwargs.get('execute')) + initiator = iscsi.get_initiator() + if initiator: + props['initiator'] = initiator + + return props + + def get_search_path(self): + """Where do we look for iSCSI based volumes.""" + return '/dev/disk/by-path' + + def get_volume_paths(self, connection_properties): + """Get the list of existing paths for a volume. + + This method's job is to simply report what might/should + already exist for a volume. We aren't trying to attach/discover + a new volume, but find any existing paths for a volume we + think is already attached. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + """ + volume_paths = [] + + # if there are no sessions, then target_portal won't exist + if (('target_portal' not in connection_properties) and + ('target_portals' not in connection_properties)): + return volume_paths + + # Don't try and connect to the portals in the list as + # this can create empty iSCSI sessions to hosts if they + # didn't exist previously. + # We are simply trying to find any existing volumes with + # already connected sessions. + host_devices, target_props = self._get_potential_volume_paths( + connection_properties, + connect_to_portal=False, + use_rescan=False) + + for path in host_devices: + if os.path.exists(path): + volume_paths.append(path) + + return volume_paths + + def _get_iscsi_sessions(self): + out, err = self._run_iscsi_session() + + iscsi_sessions = [] + + if err: + LOG.warning(_LW("Couldn't find iscsi sessions because " + "iscsiadm err: %s"), + err) + else: + # parse the output from iscsiadm + # lines are in the format of + # tcp: [1] 192.168.121.250:3260,1 iqn.2010-10.org.openstack:volume- + lines = out.split('\n') + for line in lines: + if line: + entries = line.split() + portal = entries[2].split(',') + iscsi_sessions.append(portal[0]) + + return iscsi_sessions + + def _get_potential_volume_paths(self, connection_properties, + connect_to_portal=True, + use_rescan=True): + """Build a list of potential volume paths that exist. + + Given a list of target_portals in the connection_properties, + a list of paths might exist on the system during discovery. + This method's job is to build that list of potential paths + for a volume that might show up. + + This is used during connect_volume time, in which case we want + to connect to the iSCSI target portal. + + During get_volume_paths time, we are looking to + find a list of existing volume paths for the connection_properties. + In this case, we don't want to connect to the portal. If we + blindly try and connect to a portal, it could create a new iSCSI + session that didn't exist previously, and then leave it stale. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :param connect_to_portal: Do we want to try a new connection to the + target portal(s)? Set this to False if you + want to search for existing volumes, not + discover new volumes. + :param connect_to_portal: bool + :param use_rescan: Issue iSCSI rescan during discovery? + :type use_rescan: bool + :returns: dict + """ + + target_props = None + connected_to_portal = False + if self.use_multipath: + LOG.info(_LI("Multipath discovery for iSCSI enabled")) + # Multipath installed, discovering other targets if available + try: + ips_iqns = self._discover_iscsi_portals(connection_properties) + except Exception: + if 'target_portals' in connection_properties: + raise exception.TargetPortalsNotFound( + target_portal=connection_properties['target_portals']) + elif 'target_portal' in connection_properties: + raise exception.TargetPortalNotFound( + target_portal=connection_properties['target_portal']) + else: + raise + + if not connection_properties.get('target_iqns'): + # There are two types of iSCSI multipath devices. One which + # shares the same iqn between multiple portals, and the other + # which use different iqns on different portals. + # Try to identify the type by checking the iscsiadm output + # if the iqn is used by multiple portals. If it is, it's + # the former, so use the supplied iqn. Otherwise, it's the + # latter, so try the ip,iqn combinations to find the targets + # which constitutes the multipath device. + main_iqn = connection_properties['target_iqn'] + all_portals = set([ip for ip, iqn in ips_iqns]) + match_portals = set([ip for ip, iqn in ips_iqns + if iqn == main_iqn]) + if len(all_portals) == len(match_portals): + ips_iqns = zip(all_portals, [main_iqn] * len(all_portals)) + + for ip, iqn in ips_iqns: + props = copy.deepcopy(connection_properties) + props['target_portal'] = ip + props['target_iqn'] = iqn + if connect_to_portal: + if self._connect_to_iscsi_portal(props): + connected_to_portal = True + + if use_rescan: + self._rescan_iscsi() + host_devices = self._get_device_path(connection_properties) + else: + LOG.info(_LI("Multipath discovery for iSCSI not enabled.")) + iscsi_sessions = [] + if not connect_to_portal: + iscsi_sessions = self._get_iscsi_sessions() + + host_devices = [] + target_props = connection_properties + for props in self._iterate_all_targets(connection_properties): + if connect_to_portal: + if self._connect_to_iscsi_portal(props): + target_props = props + connected_to_portal = True + host_devices = self._get_device_path(props) + break + else: + LOG.warning(_LW( + 'Failed to connect to iSCSI portal %(portal)s.'), + {'portal': props['target_portal']}) + else: + # If we aren't trying to connect to the portal, we + # want to find ALL possible paths from all of the + # alternate portals + if props['target_portal'] in iscsi_sessions: + paths = self._get_device_path(props) + host_devices = list(set(paths + host_devices)) + + if connect_to_portal and not connected_to_portal: + msg = _("Could not login to any iSCSI portal.") + LOG.error(msg) + raise exception.FailedISCSITargetPortalLogin(message=msg) + + return host_devices, target_props + + def set_execute(self, execute): + super(ISCSIConnector, self).set_execute(execute) + self._linuxscsi.set_execute(execute) + + def _validate_iface_transport(self, transport_iface): + """Check that given iscsi_iface uses only supported transports + + Accepted transport names for provided iface param are + be2iscsi, bnx2i, cxgb3i, cxgb4i, default, qla4xxx, ocs or iser. + Note the difference between transport and iface; + unlike default(iscsi_tcp)/iser, this is not one and the same for + offloaded transports, where the default format is + transport_name.hwaddress + + :param transport_iface: The iscsi transport type. + :type transport_iface: str + :returns: str + """ + # Note that default(iscsi_tcp) and iser do not require a separate + # iface file, just the transport is enough and do not need to be + # validated. This is not the case for the other entries in + # supported_transports array. + if transport_iface in ['default', 'iser']: + return transport_iface + # Will return (6) if iscsi_iface file was not found, or (2) if iscsid + # could not be contacted + out = self._run_iscsiadm_bare(['-m', + 'iface', + '-I', + transport_iface], + check_exit_code=[0, 2, 6])[0] or "" + LOG.debug("iscsiadm %(iface)s configuration: stdout=%(out)s.", + {'iface': transport_iface, 'out': out}) + for data in [line.split() for line in out.splitlines()]: + if data[0] == 'iface.transport_name': + if data[2] in self.supported_transports: + return transport_iface + + LOG.warning(_LW("No useable transport found for iscsi iface %s. " + "Falling back to default transport."), + transport_iface) + return 'default' + + def _get_transport(self): + return self.transport + + def _discover_iscsi_portals(self, connection_properties): + if all([key in connection_properties for key in ('target_portals', + 'target_iqns')]): + # Use targets specified by connection_properties + return zip(connection_properties['target_portals'], + connection_properties['target_iqns']) + + out = None + if connection_properties.get('discovery_auth_method'): + try: + self._run_iscsiadm_update_discoverydb(connection_properties) + except putils.ProcessExecutionError as exception: + # iscsiadm returns 6 for "db record not found" + if exception.exit_code == 6: + # Create a new record for this target and update the db + self._run_iscsiadm_bare( + ['-m', 'discoverydb', + '-t', 'sendtargets', + '-p', connection_properties['target_portal'], + '--op', 'new'], + check_exit_code=[0, 255]) + self._run_iscsiadm_update_discoverydb( + connection_properties + ) + else: + LOG.error(_LE("Unable to find target portal: " + "%(target_portal)s."), + {'target_portal': connection_properties[ + 'target_portal']}) + raise + out = self._run_iscsiadm_bare( + ['-m', 'discoverydb', + '-t', 'sendtargets', + '-p', connection_properties['target_portal'], + '--discover'], + check_exit_code=[0, 255])[0] or "" + else: + out = self._run_iscsiadm_bare( + ['-m', 'discovery', + '-t', 'sendtargets', + '-p', connection_properties['target_portal']], + check_exit_code=[0, 255])[0] or "" + + return self._get_target_portals_from_iscsiadm_output(out) + + def _run_iscsiadm_update_discoverydb(self, connection_properties): + return self._execute( + 'iscsiadm', + '-m', 'discoverydb', + '-t', 'sendtargets', + '-p', connection_properties['target_portal'], + '--op', 'update', + '-n', "discovery.sendtargets.auth.authmethod", + '-v', connection_properties['discovery_auth_method'], + '-n', "discovery.sendtargets.auth.username", + '-v', connection_properties['discovery_auth_username'], + '-n', "discovery.sendtargets.auth.password", + '-v', connection_properties['discovery_auth_password'], + run_as_root=True, + root_helper=self._root_helper) + + @utils.trace + @synchronized('extend_volume') + def extend_volume(self, connection_properties): + """Update the local kernel's size information. + + Try and update the local kernel's size information + for an iSCSI volume. + """ + LOG.info(_LI("Extend volume for %s"), connection_properties) + + volume_paths = self.get_volume_paths(connection_properties) + LOG.info(_LI("Found paths for volume %s"), volume_paths) + if volume_paths: + return self._linuxscsi.extend_volume(volume_paths[0]) + else: + LOG.warning(_LW("Couldn't find any volume paths on the host to " + "extend volume for %(props)s"), + {'props': connection_properties}) + raise exception.VolumePathsNotFound() + + @utils.trace + @synchronized('connect_volume') + def connect_volume(self, connection_properties): + """Attach the volume to instance_name. + + :param connection_properties: The valid dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :returns: dict + + connection_properties for iSCSI must include: + target_portal(s) - ip and optional port + target_iqn(s) - iSCSI Qualified Name + target_lun(s) - LUN id of the volume + Note that plural keys may be used when use_multipath=True + """ + + device_info = {'type': 'block'} + + host_devices, target_props = self._get_potential_volume_paths( + connection_properties) + + # The /dev/disk/by-path/... node is not always present immediately + # TODO(justinsb): This retry-with-delay is a pattern, move to utils? + tries = 0 + # Loop until at least 1 path becomes available + while all(map(lambda x: not os.path.exists(x), host_devices)): + if tries >= self.device_scan_attempts: + raise exception.VolumeDeviceNotFound(device=host_devices) + + LOG.warning(_LW("ISCSI volume not yet found at: %(host_devices)s. " + "Will rescan & retry. Try number: %(tries)s."), + {'host_devices': host_devices, + 'tries': tries}) + + # The rescan isn't documented as being necessary(?), but it helps + if self.use_multipath: + self._rescan_iscsi() + else: + if (tries): + host_devices = self._get_device_path(target_props) + self._run_iscsiadm(target_props, ("--rescan",)) + + tries = tries + 1 + if all(map(lambda x: not os.path.exists(x), host_devices)): + time.sleep(tries ** 2) + else: + break + + if tries != 0: + LOG.debug("Found iSCSI node %(host_devices)s " + "(after %(tries)s rescans)", + {'host_devices': host_devices, 'tries': tries}) + + # Choose an accessible host device + host_device = next(dev for dev in host_devices if os.path.exists(dev)) + + # find out the WWN of the device + device_wwn = self._linuxscsi.get_scsi_wwn(host_device) + LOG.debug("Device WWN = '%(wwn)s'", {'wwn': device_wwn}) + device_info['scsi_wwn'] = device_wwn + + if self.use_multipath: + (host_device, multipath_id) = (super( + ISCSIConnector, self)._discover_mpath_device( + device_wwn, connection_properties, host_device)) + if multipath_id: + device_info['multipath_id'] = multipath_id + + device_info['path'] = host_device + + LOG.debug("connect_volume returning %s", device_info) + return device_info + + @utils.trace + @synchronized('connect_volume') + def disconnect_volume(self, connection_properties, device_info): + """Detach the volume from instance_name. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :param device_info: historical difference, but same as connection_props + :type device_info: dict + + connection_properties for iSCSI must include: + target_portal(s) - IP and optional port + target_iqn(s) - iSCSI Qualified Name + target_lun(s) - LUN id of the volume + """ + if self.use_multipath: + self._rescan_multipath() + host_device = multipath_device = None + host_devices = self._get_device_path(connection_properties) + # Choose an accessible host device + for dev in host_devices: + if os.path.exists(dev): + host_device = dev + device_wwn = self._linuxscsi.get_scsi_wwn(dev) + (multipath_device, multipath_id) = (super( + ISCSIConnector, self)._discover_mpath_device( + device_wwn, connection_properties, dev)) + if multipath_device: + break + if not host_device: + LOG.error(_LE("No accessible volume device: %(host_devices)s"), + {'host_devices': host_devices}) + raise exception.VolumeDeviceNotFound(device=host_devices) + + if multipath_device: + device_realpath = os.path.realpath(host_device) + self._linuxscsi.remove_multipath_device(device_realpath) + return self._disconnect_volume_multipath_iscsi( + connection_properties, multipath_device) + + # When multiple portals/iqns/luns are specified, we need to remove + # unused devices created by logging into other LUNs' session. + for props in self._iterate_all_targets(connection_properties): + self._disconnect_volume_iscsi(props) + + def _disconnect_volume_iscsi(self, connection_properties): + # remove the device from the scsi subsystem + # this eliminates any stale entries until logout + host_devices = self._get_device_path(connection_properties) + + if host_devices: + host_device = host_devices[0] + else: + return + + dev_name = self._linuxscsi.get_name_from_path(host_device) + if dev_name: + self._linuxscsi.remove_scsi_device(dev_name) + + # NOTE(jdg): On busy systems we can have a race here + # where remove_iscsi_device is called before the device file + # has actually been removed. The result is an orphaned + # iscsi session that never gets logged out. The following + # call to wait addresses that issue. + self._linuxscsi.wait_for_volume_removal(host_device) + + # NOTE(vish): Only disconnect from the target if no luns from the + # target are in use. + device_byname = ("ip-%(portal)s-iscsi-%(iqn)s-lun-" % + {'portal': connection_properties['target_portal'], + 'iqn': connection_properties['target_iqn']}) + devices = self.driver.get_all_block_devices() + devices = [dev for dev in devices if (device_byname in dev + and + dev.startswith( + '/dev/disk/by-path/')) + and os.path.exists(dev)] + if not devices: + self._disconnect_from_iscsi_portal(connection_properties) + + def _munge_portal(self, target): + """Remove brackets from portal. + + In case IPv6 address was used the udev path should not contain any + brackets. Udev code specifically forbids that. + """ + portal, iqn, lun = target + return (portal.replace('[', '').replace(']', ''), iqn, + self._linuxscsi.process_lun_id(lun)) + + def _get_device_path(self, connection_properties): + if self._get_transport() == "default": + return ["/dev/disk/by-path/ip-%s-iscsi-%s-lun-%s" % + self._munge_portal(x) for x in + self._get_all_targets(connection_properties)] + else: + # we are looking for paths in the format : + # /dev/disk/by-path/ + # pci-XXXX:XX:XX.X-ip-PORTAL:PORT-iscsi-IQN-lun-LUN_ID + device_list = [] + for x in self._get_all_targets(connection_properties): + look_for_device = glob.glob( + '/dev/disk/by-path/*ip-%s-iscsi-%s-lun-%s' % + self._munge_portal(x)) + if look_for_device: + device_list.extend(look_for_device) + return device_list + + def get_initiator(self): + """Secure helper to read file as root.""" + file_path = '/etc/iscsi/initiatorname.iscsi' + try: + lines, _err = self._execute('cat', file_path, run_as_root=True, + root_helper=self._root_helper) + + for l in lines.split('\n'): + if l.startswith('InitiatorName='): + return l[l.index('=') + 1:].strip() + except putils.ProcessExecutionError: + LOG.warning(_LW("Could not find the iSCSI Initiator File %s"), + file_path) + return None + + def _run_iscsiadm(self, connection_properties, iscsi_command, **kwargs): + check_exit_code = kwargs.pop('check_exit_code', 0) + attempts = kwargs.pop('attempts', 1) + delay_on_retry = kwargs.pop('delay_on_retry', True) + (out, err) = self._execute('iscsiadm', '-m', 'node', '-T', + connection_properties['target_iqn'], + '-p', + connection_properties['target_portal'], + *iscsi_command, run_as_root=True, + root_helper=self._root_helper, + check_exit_code=check_exit_code, + attempts=attempts, + delay_on_retry=delay_on_retry) + msg = ("iscsiadm %(iscsi_command)s: stdout=%(out)s stderr=%(err)s" % + {'iscsi_command': iscsi_command, 'out': out, 'err': err}) + # don't let passwords be shown in log output + LOG.debug(strutils.mask_password(msg)) + + return (out, err) + + def _iscsiadm_update(self, connection_properties, property_key, + property_value, **kwargs): + iscsi_command = ('--op', 'update', '-n', property_key, + '-v', property_value) + return self._run_iscsiadm(connection_properties, iscsi_command, + **kwargs) + + def _get_target_portals_from_iscsiadm_output(self, output): + # return both portals and iqns + # + # as we are parsing a command line utility, allow for the + # possibility that additional debug data is spewed in the + # stream, and only grab actual ip / iqn lines. + targets = [] + for data in [line.split() for line in output.splitlines()]: + if len(data) == 2 and data[1].startswith('iqn.'): + targets.append(data) + return targets + + def _disconnect_volume_multipath_iscsi(self, connection_properties, + multipath_name): + """This removes a multipath device and it's LUNs.""" + LOG.debug("Disconnect multipath device %s", multipath_name) + mpath_map = self._get_multipath_device_map() + block_devices = self.driver.get_all_block_devices() + devices = [] + for dev in block_devices: + if os.path.exists(dev): + if "/mapper/" in dev: + devices.append(dev) + else: + mpdev = mpath_map.get(dev) + if mpdev: + devices.append(mpdev) + + # Do a discovery to find all targets. + # Targets for multiple paths for the same multipath device + # may not be the same. + all_ips_iqns = self._discover_iscsi_portals(connection_properties) + + # As discovery result may contain other targets' iqns, extract targets + # to be disconnected whose block devices are already deleted here. + ips_iqns = [] + entries = [device.lstrip('ip-').split('-lun-')[0] + for device in self._get_iscsi_devices()] + for ip, iqn in all_ips_iqns: + ip_iqn = "%s-iscsi-%s" % (ip.split(",")[0], iqn) + if ip_iqn not in entries: + ips_iqns.append([ip, iqn]) + + if not devices: + # disconnect if no other multipath devices + self._disconnect_mpath(connection_properties, ips_iqns) + return + + # Get a target for all other multipath devices + other_iqns = self._get_multipath_iqns(devices, mpath_map) + + # Get all the targets for the current multipath device + current_iqns = [iqn for ip, iqn in ips_iqns] + + in_use = False + for current in current_iqns: + if current in other_iqns: + in_use = True + break + + # If no other multipath device attached has the same iqn + # as the current device + if not in_use: + # disconnect if no other multipath devices with same iqn + self._disconnect_mpath(connection_properties, ips_iqns) + return + + # else do not disconnect iscsi portals, + # as they are used for other luns + return + + def _connect_to_iscsi_portal(self, connection_properties): + # NOTE(vish): If we are on the same host as nova volume, the + # discovery makes the target so we don't need to + # run --op new. Therefore, we check to see if the + # target exists, and if we get 255 (Not Found), then + # we run --op new. This will also happen if another + # volume is using the same target. + LOG.info(_LI("Trying to connect to iSCSI portal %(portal)s"), + {"portal": connection_properties['target_portal']}) + try: + self._run_iscsiadm(connection_properties, ()) + except putils.ProcessExecutionError as exc: + # iscsiadm returns 21 for "No records found" after version 2.0-871 + if exc.exit_code in [21, 255]: + self._run_iscsiadm(connection_properties, + ('--interface', self._get_transport(), + '--op', 'new')) + else: + raise + + if connection_properties.get('auth_method'): + self._iscsiadm_update(connection_properties, + "node.session.auth.authmethod", + connection_properties['auth_method']) + self._iscsiadm_update(connection_properties, + "node.session.auth.username", + connection_properties['auth_username']) + self._iscsiadm_update(connection_properties, + "node.session.auth.password", + connection_properties['auth_password']) + + # Duplicate logins crash iscsiadm after load, + # so we scan active sessions to see if the node is logged in. + out = self._run_iscsiadm_bare(["-m", "session"], + run_as_root=True, + check_exit_code=[0, 1, 21])[0] or "" + + portals = [{'portal': p.split(" ")[2], 'iqn': p.split(" ")[3]} + for p in out.splitlines() if p.startswith("tcp:")] + + stripped_portal = connection_properties['target_portal'].split(",")[0] + if len(portals) == 0 or len([s for s in portals + if stripped_portal == + s['portal'].split(",")[0] + and + s['iqn'] == + connection_properties['target_iqn']] + ) == 0: + try: + self._run_iscsiadm(connection_properties, + ("--login",), + check_exit_code=[0, 255]) + except putils.ProcessExecutionError as err: + # exit_code=15 means the session already exists, so it should + # be regarded as successful login. + if err.exit_code not in [15]: + LOG.warning(_LW('Failed to login iSCSI target %(iqn)s ' + 'on portal %(portal)s (exit code ' + '%(err)s).'), + {'iqn': connection_properties['target_iqn'], + 'portal': connection_properties[ + 'target_portal'], + 'err': err.exit_code}) + return False + + self._iscsiadm_update(connection_properties, + "node.startup", + "automatic") + return True + + def _disconnect_from_iscsi_portal(self, connection_properties): + self._iscsiadm_update(connection_properties, "node.startup", "manual", + check_exit_code=[0, 21, 255]) + self._run_iscsiadm(connection_properties, ("--logout",), + check_exit_code=[0, 21, 255]) + self._run_iscsiadm(connection_properties, ('--op', 'delete'), + check_exit_code=[0, 21, 255], + attempts=5, + delay_on_retry=True) + + def _get_iscsi_devices(self): + try: + devices = list(os.walk('/dev/disk/by-path'))[0][-1] + except IndexError: + return [] + # For iSCSI HBAs, look at an offset of len('pci-0000:00:00.0') + return [entry for entry in devices if (entry.startswith("ip-") + or (entry.startswith("pci-") + and + entry.find("ip-", 16, 21) + >= 16))] + + def _disconnect_mpath(self, connection_properties, ips_iqns): + for ip, iqn in ips_iqns: + props = copy.deepcopy(connection_properties) + props['target_portal'] = ip + props['target_iqn'] = iqn + self._disconnect_from_iscsi_portal(props) + + self._rescan_multipath() + + def _get_multipath_iqns(self, multipath_devices, mpath_map): + entries = self._get_iscsi_devices() + iqns = [] + for entry in entries: + entry_real_path = os.path.realpath("/dev/disk/by-path/%s" % entry) + entry_multipath = mpath_map.get(entry_real_path) + if entry_multipath and entry_multipath in multipath_devices: + iqns.append(entry.split("iscsi-")[1].split("-lun")[0]) + return iqns + + def _get_multipath_device_map(self): + out = self._run_multipath(['-ll'], check_exit_code=[0, 1])[0] + mpath_line = [line for line in out.splitlines() + if not re.match(initiator.MULTIPATH_ERROR_REGEX, line)] + mpath_dev = None + mpath_map = {} + for line in out.splitlines(): + m = initiator.MULTIPATH_DEV_CHECK_REGEX.split(line) + if len(m) >= 2: + mpath_dev = '/dev/mapper/' + m[0].split(" ")[0] + continue + m = initiator.MULTIPATH_PATH_CHECK_REGEX.split(line) + if len(m) >= 2: + mpath_map['/dev/' + m[1].split(" ")[0]] = mpath_dev + + if mpath_line and not mpath_map: + LOG.warning(_LW("Failed to parse the output of multipath -ll. " + "stdout: %s"), out) + return mpath_map + + def _run_iscsi_session(self): + (out, err) = self._run_iscsiadm_bare(('-m', 'session'), + check_exit_code=[0, 1, 21, 255]) + LOG.debug("iscsi session list stdout=%(out)s stderr=%(err)s", + {'out': out, 'err': err}) + return (out, err) + + def _run_iscsiadm_bare(self, iscsi_command, **kwargs): + check_exit_code = kwargs.pop('check_exit_code', 0) + (out, err) = self._execute('iscsiadm', + *iscsi_command, + run_as_root=True, + root_helper=self._root_helper, + check_exit_code=check_exit_code) + LOG.debug("iscsiadm %(iscsi_command)s: stdout=%(out)s stderr=%(err)s", + {'iscsi_command': iscsi_command, 'out': out, 'err': err}) + return (out, err) + + def _run_multipath(self, multipath_command, **kwargs): + check_exit_code = kwargs.pop('check_exit_code', 0) + (out, err) = self._execute('multipath', + *multipath_command, + run_as_root=True, + root_helper=self._root_helper, + check_exit_code=check_exit_code) + LOG.debug("multipath %(multipath_command)s: " + "stdout=%(out)s stderr=%(err)s", + {'multipath_command': multipath_command, + 'out': out, 'err': err}) + return (out, err) + + def _rescan_iscsi(self): + self._run_iscsiadm_bare(('-m', 'node', '--rescan'), + check_exit_code=[0, 1, 21, 255]) + self._run_iscsiadm_bare(('-m', 'session', '--rescan'), + check_exit_code=[0, 1, 21, 255]) + + def _rescan_multipath(self): + self._run_multipath(['-r'], check_exit_code=[0, 1, 21]) diff --git a/os_brick/initiator/connectors/local.py b/os_brick/initiator/connectors/local.py new file mode 100644 index 000000000..6b84a8e0c --- /dev/null +++ b/os_brick/initiator/connectors/local.py @@ -0,0 +1,78 @@ +# All Rights Reserved. +# +# 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. + +from os_brick.i18n import _ +from os_brick.initiator.connectors import base +from os_brick import utils + + +class LocalConnector(base.BaseLinuxConnector): + """"Connector class to attach/detach File System backed volumes.""" + + def __init__(self, root_helper, driver=None, + *args, **kwargs): + super(LocalConnector, self).__init__(root_helper, driver=driver, + *args, **kwargs) + + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The Local connector properties.""" + return {} + + def get_volume_paths(self, connection_properties): + path = connection_properties['device_path'] + return [path] + + def get_search_path(self): + return None + + def get_all_available_volumes(self, connection_properties=None): + # TODO(walter-boring): not sure what to return here. + return [] + + @utils.trace + def connect_volume(self, connection_properties): + """Connect to a volume. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + connection_properties must include: + device_path - path to the volume to be connected + :type connection_properties: dict + :returns: dict + """ + if 'device_path' not in connection_properties: + msg = (_("Invalid connection_properties specified " + "no device_path attribute")) + raise ValueError(msg) + + device_info = {'type': 'local', + 'path': connection_properties['device_path']} + return device_info + + @utils.trace + def disconnect_volume(self, connection_properties, device_info): + """Disconnect a volume from the local host. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :param device_info: historical difference, but same as connection_props + :type device_info: dict + """ + pass + + def extend_volume(self, connection_properties): + # TODO(walter-boring): is this possible? + raise NotImplementedError diff --git a/os_brick/initiator/connectors/rbd.py b/os_brick/initiator/connectors/rbd.py new file mode 100644 index 000000000..08b67e462 --- /dev/null +++ b/os_brick/initiator/connectors/rbd.py @@ -0,0 +1,166 @@ +# All Rights Reserved. +# +# 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. + +from oslo_concurrency import processutils as putils +from oslo_log import log as logging + +from os_brick.i18n import _, _LE +from os_brick import exception +from os_brick import initiator +from os_brick.initiator.connectors import base +from os_brick.initiator import linuxrbd +from os_brick import utils + +LOG = logging.getLogger(__name__) + + +class RBDConnector(base.BaseLinuxConnector): + """"Connector class to attach/detach RBD volumes.""" + + def __init__(self, root_helper, driver=None, use_multipath=False, + device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT, + *args, **kwargs): + + super(RBDConnector, self).__init__(root_helper, driver=driver, + device_scan_attempts= + device_scan_attempts, + *args, **kwargs) + self.do_local_attach = kwargs.get('do_local_attach', False) + + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The RBD connector properties.""" + return {'do_local_attach': kwargs.get('do_local_attach', False)} + + def get_volume_paths(self, connection_properties): + # TODO(e0ne): Implement this for local volume. + return [] + + def get_search_path(self): + # TODO(walter-boring): don't know where the connector + # looks for RBD volumes. + return None + + def get_all_available_volumes(self, connection_properties=None): + # TODO(e0ne): Implement this for local volume. + return [] + + def _get_rbd_handle(self, connection_properties): + try: + user = connection_properties['auth_username'] + pool, volume = connection_properties['name'].split('/') + conf = connection_properties.get('conffile') + except IndexError: + msg = _("Connect volume failed, malformed connection properties") + raise exception.BrickException(msg=msg) + + rbd_client = linuxrbd.RBDClient(user, pool) + rbd_volume = linuxrbd.RBDVolume(rbd_client, volume) + rbd_handle = linuxrbd.RBDVolumeIOWrapper( + linuxrbd.RBDImageMetadata(rbd_volume, pool, user, conf)) + return rbd_handle + + @staticmethod + def get_rbd_device_name(pool, volume): + """Return device name which will be generated by RBD kernel module. + + :param pool: RBD pool name. + :type pool: string + :param volume: RBD image name. + :type volume: string + """ + return '/dev/rbd/{pool}/{volume}'.format(pool=pool, volume=volume) + + @utils.trace + def connect_volume(self, connection_properties): + """Connect to a volume. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :returns: dict + """ + do_local_attach = connection_properties.get('do_local_attach', + self.do_local_attach) + + if do_local_attach: + # NOTE(e0ne): sanity check if ceph-common is installed. + cmd = ['which', 'rbd'] + try: + self._execute(*cmd) + except putils.ProcessExecutionError: + msg = _("ceph-common package is not installed.") + LOG.error(msg) + raise exception.BrickException(message=msg) + + # NOTE(e0ne): map volume to a block device + # via the rbd kernel module. + pool, volume = connection_properties['name'].split('/') + cmd = ['rbd', 'map', volume, '--pool', pool] + self._execute(*cmd, root_helper=self._root_helper, + run_as_root=True) + + return {'path': RBDConnector.get_rbd_device_name(pool, volume), + 'type': 'block'} + + rbd_handle = self._get_rbd_handle(connection_properties) + return {'path': rbd_handle} + + @utils.trace + def disconnect_volume(self, connection_properties, device_info): + """Disconnect a volume. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :param device_info: historical difference, but same as connection_props + :type device_info: dict + """ + do_local_attach = connection_properties.get('do_local_attach', + self.do_local_attach) + if do_local_attach: + pool, volume = connection_properties['name'].split('/') + dev_name = RBDConnector.get_rbd_device_name(pool, volume) + cmd = ['rbd', 'unmap', dev_name] + self._execute(*cmd, root_helper=self._root_helper, + run_as_root=True) + else: + if device_info: + rbd_handle = device_info.get('path', None) + if rbd_handle is not None: + rbd_handle.close() + + def check_valid_device(self, path, run_as_root=True): + """Verify an existing RBD handle is connected and valid.""" + rbd_handle = path + + if rbd_handle is None: + return False + + original_offset = rbd_handle.tell() + + try: + rbd_handle.read(4096) + except Exception as e: + LOG.error(_LE("Failed to access RBD device handle: %(error)s"), + {"error": e}) + return False + finally: + rbd_handle.seek(original_offset, 0) + + return True + + def extend_volume(self, connection_properties): + # TODO(walter-boring): is this possible? + raise NotImplementedError diff --git a/os_brick/initiator/connectors/remotefs.py b/os_brick/initiator/connectors/remotefs.py new file mode 100644 index 000000000..50f11c1ad --- /dev/null +++ b/os_brick/initiator/connectors/remotefs.py @@ -0,0 +1,119 @@ +# All Rights Reserved. +# +# 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. + +from oslo_log import log as logging + +from os_brick.i18n import _LW +from os_brick import initiator +from os_brick.initiator.connectors import base +from os_brick.remotefs import remotefs +from os_brick import utils + +LOG = logging.getLogger(__name__) + + +class RemoteFsConnector(base.BaseLinuxConnector): + """Connector class to attach/detach NFS and GlusterFS volumes.""" + + def __init__(self, mount_type, root_helper, driver=None, + execute=None, + device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT, + *args, **kwargs): + kwargs = kwargs or {} + conn = kwargs.get('conn') + mount_type_lower = mount_type.lower() + if conn: + mount_point_base = conn.get('mount_point_base') + if mount_type_lower in ('nfs', 'glusterfs', 'scality', + 'quobyte', 'vzstorage'): + kwargs[mount_type_lower + '_mount_point_base'] = ( + kwargs.get(mount_type_lower + '_mount_point_base') or + mount_point_base) + else: + LOG.warning(_LW("Connection details not present." + " RemoteFsClient may not initialize properly.")) + + if mount_type_lower == 'scality': + cls = remotefs.ScalityRemoteFsClient + else: + cls = remotefs.RemoteFsClient + self._remotefsclient = cls(mount_type, root_helper, execute=execute, + *args, **kwargs) + + super(RemoteFsConnector, self).__init__( + root_helper, driver=driver, + execute=execute, + device_scan_attempts=device_scan_attempts, + *args, **kwargs) + + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The RemoteFS connector properties.""" + return {} + + def set_execute(self, execute): + super(RemoteFsConnector, self).set_execute(execute) + self._remotefsclient.set_execute(execute) + + def get_search_path(self): + return self._remotefsclient.get_mount_base() + + def _get_volume_path(self, connection_properties): + mnt_flags = [] + if connection_properties.get('options'): + mnt_flags = connection_properties['options'].split() + + nfs_share = connection_properties['export'] + self._remotefsclient.mount(nfs_share, mnt_flags) + mount_point = self._remotefsclient.get_mount_point(nfs_share) + path = mount_point + '/' + connection_properties['name'] + return path + + def get_volume_paths(self, connection_properties): + path = self._get_volume_path(connection_properties) + return [path] + + @utils.trace + def connect_volume(self, connection_properties): + """Ensure that the filesystem containing the volume is mounted. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + connection_properties must include: + export - remote filesystem device (e.g. '172.18.194.100:/var/nfs') + name - file name within the filesystem + :type connection_properties: dict + :returns: dict + + + connection_properties may optionally include: + options - options to pass to mount + """ + path = self._get_volume_path(connection_properties) + return {'path': path} + + @utils.trace + def disconnect_volume(self, connection_properties, device_info): + """No need to do anything to disconnect a volume in a filesystem. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :param device_info: historical difference, but same as connection_props + :type device_info: dict + """ + + def extend_volume(self, connection_properties): + # TODO(walter-boring): is this possible? + raise NotImplementedError diff --git a/os_brick/initiator/connectors/scaleio.py b/os_brick/initiator/connectors/scaleio.py new file mode 100644 index 000000000..09ae68b63 --- /dev/null +++ b/os_brick/initiator/connectors/scaleio.py @@ -0,0 +1,491 @@ +# All Rights Reserved. +# +# 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. + +import json +import os +import requests +from six.moves import urllib + +from oslo_concurrency import lockutils +from oslo_concurrency import processutils as putils +from oslo_log import log as logging + +from os_brick.i18n import _, _LI, _LW +from os_brick import exception +from os_brick import initiator +from os_brick.initiator.connectors import base +from os_brick import utils + +LOG = logging.getLogger(__name__) +DEVICE_SCAN_ATTEMPTS_DEFAULT = 3 +synchronized = lockutils.synchronized_with_prefix('os-brick-') + + +class ScaleIOConnector(base.BaseLinuxConnector): + """Class implements the connector driver for ScaleIO.""" + + OK_STATUS_CODE = 200 + VOLUME_NOT_MAPPED_ERROR = 84 + VOLUME_ALREADY_MAPPED_ERROR = 81 + GET_GUID_CMD = ['/opt/emc/scaleio/sdc/bin/drv_cfg', '--query_guid'] + + def __init__(self, root_helper, driver=None, + device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT, + *args, **kwargs): + super(ScaleIOConnector, self).__init__( + root_helper, + driver=driver, + device_scan_attempts=device_scan_attempts, + *args, **kwargs + ) + + self.local_sdc_ip = None + self.server_ip = None + self.server_port = None + self.server_username = None + self.server_password = None + self.server_token = None + self.volume_id = None + self.volume_name = None + self.volume_path = None + self.iops_limit = None + self.bandwidth_limit = None + + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The ScaleIO connector properties.""" + return {} + + def get_search_path(self): + return "/dev/disk/by-id" + + def get_volume_paths(self, connection_properties): + self.get_config(connection_properties) + volume_paths = [] + device_paths = [self._find_volume_path()] + for path in device_paths: + if os.path.exists(path): + volume_paths.append(path) + return volume_paths + + def _find_volume_path(self): + LOG.info(_LI( + "Looking for volume %(volume_id)s, maximum tries: %(tries)s"), + {'volume_id': self.volume_id, 'tries': self.device_scan_attempts} + ) + + # look for the volume in /dev/disk/by-id directory + by_id_path = self.get_search_path() + + disk_filename = self._wait_for_volume_path(by_id_path) + full_disk_name = ("%(path)s/%(filename)s" % + {'path': by_id_path, 'filename': disk_filename}) + LOG.info(_LI("Full disk name is %(full_path)s"), + {'full_path': full_disk_name}) + return full_disk_name + + # NOTE: Usually 3 retries is enough to find the volume. + # If there are network issues, it could take much longer. Set + # the max retries to 15 to make sure we can find the volume. + @utils.retry(exceptions=exception.BrickException, + retries=15, + backoff_rate=1) + def _wait_for_volume_path(self, path): + if not os.path.isdir(path): + msg = ( + _("ScaleIO volume %(volume_id)s not found at " + "expected path.") % {'volume_id': self.volume_id} + ) + + LOG.debug(msg) + raise exception.BrickException(message=msg) + + disk_filename = None + filenames = os.listdir(path) + LOG.info(_LI( + "Files found in %(path)s path: %(files)s "), + {'path': path, 'files': filenames} + ) + + for filename in filenames: + if (filename.startswith("emc-vol") and + filename.endswith(self.volume_id)): + disk_filename = filename + break + + if not disk_filename: + msg = (_("ScaleIO volume %(volume_id)s not found.") % + {'volume_id': self.volume_id}) + LOG.debug(msg) + raise exception.BrickException(message=msg) + + return disk_filename + + def _get_client_id(self): + request = ( + "https://%(server_ip)s:%(server_port)s/" + "api/types/Client/instances/getByIp::%(sdc_ip)s/" % + { + 'server_ip': self.server_ip, + 'server_port': self.server_port, + 'sdc_ip': self.local_sdc_ip + } + ) + + LOG.info(_LI("ScaleIO get client id by ip request: %(request)s"), + {'request': request}) + + r = requests.get( + request, + auth=(self.server_username, self.server_token), + verify=False + ) + + r = self._check_response(r, request) + sdc_id = r.json() + if not sdc_id: + msg = (_("Client with ip %(sdc_ip)s was not found.") % + {'sdc_ip': self.local_sdc_ip}) + raise exception.BrickException(message=msg) + + if r.status_code != 200 and "errorCode" in sdc_id: + msg = (_("Error getting sdc id from ip %(sdc_ip)s: %(err)s") % + {'sdc_ip': self.local_sdc_ip, 'err': sdc_id['message']}) + + LOG.error(msg) + raise exception.BrickException(message=msg) + + LOG.info(_LI("ScaleIO sdc id is %(sdc_id)s."), + {'sdc_id': sdc_id}) + return sdc_id + + def _get_volume_id(self): + volname_encoded = urllib.parse.quote(self.volume_name, '') + volname_double_encoded = urllib.parse.quote(volname_encoded, '') + LOG.debug(_( + "Volume name after double encoding is %(volume_name)s."), + {'volume_name': volname_double_encoded} + ) + + request = ( + "https://%(server_ip)s:%(server_port)s/api/types/Volume/instances" + "/getByName::%(encoded_volume_name)s" % + { + 'server_ip': self.server_ip, + 'server_port': self.server_port, + 'encoded_volume_name': volname_double_encoded + } + ) + + LOG.info( + _LI("ScaleIO get volume id by name request: %(request)s"), + {'request': request} + ) + + r = requests.get(request, + auth=(self.server_username, self.server_token), + verify=False) + + r = self._check_response(r, request) + + volume_id = r.json() + if not volume_id: + msg = (_("Volume with name %(volume_name)s wasn't found.") % + {'volume_name': self.volume_name}) + + LOG.error(msg) + raise exception.BrickException(message=msg) + + if r.status_code != self.OK_STATUS_CODE and "errorCode" in volume_id: + msg = ( + _("Error getting volume id from name %(volume_name)s: " + "%(err)s") % + {'volume_name': self.volume_name, 'err': volume_id['message']} + ) + + LOG.error(msg) + raise exception.BrickException(message=msg) + + LOG.info(_LI("ScaleIO volume id is %(volume_id)s."), + {'volume_id': volume_id}) + return volume_id + + def _check_response(self, response, request, is_get_request=True, + params=None): + if response.status_code == 401 or response.status_code == 403: + LOG.info(_LI("Token is invalid, " + "going to re-login to get a new one")) + + login_request = ( + "https://%(server_ip)s:%(server_port)s/api/login" % + {'server_ip': self.server_ip, 'server_port': self.server_port} + ) + + r = requests.get( + login_request, + auth=(self.server_username, self.server_password), + verify=False + ) + + token = r.json() + # repeat request with valid token + LOG.debug(_("Going to perform request %(request)s again " + "with valid token"), {'request': request}) + + if is_get_request: + res = requests.get(request, + auth=(self.server_username, token), + verify=False) + else: + headers = {'content-type': 'application/json'} + res = requests.post( + request, + data=json.dumps(params), + headers=headers, + auth=(self.server_username, token), + verify=False + ) + + self.server_token = token + return res + + return response + + def get_config(self, connection_properties): + self.local_sdc_ip = connection_properties['hostIP'] + self.volume_name = connection_properties['scaleIO_volname'] + self.volume_id = connection_properties['scaleIO_volume_id'] + self.server_ip = connection_properties['serverIP'] + self.server_port = connection_properties['serverPort'] + self.server_username = connection_properties['serverUsername'] + self.server_password = connection_properties['serverPassword'] + self.server_token = connection_properties['serverToken'] + self.iops_limit = connection_properties['iopsLimit'] + self.bandwidth_limit = connection_properties['bandwidthLimit'] + device_info = {'type': 'block', + 'path': self.volume_path} + return device_info + + @utils.trace + @lockutils.synchronized('scaleio', 'scaleio-') + def connect_volume(self, connection_properties): + """Connect the volume. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :returns: dict + """ + device_info = self.get_config(connection_properties) + LOG.debug( + _( + "scaleIO Volume name: %(volume_name)s, SDC IP: %(sdc_ip)s, " + "REST Server IP: %(server_ip)s, " + "REST Server username: %(username)s, " + "iops limit:%(iops_limit)s, " + "bandwidth limit: %(bandwidth_limit)s." + ), { + 'volume_name': self.volume_name, + 'volume_id': self.volume_id, + 'sdc_ip': self.local_sdc_ip, + 'server_ip': self.server_ip, + 'username': self.server_username, + 'iops_limit': self.iops_limit, + 'bandwidth_limit': self.bandwidth_limit + } + ) + + LOG.info(_LI("ScaleIO sdc query guid command: %(cmd)s"), + {'cmd': self.GET_GUID_CMD}) + + try: + (out, err) = self._execute(*self.GET_GUID_CMD, run_as_root=True, + root_helper=self._root_helper) + + LOG.info(_LI("Map volume %(cmd)s: stdout=%(out)s " + "stderr=%(err)s"), + {'cmd': self.GET_GUID_CMD, 'out': out, 'err': err}) + + except putils.ProcessExecutionError as e: + msg = (_("Error querying sdc guid: %(err)s") % {'err': e.stderr}) + LOG.error(msg) + raise exception.BrickException(message=msg) + + guid = out + LOG.info(_LI("Current sdc guid: %(guid)s"), {'guid': guid}) + params = {'guid': guid, 'allowMultipleMappings': 'TRUE'} + self.volume_id = self.volume_id or self._get_volume_id() + + headers = {'content-type': 'application/json'} + request = ( + "https://%(server_ip)s:%(server_port)s/api/instances/" + "Volume::%(volume_id)s/action/addMappedSdc" % + {'server_ip': self.server_ip, 'server_port': self.server_port, + 'volume_id': self.volume_id} + ) + + LOG.info(_LI("map volume request: %(request)s"), {'request': request}) + r = requests.post( + request, + data=json.dumps(params), + headers=headers, + auth=(self.server_username, self.server_token), + verify=False + ) + + r = self._check_response(r, request, False, params) + if r.status_code != self.OK_STATUS_CODE: + response = r.json() + error_code = response['errorCode'] + if error_code == self.VOLUME_ALREADY_MAPPED_ERROR: + LOG.warning(_LW( + "Ignoring error mapping volume %(volume_name)s: " + "volume already mapped."), + {'volume_name': self.volume_name} + ) + else: + msg = ( + _("Error mapping volume %(volume_name)s: %(err)s") % + {'volume_name': self.volume_name, + 'err': response['message']} + ) + + LOG.error(msg) + raise exception.BrickException(message=msg) + + self.volume_path = self._find_volume_path() + device_info['path'] = self.volume_path + + # Set QoS settings after map was performed + if self.iops_limit is not None or self.bandwidth_limit is not None: + params = {'guid': guid} + if self.bandwidth_limit is not None: + params['bandwidthLimitInKbps'] = self.bandwidth_limit + if self.iops_limit is not None: + params['iopsLimit'] = self.iops_limit + + request = ( + "https://%(server_ip)s:%(server_port)s/api/instances/" + "Volume::%(volume_id)s/action/setMappedSdcLimits" % + {'server_ip': self.server_ip, 'server_port': self.server_port, + 'volume_id': self.volume_id} + ) + + LOG.info(_LI("Set client limit request: %(request)s"), + {'request': request}) + + r = requests.post( + request, + data=json.dumps(params), + headers=headers, + auth=(self.server_username, self.server_token), + verify=False + ) + r = self._check_response(r, request, False, params) + if r.status_code != self.OK_STATUS_CODE: + response = r.json() + LOG.info(_LI("Set client limit response: %(response)s"), + {'response': response}) + msg = ( + _("Error setting client limits for volume " + "%(volume_name)s: %(err)s") % + {'volume_name': self.volume_name, + 'err': response['message']} + ) + + LOG.error(msg) + + return device_info + + @utils.trace + @lockutils.synchronized('scaleio', 'scaleio-') + def disconnect_volume(self, connection_properties, device_info): + """Disconnect the ScaleIO volume. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :param device_info: historical difference, but same as connection_props + :type device_info: dict + """ + self.get_config(connection_properties) + self.volume_id = self.volume_id or self._get_volume_id() + LOG.info(_LI( + "ScaleIO disconnect volume in ScaleIO brick volume driver." + )) + + LOG.debug( + _("ScaleIO Volume name: %(volume_name)s, SDC IP: %(sdc_ip)s, " + "REST Server IP: %(server_ip)s"), + {'volume_name': self.volume_name, 'sdc_ip': self.local_sdc_ip, + 'server_ip': self.server_ip} + ) + + LOG.info(_LI("ScaleIO sdc query guid command: %(cmd)s"), + {'cmd': self.GET_GUID_CMD}) + + try: + (out, err) = self._execute(*self.GET_GUID_CMD, run_as_root=True, + root_helper=self._root_helper) + LOG.info( + _LI("Unmap volume %(cmd)s: stdout=%(out)s stderr=%(err)s"), + {'cmd': self.GET_GUID_CMD, 'out': out, 'err': err} + ) + + except putils.ProcessExecutionError as e: + msg = _("Error querying sdc guid: %(err)s") % {'err': e.stderr} + LOG.error(msg) + raise exception.BrickException(message=msg) + + guid = out + LOG.info(_LI("Current sdc guid: %(guid)s"), {'guid': guid}) + + params = {'guid': guid} + headers = {'content-type': 'application/json'} + request = ( + "https://%(server_ip)s:%(server_port)s/api/instances/" + "Volume::%(volume_id)s/action/removeMappedSdc" % + {'server_ip': self.server_ip, 'server_port': self.server_port, + 'volume_id': self.volume_id} + ) + + LOG.info(_LI("Unmap volume request: %(request)s"), + {'request': request}) + r = requests.post( + request, + data=json.dumps(params), + headers=headers, + auth=(self.server_username, self.server_token), + verify=False + ) + + r = self._check_response(r, request, False, params) + if r.status_code != self.OK_STATUS_CODE: + response = r.json() + error_code = response['errorCode'] + if error_code == self.VOLUME_NOT_MAPPED_ERROR: + LOG.warning(_LW( + "Ignoring error unmapping volume %(volume_id)s: " + "volume not mapped."), {'volume_id': self.volume_name} + ) + else: + msg = (_("Error unmapping volume %(volume_id)s: %(err)s") % + {'volume_id': self.volume_name, + 'err': response['message']}) + LOG.error(msg) + raise exception.BrickException(message=msg) + + def extend_volume(self, connection_properties): + # TODO(walter-boring): is this possible? + raise NotImplementedError diff --git a/os_brick/initiator/connectors/sheepdog.py b/os_brick/initiator/connectors/sheepdog.py new file mode 100644 index 000000000..d5ee3aaa9 --- /dev/null +++ b/os_brick/initiator/connectors/sheepdog.py @@ -0,0 +1,126 @@ +# All Rights Reserved. +# +# 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. + +from oslo_log import log as logging + +from os_brick.i18n import _, _LE +from os_brick import exception +from os_brick import initiator +from os_brick.initiator.connectors import base +from os_brick.initiator import linuxsheepdog +from os_brick import utils + +DEVICE_SCAN_ATTEMPTS_DEFAULT = 3 +LOG = logging.getLogger(__name__) + + +class SheepdogConnector(base.BaseLinuxConnector): + """"Connector class to attach/detach sheepdog volumes.""" + + def __init__(self, root_helper, driver=None, use_multipath=False, + device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT, + *args, **kwargs): + + super(SheepdogConnector, self).__init__(root_helper, driver=driver, + device_scan_attempts= + device_scan_attempts, + *args, **kwargs) + + @staticmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The Sheepdog connector properties.""" + return {} + + def get_volume_paths(self, connection_properties): + # TODO(lixiaoy1): don't know where the connector + # looks for sheepdog volumes. + return [] + + def get_search_path(self): + # TODO(lixiaoy1): don't know where the connector + # looks for sheepdog volumes. + return None + + def get_all_available_volumes(self, connection_properties=None): + # TODO(lixiaoy1): not sure what to return here for sheepdog + return [] + + def _get_sheepdog_handle(self, connection_properties): + try: + host = connection_properties['hosts'][0] + name = connection_properties['name'] + port = connection_properties['ports'][0] + except IndexError: + msg = _("Connect volume failed, malformed connection properties") + raise exception.BrickException(msg=msg) + + sheepdog_handle = linuxsheepdog.SheepdogVolumeIOWrapper( + host, port, name) + return sheepdog_handle + + @utils.trace + def connect_volume(self, connection_properties): + """Connect to a volume. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :returns: dict + """ + + sheepdog_handle = self._get_sheepdog_handle(connection_properties) + return {'path': sheepdog_handle} + + @utils.trace + def disconnect_volume(self, connection_properties, device_info): + """Disconnect a volume. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :param device_info: historical difference, but same as connection_props + :type device_info: dict + """ + if device_info: + sheepdog_handle = device_info.get('path', None) + self.check_IO_handle_valid(sheepdog_handle, + linuxsheepdog.SheepdogVolumeIOWrapper, + 'Sheepdog') + if sheepdog_handle is not None: + sheepdog_handle.close() + + def check_valid_device(self, path, run_as_root=True): + """Verify an existing sheepdog handle is connected and valid.""" + sheepdog_handle = path + + if sheepdog_handle is None: + return False + + original_offset = sheepdog_handle.tell() + + try: + sheepdog_handle.read(4096) + except Exception as e: + LOG.error(_LE("Failed to access sheepdog device " + "handle: %(error)s"), + {"error": e}) + return False + finally: + sheepdog_handle.seek(original_offset, 0) + + return True + + def extend_volume(self, connection_properties): + # TODO(lixiaoy1): is this possible? + raise NotImplementedError diff --git a/os_brick/initiator/initiator_connector.py b/os_brick/initiator/initiator_connector.py new file mode 100644 index 000000000..23db64ddb --- /dev/null +++ b/os_brick/initiator/initiator_connector.py @@ -0,0 +1,193 @@ +# All Rights Reserved. +# +# 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. + + +import abc + +from oslo_log import log as logging +import six + +from os_brick import exception +from os_brick import executor +from os_brick import initiator + +LOG = logging.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class InitiatorConnector(executor.Executor): + + # This object can be used on any platform (x86, S390) + platform = initiator.PLATFORM_ALL + + # This object can be used on any os type (linux, windows) + os_type = initiator.OS_TYPE_ALL + + def __init__(self, root_helper, driver=None, execute=None, + device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT, + *args, **kwargs): + super(InitiatorConnector, self).__init__(root_helper, execute=execute, + *args, **kwargs) + self.device_scan_attempts = device_scan_attempts + + def set_driver(self, driver): + """The driver is used to find used LUNs.""" + self.driver = driver + + @abc.abstractmethod + def get_connector_properties(root_helper, *args, **kwargs): + """The generic connector properties.""" + pass + + @abc.abstractmethod + def check_valid_device(self, path, run_as_root=True): + """Test to see if the device path is a real device. + + :param path: The file system path for the device. + :type path: str + :param run_as_root: run the tests as root user? + :type run_as_root: bool + :returns: bool + """ + pass + + @abc.abstractmethod + def connect_volume(self, connection_properties): + """Connect to a volume. + + The connection_properties describes the information needed by + the specific protocol to use to make the connection. + + The connection_properties is a dictionary that describes the target + volume. It varies slightly by protocol type (iscsi, fibre_channel), + but the structure is usually the same. + + + An example for iSCSI: + + {'driver_volume_type': 'iscsi', + 'data': { + 'target_luns': [0, 2], + 'target_iqns': ['iqn.2000-05.com.3pardata:20810002ac00383d', + 'iqn.2000-05.com.3pardata:21810002ac00383d'], + 'target_discovered': True, + 'encrypted': False, + 'qos_specs': None, + 'target_portals': ['10.52.1.11:3260', '10.52.2.11:3260'], + 'access_mode': 'rw', + }} + + An example for fibre_channel: + + {'driver_volume_type': 'fibre_channel', + 'data': { + 'initiator_target_map': {'100010604b010459': ['21230002AC00383D'], + '100010604b01045d': ['21230002AC00383D'] + }, + 'target_discovered': True, + 'encrypted': False, + 'qos_specs': None, + 'target_lun': 1, + 'access_mode': 'rw', + 'target_wwn': [ + '20210002AC00383D', + '20220002AC00383D', + ], + }} + + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :returns: dict + """ + pass + + @abc.abstractmethod + def disconnect_volume(self, connection_properties, device_info): + """Disconnect a volume from the local host. + + The connection_properties are the same as from connect_volume. + The device_info is returned from connect_volume. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + :param device_info: historical difference, but same as connection_props + :type device_info: dict + """ + pass + + @abc.abstractmethod + def get_volume_paths(self, connection_properties): + """Return the list of existing paths for a volume. + + The job of this method is to find out what paths in + the system are associated with a volume as described + by the connection_properties. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + """ + pass + + @abc.abstractmethod + def get_search_path(self): + """Return the directory where a Connector looks for volumes. + + Some Connectors need the information in the + connection_properties to determine the search path. + """ + pass + + @abc.abstractmethod + def extend_volume(self, connection_properties): + """Update the attached volume's size. + + This method will attempt to update the local hosts's + volume after the volume has been extended on the remote + system. The new volume size in bytes will be returned. + If there is a failure to update, then None will be returned. + + :param connection_properties: The volume connection properties. + :returns: new size of the volume. + """ + pass + + @abc.abstractmethod + def get_all_available_volumes(self, connection_properties=None): + """Return all volumes that exist in the search directory. + + At connect_volume time, a Connector looks in a specific + directory to discover a volume's paths showing up. + This method's job is to return all paths in the directory + that connect_volume uses to find a volume. + + This method is used in coordination with get_volume_paths() + to verify that volumes have gone away after disconnect_volume + has been called. + + :param connection_properties: The dictionary that describes all + of the target volume attributes. + :type connection_properties: dict + """ + pass + + def check_IO_handle_valid(self, handle, data_type, protocol): + """Check IO handle has correct data type.""" + if (handle and not isinstance(handle, data_type)): + raise exception.InvalidIOHandleObject( + protocol=protocol, + actual_type=type(handle)) diff --git a/os_brick/initiator/windows/base.py b/os_brick/initiator/windows/base.py index 421629447..bb2fb4693 100644 --- a/os_brick/initiator/windows/base.py +++ b/os_brick/initiator/windows/base.py @@ -17,16 +17,17 @@ from os_win import utilsfactory from oslo_log import log as logging from os_brick import exception +from os_brick import initiator from os_brick.i18n import _, _LE -from os_brick.initiator import connector +from os_brick.initiator import initiator_connector from os_brick import utils LOG = logging.getLogger(__name__) -class BaseWindowsConnector(connector.InitiatorConnector): - platform = connector.PLATFORM_ALL - os_type = connector.OS_TYPE_WINDOWS +class BaseWindowsConnector(initiator_connector.InitiatorConnector): + platform = initiator.PLATFORM_ALL + os_type = initiator.OS_TYPE_WINDOWS def __init__(self, root_helper=None, *args, **kwargs): super(BaseWindowsConnector, self).__init__(root_helper, diff --git a/os_brick/initiator/windows/iscsi.py b/os_brick/initiator/windows/iscsi.py index 7e586bbdf..52b4fb75a 100644 --- a/os_brick/initiator/windows/iscsi.py +++ b/os_brick/initiator/windows/iscsi.py @@ -19,7 +19,7 @@ from oslo_log import log as logging from os_brick import exception from os_brick.i18n import _, _LE, _LI, _LW -from os_brick.initiator import connector +from os_brick.initiator.connectors import base_iscsi from os_brick.initiator.windows import base as win_conn_base from os_brick import utils @@ -27,7 +27,7 @@ LOG = logging.getLogger(__name__) class WindowsISCSIConnector(win_conn_base.BaseWindowsConnector, - connector.BaseISCSIConnector): + base_iscsi.BaseISCSIConnector): def __init__(self, *args, **kwargs): super(WindowsISCSIConnector, self).__init__(*args, **kwargs) self.use_multipath = kwargs.pop('use_multipath', False) diff --git a/os_brick/tests/initiator/connectors/__init__.py b/os_brick/tests/initiator/connectors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/os_brick/tests/initiator/connectors/test_aoe.py b/os_brick/tests/initiator/connectors/test_aoe.py new file mode 100644 index 000000000..8ea0076da --- /dev/null +++ b/os_brick/tests/initiator/connectors/test_aoe.py @@ -0,0 +1,129 @@ +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. +import mock +import os + +from oslo_service import loopingcall + +from os_brick import exception +from os_brick.initiator.connectors import aoe +from os_brick.tests.initiator import test_connector + + +class FakeFixedIntervalLoopingCall(object): + def __init__(self, f=None, *args, **kw): + self.args = args + self.kw = kw + self.f = f + self._stop = False + + def stop(self): + self._stop = True + + def wait(self): + return self + + def start(self, interval, initial_delay=None): + while not self._stop: + try: + self.f(*self.args, **self.kw) + except loopingcall.LoopingCallDone: + return self + except Exception: + raise + + +class AoEConnectorTestCase(test_connector.ConnectorTestCase): + """Test cases for AoE initiator class.""" + + def setUp(self): + super(AoEConnectorTestCase, self).setUp() + self.connector = aoe.AoEConnector('sudo') + self.connection_properties = {'target_shelf': 'fake_shelf', + 'target_lun': 'fake_lun'} + mock.patch.object(loopingcall, 'FixedIntervalLoopingCall', + FakeFixedIntervalLoopingCall).start() + self.addCleanup(mock.patch.stopall) + + def test_get_search_path(self): + expected = "/dev/etherd" + actual_path = self.connector.get_search_path() + self.assertEqual(expected, actual_path) + + @mock.patch.object(os.path, 'exists', return_value=True) + def test_get_volume_paths(self, mock_exists): + expected = ["/dev/etherd/efake_shelf.fake_lun"] + paths = self.connector.get_volume_paths(self.connection_properties) + self.assertEqual(expected, paths) + + def test_get_connector_properties(self): + props = aoe.AoEConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + + @mock.patch.object(os.path, 'exists', side_effect=[True, True]) + def test_connect_volume(self, exists_mock): + """Ensure that if path exist aoe-revalidate was called.""" + aoe_device, aoe_path = self.connector._get_aoe_info( + self.connection_properties) + with mock.patch.object(self.connector, '_execute', + return_value=["", ""]): + self.connector.connect_volume(self.connection_properties) + + @mock.patch.object(os.path, 'exists', side_effect=[False, True]) + def test_connect_volume_without_path(self, exists_mock): + """Ensure that if path doesn't exist aoe-discovery was called.""" + + aoe_device, aoe_path = self.connector._get_aoe_info( + self.connection_properties) + expected_info = { + 'type': 'block', + 'device': aoe_device, + 'path': aoe_path, + } + + with mock.patch.object(self.connector, '_execute', + return_value=["", ""]): + volume_info = self.connector.connect_volume( + self.connection_properties) + + self.assertDictMatch(volume_info, expected_info) + + @mock.patch.object(os.path, 'exists', return_value=False) + def test_connect_volume_could_not_discover_path(self, exists_mock): + _aoe_device, aoe_path = self.connector._get_aoe_info( + self.connection_properties) + + with mock.patch.object(self.connector, '_execute', + return_value=["", ""]): + self.assertRaises(exception.VolumeDeviceNotFound, + self.connector.connect_volume, + self.connection_properties) + + @mock.patch.object(os.path, 'exists', return_value=True) + def test_disconnect_volume(self, mock_exists): + """Ensure that if path exist aoe-revaliadte was called.""" + aoe_device, aoe_path = self.connector._get_aoe_info( + self.connection_properties) + + with mock.patch.object(self.connector, '_execute', + return_value=["", ""]): + self.connector.disconnect_volume(self.connection_properties, {}) + + def test_extend_volume(self): + self.assertRaises(NotImplementedError, + self.connector.extend_volume, + self.connection_properties) diff --git a/os_brick/tests/initiator/connectors/test_base_iscsi.py b/os_brick/tests/initiator/connectors/test_base_iscsi.py new file mode 100644 index 000000000..ca7fe82a2 --- /dev/null +++ b/os_brick/tests/initiator/connectors/test_base_iscsi.py @@ -0,0 +1,77 @@ +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. +import mock + +from os_brick.initiator.connectors import base_iscsi +from os_brick.initiator.connectors import fake +from os_brick.tests import base as test_base + + +class BaseISCSIConnectorTestCase(test_base.TestCase): + + def setUp(self): + super(BaseISCSIConnectorTestCase, self).setUp() + self.connector = fake.FakeBaseISCSIConnector(None) + + @mock.patch.object(base_iscsi.BaseISCSIConnector, '_get_all_targets') + def test_iterate_all_targets(self, mock_get_all_targets): + # extra_property cannot be a sentinel, a copied sentinel will not + # identical to the original one. + connection_properties = { + 'target_portals': mock.sentinel.target_portals, + 'target_iqns': mock.sentinel.target_iqns, + 'target_luns': mock.sentinel.target_luns, + 'extra_property': 'extra_property'} + mock_get_all_targets.return_value = [( + mock.sentinel.portal, mock.sentinel.iqn, mock.sentinel.lun)] + + # method is a generator, and it yields dictionaries. list() will + # iterate over all of the method's items. + list_props = list( + self.connector._iterate_all_targets(connection_properties)) + + mock_get_all_targets.assert_called_once_with(connection_properties) + self.assertEqual(1, len(list_props)) + + expected_props = {'target_portal': mock.sentinel.portal, + 'target_iqn': mock.sentinel.iqn, + 'target_lun': mock.sentinel.lun, + 'extra_property': 'extra_property'} + self.assertDictEqual(expected_props, list_props[0]) + + def test_get_all_targets(self): + connection_properties = { + 'target_portals': [mock.sentinel.target_portals], + 'target_iqns': [mock.sentinel.target_iqns], + 'target_luns': [mock.sentinel.target_luns]} + + all_targets = self.connector._get_all_targets(connection_properties) + + expected_targets = zip([mock.sentinel.target_portals], + [mock.sentinel.target_iqns], + [mock.sentinel.target_luns]) + self.assertEqual(list(expected_targets), list(all_targets)) + + def test_get_all_targets_single_target(self): + connection_properties = { + 'target_portal': mock.sentinel.target_portal, + 'target_iqn': mock.sentinel.target_iqn, + 'target_lun': mock.sentinel.target_lun} + + all_targets = self.connector._get_all_targets(connection_properties) + + expected_target = (mock.sentinel.target_portal, + mock.sentinel.target_iqn, + mock.sentinel.target_lun) + self.assertEqual([expected_target], all_targets) diff --git a/os_brick/tests/initiator/connectors/test_disco.py b/os_brick/tests/initiator/connectors/test_disco.py new file mode 100644 index 000000000..c803acc46 --- /dev/null +++ b/os_brick/tests/initiator/connectors/test_disco.py @@ -0,0 +1,156 @@ +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. +import glob +import mock +import os + +from os_brick import exception +from os_brick.initiator.connectors import disco +from os_brick.tests.initiator import test_connector + + +class DISCOConnectorTestCase(test_connector.ConnectorTestCase): + """Test cases for DISCO connector.""" + + # Fake volume information + volume = { + 'name': 'a-disco-volume', + 'disco_id': '1234567' + } + + # Conf for test + conf = { + 'ip': test_connector.MY_IP, + 'port': 9898 + } + + def setUp(self): + super(DISCOConnectorTestCase, self).setUp() + + self.fake_connection_properties = { + 'name': self.volume['name'], + 'disco_id': self.volume['disco_id'], + 'conf': { + 'server_ip': self.conf['ip'], + 'server_port': self.conf['port']} + } + + self.fake_volume_status = {'attached': True, + 'detached': False} + self.fake_request_status = {'success': None, + 'fail': 'ERROR'} + self.volume_status = 'detached' + self.request_status = 'success' + + # Patch the request and os calls to fake versions + mock.patch.object(disco.DISCOConnector, + '_send_disco_vol_cmd', + self.perform_disco_request).start() + mock.patch.object(os.path, + 'exists', self.is_volume_attached).start() + mock.patch.object(glob, + 'glob', self.list_disco_volume).start() + self.addCleanup(mock.patch.stopall) + + # The actual DISCO connector + self.connector = disco.DISCOConnector( + 'sudo', execute=self.fake_execute) + + def perform_disco_request(self, *cmd, **kwargs): + """Fake the socket call.""" + return self.fake_request_status[self.request_status] + + def is_volume_attached(self, *cmd, **kwargs): + """Fake volume detection check.""" + return self.fake_volume_status[self.volume_status] + + def list_disco_volume(self, *cmd, **kwargs): + """Fake the glob call.""" + path_dir = self.connector.get_search_path() + volume_id = self.volume['disco_id'] + volume_items = [path_dir, '/', self.connector.DISCO_PREFIX, volume_id] + volume_path = ''.join(volume_items) + return [volume_path] + + def test_get_connector_properties(self): + props = disco.DISCOConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + + def test_get_search_path(self): + """DISCO volumes should be under /dev.""" + expected = "/dev" + actual = self.connector.get_search_path() + self.assertEqual(expected, actual) + + def test_get_volume_paths(self): + """Test to get all the path for a specific volume.""" + expected = ['/dev/dms1234567'] + self.volume_status = 'attached' + actual = self.connector.get_volume_paths( + self.fake_connection_properties) + self.assertEqual(expected, actual) + + def test_connect_volume(self): + """Attach a volume.""" + self.connector.connect_volume(self.fake_connection_properties) + + def test_connect_volume_already_attached(self): + """Make sure that we don't issue the request.""" + self.request_status = 'fail' + self.volume_status = 'attached' + self.test_connect_volume() + + def test_connect_volume_request_fail(self): + """Fail the attach request.""" + self.volume_status = 'detached' + self.request_status = 'fail' + self.assertRaises(exception.BrickException, + self.test_connect_volume) + + def test_disconnect_volume(self): + """Detach a volume.""" + self.connector.disconnect_volume(self.fake_connection_properties, None) + + def test_disconnect_volume_attached(self): + """Detach a volume attached.""" + self.request_status = 'success' + self.volume_status = 'attached' + self.test_disconnect_volume() + + def test_disconnect_volume_already_detached(self): + """Ensure that we don't issue the request.""" + self.request_status = 'fail' + self.volume_status = 'detached' + self.test_disconnect_volume() + + def test_disconnect_volume_request_fail(self): + """Fail the detach request.""" + self.volume_status = 'attached' + self.request_status = 'fail' + self.assertRaises(exception.BrickException, + self.test_disconnect_volume) + + def test_get_all_available_volumes(self): + """Test to get all the available DISCO volumes.""" + expected = ['/dev/dms1234567'] + actual = self.connector.get_all_available_volumes(None) + self.assertItemsEqual(expected, actual) + + def test_extend_volume(self): + self.assertRaises(NotImplementedError, + self.connector.extend_volume, + self.fake_connection_properties) diff --git a/os_brick/tests/initiator/connectors/test_drbd.py b/os_brick/tests/initiator/connectors/test_drbd.py new file mode 100644 index 000000000..011240a7b --- /dev/null +++ b/os_brick/tests/initiator/connectors/test_drbd.py @@ -0,0 +1,89 @@ +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +from os_brick.initiator.connectors import drbd +from os_brick.tests.initiator import test_connector + + +class DRBDConnectorTestCase(test_connector.ConnectorTestCase): + + RESOURCE_TEMPLATE = ''' + resource r0 { + on host1 { + } + net { + shared-secret "%(shared-secret)s"; + } + } +''' + + def setUp(self): + super(DRBDConnectorTestCase, self).setUp() + + self.connector = drbd.DRBDConnector( + None, execute=self._fake_exec) + + self.execs = [] + + def _fake_exec(self, *cmd, **kwargs): + self.execs.append(cmd) + + # out, err + return ('', '') + + def test_get_connector_properties(self): + props = drbd.DRBDConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + + def test_connect_volume(self): + """Test connect_volume.""" + + cprop = { + 'provider_auth': 'my-secret', + 'config': self.RESOURCE_TEMPLATE, + 'name': 'my-precious', + 'device': '/dev/drbd951722', + 'data': {}, + } + + res = self.connector.connect_volume(cprop) + + self.assertEqual(cprop['device'], res['path']) + self.assertEqual('adjust', self.execs[0][1]) + self.assertEqual(cprop['name'], self.execs[0][4]) + + def test_disconnect_volume(self): + """Test the disconnect volume case.""" + + cprop = { + 'provider_auth': 'my-secret', + 'config': self.RESOURCE_TEMPLATE, + 'name': 'my-precious', + 'device': '/dev/drbd951722', + 'data': {}, + } + dev_info = {} + + self.connector.disconnect_volume(cprop, dev_info) + + self.assertEqual('down', self.execs[0][1]) + + def test_extend_volume(self): + cprop = {'name': 'something'} + self.assertRaises(NotImplementedError, + self.connector.extend_volume, + cprop) diff --git a/os_brick/tests/initiator/connectors/test_fibre_channel.py b/os_brick/tests/initiator/connectors/test_fibre_channel.py new file mode 100644 index 000000000..f648e4736 --- /dev/null +++ b/os_brick/tests/initiator/connectors/test_fibre_channel.py @@ -0,0 +1,396 @@ +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. +import mock +import os +import six + +from os_brick import exception +from os_brick.initiator.connectors import base +from os_brick.initiator.connectors import fibre_channel +from os_brick.initiator import linuxfc +from os_brick.initiator import linuxscsi +from os_brick.tests.initiator import test_connector + + +class FibreChannelConnectorTestCase(test_connector.ConnectorTestCase): + + def setUp(self): + super(FibreChannelConnectorTestCase, self).setUp() + self.connector = fibre_channel.FibreChannelConnector( + None, execute=self.fake_execute, use_multipath=False) + self.assertIsNotNone(self.connector) + self.assertIsNotNone(self.connector._linuxfc) + self.assertIsNotNone(self.connector._linuxscsi) + + def fake_get_fc_hbas(self): + return [{'ClassDevice': 'host1', + 'ClassDevicePath': '/sys/devices/pci0000:00/0000:00:03.0' + '/0000:05:00.2/host1/fc_host/host1', + 'dev_loss_tmo': '30', + 'fabric_name': '0x1000000533f55566', + 'issue_lip': '', + 'max_npiv_vports': '255', + 'maxframe_size': '2048 bytes', + 'node_name': '0x200010604b019419', + 'npiv_vports_inuse': '0', + 'port_id': '0x680409', + 'port_name': '0x100010604b019419', + 'port_state': 'Online', + 'port_type': 'NPort (fabric via point-to-point)', + 'speed': '10 Gbit', + 'supported_classes': 'Class 3', + 'supported_speeds': '10 Gbit', + 'symbolic_name': 'Emulex 554M FV4.0.493.0 DV8.3.27', + 'tgtid_bind_type': 'wwpn (World Wide Port Name)', + 'uevent': None, + 'vport_create': '', + 'vport_delete': ''}] + + def fake_get_fc_hbas_info(self): + hbas = self.fake_get_fc_hbas() + info = [{'port_name': hbas[0]['port_name'].replace('0x', ''), + 'node_name': hbas[0]['node_name'].replace('0x', ''), + 'host_device': hbas[0]['ClassDevice'], + 'device_path': hbas[0]['ClassDevicePath']}] + return info + + def fibrechan_connection(self, volume, location, wwn): + return {'driver_volume_type': 'fibrechan', + 'data': { + 'volume_id': volume['id'], + 'target_portal': location, + 'target_wwn': wwn, + 'target_lun': 1, + }} + + @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas') + def test_get_connector_properties(self, mock_hbas): + mock_hbas.return_value = self.fake_get_fc_hbas() + multipath = True + enforce_multipath = True + props = fibre_channel.FibreChannelConnector.get_connector_properties( + 'sudo', multipath=multipath, + enforce_multipath=enforce_multipath) + + hbas = self.fake_get_fc_hbas() + expected_props = {'wwpns': [hbas[0]['port_name'].replace('0x', '')], + 'wwnns': [hbas[0]['node_name'].replace('0x', '')]} + self.assertEqual(expected_props, props) + + def test_get_search_path(self): + search_path = self.connector.get_search_path() + expected = "/dev/disk/by-path" + self.assertEqual(expected, search_path) + + def test_get_pci_num(self): + hba = {'device_path': "/sys/devices/pci0000:00/0000:00:03.0" + "/0000:05:00.3/host2/fc_host/host2"} + pci_num = self.connector._get_pci_num(hba) + self.assertEqual("0000:05:00.3", pci_num) + + hba = {'device_path': "/sys/devices/pci0000:00/0000:00:03.0" + "/0000:05:00.3/0000:06:00.6/host2/fc_host/host2"} + pci_num = self.connector._get_pci_num(hba) + self.assertEqual("0000:06:00.6", pci_num) + + hba = {'device_path': "/sys/devices/pci0000:20/0000:20:03.0" + "/0000:21:00.2/net/ens2f2/ctlr_2/host3" + "/fc_host/host3"} + pci_num = self.connector._get_pci_num(hba) + self.assertEqual("0000:21:00.2", pci_num) + + @mock.patch.object(os.path, 'exists', return_value=True) + @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas') + @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info') + def test_get_volume_paths(self, fake_fc_hbas_info, + fake_fc_hbas, fake_exists): + fake_fc_hbas.side_effect = self.fake_get_fc_hbas + fake_fc_hbas_info.side_effect = self.fake_get_fc_hbas_info + + name = 'volume-00000001' + vol = {'id': 1, 'name': name} + location = '10.0.2.15:3260' + wwn = '1234567890123456' + connection_info = self.fibrechan_connection(vol, location, wwn) + volume_paths = self.connector.get_volume_paths( + connection_info['data']) + + expected = ['/dev/disk/by-path/pci-0000:05:00.2' + '-fc-0x1234567890123456-lun-1'] + self.assertEqual(expected, volume_paths) + + @mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw') + @mock.patch.object(os.path, 'exists', return_value=True) + @mock.patch.object(os.path, 'realpath', return_value='/dev/sdb') + @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas') + @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info') + @mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device') + @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') + @mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info') + def test_connect_volume(self, get_device_info_mock, + get_scsi_wwn_mock, + remove_device_mock, + get_fc_hbas_info_mock, + get_fc_hbas_mock, + realpath_mock, + exists_mock, + wait_for_rw_mock): + get_fc_hbas_mock.side_effect = self.fake_get_fc_hbas + get_fc_hbas_info_mock.side_effect = self.fake_get_fc_hbas_info + + wwn = '1234567890' + multipath_devname = '/dev/md-1' + devices = {"device": multipath_devname, + "id": wwn, + "devices": [{'device': '/dev/sdb', + 'address': '1:0:0:1', + 'host': 1, 'channel': 0, + 'id': 0, 'lun': 1}]} + get_device_info_mock.return_value = devices['devices'][0] + get_scsi_wwn_mock.return_value = wwn + + location = '10.0.2.15:3260' + name = 'volume-00000001' + vol = {'id': 1, 'name': name} + # Should work for string, unicode, and list + wwns = ['1234567890123456', six.text_type('1234567890123456'), + ['1234567890123456', '1234567890123457']] + for wwn in wwns: + connection_info = self.fibrechan_connection(vol, location, wwn) + dev_info = self.connector.connect_volume(connection_info['data']) + exp_wwn = wwn[0] if isinstance(wwn, list) else wwn + dev_str = ('/dev/disk/by-path/pci-0000:05:00.2-fc-0x%s-lun-1' % + exp_wwn) + self.assertEqual(dev_info['type'], 'block') + self.assertEqual(dev_info['path'], dev_str) + self.assertTrue('multipath_id' not in dev_info) + self.assertTrue('devices' not in dev_info) + + self.connector.disconnect_volume(connection_info['data'], dev_info) + expected_commands = [] + self.assertEqual(expected_commands, self.cmds) + + # Should not work for anything other than string, unicode, and list + connection_info = self.fibrechan_connection(vol, location, 123) + self.assertRaises(exception.NoFibreChannelHostsFound, + self.connector.connect_volume, + connection_info['data']) + + get_fc_hbas_mock.side_effect = [[]] + get_fc_hbas_info_mock.side_effect = [[]] + self.assertRaises(exception.NoFibreChannelHostsFound, + self.connector.connect_volume, + connection_info['data']) + + def _test_connect_volume_multipath(self, get_device_info_mock, + get_scsi_wwn_mock, + remove_device_mock, + get_fc_hbas_info_mock, + get_fc_hbas_mock, + realpath_mock, + exists_mock, + wait_for_rw_mock, + find_mp_dev_mock, + access_mode, + should_wait_for_rw): + self.connector.use_multipath = True + get_fc_hbas_mock.side_effect = self.fake_get_fc_hbas + get_fc_hbas_info_mock.side_effect = self.fake_get_fc_hbas_info + + wwn = '1234567890' + multipath_devname = '/dev/md-1' + devices = {"device": multipath_devname, + "id": wwn, + "devices": [{'device': '/dev/sdb', + 'address': '1:0:0:1', + 'host': 1, 'channel': 0, + 'id': 0, 'lun': 1}]} + get_device_info_mock.return_value = devices['devices'][0] + get_scsi_wwn_mock.return_value = wwn + + location = '10.0.2.15:3260' + name = 'volume-00000001' + vol = {'id': 1, 'name': name} + initiator_wwn = ['1234567890123456', '1234567890123457'] + + find_mp_dev_mock.return_value = '/dev/disk/by-id/dm-uuid-mpath-' + wwn + + connection_info = self.fibrechan_connection(vol, location, + initiator_wwn) + connection_info['data']['access_mode'] = access_mode + + self.connector.connect_volume(connection_info['data']) + + self.assertEqual(should_wait_for_rw, wait_for_rw_mock.called) + return connection_info + + @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device') + @mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw') + @mock.patch.object(os.path, 'exists', return_value=True) + @mock.patch.object(os.path, 'realpath', return_value='/dev/sdb') + @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas') + @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info') + @mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device') + @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') + @mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info') + def test_connect_volume_multipath_rw(self, get_device_info_mock, + get_scsi_wwn_mock, + remove_device_mock, + get_fc_hbas_info_mock, + get_fc_hbas_mock, + realpath_mock, + exists_mock, + wait_for_rw_mock, + find_mp_dev_mock): + + self._test_connect_volume_multipath(get_device_info_mock, + get_scsi_wwn_mock, + remove_device_mock, + get_fc_hbas_info_mock, + get_fc_hbas_mock, + realpath_mock, + exists_mock, + wait_for_rw_mock, + find_mp_dev_mock, + 'rw', + True) + + @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device') + @mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw') + @mock.patch.object(os.path, 'exists', return_value=True) + @mock.patch.object(os.path, 'realpath', return_value='/dev/sdb') + @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas') + @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info') + @mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device') + @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') + @mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info') + def test_connect_volume_multipath_no_access_mode(self, + get_device_info_mock, + get_scsi_wwn_mock, + remove_device_mock, + get_fc_hbas_info_mock, + get_fc_hbas_mock, + realpath_mock, + exists_mock, + wait_for_rw_mock, + find_mp_dev_mock): + + self._test_connect_volume_multipath(get_device_info_mock, + get_scsi_wwn_mock, + remove_device_mock, + get_fc_hbas_info_mock, + get_fc_hbas_mock, + realpath_mock, + exists_mock, + wait_for_rw_mock, + find_mp_dev_mock, + None, + True) + + @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device') + @mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw') + @mock.patch.object(os.path, 'exists', return_value=True) + @mock.patch.object(os.path, 'realpath', return_value='/dev/sdb') + @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas') + @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info') + @mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device') + @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') + @mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info') + def test_connect_volume_multipath_ro(self, get_device_info_mock, + get_scsi_wwn_mock, + remove_device_mock, + get_fc_hbas_info_mock, + get_fc_hbas_mock, + realpath_mock, + exists_mock, + wait_for_rw_mock, + find_mp_dev_mock): + + self._test_connect_volume_multipath(get_device_info_mock, + get_scsi_wwn_mock, + remove_device_mock, + get_fc_hbas_info_mock, + get_fc_hbas_mock, + realpath_mock, + exists_mock, + wait_for_rw_mock, + find_mp_dev_mock, + 'ro', + False) + + @mock.patch.object(base.BaseLinuxConnector, '_discover_mpath_device') + @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device') + @mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw') + @mock.patch.object(os.path, 'exists', return_value=True) + @mock.patch.object(os.path, 'realpath', return_value='/dev/sdb') + @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas') + @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info') + @mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device') + @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') + @mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info') + def test_connect_volume_multipath_not_found(self, + get_device_info_mock, + get_scsi_wwn_mock, + remove_device_mock, + get_fc_hbas_info_mock, + get_fc_hbas_mock, + realpath_mock, + exists_mock, + wait_for_rw_mock, + find_mp_dev_mock, + discover_mp_dev_mock): + discover_mp_dev_mock.return_value = ("/dev/disk/by-path/something", + None) + + connection_info = self._test_connect_volume_multipath( + get_device_info_mock, get_scsi_wwn_mock, remove_device_mock, + get_fc_hbas_info_mock, get_fc_hbas_mock, realpath_mock, + exists_mock, wait_for_rw_mock, find_mp_dev_mock, + 'rw', False) + + self.assertNotIn('multipathd_id', connection_info['data']) + + @mock.patch.object(fibre_channel.FibreChannelConnector, 'get_volume_paths') + def test_extend_volume_no_path(self, mock_volume_paths): + mock_volume_paths.return_value = [] + volume = {'id': 'fake_uuid'} + wwn = '1234567890123456' + connection_info = self.fibrechan_connection(volume, + "10.0.2.15:3260", + wwn) + + self.assertRaises(exception.VolumePathsNotFound, + self.connector.extend_volume, + connection_info['data']) + + @mock.patch.object(linuxscsi.LinuxSCSI, 'extend_volume') + @mock.patch.object(fibre_channel.FibreChannelConnector, 'get_volume_paths') + def test_extend_volume(self, mock_volume_paths, mock_scsi_extend): + fake_new_size = 1024 + mock_volume_paths.return_value = ['/dev/vdx'] + mock_scsi_extend.return_value = fake_new_size + volume = {'id': 'fake_uuid'} + wwn = '1234567890123456' + connection_info = self.fibrechan_connection(volume, + "10.0.2.15:3260", + wwn) + new_size = self.connector.extend_volume(connection_info['data']) + self.assertEqual(fake_new_size, new_size) + + @mock.patch.object(os.path, 'isdir') + def test_get_all_available_volumes_path_not_dir(self, mock_isdir): + mock_isdir.return_value = False + expected = [] + actual = self.connector.get_all_available_volumes() + self.assertItemsEqual(expected, actual) diff --git a/os_brick/tests/initiator/connectors/test_fibre_channel_s390x.py b/os_brick/tests/initiator/connectors/test_fibre_channel_s390x.py new file mode 100644 index 000000000..6b02319f0 --- /dev/null +++ b/os_brick/tests/initiator/connectors/test_fibre_channel_s390x.py @@ -0,0 +1,71 @@ +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. +import mock + +from os_brick.initiator.connectors import fibre_channel_s390x +from os_brick.initiator import linuxfc +from os_brick.tests.initiator import test_connector + + +class FibreChannelConnectorS390XTestCase(test_connector.ConnectorTestCase): + + def setUp(self): + super(FibreChannelConnectorS390XTestCase, self).setUp() + self.connector = fibre_channel_s390x.FibreChannelConnectorS390X( + None, execute=self.fake_execute, use_multipath=False) + self.assertIsNotNone(self.connector) + self.assertIsNotNone(self.connector._linuxfc) + self.assertEqual(self.connector._linuxfc.__class__.__name__, + "LinuxFibreChannelS390X") + self.assertIsNotNone(self.connector._linuxscsi) + + @mock.patch.object(linuxfc.LinuxFibreChannelS390X, 'configure_scsi_device') + def test_get_host_devices(self, mock_configure_scsi_device): + lun = 2 + possible_devs = [(3, 5), ] + devices = self.connector._get_host_devices(possible_devs, lun) + mock_configure_scsi_device.assert_called_with(3, 5, + "0x0002000000000000") + self.assertEqual(1, len(devices)) + device_path = "/dev/disk/by-path/ccw-3-zfcp-5:0x0002000000000000" + self.assertEqual(devices[0], device_path) + + def test_get_lun_string(self): + lun = 1 + lunstring = self.connector._get_lun_string(lun) + self.assertEqual(lunstring, "0x0001000000000000") + lun = 0xff + lunstring = self.connector._get_lun_string(lun) + self.assertEqual(lunstring, "0x00ff000000000000") + lun = 0x101 + lunstring = self.connector._get_lun_string(lun) + self.assertEqual(lunstring, "0x0101000000000000") + lun = 0x4020400a + lunstring = self.connector._get_lun_string(lun) + self.assertEqual(lunstring, "0x4020400a00000000") + + @mock.patch.object(fibre_channel_s390x.FibreChannelConnectorS390X, + '_get_possible_devices', return_value=[(3, 5), ]) + @mock.patch.object(linuxfc.LinuxFibreChannelS390X, 'get_fc_hbas_info', + return_value=[]) + @mock.patch.object(linuxfc.LinuxFibreChannelS390X, + 'deconfigure_scsi_device') + def test_remove_devices(self, mock_deconfigure_scsi_device, + mock_get_fc_hbas_info, mock_get_possible_devices): + connection_properties = {'target_wwn': 5, 'target_lun': 2} + self.connector._remove_devices(connection_properties, devices=None) + mock_deconfigure_scsi_device.assert_called_with(3, 5, + "0x0002000000000000") + mock_get_fc_hbas_info.assert_called_once_with() + mock_get_possible_devices.assert_called_once_with([], 5) diff --git a/os_brick/tests/initiator/connectors/test_hgst.py b/os_brick/tests/initiator/connectors/test_hgst.py new file mode 100644 index 000000000..17e18c864 --- /dev/null +++ b/os_brick/tests/initiator/connectors/test_hgst.py @@ -0,0 +1,219 @@ +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. +import mock +import os + +from oslo_concurrency import processutils as putils + +from os_brick import exception +from os_brick.initiator import connector +from os_brick.initiator.connectors import hgst +from os_brick.tests.initiator import test_connector + + +class HGSTConnectorTestCase(test_connector.ConnectorTestCase): + """Test cases for HGST initiator class.""" + + IP_OUTPUT = """ +1: lo: mtu 65536 qdisc noqueue state UNKNOWN + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 + inet 127.0.0.1/8 scope host lo + valid_lft forever preferred_lft forever + inet 169.254.169.254/32 scope link lo + valid_lft forever preferred_lft forever + inet6 ::1/128 scope host + valid_lft forever preferred_lft forever +2: em1: mtu 1500 qdisc mq master + link/ether 00:25:90:d9:18:08 brd ff:ff:ff:ff:ff:ff + inet6 fe80::225:90ff:fed9:1808/64 scope link + valid_lft forever preferred_lft forever +3: em2: mtu 1500 qdisc mq state + link/ether 00:25:90:d9:18:09 brd ff:ff:ff:ff:ff:ff + inet 192.168.0.23/24 brd 192.168.0.255 scope global em2 + valid_lft forever preferred_lft forever + inet6 fe80::225:90ff:fed9:1809/64 scope link + valid_lft forever preferred_lft forever + """ + + DOMAIN_OUTPUT = """localhost""" + + DOMAIN_FAILED = """this.better.not.resolve.to.a.name.or.else""" + + SET_APPHOST_OUTPUT = """ +VLVM_SET_APPHOSTS0000000395 +Request Succeeded + """ + + def setUp(self): + super(HGSTConnectorTestCase, self).setUp() + self.connector = hgst.HGSTConnector( + None, execute=self._fake_exec) + self._fail_set_apphosts = False + self._fail_ip = False + self._fail_domain_list = False + + def _fake_exec_set_apphosts(self, *cmd): + if self._fail_set_apphosts: + raise putils.ProcessExecutionError(None, None, 1) + else: + return self.SET_APPHOST_OUTPUT, '' + + def _fake_exec_ip(self, *cmd): + if self._fail_ip: + # Remove localhost so there is no IP match + return self.IP_OUTPUT.replace("127.0.0.1", "x.x.x.x"), '' + else: + return self.IP_OUTPUT, '' + + def _fake_exec_domain_list(self, *cmd): + if self._fail_domain_list: + return self.DOMAIN_FAILED, '' + else: + return self.DOMAIN_OUTPUT, '' + + def _fake_exec(self, *cmd, **kwargs): + self.cmdline = " ".join(cmd) + if cmd[0] == "ip": + return self._fake_exec_ip(*cmd) + elif cmd[0] == "vgc-cluster": + if cmd[1] == "domain-list": + return self._fake_exec_domain_list(*cmd) + elif cmd[1] == "space-set-apphosts": + return self._fake_exec_set_apphosts(*cmd) + else: + return '', '' + + def test_factory(self): + """Can we instantiate a HGSTConnector of the right kind?""" + obj = connector.InitiatorConnector.factory('HGST', None) + self.assertEqual("HGSTConnector", obj.__class__.__name__) + + def test_get_search_path(self): + expected = "/dev" + actual = self.connector.get_search_path() + self.assertEqual(expected, actual) + + @mock.patch.object(os.path, 'exists', return_value=True) + def test_get_volume_paths(self, mock_exists): + + cprops = {'name': 'space', 'noremovehost': 'stor1'} + path = "/dev/%s" % cprops['name'] + expected = [path] + actual = self.connector.get_volume_paths(cprops) + self.assertEqual(expected, actual) + + def test_connect_volume(self): + """Tests that a simple connection succeeds""" + self._fail_set_apphosts = False + self._fail_ip = False + self._fail_domain_list = False + cprops = {'name': 'space', 'noremovehost': 'stor1'} + dev_info = self.connector.connect_volume(cprops) + self.assertEqual('block', dev_info['type']) + self.assertEqual('space', dev_info['device']) + self.assertEqual('/dev/space', dev_info['path']) + + def test_get_connector_properties(self): + props = hgst.HGSTConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + + def test_connect_volume_nohost_fail(self): + """This host should not be found, connect should fail.""" + self._fail_set_apphosts = False + self._fail_ip = True + self._fail_domain_list = False + cprops = {'name': 'space', 'noremovehost': 'stor1'} + self.assertRaises(exception.BrickException, + self.connector.connect_volume, + cprops) + + def test_connect_volume_nospace_fail(self): + """The space command will fail, exception to be thrown""" + self._fail_set_apphosts = True + self._fail_ip = False + self._fail_domain_list = False + cprops = {'name': 'space', 'noremovehost': 'stor1'} + self.assertRaises(exception.BrickException, + self.connector.connect_volume, + cprops) + + def test_disconnect_volume(self): + """Simple disconnection should pass and disconnect me""" + self._fail_set_apphosts = False + self._fail_ip = False + self._fail_domain_list = False + self._cmdline = "" + cprops = {'name': 'space', 'noremovehost': 'stor1'} + self.connector.disconnect_volume(cprops, None) + exp_cli = ("vgc-cluster space-set-apphosts -n space " + "-A localhost --action DELETE") + self.assertEqual(exp_cli, self.cmdline) + + def test_disconnect_volume_nohost(self): + """Should not run a setapphosts because localhost will""" + """be the noremotehost""" + self._fail_set_apphosts = False + self._fail_ip = False + self._fail_domain_list = False + self._cmdline = "" + cprops = {'name': 'space', 'noremovehost': 'localhost'} + self.connector.disconnect_volume(cprops, None) + # The last command should be the IP listing, not set apphosts + exp_cli = ("ip addr list") + self.assertEqual(exp_cli, self.cmdline) + + def test_disconnect_volume_fails(self): + """The set-apphosts should fail, exception to be thrown""" + self._fail_set_apphosts = True + self._fail_ip = False + self._fail_domain_list = False + self._cmdline = "" + cprops = {'name': 'space', 'noremovehost': 'stor1'} + self.assertRaises(exception.BrickException, + self.connector.disconnect_volume, + cprops, None) + + def test_bad_connection_properties(self): + """Send in connection_properties missing required fields""" + # Invalid connection_properties + self.assertRaises(exception.BrickException, + self.connector.connect_volume, + None) + # Name required for connect_volume + cprops = {'noremovehost': 'stor1'} + self.assertRaises(exception.BrickException, + self.connector.connect_volume, + cprops) + # Invalid connection_properties + self.assertRaises(exception.BrickException, + self.connector.disconnect_volume, + None, None) + # Name and noremovehost needed for disconnect_volume + cprops = {'noremovehost': 'stor1'} + self.assertRaises(exception.BrickException, + self.connector.disconnect_volume, + cprops, None) + cprops = {'name': 'space'} + self.assertRaises(exception.BrickException, + self.connector.disconnect_volume, + cprops, None) + + def test_extend_volume(self): + cprops = {'name': 'space', 'noremovehost': 'stor1'} + self.assertRaises(NotImplementedError, + self.connector.extend_volume, + cprops) diff --git a/os_brick/tests/initiator/connectors/test_huawei.py b/os_brick/tests/initiator/connectors/test_huawei.py new file mode 100644 index 000000000..d2775947b --- /dev/null +++ b/os_brick/tests/initiator/connectors/test_huawei.py @@ -0,0 +1,230 @@ +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. +import mock +import os +import tempfile + +from os_brick import exception +from os_brick.initiator.connectors import huawei +from os_brick.tests.initiator import test_connector + + +class HuaweiStorHyperConnectorTestCase(test_connector.ConnectorTestCase): + """Test cases for StorHyper initiator class.""" + + attached = False + + def setUp(self): + super(HuaweiStorHyperConnectorTestCase, self).setUp() + self.fake_sdscli_file = tempfile.mktemp() + self.addCleanup(os.remove, self.fake_sdscli_file) + newefile = open(self.fake_sdscli_file, 'w') + newefile.write('test') + newefile.close() + + self.connector = huawei.HuaweiStorHyperConnector( + None, execute=self.fake_execute) + self.connector.cli_path = self.fake_sdscli_file + self.connector.iscliexist = True + + self.connector_fail = huawei.HuaweiStorHyperConnector( + None, execute=self.fake_execute_fail) + self.connector_fail.cli_path = self.fake_sdscli_file + self.connector_fail.iscliexist = True + + self.connector_nocli = huawei.HuaweiStorHyperConnector( + None, execute=self.fake_execute_fail) + self.connector_nocli.cli_path = self.fake_sdscli_file + self.connector_nocli.iscliexist = False + + self.connection_properties = { + 'access_mode': 'rw', + 'qos_specs': None, + 'volume_id': 'volume-b2911673-863c-4380-a5f2-e1729eecfe3f' + } + + self.device_info = {'type': 'block', + 'path': '/dev/vdxxx'} + HuaweiStorHyperConnectorTestCase.attached = False + + def fake_execute(self, *cmd, **kwargs): + method = cmd[2] + self.cmds.append(" ".join(cmd)) + if 'attach' == method: + HuaweiStorHyperConnectorTestCase.attached = True + return 'ret_code=0', None + if 'querydev' == method: + if HuaweiStorHyperConnectorTestCase.attached: + return 'ret_code=0\ndev_addr=/dev/vdxxx', None + else: + return 'ret_code=1\ndev_addr=/dev/vdxxx', None + if 'detach' == method: + HuaweiStorHyperConnectorTestCase.attached = False + return 'ret_code=0', None + + def fake_execute_fail(self, *cmd, **kwargs): + method = cmd[2] + self.cmds.append(" ".join(cmd)) + if 'attach' == method: + HuaweiStorHyperConnectorTestCase.attached = False + return 'ret_code=330151401', None + if 'querydev' == method: + if HuaweiStorHyperConnectorTestCase.attached: + return 'ret_code=0\ndev_addr=/dev/vdxxx', None + else: + return 'ret_code=1\ndev_addr=/dev/vdxxx', None + if 'detach' == method: + HuaweiStorHyperConnectorTestCase.attached = True + return 'ret_code=330155007', None + + def test_get_connector_properties(self): + props = huawei.HuaweiStorHyperConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + + def test_get_search_path(self): + actual = self.connector.get_search_path() + self.assertIsNone(actual) + + @mock.patch.object(huawei.HuaweiStorHyperConnector, + '_query_attached_volume') + def test_get_volume_paths(self, mock_query_attached): + path = self.device_info['path'] + mock_query_attached.return_value = {'ret_code': 0, + 'dev_addr': path} + + expected = [path] + actual = self.connector.get_volume_paths(self.connection_properties) + self.assertEqual(expected, actual) + + def test_connect_volume(self): + """Test the basic connect volume case.""" + + retval = self.connector.connect_volume(self.connection_properties) + self.assertEqual(self.device_info, retval) + + expected_commands = [self.fake_sdscli_file + ' -c attach' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', + self.fake_sdscli_file + ' -c querydev' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f'] + + self.assertEqual(expected_commands, self.cmds) + + def test_disconnect_volume(self): + """Test the basic disconnect volume case.""" + self.connector.connect_volume(self.connection_properties) + self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached) + self.connector.disconnect_volume(self.connection_properties, + self.device_info) + self.assertEqual(False, HuaweiStorHyperConnectorTestCase.attached) + + expected_commands = [self.fake_sdscli_file + ' -c attach' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', + self.fake_sdscli_file + ' -c querydev' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', + self.fake_sdscli_file + ' -c detach' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f'] + + self.assertEqual(expected_commands, self.cmds) + + def test_is_volume_connected(self): + """Test if volume connected to host case.""" + self.connector.connect_volume(self.connection_properties) + self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached) + is_connected = self.connector.is_volume_connected( + 'volume-b2911673-863c-4380-a5f2-e1729eecfe3f') + self.assertEqual(HuaweiStorHyperConnectorTestCase.attached, + is_connected) + self.connector.disconnect_volume(self.connection_properties, + self.device_info) + self.assertEqual(False, HuaweiStorHyperConnectorTestCase.attached) + is_connected = self.connector.is_volume_connected( + 'volume-b2911673-863c-4380-a5f2-e1729eecfe3f') + self.assertEqual(HuaweiStorHyperConnectorTestCase.attached, + is_connected) + + expected_commands = [self.fake_sdscli_file + ' -c attach' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', + self.fake_sdscli_file + ' -c querydev' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', + self.fake_sdscli_file + ' -c querydev' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', + self.fake_sdscli_file + ' -c detach' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', + self.fake_sdscli_file + ' -c querydev' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f'] + + self.assertEqual(expected_commands, self.cmds) + + def test__analyze_output(self): + cliout = 'ret_code=0\ndev_addr=/dev/vdxxx\nret_desc="success"' + analyze_result = {'dev_addr': '/dev/vdxxx', + 'ret_desc': '"success"', + 'ret_code': '0'} + result = self.connector._analyze_output(cliout) + self.assertEqual(analyze_result, result) + + def test_connect_volume_fail(self): + """Test the fail connect volume case.""" + self.assertRaises(exception.BrickException, + self.connector_fail.connect_volume, + self.connection_properties) + expected_commands = [self.fake_sdscli_file + ' -c attach' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f'] + self.assertEqual(expected_commands, self.cmds) + + def test_disconnect_volume_fail(self): + """Test the fail disconnect volume case.""" + self.connector.connect_volume(self.connection_properties) + self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached) + self.assertRaises(exception.BrickException, + self.connector_fail.disconnect_volume, + self.connection_properties, + self.device_info) + + expected_commands = [self.fake_sdscli_file + ' -c attach' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', + self.fake_sdscli_file + ' -c querydev' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', + self.fake_sdscli_file + ' -c detach' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f'] + + self.assertEqual(expected_commands, self.cmds) + + def test_connect_volume_nocli(self): + """Test the fail connect volume case.""" + self.assertRaises(exception.BrickException, + self.connector_nocli.connect_volume, + self.connection_properties) + + def test_disconnect_volume_nocli(self): + """Test the fail disconnect volume case.""" + self.connector.connect_volume(self.connection_properties) + self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached) + self.assertRaises(exception.BrickException, + self.connector_nocli.disconnect_volume, + self.connection_properties, + self.device_info) + expected_commands = [self.fake_sdscli_file + ' -c attach' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', + self.fake_sdscli_file + ' -c querydev' + ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f'] + self.assertEqual(expected_commands, self.cmds) + + def test_extend_volume(self): + self.assertRaises(NotImplementedError, + self.connector.extend_volume, + self.connection_properties) diff --git a/os_brick/tests/initiator/connectors/test_iscsi.py b/os_brick/tests/initiator/connectors/test_iscsi.py new file mode 100644 index 000000000..0341edecf --- /dev/null +++ b/os_brick/tests/initiator/connectors/test_iscsi.py @@ -0,0 +1,1005 @@ +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. +import glob +import mock +import os +import testtools +import time + +from oslo_concurrency import processutils as putils + +from os_brick import exception +from os_brick.initiator.connectors import base +from os_brick.initiator.connectors import iscsi +from os_brick.initiator import host_driver +from os_brick.initiator import linuxscsi +from os_brick.privileged import rootwrap as priv_rootwrap +from os_brick.tests.initiator import test_connector + + +class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): + + def setUp(self): + super(ISCSIConnectorTestCase, self).setUp() + self.connector = iscsi.ISCSIConnector( + None, execute=self.fake_execute, use_multipath=False) + self.connector_with_multipath = iscsi.ISCSIConnector( + None, execute=self.fake_execute, use_multipath=True) + + mock.patch.object(self.connector._linuxscsi, 'get_name_from_path', + return_value="/dev/sdb").start() + self.addCleanup(mock.patch.stopall) + self._fake_iqn = 'iqn.1234-56.foo.bar:01:23456789abc' + + def generate_device(self, location, iqn, transport=None, lun=1): + dev_format = "ip-%s-iscsi-%s-lun-%s" % (location, iqn, lun) + if transport: + dev_format = "pci-0000:00:00.0-" + dev_format + fake_dev_path = "/dev/disk/by-path/" + dev_format + return fake_dev_path + + def iscsi_connection(self, volume, location, iqn): + return { + 'driver_volume_type': 'iscsi', + 'data': { + 'volume_id': volume['id'], + 'target_portal': location, + 'target_iqn': iqn, + 'target_lun': 1, + } + } + + def iscsi_connection_multipath(self, volume, locations, iqns, luns): + return { + 'driver_volume_type': 'iscsi', + 'data': { + 'volume_id': volume['id'], + 'target_portals': locations, + 'target_iqns': iqns, + 'target_luns': luns, + } + } + + def iscsi_connection_chap(self, volume, location, iqn, auth_method, + auth_username, auth_password, + discovery_auth_method, discovery_auth_username, + discovery_auth_password): + return { + 'driver_volume_type': 'iscsi', + 'data': { + 'auth_method': auth_method, + 'auth_username': auth_username, + 'auth_password': auth_password, + 'discovery_auth_method': discovery_auth_method, + 'discovery_auth_username': discovery_auth_username, + 'discovery_auth_password': discovery_auth_password, + 'target_lun': 1, + 'volume_id': volume['id'], + 'target_iqn': iqn, + 'target_portal': location, + } + } + + def _initiator_get_text(self, *arg, **kwargs): + text = ('## DO NOT EDIT OR REMOVE THIS FILE!\n' + '## If you remove this file, the iSCSI daemon ' + 'will not start.\n' + '## If you change the InitiatorName, existing ' + 'access control lists\n' + '## may reject this initiator. The InitiatorName must ' + 'be unique\n' + '## for each iSCSI initiator. Do NOT duplicate iSCSI ' + 'InitiatorNames.\n' + 'InitiatorName=%s' % self._fake_iqn) + return text, None + + def test_get_initiator(self): + def initiator_no_file(*args, **kwargs): + raise putils.ProcessExecutionError('No file') + + self.connector._execute = initiator_no_file + initiator = self.connector.get_initiator() + self.assertIsNone(initiator) + self.connector._execute = self._initiator_get_text + initiator = self.connector.get_initiator() + self.assertEqual(initiator, self._fake_iqn) + + def test_get_connector_properties(self): + with mock.patch.object(priv_rootwrap, 'execute') as mock_exec: + mock_exec.return_value = self._initiator_get_text() + multipath = True + enforce_multipath = True + props = iscsi.ISCSIConnector.get_connector_properties( + 'sudo', multipath=multipath, + enforce_multipath=enforce_multipath) + + expected_props = {'initiator': self._fake_iqn} + self.assertEqual(expected_props, props) + + @mock.patch.object(iscsi.ISCSIConnector, '_run_iscsiadm_bare') + def test_brick_iscsi_validate_transport(self, mock_iscsiadm): + sample_output = ('# BEGIN RECORD 2.0-872\n' + 'iface.iscsi_ifacename = %s.fake_suffix\n' + 'iface.net_ifacename = \n' + 'iface.ipaddress = \n' + 'iface.hwaddress = 00:53:00:00:53:00\n' + 'iface.transport_name = %s\n' + 'iface.initiatorname = \n' + '# END RECORD') + for tport in self.connector.supported_transports: + mock_iscsiadm.return_value = (sample_output % (tport, tport), '') + self.assertEqual(tport + '.fake_suffix', + self.connector._validate_iface_transport( + tport + '.fake_suffix')) + + mock_iscsiadm.return_value = ("", 'iscsiadm: Could not ' + 'read iface fake_transport (6)') + self.assertEqual('default', + self.connector._validate_iface_transport( + 'fake_transport')) + + def test_get_search_path(self): + search_path = self.connector.get_search_path() + expected = "/dev/disk/by-path" + self.assertEqual(expected, search_path) + + @mock.patch.object(os.path, 'exists', return_value=True) + @mock.patch.object(iscsi.ISCSIConnector, '_get_potential_volume_paths') + def test_get_volume_paths(self, mock_potential_paths, mock_exists): + name1 = 'volume-00000001-1' + vol = {'id': 1, 'name': name1} + location = '10.0.2.15:3260' + iqn = 'iqn.2010-10.org.openstack:%s' % name1 + + fake_path = ("/dev/disk/by-path/ip-%(ip)s-iscsi-%(iqn)s-lun-%(lun)s" % + {'ip': '10.0.2.15', 'iqn': iqn, 'lun': 1}) + fake_props = {} + fake_devices = [fake_path] + expected = fake_devices + mock_potential_paths.return_value = (fake_devices, fake_props) + + connection_properties = self.iscsi_connection(vol, [location], + [iqn]) + volume_paths = self.connector.get_volume_paths( + connection_properties['data']) + self.assertEqual(expected, volume_paths) + + @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device_path') + @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device') + def test_discover_mpath_device(self, mock_multipath_device, + mock_multipath_device_path): + location1 = '10.0.2.15:3260' + location2 = '[2001:db8::1]:3260' + name1 = 'volume-00000001-1' + name2 = 'volume-00000001-2' + iqn1 = 'iqn.2010-10.org.openstack:%s' % name1 + iqn2 = 'iqn.2010-10.org.openstack:%s' % name2 + fake_multipath_dev = '/dev/mapper/fake-multipath-dev' + fake_raw_dev = '/dev/disk/by-path/fake-raw-lun' + vol = {'id': 1, 'name': name1} + connection_properties = self.iscsi_connection_multipath( + vol, [location1, location2], [iqn1, iqn2], [1, 2]) + mock_multipath_device_path.return_value = fake_multipath_dev + mock_multipath_device.return_value = test_connector.FAKE_SCSI_WWN + (result_path, result_mpath_id) = ( + self.connector_with_multipath._discover_mpath_device( + test_connector.FAKE_SCSI_WWN, + connection_properties['data'], + fake_raw_dev)) + result = {'path': result_path, 'multipath_id': result_mpath_id} + expected_result = {'path': fake_multipath_dev, + 'multipath_id': test_connector.FAKE_SCSI_WWN} + self.assertEqual(expected_result, result) + + @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device_path') + @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device') + @mock.patch.object(os.path, 'realpath') + def test_discover_mpath_device_by_realpath(self, mock_realpath, + mock_multipath_device, + mock_multipath_device_path): + + FAKE_SCSI_WWN = '1234567890' + location1 = '10.0.2.15:3260' + location2 = '[2001:db8::1]:3260' + name1 = 'volume-00000001-1' + name2 = 'volume-00000001-2' + iqn1 = 'iqn.2010-10.org.openstack:%s' % name1 + iqn2 = 'iqn.2010-10.org.openstack:%s' % name2 + fake_multipath_dev = None + fake_raw_dev = '/dev/disk/by-path/fake-raw-lun' + vol = {'id': 1, 'name': name1} + connection_properties = self.iscsi_connection_multipath( + vol, [location1, location2], [iqn1, iqn2], [1, 2]) + mock_multipath_device_path.return_value = fake_multipath_dev + mock_multipath_device.return_value = { + 'device': '/dev/mapper/%s' % FAKE_SCSI_WWN} + mock_realpath.return_value = '/dev/sdvc' + (result_path, result_mpath_id) = ( + self.connector_with_multipath._discover_mpath_device( + FAKE_SCSI_WWN, + connection_properties['data'], + fake_raw_dev)) + mock_multipath_device.assert_called_with('/dev/sdvc') + result = {'path': result_path, 'multipath_id': result_mpath_id} + expected_result = {'path': '/dev/mapper/%s' % FAKE_SCSI_WWN, + 'multipath_id': FAKE_SCSI_WWN} + self.assertEqual(expected_result, result) + + @mock.patch('time.sleep', mock.Mock()) + def _test_connect_volume(self, extra_props, additional_commands, + transport=None, disconnect_mock=None): + # for making sure the /dev/disk/by-path is gone + exists_mock = mock.Mock() + exists_mock.return_value = True + os.path.exists = exists_mock + + location = '10.0.2.15:3260' + name = 'volume-00000001' + iqn = 'iqn.2010-10.org.openstack:%s' % name + vol = {'id': 1, 'name': name} + connection_info = self.iscsi_connection(vol, location, iqn) + for key, value in extra_props.items(): + connection_info['data'][key] = value + if transport is not None: + dev_list = self.generate_device(location, iqn, transport) + with mock.patch.object(glob, 'glob', return_value=[dev_list]): + device = self.connector.connect_volume(connection_info['data']) + else: + device = self.connector.connect_volume(connection_info['data']) + + dev_str = self.generate_device(location, iqn, transport) + self.assertEqual(device['type'], 'block') + self.assertEqual(device['path'], dev_str) + + self.count = 0 + + def mock_exists_effect(*args, **kwargs): + self.count = self.count + 1 + if self.count == 4: + return False + else: + return True + + if disconnect_mock is None: + disconnect_mock = mock_exists_effect + + with mock.patch.object(os.path, 'exists', + side_effect=disconnect_mock): + if transport is not None: + dev_list = self.generate_device(location, iqn, transport) + with mock.patch.object(glob, 'glob', return_value=[dev_list]): + self.connector.disconnect_volume(connection_info['data'], + device) + else: + self.connector.disconnect_volume(connection_info['data'], + device) + + expected_commands = [ + ('iscsiadm -m node -T %s -p %s' % (iqn, location)), + ('iscsiadm -m session'), + ('iscsiadm -m node -T %s -p %s --login' % (iqn, location)), + ('iscsiadm -m node -T %s -p %s --op update' + ' -n node.startup -v automatic' % (iqn, location)), + ('/lib/udev/scsi_id --page 0x83 --whitelisted %s' % dev_str), + ('blockdev --flushbufs /dev/sdb'), + ('tee -a /sys/block/sdb/device/delete'), + ('iscsiadm -m node -T %s -p %s --op update' + ' -n node.startup -v manual' % (iqn, location)), + ('iscsiadm -m node -T %s -p %s --logout' % (iqn, location)), + ('iscsiadm -m node -T %s -p %s --op delete' % + (iqn, location)), ] + additional_commands + + self.assertEqual(expected_commands, self.cmds) + + @testtools.skipUnless(os.path.exists('/dev/disk/by-path'), + 'Test requires /dev/disk/by-path') + def test_connect_volume(self): + self._test_connect_volume({}, []) + + @testtools.skipUnless(os.path.exists('/dev/disk/by-path'), + 'Test requires /dev/disk/by-path') + @mock.patch.object(iscsi.ISCSIConnector, '_get_transport') + def test_connect_volume_with_transport(self, mock_transport): + mock_transport.return_value = 'fake_transport' + self._test_connect_volume({}, [], 'fake_transport') + + @testtools.skipUnless(os.path.exists('/dev/disk/by-path'), + 'Test requires /dev/disk/by-path') + def test_connect_volume_with_alternative_targets(self): + location = '10.0.2.15:3260' + location2 = '[2001:db8::1]:3260' + iqn = 'iqn.2010-10.org.openstack:volume-00000001' + iqn2 = 'iqn.2010-10.org.openstack:volume-00000001-2' + extra_props = {'target_portals': [location, location2], + 'target_iqns': [iqn, iqn2], + 'target_luns': [1, 2]} + additional_commands = [('blockdev --flushbufs /dev/sdb'), + ('tee -a /sys/block/sdb/device/delete'), + ('iscsiadm -m node -T %s -p %s --op update' + ' -n node.startup -v manual' % + (iqn2, location2)), + ('iscsiadm -m node -T %s -p %s --logout' % + (iqn2, location2)), + ('iscsiadm -m node -T %s -p %s --op delete' % + (iqn2, location2))] + + def mock_exists_effect(*args, **kwargs): + self.count = self.count + 1 + # we have 2 targets in this test, so we need + # to make sure we remove and detect removal + # for both. + if (self.count == 4 or + self.count == 8): + return False + else: + return True + + self._test_connect_volume(extra_props, additional_commands, + disconnect_mock=mock_exists_effect) + + @testtools.skipUnless(os.path.exists('/dev/disk/by-path'), + 'Test requires /dev/disk/by-path') + @mock.patch.object(os.path, 'exists') + @mock.patch.object(iscsi.ISCSIConnector, '_run_iscsiadm') + def test_connect_volume_with_alternative_targets_primary_error( + self, mock_iscsiadm, mock_exists): + location = '10.0.2.15:3260' + location2 = '[2001:db8::1]:3260' + dev_loc2 = '2001:db8::1:3260' # udev location2 + name = 'volume-00000001' + iqn = 'iqn.2010-10.org.openstack:%s' % name + iqn2 = 'iqn.2010-10.org.openstack:%s-2' % name + vol = {'id': 1, 'name': name} + connection_info = self.iscsi_connection(vol, location, iqn) + connection_info['data']['target_portals'] = [location, location2] + connection_info['data']['target_iqns'] = [iqn, iqn2] + connection_info['data']['target_luns'] = [1, 2] + dev_str2 = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (dev_loc2, iqn2) + + def fake_run_iscsiadm(iscsi_properties, iscsi_command, **kwargs): + if iscsi_properties['target_portal'] == location: + if iscsi_command == ('--login',): + raise putils.ProcessExecutionError(None, None, 21) + return mock.DEFAULT + + mock_iscsiadm.side_effect = fake_run_iscsiadm + mock_exists.side_effect = lambda x: x == dev_str2 + device = self.connector.connect_volume(connection_info['data']) + self.assertEqual('block', device['type']) + self.assertEqual(dev_str2, device['path']) + props = connection_info['data'].copy() + for key in ('target_portals', 'target_iqns', 'target_luns'): + props.pop(key, None) + props['target_portal'] = location2 + props['target_iqn'] = iqn2 + props['target_lun'] = 2 + mock_iscsiadm.assert_any_call(props, ('--login',), + check_exit_code=[0, 255]) + + mock_iscsiadm.reset_mock() + with mock.patch.object(os.path, 'exists', + return_value=False): + self.connector.disconnect_volume(connection_info['data'], device) + props = connection_info['data'].copy() + for key in ('target_portals', 'target_iqns', 'target_luns'): + props.pop(key, None) + mock_iscsiadm.assert_any_call(props, ('--logout',), + check_exit_code=[0, 21, 255]) + props['target_portal'] = location2 + props['target_iqn'] = iqn2 + props['target_lun'] = 2 + mock_iscsiadm.assert_any_call(props, ('--logout',), + check_exit_code=[0, 21, 255]) + + @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') + @mock.patch.object(iscsi.ISCSIConnector, '_run_iscsiadm_bare') + @mock.patch.object(iscsi.ISCSIConnector, + '_get_target_portals_from_iscsiadm_output') + @mock.patch.object(iscsi.ISCSIConnector, '_connect_to_iscsi_portal') + @mock.patch.object(iscsi.ISCSIConnector, '_rescan_iscsi') + @mock.patch.object(iscsi.ISCSIConnector, '_rescan_multipath') + @mock.patch.object(os.path, 'exists', return_value=True) + @mock.patch.object(base.BaseLinuxConnector, '_discover_mpath_device') + def test_connect_volume_with_multipath( + self, mock_discover_mpath_device, exists_mock, + rescan_multipath_mock, rescan_iscsi_mock, connect_to_mock, + portals_mock, iscsiadm_mock, mock_iscsi_wwn): + mock_iscsi_wwn.return_value = test_connector.FAKE_SCSI_WWN + location = '10.0.2.15:3260' + name = 'volume-00000001' + iqn = 'iqn.2010-10.org.openstack:%s' % name + vol = {'id': 1, 'name': name} + connection_properties = self.iscsi_connection(vol, location, iqn) + mock_discover_mpath_device.return_value = ( + 'iqn.2010-10.org.openstack:%s' % name, + test_connector.FAKE_SCSI_WWN) + + self.connector_with_multipath = \ + iscsi.ISCSIConnector(None, use_multipath=True) + iscsiadm_mock.return_value = "%s %s" % (location, iqn) + portals_mock.return_value = [[location, iqn]] + + result = self.connector_with_multipath.connect_volume( + connection_properties['data']) + expected_result = {'multipath_id': test_connector.FAKE_SCSI_WWN, + 'path': 'iqn.2010-10.org.openstack:volume-00000001', + 'type': 'block', + 'scsi_wwn': test_connector.FAKE_SCSI_WWN} + self.assertEqual(expected_result, result) + + @mock.patch.object(iscsi.ISCSIConnector, + '_run_iscsiadm_update_discoverydb') + @mock.patch.object(os.path, 'exists', return_value=True) + def test_iscsi_portals_with_chap_discovery( + self, exists, update_discoverydb): + location = '10.0.2.15:3260' + name = 'volume-00000001' + iqn = 'iqn.2010-10.org.openstack:%s' % name + vol = {'id': 1, 'name': name} + auth_method = 'CHAP' + auth_username = 'fake_chap_username' + auth_password = 'fake_chap_password' + discovery_auth_method = 'CHAP' + discovery_auth_username = 'fake_chap_username' + discovery_auth_password = 'fake_chap_password' + connection_properties = self.iscsi_connection_chap( + vol, location, iqn, auth_method, auth_username, auth_password, + discovery_auth_method, discovery_auth_username, + discovery_auth_password) + self.connector_with_multipath = iscsi.ISCSIConnector( + None, execute=self.fake_execute, use_multipath=True) + self.cmds = [] + # The first call returns an error code = 6, mocking an empty + # discovery db. The second one mocks a successful return and the + # third one a dummy exit code, which will trigger the + # TargetPortalNotFound exception in connect_volume + update_discoverydb.side_effect = [ + putils.ProcessExecutionError(None, None, 6), + ("", ""), + putils.ProcessExecutionError(None, None, 9)] + + self.connector_with_multipath._discover_iscsi_portals( + connection_properties['data']) + update_discoverydb.assert_called_with(connection_properties['data']) + + expected_cmds = [ + 'iscsiadm -m discoverydb -t sendtargets -p %s --op new' % + location, + 'iscsiadm -m discoverydb -t sendtargets -p %s --discover' % + location] + self.assertEqual(expected_cmds, self.cmds) + + self.assertRaises(exception.TargetPortalNotFound, + self.connector_with_multipath.connect_volume, + connection_properties['data']) + + @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') + @mock.patch.object(os.path, 'exists', return_value=True) + @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') + @mock.patch.object(iscsi.ISCSIConnector, '_get_multipath_device_map', + return_value={}) + @mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_devices') + @mock.patch.object(iscsi.ISCSIConnector, '_run_multipath') + @mock.patch.object(iscsi.ISCSIConnector, '_get_multipath_iqns') + @mock.patch.object(base.BaseLinuxConnector, '_discover_mpath_device') + @mock.patch.object(linuxscsi.LinuxSCSI, 'process_lun_id') + def test_connect_volume_with_multiple_portals( + self, mock_process_lun_id, mock_discover_mpath_device, + mock_get_iqn, mock_run_multipath, mock_iscsi_devices, + mock_get_device_map, mock_devices, mock_exists, mock_scsi_wwn): + mock_scsi_wwn.return_value = test_connector.FAKE_SCSI_WWN + location1 = '10.0.2.15:3260' + location2 = '[2001:db8::1]:3260' + dev_loc2 = '2001:db8::1:3260' # udev location2 + name1 = 'volume-00000001-1' + name2 = 'volume-00000001-2' + iqn1 = 'iqn.2010-10.org.openstack:%s' % name1 + iqn2 = 'iqn.2010-10.org.openstack:%s' % name2 + fake_multipath_dev = '/dev/mapper/fake-multipath-dev' + vol = {'id': 1, 'name': name1} + connection_properties = self.iscsi_connection_multipath( + vol, [location1, location2], [iqn1, iqn2], [1, 2]) + devs = ['/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location1, iqn1), + '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (dev_loc2, iqn2)] + mock_devices.return_value = devs + mock_iscsi_devices.return_value = devs + mock_get_iqn.return_value = [iqn1, iqn2] + mock_discover_mpath_device.return_value = ( + fake_multipath_dev, test_connector.FAKE_SCSI_WWN) + mock_process_lun_id.return_value = [1, 2] + + result = self.connector_with_multipath.connect_volume( + connection_properties['data']) + expected_result = {'multipath_id': test_connector.FAKE_SCSI_WWN, + 'path': fake_multipath_dev, 'type': 'block', + 'scsi_wwn': test_connector.FAKE_SCSI_WWN} + cmd_format = 'iscsiadm -m node -T %s -p %s --%s' + expected_commands = [cmd_format % (iqn1, location1, 'login'), + cmd_format % (iqn2, location2, 'login')] + self.assertEqual(expected_result, result) + for command in expected_commands: + self.assertIn(command, self.cmds) + + self.cmds = [] + self.connector_with_multipath.disconnect_volume( + connection_properties['data'], result) + expected_commands = [cmd_format % (iqn1, location1, 'logout'), + cmd_format % (iqn2, location2, 'logout')] + for command in expected_commands: + self.assertIn(command, self.cmds) + + @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') + @mock.patch.object(os.path, 'exists') + @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') + @mock.patch.object(iscsi.ISCSIConnector, '_get_multipath_device_map', + return_value={}) + @mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_devices') + @mock.patch.object(iscsi.ISCSIConnector, '_rescan_multipath') + @mock.patch.object(iscsi.ISCSIConnector, '_run_multipath') + @mock.patch.object(iscsi.ISCSIConnector, '_get_multipath_iqns') + @mock.patch.object(iscsi.ISCSIConnector, '_run_iscsiadm') + @mock.patch.object(base.BaseLinuxConnector, '_discover_mpath_device') + @mock.patch.object(linuxscsi.LinuxSCSI, 'process_lun_id') + def test_connect_volume_with_multiple_portals_primary_error( + self, mock_process_lun_id, mock_discover_mpath_device, + mock_iscsiadm, mock_get_iqn, mock_run_multipath, + mock_rescan_multipath, mock_iscsi_devices, + mock_get_multipath_device_map, mock_devices, mock_exists, + mock_scsi_wwn): + mock_scsi_wwn.return_value = test_connector.FAKE_SCSI_WWN + location1 = '10.0.2.15:3260' + location2 = '[2001:db8::1]:3260' + dev_loc2 = '2001:db8::1:3260' # udev location2 + name1 = 'volume-00000001-1' + name2 = 'volume-00000001-2' + iqn1 = 'iqn.2010-10.org.openstack:%s' % name1 + iqn2 = 'iqn.2010-10.org.openstack:%s' % name2 + fake_multipath_dev = '/dev/mapper/fake-multipath-dev' + vol = {'id': 1, 'name': name1} + connection_properties = self.iscsi_connection_multipath( + vol, [location1, location2], [iqn1, iqn2], [1, 2]) + dev1 = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location1, iqn1) + dev2 = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (dev_loc2, iqn2) + + def fake_run_iscsiadm(iscsi_properties, iscsi_command, **kwargs): + if iscsi_properties['target_portal'] == location1: + if iscsi_command == ('--login',): + raise putils.ProcessExecutionError(None, None, 21) + return mock.DEFAULT + + mock_exists.side_effect = lambda x: x != dev1 + mock_devices.return_value = [dev2] + mock_iscsi_devices.return_value = [dev2] + mock_get_iqn.return_value = [iqn2] + mock_iscsiadm.side_effect = fake_run_iscsiadm + + mock_discover_mpath_device.return_value = ( + fake_multipath_dev, test_connector.FAKE_SCSI_WWN) + mock_process_lun_id.return_value = [1, 2] + + props = connection_properties['data'].copy() + result = self.connector_with_multipath.connect_volume( + connection_properties['data']) + + expected_result = {'multipath_id': test_connector.FAKE_SCSI_WWN, + 'path': fake_multipath_dev, 'type': 'block', + 'scsi_wwn': test_connector.FAKE_SCSI_WWN} + self.assertEqual(expected_result, result) + props['target_portal'] = location1 + props['target_iqn'] = iqn1 + mock_iscsiadm.assert_any_call(props, ('--login',), + check_exit_code=[0, 255]) + props['target_portal'] = location2 + props['target_iqn'] = iqn2 + mock_iscsiadm.assert_any_call(props, ('--login',), + check_exit_code=[0, 255]) + + mock_iscsiadm.reset_mock() + self.connector_with_multipath.disconnect_volume( + connection_properties['data'], result) + + props = connection_properties['data'].copy() + props['target_portal'] = location1 + props['target_iqn'] = iqn1 + mock_iscsiadm.assert_any_call(props, ('--logout',), + check_exit_code=[0, 21, 255]) + props['target_portal'] = location2 + props['target_iqn'] = iqn2 + mock_iscsiadm.assert_any_call(props, ('--logout',), + check_exit_code=[0, 21, 255]) + + @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') + @mock.patch.object(os.path, 'exists', return_value=True) + @mock.patch.object(iscsi.ISCSIConnector, + '_get_target_portals_from_iscsiadm_output') + @mock.patch.object(iscsi.ISCSIConnector, '_connect_to_iscsi_portal') + @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') + @mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_devices') + @mock.patch.object(iscsi.ISCSIConnector, '_rescan_multipath') + @mock.patch.object(iscsi.ISCSIConnector, '_run_multipath') + @mock.patch.object(base.BaseLinuxConnector, '_discover_mpath_device') + def test_connect_volume_with_multipath_connecting( + self, mock_discover_mpath_device, mock_run_multipath, + mock_rescan_multipath, mock_iscsi_devices, mock_devices, + mock_connect, mock_portals, mock_exists, mock_scsi_wwn): + mock_scsi_wwn.return_value = test_connector.FAKE_SCSI_WWN + location1 = '10.0.2.15:3260' + location2 = '[2001:db8::1]:3260' + dev_loc2 = '2001:db8::1:3260' # udev location2 + name1 = 'volume-00000001-1' + name2 = 'volume-00000001-2' + iqn1 = 'iqn.2010-10.org.openstack:%s' % name1 + iqn2 = 'iqn.2010-10.org.openstack:%s' % name2 + fake_multipath_dev = '/dev/mapper/fake-multipath-dev' + vol = {'id': 1, 'name': name1} + connection_properties = self.iscsi_connection(vol, location1, iqn1) + devs = ['/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location1, iqn1), + '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (dev_loc2, iqn2)] + mock_devices.return_value = devs + mock_iscsi_devices.return_value = devs + mock_portals.return_value = [[location1, iqn1], [location2, iqn1], + [location2, iqn2]] + mock_discover_mpath_device.return_value = ( + fake_multipath_dev, test_connector.FAKE_SCSI_WWN) + + result = self.connector_with_multipath.connect_volume( + connection_properties['data']) + expected_result = {'multipath_id': test_connector.FAKE_SCSI_WWN, + 'path': fake_multipath_dev, 'type': 'block', + 'scsi_wwn': test_connector.FAKE_SCSI_WWN} + props1 = connection_properties['data'].copy() + props2 = connection_properties['data'].copy() + locations = list(set([location1, location2])) # order may change + props1['target_portal'] = locations[0] + props2['target_portal'] = locations[1] + expected_calls = [mock.call(props1), mock.call(props2)] + self.assertEqual(expected_result, result) + self.assertEqual(expected_calls, mock_connect.call_args_list) + + @mock.patch.object(os.path, 'exists', return_value=True) + @mock.patch.object(iscsi.ISCSIConnector, + '_get_target_portals_from_iscsiadm_output') + @mock.patch.object(iscsi.ISCSIConnector, '_connect_to_iscsi_portal') + @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') + @mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_devices') + @mock.patch.object(iscsi.ISCSIConnector, '_rescan_multipath') + @mock.patch.object(iscsi.ISCSIConnector, '_run_multipath') + def test_connect_volume_multipath_failed_iscsi_login( + self, mock_run_multipath, mock_rescan_multipath, + mock_iscsi_devices, mock_devices, + mock_connect, mock_portals, mock_exists): + location1 = '10.0.2.15:3260' + location2 = '10.0.3.15:3260' + name1 = 'volume-00000001-1' + name2 = 'volume-00000001-2' + iqn1 = 'iqn.2010-10.org.openstack:%s' % name1 + iqn2 = 'iqn.2010-10.org.openstack:%s' % name2 + vol = {'id': 1, 'name': name1} + connection_properties = self.iscsi_connection(vol, location1, iqn1) + devs = ['/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location1, iqn1), + '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (location2, iqn2)] + mock_devices.return_value = devs + mock_iscsi_devices.return_value = devs + mock_portals.return_value = [[location1, iqn1], [location2, iqn1], + [location2, iqn2]] + + mock_connect.return_value = False + self.assertRaises(exception.FailedISCSITargetPortalLogin, + self.connector_with_multipath.connect_volume, + connection_properties['data']) + + @mock.patch.object(iscsi.ISCSIConnector, '_connect_to_iscsi_portal') + def test_connect_volume_failed_iscsi_login(self, mock_connect): + location1 = '10.0.2.15:3260' + name1 = 'volume-00000001-1' + iqn1 = 'iqn.2010-10.org.openstack:%s' % name1 + vol = {'id': 1, 'name': name1} + connection_properties = self.iscsi_connection(vol, location1, iqn1) + + mock_connect.return_value = False + self.assertRaises(exception.FailedISCSITargetPortalLogin, + self.connector.connect_volume, + connection_properties['data']) + + @mock.patch.object(time, 'sleep') + @mock.patch.object(os.path, 'exists', return_value=False) + def test_connect_volume_with_not_found_device(self, exists_mock, + sleep_mock): + location = '10.0.2.15:3260' + name = 'volume-00000001' + iqn = 'iqn.2010-10.org.openstack:%s' % name + vol = {'id': 1, 'name': name} + connection_info = self.iscsi_connection(vol, location, iqn) + self.assertRaises(exception.VolumeDeviceNotFound, + self.connector.connect_volume, + connection_info['data']) + + def test_get_target_portals_from_iscsiadm_output(self): + connector = self.connector + test_output = '''10.15.84.19:3260 iqn.1992-08.com.netapp:sn.33615311 + 10.15.85.19:3260 iqn.1992-08.com.netapp:sn.33615311''' + res = connector._get_target_portals_from_iscsiadm_output(test_output) + ip_iqn1 = ['10.15.84.19:3260', 'iqn.1992-08.com.netapp:sn.33615311'] + ip_iqn2 = ['10.15.85.19:3260', 'iqn.1992-08.com.netapp:sn.33615311'] + expected = [ip_iqn1, ip_iqn2] + self.assertEqual(expected, res) + + @mock.patch.object(os, 'walk') + def test_get_iscsi_devices(self, walk_mock): + paths = [('ip-10.0.0.1:3260-iscsi-iqn.2013-01.ro.' + 'com.netapp:node.netapp02-lun-0')] + walk_mock.return_value = [(['.'], ['by-path'], paths)] + self.assertEqual(self.connector._get_iscsi_devices(), paths) + + @mock.patch.object(os, 'walk', return_value=[]) + def test_get_iscsi_devices_with_empty_dir(self, walk_mock): + self.assertEqual(self.connector._get_iscsi_devices(), []) + + @mock.patch.object(os.path, 'realpath') + @mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_devices') + def test_get_multipath_iqns(self, get_iscsi_mock, realpath_mock): + paths = [('ip-10.0.0.1:3260-iscsi-iqn.2013-01.ro.' + 'com.netapp:node.netapp02-lun-0')] + devpath = '/dev/disk/by-path/%s' % paths[0] + realpath_mock.return_value = devpath + get_iscsi_mock.return_value = paths + mpath_map = {devpath: paths[0]} + self.assertEqual(self.connector._get_multipath_iqns([paths[0]], + mpath_map), + ['iqn.2013-01.ro.com.netapp:node.netapp02']) + + @mock.patch.object(iscsi.ISCSIConnector, '_run_multipath') + def test_get_multipath_device_map(self, multipath_mock): + multipath_mock.return_value = [ + "Mar 17 14:32:37 | sda: No fc_host device for 'host-1'\n" + "mpathb (36e00000000010001) dm-4 IET ,VIRTUAL-DISK\n" + "size=1.0G features='0' hwhandler='0' wp=rw\n" + "|-+- policy='service-time 0' prio=0 status=active\n" + "| `- 2:0:0:1 sda 8:0 active undef running\n" + "`-+- policy='service-time 0' prio=0 status=enabled\n" + " `- 3:0:0:1 sdb 8:16 active undef running\n"] + expected = {'/dev/sda': '/dev/mapper/mpathb', + '/dev/sdb': '/dev/mapper/mpathb'} + self.assertEqual(expected, self.connector._get_multipath_device_map()) + + @mock.patch.object(iscsi.ISCSIConnector, '_get_multipath_device_map') + @mock.patch.object(iscsi.ISCSIConnector, + '_get_target_portals_from_iscsiadm_output') + @mock.patch.object(iscsi.ISCSIConnector, '_rescan_iscsi') + @mock.patch.object(iscsi.ISCSIConnector, '_rescan_multipath') + @mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_devices') + @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') + @mock.patch.object(iscsi.ISCSIConnector, + '_disconnect_from_iscsi_portal') + @mock.patch.object(iscsi.ISCSIConnector, '_get_multipath_iqns') + @mock.patch.object(os.path, 'exists', return_value=True) + def test_disconnect_volume_multipath_iscsi( + self, exists_mock, multipath_iqn_mock, disconnect_mock, + get_all_devices_mock, get_iscsi_devices_mock, + rescan_multipath_mock, rescan_iscsi_mock, get_portals_mock, + get_multipath_device_map_mock): + iqn1 = 'iqn.2013-01.ro.com.netapp:node.netapp01' + iqn2 = 'iqn.2013-01.ro.com.netapp:node.netapp02' + iqns = [iqn1, iqn2] + portal = '10.0.0.1:3260' + dev = ('ip-%s-iscsi-%s-lun-0' % (portal, iqn1)) + + get_portals_mock.return_value = [[portal, iqn1]] + multipath_iqn_mock.return_value = iqns + get_all_devices_mock.return_value = [dev, '/dev/mapper/md-1'] + get_multipath_device_map_mock.return_value = {dev: '/dev/mapper/md-3'} + get_iscsi_devices_mock.return_value = [] + fake_property = {'target_portal': portal, + 'target_iqn': iqn1} + self.connector._disconnect_volume_multipath_iscsi(fake_property, + 'fake/multipath') + # Target in use by other mp devices, don't disconnect + self.assertFalse(disconnect_mock.called) + + @mock.patch.object(iscsi.ISCSIConnector, + '_get_target_portals_from_iscsiadm_output') + @mock.patch.object(iscsi.ISCSIConnector, '_rescan_iscsi') + @mock.patch.object(iscsi.ISCSIConnector, '_rescan_multipath') + @mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_devices') + @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') + @mock.patch.object(iscsi.ISCSIConnector, + '_disconnect_from_iscsi_portal') + @mock.patch.object(iscsi.ISCSIConnector, '_get_multipath_device_map') + @mock.patch.object(iscsi.ISCSIConnector, '_get_multipath_iqns') + @mock.patch.object(os.path, 'exists', return_value=True) + def test_disconnect_volume_multipath_iscsi_other_targets( + self, exists_mock, multipath_iqn_mock, get_multipath_map_mock, + disconnect_mock, get_all_devices_mock, get_iscsi_devices_mock, + rescan_multipath_mock, rescan_iscsi_mock, get_portals_mock): + iqn1 = 'iqn.2010-10.org.openstack:target-1' + iqn2 = 'iqn.2010-10.org.openstack:target-2' + portal = '10.0.0.1:3260' + dev2 = ('ip-%s-iscsi-%s-lun-0' % (portal, iqn2)) + + # Multiple targets are discovered, but only block devices for target-1 + # is deleted and target-2 is in use. + get_portals_mock.return_value = [[portal, iqn1], [portal, iqn2]] + multipath_iqn_mock.return_value = [iqn2, iqn2] + get_all_devices_mock.return_value = [dev2, '/dev/mapper/md-1'] + get_multipath_map_mock.return_value = {dev2: '/dev/mapper/md-3'} + get_iscsi_devices_mock.return_value = [dev2] + fake_property = {'target_portal': portal, + 'target_iqn': iqn1} + self.connector._disconnect_volume_multipath_iscsi(fake_property, + 'fake/multipath') + # Only target-1 should be disconneced. + disconnect_mock.assert_called_once_with(fake_property) + + @mock.patch.object(iscsi.ISCSIConnector, '_get_multipath_device_map', + return_value={}) + @mock.patch.object(iscsi.ISCSIConnector, + '_get_target_portals_from_iscsiadm_output') + @mock.patch.object(iscsi.ISCSIConnector, '_rescan_iscsi') + @mock.patch.object(iscsi.ISCSIConnector, '_rescan_multipath') + @mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_devices', + return_value=[]) + @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices', + return_value=[]) + @mock.patch.object(iscsi.ISCSIConnector, + '_disconnect_from_iscsi_portal') + @mock.patch.object(os.path, 'exists', return_value=True) + def test_disconnect_volume_multipath_iscsi_without_other_mp_devices( + self, exists_mock, disconnect_mock, get_all_devices_mock, + get_iscsi_devices_mock, rescan_multipath_mock, rescan_iscsi_mock, + get_portals_mock, get_multipath_device_map_mock): + portal = '10.0.2.15:3260' + name = 'volume-00000001' + iqn = 'iqn.2010-10.org.openstack:%s' % name + + get_portals_mock.return_value = [[portal, iqn]] + fake_property = {'target_portal': portal, + 'target_iqn': iqn} + self.connector._disconnect_volume_multipath_iscsi(fake_property, + 'fake/multipath') + # Target not in use by other mp devices, disconnect + disconnect_mock.assert_called_once_with(fake_property) + + @mock.patch.object(iscsi.ISCSIConnector, '_get_multipath_device_map', + return_value={}) + @mock.patch.object(iscsi.ISCSIConnector, + '_get_target_portals_from_iscsiadm_output') + @mock.patch.object(iscsi.ISCSIConnector, '_rescan_iscsi') + @mock.patch.object(iscsi.ISCSIConnector, '_rescan_multipath') + @mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_devices') + @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') + @mock.patch.object(iscsi.ISCSIConnector, + '_disconnect_from_iscsi_portal') + @mock.patch.object(os.path, 'exists', return_value=False) + def test_disconnect_volume_multipath_iscsi_with_invalid_symlink( + self, exists_mock, disconnect_mock, get_all_devices_mock, + get_iscsi_devices_mock, rescan_multipath_mock, rescan_iscsi_mock, + get_portals_mock, get_multipath_device_map_mock): + # Simulate a broken symlink by returning False for os.path.exists(dev) + portal = '10.0.0.1:3260' + name = 'volume-00000001' + iqn = 'iqn.2010-10.org.openstack:%s' % name + dev = ('ip-%s-iscsi-%s-lun-0' % (portal, iqn)) + + get_portals_mock.return_value = [[portal, iqn]] + get_all_devices_mock.return_value = [dev, '/dev/mapper/md-1'] + get_iscsi_devices_mock.return_value = [] + + fake_property = {'target_portal': portal, + 'target_iqn': iqn} + self.connector._disconnect_volume_multipath_iscsi(fake_property, + 'fake/multipath') + # Target not in use by other mp devices, disconnect + disconnect_mock.assert_called_once_with(fake_property) + + def test_iscsiadm_discover_parsing(self): + # Ensure that parsing iscsiadm discover ignores cruft. + + targets = [ + ["192.168.204.82:3260,1", + ("iqn.2010-10.org.openstack:volume-" + "f9b12623-6ce3-4dac-a71f-09ad4249bdd3")], + ["192.168.204.82:3261,1", + ("iqn.2010-10.org.openstack:volume-" + "f9b12623-6ce3-4dac-a71f-09ad4249bdd4")]] + + # This slight wonkiness brought to you by pep8, as the actual + # example output runs about 97 chars wide. + sample_input = """Loading iscsi modules: done +Starting iSCSI initiator service: done +Setting up iSCSI targets: unused +%s %s +%s %s +""" % (targets[0][0], targets[0][1], targets[1][0], targets[1][1]) + out = self.connector.\ + _get_target_portals_from_iscsiadm_output(sample_input) + self.assertEqual(out, targets) + + def test_sanitize_log_run_iscsiadm(self): + # Tests that the parameters to the _run_iscsiadm function + # are sanitized for when passwords are logged. + def fake_debug(*args, **kwargs): + self.assertIn('node.session.auth.password', args[0]) + self.assertNotIn('scrubme', args[0]) + + volume = {'id': 'fake_uuid'} + connection_info = self.iscsi_connection(volume, + "10.0.2.15:3260", + "fake_iqn") + + iscsi_properties = connection_info['data'] + with mock.patch.object(iscsi.LOG, 'debug', + side_effect=fake_debug) as debug_mock: + self.connector._iscsiadm_update(iscsi_properties, + 'node.session.auth.password', + 'scrubme') + + # we don't care what the log message is, we just want to make sure + # our stub method is called which asserts the password is scrubbed + self.assertTrue(debug_mock.called) + + @mock.patch.object(iscsi.ISCSIConnector, 'get_volume_paths') + def test_extend_volume_no_path(self, mock_volume_paths): + mock_volume_paths.return_value = [] + volume = {'id': 'fake_uuid'} + connection_info = self.iscsi_connection(volume, + "10.0.2.15:3260", + "fake_iqn") + + self.assertRaises(exception.VolumePathsNotFound, + self.connector.extend_volume, + connection_info['data']) + + @mock.patch.object(linuxscsi.LinuxSCSI, 'extend_volume') + @mock.patch.object(iscsi.ISCSIConnector, 'get_volume_paths') + def test_extend_volume(self, mock_volume_paths, mock_scsi_extend): + fake_new_size = 1024 + mock_volume_paths.return_value = ['/dev/vdx'] + mock_scsi_extend.return_value = fake_new_size + volume = {'id': 'fake_uuid'} + connection_info = self.iscsi_connection(volume, + "10.0.2.15:3260", + "fake_iqn") + new_size = self.connector.extend_volume(connection_info['data']) + self.assertEqual(fake_new_size, new_size) + + @mock.patch.object(os.path, 'isdir') + def test_get_all_available_volumes_path_not_dir(self, mock_isdir): + mock_isdir.return_value = False + expected = [] + actual = self.connector.get_all_available_volumes() + self.assertItemsEqual(expected, actual) + + @mock.patch.object(iscsi.ISCSIConnector, '_discover_iscsi_portals') + def test_get_potential_paths_failure_mpath_single_target(self, + mock_discover): + connection_properties = { + 'target_portal': '10.0.2.15:3260' + } + self.connector.use_multipath = True + mock_discover.side_effect = exception.BrickException() + self.assertRaises(exception.TargetPortalNotFound, + self.connector._get_potential_volume_paths, + connection_properties) + + @mock.patch.object(iscsi.ISCSIConnector, '_discover_iscsi_portals') + def test_get_potential_paths_failure_mpath_multi_target(self, + mock_discover): + connection_properties = { + 'target_portals': ['10.0.2.15:3260', '10.0.3.15:3260'] + } + self.connector.use_multipath = True + mock_discover.side_effect = exception.BrickException() + self.assertRaises(exception.TargetPortalsNotFound, + self.connector._get_potential_volume_paths, + connection_properties) diff --git a/os_brick/tests/initiator/connectors/test_local.py b/os_brick/tests/initiator/connectors/test_local.py new file mode 100644 index 000000000..bc0718b3d --- /dev/null +++ b/os_brick/tests/initiator/connectors/test_local.py @@ -0,0 +1,58 @@ +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +from os_brick.initiator.connectors import local +from os_brick.tests.initiator import test_connector + + +class LocalConnectorTestCase(test_connector.ConnectorTestCase): + + def setUp(self): + super(LocalConnectorTestCase, self).setUp() + self.connection_properties = {'name': 'foo', + 'device_path': '/tmp/bar'} + self.connector = local.LocalConnector(None) + + def test_get_connector_properties(self): + props = local.LocalConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + + def test_get_search_path(self): + actual = self.connector.get_search_path() + self.assertIsNone(actual) + + def test_get_volume_paths(self): + expected = [self.connection_properties['device_path']] + actual = self.connector.get_volume_paths( + self.connection_properties) + self.assertEqual(expected, actual) + + def test_connect_volume(self): + cprops = self.connection_properties + dev_info = self.connector.connect_volume(cprops) + self.assertEqual(dev_info['type'], 'local') + self.assertEqual(dev_info['path'], cprops['device_path']) + + def test_connect_volume_with_invalid_connection_data(self): + cprops = {} + self.assertRaises(ValueError, + self.connector.connect_volume, cprops) + + def test_extend_volume(self): + self.assertRaises(NotImplementedError, + self.connector.extend_volume, + self.connection_properties) diff --git a/os_brick/tests/initiator/connectors/test_rbd.py b/os_brick/tests/initiator/connectors/test_rbd.py new file mode 100644 index 000000000..aeb18a3ee --- /dev/null +++ b/os_brick/tests/initiator/connectors/test_rbd.py @@ -0,0 +1,126 @@ +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. +import mock + +from oslo_utils import encodeutils + +from os_brick.initiator.connectors import rbd +from os_brick.initiator import linuxrbd +from os_brick.privileged import rootwrap as priv_rootwrap +from os_brick.tests.initiator import test_connector + + +class RBDConnectorTestCase(test_connector.ConnectorTestCase): + + def setUp(self): + super(RBDConnectorTestCase, self).setUp() + + self.user = 'fake_user' + self.pool = 'fake_pool' + self.volume = 'fake_volume' + + self.connection_properties = { + 'auth_username': self.user, + 'name': '%s/%s' % (self.pool, self.volume), + } + + def test_get_search_path(self): + rbd_connector = rbd.RBDConnector(None) + path = rbd_connector.get_search_path() + self.assertIsNone(path) + + @mock.patch('os_brick.initiator.linuxrbd.rbd') + @mock.patch('os_brick.initiator.linuxrbd.rados') + def test_get_volume_paths(self, mock_rados, mock_rbd): + rbd_connector = rbd.RBDConnector(None) + expected = [] + actual = rbd_connector.get_volume_paths(self.connection_properties) + self.assertEqual(expected, actual) + + def test_get_connector_properties(self): + props = rbd.RBDConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {'do_local_attach': False} + self.assertEqual(expected_props, props) + + @mock.patch('os_brick.initiator.linuxrbd.rbd') + @mock.patch('os_brick.initiator.linuxrbd.rados') + def test_connect_volume(self, mock_rados, mock_rbd): + """Test the connect volume case.""" + rbd_connector = rbd.RBDConnector(None) + device_info = rbd_connector.connect_volume(self.connection_properties) + + # Ensure rados is instantiated correctly + mock_rados.Rados.assert_called_once_with( + clustername='ceph', + rados_id=encodeutils.safe_encode(self.user), + conffile='/etc/ceph/ceph.conf') + + # Ensure correct calls to connect to cluster + self.assertEqual(1, mock_rados.Rados.return_value.connect.call_count) + mock_rados.Rados.return_value.open_ioctx.assert_called_once_with( + encodeutils.safe_encode(self.pool)) + + # Ensure rbd image is instantiated correctly + mock_rbd.Image.assert_called_once_with( + mock_rados.Rados.return_value.open_ioctx.return_value, + encodeutils.safe_encode(self.volume), read_only=False, + snapshot=None) + + # Ensure expected object is returned correctly + self.assertTrue(isinstance(device_info['path'], + linuxrbd.RBDVolumeIOWrapper)) + + @mock.patch.object(priv_rootwrap, 'execute') + def test_connect_local_volume(self, mock_execute): + rbd_connector = rbd.RBDConnector(None, do_local_attach=True) + conn = {'name': 'pool/image'} + device_info = rbd_connector.connect_volume(conn) + execute_call1 = mock.call('which', 'rbd') + cmd = ['rbd', 'map', 'image', '--pool', 'pool'] + execute_call2 = mock.call(*cmd, root_helper=None, run_as_root=True) + mock_execute.assert_has_calls([execute_call1, execute_call2]) + expected_info = {'path': '/dev/rbd/pool/image', + 'type': 'block'} + self.assertEqual(expected_info, device_info) + + @mock.patch('os_brick.initiator.linuxrbd.rbd') + @mock.patch('os_brick.initiator.linuxrbd.rados') + @mock.patch.object(linuxrbd.RBDVolumeIOWrapper, 'close') + def test_disconnect_volume(self, volume_close, mock_rados, mock_rbd): + """Test the disconnect volume case.""" + rbd_connector = rbd.RBDConnector(None) + device_info = rbd_connector.connect_volume(self.connection_properties) + rbd_connector.disconnect_volume( + self.connection_properties, device_info) + + self.assertEqual(1, volume_close.call_count) + + @mock.patch.object(priv_rootwrap, 'execute') + def test_disconnect_local_volume(self, mock_execute): + rbd_connector = rbd.RBDConnector(None, do_local_attach=True) + conn = {'name': 'pool/image'} + rbd_connector.disconnect_volume(conn, None) + + dev_name = '/dev/rbd/pool/image' + cmd = ['rbd', 'unmap', dev_name] + mock_execute.assert_called_once_with(*cmd, root_helper=None, + run_as_root=True) + + def test_extend_volume(self): + rbd_connector = rbd.RBDConnector(None) + self.assertRaises(NotImplementedError, + rbd_connector.extend_volume, + self.connection_properties) diff --git a/os_brick/tests/initiator/connectors/test_remotefs.py b/os_brick/tests/initiator/connectors/test_remotefs.py new file mode 100644 index 000000000..534da585e --- /dev/null +++ b/os_brick/tests/initiator/connectors/test_remotefs.py @@ -0,0 +1,77 @@ +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. +import mock + +from os_brick.initiator.connectors import remotefs +from os_brick.remotefs import remotefs as remotefs_client +from os_brick.tests.initiator import test_connector + + +class RemoteFsConnectorTestCase(test_connector.ConnectorTestCase): + """Test cases for Remote FS initiator class.""" + TEST_DEV = '172.18.194.100:/var/nfs' + TEST_PATH = '/mnt/test/df0808229363aad55c27da50c38d6328' + TEST_BASE = '/mnt/test' + TEST_NAME = '9c592d52-ce47-4263-8c21-4ecf3c029cdb' + + def setUp(self): + super(RemoteFsConnectorTestCase, self).setUp() + self.connection_properties = { + 'export': self.TEST_DEV, + 'name': self.TEST_NAME} + self.connector = remotefs.RemoteFsConnector( + 'nfs', root_helper='sudo', + nfs_mount_point_base=self.TEST_BASE, + nfs_mount_options='vers=3') + + @mock.patch('os_brick.remotefs.remotefs.ScalityRemoteFsClient') + def test_init_with_scality(self, mock_scality_remotefs_client): + remotefs.RemoteFsConnector('scality', root_helper='sudo') + self.assertEqual(1, mock_scality_remotefs_client.call_count) + + def test_get_connector_properties(self): + props = remotefs.RemoteFsConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + + def test_get_search_path(self): + expected = self.TEST_BASE + actual = self.connector.get_search_path() + self.assertEqual(expected, actual) + + @mock.patch.object(remotefs_client.RemoteFsClient, 'mount') + def test_get_volume_paths(self, mock_mount): + path = ("%(path)s/%(name)s" % {'path': self.TEST_PATH, + 'name': self.TEST_NAME}) + expected = [path] + actual = self.connector.get_volume_paths(self.connection_properties) + self.assertEqual(expected, actual) + + @mock.patch.object(remotefs_client.RemoteFsClient, 'mount') + @mock.patch.object(remotefs_client.RemoteFsClient, 'get_mount_point', + return_value="something") + def test_connect_volume(self, mount_point_mock, mount_mock): + """Test the basic connect volume case.""" + self.connector.connect_volume(self.connection_properties) + + def test_disconnect_volume(self): + """Nothing should happen here -- make sure it doesn't blow up.""" + self.connector.disconnect_volume(self.connection_properties, {}) + + def test_extend_volume(self): + self.assertRaises(NotImplementedError, + self.connector.extend_volume, + self.connection_properties) diff --git a/os_brick/tests/initiator/connectors/test_scaleio.py b/os_brick/tests/initiator/connectors/test_scaleio.py new file mode 100644 index 000000000..cfc0b0069 --- /dev/null +++ b/os_brick/tests/initiator/connectors/test_scaleio.py @@ -0,0 +1,279 @@ +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. +import json +import mock +import os +import requests +import six + +from oslo_concurrency import processutils as putils + +from os_brick import exception +from os_brick.initiator.connectors import scaleio +from os_brick.tests.initiator import test_connector + + +class ScaleIOConnectorTestCase(test_connector.ConnectorTestCase): + """Test cases for ScaleIO connector.""" + + # Fake volume information + vol = { + 'id': 'vol1', + 'name': 'test_volume', + 'provider_id': 'vol1' + } + + # Fake SDC GUID + fake_guid = 'FAKE_GUID' + + def setUp(self): + super(ScaleIOConnectorTestCase, self).setUp() + + self.fake_connection_properties = { + 'hostIP': test_connector.MY_IP, + 'serverIP': test_connector.MY_IP, + 'scaleIO_volname': self.vol['name'], + 'scaleIO_volume_id': self.vol['provider_id'], + 'serverPort': 443, + 'serverUsername': 'test', + 'serverPassword': 'fake', + 'serverToken': 'fake_token', + 'iopsLimit': None, + 'bandwidthLimit': None + } + + # Formatting string for REST API calls + self.action_format = "instances/Volume::{}/action/{{}}".format( + self.vol['id']) + self.get_volume_api = 'types/Volume/instances/getByName::{}'.format( + self.vol['name']) + + # Map of REST API calls to responses + self.mock_calls = { + self.get_volume_api: + self.MockHTTPSResponse(json.dumps(self.vol['id'])), + self.action_format.format('addMappedSdc'): + self.MockHTTPSResponse(''), + self.action_format.format('setMappedSdcLimits'): + self.MockHTTPSResponse(''), + self.action_format.format('removeMappedSdc'): + self.MockHTTPSResponse(''), + } + + # Default error REST response + self.error_404 = self.MockHTTPSResponse(content=dict( + errorCode=0, + message='HTTP 404', + ), status_code=404) + + # Patch the request and os calls to fake versions + mock.patch.object( + requests, 'get', self.handle_scaleio_request).start() + mock.patch.object( + requests, 'post', self.handle_scaleio_request).start() + mock.patch.object(os.path, 'isdir', return_value=True).start() + mock.patch.object( + os, 'listdir', return_value=["emc-vol-{}".format(self.vol['id'])] + ).start() + self.addCleanup(mock.patch.stopall) + + # The actual ScaleIO connector + self.connector = scaleio.ScaleIOConnector( + 'sudo', execute=self.fake_execute) + + class MockHTTPSResponse(requests.Response): + """Mock HTTP Response + + Defines the https replies from the mocked calls to do_request() + """ + def __init__(self, content, status_code=200): + super(ScaleIOConnectorTestCase.MockHTTPSResponse, + self).__init__() + + self._content = content + self.encoding = 'UTF-8' + self.status_code = status_code + + def json(self, **kwargs): + if isinstance(self._content, six.string_types): + return super(ScaleIOConnectorTestCase.MockHTTPSResponse, + self).json(**kwargs) + + return self._content + + @property + def text(self): + if not isinstance(self._content, six.string_types): + return json.dumps(self._content) + + self._content = self._content.encode('utf-8') + return super(ScaleIOConnectorTestCase.MockHTTPSResponse, + self).text + + def fake_execute(self, *cmd, **kwargs): + """Fakes the rootwrap call""" + return self.fake_guid, None + + def fake_missing_execute(self, *cmd, **kwargs): + """Error when trying to call rootwrap drv_cfg""" + raise putils.ProcessExecutionError("Test missing drv_cfg.") + + def handle_scaleio_request(self, url, *args, **kwargs): + """Fake REST server""" + api_call = url.split(':', 2)[2].split('/', 1)[1].replace('api/', '') + + if 'setMappedSdcLimits' in api_call: + self.assertNotIn("iops_limit", kwargs['data']) + if "iopsLimit" not in kwargs['data']: + self.assertIn("bandwidthLimitInKbps", + kwargs['data']) + elif "bandwidthLimitInKbps" not in kwargs['data']: + self.assertIn("iopsLimit", kwargs['data']) + else: + self.assertIn("bandwidthLimitInKbps", + kwargs['data']) + self.assertIn("iopsLimit", kwargs['data']) + + try: + return self.mock_calls[api_call] + except KeyError: + return self.error_404 + + def test_get_search_path(self): + expected = "/dev/disk/by-id" + actual = self.connector.get_search_path() + self.assertEqual(expected, actual) + + @mock.patch.object(os.path, 'exists', return_value=True) + @mock.patch.object(scaleio.ScaleIOConnector, '_wait_for_volume_path') + def test_get_volume_paths(self, mock_wait_for_path, mock_exists): + mock_wait_for_path.return_value = "emc-vol-vol1" + expected = ['/dev/disk/by-id/emc-vol-vol1'] + actual = self.connector.get_volume_paths( + self.fake_connection_properties) + self.assertEqual(expected, actual) + + def test_get_connector_properties(self): + props = scaleio.ScaleIOConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + + def test_connect_volume(self): + """Successful connect to volume""" + self.connector.connect_volume(self.fake_connection_properties) + + def test_connect_with_bandwidth_limit(self): + """Successful connect to volume with bandwidth limit""" + self.fake_connection_properties['bandwidthLimit'] = '500' + self.test_connect_volume() + + def test_connect_with_iops_limit(self): + """Successful connect to volume with iops limit""" + self.fake_connection_properties['iopsLimit'] = '80' + self.test_connect_volume() + + def test_connect_with_iops_and_bandwidth_limits(self): + """Successful connect with iops and bandwidth limits""" + self.fake_connection_properties['bandwidthLimit'] = '500' + self.fake_connection_properties['iopsLimit'] = '80' + self.test_connect_volume() + + def test_disconnect_volume(self): + """Successful disconnect from volume""" + self.connector.disconnect_volume(self.fake_connection_properties, None) + + def test_error_id(self): + """Fail to connect with bad volume name""" + self.fake_connection_properties['scaleIO_volume_id'] = 'bad_id' + self.mock_calls[self.get_volume_api] = self.MockHTTPSResponse( + dict(errorCode='404', message='Test volume not found'), 404) + + self.assertRaises(exception.BrickException, self.test_connect_volume) + + def test_error_no_volume_id(self): + """Faile to connect with no volume id""" + self.fake_connection_properties['scaleIO_volume_id'] = None + self.mock_calls[self.get_volume_api] = self.MockHTTPSResponse( + 'null', 200) + + self.assertRaises(exception.BrickException, self.test_connect_volume) + + def test_error_bad_login(self): + """Fail to connect with bad authentication""" + self.mock_calls[self.get_volume_api] = self.MockHTTPSResponse( + 'null', 401) + + self.mock_calls['login'] = self.MockHTTPSResponse('null', 401) + self.mock_calls[self.action_format.format( + 'addMappedSdc')] = self.MockHTTPSResponse( + dict(errorCode=401, message='bad login'), 401) + self.assertRaises(exception.BrickException, self.test_connect_volume) + + def test_error_bad_drv_cfg(self): + """Fail to connect with missing rootwrap executable""" + self.connector.set_execute(self.fake_missing_execute) + self.assertRaises(exception.BrickException, self.test_connect_volume) + + def test_error_map_volume(self): + """Fail to connect with REST API failure""" + self.mock_calls[self.action_format.format( + 'addMappedSdc')] = self.MockHTTPSResponse( + dict(errorCode=self.connector.VOLUME_NOT_MAPPED_ERROR, + message='Test error map volume'), 500) + + self.assertRaises(exception.BrickException, self.test_connect_volume) + + @mock.patch('time.sleep') + def test_error_path_not_found(self, sleep_mock): + """Timeout waiting for volume to map to local file system""" + mock.patch.object( + os, 'listdir', return_value=["emc-vol-no-volume"] + ).start() + self.assertRaises(exception.BrickException, self.test_connect_volume) + self.assertTrue(sleep_mock.called) + + def test_map_volume_already_mapped(self): + """Ignore REST API failure for volume already mapped""" + self.mock_calls[self.action_format.format( + 'addMappedSdc')] = self.MockHTTPSResponse( + dict(errorCode=self.connector.VOLUME_ALREADY_MAPPED_ERROR, + message='Test error map volume'), 500) + + self.test_connect_volume() + + def test_error_disconnect_volume(self): + """Fail to disconnect with REST API failure""" + self.mock_calls[self.action_format.format( + 'removeMappedSdc')] = self.MockHTTPSResponse( + dict(errorCode=self.connector.VOLUME_ALREADY_MAPPED_ERROR, + message='Test error map volume'), 500) + + self.assertRaises(exception.BrickException, + self.test_disconnect_volume) + + def test_disconnect_volume_not_mapped(self): + """Ignore REST API failure for volume not mapped""" + self.mock_calls[self.action_format.format( + 'removeMappedSdc')] = self.MockHTTPSResponse( + dict(errorCode=self.connector.VOLUME_NOT_MAPPED_ERROR, + message='Test error map volume'), 500) + + self.test_disconnect_volume() + + def test_extend_volume(self): + self.assertRaises(NotImplementedError, + self.connector.extend_volume, + self.fake_connection_properties) diff --git a/os_brick/tests/initiator/connectors/test_sheepdog.py b/os_brick/tests/initiator/connectors/test_sheepdog.py new file mode 100644 index 000000000..72bcad0a4 --- /dev/null +++ b/os_brick/tests/initiator/connectors/test_sheepdog.py @@ -0,0 +1,87 @@ +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +import mock + +from os_brick import exception +from os_brick.initiator.connectors import sheepdog +from os_brick.initiator import linuxsheepdog +from os_brick.tests.initiator import test_connector + + +class SheepdogConnectorTestCase(test_connector.ConnectorTestCase): + + def setUp(self): + super(SheepdogConnectorTestCase, self).setUp() + + self.hosts = ['fake_hosts'] + self.ports = ['fake_ports'] + self.volume = 'fake_volume' + + self.connection_properties = { + 'hosts': self.hosts, + 'name': self.volume, + 'ports': self.ports, + } + + def test_get_connector_properties(self): + props = sheepdog.SheepdogConnector.get_connector_properties( + 'sudo', multipath=True, enforce_multipath=True) + + expected_props = {} + self.assertEqual(expected_props, props) + + def test_get_search_path(self): + sd_connector = sheepdog.SheepdogConnector(None) + path = sd_connector.get_search_path() + self.assertIsNone(path) + + def test_get_volume_paths(self): + sd_connector = sheepdog.SheepdogConnector(None) + expected = [] + actual = sd_connector.get_volume_paths(self.connection_properties) + self.assertEqual(expected, actual) + + def test_connect_volume(self): + """Test the connect volume case.""" + sd_connector = sheepdog.SheepdogConnector(None) + device_info = sd_connector.connect_volume(self.connection_properties) + + # Ensure expected object is returned correctly + self.assertTrue(isinstance(device_info['path'], + linuxsheepdog.SheepdogVolumeIOWrapper)) + + @mock.patch.object(linuxsheepdog.SheepdogVolumeIOWrapper, 'close') + def test_disconnect_volume(self, volume_close): + """Test the disconnect volume case.""" + sd_connector = sheepdog.SheepdogConnector(None) + device_info = sd_connector.connect_volume(self.connection_properties) + sd_connector.disconnect_volume(self.connection_properties, device_info) + + self.assertEqual(1, volume_close.call_count) + + def test_disconnect_volume_with_invalid_handle(self): + """Test the disconnect volume case with invalid handle.""" + sd_connector = sheepdog.SheepdogConnector(None) + device_info = {'path': 'fake_handle'} + self.assertRaises(exception.InvalidIOHandleObject, + sd_connector.disconnect_volume, + self.connection_properties, + device_info) + + def test_extend_volume(self): + sd_connector = sheepdog.SheepdogConnector(None) + self.assertRaises(NotImplementedError, + sd_connector.extend_volume, + self.connection_properties) diff --git a/os_brick/tests/initiator/test_connector.py b/os_brick/tests/initiator/test_connector.py index c2931b828..7ad4b0bdf 100644 --- a/os_brick/tests/initiator/test_connector.py +++ b/os_brick/tests/initiator/test_connector.py @@ -12,34 +12,20 @@ # License for the specific language governing permissions and limitations # under the License. -import os.path import platform import sys -import tempfile -import time -import glob -import json import mock from oslo_concurrency import processutils as putils from oslo_log import log as logging -from oslo_service import loopingcall -from oslo_utils import encodeutils -import requests -import six -import testtools -from os_brick import exception -from os_brick.i18n import _LE from os_brick.initiator import connector -from os_brick.initiator import host_driver +from os_brick.initiator.connectors import base +from os_brick.initiator.connectors import fake +from os_brick.initiator.connectors import iscsi from os_brick.initiator import linuxfc -from os_brick.initiator import linuxrbd -from os_brick.initiator import linuxscsi -from os_brick.initiator import linuxsheepdog from os_brick.privileged import rootwrap as priv_rootwrap -from os_brick.remotefs import remotefs -from os_brick.tests import base +from os_brick.tests import base as test_base LOG = logging.getLogger(__name__) @@ -47,9 +33,9 @@ MY_IP = '10.0.0.1' FAKE_SCSI_WWN = '1234567890' -class ConnectorUtilsTestCase(base.TestCase): +class ConnectorUtilsTestCase(test_base.TestCase): - @mock.patch.object(connector.ISCSIConnector, 'get_initiator', + @mock.patch.object(iscsi.ISCSIConnector, 'get_initiator', return_value='fakeinitiator') @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_wwpns', return_value=None) @@ -133,7 +119,7 @@ class ConnectorUtilsTestCase(base.TestCase): host=override_host) -class ConnectorTestCase(base.TestCase): +class ConnectorTestCase(test_base.TestCase): def setUp(self): super(ConnectorTestCase, self).setUp() @@ -155,20 +141,20 @@ class ConnectorTestCase(base.TestCase): } def test_connect_volume(self): - self.connector = connector.FakeConnector(None) + self.connector = fake.FakeConnector(None) device_info = self.connector.connect_volume(self.fake_connection()) self.assertIn('type', device_info) self.assertIn('path', device_info) def test_disconnect_volume(self): - self.connector = connector.FakeConnector(None) + self.connector = fake.FakeConnector(None) def test_get_connector_properties(self): with mock.patch.object(priv_rootwrap, 'execute') as mock_exec: mock_exec.return_value = True multipath = True enforce_multipath = True - props = connector.BaseLinuxConnector.get_connector_properties( + props = base.BaseLinuxConnector.get_connector_properties( 'sudo', multipath=multipath, enforce_multipath=enforce_multipath) @@ -177,7 +163,7 @@ class ConnectorTestCase(base.TestCase): multipath = False enforce_multipath = True - props = connector.BaseLinuxConnector.get_connector_properties( + props = base.BaseLinuxConnector.get_connector_properties( 'sudo', multipath=multipath, enforce_multipath=enforce_multipath) @@ -190,7 +176,7 @@ class ConnectorTestCase(base.TestCase): enforce_multipath = True self.assertRaises( putils.ProcessExecutionError, - connector.BaseLinuxConnector.get_connector_properties, + base.BaseLinuxConnector.get_connector_properties, 'sudo', multipath=multipath, enforce_multipath=enforce_multipath) @@ -241,13 +227,13 @@ class ConnectorTestCase(base.TestCase): "bogus", None) def test_check_valid_device_with_wrong_path(self): - self.connector = connector.FakeConnector(None) + self.connector = fake.FakeConnector(None) self.connector._execute = \ lambda *args, **kwargs: ("", None) self.assertFalse(self.connector.check_valid_device('/d0v')) def test_check_valid_device(self): - self.connector = connector.FakeConnector(None) + self.connector = fake.FakeConnector(None) self.connector._execute = \ lambda *args, **kwargs: ("", "") self.assertTrue(self.connector.check_valid_device('/dev')) @@ -255,2743 +241,7 @@ class ConnectorTestCase(base.TestCase): def test_check_valid_device_with_cmd_error(self): def raise_except(*args, **kwargs): raise putils.ProcessExecutionError - self.connector = connector.FakeConnector(None) + self.connector = fake.FakeConnector(None) with mock.patch.object(self.connector, '_execute', side_effect=putils.ProcessExecutionError): self.assertFalse(self.connector.check_valid_device('/dev')) - - -class BaseISCSIConnectorTestCase(base.TestCase): - - def setUp(self): - super(BaseISCSIConnectorTestCase, self).setUp() - self.connector = connector.FakeBaseISCSIConnector(None) - - @mock.patch.object(connector.BaseISCSIConnector, '_get_all_targets') - def test_iterate_all_targets(self, mock_get_all_targets): - # extra_property cannot be a sentinel, a copied sentinel will not - # identical to the original one. - connection_properties = { - 'target_portals': mock.sentinel.target_portals, - 'target_iqns': mock.sentinel.target_iqns, - 'target_luns': mock.sentinel.target_luns, - 'extra_property': 'extra_property'} - mock_get_all_targets.return_value = [( - mock.sentinel.portal, mock.sentinel.iqn, mock.sentinel.lun)] - - # method is a generator, and it yields dictionaries. list() will - # iterate over all of the method's items. - list_props = list( - self.connector._iterate_all_targets(connection_properties)) - - mock_get_all_targets.assert_called_once_with(connection_properties) - self.assertEqual(1, len(list_props)) - - expected_props = {'target_portal': mock.sentinel.portal, - 'target_iqn': mock.sentinel.iqn, - 'target_lun': mock.sentinel.lun, - 'extra_property': 'extra_property'} - self.assertDictEqual(expected_props, list_props[0]) - - def test_get_all_targets(self): - connection_properties = { - 'target_portals': [mock.sentinel.target_portals], - 'target_iqns': [mock.sentinel.target_iqns], - 'target_luns': [mock.sentinel.target_luns]} - - all_targets = self.connector._get_all_targets(connection_properties) - - expected_targets = zip([mock.sentinel.target_portals], - [mock.sentinel.target_iqns], - [mock.sentinel.target_luns]) - self.assertEqual(list(expected_targets), list(all_targets)) - - def test_get_all_targets_single_target(self): - connection_properties = { - 'target_portal': mock.sentinel.target_portal, - 'target_iqn': mock.sentinel.target_iqn, - 'target_lun': mock.sentinel.target_lun} - - all_targets = self.connector._get_all_targets(connection_properties) - - expected_target = (mock.sentinel.target_portal, - mock.sentinel.target_iqn, - mock.sentinel.target_lun) - self.assertEqual([expected_target], all_targets) - - -class ISCSIConnectorTestCase(ConnectorTestCase): - - def setUp(self): - super(ISCSIConnectorTestCase, self).setUp() - self.connector = connector.ISCSIConnector( - None, execute=self.fake_execute, use_multipath=False) - self.connector_with_multipath = connector.ISCSIConnector( - None, execute=self.fake_execute, use_multipath=True) - - mock.patch.object(self.connector._linuxscsi, 'get_name_from_path', - return_value="/dev/sdb").start() - self.addCleanup(mock.patch.stopall) - self._fake_iqn = 'iqn.1234-56.foo.bar:01:23456789abc' - - def generate_device(self, location, iqn, transport=None, lun=1): - dev_format = "ip-%s-iscsi-%s-lun-%s" % (location, iqn, lun) - if transport: - dev_format = "pci-0000:00:00.0-" + dev_format - fake_dev_path = "/dev/disk/by-path/" + dev_format - return fake_dev_path - - def iscsi_connection(self, volume, location, iqn): - return { - 'driver_volume_type': 'iscsi', - 'data': { - 'volume_id': volume['id'], - 'target_portal': location, - 'target_iqn': iqn, - 'target_lun': 1, - } - } - - def iscsi_connection_multipath(self, volume, locations, iqns, luns): - return { - 'driver_volume_type': 'iscsi', - 'data': { - 'volume_id': volume['id'], - 'target_portals': locations, - 'target_iqns': iqns, - 'target_luns': luns, - } - } - - def iscsi_connection_chap(self, volume, location, iqn, auth_method, - auth_username, auth_password, - discovery_auth_method, discovery_auth_username, - discovery_auth_password): - return { - 'driver_volume_type': 'iscsi', - 'data': { - 'auth_method': auth_method, - 'auth_username': auth_username, - 'auth_password': auth_password, - 'discovery_auth_method': discovery_auth_method, - 'discovery_auth_username': discovery_auth_username, - 'discovery_auth_password': discovery_auth_password, - 'target_lun': 1, - 'volume_id': volume['id'], - 'target_iqn': iqn, - 'target_portal': location, - } - } - - def _initiator_get_text(self, *arg, **kwargs): - text = ('## DO NOT EDIT OR REMOVE THIS FILE!\n' - '## If you remove this file, the iSCSI daemon ' - 'will not start.\n' - '## If you change the InitiatorName, existing ' - 'access control lists\n' - '## may reject this initiator. The InitiatorName must ' - 'be unique\n' - '## for each iSCSI initiator. Do NOT duplicate iSCSI ' - 'InitiatorNames.\n' - 'InitiatorName=%s' % self._fake_iqn) - return text, None - - def test_get_initiator(self): - def initiator_no_file(*args, **kwargs): - raise putils.ProcessExecutionError('No file') - - self.connector._execute = initiator_no_file - initiator = self.connector.get_initiator() - self.assertIsNone(initiator) - self.connector._execute = self._initiator_get_text - initiator = self.connector.get_initiator() - self.assertEqual(self._fake_iqn, initiator) - - def test_get_connector_properties(self): - with mock.patch.object(priv_rootwrap, 'execute') as mock_exec: - mock_exec.return_value = self._initiator_get_text() - multipath = True - enforce_multipath = True - props = connector.ISCSIConnector.get_connector_properties( - 'sudo', multipath=multipath, - enforce_multipath=enforce_multipath) - - expected_props = {'initiator': self._fake_iqn} - self.assertEqual(expected_props, props) - - @mock.patch.object(connector.ISCSIConnector, '_run_iscsiadm_bare') - def test_brick_iscsi_validate_transport(self, mock_iscsiadm): - sample_output = ('# BEGIN RECORD 2.0-872\n' - 'iface.iscsi_ifacename = %s.fake_suffix\n' - 'iface.net_ifacename = \n' - 'iface.ipaddress = \n' - 'iface.hwaddress = 00:53:00:00:53:00\n' - 'iface.transport_name = %s\n' - 'iface.initiatorname = \n' - '# END RECORD') - for tport in self.connector.supported_transports: - mock_iscsiadm.return_value = (sample_output % (tport, tport), '') - self.assertEqual(tport + '.fake_suffix', - self.connector._validate_iface_transport( - tport + '.fake_suffix')) - - mock_iscsiadm.return_value = ("", 'iscsiadm: Could not ' - 'read iface fake_transport (6)') - self.assertEqual('default', - self.connector._validate_iface_transport( - 'fake_transport')) - - def test_get_search_path(self): - search_path = self.connector.get_search_path() - expected = "/dev/disk/by-path" - self.assertEqual(expected, search_path) - - @mock.patch.object(os.path, 'exists', return_value=True) - @mock.patch.object(connector.ISCSIConnector, '_get_potential_volume_paths') - def test_get_volume_paths(self, mock_potential_paths, mock_exists): - name1 = 'volume-00000001-1' - vol = {'id': 1, 'name': name1} - location = '10.0.2.15:3260' - iqn = 'iqn.2010-10.org.openstack:%s' % name1 - - fake_path = ("/dev/disk/by-path/ip-%(ip)s-iscsi-%(iqn)s-lun-%(lun)s" % - {'ip': '10.0.2.15', 'iqn': iqn, 'lun': 1}) - fake_props = {} - fake_devices = [fake_path] - expected = fake_devices - mock_potential_paths.return_value = (fake_devices, fake_props) - - connection_properties = self.iscsi_connection(vol, [location], - [iqn]) - volume_paths = self.connector.get_volume_paths( - connection_properties['data']) - self.assertEqual(expected, volume_paths) - - @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device_path') - @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device') - def test_discover_mpath_device(self, mock_multipath_device, - mock_multipath_device_path): - location1 = '10.0.2.15:3260' - location2 = '[2001:db8::1]:3260' - name1 = 'volume-00000001-1' - name2 = 'volume-00000001-2' - iqn1 = 'iqn.2010-10.org.openstack:%s' % name1 - iqn2 = 'iqn.2010-10.org.openstack:%s' % name2 - fake_multipath_dev = '/dev/mapper/fake-multipath-dev' - fake_raw_dev = '/dev/disk/by-path/fake-raw-lun' - vol = {'id': 1, 'name': name1} - connection_properties = self.iscsi_connection_multipath( - vol, [location1, location2], [iqn1, iqn2], [1, 2]) - mock_multipath_device_path.return_value = fake_multipath_dev - mock_multipath_device.return_value = FAKE_SCSI_WWN - (result_path, result_mpath_id) = ( - self.connector_with_multipath._discover_mpath_device( - FAKE_SCSI_WWN, - connection_properties['data'], - fake_raw_dev)) - result = {'path': result_path, 'multipath_id': result_mpath_id} - expected_result = {'path': fake_multipath_dev, - 'multipath_id': FAKE_SCSI_WWN} - self.assertEqual(expected_result, result) - - @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device_path') - @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device') - @mock.patch.object(os.path, 'realpath') - def test_discover_mpath_device_by_realpath(self, mock_realpath, - mock_multipath_device, - mock_multipath_device_path): - location1 = '10.0.2.15:3260' - location2 = '[2001:db8::1]:3260' - name1 = 'volume-00000001-1' - name2 = 'volume-00000001-2' - iqn1 = 'iqn.2010-10.org.openstack:%s' % name1 - iqn2 = 'iqn.2010-10.org.openstack:%s' % name2 - fake_multipath_dev = None - fake_raw_dev = '/dev/disk/by-path/fake-raw-lun' - vol = {'id': 1, 'name': name1} - connection_properties = self.iscsi_connection_multipath( - vol, [location1, location2], [iqn1, iqn2], [1, 2]) - mock_multipath_device_path.return_value = fake_multipath_dev - mock_multipath_device.return_value = { - 'device': '/dev/mapper/%s' % FAKE_SCSI_WWN} - mock_realpath.return_value = '/dev/sdvc' - (result_path, result_mpath_id) = ( - self.connector_with_multipath._discover_mpath_device( - FAKE_SCSI_WWN, - connection_properties['data'], - fake_raw_dev)) - mock_multipath_device.assert_called_with('/dev/sdvc') - result = {'path': result_path, 'multipath_id': result_mpath_id} - expected_result = {'path': '/dev/mapper/%s' % FAKE_SCSI_WWN, - 'multipath_id': FAKE_SCSI_WWN} - self.assertEqual(expected_result, result) - - @mock.patch('time.sleep', mock.Mock()) - def _test_connect_volume(self, extra_props, additional_commands, - transport=None, disconnect_mock=None): - # for making sure the /dev/disk/by-path is gone - exists_mock = mock.Mock() - exists_mock.return_value = True - os.path.exists = exists_mock - - location = '10.0.2.15:3260' - name = 'volume-00000001' - iqn = 'iqn.2010-10.org.openstack:%s' % name - vol = {'id': 1, 'name': name} - connection_info = self.iscsi_connection(vol, location, iqn) - for key, value in extra_props.items(): - connection_info['data'][key] = value - if transport is not None: - dev_list = self.generate_device(location, iqn, transport) - with mock.patch.object(glob, 'glob', return_value=[dev_list]): - device = self.connector.connect_volume(connection_info['data']) - else: - device = self.connector.connect_volume(connection_info['data']) - - dev_str = self.generate_device(location, iqn, transport) - self.assertEqual('block', device['type']) - self.assertEqual(dev_str, device['path']) - - self.count = 0 - - def mock_exists_effect(*args, **kwargs): - self.count = self.count + 1 - if self.count == 4: - return False - else: - return True - - if disconnect_mock is None: - disconnect_mock = mock_exists_effect - - with mock.patch.object(os.path, 'exists', - side_effect=disconnect_mock): - if transport is not None: - dev_list = self.generate_device(location, iqn, transport) - with mock.patch.object(glob, 'glob', return_value=[dev_list]): - self.connector.disconnect_volume(connection_info['data'], - device) - else: - self.connector.disconnect_volume(connection_info['data'], - device) - - expected_commands = [ - ('iscsiadm -m node -T %s -p %s' % (iqn, location)), - ('iscsiadm -m session'), - ('iscsiadm -m node -T %s -p %s --login' % (iqn, location)), - ('iscsiadm -m node -T %s -p %s --op update' - ' -n node.startup -v automatic' % (iqn, location)), - ('/lib/udev/scsi_id --page 0x83 --whitelisted %s' % dev_str), - ('blockdev --flushbufs /dev/sdb'), - ('tee -a /sys/block/sdb/device/delete'), - ('iscsiadm -m node -T %s -p %s --op update' - ' -n node.startup -v manual' % (iqn, location)), - ('iscsiadm -m node -T %s -p %s --logout' % (iqn, location)), - ('iscsiadm -m node -T %s -p %s --op delete' % - (iqn, location)), ] + additional_commands - LOG.debug("self.cmds = %s", self.cmds) - LOG.debug("expected = %s", expected_commands) - - self.assertEqual(expected_commands, self.cmds) - - @testtools.skipUnless(os.path.exists('/dev/disk/by-path'), - 'Test requires /dev/disk/by-path') - def test_connect_volume(self): - self._test_connect_volume({}, []) - - @testtools.skipUnless(os.path.exists('/dev/disk/by-path'), - 'Test requires /dev/disk/by-path') - @mock.patch.object(connector.ISCSIConnector, '_get_transport') - def test_connect_volume_with_transport(self, mock_transport): - mock_transport.return_value = 'fake_transport' - self._test_connect_volume({}, [], 'fake_transport') - - @testtools.skipUnless(os.path.exists('/dev/disk/by-path'), - 'Test requires /dev/disk/by-path') - def test_connect_volume_with_alternative_targets(self): - location = '10.0.2.15:3260' - location2 = '[2001:db8::1]:3260' - iqn = 'iqn.2010-10.org.openstack:volume-00000001' - iqn2 = 'iqn.2010-10.org.openstack:volume-00000001-2' - extra_props = {'target_portals': [location, location2], - 'target_iqns': [iqn, iqn2], - 'target_luns': [1, 2]} - additional_commands = [('blockdev --flushbufs /dev/sdb'), - ('tee -a /sys/block/sdb/device/delete'), - ('iscsiadm -m node -T %s -p %s --op update' - ' -n node.startup -v manual' % - (iqn2, location2)), - ('iscsiadm -m node -T %s -p %s --logout' % - (iqn2, location2)), - ('iscsiadm -m node -T %s -p %s --op delete' % - (iqn2, location2))] - - def mock_exists_effect(*args, **kwargs): - self.count = self.count + 1 - # we have 2 targets in this test, so we need - # to make sure we remove and detect removal - # for both. - if (self.count == 4 or - self.count == 8): - return False - else: - return True - - self._test_connect_volume(extra_props, additional_commands, - disconnect_mock=mock_exists_effect) - - @testtools.skipUnless(os.path.exists('/dev/disk/by-path'), - 'Test requires /dev/disk/by-path') - @mock.patch.object(os.path, 'exists') - @mock.patch.object(connector.ISCSIConnector, '_run_iscsiadm') - def test_connect_volume_with_alternative_targets_primary_error( - self, mock_iscsiadm, mock_exists): - location = '10.0.2.15:3260' - location2 = '[2001:db8::1]:3260' - dev_loc2 = '2001:db8::1:3260' # udev location2 - name = 'volume-00000001' - iqn = 'iqn.2010-10.org.openstack:%s' % name - iqn2 = 'iqn.2010-10.org.openstack:%s-2' % name - vol = {'id': 1, 'name': name} - connection_info = self.iscsi_connection(vol, location, iqn) - connection_info['data']['target_portals'] = [location, location2] - connection_info['data']['target_iqns'] = [iqn, iqn2] - connection_info['data']['target_luns'] = [1, 2] - dev_str2 = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (dev_loc2, iqn2) - - def fake_run_iscsiadm(iscsi_properties, iscsi_command, **kwargs): - if iscsi_properties['target_portal'] == location: - if iscsi_command == ('--login',): - raise putils.ProcessExecutionError(None, None, 21) - return mock.DEFAULT - - mock_iscsiadm.side_effect = fake_run_iscsiadm - mock_exists.side_effect = lambda x: x == dev_str2 - device = self.connector.connect_volume(connection_info['data']) - self.assertEqual('block', device['type']) - self.assertEqual(dev_str2, device['path']) - props = connection_info['data'].copy() - for key in ('target_portals', 'target_iqns', 'target_luns'): - props.pop(key, None) - props['target_portal'] = location2 - props['target_iqn'] = iqn2 - props['target_lun'] = 2 - mock_iscsiadm.assert_any_call(props, ('--login',), - check_exit_code=[0, 255]) - - mock_iscsiadm.reset_mock() - with mock.patch.object(os.path, 'exists', - return_value=False): - self.connector.disconnect_volume(connection_info['data'], device) - props = connection_info['data'].copy() - for key in ('target_portals', 'target_iqns', 'target_luns'): - props.pop(key, None) - mock_iscsiadm.assert_any_call(props, ('--logout',), - check_exit_code=[0, 21, 255]) - props['target_portal'] = location2 - props['target_iqn'] = iqn2 - props['target_lun'] = 2 - mock_iscsiadm.assert_any_call(props, ('--logout',), - check_exit_code=[0, 21, 255]) - - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') - @mock.patch.object(connector.ISCSIConnector, '_run_iscsiadm_bare') - @mock.patch.object(connector.ISCSIConnector, - '_get_target_portals_from_iscsiadm_output') - @mock.patch.object(connector.ISCSIConnector, '_connect_to_iscsi_portal') - @mock.patch.object(connector.ISCSIConnector, '_rescan_iscsi') - @mock.patch.object(connector.ISCSIConnector, '_rescan_multipath') - @mock.patch.object(os.path, 'exists', return_value=True) - @mock.patch.object(connector.BaseLinuxConnector, '_discover_mpath_device') - def test_connect_volume_with_multipath( - self, mock_discover_mpath_device, exists_mock, - rescan_multipath_mock, rescan_iscsi_mock, connect_to_mock, - portals_mock, iscsiadm_mock, mock_iscsi_wwn): - mock_iscsi_wwn.return_value = FAKE_SCSI_WWN - location = '10.0.2.15:3260' - name = 'volume-00000001' - iqn = 'iqn.2010-10.org.openstack:%s' % name - vol = {'id': 1, 'name': name} - connection_properties = self.iscsi_connection(vol, location, iqn) - mock_discover_mpath_device.return_value = ( - 'iqn.2010-10.org.openstack:%s' % name, FAKE_SCSI_WWN) - - self.connector_with_multipath = \ - connector.ISCSIConnector(None, use_multipath=True) - iscsiadm_mock.return_value = "%s %s" % (location, iqn) - portals_mock.return_value = [[location, iqn]] - - result = self.connector_with_multipath.connect_volume( - connection_properties['data']) - expected_result = {'multipath_id': FAKE_SCSI_WWN, - 'path': 'iqn.2010-10.org.openstack:volume-00000001', - 'type': 'block', - 'scsi_wwn': FAKE_SCSI_WWN} - self.assertEqual(expected_result, result) - - @mock.patch.object(connector.ISCSIConnector, - '_run_iscsiadm_update_discoverydb') - @mock.patch.object(os.path, 'exists', return_value=True) - def test_iscsi_portals_with_chap_discovery( - self, exists, update_discoverydb): - location = '10.0.2.15:3260' - name = 'volume-00000001' - iqn = 'iqn.2010-10.org.openstack:%s' % name - vol = {'id': 1, 'name': name} - auth_method = 'CHAP' - auth_username = 'fake_chap_username' - auth_password = 'fake_chap_password' - discovery_auth_method = 'CHAP' - discovery_auth_username = 'fake_chap_username' - discovery_auth_password = 'fake_chap_password' - connection_properties = self.iscsi_connection_chap( - vol, location, iqn, auth_method, auth_username, auth_password, - discovery_auth_method, discovery_auth_username, - discovery_auth_password) - self.connector_with_multipath = connector.ISCSIConnector( - None, execute=self.fake_execute, use_multipath=True) - self.cmds = [] - # The first call returns an error code = 6, mocking an empty - # discovery db. The second one mocks a successful return and the - # third one a dummy exit code, which will trigger the - # TargetPortalNotFound exception in connect_volume - update_discoverydb.side_effect = [ - putils.ProcessExecutionError(None, None, 6), - ("", ""), - putils.ProcessExecutionError(None, None, 9)] - - self.connector_with_multipath._discover_iscsi_portals( - connection_properties['data']) - update_discoverydb.assert_called_with(connection_properties['data']) - - expected_cmds = [ - 'iscsiadm -m discoverydb -t sendtargets -p %s --op new' % - location, - 'iscsiadm -m discoverydb -t sendtargets -p %s --discover' % - location] - self.assertEqual(expected_cmds, self.cmds) - - self.assertRaises(exception.TargetPortalNotFound, - self.connector_with_multipath.connect_volume, - connection_properties['data']) - - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') - @mock.patch.object(os.path, 'exists', return_value=True) - @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') - @mock.patch.object(connector.ISCSIConnector, '_get_multipath_device_map', - return_value={}) - @mock.patch.object(connector.ISCSIConnector, '_get_iscsi_devices') - @mock.patch.object(connector.ISCSIConnector, '_run_multipath') - @mock.patch.object(connector.ISCSIConnector, '_get_multipath_iqns') - @mock.patch.object(connector.BaseLinuxConnector, '_discover_mpath_device') - @mock.patch.object(linuxscsi.LinuxSCSI, 'process_lun_id') - def test_connect_volume_with_multiple_portals( - self, mock_process_lun_id, mock_discover_mpath_device, - mock_get_iqn, mock_run_multipath, mock_iscsi_devices, - mock_get_device_map, mock_devices, mock_exists, mock_scsi_wwn): - mock_scsi_wwn.return_value = FAKE_SCSI_WWN - location1 = '10.0.2.15:3260' - location2 = '[2001:db8::1]:3260' - dev_loc2 = '2001:db8::1:3260' # udev location2 - name1 = 'volume-00000001-1' - name2 = 'volume-00000001-2' - iqn1 = 'iqn.2010-10.org.openstack:%s' % name1 - iqn2 = 'iqn.2010-10.org.openstack:%s' % name2 - fake_multipath_dev = '/dev/mapper/fake-multipath-dev' - vol = {'id': 1, 'name': name1} - connection_properties = self.iscsi_connection_multipath( - vol, [location1, location2], [iqn1, iqn2], [1, 2]) - devs = ['/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location1, iqn1), - '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (dev_loc2, iqn2)] - mock_devices.return_value = devs - mock_iscsi_devices.return_value = devs - mock_get_iqn.return_value = [iqn1, iqn2] - mock_discover_mpath_device.return_value = (fake_multipath_dev, - FAKE_SCSI_WWN) - mock_process_lun_id.return_value = [1, 2] - - result = self.connector_with_multipath.connect_volume( - connection_properties['data']) - expected_result = {'multipath_id': FAKE_SCSI_WWN, - 'path': fake_multipath_dev, 'type': 'block', - 'scsi_wwn': FAKE_SCSI_WWN} - cmd_format = 'iscsiadm -m node -T %s -p %s --%s' - expected_commands = [cmd_format % (iqn1, location1, 'login'), - cmd_format % (iqn2, location2, 'login')] - self.assertEqual(expected_result, result) - for command in expected_commands: - self.assertIn(command, self.cmds) - - self.cmds = [] - self.connector_with_multipath.disconnect_volume( - connection_properties['data'], result) - expected_commands = [cmd_format % (iqn1, location1, 'logout'), - cmd_format % (iqn2, location2, 'logout')] - for command in expected_commands: - self.assertIn(command, self.cmds) - - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') - @mock.patch.object(os.path, 'exists') - @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') - @mock.patch.object(connector.ISCSIConnector, '_get_multipath_device_map', - return_value={}) - @mock.patch.object(connector.ISCSIConnector, '_get_iscsi_devices') - @mock.patch.object(connector.ISCSIConnector, '_rescan_multipath') - @mock.patch.object(connector.ISCSIConnector, '_run_multipath') - @mock.patch.object(connector.ISCSIConnector, '_get_multipath_iqns') - @mock.patch.object(connector.ISCSIConnector, '_run_iscsiadm') - @mock.patch.object(connector.BaseLinuxConnector, '_discover_mpath_device') - @mock.patch.object(linuxscsi.LinuxSCSI, 'process_lun_id') - def test_connect_volume_with_multiple_portals_primary_error( - self, mock_process_lun_id, mock_discover_mpath_device, - mock_iscsiadm, mock_get_iqn, mock_run_multipath, - mock_rescan_multipath, mock_iscsi_devices, - mock_get_multipath_device_map, mock_devices, mock_exists, - mock_scsi_wwn): - mock_scsi_wwn.return_value = FAKE_SCSI_WWN - location1 = '10.0.2.15:3260' - location2 = '[2001:db8::1]:3260' - dev_loc2 = '2001:db8::1:3260' # udev location2 - name1 = 'volume-00000001-1' - name2 = 'volume-00000001-2' - iqn1 = 'iqn.2010-10.org.openstack:%s' % name1 - iqn2 = 'iqn.2010-10.org.openstack:%s' % name2 - fake_multipath_dev = '/dev/mapper/fake-multipath-dev' - vol = {'id': 1, 'name': name1} - connection_properties = self.iscsi_connection_multipath( - vol, [location1, location2], [iqn1, iqn2], [1, 2]) - dev1 = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location1, iqn1) - dev2 = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (dev_loc2, iqn2) - - def fake_run_iscsiadm(iscsi_properties, iscsi_command, **kwargs): - if iscsi_properties['target_portal'] == location1: - if iscsi_command == ('--login',): - raise putils.ProcessExecutionError(None, None, 21) - return mock.DEFAULT - - mock_exists.side_effect = lambda x: x != dev1 - mock_devices.return_value = [dev2] - mock_iscsi_devices.return_value = [dev2] - mock_get_iqn.return_value = [iqn2] - mock_iscsiadm.side_effect = fake_run_iscsiadm - - mock_discover_mpath_device.return_value = (fake_multipath_dev, - FAKE_SCSI_WWN) - mock_process_lun_id.return_value = [1, 2] - - props = connection_properties['data'].copy() - result = self.connector_with_multipath.connect_volume( - connection_properties['data']) - - expected_result = {'multipath_id': FAKE_SCSI_WWN, - 'path': fake_multipath_dev, 'type': 'block', - 'scsi_wwn': FAKE_SCSI_WWN} - self.assertEqual(expected_result, result) - props['target_portal'] = location1 - props['target_iqn'] = iqn1 - mock_iscsiadm.assert_any_call(props, ('--login',), - check_exit_code=[0, 255]) - props['target_portal'] = location2 - props['target_iqn'] = iqn2 - mock_iscsiadm.assert_any_call(props, ('--login',), - check_exit_code=[0, 255]) - - mock_iscsiadm.reset_mock() - self.connector_with_multipath.disconnect_volume( - connection_properties['data'], result) - - props = connection_properties['data'].copy() - props['target_portal'] = location1 - props['target_iqn'] = iqn1 - mock_iscsiadm.assert_any_call(props, ('--logout',), - check_exit_code=[0, 21, 255]) - props['target_portal'] = location2 - props['target_iqn'] = iqn2 - mock_iscsiadm.assert_any_call(props, ('--logout',), - check_exit_code=[0, 21, 255]) - - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') - @mock.patch.object(os.path, 'exists', return_value=True) - @mock.patch.object(connector.ISCSIConnector, - '_get_target_portals_from_iscsiadm_output') - @mock.patch.object(connector.ISCSIConnector, '_connect_to_iscsi_portal') - @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') - @mock.patch.object(connector.ISCSIConnector, '_get_iscsi_devices') - @mock.patch.object(connector.ISCSIConnector, '_rescan_multipath') - @mock.patch.object(connector.ISCSIConnector, '_run_multipath') - @mock.patch.object(connector.BaseLinuxConnector, '_discover_mpath_device') - def test_connect_volume_with_multipath_connecting( - self, mock_discover_mpath_device, mock_run_multipath, - mock_rescan_multipath, mock_iscsi_devices, mock_devices, - mock_connect, mock_portals, mock_exists, mock_scsi_wwn): - mock_scsi_wwn.return_value = FAKE_SCSI_WWN - location1 = '10.0.2.15:3260' - location2 = '[2001:db8::1]:3260' - dev_loc2 = '2001:db8::1:3260' # udev location2 - name1 = 'volume-00000001-1' - name2 = 'volume-00000001-2' - iqn1 = 'iqn.2010-10.org.openstack:%s' % name1 - iqn2 = 'iqn.2010-10.org.openstack:%s' % name2 - fake_multipath_dev = '/dev/mapper/fake-multipath-dev' - vol = {'id': 1, 'name': name1} - connection_properties = self.iscsi_connection(vol, location1, iqn1) - devs = ['/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location1, iqn1), - '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (dev_loc2, iqn2)] - mock_devices.return_value = devs - mock_iscsi_devices.return_value = devs - mock_portals.return_value = [[location1, iqn1], [location2, iqn1], - [location2, iqn2]] - mock_discover_mpath_device.return_value = (fake_multipath_dev, - FAKE_SCSI_WWN) - - result = self.connector_with_multipath.connect_volume( - connection_properties['data']) - expected_result = {'multipath_id': FAKE_SCSI_WWN, - 'path': fake_multipath_dev, 'type': 'block', - 'scsi_wwn': FAKE_SCSI_WWN} - props1 = connection_properties['data'].copy() - props2 = connection_properties['data'].copy() - locations = list(set([location1, location2])) # order may change - props1['target_portal'] = locations[0] - props2['target_portal'] = locations[1] - expected_calls = [mock.call(props1), mock.call(props2)] - self.assertEqual(expected_result, result) - self.assertEqual(expected_calls, mock_connect.call_args_list) - - @mock.patch.object(os.path, 'exists', return_value=True) - @mock.patch.object(connector.ISCSIConnector, - '_get_target_portals_from_iscsiadm_output') - @mock.patch.object(connector.ISCSIConnector, '_connect_to_iscsi_portal') - @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') - @mock.patch.object(connector.ISCSIConnector, '_get_iscsi_devices') - @mock.patch.object(connector.ISCSIConnector, '_rescan_multipath') - @mock.patch.object(connector.ISCSIConnector, '_run_multipath') - def test_connect_volume_multipath_failed_iscsi_login( - self, mock_run_multipath, mock_rescan_multipath, - mock_iscsi_devices, mock_devices, - mock_connect, mock_portals, mock_exists): - location1 = '10.0.2.15:3260' - location2 = '10.0.3.15:3260' - name1 = 'volume-00000001-1' - name2 = 'volume-00000001-2' - iqn1 = 'iqn.2010-10.org.openstack:%s' % name1 - iqn2 = 'iqn.2010-10.org.openstack:%s' % name2 - vol = {'id': 1, 'name': name1} - connection_properties = self.iscsi_connection(vol, location1, iqn1) - devs = ['/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location1, iqn1), - '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (location2, iqn2)] - mock_devices.return_value = devs - mock_iscsi_devices.return_value = devs - mock_portals.return_value = [[location1, iqn1], [location2, iqn1], - [location2, iqn2]] - - mock_connect.return_value = False - self.assertRaises(exception.FailedISCSITargetPortalLogin, - self.connector_with_multipath.connect_volume, - connection_properties['data']) - - @mock.patch.object(connector.ISCSIConnector, '_connect_to_iscsi_portal') - def test_connect_volume_failed_iscsi_login(self, mock_connect): - location1 = '10.0.2.15:3260' - name1 = 'volume-00000001-1' - iqn1 = 'iqn.2010-10.org.openstack:%s' % name1 - vol = {'id': 1, 'name': name1} - connection_properties = self.iscsi_connection(vol, location1, iqn1) - - mock_connect.return_value = False - self.assertRaises(exception.FailedISCSITargetPortalLogin, - self.connector.connect_volume, - connection_properties['data']) - - @mock.patch.object(time, 'sleep') - @mock.patch.object(os.path, 'exists', return_value=False) - def test_connect_volume_with_not_found_device(self, exists_mock, - sleep_mock): - location = '10.0.2.15:3260' - name = 'volume-00000001' - iqn = 'iqn.2010-10.org.openstack:%s' % name - vol = {'id': 1, 'name': name} - connection_info = self.iscsi_connection(vol, location, iqn) - self.assertRaises(exception.VolumeDeviceNotFound, - self.connector.connect_volume, - connection_info['data']) - - def test_get_target_portals_from_iscsiadm_output(self): - connector = self.connector - test_output = '''10.15.84.19:3260 iqn.1992-08.com.netapp:sn.33615311 - 10.15.85.19:3260 iqn.1992-08.com.netapp:sn.33615311''' - res = connector._get_target_portals_from_iscsiadm_output(test_output) - ip_iqn1 = ['10.15.84.19:3260', 'iqn.1992-08.com.netapp:sn.33615311'] - ip_iqn2 = ['10.15.85.19:3260', 'iqn.1992-08.com.netapp:sn.33615311'] - expected = [ip_iqn1, ip_iqn2] - self.assertEqual(expected, res) - - @mock.patch.object(os, 'walk') - def test_get_iscsi_devices(self, walk_mock): - paths = [('ip-10.0.0.1:3260-iscsi-iqn.2013-01.ro.' - 'com.netapp:node.netapp02-lun-0')] - walk_mock.return_value = [(['.'], ['by-path'], paths)] - self.assertEqual(paths, self.connector._get_iscsi_devices()) - - @mock.patch.object(os, 'walk', return_value=[]) - def test_get_iscsi_devices_with_empty_dir(self, walk_mock): - self.assertEqual([], self.connector._get_iscsi_devices()) - - @mock.patch.object(os.path, 'realpath') - @mock.patch.object(connector.ISCSIConnector, '_get_iscsi_devices') - def test_get_multipath_iqns(self, get_iscsi_mock, realpath_mock): - paths = [('ip-10.0.0.1:3260-iscsi-iqn.2013-01.ro.' - 'com.netapp:node.netapp02-lun-0')] - devpath = '/dev/disk/by-path/%s' % paths[0] - realpath_mock.return_value = devpath - get_iscsi_mock.return_value = paths - mpath_map = {devpath: paths[0]} - self.assertEqual(['iqn.2013-01.ro.com.netapp:node.netapp02'], - self.connector._get_multipath_iqns([paths[0]], - mpath_map)) - - @mock.patch.object(connector.ISCSIConnector, '_run_multipath') - def test_get_multipath_device_map(self, multipath_mock): - multipath_mock.return_value = [ - "Mar 17 14:32:37 | sda: No fc_host device for 'host-1'\n" - "mpathb (36e00000000010001) dm-4 IET ,VIRTUAL-DISK\n" - "size=1.0G features='0' hwhandler='0' wp=rw\n" - "|-+- policy='service-time 0' prio=0 status=active\n" - "| `- 2:0:0:1 sda 8:0 active undef running\n" - "`-+- policy='service-time 0' prio=0 status=enabled\n" - " `- 3:0:0:1 sdb 8:16 active undef running\n"] - expected = {'/dev/sda': '/dev/mapper/mpathb', - '/dev/sdb': '/dev/mapper/mpathb'} - self.assertEqual(expected, self.connector._get_multipath_device_map()) - - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_name_from_path') - @mock.patch.object(connector.ISCSIConnector, '_get_multipath_device_map') - @mock.patch.object(connector.ISCSIConnector, - '_get_target_portals_from_iscsiadm_output') - @mock.patch.object(connector.ISCSIConnector, '_rescan_iscsi') - @mock.patch.object(connector.ISCSIConnector, '_rescan_multipath') - @mock.patch.object(connector.ISCSIConnector, '_get_iscsi_devices') - @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') - @mock.patch.object(connector.ISCSIConnector, - '_disconnect_from_iscsi_portal') - @mock.patch.object(connector.ISCSIConnector, '_get_multipath_iqns') - @mock.patch.object(os.path, 'exists', return_value=True) - def test_disconnect_volume_multipath_iscsi( - self, exists_mock, multipath_iqn_mock, disconnect_mock, - get_all_devices_mock, get_iscsi_devices_mock, - rescan_multipath_mock, rescan_iscsi_mock, get_portals_mock, - get_multipath_device_map_mock, get_name_from_path_mock): - iqn1 = 'iqn.2013-01.ro.com.netapp:node.netapp01' - iqn2 = 'iqn.2013-01.ro.com.netapp:node.netapp02' - iqns = [iqn1, iqn2] - portal = '10.0.0.1:3260' - dev = ('ip-%s-iscsi-%s-lun-0' % (portal, iqn1)) - dev_name = '/dev/sdx' - - get_portals_mock.return_value = [[portal, iqn1]] - multipath_iqn_mock.return_value = iqns - get_name_from_path_mock.return_value = dev_name - get_all_devices_mock.return_value = [dev, '/dev/mapper/md-1'] - get_multipath_device_map_mock.return_value = { - dev_name: '/dev/mapper/md-3'} - get_iscsi_devices_mock.return_value = [] - fake_property = {'target_portal': portal, - 'target_iqn': iqn1} - self.connector._disconnect_volume_multipath_iscsi(fake_property, - 'fake/multipath') - # Target in use by other mp devices, don't disconnect - self.assertFalse(disconnect_mock.called) - - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_name_from_path') - @mock.patch.object(connector.ISCSIConnector, - '_get_target_portals_from_iscsiadm_output') - @mock.patch.object(connector.ISCSIConnector, '_rescan_iscsi') - @mock.patch.object(connector.ISCSIConnector, '_rescan_multipath') - @mock.patch.object(connector.ISCSIConnector, '_get_iscsi_devices') - @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') - @mock.patch.object(connector.ISCSIConnector, - '_disconnect_from_iscsi_portal') - @mock.patch.object(connector.ISCSIConnector, '_get_multipath_device_map') - @mock.patch.object(connector.ISCSIConnector, '_get_multipath_iqns') - @mock.patch.object(os.path, 'exists', return_value=True) - def test_disconnect_volume_multipath_iscsi_other_targets( - self, exists_mock, multipath_iqn_mock, get_multipath_map_mock, - disconnect_mock, get_all_devices_mock, get_iscsi_devices_mock, - rescan_multipath_mock, rescan_iscsi_mock, get_portals_mock, - get_name_from_path_mock): - iqn1 = 'iqn.2010-10.org.openstack:target-1' - iqn2 = 'iqn.2010-10.org.openstack:target-2' - portal = '10.0.0.1:3260' - dev2 = ('ip-%s-iscsi-%s-lun-0' % (portal, iqn2)) - dev_name = '/dev/sdx' - - # Multiple targets are discovered, but only block devices for target-1 - # is deleted and target-2 is in use. - get_portals_mock.return_value = [[portal, iqn1], [portal, iqn2]] - multipath_iqn_mock.return_value = [iqn2, iqn2] - get_name_from_path_mock.return_value = dev_name - get_all_devices_mock.return_value = [dev2, '/dev/mapper/md-1'] - get_multipath_map_mock.return_value = {dev_name: '/dev/mapper/md-3'} - get_iscsi_devices_mock.return_value = [dev2] - fake_property = {'target_portal': portal, - 'target_iqn': iqn1} - self.connector._disconnect_volume_multipath_iscsi(fake_property, - 'fake/multipath') - # Only target-1 should be disconneced. - disconnect_mock.assert_called_once_with(fake_property) - - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_name_from_path') - @mock.patch.object(connector.ISCSIConnector, '_get_multipath_device_map', - return_value={}) - @mock.patch.object(connector.ISCSIConnector, - '_get_target_portals_from_iscsiadm_output') - @mock.patch.object(connector.ISCSIConnector, '_rescan_iscsi') - @mock.patch.object(connector.ISCSIConnector, '_rescan_multipath') - @mock.patch.object(connector.ISCSIConnector, '_get_iscsi_devices', - return_value=[]) - @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices', - return_value=[]) - @mock.patch.object(connector.ISCSIConnector, - '_disconnect_from_iscsi_portal') - @mock.patch.object(os.path, 'exists', return_value=True) - def test_disconnect_volume_multipath_iscsi_without_other_mp_devices( - self, exists_mock, disconnect_mock, get_all_devices_mock, - get_iscsi_devices_mock, rescan_multipath_mock, rescan_iscsi_mock, - get_portals_mock, get_multipath_device_map_mock, - get_name_from_path_mock): - portal = '10.0.2.15:3260' - name = 'volume-00000001' - iqn = 'iqn.2010-10.org.openstack:%s' % name - - get_portals_mock.return_value = [[portal, iqn]] - fake_property = {'target_portal': portal, - 'target_iqn': iqn} - self.connector._disconnect_volume_multipath_iscsi(fake_property, - 'fake/multipath') - # Target not in use by other mp devices, disconnect - disconnect_mock.assert_called_once_with(fake_property) - - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_name_from_path') - @mock.patch.object(connector.ISCSIConnector, '_get_multipath_device_map', - return_value={}) - @mock.patch.object(connector.ISCSIConnector, - '_get_target_portals_from_iscsiadm_output') - @mock.patch.object(connector.ISCSIConnector, '_rescan_iscsi') - @mock.patch.object(connector.ISCSIConnector, '_rescan_multipath') - @mock.patch.object(connector.ISCSIConnector, '_get_iscsi_devices') - @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') - @mock.patch.object(connector.ISCSIConnector, - '_disconnect_from_iscsi_portal') - @mock.patch.object(os.path, 'exists', return_value=False) - def test_disconnect_volume_multipath_iscsi_with_invalid_symlink( - self, exists_mock, disconnect_mock, get_all_devices_mock, - get_iscsi_devices_mock, rescan_multipath_mock, rescan_iscsi_mock, - get_portals_mock, get_multipath_device_map_mock, - get_name_from_path_mock): - # Simulate a broken symlink by returning False for os.path.exists(dev) - portal = '10.0.0.1:3260' - name = 'volume-00000001' - iqn = 'iqn.2010-10.org.openstack:%s' % name - dev_name = '/dev/sdx' - - get_portals_mock.return_value = [[portal, iqn]] - get_name_from_path_mock.return_value = dev_name - get_all_devices_mock.return_value = [dev_name, '/dev/mapper/md-1'] - get_iscsi_devices_mock.return_value = [] - - fake_property = {'target_portal': portal, - 'target_iqn': iqn} - self.connector._disconnect_volume_multipath_iscsi(fake_property, - 'fake/multipath') - # Target not in use by other mp devices, disconnect - disconnect_mock.assert_called_once_with(fake_property) - - def test_iscsiadm_discover_parsing(self): - # Ensure that parsing iscsiadm discover ignores cruft. - - targets = [ - ["192.168.204.82:3260,1", - ("iqn.2010-10.org.openstack:volume-" - "f9b12623-6ce3-4dac-a71f-09ad4249bdd3")], - ["192.168.204.82:3261,1", - ("iqn.2010-10.org.openstack:volume-" - "f9b12623-6ce3-4dac-a71f-09ad4249bdd4")]] - - # This slight wonkiness brought to you by pep8, as the actual - # example output runs about 97 chars wide. - sample_input = """Loading iscsi modules: done -Starting iSCSI initiator service: done -Setting up iSCSI targets: unused -%s %s -%s %s -""" % (targets[0][0], targets[0][1], targets[1][0], targets[1][1]) - out = self.connector.\ - _get_target_portals_from_iscsiadm_output(sample_input) - self.assertEqual(targets, out) - - def test_sanitize_log_run_iscsiadm(self): - # Tests that the parameters to the _run_iscsiadm function - # are sanitized for when passwords are logged. - def fake_debug(*args, **kwargs): - self.assertIn('node.session.auth.password', args[0]) - self.assertNotIn('scrubme', args[0]) - - volume = {'id': 'fake_uuid'} - connection_info = self.iscsi_connection(volume, - "10.0.2.15:3260", - "fake_iqn") - - iscsi_properties = connection_info['data'] - with mock.patch.object(connector.LOG, 'debug', - side_effect=fake_debug) as debug_mock: - self.connector._iscsiadm_update(iscsi_properties, - 'node.session.auth.password', - 'scrubme') - - # we don't care what the log message is, we just want to make sure - # our stub method is called which asserts the password is scrubbed - self.assertTrue(debug_mock.called) - - @mock.patch.object(connector.ISCSIConnector, 'get_volume_paths') - def test_extend_volume_no_path(self, mock_volume_paths): - mock_volume_paths.return_value = [] - volume = {'id': 'fake_uuid'} - connection_info = self.iscsi_connection(volume, - "10.0.2.15:3260", - "fake_iqn") - - self.assertRaises(exception.VolumePathsNotFound, - self.connector.extend_volume, - connection_info['data']) - - @mock.patch.object(linuxscsi.LinuxSCSI, 'extend_volume') - @mock.patch.object(connector.ISCSIConnector, 'get_volume_paths') - def test_extend_volume(self, mock_volume_paths, mock_scsi_extend): - fake_new_size = 1024 - mock_volume_paths.return_value = ['/dev/vdx'] - mock_scsi_extend.return_value = fake_new_size - volume = {'id': 'fake_uuid'} - connection_info = self.iscsi_connection(volume, - "10.0.2.15:3260", - "fake_iqn") - new_size = self.connector.extend_volume(connection_info['data']) - self.assertEqual(fake_new_size, new_size) - - @mock.patch.object(os.path, 'isdir') - def test_get_all_available_volumes_path_not_dir(self, mock_isdir): - mock_isdir.return_value = False - expected = [] - actual = self.connector.get_all_available_volumes() - self.assertItemsEqual(expected, actual) - - @mock.patch.object(connector.ISCSIConnector, '_discover_iscsi_portals') - def test_get_potential_paths_failure_mpath_single_target(self, - mock_discover): - connection_properties = { - 'target_portal': '10.0.2.15:3260' - } - self.connector.use_multipath = True - mock_discover.side_effect = exception.BrickException() - self.assertRaises(exception.TargetPortalNotFound, - self.connector._get_potential_volume_paths, - connection_properties) - - @mock.patch.object(connector.ISCSIConnector, '_discover_iscsi_portals') - def test_get_potential_paths_failure_mpath_multi_target(self, - mock_discover): - connection_properties = { - 'target_portals': ['10.0.2.15:3260', '10.0.3.15:3260'] - } - self.connector.use_multipath = True - mock_discover.side_effect = exception.BrickException() - self.assertRaises(exception.TargetPortalsNotFound, - self.connector._get_potential_volume_paths, - connection_properties) - - -class FibreChannelConnectorTestCase(ConnectorTestCase): - def setUp(self): - super(FibreChannelConnectorTestCase, self).setUp() - self.connector = connector.FibreChannelConnector( - None, execute=self.fake_execute, use_multipath=False) - self.assertIsNotNone(self.connector) - self.assertIsNotNone(self.connector._linuxfc) - self.assertIsNotNone(self.connector._linuxscsi) - - def fake_get_fc_hbas(self): - return [{'ClassDevice': 'host1', - 'ClassDevicePath': '/sys/devices/pci0000:00/0000:00:03.0' - '/0000:05:00.2/host1/fc_host/host1', - 'dev_loss_tmo': '30', - 'fabric_name': '0x1000000533f55566', - 'issue_lip': '', - 'max_npiv_vports': '255', - 'maxframe_size': '2048 bytes', - 'node_name': '0x200010604b019419', - 'npiv_vports_inuse': '0', - 'port_id': '0x680409', - 'port_name': '0x100010604b019419', - 'port_state': 'Online', - 'port_type': 'NPort (fabric via point-to-point)', - 'speed': '10 Gbit', - 'supported_classes': 'Class 3', - 'supported_speeds': '10 Gbit', - 'symbolic_name': 'Emulex 554M FV4.0.493.0 DV8.3.27', - 'tgtid_bind_type': 'wwpn (World Wide Port Name)', - 'uevent': None, - 'vport_create': '', - 'vport_delete': ''}] - - def fake_get_fc_hbas_info(self): - hbas = self.fake_get_fc_hbas() - info = [{'port_name': hbas[0]['port_name'].replace('0x', ''), - 'node_name': hbas[0]['node_name'].replace('0x', ''), - 'host_device': hbas[0]['ClassDevice'], - 'device_path': hbas[0]['ClassDevicePath']}] - return info - - def fibrechan_connection(self, volume, location, wwn): - return {'driver_volume_type': 'fibrechan', - 'data': { - 'volume_id': volume['id'], - 'target_portal': location, - 'target_wwn': wwn, - 'target_lun': 1, - }} - - @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas') - def test_get_connector_properties(self, mock_hbas): - mock_hbas.return_value = self.fake_get_fc_hbas() - multipath = True - enforce_multipath = True - props = connector.FibreChannelConnector.get_connector_properties( - 'sudo', multipath=multipath, - enforce_multipath=enforce_multipath) - - hbas = self.fake_get_fc_hbas() - expected_props = {'wwpns': [hbas[0]['port_name'].replace('0x', '')], - 'wwnns': [hbas[0]['node_name'].replace('0x', '')]} - self.assertEqual(expected_props, props) - - def test_get_search_path(self): - search_path = self.connector.get_search_path() - expected = "/dev/disk/by-path" - self.assertEqual(expected, search_path) - - def test_get_pci_num(self): - hba = {'device_path': "/sys/devices/pci0000:00/0000:00:03.0" - "/0000:05:00.3/host2/fc_host/host2"} - pci_num = self.connector._get_pci_num(hba) - self.assertEqual("0000:05:00.3", pci_num) - - hba = {'device_path': "/sys/devices/pci0000:00/0000:00:03.0" - "/0000:05:00.3/0000:06:00.6/host2/fc_host/host2"} - pci_num = self.connector._get_pci_num(hba) - self.assertEqual("0000:06:00.6", pci_num) - - hba = {'device_path': "/sys/devices/pci0000:20/0000:20:03.0" - "/0000:21:00.2/net/ens2f2/ctlr_2/host3" - "/fc_host/host3"} - pci_num = self.connector._get_pci_num(hba) - self.assertEqual("0000:21:00.2", pci_num) - - @mock.patch.object(os.path, 'exists', return_value=True) - @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas') - @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info') - def test_get_volume_paths(self, fake_fc_hbas_info, - fake_fc_hbas, fake_exists): - fake_fc_hbas.side_effect = self.fake_get_fc_hbas - fake_fc_hbas_info.side_effect = self.fake_get_fc_hbas_info - - name = 'volume-00000001' - vol = {'id': 1, 'name': name} - location = '10.0.2.15:3260' - wwn = '1234567890123456' - connection_info = self.fibrechan_connection(vol, location, wwn) - volume_paths = self.connector.get_volume_paths( - connection_info['data']) - - expected = ['/dev/disk/by-path/pci-0000:05:00.2' - '-fc-0x1234567890123456-lun-1'] - self.assertEqual(expected, volume_paths) - - @mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw') - @mock.patch.object(os.path, 'exists', return_value=True) - @mock.patch.object(os.path, 'realpath', return_value='/dev/sdb') - @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas') - @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info') - @mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device') - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info') - def test_connect_volume(self, get_device_info_mock, - get_scsi_wwn_mock, - remove_device_mock, - get_fc_hbas_info_mock, - get_fc_hbas_mock, - realpath_mock, - exists_mock, - wait_for_rw_mock): - get_fc_hbas_mock.side_effect = self.fake_get_fc_hbas - get_fc_hbas_info_mock.side_effect = self.fake_get_fc_hbas_info - - wwn = '1234567890' - multipath_devname = '/dev/md-1' - devices = {"device": multipath_devname, - "id": wwn, - "devices": [{'device': '/dev/sdb', - 'address': '1:0:0:1', - 'host': 1, 'channel': 0, - 'id': 0, 'lun': 1}]} - get_device_info_mock.return_value = devices['devices'][0] - get_scsi_wwn_mock.return_value = wwn - - location = '10.0.2.15:3260' - name = 'volume-00000001' - vol = {'id': 1, 'name': name} - # Should work for string, unicode, and list - wwns = ['1234567890123456', six.text_type('1234567890123456'), - ['1234567890123456', '1234567890123457']] - for wwn in wwns: - connection_info = self.fibrechan_connection(vol, location, wwn) - dev_info = self.connector.connect_volume(connection_info['data']) - exp_wwn = wwn[0] if isinstance(wwn, list) else wwn - dev_str = ('/dev/disk/by-path/pci-0000:05:00.2-fc-0x%s-lun-1' % - exp_wwn) - self.assertEqual('block', dev_info['type']) - self.assertEqual(dev_str, dev_info['path']) - self.assertTrue('multipath_id' not in dev_info) - self.assertTrue('devices' not in dev_info) - - self.connector.disconnect_volume(connection_info['data'], dev_info) - expected_commands = [] - self.assertEqual(expected_commands, self.cmds) - - # Should not work for anything other than string, unicode, and list - connection_info = self.fibrechan_connection(vol, location, 123) - self.assertRaises(exception.NoFibreChannelHostsFound, - self.connector.connect_volume, - connection_info['data']) - - get_fc_hbas_mock.side_effect = [[]] - get_fc_hbas_info_mock.side_effect = [[]] - self.assertRaises(exception.NoFibreChannelHostsFound, - self.connector.connect_volume, - connection_info['data']) - - def _test_connect_volume_multipath(self, get_device_info_mock, - get_scsi_wwn_mock, - remove_device_mock, - get_fc_hbas_info_mock, - get_fc_hbas_mock, - realpath_mock, - exists_mock, - wait_for_rw_mock, - find_mp_dev_mock, - access_mode, - should_wait_for_rw): - self.connector.use_multipath = True - get_fc_hbas_mock.side_effect = self.fake_get_fc_hbas - get_fc_hbas_info_mock.side_effect = self.fake_get_fc_hbas_info - - wwn = '1234567890' - multipath_devname = '/dev/md-1' - devices = {"device": multipath_devname, - "id": wwn, - "devices": [{'device': '/dev/sdb', - 'address': '1:0:0:1', - 'host': 1, 'channel': 0, - 'id': 0, 'lun': 1}]} - get_device_info_mock.return_value = devices['devices'][0] - get_scsi_wwn_mock.return_value = wwn - - location = '10.0.2.15:3260' - name = 'volume-00000001' - vol = {'id': 1, 'name': name} - initiator_wwn = ['1234567890123456', '1234567890123457'] - - find_mp_dev_mock.return_value = '/dev/disk/by-id/dm-uuid-mpath-' + wwn - - connection_info = self.fibrechan_connection(vol, location, - initiator_wwn) - connection_info['data']['access_mode'] = access_mode - - self.connector.connect_volume(connection_info['data']) - - self.assertEqual(should_wait_for_rw, wait_for_rw_mock.called) - return connection_info - - @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device') - @mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw') - @mock.patch.object(os.path, 'exists', return_value=True) - @mock.patch.object(os.path, 'realpath', return_value='/dev/sdb') - @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas') - @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info') - @mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device') - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info') - def test_connect_volume_multipath_rw(self, get_device_info_mock, - get_scsi_wwn_mock, - remove_device_mock, - get_fc_hbas_info_mock, - get_fc_hbas_mock, - realpath_mock, - exists_mock, - wait_for_rw_mock, - find_mp_dev_mock): - - self._test_connect_volume_multipath(get_device_info_mock, - get_scsi_wwn_mock, - remove_device_mock, - get_fc_hbas_info_mock, - get_fc_hbas_mock, - realpath_mock, - exists_mock, - wait_for_rw_mock, - find_mp_dev_mock, - 'rw', - True) - - @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device') - @mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw') - @mock.patch.object(os.path, 'exists', return_value=True) - @mock.patch.object(os.path, 'realpath', return_value='/dev/sdb') - @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas') - @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info') - @mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device') - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info') - def test_connect_volume_multipath_no_access_mode(self, - get_device_info_mock, - get_scsi_wwn_mock, - remove_device_mock, - get_fc_hbas_info_mock, - get_fc_hbas_mock, - realpath_mock, - exists_mock, - wait_for_rw_mock, - find_mp_dev_mock): - - self._test_connect_volume_multipath(get_device_info_mock, - get_scsi_wwn_mock, - remove_device_mock, - get_fc_hbas_info_mock, - get_fc_hbas_mock, - realpath_mock, - exists_mock, - wait_for_rw_mock, - find_mp_dev_mock, - None, - True) - - @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device') - @mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw') - @mock.patch.object(os.path, 'exists', return_value=True) - @mock.patch.object(os.path, 'realpath', return_value='/dev/sdb') - @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas') - @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info') - @mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device') - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info') - def test_connect_volume_multipath_ro(self, get_device_info_mock, - get_scsi_wwn_mock, - remove_device_mock, - get_fc_hbas_info_mock, - get_fc_hbas_mock, - realpath_mock, - exists_mock, - wait_for_rw_mock, - find_mp_dev_mock): - - self._test_connect_volume_multipath(get_device_info_mock, - get_scsi_wwn_mock, - remove_device_mock, - get_fc_hbas_info_mock, - get_fc_hbas_mock, - realpath_mock, - exists_mock, - wait_for_rw_mock, - find_mp_dev_mock, - 'ro', - False) - - @mock.patch.object(connector.BaseLinuxConnector, '_discover_mpath_device') - @mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device') - @mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw') - @mock.patch.object(os.path, 'exists', return_value=True) - @mock.patch.object(os.path, 'realpath', return_value='/dev/sdb') - @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas') - @mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info') - @mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device') - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') - @mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info') - def test_connect_volume_multipath_not_found(self, - get_device_info_mock, - get_scsi_wwn_mock, - remove_device_mock, - get_fc_hbas_info_mock, - get_fc_hbas_mock, - realpath_mock, - exists_mock, - wait_for_rw_mock, - find_mp_dev_mock, - discover_mp_dev_mock): - discover_mp_dev_mock.return_value = ("/dev/disk/by-path/something", - None) - - connection_info = self._test_connect_volume_multipath( - get_device_info_mock, get_scsi_wwn_mock, remove_device_mock, - get_fc_hbas_info_mock, get_fc_hbas_mock, realpath_mock, - exists_mock, wait_for_rw_mock, find_mp_dev_mock, - 'rw', False) - - self.assertNotIn('multipathd_id', connection_info['data']) - - @mock.patch.object(connector.FibreChannelConnector, 'get_volume_paths') - def test_extend_volume_no_path(self, mock_volume_paths): - mock_volume_paths.return_value = [] - volume = {'id': 'fake_uuid'} - wwn = '1234567890123456' - connection_info = self.fibrechan_connection(volume, - "10.0.2.15:3260", - wwn) - - self.assertRaises(exception.VolumePathsNotFound, - self.connector.extend_volume, - connection_info['data']) - - @mock.patch.object(linuxscsi.LinuxSCSI, 'extend_volume') - @mock.patch.object(connector.FibreChannelConnector, 'get_volume_paths') - def test_extend_volume(self, mock_volume_paths, mock_scsi_extend): - fake_new_size = 1024 - mock_volume_paths.return_value = ['/dev/vdx'] - mock_scsi_extend.return_value = fake_new_size - volume = {'id': 'fake_uuid'} - wwn = '1234567890123456' - connection_info = self.fibrechan_connection(volume, - "10.0.2.15:3260", - wwn) - new_size = self.connector.extend_volume(connection_info['data']) - self.assertEqual(fake_new_size, new_size) - - @mock.patch.object(os.path, 'isdir') - def test_get_all_available_volumes_path_not_dir(self, mock_isdir): - mock_isdir.return_value = False - expected = [] - actual = self.connector.get_all_available_volumes() - self.assertItemsEqual(expected, actual) - - -class FibreChannelConnectorS390XTestCase(ConnectorTestCase): - - def setUp(self): - super(FibreChannelConnectorS390XTestCase, self).setUp() - self.connector = connector.FibreChannelConnectorS390X( - None, execute=self.fake_execute, use_multipath=False) - self.assertIsNotNone(self.connector) - self.assertIsNotNone(self.connector._linuxfc) - self.assertEqual("LinuxFibreChannelS390X", - self.connector._linuxfc.__class__.__name__) - self.assertIsNotNone(self.connector._linuxscsi) - - @mock.patch.object(linuxfc.LinuxFibreChannelS390X, 'configure_scsi_device') - def test_get_host_devices(self, mock_configure_scsi_device): - lun = 2 - possible_devs = [(3, 5), ] - devices = self.connector._get_host_devices(possible_devs, lun) - mock_configure_scsi_device.assert_called_with(3, 5, - "0x0002000000000000") - self.assertEqual(1, len(devices)) - device_path = "/dev/disk/by-path/ccw-3-zfcp-5:0x0002000000000000" - self.assertEqual(device_path, devices[0]) - - def test_get_lun_string(self): - lun = 1 - lunstring = self.connector._get_lun_string(lun) - self.assertEqual("0x0001000000000000", lunstring) - lun = 0xff - lunstring = self.connector._get_lun_string(lun) - self.assertEqual("0x00ff000000000000", lunstring) - lun = 0x101 - lunstring = self.connector._get_lun_string(lun) - self.assertEqual("0x0101000000000000", lunstring) - lun = 0x4020400a - lunstring = self.connector._get_lun_string(lun) - self.assertEqual("0x4020400a00000000", lunstring) - - @mock.patch.object(connector.FibreChannelConnectorS390X, - '_get_possible_devices', return_value=[(3, 5), ]) - @mock.patch.object(linuxfc.LinuxFibreChannelS390X, 'get_fc_hbas_info', - return_value=[]) - @mock.patch.object(linuxfc.LinuxFibreChannelS390X, - 'deconfigure_scsi_device') - def test_remove_devices(self, mock_deconfigure_scsi_device, - mock_get_fc_hbas_info, mock_get_possible_devices): - connection_properties = {'target_wwn': 5, 'target_lun': 2} - self.connector._remove_devices(connection_properties, devices=None) - mock_deconfigure_scsi_device.assert_called_with(3, 5, - "0x0002000000000000") - mock_get_fc_hbas_info.assert_called_once_with() - mock_get_possible_devices.assert_called_once_with([], 5) - - -class FakeFixedIntervalLoopingCall(object): - def __init__(self, f=None, *args, **kw): - self.args = args - self.kw = kw - self.f = f - self._stop = False - - def stop(self): - self._stop = True - - def wait(self): - return self - - def start(self, interval, initial_delay=None): - while not self._stop: - try: - self.f(*self.args, **self.kw) - except loopingcall.LoopingCallDone: - return self - except Exception: - LOG.exception(_LE('in fixed duration looping call')) - raise - - -class AoEConnectorTestCase(ConnectorTestCase): - """Test cases for AoE initiator class.""" - def setUp(self): - super(AoEConnectorTestCase, self).setUp() - self.connector = connector.AoEConnector('sudo') - self.connection_properties = {'target_shelf': 'fake_shelf', - 'target_lun': 'fake_lun'} - mock.patch.object(loopingcall, 'FixedIntervalLoopingCall', - FakeFixedIntervalLoopingCall).start() - self.addCleanup(mock.patch.stopall) - - def test_get_search_path(self): - expected = "/dev/etherd" - actual_path = self.connector.get_search_path() - self.assertEqual(expected, actual_path) - - @mock.patch.object(os.path, 'exists', return_value=True) - def test_get_volume_paths(self, mock_exists): - expected = ["/dev/etherd/efake_shelf.fake_lun"] - paths = self.connector.get_volume_paths(self.connection_properties) - self.assertEqual(expected, paths) - - def test_get_connector_properties(self): - props = connector.AoEConnector.get_connector_properties( - 'sudo', multipath=True, enforce_multipath=True) - - expected_props = {} - self.assertEqual(expected_props, props) - - @mock.patch.object(os.path, 'exists', side_effect=[True, True]) - def test_connect_volume(self, exists_mock): - """Ensure that if path exist aoe-revalidate was called.""" - aoe_device, aoe_path = self.connector._get_aoe_info( - self.connection_properties) - with mock.patch.object(self.connector, '_execute', - return_value=["", ""]): - self.connector.connect_volume(self.connection_properties) - - @mock.patch.object(os.path, 'exists', side_effect=[False, True]) - def test_connect_volume_without_path(self, exists_mock): - """Ensure that if path doesn't exist aoe-discovery was called.""" - - aoe_device, aoe_path = self.connector._get_aoe_info( - self.connection_properties) - expected_info = { - 'type': 'block', - 'device': aoe_device, - 'path': aoe_path, - } - - with mock.patch.object(self.connector, '_execute', - return_value=["", ""]): - volume_info = self.connector.connect_volume( - self.connection_properties) - - self.assertDictMatch(volume_info, expected_info) - - @mock.patch.object(os.path, 'exists', return_value=False) - def test_connect_volume_could_not_discover_path(self, exists_mock): - _aoe_device, aoe_path = self.connector._get_aoe_info( - self.connection_properties) - - with mock.patch.object(self.connector, '_execute', - return_value=["", ""]): - self.assertRaises(exception.VolumeDeviceNotFound, - self.connector.connect_volume, - self.connection_properties) - - @mock.patch.object(os.path, 'exists', return_value=True) - def test_disconnect_volume(self, mock_exists): - """Ensure that if path exist aoe-revaliadte was called.""" - aoe_device, aoe_path = self.connector._get_aoe_info( - self.connection_properties) - - with mock.patch.object(self.connector, '_execute', - return_value=["", ""]): - self.connector.disconnect_volume(self.connection_properties, {}) - - def test_extend_volume(self): - self.assertRaises(NotImplementedError, - self.connector.extend_volume, - self.connection_properties) - - -class RemoteFsConnectorTestCase(ConnectorTestCase): - """Test cases for Remote FS initiator class.""" - TEST_DEV = '172.18.194.100:/var/nfs' - TEST_PATH = '/mnt/test/df0808229363aad55c27da50c38d6328' - TEST_BASE = '/mnt/test' - TEST_NAME = '9c592d52-ce47-4263-8c21-4ecf3c029cdb' - - def setUp(self): - super(RemoteFsConnectorTestCase, self).setUp() - self.connection_properties = { - 'export': self.TEST_DEV, - 'name': self.TEST_NAME} - self.connector = connector.RemoteFsConnector( - 'nfs', root_helper='sudo', - nfs_mount_point_base=self.TEST_BASE, - nfs_mount_options='vers=3') - - @mock.patch('os_brick.remotefs.remotefs.ScalityRemoteFsClient') - def test_init_with_scality(self, mock_scality_remotefs_client): - connector.RemoteFsConnector('scality', root_helper='sudo') - self.assertEqual(1, mock_scality_remotefs_client.call_count) - - def test_get_connector_properties(self): - props = connector.RemoteFsConnector.get_connector_properties( - 'sudo', multipath=True, enforce_multipath=True) - - expected_props = {} - self.assertEqual(expected_props, props) - - def test_get_search_path(self): - expected = self.TEST_BASE - actual = self.connector.get_search_path() - self.assertEqual(expected, actual) - - @mock.patch.object(remotefs.RemoteFsClient, 'mount') - def test_get_volume_paths(self, mock_mount): - path = ("%(path)s/%(name)s" % {'path': self.TEST_PATH, - 'name': self.TEST_NAME}) - expected = [path] - actual = self.connector.get_volume_paths(self.connection_properties) - self.assertEqual(expected, actual) - - @mock.patch.object(remotefs.RemoteFsClient, 'mount') - @mock.patch.object(remotefs.RemoteFsClient, 'get_mount_point', - return_value="something") - def test_connect_volume(self, mount_point_mock, mount_mock): - """Test the basic connect volume case.""" - self.connector.connect_volume(self.connection_properties) - - def test_disconnect_volume(self): - """Nothing should happen here -- make sure it doesn't blow up.""" - self.connector.disconnect_volume(self.connection_properties, {}) - - def test_extend_volume(self): - self.assertRaises(NotImplementedError, - self.connector.extend_volume, - self.connection_properties) - - -class LocalConnectorTestCase(ConnectorTestCase): - - def setUp(self): - super(LocalConnectorTestCase, self).setUp() - self.connection_properties = {'name': 'foo', - 'device_path': '/tmp/bar'} - self.connector = connector.LocalConnector(None) - - def test_get_connector_properties(self): - props = connector.LocalConnector.get_connector_properties( - 'sudo', multipath=True, enforce_multipath=True) - - expected_props = {} - self.assertEqual(expected_props, props) - - def test_get_search_path(self): - actual = self.connector.get_search_path() - self.assertIsNone(actual) - - def test_get_volume_paths(self): - expected = [self.connection_properties['device_path']] - actual = self.connector.get_volume_paths( - self.connection_properties) - self.assertEqual(expected, actual) - - def test_connect_volume(self): - cprops = self.connection_properties - dev_info = self.connector.connect_volume(cprops) - self.assertEqual('local', dev_info['type']) - self.assertEqual(cprops['device_path'], dev_info['path']) - - def test_connect_volume_with_invalid_connection_data(self): - cprops = {} - self.assertRaises(ValueError, - self.connector.connect_volume, cprops) - - def test_extend_volume(self): - self.assertRaises(NotImplementedError, - self.connector.extend_volume, - self.connection_properties) - - -class HuaweiStorHyperConnectorTestCase(ConnectorTestCase): - """Test cases for StorHyper initiator class.""" - - attached = False - - def setUp(self): - super(HuaweiStorHyperConnectorTestCase, self).setUp() - self.fake_sdscli_file = tempfile.mktemp() - self.addCleanup(os.remove, self.fake_sdscli_file) - newefile = open(self.fake_sdscli_file, 'w') - newefile.write('test') - newefile.close() - - self.connector = connector.HuaweiStorHyperConnector( - None, execute=self.fake_execute) - self.connector.cli_path = self.fake_sdscli_file - self.connector.iscliexist = True - - self.connector_fail = connector.HuaweiStorHyperConnector( - None, execute=self.fake_execute_fail) - self.connector_fail.cli_path = self.fake_sdscli_file - self.connector_fail.iscliexist = True - - self.connector_nocli = connector.HuaweiStorHyperConnector( - None, execute=self.fake_execute_fail) - self.connector_nocli.cli_path = self.fake_sdscli_file - self.connector_nocli.iscliexist = False - - self.connection_properties = { - 'access_mode': 'rw', - 'qos_specs': None, - 'volume_id': 'volume-b2911673-863c-4380-a5f2-e1729eecfe3f' - } - - self.device_info = {'type': 'block', - 'path': '/dev/vdxxx'} - HuaweiStorHyperConnectorTestCase.attached = False - - def fake_execute(self, *cmd, **kwargs): - method = cmd[2] - self.cmds.append(" ".join(cmd)) - if 'attach' == method: - HuaweiStorHyperConnectorTestCase.attached = True - return 'ret_code=0', None - if 'querydev' == method: - if HuaweiStorHyperConnectorTestCase.attached: - return 'ret_code=0\ndev_addr=/dev/vdxxx', None - else: - return 'ret_code=1\ndev_addr=/dev/vdxxx', None - if 'detach' == method: - HuaweiStorHyperConnectorTestCase.attached = False - return 'ret_code=0', None - - def fake_execute_fail(self, *cmd, **kwargs): - method = cmd[2] - self.cmds.append(" ".join(cmd)) - if 'attach' == method: - HuaweiStorHyperConnectorTestCase.attached = False - return 'ret_code=330151401', None - if 'querydev' == method: - if HuaweiStorHyperConnectorTestCase.attached: - return 'ret_code=0\ndev_addr=/dev/vdxxx', None - else: - return 'ret_code=1\ndev_addr=/dev/vdxxx', None - if 'detach' == method: - HuaweiStorHyperConnectorTestCase.attached = True - return 'ret_code=330155007', None - - def test_get_connector_properties(self): - props = connector.HuaweiStorHyperConnector.get_connector_properties( - 'sudo', multipath=True, enforce_multipath=True) - - expected_props = {} - self.assertEqual(expected_props, props) - - def test_get_search_path(self): - actual = self.connector.get_search_path() - self.assertIsNone(actual) - - @mock.patch.object(connector.HuaweiStorHyperConnector, - '_query_attached_volume') - def test_get_volume_paths(self, mock_query_attached): - path = self.device_info['path'] - mock_query_attached.return_value = {'ret_code': 0, - 'dev_addr': path} - - expected = [path] - actual = self.connector.get_volume_paths(self.connection_properties) - self.assertEqual(expected, actual) - - def test_connect_volume(self): - """Test the basic connect volume case.""" - - retval = self.connector.connect_volume(self.connection_properties) - self.assertEqual(self.device_info, retval) - - expected_commands = [self.fake_sdscli_file + ' -c attach' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', - self.fake_sdscli_file + ' -c querydev' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f'] - LOG.debug("self.cmds = %s.", self.cmds) - LOG.debug("expected = %s.", expected_commands) - - self.assertEqual(expected_commands, self.cmds) - - def test_disconnect_volume(self): - """Test the basic disconnect volume case.""" - self.connector.connect_volume(self.connection_properties) - self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached) - self.connector.disconnect_volume(self.connection_properties, - self.device_info) - self.assertEqual(False, HuaweiStorHyperConnectorTestCase.attached) - - expected_commands = [self.fake_sdscli_file + ' -c attach' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', - self.fake_sdscli_file + ' -c querydev' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', - self.fake_sdscli_file + ' -c detach' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f'] - - LOG.debug("self.cmds = %s.", self.cmds) - LOG.debug("expected = %s.", expected_commands) - - self.assertEqual(expected_commands, self.cmds) - - def test_is_volume_connected(self): - """Test if volume connected to host case.""" - self.connector.connect_volume(self.connection_properties) - self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached) - is_connected = self.connector.is_volume_connected( - 'volume-b2911673-863c-4380-a5f2-e1729eecfe3f') - self.assertEqual(HuaweiStorHyperConnectorTestCase.attached, - is_connected) - self.connector.disconnect_volume(self.connection_properties, - self.device_info) - self.assertEqual(False, HuaweiStorHyperConnectorTestCase.attached) - is_connected = self.connector.is_volume_connected( - 'volume-b2911673-863c-4380-a5f2-e1729eecfe3f') - self.assertEqual(HuaweiStorHyperConnectorTestCase.attached, - is_connected) - - expected_commands = [self.fake_sdscli_file + ' -c attach' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', - self.fake_sdscli_file + ' -c querydev' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', - self.fake_sdscli_file + ' -c querydev' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', - self.fake_sdscli_file + ' -c detach' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', - self.fake_sdscli_file + ' -c querydev' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f'] - - LOG.debug("self.cmds = %s.", self.cmds) - LOG.debug("expected = %s.", expected_commands) - - self.assertEqual(expected_commands, self.cmds) - - def test__analyze_output(self): - cliout = 'ret_code=0\ndev_addr=/dev/vdxxx\nret_desc="success"' - analyze_result = {'dev_addr': '/dev/vdxxx', - 'ret_desc': '"success"', - 'ret_code': '0'} - result = self.connector._analyze_output(cliout) - self.assertEqual(analyze_result, result) - - def test_connect_volume_fail(self): - """Test the fail connect volume case.""" - self.assertRaises(exception.BrickException, - self.connector_fail.connect_volume, - self.connection_properties) - expected_commands = [self.fake_sdscli_file + ' -c attach' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f'] - LOG.debug("self.cmds = %s.", self.cmds) - LOG.debug("expected = %s.", expected_commands) - self.assertEqual(expected_commands, self.cmds) - - def test_disconnect_volume_fail(self): - """Test the fail disconnect volume case.""" - self.connector.connect_volume(self.connection_properties) - self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached) - self.assertRaises(exception.BrickException, - self.connector_fail.disconnect_volume, - self.connection_properties, - self.device_info) - - expected_commands = [self.fake_sdscli_file + ' -c attach' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', - self.fake_sdscli_file + ' -c querydev' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', - self.fake_sdscli_file + ' -c detach' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f'] - - LOG.debug("self.cmds = %s.", self.cmds) - LOG.debug("expected = %s.", expected_commands) - - self.assertEqual(expected_commands, self.cmds) - - def test_connect_volume_nocli(self): - """Test the fail connect volume case.""" - self.assertRaises(exception.BrickException, - self.connector_nocli.connect_volume, - self.connection_properties) - - def test_disconnect_volume_nocli(self): - """Test the fail disconnect volume case.""" - self.connector.connect_volume(self.connection_properties) - self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached) - self.assertRaises(exception.BrickException, - self.connector_nocli.disconnect_volume, - self.connection_properties, - self.device_info) - expected_commands = [self.fake_sdscli_file + ' -c attach' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f', - self.fake_sdscli_file + ' -c querydev' - ' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f'] - - LOG.debug("self.cmds = %s.", self.cmds) - LOG.debug("expected = %s.", expected_commands) - - def test_extend_volume(self): - self.assertRaises(NotImplementedError, - self.connector.extend_volume, - self.connection_properties) - - -class HGSTConnectorTestCase(ConnectorTestCase): - """Test cases for HGST initiator class.""" - - IP_OUTPUT = """ -1: lo: mtu 65536 qdisc noqueue state UNKNOWN - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 - inet 127.0.0.1/8 scope host lo - valid_lft forever preferred_lft forever - inet 169.254.169.254/32 scope link lo - valid_lft forever preferred_lft forever - inet6 ::1/128 scope host - valid_lft forever preferred_lft forever -2: em1: mtu 1500 qdisc mq master - link/ether 00:25:90:d9:18:08 brd ff:ff:ff:ff:ff:ff - inet6 fe80::225:90ff:fed9:1808/64 scope link - valid_lft forever preferred_lft forever -3: em2: mtu 1500 qdisc mq state - link/ether 00:25:90:d9:18:09 brd ff:ff:ff:ff:ff:ff - inet 192.168.0.23/24 brd 192.168.0.255 scope global em2 - valid_lft forever preferred_lft forever - inet6 fe80::225:90ff:fed9:1809/64 scope link - valid_lft forever preferred_lft forever - """ - - DOMAIN_OUTPUT = """localhost""" - - DOMAIN_FAILED = """this.better.not.resolve.to.a.name.or.else""" - - SET_APPHOST_OUTPUT = """ -VLVM_SET_APPHOSTS0000000395 -Request Succeeded - """ - - def setUp(self): - super(HGSTConnectorTestCase, self).setUp() - self.connector = connector.HGSTConnector( - None, execute=self._fake_exec) - self._fail_set_apphosts = False - self._fail_ip = False - self._fail_domain_list = False - - def _fake_exec_set_apphosts(self, *cmd): - if self._fail_set_apphosts: - raise putils.ProcessExecutionError(None, None, 1) - else: - return self.SET_APPHOST_OUTPUT, '' - - def _fake_exec_ip(self, *cmd): - if self._fail_ip: - # Remove localhost so there is no IP match - return self.IP_OUTPUT.replace("127.0.0.1", "x.x.x.x"), '' - else: - return self.IP_OUTPUT, '' - - def _fake_exec_domain_list(self, *cmd): - if self._fail_domain_list: - return self.DOMAIN_FAILED, '' - else: - return self.DOMAIN_OUTPUT, '' - - def _fake_exec(self, *cmd, **kwargs): - self.cmdline = " ".join(cmd) - if cmd[0] == "ip": - return self._fake_exec_ip(*cmd) - elif cmd[0] == "vgc-cluster": - if cmd[1] == "domain-list": - return self._fake_exec_domain_list(*cmd) - elif cmd[1] == "space-set-apphosts": - return self._fake_exec_set_apphosts(*cmd) - else: - return '', '' - - def test_factory(self): - """Can we instantiate a HGSTConnector of the right kind?""" - obj = connector.InitiatorConnector.factory('HGST', None) - self.assertEqual("HGSTConnector", obj.__class__.__name__) - - def test_get_search_path(self): - expected = "/dev" - actual = self.connector.get_search_path() - self.assertEqual(expected, actual) - - @mock.patch.object(os.path, 'exists', return_value=True) - def test_get_volume_paths(self, mock_exists): - - cprops = {'name': 'space', 'noremovehost': 'stor1'} - path = "/dev/%s" % cprops['name'] - expected = [path] - actual = self.connector.get_volume_paths(cprops) - self.assertEqual(expected, actual) - - def test_connect_volume(self): - """Tests that a simple connection succeeds""" - self._fail_set_apphosts = False - self._fail_ip = False - self._fail_domain_list = False - cprops = {'name': 'space', 'noremovehost': 'stor1'} - dev_info = self.connector.connect_volume(cprops) - self.assertEqual('block', dev_info['type']) - self.assertEqual('space', dev_info['device']) - self.assertEqual('/dev/space', dev_info['path']) - - def test_get_connector_properties(self): - props = connector.HGSTConnector.get_connector_properties( - 'sudo', multipath=True, enforce_multipath=True) - - expected_props = {} - self.assertEqual(expected_props, props) - - def test_connect_volume_nohost_fail(self): - """This host should not be found, connect should fail.""" - self._fail_set_apphosts = False - self._fail_ip = True - self._fail_domain_list = False - cprops = {'name': 'space', 'noremovehost': 'stor1'} - self.assertRaises(exception.BrickException, - self.connector.connect_volume, - cprops) - - def test_connect_volume_nospace_fail(self): - """The space command will fail, exception to be thrown""" - self._fail_set_apphosts = True - self._fail_ip = False - self._fail_domain_list = False - cprops = {'name': 'space', 'noremovehost': 'stor1'} - self.assertRaises(exception.BrickException, - self.connector.connect_volume, - cprops) - - def test_disconnect_volume(self): - """Simple disconnection should pass and disconnect me""" - self._fail_set_apphosts = False - self._fail_ip = False - self._fail_domain_list = False - self._cmdline = "" - cprops = {'name': 'space', 'noremovehost': 'stor1'} - self.connector.disconnect_volume(cprops, None) - exp_cli = ("vgc-cluster space-set-apphosts -n space " - "-A localhost --action DELETE") - self.assertEqual(exp_cli, self.cmdline) - - def test_disconnect_volume_nohost(self): - """Should not run a setapphosts because localhost will""" - """be the noremotehost""" - self._fail_set_apphosts = False - self._fail_ip = False - self._fail_domain_list = False - self._cmdline = "" - cprops = {'name': 'space', 'noremovehost': 'localhost'} - self.connector.disconnect_volume(cprops, None) - # The last command should be the IP listing, not set apphosts - exp_cli = ("ip addr list") - self.assertEqual(exp_cli, self.cmdline) - - def test_disconnect_volume_fails(self): - """The set-apphosts should fail, exception to be thrown""" - self._fail_set_apphosts = True - self._fail_ip = False - self._fail_domain_list = False - self._cmdline = "" - cprops = {'name': 'space', 'noremovehost': 'stor1'} - self.assertRaises(exception.BrickException, - self.connector.disconnect_volume, - cprops, None) - - def test_bad_connection_properties(self): - """Send in connection_properties missing required fields""" - # Invalid connection_properties - self.assertRaises(exception.BrickException, - self.connector.connect_volume, - None) - # Name required for connect_volume - cprops = {'noremovehost': 'stor1'} - self.assertRaises(exception.BrickException, - self.connector.connect_volume, - cprops) - # Invalid connection_properties - self.assertRaises(exception.BrickException, - self.connector.disconnect_volume, - None, None) - # Name and noremovehost needed for disconnect_volume - cprops = {'noremovehost': 'stor1'} - self.assertRaises(exception.BrickException, - self.connector.disconnect_volume, - cprops, None) - cprops = {'name': 'space'} - self.assertRaises(exception.BrickException, - self.connector.disconnect_volume, - cprops, None) - - def test_extend_volume(self): - cprops = {'name': 'space', 'noremovehost': 'stor1'} - self.assertRaises(NotImplementedError, - self.connector.extend_volume, - cprops) - - -class RBDConnectorTestCase(ConnectorTestCase): - - def setUp(self): - super(RBDConnectorTestCase, self).setUp() - - self.user = 'fake_user' - self.pool = 'fake_pool' - self.volume = 'fake_volume' - - self.connection_properties = { - 'auth_username': self.user, - 'name': '%s/%s' % (self.pool, self.volume), - } - - def test_get_search_path(self): - rbd = connector.RBDConnector(None) - path = rbd.get_search_path() - self.assertIsNone(path) - - @mock.patch('os_brick.initiator.linuxrbd.rbd') - @mock.patch('os_brick.initiator.linuxrbd.rados') - def test_get_volume_paths(self, mock_rados, mock_rbd): - rbd = connector.RBDConnector(None) - expected = [] - actual = rbd.get_volume_paths(self.connection_properties) - self.assertEqual(expected, actual) - - def test_get_connector_properties(self): - props = connector.RBDConnector.get_connector_properties( - 'sudo', multipath=True, enforce_multipath=True) - - expected_props = {'do_local_attach': False} - self.assertEqual(expected_props, props) - - @mock.patch('os_brick.initiator.linuxrbd.rbd') - @mock.patch('os_brick.initiator.linuxrbd.rados') - def test_connect_volume(self, mock_rados, mock_rbd): - """Test the connect volume case.""" - rbd = connector.RBDConnector(None) - device_info = rbd.connect_volume(self.connection_properties) - - # Ensure rados is instantiated correctly - mock_rados.Rados.assert_called_once_with( - clustername='ceph', - rados_id=encodeutils.safe_encode(self.user), - conffile='/etc/ceph/ceph.conf') - - # Ensure correct calls to connect to cluster - self.assertEqual(1, mock_rados.Rados.return_value.connect.call_count) - mock_rados.Rados.return_value.open_ioctx.assert_called_once_with( - encodeutils.safe_encode(self.pool)) - - # Ensure rbd image is instantiated correctly - mock_rbd.Image.assert_called_once_with( - mock_rados.Rados.return_value.open_ioctx.return_value, - encodeutils.safe_encode(self.volume), read_only=False, - snapshot=None) - - # Ensure expected object is returned correctly - self.assertTrue(isinstance(device_info['path'], - linuxrbd.RBDVolumeIOWrapper)) - - @mock.patch.object(priv_rootwrap, 'execute') - def test_connect_local_volume(self, mock_execute): - rbd = connector.RBDConnector(None, do_local_attach=True) - conn = {'name': 'pool/image'} - device_info = rbd.connect_volume(conn) - execute_call1 = mock.call('which', 'rbd') - cmd = ['rbd', 'map', 'image', '--pool', 'pool'] - execute_call2 = mock.call(*cmd, root_helper=None, run_as_root=True) - mock_execute.assert_has_calls([execute_call1, execute_call2]) - expected_info = {'path': '/dev/rbd/pool/image', - 'type': 'block'} - self.assertEqual(expected_info, device_info) - - @mock.patch('os_brick.initiator.linuxrbd.rbd') - @mock.patch('os_brick.initiator.linuxrbd.rados') - @mock.patch.object(linuxrbd.RBDVolumeIOWrapper, 'close') - def test_disconnect_volume(self, volume_close, mock_rados, mock_rbd): - """Test the disconnect volume case.""" - rbd = connector.RBDConnector(None) - device_info = rbd.connect_volume(self.connection_properties) - rbd.disconnect_volume(self.connection_properties, device_info) - - self.assertEqual(1, volume_close.call_count) - - @mock.patch.object(priv_rootwrap, 'execute') - def test_disconnect_local_volume(self, mock_execute): - rbd = connector.RBDConnector(None, do_local_attach=True) - conn = {'name': 'pool/image'} - rbd.disconnect_volume(conn, None) - - dev_name = '/dev/rbd/pool/image' - cmd = ['rbd', 'unmap', dev_name] - mock_execute.assert_called_once_with(*cmd, root_helper=None, - run_as_root=True) - - def test_extend_volume(self): - rbd = connector.RBDConnector(None) - self.assertRaises(NotImplementedError, - rbd.extend_volume, - self.connection_properties) - - -class DRBDConnectorTestCase(ConnectorTestCase): - - RESOURCE_TEMPLATE = ''' - resource r0 { - on host1 { - } - net { - shared-secret "%(shared-secret)s"; - } - } -''' - - def setUp(self): - super(DRBDConnectorTestCase, self).setUp() - - self.connector = connector.DRBDConnector( - None, execute=self._fake_exec) - - self.execs = [] - - def _fake_exec(self, *cmd, **kwargs): - self.execs.append(cmd) - - # out, err - return ('', '') - - def test_get_connector_properties(self): - props = connector.DRBDConnector.get_connector_properties( - 'sudo', multipath=True, enforce_multipath=True) - - expected_props = {} - self.assertEqual(expected_props, props) - - def test_connect_volume(self): - """Test connect_volume.""" - - cprop = { - 'provider_auth': 'my-secret', - 'config': self.RESOURCE_TEMPLATE, - 'name': 'my-precious', - 'device': '/dev/drbd951722', - 'data': {}, - } - - res = self.connector.connect_volume(cprop) - - self.assertEqual(cprop['device'], res['path']) - self.assertEqual('adjust', self.execs[0][1]) - self.assertEqual(cprop['name'], self.execs[0][4]) - - def test_disconnect_volume(self): - """Test the disconnect volume case.""" - - cprop = { - 'provider_auth': 'my-secret', - 'config': self.RESOURCE_TEMPLATE, - 'name': 'my-precious', - 'device': '/dev/drbd951722', - 'data': {}, - } - dev_info = {} - - self.connector.disconnect_volume(cprop, dev_info) - - self.assertEqual('down', self.execs[0][1]) - - def test_extend_volume(self): - cprop = {'name': 'something'} - self.assertRaises(NotImplementedError, - self.connector.extend_volume, - cprop) - - -class ScaleIOConnectorTestCase(ConnectorTestCase): - """Test cases for ScaleIO connector""" - # Fake volume information - vol = { - 'id': 'vol1', - 'name': 'test_volume', - 'provider_id': 'vol1' - } - - # Fake SDC GUID - fake_guid = 'FAKE_GUID' - - def setUp(self): - super(ScaleIOConnectorTestCase, self).setUp() - - self.fake_connection_properties = { - 'hostIP': MY_IP, - 'serverIP': MY_IP, - 'scaleIO_volname': self.vol['name'], - 'scaleIO_volume_id': self.vol['provider_id'], - 'serverPort': 443, - 'serverUsername': 'test', - 'serverPassword': 'fake', - 'serverToken': 'fake_token', - 'iopsLimit': None, - 'bandwidthLimit': None - } - - # Formatting string for REST API calls - self.action_format = "instances/Volume::{}/action/{{}}".format( - self.vol['id']) - self.get_volume_api = 'types/Volume/instances/getByName::{}'.format( - self.vol['name']) - - # Map of REST API calls to responses - self.mock_calls = { - self.get_volume_api: - self.MockHTTPSResponse(json.dumps(self.vol['id'])), - self.action_format.format('addMappedSdc'): - self.MockHTTPSResponse(''), - self.action_format.format('setMappedSdcLimits'): - self.MockHTTPSResponse(''), - self.action_format.format('removeMappedSdc'): - self.MockHTTPSResponse(''), - } - - # Default error REST response - self.error_404 = self.MockHTTPSResponse(content=dict( - errorCode=0, - message='HTTP 404', - ), status_code=404) - - # Patch the request and os calls to fake versions - mock.patch.object( - requests, 'get', self.handle_scaleio_request).start() - mock.patch.object( - requests, 'post', self.handle_scaleio_request).start() - mock.patch.object(os.path, 'isdir', return_value=True).start() - mock.patch.object( - os, 'listdir', return_value=["emc-vol-{}".format(self.vol['id'])] - ).start() - self.addCleanup(mock.patch.stopall) - - # The actual ScaleIO connector - self.connector = connector.ScaleIOConnector( - 'sudo', execute=self.fake_execute) - - class MockHTTPSResponse(requests.Response): - """Mock HTTP Response - - Defines the https replies from the mocked calls to do_request() - """ - def __init__(self, content, status_code=200): - super(ScaleIOConnectorTestCase.MockHTTPSResponse, - self).__init__() - - self._content = content - self.encoding = 'UTF-8' - self.status_code = status_code - - def json(self, **kwargs): - if isinstance(self._content, six.string_types): - return super(ScaleIOConnectorTestCase.MockHTTPSResponse, - self).json(**kwargs) - - return self._content - - @property - def text(self): - if not isinstance(self._content, six.string_types): - return json.dumps(self._content) - - self._content = self._content.encode('utf-8') - return super(ScaleIOConnectorTestCase.MockHTTPSResponse, - self).text - - def fake_execute(self, *cmd, **kwargs): - """Fakes the rootwrap call""" - return self.fake_guid, None - - def fake_missing_execute(self, *cmd, **kwargs): - """Error when trying to call rootwrap drv_cfg""" - raise putils.ProcessExecutionError("Test missing drv_cfg.") - - def handle_scaleio_request(self, url, *args, **kwargs): - """Fake REST server""" - api_call = url.split(':', 2)[2].split('/', 1)[1].replace('api/', '') - - if 'setMappedSdcLimits' in api_call: - self.assertNotIn("iops_limit", kwargs['data']) - if "iopsLimit" not in kwargs['data']: - self.assertIn("bandwidthLimitInKbps", - kwargs['data']) - elif "bandwidthLimitInKbps" not in kwargs['data']: - self.assertIn("iopsLimit", kwargs['data']) - else: - self.assertIn("bandwidthLimitInKbps", - kwargs['data']) - self.assertIn("iopsLimit", kwargs['data']) - - try: - return self.mock_calls[api_call] - except KeyError: - return self.error_404 - - def test_get_search_path(self): - expected = "/dev/disk/by-id" - actual = self.connector.get_search_path() - self.assertEqual(expected, actual) - - @mock.patch.object(os.path, 'exists', return_value=True) - @mock.patch.object(connector.ScaleIOConnector, '_wait_for_volume_path') - def test_get_volume_paths(self, mock_wait_for_path, mock_exists): - mock_wait_for_path.return_value = "emc-vol-vol1" - expected = ['/dev/disk/by-id/emc-vol-vol1'] - actual = self.connector.get_volume_paths( - self.fake_connection_properties) - self.assertEqual(expected, actual) - - def test_get_connector_properties(self): - props = connector.ScaleIOConnector.get_connector_properties( - 'sudo', multipath=True, enforce_multipath=True) - - expected_props = {} - self.assertEqual(expected_props, props) - - def test_connect_volume(self): - """Successful connect to volume""" - self.connector.connect_volume(self.fake_connection_properties) - - def test_connect_with_bandwidth_limit(self): - """Successful connect to volume with bandwidth limit""" - self.fake_connection_properties['bandwidthLimit'] = '500' - self.test_connect_volume() - - def test_connect_with_iops_limit(self): - """Successful connect to volume with iops limit""" - self.fake_connection_properties['iopsLimit'] = '80' - self.test_connect_volume() - - def test_connect_with_iops_and_bandwidth_limits(self): - """Successful connect with iops and bandwidth limits""" - self.fake_connection_properties['bandwidthLimit'] = '500' - self.fake_connection_properties['iopsLimit'] = '80' - self.test_connect_volume() - - def test_disconnect_volume(self): - """Successful disconnect from volume""" - self.connector.disconnect_volume(self.fake_connection_properties, None) - - def test_error_id(self): - """Fail to connect with bad volume name""" - self.fake_connection_properties['scaleIO_volume_id'] = 'bad_id' - self.mock_calls[self.get_volume_api] = self.MockHTTPSResponse( - dict(errorCode='404', message='Test volume not found'), 404) - - self.assertRaises(exception.BrickException, self.test_connect_volume) - - def test_error_no_volume_id(self): - """Faile to connect with no volume id""" - self.fake_connection_properties['scaleIO_volume_id'] = None - self.mock_calls[self.get_volume_api] = self.MockHTTPSResponse( - 'null', 200) - - self.assertRaises(exception.BrickException, self.test_connect_volume) - - def test_error_bad_login(self): - """Fail to connect with bad authentication""" - self.mock_calls[self.get_volume_api] = self.MockHTTPSResponse( - 'null', 401) - self.mock_calls[self.action_format.format( - 'addMappedSdc')] = self.MockHTTPSResponse( - dict(errorCode=401, message='bad login'), 401) - self.assertRaises(exception.BrickException, self.test_connect_volume) - - def test_error_bad_drv_cfg(self): - """Fail to connect with missing rootwrap executable""" - self.connector.set_execute(self.fake_missing_execute) - self.assertRaises(exception.BrickException, self.test_connect_volume) - - def test_error_map_volume(self): - """Fail to connect with REST API failure""" - self.mock_calls[self.action_format.format( - 'addMappedSdc')] = self.MockHTTPSResponse( - dict(errorCode=self.connector.VOLUME_NOT_MAPPED_ERROR, - message='Test error map volume'), 500) - - self.assertRaises(exception.BrickException, self.test_connect_volume) - - @mock.patch('time.sleep') - def test_error_path_not_found(self, sleep_mock): - """Timeout waiting for volume to map to local file system""" - mock.patch.object( - os, 'listdir', return_value=["emc-vol-no-volume"] - ).start() - self.assertRaises(exception.BrickException, self.test_connect_volume) - self.assertTrue(sleep_mock.called) - - def test_map_volume_already_mapped(self): - """Ignore REST API failure for volume already mapped""" - self.mock_calls[self.action_format.format( - 'addMappedSdc')] = self.MockHTTPSResponse( - dict(errorCode=self.connector.VOLUME_ALREADY_MAPPED_ERROR, - message='Test error map volume'), 500) - - self.test_connect_volume() - - def test_error_disconnect_volume(self): - """Fail to disconnect with REST API failure""" - self.mock_calls[self.action_format.format( - 'removeMappedSdc')] = self.MockHTTPSResponse( - dict(errorCode=self.connector.VOLUME_ALREADY_MAPPED_ERROR, - message='Test error map volume'), 500) - - self.assertRaises(exception.BrickException, - self.test_disconnect_volume) - - def test_disconnect_volume_not_mapped(self): - """Ignore REST API failure for volume not mapped""" - self.mock_calls[self.action_format.format( - 'removeMappedSdc')] = self.MockHTTPSResponse( - dict(errorCode=self.connector.VOLUME_NOT_MAPPED_ERROR, - message='Test error map volume'), 500) - - self.test_disconnect_volume() - - def test_extend_volume(self): - self.assertRaises(NotImplementedError, - self.connector.extend_volume, - self.fake_connection_properties) - - -class DISCOConnectorTestCase(ConnectorTestCase): - """Test cases for DISCO connector.""" - - # Fake volume information - volume = { - 'name': 'a-disco-volume', - 'disco_id': '1234567' - } - - # Conf for test - conf = { - 'ip': MY_IP, - 'port': 9898 - } - - def setUp(self): - super(DISCOConnectorTestCase, self).setUp() - - self.fake_connection_properties = { - 'name': self.volume['name'], - 'disco_id': self.volume['disco_id'], - 'conf': { - 'server_ip': self.conf['ip'], - 'server_port': self.conf['port']} - } - - self.fake_volume_status = {'attached': True, - 'detached': False} - self.fake_request_status = {'success': None, - 'fail': 'ERROR'} - self.volume_status = 'detached' - self.request_status = 'success' - - # Patch the request and os calls to fake versions - mock.patch.object(connector.DISCOConnector, - '_send_disco_vol_cmd', - self.perform_disco_request).start() - mock.patch.object(os.path, - 'exists', self.is_volume_attached).start() - mock.patch.object(glob, - 'glob', self.list_disco_volume).start() - self.addCleanup(mock.patch.stopall) - - # The actual DISCO connector - self.connector = connector.DISCOConnector( - 'sudo', execute=self.fake_execute) - - def perform_disco_request(self, *cmd, **kwargs): - """Fake the socket call.""" - return self.fake_request_status[self.request_status] - - def is_volume_attached(self, *cmd, **kwargs): - """Fake volume detection check.""" - return self.fake_volume_status[self.volume_status] - - def list_disco_volume(self, *cmd, **kwargs): - """Fake the glob call.""" - path_dir = self.connector.get_search_path() - volume_id = self.volume['disco_id'] - volume_items = [path_dir, '/', self.connector.DISCO_PREFIX, volume_id] - volume_path = ''.join(volume_items) - return [volume_path] - - def test_get_connector_properties(self): - props = connector.DISCOConnector.get_connector_properties( - 'sudo', multipath=True, enforce_multipath=True) - - expected_props = {} - self.assertEqual(expected_props, props) - - def test_get_search_path(self): - """DISCO volumes should be under /dev.""" - expected = "/dev" - actual = self.connector.get_search_path() - self.assertEqual(expected, actual) - - def test_get_volume_paths(self): - """Test to get all the path for a specific volume.""" - expected = ['/dev/dms1234567'] - self.volume_status = 'attached' - actual = self.connector.get_volume_paths( - self.fake_connection_properties) - self.assertEqual(expected, actual) - - def test_connect_volume(self): - """Attach a volume.""" - self.connector.connect_volume(self.fake_connection_properties) - - def test_connect_volume_already_attached(self): - """Make sure that we don't issue the request.""" - self.request_status = 'fail' - self.volume_status = 'attached' - self.test_connect_volume() - - def test_connect_volume_request_fail(self): - """Fail the attach request.""" - self.volume_status = 'detached' - self.request_status = 'fail' - self.assertRaises(exception.BrickException, - self.test_connect_volume) - - def test_disconnect_volume(self): - """Detach a volume.""" - self.connector.disconnect_volume(self.fake_connection_properties, None) - - def test_disconnect_volume_attached(self): - """Detach a volume attached.""" - self.request_status = 'success' - self.volume_status = 'attached' - self.test_disconnect_volume() - - def test_disconnect_volume_already_detached(self): - """Ensure that we don't issue the request.""" - self.request_status = 'fail' - self.volume_status = 'detached' - self.test_disconnect_volume() - - def test_disconnect_volume_request_fail(self): - """Fail the detach request.""" - self.volume_status = 'attached' - self.request_status = 'fail' - self.assertRaises(exception.BrickException, - self.test_disconnect_volume) - - def test_get_all_available_volumes(self): - """Test to get all the available DISCO volumes.""" - expected = ['/dev/dms1234567'] - actual = self.connector.get_all_available_volumes(None) - self.assertItemsEqual(expected, actual) - - def test_extend_volume(self): - self.assertRaises(NotImplementedError, - self.connector.extend_volume, - self.fake_connection_properties) - - -class SheepdogConnectorTestCase(ConnectorTestCase): - - def setUp(self): - super(SheepdogConnectorTestCase, self).setUp() - - self.hosts = ['fake_hosts'] - self.ports = ['fake_ports'] - self.volume = 'fake_volume' - - self.connection_properties = { - 'hosts': self.hosts, - 'name': self.volume, - 'ports': self.ports, - } - - def test_get_connector_properties(self): - props = connector.SheepdogConnector.get_connector_properties( - 'sudo', multipath=True, enforce_multipath=True) - - expected_props = {} - self.assertEqual(expected_props, props) - - def test_get_search_path(self): - sheepdog = connector.SheepdogConnector(None) - path = sheepdog.get_search_path() - self.assertIsNone(path) - - def test_get_volume_paths(self): - sheepdog = connector.SheepdogConnector(None) - expected = [] - actual = sheepdog.get_volume_paths(self.connection_properties) - self.assertEqual(expected, actual) - - def test_connect_volume(self): - """Test the connect volume case.""" - sheepdog = connector.SheepdogConnector(None) - device_info = sheepdog.connect_volume(self.connection_properties) - - # Ensure expected object is returned correctly - self.assertTrue(isinstance(device_info['path'], - linuxsheepdog.SheepdogVolumeIOWrapper)) - - @mock.patch.object(linuxsheepdog.SheepdogVolumeIOWrapper, 'close') - def test_disconnect_volume(self, volume_close): - """Test the disconnect volume case.""" - sheepdog = connector.SheepdogConnector(None) - device_info = sheepdog.connect_volume(self.connection_properties) - sheepdog.disconnect_volume(self.connection_properties, device_info) - - self.assertEqual(1, volume_close.call_count) - - def test_disconnect_volume_with_invalid_handle(self): - """Test the disconnect volume case with invalid handle.""" - sheepdog = connector.SheepdogConnector(None) - device_info = {'path': 'fake_handle'} - self.assertRaises(exception.InvalidIOHandleObject, - sheepdog.disconnect_volume, - self.connection_properties, - device_info) - - def test_extend_volume(self): - sheepdog = connector.SheepdogConnector(None) - self.assertRaises(NotImplementedError, - sheepdog.extend_volume, - self.connection_properties) diff --git a/os_brick/tests/windows/test_factory.py b/os_brick/tests/windows/test_factory.py index 15cb73623..535166092 100644 --- a/os_brick/tests/windows/test_factory.py +++ b/os_brick/tests/windows/test_factory.py @@ -16,6 +16,7 @@ import ddt import mock +from os_brick import initiator from os_brick.initiator import connector from os_brick.initiator.windows import iscsi from os_brick.tests.windows import test_base @@ -23,7 +24,7 @@ from os_brick.tests.windows import test_base @ddt.ddt class WindowsConnectorFactoryTestCase(test_base.WindowsConnectorTestBase): - @ddt.data({'proto': connector.ISCSI, + @ddt.data({'proto': initiator.ISCSI, 'expected_cls': iscsi.WindowsISCSIConnector}) @ddt.unpack @mock.patch('sys.platform', 'win32')