diff --git a/api-ref/source/baremetal-api-v1-nodes.inc b/api-ref/source/baremetal-api-v1-nodes.inc index c25f9fa2a5..8c6b4274bc 100644 --- a/api-ref/source/baremetal-api-v1-nodes.inc +++ b/api-ref/source/baremetal-api-v1-nodes.inc @@ -326,6 +326,8 @@ Updates the information stored about a Node. Note that this endpoint can not be used to request state changes, which are managed through sub-resources. +API microversion 1.25 introduced the ability to unset a node's chassis UUID. + Normal response codes: 200 .. TODO: add error codes diff --git a/doc/source/dev/webapi-version-history.rst b/doc/source/dev/webapi-version-history.rst index cb284e1b25..a1265a9a34 100644 --- a/doc/source/dev/webapi-version-history.rst +++ b/doc/source/dev/webapi-version-history.rst @@ -2,6 +2,10 @@ REST API Version History ======================== +**1.25** + + Add possibility to unset chassis_uuid from a node. + **1.24** Added new endpoints '/v1/nodes//portgroups' and '/v1/portgroups//ports'. diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py index d57c5a947f..c14cdcfb00 100644 --- a/ironic/api/controllers/v1/node.py +++ b/ironic/api/controllers/v1/node.py @@ -893,8 +893,6 @@ class NodePatchType(types.JsonPatchType): _api_base = Node - _extra_non_removable_attrs = {'/chassis_uuid'} - @staticmethod def internal_attrs(): defaults = types.JsonPatchType.internal_attrs() @@ -1192,7 +1190,17 @@ class NodesController(rest.RestController): try: patch_val = getattr(node, field) except AttributeError: - # Ignore fields that aren't exposed in the API + # Ignore fields that aren't exposed in the API, except + # chassis_id. chassis_id would have been set (instead of + # chassis_uuid) if the node belongs to a chassis. This + # AttributeError is raised for chassis_id only if + # 1. the node doesn't belong to a chassis or + # 2. the node belonged to a chassis but is now being removed + # from the chassis. + if (field == "chassis_id" and rpc_node[field] is not None): + if not api_utils.allow_remove_chassis_uuid(): + raise exception.NotAcceptable() + rpc_node[field] = None continue if patch_val == wtypes.Unset: patch_val = None diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py index 91ead315cf..832f7f8a95 100644 --- a/ironic/api/controllers/v1/utils.py +++ b/ironic/api/controllers/v1/utils.py @@ -432,6 +432,16 @@ def allow_portgroups_subcontrollers(): versions.MINOR_24_PORTGROUPS_SUBCONTROLLERS) +def allow_remove_chassis_uuid(): + """Check if chassis_uuid can be removed from node. + + Version 1.25 of the API added support for chassis_uuid + removal + """ + return (pecan.request.version.minor >= + versions.MINOR_25_UNSET_CHASSIS_UUID) + + def get_controller_reserved_names(cls): """Get reserved names for a given controller. diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py index 82635e4d06..a029b22c7d 100644 --- a/ironic/api/controllers/v1/versions.py +++ b/ironic/api/controllers/v1/versions.py @@ -55,6 +55,7 @@ BASE_VERSION = 1 # v1.23: Add portgroup support. # v1.24: Add subcontrollers: node.portgroup, portgroup.ports. # Add port.portgroup_uuid field. +# v1.25: Add possibility to unset chassis_uuid from node. MINOR_0_JUNO = 0 MINOR_1_INITIAL_VERSION = 1 @@ -81,11 +82,12 @@ MINOR_21_RESOURCE_CLASS = 21 MINOR_22_LOOKUP_HEARTBEAT = 22 MINOR_23_PORTGROUPS = 23 MINOR_24_PORTGROUPS_SUBCONTROLLERS = 24 +MINOR_25_UNSET_CHASSIS_UUID = 25 # When adding another version, update MINOR_MAX_VERSION and also update # doc/source/dev/webapi-version-history.rst with a detailed explanation of # what the version has changed. -MINOR_MAX_VERSION = MINOR_24_PORTGROUPS_SUBCONTROLLERS +MINOR_MAX_VERSION = MINOR_25_UNSET_CHASSIS_UUID # String representations of the minor and maximum versions MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION) diff --git a/ironic/tests/unit/api/v1/test_nodes.py b/ironic/tests/unit/api/v1/test_nodes.py index b99deec315..917a952470 100644 --- a/ironic/tests/unit/api/v1/test_nodes.py +++ b/ironic/tests/unit/api/v1/test_nodes.py @@ -32,6 +32,7 @@ from ironic.api.controllers import base as api_base from ironic.api.controllers import v1 as api_v1 from ironic.api.controllers.v1 import node as api_node from ironic.api.controllers.v1 import utils as api_utils +from ironic.api.controllers.v1 import versions from ironic.common import boot_devices from ironic.common import exception from ironic.common import states @@ -1317,6 +1318,40 @@ class TestPatch(test_api_base.BaseApiTest): self.assertEqual('application/json', response.content_type) self.assertEqual(http_client.OK, response.status_code) + def test_remove_chassis_uuid(self): + self.mock_update_node.return_value = self.node + headers = {api_base.Version.string: "1.25"} + response = self.patch_json('/nodes/%s' % self.node.uuid, + [{'path': '/chassis_uuid', + 'op': 'remove'}], + headers=headers) + self.assertEqual('application/json', response.content_type) + self.assertEqual(http_client.OK, response.status_code) + + def test_remove_chassis_uuid_invalid_api_version(self): + self.mock_update_node.return_value = self.node + headers = {api_base.Version.string: "1.24"} + response = self.patch_json('/nodes/%s' % self.node.uuid, + [{'path': '/chassis_uuid', + 'op': 'remove'}], + headers=headers, + expect_errors=True) + self.assertEqual('application/json', response.content_type) + self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code) + self.assertTrue(response.json['error_message']) + + @mock.patch("pecan.request") + def test__update_changed_fields_remove_chassis_uuid(self, mock_pecan_req): + mock_pecan_req.version.minor = versions.MINOR_MAX_VERSION + controller = api_node.NodesController() + + node_dict = self.node.as_dict() + del node_dict['chassis_id'] + node_no_chassis = api_node.Node(**node_dict) + + controller._update_changed_fields(node_no_chassis, self.node) + self.assertIsNone(self.node.chassis_id) + def test_add_chassis_id(self): response = self.patch_json('/nodes/%s' % self.node.uuid, [{'path': '/chassis_id', diff --git a/ironic/tests/unit/api/v1/test_utils.py b/ironic/tests/unit/api/v1/test_utils.py index 5e507ff54d..a6bfc59b2d 100644 --- a/ironic/tests/unit/api/v1/test_utils.py +++ b/ironic/tests/unit/api/v1/test_utils.py @@ -299,6 +299,13 @@ class TestApiUtils(base.TestCase): mock_request.version.minor = 22 self.assertFalse(utils.allow_portgroups()) + @mock.patch.object(pecan, 'request', spec_set=['version']) + def test_allow_remove_chassis_uuid(self, mock_request): + mock_request.version.minor = 25 + self.assertTrue(utils.allow_remove_chassis_uuid()) + mock_request.version.minor = 24 + self.assertFalse(utils.allow_remove_chassis_uuid()) + class TestNodeIdent(base.TestCase): diff --git a/releasenotes/notes/add-chassis_uuid-removal-possibility-8b06341a91f7c676.yaml b/releasenotes/notes/add-chassis_uuid-removal-possibility-8b06341a91f7c676.yaml new file mode 100644 index 0000000000..aaa591e7cb --- /dev/null +++ b/releasenotes/notes/add-chassis_uuid-removal-possibility-8b06341a91f7c676.yaml @@ -0,0 +1,4 @@ +--- +features: + - Adds support for removing the chassis UUID associated with a node (via + PATCH /v1/nodes/). This is available starting with API version 1.25.