Add api endpoints for changing boot_mode and secure_boot state

Done:
  - Node API endpoints expose
  - RPC methods
  - Conductor Manager methods
  - Conductor utils new methods
  - RBAC new policies
  - Node API tests
  - Manager Tests (+ some testing for utils methods)
  - RBAC tests
  - Docs (api-ref)
  - REST API version history
  - Releasenotes

Story: 2008567
Task: 41709

Change-Id: I2d72389edf546b99c536c6b130ca85ababf80591
This commit is contained in:
Cenne 2021-07-08 18:37:45 +02:00
parent 9f32ceda1a
commit bc95c92f7c
22 changed files with 1248 additions and 11 deletions

View File

@ -299,6 +299,75 @@ Response
.. literalinclude:: samples/node-get-state-response.json
Change Node Boot Mode
=====================
.. rest_method:: PUT /v1/nodes/{node_ident}/states/boot_mode
Request a change to the Node's boot mode.
.. versionadded:: 1.76
A change in node's boot mode can be requested.
Normal response code: 202 (Accepted)
Error codes:
- 409 (Conflict, NodeLocked, ClientError)
- 400 (Invalid, InvalidStateRequested, InvalidParameterValue)
- 404 (NotFound)
- 503 (NoFreeConductorWorkers)
Request
-------
.. rest_parameters:: parameters.yaml
- node_ident: node_ident
- target: req_target_boot_mode
**Example request for UEFI boot:**
.. literalinclude:: samples/node-set-boot-mode-uefi.json
**Example request for Legacy BIOS boot:**
.. literalinclude:: samples/node-set-boot-mode-bios.json
Change Node Secure Boot
=======================
.. rest_method:: PUT /v1/nodes/{node_ident}/states/secure_boot
Request a change to the Node's secure boot state.
.. versionadded:: 1.76
A change in node's secure boot state can be requested.
Normal response code: 202 (Accepted)
Error codes:
- 409 (Conflict, NodeLocked, ClientError)
- 400 (Invalid, InvalidStateRequested, InvalidParameterValue)
- 404 (NotFound)
- 503 (NoFreeConductorWorkers)
Request
-------
.. rest_parameters:: parameters.yaml
- node_ident: node_ident
- target: req_target_secure_boot
**Example request to turn off secure boot:**
.. literalinclude:: samples/node-set-secure-boot-off.json
**Example request to turn on secure boot:**
.. literalinclude:: samples/node-set-secure-boot-on.json
Change Node Power State
=======================

View File

@ -1754,6 +1754,13 @@ req_storage_interface:
in: body
required: false
type: string
req_target_boot_mode:
description: |
If a boot mode change has been requested, this field represents the
requested (ie, "target") state, either "uefi" or "bios".
in: body
required: true
type: string
req_target_power_state:
description: |
If a power state transition has been requested, this field represents the
@ -1770,6 +1777,13 @@ req_target_raid_config:
in: body
required: true
type: JSON
req_target_secure_boot:
description: |
If a secure boot change has been requested, this field represents the
requested (ie, "target") state, either ``true`` or ``false``.
in: body
required: true
type: boolean
req_uuid:
description: |
The UUID for the resource.

View File

@ -0,0 +1,3 @@
{
"target": "bios"
}

View File

@ -0,0 +1,3 @@
{
"target": "uefi"
}

View File

@ -0,0 +1,3 @@
{
"target": false
}

View File

@ -0,0 +1,3 @@
{
"target": true
}

View File

@ -2,9 +2,17 @@
REST API Version History
========================
1.76 (Xena, ?)
----------------------
Add endpoints for changing boot mode and secure boot state of node
asynchronously:
* ``PUT /v1/nodes/{node_ident}/states/boot_mode``
* ``PUT /v1/nodes/{node_ident}/states/secure_boot``
1.75 (Xena, 18.1)
----------------------
Add `boot_mode` and `secure_boot` to node object and expose their state at:
Add ``boot_mode`` and ``secure_boot`` to node object and expose their state at:
* ``/v1/nodes/{node_ident}/states``

View File

@ -40,6 +40,7 @@ from ironic.api.controllers.v1 import versions
from ironic.api.controllers.v1 import volume
from ironic.api import method
from ironic.common import args
from ironic.common import boot_modes
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import policy
@ -120,6 +121,9 @@ ALLOWED_TARGET_POWER_STATES = (ir_states.POWER_ON,
ir_states.SOFT_REBOOT,
ir_states.SOFT_POWER_OFF)
ALLOWED_TARGET_BOOT_MODES = (boot_modes.LEGACY_BIOS,
boot_modes.UEFI)
_NODE_DESCRIPTION_MAX_LENGTH = 4096
_NETWORK_DATA_SCHEMA = None
@ -710,6 +714,8 @@ def node_states_convert(rpc_node):
class NodeStatesController(rest.RestController):
_custom_actions = {
'boot_mode': ['PUT'],
'secure_boot': ['PUT'],
'power': ['PUT'],
'provision': ['PUT'],
'raid': ['PUT'],
@ -822,6 +828,107 @@ class NodeStatesController(rest.RestController):
url_args = '/'.join([node_ident, 'states'])
api.response.location = link.build_url('nodes', url_args)
@METRICS.timer('NodeStatesController.boot_mode')
@method.expose(status_code=http_client.ACCEPTED)
@args.validate(node_ident=args.uuid_or_name, target=args.string)
def boot_mode(self, node_ident, target):
"""Asynchronous set the boot mode of the node.
:param node_ident: the UUID or logical name of a node.
:param target: The desired boot_mode for the node. (uefi/bios)
:raises: NotFound (HTTP 404) if requested version of the API
is less than 1.76.
:raises: InvalidParameterValue (HTTP 400) if the requested target
state is not valid.
:raises: Conflict (HTTP 409) if a node is in adopting state or
another transient state.
"""
rpc_node = api_utils.check_node_policy_and_retrieve(
'baremetal:node:set_boot_mode', node_ident)
topic = api.request.rpcapi.get_topic_for(rpc_node)
if (api.request.version.minor
< versions.MINOR_76_NODE_CHANGE_BOOT_MODE):
raise exception.NotFound(
(_("This endpoint is supported starting with the API version "
"1.%(min_version)s") %
{'min_version': versions.MINOR_76_NODE_CHANGE_BOOT_MODE}))
if target not in ALLOWED_TARGET_BOOT_MODES:
msg = (_("Invalid boot mode %(mode)s requested for node. "
"Allowed boot modes are: "
"%(modes)s") %
{'mode': target,
'modes': ', '.join(ALLOWED_TARGET_BOOT_MODES)})
raise exception.InvalidParameterValue(msg)
# NOTE(cenne): This currenly includes the ADOPTING state
if rpc_node.provision_state in ir_states.UNSTABLE_STATES:
msg = _("Node is in %(state)s state. Since node is transitioning, "
"the boot mode will not be set as this may interfere "
"with ongoing changes and result in erroneous modification"
". Try again later.")
raise exception.Conflict(msg,
action=target, node=node_ident,
state=rpc_node.provision_state
)
api.request.rpcapi.change_node_boot_mode(api.request.context,
rpc_node.uuid, target,
topic=topic)
# Set the HTTP Location Header
url_args = '/'.join([node_ident, 'states'])
api.response.location = link.build_url('nodes', url_args)
@METRICS.timer('NodeStatesController.secure_boot')
@method.expose(status_code=http_client.ACCEPTED)
@args.validate(node_ident=args.uuid_or_name, target=args.boolean)
def secure_boot(self, node_ident, target):
"""Asynchronous set the secure_boot state of the node.
:param node_ident: the UUID or logical name of a node.
:param target: The desired secure_boot for the node. (True/False)
:raises: NotFound (HTTP 404) if requested version of the API
is less than 1.76.
:raises: InvalidParameterValue (HTTP 400) if the requested target
state is not valid.
:raises: Conflict (HTTP 409) if a node is in adopting state.
"""
rpc_node = api_utils.check_node_policy_and_retrieve(
'baremetal:node:set_secure_boot', node_ident)
topic = api.request.rpcapi.get_topic_for(rpc_node)
if (api.request.version.minor
< versions.MINOR_76_NODE_CHANGE_BOOT_MODE):
raise exception.NotFound(
(_("This endpoint is supported starting with the API version "
"1.%(min_version)s") %
{'min_version': versions.MINOR_76_NODE_CHANGE_BOOT_MODE}))
# NOTE(cenne): This is to exclude target=None or other invalid values
if target not in (True, False):
msg = (_("Invalid secure_boot %(state)s requested for node. "
"Allowed secure_boot states are: True, False) ") %
{'state': target})
raise exception.InvalidParameterValue(msg)
# NOTE(cenne): This currenly includes the ADOPTING state
if rpc_node.provision_state in ir_states.UNSTABLE_STATES:
msg = _("Node is in %(state)s state. Since node is transitioning, "
"the boot mode will not be set as this may interfere "
"with ongoing changes and result in erroneous modification"
". Try again later.")
raise exception.Conflict(msg,
action=target, node=node_ident,
state=rpc_node.provision_state
)
api.request.rpcapi.change_node_secure_boot(api.request.context,
rpc_node.uuid, target,
topic=topic)
# Set the HTTP Location Header
url_args = '/'.join([node_ident, 'states'])
api.response.location = link.build_url('nodes', url_args)
def _do_provision_action(self, rpc_node, target, configdrive=None,
clean_steps=None, deploy_steps=None,
rescue_password=None, disable_ramdisk=None):

View File

@ -113,6 +113,7 @@ BASE_VERSION = 1
# v1.73: Add support for deploy and undeploy verbs
# v1.74: Add bios registry to /v1/nodes/{node}/bios/{setting}
# v1.75: Add boot_mode, secure_boot fields to node object.
# v1.76: Add support for changing boot_mode and secure_boot state
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@ -190,6 +191,7 @@ MINOR_72_HEARTBEAT_STATUS = 72
MINOR_73_DEPLOY_UNDEPLOY_VERBS = 73
MINOR_74_BIOS_REGISTRY = 74
MINOR_75_NODE_BOOT_MODE = 75
MINOR_76_NODE_CHANGE_BOOT_MODE = 76
# When adding another version, update:
# - MINOR_MAX_VERSION
@ -197,7 +199,7 @@ MINOR_75_NODE_BOOT_MODE = 75
# explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
MINOR_MAX_VERSION = MINOR_75_NODE_BOOT_MODE
MINOR_MAX_VERSION = MINOR_76_NODE_CHANGE_BOOT_MODE
# String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -770,6 +770,26 @@ node_policies = [
],
deprecated_rule=deprecated_node_set_power_state
),
policy.DocumentedRuleDefault(
name='baremetal:node:set_boot_mode',
check_str=SYSTEM_OR_PROJECT_MEMBER,
scope_types=['system', 'project'],
description='Change Node boot mode',
operations=[
{'path': '/nodes/{node_ident}/states/boot_mode', 'method': 'PUT'}
],
deprecated_rule=deprecated_node_set_power_state
),
policy.DocumentedRuleDefault(
name='baremetal:node:set_secure_boot',
check_str=SYSTEM_OR_PROJECT_MEMBER,
scope_types=['system', 'project'],
description='Change Node secure boot state',
operations=[
{'path': '/nodes/{node_ident}/states/secure_boot', 'method': 'PUT'}
],
deprecated_rule=deprecated_node_set_power_state
),
policy.DocumentedRuleDefault(
name='baremetal:node:set_provision_state',
check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN,

View File

@ -371,8 +371,8 @@ RELEASE_MAPPING = {
}
},
'master': {
'api': '1.75',
'rpc': '1.54',
'api': '1.76',
'rpc': '1.55',
'objects': {
'Allocation': ['1.1'],
'BIOSSetting': ['1.1'],

View File

@ -91,7 +91,7 @@ class ConductorManager(base_manager.BaseConductorManager):
# NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's.
# NOTE(pas-ha): This also must be in sync with
# ironic.common.release_mappings.RELEASE_MAPPING['master']
RPC_API_VERSION = '1.54'
RPC_API_VERSION = '1.55'
target = messaging.Target(version=RPC_API_VERSION)
@ -346,6 +346,64 @@ class ConductorManager(base_manager.BaseConductorManager):
task.spawn_after(self._spawn_worker, utils.node_power_action,
task, new_state, timeout=power_timeout)
@METRICS.timer('ConductorManager.change_node_boot_mode')
@messaging.expected_exceptions(exception.InvalidParameterValue,
exception.NoFreeConductorWorker,
exception.NodeLocked)
def change_node_boot_mode(self, context, node_id, new_state):
"""RPC method to encapsulate changes to a node's boot_mode.
:param context: an admin context.
:param node_id: the id or uuid of a node.
:param new_state: the desired boot mode for the node.
:raises: NoFreeConductorWorker when there is no free worker to start
async task.
:raises: InvalidParameterValue
"""
LOG.debug("RPC change_node_boot_mode called for node %(node)s. "
"The desired new state is %(state)s.",
{'node': node_id, 'state': new_state})
with task_manager.acquire(context, node_id, shared=False,
purpose='changing node boot mode') as task:
task.driver.management.validate(task)
# Starting new operation, so clear the previous error.
# We'll be putting an error here soon if we fail task.
task.node.last_error = None
task.node.save()
task.set_spawn_error_hook(utils._spawn_error_handler,
task.node, "changing node boot mode")
task.spawn_after(self._spawn_worker,
utils.node_change_boot_mode, task, new_state)
@METRICS.timer('ConductorManager.change_node_secure_boot')
@messaging.expected_exceptions(exception.InvalidParameterValue,
exception.NoFreeConductorWorker,
exception.NodeLocked)
def change_node_secure_boot(self, context, node_id, new_state):
"""RPC method to encapsulate changes to a node's secure_boot state.
:param context: an admin context.
:param node_id: the id or uuid of a node.
:param new_state: the desired secure_boot state for the node.
:raises: NoFreeConductorWorker when there is no free worker to start
async task.
:raises: InvalidParameterValue
"""
LOG.debug("RPC change_node_secure_boot called for node %(node)s. "
"The desired new state is %(state)s.",
{'node': node_id, 'state': new_state})
with task_manager.acquire(context, node_id, shared=False,
purpose='changing node secure') as task:
task.driver.management.validate(task)
# Starting new operation, so clear the previous error.
# We'll be putting an error here soon if we fail task.
task.node.last_error = None
task.node.save()
task.set_spawn_error_hook(utils._spawn_error_handler,
task.node, "changing node secure boot")
task.spawn_after(self._spawn_worker,
utils.node_change_secure_boot, task, new_state)
@METRICS.timer('ConductorManager.vendor_passthru')
@messaging.expected_exceptions(exception.NoFreeConductorWorker,
exception.NodeLocked,

View File

@ -108,12 +108,13 @@ class ConductorAPI(object):
| 1.53 - Added disable_ramdisk to do_node_clean.
| 1.54 - Added optional agent_status and agent_status_message to
heartbeat
| 1.55 - Added change_node_boot_mode
"""
# NOTE(rloo): This must be in sync with manager.ConductorManager's.
# NOTE(pas-ha): This also must be in sync with
# ironic.common.release_mappings.RELEASE_MAPPING['master']
RPC_API_VERSION = '1.54'
RPC_API_VERSION = '1.55'
def __init__(self, topic=None):
super(ConductorAPI, self).__init__()
@ -281,6 +282,46 @@ class ConductorAPI(object):
return cctxt.call(context, 'change_node_power_state', node_id=node_id,
new_state=new_state, timeout=timeout)
def change_node_boot_mode(self, context, node_id, new_state,
topic=None):
"""Change a node's boot mode.
Synchronously, acquire lock and start the conductor background task
to change boot mode of a node.
:param context: request context.
:param node_id: node id or uuid.
:param new_state: one of ironic.common.boot_modes values
('bios' / 'uefi')
:param topic: RPC topic. Defaults to self.topic.
:raises: NoFreeConductorWorker when there is no free worker to start
async task.
"""
cctxt = self.client.prepare(topic=topic or self.topic, version='1.55')
return cctxt.call(context, 'change_node_boot_mode', node_id=node_id,
new_state=new_state)
def change_node_secure_boot(self, context, node_id, new_state,
topic=None):
"""Change a node's secure_boot state.
Synchronously, acquire lock and start the conductor background task
to change secure_boot state of a node.
:param context: request context.
:param node_id: node id or uuid.
:param new_state: Target secure boot state
(True => 'on' / False => 'off')
:param topic: RPC topic. Defaults to self.topic.
:raises: NoFreeConductorWorker when there is no free worker to start
async task.
"""
cctxt = self.client.prepare(topic=topic or self.topic, version='1.55')
return cctxt.call(context, 'change_node_secure_boot', node_id=node_id,
new_state=new_state)
def vendor_passthru(self, context, node_id, driver_method, http_method,
info, topic=None):
"""Receive requests for vendor-specific actions.

View File

@ -133,14 +133,22 @@ def node_set_boot_mode(task, mode):
return
task.driver.management.validate(task)
try:
supported_boot_modes = (
task.driver.management.get_supported_boot_modes(task)
)
except exception.UnsupportedDriverExtension:
LOG.debug(
"Cannot determine supported boot modes of driver "
"%(driver)s. Will make an attempt to set boot mode %(mode)s",
{'driver': task.node.driver, 'mode': mode})
supported_boot_modes = ()
boot_modes = task.driver.management.get_supported_boot_modes(task)
if mode not in boot_modes:
if supported_boot_modes and mode not in supported_boot_modes:
msg = _("Unsupported boot mode %(mode)s specified for "
"node %(node_id)s. Supported boot modes are: "
"%(modes)s") % {'mode': mode,
'modes': ', '.join(boot_modes),
'modes': ', '.join(supported_boot_modes),
'node_id': task.node.uuid}
raise exception.InvalidParameterValue(msg)
@ -1473,3 +1481,95 @@ def node_cache_boot_mode(task):
"for node %(node)s",
{'boot_mode': boot_mode, 'secure_boot': secure_boot,
'node': task.node.uuid})
def node_change_boot_mode(task, target_boot_mode):
"""Change boot mode to requested state for node
:param task: a TaskManager instance containing the node to act on.
:param target_boot_mode: Any boot mode in :mod:`ironic.common.boot_modes`.
"""
try:
current_boot_mode = task.driver.management.get_boot_mode(task)
except Exception as exc:
current_boot_mode = None
LOG.warning('Unexpected exception when trying to detect boot_mode '
'while changing boot mode for node '
'%(node)s. %(class)s: %(exc)s',
{'node': task.node.uuid,
'class': type(exc).__name__, 'exc': exc},
exc_info=not isinstance(exc, exception.IronicException))
if (current_boot_mode is not None
and target_boot_mode == current_boot_mode):
LOG.info("Target boot mode '%(target)s', and current boot mode "
"'%(current)s' are identical. No change being made "
"for node %(node)s",
{'target': target_boot_mode, 'current': current_boot_mode,
'node': task.node.uuid})
return
try:
task.driver.management.set_boot_mode(task, mode=target_boot_mode)
except Exception as exc:
LOG.error('Unexpected exception when trying to change boot_mode '
'to %(target)s for node %(node)s. %(class)s: %(exc)s',
{'node': task.node.uuid, 'target': target_boot_mode,
'class': type(exc).__name__, 'exc': exc},
exc_info=not isinstance(exc, exception.IronicException))
task.node.last_error = (
"Failed to change boot mode to '%(target)s'. "
"Error: %(err)s" % {'target': target_boot_mode, 'err': exc})
task.node.save()
else:
LOG.info("Changed boot_mode to %(mode)s for node %(node)s",
{'mode': target_boot_mode, 'node': task.node.uuid})
task.node.boot_mode = target_boot_mode
task.node.save()
def node_change_secure_boot(task, secure_boot_target):
"""Change secure_boot state to requested state for node
:param task: a TaskManager instance containing the node to act on.
:param secure_boot_target: Target secure_boot state
OneOf(True => on, False => off)
:type secure_boot_target: boolean
"""
try:
secure_boot_current = task.driver.management.get_secure_boot_state(
task)
except Exception as exc:
secure_boot_current = None
LOG.warning('Unexpected exception when trying to detect secure_boot '
'state while changing secure_boot for node '
'%(node)s. %(class)s: %(exc)s',
{'node': task.node.uuid,
'class': type(exc).__name__, 'exc': exc},
exc_info=not isinstance(exc, exception.IronicException))
if (secure_boot_current is not None
and secure_boot_target == secure_boot_current):
LOG.info("Target secure_boot state '%(target)s', and current "
"secure_boot state '%(current)s' are identical. "
"No change being made for node %(node)s",
{'target': secure_boot_target,
'current': secure_boot_current,
'node': task.node.uuid})
return
try:
task.driver.management.set_secure_boot_state(task, secure_boot_target)
except Exception as exc:
LOG.error('Unexpected exception when trying to change secure_boot '
'to %(target)s for node %(node)s. %(class)s: %(exc)s',
{'node': task.node.uuid, 'target': secure_boot_target,
'class': type(exc).__name__, 'exc': exc},
exc_info=not isinstance(exc, exception.IronicException))
task.node.last_error = (
"Failed to change secure_boot state to '%(target)s'. "
"Error: %(err)s" % {'target': secure_boot_target, 'err': exc})
task.node.save()
else:
LOG.info("Changed secure_boot state to %(state)s for node %(node)s",
{'state': secure_boot_target, 'node': task.node.uuid})
task.node.secure_boot = secure_boot_target
task.node.save()

View File

@ -35,6 +35,7 @@ from ironic.api.controllers.v1 import notification_utils
from ironic.api.controllers.v1 import utils as api_utils
from ironic.api.controllers.v1 import versions
from ironic.common import boot_devices
from ironic.common import boot_modes
from ironic.common import components
from ironic.common import driver_factory
from ironic.common import exception
@ -5136,6 +5137,14 @@ class TestPut(test_api_base.BaseApiTest):
autospec=True)
self.mock_cnps = p.start()
self.addCleanup(p.stop)
p = mock.patch.object(rpcapi.ConductorAPI, 'change_node_boot_mode',
autospec=True)
self.mock_cnbm = p.start()
self.addCleanup(p.stop)
p = mock.patch.object(rpcapi.ConductorAPI, 'change_node_secure_boot',
autospec=True)
self.mock_cnsb = p.start()
self.addCleanup(p.stop)
p = mock.patch.object(rpcapi.ConductorAPI, 'do_node_deploy',
autospec=True)
self.mock_dnd = p.start()
@ -5301,6 +5310,147 @@ class TestPut(test_api_base.BaseApiTest):
{'target': 'not-supported'}, expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
def _test_boot_mode_success(self, target_state, api_version):
body = {'target': target_state}
if api_version is None:
response = self.put_json(
'/nodes/%s/states/boot_mode' % self.node.uuid, body)
else:
response = self.put_json(
'/nodes/%s/states/boot_mode' % self.node.uuid, body,
headers={api_base.Version.string: api_version})
self.assertEqual(http_client.ACCEPTED, response.status_code)
self.assertEqual(b'', response.body)
self.mock_cnbm.assert_called_once_with(mock.ANY,
mock.ANY,
self.node.uuid,
target_state,
topic='test-topic')
# Check location header
self.assertIsNotNone(response.location)
expected_location = '/v1/nodes/%s/states' % self.node.uuid
self.assertEqual(urlparse.urlparse(response.location).path,
expected_location)
def _test_boot_mode_failure(self, target_state, http_status_code,
api_version):
body = {'target': target_state}
if api_version is None:
response = self.put_json(
'/nodes/%s/states/boot_mode' % self.node.uuid, body,
expect_errors=True)
else:
response = self.put_json(
'/nodes/%s/states/boot_mode' % self.node.uuid, body,
headers={api_base.Version.string: api_version},
expect_errors=True)
self.assertEqual(http_status_code, response.status_code)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
def test_boot_mode_uefi_valid_soft_ver(self):
self._test_boot_mode_success(boot_modes.UEFI, "1.76")
def test_boot_mode_uefi_older_soft_ver(self):
self._test_boot_mode_failure(
boot_modes.UEFI, http_client.NOT_FOUND, "1.75")
def test_boot_mode_bios_valid_soft_ver(self):
self._test_boot_mode_success(boot_modes.LEGACY_BIOS, "1.76")
def test_boot_mode_bios_older_soft_ver(self):
self._test_boot_mode_failure(
boot_modes.LEGACY_BIOS, http_client.NOT_FOUND, "1.75")
def test_boot_mode_invalid_request(self):
self._test_boot_mode_failure(
'unsupported-efi', http_client.BAD_REQUEST, "1.76")
def _test_secure_boot_success(self, target_state, api_version):
body = {'target': target_state}
if api_version is None:
response = self.put_json(
'/nodes/%s/states/secure_boot' % self.node.uuid, body)
else:
response = self.put_json(
'/nodes/%s/states/secure_boot' % self.node.uuid, body,
headers={api_base.Version.string: api_version})
self.assertEqual(http_client.ACCEPTED, response.status_code)
self.assertEqual(b'', response.body)
self.mock_cnsb.assert_called_once_with(mock.ANY,
mock.ANY,
self.node.uuid,
target_state,
topic='test-topic')
# Check location header
self.assertIsNotNone(response.location)
expected_location = '/v1/nodes/%s/states' % self.node.uuid
self.assertEqual(urlparse.urlparse(response.location).path,
expected_location)
def _test_secure_boot_failure(self, target_state, http_status_code,
api_version):
body = {'target': target_state}
if api_version is None:
response = self.put_json(
'/nodes/%s/states/secure_boot' % self.node.uuid, body,
expect_errors=True)
else:
response = self.put_json(
'/nodes/%s/states/secure_boot' % self.node.uuid, body,
headers={api_base.Version.string: api_version},
expect_errors=True)
self.assertEqual(http_status_code, response.status_code)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
def test_secure_boot_on_valid_soft_ver(self):
self._test_secure_boot_success(True, "1.76")
def test_secure_boot_on_older_soft_ver(self):
self._test_secure_boot_failure(
True, http_client.NOT_FOUND, "1.75")
def test_secure_boot_off_valid_soft_ver(self):
self._test_secure_boot_success(False, "1.76")
def test_secure_boot_off_older_soft_ver(self):
self._test_secure_boot_failure(
False, http_client.NOT_FOUND, "1.75")
def test_secure_boot_off_valid_undocumented_request_zero(self):
self._test_secure_boot_success(0, "1.76")
def test_secure_boot_on_valid_undocumented_request_one(self):
self._test_secure_boot_success(1, "1.76")
def test_secure_boot_on_invalid_request_two(self):
self._test_secure_boot_failure(2, http_client.BAD_REQUEST, "1.76")
def test_secure_boot_invalid_request_nullstr(self):
self._test_secure_boot_failure(
'', http_client.BAD_REQUEST, "1.76")
def test_secure_boot_invalid_request_boo(self):
self._test_secure_boot_failure(
'boo!', http_client.BAD_REQUEST, "1.76")
def test_secure_boot_invalid_request_None(self):
self._test_secure_boot_failure(
None, http_client.BAD_REQUEST, "1.76")
def test_power_change_when_being_cleaned(self):
for state in (states.CLEANING, states.CLEANWAIT):
self.node.provision_state = state

View File

@ -412,6 +412,56 @@ nodes_states_power_put_observer:
assert_status: 403
deprecated: true
nodes_states_boot_mode_put_admin:
path: '/v1/nodes/{node_ident}/states/boot_mode'
method: put
headers: *admin_headers
body: &boot_mode_body
target: "uefi"
assert_status: 503
deprecated: true
nodes_states_boot_mode_put_member:
path: '/v1/nodes/{node_ident}/states/boot_mode'
method: put
headers: *member_headers
body: *boot_mode_body
assert_status: 404
deprecated: true
nodes_states_boot_mode_put_observer:
path: '/v1/nodes/{node_ident}/states/boot_mode'
method: put
headers: *observer_headers
body: *boot_mode_body
assert_status: 403
deprecated: true
nodes_states_secure_boot_put_admin:
path: '/v1/nodes/{node_ident}/states/secure_boot'
method: put
headers: *admin_headers
body: &secure_boot_body
target: "true"
assert_status: 503
deprecated: true
nodes_states_secure_boot_put_member:
path: '/v1/nodes/{node_ident}/states/secure_boot'
method: put
headers: *member_headers
body: *secure_boot_body
assert_status: 404
deprecated: true
nodes_states_secure_boot_put_observer:
path: '/v1/nodes/{node_ident}/states/secure_boot'
method: put
headers: *observer_headers
body: *secure_boot_body
assert_status: 403
deprecated: true
nodes_states_provision_put_admin:
path: '/v1/nodes/{node_ident}/states/provision'
method: put

View File

@ -863,6 +863,110 @@ third_party_admin_cannot_put_power_state_change:
body: *power_body
assert_status: 404
# Boot mode state
owner_admin_can_put_boot_mode_state_change:
path: '/v1/nodes/{owner_node_ident}/states/boot_mode'
method: put
headers: *owner_admin_headers
body: &boot_mode_body
target: "uefi"
assert_status: 503
lessee_admin_can_put_boot_mode_state_change:
path: '/v1/nodes/{lessee_node_ident}/states/boot_mode'
method: put
headers: *lessee_admin_headers
body: *boot_mode_body
assert_status: 503
owner_member_can_put_boot_mode_state_change:
path: '/v1/nodes/{owner_node_ident}/states/boot_mode'
method: put
headers: *owner_member_headers
body: *boot_mode_body
assert_status: 503
lessee_member_can_put_boot_mode_state_change:
path: '/v1/nodes/{lessee_node_ident}/states/boot_mode'
method: put
headers: *lessee_member_headers
body: *boot_mode_body
assert_status: 503
owner_reader_cannot_put_boot_mode_state_change:
path: '/v1/nodes/{owner_node_ident}/states/boot_mode'
method: put
headers: *owner_reader_headers
body: *boot_mode_body
assert_status: 403
lessee_reader_cannot_put_boot_mode_state_change:
path: '/v1/nodes/{lessee_node_ident}/states/boot_mode'
method: put
headers: *lessee_reader_headers
body: *boot_mode_body
assert_status: 403
third_party_admin_cannot_put_boot_mode_state_change:
path: '/v1/nodes/{node_ident}/states/boot_mode'
method: put
headers: *third_party_admin_headers
body: *boot_mode_body
assert_status: 404
# Secure Boot state
owner_admin_can_put_secure_boot_state_change:
path: '/v1/nodes/{owner_node_ident}/states/secure_boot'
method: put
headers: *owner_admin_headers
body: &secure_boot_body
target: "true"
assert_status: 503
lessee_admin_can_put_secure_boot_state_change:
path: '/v1/nodes/{lessee_node_ident}/states/secure_boot'
method: put
headers: *lessee_admin_headers
body: *secure_boot_body
assert_status: 503
owner_member_can_put_secure_boot_state_change:
path: '/v1/nodes/{owner_node_ident}/states/secure_boot'
method: put
headers: *owner_member_headers
body: *secure_boot_body
assert_status: 503
lessee_member_can_put_secure_boot_state_change:
path: '/v1/nodes/{lessee_node_ident}/states/secure_boot'
method: put
headers: *lessee_member_headers
body: *secure_boot_body
assert_status: 503
owner_reader_cannot_put_secure_boot_state_change:
path: '/v1/nodes/{owner_node_ident}/states/secure_boot'
method: put
headers: *owner_reader_headers
body: *secure_boot_body
assert_status: 403
lessee_reader_cannot_put_secure_boot_state_change:
path: '/v1/nodes/{lessee_node_ident}/states/secure_boot'
method: put
headers: *lessee_reader_headers
body: *secure_boot_body
assert_status: 403
third_party_admin_cannot_put_secure_boot_state_change:
path: '/v1/nodes/{node_ident}/states/secure_boot'
method: put
headers: *third_party_admin_headers
body: *secure_boot_body
assert_status: 404
# Provision states
owner_admin_can_change_provision_state:

View File

@ -378,6 +378,54 @@ nodes_states_power_put_reader:
body: *power_body
assert_status: 403
# Boot mode state
nodes_states_boot_mode_put_admin:
path: '/v1/nodes/{node_ident}/states/boot_mode'
method: put
headers: *admin_headers
body: &boot_mode_body
target: "uefi"
assert_status: 503
nodes_states_boot_mode_put_member:
path: '/v1/nodes/{node_ident}/states/boot_mode'
method: put
headers: *scoped_member_headers
body: *boot_mode_body
assert_status: 503
nodes_states_boot_mode_put_reader:
path: '/v1/nodes/{node_ident}/states/boot_mode'
method: put
headers: *reader_headers
body: *boot_mode_body
assert_status: 403
# Secure Boot state
nodes_states_secure_boot_put_admin:
path: '/v1/nodes/{node_ident}/states/secure_boot'
method: put
headers: *admin_headers
body: &secure_boot_body
target: "true"
assert_status: 503
nodes_states_secure_boot_put_member:
path: '/v1/nodes/{node_ident}/states/secure_boot'
method: put
headers: *scoped_member_headers
body: *secure_boot_body
assert_status: 503
nodes_states_secure_boot_put_reader:
path: '/v1/nodes/{node_ident}/states/secure_boot'
method: put
headers: *reader_headers
body: *secure_boot_body
assert_status: 403
nodes_states_provision_put_admin:
path: '/v1/nodes/{node_ident}/states/provision'
method: put

View File

@ -34,6 +34,7 @@ from oslo_versionedobjects import fields
import tenacity
from ironic.common import boot_devices
from ironic.common import boot_modes
from ironic.common import components
from ironic.common import driver_factory
from ironic.common import exception
@ -447,6 +448,431 @@ class ChangeNodePowerStateTestCase(mgr_utils.ServiceSetUpMixin,
self.assertIsNone(node.last_error)
@mgr_utils.mock_record_keepalive
class ChangeNodeBootModeTestCase(mgr_utils.ServiceSetUpMixin,
db_base.DbTestCase):
@mock.patch.object(fake.FakeManagement, 'set_boot_mode', autospec=True)
@mock.patch.object(fake.FakeManagement, 'get_boot_mode', autospec=True)
def test_change_node_boot_mode_valid(self, get_boot_mock, set_boot_mock):
# Test change_node_boot_mode including integration with
# conductor.utils.node_change_boot_mode
get_boot_mock.side_effect = [boot_modes.LEGACY_BIOS, # before setting
boot_modes.UEFI] # after setting
node = obj_utils.create_test_node(self.context,
driver='fake-hardware',
boot_mode=boot_modes.LEGACY_BIOS)
self._start_service()
self.service.change_node_boot_mode(self.context,
node.uuid,
boot_modes.UEFI)
self._stop_service()
set_boot_mock.assert_called_once_with(mock.ANY, mock.ANY,
mode=boot_modes.UEFI)
self.assertEqual(get_boot_mock.call_count, 1)
# Call once before setting to see if it's required
node.refresh()
self.assertEqual(boot_modes.UEFI, node.boot_mode)
self.assertIsNone(node.last_error)
# Verify the reservation has been cleared by
# background task's link callback.
self.assertIsNone(node.reservation)
@mock.patch.object(fake.FakeManagement, 'set_boot_mode', autospec=True)
@mock.patch.object(fake.FakeManagement, 'get_boot_mode', autospec=True)
def test_change_node_boot_mode_existing(self, get_boot_mock,
set_boot_mock):
# Test change_node_boot_mode including integration with
# conductor.utils.node_change_boot_mode when target==current
get_boot_mock.return_value = boot_modes.LEGACY_BIOS
node = obj_utils.create_test_node(self.context,
driver='fake-hardware',
boot_mode=boot_modes.LEGACY_BIOS)
self._start_service()
self.service.change_node_boot_mode(self.context,
node.uuid,
boot_modes.LEGACY_BIOS)
self._stop_service()
set_boot_mock.assert_not_called()
self.assertEqual(get_boot_mock.call_count, 1)
# Called once before setting to see if it's even required
node.refresh()
self.assertEqual(boot_modes.LEGACY_BIOS, node.boot_mode)
self.assertIsNone(node.last_error)
# Verify the reservation has been cleared by
# background task's link callback.
self.assertIsNone(node.reservation)
@mock.patch.object(conductor_utils, 'node_change_boot_mode',
autospec=True)
def test_change_node_boot_mode_node_already_locked(self, ncbm_mock):
# Test change_node_boot_mode with mocked
# conductor.utils.node_change_boot_mode.
fake_reservation = 'fake-reserv'
node = obj_utils.create_test_node(self.context,
driver='fake-hardware',
boot_mode=boot_modes.LEGACY_BIOS,
reservation=fake_reservation)
self._start_service()
exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.change_node_boot_mode,
self.context,
node.uuid,
boot_modes.LEGACY_BIOS)
# Compare true exception hidden by @messaging.expected_exceptions
self.assertEqual(exception.NodeLocked, exc.exc_info[0])
# In this test worker should not be spawned, but waiting to make sure
# the below perform_mock assertion is valid.
self._stop_service()
self.assertFalse(ncbm_mock.called, 'node_change_boot_mode has been '
'unexpectedly called.')
# Verify existing reservation wasn't broken.
node.refresh()
self.assertEqual(fake_reservation, node.reservation)
def test_change_node_boot_mode_worker_pool_full(self):
# Test change_node_boot_mode including integration with
# conductor.utils.change_node_boot_mode.
initial_state = boot_modes.LEGACY_BIOS
node = obj_utils.create_test_node(self.context, driver='fake-hardware',
boot_mode=initial_state)
self._start_service()
with mock.patch.object(self.service,
'_spawn_worker', autospec=True) as spawn_mock:
spawn_mock.side_effect = exception.NoFreeConductorWorker()
exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.change_node_boot_mode,
self.context,
node.uuid,
boot_modes.UEFI)
# Compare true exception hidden by @messaging.expected_exceptions
self.assertEqual(exception.NoFreeConductorWorker, exc.exc_info[0])
spawn_mock.assert_called_once_with(mock.ANY, mock.ANY,
mock.ANY)
node.refresh()
self.assertEqual(initial_state, node.boot_mode)
self.assertIsNotNone(node.last_error)
# Verify the picked reservation has been cleared due to full pool.
self.assertIsNone(node.reservation)
@mock.patch.object(fake.FakeManagement, 'set_boot_mode', autospec=True)
@mock.patch.object(fake.FakeManagement, 'get_boot_mode', autospec=True)
def test_change_node_boot_mode_exception_in_background_task(
self, get_boot_mock, set_boot_mock):
# Test change_node_boot_mode including integration with
# conductor.utils.node_change_boot_mode.
initial_state = boot_modes.LEGACY_BIOS
node = obj_utils.create_test_node(self.context, driver='fake-hardware',
boot_mode=initial_state)
self._start_service()
get_boot_mock.return_value = boot_modes.LEGACY_BIOS
new_state = boot_modes.UEFI
set_boot_mock.side_effect = exception.UnsupportedDriverExtension(
driver=fake, extension='set_boot_mode')
self.service.change_node_boot_mode(self.context,
node.uuid,
new_state)
self._stop_service()
# Call once before setting to see if it was required
self.assertEqual(get_boot_mock.call_count, 1)
set_boot_mock.assert_called_once_with(mock.ANY, mock.ANY,
new_state)
node.refresh()
self.assertEqual(initial_state, node.boot_mode)
self.assertIsNotNone(node.last_error)
# Verify the reservation has been cleared by background task's
# link callback despite exception in background task.
self.assertIsNone(node.reservation)
@mock.patch.object(fake.FakeManagement, 'validate', autospec=True)
def test_change_node_boot_mode_validate_fail(self, validate_mock):
# Test change_node_power_state where task.driver.management.validate
# fails
initial_state = boot_modes.UEFI
node = obj_utils.create_test_node(self.context,
driver='fake-hardware',
boot_mode=initial_state)
self._start_service()
validate_mock.side_effect = exception.InvalidParameterValue(
'wrong management driver info')
new_state = boot_modes.LEGACY_BIOS
exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.change_node_boot_mode,
self.context,
node.uuid,
new_state)
self.assertEqual(exception.InvalidParameterValue, exc.exc_info[0])
node.refresh()
validate_mock.assert_called_once_with(mock.ANY, mock.ANY)
self.assertEqual(initial_state, node.boot_mode)
self.assertIsNone(node.last_error)
@mock.patch.object(fake.FakeManagement, 'set_boot_mode', autospec=True)
@mock.patch.object(fake.FakeManagement, 'get_boot_mode', autospec=True)
def test_change_node_boot_mode_exception_getting_current(self,
get_boot_mock,
set_boot_mock):
# Test change_node_boot_mode smooth opertion when get_boot_mode mode
# raises an exception
initial_state = boot_modes.LEGACY_BIOS
node = obj_utils.create_test_node(self.context, driver='fake-hardware',
boot_mode=initial_state)
self._start_service()
get_boot_mock.side_effect = exception.UnsupportedDriverExtension(
driver=fake, extension='get_boot_mode')
new_state = boot_modes.UEFI
self.service.change_node_boot_mode(self.context,
node.uuid,
new_state)
self._stop_service()
# Call once before setting to see if it is required
self.assertEqual(get_boot_mock.call_count, 1)
set_boot_mock.assert_called_once_with(mock.ANY, mock.ANY,
new_state)
node.refresh()
self.assertEqual(new_state, node.boot_mode)
self.assertIsNone(node.last_error)
# Verify the reservation has been cleared by
# background task's link callback.
self.assertIsNone(node.reservation)
@mgr_utils.mock_record_keepalive
class ChangeNodeSecureBootTestCase(mgr_utils.ServiceSetUpMixin,
db_base.DbTestCase):
@mock.patch.object(fake.FakeManagement, 'set_secure_boot_state',
autospec=True)
@mock.patch.object(fake.FakeManagement, 'get_secure_boot_state',
autospec=True)
def test_change_node_secure_boot_valid(self, get_boot_mock, set_boot_mock):
# Test change_node_secure_boot including integration with
# conductor.utils.node_change_secure_boot
get_boot_mock.side_effect = [False, # before setting
True] # after setting
initial_state = False
node = obj_utils.create_test_node(self.context,
driver='fake-hardware',
secure_boot=initial_state)
self._start_service()
target_state = True
self.service.change_node_secure_boot(self.context,
node.uuid,
target_state)
self._stop_service()
set_boot_mock.assert_called_once_with(mock.ANY, mock.ANY,
target_state)
self.assertEqual(get_boot_mock.call_count, 1)
# Call once before setting to see if it's required
node.refresh()
self.assertEqual(target_state, node.secure_boot)
self.assertIsNone(node.last_error)
# Verify the reservation has been cleared by
# background task's link callback.
self.assertIsNone(node.reservation)
@mock.patch.object(fake.FakeManagement, 'set_secure_boot_state',
autospec=True)
@mock.patch.object(fake.FakeManagement, 'get_secure_boot_state',
autospec=True)
def test_change_node_secure_boot_existing(self, get_boot_mock,
set_boot_mock):
# Test change_node_secure_boot including integration with
# conductor.utils.node_change_secure_boot when target==current
get_boot_mock.return_value = False
node = obj_utils.create_test_node(self.context,
driver='fake-hardware',
secure_boot=False)
self._start_service()
self.service.change_node_secure_boot(self.context,
node.uuid,
False)
self._stop_service()
set_boot_mock.assert_not_called()
self.assertEqual(get_boot_mock.call_count, 1)
# Called once before setting to see if it's even required
node.refresh()
self.assertEqual(False, node.secure_boot)
self.assertIsNone(node.last_error)
# Verify the reservation has been cleared by
# background task's link callback.
self.assertIsNone(node.reservation)
@mock.patch.object(conductor_utils, 'node_change_secure_boot',
autospec=True)
def test_change_node_secure_boot_node_already_locked(self, ncbm_mock):
# Test change_node_secure_boot with mocked
# conductor.utils.node_change_secure_boot.
fake_reservation = 'fake-reserv'
node = obj_utils.create_test_node(self.context,
driver='fake-hardware',
secure_boot=False,
reservation=fake_reservation)
self._start_service()
exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.change_node_secure_boot,
self.context,
node.uuid,
False)
# Compare true exception hidden by @messaging.expected_exceptions
self.assertEqual(exception.NodeLocked, exc.exc_info[0])
# In this test worker should not be spawned, but waiting to make sure
# the below perform_mock assertion is valid.
self._stop_service()
self.assertFalse(ncbm_mock.called, 'node_change_secure_boot has been '
'unexpectedly called.')
# Verify existing reservation wasn't broken.
node.refresh()
self.assertEqual(fake_reservation, node.reservation)
def test_change_node_secure_boot_worker_pool_full(self):
# Test change_node_secure_boot including integration with
# conductor.utils.change_node_secure_boot.
initial_state = False
node = obj_utils.create_test_node(self.context, driver='fake-hardware',
secure_boot=initial_state)
self._start_service()
with mock.patch.object(self.service,
'_spawn_worker', autospec=True) as spawn_mock:
spawn_mock.side_effect = exception.NoFreeConductorWorker()
exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.change_node_secure_boot,
self.context,
node.uuid,
True)
# Compare true exception hidden by @messaging.expected_exceptions
self.assertEqual(exception.NoFreeConductorWorker, exc.exc_info[0])
spawn_mock.assert_called_once_with(mock.ANY, mock.ANY,
mock.ANY)
node.refresh()
self.assertEqual(initial_state, node.secure_boot)
self.assertIsNotNone(node.last_error)
# Verify the picked reservation has been cleared due to full pool.
self.assertIsNone(node.reservation)
@mock.patch.object(fake.FakeManagement, 'set_secure_boot_state',
autospec=True)
@mock.patch.object(fake.FakeManagement, 'get_secure_boot_state',
autospec=True)
def test_change_node_secure_boot_exception_in_background_task(
self, get_boot_mock, set_boot_mock):
# Test change_node_secure_boot including integration with
# conductor.utils.node_change_secure_boot.
initial_state = False
node = obj_utils.create_test_node(self.context, driver='fake-hardware',
secure_boot=initial_state)
self._start_service()
get_boot_mock.return_value = False
new_state = True
set_boot_mock.side_effect = exception.UnsupportedDriverExtension(
driver=fake, extension='set_secure_boot_state')
self.service.change_node_secure_boot(self.context,
node.uuid,
new_state)
self._stop_service()
# Call once before setting to see if it was required
self.assertEqual(get_boot_mock.call_count, 1)
set_boot_mock.assert_called_once_with(mock.ANY, mock.ANY,
new_state)
node.refresh()
self.assertEqual(initial_state, node.secure_boot)
self.assertIsNotNone(node.last_error)
# Verify the reservation has been cleared by background task's
# link callback despite exception in background task.
self.assertIsNone(node.reservation)
@mock.patch.object(fake.FakeManagement, 'validate', autospec=True)
def test_change_node_secure_boot_validate_fail(self, validate_mock):
# Test change_node_power_state where task.driver.management.validate
# fails
initial_state = True
node = obj_utils.create_test_node(self.context,
driver='fake-hardware',
secure_boot=initial_state)
self._start_service()
validate_mock.side_effect = exception.InvalidParameterValue(
'wrong management driver info')
new_state = False
exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.change_node_secure_boot,
self.context,
node.uuid,
new_state)
self.assertEqual(exception.InvalidParameterValue, exc.exc_info[0])
node.refresh()
validate_mock.assert_called_once_with(mock.ANY, mock.ANY)
self.assertEqual(initial_state, node.secure_boot)
self.assertIsNone(node.last_error)
@mock.patch.object(fake.FakeManagement, 'set_secure_boot_state',
autospec=True)
@mock.patch.object(fake.FakeManagement, 'get_secure_boot_state',
autospec=True)
def test_change_node_secure_boot_exception_getting_current(self,
get_boot_mock,
set_boot_mock):
# Test change_node_secure_boot smooth opertion when
# get_secure_boot_state raises an exception
initial_state = False
node = obj_utils.create_test_node(self.context, driver='fake-hardware',
secure_boot=initial_state)
self._start_service()
get_boot_mock.side_effect = exception.UnsupportedDriverExtension(
driver=fake, extension='get_secure_boot_state')
new_state = True
self.service.change_node_secure_boot(self.context,
node.uuid,
new_state)
self._stop_service()
# Call once before setting to see if it is required
self.assertEqual(get_boot_mock.call_count, 1)
set_boot_mock.assert_called_once_with(mock.ANY, mock.ANY,
new_state)
node.refresh()
self.assertEqual(new_state, node.secure_boot)
self.assertIsNone(node.last_error)
# Verify the reservation has been cleared by
# background task's link callback.
self.assertIsNone(node.reservation)
@mgr_utils.mock_record_keepalive
class CreateNodeTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
def test_create_node(self):

View File

@ -26,6 +26,7 @@ import oslo_messaging as messaging
from oslo_messaging import _utils as messaging_utils
from ironic.common import boot_devices
from ironic.common import boot_modes
from ironic.common import components
from ironic.common import exception
from ironic.common import indicator_states
@ -301,6 +302,20 @@ class RPCAPITestCase(db_base.DbTestCase):
node_id=self.fake_node['uuid'],
new_state=states.POWER_ON)
def test_change_node_boot_mode(self):
self._test_rpcapi('change_node_boot_mode',
'call',
version='1.55',
node_id=self.fake_node['uuid'],
new_state=boot_modes.LEGACY_BIOS)
def test_change_node_secure_boot(self):
self._test_rpcapi('change_node_secure_boot',
'call',
version='1.55',
node_id=self.fake_node['uuid'],
new_state=True)
def test_vendor_passthru(self):
self._test_rpcapi('vendor_passthru',
'call',

View File

@ -7,4 +7,4 @@ features:
If underlying driver does not support detecting these, they shall be
populated with null values.
These fields are also available under a node's states endpoint:
* ``/v1/nodes/{node_ident}/states``
``/v1/nodes/{node_ident}/states``

View File

@ -0,0 +1,13 @@
---
features:
- |
Adds endpoints to change boot mode and secure boot state of node.
* ``PUT /v1/nodes/{node_ident}/states/boot_mode``
* ``PUT /v1/nodes/{node_ident}/states/secure_boot``
The API will respond with 202 (Accepted) on validating the request
and accepting to process it. Changes occur asynchronously in a
background task. The user can then poll the states endpoint
``/v1/nodes/{node_ident}/states`` for observing current status of the
requested change.