Support disable_ramdisk during servicing

The node provision API rejects ``disable_ramdisk`` for servicing
requests, even though the servicing backend supports skipping the
ramdisk for steps marked as not requiring it and the client-side
interface exposes the option.

This change adds a new microversion to allow ``disable_ramdisk``
with ``target=service``.

Closes-Bug: #2137065
Change-Id: I7e41ec27ac2cfc0255732bd14cad624ea612bd65
Signed-off-by: Afonne-CID <afonnepaulc@gmail.com>
This commit is contained in:
Afonne-CID
2026-01-19 11:01:07 +01:00
parent 09761d9549
commit 23948dabf1
8 changed files with 100 additions and 22 deletions
+5 -3
View File
@@ -933,9 +933,11 @@ disable_power_off:
type: boolean
disable_ramdisk:
description: |
If set to ``true``, the ironic-python-agent ramdisk will not be booted for
cleaning. Only clean steps explicitly marked as not requiring ramdisk can
be executed in this mode. Only allowed for manual cleaning.
If set to ``true``, the ironic-python-agent ramdisk will not be booted.
Only steps explicitly marked as not requiring ramdisk can be executed in
this mode.
Allowed for manual cleaning (starting with API microversion 1.70) and for
manual servicing (starting with API microversion 1.108).
in: body
required: false
type: boolean
@@ -2,6 +2,11 @@
REST API Version History
========================
1.108 (Gazpacho)
------------------------
Add support for ``disable_ramdisk`` parameter to provisioning endpoint
``/v1/nodes/{node_ident}/states/provision`` for the 'service' verb.
1.107 (Gazpacho)
------------------------
+1 -1
View File
@@ -1266,7 +1266,7 @@ class NodeStatesController(rest.RestController):
rpc_node, target, runbook, clean_steps, service_steps
)
api_utils.check_allow_clean_disable_ramdisk(target, disable_ramdisk)
api_utils.check_allow_disable_ramdisk(target, disable_ramdisk)
if clean_steps and target != ir_states.VERBS['clean']:
msg = (_('"clean_steps" is only valid when setting target '
+22 -7
View File
@@ -2223,15 +2223,30 @@ def check_allow_boot_mode(node_capabilities, disallowed_boot_modes):
op=_('provisioning'))
def check_allow_clean_disable_ramdisk(target, disable_ramdisk):
def check_allow_disable_ramdisk(target, disable_ramdisk):
if disable_ramdisk is None:
return
elif api.request.version.minor < versions.MINOR_70_CLEAN_DISABLE_RAMDISK:
raise exception.NotAcceptable(
_("disable_ramdisk is not acceptable in this API version"))
elif target != "clean":
raise exception.BadRequest(
_("disable_ramdisk is supported only with manual cleaning"))
minor = api.request.version.minor
if target == states.VERBS['clean']:
if minor < versions.MINOR_70_CLEAN_DISABLE_RAMDISK:
raise exception.NotAcceptable(
_("disable_ramdisk is not acceptable in this API version"))
return
if target == states.VERBS['service']:
if minor < versions.MINOR_108_SERVICE_DISABLE_RAMDISK:
raise exception.NotAcceptable(
_("disable_ramdisk is not acceptable in this API version"))
return
raise exception.BadRequest(
_("disable_ramdisk is supported only with manual cleaning "
"or servicing"))
def check_allow_clean_disable_ramdisk(target, disable_ramdisk):
return check_allow_disable_ramdisk(target, disable_ramdisk)
def allow_shards_endpoint():
+3 -1
View File
@@ -145,6 +145,7 @@ BASE_VERSION = 1
# v1.105: Remove broken ovn vtep metadata support
# v1.106: Add shard filtering support for portgroups
# v1.107: Add X-OpenStack-Request-Id header.
# v1.108: Add disable_ramdisk support for servicing
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@@ -254,6 +255,7 @@ MINOR_104_NODE_INSTANCE_NAME = 104
MINOR_105_REMOVE_OVN_VTEP = 105
MINOR_106_PORTGROUP_SHARD = 106
MINOR_107_X_OPENSTACK_REQUEST_ID = 107
MINOR_108_SERVICE_DISABLE_RAMDISK = 108
# When adding another version, update:
# - MINOR_MAX_VERSION
@@ -263,7 +265,7 @@ MINOR_107_X_OPENSTACK_REQUEST_ID = 107
# - Add a comment describing the change above the list of consts
MINOR_MAX_VERSION = MINOR_107_X_OPENSTACK_REQUEST_ID
MINOR_MAX_VERSION = MINOR_108_SERVICE_DISABLE_RAMDISK
# String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
+1 -1
View File
@@ -944,7 +944,7 @@ RELEASE_MAPPING = {
# make it below. To release, we will preserve a version matching
# the release as a separate block of text, like above.
'master': {
'api': '1.107',
'api': '1.108',
'rpc': '1.62',
'networking_rpc': '1.0',
'objects': {
@@ -6686,36 +6686,83 @@ ORHMKeXMO8fcK0By7CiMKwHSXCoEQgfQhWwpMdSsO8LgHCjh87DQc= """
expect_errors=True)
self.assertEqual(http_client.NOT_ACCEPTABLE, ret.status_code)
@mock.patch.object(rpcapi.ConductorAPI, 'do_node_service', autospec=True)
@mock.patch.object(api_node, '_check_service_steps', autospec=True)
def test_service_disable_ramdisk(self, mock_check, mock_rpcapi):
self.node.provision_state = states.SERVICEHOLD
self.node.save()
service_steps = [{"step": "upgrade_firmware", "interface": "deploy"}]
ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
{'target': states.VERBS['service'],
'service_steps': service_steps,
'disable_ramdisk': True},
headers={api_base.Version.string: "1.108"})
self.assertEqual(http_client.ACCEPTED, ret.status_code)
self.assertEqual(b'', ret.body)
mock_check.assert_called_once_with(service_steps)
mock_rpcapi.assert_called_once_with(mock.ANY, mock.ANY,
self.node.uuid,
service_steps, True,
topic='test-topic')
@mock.patch.object(rpcapi.ConductorAPI, 'do_node_service', autospec=True)
@mock.patch.object(api_node, '_check_service_steps', autospec=True)
def test_service_disable_ramdisk_old_api(self, mock_check, mock_rpcapi):
self.node.provision_state = states.SERVICEHOLD
self.node.save()
service_steps = [{"step": "upgrade_firmware", "interface": "deploy"}]
ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
{'target': states.VERBS['service'],
'service_steps': service_steps,
'disable_ramdisk': True},
headers={api_base.Version.string: "1.106"},
expect_errors=True)
self.assertEqual(http_client.NOT_ACCEPTABLE, ret.status_code)
mock_rpcapi.assert_not_called()
@mock.patch.object(api_utils, 'check_runbook_policy_and_retrieve',
autospec=True)
@mock.patch.object(rpcapi.ConductorAPI, 'do_node_clean', autospec=True)
@mock.patch.object(api_node, '_check_clean_steps', autospec=True)
def test_clean_with_runbook_disable_ramdisk(self, mock_check,
mock_rpcapi, mock_policy):
@mock.patch.object(rpcapi.ConductorAPI, 'do_node_service', autospec=True)
@mock.patch.object(api_node, '_check_service_steps', autospec=True)
def test_service_with_runbook_disable_ramdisk(self, mock_check,
mock_rpcapi, mock_policy):
objects.TraitList.create(self.context, self.node.id, ['CUSTOM_1'])
self.node.refresh()
self.node.provision_state = states.MANAGEABLE
self.node.provision_state = states.SERVICEHOLD
self.node.save()
runbook = mock.Mock()
runbook.name = 'CUSTOM_1'
runbook.steps = [{"step": "erase_devices", "interface": "deploy",
runbook.steps = [{"step": "upgrade_firmware", "interface": "deploy",
"args": {}}]
runbook.disable_ramdisk = True
mock_policy.return_value = runbook
ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
{'target': states.VERBS['clean'],
{'target': states.VERBS['service'],
'runbook': runbook.name},
headers={api_base.Version.string: "1.106"})
headers={api_base.Version.string: "1.108"})
self.assertEqual(http_client.ACCEPTED, ret.status_code)
self.assertEqual(b'', ret.body)
mock_policy.assert_has_calls([mock.call('baremetal:runbook:use',
runbook.name)])
mock_check.assert_called_once_with(runbook.steps)
mock_rpcapi.assert_called_once_with(mock.ANY, mock.ANY,
self.node.uuid, runbook.steps,
True, topic='test-topic')
def test_disable_ramdisk_wrong_target(self):
self.node.provision_state = states.AVAILABLE
self.node.save()
ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
{'target': states.VERBS['active'],
'disable_ramdisk': True},
headers={api_base.Version.string: "1.108"},
expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
self.assertIn('disable_ramdisk is supported only with manual '
'cleaning or servicing', ret.json['error_message'])
def test_adopt_raises_error_before_1_17(self):
"""Test that a lower API client cannot use the adopt verb"""
ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
@@ -0,0 +1,7 @@
---
features:
- |
Adds support for using ``disable_ramdisk`` with manual servicing
(``target=service``) starting with Bare Metal API microversion ``1.108``.
This enables running service steps that are explicitly marked as not
requiring a ramdisk.