
This patch addresses some frequently failing tests with a few minor changes related around how the exceptions are tested and how the data is passed to the exceptions that are failing. I've been able to duplicate the failing tests in CI while streaming video on my desktop of all things. With these changes, I've been unable to reproduce the failures. This also fixes entries for DeployTemplate generation which were noticed as well, however the failures were not observed in CI. And a missing i18n tag was added into the pxe code, where it was previously missing. Change-Id: I719fa12340b51c55c0441df572ee7e3b33b1910c
388 lines
17 KiB
Python
388 lines
17 KiB
Python
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
"""
|
|
PXE Boot Interface
|
|
"""
|
|
|
|
from ironic_lib import metrics_utils
|
|
from oslo_log import log as logging
|
|
from oslo_utils import strutils
|
|
|
|
from ironic.common import boot_devices
|
|
from ironic.common import dhcp_factory
|
|
from ironic.common import exception
|
|
from ironic.common.glance_service import service_utils
|
|
from ironic.common.i18n import _
|
|
from ironic.common import pxe_utils
|
|
from ironic.common import states
|
|
from ironic.conductor import task_manager
|
|
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 pxe_base
|
|
from ironic.drivers import utils as driver_utils
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
METRICS = metrics_utils.get_metrics_logger(__name__)
|
|
|
|
COMMON_PROPERTIES = pxe_base.COMMON_PROPERTIES
|
|
|
|
# NOTE(TheJulia): This was previously a public method to the code being
|
|
# moved. This mapping should be removed in the T* cycle.
|
|
validate_boot_parameters_for_trusted_boot = pxe_utils.validate_boot_parameters_for_trusted_boot # noqa
|
|
TFTPImageCache = pxe_utils.TFTPImageCache
|
|
# NOTE(TheJulia): End section of mappings for migrated common pxe code.
|
|
|
|
|
|
class PXEBoot(pxe_base.PXEBaseMixin, base.BootInterface):
|
|
|
|
# TODO(TheJulia): iscsi_volume_boot should be removed from
|
|
# the list below once ipxe support is removed from the PXE
|
|
# interface.
|
|
capabilities = ['iscsi_volume_boot', 'ramdisk_boot', 'ipxe_boot',
|
|
'pxe_boot']
|
|
|
|
def __init__(self):
|
|
# TODO(TheJulia): Once the pxe/ipxe interfaces split is complete,
|
|
# this can be removed.
|
|
if CONF.pxe.ipxe_enabled:
|
|
pxe_utils.create_ipxe_boot_script()
|
|
|
|
@METRICS.timer('PXEBoot.validate')
|
|
def validate(self, task):
|
|
"""Validate the PXE-specific info for booting deploy/instance images.
|
|
|
|
This method validates the PXE-specific info for booting the
|
|
ramdisk and instance on the node. If invalid, raises an
|
|
exception; otherwise returns None.
|
|
|
|
:param task: a task from TaskManager.
|
|
:returns: None
|
|
:raises: InvalidParameterValue, if some parameters are invalid.
|
|
:raises: MissingParameterValue, if some required parameters are
|
|
missing.
|
|
"""
|
|
node = task.node
|
|
|
|
if not driver_utils.get_node_mac_addresses(task):
|
|
raise exception.MissingParameterValue(
|
|
_("Node %s does not have any port associated with it.")
|
|
% node.uuid)
|
|
|
|
# TODO(TheJulia): Once ipxe support is remove from the pxe
|
|
# interface, this can be removed.
|
|
if CONF.pxe.ipxe_enabled:
|
|
if (not CONF.deploy.http_url
|
|
or not CONF.deploy.http_root):
|
|
raise exception.MissingParameterValue(_(
|
|
"iPXE boot is enabled but no HTTP URL or HTTP "
|
|
"root was specified."))
|
|
|
|
# Check the trusted_boot capabilities value.
|
|
deploy_utils.validate_capabilities(node)
|
|
if deploy_utils.is_trusted_boot_requested(node):
|
|
# Check if 'boot_option' and boot mode is compatible with
|
|
# trusted boot.
|
|
validate_boot_parameters_for_trusted_boot(node)
|
|
|
|
pxe_utils.parse_driver_info(node)
|
|
# NOTE(TheJulia): If we're not writing an image, we can skip
|
|
# the remainder of this method.
|
|
if (not task.driver.storage.should_write_image(task)):
|
|
return
|
|
|
|
d_info = deploy_utils.get_image_instance_info(node)
|
|
if (node.driver_internal_info.get('is_whole_disk_image')
|
|
or deploy_utils.get_boot_option(node) == 'local'):
|
|
props = []
|
|
elif service_utils.is_glance_image(d_info['image_source']):
|
|
props = ['kernel_id', 'ramdisk_id']
|
|
else:
|
|
props = ['kernel', 'ramdisk']
|
|
deploy_utils.validate_image_properties(task.context, d_info, props)
|
|
|
|
@METRICS.timer('PXEBoot.prepare_ramdisk')
|
|
def prepare_ramdisk(self, task, ramdisk_params):
|
|
"""Prepares the boot of Ironic ramdisk using PXE.
|
|
|
|
This method prepares the boot of the deploy or rescue kernel/ramdisk
|
|
after reading relevant information from the node's driver_info and
|
|
instance_info.
|
|
|
|
:param task: a task from TaskManager.
|
|
:param ramdisk_params: the parameters to be passed to the ramdisk.
|
|
pxe driver passes these parameters as kernel command-line
|
|
arguments.
|
|
:returns: None
|
|
:raises: MissingParameterValue, if some information is missing in
|
|
node's driver_info or instance_info.
|
|
:raises: InvalidParameterValue, if some information provided is
|
|
invalid.
|
|
:raises: IronicException, if some power or set boot boot device
|
|
operation failed on the node.
|
|
"""
|
|
node = task.node
|
|
|
|
# Label indicating a deploy or rescue operation being carried out on
|
|
# the node, 'deploy' or 'rescue'. Unless the node is in a rescue like
|
|
# state, the mode is set to 'deploy', indicating deploy operation is
|
|
# being carried out.
|
|
mode = deploy_utils.rescue_or_deploy_mode(node)
|
|
ipxe_enabled = CONF.pxe.ipxe_enabled
|
|
if ipxe_enabled:
|
|
# NOTE(mjturek): At this point, the ipxe boot script should
|
|
# already exist as it is created at startup time. However, we
|
|
# call the boot script create method here to assert its
|
|
# existence and handle the unlikely case that it wasn't created
|
|
# or was deleted.
|
|
pxe_utils.create_ipxe_boot_script()
|
|
|
|
dhcp_opts = pxe_utils.dhcp_options_for_instance(
|
|
task, ipxe_enabled=ipxe_enabled)
|
|
provider = dhcp_factory.DHCPFactory()
|
|
provider.update_dhcp(task, dhcp_opts)
|
|
|
|
pxe_info = pxe_utils.get_image_info(node, mode=mode)
|
|
|
|
# NODE: Try to validate and fetch instance images only
|
|
# if we are in DEPLOYING state.
|
|
if node.provision_state == states.DEPLOYING:
|
|
pxe_info.update(pxe_utils.get_instance_image_info(task))
|
|
boot_mode_utils.sync_boot_mode(task)
|
|
|
|
pxe_options = pxe_utils.build_pxe_config_options(
|
|
task, pxe_info, ipxe_enabled=ipxe_enabled)
|
|
pxe_options.update(ramdisk_params)
|
|
|
|
pxe_config_template = deploy_utils.get_pxe_config_template(node)
|
|
|
|
pxe_utils.create_pxe_config(task, pxe_options,
|
|
pxe_config_template,
|
|
ipxe_enabled=CONF.pxe.ipxe_enabled)
|
|
|
|
persistent = False
|
|
value = node.driver_info.get('force_persistent_boot_device',
|
|
'Default')
|
|
if value in {'Always', 'Default', 'Never'}:
|
|
if value == 'Always':
|
|
persistent = True
|
|
else:
|
|
persistent = strutils.bool_from_string(value, False)
|
|
manager_utils.node_set_boot_device(task, boot_devices.PXE,
|
|
persistent=persistent)
|
|
|
|
if CONF.pxe.ipxe_enabled and CONF.pxe.ipxe_use_swift:
|
|
kernel_label = '%s_kernel' % mode
|
|
ramdisk_label = '%s_ramdisk' % mode
|
|
pxe_info.pop(kernel_label, None)
|
|
pxe_info.pop(ramdisk_label, None)
|
|
|
|
if pxe_info:
|
|
pxe_utils.cache_ramdisk_kernel(task, pxe_info,
|
|
ipxe_enabled=CONF.pxe.ipxe_enabled)
|
|
|
|
@METRICS.timer('PXEBoot.prepare_instance')
|
|
def prepare_instance(self, task):
|
|
"""Prepares the boot of instance.
|
|
|
|
This method prepares the boot of the instance after reading
|
|
relevant information from the node's instance_info. In case of netboot,
|
|
it updates the dhcp entries and switches the PXE config. In case of
|
|
localboot, it cleans up the PXE config.
|
|
|
|
:param task: a task from TaskManager.
|
|
:returns: None
|
|
"""
|
|
ipxe_enabled = CONF.pxe.ipxe_enabled
|
|
boot_mode_utils.sync_boot_mode(task)
|
|
|
|
node = task.node
|
|
boot_option = deploy_utils.get_boot_option(node)
|
|
boot_device = None
|
|
instance_image_info = {}
|
|
if boot_option == "ramdisk":
|
|
instance_image_info = pxe_utils.get_instance_image_info(task)
|
|
pxe_utils.cache_ramdisk_kernel(task, instance_image_info,
|
|
ipxe_enabled=CONF.pxe.ipxe_enabled)
|
|
|
|
if deploy_utils.is_iscsi_boot(task) or boot_option == "ramdisk":
|
|
pxe_utils.prepare_instance_pxe_config(
|
|
task, instance_image_info,
|
|
iscsi_boot=deploy_utils.is_iscsi_boot(task),
|
|
ramdisk_boot=(boot_option == "ramdisk"),
|
|
ipxe_enabled=CONF.pxe.ipxe_enabled)
|
|
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.
|
|
# This is for the takeover scenario for active nodes.
|
|
instance_image_info = pxe_utils.get_instance_image_info(task)
|
|
pxe_utils.cache_ramdisk_kernel(
|
|
task, instance_image_info,
|
|
ipxe_enabled=CONF.pxe.ipxe_enabled)
|
|
|
|
# If it's going to PXE boot we need to update the DHCP server
|
|
dhcp_opts = pxe_utils.dhcp_options_for_instance(
|
|
task, ipxe_enabled)
|
|
provider = dhcp_factory.DHCPFactory()
|
|
provider.update_dhcp(task, dhcp_opts)
|
|
|
|
iwdi = task.node.driver_internal_info.get('is_whole_disk_image')
|
|
try:
|
|
root_uuid_or_disk_id = task.node.driver_internal_info[
|
|
'root_uuid_or_disk_id'
|
|
]
|
|
except KeyError:
|
|
if not task.driver.storage.should_write_image(task):
|
|
pass
|
|
elif not iwdi:
|
|
LOG.warning("The UUID for the root partition can't be "
|
|
"found, unable to switch the pxe config from "
|
|
"deployment mode to service (boot) mode for "
|
|
"node %(node)s", {"node": task.node.uuid})
|
|
else:
|
|
LOG.warning("The disk id for the whole disk image can't "
|
|
"be found, unable to switch the pxe config "
|
|
"from deployment mode to service (boot) mode "
|
|
"for node %(node)s. Booting the instance "
|
|
"from disk.", {"node": task.node.uuid})
|
|
pxe_utils.clean_up_pxe_config(
|
|
task, ipxe_enabled=CONF.pxe.ipxe_enabled)
|
|
boot_device = boot_devices.DISK
|
|
else:
|
|
pxe_utils.build_service_pxe_config(task, instance_image_info,
|
|
root_uuid_or_disk_id,
|
|
ipxe_enabled=ipxe_enabled)
|
|
boot_device = boot_devices.PXE
|
|
else:
|
|
# 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, ipxe_enabled=CONF.pxe.ipxe_enabled)
|
|
boot_device = boot_devices.DISK
|
|
|
|
# NOTE(pas-ha) do not re-set boot device on ACTIVE nodes
|
|
# during takeover
|
|
if boot_device and task.node.provision_state != states.ACTIVE:
|
|
persistent = True
|
|
if node.driver_info.get('force_persistent_boot_device',
|
|
'Default') == 'Never':
|
|
persistent = False
|
|
manager_utils.node_set_boot_device(task, boot_device,
|
|
persistent=persistent)
|
|
|
|
@METRICS.timer('PXEBoot.clean_up_instance')
|
|
def clean_up_instance(self, task):
|
|
"""Cleans up the boot of instance.
|
|
|
|
This method cleans up the environment that was setup for booting
|
|
the instance. It unlinks the instance kernel/ramdisk in node's
|
|
directory in tftproot and removes the PXE config.
|
|
|
|
:param task: a task from TaskManager.
|
|
:returns: None
|
|
"""
|
|
node = task.node
|
|
|
|
try:
|
|
images_info = pxe_utils.get_instance_image_info(task)
|
|
except exception.MissingParameterValue as e:
|
|
LOG.warning('Could not get instance image info '
|
|
'to clean up images for node %(node)s: %(err)s',
|
|
{'node': node.uuid, 'err': e})
|
|
else:
|
|
pxe_utils.clean_up_pxe_env(task, images_info)
|
|
|
|
|
|
class PXERamdiskDeploy(agent.AgentDeploy):
|
|
|
|
def validate(self, task):
|
|
if 'ramdisk_boot' not in task.driver.boot.capabilities:
|
|
raise exception.InvalidParameterValue(
|
|
message=_('Invalid configuration: The boot interface '
|
|
'must have the `ramdisk_boot` capability. '
|
|
'You are using an incompatible boot interface.'))
|
|
task.driver.boot.validate(task)
|
|
|
|
# Validate node capabilities
|
|
deploy_utils.validate_capabilities(task.node)
|
|
|
|
@METRICS.timer('RamdiskDeploy.deploy')
|
|
@base.deploy_step(priority=100)
|
|
@task_manager.require_exclusive_lock
|
|
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.
|
|
power_state_to_restore = manager_utils.power_on_node_if_needed(task)
|
|
task.driver.network.configure_tenant_networks(task)
|
|
manager_utils.restore_power_state_if_needed(
|
|
task, power_state_to_restore)
|
|
|
|
# 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)
|
|
return None
|
|
|
|
@METRICS.timer('RamdiskDeploy.prepare')
|
|
@task_manager.require_exclusive_lock
|
|
def prepare(self, task):
|
|
node = task.node
|
|
# Log a warning if the boot_option is wrong... and
|
|
# otherwise reset it.
|
|
boot_option = deploy_utils.get_boot_option(node)
|
|
if boot_option != 'ramdisk':
|
|
LOG.warning('Incorrect "boot_option" set for node %(node)s '
|
|
'and will be overridden to "ramdisk" as to '
|
|
'match the deploy interface. Found: %(boot_opt)s.',
|
|
{'node': node.uuid,
|
|
'boot_opt': boot_option})
|
|
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)
|