Merge "Allow disabling specific boot modes during deployment/enrollment"

This commit is contained in:
Zuul 2024-07-22 11:43:58 +00:00 committed by Gerrit Code Review
commit 9aada0524c
9 changed files with 218 additions and 1 deletions
ironic
api/controllers/v1
common
conductor
conf
tests/unit
api/controllers/v1
conductor
releasenotes/notes

@ -2775,6 +2775,12 @@ class NodesController(rest.RestController):
if self.from_chassis:
raise exception.OperationNotPermitted()
node_capabilities = node.get('properties', {}).get('capabilities', '')
# ``check_allow_boot_mode`` expects ``node_capabilities`` to be a list
api_utils.check_allow_boot_mode(
[node_capabilities],
CONF.api.disallowed_enrollment_boot_modes)
context = api.request.context
owned_node = False
if CONF.api.project_admin_can_manage_own_nodes:
@ -2870,6 +2876,12 @@ class NodesController(rest.RestController):
if self.from_chassis:
raise exception.OperationNotPermitted()
node_capabilities = api_utils.get_patch_values(
patch, '/properties/capabilities')
api_utils.check_allow_boot_mode(
node_capabilities,
CONF.api.disallowed_enrollment_boot_modes)
api_utils.patch_validate_allowed_fields(patch, PATCH_ALLOWED_FIELDS)
reject_patch_in_newer_versions(patch)

@ -2006,6 +2006,22 @@ def check_allow_deploy_steps(target, deploy_steps):
msg, status_code=http_client.BAD_REQUEST)
def check_allow_boot_mode(node_capabilities, disallowed_boot_modes):
"""Check if boot mode is allowed"""
if (not node_capabilities) or (not disallowed_boot_modes):
return
disallowed_set = set(disallowed_boot_modes)
for capability in node_capabilities:
for item in capability.lower().split(','):
key, value = item.split(':')
if key.strip() == 'boot_mode' and value.strip() in disallowed_set:
raise exception.BootModeNotAllowed(mode=value.strip(),
op=_('provisioning'))
def check_allow_clean_disable_ramdisk(target, disable_ramdisk):
if disable_ramdisk is None:
return

@ -885,3 +885,7 @@ class UnsupportedHardwareFeature(Invalid):
_msg_fmt = _("Node %(node)s hardware does not support feature "
"%(feature)s, which is required based upon the "
"requested configuration.")
class BootModeNotAllowed(Invalid):
_msg_fmt = _("'%(mode)s' boot mode is not allowed for %(op)s operation.")

@ -43,7 +43,8 @@ def validate_node(task, event='deploy'):
:param task: a TaskManager instance.
:param event: event to process: deploy or rebuild.
:raises: NodeInMaintenance, NodeProtected, InvalidStateRequested
:raises: NodeInMaintenance, NodeProtected, InvalidStateRequested,
BootModeNotAllowed
"""
if task.node.maintenance:
raise exception.NodeInMaintenance(op=_('provisioning'),
@ -56,6 +57,12 @@ def validate_node(task, event='deploy'):
raise exception.InvalidStateRequested(
action=event, node=task.node.uuid, state=task.node.provision_state)
disallowed_boot_modes = CONF.conductor.disallowed_deployment_boot_modes
boot_mode = task.node.properties.get('boot_mode', '').lower()
if disallowed_boot_modes and boot_mode.strip() in disallowed_boot_modes:
raise exception.BootModeNotAllowed(mode=boot_mode,
op=_('provisioning'))
@METRICS.timer('start_deploy')
@task_manager.require_exclusive_lock

@ -17,6 +17,7 @@
from oslo_config import cfg
from oslo_config import types as cfg_types
from ironic.common import boot_modes
from ironic.common.i18n import _
@ -92,6 +93,16 @@ opts = [
mutable=True,
help=_('If a project scoped administrative user is permitted '
'to create/delete baremetal nodes in their project.')),
cfg.ListOpt('disallowed_enrollment_boot_modes',
item_type=cfg_types.String(
choices=[
(boot_modes.UEFI, _('UEFI boot mode')),
(boot_modes.LEGACY_BIOS, _('Legacy BIOS boot mode'))],
),
default=[],
mutable=True,
help=_("Specifies a list of boot modes that are not allowed "
"during enrollment. Eg: ['bios']")),
]
opt_group = cfg.OptGroup(name='api',

@ -18,8 +18,10 @@
from oslo_config import cfg
from oslo_config import types
from ironic.common import boot_modes
from ironic.common.i18n import _
opts = [
cfg.IntOpt('workers_pool_size',
default=300, min=3,
@ -417,6 +419,16 @@ opts = [
'seconds, or 30 minutes. If you need to wait longer '
'than the maximum value, we recommend exploring '
'hold steps.')),
cfg.ListOpt('disallowed_deployment_boot_modes',
item_type=types.String(
choices=[
(boot_modes.UEFI, _('UEFI boot mode')),
(boot_modes.LEGACY_BIOS, _('Legacy BIOS boot mode'))],
),
default=[],
mutable=True,
help=_("Specifies a list of boot modes that are not allowed "
"during deployment. Eg: ['bios']")),
]

@ -2903,6 +2903,60 @@ class TestPatch(test_api_base.BaseApiTest):
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
def test_update_fails_on_disabled_bios_boot_mode(self):
self.config(disallowed_enrollment_boot_modes=['bios'], group='api')
patch = [{
'path': '/properties/capabilities',
'value': 'boot_mode:bios',
'op': 'replace'
}]
response = self.patch_json('/nodes/%s/' % self.node.uuid, patch,
expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
self.assertIn("'bios' boot mode is not allowed",
response.json['error_message'])
def test_update_fails_on_disabled_uefi_boot_mode(self):
self.config(disallowed_enrollment_boot_modes=['uefi'], group='api')
patch = [{
'path': '/properties/capabilities',
'value': 'boot_mode:uefi',
'op': 'replace'
}]
response = self.patch_json('/nodes/%s/' % self.node.uuid, patch,
expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
self.assertIn("'uefi' boot mode is not allowed",
response.json['error_message'])
def test_update_fails_on_invalid_boot_mode(self):
# NOTE(cid): This test might need updating if boot modes' naming
# convention changes
self.assertRaises(ValueError,
self.config,
disallowed_enrollment_boot_modes=['BIOS'],
group='api')
self.assertRaises(ValueError,
self.config,
disallowed_enrollment_boot_modes=['Bios'],
group='api')
self.assertRaises(ValueError,
self.config,
disallowed_enrollment_boot_modes=['UEFI'],
group='api')
self.assertRaises(ValueError,
self.config,
disallowed_enrollment_boot_modes=['Uefi'],
group='api')
self.assertRaises(ValueError,
self.config,
disallowed_enrollment_boot_modes=['blah'],
group='api')
def test_update_with_reset_interfaces(self):
self.mock_update_node.return_value = self.node
(self
@ -4436,6 +4490,50 @@ class TestPost(test_api_base.BaseApiTest):
self.assertFalse(mock_warning.called)
self.assertFalse(mock_exception.called)
def test_create_node_fails_on_disabled_bios_boot_mode(self):
self.config(disallowed_enrollment_boot_modes=['bios'], group='api')
ndict = test_api_utils.post_get_test_node()
ndict['properties'] = {'capabilities': 'boot_mode:bios'}
response = self.post_json('/nodes', ndict, expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
self.assertIn("'bios' boot mode is not allowed",
response.json['error_message'])
def test_create_node_fails_on_disabled_uefi_boot_mode(self):
self.config(disallowed_enrollment_boot_modes=['uefi'], group='api')
ndict = test_api_utils.post_get_test_node()
ndict['properties'] = {'capabilities': 'boot_mode:uefi'}
response = self.post_json('/nodes', ndict, expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
self.assertIn("'uefi' boot mode is not allowed",
response.json['error_message'])
def test_create_node_fails_on_invalid_boot_mode(self):
# NOTE(cid): This test might need updating if boot modes' naming
# convention changes
self.assertRaises(ValueError,
self.config,
disallowed_enrollment_boot_modes=['BIOS'],
group='api')
self.assertRaises(ValueError,
self.config,
disallowed_enrollment_boot_modes=['Bios'],
group='api')
self.assertRaises(ValueError,
self.config,
disallowed_enrollment_boot_modes=['UEFI'],
group='api')
self.assertRaises(ValueError,
self.config,
disallowed_enrollment_boot_modes=['Uefi'],
group='api')
self.assertRaises(ValueError,
self.config,
disallowed_enrollment_boot_modes=['blah'],
group='api')
def test_create_node_chassis_uuid_always_in_response(self):
result = self._test_create_node(chassis_uuid=None)
self.assertIsNone(result['chassis_uuid'])

@ -282,6 +282,56 @@ class DoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
self.assertIsNone(node.last_error)
mock_deploy.assert_called_once_with(mock.ANY, mock.ANY)
def test_node_validation_in_disabled_bios_boot_mode_fails(self):
self.config(disallowed_deployment_boot_modes=['bios'],
group='conductor')
node = obj_utils.create_test_node(self.context,
properties={'boot_mode': 'bios'},
driver='fake-hardware')
with task_manager.acquire(self.context, node.uuid,
shared=False) as task:
self.assertRaises(exception.BootModeNotAllowed,
deployments.validate_node,
task, event='deploy')
def test_node_validation_in_disabled_uefi_boot_mode_fails(self):
self.config(disallowed_deployment_boot_modes=['uefi'],
group='conductor')
node = obj_utils.create_test_node(self.context,
properties={'boot_mode': 'uefi'},
driver='fake-hardware')
with task_manager.acquire(self.context, node.uuid,
shared=False) as task:
self.assertRaises(exception.BootModeNotAllowed,
deployments.validate_node,
task, event='deploy')
def test_update_fails_on_invalid_boot_mode(self):
# NOTE(cid): This test might need updating if boot modes' naming
# convention changes
self.assertRaises(ValueError,
self.config,
disallowed_deployment_boot_modes=['BIOS'],
group='conductor')
self.assertRaises(ValueError,
self.config,
disallowed_deployment_boot_modes=['Bios'],
group='conductor')
self.assertRaises(ValueError,
self.config,
disallowed_deployment_boot_modes=['UEFI'],
group='conductor')
self.assertRaises(ValueError,
self.config,
disallowed_deployment_boot_modes=['Uefi'],
group='conductor')
self.assertRaises(ValueError,
self.config,
disallowed_deployment_boot_modes=['blah'],
group='conductor')
@mock.patch.object(deployments, 'do_next_deploy_step', autospec=True)
@mock.patch.object(conductor_steps, 'set_node_deployment_steps',
autospec=True)

@ -0,0 +1,7 @@
---
features:
- |
Adds configuration options for operators to specify any or what boot modes
to disallow for enrollment (`disallowed_enrollment_boot_modes`) and/or
deployment (`disallowed_deployment_boot_modes`). Defaults are empty lists,
indicating all boot modes are allowed.