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:
parent
9f32ceda1a
commit
bc95c92f7c
@ -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
|
||||||
=======================
|
=======================
|
||||||
|
@ -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.
|
||||||
|
3
api-ref/source/samples/node-set-boot-mode-bios.json
Normal file
3
api-ref/source/samples/node-set-boot-mode-bios.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"target": "bios"
|
||||||
|
}
|
3
api-ref/source/samples/node-set-boot-mode-uefi.json
Normal file
3
api-ref/source/samples/node-set-boot-mode-uefi.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"target": "uefi"
|
||||||
|
}
|
3
api-ref/source/samples/node-set-secure-boot-off.json
Normal file
3
api-ref/source/samples/node-set-secure-boot-off.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"target": false
|
||||||
|
}
|
3
api-ref/source/samples/node-set-secure-boot-on.json
Normal file
3
api-ref/source/samples/node-set-secure-boot-on.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"target": true
|
||||||
|
}
|
@ -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``
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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'],
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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',
|
||||||
|
@ -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``
|
||||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user