From 731af401290a1eaad19bbe89ae227fe4c2b5816b Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Wed, 16 May 2018 12:11:21 -0700 Subject: [PATCH] Adds ramdisk deploy driver Adds a pxe deploy driver to support the concept of a deployment just consisting of a ramdisk. Ideally, as long as a kernel and ramdisk are defined, either by the operator or via a glance image, the PXE/iPXE template should point the booted kernel to using ramdisk as the root. In theory, this would allow deployment via nova, or directly using the parameters posted to the node's instance_info. There may be additional features realistically needed for this to be beyond minimally useful, but that would also depend on the contents of the ramdisk that is deployed by an API user. Change-Id: Id7067527cba27ed49753736f33ccb35e9b35bcba Story: 1753842 Task: 10666 --- ironic/drivers/generic.py | 2 +- ironic/drivers/modules/deploy_utils.py | 24 ++- ironic/drivers/modules/ipxe_config.template | 6 + ironic/drivers/modules/pxe.py | 113 ++++++++++- ironic/drivers/modules/pxe_config.template | 4 + .../drivers/modules/pxe_grub_config.template | 5 + ironic/tests/unit/common/test_pxe_utils.py | 1 + .../tests/unit/drivers/ipxe_config.template | 6 + ...fig_boot_from_volume_extra_volume.template | 6 + ...boot_from_volume_no_extra_volumes.template | 6 + .../unit/drivers/ipxe_config_timeout.template | 6 + .../unit/drivers/modules/test_deploy_utils.py | 2 +- ironic/tests/unit/drivers/modules/test_pxe.py | 178 +++++++++++++++++- ironic/tests/unit/drivers/pxe_config.template | 4 + .../unit/drivers/pxe_grub_config.template | 5 + ...isk-deploy-interface-39fc61bc77b57beb.yaml | 13 ++ setup.cfg | 1 + 17 files changed, 365 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/adds-ramdisk-deploy-interface-39fc61bc77b57beb.yaml 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