Add iSCSI SCST Target support to cinder

Currently cinder supports tgt,lio targets.
We are adding SCST Target support in cinder.

Change-Id: I449143d125808d28758f3d438d7d1476a544c38b
Implements: blueprint scst-iscsi-backend-support
This commit is contained in:
nikeshmahalka 2015-01-08 18:38:19 +05:30
parent 734f98f309
commit e7e1b1466c
6 changed files with 608 additions and 2 deletions

View File

@ -914,6 +914,10 @@ class ISCSITargetDetachFailed(CinderException):
message = _("Failed to detach iSCSI target for volume %(volume_id)s.")
class ISCSITargetHelperCommandFailed(CinderException):
message = _("%(error_message)s")
# X-IO driver exception.
class XIODriverException(VolumeDriverException):
message = _("X-IO Volume Driver exception!")

View File

@ -0,0 +1,237 @@
# 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 tempfile
import mock
from oslo_utils import timeutils
from cinder import context
from cinder import test
from cinder import utils
from cinder.volume import configuration as conf
from cinder.volume.targets import scst
from cinder.volume import utils as vutils
class TestSCSTAdmDriver(test.TestCase):
def setUp(self):
super(TestSCSTAdmDriver, self).setUp()
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_volumes_dir = tempfile.mkdtemp()
self.fake_id_1 = 'ed2c1fd4-5fc0-11e4-aa15-123b93f75cba'
self.fake_id_2 = 'ed2c2222-5fc0-11e4-aa15-123b93f75cba'
self.fake_id_3 = 'ed2c3333-5fc0-11e4-aa15-123b93f75cba'
self.fake_id_4 = 'ed2c4444-5fc0-11e4-aa15-123b93f75cba'
self.stubs.Set(self.configuration, 'safe_get', self.fake_safe_get)
self.target = scst.SCSTAdm(root_helper=utils.get_root_helper(),
configuration=self.configuration)
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 1' % 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.testvol_2 =\
{'project_id': self.fake_id_3,
'name': 'testvol2',
'size': 1,
'id': self.fake_id_4,
'volume_type_id': None,
'provider_location': '10.9.8.7:3260 '
'iqn.2010-10.org.openstack:'
'volume-%s 2' % self.fake_id_4,
'provider_auth': 'CHAP stack-1-a60e2611875f40199931f2'
'c76370d66b 2FE0CQ8J196R',
'provider_geometry': '512 512',
'created_at': timeutils.utcnow(),
'host': 'fake_host@lvm#lvm'}
self.fake_iscsi_scan = \
('Collecting current configuration: done.\n'
'Driver Target\n'
'----------------------------------------------\n'
'iscsi iqn.2010-10.org.openstack:'
'volume-ed2c2222-5fc0-11e4-aa15-123b93f75cba\n'
'All done.\n')
self.fake_iscsi_attribute_scan = \
('Collecting current configuration: done.\n'
'Attribute Value Writable KEY\n'
'------------------------------------------\n'
'rel_tgt_id 1 Yes Yes\n'
'Dynamic attributes available\n'
'----------------------------\n'
'IncomingUser\n'
'OutgoingUser\n'
'allowed_portal\n'
'LUN CREATE attributes available\n'
'-------------------------------\n'
'read_only\n'
'All done.\n')
self.fake_list_group = \
('org.openstack:volume-vedams\n'
'Collecting current configuration: done.\n'
'Driver: iscsi\n'
'Target: iqn.2010-10.org.openstack:volume-vedams\n'
'Driver/target \'iscsi/iqn.2010-10.org.openstack:volume-vedams\''
'has no associated LUNs.\n'
'Group: iqn.1993-08.org.debian:01:626bf14ebdc\n'
'Assigned LUNs:\n'
'LUN Device\n'
'------------------\n'
'1 1b67387810256\n'
'2 2a0f1cc9cd595\n'
'Assigned Initiators:\n'
'Initiator\n'
'-------------------------------------\n'
'iqn.1993-08.org.debian:01:626bf14ebdc\n'
'All done.\n')
self.target.db = mock.MagicMock(
volume_get=lambda x, y: {'provider_auth': 'IncomingUser foo bar'})
def fake_safe_get(self, value):
if value == 'volumes_dir':
return self.fake_volumes_dir
@mock.patch.object(utils, 'execute')
@mock.patch.object(scst.SCSTAdm, '_target_attribute')
@mock.patch.object(scst.SCSTAdm, 'scst_execute')
def test_get_target(self, mock_execute,
mock_target_attribute,
mock_scst_execute):
mock_target_attribute.return_value = 1
mock_execute.return_value = (self.fake_iscsi_scan, None)
expected = 1
self.assertEqual(expected, self.target._get_target(
'iqn.2010-10.org.openstack:'
'volume-ed2c2222-5fc0-11e4-aa15-123b93f75cba'))
@mock.patch.object(utils, 'execute')
def test_target_attribute(self, mock_execute):
mock_execute.return_value = (self.fake_iscsi_attribute_scan, None)
self.assertEqual(str(1), self.target._target_attribute(
'iqn.2010-10.org.openstack:'
'volume-ed2c2222-5fc0-11e4-aa15-123b93f75cba'))
def test_single_lun_get_target_and_lun(self):
ctxt = context.get_admin_context()
self.assertEqual((0, 1), self.target._get_target_and_lun(
ctxt, self.testvol_1))
@mock.patch.object(utils, 'execute')
@mock.patch.object(scst.SCSTAdm, '_get_group')
@mock.patch.object(scst.SCSTAdm, 'scst_execute')
def test_multi_lun_get_target_and_lun(self, mock_execute, mock_get_group,
mock_scst_execute):
mock_execute.return_value = (self.fake_list_group, None)
mock_get_group.return_value = self.fake_list_group
self.stubs.Set(self.target,
'target_name',
'iqn.2010-10.org.openstack:volume-vedams')
ctxt = context.get_admin_context()
self.assertEqual((0, 3), self.target._get_target_and_lun(
ctxt, self.testvol_1))
@mock.patch.object(utils, 'execute')
@mock.patch.object(scst.SCSTAdm, '_get_target')
@mock.patch.object(scst.SCSTAdm, 'scst_execute')
def test_create_iscsi_target(self, mock_execute, mock_get_target,
mock_scst_execute):
mock_execute.return_value = (None, None)
mock_get_target.return_value = 1
self.assertEqual(1,
self.target.create_iscsi_target(
'iqn.2010-10.org.openstack:'
'volume-ed2c2222-5fc0-11e4-aa15-123b93f75cba',
'vol1',
0, 1, self.fake_volumes_dir))
@mock.patch.object(utils, 'execute')
@mock.patch.object(scst.SCSTAdm, '_get_target')
@mock.patch.object(scst.SCSTAdm, 'scst_execute')
def test_create_export(self, mock_execute,
mock_get_target,
mock_scst_execute):
mock_execute.return_value = (None, None)
mock_scst_execute.return_value = (None, None)
mock_get_target.return_value = 1
def _fake_get_target_and_lun(*args, **kwargs):
return 0, 1
def _fake_iscsi_location(*args, **kwargs):
return '10.9.8.7:3260,1 iqn.2010-10.org.openstack:' \
'volume-ed2c2222-5fc0-11e4-aa15-123b93f75cba 1'
self.stubs.Set(self.target,
'_get_target_and_lun',
_fake_get_target_and_lun)
self.stubs.Set(self.target,
'initiator_iqn',
'iqn.1993-08.org.debian:01:626bf14ebdc')
self.stubs.Set(self.target,
'_iscsi_location',
_fake_iscsi_location)
self.stubs.Set(self.target,
'_get_target_chap_auth',
lambda x: None)
self.stubs.Set(self.target,
'target_driver',
'iscsi')
self.stubs.Set(self.target,
'initiator_iqn',
'iqn.1993-08.org.debian:01:626bf14ebdc')
self.stubs.Set(vutils,
'generate_username',
lambda: 'QZJbisGmn9AL954FNF4D')
self.stubs.Set(vutils,
'generate_password',
lambda: 'P68eE7u9eFqDGexd28DQ')
ctxt = context.get_admin_context()
expected_result = {'location': '10.9.8.7:3260,1 '
'iqn.2010-10.org.openstack:'
'volume-ed2c2222-5fc0-11e4-aa15-123b93f75cba 1',
'auth': 'CHAP '
'QZJbisGmn9AL954FNF4D P68eE7u9eFqDGexd28DQ'}
self.assertEqual(expected_result,
self.target.create_export(ctxt,
self.testvol_1,
self.fake_volumes_dir))
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', 'testvol',
1, 0, self.fake_volumes_dir)

View File

@ -147,6 +147,20 @@ volume_opts = [
'provisioned capacity cannot exceed the total physical '
'capacity. A ratio lower than 1.0 will be ignored and '
'the default value will be used instead.'),
cfg.StrOpt('scst_target_iqn_name',
default=None,
help='Certain ISCSI targets have predefined target names, '
'SCST target driver uses this name.'),
cfg.StrOpt('scst_target_driver',
default='iscsi',
help='SCST target implementation can choose from multiple '
'SCST target drivers.'),
cfg.StrOpt('chap_username',
default=None,
help='CHAP username to use for iSCSI Targets'),
cfg.StrOpt('chap_password',
default=None,
help='CHAP password to use for iSCSI Targets'),
]
# for backward compatibility
@ -227,7 +241,8 @@ class VolumeDriver(object):
'ietadm': 'cinder.volume.targets.iet.IetAdm',
'iseradm': 'cinder.volume.targets.iser.ISERTgtAdm',
'lioadm': 'cinder.volume.targets.lio.LioAdm',
'tgtadm': 'cinder.volume.targets.tgt.TgtAdm', }
'tgtadm': 'cinder.volume.targets.tgt.TgtAdm',
'scstadmin': 'cinder.volume.targets.scst.SCSTAdm', }
# set True by manager after successful check_for_setup
self._initialized = False

View File

@ -0,0 +1,349 @@
# 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.
from oslo_concurrency import processutils as putils
from cinder import exception
from cinder import utils
from cinder.i18n import _, _LE
from cinder.openstack.common import log as logging
from cinder.volume.targets import iscsi
from cinder.volume import utils as vutils
LOG = logging.getLogger(__name__)
class SCSTAdm(iscsi.ISCSITarget):
def __init__(self, *args, **kwargs):
super(SCSTAdm, self).__init__(*args, **kwargs)
self.volumes_dir = self.configuration.safe_get('volumes_dir')
self.iscsi_target_prefix = self.configuration.safe_get(
'iscsi_target_prefix')
self.target_name = self.configuration.safe_get('scst_target_iqn_name')
self.target_driver = self.configuration.safe_get('scst_target_driver')
self.chap_username = self.configuration.safe_get('chap_username')
self.chap_password = self.configuration.safe_get('chap_password')
self.initiator_iqn = None
self.remove_initiator_iqn = None
def scst_execute(self, *args):
return utils.execute('scstadmin', *args, run_as_root=True)
def validate_connector(self, connector):
# iSCSI drivers require the initiator information
if 'initiator' not in connector:
err_msg = _('The volume driver requires the iSCSI initiator '
'name in the connector.')
LOG.error(err_msg)
raise exception.VolumeBackendAPIException(data=err_msg)
self.initiator_iqn = connector['initiator']
def terminate_connection(self, volume, connector, **kwargs):
self.remove_initiator_iqn = connector['initiator']
def _get_target(self, iqn):
(out, _err) = self.scst_execute('-list_target')
if iqn in out:
return self._target_attribute(iqn)
return None
def _target_attribute(self, iqn):
(out, _err) = self.scst_execute('-list_tgt_attr', iqn,
'-driver', self.target_driver)
lines = out.split('\n')
for line in lines:
if "rel_tgt_id" in line:
parsed = line.split()
return parsed[1]
def _get_group(self):
scst_group = self.initiator_iqn
(out, _err) = self.scst_execute('-list_group')
if scst_group in out:
return out
return None
def _get_luns_info(self):
scst_group = self.initiator_iqn
(out, _err) = self.scst_execute('-list_group', scst_group,
'-driver', self.target_driver,
'-target', self.target_name)
first = "Assigned LUNs:"
last = "Assigned Initiators:"
start = out.index(first) + len(first)
end = out.index(last, start)
out = out[start:end]
luns = []
for line in out.strip().split("\n")[2:]:
luns.append(int(line.strip().split(" ")[0]))
luns = sorted(set(luns))
return luns
def _get_target_and_lun(self, context, volume):
iscsi_target = 0
if not self.target_name or not self._get_group():
lun = 1
return iscsi_target, lun
luns = self._get_luns_info()
if (not luns) or (luns[0] != 1):
lun = 1
return iscsi_target, lun
else:
for lun in luns:
if (luns[-1] == lun) or (luns[lun - 1] + 1 != luns[lun]):
return iscsi_target, (lun + 1)
def create_iscsi_target(self, name, vol_id, tid, lun, path,
chap_auth=None):
scst_group = self.initiator_iqn
vol_name = path.split("/")[3]
try:
(out, _err) = self.scst_execute('-noprompt',
'-set_drv_attr',
self.target_driver,
'-attributes',
'enabled=1')
LOG.debug('StdOut from set driver attribute: %s', out)
except putils.ProcessExecutionError as e:
LOG.error(_LE("Failed to set attribute for enable target driver "
"%s"), e)
raise exception.ISCSITargetHelperCommandFailed(
error_message="Failed to enable SCST Target driver.")
if self._get_target(name) is None:
try:
(out, _err) = self.scst_execute('-add_target', name,
'-driver', self.target_driver)
LOG.debug("StdOut from scstadmin create target: %s", out)
except putils.ProcessExecutionError as e:
LOG.error(_LE("Failed to create iscsi target for volume "
"id:%(vol_id)s: %(e)s"), {'vol_id': name, 'e': e})
raise exception.ISCSITargetCreateFailed(volume_id=vol_name)
try:
(out, _err) = self.scst_execute('-enable_target', name,
'-driver', self.target_driver)
LOG.debug("StdOut from scstadmin enable target: %s", out)
except putils.ProcessExecutionError as e:
LOG.error(_LE("Failed to set 'enable' attribute for "
"SCST target %s"), e)
raise exception.ISCSITargetHelperCommandFailed(
error_mesage="Failed to enable SCST Target.")
if self.target_name:
if self._get_group() is None:
try:
(out, _err) = self.scst_execute('-add_group', scst_group,
'-driver',
self.target_driver,
'-target', name)
LOG.debug("StdOut from scstadmin create group: %s", out)
except putils.ProcessExecutionError as e:
LOG.error(_LE("Failed to create group to SCST target "
"%s"), e)
raise exception.ISCSITargetHelperCommandFailed(
error_message="Failed to create group to SCST target.")
try:
(out, _err) = self.scst_execute('-add_init',
self.initiator_iqn,
'-driver', self.target_driver,
'-target', name,
'-group', scst_group)
LOG.debug("StdOut from scstadmin add initiator: %s", out)
except putils.ProcessExecutionError as e:
LOG.error(_LE("Failed to add initiator to group "
" for SCST target %s"), e)
raise exception.ISCSITargetHelperCommandFailed(
error_message="Failed to add Initiator to group for "
"SCST target.")
tid = self._get_target(name)
if self.target_name is None:
disk_id = "disk%s" % tid
else:
disk_id = "%s%s" % (lun, vol_id.split('-')[-1])
try:
self.scst_execute('-open_dev', disk_id,
'-handler', 'vdisk_fileio',
'-attributes', 'filename=%s' % path)
except putils.ProcessExecutionError as e:
LOG.error(_LE("Failed to add device to handler %s"), e)
raise exception.ISCSITargetHelperCommandFailed(
error_message="Failed to add device to SCST handler.")
try:
if self.target_name:
self.scst_execute('-add_lun', lun,
'-driver', self.target_driver,
'-target', name,
'-device', disk_id,
'-group', scst_group)
else:
self.scst_execute('-add_lun', lun,
'-driver', self.target_driver,
'-target', name,
'-device', disk_id)
except putils.ProcessExecutionError as e:
LOG.error(_LE("Failed to add lun to SCST target "
"id:%(vol_id)s: %(e)s"), {'vol_id': name, 'e': e})
raise exception.ISCSITargetHelperCommandFailed(
error_message="Failed to add LUN to SCST Target for "
"volume " + vol_name)
# SCST uses /etc/scst.conf as the default configuration when it
# starts
try:
self.scst_execute('-write_config', '/etc/scst.conf')
except putils.ProcessExecutionError as e:
LOG.error(_LE("Failed to write in /etc/scst.conf."))
raise exception.ISCSITargetHelperCommandFailed(
error_message="Failed to write in /etc/scst.conf.")
return tid
def _iscsi_location(self, ip, target, iqn, lun=None):
return "%s:%s,%s %s %s" % (ip, self.configuration.iscsi_port,
target, iqn, lun)
def ensure_export(self, context, volume, volume_path):
iscsi_name = "%s%s" % (self.configuration.iscsi_target_prefix,
volume['name'])
self.create_iscsi_target(iscsi_name, volume['name'], 1, 0, volume_path)
def create_export(self, context, volume, volume_path):
"""Creates an export for a logical volume."""
iscsi_target, lun = self._get_target_and_lun(context, volume)
if self.target_name is None:
iscsi_name = "%s%s" % (self.configuration.iscsi_target_prefix,
volume['name'])
else:
iscsi_name = self.target_name
if self.chap_username and self.chap_password:
chap_username = self.chap_username
chap_password = self.chap_password
else:
chap_username = vutils.generate_username()
chap_password = vutils.generate_password()
chap_auth = self._iscsi_authentication('IncomingUser', chap_username,
chap_password)
tid = self.create_iscsi_target(iscsi_name, volume['id'], iscsi_target,
lun, volume_path, chap_auth)
data = {}
data['location'] = self._iscsi_location(
self.configuration.iscsi_ip_address, tid, iscsi_name, lun)
LOG.debug('Set provider_location to: %s', data['location'])
data['auth'] = self._iscsi_authentication(
'CHAP', chap_username, chap_password)
return data
def remove_export(self, context, volume):
try:
location = volume['provider_location'].split(' ')
iqn = location[1]
iscsi_target = self._get_target(iqn)
self.show_target(iscsi_target, iqn)
except Exception:
LOG.error(_LE("Skipping remove_export. No iscsi_target is"
"presently exported for volume: %s"), volume['id'])
return
vol = self.db.volume_get(context, volume['id'])
lun = "".join(vol['provider_location'].split(" ")[-1:])
self.remove_iscsi_target(iscsi_target, lun,
volume['id'], volume['name'])
def remove_iscsi_target(self, tid, lun, vol_id, vol_name, **kwargs):
disk_id = "%s%s" % (lun, vol_id.split('-')[-1])
vol_uuid_file = vol_name
if self.target_name is None:
iqn = '%s%s' % (self.iscsi_target_prefix, vol_uuid_file)
else:
iqn = self.target_name
if self.target_name is None:
try:
self.scst_execute('-noprompt',
'-rem_target', iqn,
'-driver', 'iscsi')
except putils.ProcessExecutionError as e:
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)
try:
self.scst_execute('-noprompt',
'-close_dev', "disk%s" % tid,
'-handler', 'vdisk_fileio')
except putils.ProcessExecutionError as e:
LOG.error(_LE("Failed to close disk device %s"), e)
raise exception.ISCSITargetHelperCommandFailed(
error_message="Failed to close disk device for "
"SCST handler.")
if self._get_target(iqn):
try:
self.scst_execute('-noprompt',
'-rem_target', iqn,
'-driver', self.target_driver)
except putils.ProcessExecutionError as e:
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)
else:
if not int(lun) in self._get_luns_info():
raise exception.ISCSITargetRemoveFailed(volume_id=vol_id)
try:
self.scst_execute('-noprompt', '-rem_lun', lun,
'-driver', self.target_driver,
'-target', iqn, '-group',
self.remove_initiator_iqn)
except putils.ProcessExecutionError as e:
LOG.error(_LE("Failed to remove LUN %s"), e)
raise exception.ISCSITargetHelperCommandFailed(
error_message="Failed to remove LUN for SCST Target.")
try:
self.scst_execute('-noprompt',
'-close_dev', disk_id,
'-handler', 'vdisk_fileio')
except putils.ProcessExecutionError as e:
LOG.error(_LE("Failed to close disk device %s"), e)
raise exception.ISCSITargetHelperCommandFailed(
error_message="Failed to close disk device for "
"SCST handler.")
self.scst_execute('-write_config', '/etc/scst.conf')
def show_target(self, tid, iqn):
if iqn is None:
raise exception.InvalidParameterValue(
err=_('valid iqn needed for show_target'))
tid = self._get_target(iqn)
if tid is None:
raise exception.ISCSITargetHelperCommandFailed(
error_message="Target not found")
def initialize_connection(self, volume, connector):
iscsi_properties = self._get_iscsi_properties(volume)
return {
'driver_volume_type': 'iscsi',
'data': iscsi_properties
}

View File

@ -10,7 +10,7 @@ filters_path=/etc/cinder/rootwrap.d,/usr/share/cinder/rootwrap
# explicitely specify a full path (separated by ',')
# If not specified, defaults to system PATH environment variable.
# These directories MUST all be only writeable by root !
exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin,/usr/local/bin
exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin,/usr/local/bin,/usr/local/sbin
# Enable logging to syslog
# Default value is False

View File

@ -7,6 +7,7 @@ ietadm: CommandFilter, ietadm, root
tgtadm: CommandFilter, tgtadm, root
tgt-admin: CommandFilter, tgt-admin, root
cinder-rtstool: CommandFilter, cinder-rtstool, root
scstadmin: CommandFilter, scstadmin, root
# LVM related show commands
pvs: EnvFilter, env, root, LC_ALL=C, pvs