diff --git a/ironic_python_agent/errors.py b/ironic_python_agent/errors.py index 6b98fbd06..eb4c9c6eb 100644 --- a/ironic_python_agent/errors.py +++ b/ironic_python_agent/errors.py @@ -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): diff --git a/ironic_python_agent/extensions/iscsi.py b/ironic_python_agent/extensions/iscsi.py index 57e50153f..8d38c6b24 100644 --- a/ironic_python_agent/extensions/iscsi.py +++ b/ironic_python_agent/extensions/iscsi.py @@ -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} diff --git a/ironic_python_agent/tests/unit/extensions/test_iscsi.py b/ironic_python_agent/tests/unit/extensions/test_iscsi.py index efd7b51b1..eaae2c50b 100644 --- a/ironic_python_agent/tests/unit/extensions/test_iscsi.py +++ b/ironic_python_agent/tests/unit/extensions/test_iscsi.py @@ -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') diff --git a/requirements.txt b/requirements.txt index 0d77faf44..c68a4b503 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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