Allow disabling specific boot modes during deployment/enrollment
Allow operators to provide a list of disabled boot modes for new deployments ``disallowed_deployment_boot_modes`` and/or enrollments ``disallowed_enrollment_boot_modes``. Defaults are an empty list, [], indicating all modes are allowed. Closes-Bug: #2068530 Change-Id: I1404c81718cd6bb2977e6f298d9b7d11664226d0
This commit is contained in:
parent
ebbc8300c3
commit
58f84d2854
@ -2775,6 +2775,12 @@ class NodesController(rest.RestController):
|
|||||||
if self.from_chassis:
|
if self.from_chassis:
|
||||||
raise exception.OperationNotPermitted()
|
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
|
context = api.request.context
|
||||||
owned_node = False
|
owned_node = False
|
||||||
if CONF.api.project_admin_can_manage_own_nodes:
|
if CONF.api.project_admin_can_manage_own_nodes:
|
||||||
@ -2870,6 +2876,12 @@ class NodesController(rest.RestController):
|
|||||||
if self.from_chassis:
|
if self.from_chassis:
|
||||||
raise exception.OperationNotPermitted()
|
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)
|
api_utils.patch_validate_allowed_fields(patch, PATCH_ALLOWED_FIELDS)
|
||||||
|
|
||||||
reject_patch_in_newer_versions(patch)
|
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)
|
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):
|
def check_allow_clean_disable_ramdisk(target, disable_ramdisk):
|
||||||
if disable_ramdisk is None:
|
if disable_ramdisk is None:
|
||||||
return
|
return
|
||||||
|
@ -893,3 +893,7 @@ class UnsupportedHardwareFeature(Invalid):
|
|||||||
_msg_fmt = _("Node %(node)s hardware does not support feature "
|
_msg_fmt = _("Node %(node)s hardware does not support feature "
|
||||||
"%(feature)s, which is required based upon the "
|
"%(feature)s, which is required based upon the "
|
||||||
"requested configuration.")
|
"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 task: a TaskManager instance.
|
||||||
:param event: event to process: deploy or rebuild.
|
:param event: event to process: deploy or rebuild.
|
||||||
:raises: NodeInMaintenance, NodeProtected, InvalidStateRequested
|
:raises: NodeInMaintenance, NodeProtected, InvalidStateRequested,
|
||||||
|
BootModeNotAllowed
|
||||||
"""
|
"""
|
||||||
if task.node.maintenance:
|
if task.node.maintenance:
|
||||||
raise exception.NodeInMaintenance(op=_('provisioning'),
|
raise exception.NodeInMaintenance(op=_('provisioning'),
|
||||||
@ -56,6 +57,12 @@ def validate_node(task, event='deploy'):
|
|||||||
raise exception.InvalidStateRequested(
|
raise exception.InvalidStateRequested(
|
||||||
action=event, node=task.node.uuid, state=task.node.provision_state)
|
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')
|
@METRICS.timer('start_deploy')
|
||||||
@task_manager.require_exclusive_lock
|
@task_manager.require_exclusive_lock
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_config import types as cfg_types
|
from oslo_config import types as cfg_types
|
||||||
|
|
||||||
|
from ironic.common import boot_modes
|
||||||
from ironic.common.i18n import _
|
from ironic.common.i18n import _
|
||||||
|
|
||||||
|
|
||||||
@ -92,6 +93,16 @@ opts = [
|
|||||||
mutable=True,
|
mutable=True,
|
||||||
help=_('If a project scoped administrative user is permitted '
|
help=_('If a project scoped administrative user is permitted '
|
||||||
'to create/delete baremetal nodes in their project.')),
|
'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',
|
opt_group = cfg.OptGroup(name='api',
|
||||||
|
@ -18,8 +18,10 @@
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_config import types
|
from oslo_config import types
|
||||||
|
|
||||||
|
from ironic.common import boot_modes
|
||||||
from ironic.common.i18n import _
|
from ironic.common.i18n import _
|
||||||
|
|
||||||
|
|
||||||
opts = [
|
opts = [
|
||||||
cfg.IntOpt('workers_pool_size',
|
cfg.IntOpt('workers_pool_size',
|
||||||
default=300, min=3,
|
default=300, min=3,
|
||||||
@ -417,6 +419,16 @@ opts = [
|
|||||||
'seconds, or 30 minutes. If you need to wait longer '
|
'seconds, or 30 minutes. If you need to wait longer '
|
||||||
'than the maximum value, we recommend exploring '
|
'than the maximum value, we recommend exploring '
|
||||||
'hold steps.')),
|
'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('application/json', response.content_type)
|
||||||
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
|
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):
|
def test_update_with_reset_interfaces(self):
|
||||||
self.mock_update_node.return_value = self.node
|
self.mock_update_node.return_value = self.node
|
||||||
(self
|
(self
|
||||||
@ -4436,6 +4490,50 @@ class TestPost(test_api_base.BaseApiTest):
|
|||||||
self.assertFalse(mock_warning.called)
|
self.assertFalse(mock_warning.called)
|
||||||
self.assertFalse(mock_exception.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):
|
def test_create_node_chassis_uuid_always_in_response(self):
|
||||||
result = self._test_create_node(chassis_uuid=None)
|
result = self._test_create_node(chassis_uuid=None)
|
||||||
self.assertIsNone(result['chassis_uuid'])
|
self.assertIsNone(result['chassis_uuid'])
|
||||||
|
@ -282,6 +282,56 @@ class DoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
|||||||
self.assertIsNone(node.last_error)
|
self.assertIsNone(node.last_error)
|
||||||
mock_deploy.assert_called_once_with(mock.ANY, mock.ANY)
|
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(deployments, 'do_next_deploy_step', autospec=True)
|
||||||
@mock.patch.object(conductor_steps, 'set_node_deployment_steps',
|
@mock.patch.object(conductor_steps, 'set_node_deployment_steps',
|
||||||
autospec=True)
|
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.
|
Loading…
x
Reference in New Issue
Block a user