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
This commit is contained in:
Julia Kreger 2018-05-16 12:11:21 -07:00
parent bf76dd4fdc
commit 731af40129
17 changed files with 365 additions and 17 deletions

View File

@ -47,7 +47,7 @@ class GenericHardware(hardware_type.AbstractHardwareType):
def supported_deploy_interfaces(self): def supported_deploy_interfaces(self):
"""List of supported deploy interfaces.""" """List of supported deploy interfaces."""
return [iscsi_deploy.ISCSIDeploy, agent.AgentDeploy, return [iscsi_deploy.ISCSIDeploy, agent.AgentDeploy,
ansible_deploy.AnsibleDeploy] ansible_deploy.AnsibleDeploy, pxe.PXERamdiskDeploy]
@property @property
def supported_inspect_interfaces(self): def supported_inspect_interfaces(self):

View File

@ -58,7 +58,7 @@ LOG = logging.getLogger(__name__)
METRICS = metrics_utils.get_metrics_logger(__name__) METRICS = metrics_utils.get_metrics_logger(__name__)
SUPPORTED_CAPABILITIES = { SUPPORTED_CAPABILITIES = {
'boot_option': ('local', 'netboot'), 'boot_option': ('local', 'netboot', 'ramdisk'),
'boot_mode': ('bios', 'uefi'), 'boot_mode': ('bios', 'uefi'),
'secure_boot': ('true', 'false'), 'secure_boot': ('true', 'false'),
'trusted_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, 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: if is_whole_disk_image:
boot_disk_type = 'boot_whole_disk' boot_disk_type = 'boot_whole_disk'
elif trusted_boot: elif trusted_boot:
boot_disk_type = 'trusted_boot' boot_disk_type = 'trusted_boot'
elif iscsi_boot: elif iscsi_boot:
boot_disk_type = 'boot_iscsi' boot_disk_type = 'boot_iscsi'
elif ramdisk_boot:
boot_disk_type = 'boot_ramdisk'
else: else:
boot_disk_type = 'boot_partition' 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, def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode,
is_whole_disk_image, trusted_boot=False, 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. """Switch a pxe config from deployment mode to service mode.
:param path: path to the pxe config file in tftpboot. :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 is_whole_disk_image and trusted_boot are mutually exclusive. You can
have one or neither, but not both. have one or neither, but not both.
:param iscsi_boot: if boot is from an iSCSI volume or not. :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: if not ramdisk_boot:
_replace_root_uuid(path, root_uuid_or_disk_id) if not is_whole_disk_image:
else: _replace_root_uuid(path, root_uuid_or_disk_id)
_replace_disk_identifier(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, _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): def get_dev(address, port, iqn, lun):
@ -365,7 +370,8 @@ def deploy_partition_image(
partition table has not changed). partition table has not changed).
:param configdrive: Optional. Base64 encoded Gzipped configdrive content :param configdrive: Optional. Base64 encoded Gzipped configdrive content
or configdrive HTTP URL. 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 boot_mode: Can be "bios" or "uefi". "bios" by default.
:param disk_label: The disk label to be used when creating the :param disk_label: The disk label to be used when creating the
partition table. Valid values are: "msdos", "gpt" or None; If None partition table. Valid values are: "msdos", "gpt" or None; If None

View File

@ -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 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 initrd {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.ari_path }} || goto boot_partition
boot 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 %} {%- if pxe_options.boot_from_volume %}
:boot_iscsi :boot_iscsi

View File

@ -35,6 +35,7 @@ from ironic.common import states
from ironic.conductor import utils as manager_utils from ironic.conductor import utils as manager_utils
from ironic.conf import CONF from ironic.conf import CONF
from ironic.drivers import base from ironic.drivers import base
from ironic.drivers.modules import agent
from ironic.drivers.modules import boot_mode_utils from ironic.drivers.modules import boot_mode_utils
from ironic.drivers.modules import deploy_utils from ironic.drivers.modules import deploy_utils
from ironic.drivers.modules import image_cache 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('aki_path', 'no_kernel')
pxe_opts.setdefault('ari_path', 'no_ramdisk') 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 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, 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 node = task.node
pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid) 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 # 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, pxe_config_path, root_uuid_or_disk_id,
boot_mode_utils.get_boot_mode_for_deploy(node), boot_mode_utils.get_boot_mode_for_deploy(node),
iwdi, deploy_utils.is_trusted_boot_requested(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): def _get_volume_pxe_options(task):
@ -417,7 +431,7 @@ def _clean_up_pxe_env(task, images_info):
class PXEBoot(base.BootInterface): class PXEBoot(base.BootInterface):
capabilities = ['iscsi_volume_boot'] capabilities = ['iscsi_volume_boot', 'ramdisk_boot']
def __init__(self): def __init__(self):
if CONF.pxe.ipxe_enabled: if CONF.pxe.ipxe_enabled:
@ -597,7 +611,6 @@ class PXEBoot(base.BootInterface):
node = task.node node = task.node
boot_option = deploy_utils.get_boot_option(node) boot_option = deploy_utils.get_boot_option(node)
boot_device = None boot_device = None
if deploy_utils.is_iscsi_boot(task): if deploy_utils.is_iscsi_boot(task):
dhcp_opts = pxe_utils.dhcp_options_for_instance(task) dhcp_opts = pxe_utils.dhcp_options_for_instance(task)
provider = dhcp_factory.DHCPFactory() provider = dhcp_factory.DHCPFactory()
@ -618,6 +631,22 @@ class PXEBoot(base.BootInterface):
iscsi_boot=True) iscsi_boot=True)
boot_device = boot_devices.PXE 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": elif boot_option != "local":
if task.driver.storage.should_write_image(task): if task.driver.storage.should_write_image(task):
# Make sure that the instance kernel/ramdisk is cached. # Make sure that the instance kernel/ramdisk is cached.
@ -702,3 +731,79 @@ class PXEBoot(base.BootInterface):
parameters parameters
""" """
_parse_driver_info(task.node, mode='rescue') _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)

View File

@ -18,3 +18,7 @@ append mbr:{{ DISK_IDENTIFIER }}
label trusted_boot label trusted_boot
kernel mboot 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}} 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) }}

View File

@ -12,6 +12,11 @@ menuentry "boot_partition" {
initrdefi {{ pxe_options.ari_path }} 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" { menuentry "boot_whole_disk" {
linuxefi chain.c32 mbr:{{ DISK_IDENTIFIER }} linuxefi chain.c32 mbr:{{ DISK_IDENTIFIER }}
} }

View File

@ -49,6 +49,7 @@ class TestPXEUtils(db_base.DbTestCase):
u'f33c123/deploy_ramdisk', u'f33c123/deploy_ramdisk',
'ipa-api-url': 'http://192.168.122.184:6385', 'ipa-api-url': 'http://192.168.122.184:6385',
'ipxe_timeout': 0, 'ipxe_timeout': 0,
'ramdisk_opts': 'ramdisk_param',
} }
self.ipxe_options = self.pxe_options.copy() self.ipxe_options = self.pxe_options.copy()

View File

@ -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 initrd http://1.2.3.4:1234/ramdisk || goto boot_partition
boot 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 :boot_whole_disk
sanboot --no-describe sanboot --no-describe

View File

@ -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 initrd http://1.2.3.4:1234/ramdisk || goto boot_partition
boot 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 :boot_iscsi
imgfree imgfree
set username fake_username set username fake_username

View File

@ -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 initrd http://1.2.3.4:1234/ramdisk || goto boot_partition
boot 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 :boot_iscsi
imgfree imgfree
set username fake_username set username fake_username

View File

@ -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 initrd --timeout 120 http://1.2.3.4:1234/ramdisk || goto boot_partition
boot 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 :boot_whole_disk
sanboot --no-describe sanboot --no-describe

View File

@ -1420,7 +1420,7 @@ class ParseInstanceInfoCapabilitiesTestCase(tests_base.TestCase):
utils.validate_capabilities, self.node) utils.validate_capabilities, self.node)
def test_all_supported_capabilities(self): def test_all_supported_capabilities(self):
self.assertEqual(('local', 'netboot'), self.assertEqual(('local', 'netboot', 'ramdisk'),
utils.SUPPORTED_CAPABILITIES['boot_option']) utils.SUPPORTED_CAPABILITIES['boot_option'])
self.assertEqual(('bios', 'uefi'), self.assertEqual(('bios', 'uefi'),
utils.SUPPORTED_CAPABILITIES['boot_mode']) utils.SUPPORTED_CAPABILITIES['boot_mode'])

View File

@ -1254,7 +1254,7 @@ class PXEBootTestCase(db_base.DbTestCase):
provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts)
switch_pxe_config_mock.assert_called_once_with( switch_pxe_config_mock.assert_called_once_with(
pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50", 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, set_boot_device_mock.assert_called_once_with(task,
boot_devices.PXE, boot_devices.PXE,
persistent=True) persistent=True)
@ -1297,7 +1297,7 @@ class PXEBootTestCase(db_base.DbTestCase):
task, mock.ANY, CONF.pxe.pxe_config_template) task, mock.ANY, CONF.pxe.pxe_config_template)
switch_pxe_config_mock.assert_called_once_with( switch_pxe_config_mock.assert_called_once_with(
pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50", pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50",
'bios', False, False, False) 'bios', False, False, False, False)
self.assertFalse(set_boot_device_mock.called) self.assertFalse(set_boot_device_mock.called)
@mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True)
@ -1467,6 +1467,180 @@ class PXEBootTestCase(db_base.DbTestCase):
task.node, task.context) 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): class PXEValidateRescueTestCase(db_base.DbTestCase):
def setUp(self): def setUp(self):

View File

@ -18,3 +18,7 @@ append mbr:{{ DISK_IDENTIFIER }}
label trusted_boot label trusted_boot
kernel mboot 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 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

View File

@ -12,6 +12,11 @@ menuentry "boot_partition" {
initrdefi /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/ramdisk 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" { menuentry "boot_whole_disk" {
linuxefi chain.c32 mbr:(( DISK_IDENTIFIER )) linuxefi chain.c32 mbr:(( DISK_IDENTIFIER ))
} }

View File

@ -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.

View File

@ -80,6 +80,7 @@ ironic.hardware.interfaces.deploy =
iscsi = ironic.drivers.modules.iscsi_deploy:ISCSIDeploy iscsi = ironic.drivers.modules.iscsi_deploy:ISCSIDeploy
oneview-direct = ironic.drivers.modules.oneview.deploy:OneViewAgentDeploy oneview-direct = ironic.drivers.modules.oneview.deploy:OneViewAgentDeploy
oneview-iscsi = ironic.drivers.modules.oneview.deploy:OneViewIscsiDeploy oneview-iscsi = ironic.drivers.modules.oneview.deploy:OneViewIscsiDeploy
ramdisk = ironic.drivers.modules.pxe:PXERamdiskDeploy
ironic.hardware.interfaces.inspect = ironic.hardware.interfaces.inspect =
fake = ironic.drivers.modules.fake:FakeInspect fake = ironic.drivers.modules.fake:FakeInspect