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``
or ``service_steps`` dictionary.
.. versionadded:: 1.95
Added the ability to set/unset ``disable_power_off`` on a node.
Normal response code: 202
Error codes:

View File

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

View File

@ -894,6 +894,15 @@ description:
in: body
required: true
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:
description: |
If set to ``true``, the ironic-python-agent ramdisk will not be booted for
@ -1683,6 +1692,15 @@ req_description:
in: body
required: false
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:
description: |
Whether to boot ramdisk while using a runbook for cleaning or servicing

View File

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

View File

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

View File

@ -877,7 +877,8 @@ VERSIONED_FIELDS = {
'shard': versions.MINOR_82_NODE_SHARD,
'parent_node': versions.MINOR_83_PARENT_CHILD_NODES,
'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:

View File

@ -132,6 +132,7 @@ BASE_VERSION = 1
# v1.92: Add runbooks API
# v1.93: Add GET API for virtual media
# v1.94: Add node name support for port creation
# v1.95: Add node support for disable_power_off
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@ -228,6 +229,7 @@ MINOR_91_DOT_JSON = 91
MINOR_92_RUNBOOKS = 92
MINOR_93_GET_VMEDIA = 93
MINOR_94_PORT_NODENAME = 94
MINOR_95_DISABLE_POWER_OFF = 95
# When adding another version, update:
# - MINOR_MAX_VERSION
@ -235,7 +237,7 @@ MINOR_94_PORT_NODENAME = 94
# explanation of what changed in the new version
# - 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
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -1052,6 +1052,16 @@ node_policies = [
'the API clients.',
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(
name='baremetal:node:firmware:get',
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
# the release as a separate block of text, like above.
'master': {
'api': '1.94',
'api': '1.95',
'rpc': '1.61',
'objects': {
'Allocation': ['1.1'],

View File

@ -145,6 +145,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertNotIn('lessee', data['nodes'][0])
self.assertNotIn('network_data', 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_policy', autospec=True)
@ -220,6 +221,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertNotIn('allocation_id', data)
self.assertIn('allocation_uuid', data)
self.assertIn('service_step', data)
self.assertIn('disable_power_off', data)
def test_get_one_configdrive_dict(self):
fake_instance_info = {
@ -518,6 +520,30 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertEqual(data['boot_mode'], 'uefi')
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):
node = obj_utils.create_test_node(self.context,
chassis_id=self.chassis.id)
@ -831,6 +857,14 @@ class TestListNodes(test_api_base.BaseApiTest):
token_value = response['driver_internal_info']['agent_secret_token']
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):
node = obj_utils.create_test_node(self.context,
chassis_id=self.chassis.id)
@ -873,6 +907,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertIn('retired', data['nodes'][0])
self.assertIn('retired_reason', data['nodes'][0])
self.assertIn('network_data', data['nodes'][0])
self.assertIn('disable_power_off', data['nodes'][0])
def test_detail_snmpv3(self):
driver_info = {
@ -931,6 +966,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertIn('retired', data['nodes'][0])
self.assertIn('retired_reason', data['nodes'][0])
self.assertIn('network_data', data['nodes'][0])
self.assertIn('disable_power_off', data['nodes'][0])
def test_detail_instance_uuid(self):
instance_uuid = '6eccd391-961c-4da5-b3c5-e2fa5cfbbd9d'
@ -951,7 +987,8 @@ class TestListNodes(test_api_base.BaseApiTest):
'network_interface', 'resource_class', 'owner', 'lessee',
'storage_interface', 'traits', 'automated_clean',
'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:
@ -1045,6 +1082,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertIn('retired', data['nodes'][0])
self.assertIn('retired_reason', data['nodes'][0])
self.assertIn('network_data', data['nodes'][0])
self.assertIn('disable_power_off', data['nodes'][0])
def test_detail_query_false(self):
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(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):

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.