]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add support for chiscsi iscsi helper
authorAnish Bhatt <anish@chelsio.com>
Fri, 16 Jan 2015 05:31:58 +0000 (21:31 -0800)
committerAnish Bhatt <anish@chelsio.com>
Mon, 16 Feb 2015 20:56:22 +0000 (12:56 -0800)
The chiscsi target works as a drop in replacement for IET/TGT with
minor configuration differences. This patch implements support for
this as the 'cxtadm' iscsi_helper.
Certification results :  https://bugs.launchpad.net/cinder/+bug/1417499

DocImpact

Implements: blueprint chiscsi-iscsi-helper

Change-Id: Ib8e94f532cd07fea44aaeeac266e7f6750bf00c1

cinder/tests/targets/test_cxt_driver.py [new file with mode: 0644]
cinder/volume/driver.py
cinder/volume/targets/cxt.py [new file with mode: 0644]
etc/cinder/rootwrap.d/volume.filters

diff --git a/cinder/tests/targets/test_cxt_driver.py b/cinder/tests/targets/test_cxt_driver.py
new file mode 100644 (file)
index 0000000..6182348
--- /dev/null
@@ -0,0 +1,208 @@
+# Copyright 2015 Chelsio Communications 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 contextlib
+import os
+import shutil
+import StringIO
+import tempfile
+
+import mock
+from oslo_utils import timeutils
+
+from cinder import context
+from cinder.openstack.common import fileutils
+from cinder import test
+from cinder import utils
+from cinder.volume import configuration as conf
+from cinder.volume.targets import cxt
+
+
+class TestCxtAdmDriver(test.TestCase):
+
+    def __init__(self, *args, **kwargs):
+        super(TestCxtAdmDriver, self).__init__(*args, **kwargs)
+        self.configuration = conf.Configuration(None)
+        self.configuration.append_config_values = mock.Mock(return_value=0)
+        self.configuration.iscsi_ip_address = '10.9.8.7'
+        self.cxt_subdir = cxt.CxtAdm.cxt_subdir
+        self.fake_id_1 = 'ed2c1fd4-5fc0-11e4-aa15-123b93f75cba'
+        self.fake_id_2 = 'ed2c2222-5fc0-11e4-aa15-123b93f75cba'
+        self.target = cxt.CxtAdm(root_helper=utils.get_root_helper(),
+                                 configuration=self.configuration)
+        self.fake_volume = 'volume-83c2e877-feed-46be-8435-77884fe55b45'
+        self.testvol_1 =\
+            {'project_id': self.fake_id_1,
+             'name': 'testvol',
+             'size': 1,
+             'id': self.fake_id_2,
+             'volume_type_id': None,
+             'provider_location': '10.9.8.7:3260 '
+                                  'iqn.2010-10.org.openstack:'
+                                  'volume-%s 0' % self.fake_id_2,
+             'provider_auth': 'CHAP stack-1-a60e2611875f40199931f2'
+                              'c76370d66b 2FE0CQ8J196R',
+             'provider_geometry': '512 512',
+             'created_at': timeutils.utcnow(),
+             'host': 'fake_host@lvm#lvm'}
+
+        self.expected_iscsi_properties = \
+            {'auth_method': 'CHAP',
+             'auth_password': '2FE0CQ8J196R',
+             'auth_username': 'stack-1-a60e2611875f40199931f2c76370d66b',
+             'encrypted': False,
+             'logical_block_size': '512',
+             'physical_block_size': '512',
+             'target_discovered': False,
+             'target_iqn': 'iqn.2010-10.org.openstack:volume-%s' %
+                           self.fake_id_2,
+             'target_lun': 0,
+             'target_portal': '10.10.7.1:3260',
+             'volume_id': self.fake_id_2}
+
+        self.fake_iscsi_scan =\
+            ('\n'
+             'TARGET: iqn.2010-10.org.openstack:%s, id=1, login_ip=0\n'  # noqa
+             '        PortalGroup=1@10.9.8.7:3260,timeout=0\n'
+             '        TargetDevice=/dev/stack-volumes-lvmdriver-1/%s,BLK,PROD=CHISCSI Target,SN=0N0743000000000,ID=0D074300000000000000000,WWN=:W00743000000000\n'  # noqa
+             % (self.fake_volume, self.fake_volume))
+
+    def setUp(self):
+        super(TestCxtAdmDriver, self).setUp()
+        self.fake_base_dir = tempfile.mkdtemp()
+        self.fake_volumes_dir = os.path.join(self.fake_base_dir,
+                                             self.cxt_subdir)
+        fileutils.ensure_tree(self.fake_volumes_dir)
+        self.addCleanup(self._cleanup)
+
+        self.exec_patcher = mock.patch.object(utils, 'execute')
+        self.mock_execute = self.exec_patcher.start()
+        self.addCleanup(self.exec_patcher.stop)
+
+    def _cleanup(self):
+        if os.path.exists(self.fake_base_dir):
+            shutil.rmtree(self.fake_base_dir)
+
+    @mock.patch('cinder.utils.execute')
+    def test_get_target(self, mock_execute):
+        mock_execute.return_value = (self.fake_iscsi_scan, None)
+        with mock.patch.object(self.target, '_get_volumes_dir') as mock_get:
+            mock_get.return_value = self.fake_volumes_dir
+            self.assertEqual('1',
+                             self.target._get_target(
+                                                     'iqn.2010-10.org.openstack:volume-83c2e877-feed-46be-8435-77884fe55b45'  # noqa
+                                                    ))
+            self.assertTrue(mock_execute.called)
+
+    def test_get_target_chap_auth(self):
+        tmp_file = StringIO.StringIO()
+        tmp_file.write(
+            'target:\n'
+            '        TargetName=iqn.2010-10.org.openstack:volume-83c2e877-feed-46be-8435-77884fe55b45\n'  # noqa
+            '        TargetDevice=/dev/stack-volumes-lvmdriver-1/volume-83c2e877-feed-46be-8435-77884fe55b45\n'  # noqa
+            '        PortalGroup=1@10.9.8.7:3260\n'
+            '        AuthMethod=CHAP\n'
+            '        Auth_CHAP_Policy=Oneway\n'
+            '        Auth_CHAP_Initiator="otzLy2UYbYfnP4zXLG5z":"234Zweo38VGBBvrpK9nt"\n'  # noqa
+        )
+        tmp_file.seek(0)
+        test_vol = ('iqn.2010-10.org.openstack:'
+                    'volume-83c2e877-feed-46be-8435-77884fe55b45')
+        expected = ('otzLy2UYbYfnP4zXLG5z', '234Zweo38VGBBvrpK9nt')
+        with mock.patch('__builtin__.open') as mock_open:
+            ctx = context.get_admin_context()
+            mock_open.return_value = contextlib.closing(tmp_file)
+            self.assertEqual(expected,
+                             self.target._get_target_chap_auth(ctx, test_vol))
+            self.assertTrue(mock_open.called)
+
+    @mock.patch('cinder.volume.targets.cxt.CxtAdm._get_target',
+                return_value=1)
+    @mock.patch('cinder.utils.execute')
+    def test_create_iscsi_target(self, mock_execute, mock_get_targ):
+        mock_execute.return_value = ('', '')
+        with mock.patch.object(self.target, '_get_volumes_dir') as mock_get:
+            mock_get.return_value = self.fake_volumes_dir
+            test_vol = 'iqn.2010-10.org.openstack:'\
+                       'volume-83c2e877-feed-46be-8435-77884fe55b45'
+            self.assertEqual(
+                1,
+                self.target.create_iscsi_target(
+                    test_vol,
+                    1,
+                    0,
+                    self.fake_volumes_dir))
+            self.assertTrue(mock_get.called)
+            self.assertTrue(mock_execute.called)
+            self.assertTrue(mock_get_targ.called)
+
+    @mock.patch('cinder.volume.targets.cxt.CxtAdm._get_target',
+                return_value=1)
+    @mock.patch('cinder.utils.execute')
+    def test_create_iscsi_target_already_exists(self, mock_execute,
+                                                mock_get_targ):
+        mock_execute.return_value = ('fake out', 'fake err')
+        with mock.patch.object(self.target, '_get_volumes_dir') as mock_get:
+            mock_get.return_value = self.fake_volumes_dir
+            test_vol = 'iqn.2010-10.org.openstack:'\
+                       'volume-83c2e877-feed-46be-8435-77884fe55b45'
+            self.assertEqual(
+                1,
+                self.target.create_iscsi_target(
+                    test_vol,
+                    1,
+                    0,
+                    self.fake_volumes_dir))
+            self.assertTrue(mock_get.called)
+            self.assertTrue(mock_get_targ.called)
+            self.assertTrue(mock_execute.called)
+
+    @mock.patch('cinder.volume.targets.cxt.CxtAdm._get_target',
+                return_value=1)
+    @mock.patch('cinder.utils.execute')
+    @mock.patch('cinder.volume.utils.generate_password',
+                return_value="P68eE7u9eFqDGexd28DQ")
+    @mock.patch('cinder.volume.utils.generate_username',
+                return_value="QZJbisGmn9AL954FNF4D")
+    def test_create_export(self, mock_user, mock_pass, mock_execute,
+                           mock_get_targ):
+        mock_execute.return_value = ('', '')
+        with mock.patch.object(self.target, '_get_volumes_dir') as mock_get:
+            mock_get.return_value = self.fake_volumes_dir
+
+            expected_result = {'location': '10.9.8.7:3260,1 '
+                               'iqn.2010-10.org.openstack:testvol 0',
+                               'auth': 'CHAP '
+                               'QZJbisGmn9AL954FNF4D P68eE7u9eFqDGexd28DQ'}
+
+            ctxt = context.get_admin_context()
+            self.assertEqual(expected_result,
+                             self.target.create_export(ctxt,
+                                                       self.testvol_1,
+                                                       self.fake_volumes_dir))
+            self.assertTrue(mock_get.called)
+            self.assertTrue(mock_execute.called)
+
+    def test_ensure_export(self):
+        ctxt = context.get_admin_context()
+        with mock.patch.object(self.target, 'create_iscsi_target'):
+            self.target.ensure_export(ctxt,
+                                      self.testvol_1,
+                                      self.fake_volumes_dir)
+            self.target.create_iscsi_target.assert_called_once_with(
+                'iqn.2010-10.org.openstack:testvol',
+                1, 0, self.fake_volumes_dir, None,
+                check_exit_code=False,
+                old_name=None)
index eb9cd552b655985558b639003f3f80f42f809705..cea0eea8340ed74c0985fa27bd1b203a15f537a6 100644 (file)
@@ -91,7 +91,8 @@ volume_opts = [
                default='tgtadm',
                help='iSCSI target user-land tool to use. tgtadm is default, '
                     'use lioadm for LIO iSCSI support, iseradm for the ISER '
-                    'protocol, or fake for testing.'),
+                    'protocol, iscsictl for Chelsio iSCSI Target or fake for '
+                    'testing.'),
     cfg.StrOpt('volumes_dir',
                default='$state_path/volumes',
                help='Volume configuration file storage '
@@ -99,6 +100,9 @@ volume_opts = [
     cfg.StrOpt('iet_conf',
                default='/etc/iet/ietd.conf',
                help='IET configuration file'),
+    cfg.StrOpt('chiscsi_conf',
+               default='/etc/chelsio-iscsi/chiscsi.conf',
+               help='Chiscsi (CXT) global defaults configuration file'),
     cfg.StrOpt('lio_initiator_iqns',
                default='',
                help='This option is deprecated and unused. '
@@ -252,7 +256,8 @@ class BaseVD(object):
             'iseradm': 'cinder.volume.targets.iser.ISERTgtAdm',
             'lioadm': 'cinder.volume.targets.lio.LioAdm',
             'tgtadm': 'cinder.volume.targets.tgt.TgtAdm',
-            'scstadmin': 'cinder.volume.targets.scst.SCSTAdm', }
+            'scstadmin': 'cinder.volume.targets.scst.SCSTAdm',
+            'iscsictl': 'cinder.volume.targets.cxt.CxtAdm'}
 
         # set True by manager after successful check_for_setup
         self._initialized = False
diff --git a/cinder/volume/targets/cxt.py b/cinder/volume/targets/cxt.py
new file mode 100644 (file)
index 0000000..4e5f913
--- /dev/null
@@ -0,0 +1,291 @@
+# Copyright 2015 Chelsio Communications 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 os
+import re
+
+from oslo_concurrency import processutils as putils
+from oslo_utils import netutils
+import six
+
+from cinder import exception
+from cinder.openstack.common import fileutils
+from cinder.i18n import _LI, _LW, _LE
+from cinder.openstack.common import log as logging
+from cinder import utils
+from cinder.volume.targets import iscsi
+
+LOG = logging.getLogger(__name__)
+
+
+class CxtAdm(iscsi.ISCSITarget):
+    """Chiscsi target configuration for block storage devices.
+    This includes things like create targets, attach, detach
+    etc.
+    """
+
+    TARGET_FMT = """
+               target:
+                       TargetName=%s
+                       TargetDevice=%s
+                       PortalGroup=1@%s
+                 """
+    TARGET_FMT_WITH_CHAP = """
+                         target:
+                                 TargetName=%s
+                                 TargetDevice=%s
+                                 PortalGroup=1@%s
+                                 AuthMethod=CHAP
+                                 Auth_CHAP_Policy=Oneway
+                                 Auth_CHAP_Initiator=%s
+                           """
+
+    cxt_subdir = 'cxt'
+
+    def __init__(self, *args, **kwargs):
+        super(CxtAdm, self).__init__(*args, **kwargs)
+        self.volumes_dir = self.configuration.safe_get('volumes_dir')
+        self.volumes_dir = os.path.join(self.volumes_dir, self.cxt_subdir)
+        self.config = self.configuration.safe_get('chiscsi_conf')
+
+    def _get_volumes_dir(self):
+        return self.volumes_dir
+
+    def _get_target(self, iqn):
+        # We can use target=iqn here, but iscsictl has no --brief mode, and
+        # this way we save on a lot of unnecessary parsing
+        (out, err) = utils.execute('iscsictl',
+                                   '-c',
+                                   'target=ALL',
+                                   run_as_root=True)
+        lines = out.split('\n')
+        for line in lines:
+            if iqn in line:
+                parsed = line.split()
+                tid = parsed[2]
+                return tid[3:].rstrip(',')
+
+        return None
+
+    def _get_iscsi_target(self, context, vol_id):
+        return 0
+
+    def _get_target_and_lun(self, context, volume):
+        lun = 0  # For chiscsi dev starts at lun 0
+        iscsi_target = 1
+        return iscsi_target, lun
+
+    def _ensure_iscsi_targets(self, context, host):
+        """Ensure that target ids have been created in datastore."""
+        # NOTE : This is probably not required for chiscsi
+        # TODO(jdg): In the future move all of the dependent stuff into the
+        # cooresponding target admin class
+        host_iscsi_targets = self.db.iscsi_target_count_by_host(context,
+                                                                host)
+        if host_iscsi_targets >= self.configuration.iscsi_num_targets:
+            return
+
+        # NOTE Chiscsi target ids start at 1.
+        target_end = self.configuration.iscsi_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 _get_target_chap_auth(self, context, name):
+        volumes_dir = self._get_volumes_dir()
+        vol_id = name.split(':')[1]
+        volume_path = os.path.join(volumes_dir, vol_id)
+
+        try:
+            with open(volume_path, 'r') as f:
+                volume_conf = f.read()
+        except IOError as e_fnf:
+            LOG.debug('Failed to open config for %(vol_id)s: %(e)s',
+                      {'vol_id': vol_id, 'e':
+                       six.text_type(e_fnf)})
+            # We don't run on anything non-linux
+            if e_fnf.errno == 2:
+                return None
+            else:
+                raise
+        except Exception as e_vol:
+            LOG.debug('Failed to open config for %(vol_id)s: %(e)s',
+                      {'vol_id': vol_id, 'e':
+                       six.text_type(e_vol)})
+            raise
+
+        m = re.search('Auth_CHAP_Initiator="(\w+)":"(\w+)"', volume_conf)
+        if m:
+            return (m.group(1), m.group(2))
+        LOG.debug('Failed to find CHAP auth from config for %s', vol_id)
+        return None
+
+    def create_iscsi_target(self, name, tid, lun, path,
+                            chap_auth=None, **kwargs):
+
+        (out, err) = utils.execute('iscsictl',
+                                   '-c',
+                                   'target=ALL',
+                                   run_as_root=True)
+        LOG.debug("Targets prior to update: %s", out)
+        volumes_dir = self._get_volumes_dir()
+        fileutils.ensure_tree(volumes_dir)
+
+        vol_id = name.split(':')[1]
+
+        if netutils.is_valid_ipv4(self.configuration.iscsi_ip_address):
+            portal = "%s:%s" % (self.configuration.iscsi_ip_address,
+                                self.configuration.iscsi_port)
+        else:
+            # ipv6 addresses use [ip]:port format, ipv4 use ip:port
+            portal = "[%s]:%s" % (self.configuration.iscsi_ip_address,
+                                  self.configuration.iscsi_port)
+
+        if chap_auth is None:
+            volume_conf = self.TARGET_FMT % (name, path, portal)
+        else:
+            volume_conf = self.TARGET_FMT_WITH_CHAP % (name,
+                                                       path, portal,
+                                                       '"%s":"%s"' % chap_auth)
+        LOG.debug('Creating iscsi_target for: %s', vol_id)
+        volume_path = os.path.join(volumes_dir, vol_id)
+
+        if os.path.exists(volume_path):
+            LOG.warning(_LW('Persistence file already exists for volume, '
+                            'found file at: %s'), volume_path)
+        f = open(volume_path, 'w+')
+        f.write(volume_conf)
+        f.close()
+        LOG.debug('Created volume path %(vp)s,\n'
+                  'content: %(vc)s',
+                  {'vp': volume_path, 'vc': volume_conf})
+
+        old_persist_file = None
+        old_name = kwargs.get('old_name', None)
+        if old_name:
+            LOG.debug('Detected old persistence file for volume '
+                      '%{vol}s at %{old_name}s',
+                      {'vol': vol_id, 'old_name': old_name})
+            old_persist_file = os.path.join(volumes_dir, old_name)
+
+        try:
+            # With the persistent tgts we create them
+            # by creating the entry in the persist file
+            # and then doing an update to get the target
+            # created.
+            (out, err) = utils.execute('iscsictl', '-S', 'target=%s' % name,
+                                       '-f', volume_path,
+                                       '-x', self.config,
+                                       run_as_root=True)
+        except putils.ProcessExecutionError as e:
+            LOG.error(_LE("Failed to create iscsi target for volume "
+                          "id:%(vol_id)s: %(e)s"),
+                      {'vol_id': vol_id, 'e': e})
+
+            # Don't forget to remove the persistent file we created
+            os.unlink(volume_path)
+            raise exception.ISCSITargetCreateFailed(volume_id=vol_id)
+        finally:
+            LOG.debug("StdOut from iscsictl -S: %s", out)
+            LOG.debug("StdErr from iscsictl -S: %s", err)
+
+        # Grab targets list for debug
+        (out, err) = utils.execute('iscsictl',
+                                   '-c',
+                                   'target=ALL',
+                                   run_as_root=True)
+        LOG.debug("Targets after update: %s", out)
+
+        iqn = '%s%s' % (self.iscsi_target_prefix, vol_id)
+        tid = self._get_target(iqn)
+        if tid is None:
+            LOG.error(_LE("Failed to create iscsi target for volume "
+                          "id:%(vol_id)s. Please verify your configuration "
+                          "in %(volumes_dir)'"), {
+                      'vol_id': vol_id,
+                      'volumes_dir': volumes_dir, })
+            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_iscsi_target(self, tid, lun, vol_id, vol_name, **kwargs):
+        LOG.info(_LI('Removing iscsi_target for: %s'), vol_id)
+        vol_uuid_file = vol_name
+        volume_path = os.path.join(self._get_volumes_dir(), vol_uuid_file)
+        if not os.path.exists(volume_path):
+            LOG.warning(_LW('Volume path %s does not exist, '
+                            'nothing to remove.'), volume_path)
+            return
+
+        if os.path.isfile(volume_path):
+            iqn = '%s%s' % (self.iscsi_target_prefix,
+                            vol_uuid_file)
+        else:
+            raise exception.ISCSITargetRemoveFailed(volume_id=vol_id)
+
+        target_exists = False
+        try:
+            (out, err) = utils.execute('iscsictl',
+                                       '-c',
+                                       'target=%s' % iqn,
+                                       run_as_root=True)
+            LOG.debug("StdOut from iscsictl -c: %s", out)
+            LOG.debug("StdErr from iscsictl -c: %s", err)
+        except putils.ProcessExecutionError as e:
+            if "NOT found" in e.stdout:
+                LOG.info(_LI("No iscsi target present for volume "
+                             "id:%(vol_id)s: %(e)s"),
+                         {'vol_id': vol_id, 'e': e})
+                return
+            else:
+                raise exception.ISCSITargetRemoveFailed(volume_id=vol_id)
+        else:
+            target_exists = True
+
+        try:
+            utils.execute('iscsictl',
+                          '-s',
+                          'target=%s' % iqn,
+                          run_as_root=True)
+        except putils.ProcessExecutionError as e:
+            # There exists a race condition where multiple calls to
+            # remove_iscsi_target come in simultaneously. If we can poll
+            # for a target successfully but it is gone before we can remove
+            # it, fail silently
+            if "is not found" in e.stderr and target_exists:
+                LOG.info(_LI("No iscsi target present for volume "
+                             "id:%(vol_id)s: %(e)s"),
+                         {'vol_id': vol_id, 'e': e})
+                return
+            else:
+                LOG.error(_LE("Failed to remove iscsi target for volume "
+                              "id:%(vol_id)s: %(e)s"),
+                          {'vol_id': vol_id, 'e': e})
+                raise exception.ISCSITargetRemoveFailed(volume_id=vol_id)
+
+        # Carried over from tgt
+        # NOTE(jdg): This *should* be there still but incase
+        # it's not we don't care, so just ignore it if was
+        # somehow deleted between entry of this method
+        # and here
+        if os.path.exists(volume_path):
+            os.unlink(volume_path)
+        else:
+            LOG.debug('Volume path %s not found at end, '
+                      'of remove_iscsi_target.', volume_path)
index 999026a7b50c6097e4ee12e33e30f3463e0b5deb..05c8d23aa4a431729d5f96680b8fda9371894a6e 100644 (file)
@@ -5,6 +5,7 @@
 # cinder/volume/iscsi.py: iscsi_helper '--op' ...
 ietadm: CommandFilter, ietadm, root
 tgtadm: CommandFilter, tgtadm, root
+iscsictl: CommandFilter, iscsictl, root
 tgt-admin: CommandFilter, tgt-admin, root
 cinder-rtstool: CommandFilter, cinder-rtstool, root
 scstadmin: CommandFilter, scstadmin, root