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:
Gorka Eguileor 2023-02-10 14:06:08 +01:00
parent f724ab722a
commit 2de794d302
12 changed files with 1315 additions and 87 deletions

View File

@ -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]")

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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__,
)

View File

@ -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

View File

@ -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])

View File

@ -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,

View File

@ -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')

View File

@ -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])

View File

@ -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.