Add support for node name in port creation

Add support for creating ports using either node UUID or node name.

Closes-Bug: #1439901
Change-Id: I215619648bbe7aa1152e0f117971bece74ffe1fe
This commit is contained in:
cid 2024-09-30 20:31:42 +01:00
parent be33fcf38b
commit da002a341c
9 changed files with 117 additions and 9 deletions

View File

@ -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`` of ``vtep-logical-switch``, ``vtep-physical-switch`` and ``port_id``
to identify ovn vtep switches. 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 Normal response code: 201
Request Request
@ -126,7 +129,7 @@ Request
.. rest_parameters:: parameters.yaml .. rest_parameters:: parameters.yaml
- node_uuid: req_node_uuid - node_ident: node_ident
- address: req_port_address - address: req_port_address
- portgroup_uuid: req_portgroup_uuid - portgroup_uuid: req_portgroup_uuid
- name: req_port_name - name: req_port_name
@ -137,6 +140,9 @@ Request
- is_smartnic: req_is_smartnic - is_smartnic: req_is_smartnic
- uuid: req_uuid - uuid: req_uuid
.. note::
Either `node_ident` or `node_uuid` is a valid parameter.
**Example Port creation request:** **Example Port creation request:**
.. literalinclude:: samples/port-create-request.json .. literalinclude:: samples/port-create-request.json

View File

@ -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", "portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a",
"name": "port1", "name": "port1",
"address": "11:11:11:11:11:11", "address": "11:11:11:11:11:11",

View File

@ -2,6 +2,11 @@
REST API Version History REST API Version History
======================== ========================
1.94 (Epoxy)
-----------------------
Add support to create ports passing in either the node name or UUID.
1.92 (Dalmatian) 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 list of steps for manual cleaning or servicing, to enable
self-service of maintenance items by project members. self-service of maintenance items by project members.
* Adds a new REST API endpoint `/v1/runbooks/` with basic CRUD support. * Adds a new REST API endpoint ``/v1/runbooks/`` with basic CRUD support.
* Extends the `/v1/nodes/<node>/states/provision` API to accept a runbook * Extends the ``/v1/nodes/<node>/states/provision`` API to accept a runbook
identifier (name or UUID) instead of `clean_steps` or `service_steps` for identifier (name or UUID) instead of ``clean_steps`` or ``service_steps`` for
servicing or manual cleaning. servicing or manual cleaning.
* Implements RBAC-aware lifecycle management for runbooks, allowing projects * Implements RBAC-aware lifecycle management for runbooks, allowing projects
to limit who can CRUD and use a runbook. to limit who can CRUD and use a runbook.

View File

@ -46,6 +46,7 @@ PORT_SCHEMA = {
'extra': {'type': ['object', 'null']}, 'extra': {'type': ['object', 'null']},
'is_smartnic': {'type': ['string', 'boolean', 'null']}, 'is_smartnic': {'type': ['string', 'boolean', 'null']},
'local_link_connection': {'type': ['null', 'object']}, 'local_link_connection': {'type': ['null', 'object']},
'node_ident': {'type': 'string'},
'node_uuid': {'type': 'string'}, 'node_uuid': {'type': 'string'},
'physical_network': {'type': ['string', 'null'], 'maxLength': 64}, 'physical_network': {'type': ['string', 'null'], 'maxLength': 64},
'portgroup_uuid': {'type': ['string', 'null']}, 'portgroup_uuid': {'type': ['string', 'null']},
@ -53,7 +54,11 @@ PORT_SCHEMA = {
'uuid': {'type': ['string', 'null']}, 'uuid': {'type': ['string', 'null']},
'name': {'type': ['string', 'null']}, 'name': {'type': ['string', 'null']},
}, },
'required': ['address', 'node_uuid'], 'required': ['address'],
'oneOf': [
{'required': ['node_ident']},
{'required': ['node_uuid']},
],
'additionalProperties': False, 'additionalProperties': False,
} }
@ -65,6 +70,7 @@ PATCH_ALLOWED_FIELDS = [
'extra', 'extra',
'is_smartnic', 'is_smartnic',
'local_link_connection', 'local_link_connection',
'node_ident',
'node_uuid', 'node_uuid',
'physical_network', 'physical_network',
'portgroup_uuid', 'portgroup_uuid',
@ -554,8 +560,17 @@ class PortsController(rest.RestController):
node = None node = None
owner = None owner = None
lessee = 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: try:
node = api_utils.get_rpc_node(ident)
port['node_uuid'] = node['uuid']
node = api_utils.replace_node_uuid_with_id(port) node = api_utils.replace_node_uuid_with_id(port)
owner = node.owner owner = node.owner
lessee = node.lessee lessee = node.lessee

View File

@ -2214,3 +2214,8 @@ def allow_attach_detach_vmedia():
def allow_get_vmedia(): def allow_get_vmedia():
"""Check if we should support get virtual media action.""" """Check if we should support get virtual media action."""
return api.request.version.minor >= versions.MINOR_93_GET_VMEDIA 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

View File

@ -131,6 +131,7 @@ BASE_VERSION = 1
# v1.91: Remove special treatment of .json for API objects # v1.91: Remove special treatment of .json for API objects
# 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
MINOR_0_JUNO = 0 MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1 MINOR_1_INITIAL_VERSION = 1
@ -226,6 +227,7 @@ MINOR_90_OVN_VTEP = 90
MINOR_91_DOT_JSON = 91 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
# When adding another version, update: # When adding another version, update:
# - MINOR_MAX_VERSION # - MINOR_MAX_VERSION
@ -233,7 +235,7 @@ MINOR_93_GET_VMEDIA = 93
# 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_93_GET_VMEDIA MINOR_MAX_VERSION = MINOR_94_PORT_NODENAME
# 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

@ -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.93', 'api': '1.94',
'rpc': '1.61', 'rpc': '1.61',
'objects': { 'objects': {
'Allocation': ['1.1'], 'Allocation': ['1.1'],

View File

@ -1958,6 +1958,75 @@ class TestPost(test_api_base.BaseApiTest):
mock_create.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY, mock_create.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY,
'test-topic') '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', @mock.patch.object(notification_utils, '_emit_api_notification',
autospec=True) autospec=True)
def test_create_port_error(self, mock_notify, mock_create): def test_create_port_error(self, mock_notify, mock_create):

View File

@ -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.