# 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): 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 = [] 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 = [] for wwn in wwns: targets.append((wwn, luns[0])) else: # Something is wrong, this shouldn't happen msg = _("Unable to find potential volume paths for FC device " "with lun: %(lun)s and wwn: %(wwn)s") % { "lun": target_lun, "wwn": target_wwn} LOG.error(msg) raise exception.VolumePathsNotFound(msg) connection_properties['targets'] = targets wwpn_lun_map = dict() for wwpn, lun in targets: wwpn_lun_map[wwpn] = lun # If there is an initiator_target_map we can update it too if 'initiator_target_map' in connection_properties: itmap = connection_properties['initiator_target_map'] new_itmap = dict() for init_wwpn in itmap: target_wwpns = itmap[init_wwpn] init_targets = [] for target_wwpn in target_wwpns: if target_wwpn in wwpn_lun_map: init_targets.append((target_wwpn, wwpn_lun_map[target_wwpn])) new_itmap[init_wwpn] = init_targets connection_properties['initiator_target_lun_map'] = new_itmap 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) 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 """ LOG.debug("execute = %s", self._execute) device_info = {'type': 'block'} connection_properties = self._add_targets_to_connection_properties( connection_properties) hbas = self._linuxfc.get_fc_hbas_info() host_devices = self._get_possible_volume_paths( connection_properties, hbas) if len(host_devices) == 0: # this is empty because we don't have any FC HBAs LOG.warning("We are unable to locate any Fibre Channel devices") raise exception.NoFibreChannelHostsFound() # The /dev/disk/by-path/... node is not always present immediately # We only need to find the first device. Once we see the first device # multipath will have any others. def _wait_for_device_discovery(host_devices): 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() if self.host_device is not None and self.device_name is not None: LOG.debug("Found Fibre Channel volume %(name)s " "(after %(tries)s rescans)", {'name': self.device_name, 'tries': 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) = (super( FibreChannelConnector, self)._discover_mpath_device( device_wwn, connection_properties, self.device_name)) if multipath_id: # only set the multipath_id if we found one device_info['multipath_id'] = multipath_id else: device_path = self.host_device device_info['path'] = device_path LOG.debug("connect_volume returning %s", device_info) return device_info def _get_host_devices(self, possible_devs): """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: 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