Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @stackhpc/openstack
12 changes: 12 additions & 0 deletions .github/workflows/tag-and-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: Tag & Release
'on':
push:
branches:
- stackhpc/2025.1
permissions:
actions: read
contents: write
jobs:
tag-and-release:
uses: stackhpc/.github/.github/workflows/tag-and-release.yml@main
7 changes: 7 additions & 0 deletions .github/workflows/tox.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
name: Tox Continuous Integration
'on':
pull_request:
jobs:
tox:
uses: stackhpc/.github/.github/workflows/tox.yml@main
80 changes: 80 additions & 0 deletions doc/source/admin/interfaces/deploy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,66 @@ To disable the conductor-side conversion completely, set
[DEFAULT]
force_raw_images = False

Autodetect support
------------------

The ``direct`` deploy interface supports being auto-detected by the
``autodetect`` deploy interface (see :ref:`autodetect-deploy` for details).
The ``direct`` interface will always claim to support switching to it for
deployment regardless of the supplied node information. This makes it
appropriate to use as the last fallback value in the
``autodetect_deploy_interfaces`` configuration option.

.. _autodetect-deploy:

Autodetect deploy
=================

The ``autodetect`` deploy interface automatically selects the most appropriate
concrete deploy interface based on image metadata and node configuration. This
simplifies node configuration by eliminating the need to manually specify
different deploy interfaces for different image types.

When a deployment begins, the autodetect interface inspects the image and node
configuration, then switches to the most appropriate deploy interface from the
configured list. The detection logic checks interfaces in priority order using
each interface's ``supports_deploy()`` method. The first interface that
returns ``True`` is selected. If no interfaces are detected as supported, the
last interface in the configured list is used as a fallback.

You can specify this deploy interface when creating or updating a node::

baremetal node create --driver ipmi --deploy-interface autodetect
baremetal node set <NODE> --deploy-interface autodetect

Configuration
-------------

The autodetect interface uses the ``autodetect_deploy_interfaces``
configuration option to control which interfaces can be auto-selected and
their priority order. The default configuration is:

.. code-block:: ini

[DEFAULT]
autodetect_deploy_interfaces = ramdisk,bootc,direct

This example configuration checks :doc:`Ramdisk deploy </admin/ramdisk-boot>` first
(for nodes with ``kernel``/``ramdisk`` or ``boot_iso`` in ``instance_info``),
then :ref:`Bootc deploy <bootc-deploy>` (for OCI container images), and falls
back to :ref:`Direct deploy <direct-deploy>` for standard disk images.

.. note::
The order of interfaces in the configuration list matters. Interfaces are
checked in the order listed, and the first supported interface is selected.
The last interface in the list serves as the fallback if no other interface
is detected as supported.

.. note::
After deployment teardown, the node's deploy interface is automatically
restored to ``autodetect`` from whatever concrete interface was used during
deployment.

.. _ansible-deploy:

Ansible deploy
Expand Down Expand Up @@ -178,6 +238,16 @@ The ramdisk interface is intended to provide a mechanism to "deploy" an
instance where the item to be deployed is in reality a ramdisk. It is
documented separately, see :doc:`/admin/ramdisk-boot`.

Autodetect support
------------------

The ``ramdisk`` deploy interface supports being auto-detected by the
``autodetect`` deploy interface (see :ref:`autodetect-deploy` for details).
It checks whether the node's ``instance_info`` contains ``kernel`` and
``ramdisk`` (without an ``image_source``), or whether ``boot_iso`` is set.
When either condition is met, autodetect will select the ``ramdisk`` deploy
interface.

.. _custom-agent-deploy:

Custom agent deploy
Expand Down Expand Up @@ -254,6 +324,16 @@ to enable download from the remote image registry, which is part of the
support for retrieval of artifacts from OCI Container registires.
This parameter is ``image_pull_secret``.

Autodetect support
------------------

The ``bootc`` deploy interface supports being auto-detected by the
``autodetect`` deploy interface (see :ref:`autodetect-deploy` for details). It
checks whether the image source URL is an OCI container image (``oci://``
URL), then introspects the image registry to confirm the artifact is a
container image (OCI artifacts are handled by the ``direct`` deploy
interface).

Caveats
-------

Expand Down
99 changes: 99 additions & 0 deletions doc/source/admin/ramdisk-boot.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ or update an existing node:
You can also use it with :ref:`redfish virtual media
<redfish-virtual-media-ramdisk>` instead of iPXE.

Alternatively, if the node uses the ``autodetect`` deploy interface, the
ramdisk interface is selected automatically when ``kernel`` and ``ramdisk``
are set in ``instance_info`` (without an ``image_source``), or when
``boot_iso`` is set. To enable this, add ``ramdisk`` to the
``autodetect_deploy_interfaces`` configuration option. See
:ref:`autodetect-deploy` for details.

Creating a ramdisk
------------------

Expand Down Expand Up @@ -125,6 +132,98 @@ ISO images are also cached across deployments, similarly to how it is done for
normal instance images. The URL together with the last modified response header
are used to determine if an image needs updating.

Using with Nova (Glance images)
-------------------------------

When deploying ramdisk nodes through Nova, the user creates a Glance image with
``kernel_id`` and ``ramdisk_id`` properties — the same AMI-style pattern used
for partition images. Nova sets ``image_source`` in the node's ``instance_info``
and Ironic resolves the kernel and ramdisk from Glance automatically.

#. Upload the kernel to Glance:

.. code-block:: shell

openstack image create my-ramdisk-kernel --public \
--disk-format raw --container-format bare \
--file my-ramdisk.kernel

Store the image UUID as ``MY_KERNEL_UUID``.

#. Upload the ramdisk (initramfs) to Glance:

.. code-block:: shell

openstack image create my-ramdisk-initrd --public \
--disk-format raw --container-format bare \
--file my-ramdisk.initramfs

Store the image UUID as ``MY_RAMDISK_UUID``.

#. Create the Glance image with ``kernel_id`` and ``ramdisk_id`` properties.
Since the ramdisk deploy interface does not write to disk, the disk image
file is not used — an empty placeholder file is sufficient.

Include ``ironic_ramdisk_deploy=True`` so that the deploy interface
autodetect mechanism can distinguish this image from a partition image
(which also carries ``kernel_id`` / ``ramdisk_id``):

.. code-block:: shell

touch /tmp/placeholder
openstack image create my-ramdisk-image --public \
--disk-format raw --container-format bare \
--property kernel_id=$MY_KERNEL_UUID \
--property ramdisk_id=$MY_RAMDISK_UUID \
--property ironic_ramdisk_deploy=True \
--file /tmp/placeholder

#. Boot a Nova instance using this image:

.. code-block:: shell

openstack server create --image my-ramdisk-image \
--flavor my-baremetal-flavor my-ramdisk-instance

Alternative: Using a boot ISO in Glance
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Instead of separate kernel and ramdisk images, you can reference a single boot
ISO stored in Glance via the ``boot_iso_id`` property:

#. Upload the boot ISO to Glance:

.. code-block:: shell

openstack image create my-ramdisk-boot-iso --public \
--disk-format iso --container-format bare \
--file my-ramdisk.iso

Store the image UUID as ``MY_BOOT_ISO_UUID``.

#. Create the Glance image with the ``boot_iso_id`` property:

.. code-block:: shell

touch /tmp/placeholder
openstack image create my-ramdisk-image --public \
--disk-format raw --container-format bare \
--property boot_iso_id=$MY_BOOT_ISO_UUID \
--file /tmp/placeholder

#. Boot a Nova instance as above:

.. code-block:: shell

openstack server create --image my-ramdisk-image \
--flavor my-baremetal-flavor my-ramdisk-instance

.. note::
The disk image file attached to the Glance image is **not** used by the
ramdisk deploy interface. Only the kernel and ramdisk referenced by
``kernel_id`` / ``ramdisk_id``, or the ISO referenced by ``boot_iso_id``,
are booted.

Limitations
-----------

Expand Down
6 changes: 6 additions & 0 deletions doc/source/install/configure-glance-images.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ the Image service for each one as it is generated.
kernel_id=$MY_VMLINUZ_UUID --property \
ramdisk_id=$MY_INITRD_UUID --file my-image.qcow2

- For images used with the *ramdisk deploy interface* (e.g. when deploying
ramdisk nodes through Nova), you can use either ``kernel_id`` /
``ramdisk_id`` properties (with ``ironic_ramdisk_deploy=True`` to
distinguish from partition images) or a single ``boot_iso_id`` property
pointing to an ISO in Glance. See :doc:`/admin/ramdisk-boot` for details.

Deploy ramdisk images
~~~~~~~~~~~~~~~~~~~~~

Expand Down
3 changes: 3 additions & 0 deletions ironic/conductor/cleaning.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ def do_node_clean(task, clean_steps=None, disable_ramdisk=False):
node.clean_step = None
node.save()

# Switch back to original interface, if necessary
task.driver.deploy.restore_interface(task)

task.process_event('done')
how = ('API' if node.automated_clean is False else 'configuration')
LOG.info('Automated cleaning is disabled via %(how)s, node %(node)s '
Expand Down
5 changes: 5 additions & 0 deletions ironic/conductor/deployments.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ def start_deploy(task, manager, configdrive=None, event='deploy',
instance_info = node.instance_info
instance_info.pop('kernel', None)
instance_info.pop('ramdisk', None)
instance_info.pop('boot_iso', None)
instance_info.pop('original_image_source', None)
node.instance_info = instance_info
else:
# NOTE(JayF): Don't apply lessee when rebuilding
Expand All @@ -173,6 +175,9 @@ def start_deploy(task, manager, configdrive=None, event='deploy',
node.save()

try:
# Give the deploy interface the opportunity to switch interfaces
task.driver.deploy.switch_interface(task)

task.driver.power.validate(task)
task.driver.deploy.validate(task)
utils.validate_instance_info_traits(task.node)
Expand Down
3 changes: 3 additions & 0 deletions ironic/conductor/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,9 @@ def cleaning_error_handler(task, logmsg, errmsg=None, traceback=False,

node.save()

# Switch back to original interface, if necessary
task.driver.deploy.restore_interface(task)

if set_fail_state and node.provision_state != states.CLEANFAIL:
target_state = states.MANAGEABLE if manual_clean else None
task.process_event('fail', target_state=target_state)
Expand Down
15 changes: 14 additions & 1 deletion ironic/conf/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,23 @@
cfg.StrOpt('default_console_interface',
help=_DEFAULT_IFACE_HELP.format('console')),
cfg.ListOpt('enabled_deploy_interfaces',
default=['direct', 'ramdisk'],
default=['autodetect', 'direct', 'ramdisk'],
help=_ENABLED_IFACE_HELP.format('deploy')),
cfg.StrOpt('default_deploy_interface',
help=_DEFAULT_IFACE_HELP.format('deploy')),
cfg.ListOpt('autodetect_deploy_interfaces',
default=['ramdisk', 'direct'],
help=_('List of deploy interfaces that the '
'autodetect deploy interface is allowed to '
'switch to. The order of interfaces in the list '
'indicates the order support will be checked for,'
'with the first interface having the highest '
'priority. The last interface in the list will be '
'chosen if no interfaces are detected as supported. '
'"ramdisk" and "bootc" have detection support '
'and "direct" is an appropriate fallback for '
'most cases. All interfaces listed here must '
'also be in "enabled_deploy_interfaces".')),
cfg.ListOpt('enabled_firmware_interfaces',
default=['no-firmware'],
help=_ENABLED_IFACE_HELP.format('firmware')),
Expand Down
34 changes: 34 additions & 0 deletions ironic/drivers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,40 @@ def prepare_service(self, task):
"""
pass

def switch_interface(self, task):
"""Optionally switch the interface to use for deployment.

This method is called at the beginning of deployment before validation
to accommodate interfaces which auto-detect another interface to use
for deployment.

:param task: A TaskManager instance containing the node to act on.
:raises: InvalidParameterValue if the interface is not enabled.
"""
pass

def restore_interface(self, task):
"""Restore the original deploy interface for the node.

This restores the deploy interface to the original interface
in case it was changed by switch_interface.

:param task: a TaskManager instance containing the node to act on.
"""
pass

def supports_deploy(self, task):
"""Check if deploy is supported for the given node by this interface.

By default interfaces will claim to support deploy. Interfaces
which can infer actual support using (for example) node instance_info
should override this method.

:param task: A TaskManager instance containing the node to act on.
:returns: boolean, whether deploy is supported.
"""
return True


class BootInterface(BaseInterface):
"""Interface for boot-related actions."""
Expand Down
8 changes: 5 additions & 3 deletions ironic/drivers/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from ironic.drivers.modules import agent
from ironic.drivers.modules import agent_power
from ironic.drivers.modules.ansible import deploy as ansible_deploy
from ironic.drivers.modules import autodetect
from ironic.drivers.modules import fake
from ironic.drivers.modules import inspector
from ironic.drivers.modules import ipxe
Expand Down Expand Up @@ -49,9 +50,10 @@ def supported_boot_interfaces(self):
@property
def supported_deploy_interfaces(self):
"""List of supported deploy interfaces."""
return [agent.AgentDeploy, ansible_deploy.AnsibleDeploy,
ramdisk.RamdiskDeploy, pxe.PXEAnacondaDeploy,
agent.BootcAgentDeploy, agent.CustomAgentDeploy]
return [autodetect.AutodetectDeploy, agent.AgentDeploy,
ansible_deploy.AnsibleDeploy, ramdisk.RamdiskDeploy,
pxe.PXEAnacondaDeploy, agent.BootcAgentDeploy,
agent.CustomAgentDeploy]

@property
def supported_inspect_interfaces(self):
Expand Down
Loading