From b43b36e9fa3c44845150c92f88ce9fb26e056fff Mon Sep 17 00:00:00 2001 From: Anish Bhatt Date: Thu, 15 Jan 2015 21:31:58 -0800 Subject: [PATCH] Add support for chiscsi iscsi helper 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 | 208 +++++++++++++++++ cinder/volume/driver.py | 9 +- cinder/volume/targets/cxt.py | 291 ++++++++++++++++++++++++ etc/cinder/rootwrap.d/volume.filters | 1 + 4 files changed, 507 insertions(+), 2 deletions(-) create mode 100644 cinder/tests/targets/test_cxt_driver.py create mode 100644 cinder/volume/targets/cxt.py diff --git a/cinder/tests/targets/test_cxt_driver.py b/cinder/tests/targets/test_cxt_driver.py new file mode 100644 index 000000000..6182348e5 --- /dev/null +++ b/cinder/tests/targets/test_cxt_driver.py @@ -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) diff --git a/cinder/volume/driver.py b/cinder/volume/driver.py index eb9cd552b..cea0eea83 100644 --- a/cinder/volume/driver.py +++ b/cinder/volume/driver.py @@ -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 index 000000000..4e5f9134b --- /dev/null +++ b/cinder/volume/targets/cxt.py @@ -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) diff --git a/etc/cinder/rootwrap.d/volume.filters b/etc/cinder/rootwrap.d/volume.filters index 999026a7b..05c8d23aa 100644 --- a/etc/cinder/rootwrap.d/volume.filters +++ b/etc/cinder/rootwrap.d/volume.filters @@ -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 -- 2.45.2