--- /dev/null
+# 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_concurrency import processutils as putils
+from oslo_utils import timeutils
+
+from cinder import context
+from cinder import exception
+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 iet
+
+
+class TestIetAdmDriver(test.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestIetAdmDriver, 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.fake_project_id = 'ed2c1fd4-5fc0-11e4-aa15-123b93f75cba'
+ self.fake_volume_id = '83c2e877-feed-46be-8435-77884fe55b45'
+ self.target = iet.IetAdm(root_helper=utils.get_root_helper(),
+ configuration=self.configuration)
+ self.testvol =\
+ {'project_id': self.fake_project_id,
+ 'name': 'testvol',
+ 'size': 1,
+ 'id': self.fake_volume_id,
+ 'volume_type_id': None,
+ 'provider_location': '10.9.8.7:3260 '
+ 'iqn.2010-10.org.openstack:'
+ 'volume-%s 0' % self.fake_volume_id,
+ '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_volume_id,
+ 'target_lun': 0,
+ 'target_portal': '10.10.7.1:3260',
+ 'volume_id': self.fake_volume_id}
+
+ def setUp(self):
+ super(TestIetAdmDriver, self).setUp()
+ self.fake_volumes_dir = tempfile.mkdtemp()
+ 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_volumes_dir):
+ shutil.rmtree(self.fake_volumes_dir)
+
+ def test_get_target(self):
+ tmp_file = StringIO.StringIO()
+ tmp_file.write(
+ 'tid:1 name:iqn.2010-10.org.openstack:volume-83c2e877-feed-46be-8435-77884fe55b45\n' # noqa
+ ' sid:844427031282176 initiator:iqn.1994-05.com.redhat:5a6894679665\n' # noqa
+ ' cid:0 ip:10.9.8.7 state:active hd:none dd:none')
+ tmp_file.seek(0)
+ with mock.patch('__builtin__.open') as mock_open:
+ mock_open.return_value = contextlib.closing(tmp_file)
+ self.assertEqual('1',
+ self.target._get_target(
+ 'iqn.2010-10.org.openstack:volume-83c2e877-feed-46be-8435-77884fe55b45' # noqa
+ ))
+
+ # Test the failure case: Failed to handle the config file
+ mock_open.side_effect = StandardError()
+ self.assertRaises(StandardError,
+ self.target._get_target,
+ '')
+
+ @mock.patch('os.path.exists', return_value=True)
+ @mock.patch('cinder.utils.temporary_chown')
+ def test_get_target_chap_auth(self, mock_chown, mock_exists):
+ tmp_file = StringIO.StringIO()
+ tmp_file.write(
+ 'Target iqn.2010-10.org.openstack:volume-83c2e877-feed-46be-8435-77884fe55b45\n' # noqa
+ ' IncomingUser otzLy2UYbYfnP4zXLG5z 234Zweo38VGBBvrpK9nt\n'
+ ' Lun 0 Path=/dev/stack-volumes-lvmdriver-1/volume-83c2e877-feed-46be-8435-77884fe55b45,Type=fileio\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:
+ ictx = context.get_admin_context()
+ mock_open.return_value = contextlib.closing(tmp_file)
+ self.assertEqual(expected,
+ self.target._get_target_chap_auth(ictx, test_vol))
+ self.assertTrue(mock_open.called)
+
+ # Test the failure case: Failed to handle the config file
+ mock_open.side_effect = StandardError()
+ self.assertRaises(StandardError,
+ self.target._get_target_chap_auth,
+ ictx,
+ test_vol)
+
+ @mock.patch('cinder.volume.targets.iet.IetAdm._get_target',
+ return_value=0)
+ @mock.patch('cinder.utils.execute')
+ @mock.patch('os.path.exists', return_value=True)
+ @mock.patch('cinder.utils.temporary_chown')
+ def test_create_iscsi_target(self, mock_chown, mock_exists,
+ mock_execute, mock_get_targ):
+ mock_execute.return_value = ('', '')
+ tmp_file = StringIO.StringIO()
+ test_vol = ('iqn.2010-10.org.openstack:'
+ 'volume-83c2e877-feed-46be-8435-77884fe55b45')
+ with mock.patch('__builtin__.open') as mock_open:
+ mock_open.return_value = contextlib.closing(tmp_file)
+ self.assertEqual(
+ 0,
+ self.target.create_iscsi_target(
+ test_vol,
+ 0,
+ 0,
+ self.fake_volumes_dir))
+ self.assertTrue(mock_execute.called)
+ self.assertTrue(mock_open.called)
+ self.assertTrue(mock_get_targ.called)
+
+ # Test the failure case: Failed to chown the config file
+ mock_open.side_effect = putils.ProcessExecutionError
+ self.assertRaises(exception.ISCSITargetCreateFailed,
+ self.target.create_iscsi_target,
+ test_vol,
+ 0,
+ 0,
+ self.fake_volumes_dir)
+
+ # Test the failure case: Failed to set new auth
+ mock_execute.side_effect = putils.ProcessExecutionError
+ self.assertRaises(exception.ISCSITargetCreateFailed,
+ self.target.create_iscsi_target,
+ test_vol,
+ 0,
+ 0,
+ self.fake_volumes_dir)
+
+ @mock.patch('cinder.utils.execute')
+ @mock.patch('os.path.exists', return_value=True)
+ def test_update_config_file_failure(self, mock_exists, mock_execute):
+ test_vol = ('iqn.2010-10.org.openstack:'
+ 'volume-83c2e877-feed-46be-8435-77884fe55b45')
+
+ # Test the failure case: conf file does not exist
+ mock_exists.return_value = False
+ mock_execute.side_effect = putils.ProcessExecutionError
+ self.assertRaises(exception.ISCSITargetCreateFailed,
+ self.target.update_config_file,
+ test_vol,
+ 0,
+ self.fake_volumes_dir,
+ "foo bar")
+
+ @mock.patch('cinder.volume.targets.iet.IetAdm._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')
+ 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_targ.called)
+ self.assertTrue(mock_execute.called)
+
+ @mock.patch('cinder.volume.targets.iet.IetAdm._find_sid_cid_for_target',
+ return_value=None)
+ @mock.patch('os.path.exists', return_value=False)
+ @mock.patch.object(utils, 'execute')
+ def test_remove_iscsi_target(self, mock_execute, mock_exists, mock_find):
+
+ # Test the normal case
+ self.target.remove_iscsi_target(1,
+ 0,
+ self.testvol['id'],
+ self.testvol['name'])
+ mock_execute.assert_any_calls('ietadm',
+ '--op',
+ 'delete',
+ '--tid=1',
+ run_as_root=True)
+
+ # Test the failure case: putils.ProcessExecutionError
+ mock_execute.side_effect = putils.ProcessExecutionError
+ self.assertRaises(exception.ISCSITargetRemoveFailed,
+ self.target.remove_iscsi_target,
+ 1,
+ 0,
+ self.testvol['id'],
+ self.testvol['name'])
+
+ def test_find_sid_cid_for_target(self):
+ tmp_file = StringIO.StringIO()
+ tmp_file.write(
+ 'tid:1 name:iqn.2010-10.org.openstack:volume-83c2e877-feed-46be-8435-77884fe55b45\n' # noqa
+ ' sid:844427031282176 initiator:iqn.1994-05.com.redhat:5a6894679665\n' # noqa
+ ' cid:0 ip:10.9.8.7 state:active hd:none dd:none')
+ tmp_file.seek(0)
+ with mock.patch('__builtin__.open') as mock_open:
+ mock_open.return_value = contextlib.closing(tmp_file)
+ self.assertEqual(('844427031282176', '0'),
+ self.target._find_sid_cid_for_target(
+ '1',
+ 'iqn.2010-10.org.openstack:volume-83c2e877-feed-46be-8435-77884fe55b45', # noqa
+ 'volume-83c2e877-feed-46be-8435-77884fe55b45' # noqa
+ ))
+
+ @mock.patch('cinder.volume.targets.iet.IetAdm._get_target',
+ return_value=1)
+ @mock.patch('cinder.utils.execute')
+ @mock.patch.object(iet.IetAdm, '_get_target_chap_auth')
+ def test_create_export(self, mock_get_chap, mock_execute,
+ mock_get_targ):
+ mock_execute.return_value = ('', '')
+ mock_get_chap.return_value = ('QZJbisGmn9AL954FNF4D',
+ 'P68eE7u9eFqDGexd28DQ')
+ 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,
+ self.fake_volumes_dir))
+ self.assertTrue(mock_execute.called)
+
+ @mock.patch('cinder.volume.targets.iet.IetAdm._get_target',
+ return_value=1)
+ def test_ensure_export(self, mock_get_target):
+ ctxt = context.get_admin_context()
+ with mock.patch.object(self.target, 'create_iscsi_target'):
+ self.target.ensure_export(ctxt,
+ self.testvol,
+ 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)
# License for the specific language governing permissions and limitations
# under the License.
+import os
+import re
+import stat
-class IetAdm(object):
+from oslo_concurrency import processutils as putils
+from oslo_log import log as logging
+
+from cinder import exception
+from cinder.i18n import _LI, _LE, _LW
+from cinder import utils
+from cinder.volume.targets import iscsi
+
+LOG = logging.getLogger(__name__)
+
+
+class IetAdm(iscsi.ISCSITarget):
VERSION = '0.1'
def __init__(self, *args, **kwargs):
super(IetAdm, self).__init__(*args, **kwargs)
+ self.iet_conf = self.configuration.safe_get('iet_conf')
+ self.iscsi_iotype = self.configuration.safe_get('iscsi_iotype')
+ self.auth_type = 'IncomingUser'
+ self.iet_sessions = '/proc/net/iet/session'
- def _get_target_chap_auth(self, name):
- pass
+ def _get_target(self, iqn):
- def ensure_export(self, context, volume, volume_path):
- pass
+ # Find existing iSCSI target session from /proc/net/iet/session
+ #
+ # tid:2 name:iqn.2010-10.org:volume-222
+ # sid:562950561399296 initiator:iqn.1994-05.com:5a6894679665
+ # cid:0 ip:192.168.122.1 state:active hd:none dd:none
+ # tid:1 name:iqn.2010-10.org:volume-111
+ # sid:281475567911424 initiator:iqn.1994-05.com:5a6894679665
+ # cid:0 ip:192.168.122.1 state:active hd:none dd:none
- def create_export(self, context, volume, volume_path):
- pass
+ iscsi_target = 0
+ try:
+ with open(self.iet_sessions, 'r') as f:
+ sessions = f.read()
+ except Exception:
+ LOG.exception(_LE("Failed to open iet session list for %s"), iqn)
+ raise
- def remove_export(self, context, volume):
- pass
+ session_list = re.split('^tid:(?m)', sessions)[1:]
+ for ses in session_list:
+ m = re.match('(\d+) name:(\S+)\s+', ses)
+ if m and iqn in m.group(2):
+ return m.group(1)
+
+ return iscsi_target
- def initialize_connection(self, volume, connector):
+ def _get_iscsi_target(self, context, vol_id):
pass
+
+ def _get_target_and_lun(self, context, volume):
+
+ # For ietadm dev starts at lun 0
+ lun = 0
+
+ # Using 0, ietadm tries to search empty tid for creating
+ # new iSCSI target
+ iscsi_target = 0
+
+ # Find existing iSCSI target based on iqn
+ iqn = '%svolume-%s' % (self.iscsi_target_prefix, volume['id'])
+ iscsi_target = self._get_target(iqn)
+
+ return iscsi_target, lun
+
+ def _get_target_chap_auth(self, context, name):
+
+ vol_id = name.split(':')[1]
+ if os.path.exists(self.iet_conf):
+ try:
+ with utils.temporary_chown(self.iet_conf):
+ with open(self.iet_conf, 'r') as f:
+ iet_conf_text = f.readlines()
+ except Exception:
+ # If we fail to handle config file, raise exception here to
+ # prevent unexpected behavior during subsequent operations.
+ LOG.exception(_LE("Failed to open config for %s."), vol_id)
+ raise
+
+ target_found = False
+ for line in iet_conf_text:
+ if target_found:
+ m = re.search('(\w+) (\w+) (\w+)', line)
+ if m:
+ return (m.group(2), m.group(3))
+ else:
+ LOG.debug("Failed to find CHAP auth from config "
+ "for %s", vol_id)
+ return None
+ elif name in line:
+ target_found = True
+ else:
+ # Missing config file is unxepected sisuation. But we will create
+ # new config file during create_iscsi_target(). Just we warn the
+ # operator here.
+ LOG.warn(_LW("Failed to find CHAP auth from config for "
+ "%(vol_id)s. Config file %(conf)s does not exist."),
+ {'vol_id': vol_id, 'conf': self.iet_conf})
+ return None
+
+ def create_iscsi_target(self, name, tid, lun, path,
+ chap_auth=None, **kwargs):
+
+ config_auth = None
+ vol_id = name.split(':')[1]
+
+ # Check the target is already existing.
+ tmp_tid = self._get_target(name)
+
+ # Create a new iSCSI target. If a target already exists,
+ # the command returns 234, but we ignore it.
+ try:
+ self._new_target(name, tid)
+ tid = self._get_target(name)
+ self._new_logicalunit(tid, lun, path)
+
+ if chap_auth is not None:
+ (username, password) = chap_auth
+ config_auth = ' '.join((self.auth_type,) + chap_auth)
+ self._new_auth(tid, self.auth_type, username, password)
+ except putils.ProcessExecutionError:
+ LOG.exception(_LE("Failed to create iscsi target for volume "
+ "id:%s"), vol_id)
+ raise exception.ISCSITargetCreateFailed(volume_id=vol_id)
+
+ # Update config file only if new scsi target is created.
+ if not tmp_tid:
+ self.update_config_file(name, tid, path, config_auth)
+
+ return tid
+
+ def update_config_file(self, name, tid, path, config_auth):
+
+ conf_file = self.iet_conf
+ vol_id = name.split(':')[1]
+
+ # If config file does not exist, create a blank conf file and
+ # add configuration for the volume on the new file.
+ if not os.path.exists(conf_file):
+ try:
+ utils.execute("truncate", conf_file, "--size=0",
+ run_as_root=True)
+ except putils.ProcessExecutionError:
+ LOG.exception(_LE("Failed to create %(conf)s for volume "
+ "id:%(vol_id)s"),
+ {'conf': conf_file, 'vol_id': vol_id})
+ raise exception.ISCSITargetCreateFailed(volume_id=vol_id)
+
+ try:
+ volume_conf = """
+ Target %s
+ %s
+ Lun 0 Path=%s,Type=%s
+ """ % (name, config_auth, path, self._iotype(path))
+
+ with utils.temporary_chown(conf_file):
+ with open(conf_file, 'a+') as f:
+ f.write(volume_conf)
+ except Exception:
+ LOG.exception(_LE("Failed to update %(conf)s for volume "
+ "id:%(vol_id)s"),
+ {'conf': conf_file, 'vol_id': vol_id})
+ raise exception.ISCSITargetCreateFailed(volume_id=vol_id)
+
+ def remove_iscsi_target(self, tid, lun, vol_id, vol_name, **kwargs):
+ LOG.info(_LI("Removing iscsi_target for volume: %s"), vol_id)
+
+ try:
+ self._delete_logicalunit(tid, lun)
+ session_info = self._find_sid_cid_for_target(tid, vol_name, vol_id)
+ if session_info:
+ sid, cid = session_info
+ self._force_delete_target(tid, sid, cid)
+
+ self._delete_target(tid)
+ except putils.ProcessExecutionError:
+ LOG.exception(_LE("Failed to remove iscsi target for volume "
+ "id:%s"), vol_id)
+ raise exception.ISCSITargetRemoveFailed(volume_id=vol_id)
+
+ vol_uuid_file = vol_name
+ conf_file = self.iet_conf
+ if os.path.exists(conf_file):
+ try:
+ with utils.temporary_chown(conf_file):
+ with open(conf_file, 'r+') as iet_conf_text:
+ full_txt = iet_conf_text.readlines()
+ new_iet_conf_txt = []
+ count = 0
+ for line in full_txt:
+ if count > 0:
+ count -= 1
+ continue
+ elif vol_uuid_file in line:
+ count = 2
+ continue
+ else:
+ new_iet_conf_txt.append(line)
+
+ iet_conf_text.seek(0)
+ iet_conf_text.truncate(0)
+ iet_conf_text.writelines(new_iet_conf_txt)
+ except Exception:
+ LOG.exception(_LE("Failed to update %(conf)s for volume id "
+ "%(vol_id) after removing iscsi target"),
+ {'conf': conf_file, 'vol_id': vol_id})
+ raise exception.ISCSITargetRemoveFailed(volume_id=vol_id)
+ else:
+ LOG.warn(_LW("Failed to update %(conf)s for volume id %(vol_id) "
+ "after removing iscsi target. "
+ "%(conf)s does not exist."),
+ {'conf': conf_file, 'vol_id': vol_id})
+
+ def _find_sid_cid_for_target(self, tid, name, vol_id):
+ """Find sid, cid for existing iscsi target"""
+
+ try:
+ with open(self.iet_sessions, 'r') as f:
+ sessions = f.read()
+ except Exception as e:
+ LOG.info(_LI("Failed to open iet session list for "
+ "%(vol_id)s: %(e)s"),
+ {'vol_id': vol_id, 'e': e})
+ return None
+
+ session_list = re.split('^tid:(?m)', sessions)[1:]
+ for ses in session_list:
+ m = re.match('(\d+) name:(\S+)\s+sid:(\d+).+\s+cid:(\d+)', ses)
+ if m and tid in m.group(1) and name in m.group(2):
+ return m.group(3), m.group(4)
+
+ def _is_block(self, path):
+ mode = os.stat(path).st_mode
+ return stat.S_ISBLK(mode)
+
+ def _iotype(self, path):
+ if self.iscsi_iotype == 'auto':
+ return 'blockio' if self._is_block(path) else 'fileio'
+ else:
+ return self.iscsi_iotype
+
+ def _new_target(self, name, tid):
+ """Create new scsi target using specified parameters.
+
+ If the target already exists, ietadm returns
+ 'Invalid argument' and error code '234'.
+ This should be ignored for ensure export case.
+ """
+ utils.execute('ietadm', '--op', 'new',
+ '--tid=%s' % tid,
+ '--params', 'Name=%s' % name,
+ run_as_root=True, check_exit_code=[0, 234])
+
+ def _delete_target(self, tid):
+ utils.execute('ietadm', '--op', 'delete',
+ '--tid=%s' % tid,
+ run_as_root=True)
+
+ def _force_delete_target(self, tid, sid, cid):
+ utils.execute('ietadm', '--op', 'delete',
+ '--tid=%s' % tid,
+ '--sid=%s' % sid,
+ '--cid=%s' % cid,
+ run_as_root=True)
+
+ def show_target(self, tid, iqn=None):
+ utils.execute('ietadm', '--op', 'show',
+ '--tid=%s' % tid,
+ run_as_root=True)
+
+ def _new_logicalunit(self, tid, lun, path):
+ """Attach a new volume to scsi target as a logical unit.
+
+ If a logical unit exists on the specified target lun,
+ ietadm returns 'File exists' and error code '239'.
+ This should be ignored for ensure export case.
+ """
+
+ utils.execute('ietadm', '--op', 'new',
+ '--tid=%s' % tid,
+ '--lun=%d' % lun,
+ '--params',
+ 'Path=%s,Type=%s' % (path, self._iotype(path)),
+ run_as_root=True, check_exit_code=[0, 239])
+
+ def _delete_logicalunit(self, tid, lun):
+ utils.execute('ietadm', '--op', 'delete',
+ '--tid=%s' % tid,
+ '--lun=%d' % lun,
+ run_as_root=True)
+
+ def _new_auth(self, tid, type, username, password):
+ utils.execute('ietadm', '--op', 'new',
+ '--tid=%s' % tid,
+ '--user',
+ '--params=%s=%s,Password=%s' % (type,
+ username,
+ password),
+ run_as_root=True)