From 13f61e674ee014d2880250b669d5f68175bf0816 Mon Sep 17 00:00:00 2001 From: Derek Higgins Date: Fri, 8 Nov 2024 10:57:55 +0000 Subject: [PATCH] Allow setting of disable_power_off via API Change-Id: I1555878ba847805fddaf0d7a2a4babe50acfa674 --- .../baremetal-api-v1-node-management.inc | 3 + api-ref/source/baremetal-api-v1-nodes.inc | 13 +++- api-ref/source/parameters.yaml | 18 ++++++ .../contributor/webapi-version-history.rst | 5 ++ ironic/api/controllers/v1/node.py | 7 ++- ironic/api/controllers/v1/utils.py | 3 +- ironic/api/controllers/v1/versions.py | 4 +- ironic/common/policy.py | 10 ++++ ironic/common/release_mappings.py | 2 +- .../unit/api/controllers/v1/test_node.py | 60 ++++++++++++++++++- ...isable_power_off_api-a8fa90405a9261e3.yaml | 9 +++ 11 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/disable_power_off_api-a8fa90405a9261e3.yaml diff --git a/api-ref/source/baremetal-api-v1-node-management.inc b/api-ref/source/baremetal-api-v1-node-management.inc index d3b485324a..46ba106fce 100644 --- a/api-ref/source/baremetal-api-v1-node-management.inc +++ b/api-ref/source/baremetal-api-v1-node-management.inc @@ -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: diff --git a/api-ref/source/baremetal-api-v1-nodes.inc b/api-ref/source/baremetal-api-v1-nodes.inc index f55b5abfeb..94c6bcb6fb 100644 --- a/api-ref/source/baremetal-api-v1-nodes.inc +++ b/api-ref/source/baremetal-api-v1-nodes.inc @@ -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:** diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 6e3cafc671..421b8809b3 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -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 diff --git a/doc/source/contributor/webapi-version-history.rst b/doc/source/contributor/webapi-version-history.rst index d5057bade1..79c3e03810 100644 --- a/doc/source/contributor/webapi-version-history.rst +++ b/doc/source/contributor/webapi-version-history.rst @@ -2,6 +2,11 @@ REST API Version History ======================== +1.95 (Epoxy) +----------------------- + +Add support to set/unset disable_power_off on nodes. + 1.94 (Epoxy) ----------------------- diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py index 0dde80f1fe..e585423a0a 100644 --- a/ironic/api/controllers/v1/node.py +++ b/ironic/api/controllers/v1/node.py @@ -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', @@ -1551,6 +1554,7 @@ def _get_fields_for_node_query(fields=None): 'conductor_group', 'console_enabled', 'console_interface', + 'disable_power_off', 'deploy_interface', 'deploy_step', 'description', @@ -3036,7 +3040,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 diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py index 556dd59280..8108a52fdd 100644 --- a/ironic/api/controllers/v1/utils.py +++ b/ironic/api/controllers/v1/utils.py @@ -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: diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py index 23cdaeab61..67a6e0ca42 100644 --- a/ironic/api/controllers/v1/versions.py +++ b/ironic/api/controllers/v1/versions.py @@ -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) diff --git a/ironic/common/policy.py b/ironic/common/policy.py index ea890c73ba..0d0b3ed71e 100644 --- a/ironic/common/policy.py +++ b/ironic/common/policy.py @@ -1061,6 +1061,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, diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py index d0d7b3b1e1..aada98e899 100644 --- a/ironic/common/release_mappings.py +++ b/ironic/common/release_mappings.py @@ -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'], diff --git a/ironic/tests/unit/api/controllers/v1/test_node.py b/ironic/tests/unit/api/controllers/v1/test_node.py index 006e55f76d..12c50d87d4 100644 --- a/ironic/tests/unit/api/controllers/v1/test_node.py +++ b/ironic/tests/unit/api/controllers/v1/test_node.py @@ -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): diff --git a/releasenotes/notes/disable_power_off_api-a8fa90405a9261e3.yaml b/releasenotes/notes/disable_power_off_api-a8fa90405a9261e3.yaml new file mode 100644 index 0000000000..b6a826e1b2 --- /dev/null +++ b/releasenotes/notes/disable_power_off_api-a8fa90405a9261e3.yaml @@ -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.