Add Brick Fibre Channel attach/detach support.

This patch adds the required code to do
Fibre Channel attach and detaches of volumes.
This code has been pulled over from Nova's
implementation of FC attach/detach.

Also adds a new driver config entry to enable
multipath support for iSCSI and FC attaches
during volume to image and image
to volume transfers.

DocImpact

blueprint cinder-refactor-attach

Change-Id: I436592f958a6c14cd2a0b5d7e53362dd1a7c1a48
This commit is contained in:
Walter A. Boring IV
2013-07-10 15:22:06 -07:00
parent 5be685218b
commit cb6faab4d9
9 changed files with 866 additions and 200 deletions

View File

@@ -17,8 +17,10 @@
import executor
import host_driver
import linuxfc
import linuxscsi
import os
import socket
import time
from oslo.config import cfg
@@ -27,6 +29,7 @@ from cinder import exception
from cinder.openstack.common.gettextutils import _
from cinder.openstack.common import lockutils
from cinder.openstack.common import log as logging
from cinder.openstack.common import loopingcall
from cinder.openstack.common import processutils as putils
LOG = logging.getLogger(__name__)
@@ -34,6 +37,21 @@ CONF = cfg.CONF
synchronized = lockutils.synchronized_with_prefix('brick-')
def get_connector_properties():
"""Get the connection properties for all protocols."""
iscsi = ISCSIConnector()
fc = linuxfc.LinuxFibreChannel()
props = {}
props['ip'] = CONF.my_ip
props['host'] = socket.gethostname()
props['initiator'] = iscsi.get_initiator()
props['wwpns'] = fc.get_fc_wwpns()
return props
class InitiatorConnector(executor.Executor):
def __init__(self, driver=None, execute=putils.execute,
root_helper="sudo", *args, **kwargs):
@@ -43,17 +61,61 @@ class InitiatorConnector(executor.Executor):
driver = host_driver.HostDriver()
self.set_driver(driver)
self._linuxscsi = linuxscsi.LinuxSCSI(execute, root_helper)
def set_driver(self, driver):
"""The driver used to find used LUNs."""
"""The driver is used to find used LUNs."""
self.driver = driver
@staticmethod
def factory(protocol, execute=putils.execute,
root_helper="sudo", use_multipath=False):
"""Build a Connector object based upon protocol."""
LOG.debug("Factory for %s" % protocol)
protocol = protocol.upper()
if protocol == "ISCSI":
return ISCSIConnector(execute=execute,
root_helper=root_helper,
use_multipath=use_multipath)
elif protocol == "FIBRE_CHANNEL":
return FibreChannelConnector(execute=execute,
root_helper=root_helper,
use_multipath=use_multipath)
else:
msg = (_("Invalid InitiatorConnector protocol "
"specified %(protocol)s") %
dict(protocol=protocol))
raise ValueError(msg)
def check_valid_device(self, path):
cmd = ('dd', 'if=%(path)s' % {"path": path},
'of=/dev/null', 'count=1')
out, info = None, None
try:
out, info = self._execute(*cmd, run_as_root=True,
root_helper=self._root_helper)
except exception.ProcessExecutionError as e:
LOG.error(_("Failed to access the device on the path "
"%(path)s: %(error)s %(info)s.") %
{"path": path, "error": e.stderr,
"info": info})
return False
# If the info is none, the path does not exist.
if info is None:
return False
return True
def connect_volume(self, connection_properties):
"""Connect to a volume. The connection_properties
describes the information needed by the specific
protocol to use to make the connection.
"""
raise NotImplementedError()
def disconnect_volume(self, connection_properties):
def disconnect_volume(self, connection_properties, device_info):
"""Disconnect a volume from the local host.
The connection_properties are the same as from connect_volume.
The device_info is returned from connect_volume.
"""
raise NotImplementedError()
@@ -66,6 +128,7 @@ class ISCSIConnector(InitiatorConnector):
super(ISCSIConnector, self).__init__(driver, execute, root_helper,
*args, **kwargs)
self.use_multipath = use_multipath
self._linuxscsi = linuxscsi.LinuxSCSI(execute, root_helper)
@synchronized('connect_volume')
def connect_volume(self, connection_properties):
@@ -138,7 +201,7 @@ class ISCSIConnector(InitiatorConnector):
return device_info
@synchronized('connect_volume')
def disconnect_volume(self, connection_properties):
def disconnect_volume(self, connection_properties, device_info):
"""Detach the volume from instance_name.
connection_properties for iSCSI must include:
@@ -179,6 +242,19 @@ class ISCSIConnector(InitiatorConnector):
'lun': connection_properties.get('target_lun', 0)})
return path
def get_initiator(self):
"""Secure helper to read file as root."""
try:
file_path = '/etc/iscsi/initiatorname.iscsi'
lines, _err = self._execute('cat', file_path, run_as_root=True,
root_helper=self._root_helper)
for l in lines.split('\n'):
if l.startswith('InitiatorName='):
return l[l.index('=') + 1:].strip()
except exception.ProcessExecutionError:
raise exception.FileNotFound(file_path=file_path)
def _run_iscsiadm(self, connection_properties, iscsi_command, **kwargs):
check_exit_code = kwargs.pop('check_exit_code', 0)
(out, err) = self._execute('iscsiadm', '-m', 'node', '-T',
@@ -373,3 +449,169 @@ class ISCSIConnector(InitiatorConnector):
def _rescan_multipath(self):
self._run_multipath('-r', check_exit_code=[0, 1, 21])
class FibreChannelConnector(InitiatorConnector):
""""Connector class to attach/detach Fibre Channel volumes."""
def __init__(self, driver=None, execute=putils.execute,
root_helper="sudo", use_multipath=False,
*args, **kwargs):
super(FibreChannelConnector, self).__init__(driver, execute,
root_helper,
*args, **kwargs)
self.use_multipath = use_multipath
self._linuxscsi = linuxscsi.LinuxSCSI(execute, root_helper)
self._linuxfc = linuxfc.LinuxFibreChannel(execute, root_helper)
@synchronized('connect_volume')
def connect_volume(self, connection_properties):
"""Attach the volume to instance_name.
connection_properties for Fibre Channel must include:
target_portal - ip and optional port
target_iqn - iSCSI Qualified Name
target_lun - LUN id of the volume
"""
LOG.debug("execute = %s" % self._execute)
device_info = {'type': 'block'}
ports = connection_properties['target_wwn']
wwns = []
# we support a list of wwns or a single wwn
if isinstance(ports, list):
for wwn in ports:
wwns.append(wwn)
elif isinstance(ports, str):
wwns.append(ports)
# We need to look for wwns on every hba
# because we don't know ahead of time
# where they will show up.
hbas = self._linuxfc.get_fc_hbas_info()
host_devices = []
for hba in hbas:
pci_num = self._get_pci_num(hba)
if pci_num is not None:
for wwn in wwns:
target_wwn = "0x%s" % wwn.lower()
host_device = ("/dev/disk/by-path/pci-%s-fc-%s-lun-%s" %
(pci_num,
target_wwn,
connection_properties.get('target_lun', 0)))
host_devices.append(host_device)
if len(host_devices) == 0:
# this is empty because we don't have any FC HBAs
msg = _("We are unable to locate any Fibre Channel devices")
raise exception.CinderException(msg)
# 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):
tries = self.tries
for device in host_devices:
LOG.debug(_("Looking for Fibre Channel dev %(device)s"),
{'device': device})
if os.path.exists(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 >= CONF.num_iscsi_scan_tries:
msg = _("Fibre Channel device not found.")
raise exception.CinderException(msg)
LOG.warn(_("Fibre volume not yet found. "
"Will rescan & retry. Try number: %(tries)s"),
{'tries': tries})
self._linuxfc.rescan_hosts(hbas)
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()
tries = self.tries
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': tries})
# see if the new drive is part of a multipath
# device. If so, we'll use the multipath device.
if self.use_multipath:
mdev_info = self._linuxscsi.find_multipath_device(self.device_name)
if mdev_info is not None:
LOG.debug(_("Multipath device discovered %(device)s")
% {'device': mdev_info['device']})
device_path = mdev_info['device']
devices = mdev_info['devices']
device_info['multipath_id'] = mdev_info['id']
else:
# we didn't find a multipath device.
# so we assume the kernel only sees 1 device
device_path = self.host_device
dev_info = self._linuxscsi.get_device_info(self.device_name)
devices = [dev_info]
else:
device_path = self.host_device
dev_info = self._linuxscsi.get_device_info(self.device_name)
devices = [dev_info]
device_info['path'] = device_path
device_info['devices'] = devices
return device_info
@synchronized('connect_volume')
def disconnect_volume(self, connection_properties, device_info):
"""Detach the volume from instance_name.
connection_properties for Fibre Channel must include:
target_wwn - iSCSI Qualified Name
target_lun - LUN id of the volume
"""
devices = device_info['devices']
# If this is a multipath device, we need to search again
# and make sure we remove all the devices. Some of them
# might not have shown up at attach time.
if self.use_multipath and 'multipath_id' in device_info:
multipath_id = device_info['multipath_id']
mdev_info = self._linuxscsi.find_multipath_device(multipath_id)
devices = mdev_info['devices']
LOG.debug("devices to remove = %s" % devices)
# There may have been more than 1 device mounted
# by the kernel for this volume. We have to remove
# all of them
for device in devices:
self._linuxscsi.remove_scsi_device(device["device"])
def _get_pci_num(self, hba):
# NOTE(walter-boring)
# device path is in format of
# /sys/devices/pci0000:00/0000:00:03.0/0000:05:00.3/host2/fc_host/host2
# sometimes an extra entry exists before the host2 value
# we always want the value prior to the host2 value
pci_num = None
if hba is not None:
if "device_path" in hba:
index = 0
device_path = hba['device_path'].split('/')
for value in device_path:
if value.startswith('host'):
break
index = index + 1
if index > 0:
pci_num = device_path[index - 1]
return pci_num

View File

@@ -0,0 +1,136 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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.
"""Generic linux Fibre Channel utilities."""
import errno
import executor
import linuxscsi
from cinder.openstack.common.gettextutils import _
from cinder.openstack.common import log as logging
from cinder.openstack.common import processutils as putils
LOG = logging.getLogger(__name__)
class LinuxFibreChannel(linuxscsi.LinuxSCSI):
def __init__(self, execute=putils.execute, root_helper="sudo",
*args, **kwargs):
super(LinuxFibreChannel, self).__init__(execute, root_helper,
*args, **kwargs)
def rescan_hosts(self, hbas):
for hba in hbas:
self.echo_scsi_command("/sys/class/scsi_host/%s/scan"
% hba['host_device'], "- - -")
def get_fc_hbas(self):
"""Get the Fibre Channel HBA information."""
out = None
try:
out, err = self._execute('systool', '-c', 'fc_host', '-v',
run_as_root=True,
root_helper=self._root_helper)
except putils.ProcessExecutionError as exc:
# This handles the case where rootwrap is used
# and systool is not installed
# 96 = nova.cmd.rootwrap.RC_NOEXECFOUND:
if exc.exit_code == 96:
LOG.warn(_("systool is not installed"))
return []
except OSError as exc:
# This handles the case where rootwrap is NOT used
# and systool is not installed
if exc.errno == errno.ENOENT:
LOG.warn(_("systool is not installed"))
return []
if out is None:
raise RuntimeError(_("Cannot find any Fibre Channel HBAs"))
lines = out.split('\n')
# ignore the first 2 lines
lines = lines[2:]
hbas = []
hba = {}
lastline = None
for line in lines:
line = line.strip()
# 2 newlines denotes a new hba port
if line == '' and lastline == '':
if len(hba) > 0:
hbas.append(hba)
hba = {}
else:
val = line.split('=')
if len(val) == 2:
key = val[0].strip().replace(" ", "")
value = val[1].strip()
hba[key] = value.replace('"', '')
lastline = line
return hbas
def get_fc_hbas_info(self):
"""Get Fibre Channel WWNs and device paths from the system, if any."""
# Note(walter-boring) modern linux kernels contain the FC HBA's in /sys
# and are obtainable via the systool app
hbas = self.get_fc_hbas()
hbas_info = []
for hba in hbas:
wwpn = hba['port_name'].replace('0x', '')
wwnn = hba['node_name'].replace('0x', '')
device_path = hba['ClassDevicepath']
device = hba['ClassDevice']
hbas_info.append({'port_name': wwpn,
'node_name': wwnn,
'host_device': device,
'device_path': device_path})
return hbas_info
def get_fc_wwpns(self):
"""Get Fibre Channel WWPNs from the system, if any."""
# Note(walter-boring) modern linux kernels contain the FC HBA's in /sys
# and are obtainable via the systool app
hbas = self.get_fc_hbas()
wwpns = []
if hbas:
for hba in hbas:
if hba['port_state'] == 'Online':
wwpn = hba['port_name'].replace('0x', '')
wwpns.append(wwpn)
return wwpns
def get_fc_wwnns(self):
"""Get Fibre Channel WWNNs from the system, if any."""
# Note(walter-boring) modern linux kernels contain the FC HBA's in /sys
# and are obtainable via the systool app
hbas = self.get_fc_hbas()
wwnns = []
if hbas:
for hba in hbas:
if hba['port_state'] == 'Online':
wwnn = hba['node_name'].replace('0x', '')
wwnns.append(wwnn)
return wwnns

View File

@@ -24,6 +24,7 @@ import os
from cinder.openstack.common.gettextutils import _
from cinder.openstack.common import log as logging
from cinder.openstack.common import loopingcall
from cinder.openstack.common import processutils as putils
LOG = logging.getLogger(__name__)
@@ -61,6 +62,25 @@ class LinuxSCSI(executor.Executor):
LOG.debug("Remove SCSI device(%s) with %s" % (device, path))
self.echo_scsi_command(path, "1")
def get_device_info(self, device):
(out, err) = self._execute('sg_scan', device, run_as_root=True,
root_helper=self._root_helper)
dev_info = {'device': device, 'host': None,
'channel': None, 'id': None, 'lun': None}
if out:
line = out.strip()
line = line.replace(device + ": ", "")
info = line.split(" ")
for item in info:
if '=' in item:
pair = item.split('=')
dev_info[pair[0]] = pair[1]
elif 'scsi' in item:
dev_info['host'] = item.replace('scsi', '')
return dev_info
def remove_multipath_device(self, multipath_name):
"""This removes LUNs associated with a multipath device
and the multipath device itself.
@@ -104,7 +124,6 @@ class LinuxSCSI(executor.Executor):
(out, err) = self._execute('multipath', '-l', device,
run_as_root=True,
root_helper=self._root_helper)
LOG.error("PISS = %s" % out)
except putils.ProcessExecutionError as exc:
LOG.warn(_("multipath call failed exit (%(code)s)")
% {'code': exc.exit_code})

View File

@@ -0,0 +1,231 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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.path
import string
from cinder.brick.initiator import connector
from cinder.brick.initiator import host_driver
from cinder.brick.initiator import linuxfc
from cinder.brick.initiator import linuxscsi
from cinder import exception
from cinder.openstack.common import log as logging
from cinder import test
LOG = logging.getLogger(__name__)
class ConnectorTestCase(test.TestCase):
def setUp(self):
super(ConnectorTestCase, self).setUp()
self.cmds = []
self.stubs.Set(os.path, 'exists', lambda x: True)
def fake_execute(self, *cmd, **kwargs):
self.cmds.append(string.join(cmd))
return "", None
def test_connect_volume(self):
self.connector = connector.InitiatorConnector()
self.assertRaises(NotImplementedError,
self.connector.connect_volume, None)
def test_disconnect_volume(self):
self.connector = connector.InitiatorConnector()
self.assertRaises(NotImplementedError,
self.connector.connect_volume, None)
def test_factory(self):
obj = connector.InitiatorConnector.factory('iscsi')
self.assertTrue(obj.__class__.__name__,
"ISCSIConnector")
obj = connector.InitiatorConnector.factory('fibre_channel')
self.assertTrue(obj.__class__.__name__,
"FibreChannelConnector")
self.assertRaises(ValueError,
connector.InitiatorConnector.factory,
"bogus")
class HostDriverTestCase(test.TestCase):
def setUp(self):
super(HostDriverTestCase, self).setUp()
self.devlist = ['device1', 'device2']
self.stubs.Set(os, 'listdir', lambda x: self.devlist)
def test_host_driver(self):
expected = ['/dev/disk/by-path/' + dev for dev in self.devlist]
driver = host_driver.HostDriver()
actual = driver.get_all_block_devices()
self.assertEquals(expected, actual)
class ISCSIConnectorTestCase(ConnectorTestCase):
def setUp(self):
super(ISCSIConnectorTestCase, self).setUp()
self.connector = connector.ISCSIConnector(execute=self.fake_execute,
use_multipath=False)
self.stubs.Set(self.connector._linuxscsi,
'get_name_from_path', lambda x: "/dev/sdb")
def tearDown(self):
super(ISCSIConnectorTestCase, self).tearDown()
def iscsi_connection(self, volume, location, iqn):
return {
'driver_volume_type': 'iscsi',
'data': {
'volume_id': volume['id'],
'target_portal': location,
'target_iqn': iqn,
'target_lun': 1,
}
}
@test.testtools.skipUnless(os.path.exists('/dev/disk/by-path'),
'Test requires /dev/disk/by-path')
def test_connect_volume(self):
self.stubs.Set(os.path, 'exists', lambda x: True)
location = '10.0.2.15:3260'
name = 'volume-00000001'
iqn = 'iqn.2010-10.org.openstack:%s' % name
vol = {'id': 1, 'name': name}
connection_info = self.iscsi_connection(vol, location, iqn)
device = self.connector.connect_volume(connection_info['data'])
dev_str = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location, iqn)
self.assertEquals(device['type'], 'block')
self.assertEquals(device['path'], dev_str)
self.connector.disconnect_volume(connection_info['data'], device)
expected_commands = [('iscsiadm -m node -T %s -p %s' %
(iqn, location)),
('iscsiadm -m session'),
('iscsiadm -m node -T %s -p %s --login' %
(iqn, location)),
('iscsiadm -m node -T %s -p %s --op update'
' -n node.startup -v automatic' % (iqn,
location)),
('tee -a /sys/block/sdb/device/delete'),
('iscsiadm -m node -T %s -p %s --op update'
' -n node.startup -v manual' % (iqn, location)),
('iscsiadm -m node -T %s -p %s --logout' %
(iqn, location)),
('iscsiadm -m node -T %s -p %s --op delete' %
(iqn, location)), ]
LOG.debug("self.cmds = %s" % self.cmds)
LOG.debug("expected = %s" % expected_commands)
self.assertEqual(expected_commands, self.cmds)
class FibreChannelConnectorTestCase(ConnectorTestCase):
def setUp(self):
super(FibreChannelConnectorTestCase, self).setUp()
self.connector = connector.FibreChannelConnector(
execute=self.fake_execute, use_multipath=False)
self.assertIsNotNone(self.connector)
self.assertIsNotNone(self.connector._linuxfc)
self.assertIsNotNone(self.connector._linuxscsi)
def fake_get_fc_hbas(self):
return [{'ClassDevice': 'host1',
'ClassDevicePath': '/sys/devices/pci0000:00/0000:00:03.0'
'/0000:05:00.2/host1/fc_host/host1',
'dev_loss_tmo': '30',
'fabric_name': '0x1000000533f55566',
'issue_lip': '<store method only>',
'max_npiv_vports': '255',
'maxframe_size': '2048 bytes',
'node_name': '0x200010604b019419',
'npiv_vports_inuse': '0',
'port_id': '0x680409',
'port_name': '0x100010604b019419',
'port_state': 'Online',
'port_type': 'NPort (fabric via point-to-point)',
'speed': '10 Gbit',
'supported_classes': 'Class 3',
'supported_speeds': '10 Gbit',
'symbolic_name': 'Emulex 554M FV4.0.493.0 DV8.3.27',
'tgtid_bind_type': 'wwpn (World Wide Port Name)',
'uevent': None,
'vport_create': '<store method only>',
'vport_delete': '<store method only>'}]
def fake_get_fc_hbas_info(self):
hbas = self.fake_get_fc_hbas()
info = [{'port_name': hbas[0]['port_name'].replace('0x', ''),
'node_name': hbas[0]['node_name'].replace('0x', ''),
'host_device': hbas[0]['ClassDevice'],
'device_path': hbas[0]['ClassDevicePath']}]
return info
def fibrechan_connection(self, volume, location, wwn):
return {'driver_volume_type': 'fibrechan',
'data': {
'volume_id': volume['id'],
'target_portal': location,
'target_wwn': wwn,
'target_lun': 1,
}}
def test_connect_volume(self):
self.stubs.Set(self.connector._linuxfc, "get_fc_hbas",
self.fake_get_fc_hbas)
self.stubs.Set(self.connector._linuxfc, "get_fc_hbas_info",
self.fake_get_fc_hbas_info)
self.stubs.Set(os.path, 'exists', lambda x: True)
self.stubs.Set(os.path, 'realpath', lambda x: '/dev/sdb')
multipath_devname = '/dev/md-1'
devices = {"device": multipath_devname,
"id": "1234567890",
"devices": [{'device': '/dev/sdb',
'address': '1:0:0:1',
'host': 1, 'channel': 0,
'id': 0, 'lun': 1}]}
self.stubs.Set(self.connector._linuxscsi, 'find_multipath_device',
lambda x: devices)
self.stubs.Set(self.connector._linuxscsi, 'remove_scsi_device',
lambda x: None)
self.stubs.Set(self.connector._linuxscsi, 'get_device_info',
lambda x: devices['devices'][0])
location = '10.0.2.15:3260'
name = 'volume-00000001'
wwn = '1234567890123456'
vol = {'id': 1, 'name': name}
connection_info = self.fibrechan_connection(vol, location, wwn)
mount_device = "vde"
device_info = self.connector.connect_volume(connection_info['data'])
dev_str = '/dev/disk/by-path/pci-0000:05:00.2-fc-0x%s-lun-1' % wwn
self.assertEquals(device_info['type'], 'block')
self.assertEquals(device_info['path'], dev_str)
self.connector.disconnect_volume(connection_info['data'], device_info)
expected_commands = []
self.assertEqual(expected_commands, self.cmds)
self.stubs.Set(self.connector._linuxfc, 'get_fc_hbas',
lambda: [])
self.stubs.Set(self.connector._linuxfc, 'get_fc_hbas_info',
lambda: [])
self.assertRaises(exception.CinderException,
self.connector.connect_volume,
connection_info['data'])

View File

@@ -0,0 +1,158 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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.path
import string
from cinder.brick.initiator import linuxfc
from cinder.openstack.common import log as logging
from cinder import test
LOG = logging.getLogger(__name__)
class LinuxFCTestCase(test.TestCase):
def setUp(self):
super(LinuxFCTestCase, self).setUp()
self.cmds = []
self.stubs.Set(os.path, 'exists', lambda x: True)
self.lfc = linuxfc.LinuxFibreChannel(execute=self.fake_execute)
def fake_execute(self, *cmd, **kwargs):
self.cmds.append(string.join(cmd))
return "", None
def test_rescan_hosts(self):
hbas = [{'host_device': 'foo'},
{'host_device': 'bar'}, ]
self.lfc.rescan_hosts(hbas)
expected_commands = ['tee -a /sys/class/scsi_host/foo/scan',
'tee -a /sys/class/scsi_host/bar/scan']
self.assertEquals(expected_commands, self.cmds)
def test_get_fc_hbas(self):
def fake_exec(a, b, c, d, run_as_root=True, root_helper='sudo'):
return SYSTOOL_FC, None
self.stubs.Set(self.lfc, "_execute", fake_exec)
hbas = self.lfc.get_fc_hbas()
self.assertEquals(2, len(hbas))
hba1 = hbas[0]
self.assertEquals(hba1["ClassDevice"], "host0")
hba2 = hbas[1]
self.assertEquals(hba2["ClassDevice"], "host2")
def test_get_fc_hbas_info(self):
def fake_exec(a, b, c, d, run_as_root=True, root_helper='sudo'):
return SYSTOOL_FC, None
self.stubs.Set(self.lfc, "_execute", fake_exec)
hbas_info = self.lfc.get_fc_hbas_info()
expected_info = [{'device_path': '/sys/devices/pci0000:20/'
'0000:20:03.0/0000:21:00.0/'
'host0/fc_host/host0',
'host_device': 'host0',
'node_name': '50014380242b9751',
'port_name': '50014380242b9750'},
{'device_path': '/sys/devices/pci0000:20/'
'0000:20:03.0/0000:21:00.1/'
'host2/fc_host/host2',
'host_device': 'host2',
'node_name': '50014380242b9753',
'port_name': '50014380242b9752'}, ]
self.assertEquals(expected_info, hbas_info)
def test_get_fc_wwpns(self):
def fake_exec(a, b, c, d, run_as_root=True, root_helper='sudo'):
return SYSTOOL_FC, None
self.stubs.Set(self.lfc, "_execute", fake_exec)
wwpns = self.lfc.get_fc_wwpns()
expected_wwpns = ['50014380242b9750', '50014380242b9752']
self.assertEquals(expected_wwpns, wwpns)
def test_get_fc_wwnns(self):
def fake_exec(a, b, c, d, run_as_root=True, root_helper='sudo'):
return SYSTOOL_FC, None
self.stubs.Set(self.lfc, "_execute", fake_exec)
wwnns = self.lfc.get_fc_wwpns()
expected_wwnns = ['50014380242b9750', '50014380242b9752']
self.assertEquals(expected_wwnns, wwnns)
SYSTOOL_FC = """
Class = "fc_host"
Class Device = "host0"
Class Device path = "/sys/devices/pci0000:20/0000:20:03.0/\
0000:21:00.0/host0/fc_host/host0"
dev_loss_tmo = "16"
fabric_name = "0x100000051ea338b9"
issue_lip = <store method only>
max_npiv_vports = "0"
node_name = "0x50014380242b9751"
npiv_vports_inuse = "0"
port_id = "0x960d0d"
port_name = "0x50014380242b9750"
port_state = "Online"
port_type = "NPort (fabric via point-to-point)"
speed = "8 Gbit"
supported_classes = "Class 3"
supported_speeds = "1 Gbit, 2 Gbit, 4 Gbit, 8 Gbit"
symbolic_name = "QMH2572 FW:v4.04.04 DVR:v8.03.07.12-k"
system_hostname = ""
tgtid_bind_type = "wwpn (World Wide Port Name)"
uevent =
vport_create = <store method only>
vport_delete = <store method only>
Device = "host0"
Device path = "/sys/devices/pci0000:20/0000:20:03.0/0000:21:00.0/host0"
edc = <store method only>
optrom_ctl = <store method only>
reset = <store method only>
uevent = "DEVTYPE=scsi_host"
Class Device = "host2"
Class Device path = "/sys/devices/pci0000:20/0000:20:03.0/\
0000:21:00.1/host2/fc_host/host2"
dev_loss_tmo = "16"
fabric_name = "0x100000051ea33b79"
issue_lip = <store method only>
max_npiv_vports = "0"
node_name = "0x50014380242b9753"
npiv_vports_inuse = "0"
port_id = "0x970e09"
port_name = "0x50014380242b9752"
port_state = "Online"
port_type = "NPort (fabric via point-to-point)"
speed = "8 Gbit"
supported_classes = "Class 3"
supported_speeds = "1 Gbit, 2 Gbit, 4 Gbit, 8 Gbit"
symbolic_name = "QMH2572 FW:v4.04.04 DVR:v8.03.07.12-k"
system_hostname = ""
tgtid_bind_type = "wwpn (World Wide Port Name)"
uevent =
vport_create = <store method only>
vport_delete = <store method only>
Device = "host2"
Device path = "/sys/devices/pci0000:20/0000:20:03.0/0000:21:00.1/host2"
edc = <store method only>
optrom_ctl = <store method only>
reset = <store method only>
uevent = "DEVTYPE=scsi_host"
"""

View File

@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 Red Hat, Inc.
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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
@@ -17,8 +17,6 @@
import os.path
import string
from cinder.brick.initiator import connector
from cinder.brick.initiator import host_driver
from cinder.brick.initiator import linuxscsi
from cinder.openstack.common import log as logging
from cinder import test
@@ -26,45 +24,6 @@ from cinder import test
LOG = logging.getLogger(__name__)
class ConnectorTestCase(test.TestCase):
def setUp(self):
super(ConnectorTestCase, self).setUp()
self.cmds = []
self.stubs.Set(os.path, 'exists', lambda x: True)
def fake_init(obj):
return
def fake_execute(self, *cmd, **kwargs):
self.cmds.append(string.join(cmd))
return "", None
def test_connect_volume(self):
self.connector = connector.InitiatorConnector()
self.assertRaises(NotImplementedError,
self.connector.connect_volume, None)
def test_disconnect_volume(self):
self.connector = connector.InitiatorConnector()
self.assertRaises(NotImplementedError,
self.connector.connect_volume, None)
class HostDriverTestCase(test.TestCase):
def setUp(self):
super(HostDriverTestCase, self).setUp()
self.devlist = ['device1', 'device2']
self.stubs.Set(os, 'listdir', lambda x: self.devlist)
def test_host_driver(self):
expected = ['/dev/disk/by-path/' + dev for dev in self.devlist]
driver = host_driver.HostDriver()
actual = driver.get_all_block_devices()
self.assertEquals(expected, actual)
class LinuxSCSITestCase(test.TestCase):
def setUp(self):
super(LinuxSCSITestCase, self).setUp()
@@ -76,6 +35,11 @@ class LinuxSCSITestCase(test.TestCase):
self.cmds.append(string.join(cmd))
return "", None
def test_echo_scsi_command(self):
self.linuxscsi.echo_scsi_command("/some/path", "1")
expected_commands = ['tee -a /some/path']
self.assertEquals(expected_commands, self.cmds)
def test_get_name_from_path(self):
device_name = "/dev/sdc"
self.stubs.Set(os.path, 'realpath', lambda x: device_name)
@@ -223,62 +187,3 @@ class LinuxSCSITestCase(test.TestCase):
self.assertEqual("1", info['devices'][1]['channel'])
self.assertEqual("0", info['devices'][1]['id'])
self.assertEqual("3", info['devices'][1]['lun'])
class ISCSIConnectorTestCase(ConnectorTestCase):
def setUp(self):
super(ISCSIConnectorTestCase, self).setUp()
self.connector = connector.ISCSIConnector(execute=self.fake_execute)
self.stubs.Set(self.connector._linuxscsi,
'get_name_from_path',
lambda x: "/dev/sdb")
def tearDown(self):
super(ISCSIConnectorTestCase, self).tearDown()
def iscsi_connection(self, volume, location, iqn):
return {
'driver_volume_type': 'iscsi',
'data': {
'volume_id': volume['id'],
'target_portal': location,
'target_iqn': iqn,
'target_lun': 1,
}
}
@test.testtools.skipUnless(os.path.exists('/dev/disk/by-path'),
'Test requires /dev/disk/by-path')
def test_connect_volume(self):
self.stubs.Set(os.path, 'exists', lambda x: True)
location = '10.0.2.15:3260'
name = 'volume-00000001'
iqn = 'iqn.2010-10.org.openstack:%s' % name
vol = {'id': 1, 'name': name}
connection_info = self.iscsi_connection(vol, location, iqn)
conf = self.connector.connect_volume(connection_info['data'])
dev_str = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location, iqn)
self.assertEquals(conf['type'], 'block')
self.assertEquals(conf['path'], dev_str)
self.connector.disconnect_volume(connection_info['data'])
expected_commands = [('iscsiadm -m node -T %s -p %s' %
(iqn, location)),
('iscsiadm -m session'),
('iscsiadm -m node -T %s -p %s --login' %
(iqn, location)),
('iscsiadm -m node -T %s -p %s --op update'
' -n node.startup -v automatic' % (iqn,
location)),
('tee -a /sys/block/sdb/device/delete'),
('iscsiadm -m node -T %s -p %s --op update'
' -n node.startup -v manual' % (iqn, location)),
('iscsiadm -m node -T %s -p %s --logout' %
(iqn, location)),
('iscsiadm -m node -T %s -p %s --op delete' %
(iqn, location)), ]
LOG.debug("self.cmds = %s" % self.cmds)
LOG.debug("expected = %s" % expected_commands)
self.assertEqual(expected_commands, self.cmds)

View File

@@ -58,7 +58,11 @@ volume_opts = [
help='The port that the iSCSI daemon is listening on'),
cfg.StrOpt('volume_backend_name',
default=None,
help='The backend name for a given driver implementation'), ]
help='The backend name for a given driver implementation'),
cfg.StrOpt('use_multipath_for_image_xfer',
default=False,
help='Do we attach/detach volumes in cinder using multipath '
'for volume to image and image to volume transfers?'), ]
CONF = cfg.CONF
CONF.register_opts(volume_opts)
@@ -188,11 +192,66 @@ class VolumeDriver(object):
def copy_image_to_volume(self, context, volume, image_service, image_id):
"""Fetch the image from image_service and write it to the volume."""
raise NotImplementedError()
LOG.debug(_('copy_image_to_volume %s.') % volume['name'])
properties = initiator.get_connector_properties()
connection, device, connector = self._attach_volume(context, volume,
properties)
try:
image_utils.fetch_to_raw(context,
image_service,
image_id,
device['path'])
finally:
self._detach_volume(connection, device, connector)
self.terminate_connection(volume, properties)
def copy_volume_to_image(self, context, volume, image_service, image_meta):
"""Copy the volume to the specified image."""
raise NotImplementedError()
LOG.debug(_('copy_volume_to_image %s.') % volume['name'])
properties = initiator.get_connector_properties()
connection, device, connector = self._attach_volume(context, volume,
properties)
try:
image_utils.upload_volume(context,
image_service,
image_meta,
device['path'])
finally:
self._detach_volume(connection, device, connector)
self.terminate_connection(volume, properties)
def _attach_volume(self, context, volume, properties):
"""Attach the volume."""
host_device = None
conn = self.initialize_connection(volume, properties)
# Use Brick's code to do attach/detach
use_multipath = self.configuration.use_multipath_for_image_xfer
protocol = conn['driver_volume_type']
connector = initiator.InitiatorConnector.factory(protocol,
use_multipath=
use_multipath)
device = connector.connect_volume(conn['data'])
host_device = device['path']
if not connector.check_valid_device(host_device):
raise exception.DeviceUnavailable(path=host_device,
reason=(_("Unable to access "
"the backend storage "
"via the path "
"%(path)s.") %
{'path': host_device}))
return conn, device, connector
def _detach_volume(self, connection, device, connector):
"""Disconnect the volume from the host."""
protocol = connection['driver_volume_type']
# Use Brick's code to do attach/detach
connector.disconnect_volume(connection['data'], device)
def clone_image(self, volume, image_location):
"""Create a volume efficiently from an existing image.
@@ -397,22 +456,6 @@ class ISCSIDriver(VolumeDriver):
def terminate_connection(self, volume, connector, **kwargs):
pass
def _check_valid_device(self, path):
cmd = ('dd', 'if=%(path)s' % {"path": path},
'of=/dev/null', 'count=1')
out, info = None, None
try:
out, info = self._execute(*cmd, run_as_root=True)
except exception.ProcessExecutionError as e:
LOG.error(_("Failed to access the device on the path "
"%(path)s: %(error)s.") %
{"path": path, "error": e.stderr})
return False
# If the info is none, the path does not exist.
if info is None:
return False
return True
def _get_iscsi_initiator(self):
"""Get iscsi initiator name for this machine"""
# NOTE openiscsi stores initiator name in a file that
@@ -422,74 +465,6 @@ class ISCSIDriver(VolumeDriver):
if l.startswith('InitiatorName='):
return l[l.index('=') + 1:].strip()
def copy_image_to_volume(self, context, volume, image_service, image_id):
"""Fetch the image from image_service and write it to the volume."""
LOG.debug(_('copy_image_to_volume %s.') % volume['name'])
connector = {'initiator': self._get_iscsi_initiator(),
'host': socket.gethostname()}
iscsi_properties, volume_path = self._attach_volume(
context, volume, connector)
try:
image_utils.fetch_to_raw(context,
image_service,
image_id,
volume_path)
finally:
self._detach_volume(iscsi_properties)
self.terminate_connection(volume, connector)
def copy_volume_to_image(self, context, volume, image_service, image_meta):
"""Copy the volume to the specified image."""
LOG.debug(_('copy_volume_to_image %s.') % volume['name'])
connector = {'initiator': self._get_iscsi_initiator(),
'host': socket.gethostname()}
iscsi_properties, volume_path = self._attach_volume(
context, volume, connector)
try:
image_utils.upload_volume(context,
image_service,
image_meta,
volume_path)
finally:
self._detach_volume(iscsi_properties)
self.terminate_connection(volume, connector)
def _attach_volume(self, context, volume, connector):
"""Attach the volume."""
iscsi_properties = None
host_device = None
init_conn = self.initialize_connection(volume, connector)
iscsi_properties = init_conn['data']
# Use Brick's code to do attach/detach
iscsi = initiator.ISCSIConnector()
conf = iscsi.connect_volume(iscsi_properties)
host_device = conf['path']
if not self._check_valid_device(host_device):
raise exception.DeviceUnavailable(path=host_device,
reason=(_("Unable to access "
"the backend storage "
"via the path "
"%(path)s.") %
{'path': host_device}))
LOG.debug("Volume attached %s" % host_device)
return iscsi_properties, host_device
def _detach_volume(self, iscsi_properties):
LOG.debug("Detach volume %s:%s:%s" %
(iscsi_properties["target_portal"],
iscsi_properties["target_iqn"],
iscsi_properties["target_lun"]))
# Use Brick's code to do attach/detach
iscsi = initiator.ISCSIConnector()
conf = iscsi.disconnect_volume(iscsi_properties)
def get_volume_stats(self, refresh=False):
"""Get volume status.
@@ -586,9 +561,3 @@ class FibreChannelDriver(VolumeDriver):
"""
msg = _("Driver must implement initialize_connection")
raise NotImplementedError(msg)
def copy_image_to_volume(self, context, volume, image_service, image_id):
raise NotImplementedError()
def copy_volume_to_image(self, context, volume, image_service, image_meta):
raise NotImplementedError()

View File

@@ -855,6 +855,11 @@
# value)
#volume_backend_name=<None>
# Do we attach/detach volumes in cinder using multipath
# for volume to image and image to volume transfers?
# (boolean value)
#use_multipath_for_image_xfer=False
#
# Options defined in cinder.volume.drivers.block_device

View File

@@ -61,6 +61,7 @@ hus_cmd: CommandFilter, hus_cmd, root
ls: CommandFilter, ls, root
tee: CommandFilter, tee, root
multipath: CommandFilter, multipath, root
systool: CommandFilter, systool, root
# cinder/volume/drivers/block_device.py
blockdev: CommandFilter, blockdev, root