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:
parent
f4ad4d7500
commit
c474a5ac6c
@ -310,10 +310,18 @@ class ISCSIError(RESTError):
|
|||||||
|
|
||||||
message = 'Error starting iSCSI target'
|
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):
|
def __init__(self, error_msg, exit_code, stdout, stderr):
|
||||||
details = ('{0}. Failed with exit code {1}. stdout: {2}. stderr: {3}')
|
details = ('{0}. Failed with exit code {1}. stdout: {2}. stderr: {3}')
|
||||||
details = details.format(error_msg, exit_code, stdout, stderr)
|
details = details.format(error_msg, exit_code, stdout, stderr)
|
||||||
super(ISCSIError, self).__init__(details)
|
super(ISCSICommandError, self).__init__(details)
|
||||||
|
|
||||||
|
|
||||||
class DeviceNotFound(NotFound):
|
class DeviceNotFound(NotFound):
|
||||||
|
@ -19,6 +19,10 @@
|
|||||||
from oslo_concurrency import processutils
|
from oslo_concurrency import processutils
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from oslo_utils import uuidutils
|
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 import errors
|
||||||
from ironic_python_agent.extensions import base
|
from ironic_python_agent.extensions import base
|
||||||
@ -33,10 +37,11 @@ def _execute(cmd, error_msg, **kwargs):
|
|||||||
stdout, stderr = utils.execute(*cmd, **kwargs)
|
stdout, stderr = utils.execute(*cmd, **kwargs)
|
||||||
except processutils.ProcessExecutionError as e:
|
except processutils.ProcessExecutionError as e:
|
||||||
LOG.error(error_msg)
|
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."""
|
"""Wait for the ISCSI daemon to start."""
|
||||||
# here, iscsi daemon is considered not running in case
|
# here, iscsi daemon is considered not running in case
|
||||||
# tgtadm is not able to talk to tgtd to show iscsi targets
|
# 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)
|
_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."""
|
"""Start a ISCSI target for the device."""
|
||||||
LOG.debug("Starting ISCSI target on device %(device)s", {'device': device})
|
|
||||||
|
|
||||||
# Start ISCSI Target daemon
|
# Start ISCSI Target daemon
|
||||||
_execute(['tgtd'], "Unable to start the ISCSI daemon")
|
_execute(['tgtd'], "Unable to start the ISCSI daemon")
|
||||||
|
|
||||||
_wait_for_iscsi_daemon()
|
_wait_for_tgtd()
|
||||||
|
|
||||||
cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'target', '--op',
|
cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'target', '--op',
|
||||||
'new', '--tid', '1', '--targetname', iqn]
|
'new', '--tid', '1', '--targetname', iqn]
|
||||||
@ -67,15 +70,59 @@ def _start_iscsi_daemon(iqn, device):
|
|||||||
"initiators for iqn %s" % iqn)
|
"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')
|
@base.sync_command('start_iscsi_target')
|
||||||
def start_iscsi_target(self, iqn=None):
|
def start_iscsi_target(self, iqn=None):
|
||||||
"""Expose the disk as an ISCSI target."""
|
"""Expose the disk as an ISCSI target."""
|
||||||
# If iqn is not given, generate one
|
# If iqn is not given, generate one
|
||||||
if iqn is None:
|
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')
|
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}
|
return {"iscsi_target_iqn": iqn}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
import time
|
|
||||||
|
|
||||||
from oslo_concurrency import processutils
|
from oslo_concurrency import processutils
|
||||||
from oslotest import base as test_base
|
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(hardware, 'dispatch_to_managers')
|
||||||
@mock.patch.object(utils, 'execute')
|
@mock.patch.object(utils, 'execute')
|
||||||
@mock.patch.object(time, 'sleep', lambda *_: None)
|
@mock.patch.object(iscsi.rtslib_fb, 'RTSRoot',
|
||||||
class TestISCSIExtension(test_base.BaseTestCase):
|
mock.Mock(side_effect=iscsi.rtslib_fb.RTSLibError()))
|
||||||
|
class TestISCSIExtensionTgt(test_base.BaseTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestISCSIExtension, self).setUp()
|
super(TestISCSIExtensionTgt, self).setUp()
|
||||||
self.agent_extension = iscsi.ISCSIExtension()
|
self.agent_extension = iscsi.ISCSIExtension()
|
||||||
self.fake_dev = '/dev/fake'
|
self.fake_dev = '/dev/fake'
|
||||||
self.fake_iqn = 'iqn-fake'
|
self.fake_iqn = 'iqn-fake'
|
||||||
@ -78,11 +78,11 @@ class TestISCSIExtension(test_base.BaseTestCase):
|
|||||||
mock_execute.assert_has_calls(expected)
|
mock_execute.assert_has_calls(expected)
|
||||||
mock_dispatch.assert_called_once_with('get_os_install_device')
|
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,
|
def test_start_iscsi_target_fail_command(self, mock_wait_iscsi,
|
||||||
mock_execute, mock_dispatch):
|
mock_execute, mock_dispatch):
|
||||||
mock_dispatch.return_value = self.fake_dev
|
mock_dispatch.return_value = self.fake_dev
|
||||||
mock_execute.side_effect = [('', ''),
|
mock_execute.side_effect = [('', ''), ('', ''),
|
||||||
processutils.ProcessExecutionError('blah')]
|
processutils.ProcessExecutionError('blah')]
|
||||||
self.assertRaises(errors.ISCSIError,
|
self.assertRaises(errors.ISCSIError,
|
||||||
self.agent_extension.start_iscsi_target,
|
self.agent_extension.start_iscsi_target,
|
||||||
@ -94,3 +94,64 @@ class TestISCSIExtension(test_base.BaseTestCase):
|
|||||||
'--targetname', self.fake_iqn)]
|
'--targetname', self.fake_iqn)]
|
||||||
mock_execute.assert_has_calls(expected)
|
mock_execute.assert_has_calls(expected)
|
||||||
mock_dispatch.assert_called_once_with('get_os_install_device')
|
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
|
psutil<2.0.0,>=1.1.1
|
||||||
pyudev
|
pyudev
|
||||||
requests>=2.8.1
|
requests>=2.8.1
|
||||||
|
rtslib-fb>=2.1.41
|
||||||
six>=1.9.0
|
six>=1.9.0
|
||||||
stevedore>=1.5.0 # Apache-2.0
|
stevedore>=1.5.0 # Apache-2.0
|
||||||
WSME>=0.8
|
WSME>=0.8
|
||||||
|
Loading…
x
Reference in New Issue
Block a user