From de66692e93f83ea0207106c9dc9c16b7c977853b Mon Sep 17 00:00:00 2001 From: Ramakrishnan G Date: Sat, 7 Mar 2015 11:28:28 +0000 Subject: [PATCH] Refactor agent iscsi deploy out of pxe driver This commit refactors the continue_deploy method in pxe.VendorPassthru. This takes the agent iscsi deploy part out of pxe driver so that it can be reused in other drivers (like iscsi_ilo). Change-Id: I39d0120d8e7aa02b7f244de9a4d193b8c1666ebe --- ironic/drivers/modules/agent.py | 4 +- ironic/drivers/modules/agent_base_vendor.py | 60 +++++ ironic/drivers/modules/deploy_utils.py | 32 +++ ironic/drivers/modules/iscsi_deploy.py | 57 +++++ ironic/drivers/modules/pxe.py | 112 ++------- .../tests/drivers/test_agent_base_vendor.py | 85 +++++++ ironic/tests/drivers/test_deploy_utils.py | 59 +++++ ironic/tests/drivers/test_iscsi_deploy.py | 85 +++++++ ironic/tests/drivers/test_pxe.py | 221 ++++-------------- 9 files changed, 443 insertions(+), 272 deletions(-) diff --git a/ironic/drivers/modules/agent.py b/ironic/drivers/modules/agent.py index 44dfa46b1a..926c336ebd 100644 --- a/ironic/drivers/modules/agent.py +++ b/ironic/drivers/modules/agent.py @@ -376,6 +376,4 @@ class AgentVendorInterface(agent_base_vendor.BaseAgentVendor): LOG.debug('Rebooting node %s to disk', node.uuid) manager_utils.node_set_boot_device(task, 'disk', persistent=True) - manager_utils.node_power_action(task, states.REBOOT) - - task.process_event('done') + self.reboot_and_finish_deploy(task) diff --git a/ironic/drivers/modules/agent_base_vendor.py b/ironic/drivers/modules/agent_base_vendor.py index 65718a7b7f..fc33ca9fc3 100644 --- a/ironic/drivers/modules/agent_base_vendor.py +++ b/ironic/drivers/modules/agent_base_vendor.py @@ -22,12 +22,15 @@ import time from oslo_config import cfg from oslo_utils import excutils +from ironic.common import boot_devices from ironic.common import exception from ironic.common.i18n import _ from ironic.common.i18n import _LE +from ironic.common.i18n import _LI from ironic.common.i18n import _LW from ironic.common import states from ironic.common import utils +from ironic.conductor import utils as manager_utils from ironic.drivers import base from ironic.drivers.modules import agent_client from ironic.drivers.modules import deploy_utils @@ -316,3 +319,60 @@ class BaseAgentVendor(base.VendorInterface): # Only have one node_id left, return it. return node_ids.pop() + + def _log_and_raise_deployment_error(self, task, msg): + """Helper method to log the error and raise exception.""" + LOG.error(msg) + deploy_utils.set_failed_state(task, msg) + raise exception.InstanceDeployFailure(msg) + + def reboot_and_finish_deploy(self, task): + """Helper method to trigger reboot on the node and finish deploy. + + This method initiates a reboot on the node. On success, it + marks the deploy as complete. On failure, it logs the error + and marks deploy as failure. + + :param task: a TaskManager object containing the node + :raises: InstanceDeployFailure, if node reboot failed. + """ + try: + manager_utils.node_power_action(task, states.REBOOT) + except Exception as e: + msg = (_('Error rebooting node %(node)s. Error: %(error)s') % + {'node': task.node.uuid, 'error': e}) + self._log_and_raise_deployment_error(task, msg) + + task.process_event('done') + LOG.info(_LI('Deployment to node %s done'), task.node.uuid) + + def configure_local_boot(self, task, root_uuid): + """Helper method to configure local boot on the node. + + This method triggers bootloader installation on the node. + On successful installation of bootloader, this method sets the + node to boot from disk. + + :param task: a TaskManager object containing the node + :param root_uuid: The UUID of the root partition. This is used + for identifying the partition which contains the image deployed. + :raises: InstanceDeployFailure if bootloader installation failed or + on encountering error while setting the boot device on the node. + """ + node = task.node + result = self._client.install_bootloader(node, root_uuid) + if result['command_status'] == 'FAILED': + msg = (_("Failed to install a bootloader when " + "deploying node %(node)s. Error: %(error)s") % + {'node': node.uuid, + 'error': result['command_error']}) + self._log_and_raise_deployment_error(task, msg) + + try: + deploy_utils.try_set_boot_device(task, boot_devices.DISK) + except Exception as e: + msg = (_("Failed to change the boot device to %(boot_dev)s " + "when deploying node %(node)s. Error: %(error)s") % + {'boot_dev': boot_devices.DISK, 'node': node.uuid, + 'error': e}) + self._log_and_raise_deployment_error(task, msg) diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index 1669a68f08..e3efbbd8a0 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -37,11 +37,13 @@ from ironic.common import disk_partitioner from ironic.common import exception from ironic.common.i18n import _ from ironic.common.i18n import _LE +from ironic.common.i18n import _LW from ironic.common import images from ironic.common import states from ironic.common import utils from ironic.conductor import utils as manager_utils from ironic.drivers.modules import image_cache +from ironic.drivers import utils as driver_utils from ironic.openstack.common import log as logging @@ -734,3 +736,33 @@ def parse_instance_info_capabilities(node): parse_error() return capabilities + + +def try_set_boot_device(task, device, persistent=True): + """Tries to set the boot device on the node. + + This method tries to set the boot device on the node to the given + boot device. Under uefi boot mode, setting of boot device may differ + between different machines. IPMI does not work for setting boot + devices in uefi mode for certain machines. This method ignores the + expected IPMI failure for uefi boot mode and just logs a message. + In error cases, it is expected the operator has to manually set the + node to boot from the correct device. + + :param task: a TaskManager object containing the node + :param device: the boot device + :param persistent: Whether to set the boot device persistently + :raises: Any exception from set_boot_device except IPMIFailure + (setting of boot device using ipmi is expected to fail). + """ + try: + manager_utils.node_set_boot_device(task, device, + persistent=persistent) + except exception.IPMIFailure: + if driver_utils.get_node_capability(task.node, + 'boot_mode') == 'uefi': + LOG.warning(_LW("ipmitool is unable to set boot device while " + "the node %s is in UEFI boot mode. Please set " + "the boot device manually.") % task.node.uuid) + else: + raise diff --git a/ironic/drivers/modules/iscsi_deploy.py b/ironic/drivers/modules/iscsi_deploy.py index 46f127fcac..158113ca1c 100644 --- a/ironic/drivers/modules/iscsi_deploy.py +++ b/ironic/drivers/modules/iscsi_deploy.py @@ -290,6 +290,63 @@ def continue_deploy(task, **kwargs): return root_uuid +def do_agent_iscsi_deploy(task, agent_client): + """Method invoked when deployed with the agent ramdisk. + + This method is invoked by drivers for doing iSCSI deploy + using agent ramdisk. This method assumes that the agent + is booted up on the node and is heartbeating. + + :param task: a TaskManager object containing the node. + :param agent_client: an instance of agent_client.AgentClient + which will be used during iscsi deploy (for exposing node's + target disk via iSCSI, for install boot loader, etc). + :returns: UUID of the root partition which was deployed. + :raises: InstanceDeployFailure, if it encounters some error + during the deploy. + """ + node = task.node + iscsi_options = build_deploy_ramdisk_options(node) + + iqn = iscsi_options['iscsi_target_iqn'] + result = agent_client.start_iscsi_target(node, iqn) + if result['command_status'] == 'FAILED': + msg = (_("Failed to start the iSCSI target to deploy the " + "node %(node)s. Error: %(error)s") % + {'node': node.uuid, 'error': result['command_error']}) + deploy_utils.set_failed_state(task, msg) + raise exception.InstanceDeployFailure(reason=msg) + + address = parse.urlparse(node.driver_internal_info['agent_url']) + address = address.hostname + + # TODO(lucasagomes): The 'error' and 'key' parameters in the + # dictionary below are just being passed because it's needed for + # the iscsi_deploy.continue_deploy() method, we are fooling it + # for now. The agent driver doesn't use/need those. So we need to + # refactor this bits here later. + iscsi_params = {'error': result['command_error'], + 'iqn': iqn, + 'key': iscsi_options['deployment_key'], + 'address': address} + + root_uuid = continue_deploy(task, **iscsi_params) + if not root_uuid: + msg = (_("Couldn't determine the UUID of the root partition " + "when deploying node %s") % node.uuid) + deploy_utils.set_failed_state(task, msg) + raise exception.InstanceDeployFailure(reason=msg) + + # TODO(lucasagomes): Move this bit saving the root_uuid to + # iscsi_deploy.continue_deploy() + driver_internal_info = node.driver_internal_info + driver_internal_info['root_uuid'] = root_uuid + node.driver_internal_info = driver_internal_info + node.save() + + return root_uuid + + def parse_root_device_hints(node): """Parse the root_device property of a node. diff --git a/ironic/drivers/modules/pxe.py b/ironic/drivers/modules/pxe.py index 787c393979..ded85cf903 100644 --- a/ironic/drivers/modules/pxe.py +++ b/ironic/drivers/modules/pxe.py @@ -21,7 +21,6 @@ import os import shutil from oslo_config import cfg -from six.moves.urllib import parse as urlparse from ironic.common import boot_devices from ironic.common import dhcp_factory @@ -279,24 +278,6 @@ def _destroy_token_file(node): utils.unlink_without_raise(token_file_path) -def try_set_boot_device(task, device, persistent=True): - # NOTE(faizan): Under UEFI boot mode, setting of boot device may differ - # between different machines. IPMI does not work for setting boot - # devices in UEFI mode for certain machines. - # Expected IPMI failure for uefi boot mode. Logging a message to - # set the boot device manually and continue with deploy. - try: - manager_utils.node_set_boot_device(task, device, persistent=persistent) - except exception.IPMIFailure: - if driver_utils.get_node_capability(task.node, - 'boot_mode') == 'uefi': - LOG.warning(_LW("ipmitool is unable to set boot device while " - "the node %s is in UEFI boot mode. Please set " - "the boot device manually.") % task.node.uuid) - else: - raise - - class PXEDeploy(base.DeployInterface): """PXE Deploy Interface for deploy-related actions.""" @@ -377,7 +358,7 @@ class PXEDeploy(base.DeployInterface): provider = dhcp_factory.DHCPFactory() provider.update_dhcp(task, dhcp_opts) - try_set_boot_device(task, boot_devices.PXE) + deploy_utils.try_set_boot_device(task, boot_devices.PXE) manager_utils.node_power_action(task, states.REBOOT) return states.DEPLOYWAIT @@ -544,7 +525,7 @@ class VendorPassthru(agent_base_vendor.BaseAgentVendor): try: if iscsi_deploy.get_boot_option(node) == "local": - try_set_boot_device(task, boot_devices.DISK) + deploy_utils.try_set_boot_device(task, boot_devices.DISK) # If it's going to boot from the local disk, get rid of # the PXE configuration files used for the deployment pxe_utils.clean_up_pxe_config(task) @@ -563,95 +544,42 @@ class VendorPassthru(agent_base_vendor.BaseAgentVendor): msg = _('Failed to continue iSCSI deployment.') deploy_utils.set_failed_state(task, msg) - def _log_and_raise_deployment_error(self, task, msg): - LOG.error(msg) - deploy_utils.set_failed_state(task, msg) - raise exception.InstanceDeployFailure(msg) - @task_manager.require_exclusive_lock def continue_deploy(self, task, **kwargs): - """Method invoked when deployed with the IPA ramdisk.""" + """Method invoked when deployed with the IPA ramdisk. + + This method is invoked during a heartbeat from an agent when + the node is in wait-call-back state. This deploys the image on + the node and then configures the node to boot according to the + desired boot option (netboot or localboot). + + :param task: a TaskManager object containing the node. + :param kwargs: the kwargs passed from the heartbeat method. + :raises: InstanceDeployFailure, if it encounters some error during + the deploy. + """ + task.process_event('resume') node = task.node LOG.debug('Continuing the deployment on node %s', node.uuid) - task.process_event('resume') - - pxe_info = _get_image_info(node, task.context) - pxe_options = _build_pxe_config_options(task.node, pxe_info, - task.context) - - iqn = pxe_options['iscsi_target_iqn'] - result = self._client.start_iscsi_target(node, iqn) - if result['command_status'] == 'FAILED': - msg = (_("Failed to start the iSCSI target to deploy the " - "node %(node)s. Error: %(error)s") % - {'node': node.uuid, 'error': result['command_error']}) - self._log_and_raise_deployment_error(task, msg) - - address = urlparse.urlparse(node.driver_internal_info['agent_url']) - address = address.hostname - - # TODO(lucasagomes): The 'error' and 'key' parameters in the - # dictionary below are just being passed because it's needed for - # the iscsi_deploy.continue_deploy() method, we are fooling it - # for now. The agent driver doesn't use/need those. So we need to - # refactor this bits here later. - iscsi_params = {'error': result['command_error'], - 'iqn': iqn, - 'key': pxe_options['deployment_key'], - 'address': address} - # NOTE(lucasagomes): We don't use the token file with the agent, # but as it's created as part of deploy() we are going to remove # it here. _destroy_token_file(node) - root_uuid = iscsi_deploy.continue_deploy(task, **iscsi_params) - if not root_uuid: - msg = (_("Couldn't determine the UUID of the root partition " - "when deploying node %s") % node.uuid) - self._log_and_raise_deployment_error(task, msg) - - # TODO(lucasagomes): Move this bit saving the root_uuid to - # iscsi_deploy.continue_deploy() - driver_internal_info = node.driver_internal_info - driver_internal_info['root_uuid'] = root_uuid - node.driver_internal_info = driver_internal_info - node.save() + root_uuid = iscsi_deploy.do_agent_iscsi_deploy(task, self._client) if iscsi_deploy.get_boot_option(node) == "local": # Install the boot loader - result = self._client.install_bootloader(node, root_uuid) - if result['command_status'] == 'FAILED': - msg = (_("Failed to install a bootloader when " - "deploying node %(node)s. Error: %(error)s") % - {'node': node.uuid, - 'error': result['command_error']}) - self._log_and_raise_deployment_error(task, msg) - - try: - try_set_boot_device(task, boot_devices.DISK) - except Exception as e: - msg = (_("Failed to change the boot device to %(boot_dev)s " - "when deploying node %(node)s. Error: %(error)s") % - {'boot_dev': boot_devices.DISK, 'node': node.uuid, - 'error': e}) - self._log_and_raise_deployment_error(task, msg) + self.configure_local_boot(task, root_uuid) # If it's going to boot from the local disk, get rid of # the PXE configuration files used for the deployment pxe_utils.clean_up_pxe_config(task) else: pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid) + boot_mode = driver_utils.get_node_capability(node, 'boot_mode') deploy_utils.switch_pxe_config(pxe_config_path, root_uuid, - driver_utils.get_node_capability(node, 'boot_mode')) + boot_mode) - try: - manager_utils.node_power_action(task, states.REBOOT) - except Exception as e: - msg = (_('Error rebooting node %(node)s. Error: %(error)s') % - {'node': node.uuid, 'error': e}) - self._log_and_raise_deployment_error(task, msg) - - task.process_event('done') - LOG.info(_LI('Deployment to node %s done'), node.uuid) + self.reboot_and_finish_deploy(task) diff --git a/ironic/tests/drivers/test_agent_base_vendor.py b/ironic/tests/drivers/test_agent_base_vendor.py index 6eac45828c..e764b98713 100644 --- a/ironic/tests/drivers/test_agent_base_vendor.py +++ b/ironic/tests/drivers/test_agent_base_vendor.py @@ -17,10 +17,13 @@ import mock +from ironic.common import boot_devices from ironic.common import exception from ironic.common import states from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils from ironic.drivers.modules import agent_base_vendor +from ironic.drivers.modules import agent_client from ironic.drivers.modules import deploy_utils from ironic import objects from ironic.tests.conductor import utils as mgr_utils @@ -296,3 +299,85 @@ class TestBaseAgentVendor(db_base.DbTestCase): driver_routes = task.driver.vendor.driver_routes self.assertIsInstance(driver_routes, dict) self.assertEqual(expected, list(driver_routes)) + + @mock.patch.object(manager_utils, 'node_power_action') + def test_reboot_and_finish_deploy_success(self, node_power_action_mock): + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + self.passthru.reboot_and_finish_deploy(task) + node_power_action_mock.assert_called_once_with(task, states.REBOOT) + self.assertEqual(states.ACTIVE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + + @mock.patch.object(manager_utils, 'node_power_action') + def test_reboot_and_finish_deploy_reboot_failure(self, + node_power_action_mock): + exc = exception.PowerStateFailure(pstate=states.REBOOT) + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + node_power_action_mock.side_effect = exc + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + self.assertRaises(exception.InstanceDeployFailure, + self.passthru.reboot_and_finish_deploy, task) + node_power_action_mock.assert_any_call(task, states.REBOOT) + self.assertEqual(states.DEPLOYFAIL, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + + @mock.patch.object(deploy_utils, 'try_set_boot_device') + @mock.patch.object(agent_client.AgentClient, 'install_bootloader') + def test_configure_local_boot(self, install_bootloader_mock, + try_set_boot_device_mock): + install_bootloader_mock.return_value = { + 'command_status': 'SUCCESS', 'command_error': None} + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + self.passthru.configure_local_boot(task, 'some-root-uuid') + install_bootloader_mock.assert_called_once_with( + task.node, 'some-root-uuid') + try_set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK) + + @mock.patch.object(agent_client.AgentClient, 'install_bootloader') + def test_configure_local_boot_boot_loader_install_fail( + self, install_bootloader_mock): + install_bootloader_mock.return_value = { + 'command_status': 'FAILED', 'command_error': 'boom'} + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + self.assertRaises(exception.InstanceDeployFailure, + self.passthru.configure_local_boot, + task, 'some-root-uuid') + install_bootloader_mock.assert_called_once_with( + task.node, 'some-root-uuid') + self.assertEqual(states.DEPLOYFAIL, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + + @mock.patch.object(deploy_utils, 'try_set_boot_device') + @mock.patch.object(agent_client.AgentClient, 'install_bootloader') + def test_configure_local_boot_set_boot_device_fail( + self, install_bootloader_mock, try_set_boot_device_mock): + install_bootloader_mock.return_value = { + 'command_status': 'SUCCESS', 'command_error': None} + try_set_boot_device_mock.side_effect = RuntimeError('error') + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + self.assertRaises(exception.InstanceDeployFailure, + self.passthru.configure_local_boot, + task, 'some-root-uuid') + install_bootloader_mock.assert_called_once_with( + task.node, 'some-root-uuid') + try_set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK) + self.assertEqual(states.DEPLOYFAIL, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) diff --git a/ironic/tests/drivers/test_deploy_utils.py b/ironic/tests/drivers/test_deploy_utils.py index 6bc7ce5195..6308a769b3 100644 --- a/ironic/tests/drivers/test_deploy_utils.py +++ b/ironic/tests/drivers/test_deploy_utils.py @@ -28,11 +28,13 @@ from oslo_config import cfg from oslo_utils import uuidutils import requests +from ironic.common import boot_devices from ironic.common import disk_partitioner from ironic.common import exception from ironic.common import images from ironic.common import utils as common_utils from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils from ironic.drivers.modules import deploy_utils as utils from ironic.drivers.modules import image_cache from ironic.tests import base as tests_base @@ -1158,3 +1160,60 @@ class ParseInstanceInfoCapabilitiesTestCase(tests_base.TestCase): self.node.instance_info = {'capabilities': 'not-a-dict'} self.assertRaises(exception.InvalidParameterValue, utils.parse_instance_info_capabilities, self.node) + + +class TrySetBootDeviceTestCase(db_base.DbTestCase): + + def setUp(self): + super(TrySetBootDeviceTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake") + self.node = obj_utils.create_test_node(self.context, driver="fake") + + @mock.patch.object(manager_utils, 'node_set_boot_device') + def test_try_set_boot_device_okay(self, node_set_boot_device_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + utils.try_set_boot_device(task, boot_devices.DISK, + persistent=True) + node_set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK, persistent=True) + + @mock.patch.object(utils, 'LOG') + @mock.patch.object(manager_utils, 'node_set_boot_device') + def test_try_set_boot_device_ipmifailure_uefi(self, + node_set_boot_device_mock, log_mock): + self.node.properties = {'capabilities': 'boot_mode:uefi'} + self.node.save() + node_set_boot_device_mock.side_effect = exception.IPMIFailure(cmd='a') + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + utils.try_set_boot_device(task, boot_devices.DISK, + persistent=True) + node_set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK, persistent=True) + log_mock.warning.assert_called_once_with(mock.ANY) + + @mock.patch.object(manager_utils, 'node_set_boot_device') + def test_try_set_boot_device_ipmifailure_bios( + self, node_set_boot_device_mock): + node_set_boot_device_mock.side_effect = exception.IPMIFailure(cmd='a') + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IPMIFailure, + utils.try_set_boot_device, + task, boot_devices.DISK, persistent=True) + node_set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK, persistent=True) + + @mock.patch.object(manager_utils, 'node_set_boot_device') + def test_try_set_boot_device_some_other_exception( + self, node_set_boot_device_mock): + exc = exception.IloOperationError(operation="qwe", error="error") + node_set_boot_device_mock.side_effect = exc + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationError, + utils.try_set_boot_device, + task, boot_devices.DISK, persistent=True) + node_set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK, persistent=True) diff --git a/ironic/tests/drivers/test_iscsi_deploy.py b/ironic/tests/drivers/test_iscsi_deploy.py index e4bce1315d..3e138e45d3 100644 --- a/ironic/tests/drivers/test_iscsi_deploy.py +++ b/ironic/tests/drivers/test_iscsi_deploy.py @@ -515,3 +515,88 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): self.assertEqual('1.1.1.1', ret_val['address']) self.assertEqual('target-iqn', ret_val['iqn']) self.assertEqual('local', ret_val['boot_option']) + + @mock.patch.object(iscsi_deploy, 'continue_deploy') + @mock.patch.object(iscsi_deploy, 'build_deploy_ramdisk_options') + def test_do_agent_iscsi_deploy_okay(self, build_options_mock, + continue_deploy_mock): + build_options_mock.return_value = {'deployment_key': 'abcdef', + 'iscsi_target_iqn': 'iqn-qweqwe'} + agent_client_mock = mock.MagicMock() + agent_client_mock.start_iscsi_target.return_value = { + 'command_status': 'SUCCESS', 'command_error': None} + driver_internal_info = {'agent_url': 'http://1.2.3.4:1234'} + self.node.driver_internal_info = driver_internal_info + self.node.save() + continue_deploy_mock.return_value = 'some-root-uuid' + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ret_val = iscsi_deploy.do_agent_iscsi_deploy( + task, agent_client_mock) + build_options_mock.assert_called_once_with(task.node) + agent_client_mock.start_iscsi_target.assert_called_once_with( + task.node, 'iqn-qweqwe') + continue_deploy_mock.assert_called_once_with( + task, error=None, iqn='iqn-qweqwe', key='abcdef', + address='1.2.3.4') + self.assertEqual('some-root-uuid', ret_val) + self.assertEqual('some-root-uuid', + task.node.driver_internal_info['root_uuid']) + + @mock.patch.object(iscsi_deploy, 'build_deploy_ramdisk_options') + def test_do_agent_iscsi_deploy_start_iscsi_failure(self, + build_options_mock): + build_options_mock.return_value = {'deployment_key': 'abcdef', + 'iscsi_target_iqn': 'iqn-qweqwe'} + agent_client_mock = mock.MagicMock() + agent_client_mock.start_iscsi_target.return_value = { + 'command_status': 'FAILED', 'command_error': 'booom'} + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.InstanceDeployFailure, + iscsi_deploy.do_agent_iscsi_deploy, + task, agent_client_mock) + build_options_mock.assert_called_once_with(task.node) + agent_client_mock.start_iscsi_target.assert_called_once_with( + task.node, 'iqn-qweqwe') + self.node.refresh() + self.assertEqual(states.DEPLOYFAIL, self.node.provision_state) + self.assertEqual(states.ACTIVE, self.node.target_provision_state) + self.assertIsNotNone(self.node.last_error) + + @mock.patch.object(iscsi_deploy, 'continue_deploy') + @mock.patch.object(iscsi_deploy, 'build_deploy_ramdisk_options') + def test_do_agent_iscsi_deploy_no_root_uuid(self, build_options_mock, + continue_deploy_mock): + build_options_mock.return_value = {'deployment_key': 'abcdef', + 'iscsi_target_iqn': 'iqn-qweqwe'} + agent_client_mock = mock.MagicMock() + agent_client_mock.start_iscsi_target.return_value = { + 'command_status': 'SUCCESS', 'command_error': None} + driver_internal_info = {'agent_url': 'http://1.2.3.4:1234'} + self.node.driver_internal_info = driver_internal_info + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + continue_deploy_mock.return_value = None + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.InstanceDeployFailure, + iscsi_deploy.do_agent_iscsi_deploy, + task, agent_client_mock) + self.node.refresh() + build_options_mock.assert_called_once_with(task.node) + agent_client_mock.start_iscsi_target.assert_called_once_with( + task.node, 'iqn-qweqwe') + continue_deploy_mock.assert_called_once_with( + task, error=None, iqn='iqn-qweqwe', key='abcdef', + address='1.2.3.4') + self.assertEqual(states.DEPLOYFAIL, self.node.provision_state) + self.assertEqual(states.ACTIVE, self.node.target_provision_state) + self.assertIsNotNone(self.node.last_error) diff --git a/ironic/tests/drivers/test_pxe.py b/ironic/tests/drivers/test_pxe.py index 90d6ccfdc1..de6e1446e7 100644 --- a/ironic/tests/drivers/test_pxe.py +++ b/ironic/tests/drivers/test_pxe.py @@ -21,7 +21,6 @@ import os import tempfile import mock -from oslo_concurrency import processutils from oslo_config import cfg from oslo_serialization import jsonutils as json @@ -36,7 +35,7 @@ from ironic.common import states from ironic.common import utils from ironic.conductor import task_manager from ironic.conductor import utils as manager_utils -from ironic.drivers.modules import agent_client +from ironic.drivers.modules import agent_base_vendor from ironic.drivers.modules import deploy_utils from ironic.drivers.modules import iscsi_deploy from ironic.drivers.modules import pxe @@ -928,10 +927,6 @@ class CleanUpFullFlowTestCase(db_base.DbTestCase): '%s is not expected to exist' % path) -@mock.patch.object(iscsi_deploy, 'continue_deploy') -@mock.patch.object(agent_client.AgentClient, 'start_iscsi_target') -@mock.patch.object(pxe, '_build_pxe_config_options') -@mock.patch.object(pxe, '_get_image_info') class TestAgentVendorPassthru(db_base.DbTestCase): def setUp(self): @@ -949,179 +944,51 @@ class TestAgentVendorPassthru(db_base.DbTestCase): self.task.driver = self.driver self.task.context = self.context + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'reboot_and_finish_deploy') @mock.patch.object(deploy_utils, 'switch_pxe_config') - @mock.patch.object(manager_utils, 'node_power_action') - def test_continue_deploy(self, mock_node_power, mock_pxe_config, - mock_image_info, mock_pxe_opts, mock_start_iscsi, - mock_cont_deploy): - mock_pxe_opts.return_value = {'iscsi_target_iqn': 'fake-iqn', - 'deployment_key': 'fake-deploy-key'} - mock_start_iscsi.return_value = {'command_error': None, - 'command_status': 'SUCCEEDED'} - mock_cont_deploy.return_value = 'fake-root-uuid' + @mock.patch.object(iscsi_deploy, 'do_agent_iscsi_deploy') + @mock.patch.object(pxe, '_destroy_token_file') + def test_continue_deploy_netboot(self, destroy_token_file_mock, + do_agent_iscsi_deploy_mock, + switch_pxe_config_mock, + reboot_and_finish_deploy_mock): + + do_agent_iscsi_deploy_mock.return_value = 'some-root-uuid' + self.driver.vendor.continue_deploy(self.task) - mock_image_info.assert_called_once_with(self.node, self.context) - mock_pxe_opts.assert_called_once_with(self.task.node, mock.ANY, - self.task.context) - mock_start_iscsi.assert_called_once_with(self.node, 'fake-iqn') - mock_pxe_config.assert_called_once_with(mock.ANY, 'fake-root-uuid', - None) - mock_node_power.assert_called_once_with(self.task, states.REBOOT) - self.assertIn('root_uuid', self.node.driver_internal_info) - self.assertIsNone(self.node.last_error) - - def test_continue_deploy_command_failed(self, mock_image_info, - mock_pxe_opts, mock_start_iscsi, - mock_cont_deploy): - command_error = 'Gotham city is in danger!' - mock_start_iscsi.return_value = {'command_error': command_error, - 'command_status': 'FAILED'} - mock_pxe_opts.return_value = {'iscsi_target_iqn': 'fake-iqn', - 'deployment_key': 'fake-deploy-key'} - result = self.assertRaises(exception.InstanceDeployFailure, - self.driver.vendor.continue_deploy, self.task) - self.assertIn(command_error, result.format_message()) - mock_image_info.assert_called_once_with(self.node, self.context) - mock_pxe_opts.assert_called_once_with(self.task.node, mock.ANY, - self.task.context) - mock_start_iscsi.assert_called_once_with(self.node, 'fake-iqn') - # assert iscsi_deploy.continue_deploy wasn't called because the - # iSCSI target couldn't be initiated - self.assertFalse(mock_cont_deploy.called) - self.assertIsNotNone(self.node.last_error) - - @mock.patch.object(deploy_utils, 'switch_pxe_config') - @mock.patch.object(manager_utils, 'node_power_action') - def test_continue_deploy_cannot_reboot(self, mock_node_power, - mock_pxe_config, mock_image_info, - mock_pxe_opts, mock_start_iscsi, - mock_cont_deploy): - mock_pxe_opts.return_value = {'iscsi_target_iqn': 'fake-iqn', - 'deployment_key': 'fake-deploy-key'} - mock_start_iscsi.return_value = {'command_error': None, - 'command_status': 'SUCCEEDED'} - mock_cont_deploy.return_value = 'fake-root-uuid' - mock_node_power.side_effect = processutils.ProcessExecutionError() - result = self.assertRaises(exception.InstanceDeployFailure, - self.driver.vendor.continue_deploy, self.task) - self.assertIn("Error rebooting node %s" % self.node.uuid, - result.format_message()) - mock_image_info.assert_called_once_with(self.node, self.context) - mock_pxe_opts.assert_called_once_with(self.task.node, mock.ANY, - self.task.context) - mock_start_iscsi.assert_called_once_with(self.node, 'fake-iqn') - mock_pxe_config.assert_called_once_with(mock.ANY, 'fake-root-uuid', - None) - self.assertIsNotNone(self.node.last_error) - - def test_continue_deploy_no_root_uuid(self, mock_image_info, mock_pxe_opts, - mock_start_iscsi, mock_cont_deploy): - mock_pxe_opts.return_value = {'iscsi_target_iqn': 'fake-iqn', - 'deployment_key': 'fake-deploy-key'} - mock_start_iscsi.return_value = {'command_error': None, - 'command_status': 'SUCCEEDED'} - mock_cont_deploy.return_value = None - result = self.assertRaises(exception.InstanceDeployFailure, - self.driver.vendor.continue_deploy, self.task) - self.assertIn("Couldn't determine the UUID of the root partition " - "when deploying node %s" % self.node.uuid, - result.format_message()) - mock_image_info.assert_called_once_with(self.node, self.context) - mock_pxe_opts.assert_called_once_with(self.task.node, mock.ANY, - self.task.context) - mock_start_iscsi.assert_called_once_with(self.node, 'fake-iqn') - self.assertNotIn('root_uuid', self.node.driver_internal_info) - self.assertIsNotNone(self.node.last_error) + destroy_token_file_mock.assert_called_once_with(self.node) + do_agent_iscsi_deploy_mock.assert_called_once_with( + self.task, self.driver.vendor._client) + tftp_config = '/tftpboot/%s/config' % self.node.uuid + switch_pxe_config_mock.assert_called_once_with(tftp_config, + 'some-root-uuid', + None) + reboot_and_finish_deploy_mock.assert_called_once_with(self.task) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'reboot_and_finish_deploy') @mock.patch.object(pxe_utils, 'clean_up_pxe_config') - @mock.patch.object(manager_utils, 'node_set_boot_device') - @mock.patch.object(agent_client.AgentClient, 'install_bootloader') - @mock.patch.object(deploy_utils, 'switch_pxe_config') - @mock.patch.object(manager_utils, 'node_power_action') - def test_continue_deploy_localboot(self, mock_node_power, mock_pxe_config, - mock_install_bootloader, mock_boot_dev, - mock_clean_pxe, mock_image_info, mock_pxe_opts, - mock_start_iscsi, mock_cont_deploy): - mock_pxe_opts.return_value = {'iscsi_target_iqn': 'fake-iqn', - 'deployment_key': 'fake-deploy-key'} - mock_start_iscsi.return_value = {'command_error': None, - 'command_status': 'SUCCEEDED'} - mock_cont_deploy.return_value = 'fake-root-uuid' - i_info = self.node.instance_info - i_info['capabilities'] = {'boot_option': 'local'} - mock_install_bootloader.return_value = {'command_status': 'SUCCEEDED'} + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'configure_local_boot') + @mock.patch.object(iscsi_deploy, 'do_agent_iscsi_deploy') + @mock.patch.object(pxe, '_destroy_token_file') + def test_continue_deploy_localboot(self, destroy_token_file_mock, + do_agent_iscsi_deploy_mock, + configure_local_boot_mock, + clean_up_pxe_config_mock, + reboot_and_finish_deploy_mock): + + self.node.instance_info = { + 'capabilities': {'boot_option': 'local'}} + self.node.save() + do_agent_iscsi_deploy_mock.return_value = 'some-root-uuid' + self.driver.vendor.continue_deploy(self.task) - - mock_image_info.assert_called_once_with(self.node, self.context) - mock_pxe_opts.assert_called_once_with(self.task.node, mock.ANY, - self.task.context) - mock_start_iscsi.assert_called_once_with(self.node, 'fake-iqn') - self.assertIn('root_uuid', self.node.driver_internal_info) - mock_node_power.assert_called_once_with(self.task, states.REBOOT) - mock_install_bootloader.assert_called_once_with(self.node, - 'fake-root-uuid') - self.assertIsNone(self.node.last_error) - # Assert we clean up the PXE configuration and make set the boot - # device to boot from disk - mock_clean_pxe.assert_called_once_with(self.task) - mock_boot_dev.assert_called_once_with(self.task, boot_devices.DISK, - persistent=True) - - # Assert we dont try to switch the PXE configuration - self.assertFalse(mock_pxe_config.called) - - @mock.patch.object(agent_client.AgentClient, 'install_bootloader') - def test_continue_deploy_localboot_command_failed(self, - mock_install_bootloader, mock_image_info, - mock_pxe_opts, mock_start_iscsi, - mock_cont_deploy): - command_error = 'Gotham city is in danger!' - mock_install_bootloader.return_value = {'command_error': command_error, - 'command_status': 'FAILED'} - mock_pxe_opts.return_value = {'iscsi_target_iqn': 'fake-iqn', - 'deployment_key': 'fake-deploy-key'} - mock_start_iscsi.return_value = {'command_error': None, - 'command_status': 'SUCCEEDED'} - mock_cont_deploy.return_value = 'fake-root-uuid' - i_info = self.node.instance_info - i_info['capabilities'] = {'boot_option': 'local'} - - result = self.assertRaises(exception.InstanceDeployFailure, - self.driver.vendor.continue_deploy, self.task) - self.assertIn(command_error, result.format_message()) - mock_image_info.assert_called_once_with(self.node, self.context) - mock_pxe_opts.assert_called_once_with(self.task.node, mock.ANY, - self.task.context) - mock_start_iscsi.assert_called_once_with(self.node, 'fake-iqn') - mock_install_bootloader.assert_called_once_with(self.node, - 'fake-root-uuid') - self.assertIsNotNone(self.node.last_error) - - @mock.patch.object(pxe, 'try_set_boot_device') - @mock.patch.object(agent_client.AgentClient, 'install_bootloader') - def test_continue_deploy_set_boot_device_failed(self, - mock_install_bootloader, mock_set_boot_dev, - mock_image_info, mock_pxe_opts, mock_start_iscsi, - mock_cont_deploy): - error_msg = "User error. Please replace the user." - mock_set_boot_dev.side_effect = exception.IPMIFailure(error_msg) - mock_install_bootloader.return_value = {'command_error': None, - 'command_status': 'SUCCEEDED'} - mock_pxe_opts.return_value = {'iscsi_target_iqn': 'fake-iqn', - 'deployment_key': 'fake-deploy-key'} - mock_start_iscsi.return_value = {'command_error': None, - 'command_status': 'SUCCEEDED'} - mock_cont_deploy.return_value = 'fake-root-uuid' - i_info = self.node.instance_info - i_info['capabilities'] = {'boot_option': 'local'} - - result = self.assertRaises(exception.InstanceDeployFailure, - self.driver.vendor.continue_deploy, self.task) - self.assertIn(error_msg, result.format_message()) - mock_image_info.assert_called_once_with(self.node, self.context) - mock_pxe_opts.assert_called_once_with(self.task.node, mock.ANY, - self.task.context) - mock_start_iscsi.assert_called_once_with(self.node, 'fake-iqn') - mock_install_bootloader.assert_called_once_with(self.node, - 'fake-root-uuid') - self.assertIsNotNone(self.node.last_error) + destroy_token_file_mock.assert_called_once_with(self.node) + do_agent_iscsi_deploy_mock.assert_called_once_with( + self.task, self.driver.vendor._client) + configure_local_boot_mock.assert_called_once_with(self.task, + 'some-root-uuid') + clean_up_pxe_config_mock.assert_called_once_with(self.task) + reboot_and_finish_deploy_mock.assert_called_once_with(self.task)