From d3aa7c93aa166142a544303bcbe36767bd5689fa Mon Sep 17 00:00:00 2001 From: Lucas Alvares Gomes Date: Tue, 10 Feb 2015 16:30:21 +0000 Subject: [PATCH] Add iscsi extension This extension allows IPA to be used with the PXE/iSCSI methodology of deployment in Ironic. Change-Id: I32ec9fa74182c0d03c7ef1b698b1d0c0e3007773 --- Dockerfile | 4 +- ironic_python_agent/errors.py | 12 +++ ironic_python_agent/extensions/iscsi.py | 90 ++++++++++++++++++ ironic_python_agent/tests/extensions/iscsi.py | 93 +++++++++++++++++++ setup.cfg | 1 + 5 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 ironic_python_agent/extensions/iscsi.py create mode 100644 ironic_python_agent/tests/extensions/iscsi.py diff --git a/Dockerfile b/Dockerfile index bd7a8961d..cea28bf5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN apt-get update && \ apt-get -y upgrade && \ apt-get install -y --no-install-recommends python2.7 python2.7-dev \ python-pip qemu-utils parted hdparm util-linux genisoimage git gcc \ - bash coreutils && \ + bash coreutils tgt && \ apt-get -y autoremove && \ apt-get clean @@ -24,7 +24,7 @@ RUN pip install /tmp/ironic-python-agent RUN rm -rf /tmp/ironic-python-agent RUN rm -rf /var/lib/apt/lists/* -RUN apt-get -y purge perl gcc-4.6 gcc python2.7-dev git python3 \ +RUN apt-get -y purge gcc-4.6 gcc python2.7-dev git python3 \ python3-minimal python3.4 python3.4-minimal && \ apt-get -y autoremove && \ apt-get clean diff --git a/ironic_python_agent/errors.py b/ironic_python_agent/errors.py index f21678d44..26dac1540 100644 --- a/ironic_python_agent/errors.py +++ b/ironic_python_agent/errors.py @@ -282,3 +282,15 @@ class IncompatibleHardwareMethodError(RESTError): else: details = self.message super(IncompatibleHardwareMethodError, self).__init__(details) + + +class ISCSIError(RESTError): + """Error raised when an image cannot be written to a device.""" + + message = 'Error starting iSCSI target.' + + def __init__(self, error_msg, exit_code, stdout, stderr): + details = ('Error starting iSCSI target: {0}. Failed with exit code ' + '{1}. stdout: {2}. stderr: {3}') + details = details.format(error_msg, exit_code, stdout, stderr) + super(ISCSIError, self).__init__(details) diff --git a/ironic_python_agent/extensions/iscsi.py b/ironic_python_agent/extensions/iscsi.py new file mode 100644 index 000000000..0904757fa --- /dev/null +++ b/ironic_python_agent/extensions/iscsi.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2015 Red Hat, 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 time + +from oslo_concurrency import processutils +from oslo_utils import uuidutils + +from ironic_python_agent import errors +from ironic_python_agent.extensions import base +from ironic_python_agent import hardware +from ironic_python_agent.openstack.common import log +from ironic_python_agent import utils + +LOG = log.getLogger(__name__) + + +def _execute(cmd, error_msg, check_exit_code=None): + if check_exit_code is None: + check_exit_code = [0] + + try: + stdout, stderr = utils.execute(*cmd, check_exit_code=check_exit_code) + except processutils.ProcessExecutionError as e: + LOG.error(error_msg) + raise errors.ISCSIError(error_msg, e.exit_code, e.stdout, e.stderr) + + +def _wait_for_iscsi_daemon(interval=1, attempts=10): + """Wait for the ISCSI daemon to start.""" + for attempt in range(attempts): + if os.path.exists("/var/run/tgtd.ipc_abstract_namespace.0"): + break + time.sleep(interval) + else: + error_msg = "ISCSI daemon didn't initialize" + LOG.error(error_msg) + raise errors.ISCSIError(error_msg, 1, '', error_msg) + + +def _start_iscsi_daemon(iqn, device): + """Start a ISCSI target for the device.""" + LOG.debug("Starting ISCSI target on device %{device}", {'device': device}) + + # Start ISCSI Target daemon + _execute(['tgtd'], "Unable to start the ISCSI daemon") + + _wait_for_iscsi_daemon() + + cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'target', '--op', + 'new', '--tid', '1', '--targetname', iqn] + _execute(cmd, "Error when adding a new target for iqn %s" % iqn) + + cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'logicalunit', '--op', + 'new', '--tid', '1', '--lun', '1', '--backing-store', device] + _execute(cmd, "Error when adding a new logical unit for iqn %s" % iqn) + + cmd = ['tgtadm', '--lld', 'iscsi', '--mode', 'target', '--op', + 'bind', '--tid', '1', '--initiator-address', 'ALL'] + _execute(cmd, "Error when enabling the target to accept the specific " + "initiators for iqn %s" % iqn) + + +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() + + device = hardware.dispatch_to_managers('get_os_install_device') + _start_iscsi_daemon(iqn, device) + return {"iscsi_target_iqn": iqn} diff --git a/ironic_python_agent/tests/extensions/iscsi.py b/ironic_python_agent/tests/extensions/iscsi.py new file mode 100644 index 000000000..54ddb2025 --- /dev/null +++ b/ironic_python_agent/tests/extensions/iscsi.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2015 Red Hat, 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 mock +import time + +from oslo_concurrency import processutils +from oslotest import base as test_base + +from ironic_python_agent import errors +from ironic_python_agent.extensions import iscsi +from ironic_python_agent import hardware +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): + + def setUp(self): + super(TestISCSIExtension, self).setUp() + self.agent_extension = iscsi.ISCSIExtension() + self.fake_dev = '/dev/fake' + self.fake_iqn = 'iqn-fake' + + @mock.patch.object(iscsi, '_wait_for_iscsi_daemon') + def test_start_iscsi_target(self, mock_wait_iscsi, mock_execute, + mock_dispatch): + mock_dispatch.return_value = self.fake_dev + mock_execute.return_value = ('', '') + result = self.agent_extension.start_iscsi_target(iqn=self.fake_iqn) + + expected = [mock.call('tgtd', check_exit_code=[0]), + mock.call('tgtadm', '--lld', 'iscsi', '--mode', + 'target', '--op', 'new', '--tid', '1', + '--targetname', self.fake_iqn, + check_exit_code=[0]), + mock.call('tgtadm', '--lld', 'iscsi', '--mode', + 'logicalunit', '--op', 'new', '--tid', '1', + '--lun', '1', '--backing-store', + self.fake_dev, check_exit_code=[0]), + mock.call('tgtadm', '--lld', 'iscsi', '--mode', 'target', + '--op', 'bind', '--tid', '1', + '--initiator-address', 'ALL', + check_exit_code=[0])] + mock_execute.assert_has_calls(expected) + mock_dispatch.assert_called_once_with('get_os_install_device') + mock_wait_iscsi.assert_called_once_with() + self.assertEqual({'iscsi_target_iqn': self.fake_iqn}, + result.command_result) + + def test_start_iscsi_target_fail_wait_daemon(self, mock_execute, + mock_dispatch): + mock_dispatch.return_value = self.fake_dev + mock_execute.return_value = ('', '') + self.assertRaises(errors.ISCSIError, + self.agent_extension.start_iscsi_target, + iqn=self.fake_iqn) + mock_execute.assert_called_once_with('tgtd', check_exit_code=[0]) + mock_dispatch.assert_called_once_with('get_os_install_device') + + @mock.patch.object(iscsi, '_wait_for_iscsi_daemon') + 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 = [('', ''), + processutils.ProcessExecutionError('blah')] + self.assertRaises(errors.ISCSIError, + self.agent_extension.start_iscsi_target, + iqn=self.fake_iqn) + + expected = [mock.call('tgtd', check_exit_code=[0]), + mock.call('tgtadm', '--lld', 'iscsi', '--mode', + 'target', '--op', 'new', '--tid', '1', + '--targetname', self.fake_iqn, + check_exit_code=[0])] + mock_execute.assert_has_calls(expected) + mock_dispatch.assert_called_once_with('get_os_install_device') diff --git a/setup.cfg b/setup.cfg index f05cf16b2..74469df0c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,7 @@ ironic_python_agent.extensions = standby = ironic_python_agent.extensions.standby:StandbyExtension decom = ironic_python_agent.extensions.decom:DecomExtension flow = ironic_python_agent.extensions.flow:FlowExtension + iscsi = ironic_python_agent.extensions.iscsi:ISCSIExtension ironic_python_agent.hardware_managers = generic = ironic_python_agent.hardware:GenericHardwareManager