Merge "Allow setting of disable_power_off via API"

This commit is contained in:
Zuul 2024-12-02 12:25:06 +00:00 committed by Gerrit Code Review
commit 3421bb614a
11 changed files with 128 additions and 6 deletions

View File

@ -460,6 +460,9 @@ detailed documentation of the Ironic State Machine is available
approved for the given node, as an alternative to providing ``clean_steps`` approved for the given node, as an alternative to providing ``clean_steps``
or ``service_steps`` dictionary. or ``service_steps`` dictionary.
.. versionadded:: 1.95
Added the ability to set/unset ``disable_power_off`` on a node.
Normal response code: 202 Normal response code: 202
Error codes: Error codes:

View File

@ -119,6 +119,9 @@ supplied when the Node is created, or the resource may be updated later.
.. versionadded: 1.83 .. versionadded: 1.83
Introduced the ``parent_node`` field. Introduced the ``parent_node`` field.
.. versionadded: 1.95
Introduced the ``disable_power_off`` field.
Normal response codes: 201 Normal response codes: 201
Error codes: 400,403,406 Error codes: 400,403,406
@ -132,6 +135,7 @@ Request
- conductor_group: req_conductor_group - conductor_group: req_conductor_group
- console_interface: req_console_interface - console_interface: req_console_interface
- deploy_interface: req_deploy_interface - deploy_interface: req_deploy_interface
- disable_power_off: req_disable_power_off
- driver_info: req_driver_info - driver_info: req_driver_info
- driver: req_driver_name - driver: req_driver_name
- extra: req_extra - extra: req_extra
@ -178,7 +182,7 @@ and any defaults added for non-specified fields. Most fields default to "null"
or "". or "".
The list and example below are representative of the response as of API The list and example below are representative of the response as of API
microversion 1.81. microversion 1.95.
.. rest_parameters:: parameters.yaml .. rest_parameters:: parameters.yaml
@ -239,6 +243,7 @@ microversion 1.81.
- network_data: network_data - network_data: network_data
- retired: retired - retired: retired
- retired_reason: retired_reason - retired_reason: retired_reason
- disable_power_off: disable_power_off
**Example JSON representation of a Node:** **Example JSON representation of a Node:**
@ -504,6 +509,7 @@ Response
- inspection_finished_at: inspection_finished_at - inspection_finished_at: inspection_finished_at
- created_at: created_at - created_at: created_at
- updated_at: updated_at - updated_at: updated_at
- disable_power_off: disable_power_off
**Example detailed list of Nodes:** **Example detailed list of Nodes:**
@ -562,6 +568,9 @@ only the specified set.
.. versionadded:: 1.83 .. versionadded:: 1.83
Introduced the ``parent_node`` field. Introduced the ``parent_node`` field.
.. versionadded:: 1.95
Introduced the ``disable_power_off`` field.
Normal response codes: 200 Normal response codes: 200
Error codes: 400,403,404,406 Error codes: 400,403,404,406
@ -632,6 +641,7 @@ Response
- conductor: conductor - conductor: conductor
- allocation_uuid: allocation_uuid - allocation_uuid: allocation_uuid
- network_data: network_data - network_data: network_data
- disable_power_off: disable_power_off
**Example JSON representation of a Node:** **Example JSON representation of a Node:**
@ -733,6 +743,7 @@ Response
- conductor: conductor - conductor: conductor
- allocation_uuid: allocation_uuid - allocation_uuid: allocation_uuid
- network_data: network_data - network_data: network_data
- disable_power_off: disable_power_off
**Example JSON representation of a Node:** **Example JSON representation of a Node:**

View File

@ -894,6 +894,15 @@ description:
in: body in: body
required: true required: true
type: string type: string
disable_power_off:
description: |
If set to true, power off for the node is explicitly disabled, instead, a
reboot will be used in place of power on/off. Additionally, when possible,
the node will be disabled (i.e., its API agent will be rendered unusable
and network configuration will be removed) instead of being powered off.
in: body
required: false
type: boolean
disable_ramdisk: disable_ramdisk:
description: | description: |
If set to ``true``, the ironic-python-agent ramdisk will not be booted for If set to ``true``, the ironic-python-agent ramdisk will not be booted for
@ -1683,6 +1692,15 @@ req_description:
in: body in: body
required: false required: false
type: string type: string
req_disable_power_off:
description: |
If set to ``true``, power off for the node is explicitly disabled, instead, a
reboot will be used in place of power on/off. Additionally, when possible,
the node will be disabled (i.e., its API agent will be rendered unusable
and network configuration will be removed) instead of being powered off.
in: body
required: false
type: boolean
req_disable_ramdisk: req_disable_ramdisk:
description: | description: |
Whether to boot ramdisk while using a runbook for cleaning or servicing Whether to boot ramdisk while using a runbook for cleaning or servicing

View File

@ -2,6 +2,11 @@
REST API Version History REST API Version History
======================== ========================
1.95 (Epoxy)
-----------------------
Add support to set/unset disable_power_off on nodes.
1.94 (Epoxy) 1.94 (Epoxy)
----------------------- -----------------------

View File

@ -177,6 +177,7 @@ def node_schema():
'deploy_interface': {'type': ['string', 'null']}, 'deploy_interface': {'type': ['string', 'null']},
'description': {'type': ['string', 'null'], 'description': {'type': ['string', 'null'],
'maxLength': _NODE_DESCRIPTION_MAX_LENGTH}, 'maxLength': _NODE_DESCRIPTION_MAX_LENGTH},
'disable_power_off': {'type': ['string', 'boolean', 'null']},
'driver': {'type': 'string'}, 'driver': {'type': 'string'},
'driver_info': {'type': ['object', 'null']}, 'driver_info': {'type': ['object', 'null']},
'extra': {'type': ['object', 'null']}, 'extra': {'type': ['object', 'null']},
@ -229,6 +230,7 @@ NODE_VALIDATE_EXTRA = args.dict_valid(
automated_clean=args.boolean, automated_clean=args.boolean,
chassis_uuid=args.uuid, chassis_uuid=args.uuid,
console_enabled=args.boolean, console_enabled=args.boolean,
disable_power_off=args.boolean,
instance_uuid=args.uuid, instance_uuid=args.uuid,
protected=args.boolean, protected=args.boolean,
maintenance=args.boolean, maintenance=args.boolean,
@ -270,6 +272,7 @@ PATCH_ALLOWED_FIELDS = [
'console_interface', 'console_interface',
'deploy_interface', 'deploy_interface',
'description', 'description',
'disable_power_off',
'driver', 'driver',
'driver_info', 'driver_info',
'extra', 'extra',
@ -1565,6 +1568,7 @@ def _get_fields_for_node_query(fields=None):
'conductor_group', 'conductor_group',
'console_enabled', 'console_enabled',
'console_interface', 'console_interface',
'disable_power_off',
'deploy_interface', 'deploy_interface',
'deploy_step', 'deploy_step',
'description', 'description',
@ -3050,7 +3054,8 @@ class NodesController(rest.RestController):
('/name', 'baremetal:node:update:name'), ('/name', 'baremetal:node:update:name'),
('/retired', 'baremetal:node:update:retired'), ('/retired', 'baremetal:node:update:retired'),
('/shard', 'baremetal:node:update:shard'), ('/shard', 'baremetal:node:update:shard'),
('/parent_node', 'baremetal:node:update:parent_node') ('/parent_node', 'baremetal:node:update:parent_node'),
('/disable_power_off', 'baremetal:node:update:disable_power_off')
) )
for p in patch: for p in patch:
# Process general direct path to policy map # Process general direct path to policy map

View File

@ -877,7 +877,8 @@ VERSIONED_FIELDS = {
'shard': versions.MINOR_82_NODE_SHARD, 'shard': versions.MINOR_82_NODE_SHARD,
'parent_node': versions.MINOR_83_PARENT_CHILD_NODES, 'parent_node': versions.MINOR_83_PARENT_CHILD_NODES,
'firmware_interface': versions.MINOR_86_FIRMWARE_INTERFACE, 'firmware_interface': versions.MINOR_86_FIRMWARE_INTERFACE,
'service_step': versions.MINOR_87_SERVICE 'service_step': versions.MINOR_87_SERVICE,
'disable_power_off': versions.MINOR_95_DISABLE_POWER_OFF,
} }
for field in V31_FIELDS: for field in V31_FIELDS:

View File

@ -132,6 +132,7 @@ BASE_VERSION = 1
# v1.92: Add runbooks API # v1.92: Add runbooks API
# v1.93: Add GET API for virtual media # v1.93: Add GET API for virtual media
# v1.94: Add node name support for port creation # v1.94: Add node name support for port creation
# v1.95: Add node support for disable_power_off
MINOR_0_JUNO = 0 MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1 MINOR_1_INITIAL_VERSION = 1
@ -228,6 +229,7 @@ MINOR_91_DOT_JSON = 91
MINOR_92_RUNBOOKS = 92 MINOR_92_RUNBOOKS = 92
MINOR_93_GET_VMEDIA = 93 MINOR_93_GET_VMEDIA = 93
MINOR_94_PORT_NODENAME = 94 MINOR_94_PORT_NODENAME = 94
MINOR_95_DISABLE_POWER_OFF = 95
# When adding another version, update: # When adding another version, update:
# - MINOR_MAX_VERSION # - MINOR_MAX_VERSION
@ -235,7 +237,7 @@ MINOR_94_PORT_NODENAME = 94
# 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_94_PORT_NODENAME MINOR_MAX_VERSION = MINOR_95_DISABLE_POWER_OFF
# 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

@ -1052,6 +1052,16 @@ node_policies = [
'the API clients.', 'the API clients.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}], operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
), ),
policy.DocumentedRuleDefault(
name='baremetal:node:update:disable_power_off',
check_str=SYSTEM_ADMIN,
scope_types=['system', 'project'],
description='Governs if power off can be disabled via the API '
'clients.',
operations=[
{'path': '/nodes/{node_ident}', 'method': 'PATCH'}
],
),
policy.DocumentedRuleDefault( policy.DocumentedRuleDefault(
name='baremetal:node:firmware:get', name='baremetal:node:firmware:get',
check_str=SYSTEM_OR_PROJECT_READER, check_str=SYSTEM_OR_PROJECT_READER,

View File

@ -776,7 +776,7 @@ RELEASE_MAPPING = {
# make it below. To release, we will preserve a version matching # make it below. To release, we will preserve a version matching
# the release as a separate block of text, like above. # the release as a separate block of text, like above.
'master': { 'master': {
'api': '1.94', 'api': '1.95',
'rpc': '1.61', 'rpc': '1.61',
'objects': { 'objects': {
'Allocation': ['1.1'], 'Allocation': ['1.1'],

View File

@ -145,6 +145,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertNotIn('lessee', data['nodes'][0]) self.assertNotIn('lessee', data['nodes'][0])
self.assertNotIn('network_data', data['nodes'][0]) self.assertNotIn('network_data', data['nodes'][0])
self.assertNotIn('service_steps', data['nodes'][0]) self.assertNotIn('service_steps', data['nodes'][0])
self.assertNotIn('disable_power_off', data['nodes'][0])
@mock.patch.object(policy, 'check', autospec=True) @mock.patch.object(policy, 'check', autospec=True)
@mock.patch.object(policy, 'check_policy', autospec=True) @mock.patch.object(policy, 'check_policy', autospec=True)
@ -220,6 +221,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertNotIn('allocation_id', data) self.assertNotIn('allocation_id', data)
self.assertIn('allocation_uuid', data) self.assertIn('allocation_uuid', data)
self.assertIn('service_step', data) self.assertIn('service_step', data)
self.assertIn('disable_power_off', data)
def test_get_one_configdrive_dict(self): def test_get_one_configdrive_dict(self):
fake_instance_info = { fake_instance_info = {
@ -518,6 +520,30 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertEqual(data['boot_mode'], 'uefi') self.assertEqual(data['boot_mode'], 'uefi')
self.assertEqual(data['secure_boot'], value) self.assertEqual(data['secure_boot'], value)
def test_node_disable_power_off_hidden_in_lower_version(self):
self._test_node_field_hidden_in_lower_version('disable_power_off',
'1.94', '1.95')
def test_node_disable_power_off_null_field(self):
node = obj_utils.create_test_node(self.context, disable_power_off=None)
data = self.get_json('/nodes/%s' % node.uuid,
headers={api_base.Version.string: '1.95'})
# Default for disable_power_off is False (so not Null)
self.assertIs(data['disable_power_off'], False)
def test_node_disable_power_off_true_field(self):
node = obj_utils.create_test_node(self.context, disable_power_off=True)
data = self.get_json('/nodes/%s' % node.uuid,
headers={api_base.Version.string: '1.95'})
self.assertEqual(data['disable_power_off'], True)
def test_node_disable_power_off_false_field(self):
node = obj_utils.create_test_node(self.context,
disable_power_off=False)
data = self.get_json('/nodes/%s' % node.uuid,
headers={api_base.Version.string: '1.95'})
self.assertEqual(data['disable_power_off'], False)
def test_get_one_custom_fields(self): def test_get_one_custom_fields(self):
node = obj_utils.create_test_node(self.context, node = obj_utils.create_test_node(self.context,
chassis_id=self.chassis.id) chassis_id=self.chassis.id)
@ -831,6 +857,14 @@ class TestListNodes(test_api_base.BaseApiTest):
token_value = response['driver_internal_info']['agent_secret_token'] token_value = response['driver_internal_info']['agent_secret_token']
self.assertEqual('******', token_value) self.assertEqual('******', token_value)
def test_get_disable_power_off_fields(self):
node = obj_utils.create_test_node(self.context,
disable_power_off=True)
fields = 'disable_power_off'
response = self.get_json('/nodes/%s?fields=%s' % (node.uuid, fields),
headers={api_base.Version.string: '1.95'})
self.assertIn('disable_power_off', response)
def test_detail(self): def test_detail(self):
node = obj_utils.create_test_node(self.context, node = obj_utils.create_test_node(self.context,
chassis_id=self.chassis.id) chassis_id=self.chassis.id)
@ -873,6 +907,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertIn('retired', data['nodes'][0]) self.assertIn('retired', data['nodes'][0])
self.assertIn('retired_reason', data['nodes'][0]) self.assertIn('retired_reason', data['nodes'][0])
self.assertIn('network_data', data['nodes'][0]) self.assertIn('network_data', data['nodes'][0])
self.assertIn('disable_power_off', data['nodes'][0])
def test_detail_snmpv3(self): def test_detail_snmpv3(self):
driver_info = { driver_info = {
@ -931,6 +966,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertIn('retired', data['nodes'][0]) self.assertIn('retired', data['nodes'][0])
self.assertIn('retired_reason', data['nodes'][0]) self.assertIn('retired_reason', data['nodes'][0])
self.assertIn('network_data', data['nodes'][0]) self.assertIn('network_data', data['nodes'][0])
self.assertIn('disable_power_off', data['nodes'][0])
def test_detail_instance_uuid(self): def test_detail_instance_uuid(self):
instance_uuid = '6eccd391-961c-4da5-b3c5-e2fa5cfbbd9d' instance_uuid = '6eccd391-961c-4da5-b3c5-e2fa5cfbbd9d'
@ -951,7 +987,8 @@ class TestListNodes(test_api_base.BaseApiTest):
'network_interface', 'resource_class', 'owner', 'lessee', 'network_interface', 'resource_class', 'owner', 'lessee',
'storage_interface', 'traits', 'automated_clean', 'storage_interface', 'traits', 'automated_clean',
'conductor_group', 'protected', 'protected_reason', 'conductor_group', 'protected', 'protected_reason',
'retired', 'retired_reason', 'allocation_uuid', 'network_data' 'retired', 'retired_reason', 'allocation_uuid', 'network_data',
'disable_power_off'
] ]
for field in expected_fields: for field in expected_fields:
@ -1045,6 +1082,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertIn('retired', data['nodes'][0]) self.assertIn('retired', data['nodes'][0])
self.assertIn('retired_reason', data['nodes'][0]) self.assertIn('retired_reason', data['nodes'][0])
self.assertIn('network_data', data['nodes'][0]) self.assertIn('network_data', data['nodes'][0])
self.assertIn('disable_power_off', data['nodes'][0])
def test_detail_query_false(self): def test_detail_query_false(self):
obj_utils.create_test_node(self.context) obj_utils.create_test_node(self.context)
@ -5207,6 +5245,26 @@ class TestPost(test_api_base.BaseApiTest):
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int) self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
def test_create_node_disable_power_off(self):
ndict = test_api_utils.post_get_test_node(
disable_power_off=True)
response = self.post_json('/nodes', ndict,
headers={api_base.Version.string:
str(api_v1.max_version())})
self.assertEqual(http_client.CREATED, response.status_int)
result = self.get_json('/nodes/%s' % ndict['uuid'],
headers={api_base.Version.string:
str(api_v1.max_version())})
self.assertEqual(True, result['disable_power_off'])
def test_create_node_disable_power_off_old_api_version(self):
headers = {api_base.Version.string: '1.94'}
ndict = test_api_utils.post_get_test_node(disable_power_off=True)
response = self.post_json('/nodes', ndict, headers=headers,
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
class TestDelete(test_api_base.BaseApiTest): class TestDelete(test_api_base.BaseApiTest):

View File

@ -0,0 +1,9 @@
---
features:
- |
Adds support for setting ``disable_power_off`` on node creation along with
set/unset ``disable_power_off`` on existing nodes.
If set to ``true``, power off for the node is explicitly disabled, instead, a
reboot will be used in place of power on/off. Additionally, when possible,
the node will be disabled (i.e., its API agent will be rendered unusable
and network configurationwill be removed) instead of being powered off.