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 .. 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 Change Node Power State
======================= =======================

View File

@ -1754,6 +1754,13 @@ req_storage_interface:
in: body in: body
required: false required: false
type: string 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: req_target_power_state:
description: | description: |
If a power state transition has been requested, this field represents the If a power state transition has been requested, this field represents the
@ -1770,6 +1777,13 @@ req_target_raid_config:
in: body in: body
required: true required: true
type: JSON 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: req_uuid:
description: | description: |
The UUID for the resource. 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 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) 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`` * ``/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.controllers.v1 import volume
from ironic.api import method from ironic.api import method
from ironic.common import args from ironic.common import args
from ironic.common import boot_modes
from ironic.common import exception from ironic.common import exception
from ironic.common.i18n import _ from ironic.common.i18n import _
from ironic.common import policy from ironic.common import policy
@ -120,6 +121,9 @@ ALLOWED_TARGET_POWER_STATES = (ir_states.POWER_ON,
ir_states.SOFT_REBOOT, ir_states.SOFT_REBOOT,
ir_states.SOFT_POWER_OFF) ir_states.SOFT_POWER_OFF)
ALLOWED_TARGET_BOOT_MODES = (boot_modes.LEGACY_BIOS,
boot_modes.UEFI)
_NODE_DESCRIPTION_MAX_LENGTH = 4096 _NODE_DESCRIPTION_MAX_LENGTH = 4096
_NETWORK_DATA_SCHEMA = None _NETWORK_DATA_SCHEMA = None
@ -710,6 +714,8 @@ def node_states_convert(rpc_node):
class NodeStatesController(rest.RestController): class NodeStatesController(rest.RestController):
_custom_actions = { _custom_actions = {
'boot_mode': ['PUT'],
'secure_boot': ['PUT'],
'power': ['PUT'], 'power': ['PUT'],
'provision': ['PUT'], 'provision': ['PUT'],
'raid': ['PUT'], 'raid': ['PUT'],
@ -822,6 +828,107 @@ class NodeStatesController(rest.RestController):
url_args = '/'.join([node_ident, 'states']) url_args = '/'.join([node_ident, 'states'])
api.response.location = link.build_url('nodes', url_args) 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, def _do_provision_action(self, rpc_node, target, configdrive=None,
clean_steps=None, deploy_steps=None, clean_steps=None, deploy_steps=None,
rescue_password=None, disable_ramdisk=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.73: Add support for deploy and undeploy verbs
# v1.74: Add bios registry to /v1/nodes/{node}/bios/{setting} # v1.74: Add bios registry to /v1/nodes/{node}/bios/{setting}
# v1.75: Add boot_mode, secure_boot fields to node object. # 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_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1 MINOR_1_INITIAL_VERSION = 1
@ -190,6 +191,7 @@ MINOR_72_HEARTBEAT_STATUS = 72
MINOR_73_DEPLOY_UNDEPLOY_VERBS = 73 MINOR_73_DEPLOY_UNDEPLOY_VERBS = 73
MINOR_74_BIOS_REGISTRY = 74 MINOR_74_BIOS_REGISTRY = 74
MINOR_75_NODE_BOOT_MODE = 75 MINOR_75_NODE_BOOT_MODE = 75
MINOR_76_NODE_CHANGE_BOOT_MODE = 76
# When adding another version, update: # When adding another version, update:
# - MINOR_MAX_VERSION # - MINOR_MAX_VERSION
@ -197,7 +199,7 @@ MINOR_75_NODE_BOOT_MODE = 75
# explanation of what changed in the new version # explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api'] # - 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 # String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION) _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 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( policy.DocumentedRuleDefault(
name='baremetal:node:set_provision_state', name='baremetal:node:set_provision_state',
check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN, check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN,

View File

@ -371,8 +371,8 @@ RELEASE_MAPPING = {
} }
}, },
'master': { 'master': {
'api': '1.75', 'api': '1.76',
'rpc': '1.54', 'rpc': '1.55',
'objects': { 'objects': {
'Allocation': ['1.1'], 'Allocation': ['1.1'],
'BIOSSetting': ['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(rloo): This must be in sync with rpcapi.ConductorAPI's.
# NOTE(pas-ha): This also must be in sync with # NOTE(pas-ha): This also must be in sync with
# ironic.common.release_mappings.RELEASE_MAPPING['master'] # ironic.common.release_mappings.RELEASE_MAPPING['master']
RPC_API_VERSION = '1.54' RPC_API_VERSION = '1.55'
target = messaging.Target(version=RPC_API_VERSION) 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.spawn_after(self._spawn_worker, utils.node_power_action,
task, new_state, timeout=power_timeout) 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') @METRICS.timer('ConductorManager.vendor_passthru')
@messaging.expected_exceptions(exception.NoFreeConductorWorker, @messaging.expected_exceptions(exception.NoFreeConductorWorker,
exception.NodeLocked, exception.NodeLocked,

View File

@ -108,12 +108,13 @@ class ConductorAPI(object):
| 1.53 - Added disable_ramdisk to do_node_clean. | 1.53 - Added disable_ramdisk to do_node_clean.
| 1.54 - Added optional agent_status and agent_status_message to | 1.54 - Added optional agent_status and agent_status_message to
heartbeat heartbeat
| 1.55 - Added change_node_boot_mode
""" """
# NOTE(rloo): This must be in sync with manager.ConductorManager's. # NOTE(rloo): This must be in sync with manager.ConductorManager's.
# NOTE(pas-ha): This also must be in sync with # NOTE(pas-ha): This also must be in sync with
# ironic.common.release_mappings.RELEASE_MAPPING['master'] # ironic.common.release_mappings.RELEASE_MAPPING['master']
RPC_API_VERSION = '1.54' RPC_API_VERSION = '1.55'
def __init__(self, topic=None): def __init__(self, topic=None):
super(ConductorAPI, self).__init__() super(ConductorAPI, self).__init__()
@ -281,6 +282,46 @@ class ConductorAPI(object):
return cctxt.call(context, 'change_node_power_state', node_id=node_id, return cctxt.call(context, 'change_node_power_state', node_id=node_id,
new_state=new_state, timeout=timeout) 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, def vendor_passthru(self, context, node_id, driver_method, http_method,
info, topic=None): info, topic=None):
"""Receive requests for vendor-specific actions. """Receive requests for vendor-specific actions.

View File

@ -133,14 +133,22 @@ def node_set_boot_mode(task, mode):
return return
task.driver.management.validate(task) 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 supported_boot_modes and mode not in supported_boot_modes:
if mode not in boot_modes:
msg = _("Unsupported boot mode %(mode)s specified for " msg = _("Unsupported boot mode %(mode)s specified for "
"node %(node_id)s. Supported boot modes are: " "node %(node_id)s. Supported boot modes are: "
"%(modes)s") % {'mode': mode, "%(modes)s") % {'mode': mode,
'modes': ', '.join(boot_modes), 'modes': ', '.join(supported_boot_modes),
'node_id': task.node.uuid} 'node_id': task.node.uuid}
raise exception.InvalidParameterValue(msg) raise exception.InvalidParameterValue(msg)
@ -1473,3 +1481,95 @@ def node_cache_boot_mode(task):
"for node %(node)s", "for node %(node)s",
{'boot_mode': boot_mode, 'secure_boot': secure_boot, {'boot_mode': boot_mode, 'secure_boot': secure_boot,
'node': task.node.uuid}) '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 utils as api_utils
from ironic.api.controllers.v1 import versions from ironic.api.controllers.v1 import versions
from ironic.common import boot_devices from ironic.common import boot_devices
from ironic.common import boot_modes
from ironic.common import components from ironic.common import components
from ironic.common import driver_factory from ironic.common import driver_factory
from ironic.common import exception from ironic.common import exception
@ -5136,6 +5137,14 @@ class TestPut(test_api_base.BaseApiTest):
autospec=True) autospec=True)
self.mock_cnps = p.start() self.mock_cnps = p.start()
self.addCleanup(p.stop) 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', p = mock.patch.object(rpcapi.ConductorAPI, 'do_node_deploy',
autospec=True) autospec=True)
self.mock_dnd = p.start() self.mock_dnd = p.start()
@ -5301,6 +5310,147 @@ class TestPut(test_api_base.BaseApiTest):
{'target': 'not-supported'}, expect_errors=True) {'target': 'not-supported'}, expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, ret.status_code) 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): def test_power_change_when_being_cleaned(self):
for state in (states.CLEANING, states.CLEANWAIT): for state in (states.CLEANING, states.CLEANWAIT):
self.node.provision_state = state self.node.provision_state = state

View File

@ -412,6 +412,56 @@ nodes_states_power_put_observer:
assert_status: 403 assert_status: 403
deprecated: true 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: nodes_states_provision_put_admin:
path: '/v1/nodes/{node_ident}/states/provision' path: '/v1/nodes/{node_ident}/states/provision'
method: put method: put

View File

@ -863,6 +863,110 @@ third_party_admin_cannot_put_power_state_change:
body: *power_body body: *power_body
assert_status: 404 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 # Provision states
owner_admin_can_change_provision_state: owner_admin_can_change_provision_state:

View File

@ -378,6 +378,54 @@ nodes_states_power_put_reader:
body: *power_body body: *power_body
assert_status: 403 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: nodes_states_provision_put_admin:
path: '/v1/nodes/{node_ident}/states/provision' path: '/v1/nodes/{node_ident}/states/provision'
method: put method: put

View File

@ -34,6 +34,7 @@ from oslo_versionedobjects import fields
import tenacity import tenacity
from ironic.common import boot_devices from ironic.common import boot_devices
from ironic.common import boot_modes
from ironic.common import components from ironic.common import components
from ironic.common import driver_factory from ironic.common import driver_factory
from ironic.common import exception from ironic.common import exception
@ -447,6 +448,431 @@ class ChangeNodePowerStateTestCase(mgr_utils.ServiceSetUpMixin,
self.assertIsNone(node.last_error) 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 @mgr_utils.mock_record_keepalive
class CreateNodeTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): class CreateNodeTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
def test_create_node(self): 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 oslo_messaging import _utils as messaging_utils
from ironic.common import boot_devices from ironic.common import boot_devices
from ironic.common import boot_modes
from ironic.common import components from ironic.common import components
from ironic.common import exception from ironic.common import exception
from ironic.common import indicator_states from ironic.common import indicator_states
@ -301,6 +302,20 @@ class RPCAPITestCase(db_base.DbTestCase):
node_id=self.fake_node['uuid'], node_id=self.fake_node['uuid'],
new_state=states.POWER_ON) 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): def test_vendor_passthru(self):
self._test_rpcapi('vendor_passthru', self._test_rpcapi('vendor_passthru',
'call', 'call',

View File

@ -7,4 +7,4 @@ features:
If underlying driver does not support detecting these, they shall be If underlying driver does not support detecting these, they shall be
populated with null values. populated with null values.
These fields are also available under a node's states endpoint: 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.