46e093bfe6
This patch improves logging for the FC connector and simplifies a couple of list and dict creations using generators. The logging is around the modification that os-brick does of the connection dictionary that Cinder sent. It displays what the contents were on entry and on exit, and it also warns if we are dropping some targets because the Cinder driver returned inconsistent data. Change-Id: I44bfd4939a850e8e080c4f8760244146a75f7218
380 lines
15 KiB
Python
380 lines
15 KiB
Python
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import os
|
|
|
|
from oslo_concurrency import lockutils
|
|
from oslo_log import log as logging
|
|
from oslo_service import loopingcall
|
|
import six
|
|
|
|
from os_brick import exception
|
|
from os_brick.i18n import _
|
|
from os_brick import initiator
|
|
from os_brick.initiator.connectors import base
|
|
from os_brick.initiator import linuxfc
|
|
from os_brick import utils
|
|
|
|
synchronized = lockutils.synchronized_with_prefix('os-brick-')
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class FibreChannelConnector(base.BaseLinuxConnector):
|
|
"""Connector class to attach/detach Fibre Channel volumes."""
|
|
|
|
def __init__(self, root_helper, driver=None,
|
|
execute=None, use_multipath=False,
|
|
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
|
|
*args, **kwargs):
|
|
self._linuxfc = linuxfc.LinuxFibreChannel(root_helper, execute)
|
|
super(FibreChannelConnector, self).__init__(
|
|
root_helper, driver=driver,
|
|
execute=execute,
|
|
device_scan_attempts=device_scan_attempts,
|
|
*args, **kwargs)
|
|
self.use_multipath = use_multipath
|
|
|
|
def set_execute(self, execute):
|
|
super(FibreChannelConnector, self).set_execute(execute)
|
|
self._linuxscsi.set_execute(execute)
|
|
self._linuxfc.set_execute(execute)
|
|
|
|
@staticmethod
|
|
def get_connector_properties(root_helper, *args, **kwargs):
|
|
"""The Fibre Channel connector properties."""
|
|
props = {}
|
|
fc = linuxfc.LinuxFibreChannel(root_helper,
|
|
execute=kwargs.get('execute'))
|
|
|
|
wwpns = fc.get_fc_wwpns()
|
|
if wwpns:
|
|
props['wwpns'] = wwpns
|
|
wwnns = fc.get_fc_wwnns()
|
|
if wwnns:
|
|
props['wwnns'] = wwnns
|
|
|
|
return props
|
|
|
|
def get_search_path(self):
|
|
"""Where do we look for FC based volumes."""
|
|
return '/dev/disk/by-path'
|
|
|
|
def _add_targets_to_connection_properties(self, connection_properties):
|
|
LOG.debug('Adding targets to connection properties receives: %s',
|
|
connection_properties)
|
|
target_wwn = connection_properties.get('target_wwn')
|
|
target_wwns = connection_properties.get('target_wwns')
|
|
if target_wwns:
|
|
wwns = target_wwns
|
|
elif isinstance(target_wwn, list):
|
|
wwns = target_wwn
|
|
elif isinstance(target_wwn, six.string_types):
|
|
wwns = [target_wwn]
|
|
else:
|
|
wwns = []
|
|
|
|
# Convert wwns to lower case
|
|
wwns = [wwn.lower() for wwn in wwns]
|
|
if target_wwns:
|
|
connection_properties['target_wwns'] = wwns
|
|
elif target_wwn:
|
|
connection_properties['target_wwn'] = wwns
|
|
|
|
target_lun = connection_properties.get('target_lun', 0)
|
|
target_luns = connection_properties.get('target_luns')
|
|
if target_luns:
|
|
luns = target_luns
|
|
elif isinstance(target_lun, int):
|
|
luns = [target_lun]
|
|
else:
|
|
luns = []
|
|
|
|
if len(luns) == len(wwns):
|
|
# Handles single wwwn + lun or multiple, potentially
|
|
# different wwns or luns
|
|
targets = list(zip(wwns, luns))
|
|
elif len(luns) == 1 and len(wwns) > 1:
|
|
# For the case of multiple wwns, but a single lun (old path)
|
|
targets = [(wwn, luns[0]) for wwn in wwns]
|
|
else:
|
|
# Something is wrong, this shouldn't happen.
|
|
msg = _("Unable to find potential volume paths for FC device "
|
|
"with luns: %(luns)s and wwns: %(wwns)s.") % {
|
|
"luns": luns, "wwns": wwns}
|
|
LOG.error(msg)
|
|
raise exception.VolumePathsNotFound(msg)
|
|
|
|
connection_properties['targets'] = targets
|
|
wwpn_lun_map = {wwpn: lun for wwpn, lun in targets}
|
|
|
|
# If there is an initiator_target_map we can update it too and generate
|
|
# the initiator_target_lun_map from it
|
|
if connection_properties.get('initiator_target_map') is not None:
|
|
# Convert it to lower case
|
|
itmap = connection_properties['initiator_target_map']
|
|
itmap = {k.lower(): [port.lower() for port in v]
|
|
for k, v in itmap.items()}
|
|
connection_properties['initiator_target_map'] = itmap
|
|
|
|
itmaplun = dict()
|
|
for init_wwpn, target_wwpns in itmap.items():
|
|
itmaplun[init_wwpn] = [(target_wwpn, wwpn_lun_map[target_wwpn])
|
|
for target_wwpn in target_wwpns
|
|
if target_wwpn in wwpn_lun_map]
|
|
|
|
# We added the if in the previous list comprehension in case
|
|
# drivers return targets in the map that are not reported in
|
|
# target_wwn or target_wwns, but we warn about it.
|
|
if len(itmaplun[init_wwpn]) != len(itmap[init_wwpn]):
|
|
unknown = set(itmap[init_wwpn])
|
|
unknown.difference_update(itmaplun[init_wwpn])
|
|
LOG.warning('Driver returned an unknown targets in the '
|
|
'initiator mapping %s', ', '.join(unknown))
|
|
connection_properties['initiator_target_lun_map'] = itmaplun
|
|
|
|
LOG.debug('Adding targets to connection properties returns: %s',
|
|
connection_properties)
|
|
return connection_properties
|
|
|
|
def _get_possible_volume_paths(self, connection_properties, hbas):
|
|
targets = connection_properties['targets']
|
|
possible_devs = self._get_possible_devices(hbas, targets)
|
|
host_paths = self._get_host_devices(possible_devs)
|
|
return host_paths
|
|
|
|
def get_volume_paths(self, connection_properties):
|
|
volume_paths = []
|
|
# first fetch all of the potential paths that might exist
|
|
# how the FC fabric is zoned may alter the actual list
|
|
# that shows up on the system. So, we verify each path.
|
|
hbas = self._linuxfc.get_fc_hbas_info()
|
|
device_paths = self._get_possible_volume_paths(
|
|
connection_properties, hbas)
|
|
for path in device_paths:
|
|
if os.path.exists(path):
|
|
volume_paths.append(path)
|
|
|
|
return volume_paths
|
|
|
|
@utils.trace
|
|
@synchronized('extend_volume')
|
|
def extend_volume(self, connection_properties):
|
|
"""Update the local kernel's size information.
|
|
|
|
Try and update the local kernel's size information
|
|
for an FC volume.
|
|
"""
|
|
connection_properties = self._add_targets_to_connection_properties(
|
|
connection_properties)
|
|
|
|
volume_paths = self.get_volume_paths(connection_properties)
|
|
if volume_paths:
|
|
return self._linuxscsi.extend_volume(
|
|
volume_paths, use_multipath=self.use_multipath)
|
|
else:
|
|
LOG.warning("Couldn't find any volume paths on the host to "
|
|
"extend volume for %(props)s",
|
|
{'props': connection_properties})
|
|
raise exception.VolumePathsNotFound()
|
|
|
|
@utils.trace
|
|
@synchronized('connect_volume')
|
|
def connect_volume(self, connection_properties):
|
|
"""Attach the volume to instance_name.
|
|
|
|
:param connection_properties: The dictionary that describes all
|
|
of the target volume attributes.
|
|
:type connection_properties: dict
|
|
:returns: dict
|
|
|
|
connection_properties for Fibre Channel must include:
|
|
target_wwn - World Wide Name
|
|
target_lun - LUN id of the volume
|
|
"""
|
|
device_info = {'type': 'block'}
|
|
|
|
connection_properties = self._add_targets_to_connection_properties(
|
|
connection_properties)
|
|
|
|
hbas = self._linuxfc.get_fc_hbas_info()
|
|
if not hbas:
|
|
LOG.warning("We are unable to locate any Fibre Channel devices.")
|
|
raise exception.NoFibreChannelHostsFound()
|
|
|
|
host_devices = self._get_possible_volume_paths(
|
|
connection_properties, hbas)
|
|
|
|
# The /dev/disk/by-path/... node is not always present immediately
|
|
# We only need to find the first device. Once we see the first device
|
|
# multipath will have any others.
|
|
def _wait_for_device_discovery(host_devices):
|
|
for device in host_devices:
|
|
LOG.debug("Looking for Fibre Channel dev %(device)s",
|
|
{'device': device})
|
|
if os.path.exists(device) and self.check_valid_device(device):
|
|
self.host_device = device
|
|
# get the /dev/sdX device. This is used
|
|
# to find the multipath device.
|
|
self.device_name = os.path.realpath(device)
|
|
raise loopingcall.LoopingCallDone()
|
|
|
|
if self.tries >= self.device_scan_attempts:
|
|
LOG.error("Fibre Channel volume device not found.")
|
|
raise exception.NoFibreChannelVolumeDeviceFound()
|
|
|
|
LOG.info("Fibre Channel volume device not yet found. "
|
|
"Will rescan & retry. Try number: %(tries)s.",
|
|
{'tries': self.tries})
|
|
|
|
self._linuxfc.rescan_hosts(hbas, connection_properties)
|
|
self.tries = self.tries + 1
|
|
|
|
self.host_device = None
|
|
self.device_name = None
|
|
self.tries = 0
|
|
timer = loopingcall.FixedIntervalLoopingCall(
|
|
_wait_for_device_discovery, host_devices)
|
|
timer.start(interval=2).wait()
|
|
|
|
LOG.debug("Found Fibre Channel volume %(name)s "
|
|
"(after %(tries)s rescans.)",
|
|
{'name': self.device_name, 'tries': self.tries})
|
|
|
|
# find out the WWN of the device
|
|
device_wwn = self._linuxscsi.get_scsi_wwn(self.host_device)
|
|
LOG.debug("Device WWN = '%(wwn)s'", {'wwn': device_wwn})
|
|
device_info['scsi_wwn'] = device_wwn
|
|
|
|
# see if the new drive is part of a multipath
|
|
# device. If so, we'll use the multipath device.
|
|
if self.use_multipath:
|
|
(device_path, multipath_id) = self._discover_mpath_device(
|
|
device_wwn, connection_properties, self.device_name)
|
|
if multipath_id:
|
|
# only set the multipath_id if we found one
|
|
device_info['multipath_id'] = multipath_id
|
|
|
|
else:
|
|
device_path = self.host_device
|
|
|
|
device_info['path'] = device_path
|
|
return device_info
|
|
|
|
def _get_host_devices(self, possible_devs):
|
|
"""Compute the device paths on the system with an id, wwn, and lun
|
|
|
|
:param possible_devs: list of (pci_id, wwn, lun) tuples
|
|
:return: list of device paths on the system based on the possible_devs
|
|
"""
|
|
host_devices = []
|
|
for pci_num, target_wwn, lun in possible_devs:
|
|
host_device = "/dev/disk/by-path/pci-%s-fc-%s-lun-%s" % (
|
|
pci_num,
|
|
target_wwn,
|
|
self._linuxscsi.process_lun_id(lun))
|
|
host_devices.append(host_device)
|
|
return host_devices
|
|
|
|
def _get_possible_devices(self, hbas, targets):
|
|
"""Compute the possible fibre channel device options.
|
|
|
|
:param hbas: available hba devices.
|
|
:param targets: tuple of possible wwn addresses and lun combinations.
|
|
|
|
:returns: list of (pci_id, wwn, lun) tuples
|
|
|
|
Given one or more wwn (mac addresses for fibre channel) ports
|
|
do the matrix math to figure out a set of pci device, wwn
|
|
tuples that are potentially valid (they won't all be). This
|
|
provides a search space for the device connection.
|
|
|
|
"""
|
|
raw_devices = []
|
|
for hba in hbas:
|
|
pci_num = self._get_pci_num(hba)
|
|
if pci_num is not None:
|
|
for wwn, lun in targets:
|
|
target_wwn = "0x%s" % wwn.lower()
|
|
raw_devices.append((pci_num, target_wwn, lun))
|
|
return raw_devices
|
|
|
|
@utils.trace
|
|
@synchronized('connect_volume')
|
|
def disconnect_volume(self, connection_properties, device_info,
|
|
force=False, ignore_errors=False):
|
|
"""Detach the volume from instance_name.
|
|
|
|
:param connection_properties: The dictionary that describes all
|
|
of the target volume attributes.
|
|
:type connection_properties: dict
|
|
:param device_info: historical difference, but same as connection_props
|
|
:type device_info: dict
|
|
|
|
connection_properties for Fibre Channel must include:
|
|
target_wwn - World Wide Name
|
|
target_lun - LUN id of the volume
|
|
"""
|
|
|
|
devices = []
|
|
wwn = None
|
|
|
|
connection_properties = self._add_targets_to_connection_properties(
|
|
connection_properties)
|
|
|
|
volume_paths = self.get_volume_paths(connection_properties)
|
|
mpath_path = None
|
|
for path in volume_paths:
|
|
real_path = self._linuxscsi.get_name_from_path(path)
|
|
if (self.use_multipath and not mpath_path
|
|
and self.check_valid_device(path)):
|
|
wwn = self._linuxscsi.get_scsi_wwn(path)
|
|
mpath_path = self._linuxscsi.find_multipath_device_path(wwn)
|
|
if mpath_path:
|
|
self._linuxscsi.flush_multipath_device(mpath_path)
|
|
device_info = self._linuxscsi.get_device_info(real_path)
|
|
devices.append(device_info)
|
|
|
|
LOG.debug("devices to remove = %s", devices)
|
|
self._remove_devices(connection_properties, devices, device_info)
|
|
|
|
def _remove_devices(self, connection_properties, devices, device_info):
|
|
# There may have been more than 1 device mounted
|
|
# by the kernel for this volume. We have to remove
|
|
# all of them
|
|
path_used = self._linuxscsi.get_dev_path(connection_properties,
|
|
device_info)
|
|
was_multipath = '/pci-' not in path_used
|
|
for device in devices:
|
|
device_path = device['device']
|
|
flush = self._linuxscsi.requires_flush(device_path,
|
|
path_used,
|
|
was_multipath)
|
|
self._linuxscsi.remove_scsi_device(device_path, flush=flush)
|
|
|
|
def _get_pci_num(self, hba):
|
|
# NOTE(walter-boring)
|
|
# device path is in format of (FC and FCoE) :
|
|
# /sys/devices/pci0000:00/0000:00:03.0/0000:05:00.3/host2/fc_host/host2
|
|
# /sys/devices/pci0000:20/0000:20:03.0/0000:21:00.2/net/ens2f2/ctlr_2
|
|
# /host3/fc_host/host3
|
|
# we always want the value prior to the host or net value
|
|
if hba is not None:
|
|
if "device_path" in hba:
|
|
device_path = hba['device_path'].split('/')
|
|
for index, value in enumerate(device_path):
|
|
if value.startswith('net') or value.startswith('host'):
|
|
return device_path[index - 1]
|
|
return None
|