]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add Brick Fibre Channel attach/detach support.
authorWalter A. Boring IV <walter.boring@hp.com>
Wed, 10 Jul 2013 22:22:06 +0000 (15:22 -0700)
committerWalter A. Boring IV <walter.boring@hp.com>
Wed, 17 Jul 2013 00:57:57 +0000 (17:57 -0700)
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
cinder/brick/initiator/linuxfc.py [new file with mode: 0644]
cinder/brick/initiator/linuxscsi.py
cinder/tests/brick/test_brick_connector.py [new file with mode: 0644]
cinder/tests/brick/test_brick_linuxfc.py [new file with mode: 0644]
cinder/tests/brick/test_brick_linuxscsi.py [moved from cinder/tests/brick/test_brick_initiator.py with 67% similarity]
cinder/volume/driver.py
etc/cinder/cinder.conf.sample
etc/cinder/rootwrap.d/volume.filters

index ac67f0d7131de1451034192777f58ef97a75aa86..e317b0e211825b044c70c3f1423d84e3792c8aaf 100644 (file)
 
 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 (file)
index 0000000..1ee144a
--- /dev/null
@@ -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
index d5000d2fa55ef32e721b8c8e1c6ecb1f0c528f09..f197d3b7c2e74f11c9d8f5ad86444c2de26e5a7e 100644 (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})
diff --git a/cinder/tests/brick/test_brick_connector.py b/cinder/tests/brick/test_brick_connector.py
new file mode 100644 (file)
index 0000000..f56ce20
--- /dev/null
@@ -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'])
diff --git a/cinder/tests/brick/test_brick_linuxfc.py b/cinder/tests/brick/test_brick_linuxfc.py
new file mode 100644 (file)
index 0000000..8fbbc89
--- /dev/null
@@ -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"
+
+
+"""
similarity index 67%
rename from cinder/tests/brick/test_brick_initiator.py
rename to cinder/tests/brick/test_brick_linuxscsi.py
index 0d0f6b0d7ad86c9ca8779fcf5dd06b2df7914982..29be9bb98b9cb73538327430d948d7645451cd36 100644 (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)
index a5ce64444264c9dc5eb32e4f85f9ddf8f1c31254..25cc0a7286360d9d278dca46df8825e567218c4a 100644 (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()
index 784745cc90f58cc02cc9aa7a9d133923c53ba6e2..4077da62e3b910a7a556759dbeacf904c0d25df9 100644 (file)
 # 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
index 32dfa4c2e73f14be04b505013feee2c23896d624..079300b878415feec770904c3bfce6048c760a06 100644 (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