]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add LIO iSCSI backend support using python-rtslib
authorEric Harney <eharney@redhat.com>
Mon, 17 Dec 2012 22:31:40 +0000 (17:31 -0500)
committerEric Harney <eharney@redhat.com>
Fri, 15 Feb 2013 16:20:10 +0000 (11:20 -0500)
This patch enables LIO as an iSCSI backend for Cinder, using
python-rtslib.

To enable, set "iscsi_helper = lioadm" in cinder.conf.

This requires python-rtslib 2.1.fb27, which is available from pip.

Implements blueprint lio-iscsi-support
DocImpact

Change-Id: Ifb23de65f26a40997afd6148a1d0be39bcc8d196

bin/cinder-rtstool [new file with mode: 0755]
cinder/tests/test_iscsi.py
cinder/volume/drivers/lvm.py
cinder/volume/iscsi.py
etc/cinder/rootwrap.d/volume.filters
setup.py
tools/test-requires

diff --git a/bin/cinder-rtstool b/bin/cinder-rtstool
new file mode 100755 (executable)
index 0000000..09d2a1d
--- /dev/null
@@ -0,0 +1,177 @@
+#!/usr/bin/env python
+# vim: et tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 - 2013 Red Hat, Inc.
+# 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 gettext
+import re
+import sys
+
+import rtslib
+
+gettext.install('cinder-rtstool', unicode=1)
+
+
+class RtstoolError(Exception):
+    pass
+
+
+class RtstoolImportError(RtstoolError):
+    pass
+
+
+def create(backing_device, name, userid, password):
+    try:
+        rtsroot = rtslib.root.RTSRoot()
+    except rtslib.utils.RTSLibError:
+        print _('Ensure that configfs is mounted at /sys/kernel/config.')
+        raise
+
+    # Look to see if BlockStorageObject already exists
+    for x in rtsroot.storage_objects:
+        if x.dump()['name'] == name:
+            # Already exists, use this one
+            return
+
+    so_new = rtslib.BlockStorageObject(name=name,
+                                       dev=backing_device)
+
+    target_new = rtslib.Target(rtslib.FabricModule('iscsi'), name, 'create')
+
+    tpg_new = rtslib.TPG(target_new, mode='create')
+    tpg_new.set_attribute('authentication', '1')
+
+    lun_new = rtslib.LUN(tpg_new, storage_object=so_new)
+
+    initiator_name = None
+    name_file = '/etc/iscsi/initiatorname.iscsi'
+
+    try:
+        with open(name_file, 'r') as f:
+            for line in f:
+                m = re.match('InitiatorName=(.+)', line)
+                if m != None:
+                    initiator_name = m.group(1)
+                    break
+    except IOError:
+        raise RtstoolError(_('Could not open %s') % name_file)
+
+    if initiator_name == None:
+        raise RtstoolError(_('Could not read InitiatorName from %s') %
+                           name_file)
+
+    acl_new = rtslib.NodeACL(tpg_new, initiator_name, mode='create')
+
+    acl_new.chap_userid = userid
+    acl_new.chap_password = password
+
+    tpg_new.enable = 1
+
+    m = rtslib.MappedLUN(acl_new, lun_new.lun, lun_new.lun)
+
+    try:
+        rtslib.NetworkPortal(tpg_new, '0.0.0.0', 3260, mode='any')
+    except rtslib.utils.RTSLibError:
+        print _('Error creating NetworkPortal: ensure port 3260 '
+                'is not in use by another service.')
+        raise
+
+    try:
+        rtslib.NetworkPortal(tpg_new, '::0', 3260, mode='any')
+    except rtslib.utils.RTSLibError:
+        # TODO(emh): Binding to IPv6 fails sometimes -- let pass for now.
+        pass
+
+
+def get_targets():
+    rtsroot = rtslib.root.RTSRoot()
+    for x in rtsroot.targets:
+        print(x.dump()['wwn'])
+
+
+def delete(iqn):
+    rtsroot = rtslib.root.RTSRoot()
+    for x in rtsroot.targets:
+        if x.dump()['wwn'] == iqn:
+            x.delete()
+            break
+
+    for x in rtsroot.storage_objects:
+        if x.dump()['name'] == iqn:
+            x.delete()
+            break
+
+
+def verify_rtslib():
+    for member in ['BlockStorageObject', 'FabricModule', 'LUN',
+                   'MappedLUN', 'NetworkPortal', 'NodeACL', 'root',
+                   'Target', 'TPG']:
+        if not hasattr(rtslib, member):
+            raise RtstoolImportError(_("rtslib is missing member %s: "
+                                       "You may need a newer python-rtslib.") %
+                                     member)
+
+
+def usage():
+    print "Usage:"
+    print sys.argv[0], "create [device] [name] [userid] [password]"
+    print sys.argv[0], "get-targets"
+    print sys.argv[0], "delete [iqn]"
+    print sys.argv[0], "verify"
+    sys.exit(1)
+
+
+def main(argv=None):
+    if argv is None:
+        argv = sys.argv
+
+    if len(argv) < 2:
+        usage()
+
+    if argv[1] == 'create':
+        if len(argv) < 6:
+            usage()
+
+        backing_device = argv[2]
+        name = argv[3]
+        userid = argv[4]
+        password = argv[5]
+
+        create(backing_device, name, userid, password)
+
+    elif argv[1] == 'get-targets':
+        get_targets()
+
+    elif argv[1] == 'delete':
+        if len(argv) < 3:
+            usage()
+
+        iqn = argv[2]
+        delete(iqn)
+
+    elif argv[1] == 'verify':
+        # This is used to verify that this script can be called by cinder,
+        # and that rtslib is new enough to work.
+        verify_rtslib()
+        return 0
+
+    else:
+        usage()
+
+    return 0
+
+if __name__ == '__main__':
+    sys.exit(main())
index a724ad3216fb3e29f509a3403d32c0847c8bf1f6..4bd942852352c51192fff8f1153a9413964d57d5 100644 (file)
@@ -38,6 +38,11 @@ class TargetAdminTestCase(object):
         self.stubs.Set(os.path, 'isfile', lambda _: True)
         self.stubs.Set(os, 'unlink', lambda _: '')
         self.stubs.Set(iscsi.TgtAdm, '_get_target', self.fake_get_target)
+        self.stubs.Set(iscsi.LioAdm, '_get_target', self.fake_get_target)
+        self.stubs.Set(iscsi.LioAdm, '__init__', self.fake_init)
+
+    def fake_init(obj):
+        return
 
     def fake_get_target(obj, iqn):
         return 1
@@ -119,3 +124,16 @@ class IetAdmTestCase(test.TestCase, TargetAdminTestCase):
             'ietadm --op show --tid=%(tid)s',
             'ietadm --op delete --tid=%(tid)s --lun=%(lun)s',
             'ietadm --op delete --tid=%(tid)s'])
+
+
+class LioAdmTestCase(test.TestCase, TargetAdminTestCase):
+
+    def setUp(self):
+        super(LioAdmTestCase, self).setUp()
+        TargetAdminTestCase.setUp(self)
+        self.persist_tempdir = tempfile.mkdtemp()
+        self.flags(iscsi_helper='lioadm')
+        self.script_template = "\n".join([
+            'cinder-rtstool create '
+                '/foo iqn.2011-09.org.foo.bar:blaa test_id test_pass',
+            'cinder-rtstool delete iqn.2010-10.org.openstack:volume-blaa'])
index d3dc9a2285c82f479bea3f2ee32370329e56fdb0..bd3e8c3b2e9a3bfeb8bf146ab7d29858a8e83b99 100644 (file)
@@ -281,6 +281,31 @@ class LVMISCSIDriver(LVMVolumeDriver, driver.ISCSIDriver):
         # NOTE(jdg): tgtadm doesn't use the iscsi_targets table
         # TODO(jdg): In the future move all of the dependent stuff into the
         # cooresponding target admin class
+
+        if isinstance(self.tgtadm, iscsi.LioAdm):
+            try:
+                volume_info = self.db.volume_get(context, volume['id'])
+                (auth_method,
+                 auth_user,
+                 auth_pass) = volume_info['provider_auth'].split(' ', 3)
+                chap_auth = self._iscsi_authentication(auth_method,
+                                                       auth_user,
+                                                       auth_pass)
+            except exception.NotFound:
+                LOG.debug("volume_info:", volume_info)
+                LOG.info(_("Skipping ensure_export. No iscsi_target "
+                           "provision for volume: %s"), volume['id'])
+                return
+
+            iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
+            volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name'])
+            iscsi_target = 1
+
+            self.tgtadm.create_iscsi_target(iscsi_name, iscsi_target,
+                                            0, volume_path, chap_auth,
+                                            check_exit_code=False)
+            return
+
         if not isinstance(self.tgtadm, iscsi.TgtAdm):
             try:
                 iscsi_target = self.db.volume_get_iscsi_target_num(
@@ -293,6 +318,8 @@ class LVMISCSIDriver(LVMVolumeDriver, driver.ISCSIDriver):
         else:
             iscsi_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']
@@ -312,7 +339,7 @@ class LVMISCSIDriver(LVMVolumeDriver, driver.ISCSIDriver):
         # NOTE(jdg): For TgtAdm case iscsi_name is the ONLY param we need
         # should clean this all up at some point in the future
         self.tgtadm.create_iscsi_target(iscsi_name, iscsi_target,
-                                        0, volume_path,
+                                        0, volume_path, chap_auth,
                                         check_exit_code=False,
                                         old_name=old_name)
 
@@ -418,7 +445,22 @@ class LVMISCSIDriver(LVMVolumeDriver, driver.ISCSIDriver):
         # NOTE(jdg): tgtadm doesn't use the iscsi_targets table
         # TODO(jdg): In the future move all of the dependent stuff into the
         # cooresponding target admin class
-        if not isinstance(self.tgtadm, iscsi.TgtAdm):
+
+        if isinstance(self.tgtadm, iscsi.LioAdm):
+            try:
+                iscsi_target = self.db.volume_get_iscsi_target_num(
+                    context,
+                    volume['id'])
+            except exception.NotFound:
+                LOG.info(_("Skipping remove_export. No iscsi_target "
+                           "provisioned for volume: %s"), volume['id'])
+                return
+
+            self.tgtadm.remove_iscsi_target(iscsi_target, 0, volume['id'])
+
+            return
+
+        elif not isinstance(self.tgtadm, iscsi.TgtAdm):
             try:
                 iscsi_target = self.db.volume_get_iscsi_target_num(
                     context,
index 9f7a78de9929a3ec6543f8423dc37e8461829f49..04e2417a23f3d55a0d5dd782fc25d153646741b6 100644 (file)
@@ -315,10 +315,99 @@ class FakeIscsiHelper(object):
         return self.tid
 
 
+class LioAdm(TargetAdmin):
+    """iSCSI target administration for LIO using python-rtslib."""
+    def __init__(self, execute=utils.execute):
+        super(LioAdm, self).__init__('cinder-rtstool', execute)
+
+        try:
+            self._execute('cinder-rtstool', 'verify')
+        except (OSError, exception.ProcessExecutionError):
+            LOG.error(_('cinder-rtstool is not installed correctly'))
+            raise
+
+    def _get_target(self, iqn):
+        (out, err) = self._execute('cinder-rtstool',
+                                   'get-targets',
+                                   run_as_root=True)
+        lines = out.split('\n')
+        for line in lines:
+            if iqn in line:
+                return line
+
+        return None
+
+    def create_iscsi_target(self, name, tid, lun, path,
+                            chap_auth=None, **kwargs):
+        # tid and lun are not used
+
+        vol_id = name.split(':')[1]
+
+        LOG.info(_('Creating volume: %s') % vol_id)
+
+        # cinder-rtstool requires chap_auth, but unit tests don't provide it
+        chap_auth_userid = 'test_id'
+        chap_auth_password = 'test_pass'
+
+        if chap_auth != None:
+            (chap_auth_userid, chap_auth_password) = chap_auth.split(' ')[1:]
+
+        try:
+            self._execute('cinder-rtstool',
+                          'create',
+                          path,
+                          name,
+                          chap_auth_userid,
+                          chap_auth_password,
+                          run_as_root=True)
+        except exception.ProcessExecutionError as e:
+                LOG.error(_("Failed to create iscsi target for volume "
+                            "id:%(vol_id)s.") % locals())
+                LOG.error("%s" % str(e))
+
+                raise exception.ISCSITargetCreateFailed(volume_id=vol_id)
+
+        iqn = '%s%s' % (FLAGS.iscsi_target_prefix, vol_id)
+        tid = self._get_target(iqn)
+        if tid is None:
+            LOG.error(_("Failed to create iscsi target for volume "
+                        "id:%(vol_id)s.") % locals())
+            raise exception.NotFound()
+
+        return tid
+
+    def remove_iscsi_target(self, tid, lun, vol_id, **kwargs):
+        LOG.info(_('Removing volume: %s') % vol_id)
+        vol_uuid_name = 'volume-%s' % vol_id
+        iqn = '%s%s' % (FLAGS.iscsi_target_prefix, vol_uuid_name)
+
+        try:
+            self._execute('cinder-rtstool',
+                          'delete',
+                          iqn,
+                          run_as_root=True)
+        except exception.ProcessExecutionError as e:
+            LOG.error(_("Failed to remove iscsi target for volume "
+                        "id:%(vol_id)s.") % locals())
+            LOG.error("%s" % str(e))
+            raise exception.ISCSITargetRemoveFailed(volume_id=vol_id)
+
+    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()
+
+
 def get_target_admin():
     if FLAGS.iscsi_helper == 'tgtadm':
         return TgtAdm()
     elif FLAGS.iscsi_helper == 'fake':
         return FakeIscsiHelper()
+    elif FLAGS.iscsi_helper == 'lioadm':
+        return LioAdm()
     else:
         return IetAdm()
index a51cad891d454c4a0de6f2846bd22609257b1365..3a22fa9cbfe3d66eb5abd8939037dc5e97bbee69 100644 (file)
@@ -6,6 +6,7 @@
 ietadm: CommandFilter, /usr/sbin/ietadm, root
 tgtadm: CommandFilter, /usr/sbin/tgtadm, root
 tgt-admin: CommandFilter, /usr/sbin/tgt-admin, root
+cinder-rtstool: CommandFilter, cinder-rtstool, root
 
 # cinder/volume/driver.py: 'vgs', '--noheadings', '-o', 'name'
 vgs: CommandFilter, /sbin/vgs, root
index 6b7cccf3a0cce8e86b4b96f31d6cba89973f6506..b65ce9558abc4001e4f329890372732a6f9e27a6 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -72,6 +72,7 @@ setuptools.setup(
              'bin/cinder-clear-rabbit-queues',
              'bin/cinder-manage',
              'bin/cinder-rootwrap',
+             'bin/cinder-rtstool',
              'bin/cinder-scheduler',
              'bin/cinder-volume',
              'bin/cinder-volume-usage-audit'],
index 02f256f11ee6a0456540b3590d51e768968a7aff..cc907a4c11cb5dba12346580c9dd3e3b53003d51 100644 (file)
@@ -9,6 +9,7 @@ openstack.nose_plugin
 nosehtmloutput
 pep8==1.3.3
 pylint==0.25.2
+rtslib>=2.1.fb27
 sphinx>=1.1.2
 MySQL-python
 hp3parclient>=1.0.0