Implement IET target driver

When introducing Change-Id: I43190d1dac33748fe55fa00f260f32ab209be656,
IET driver was not implemented. This patch adds IET target for new
iscsi target driver model.

Certification results:
    https://bugs.launchpad.net/cinder/+bug/1428758

Closes-Bug: #1409918
Closes-Bug: #1329139

Co-Authored-By: Anish Bhatt <anish@chelsio.com>
Change-Id: I165a592bb3a39728fcc3d8ee4162b579c13ba928
This commit is contained in:
Mitsuhiro Tanino 2015-02-25 19:16:39 -05:00
parent ef6cacd3c1
commit 8bb865a0aa
2 changed files with 576 additions and 10 deletions

View File

@ -0,0 +1,283 @@
# 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)

View File

@ -10,24 +10,307 @@
# 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):
def _get_target(self, iqn):
# 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
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
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 _get_iscsi_target(self, context, vol_id):
pass
def ensure_export(self, context, volume, volume_path):
pass
def _get_target_and_lun(self, context, volume):
def create_export(self, context, volume, volume_path):
pass
# For ietadm dev starts at lun 0
lun = 0
def remove_export(self, context, volume):
pass
# Using 0, ietadm tries to search empty tid for creating
# new iSCSI target
iscsi_target = 0
def initialize_connection(self, volume, connector):
pass
# 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)