From: Eric Harney Date: Mon, 17 Dec 2012 22:31:40 +0000 (-0500) Subject: Add LIO iSCSI backend support using python-rtslib X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=1fc557561b711f6edb06cf28edb0f90900e2c21e;p=openstack-build%2Fcinder-build.git 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 --- diff --git a/bin/cinder-rtstool b/bin/cinder-rtstool new file mode 100755 index 000000000..09d2a1d60 --- /dev/null +++ b/bin/cinder-rtstool @@ -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()) diff --git a/cinder/tests/test_iscsi.py b/cinder/tests/test_iscsi.py index a724ad321..4bd942852 100644 --- a/cinder/tests/test_iscsi.py +++ b/cinder/tests/test_iscsi.py @@ -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']) diff --git a/cinder/volume/drivers/lvm.py b/cinder/volume/drivers/lvm.py index d3dc9a228..bd3e8c3b2 100644 --- a/cinder/volume/drivers/lvm.py +++ b/cinder/volume/drivers/lvm.py @@ -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, diff --git a/cinder/volume/iscsi.py b/cinder/volume/iscsi.py index 9f7a78de9..04e2417a2 100644 --- a/cinder/volume/iscsi.py +++ b/cinder/volume/iscsi.py @@ -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() diff --git a/etc/cinder/rootwrap.d/volume.filters b/etc/cinder/rootwrap.d/volume.filters index a51cad891..3a22fa9cb 100644 --- a/etc/cinder/rootwrap.d/volume.filters +++ b/etc/cinder/rootwrap.d/volume.filters @@ -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 diff --git a/setup.py b/setup.py index 6b7cccf3a..b65ce9558 100644 --- 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'], diff --git a/tools/test-requires b/tools/test-requires index 02f256f11..cc907a4c1 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -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