diff --git a/ironic/drivers/generic.py b/ironic/drivers/generic.py index 6f3d280e57..693c2a84d3 100644 --- a/ironic/drivers/generic.py +++ b/ironic/drivers/generic.py @@ -47,7 +47,7 @@ class GenericHardware(hardware_type.AbstractHardwareType): def supported_deploy_interfaces(self): """List of supported deploy interfaces.""" return [iscsi_deploy.ISCSIDeploy, agent.AgentDeploy, - ansible_deploy.AnsibleDeploy] + ansible_deploy.AnsibleDeploy, pxe.PXERamdiskDeploy] @property def supported_inspect_interfaces(self): diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index 3b9920c679..d40d721ce0 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -58,7 +58,7 @@ LOG = logging.getLogger(__name__) METRICS = metrics_utils.get_metrics_logger(__name__) SUPPORTED_CAPABILITIES = { - 'boot_option': ('local', 'netboot'), + 'boot_option': ('local', 'netboot', 'ramdisk'), 'boot_mode': ('bios', 'uefi'), 'secure_boot': ('true', 'false'), 'trusted_boot': ('true', 'false'), @@ -284,13 +284,16 @@ def _replace_root_uuid(path, root_uuid): def _replace_boot_line(path, boot_mode, is_whole_disk_image, - trusted_boot=False, iscsi_boot=False): + trusted_boot=False, iscsi_boot=False, + ramdisk_boot=False): if is_whole_disk_image: boot_disk_type = 'boot_whole_disk' elif trusted_boot: boot_disk_type = 'trusted_boot' elif iscsi_boot: boot_disk_type = 'boot_iscsi' + elif ramdisk_boot: + boot_disk_type = 'boot_ramdisk' else: boot_disk_type = 'boot_partition' @@ -312,7 +315,7 @@ def _replace_disk_identifier(path, disk_identifier): def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode, is_whole_disk_image, trusted_boot=False, - iscsi_boot=False): + iscsi_boot=False, ramdisk_boot=False): """Switch a pxe config from deployment mode to service mode. :param path: path to the pxe config file in tftpboot. @@ -324,14 +327,16 @@ def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode, is_whole_disk_image and trusted_boot are mutually exclusive. You can have one or neither, but not both. :param iscsi_boot: if boot is from an iSCSI volume or not. + :param ramdisk_boot: if the boot is to be to a ramdisk configuration. """ - if not is_whole_disk_image: - _replace_root_uuid(path, root_uuid_or_disk_id) - else: - _replace_disk_identifier(path, root_uuid_or_disk_id) + if not ramdisk_boot: + if not is_whole_disk_image: + _replace_root_uuid(path, root_uuid_or_disk_id) + else: + _replace_disk_identifier(path, root_uuid_or_disk_id) _replace_boot_line(path, boot_mode, is_whole_disk_image, trusted_boot, - iscsi_boot) + iscsi_boot, ramdisk_boot) def get_dev(address, port, iqn, lun): @@ -365,7 +370,8 @@ def deploy_partition_image( partition table has not changed). :param configdrive: Optional. Base64 encoded Gzipped configdrive content or configdrive HTTP URL. - :param boot_option: Can be "local" or "netboot". "netboot" by default. + :param boot_option: Can be "local" or "netboot", or "ramdisk". + "netboot" by default. :param boot_mode: Can be "bios" or "uefi". "bios" by default. :param disk_label: The disk label to be used when creating the partition table. Valid values are: "msdos", "gpt" or None; If None diff --git a/ironic/drivers/modules/ipxe_config.template b/ironic/drivers/modules/ipxe_config.template index 0f530244ac..2fa31c1bf0 100644 --- a/ironic/drivers/modules/ipxe_config.template +++ b/ironic/drivers/modules/ipxe_config.template @@ -30,6 +30,12 @@ imgfree kernel {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.aki_path }} root={{ ROOT }} ro text {{ pxe_options.pxe_append_params|default("", true) }} initrd=ramdisk || goto boot_partition initrd {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.ari_path }} || goto boot_partition boot + +:boot_ramdisk +imgfree +kernel {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.aki_path }} root=/dev/ram0 text {{ pxe_options.pxe_append_params|default("", true) }} {{ pxe_options.ramdisk_opts|default('', true) }} initrd=ramdisk || goto boot_ramdisk +initrd {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.ari_path }} || goto boot_ramdisk +boot {%- if pxe_options.boot_from_volume %} :boot_iscsi diff --git a/ironic/drivers/modules/pxe.py b/ironic/drivers/modules/pxe.py index 6dc9478821..92d78454f9 100644 --- a/ironic/drivers/modules/pxe.py +++ b/ironic/drivers/modules/pxe.py @@ -35,6 +35,7 @@ from ironic.common import states from ironic.conductor import utils as manager_utils from ironic.conf import CONF from ironic.drivers import base +from ironic.drivers.modules import agent from ironic.drivers.modules import boot_mode_utils from ironic.drivers.modules import deploy_utils from ironic.drivers.modules import image_cache @@ -211,6 +212,18 @@ def _build_instance_pxe_options(task, pxe_info): pxe_opts.setdefault('aki_path', 'no_kernel') pxe_opts.setdefault('ari_path', 'no_ramdisk') + # TODO(TheJulia): We should only do this if we have a ramdisk interface. + # We should check the capabilities of the class, but that becomes a bit + # of a pain for unit testing. We can sort this out in Stein since we will + # need to revisit a major portion of this file to effetively begin the + # ipxe boot interface promotion. + if isinstance(task.driver.deploy, PXERamdiskDeploy): + i_info = task.node.instance_info + try: + pxe_opts['ramdisk_opts'] = i_info['ramdisk_kernel_arguments'] + except KeyError: + pass + return pxe_opts @@ -266,7 +279,8 @@ def _build_pxe_config_options(task, pxe_info, service=False): def _build_service_pxe_config(task, instance_image_info, - root_uuid_or_disk_id): + root_uuid_or_disk_id, + ramdisk_boot=False): node = task.node pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid) # NOTE(pas-ha) if it is takeover of ACTIVE node or node performing @@ -283,7 +297,7 @@ def _build_service_pxe_config(task, instance_image_info, pxe_config_path, root_uuid_or_disk_id, boot_mode_utils.get_boot_mode_for_deploy(node), iwdi, deploy_utils.is_trusted_boot_requested(node), - deploy_utils.is_iscsi_boot(task)) + deploy_utils.is_iscsi_boot(task), ramdisk_boot) def _get_volume_pxe_options(task): @@ -417,7 +431,7 @@ def _clean_up_pxe_env(task, images_info): class PXEBoot(base.BootInterface): - capabilities = ['iscsi_volume_boot'] + capabilities = ['iscsi_volume_boot', 'ramdisk_boot'] def __init__(self): if CONF.pxe.ipxe_enabled: @@ -597,7 +611,6 @@ class PXEBoot(base.BootInterface): node = task.node boot_option = deploy_utils.get_boot_option(node) boot_device = None - if deploy_utils.is_iscsi_boot(task): dhcp_opts = pxe_utils.dhcp_options_for_instance(task) provider = dhcp_factory.DHCPFactory() @@ -618,6 +631,22 @@ class PXEBoot(base.BootInterface): iscsi_boot=True) boot_device = boot_devices.PXE + elif boot_option == "ramdisk": + instance_image_info = _get_instance_image_info( + task.node, task.context) + _cache_ramdisk_kernel(task.context, task.node, + instance_image_info) + dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + provider = dhcp_factory.DHCPFactory() + provider.update_dhcp(task, dhcp_opts) + pxe_config_path = pxe_utils.get_pxe_config_file_path( + task.node.uuid) + deploy_utils.switch_pxe_config( + pxe_config_path, None, + boot_mode_utils.get_boot_mode_for_deploy(node), False, + iscsi_boot=False, ramdisk_boot=True) + boot_device = boot_devices.PXE + elif boot_option != "local": if task.driver.storage.should_write_image(task): # Make sure that the instance kernel/ramdisk is cached. @@ -702,3 +731,79 @@ class PXEBoot(base.BootInterface): parameters """ _parse_driver_info(task.node, mode='rescue') + + +class PXERamdiskDeploy(agent.AgentDeploy, agent.AgentDeployMixin, + base.DeployInterface): + + def validate(self, task): + # Initially this is likely okay, we can iterate on this and + # enable other drivers that have similar functionality that + # be invoked in a ramdisk friendly way. + if not isinstance(task.driver.boot, PXEBoot): + raise exception.InvalidParameterValue( + err=('Invalid configuration: The ramdisk deploy ' + 'interface requires the pxe boot interface.')) + # Eventually we should be doing this. + if 'ramdisk_boot' not in task.driver.boot.capabilities: + raise exception.InvalidParameterValue( + err=('Invalid configuration: The boot interface ' + 'must have the `ramdisk_boot` capability. ' + 'Not found.')) + task.driver.boot.validate(task) + + # Validate node capabilities + deploy_utils.validate_capabilities(task.node) + + def deploy(self, task): + if 'configdrive' in task.node.instance_info: + LOG.warning('A configuration drive is present with ' + 'in the deployment request of node %(node)s. ' + 'The configuration drive will be ignored for ' + 'this deployment.', + {'node': task.node}) + manager_utils.node_power_action(task, states.POWER_OFF) + # Tenant neworks must enable connectivity to the boot + # location, as reboot() can otherwise be very problematic. + # IDEA(TheJulia): Maybe a "trusted environment" mode flag + # that we otherwise fail validation on for drivers that + # require explicit security postures. + task.driver.network.configure_tenant_networks(task) + + # calling boot.prepare_instance will also set the node + # to PXE boot, and update PXE templates accordingly + task.driver.boot.prepare_instance(task) + + # Power-on the instance, with PXE prepared, we're done. + manager_utils.node_power_action(task, states.POWER_ON) + LOG.info('Deployment setup for node %s done', task.node.uuid) + # TODO(TheJulia): Update this in stein to support deploy steps. + return states.DEPLOYDONE + + def prepare(self, task): + node = task.node + # Log a warning if the boot_option is wrong... and + # otherwise reset it. + if deploy_utils.get_boot_option(node) != 'ramdisk': + LOG.warning('Incorrect "boot_option" set for node %(node)s ' + 'and will be overridden to "ramdisk" as the ' + 'to match the deploy interface.', + {'node': node.uuid}) + i_info = task.node.instance_info + i_info.update({'capabilities': {'boot_option': 'ramdisk'}}) + node.instance_info = i_info + node.save() + + deploy_utils.populate_storage_driver_internal_info(task) + if node.provision_state == states.DEPLOYING: + # Ask the network interface to validate itself so + # we can ensure we are able to proceed. + task.driver.network.validate(task) + + manager_utils.node_power_action(task, states.POWER_OFF) + # NOTE(TheJulia): If this was any other interface, we would + # unconfigure tenant networks, add provisioning networks, etc. + task.driver.storage.attach_volumes(task) + if node.provision_state in (states.ACTIVE, states.UNRESCUING): + # In the event of takeover or unrescue. + task.driver.boot.prepare_instance(task) diff --git a/ironic/drivers/modules/pxe_config.template b/ironic/drivers/modules/pxe_config.template index bdffdd7ab3..555938eb89 100644 --- a/ironic/drivers/modules/pxe_config.template +++ b/ironic/drivers/modules/pxe_config.template @@ -18,3 +18,7 @@ append mbr:{{ DISK_IDENTIFIER }} label trusted_boot kernel mboot append tboot.gz --- {{pxe_options.aki_path}} root={{ ROOT }} ro text {{ pxe_options.pxe_append_params|default("", true) }} intel_iommu=on --- {{pxe_options.ari_path}} + +label boot_ramdisk +kernel {{ pxe_options.aki_path }} +append initrd={{ pxe_options.ari_path }} root=/dev/ram0 text {{ pxe_options.pxe_append_params|default("", true) }} {{ pxe_options.ramdisk_opts|default('', true) }} diff --git a/ironic/drivers/modules/pxe_grub_config.template b/ironic/drivers/modules/pxe_grub_config.template index fe22418db0..b27e757d79 100644 --- a/ironic/drivers/modules/pxe_grub_config.template +++ b/ironic/drivers/modules/pxe_grub_config.template @@ -12,6 +12,11 @@ menuentry "boot_partition" { initrdefi {{ pxe_options.ari_path }} } +menuentry "boot_ramdisk" { + linuxefi {{ pxe_options.deployment_aki_path }} root=/dev/ram0 text {{ pxe_options.pxe_append_params|default("", true) }} {{ pxe_options.ramdisk_opts|default('', true) }} + initrdefi {{ pxe_options.deployment_ari_path }} +} + menuentry "boot_whole_disk" { linuxefi chain.c32 mbr:{{ DISK_IDENTIFIER }} } diff --git a/ironic/tests/unit/common/test_pxe_utils.py b/ironic/tests/unit/common/test_pxe_utils.py index a2677d7c37..bc174d11d3 100644 --- a/ironic/tests/unit/common/test_pxe_utils.py +++ b/ironic/tests/unit/common/test_pxe_utils.py @@ -49,6 +49,7 @@ class TestPXEUtils(db_base.DbTestCase): u'f33c123/deploy_ramdisk', 'ipa-api-url': 'http://192.168.122.184:6385', 'ipxe_timeout': 0, + 'ramdisk_opts': 'ramdisk_param', } self.ipxe_options = self.pxe_options.copy() diff --git a/ironic/tests/unit/drivers/ipxe_config.template b/ironic/tests/unit/drivers/ipxe_config.template index e0eca334f5..bb0c63db7f 100644 --- a/ironic/tests/unit/drivers/ipxe_config.template +++ b/ironic/tests/unit/drivers/ipxe_config.template @@ -31,5 +31,11 @@ kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramd initrd http://1.2.3.4:1234/ramdisk || goto boot_partition boot +:boot_ramdisk +imgfree +kernel http://1.2.3.4:1234/kernel root=/dev/ram0 text test_param ramdisk_param initrd=ramdisk || goto boot_ramdisk +initrd http://1.2.3.4:1234/ramdisk || goto boot_ramdisk +boot + :boot_whole_disk sanboot --no-describe diff --git a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_extra_volume.template b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_extra_volume.template index 9b86e04cec..cc8029f0cf 100644 --- a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_extra_volume.template +++ b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_extra_volume.template @@ -31,6 +31,12 @@ kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramd initrd http://1.2.3.4:1234/ramdisk || goto boot_partition boot +:boot_ramdisk +imgfree +kernel http://1.2.3.4:1234/kernel root=/dev/ram0 text test_param ramdisk_param initrd=ramdisk || goto boot_ramdisk +initrd http://1.2.3.4:1234/ramdisk || goto boot_ramdisk +boot + :boot_iscsi imgfree set username fake_username diff --git a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_extra_volumes.template b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_extra_volumes.template index 244eb53c86..32a191dbbb 100644 --- a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_extra_volumes.template +++ b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_extra_volumes.template @@ -31,6 +31,12 @@ kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramd initrd http://1.2.3.4:1234/ramdisk || goto boot_partition boot +:boot_ramdisk +imgfree +kernel http://1.2.3.4:1234/kernel root=/dev/ram0 text test_param ramdisk_param initrd=ramdisk || goto boot_ramdisk +initrd http://1.2.3.4:1234/ramdisk || goto boot_ramdisk +boot + :boot_iscsi imgfree set username fake_username diff --git a/ironic/tests/unit/drivers/ipxe_config_timeout.template b/ironic/tests/unit/drivers/ipxe_config_timeout.template index 821b66ad3d..82f3efba46 100644 --- a/ironic/tests/unit/drivers/ipxe_config_timeout.template +++ b/ironic/tests/unit/drivers/ipxe_config_timeout.template @@ -31,5 +31,11 @@ kernel --timeout 120 http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_par initrd --timeout 120 http://1.2.3.4:1234/ramdisk || goto boot_partition boot +:boot_ramdisk +imgfree +kernel --timeout 120 http://1.2.3.4:1234/kernel root=/dev/ram0 text test_param ramdisk_param initrd=ramdisk || goto boot_ramdisk +initrd --timeout 120 http://1.2.3.4:1234/ramdisk || goto boot_ramdisk +boot + :boot_whole_disk sanboot --no-describe diff --git a/ironic/tests/unit/drivers/modules/test_deploy_utils.py b/ironic/tests/unit/drivers/modules/test_deploy_utils.py index 6bf0543c49..066d57420c 100644 --- a/ironic/tests/unit/drivers/modules/test_deploy_utils.py +++ b/ironic/tests/unit/drivers/modules/test_deploy_utils.py @@ -1420,7 +1420,7 @@ class ParseInstanceInfoCapabilitiesTestCase(tests_base.TestCase): utils.validate_capabilities, self.node) def test_all_supported_capabilities(self): - self.assertEqual(('local', 'netboot'), + self.assertEqual(('local', 'netboot', 'ramdisk'), utils.SUPPORTED_CAPABILITIES['boot_option']) self.assertEqual(('bios', 'uefi'), utils.SUPPORTED_CAPABILITIES['boot_mode']) diff --git a/ironic/tests/unit/drivers/modules/test_pxe.py b/ironic/tests/unit/drivers/modules/test_pxe.py index a14ec03149..a423654834 100644 --- a/ironic/tests/unit/drivers/modules/test_pxe.py +++ b/ironic/tests/unit/drivers/modules/test_pxe.py @@ -1254,7 +1254,7 @@ class PXEBootTestCase(db_base.DbTestCase): provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) switch_pxe_config_mock.assert_called_once_with( pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50", - 'bios', False, False, False) + 'bios', False, False, False, False) set_boot_device_mock.assert_called_once_with(task, boot_devices.PXE, persistent=True) @@ -1297,7 +1297,7 @@ class PXEBootTestCase(db_base.DbTestCase): task, mock.ANY, CONF.pxe.pxe_config_template) switch_pxe_config_mock.assert_called_once_with( pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50", - 'bios', False, False, False) + 'bios', False, False, False, False) self.assertFalse(set_boot_device_mock.called) @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) @@ -1467,6 +1467,180 @@ class PXEBootTestCase(db_base.DbTestCase): task.node, task.context) +class PXEBootDeployTestCase(db_base.DbTestCase): + + driver = 'fake-hardware' + + def setUp(self): + super(PXEBootDeployTestCase, self).setUp() + self.temp_dir = tempfile.mkdtemp() + self.config(tftp_root=self.temp_dir, group='pxe') + self.temp_dir = tempfile.mkdtemp() + self.config(images_path=self.temp_dir, group='pxe') + self.config(enabled_deploy_interfaces=['ramdisk']) + self.config(enabled_boot_interfaces=['pxe']) + for iface in drivers_base.ALL_INTERFACES: + impl = 'fake' + if iface == 'network': + impl = 'noop' + if iface == 'deploy': + impl = 'ramdisk' + if iface == 'boot': + impl = 'pxe' + config_kwarg = {'enabled_%s_interfaces' % iface: [impl], + 'default_%s_interface' % iface: impl} + self.config(**config_kwarg) + self.config(enabled_hardware_types=[self.driver]) + instance_info = INST_INFO_DICT + self.node = obj_utils.create_test_node( + self.context, + driver=self.driver, + instance_info=instance_info, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT) + self.port = obj_utils.create_test_port(self.context, + node_id=self.node.id) + self.config(group='conductor', api_url='http://127.0.0.1:1234/') + + @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) + @mock.patch.object(deploy_utils, 'switch_pxe_config', autospec=True) + @mock.patch.object(dhcp_factory, 'DHCPFactory', autospec=True) + @mock.patch.object(pxe, '_cache_ramdisk_kernel', autospec=True) + @mock.patch.object(pxe, '_get_instance_image_info', autospec=True) + def test_prepare_instance_ramdisk( + self, get_image_info_mock, cache_mock, + dhcp_factory_mock, switch_pxe_config_mock, + set_boot_device_mock): + provider_mock = mock.MagicMock() + dhcp_factory_mock.return_value = provider_mock + self.node.provision_state = states.DEPLOYING + image_info = {'kernel': ('', '/path/to/kernel'), + 'ramdisk': ('', '/path/to/ramdisk')} + get_image_info_mock.return_value = image_info + with task_manager.acquire(self.context, self.node.uuid) as task: + dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + pxe_config_path = pxe_utils.get_pxe_config_file_path( + task.node.uuid) + task.node.properties['capabilities'] = 'boot_option:netboot' + task.node.driver_internal_info['is_whole_disk_image'] = False + task.driver.deploy.prepare(task) + task.driver.deploy.deploy(task) + + get_image_info_mock.assert_called_once_with( + task.node, task.context) + cache_mock.assert_called_once_with( + task.context, task.node, image_info) + provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) + switch_pxe_config_mock.assert_called_once_with( + pxe_config_path, None, + 'bios', False, iscsi_boot=False, ramdisk_boot=True) + set_boot_device_mock.assert_called_once_with(task, + boot_devices.PXE, + persistent=True) + + @mock.patch.object(pxe.LOG, 'warning', autospec=True) + @mock.patch.object(deploy_utils, 'switch_pxe_config', autospec=True) + @mock.patch.object(dhcp_factory, 'DHCPFactory', autospec=True) + @mock.patch.object(pxe, '_cache_ramdisk_kernel', autospec=True) + @mock.patch.object(pxe, '_get_instance_image_info', autospec=True) + def test_deploy(self, mock_image_info, mock_cache, + mock_dhcp_factory, mock_switch_config, mock_warning): + image_info = {'kernel': ('', '/path/to/kernel'), + 'ramdisk': ('', '/path/to/ramdisk')} + mock_image_info.return_value = image_info + i_info = self.node.instance_info + i_info.update({'capabilities': {'boot_option': 'ramdisk'}}) + self.node.instance_info = i_info + self.node.save() + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertEqual(states.DEPLOYDONE, + task.driver.deploy.deploy(task)) + mock_image_info.assert_called_once_with( + task.node, task.context) + mock_cache.assert_called_once_with( + task.context, task.node, image_info) + self.assertFalse(mock_warning.called) + i_info['configdrive'] = 'meow' + self.node.instance_info = i_info + self.node.save() + mock_warning.reset_mock() + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertEqual(states.DEPLOYDONE, + task.driver.deploy.deploy(task)) + self.assertTrue(mock_warning.called) + + @mock.patch.object(pxe.PXEBoot, 'prepare_instance', autospec=True) + def test_prepare(self, mock_prepare_instance): + node = self.node + node.provision_state = states.DEPLOYING + node.instance_info = {} + node.save() + with task_manager.acquire(self.context, node.uuid) as task: + task.driver.deploy.prepare(task) + self.assertFalse(mock_prepare_instance.called) + self.assertEqual({'boot_option': 'ramdisk'}, + task.node.instance_info['capabilities']) + + node.provision_state = states.ACTIVE + node.save() + with task_manager.acquire(self.context, node.uuid) as task: + task.driver.deploy.prepare(task) + mock_prepare_instance.assert_called_once_with(mock.ANY, task) + mock_prepare_instance.reset_mock() + + node.provision_state = states.UNRESCUING + node.save() + with task_manager.acquire(self.context, node.uuid) as task: + task.driver.deploy.prepare(task) + mock_prepare_instance.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(pxe.LOG, 'warning', autospec=True) + @mock.patch.object(pxe.PXEBoot, 'prepare_instance', autospec=True) + def test_prepare_fixes_and_logs_boot_option_warning( + self, mock_prepare_instance, mock_warning): + node = self.node + node.properties['capabilities'] = 'boot_option:ramdisk' + node.provision_state = states.DEPLOYING + node.instance_info = {} + node.save() + with task_manager.acquire(self.context, node.uuid) as task: + task.driver.deploy.prepare(task) + self.assertFalse(mock_prepare_instance.called) + self.assertEqual({'boot_option': 'ramdisk'}, + task.node.instance_info['capabilities']) + self.assertTrue(mock_warning.called) + + @mock.patch.object(deploy_utils, 'validate_image_properties', + autospec=True) + def test_validate(self, mock_validate_img): + node = self.node + node.properties['capabilities'] = 'boot_option:netboot' + node.save() + with task_manager.acquire(self.context, node.uuid) as task: + task.driver.deploy.validate(task) + self.assertTrue(mock_validate_img.called) + + @mock.patch.object(deploy_utils, 'validate_image_properties', + autospec=True) + def test_validate_interface_mismatch(self, mock_validate_image): + node = self.node + node.boot_interface = 'fake' + node.save() + self.config(enabled_boot_interfaces=['fake'], + default_boot_interface='fake') + with task_manager.acquire(self.context, node.uuid) as task: + self.assertRaisesRegexp(exception.InvalidParameterValue, + 'requires the pxe boot interface', + task.driver.deploy.validate, task) + self.assertFalse(mock_validate_image.called) + + @mock.patch.object(pxe.PXEBoot, 'validate', autospec=True) + def test_validate_calls_boot_validate(self, mock_validate): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.deploy.validate(task) + mock_validate.assert_called_once_with(mock.ANY, task) + + class PXEValidateRescueTestCase(db_base.DbTestCase): def setUp(self): diff --git a/ironic/tests/unit/drivers/pxe_config.template b/ironic/tests/unit/drivers/pxe_config.template index cd571485a7..49b738e681 100644 --- a/ironic/tests/unit/drivers/pxe_config.template +++ b/ironic/tests/unit/drivers/pxe_config.template @@ -18,3 +18,7 @@ append mbr:{{ DISK_IDENTIFIER }} label trusted_boot kernel mboot append tboot.gz --- /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/kernel root={{ ROOT }} ro text test_param intel_iommu=on --- /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/ramdisk + +label boot_ramdisk +kernel /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/kernel +append initrd=/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/ramdisk root=/dev/ram0 text test_param ramdisk_param diff --git a/ironic/tests/unit/drivers/pxe_grub_config.template b/ironic/tests/unit/drivers/pxe_grub_config.template index b9fe1f0835..d4996c2022 100644 --- a/ironic/tests/unit/drivers/pxe_grub_config.template +++ b/ironic/tests/unit/drivers/pxe_grub_config.template @@ -12,6 +12,11 @@ menuentry "boot_partition" { initrdefi /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/ramdisk } +menuentry "boot_ramdisk" { + linuxefi /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/deploy_kernel root=/dev/ram0 text test_param ramdisk_param + initrdefi /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/deploy_ramdisk +} + menuentry "boot_whole_disk" { linuxefi chain.c32 mbr:(( DISK_IDENTIFIER )) } diff --git a/releasenotes/notes/adds-ramdisk-deploy-interface-39fc61bc77b57beb.yaml b/releasenotes/notes/adds-ramdisk-deploy-interface-39fc61bc77b57beb.yaml new file mode 100644 index 0000000000..92789e6659 --- /dev/null +++ b/releasenotes/notes/adds-ramdisk-deploy-interface-39fc61bc77b57beb.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Adds a ``ramdisk`` deploy interface for deployments that wish to network + boot to a ramdisk, as opposed to perform a complete + traditional deployment to a physical media. This may be useful in + scientific use cases or where ephemeral baremetal machines are desired. + + The ``ramdisk`` deploy interface is intended for advanced users and has + some particular operational caveats that the users should be aware of + prior to use, such as network access list requirements and configuration + drive architectural restrictions and the inability to leverage + configuration drives. diff --git a/setup.cfg b/setup.cfg index ae07551a06..0a1df11ea5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -80,6 +80,7 @@ ironic.hardware.interfaces.deploy = iscsi = ironic.drivers.modules.iscsi_deploy:ISCSIDeploy oneview-direct = ironic.drivers.modules.oneview.deploy:OneViewAgentDeploy oneview-iscsi = ironic.drivers.modules.oneview.deploy:OneViewIscsiDeploy + ramdisk = ironic.drivers.modules.pxe:PXERamdiskDeploy ironic.hardware.interfaces.inspect = fake = ironic.drivers.modules.fake:FakeInspect