375 lines
15 KiB
Python
375 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):
|
|
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 = []
|
|
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 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 = dict()
|
|
for wwpn, lun in targets:
|
|
wwpn_lun_map[wwpn] = lun
|
|
|
|
# If there is an initiator_target_map we can update it too
|
|
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
|
|
|
|
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, 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
|