diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index 985914b083..a6a64fea9c 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -29,6 +29,7 @@ from oslo.utils import excutils from oslo.utils import units from oslo_concurrency import processutils from oslo_config import cfg +from oslo_serialization import jsonutils import requests import six @@ -677,3 +678,39 @@ def get_single_nic_with_vif_port_id(task): for port in task.ports: if port.extra.get('vif_port_id'): return port.address + + +def parse_instance_info_capabilities(node): + """Parse the instance_info capabilities. + + These capabilities are defined in the Flavor extra_spec and passed + to Ironic by the Nova Ironic driver. + + NOTE: Although our API fully supports JSON fields, to maintain the + backward compatibility with Juno the Nova Ironic driver is sending + it as a string. + + :param node: a single Node. + :raises: InvalidParameterValue if the capabilities string is not a + dictionary or is malformed. + :returns: A dictionary with the capabilities if found, otherwise an + empty dictionary. + """ + + def parse_error(): + error_msg = (_("Error parsing capabilities from Node %s instance_info " + "field. A dictionary or a dictionary string is " + "expected.") % node.uuid) + raise exception.InvalidParameterValue(error_msg) + + capabilities = node.instance_info.get('capabilities', {}) + if isinstance(capabilities, six.string_types): + try: + capabilities = jsonutils.loads(capabilities) + except (ValueError, TypeError): + parse_error() + + if not isinstance(capabilities, dict): + parse_error() + + return capabilities diff --git a/ironic/drivers/modules/ipxe_config.template b/ironic/drivers/modules/ipxe_config.template index e25d7f01f5..ecbc86f3c7 100644 --- a/ironic/drivers/modules/ipxe_config.template +++ b/ironic/drivers/modules/ipxe_config.template @@ -5,7 +5,7 @@ dhcp goto deploy :deploy -kernel {{ pxe_options.deployment_aki_path }} selinux=0 disk={{ pxe_options.disk }} iscsi_target_iqn={{ pxe_options.iscsi_target_iqn }} deployment_id={{ pxe_options.deployment_id }} deployment_key={{ pxe_options.deployment_key }} ironic_api_url={{ pxe_options.ironic_api_url }} troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }} ip=${ip}:${next-server}:${gateway}:${netmask} BOOTIF=${mac} {% if pxe_options.root_device %}root_device={{ pxe_options.root_device }}{% endif %} +kernel {{ pxe_options.deployment_aki_path }} selinux=0 disk={{ pxe_options.disk }} iscsi_target_iqn={{ pxe_options.iscsi_target_iqn }} deployment_id={{ pxe_options.deployment_id }} deployment_key={{ pxe_options.deployment_key }} ironic_api_url={{ pxe_options.ironic_api_url }} troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }} boot_option={{ pxe_options.boot_option }} ip=${ip}:${next-server}:${gateway}:${netmask} BOOTIF=${mac} {% if pxe_options.root_device %}root_device={{ pxe_options.root_device }}{% endif %} initrd {{ pxe_options.deployment_ari_path }} boot diff --git a/ironic/drivers/modules/iscsi_deploy.py b/ironic/drivers/modules/iscsi_deploy.py index 3aeacbb0a2..d929f5a724 100644 --- a/ironic/drivers/modules/iscsi_deploy.py +++ b/ironic/drivers/modules/iscsi_deploy.py @@ -316,6 +316,18 @@ def parse_root_device_hints(node): return ','.join(hints) +def get_boot_option(node): + """Get the boot mode. + + :param node: A single Node. + :raises: InvalidParameterValue if the capabilities string is not a + dict or is malformed. + :returns: A string representing the boot mode type. Defaults to 'netboot'. + """ + capabilities = deploy_utils.parse_instance_info_capabilities(node) + return capabilities.get('boot_option', 'netboot').lower() + + def build_deploy_ramdisk_options(node): """Build the ramdisk config options for a node @@ -343,6 +355,7 @@ def build_deploy_ramdisk_options(node): 'iscsi_target_iqn': "iqn-%s" % node.uuid, 'ironic_api_url': ironic_api, 'disk': CONF.pxe.disk_devices, + 'boot_option': get_boot_option(node), } root_device = parse_root_device_hints(node) diff --git a/ironic/drivers/modules/pxe.py b/ironic/drivers/modules/pxe.py index 80097803be..912c7813b3 100644 --- a/ironic/drivers/modules/pxe.py +++ b/ironic/drivers/modules/pxe.py @@ -22,6 +22,7 @@ import shutil from oslo_config import cfg +from ironic.common import boot_devices from ironic.common import dhcp_factory from ironic.common import exception from ironic.common.i18n import _ @@ -267,6 +268,24 @@ 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.""" @@ -280,9 +299,24 @@ class PXEDeploy(base.DeployInterface): :raises: InvalidParameterValue. :raises: MissingParameterValue """ + node = task.node - # Check the boot_mode capability parameter value. - driver_utils.validate_boot_mode_capability(task.node) + # Check the boot_mode and boot_option capabilities values. + driver_utils.validate_boot_mode_capability(node) + driver_utils.validate_boot_option_capability(node) + + boot_mode = driver_utils.get_node_capability(node, 'boot_mode') + boot_option = driver_utils.get_node_capability(node, 'boot_option') + + # NOTE(lucasagomes): We don't support UEFI + localboot because + # we do not support creating an EFI boot partition, including the + # EFI modules and managing the bootloader variables via efibootmgr. + if boot_mode == 'uefi' and boot_option == 'local': + error_msg = (_("Local boot is requested, but can't be " + "used with node %s because it's configured " + "to use UEFI boot") % node.uuid) + LOG.error(error_msg) + raise exception.InvalidParameterValue(error_msg) if CONF.pxe.ipxe_enabled: if not CONF.pxe.http_url or not CONF.pxe.http_root: @@ -290,16 +324,15 @@ class PXEDeploy(base.DeployInterface): "iPXE boot is enabled but no HTTP URL or HTTP " "root was specified.")) # iPXE and UEFI should not be configured together. - if driver_utils.get_node_capability(task.node, - 'boot_mode') == 'uefi': + if boot_mode == 'uefi': LOG.error(_LE("UEFI boot mode is not supported with " "iPXE boot enabled.")) raise exception.InvalidParameterValue(_( "Conflict: iPXE is enabled, but cannot be used with node" "%(node_uuid)s configured to use UEFI boot") % - {'node_uuid': task.node.uuid}) + {'node_uuid': node.uuid}) - d_info = _parse_deploy_info(task.node) + d_info = _parse_deploy_info(node) iscsi_deploy.validate(task) @@ -331,22 +364,7 @@ class PXEDeploy(base.DeployInterface): provider = dhcp_factory.DHCPFactory() provider.update_dhcp(task, dhcp_opts) - # 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, 'pxe', persistent=True) - 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 is in UEFI boot mode." - "Please set the boot device manually.")) - else: - raise - + try_set_boot_device(task, boot_devices.PXE) manager_utils.node_power_action(task, states.REBOOT) return states.DEPLOYWAIT @@ -390,6 +408,9 @@ class PXEDeploy(base.DeployInterface): pxe_utils.create_pxe_config(task, pxe_options, pxe_config_template) + + # FIXME(lucasagomes): If it's local boot we should not cache + # the image kernel and ramdisk (Or even require it). _cache_ramdisk_kernel(task.context, task.node, pxe_info) def clean_up(self, task): @@ -425,6 +446,13 @@ class PXEDeploy(base.DeployInterface): provider = dhcp_factory.DHCPFactory() provider.update_dhcp(task, dhcp_opts) + if iscsi_deploy.get_boot_option(task.node) == "local": + # If it's going to boot from the local disk, we don't need + # PXE config files. They still need to be generated as part + # of the prepare() because the deployment does PXE boot the + # deploy ramdisk + pxe_utils.clean_up_pxe_config(task) + class VendorPassthru(base.VendorInterface): """Interface to mix IPMI and PXE vendor-specific interfaces.""" @@ -444,6 +472,7 @@ class VendorPassthru(base.VendorInterface): :param kwargs: kwargs containins the method's parameters. :raises: InvalidParameterValue if any parameters is invalid. """ + driver_utils.validate_boot_option_capability(task.node) iscsi_deploy.get_deploy_info(task.node, **kwargs) @base.passthru(['POST'], method='pass_deploy_info') @@ -469,12 +498,17 @@ class VendorPassthru(base.VendorInterface): return try: - pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid) - deploy_utils.switch_pxe_config(pxe_config_path, root_uuid, - driver_utils.get_node_capability(node, 'boot_mode')) + if iscsi_deploy.get_boot_option(node) == "local": + 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) + else: + pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid) + deploy_utils.switch_pxe_config(pxe_config_path, root_uuid, + driver_utils.get_node_capability(node, 'boot_mode')) deploy_utils.notify_deploy_complete(kwargs['address']) - LOG.info(_LI('Deployment to node %s done'), node.uuid) task.process_event('done') except Exception as e: diff --git a/ironic/drivers/modules/pxe_config.template b/ironic/drivers/modules/pxe_config.template index 12d2517036..bd4c5cbb2e 100644 --- a/ironic/drivers/modules/pxe_config.template +++ b/ironic/drivers/modules/pxe_config.template @@ -2,7 +2,7 @@ default deploy label deploy kernel {{ pxe_options.deployment_aki_path }} -append initrd={{ pxe_options.deployment_ari_path }} rootfstype=ramfs selinux=0 disk={{ pxe_options.disk }} iscsi_target_iqn={{ pxe_options.iscsi_target_iqn }} deployment_id={{ pxe_options.deployment_id }} deployment_key={{ pxe_options.deployment_key }} ironic_api_url={{ pxe_options.ironic_api_url }} troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }} {% if pxe_options.root_device %}root_device={{ pxe_options.root_device }}{% endif %} +append initrd={{ pxe_options.deployment_ari_path }} rootfstype=ramfs selinux=0 disk={{ pxe_options.disk }} iscsi_target_iqn={{ pxe_options.iscsi_target_iqn }} deployment_id={{ pxe_options.deployment_id }} deployment_key={{ pxe_options.deployment_key }} ironic_api_url={{ pxe_options.ironic_api_url }} troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }} boot_option={{ pxe_options.boot_option }} {% if pxe_options.root_device %}root_device={{ pxe_options.root_device }}{% endif %} ipappend 3 diff --git a/ironic/drivers/utils.py b/ironic/drivers/utils.py index 13ecee42a8..8dc0036632 100644 --- a/ironic/drivers/utils.py +++ b/ironic/drivers/utils.py @@ -198,16 +198,44 @@ def add_node_capability(task, capability, value): node.save() +def validate_capability(node, capability_name, valid_values): + """Validate a capabability set in node property + + :param node: an ironic node object. + :param capability_name: the name of the capability. + :parameter valid_values: an iterable with valid values expected for + that capability. + :raises: InvalidParameterValue, if the capability is not set to the + expected values. + """ + value = get_node_capability(node, capability_name) + + if value and value not in valid_values: + valid_value_str = ', '.join(valid_values) + raise exception.InvalidParameterValue( + _("Invalid %(capability)s parameter '%(value)s'. " + "Acceptable values are: %(valid_values)s.") % + {'capability': capability_name, 'value': value, + 'valid_values': valid_value_str}) + + def validate_boot_mode_capability(node): - """Validate the boot_mode capability set in node property. + """Validate the boot_mode capability set in node properties. :param node: an ironic node object. :raises: InvalidParameterValue, if 'boot_mode' capability is set other than 'bios' or 'uefi' or None. """ - boot_mode = get_node_capability(node, 'boot_mode') + validate_capability(node, 'boot_mode', ('bios', 'uefi')) - if boot_mode and boot_mode not in ['bios', 'uefi']: - raise exception.InvalidParameterValue(_("Invalid boot_mode " - "parameter '%s'.") % boot_mode) + +def validate_boot_option_capability(node): + """Validate the boot_option capability set in node properties. + + :param node: an ironic node object. + :raises: InvalidParameterValue, if 'boot_option' capability is set + other than 'local' or 'netboot' or None. + + """ + validate_capability(node, 'boot_option', ('local', 'netboot')) diff --git a/ironic/tests/drivers/pxe_config.template b/ironic/tests/drivers/pxe_config.template index 5c8931d496..b79bc790bf 100644 --- a/ironic/tests/drivers/pxe_config.template +++ b/ironic/tests/drivers/pxe_config.template @@ -2,7 +2,7 @@ default deploy label deploy kernel /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/deploy_kernel -append initrd=/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/deploy_ramdisk rootfstype=ramfs selinux=0 disk=cciss/c0d0,sda,hda,vda iscsi_target_iqn=iqn-1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_id=1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_key=0123456789ABCDEFGHIJKLMNOPQRSTUV ironic_api_url=http://192.168.122.184:6385 troubleshoot=0 text test_param root_device=vendor=fake,size=123 +append initrd=/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/deploy_ramdisk rootfstype=ramfs selinux=0 disk=cciss/c0d0,sda,hda,vda iscsi_target_iqn=iqn-1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_id=1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_key=0123456789ABCDEFGHIJKLMNOPQRSTUV ironic_api_url=http://192.168.122.184:6385 troubleshoot=0 text test_param boot_option=netboot root_device=vendor=fake,size=123 ipappend 3 diff --git a/ironic/tests/drivers/test_deploy_utils.py b/ironic/tests/drivers/test_deploy_utils.py index 27ce387648..600797bee2 100644 --- a/ironic/tests/drivers/test_deploy_utils.py +++ b/ironic/tests/drivers/test_deploy_utils.py @@ -1081,3 +1081,27 @@ class VirtualMediaDeployUtilsTestCase(db_base.DbTestCase): shared=False) as task: address = utils.get_single_nic_with_vif_port_id(task) self.assertEqual('aa:bb:cc', address) + + +class ParseInstanceInfoCapabilitiesTestCase(tests_base.TestCase): + + def setUp(self): + super(ParseInstanceInfoCapabilitiesTestCase, self).setUp() + self.node = obj_utils.get_test_node(self.context, driver='fake') + + def test_parse_instance_info_capabilities_string(self): + self.node.instance_info = {'capabilities': '{"cat": "meow"}'} + expected_result = {"cat": "meow"} + result = utils.parse_instance_info_capabilities(self.node) + self.assertEqual(expected_result, result) + + def test_parse_instance_info_capabilities(self): + self.node.instance_info = {'capabilities': {"dog": "wuff"}} + expected_result = {"dog": "wuff"} + result = utils.parse_instance_info_capabilities(self.node) + self.assertEqual(expected_result, result) + + def test_parse_instance_info_invalid_type(self): + self.node.instance_info = {'capabilities': 'not-a-dict'} + self.assertRaises(exception.InvalidParameterValue, + utils.parse_instance_info_capabilities, self.node) diff --git a/ironic/tests/drivers/test_iscsi_deploy.py b/ironic/tests/drivers/test_iscsi_deploy.py index 7a00917708..ab1b6ae0a3 100644 --- a/ironic/tests/drivers/test_iscsi_deploy.py +++ b/ironic/tests/drivers/test_iscsi_deploy.py @@ -214,7 +214,8 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): mock_rmtree.assert_called_once_with('/path/uuid') def _test_build_deploy_ramdisk_options(self, mock_alnum, api_url, - expected_root_device=None): + expected_root_device=None, + expected_boot_option='netboot'): fake_key = '0123456789ABCDEFGHIJKLMNOPQRSTUV' fake_disk = 'fake-disk' @@ -226,7 +227,8 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): 'deployment_id': self.node.uuid, 'deployment_key': fake_key, 'disk': fake_disk, - 'ironic_api_url': api_url} + 'ironic_api_url': api_url, + 'boot_option': expected_boot_option} if expected_root_device: expected_opts['root_device'] = expected_root_device @@ -272,6 +274,17 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): self._test_build_deploy_ramdisk_options(mock_alnum, fake_api_url, expected_root_device=expected) + @mock.patch.object(keystone, 'get_service_url') + @mock.patch.object(utils, 'random_alnum') + def test_build_deploy_ramdisk_options_boot_option(self, mock_alnum, + mock_get_url): + self.node.instance_info = {'capabilities': '{"boot_option": "local"}'} + expected = 'local' + fake_api_url = 'http://127.0.0.1:6385' + self.config(api_url=fake_api_url, group='conductor') + self._test_build_deploy_ramdisk_options(mock_alnum, fake_api_url, + expected_boot_option=expected) + def test_parse_root_device_hints(self): self.node.properties['root_device'] = {'wwn': 123456} expected = 'wwn=123456' @@ -288,3 +301,13 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): self.node.properties = {} result = iscsi_deploy.parse_root_device_hints(self.node) self.assertIsNone(result) + + def test_get_boot_option(self): + self.node.instance_info = {'capabilities': '{"boot_option": "local"}'} + result = iscsi_deploy.get_boot_option(self.node) + self.assertEqual("local", result) + + def test_get_boot_option_default_value(self): + self.node.instance_info = {} + result = iscsi_deploy.get_boot_option(self.node) + self.assertEqual("netboot", result) diff --git a/ironic/tests/drivers/test_pxe.py b/ironic/tests/drivers/test_pxe.py index 8d93f2d46e..b4bf3d7e52 100644 --- a/ironic/tests/drivers/test_pxe.py +++ b/ironic/tests/drivers/test_pxe.py @@ -25,6 +25,7 @@ import mock from oslo_config import cfg from oslo_serialization import jsonutils as json +from ironic.common import boot_devices from ironic.common import dhcp_factory from ironic.common import exception from ironic.common.glance_service import base_image_service @@ -158,7 +159,8 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): 'deployment_id': 'fake-deploy-id', 'deployment_key': 'fake-deploy-key', 'disk': 'fake-disk', - 'ironic_api_url': 'fake-api-url'} + 'ironic_api_url': 'fake-api-url', + 'boot_option': 'netboot'} deploy_opts_mock.return_value = fake_deploy_opts @@ -193,7 +195,8 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): 'pxe_append_params': 'test_param', 'aki_path': kernel, 'deployment_aki_path': deploy_kernel, - 'tftp_server': tftp_server + 'tftp_server': tftp_server, + 'boot_option': 'netboot' } expected_options.update(fake_deploy_opts) @@ -353,6 +356,28 @@ class PXEDriverTestCase(db_base.DbTestCase): self.assertRaises(exception.InvalidParameterValue, task.driver.deploy.validate, task) + @mock.patch.object(base_image_service.BaseImageService, '_show') + def test_validate_fail_invalid_boot_option(self, mock_glance): + properties = {'capabilities': 'boot_option:foo,dog:wuff'} + mock_glance.return_value = {'properties': {'kernel_id': 'fake-kernel', + 'ramdisk_id': 'fake-initr'}} + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.properties = properties + self.assertRaises(exception.InvalidParameterValue, + task.driver.deploy.validate, task) + + @mock.patch.object(base_image_service.BaseImageService, '_show') + def test_validate_fail_invalid_uefi_and_localboot(self, mock_glance): + properties = {'capabilities': 'boot_mode:uefi,boot_option:local'} + mock_glance.return_value = {'properties': {'kernel_id': 'fake-kernel', + 'ramdisk_id': 'fake-initr'}} + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.properties = properties + self.assertRaises(exception.InvalidParameterValue, + task.driver.deploy.validate, task) + def test_validate_fail_no_port(self): new_node = obj_utils.create_test_node( self.context, @@ -580,12 +605,34 @@ class PXEDriverTestCase(db_base.DbTestCase): update_dhcp_mock.assert_called_once_with( task, dhcp_opts) + @mock.patch.object(pxe_utils, 'clean_up_pxe_config') + @mock.patch.object(dhcp_factory.DHCPFactory, 'update_dhcp') + def test_take_over_localboot(self, update_dhcp_mock, clean_pxe_mock): + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + task.node.instance_info['capabilities'] = {"boot_option": "local"} + dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + task.driver.deploy.take_over(task) + update_dhcp_mock.assert_called_once_with( + task, dhcp_opts) + clean_pxe_mock.assert_called_once_with(task) + + @mock.patch.object(pxe_utils, 'clean_up_pxe_config') + @mock.patch.object(manager_utils, 'node_set_boot_device') @mock.patch.object(deploy_utils, 'notify_deploy_complete') @mock.patch.object(deploy_utils, 'switch_pxe_config') @mock.patch.object(iscsi_deploy, 'InstanceImageCache') - def test_continue_deploy_good(self, mock_image_cache, mock_switch_config, - notify_mock): + def _test_continue_deploy(self, is_localboot, mock_image_cache, + mock_switch_config, notify_mock, + mock_node_boot_dev, mock_clean_pxe): token_path = self._create_token_file() + + # set local boot + if is_localboot: + i_info = self.node.instance_info + i_info['capabilities'] = '{"boot_option": "local"}' + self.node.instance_info = i_info + self.node.power_state = states.POWER_ON self.node.provision_state = states.DEPLOYWAIT self.node.target_provision_state = states.ACTIVE @@ -614,9 +661,23 @@ class PXEDriverTestCase(db_base.DbTestCase): mock_image_cache.assert_called_once_with() mock_image_cache.return_value.clean_up.assert_called_once_with() pxe_config_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) - mock_switch_config.assert_called_once_with(pxe_config_path, root_uuid, - boot_mode) notify_mock.assert_called_once_with('123456') + if is_localboot: + mock_node_boot_dev.assert_called_once_with( + mock.ANY, boot_devices.DISK, persistent=True) + mock_clean_pxe.assert_called_once_with(mock.ANY) + self.assertFalse(mock_switch_config.called) + else: + mock_switch_config.assert_called_once_with( + pxe_config_path, root_uuid, boot_mode) + self.assertFalse(mock_node_boot_dev.called) + self.assertFalse(mock_clean_pxe.called) + + def test_continue_deploy(self): + self._test_continue_deploy(False) + + def test_continue_deploy_localboot(self): + self._test_continue_deploy(True) @mock.patch.object(iscsi_deploy, 'InstanceImageCache') def test_continue_deploy_fail(self, mock_image_cache): diff --git a/ironic/tests/drivers/test_utils.py b/ironic/tests/drivers/test_utils.py index f37620d165..5570890804 100644 --- a/ironic/tests/drivers/test_utils.py +++ b/ironic/tests/drivers/test_utils.py @@ -133,6 +133,22 @@ class UtilsTestCase(db_base.DbTestCase): self.assertIsNone(driver_utils.rm_node_capability(task, 'x')) self.assertEqual('a:b', task.node.properties['capabilities']) + def test_validate_capability(self): + properties = {'capabilities': 'cat:meow,cap2:value2'} + self.node.properties = properties + + result = driver_utils.validate_capability( + self.node, 'cat', ['meow', 'purr']) + self.assertIsNone(result) + + def test_validate_capability_with_exception(self): + properties = {'capabilities': 'cat:bark,cap2:value2'} + self.node.properties = properties + + self.assertRaises(exception.InvalidParameterValue, + driver_utils.validate_capability, + self.node, 'cat', ['meow', 'purr']) + def test_validate_boot_mode_capability(self): properties = {'capabilities': 'boot_mode:uefi,cap2:value2'} self.node.properties = properties @@ -146,3 +162,17 @@ class UtilsTestCase(db_base.DbTestCase): self.assertRaises(exception.InvalidParameterValue, driver_utils.validate_boot_mode_capability, self.node) + + def test_validate_boot_option_capability(self): + properties = {'capabilities': 'boot_option:netboot,cap2:value2'} + self.node.properties = properties + + result = driver_utils.validate_boot_option_capability(self.node) + self.assertIsNone(result) + + def test_validate_boot_option_capability_with_exception(self): + properties = {'capabilities': 'boot_option:foo,cap2:value2'} + self.node.properties = properties + + self.assertRaises(exception.InvalidParameterValue, + driver_utils.validate_boot_option_capability, self.node) diff --git a/ironic/tests/test_pxe_utils.py b/ironic/tests/test_pxe_utils.py index 7154180d3f..7065908377 100644 --- a/ironic/tests/test_pxe_utils.py +++ b/ironic/tests/test_pxe_utils.py @@ -49,7 +49,8 @@ class TestPXEUtils(db_base.DbTestCase): 'deployment_aki_path': u'/tftpboot/1be26c0b-03f2-4d2e-ae87-' u'c02d7f33c123/deploy_kernel', 'disk': 'cciss/c0d0,sda,hda,vda', - 'root_device': 'vendor=fake,size=123' + 'root_device': 'vendor=fake,size=123', + 'boot_option': 'netboot', } self.agent_pxe_options = { 'deployment_ari_path': u'/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7'