import executor
import host_driver
+import linuxfc
import linuxscsi
import os
+import socket
import time
from oslo.config import cfg
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__)
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):
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()
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):
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:
'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',
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
--- /dev/null
+# 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
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__)
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.
(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})
--- /dev/null
+# 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'])
--- /dev/null
+# 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"
+
+
+"""
# 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
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
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()
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)
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)
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)
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.
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
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.
"""
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()
# 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
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