From cb6faab4d99801e76195802659013a5ccdc6b5b5 Mon Sep 17 00:00:00 2001 From: "Walter A. Boring IV" Date: Wed, 10 Jul 2013 15:22:06 -0700 Subject: [PATCH] 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 --- cinder/brick/initiator/connector.py | 252 +++++++++++++++++- cinder/brick/initiator/linuxfc.py | 136 ++++++++++ cinder/brick/initiator/linuxscsi.py | 21 +- cinder/tests/brick/test_brick_connector.py | 231 ++++++++++++++++ cinder/tests/brick/test_brick_linuxfc.py | 158 +++++++++++ ...k_initiator.py => test_brick_linuxscsi.py} | 107 +------- cinder/volume/driver.py | 155 +++++------ etc/cinder/cinder.conf.sample | 5 + etc/cinder/rootwrap.d/volume.filters | 1 + 9 files changed, 866 insertions(+), 200 deletions(-) create mode 100644 cinder/brick/initiator/linuxfc.py create mode 100644 cinder/tests/brick/test_brick_connector.py create mode 100644 cinder/tests/brick/test_brick_linuxfc.py rename cinder/tests/brick/{test_brick_initiator.py => test_brick_linuxscsi.py} (67%) diff --git a/cinder/brick/initiator/connector.py b/cinder/brick/initiator/connector.py index ac67f0d71..e317b0e21 100644 --- a/cinder/brick/initiator/connector.py +++ b/cinder/brick/initiator/connector.py @@ -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 diff --git a/cinder/brick/initiator/linuxfc.py b/cinder/brick/initiator/linuxfc.py new file mode 100644 index 000000000..1ee144a65 --- /dev/null +++ b/cinder/brick/initiator/linuxfc.py @@ -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 diff --git a/cinder/brick/initiator/linuxscsi.py b/cinder/brick/initiator/linuxscsi.py index d5000d2fa..f197d3b7c 100644 --- a/cinder/brick/initiator/linuxscsi.py +++ b/cinder/brick/initiator/linuxscsi.py @@ -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}) diff --git a/cinder/tests/brick/test_brick_connector.py b/cinder/tests/brick/test_brick_connector.py new file mode 100644 index 000000000..f56ce2007 --- /dev/null +++ b/cinder/tests/brick/test_brick_connector.py @@ -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': '', + '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': '', + 'vport_delete': ''}] + + 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']) diff --git a/cinder/tests/brick/test_brick_linuxfc.py b/cinder/tests/brick/test_brick_linuxfc.py new file mode 100644 index 000000000..8fbbc89e4 --- /dev/null +++ b/cinder/tests/brick/test_brick_linuxfc.py @@ -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 = + 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 = + vport_delete = + + Device = "host0" + Device path = "/sys/devices/pci0000:20/0000:20:03.0/0000:21:00.0/host0" + edc = + optrom_ctl = + reset = + 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 = + 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 = + vport_delete = + + Device = "host2" + Device path = "/sys/devices/pci0000:20/0000:20:03.0/0000:21:00.1/host2" + edc = + optrom_ctl = + reset = + uevent = "DEVTYPE=scsi_host" + + +""" diff --git a/cinder/tests/brick/test_brick_initiator.py b/cinder/tests/brick/test_brick_linuxscsi.py similarity index 67% rename from cinder/tests/brick/test_brick_initiator.py rename to cinder/tests/brick/test_brick_linuxscsi.py index 0d0f6b0d7..29be9bb98 100644 --- a/cinder/tests/brick/test_brick_initiator.py +++ b/cinder/tests/brick/test_brick_linuxscsi.py @@ -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) diff --git a/cinder/volume/driver.py b/cinder/volume/driver.py index a5ce64444..25cc0a728 100644 --- a/cinder/volume/driver.py +++ b/cinder/volume/driver.py @@ -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() diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index 784745cc9..4077da62e 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -855,6 +855,11 @@ # value) #volume_backend_name= +# 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 diff --git a/etc/cinder/rootwrap.d/volume.filters b/etc/cinder/rootwrap.d/volume.filters index 32dfa4c2e..079300b87 100644 --- a/etc/cinder/rootwrap.d/volume.filters +++ b/etc/cinder/rootwrap.d/volume.filters @@ -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 -- 2.45.2