--- /dev/null
+# 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)
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 '
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. '
'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
--- /dev/null
+# 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)
# 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