diff --git a/api-ref/source/baremetal-api-v1-ports.inc b/api-ref/source/baremetal-api-v1-ports.inc index ede6624a89..60f630d69a 100644 --- a/api-ref/source/baremetal-api-v1-ports.inc +++ b/api-ref/source/baremetal-api-v1-ports.inc @@ -119,6 +119,9 @@ This method requires a Node UUID and the physical hardware address for the Port of ``vtep-logical-switch``, ``vtep-physical-switch`` and ``port_id`` to identify ovn vtep switches. +.. versionadded:: 1.94 + Added support to create ports passing in either the node name or UUID. + Normal response code: 201 Request @@ -126,7 +129,7 @@ Request .. rest_parameters:: parameters.yaml - - node_uuid: req_node_uuid + - node_ident: node_ident - address: req_port_address - portgroup_uuid: req_portgroup_uuid - name: req_port_name @@ -137,6 +140,9 @@ Request - is_smartnic: req_is_smartnic - uuid: req_uuid +.. note:: + Either `node_ident` or `node_uuid` is a valid parameter. + **Example Port creation request:** .. literalinclude:: samples/port-create-request.json diff --git a/api-ref/source/samples/port-create-request.json b/api-ref/source/samples/port-create-request.json index 3903185b30..5c2988c783 100644 --- a/api-ref/source/samples/port-create-request.json +++ b/api-ref/source/samples/port-create-request.json @@ -1,5 +1,5 @@ { - "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", + "node_ident": "6d85703a-565d-469a-96ce-30b6de53079d", "portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a", "name": "port1", "address": "11:11:11:11:11:11", diff --git a/doc/source/contributor/webapi-version-history.rst b/doc/source/contributor/webapi-version-history.rst index 2f1097d373..d5057bade1 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.94 (Epoxy) +----------------------- + +Add support to create ports passing in either the node name or UUID. + 1.92 (Dalmatian) ----------------------- @@ -10,9 +15,9 @@ nodes associated via traits and used in place of an explicit list of steps for manual cleaning or servicing, to enable self-service of maintenance items by project members. -* Adds a new REST API endpoint `/v1/runbooks/` with basic CRUD support. -* Extends the `/v1/nodes//states/provision` API to accept a runbook - identifier (name or UUID) instead of `clean_steps` or `service_steps` for +* Adds a new REST API endpoint ``/v1/runbooks/`` with basic CRUD support. +* Extends the ``/v1/nodes//states/provision`` API to accept a runbook + identifier (name or UUID) instead of ``clean_steps`` or ``service_steps`` for servicing or manual cleaning. * Implements RBAC-aware lifecycle management for runbooks, allowing projects to limit who can CRUD and use a runbook. diff --git a/ironic/api/controllers/v1/port.py b/ironic/api/controllers/v1/port.py index 020e50b56e..3c0f944bf1 100644 --- a/ironic/api/controllers/v1/port.py +++ b/ironic/api/controllers/v1/port.py @@ -46,6 +46,7 @@ PORT_SCHEMA = { 'extra': {'type': ['object', 'null']}, 'is_smartnic': {'type': ['string', 'boolean', 'null']}, 'local_link_connection': {'type': ['null', 'object']}, + 'node_ident': {'type': 'string'}, 'node_uuid': {'type': 'string'}, 'physical_network': {'type': ['string', 'null'], 'maxLength': 64}, 'portgroup_uuid': {'type': ['string', 'null']}, @@ -53,7 +54,11 @@ PORT_SCHEMA = { 'uuid': {'type': ['string', 'null']}, 'name': {'type': ['string', 'null']}, }, - 'required': ['address', 'node_uuid'], + 'required': ['address'], + 'oneOf': [ + {'required': ['node_ident']}, + {'required': ['node_uuid']}, + ], 'additionalProperties': False, } @@ -65,6 +70,7 @@ PATCH_ALLOWED_FIELDS = [ 'extra', 'is_smartnic', 'local_link_connection', + 'node_ident', 'node_uuid', 'physical_network', 'portgroup_uuid', @@ -554,8 +560,17 @@ class PortsController(rest.RestController): node = None owner = None lessee = None - node_uuid = port.get('node_uuid') + node_uuid = port.get('node_uuid', None) + node_ident = port.get('node_ident', None) + + if node_ident: + if not api_utils.allow_node_ident_as_param_for_port_creation(): + raise exception.NotAcceptable() + + ident = node_ident or node_uuid try: + node = api_utils.get_rpc_node(ident) + port['node_uuid'] = node['uuid'] node = api_utils.replace_node_uuid_with_id(port) owner = node.owner lessee = node.lessee diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py index f8846f38de..556dd59280 100644 --- a/ironic/api/controllers/v1/utils.py +++ b/ironic/api/controllers/v1/utils.py @@ -2214,3 +2214,8 @@ def allow_attach_detach_vmedia(): def allow_get_vmedia(): """Check if we should support get virtual media action.""" return api.request.version.minor >= versions.MINOR_93_GET_VMEDIA + + +def allow_node_ident_as_param_for_port_creation(): + """Check if 'node_ident' parameter is allowed for port creation.""" + return api.request.version.minor >= versions.MINOR_94_PORT_NODENAME diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py index df41028b46..23cdaeab61 100644 --- a/ironic/api/controllers/v1/versions.py +++ b/ironic/api/controllers/v1/versions.py @@ -131,6 +131,7 @@ BASE_VERSION = 1 # v1.91: Remove special treatment of .json for API objects # v1.92: Add runbooks API # v1.93: Add GET API for virtual media +# v1.94: Add node name support for port creation MINOR_0_JUNO = 0 MINOR_1_INITIAL_VERSION = 1 @@ -226,6 +227,7 @@ MINOR_90_OVN_VTEP = 90 MINOR_91_DOT_JSON = 91 MINOR_92_RUNBOOKS = 92 MINOR_93_GET_VMEDIA = 93 +MINOR_94_PORT_NODENAME = 94 # When adding another version, update: # - MINOR_MAX_VERSION @@ -233,7 +235,7 @@ MINOR_93_GET_VMEDIA = 93 # explanation of what changed in the new version # - common/release_mappings.py, RELEASE_MAPPING['master']['api'] -MINOR_MAX_VERSION = MINOR_93_GET_VMEDIA +MINOR_MAX_VERSION = MINOR_94_PORT_NODENAME # String representations of the minor and maximum versions _MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION) diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py index 0cfe6a6b98..77e92fddd6 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.93', + 'api': '1.94', 'rpc': '1.61', 'objects': { 'Allocation': ['1.1'], diff --git a/ironic/tests/unit/api/controllers/v1/test_port.py b/ironic/tests/unit/api/controllers/v1/test_port.py index eb5cbb7df1..85edf9b178 100644 --- a/ironic/tests/unit/api/controllers/v1/test_port.py +++ b/ironic/tests/unit/api/controllers/v1/test_port.py @@ -1958,6 +1958,75 @@ class TestPost(test_api_base.BaseApiTest): mock_create.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY, 'test-topic') + def test_create_port_missing_address_fails(self, mock_create): + pdict = post_get_test_port(node_uuid=self.node.uuid) + del pdict['address'] + + response = self.post_json('/ports', pdict, headers=self.headers, + expect_errors=True) + self.assertEqual(http_client.BAD_REQUEST, response.status_int) + self.assertIn("'address' is a required property", + response.json['error_message']) + self.assertFalse(mock_create.called) + + def test_create_port_with_node_uuid(self, mock_create): + pdict = post_get_test_port(node_uuid=self.node.uuid) + response = self.post_json('/ports', pdict, headers=self.headers) + self.assertEqual(http_client.CREATED, response.status_int) + result = self.get_json('/ports/%s' % response.json['uuid'], + headers=self.headers) + self.assertEqual(self.node.uuid, result['node_uuid']) + mock_create.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY, + 'test-topic') + + def test_create_port_with_node_ident(self, mock_create): + self.node.name = 'test-node-name' + self.node.save() + + pdict = post_get_test_port() + pdict['node_ident'] = self.node.name + del pdict['node_uuid'] + + response = self.post_json('/ports', pdict, headers=self.headers) + self.assertEqual(http_client.CREATED, response.status_int) + result = self.get_json('/ports/%s' % response.json['uuid'], + headers=self.headers) + self.assertEqual(self.node.uuid, result['node_uuid']) + mock_create.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY, + 'test-topic') + + def test_create_port_with_both_node_ident_and_node_uuid(self, + mock_create): + self.node.name = 'test-node-name' + self.node.save() + + pdict = post_get_test_port(node_uuid=self.node.uuid) + pdict['node_ident'] = self.node.name + response = self.post_json('/ports', pdict, headers=self.headers, + expect_errors=True) + self.assertEqual(http_client.BAD_REQUEST, response.status_int) + + def test_create_port_without_node_or_node_uuid(self, mock_create): + pdict = post_get_test_port(node_uuid=self.node.uuid) + del pdict['node_uuid'] + response = self.post_json('/ports', pdict, headers=self.headers, + expect_errors=True) + self.assertEqual(http_client.BAD_REQUEST, response.status_int) + + def test_create_port_with_node_ident_unsupported_api_version(self, + mock_create): + headers = {api_base.Version.string: '1.93'} + self.node.name = 'test-node-name' + self.node.save() + + pdict = post_get_test_port(node_uuid=self.node.uuid) + pdict['node_ident'] = self.node.name + del pdict['node_uuid'] + + response = self.post_json('/ports', pdict, headers=headers, + expect_errors=True) + self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int) + @mock.patch.object(notification_utils, '_emit_api_notification', autospec=True) def test_create_port_error(self, mock_notify, mock_create): diff --git a/releasenotes/notes/support-for-node-name-in-port-creation-66f994e3d46a7e6c.yaml b/releasenotes/notes/support-for-node-name-in-port-creation-66f994e3d46a7e6c.yaml new file mode 100644 index 0000000000..b40e30d15a --- /dev/null +++ b/releasenotes/notes/support-for-node-name-in-port-creation-66f994e3d46a7e6c.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Add support for passing either a node's name or UUID through the + 'node_ident' parameter during port creation. The 'node_uuid' parameter is + now deprecated.