Support Linux-IO in addition to tgtd

The iSCSI extension now tries to use Linux-IO first (via rtslib)
and falls back to tgtd if Linux-IO can't be used (e.g. in the CoreOS-based
image which uses containers).

Change-Id: I9cc7a30d9c93c445a66d183146e9260c2b096d33
Closes-Bug: #1504562
This commit is contained in:
Dmitry Tantsur 2015-10-14 18:03:18 +02:00
parent f4ad4d7500
commit c474a5ac6c
4 changed files with 133 additions and 16 deletions
ironic_python_agent
errors.py
extensions
tests/unit/extensions
requirements.txt

@ -310,10 +310,18 @@ class ISCSIError(RESTError):
message = 'Error starting iSCSI target'
def __init__(self, error_msg):
details = 'Error starting iSCSI target: {0}'.format(error_msg)
super(ISCSIError, self).__init__(details)
class ISCSICommandError(ISCSIError):
"""Error executing TGT command."""
def __init__(self, error_msg, exit_code, stdout, stderr):
details = ('{0}. Failed with exit code {1}. stdout: {2}. stderr: {3}')
details = details.format(error_msg, exit_code, stdout, stderr)
super(ISCSIError, self).__init__(details)
super(ISCSICommandError, self).__init__(details)
class DeviceNotFound(NotFound):

@ -19,6 +19,10 @@
from oslo_concurrency import processutils
from oslo_log import log
from oslo_utils import uuidutils
try:
import rtslib_fb
except ImportError:
import rtslib as rtslib_fb
from ironic_python_agent import errors
from ironic_python_agent.extensions import base
@ -33,10 +37,11 @@ def _execute(cmd, error_msg, **kwargs):
stdout, stderr = utils.execute(*cmd, **kwargs)
except processutils.ProcessExecutionError as e:
LOG.error(error_msg)
raise errors.ISCSIError(error_msg, e.exit_code, e.stdout, e.stderr)
raise errors.ISCSICommandError(error_msg, e.exit_code,
e.stdout, e.stderr)
def _wait_for_iscsi_daemon(attempts=10):
def _wait_for_tgtd(attempts=10):
"""Wait for the ISCSI daemon to start."""
# here, iscsi daemon is considered not running in case
# tgtadm is not able to talk to tgtd to show iscsi targets
@ -44,14 +49,12 @@ def _wait_for_iscsi_daemon(attempts=10):
_execute(cmd, "ISCSI daemon didn't initialize", attempts=attempts)
def _start_iscsi_daemon(iqn, device):
def _start_tgtd(iqn, device):
"""Start a ISCSI target for the device."""
LOG.debug("Starting ISCSI target on device %(device)s", {'device': device})
# Start ISCSI Target daemon
_execute(['tgtd'], "Unable to start the ISCSI daemon")
_wait_for_iscsi_daemon()
_wait_for_tgtd()
cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'target', '--op',
'new', '--tid', '1', '--targetname', iqn]
@ -67,15 +70,59 @@ def _start_iscsi_daemon(iqn, device):
"initiators for iqn %s" % iqn)
class ISCSIExtension(base.BaseAgentExtension):
def _start_lio(iqn, device):
try:
storage = rtslib_fb.BlockStorageObject(name=iqn, dev=device)
target = rtslib_fb.Target(rtslib_fb.FabricModule('iscsi'), iqn,
mode='create')
tpg = rtslib_fb.TPG(target, mode='create')
# disable all authentication
tpg.set_attribute('authentication', '0')
tpg.set_attribute('demo_mode_write_protect', '0')
tpg.set_attribute('generate_node_acls', '1')
# lun=1 is hardcoded in ironic
rtslib_fb.LUN(tpg, storage_object=storage, lun=1)
tpg.enable = 1
except rtslib_fb.utils.RTSLibError as exc:
msg = 'Failed to create a target: {0}'.format(exc)
raise errors.ISCSIError(msg)
try:
# bind to the default port on all interfaces
rtslib_fb.NetworkPortal(tpg, '0.0.0.0')
except rtslib_fb.utils.RTSLibError as exc:
msg = 'Failed to publish a target: {0}'.format(exc)
raise errors.ISCSIError(msg)
class ISCSIExtension(base.BaseAgentExtension):
@base.sync_command('start_iscsi_target')
def start_iscsi_target(self, iqn=None):
"""Expose the disk as an ISCSI target."""
# If iqn is not given, generate one
if iqn is None:
iqn = 'iqn-' + uuidutils.generate_uuid()
iqn = 'iqn.2008-10.org.openstack:%s' % uuidutils.generate_uuid()
device = hardware.dispatch_to_managers('get_os_install_device')
_start_iscsi_daemon(iqn, device)
LOG.debug("Starting ISCSI target with iqn %(iqn)s on device "
"%(device)s", {'iqn': iqn, 'device': device})
try:
rts_root = rtslib_fb.RTSRoot()
except (EnvironmentError, rtslib_fb.RTSLibError) as exc:
LOG.warn('Linux-IO is not available, falling back to TGT. '
'Error: %s.', exc)
rts_root = None
if rts_root is None:
_start_tgtd(iqn, device)
else:
_start_lio(iqn, device)
LOG.debug('Linux-IO configuration: %s', rts_root.dump())
LOG.info('Created iSCSI target with iqn %(iqn)s on device %(dev)s '
'using %(method)s',
{'iqn': iqn, 'dev': device,
'method': 'tgtd' if rts_root is None else 'linux-io'})
return {"iscsi_target_iqn": iqn}

@ -16,7 +16,6 @@
# under the License.
import mock
import time
from oslo_concurrency import processutils
from oslotest import base as test_base
@ -29,11 +28,12 @@ from ironic_python_agent import utils
@mock.patch.object(hardware, 'dispatch_to_managers')
@mock.patch.object(utils, 'execute')
@mock.patch.object(time, 'sleep', lambda *_: None)
class TestISCSIExtension(test_base.BaseTestCase):
@mock.patch.object(iscsi.rtslib_fb, 'RTSRoot',
mock.Mock(side_effect=iscsi.rtslib_fb.RTSLibError()))
class TestISCSIExtensionTgt(test_base.BaseTestCase):
def setUp(self):
super(TestISCSIExtension, self).setUp()
super(TestISCSIExtensionTgt, self).setUp()
self.agent_extension = iscsi.ISCSIExtension()
self.fake_dev = '/dev/fake'
self.fake_iqn = 'iqn-fake'
@ -78,11 +78,11 @@ class TestISCSIExtension(test_base.BaseTestCase):
mock_execute.assert_has_calls(expected)
mock_dispatch.assert_called_once_with('get_os_install_device')
@mock.patch.object(iscsi, '_wait_for_iscsi_daemon')
@mock.patch.object(iscsi, '_wait_for_tgtd')
def test_start_iscsi_target_fail_command(self, mock_wait_iscsi,
mock_execute, mock_dispatch):
mock_dispatch.return_value = self.fake_dev
mock_execute.side_effect = [('', ''),
mock_execute.side_effect = [('', ''), ('', ''),
processutils.ProcessExecutionError('blah')]
self.assertRaises(errors.ISCSIError,
self.agent_extension.start_iscsi_target,
@ -94,3 +94,64 @@ class TestISCSIExtension(test_base.BaseTestCase):
'--targetname', self.fake_iqn)]
mock_execute.assert_has_calls(expected)
mock_dispatch.assert_called_once_with('get_os_install_device')
_ORIG_UTILS = iscsi.rtslib_fb.utils
@mock.patch.object(hardware, 'dispatch_to_managers')
# Don't mock the utils module, as it contains exceptions
@mock.patch.object(iscsi, 'rtslib_fb', utils=_ORIG_UTILS)
class TestISCSIExtensionLIO(test_base.BaseTestCase):
def setUp(self):
super(TestISCSIExtensionLIO, self).setUp()
self.agent_extension = iscsi.ISCSIExtension()
self.fake_dev = '/dev/fake'
self.fake_iqn = 'iqn-fake'
def test_start_iscsi_target(self, mock_rtslib, mock_dispatch):
mock_dispatch.return_value = self.fake_dev
result = self.agent_extension.start_iscsi_target(iqn=self.fake_iqn)
self.assertEqual({'iscsi_target_iqn': self.fake_iqn},
result.command_result)
mock_rtslib.BlockStorageObject.assert_called_once_with(
name=self.fake_iqn, dev=self.fake_dev)
mock_rtslib.Target.assert_called_once_with(mock.ANY, self.fake_iqn,
mode='create')
mock_rtslib.TPG.assert_called_once_with(
mock_rtslib.Target.return_value, mode='create')
mock_rtslib.LUN.assert_called_once_with(
mock_rtslib.TPG.return_value,
storage_object=mock_rtslib.BlockStorageObject.return_value,
lun=1)
mock_rtslib.NetworkPortal.assert_called_once_with(
mock_rtslib.TPG.return_value, '0.0.0.0')
def test_failed_to_start_iscsi(self, mock_rtslib, mock_dispatch):
mock_dispatch.return_value = self.fake_dev
mock_rtslib.Target.side_effect = _ORIG_UTILS.RTSLibError()
self.assertRaisesRegexp(
errors.ISCSIError, 'Failed to create a target',
self.agent_extension.start_iscsi_target, iqn=self.fake_iqn)
def test_failed_to_bind_iscsi(self, mock_rtslib, mock_dispatch):
mock_dispatch.return_value = self.fake_dev
mock_rtslib.NetworkPortal.side_effect = _ORIG_UTILS.RTSLibError()
self.assertRaisesRegexp(
errors.ISCSIError, 'Failed to publish a target',
self.agent_extension.start_iscsi_target, iqn=self.fake_iqn)
mock_rtslib.BlockStorageObject.assert_called_once_with(
name=self.fake_iqn, dev=self.fake_dev)
mock_rtslib.Target.assert_called_once_with(mock.ANY, self.fake_iqn,
mode='create')
mock_rtslib.TPG.assert_called_once_with(
mock_rtslib.Target.return_value, mode='create')
mock_rtslib.LUN.assert_called_once_with(
mock_rtslib.TPG.return_value,
storage_object=mock_rtslib.BlockStorageObject.return_value,
lun=1)
mock_rtslib.NetworkPortal.assert_called_once_with(
mock_rtslib.TPG.return_value, '0.0.0.0')

@ -18,6 +18,7 @@ Pint>=0.5 # BSD
psutil<2.0.0,>=1.1.1
pyudev
requests>=2.8.1
rtslib-fb>=2.1.41
six>=1.9.0
stevedore>=1.5.0 # Apache-2.0
WSME>=0.8