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
This commit is contained in:
Anish Bhatt 2015-01-15 21:31:58 -08:00
parent 7cd5fe6bb5
commit b43b36e9fa
4 changed files with 507 additions and 2 deletions

View File

@ -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)

View 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

View File

@ -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)

View 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