Add LIO iSCSI backend support using python-rtslib
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
This commit is contained in:
		
							
								
								
									
										177
									
								
								bin/cinder-rtstool
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										177
									
								
								bin/cinder-rtstool
									
									
									
									
									
										Executable file
									
								
							@@ -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())
 | 
			
		||||
@@ -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'])
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								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'],
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user