]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Adding support for iSER transport protocol
authorShlomi Sasson <shlomis@mellanox.com>
Mon, 17 Jun 2013 17:41:36 +0000 (20:41 +0300)
committerShlomi Sasson <shlomis@mellanox.com>
Mon, 5 Aug 2013 13:17:36 +0000 (16:17 +0300)
DocImpact

Implements: blueprint add-iser-support-to-cinder
Change-Id: I55fb7add68151141be571cb9004389951851226b

cinder/brick/iser/__init__.py [new file with mode: 0644]
cinder/brick/iser/iser.py [new file with mode: 0755]
cinder/exception.py
cinder/tests/fake_driver.py
cinder/tests/fake_flags.py
cinder/tests/test_iser.py [new file with mode: 0644]
cinder/tests/test_volume.py
cinder/volume/driver.py
cinder/volume/drivers/lvm.py
etc/cinder/cinder.conf.sample

diff --git a/cinder/brick/iser/__init__.py b/cinder/brick/iser/__init__.py
new file mode 100644 (file)
index 0000000..5e8da71
--- /dev/null
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 OpenStack Foundation.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
diff --git a/cinder/brick/iser/iser.py b/cinder/brick/iser/iser.py
new file mode 100755 (executable)
index 0000000..b82c2d4
--- /dev/null
@@ -0,0 +1,231 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2013 Mellanox Technologies. All rights reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+"""
+Helper code for the iSER volume driver.
+
+"""
+
+
+import os
+import re
+
+from oslo.config import cfg
+
+from cinder import exception
+from cinder.openstack.common import fileutils
+from cinder.openstack.common import log as logging
+from cinder import utils
+from cinder.volume import utils as volume_utils
+
+
+LOG = logging.getLogger(__name__)
+
+iser_helper_opt = [cfg.StrOpt('iser_helper',
+                              default='tgtadm',
+                              help='iser target user-land tool to use'),
+                   cfg.StrOpt('volumes_dir',
+                              default='$state_path/volumes',
+                              help='Volume configuration file storage '
+                                   'directory'
+                              )
+                   ]
+
+CONF = cfg.CONF
+CONF.register_opts(iser_helper_opt)
+CONF.import_opt('volume_name_template', 'cinder.db')
+
+
+class TargetAdmin(object):
+    """iSER target administration.
+
+    Base class for iSER target admin helpers.
+    """
+
+    def __init__(self, cmd, execute):
+        self._cmd = cmd
+        self.set_execute(execute)
+
+    def set_execute(self, execute):
+        """Set the function to be used to execute commands."""
+        self._execute = execute
+
+    def _run(self, *args, **kwargs):
+        self._execute(self._cmd, *args, run_as_root=True, **kwargs)
+
+    def create_iser_target(self, name, tid, lun, path,
+                           chap_auth=None, **kwargs):
+        """Create a iSER target and logical unit."""
+        raise NotImplementedError()
+
+    def remove_iser_target(self, tid, lun, vol_id, **kwargs):
+        """Remove a iSER target and logical unit."""
+        raise NotImplementedError()
+
+    def _new_target(self, name, tid, **kwargs):
+        """Create a new iSER target."""
+        raise NotImplementedError()
+
+    def _delete_target(self, tid, **kwargs):
+        """Delete a target."""
+        raise NotImplementedError()
+
+    def show_target(self, tid, iqn=None, **kwargs):
+        """Query the given target ID."""
+        raise NotImplementedError()
+
+    def _new_logicalunit(self, tid, lun, path, **kwargs):
+        """Create a new LUN on a target using the supplied path."""
+        raise NotImplementedError()
+
+    def _delete_logicalunit(self, tid, lun, **kwargs):
+        """Delete a logical unit from a target."""
+        raise NotImplementedError()
+
+
+class TgtAdm(TargetAdmin):
+    """iSER target administration using tgtadm."""
+
+    def __init__(self, execute=utils.execute):
+        super(TgtAdm, self).__init__('tgtadm', execute)
+
+    def _get_target(self, iqn):
+        (out, err) = self._execute('tgt-admin', '--show', run_as_root=True)
+        lines = out.split('\n')
+        for line in lines:
+            if iqn in line:
+                parsed = line.split()
+                tid = parsed[1]
+                return tid[:-1]
+
+        return None
+
+    def create_iser_target(self, name, tid, lun, path,
+                           chap_auth=None, **kwargs):
+        # Note(jdg) tid and lun aren't used by TgtAdm but remain for
+        # compatibility
+
+        fileutils.ensure_tree(CONF.volumes_dir)
+
+        vol_id = name.split(':')[1]
+        if chap_auth is None:
+            volume_conf = """
+                <target %s>
+                    driver iser
+                    backing-store %s
+                </target>
+            """ % (name, path)
+        else:
+            volume_conf = """
+                <target %s>
+                    driver iser
+                    backing-store %s
+                    %s
+                </target>
+            """ % (name, path, chap_auth)
+
+        LOG.info(_('Creating iser_target for: %s') % vol_id)
+        volumes_dir = CONF.volumes_dir
+        volume_path = os.path.join(volumes_dir, vol_id)
+
+        f = open(volume_path, 'w+')
+        f.write(volume_conf)
+        f.close()
+
+        old_persist_file = None
+        old_name = kwargs.get('old_name', None)
+        if old_name is not None:
+            old_persist_file = os.path.join(volumes_dir, old_name)
+
+        try:
+            (out, err) = self._execute('tgt-admin',
+                                       '--update',
+                                       name,
+                                       run_as_root=True)
+        except exception.ProcessExecutionError as e:
+            LOG.error(_("Failed to create iser target for volume "
+                        "id:%(vol_id)s: %(e)s")
+                      % {'vol_id': vol_id, 'e': str(e)})
+
+            #Don't forget to remove the persistent file we created
+            os.unlink(volume_path)
+            raise exception.ISERTargetCreateFailed(volume_id=vol_id)
+
+        iqn = '%s%s' % (CONF.iser_target_prefix, vol_id)
+        tid = self._get_target(iqn)
+        if tid is None:
+            LOG.error(_("Failed to create iser target for volume "
+                        "id:%(vol_id)s. Please ensure your tgtd config file "
+                        "contains 'include %(volumes_dir)s/*'") % locals())
+            raise exception.NotFound()
+
+        if old_persist_file is not None and os.path.exists(old_persist_file):
+            os.unlink(old_persist_file)
+
+        return tid
+
+    def remove_iser_target(self, tid, lun, vol_id, **kwargs):
+        LOG.info(_('Removing iser_target for: %s') % vol_id)
+        vol_uuid_file = CONF.volume_name_template % vol_id
+        volume_path = os.path.join(CONF.volumes_dir, vol_uuid_file)
+        if os.path.isfile(volume_path):
+            iqn = '%s%s' % (CONF.iser_target_prefix,
+                            vol_uuid_file)
+        else:
+            raise exception.ISERTargetRemoveFailed(volume_id=vol_id)
+        try:
+            # NOTE(vish): --force is a workaround for bug:
+            #             https://bugs.launchpad.net/cinder/+bug/1159948
+            self._execute('tgt-admin',
+                          '--force',
+                          '--delete',
+                          iqn,
+                          run_as_root=True)
+        except exception.ProcessExecutionError as e:
+            LOG.error(_("Failed to remove iser target for volume "
+                        "id:%(vol_id)s: %(e)s")
+                      % {'vol_id': vol_id, 'e': str(e)})
+            raise exception.ISERTargetRemoveFailed(volume_id=vol_id)
+
+        os.unlink(volume_path)
+
+    def show_target(self, tid, iqn=None, **kwargs):
+        if iqn is None:
+            raise exception.InvalidParameterValue(
+                err=_('valid iqn needed for show_target'))
+
+        tid = self._get_target(iqn)
+        if tid is None:
+            raise exception.NotFound()
+
+
+class FakeIserHelper(object):
+
+    def __init__(self):
+        self.tid = 1
+
+    def set_execute(self, execute):
+        self._execute = execute
+
+    def create_iser_target(self, *args, **kwargs):
+        self.tid += 1
+        return self.tid
+
+
+def get_target_admin():
+    if CONF.iser_helper == 'fake':
+        return FakeIserHelper()
+    else:
+        return TgtAdm()
index c668e140407e8b312b4b8a9a8eabea8d5d62a1f2..448d8fb7152784bec3f83b24d7a6370a29c31a50 100644 (file)
@@ -298,6 +298,22 @@ class ISCSITargetRemoveFailed(CinderException):
     message = _("Failed to remove iscsi target for volume %(volume_id)s.")
 
 
+class ISERTargetNotFoundForVolume(NotFound):
+    message = _("No target id found for volume %(volume_id)s.")
+
+
+class ISERTargetCreateFailed(CinderException):
+    message = _("Failed to create iser target for volume %(volume_id)s.")
+
+
+class ISERTargetAttachFailed(CinderException):
+    message = _("Failed to attach iser target for volume %(volume_id)s.")
+
+
+class ISERTargetRemoveFailed(CinderException):
+    message = _("Failed to remove iser target for volume %(volume_id)s.")
+
+
 class DiskNotFound(NotFound):
     message = _("No disk at %(location)s")
 
index 10f791e77b83b351e73cb0ac9a655e04b132901e..6f352a7c7739a152efae3516a025b95b61a85759 100644 (file)
@@ -46,6 +46,25 @@ class FakeISCSIDriver(lvm.LVMISCSIDriver):
         return (None, None)
 
 
+class FakeISERDriver(FakeISCSIDriver):
+    """Logs calls instead of executing."""
+    def __init__(self, *args, **kwargs):
+        super(FakeISERDriver, self).__init__(execute=self.fake_execute,
+                                             *args, **kwargs)
+
+    def initialize_connection(self, volume, connector):
+        return {
+            'driver_volume_type': 'iser',
+            'data': {}
+        }
+
+    @staticmethod
+    def fake_execute(cmd, *_args, **_kwargs):
+        """Execute that simply logs the command."""
+        LOG.debug(_("FAKE ISER: %s"), cmd)
+        return (None, None)
+
+
 class LoggingVolumeDriver(driver.VolumeDriver):
     """Logs and records calls, for unit tests."""
 
index ef0b1a827d95002e758065c7d392c8dc120aa3cf..c966fc0d594bdea3b5c6c8ca428b485e79e9e06b 100644 (file)
@@ -21,6 +21,7 @@ from cinder import flags
 FLAGS = flags.FLAGS
 
 flags.DECLARE('iscsi_num_targets', 'cinder.volume.drivers.lvm')
+flags.DECLARE('iser_num_targets', 'cinder.volume.drivers.lvm')
 flags.DECLARE('policy_file', 'cinder.policy')
 flags.DECLARE('volume_driver', 'cinder.volume.manager')
 flags.DECLARE('xiv_proxy', 'cinder.volume.drivers.xiv')
@@ -34,10 +35,12 @@ def set_defaults(conf):
     conf.set_default('volume_driver',
                      'cinder.tests.fake_driver.FakeISCSIDriver')
     conf.set_default('iscsi_helper', 'fake')
+    conf.set_default('iser_helper', 'fake')
     conf.set_default('connection_type', 'fake')
     conf.set_default('fake_rabbit', True)
     conf.set_default('rpc_backend', 'cinder.openstack.common.rpc.impl_fake')
     conf.set_default('iscsi_num_targets', 8)
+    conf.set_default('iser_num_targets', 8)
     conf.set_default('verbose', True)
     conf.set_default('connection', 'sqlite://', group='database')
     conf.set_default('sqlite_synchronous', False)
diff --git a/cinder/tests/test_iser.py b/cinder/tests/test_iser.py
new file mode 100644 (file)
index 0000000..789fbe3
--- /dev/null
@@ -0,0 +1,111 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2013 Mellanox Technologies. All rights reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import os.path
+import shutil
+import string
+import tempfile
+
+from cinder.brick.iser import iser
+from cinder import test
+from cinder.volume import utils as volume_utils
+
+
+class TargetAdminTestCase(object):
+
+    def setUp(self):
+        self.cmds = []
+
+        self.tid = 1
+        self.target_name = 'iqn.2011-09.org.foo.bar:blaa'
+        self.lun = 10
+        self.path = '/foo'
+        self.vol_id = 'blaa'
+
+        self.script_template = None
+        self.stubs.Set(os.path, 'isfile', lambda _: True)
+        self.stubs.Set(os, 'unlink', lambda _: '')
+        self.stubs.Set(iser.TgtAdm, '_get_target', self.fake_get_target)
+
+    def fake_init(obj):
+        return
+
+    def fake_get_target(obj, iqn):
+        return 1
+
+    def get_script_params(self):
+        return {'tid': self.tid,
+                'target_name': self.target_name,
+                'lun': self.lun,
+                'path': self.path}
+
+    def get_script(self):
+        return self.script_template % self.get_script_params()
+
+    def fake_execute(self, *cmd, **kwargs):
+        self.cmds.append(string.join(cmd))
+        return "", None
+
+    def clear_cmds(self):
+        self.cmds = []
+
+    def verify_cmds(self, cmds):
+        self.assertEqual(len(cmds), len(self.cmds))
+        for a, b in zip(cmds, self.cmds):
+            self.assertEqual(a, b)
+
+    def verify(self):
+        script = self.get_script()
+        cmds = []
+        for line in script.split('\n'):
+            if not line.strip():
+                continue
+            cmds.append(line)
+        self.verify_cmds(cmds)
+
+    def run_commands(self):
+        tgtadm = iser.get_target_admin()
+        tgtadm.set_execute(self.fake_execute)
+        tgtadm.create_iser_target(self.target_name, self.tid,
+                                  self.lun, self.path)
+        tgtadm.show_target(self.tid, iqn=self.target_name)
+        tgtadm.remove_iser_target(self.tid, self.lun, self.vol_id)
+
+    def test_target_admin(self):
+        self.clear_cmds()
+        self.run_commands()
+        self.verify()
+
+
+class TgtAdmTestCase(test.TestCase, TargetAdminTestCase):
+
+    def setUp(self):
+        super(TgtAdmTestCase, self).setUp()
+        TargetAdminTestCase.setUp(self)
+        self.persist_tempdir = tempfile.mkdtemp()
+        self.flags(iser_helper='tgtadm')
+        self.flags(volumes_dir=self.persist_tempdir)
+        self.script_template = "\n".join([
+            'tgt-admin --update iqn.2011-09.org.foo.bar:blaa',
+            'tgt-admin --force '
+            '--delete iqn.2010-10.org.iser.openstack:volume-blaa'])
+
+    def tearDown(self):
+        try:
+            shutil.rmtree(self.persist_tempdir)
+        except OSError:
+            pass
+        super(TgtAdmTestCase, self).tearDown()
index bdfe0154d58d49e205d21891f67b46602c5ed607..71dab10bc85e8689089354197a9f1349b04f471a 100644 (file)
@@ -29,6 +29,7 @@ import mox
 from oslo.config import cfg
 
 from cinder.brick.iscsi import iscsi
+from cinder.brick.iser import iser
 from cinder import context
 from cinder import db
 from cinder import exception
@@ -1301,8 +1302,36 @@ class ISCSITestCase(DriverTestCase):
         self.assertEquals(stats['free_capacity_gb'], float('0.52'))
 
 
+class ISERTestCase(ISCSITestCase):
+    """Test Case for ISERDriver."""
+    driver_name = "cinder.volume.drivers.lvm.LVMISERDriver"
+
+    def test_do_iscsi_discovery(self):
+        configuration = mox.MockObject(conf.Configuration)
+        configuration.iser_ip_address = '0.0.0.0'
+        configuration.append_config_values(mox.IgnoreArg())
+
+        iser_driver = driver.ISERDriver(configuration=configuration)
+        iser_driver._execute = lambda *a, **kw: \
+            ("%s dummy" % CONF.iser_ip_address, '')
+        volume = {"name": "dummy",
+                  "host": "0.0.0.0"}
+        iser_driver._do_iser_discovery(volume)
+
+    def test_get_iscsi_properties(self):
+        volume = {"provider_location": '',
+                  "id": "0",
+                  "provider_auth": "a b c"}
+        iser_driver = driver.ISERDriver()
+        iser_driver._do_iser_discovery = lambda v: "0.0.0.0:0000,0 iqn:iqn 0"
+        result = iser_driver._get_iser_properties(volume)
+        self.assertEquals(result["target_portal"], "0.0.0.0:0000")
+        self.assertEquals(result["target_iqn"], "iqn:iqn")
+        self.assertEquals(result["target_lun"], 0)
+
+
 class FibreChannelTestCase(DriverTestCase):
-    """Test Case for FibreChannelDriver"""
+    """Test Case for FibreChannelDriver."""
     driver_name = "cinder.volume.driver.FibreChannelDriver"
 
     def test_initialize_connection(self):
index a6b4455719eda63ac66b6d274e5a35cf625b1a5f..cfd13403f06f0e1b8d9177ad42e13231bf343c68 100644 (file)
@@ -42,19 +42,36 @@ volume_opts = [
                help='The percentage of backend capacity is reserved'),
     cfg.IntOpt('num_iscsi_scan_tries',
                default=3,
-               help='number of times to rescan iSCSI target to find volume'),
+               help='The maximum number of times to rescan iSCSI target'
+                    'to find volume'),
     cfg.IntOpt('iscsi_num_targets',
                default=100,
-               help='Number of iscsi target ids per host'),
+               help='The maximum number of iscsi target ids per host'),
     cfg.StrOpt('iscsi_target_prefix',
                default='iqn.2010-10.org.openstack:',
                help='prefix for iscsi volumes'),
     cfg.StrOpt('iscsi_ip_address',
                default='$my_ip',
-               help='The port that the iSCSI daemon is listening on'),
+               help='The IP address that the iSCSI daemon is listening on'),
     cfg.IntOpt('iscsi_port',
                default=3260,
                help='The port that the iSCSI daemon is listening on'),
+    cfg.IntOpt('num_iser_scan_tries',
+               default=3,
+               help='The maximum number of times to rescan iSER target'
+                    'to find volume'),
+    cfg.IntOpt('iser_num_targets',
+               default=100,
+               help='The maximum number of iser target ids per host'),
+    cfg.StrOpt('iser_target_prefix',
+               default='iqn.2010-10.org.iser.openstack:',
+               help='prefix for iser volumes'),
+    cfg.StrOpt('iser_ip_address',
+               default='$my_ip',
+               help='The IP address that the iSER daemon is listening on'),
+    cfg.IntOpt('iser_port',
+               default=3260,
+               help='The port that the iSER daemon is listening on'),
     cfg.StrOpt('volume_backend_name',
                default=None,
                help='The backend name for a given driver implementation'), ]
@@ -62,6 +79,7 @@ volume_opts = [
 CONF = cfg.CONF
 CONF.register_opts(volume_opts)
 CONF.import_opt('iscsi_helper', 'cinder.brick.iscsi.iscsi')
+CONF.import_opt('iser_helper', 'cinder.brick.iser.iser')
 
 
 class VolumeDriver(object):
@@ -567,6 +585,281 @@ class FakeISCSIDriver(ISCSIDriver):
         return (None, None)
 
 
+class ISERDriver(ISCSIDriver):
+    """Executes commands relating to ISER volumes.
+
+    We make use of model provider properties as follows:
+
+    ``provider_location``
+      if present, contains the iSER target information in the same
+      format as an ietadm discovery
+      i.e. '<ip>:<port>,<portal> <target IQN>'
+
+    ``provider_auth``
+      if present, contains a space-separated triple:
+      '<auth method> <auth username> <auth password>'.
+      `CHAP` is the only auth_method in use at the moment.
+    """
+
+    def __init__(self, *args, **kwargs):
+        super(ISERDriver, self).__init__(*args, **kwargs)
+
+    def _do_iser_discovery(self, volume):
+        LOG.warn(_("ISER provider_location not stored, using discovery"))
+
+        volume_name = volume['name']
+
+        (out, _err) = self._execute('iscsiadm', '-m', 'discovery',
+                                    '-t', 'sendtargets', '-p', volume['host'],
+                                    run_as_root=True)
+        for target in out.splitlines():
+            if (self.configuration.iser_ip_address in target
+                    and volume_name in target):
+                return target
+        return None
+
+    def _get_iser_properties(self, volume):
+        """Gets iser configuration
+
+        We ideally get saved information in the volume entity, but fall back
+        to discovery if need be. Discovery may be completely removed in future
+        The properties are:
+
+        :target_discovered:    boolean indicating whether discovery was used
+
+        :target_iqn:    the IQN of the iSER target
+
+        :target_portal:    the portal of the iSER target
+
+        :target_lun:    the lun of the iSER target
+
+        :volume_id:    the id of the volume (currently used by xen)
+
+        :auth_method:, :auth_username:, :auth_password:
+
+            the authentication details. Right now, either auth_method is not
+            present meaning no authentication, or auth_method == `CHAP`
+            meaning use CHAP with the specified credentials.
+        """
+
+        properties = {}
+
+        location = volume['provider_location']
+
+        if location:
+            # provider_location is the same format as iSER discovery output
+            properties['target_discovered'] = False
+        else:
+            location = self._do_iser_discovery(volume)
+
+            if not location:
+                msg = (_("Could not find iSER export for volume %s") %
+                        (volume['name']))
+                raise exception.InvalidVolume(reason=msg)
+
+            LOG.debug(_("ISER Discovery: Found %s") % (location))
+            properties['target_discovered'] = True
+
+        results = location.split(" ")
+        properties['target_portal'] = results[0].split(",")[0]
+        properties['target_iqn'] = results[1]
+        try:
+            properties['target_lun'] = int(results[2])
+        except (IndexError, ValueError):
+            if (self.configuration.volume_driver in
+                    ['cinder.volume.drivers.lvm.LVMISERDriver',
+                     'cinder.volume.drivers.lvm.ThinLVMVolumeDriver'] and
+                    self.configuration.iser_helper == 'tgtadm'):
+                properties['target_lun'] = 1
+            else:
+                properties['target_lun'] = 0
+
+        properties['volume_id'] = volume['id']
+
+        auth = volume['provider_auth']
+        if auth:
+            (auth_method, auth_username, auth_secret) = auth.split()
+
+            properties['auth_method'] = auth_method
+            properties['auth_username'] = auth_username
+            properties['auth_password'] = auth_secret
+
+        return properties
+
+    def initialize_connection(self, volume, connector):
+        """Initializes the connection and returns connection info.
+
+        The iser driver returns a driver_volume_type of 'iser'.
+        The format of the driver data is defined in _get_iser_properties.
+        Example return value::
+
+            {
+                'driver_volume_type': 'iser'
+                'data': {
+                    'target_discovered': True,
+                    'target_iqn':
+                    'iqn.2010-10.org.iser.openstack:volume-00000001',
+                    'target_portal': '127.0.0.0.1:3260',
+                    'volume_id': 1,
+                }
+            }
+
+        """
+
+        iser_properties = self._get_iser_properties(volume)
+        return {
+            'driver_volume_type': 'iser',
+            'data': iser_properties
+        }
+
+    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 _attach_volume(self, context, volume, connector):
+        """Attach the volume."""
+        iser_properties = None
+        host_device = None
+        init_conn = self.initialize_connection(volume, connector)
+        iser_properties = init_conn['data']
+
+        # code "inspired by" nova/virt/libvirt/volume.py
+        try:
+            self._run_iscsiadm(iser_properties, ())
+        except exception.ProcessExecutionError as exc:
+            # iscsiadm returns 21 for "No records found" after version 2.0-871
+            if exc.exit_code in [21, 255]:
+                self._run_iscsiadm(iser_properties, ('--op', 'new'))
+            else:
+                raise
+
+        if iser_properties.get('auth_method'):
+            self._iscsiadm_update(iser_properties,
+                                  "node.session.auth.authmethod",
+                                  iser_properties['auth_method'])
+            self._iscsiadm_update(iser_properties,
+                                  "node.session.auth.username",
+                                  iser_properties['auth_username'])
+            self._iscsiadm_update(iser_properties,
+                                  "node.session.auth.password",
+                                  iser_properties['auth_password'])
+
+        host_device = ("/dev/disk/by-path/ip-%s-iser-%s-lun-%s" %
+                       (iser_properties['target_portal'],
+                        iser_properties['target_iqn'],
+                        iser_properties.get('target_lun', 0)))
+
+        out = self._run_iscsiadm_bare(["-m", "session"],
+                                      run_as_root=True,
+                                      check_exit_code=[0, 1, 21])[0] or ""
+
+        portals = [{'portal': p.split(" ")[2], 'iqn': p.split(" ")[3]}
+                   for p in out.splitlines() if p.startswith("iser:")]
+
+        stripped_portal = iser_properties['target_portal'].split(",")[0]
+        length_iqn = [s for s in portals
+                      if stripped_portal ==
+                      s['portal'].split(",")[0] and
+                      s['iqn'] == iser_properties['target_iqn']]
+        if len(portals) == 0 or len(length_iqn) == 0:
+            try:
+                self._run_iscsiadm(iser_properties, ("--login",),
+                                   check_exit_code=[0, 255])
+            except exception.ProcessExecutionError as err:
+                if err.exit_code in [15]:
+                    self._iscsiadm_update(iser_properties,
+                                          "node.startup",
+                                          "automatic")
+                    return iser_properties, host_device
+                else:
+                    raise
+
+            self._iscsiadm_update(iser_properties,
+                                  "node.startup", "automatic")
+
+            tries = 0
+            while not os.path.exists(host_device):
+                if tries >= self.configuration.num_iser_scan_tries:
+                    raise exception.CinderException(_("iSER device "
+                                                      "not found "
+                                                      "at %s") % (host_device))
+
+                LOG.warn(_("ISER volume not yet found at: %(host_device)s. "
+                           "Will rescan & retry.  Try number: %(tries)s.") %
+                         {'host_device': host_device, 'tries': tries})
+
+                # The rescan isn't documented as being necessary(?),
+                # but it helps
+                self._run_iscsiadm(iser_properties, ("--rescan",))
+
+                tries = tries + 1
+                if not os.path.exists(host_device):
+                    time.sleep(tries ** 2)
+
+            if tries != 0:
+                LOG.debug(_("Found iSER node %(host_device)s "
+                            "(after %(tries)s rescans).") %
+                          {'host_device': host_device,
+                           'tries': tries})
+
+        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}))
+        return iser_properties, host_device
+
+    def _update_volume_status(self):
+        """Retrieve status info from volume group."""
+
+        LOG.debug(_("Updating volume status"))
+        data = {}
+        backend_name = self.configuration.safe_get('volume_backend_name')
+        data["volume_backend_name"] = backend_name or 'Generic_iSER'
+        data["vendor_name"] = 'Open Source'
+        data["driver_version"] = '1.0'
+        data["storage_protocol"] = 'iSER'
+
+        data['total_capacity_gb'] = 'infinite'
+        data['free_capacity_gb'] = 'infinite'
+        data['reserved_percentage'] = 100
+        data['QoS_support'] = False
+        self._stats = data
+
+
+class FakeISERDriver(FakeISCSIDriver):
+    """Logs calls instead of executing."""
+    def __init__(self, *args, **kwargs):
+        super(FakeISERDriver, self).__init__(execute=self.fake_execute,
+                                             *args, **kwargs)
+
+    def initialize_connection(self, volume, connector):
+        return {
+            'driver_volume_type': 'iser',
+            'data': {}
+        }
+
+    @staticmethod
+    def fake_execute(cmd, *_args, **_kwargs):
+        """Execute that simply logs the command."""
+        LOG.debug(_("FAKE ISER: %s"), cmd)
+        return (None, None)
+
+
 class FibreChannelDriver(VolumeDriver):
     """Executes commands relating to Fibre Channel volumes."""
     def __init__(self, *args, **kwargs):
index f4705e2b1ca38e878cc8c7e94b44aea3ee296940..8cb87426a773685f922a60984ae33a3ac943013f 100644 (file)
@@ -27,6 +27,7 @@ import re
 from oslo.config import cfg
 
 from cinder.brick.iscsi import iscsi
+from cinder.brick.iser import iser
 from cinder import exception
 from cinder.image import image_utils
 from cinder.openstack.common import fileutils
@@ -600,6 +601,196 @@ class LVMISCSIDriver(LVMVolumeDriver, driver.ISCSIDriver):
         return "%s %s %s" % (chap, name, password)
 
 
+class LVMISERDriver(LVMISCSIDriver, driver.ISERDriver):
+    """Executes commands relating to ISER volumes.
+
+    We make use of model provider properties as follows:
+
+    ``provider_location``
+      if present, contains the iSER target information in the same
+      format as an ietadm discovery
+      i.e. '<ip>:<port>,<portal> <target IQN>'
+
+    ``provider_auth``
+      if present, contains a space-separated triple:
+      '<auth method> <auth username> <auth password>'.
+      `CHAP` is the only auth_method in use at the moment.
+    """
+
+    def __init__(self, *args, **kwargs):
+        self.tgtadm = iser.get_target_admin()
+        LVMVolumeDriver.__init__(self, *args, **kwargs)
+
+    def set_execute(self, execute):
+        LVMVolumeDriver.set_execute(self, execute)
+        self.tgtadm.set_execute(execute)
+
+    def ensure_export(self, context, volume):
+        """Synchronously recreates an export for a logical volume."""
+
+        if not isinstance(self.tgtadm, iser.TgtAdm):
+            try:
+                iser_target = self.db.volume_get_iscsi_target_num(
+                    context,
+                    volume['id'])
+            except exception.NotFound:
+                LOG.info(_("Skipping ensure_export. No iser_target "
+                           "provisioned for volume: %s"), volume['id'])
+                return
+        else:
+            iser_target = 1  # dummy value when using TgtAdm
+
+        chap_auth = None
+
+        # Check for https://bugs.launchpad.net/cinder/+bug/1065702
+        old_name = None
+        volume_name = volume['name']
+        if (volume['provider_location'] is not None and
+                volume['name'] not in volume['provider_location']):
+
+            msg = _('Detected inconsistency in provider_location id')
+            LOG.debug(msg)
+            old_name = self._fix_id_migration(context, volume)
+            if 'in-use' in volume['status']:
+                volume_name = old_name
+                old_name = None
+
+        iser_name = "%s%s" % (self.configuration.iser_target_prefix,
+                              volume_name)
+        volume_path = "/dev/%s/%s" % (self.configuration.volume_group,
+                                      volume_name)
+
+        self.tgtadm.create_iser_target(iser_name, iser_target,
+                                       0, volume_path, chap_auth,
+                                       check_exit_code=False,
+                                       old_name=old_name)
+
+    def _ensure_iser_targets(self, context, host):
+        """Ensure that target ids have been created in datastore."""
+        if not isinstance(self.tgtadm, iser.TgtAdm):
+            host_iser_targets = self.db.iscsi_target_count_by_host(context,
+                                                                   host)
+            if host_iser_targets >= self.configuration.iser_num_targets:
+                return
+
+            # NOTE(vish): Target ids start at 1, not 0.
+            target_end = self.configuration.iser_num_targets + 1
+            for target_num in xrange(1, target_end):
+                target = {'host': host, 'target_num': target_num}
+                self.db.iscsi_target_create_safe(context, target)
+
+    def create_export(self, context, volume):
+        """Creates an export for a logical volume."""
+
+        iser_name = "%s%s" % (self.configuration.iser_target_prefix,
+                              volume['name'])
+        volume_path = "/dev/%s/%s" % (self.configuration.volume_group,
+                                      volume['name'])
+        model_update = {}
+
+        # TODO(jdg): In the future move all of the dependent stuff into the
+        # cooresponding target admin class
+        if not isinstance(self.tgtadm, iser.TgtAdm):
+            lun = 0
+            self._ensure_iser_targets(context, volume['host'])
+            iser_target = self.db.volume_allocate_iscsi_target(context,
+                                                               volume['id'],
+                                                               volume['host'])
+        else:
+            lun = 1  # For tgtadm the controller is lun 0, dev starts at lun 1
+            iser_target = 0
+
+        # Use the same method to generate the username and the password.
+        chap_username = utils.generate_username()
+        chap_password = utils.generate_password()
+        chap_auth = self._iser_authentication('IncomingUser', chap_username,
+                                              chap_password)
+        tid = self.tgtadm.create_iser_target(iser_name,
+                                             iser_target,
+                                             0,
+                                             volume_path,
+                                             chap_auth)
+        model_update['provider_location'] = self._iser_location(
+            self.configuration.iser_ip_address, tid, iser_name, lun)
+        model_update['provider_auth'] = self._iser_authentication(
+            'CHAP', chap_username, chap_password)
+        return model_update
+
+    def remove_export(self, context, volume):
+        """Removes an export for a logical volume."""
+
+        if not isinstance(self.tgtadm, iser.TgtAdm):
+            try:
+                iser_target = self.db.volume_get_iscsi_target_num(
+                    context,
+                    volume['id'])
+            except exception.NotFound:
+                LOG.info(_("Skipping remove_export. No iser_target "
+                           "provisioned for volume: %s"), volume['id'])
+                return
+        else:
+            iser_target = 0
+
+        try:
+
+            # NOTE: provider_location may be unset if the volume hasn't
+            # been exported
+            location = volume['provider_location'].split(' ')
+            iqn = location[1]
+
+            self.tgtadm.show_target(iser_target, iqn=iqn)
+
+        except Exception as e:
+            LOG.info(_("Skipping remove_export. No iser_target "
+                       "is presently exported for volume: %s"), volume['id'])
+            return
+
+        self.tgtadm.remove_iser_target(iser_target, 0, volume['id'])
+
+    def _update_volume_status(self):
+        """Retrieve status info from volume group."""
+
+        LOG.debug(_("Updating volume status"))
+        data = {}
+
+        # Note(zhiteng): These information are driver/backend specific,
+        # each driver may define these values in its own config options
+        # or fetch from driver specific configuration file.
+        backend_name = self.configuration.safe_get('volume_backend_name')
+        data["volume_backend_name"] = backend_name or 'LVM_iSER'
+        data["vendor_name"] = 'Open Source'
+        data["driver_version"] = self.VERSION
+        data["storage_protocol"] = 'iSER'
+
+        data['total_capacity_gb'] = 0
+        data['free_capacity_gb'] = 0
+        data['reserved_percentage'] = self.configuration.reserved_percentage
+        data['QoS_support'] = False
+
+        try:
+            out, err = self._execute('vgs', '--noheadings', '--nosuffix',
+                                     '--unit=G', '-o', 'name,size,free',
+                                     self.configuration.volume_group,
+                                     run_as_root=True)
+        except exception.ProcessExecutionError as exc:
+            LOG.error(_("Error retrieving volume status: %s"), exc.stderr)
+            out = False
+
+        if out:
+            volume = out.split()
+            data['total_capacity_gb'] = float(volume[1].replace(',', '.'))
+            data['free_capacity_gb'] = float(volume[2].replace(',', '.'))
+
+        self._stats = data
+
+    def _iser_location(self, ip, target, iqn, lun=None):
+        return "%s:%s,%s %s %s" % (ip, self.configuration.iser_port,
+                                   target, iqn, lun)
+
+    def _iser_authentication(self, chap, name, password):
+        return "%s %s %s" % (chap, name, password)
+
+
 class ThinLVMVolumeDriver(LVMISCSIDriver):
     """Subclass for thin provisioned LVM's."""
 
index 871818ca383b9484eb9b0cf9cffb76f3a4cbade0..7de087149b9dbe782d9d2e9f3c1fa91f66f1020e 100644 (file)
 # value)
 #iscsi_port=3260
 
+# number of times to rescan iSER target to find volume
+# (integer value)
+#num_iser_scan_tries=3
+
+# Number of iser target ids per host (integer value)
+#iser_num_targets=100
+
+# prefix for iser volumes (string value)
+#iser_target_prefix=iqn.2010-10.org.iser.openstack:
+
+# The port that the iSER daemon is listening on (string
+# value)
+#iser_ip_address=$my_ip
+
+# The port that the iSER daemon is listening on (integer
+# value)
+#iser_port=3260
+
 # The backend name for a given driver implementation (string
 # value)
 #volume_backend_name=<None>
 # iscsi target user-land tool to use (string value)
 #iscsi_helper=tgtadm
 
+# iser target user-land tool to use (string value)
+#iser_helper=tgtadm
+
 # Volume configuration file storage directory (string value)
 #volumes_dir=$state_path/volumes
 
 #
 
 # Driver to use for volume creation (string value)
-#volume_driver=cinder.volume.drivers.lvm.LVMISCSIDriver
+#volume_driver=cinder.volume.drivers.lvm.LVMISCSIDriver,cinder.volume.drivers.lvm.LVMISERDriver
 
 # Total option count: 300