SCSI: Cleanup leftover devices and validate result
For SCSI based protocols (iSCSI and FCP) the ``connect_volume`` method now cleans up leftover devices as best it can at the begining, and it also validates the result of the operation at the end before returning it to the caller. OS-brick can now validate the WWN of iSCSI and FCP volumes for Cinder drivers that provide the value in the connection properties ``wwn`` key. All Cinder drivers are encouraged to provide this value in their connection properties to increase robustness and security. This ``wwn`` value also helps with the cleanup of leftover devices at the beginning of the ``connect_volume`` which can help detect conflicts before any scans have been made. Deployments using this os-brick release and iSCSI and FCP storage with multipathing are encouraged to update their ``multipath.conf`` file to add the ``recheck_wwid yes`` option to the ``defaults`` section. This can help with some scenarios where there can be a window where a volume is incorrectly being used as a path for one, or multiple, multipath devices which can lead to data leak or corruption. The window happens when there are leftover devices and it's between Cinder mapping the volume on the storage array and os-brick cleaning the leftover devices. The cleanup will remove all devices that match this connection information, all devices that match the WWN, and the multipath that also matches the WWN. It checks whether these devices are in use or not. The validation has to double check some of those things before returning the result to the caller because os-brick is not as effective when Cinder hasn't provided the WWN, which is the case for most Cinder drivers at this time. Closes-Bug: #2004555 Change-Id: Icd4cde6b3fbde4d529b392b968ef0d9594dfcf14
This commit is contained in:
parent
f724ab722a
commit
2de794d302
|
@ -237,3 +237,37 @@ class ExceptionChainer(BrickException):
|
|||
|
||||
class ExecutionTimeout(putils.ProcessExecutionError):
|
||||
pass
|
||||
|
||||
|
||||
class DeviceInUse(BrickException):
|
||||
message = _('Potential data leak/corruption detected!! Device %(device)s '
|
||||
'used by this volume was already in use. Aborting.')
|
||||
|
||||
|
||||
class ActivePaths(BrickException):
|
||||
message = _('Devices %(devices)s from other connections are in our mpath '
|
||||
'%(mpath)s and are being used.')
|
||||
|
||||
|
||||
class UnknownMultipath(BrickException):
|
||||
message = _("Multipath %(mpath)s doesn't exist")
|
||||
|
||||
|
||||
class AttributeMismatch(BrickException):
|
||||
message = _("Detected block attribute inconsistency. %(attr)s is "
|
||||
"%(val1)s on %(dev1)s but %(val2)s on %(dev2)s.")
|
||||
|
||||
|
||||
class UnknownWWN(BrickException):
|
||||
message = _("Cannot determine volume's WWN for %(devices)s, aborting.")
|
||||
|
||||
|
||||
class WWNValidationFail(BrickException):
|
||||
message = _("Validating WWN failed, probably due to leftover devices. "
|
||||
"Device's cinder reported WWN is %(cinder_wwn)s but in "
|
||||
"%(devices)s we have %(devices_wwns)s")
|
||||
|
||||
|
||||
class WWNMismatch(BrickException):
|
||||
message = _("Mismatch in WWN between this volume's SCSI WWN "
|
||||
"[%(wwn)s] and the one in the multipath [%(mpath_wwn)s]")
|
||||
|
|
|
@ -201,6 +201,29 @@ class FibreChannelConnector(base.BaseLinuxConnector):
|
|||
{'props': connection_properties})
|
||||
raise exception.VolumePathsNotFound()
|
||||
|
||||
@staticmethod
|
||||
def _existing_devices(possible_paths):
|
||||
devices = []
|
||||
for path in possible_paths:
|
||||
real_path = os.path.realpath(path)
|
||||
if os.path.exists(real_path):
|
||||
devices.append(os.path.basename(real_path))
|
||||
return devices
|
||||
|
||||
def cleanup_pre_existing(self, connection_properties, possible_paths):
|
||||
"""Cleans up pre-existing devices present in the system.
|
||||
|
||||
Since FC doesn't do automatic rescans on LUN map change notifications
|
||||
from the array, the only case we can have leftover devices is because
|
||||
the export/mapping has been removed in the storage array without
|
||||
removing the devices.
|
||||
"""
|
||||
# This is not the nicest way to locate existing devices, since it
|
||||
# realies on symlinks, but that's the current approach of the driver.
|
||||
pre_existing_devs = self._existing_devices(possible_paths)
|
||||
cinder_wwn = connection_properties.get('wwn')
|
||||
self._linuxscsi.cleanup_pre_existing(pre_existing_devs, cinder_wwn)
|
||||
|
||||
@utils.trace
|
||||
@utils.connect_volume_prepare_result
|
||||
@base.synchronized('connect_volume', external=True)
|
||||
|
@ -229,6 +252,8 @@ class FibreChannelConnector(base.BaseLinuxConnector):
|
|||
host_devices = self._get_possible_volume_paths(
|
||||
connection_properties, hbas)
|
||||
|
||||
self.cleanup_pre_existing(connection_properties, host_devices)
|
||||
|
||||
# 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.
|
||||
|
@ -274,6 +299,7 @@ class FibreChannelConnector(base.BaseLinuxConnector):
|
|||
|
||||
# see if the new drive is part of a multipath
|
||||
# device. If so, we'll use the multipath device.
|
||||
mpath = None
|
||||
if self.use_multipath:
|
||||
# Pass a symlink, not a real path, otherwise we'll get a real path
|
||||
# back if we don't find a multipath and we'll return that to the
|
||||
|
@ -284,6 +310,7 @@ class FibreChannelConnector(base.BaseLinuxConnector):
|
|||
if multipath_id:
|
||||
# only set the multipath_id if we found one
|
||||
device_info['multipath_id'] = multipath_id
|
||||
mpath = os.path.basename(os.path.realpath(device_path))
|
||||
|
||||
else:
|
||||
device_path = self.host_device
|
||||
|
@ -291,6 +318,10 @@ class FibreChannelConnector(base.BaseLinuxConnector):
|
|||
device_path = typing.cast(str, device_path)
|
||||
|
||||
device_info['path'] = device_path
|
||||
|
||||
real_wwn = connection_properties.get('wwn', device_wwn)
|
||||
self._linuxscsi.validate_devices(self._existing_devices(host_devices),
|
||||
mpath, real_wwn)
|
||||
return device_info
|
||||
|
||||
def _get_host_devices(self, possible_devs: list) -> list:
|
||||
|
@ -385,11 +416,11 @@ class FibreChannelConnector(base.BaseLinuxConnector):
|
|||
self._remove_devices(connection_properties, devices, device_info,
|
||||
force, exc)
|
||||
|
||||
if exc: # type: ignore
|
||||
if bool(exc):
|
||||
LOG.warning('There were errors removing %s, leftovers may remain '
|
||||
'in the system', volume_paths)
|
||||
if not ignore_errors:
|
||||
raise exc # type: ignore
|
||||
raise exc
|
||||
|
||||
def _remove_devices(self,
|
||||
connection_properties: dict,
|
||||
|
|
|
@ -519,6 +519,7 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
|
|||
target_lun(s) - LUN id of the volume
|
||||
Note that plural keys may be used when use_multipath=True
|
||||
"""
|
||||
self.cleanup_pre_existing(connection_properties)
|
||||
try:
|
||||
if self.use_multipath:
|
||||
return self._connect_multipath_volume(connection_properties)
|
||||
|
@ -532,6 +533,26 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
|
|||
|
||||
return None
|
||||
|
||||
def cleanup_pre_existing(self, connection_properties):
|
||||
"""Cleans up pre-existing devices present in the system.
|
||||
|
||||
The only valid case where there could be devices present in the system
|
||||
is when using an old iSCSI initiator that doesn't support manual scans.
|
||||
|
||||
In any other case it is very likely that the devices present are
|
||||
leftover devices.
|
||||
|
||||
We cannot always determine which one it is, so we'll remove any
|
||||
pre-existing devices and multipaths if they are not being used.
|
||||
"""
|
||||
devices_map = self._get_connection_devices(connection_properties,
|
||||
is_disconnect_call=True)
|
||||
pre_existing_devs = []
|
||||
for belong, others in devices_map.values():
|
||||
pre_existing_devs.extend(belong)
|
||||
cinder_wwn = connection_properties.get('wwn')
|
||||
self._linuxscsi.cleanup_pre_existing(pre_existing_devs, cinder_wwn)
|
||||
|
||||
def _get_connect_result(self,
|
||||
con_props: dict,
|
||||
wwn: str,
|
||||
|
@ -551,7 +572,8 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
|
|||
data: dict[str, Any] = {'stop_connecting': False,
|
||||
'num_logins': 0, 'failed_logins': 0,
|
||||
'stopped_threads': 0, 'found_devices': [],
|
||||
'just_added_devices': []}
|
||||
'just_added_devices': [],
|
||||
'not_found_session_hctl': []}
|
||||
|
||||
for props in self._iterate_all_targets(connection_properties):
|
||||
self._connect_vol(self.device_scan_attempts, props, data)
|
||||
|
@ -565,8 +587,14 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
|
|||
else:
|
||||
LOG.debug('Could not find the WWN for %s.',
|
||||
found_devs[0])
|
||||
return self._get_connect_result(connection_properties,
|
||||
wwn, found_devs)
|
||||
|
||||
# Some vendors provide the real WWN for the volume
|
||||
cinder_wwn = connection_properties.get('wwn')
|
||||
wwn = self._linuxscsi.validate_devices(found_devs,
|
||||
cinder_wwn=cinder_wwn)
|
||||
|
||||
return self._get_connect_result(connection_properties, wwn,
|
||||
found_devs)
|
||||
|
||||
# If we failed we must cleanup the connection, as we could be
|
||||
# leaving the node entry if it's not being used by another device.
|
||||
|
@ -662,6 +690,9 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
|
|||
LOG.debug('Connected to %s using %s', device,
|
||||
strutils.mask_password(props))
|
||||
else:
|
||||
# Store shctl to check if devices appear between here and the
|
||||
# leak detection code.
|
||||
data['not_found_session_hctl'].append((session, hctl))
|
||||
LOG.warning('LUN %(lun)s on iSCSI portal %(portal)s not found '
|
||||
'on sysfs after logging in.',
|
||||
{'lun': props['target_lun'], 'portal': portal})
|
||||
|
@ -699,10 +730,12 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
|
|||
last_try_on = 0.0
|
||||
found: list = []
|
||||
just_added_devices: list = []
|
||||
not_found_shctl: list = []
|
||||
# Dict used to communicate with threads as detailed in _connect_vol
|
||||
data = {'stop_connecting': False, 'num_logins': 0, 'failed_logins': 0,
|
||||
'stopped_threads': 0, 'found_devices': found,
|
||||
'just_added_devices': just_added_devices}
|
||||
'just_added_devices': just_added_devices,
|
||||
'not_found_session_hctl': not_found_shctl}
|
||||
|
||||
ips_iqns_luns = self._get_ips_iqns_luns(connection_properties)
|
||||
# Launch individual threads for each session with the own properties
|
||||
|
@ -777,13 +810,47 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
|
|||
if not mpath:
|
||||
LOG.warning('No dm was created, connection to volume is probably '
|
||||
'bad and will perform poorly.')
|
||||
elif not wwn:
|
||||
wwn = self._linuxscsi.get_sysfs_wwn(found, mpath)
|
||||
|
||||
assert wwn is not None
|
||||
# Some vendors provide the real WWN for the volume
|
||||
cinder_wwn = connection_properties.get('wwn')
|
||||
self._update_devices_found(found, not_found_shctl)
|
||||
wwn = self._linuxscsi.validate_devices(found, mpath, cinder_wwn)
|
||||
return self._get_connect_result(connection_properties, wwn, found,
|
||||
mpath)
|
||||
|
||||
def _update_devices_found(
|
||||
self,
|
||||
devices: list[str],
|
||||
not_found_shctl: list[Any]) -> None:
|
||||
"""Update iSCSI devices found.
|
||||
|
||||
If we haven't already found all possible devices there could be a race
|
||||
that added new devices.
|
||||
|
||||
Uses the list of session, host, channel, target, lun for the devices
|
||||
that have been connected to but have not yet appeared on the host.
|
||||
|
||||
not_found_shctl is an iterable with a string and a 4-tuple of strings
|
||||
"""
|
||||
if not_found_shctl:
|
||||
LOG.debug('Updating devices found for missing %s', not_found_shctl)
|
||||
still_not_found = []
|
||||
found_now = set()
|
||||
for session, hctl in not_found_shctl:
|
||||
dev = self._linuxscsi.device_name_by_hctl(session, hctl)
|
||||
if dev:
|
||||
found_now.add(dev)
|
||||
else:
|
||||
still_not_found.append((session, hctl))
|
||||
|
||||
if found_now:
|
||||
LOG.debug('Additional volumes found %s. Still missing %s',
|
||||
found_now, still_not_found)
|
||||
devices.extend(found_now)
|
||||
if still_not_found:
|
||||
not_found_shctl.clear()
|
||||
not_found_shctl.extend(still_not_found)
|
||||
|
||||
def _get_connection_devices(
|
||||
self,
|
||||
connection_properties: dict,
|
||||
|
|
|
@ -131,6 +131,13 @@ class InitiatorConnector(executor.Executor, metaclass=abc.ABCMeta):
|
|||
os_brick.utils.connect_volume_prepare_result to ensure that the right
|
||||
device path is returned to the caller.
|
||||
|
||||
WARNING: For security reasons the method may not be idempotent, in
|
||||
fact, for some transport protocols (eg: iSCSI, FCP) each time it is
|
||||
called it will disconnect any existing volume that matches the provided
|
||||
connection_information first and then reconnect it again to ensure that
|
||||
the right volume will be used and that the metadata present in sysfs
|
||||
(such as the WWID and size) are up to date.
|
||||
|
||||
:param connection_properties: The dictionary that describes all
|
||||
of the target volume attributes.
|
||||
:type connection_properties: dict
|
||||
|
|
|
@ -32,6 +32,7 @@ from oslo_utils import excutils
|
|||
from os_brick import exception
|
||||
from os_brick import executor
|
||||
from os_brick.privileged import rootwrap as priv_rootwrap
|
||||
from os_brick.privileged import utils as priv_utils
|
||||
from os_brick import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -135,21 +136,40 @@ class LinuxSCSI(executor.Executor):
|
|||
LOG.debug('dev_info=%s', str(dev_info))
|
||||
return dev_info
|
||||
|
||||
def get_sysfs_wwn(self, device_names: list[str], mpath=None) -> str:
|
||||
@staticmethod
|
||||
def get_mpath_sysfs_wwn(mpath: str) -> Optional[str]:
|
||||
"""Return the wwid from sysfs for a multipath in udev format."""
|
||||
# We have the WWN in /uuid even with friendly names, unline /name
|
||||
try:
|
||||
with open('/sys/block/%s/dm/uuid' % mpath) as f:
|
||||
# Contents are matph-WWN, so get the part we want
|
||||
wwid = f.read().strip()[6:]
|
||||
return wwid
|
||||
except Exception as exc:
|
||||
LOG.warning('Failed to read the DM uuid: %s', exc)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_dev_names_in_mpath(mpath: str) -> list[str]:
|
||||
"""Give a multipath device (dm-1) returns all device names under it."""
|
||||
# Get all devices in the mpath
|
||||
slaves_path = '/sys/class/block/%s/slaves' % mpath
|
||||
try:
|
||||
dm_devs = os.listdir(slaves_path)
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
return dm_devs
|
||||
|
||||
@classmethod
|
||||
def get_sysfs_wwn(cls, device_names: list[str], mpath=None) -> str:
|
||||
"""Return the wwid from sysfs in any of devices in udev format."""
|
||||
# If we have a multipath DM we know that it has found the WWN
|
||||
if mpath:
|
||||
# We have the WWN in /uuid even with friendly names, unline /name
|
||||
try:
|
||||
with open('/sys/block/%s/dm/uuid' % mpath) as f:
|
||||
# Contents are matph-WWN, so get the part we want
|
||||
wwid = f.read().strip()[6:]
|
||||
if wwid: # Check should not be needed, but just in case
|
||||
return wwid
|
||||
except Exception as exc:
|
||||
LOG.warning('Failed to read the DM uuid: %s', exc)
|
||||
wwid = cls.get_mpath_sysfs_wwn(mpath)
|
||||
if wwid: # Check should not be needed, but just in case
|
||||
return wwid
|
||||
|
||||
wwid = self.get_sysfs_wwid(device_names)
|
||||
wwid = cls.get_one_sysfs_wwid(device_names)
|
||||
glob_str = '/dev/disk/by-id/scsi-'
|
||||
wwn_paths = glob.glob(glob_str + '*')
|
||||
# If we don't have multiple designators on page 0x83
|
||||
|
@ -169,8 +189,7 @@ class LinuxSCSI(executor.Executor):
|
|||
# devices belonging to the multipath DM.
|
||||
if name.startswith('dm-'):
|
||||
# Get the devices that belong to the DM
|
||||
slaves_path = '/sys/class/block/%s/slaves' % name
|
||||
dm_devs = os.listdir(slaves_path)
|
||||
dm_devs = cls._get_dev_names_in_mpath(name)
|
||||
# This is the right wwn_path if the devices we have
|
||||
# attached belong to the dm we followed
|
||||
if device_names_set.intersection(dm_devs):
|
||||
|
@ -185,19 +204,26 @@ class LinuxSCSI(executor.Executor):
|
|||
return ''
|
||||
return wwn_path[len(glob_str):]
|
||||
|
||||
def get_sysfs_wwid(self, device_names):
|
||||
@classmethod
|
||||
def get_sysfs_wwid(cls, device_name):
|
||||
try:
|
||||
with open('/sys/block/%s/device/wwid' % device_name) as f:
|
||||
wwid = f.read().strip()
|
||||
except IOError:
|
||||
return ''
|
||||
# The sysfs wwid has the wwn type in string format as a prefix,
|
||||
# but udev uses its numerical representation as returned by
|
||||
# scsi_id's page 0x83, so we need to map it
|
||||
udev_wwid = cls.WWN_TYPES.get(wwid[:4], '8') + wwid[4:]
|
||||
return udev_wwid
|
||||
|
||||
@classmethod
|
||||
def get_one_sysfs_wwid(cls, device_names):
|
||||
"""Return the wwid from sysfs in any of devices in udev format."""
|
||||
for device_name in device_names:
|
||||
try:
|
||||
with open('/sys/block/%s/device/wwid' % device_name) as f:
|
||||
wwid = f.read().strip()
|
||||
except IOError:
|
||||
continue
|
||||
# The sysfs wwid has the wwn type in string format as a prefix,
|
||||
# but udev uses its numerical representation as returned by
|
||||
# scsi_id's page 0x83, so we need to map it
|
||||
udev_wwid = self.WWN_TYPES.get(wwid[:4], '8') + wwid[4:]
|
||||
return udev_wwid
|
||||
wwid = cls.get_sysfs_wwid(device_name)
|
||||
if wwid:
|
||||
return wwid
|
||||
return ''
|
||||
|
||||
def get_scsi_wwn(self, path: str) -> str:
|
||||
|
@ -777,3 +803,288 @@ class LinuxSCSI(executor.Executor):
|
|||
if map_name and self.get_dm_name(mpath):
|
||||
raise exception.BrickException("Multipath doesn't go away")
|
||||
LOG.debug('Multipath %s no longer present', mpath)
|
||||
|
||||
###########################################################################
|
||||
# Data leak/corruption check code
|
||||
###########################################################################
|
||||
def cleanup_pre_existing(self, devices, cinder_wwn):
|
||||
"""Cleans up pre-existing devices present in the system.
|
||||
|
||||
There are 2 kind of devices we care about here:
|
||||
|
||||
- Devices that match this connection criteria and are already present
|
||||
in the system. These must be provided by the caller in the devices
|
||||
parameter.
|
||||
|
||||
- Devices that match the WWN even if they don't match the current
|
||||
connection information.
|
||||
|
||||
A case where there are valid devices present in the system is when
|
||||
using an old iSCSI initiator that doesn't support manual scans.
|
||||
|
||||
In any other cases it is very likely that the devices present are
|
||||
leftover devices.
|
||||
|
||||
We cannot always determine which one it is, so we'll remove any
|
||||
pre-existing devices and multipaths if they are not being used.
|
||||
|
||||
The cinder_wwn is the WWN value provided by some Cinder drivers.
|
||||
"""
|
||||
def find_devices(wwn):
|
||||
return [os.path.basename(dev)
|
||||
for dev in glob.glob('/sys/block/sd*')
|
||||
if wwn == self.get_sysfs_wwid(os.path.basename(dev))]
|
||||
|
||||
def find_mpath(wwn):
|
||||
for path in glob.glob('/sys/block/dm-*'):
|
||||
mpath = os.path.basename(path)
|
||||
dm_wwn = self.get_mpath_sysfs_wwn(mpath)
|
||||
if wwn == dm_wwn:
|
||||
return mpath
|
||||
|
||||
LOG.debug('Search for pre-existing devices')
|
||||
if not devices and not cinder_wwn:
|
||||
return
|
||||
mpath = None
|
||||
|
||||
# Build list of devices to check usage: pre-existing + have our wwn
|
||||
if cinder_wwn:
|
||||
devices = [] if devices is None else devices.copy()
|
||||
devices.extend(find_devices(cinder_wwn))
|
||||
mpath = find_mpath(cinder_wwn)
|
||||
# Add mpath to the list of devices to confirm it's not in use
|
||||
if mpath:
|
||||
devices.append(mpath)
|
||||
|
||||
if not devices:
|
||||
return
|
||||
|
||||
LOG.warning('Pre-existing devices present for volume %s, checking '
|
||||
'they are not being used.', devices)
|
||||
devs_paths = ['/dev/' + dev for dev in devices]
|
||||
usage = priv_utils.processes_using_devices(devs_paths)
|
||||
for dev_path, dev in zip(devs_paths, devices):
|
||||
# Not count multipath devices from device users count
|
||||
dm_usage = len(glob.glob(f'/sys/block/{dev}/holders/dm-*'))
|
||||
if usage[dev_path] > dm_usage:
|
||||
raise exception.DeviceInUse(device=dev_path)
|
||||
|
||||
if mpath:
|
||||
del devices[-1]
|
||||
|
||||
if devices:
|
||||
LOG.debug('Removing pre-existing unused devices %s', devices)
|
||||
self._delete_devices(devices)
|
||||
|
||||
if mpath:
|
||||
LOG.debug('Cleaning existing multipath (%s)', mpath)
|
||||
self._remove_unwanted_multipath_paths(mpath)
|
||||
# Size may have changed, so we don't want an old mpath device
|
||||
self.multipath_del_map(mpath)
|
||||
|
||||
def _remove_unwanted_multipath_paths(self, mpath, ignore_list=None):
|
||||
"""Cleanup an existing multipath of unwanted devices.
|
||||
|
||||
Check devices in a multipath and remove all the ones we don't want,
|
||||
based on the ignore_list, that are not being used.
|
||||
|
||||
Fails if there are some devices in the multipath that are being used
|
||||
directly, since we have no way of removing them permanently from the
|
||||
device mapper, as multipathd could decide to bring it under the device
|
||||
mapper in the future.
|
||||
"""
|
||||
if not mpath:
|
||||
return
|
||||
if ignore_list is None:
|
||||
ignore_list = []
|
||||
|
||||
unwanted = self._get_dev_names_in_mpath(mpath)
|
||||
unwanted = ['/dev/' + u for u in unwanted if u not in ignore_list]
|
||||
if not unwanted:
|
||||
return
|
||||
LOG.warning('Some unknown devices (%s) found in our multipath '
|
||||
'%s', unwanted, mpath)
|
||||
|
||||
usage_map = priv_utils.processes_using_devices(unwanted)
|
||||
# Multipathd is already using the devices, so don't count it
|
||||
unused = [d for d, usage in usage_map.items() if usage < 2]
|
||||
|
||||
if unused:
|
||||
LOG.debug('Removing unused unknown devices %s', unused)
|
||||
self._delete_devices(unused)
|
||||
|
||||
if len(unwanted) > len(unused):
|
||||
used = [d for d, usage in usage_map.items() if usage >= 2]
|
||||
raise exception.ActivePaths(mpath=mpath, devices=used)
|
||||
|
||||
def _delete_devices(self, devices: list[str]) -> None:
|
||||
"""Forcefully remove devices from the system
|
||||
|
||||
If multipath is running it also ensures that multipathd stops
|
||||
monitoring those devices. This prevents race conditions caused by the
|
||||
burst of remove udev rule triggers and the additions that may happen
|
||||
afterwards.
|
||||
"""
|
||||
multipath_running = self.is_multipath_running(
|
||||
enforce_multipath=False, root_helper=self._root_helper)
|
||||
|
||||
for dev in devices:
|
||||
if '/' not in dev:
|
||||
dev = '/dev/' + dev
|
||||
if multipath_running:
|
||||
# Manually remove path to prevent multipathd race when adding
|
||||
# it back again if multipathd queues the deletion udev trigger
|
||||
self.multipath_del_path(dev)
|
||||
self.remove_scsi_device(dev, force=True, flush=False)
|
||||
|
||||
def validate_devices(self,
|
||||
devices_found: list[str],
|
||||
mpath: Optional[str] = None,
|
||||
cinder_wwn: Optional[str] = None) -> str:
|
||||
"""Check for correctness of devices and multipath from this connection
|
||||
|
||||
Even though calling cleanup_pre_existing cannot clean everything in
|
||||
all cases (for example when cinder doesn't provide the wwn), this
|
||||
method assumes that it has been called earlier.
|
||||
|
||||
devices_found cannot be empty
|
||||
Updates devices_found.
|
||||
"""
|
||||
assert devices_found
|
||||
|
||||
LOG.debug('Validating: mpath %s and devices %s', mpath, devices_found)
|
||||
if mpath:
|
||||
self._tidy_mpath(devices_found, mpath)
|
||||
|
||||
# Also check the mpath for discrepancies
|
||||
all_devices = devices_found + [mpath]
|
||||
else:
|
||||
all_devices = devices_found
|
||||
|
||||
self._validate_block_attributes(all_devices)
|
||||
wwn = self._validate_wwn(devices_found, mpath, cinder_wwn)
|
||||
return wwn
|
||||
|
||||
def _tidy_mpath(self, devices: list[str], mpath: str):
|
||||
"""Validate and tidy a multipath
|
||||
|
||||
Ensures that the multipath is not being used and that any unwanted
|
||||
paths/devices under the multipath are removed.
|
||||
|
||||
All parameters are names, not paths: sda, dm-7
|
||||
"""
|
||||
LOG.debug('Validating and tidying up %s, devices from this connection'
|
||||
' are %s', mpath, devices)
|
||||
|
||||
# Multipath device cannot be in use before we have returned.
|
||||
mpath_dev = '/dev/' + mpath
|
||||
if priv_utils.device_is_used(mpath_dev):
|
||||
exc = exception.DeviceInUse(device=mpath_dev)
|
||||
LOG.error(exc.msg)
|
||||
raise exc
|
||||
|
||||
# Ensure only this connection's paths are in the multipath
|
||||
self._remove_unwanted_multipath_paths(mpath, ignore_list=devices)
|
||||
|
||||
ATTRIBUTE_TEMPLATES = ('/sys/block/%s/size',
|
||||
'/sys/block/%s/alignment_offset',
|
||||
'/sys/block/%s/queue/physical_block_size',
|
||||
'/sys/block/%s/queue/logical_block_size')
|
||||
|
||||
@classmethod
|
||||
def _validate_block_attributes(cls, devices):
|
||||
"""Check consistency of attributes among devices of a volume
|
||||
|
||||
Can accept devices together with a multipath device as well
|
||||
(eg: ['sda', 'sdb', 'dm-7']).
|
||||
|
||||
Attributes compared are:
|
||||
- Size
|
||||
- Alignment offset
|
||||
- Physical block size
|
||||
- Logical block size
|
||||
"""
|
||||
if len(devices) == 1:
|
||||
LOG.debug('Only 1 device, skipping block attribute checks')
|
||||
return
|
||||
|
||||
LOG.debug('Validating consistency of block attributes')
|
||||
for attribute_template in cls.ATTRIBUTE_TEMPLATES:
|
||||
previous = None
|
||||
|
||||
for device in devices:
|
||||
fname = attribute_template % device
|
||||
try:
|
||||
value = open(fname, 'r').read().strip()
|
||||
except Exception:
|
||||
value = None
|
||||
|
||||
if value is not None:
|
||||
if previous is None:
|
||||
previous = (value, device)
|
||||
elif value != previous[0]:
|
||||
exc = exception.AttributeMismatch(attr=fname,
|
||||
value1=previous[0],
|
||||
dev1=previous[1],
|
||||
value2=value,
|
||||
dev2=device)
|
||||
LOG.error(exc.msg)
|
||||
raise exc
|
||||
|
||||
def _validate_wwn(self,
|
||||
devices: list[str],
|
||||
mpath: Optional[str],
|
||||
cinder_wwn: Optional[str]) -> str:
|
||||
"""Ensure WWN is correct for devices and mpath.
|
||||
|
||||
All devices must have the same WWN and they should match the real one,
|
||||
which is either provided by the Cinder driver or retrieved using the
|
||||
scsi_id command.
|
||||
|
||||
This should prevent having mismatches between the devices WWN and the
|
||||
DM's, as well as having devices in multiple DMs.
|
||||
|
||||
It does not however prevent a device from not being part of the DM, as
|
||||
the network path could be out.
|
||||
|
||||
Returns the WWN of the volume.
|
||||
"""
|
||||
LOG.debug('Checking WWIDs for volumes')
|
||||
|
||||
# We want to have a real wwn to compare with, so if we don't get one
|
||||
# from Cinder we'll have to query via SCSI, even if it's slow and
|
||||
# there's a small chance of hanging when network I/O errors are high
|
||||
if not cinder_wwn:
|
||||
LOG.debug("Cinder didn't provide the volume's WWN, reading it with"
|
||||
" a SCSI command, this will detect sysfs out of sync "
|
||||
"cases, but not the wrong volume being attached.")
|
||||
for dev in devices:
|
||||
try:
|
||||
cinder_wwn = self.get_scsi_wwn('/dev/' + dev)
|
||||
if cinder_wwn:
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
wwns = {self.get_sysfs_wwid(device) for device in devices}
|
||||
wwns.discard('')
|
||||
wwn = wwns and next(iter(wwns))
|
||||
exc: exception.BrickException
|
||||
if len(wwns) != 1 or cinder_wwn != wwn:
|
||||
exc = exception.WWNValidationFail(cinder_wwn=cinder_wwn,
|
||||
devices=devices,
|
||||
devices_wwns=wwns)
|
||||
LOG.error(exc.msg)
|
||||
raise exc
|
||||
|
||||
LOG.debug('No WWN error on devices (%s)', wwn)
|
||||
|
||||
if mpath:
|
||||
mpath_wwn = self.get_mpath_sysfs_wwn(mpath)
|
||||
# This should not be possible, but just in case
|
||||
if wwn != mpath_wwn:
|
||||
exc = exception.WWNMismatch(wwn=wwn, mpath_wwn=mpath_wwn)
|
||||
LOG.error(exc.msg)
|
||||
raise exc
|
||||
|
||||
return cinder_wwn # type: ignore
|
||||
|
|
|
@ -33,3 +33,12 @@ default = priv_context.PrivContext(
|
|||
capabilities=capabilities,
|
||||
logger_name=__name__,
|
||||
)
|
||||
|
||||
capabilities_ptrace = [c.CAP_SYS_PTRACE]
|
||||
proc_fds = priv_context.PrivContext(
|
||||
__name__,
|
||||
cfg_section='privsep_osbrick',
|
||||
pypath=__name__ + '.proc_fds',
|
||||
capabilities=capabilities_ptrace,
|
||||
logger_name=__name__,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
# Copyright (c) 2023, Red Hat, Inc.
|
||||
# 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 __future__ import annotations
|
||||
|
||||
import glob
|
||||
import os
|
||||
|
||||
import os_brick.privileged
|
||||
|
||||
|
||||
@os_brick.privileged.proc_fds.entrypoint
|
||||
def device_is_used(device_path: str) -> bool:
|
||||
"""Checks if a device path is being used by a process.
|
||||
|
||||
Input is the real device, as in /dev/sda or /dev/dm-7
|
||||
"""
|
||||
return any(device_path == os.path.realpath(p)
|
||||
for p in glob.glob('/proc/*/fd/*'))
|
||||
|
||||
|
||||
@os_brick.privileged.proc_fds.entrypoint
|
||||
def processes_using_devices(device_paths: list[str]) -> dict:
|
||||
"""Return a dict with how many processes are using a list of devices.
|
||||
|
||||
Input is the real devices, eg: ['/dev/sda', '/dev/dm-7']
|
||||
"""
|
||||
result = {path: 0 for path in device_paths}
|
||||
for path in glob.glob('/proc/*/fd/*'):
|
||||
real_path = os.path.realpath(path)
|
||||
if real_path in device_paths:
|
||||
result[real_path] += 1
|
||||
return result
|
|
@ -219,6 +219,47 @@ class FibreChannelConnectorTestCase(test_connector.ConnectorTestCase):
|
|||
'-fc-0x1234567890123456-lun-1']
|
||||
self.assertEqual(expected, volume_paths)
|
||||
|
||||
@mock.patch('os.path.exists')
|
||||
@mock.patch('os.path.realpath')
|
||||
def test__existing_devices(self, realpath_mock, exists_mock):
|
||||
possible_paths = ['/dev/disk/by-id/wwn1', '/dev/disk/by-id/wwn2',
|
||||
'/dev/disk/by-id/wwn3', '/dev/disk/by-id/wwn4']
|
||||
real_paths = ['/dev/dm-1', '/dev/dm-2', '/dev/dm-3', '/dev/dm-4']
|
||||
realpath_mock.side_effect = real_paths
|
||||
exists_mock.side_effect = [False, True, False, True]
|
||||
|
||||
res = self.connector._existing_devices(possible_paths)
|
||||
|
||||
self.assertEqual(len(possible_paths), realpath_mock.call_count)
|
||||
realpath_mock.assert_has_calls([mock.call(path)
|
||||
for path in possible_paths])
|
||||
self.assertEqual(len(possible_paths), exists_mock.call_count)
|
||||
exists_mock.assert_has_calls([mock.call(path) for path in real_paths])
|
||||
self.assertListEqual(['dm-2', 'dm-4'], res)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'cleanup_pre_existing')
|
||||
@mock.patch.object(fibre_channel.FibreChannelConnector,
|
||||
'_existing_devices')
|
||||
def test_cleanup_pre_existing(self, existing_mock, cleanup_mock):
|
||||
self.connector.cleanup_pre_existing({'wwn': mock.sentinel.wwn},
|
||||
mock.sentinel.existing)
|
||||
existing_mock.assert_called_once_with(mock.sentinel.existing)
|
||||
cleanup_mock.assert_called_once_with(existing_mock.return_value,
|
||||
mock.sentinel.wwn)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'cleanup_pre_existing')
|
||||
@mock.patch.object(fibre_channel.FibreChannelConnector,
|
||||
'_existing_devices')
|
||||
def test_cleanup_pre_existing_no_wwn(self, existing_mock, cleanup_mock):
|
||||
self.connector.cleanup_pre_existing({}, mock.sentinel.existing)
|
||||
existing_mock.assert_called_once_with(mock.sentinel.existing)
|
||||
cleanup_mock.assert_called_once_with(existing_mock.return_value, None)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'validate_devices')
|
||||
@mock.patch.object(fibre_channel.FibreChannelConnector,
|
||||
'_existing_devices')
|
||||
@mock.patch.object(fibre_channel.FibreChannelConnector,
|
||||
'cleanup_pre_existing')
|
||||
@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')
|
||||
|
@ -236,7 +277,10 @@ class FibreChannelConnectorTestCase(test_connector.ConnectorTestCase):
|
|||
get_fc_hbas_mock,
|
||||
realpath_mock,
|
||||
exists_mock,
|
||||
wait_for_rw_mock):
|
||||
wait_for_rw_mock,
|
||||
cleanup_mock,
|
||||
existing_mock,
|
||||
validate_mock):
|
||||
check_valid_device_mock.return_value = 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
|
||||
|
@ -265,15 +309,24 @@ class FibreChannelConnectorTestCase(test_connector.ConnectorTestCase):
|
|||
for wwn, lun in wwns_luns:
|
||||
connection_info = self.fibrechan_connection(vol, location,
|
||||
wwn, lun)
|
||||
connection_info['data']['wwn'] = mock.sentinel.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)
|
||||
host_devs = [f'/dev/disk/by-path/pci-0000:05:00.2-fc-0x{w}-lun-1'
|
||||
for w in ([wwn] if isinstance(wwn, str) else wwn)]
|
||||
self.assertEqual(dev_info['type'], 'block')
|
||||
self.assertEqual(dev_info['path'], dev_str)
|
||||
self.assertEqual(dev_info['path'], host_devs[0])
|
||||
self.assertNotIn('multipath_id', dev_info)
|
||||
self.assertNotIn('devices', dev_info)
|
||||
|
||||
cleanup_mock.assert_called_once_with(connection_info['data'],
|
||||
host_devs)
|
||||
cleanup_mock.reset_mock()
|
||||
existing_mock.assert_called_once_with(host_devs)
|
||||
existing_mock.reset_mock()
|
||||
validate_mock.assert_called_once_with(existing_mock.return_value,
|
||||
None, mock.sentinel.wwn)
|
||||
validate_mock.reset_mock()
|
||||
|
||||
self.connector.disconnect_volume(connection_info['data'], dev_info)
|
||||
expected_commands = []
|
||||
self.assertEqual(expected_commands, self.cmds)
|
||||
|
@ -290,6 +343,11 @@ class FibreChannelConnectorTestCase(test_connector.ConnectorTestCase):
|
|||
self.connector.connect_volume,
|
||||
connection_info['data'])
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'validate_devices')
|
||||
@mock.patch.object(fibre_channel.FibreChannelConnector,
|
||||
'_existing_devices')
|
||||
@mock.patch.object(fibre_channel.FibreChannelConnector,
|
||||
'cleanup_pre_existing')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device_path')
|
||||
def _test_connect_volume_multipath(self, get_device_info_mock,
|
||||
get_scsi_wwn_mock,
|
||||
|
@ -301,7 +359,10 @@ class FibreChannelConnectorTestCase(test_connector.ConnectorTestCase):
|
|||
find_mp_dev_mock,
|
||||
access_mode,
|
||||
should_wait_for_rw,
|
||||
find_mp_device_path_mock):
|
||||
find_mp_device_path_mock,
|
||||
cleanup_mock,
|
||||
existing_mock,
|
||||
validate_mock):
|
||||
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
|
||||
|
@ -326,6 +387,7 @@ class FibreChannelConnectorTestCase(test_connector.ConnectorTestCase):
|
|||
vol = {'id': 1, 'name': name}
|
||||
initiator_wwn = ['1234567890123456', '1234567890123457']
|
||||
|
||||
realpath_mock.return_value = 'dm-7'
|
||||
find_mp_device_path_mock.return_value = '/dev/mapper/mpatha'
|
||||
find_mp_dev_mock.return_value = {"device": "dm-3",
|
||||
"id": wwn,
|
||||
|
@ -338,6 +400,16 @@ class FibreChannelConnectorTestCase(test_connector.ConnectorTestCase):
|
|||
self.connector.connect_volume(connection_info['data'])
|
||||
|
||||
self.assertEqual(should_wait_for_rw, wait_for_rw_mock.called)
|
||||
host_devs = [f'/dev/disk/by-path/pci-0000:05:00.2-fc-0x{w}-lun-1'
|
||||
for w in initiator_wwn]
|
||||
cleanup_mock.assert_called_once_with(connection_info['data'],
|
||||
host_devs)
|
||||
existing_mock.assert_called_once_with(host_devs)
|
||||
|
||||
mpath = 'dm-7' if realpath_mock.call_count > 1 else None
|
||||
validate_mock.assert_called_once_with(existing_mock.return_value,
|
||||
mpath,
|
||||
wwn)
|
||||
|
||||
self.connector.disconnect_volume(connection_info['data'],
|
||||
devices['devices'][0])
|
||||
|
|
|
@ -396,16 +396,49 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
|
|||
'multipath_id': FAKE_SCSI_WWN}
|
||||
self.assertEqual(expected_result, result)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'cleanup_pre_existing')
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_get_connection_devices')
|
||||
def test_cleanup_pre_existing(self, get_devs_mock, cleanup_mock):
|
||||
get_devs_mock.return_value = {
|
||||
('ip1:port1', 'tgt1'): ({'sda'}, set()),
|
||||
('ip2:port2', 'tgt2'): ({'sdb'}, {'sdc'}),
|
||||
('ip3:port3', 'tgt3'): (set(), set()),
|
||||
}
|
||||
conn_props = {'wwn': mock.sentinel.wwn}
|
||||
self.connector.cleanup_pre_existing(conn_props)
|
||||
get_devs_mock.assert_called_once_with(conn_props,
|
||||
is_disconnect_call=True)
|
||||
expected_devs = ['sda', 'sdb']
|
||||
cleanup_mock.assert_called_once_with(expected_devs, mock.sentinel.wwn)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'cleanup_pre_existing')
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_get_connection_devices')
|
||||
def test_cleanup_pre_existing_no_wwn(self, get_devs_mock, cleanup_mock):
|
||||
get_devs_mock.return_value = {
|
||||
('ip1:port1', 'tgt1'): ({'sda'}, set()),
|
||||
('ip2:port2', 'tgt2'): ({'sdb'}, {'sdc'}),
|
||||
('ip3:port3', 'tgt3'): (set(), set()),
|
||||
}
|
||||
conn_props = {}
|
||||
self.connector.cleanup_pre_existing(conn_props)
|
||||
get_devs_mock.assert_called_once_with(conn_props,
|
||||
is_disconnect_call=True)
|
||||
expected_devs = ['sda', 'sdb']
|
||||
cleanup_mock.assert_called_once_with(expected_devs, None)
|
||||
|
||||
@mock.patch.object(iscsi.ISCSIConnector, 'cleanup_pre_existing')
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_cleanup_connection')
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_connect_multipath_volume')
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_connect_single_volume')
|
||||
def test_connect_volume_mp(self, con_single_mock, con_mp_mock, clean_mock):
|
||||
def test_connect_volume_mp(self, con_single_mock, con_mp_mock, clean_mock,
|
||||
pre_mock):
|
||||
self.connector.use_multipath = True
|
||||
res = self.connector.connect_volume(self.CON_PROPS)
|
||||
self.assertEqual(con_mp_mock.return_value, res)
|
||||
con_single_mock.assert_not_called()
|
||||
con_mp_mock.assert_called_once_with(self.CON_PROPS)
|
||||
clean_mock.assert_not_called()
|
||||
pre_mock.assert_called_once_with(self.CON_PROPS)
|
||||
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_cleanup_connection')
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_connect_multipath_volume')
|
||||
|
@ -420,16 +453,19 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
|
|||
con_mp_mock.assert_called_once_with(self.CON_PROPS)
|
||||
clean_mock.assert_called_once_with(self.CON_PROPS, force=True)
|
||||
|
||||
@mock.patch.object(iscsi.ISCSIConnector, 'cleanup_pre_existing')
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_cleanup_connection')
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_connect_multipath_volume')
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_connect_single_volume')
|
||||
def test_connect_volume_sp(self, con_single_mock, con_mp_mock, clean_mock):
|
||||
def test_connect_volume_sp(self, con_single_mock, con_mp_mock, clean_mock,
|
||||
pre_mock):
|
||||
self.connector.use_multipath = False
|
||||
res = self.connector.connect_volume(self.CON_PROPS)
|
||||
self.assertEqual(con_single_mock.return_value, res)
|
||||
con_mp_mock.assert_not_called()
|
||||
con_single_mock.assert_called_once_with(self.CON_PROPS)
|
||||
clean_mock.assert_not_called()
|
||||
pre_mock.assert_called_once_with(self.CON_PROPS)
|
||||
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_cleanup_connection')
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_connect_multipath_volume')
|
||||
|
@ -1174,19 +1210,21 @@ Setting up iSCSI targets: unused
|
|||
# Called twice by the retry mechanism
|
||||
self.assertEqual(2, sleep_mock.call_count)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'validate_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn',
|
||||
side_effect=(None, 'tgt2'))
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_connect_vol')
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_cleanup_connection')
|
||||
@mock.patch('os_brick.utils._time_sleep')
|
||||
def test_connect_single_volume(self, sleep_mock, cleanup_mock,
|
||||
connect_mock, get_wwn_mock):
|
||||
connect_mock, get_wwn_mock, validate_mock):
|
||||
def my_connect(rescans, props, data):
|
||||
if props['target_iqn'] == 'tgt2':
|
||||
# Succeed on second call
|
||||
data['found_devices'].append('sdz')
|
||||
|
||||
connect_mock.side_effect = my_connect
|
||||
validate_mock.return_value = 'tgt2'
|
||||
|
||||
res = self.connector._connect_single_volume(self.CON_PROPS)
|
||||
|
||||
|
@ -1199,41 +1237,49 @@ Setting up iSCSI targets: unused
|
|||
'target_portal': 'ip1:port1', 'target_iqn': 'tgt1'},
|
||||
[('ip1:port1', 'tgt1', 4), ],
|
||||
force=True, ignore_errors=True)
|
||||
validate_mock.assert_called_once_with(['sdz'], cinder_wwn=None)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'validate_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn', return_value='')
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_connect_vol')
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_cleanup_connection')
|
||||
@mock.patch('os_brick.utils._time_sleep')
|
||||
def test_connect_single_volume_no_wwn(self, sleep_mock, cleanup_mock,
|
||||
connect_mock, get_wwn_mock):
|
||||
connect_mock, get_wwn_mock,
|
||||
validate_mock):
|
||||
def my_connect(rescans, props, data):
|
||||
data['found_devices'].append('sdz')
|
||||
|
||||
connect_mock.side_effect = my_connect
|
||||
|
||||
validate_mock.return_value = mock.sentinel.wwn
|
||||
res = self.connector._connect_single_volume(self.CON_PROPS)
|
||||
|
||||
expected = {'type': 'block', 'scsi_wwn': '', 'path': '/dev/sdz'}
|
||||
expected = {'type': 'block', 'scsi_wwn': mock.sentinel.wwn,
|
||||
'path': '/dev/sdz'}
|
||||
self.assertEqual(expected, res)
|
||||
get_wwn_mock.assert_has_calls([mock.call(['sdz'])] * 10)
|
||||
self.assertEqual(10, get_wwn_mock.call_count)
|
||||
sleep_mock.assert_has_calls([mock.call(1)] * 10)
|
||||
self.assertEqual(10, sleep_mock.call_count)
|
||||
cleanup_mock.assert_not_called()
|
||||
validate_mock.assert_called_once_with(['sdz'], cinder_wwn=None)
|
||||
|
||||
@staticmethod
|
||||
def _get_connect_vol_data():
|
||||
return {'stop_connecting': False, 'num_logins': 0, 'failed_logins': 0,
|
||||
'stopped_threads': 0, 'found_devices': [],
|
||||
'just_added_devices': []}
|
||||
'just_added_devices': [], 'not_found_session_hctl': []}
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'validate_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn',
|
||||
side_effect=(None, 'tgt2'))
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_connect_vol')
|
||||
@mock.patch.object(iscsi.ISCSIConnector, '_cleanup_connection')
|
||||
@mock.patch('os_brick.utils._time_sleep')
|
||||
def test_connect_single_volume_not_found(self, sleep_mock, cleanup_mock,
|
||||
connect_mock, get_wwn_mock):
|
||||
connect_mock, get_wwn_mock,
|
||||
validate_mock):
|
||||
|
||||
self.assertRaises(exception.VolumeDeviceNotFound,
|
||||
self.connector._connect_single_volume,
|
||||
|
@ -1262,7 +1308,9 @@ Setting up iSCSI targets: unused
|
|||
data)
|
||||
for prop in props]
|
||||
connect_mock.assert_has_calls(calls_per_try * 3)
|
||||
validate_mock.assert_not_called()
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'validate_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_sysfs_multipath_dm',
|
||||
side_effect=[None, 'dm-0'])
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn',
|
||||
|
@ -1274,7 +1322,7 @@ Setting up iSCSI targets: unused
|
|||
def test_connect_multipath_volume_all_succeed(self, sleep_mock,
|
||||
connect_mock, add_wwid_mock,
|
||||
add_path_mock, get_wwn_mock,
|
||||
find_dm_mock):
|
||||
find_dm_mock, validate_mock):
|
||||
def my_connect(rescans, props, data):
|
||||
devs = {'tgt1': 'sda', 'tgt2': 'sdb', 'tgt3': 'sdc', 'tgt4': 'sdd'}
|
||||
data['stopped_threads'] += 1
|
||||
|
@ -1284,6 +1332,7 @@ Setting up iSCSI targets: unused
|
|||
data['just_added_devices'].append(dev)
|
||||
|
||||
connect_mock.side_effect = my_connect
|
||||
validate_mock.return_value = 'wwn'
|
||||
|
||||
res = self.connector._connect_multipath_volume(self.CON_PROPS)
|
||||
|
||||
|
@ -1301,7 +1350,10 @@ Setting up iSCSI targets: unused
|
|||
self.assertNotEqual(0, add_path_mock.call_count)
|
||||
self.assertGreaterEqual(find_dm_mock.call_count, 2)
|
||||
self.assertEqual(4, connect_mock.call_count)
|
||||
validate_mock.assert_called_once_with(['sda', 'sdb', 'sdc', 'sdd'],
|
||||
'dm-0', None)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'validate_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_sysfs_multipath_dm',
|
||||
side_effect=[None, 'dm-0'])
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn', return_value='')
|
||||
|
@ -1311,7 +1363,8 @@ Setting up iSCSI targets: unused
|
|||
@mock.patch('os_brick.utils._time_sleep')
|
||||
def test_connect_multipath_volume_no_wwid(self, sleep_mock, connect_mock,
|
||||
add_wwid_mock, add_path_mock,
|
||||
get_wwn_mock, find_dm_mock):
|
||||
get_wwn_mock, find_dm_mock,
|
||||
validate_mock):
|
||||
# Even if we don't have the wwn we'll be able to find the multipath
|
||||
def my_connect(rescans, props, data):
|
||||
devs = {'tgt1': 'sda', 'tgt2': 'sdb', 'tgt3': 'sdc', 'tgt4': 'sdd'}
|
||||
|
@ -1322,27 +1375,32 @@ Setting up iSCSI targets: unused
|
|||
data['just_added_devices'].append(dev)
|
||||
|
||||
connect_mock.side_effect = my_connect
|
||||
validate_mock.return_value = mock.sentinel.wwn
|
||||
|
||||
with mock.patch.object(self.connector,
|
||||
'use_multipath'):
|
||||
res = self.connector._connect_multipath_volume(self.CON_PROPS)
|
||||
|
||||
expected = {'type': 'block', 'scsi_wwn': '', 'multipath_id': '',
|
||||
expected = {'type': 'block', 'scsi_wwn': mock.sentinel.wwn,
|
||||
'multipath_id': mock.sentinel.wwn,
|
||||
'path': '/dev/dm-0'}
|
||||
self.assertEqual(expected, res)
|
||||
|
||||
self.assertEqual(3, get_wwn_mock.call_count)
|
||||
self.assertEqual(2, get_wwn_mock.call_count)
|
||||
result = list(get_wwn_mock.call_args[0][0])
|
||||
result.sort()
|
||||
self.assertEqual(['sda', 'sdb', 'sdc', 'sdd'], result)
|
||||
# Initially mpath we pass is None, but on last call is the mpath
|
||||
mpath_values = [c[1][1] for c in get_wwn_mock._mock_mock_calls]
|
||||
self.assertEqual([None, None, 'dm-0'], mpath_values)
|
||||
self.assertEqual([None, None], mpath_values)
|
||||
add_wwid_mock.assert_not_called()
|
||||
add_path_mock.assert_not_called()
|
||||
self.assertGreaterEqual(find_dm_mock.call_count, 2)
|
||||
self.assertEqual(4, connect_mock.call_count)
|
||||
validate_mock.assert_called_once_with(['sda', 'sdb', 'sdc', 'sdd'],
|
||||
'dm-0', None)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'validate_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_sysfs_multipath_dm',
|
||||
side_effect=[None, 'dm-0'])
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn',
|
||||
|
@ -1353,7 +1411,8 @@ Setting up iSCSI targets: unused
|
|||
@mock.patch('os_brick.utils._time_sleep')
|
||||
def test_connect_multipath_volume_all_fail(self, sleep_mock, connect_mock,
|
||||
add_wwid_mock, add_path_mock,
|
||||
get_wwn_mock, find_dm_mock):
|
||||
get_wwn_mock, find_dm_mock,
|
||||
validate_mock):
|
||||
def my_connect(rescans, props, data):
|
||||
data['stopped_threads'] += 1
|
||||
data['failed_logins'] += 1
|
||||
|
@ -1369,7 +1428,9 @@ Setting up iSCSI targets: unused
|
|||
add_path_mock.assert_not_called()
|
||||
find_dm_mock.assert_not_called()
|
||||
self.assertEqual(4 * 3, connect_mock.call_count)
|
||||
validate_mock.assert_not_called()
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'validate_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_sysfs_multipath_dm',
|
||||
side_effect=[None, 'dm-0'])
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn',
|
||||
|
@ -1383,7 +1444,8 @@ Setting up iSCSI targets: unused
|
|||
add_wwid_mock,
|
||||
add_path_mock,
|
||||
get_wwn_mock,
|
||||
find_dm_mock):
|
||||
find_dm_mock,
|
||||
validate_mock):
|
||||
def my_connect(rescans, props, data):
|
||||
devs = {'tgt1': '', 'tgt2': 'sdb', 'tgt3': '', 'tgt4': 'sdd'}
|
||||
data['stopped_threads'] += 1
|
||||
|
@ -1397,7 +1459,10 @@ Setting up iSCSI targets: unused
|
|||
|
||||
connect_mock.side_effect = my_connect
|
||||
|
||||
res = self.connector._connect_multipath_volume(self.CON_PROPS)
|
||||
conn_props = self.CON_PROPS.copy()
|
||||
conn_props['wwn'] = mock.sentinel.cinder_wwn
|
||||
validate_mock.return_value = 'wwn'
|
||||
res = self.connector._connect_multipath_volume(conn_props)
|
||||
|
||||
expected = {'type': 'block', 'scsi_wwn': 'wwn', 'multipath_id': 'wwn',
|
||||
'path': '/dev/dm-0'}
|
||||
|
@ -1410,7 +1475,10 @@ Setting up iSCSI targets: unused
|
|||
self.assertNotEqual(0, add_path_mock.call_count)
|
||||
self.assertGreaterEqual(find_dm_mock.call_count, 2)
|
||||
self.assertEqual(4, connect_mock.call_count)
|
||||
validate_mock.assert_called_once_with(['sdb', 'sdd'], 'dm-0',
|
||||
mock.sentinel.cinder_wwn)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'validate_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_sysfs_multipath_dm',
|
||||
return_value=None)
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn',
|
||||
|
@ -1426,7 +1494,8 @@ Setting up iSCSI targets: unused
|
|||
add_wwid_mock,
|
||||
add_path_mock,
|
||||
get_wwn_mock,
|
||||
find_dm_mock):
|
||||
find_dm_mock,
|
||||
validate_mock):
|
||||
def my_connect(rescans, props, data):
|
||||
devs = {'tgt1': '', 'tgt2': 'sdb', 'tgt3': '', 'tgt4': 'sdd'}
|
||||
data['stopped_threads'] += 1
|
||||
|
@ -1439,6 +1508,7 @@ Setting up iSCSI targets: unused
|
|||
data['failed_logins'] += 1
|
||||
|
||||
connect_mock.side_effect = my_connect
|
||||
validate_mock.return_value = 'wwn'
|
||||
|
||||
res = self.connector._connect_multipath_volume(self.CON_PROPS)
|
||||
|
||||
|
@ -1454,7 +1524,9 @@ Setting up iSCSI targets: unused
|
|||
self.assertNotEqual(0, add_path_mock.call_count)
|
||||
self.assertGreaterEqual(find_dm_mock.call_count, 4)
|
||||
self.assertEqual(4, connect_mock.call_count)
|
||||
validate_mock.assert_called_once_with(['sdb', 'sdd'], None, None)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'validate_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_sysfs_multipath_dm',
|
||||
return_value=None)
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn',
|
||||
|
@ -1470,7 +1542,8 @@ Setting up iSCSI targets: unused
|
|||
add_wwid_mock,
|
||||
add_path_mock,
|
||||
get_wwn_mock,
|
||||
find_dm_mock):
|
||||
find_dm_mock,
|
||||
validate_mock):
|
||||
def my_connect(rescans, props, data):
|
||||
data['stopped_threads'] += 1
|
||||
data['num_logins'] += 1
|
||||
|
@ -1486,6 +1559,7 @@ Setting up iSCSI targets: unused
|
|||
add_path_mock.assert_not_called()
|
||||
find_dm_mock.assert_not_called()
|
||||
self.assertEqual(12, connect_mock.call_count)
|
||||
validate_mock.assert_not_called()
|
||||
|
||||
@mock.patch('os_brick.utils._time_sleep')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'scan_iscsi')
|
||||
|
@ -1626,7 +1700,12 @@ Setting up iSCSI targets: unused
|
|||
self.connector._connect_vol(3, self.CON_PROPS, data)
|
||||
|
||||
expected = self._get_connect_vol_data()
|
||||
expected.update(num_logins=1, stopped_threads=1)
|
||||
not_found = [(mock.sentinel.session, [mock.sentinel.host,
|
||||
mock.sentinel.channel,
|
||||
mock.sentinel.target,
|
||||
mock.sentinel.lun])]
|
||||
expected.update(num_logins=1, stopped_threads=1,
|
||||
not_found_session_hctl=not_found)
|
||||
self.assertDictEqual(expected, data)
|
||||
|
||||
hctl_mock.assert_called_once_with(mock.sentinel.session,
|
||||
|
@ -1662,7 +1741,12 @@ Setting up iSCSI targets: unused
|
|||
self.connector._connect_vol(3, self.CON_PROPS, data)
|
||||
|
||||
expected = self._get_connect_vol_data()
|
||||
expected.update(num_logins=1, stopped_threads=1, stop_connecting=True)
|
||||
not_found = [(mock.sentinel.session, [mock.sentinel.host,
|
||||
mock.sentinel.channel,
|
||||
mock.sentinel.target,
|
||||
mock.sentinel.lun])]
|
||||
expected.update(num_logins=1, stopped_threads=1, stop_connecting=True,
|
||||
not_found_session_hctl=not_found)
|
||||
self.assertDictEqual(expected, data)
|
||||
|
||||
hctl_mock.assert_called_once_with(mock.sentinel.session,
|
||||
|
|
|
@ -1047,18 +1047,16 @@ loop0 0"""
|
|||
'id': '0',
|
||||
'lun': '0'})
|
||||
|
||||
@mock.patch('builtins.open')
|
||||
def test_get_sysfs_wwn_mpath(self, open_mock):
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_mpath_sysfs_wwn')
|
||||
def test_get_sysfs_wwn_mpath(self, wwn_mock):
|
||||
wwn = '3600d0230000000000e13955cc3757800'
|
||||
cm_open = open_mock.return_value.__enter__.return_value
|
||||
cm_open.read.return_value = 'mpath-' + wwn
|
||||
|
||||
wwn_mock.return_value = wwn
|
||||
res = self.linuxscsi.get_sysfs_wwn(mock.sentinel.device_names, 'dm-1')
|
||||
open_mock.assert_called_once_with('/sys/block/dm-1/dm/uuid')
|
||||
wwn_mock.assert_called_once_with('dm-1')
|
||||
self.assertEqual(wwn, res)
|
||||
|
||||
@mock.patch('glob.glob')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_one_sysfs_wwid')
|
||||
def test_get_sysfs_wwn_single_designator(self, get_wwid_mock, glob_mock):
|
||||
glob_mock.return_value = ['/dev/disk/by-id/scsi-wwid1',
|
||||
'/dev/disk/by-id/scsi-wwid2']
|
||||
|
@ -1068,16 +1066,17 @@ loop0 0"""
|
|||
glob_mock.assert_called_once_with('/dev/disk/by-id/scsi-*')
|
||||
get_wwid_mock.assert_called_once_with(mock.sentinel.device_names)
|
||||
|
||||
@mock.patch('builtins.open', side_effect=Exception)
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_mpath_sysfs_wwn')
|
||||
@mock.patch('glob.glob')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_one_sysfs_wwid')
|
||||
def test_get_sysfs_wwn_mpath_exc(self, get_wwid_mock, glob_mock,
|
||||
open_mock):
|
||||
wwn_mock):
|
||||
wwn_mock.return_value = None
|
||||
glob_mock.return_value = ['/dev/disk/by-id/scsi-wwid1',
|
||||
'/dev/disk/by-id/scsi-wwid2']
|
||||
get_wwid_mock.return_value = 'wwid1'
|
||||
res = self.linuxscsi.get_sysfs_wwn(mock.sentinel.device_names, 'dm-1')
|
||||
open_mock.assert_called_once_with('/sys/block/dm-1/dm/uuid')
|
||||
wwn_mock.assert_called_once_with('dm-1')
|
||||
self.assertEqual('wwid1', res)
|
||||
glob_mock.assert_called_once_with('/dev/disk/by-id/scsi-*')
|
||||
get_wwid_mock.assert_called_once_with(mock.sentinel.device_names)
|
||||
|
@ -1089,7 +1088,7 @@ loop0 0"""
|
|||
@mock.patch('os.path.islink', side_effect=(False,) + (True,) * 5)
|
||||
@mock.patch('os.stat', side_effect=(False,) + (True,) * 4)
|
||||
@mock.patch('glob.glob')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_one_sysfs_wwid')
|
||||
def test_get_sysfs_wwn_multiple_designators(self, get_wwid_mock, glob_mock,
|
||||
stat_mock, islink_mock,
|
||||
realpath_mock, listdir_mock):
|
||||
|
@ -1115,7 +1114,8 @@ loop0 0"""
|
|||
@mock.patch('os.path.islink', mock.Mock())
|
||||
@mock.patch('os.stat', mock.Mock())
|
||||
@mock.patch('glob.glob')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid', return_value='')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_one_sysfs_wwid',
|
||||
return_value='')
|
||||
def test_get_sysfs_wwn_dm_link(self, get_wwid_mock, glob_mock,
|
||||
realpath_mock, listdir_mock):
|
||||
glob_mock.return_value = ['/dev/disk/by-id/scsi-wwid1',
|
||||
|
@ -1135,7 +1135,7 @@ loop0 0"""
|
|||
@mock.patch('os.path.islink', return_value=True)
|
||||
@mock.patch('os.stat', return_value=True)
|
||||
@mock.patch('glob.glob')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_one_sysfs_wwid')
|
||||
def test_get_sysfs_wwn_not_found(self, get_wwid_mock, glob_mock, stat_mock,
|
||||
islink_mock, realpath_mock):
|
||||
glob_mock.return_value = ['/dev/disk/by-id/scsi-wwid1',
|
||||
|
@ -1148,7 +1148,7 @@ loop0 0"""
|
|||
get_wwid_mock.assert_called_once_with(devices)
|
||||
|
||||
@mock.patch('glob.glob', return_value=[])
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_one_sysfs_wwid')
|
||||
def test_get_sysfs_wwn_no_links(self, get_wwid_mock, glob_mock):
|
||||
get_wwid_mock.return_value = ''
|
||||
devices = ['sdc']
|
||||
|
@ -1163,25 +1163,79 @@ loop0 0"""
|
|||
@ddt.unpack
|
||||
@mock.patch('builtins.open')
|
||||
def test_get_sysfs_wwid(self, open_mock, wwn_type, num_val):
|
||||
read_fail = mock.MagicMock()
|
||||
read_fail.__enter__.return_value.read.side_effect = IOError
|
||||
read_data = mock.MagicMock()
|
||||
read_data.__enter__.return_value.read.return_value = (wwn_type +
|
||||
'wwid1\n')
|
||||
open_mock.side_effect = (IOError, read_fail, read_data)
|
||||
read_data = wwn_type + 'wwid1\n'
|
||||
open_mock.return_value.__enter__.return_value.read.return_value = \
|
||||
read_data
|
||||
|
||||
res = self.linuxscsi.get_sysfs_wwid(['sda', 'sdb', 'sdc'])
|
||||
res = self.linuxscsi.get_sysfs_wwid('sda')
|
||||
self.assertEqual(num_val + 'wwid1', res)
|
||||
open_mock.assert_has_calls([mock.call('/sys/block/sda/device/wwid'),
|
||||
mock.call('/sys/block/sdb/device/wwid'),
|
||||
mock.call('/sys/block/sdc/device/wwid')])
|
||||
open_mock.assert_called_once_with('/sys/block/sda/device/wwid')
|
||||
|
||||
@mock.patch('builtins.open', side_effect=IOError)
|
||||
def test_get_sysfs_wwid_not_found(self, open_mock):
|
||||
res = self.linuxscsi.get_sysfs_wwid(['sda', 'sdb'])
|
||||
def test_get_sysfs_wwid_not_found_open_error(self, open_mock):
|
||||
res = self.linuxscsi.get_sysfs_wwid('sda')
|
||||
self.assertEqual('', res)
|
||||
open_mock.assert_has_calls([mock.call('/sys/block/sda/device/wwid'),
|
||||
mock.call('/sys/block/sdb/device/wwid')])
|
||||
open_mock.assert_called_once_with('/sys/block/sda/device/wwid')
|
||||
|
||||
@mock.patch('builtins.open', side_effect=IOError)
|
||||
def test_get_sysfs_wwid_not_found_read_error(self, open_mock):
|
||||
open_mock.return_value.__enter__.return_value.read.side_effect = \
|
||||
IOError
|
||||
res = self.linuxscsi.get_sysfs_wwid('sda')
|
||||
self.assertEqual('', res)
|
||||
open_mock.assert_called_once_with('/sys/block/sda/device/wwid')
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
def test_get_one_sysfs_wwid(self, get_mock):
|
||||
get_mock.side_effect = ['', 'wwid1']
|
||||
res = self.linuxscsi.get_one_sysfs_wwid(['sda', 'sdb', 'sdc'])
|
||||
self.assertEqual(2, get_mock.call_count)
|
||||
get_mock.has_calls([mock.call('sda'), mock.call('sdb')])
|
||||
self.assertEqual('wwid1', res)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
def test_get_one_sysfs_wwid_not_found(self, get_mock):
|
||||
get_mock.return_value = ''
|
||||
res = self.linuxscsi.get_one_sysfs_wwid(['sda', 'sdb'])
|
||||
self.assertEqual(2, get_mock.call_count)
|
||||
get_mock.has_calls([mock.call('sda'), mock.call('sdb')])
|
||||
self.assertEqual('', res)
|
||||
|
||||
@mock.patch('os.listdir')
|
||||
def test__get_dev_names_in_mapth(self, list_mock):
|
||||
devs = ['sda', 'sdb', 'sdc', 'sdd']
|
||||
list_mock.return_value = devs
|
||||
res = self.linuxscsi._get_dev_names_in_mpath('dm-1')
|
||||
self.assertListEqual(devs, res)
|
||||
list_mock.assert_called_once_with('/sys/class/block/dm-1/slaves')
|
||||
|
||||
@mock.patch('os.listdir', side_effect=FileNotFoundError)
|
||||
def test__get_dev_names_in_mapth_not_found(self, list_mock):
|
||||
res = self.linuxscsi._get_dev_names_in_mpath('dm-1')
|
||||
self.assertListEqual([], res)
|
||||
|
||||
@mock.patch('builtins.open')
|
||||
def test_get_mpath_sysfs_wwn(self, open_mock):
|
||||
wwn = '3600d0230000000000e13955cc3757800'
|
||||
cm_open = open_mock.return_value.__enter__.return_value
|
||||
cm_open.read.return_value = 'mpath-' + wwn
|
||||
res = self.linuxscsi.get_mpath_sysfs_wwn('dm-1')
|
||||
open_mock.assert_called_once_with('/sys/block/dm-1/dm/uuid')
|
||||
self.assertEqual(wwn, res)
|
||||
|
||||
@mock.patch('builtins.open', side_effect=IOError)
|
||||
def test_get_mpath_sysfs_wwn_no_file(self, open_mock):
|
||||
res = self.linuxscsi.get_mpath_sysfs_wwn('dm-1')
|
||||
open_mock.assert_called_once_with('/sys/block/dm-1/dm/uuid')
|
||||
self.assertIsNone(res)
|
||||
|
||||
@mock.patch('builtins.open')
|
||||
def test_get_mpath_sysfs_wwn_read_error(self, open_mock):
|
||||
open_mock.return_value.__enter__.return_value.read.side_effect = \
|
||||
IOError
|
||||
res = self.linuxscsi.get_mpath_sysfs_wwn('dm-1')
|
||||
open_mock.assert_called_once_with('/sys/block/dm-1/dm/uuid')
|
||||
self.assertIsNone(res)
|
||||
|
||||
@mock.patch.object(linuxscsi.priv_rootwrap, 'unlink_root')
|
||||
@mock.patch('glob.glob')
|
||||
|
@ -1380,3 +1434,417 @@ loop0 0"""
|
|||
if real_paths:
|
||||
mocked.assert_has_calls([mock.call(path),
|
||||
mock.call(path_used)])
|
||||
|
||||
@mock.patch('glob.glob')
|
||||
@mock.patch('os_brick.privileged.utils.processes_using_devices')
|
||||
def test_cleanup_pre_existing_nothing(self, using_mock, glob_mock):
|
||||
self.linuxscsi.cleanup_pre_existing([], None)
|
||||
using_mock.assert_not_called()
|
||||
glob_mock.assert_not_called()
|
||||
|
||||
@mock.patch('glob.glob')
|
||||
@mock.patch('os_brick.privileged.utils.processes_using_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_del_map')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_remove_unwanted_multipath_paths')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_delete_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_mpath_sysfs_wwn')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
def test_cleanup_pre_existing_no_wwn(self, wwid_mock, wwn_mock, del_mock,
|
||||
remove_mock, mp_del_mock, using_mock,
|
||||
glob_mock):
|
||||
devices = ['sda', 'sdb']
|
||||
using_mock.return_value = {'/dev/sda': 1, '/dev/sdb': 1}
|
||||
glob_mock.side_effect = [['/sys/block/sda/holders/dm-6'],
|
||||
['/sys/block/sdb/holders/dm-7']]
|
||||
self.linuxscsi.cleanup_pre_existing(devices, None)
|
||||
using_mock.assert_called_once_with(['/dev/sda', '/dev/sdb'])
|
||||
self.assertEqual(2, glob_mock.call_count)
|
||||
glob_mock.assert_has_calls(
|
||||
[mock.call('/sys/block/sda/holders/dm-*'),
|
||||
mock.call('/sys/block/sdb/holders/dm-*')])
|
||||
del_mock.assert_called_once_with(devices)
|
||||
remove_mock.assert_not_called()
|
||||
mp_del_mock.assert_not_called()
|
||||
|
||||
@mock.patch('glob.glob')
|
||||
@mock.patch('os_brick.privileged.utils.processes_using_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_del_map')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_remove_unwanted_multipath_paths')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_delete_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_mpath_sysfs_wwn')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
def test_cleanup_pre_existing_only_wwn(self, wwid_mock, wwn_mock, del_mock,
|
||||
remove_mock, mp_del_mock,
|
||||
using_mock, glob_mock):
|
||||
glob_mock.side_effect = [
|
||||
['/sys/block/sda', '/sys/block/sdb', '/sys/block/sdc'],
|
||||
['/sys/block/dm-6', '/sys/block/dm-7', '/sys/block/dm-8'],
|
||||
['/sys/block/sda/holders/dm-6'], [], [],
|
||||
]
|
||||
wwid_mock.side_effect = [mock.sentinel.wwn, mock.sentinel.wwn2,
|
||||
mock.sentinel.wwn]
|
||||
wwn_mock.side_effect = [mock.sentinel.wwn2, mock.sentinel.wwn]
|
||||
using_mock.return_value = {'/dev/sda': 1, '/dev/sdc': 0,
|
||||
'/dev/dm-7': 0}
|
||||
|
||||
self.linuxscsi.cleanup_pre_existing([], mock.sentinel.wwn)
|
||||
using_mock.assert_called_once_with(['/dev/sda', '/dev/sdc',
|
||||
'/dev/dm-7'])
|
||||
del_mock.assert_called_once_with(['sda', 'sdc'])
|
||||
remove_mock.assert_called_once_with('dm-7')
|
||||
mp_del_mock.assert_called_once_with('dm-7')
|
||||
|
||||
@mock.patch('glob.glob')
|
||||
@mock.patch('os_brick.privileged.utils.processes_using_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_del_map')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_remove_unwanted_multipath_paths')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_delete_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_mpath_sysfs_wwn')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
def test_cleanup_pre_existing(self, wwid_mock, wwn_mock, del_mock,
|
||||
remove_mock, mp_del_mock, using_mock,
|
||||
glob_mock):
|
||||
glob_mock.side_effect = [
|
||||
['/sys/block/sda', '/sys/block/sdb', '/sys/block/sdc',
|
||||
'/sys/block/sdw'],
|
||||
['/sys/block/dm-6', '/sys/block/dm-7', '/sys/block/dm-8'],
|
||||
['/sys/block/sda/holders/dm-6'], [], [], [],
|
||||
]
|
||||
wwid_mock.side_effect = [mock.sentinel.wwn2, mock.sentinel.wwn2,
|
||||
mock.sentinel.wwn, mock.sentinel.wwn3]
|
||||
wwn_mock.side_effect = [mock.sentinel.wwn2, mock.sentinel.wwn]
|
||||
using_mock.return_value = {'/dev/sda': 1, '/dev/sdc': 0,
|
||||
'/dev/dm-7': 0, '/dev/sdw': 0}
|
||||
|
||||
self.linuxscsi.cleanup_pre_existing(['sda', 'sdw'], mock.sentinel.wwn)
|
||||
using_mock.assert_called_once_with(['/dev/sda', '/dev/sdw', '/dev/sdc',
|
||||
'/dev/dm-7'])
|
||||
del_mock.assert_called_once_with(['sda', 'sdw', 'sdc'])
|
||||
remove_mock.assert_called_once_with('dm-7')
|
||||
mp_del_mock.assert_called_once_with('dm-7')
|
||||
|
||||
@mock.patch('glob.glob')
|
||||
@mock.patch('os_brick.privileged.utils.processes_using_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_del_map')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_remove_unwanted_multipath_paths')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_delete_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_mpath_sysfs_wwn')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
def test_cleanup_pre_existing_using_dev(self, wwid_mock, wwn_mock,
|
||||
del_mock, remove_mock, mp_del_mock,
|
||||
using_mock, glob_mock):
|
||||
devices = ['sda', 'sdb']
|
||||
using_mock.return_value = {'/dev/sda': 1, '/dev/sdb': 1}
|
||||
glob_mock.side_effect = [['/sys/block/sda/holders/dm-6'], []]
|
||||
self.assertRaises(exception.DeviceInUse,
|
||||
self.linuxscsi.cleanup_pre_existing,
|
||||
devices, None)
|
||||
using_mock.assert_called_once_with(['/dev/sda', '/dev/sdb'])
|
||||
self.assertEqual(2, glob_mock.call_count)
|
||||
glob_mock.assert_has_calls(
|
||||
[mock.call('/sys/block/sda/holders/dm-*'),
|
||||
mock.call('/sys/block/sdb/holders/dm-*')])
|
||||
del_mock.assert_not_called()
|
||||
remove_mock.assert_not_called()
|
||||
mp_del_mock.assert_not_called()
|
||||
|
||||
@mock.patch('glob.glob')
|
||||
@mock.patch('os_brick.privileged.utils.processes_using_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_del_map')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_remove_unwanted_multipath_paths')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_delete_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_mpath_sysfs_wwn')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
def test_cleanup_pre_existing_using_mpath(self, wwid_mock, wwn_mock,
|
||||
del_mock, remove_mock,
|
||||
mp_del_mock, using_mock,
|
||||
glob_mock):
|
||||
glob_mock.side_effect = [
|
||||
['/sys/block/sda', '/sys/block/sdb', '/sys/block/sdc'],
|
||||
['/sys/block/dm-6', '/sys/block/dm-7', '/sys/block/dm-8'],
|
||||
['/sys/block/sda/holders/dm-6'], [], [],
|
||||
]
|
||||
wwid_mock.side_effect = [mock.sentinel.wwn, mock.sentinel.wwn2,
|
||||
mock.sentinel.wwn]
|
||||
wwn_mock.side_effect = [mock.sentinel.wwn2, mock.sentinel.wwn]
|
||||
using_mock.return_value = {'/dev/sda': 1, '/dev/sdc': 0,
|
||||
'/dev/dm-7': 1}
|
||||
self.assertRaises(exception.DeviceInUse,
|
||||
self.linuxscsi.cleanup_pre_existing,
|
||||
[], mock.sentinel.wwn)
|
||||
using_mock.assert_called_once_with(['/dev/sda', '/dev/sdc',
|
||||
'/dev/dm-7'])
|
||||
del_mock.assert_not_called()
|
||||
remove_mock.assert_not_called()
|
||||
mp_del_mock.assert_not_called()
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_delete_devices')
|
||||
@mock.patch('os_brick.privileged.utils.processes_using_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_get_dev_names_in_mpath')
|
||||
def test__remove_unwanted_multipath_paths_no_mpath(self, names_mock,
|
||||
using_mock, del_mock):
|
||||
"""Test when the mpath has not paths or doesn't exist"""
|
||||
names_mock.return_value = []
|
||||
self.linuxscsi._remove_unwanted_multipath_paths('dm-7')
|
||||
names_mock.assert_called_once_with('dm-7')
|
||||
using_mock.assert_not_called()
|
||||
del_mock.assert_not_called()
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_delete_devices')
|
||||
@mock.patch('os_brick.privileged.utils.processes_using_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_get_dev_names_in_mpath')
|
||||
def test__remove_unwanted_multipath_paths_no_unwanted(
|
||||
self, names_mock, using_mock, del_mock):
|
||||
"""Test when the mpath exists but has no unwanted devices"""
|
||||
names_mock.return_value = ['sda', 'sdb']
|
||||
self.linuxscsi._remove_unwanted_multipath_paths(
|
||||
'dm-7', ignore_list=['sda', 'sdb', 'sdc'])
|
||||
names_mock.assert_called_once_with('dm-7')
|
||||
using_mock.assert_not_called()
|
||||
del_mock.assert_not_called()
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_delete_devices')
|
||||
@mock.patch('os_brick.privileged.utils.processes_using_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_get_dev_names_in_mpath')
|
||||
def test__remove_unwanted_multipath_paths_unused(
|
||||
self, names_mock, using_mock, del_mock):
|
||||
"""Test when the mpath has some unwanted paths"""
|
||||
names_mock.return_value = ['sda', 'sdb', 'sdc']
|
||||
using_mock.return_value = {'/dev/sdb': 0, '/dev/sdc': 0}
|
||||
self.linuxscsi._remove_unwanted_multipath_paths('dm-7',
|
||||
ignore_list=['sda'])
|
||||
names_mock.assert_called_once_with('dm-7')
|
||||
unused = ['/dev/sdb', '/dev/sdc']
|
||||
using_mock.assert_called_once_with(unused)
|
||||
del_mock.assert_called_once_with(unused)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_delete_devices')
|
||||
@mock.patch('os_brick.privileged.utils.processes_using_devices')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_get_dev_names_in_mpath')
|
||||
def test__remove_unwanted_multipath_paths_unwanted(
|
||||
self, names_mock, using_mock, del_mock):
|
||||
"""Test when the mpath has some unwanted paths in use"""
|
||||
names_mock.return_value = ['sda', 'sdb', 'sdc']
|
||||
using_mock.return_value = {'/dev/sdb': 0, '/dev/sdc': 2}
|
||||
self.assertRaises(exception.ActivePaths,
|
||||
self.linuxscsi._remove_unwanted_multipath_paths,
|
||||
'dm-7', ignore_list=['sda'])
|
||||
names_mock.assert_called_once_with('dm-7')
|
||||
using_mock.assert_called_once_with(['/dev/sdb', '/dev/sdc'])
|
||||
del_mock.assert_called_once_with(['/dev/sdb'])
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_del_path')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'is_multipath_running',
|
||||
return_value=True)
|
||||
def test__delete_devices(self, running_mock, del_mock, remove_mock):
|
||||
devices = ['sda', '/dev/sdb']
|
||||
self.linuxscsi._delete_devices(devices)
|
||||
running_mock.assert_called_once_with(
|
||||
enforce_multipath=False, root_helper=self.linuxscsi._root_helper)
|
||||
self.assertEqual(2, del_mock.call_count)
|
||||
del_mock.assert_has_calls([mock.call('/dev/sda'),
|
||||
mock.call('/dev/sdb')])
|
||||
self.assertEqual(2, remove_mock.call_count)
|
||||
remove_mock.assert_has_calls(
|
||||
[mock.call('/dev/sda', force=True, flush=False),
|
||||
mock.call('/dev/sdb', force=True, flush=False)])
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_del_path')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'is_multipath_running',
|
||||
return_value=False)
|
||||
def test__delete_devices_no_mpath_running(self, running_mock, del_mock,
|
||||
remove_mock):
|
||||
devices = ['sda', '/dev/sdb']
|
||||
self.linuxscsi._delete_devices(devices)
|
||||
running_mock.assert_called_once_with(
|
||||
enforce_multipath=False, root_helper=self.linuxscsi._root_helper)
|
||||
del_mock.assert_not_called()
|
||||
self.assertEqual(2, remove_mock.call_count)
|
||||
remove_mock.assert_has_calls(
|
||||
[mock.call('/dev/sda', force=True, flush=False),
|
||||
mock.call('/dev/sdb', force=True, flush=False)])
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_validate_wwn')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_validate_block_attributes')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_tidy_mpath')
|
||||
def test_validate_devices(self, mpath_mock, attr_mock, wwn_mock):
|
||||
devices = ['sda', 'sdb', 'sdc', 'sdd']
|
||||
res = self.linuxscsi.validate_devices(devices,
|
||||
'dm-7',
|
||||
mock.sentinel.wwn)
|
||||
mpath_mock.assert_called_once_with(devices, 'dm-7')
|
||||
attr_mock.assert_called_once_with(devices + ['dm-7'])
|
||||
wwn_mock.assert_called_once_with(devices, 'dm-7', mock.sentinel.wwn)
|
||||
self.assertEqual(wwn_mock.return_value, res)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_validate_wwn')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_validate_block_attributes')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_tidy_mpath')
|
||||
def test_validate_devices_single_path_no_wwn(self, mpath_mock,
|
||||
attr_mock, wwn_mock):
|
||||
devices = ['sda', 'sdb', 'sdc', 'sdd']
|
||||
res = self.linuxscsi.validate_devices(devices)
|
||||
mpath_mock.assert_not_called()
|
||||
attr_mock.assert_called_once_with(devices)
|
||||
wwn_mock.assert_called_once_with(devices, None, None)
|
||||
self.assertEqual(wwn_mock.return_value, res)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_remove_unwanted_multipath_paths')
|
||||
@mock.patch('os_brick.privileged.utils.device_is_used', return_value=False)
|
||||
def test__tidy_mpath(self, used_mock, remove_mock):
|
||||
devices = ['sda', 'sdb']
|
||||
self.linuxscsi._tidy_mpath(devices, 'dm-7')
|
||||
used_mock.assert_called_once_with('/dev/dm-7')
|
||||
remove_mock.assert_called_once_with('dm-7', ignore_list=devices)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, '_remove_unwanted_multipath_paths')
|
||||
@mock.patch('os_brick.privileged.utils.device_is_used', return_value=True)
|
||||
def test__tidy_mpath_used(self, used_mock, remove_mock):
|
||||
devices = ['sda', 'sdb']
|
||||
self.assertRaises(exception.DeviceInUse,
|
||||
self.linuxscsi._tidy_mpath,
|
||||
devices, 'dm-7')
|
||||
used_mock.assert_called_once_with('/dev/dm-7')
|
||||
remove_mock.assert_not_called()
|
||||
|
||||
@mock.patch('builtins.open')
|
||||
def test__validate_block_attributes_single_attribute(self, open_mock):
|
||||
"""Nothing to check on single path"""
|
||||
self.linuxscsi._validate_block_attributes(['sda'])
|
||||
open_mock.assert_not_called()
|
||||
|
||||
@mock.patch('builtins.open')
|
||||
def test__validate_block_attributes(self, open_mock):
|
||||
"""Attributes match"""
|
||||
open_mock.return_value.read.side_effect = \
|
||||
['500118192\n', '500118192\n', '500118192\n', # size
|
||||
'0\n', '0\n', '0\n', # alignment_offset
|
||||
'512\n', '512\n', '512\n', # physical_block_size
|
||||
'512\n', '512\n', '512\n'] # logical_block_size
|
||||
self.linuxscsi._validate_block_attributes(['sda', 'sdb', 'dm-7'])
|
||||
self.assertEqual(12, open_mock.call_count)
|
||||
open_mock.assert_has_calls(
|
||||
[mock.call('/sys/block/sda/size', 'r'),
|
||||
mock.call('/sys/block/sdb/size', 'r'),
|
||||
mock.call('/sys/block/dm-7/size', 'r'),
|
||||
mock.call('/sys/block/sda/alignment_offset', 'r'),
|
||||
mock.call('/sys/block/sdb/alignment_offset', 'r'),
|
||||
mock.call('/sys/block/dm-7/alignment_offset', 'r'),
|
||||
mock.call('/sys/block/sda/queue/physical_block_size', 'r'),
|
||||
mock.call('/sys/block/sdb/queue/physical_block_size', 'r'),
|
||||
mock.call('/sys/block/dm-7/queue/physical_block_size', 'r'),
|
||||
mock.call('/sys/block/sda/queue/logical_block_size', 'r'),
|
||||
mock.call('/sys/block/sdb/queue/logical_block_size', 'r'),
|
||||
mock.call('/sys/block/dm-7/queue/logical_block_size', 'r')],
|
||||
any_order=True)
|
||||
|
||||
@mock.patch('builtins.open')
|
||||
def test__validate_block_attributes_error(self, open_mock):
|
||||
"""Multipath size is wrong"""
|
||||
open_mock.return_value.read.side_effect = \
|
||||
['500118192\n', '500118192\n', '250059096\n']
|
||||
self.assertRaises(exception.AttributeMismatch,
|
||||
self.linuxscsi._validate_block_attributes,
|
||||
['sda', 'sdb', 'dm-7'])
|
||||
self.assertEqual(3, open_mock.call_count)
|
||||
open_mock.assert_has_calls([mock.call('/sys/block/sda/size', 'r'),
|
||||
mock.call('/sys/block/sdb/size', 'r'),
|
||||
mock.call('/sys/block/dm-7/size', 'r')],
|
||||
any_order=True)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_mpath_sysfs_wwn')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
|
||||
def test__validate_wwn(self, wwn_mock, wwid_mock, fs_wwn_mock):
|
||||
"""Cinder provides the WWN and everything is ok."""
|
||||
cinder_wwn = '3600d0230000000000e13955cc3757800'
|
||||
wwid_mock.return_value = cinder_wwn
|
||||
fs_wwn_mock.return_value = cinder_wwn
|
||||
wwn = self.linuxscsi._validate_wwn(['sda', 'sdb'], 'dm-7', cinder_wwn)
|
||||
self.assertEqual(cinder_wwn, wwn)
|
||||
wwn_mock.assert_not_called()
|
||||
self.assertEqual(2, wwid_mock.call_count)
|
||||
wwid_mock.assert_has_calls([mock.call('sda'), mock.call('sdb')])
|
||||
fs_wwn_mock.assert_called_once_with('dm-7')
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_mpath_sysfs_wwn')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
|
||||
def test__validate_wwn_no_cinder_wwn(self, wwn_mock, wwid_mock,
|
||||
fs_wwn_mock):
|
||||
"""Test with some missing information with single path but ok.
|
||||
|
||||
Cinder doesn't provide WWN, so the code gets it via SCSI.
|
||||
Also one of the devices doesn't have it on sysfs.
|
||||
"""
|
||||
cinder_wwn = '3600d0230000000000e13955cc3757800'
|
||||
wwid_mock.side_effect = [cinder_wwn, '']
|
||||
wwn_mock.side_effect = ['', cinder_wwn]
|
||||
fs_wwn_mock.return_value = cinder_wwn
|
||||
wwn = self.linuxscsi._validate_wwn(['sda', 'sdb'], None, None)
|
||||
self.assertEqual(cinder_wwn, wwn)
|
||||
self.assertEqual(2, wwn_mock.call_count)
|
||||
wwn_mock.assert_has_calls([mock.call('/dev/sda'),
|
||||
mock.call('/dev/sdb')])
|
||||
self.assertEqual(2, wwid_mock.call_count)
|
||||
wwid_mock.assert_has_calls([mock.call('sda'), mock.call('sdb')])
|
||||
fs_wwn_mock.assert_not_called()
|
||||
|
||||
@ddt.data('', '3600d0230000000000e13955cc3757800')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
|
||||
def test__validate_wwn_read_errors(self, wwid, wwn_mock, wwid_mock):
|
||||
"""SCSI command fails and optionally FS read fail"""
|
||||
wwid_mock.return_value = wwid
|
||||
wwn_mock.side_effect = Exception
|
||||
|
||||
self.assertRaises(exception.WWNValidationFail,
|
||||
self.linuxscsi._validate_wwn,
|
||||
['sda', 'sdb'], None, None)
|
||||
|
||||
self.assertEqual(2, wwn_mock.call_count)
|
||||
wwn_mock.assert_has_calls([mock.call('/dev/sda'),
|
||||
mock.call('/dev/sdb')])
|
||||
|
||||
self.assertEqual(2, wwid_mock.call_count)
|
||||
wwid_mock.assert_has_calls([mock.call('sda'), mock.call('sdb')])
|
||||
|
||||
@ddt.data(['3600d0230000000000e13955cc3757801',
|
||||
'3600d0230000000000e13955cc3757801'],
|
||||
['3600d0230000000000e13955cc3757801',
|
||||
'3600d0230000000000e13955cc3757802'])
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_mpath_sysfs_wwn')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
|
||||
def test__validate_wwn_wrong_sysfs(self, fs_wwids, wwn_mock, wwid_mock,
|
||||
fs_wwn_mock):
|
||||
"""Cinder provides the WWN and everything is ok."""
|
||||
cinder_wwn = '3600d0230000000000e13955cc3757800'
|
||||
wwid_mock.side_effect = fs_wwids
|
||||
|
||||
self.assertRaises(exception.WWNValidationFail,
|
||||
self.linuxscsi._validate_wwn,
|
||||
['sda', 'sdb'], 'dm-7', cinder_wwn)
|
||||
|
||||
wwn_mock.assert_not_called()
|
||||
self.assertEqual(2, wwid_mock.call_count)
|
||||
wwid_mock.assert_has_calls([mock.call('sda'), mock.call('sdb')])
|
||||
fs_wwn_mock.assert_not_called()
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_mpath_sysfs_wwn')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwid')
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
|
||||
def test__validate_wwn_fails_mpath(self, wwn_mock, wwid_mock, fs_wwn_mock):
|
||||
"""Wrong WWN in mpath"""
|
||||
cinder_wwn = '3600d0230000000000e13955cc3757800'
|
||||
wwid_mock.return_value = cinder_wwn
|
||||
fs_wwn_mock.return_value = '3600d0230000000000e13955cc3757801'
|
||||
self.assertRaises(exception.WWNMismatch,
|
||||
self.linuxscsi._validate_wwn,
|
||||
['sda', 'sdb'], 'dm-7', cinder_wwn)
|
||||
wwn_mock.assert_not_called()
|
||||
self.assertEqual(2, wwid_mock.call_count)
|
||||
wwid_mock.assert_has_calls([mock.call('sda'), mock.call('sdb')])
|
||||
fs_wwn_mock.assert_called_once_with('dm-7')
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
# Copyright (c) 2023, Red Hat, Inc.
|
||||
# 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 unittest import mock
|
||||
|
||||
import os_brick.privileged as privsep_brick
|
||||
import os_brick.privileged.utils as privsep_utils
|
||||
from os_brick.tests import base
|
||||
|
||||
|
||||
class PrivUtilsTestCase(base.TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# Disable privsep server/client mode
|
||||
privsep_brick.proc_fds.set_client_mode(False)
|
||||
self.addCleanup(privsep_brick.proc_fds.set_client_mode, True)
|
||||
self.glob_data = [
|
||||
'/proc/0/fd/0', '/proc/0/fd/1', '/proc/0/fd/2',
|
||||
'/proc/1/fd/0', '/proc/1/fd/1', '/proc/1/fd/2',
|
||||
'/proc/1/fd/10', '/proc/1/fd/11',
|
||||
'/proc/2/fd/0', '/proc/2/fd/1', '/proc/2/fd/2',
|
||||
'/proc/2/fd/10', '/proc/2/fd/11',
|
||||
]
|
||||
self.mock_glob = self.patch('glob.glob', return_value=self.glob_data)
|
||||
self.path_data = [
|
||||
'/dev/pts/10', '/dev/pts/10', '/dev/pts/10',
|
||||
'/dev/pts/11', '/dev/pts/11', '/dev/pts/11',
|
||||
'/dev/sda', '/dev/dm-7',
|
||||
'/dev/pts/12', '/dev/pts/12', '/dev/pts/12',
|
||||
'/dev/dm-6', '/dev/dm-7',
|
||||
]
|
||||
self.mock_realpath = self.patch('os.path.realpath',
|
||||
side_effect=self.path_data)
|
||||
|
||||
def test_device_is_used_no_match(self):
|
||||
"""Test method tries all symlinks before giving up."""
|
||||
res = privsep_utils.device_is_used('/dev/dm-5')
|
||||
self.assertFalse(res)
|
||||
self.mock_glob.assert_called_once_with('/proc/*/fd/*')
|
||||
self.assertEqual(len(self.glob_data), self.mock_realpath.call_count)
|
||||
self.mock_realpath.assert_has_calls([mock.call(path)
|
||||
for path in self.glob_data])
|
||||
|
||||
def test_device_is_used(self):
|
||||
"""Test method stops search on first found."""
|
||||
res = privsep_utils.device_is_used('/dev/dm-7')
|
||||
self.assertTrue(res)
|
||||
self.mock_glob.assert_called_once_with('/proc/*/fd/*')
|
||||
glob_data = self.glob_data[:-5]
|
||||
self.assertEqual(len(glob_data), self.mock_realpath.call_count)
|
||||
self.mock_realpath.assert_has_calls([mock.call(path)
|
||||
for path in glob_data])
|
||||
|
||||
def test_processes_using_devices(self):
|
||||
res = privsep_utils.processes_using_devices(
|
||||
['/dev/dm-5', '/dev/dm-6', '/dev/dm-7'])
|
||||
self.assertDictEqual({'/dev/dm-5': 0, '/dev/dm-6': 1, '/dev/dm-7': 2},
|
||||
res)
|
||||
self.mock_glob.assert_called_once_with('/proc/*/fd/*')
|
||||
self.assertEqual(len(self.glob_data), self.mock_realpath.call_count)
|
||||
self.mock_realpath.assert_has_calls([mock.call(path)
|
||||
for path in self.glob_data])
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
security:
|
||||
- |
|
||||
As part of the fix for `Bug #2004555
|
||||
<https://bugs.launchpad.net/os-brick/+bug/2004555>`_, the
|
||||
``connect_volume`` method, for iSCSI and FCP, now cleans up leftover
|
||||
devices as best it can at the begining, and it also validates the result
|
||||
of the operation at the end before returning it to the caller.
|
||||
features:
|
||||
- |
|
||||
OS-brick can validate the WWN of iSCSI and FCP volumes for Cinder drivers
|
||||
that provide the volume's WWN in the connection properties ``wwn`` key.
|
||||
All Cinder drivers are encouraged to provide this value in their connection
|
||||
properties to increase robustness and security.
|
||||
upgrade:
|
||||
- |
|
||||
Deployments using iSCSI and FCP storage with multipathing are encouraged
|
||||
to update their ``multipath.conf`` file to add the ``recheck_wwid yes``
|
||||
option to the ``defaults`` section.
|
||||
- |
|
||||
For security reasons the ``connect_volume`` method is no longer guaranteed
|
||||
to be idempotent (it actually never was), and in some transport protocols
|
||||
(eg: iSCSI, FCP) each time it is called it will disconnect any existing
|
||||
volume that matches the provided connection_information first and then
|
||||
reconnect it again to ensure that the right volume will be used and that
|
||||
the metadata present in sysfs (such as the WWID and size) are up to date.
|
Loading…
Reference in New Issue