From 7e602d84273cc8b158aa4b4c12e6dfcf540dd089 Mon Sep 17 00:00:00 2001 From: Clif Houck Date: Sun, 20 Jul 2025 17:04:47 -0500 Subject: [PATCH] Add a new 'category' field to the Port object Adds a new category field to the port object. This is foundational work for the first milestone of trait based port scheduling. Change-Id: Ica76ae3da08bdf743a495781fe958cb71493a2e7 Signed-off-by: Clif Houck Signed-off-by: Jay Faulkner --- .../baremetal-api-v1-portgroups-ports.inc | 7 +++++ api-ref/source/baremetal-api-v1-ports.inc | 12 +++++++ api-ref/source/parameters.yaml | 12 +++++++ .../samples/node-port-detail-response.json | 1 + .../source/samples/port-create-request.json | 1 + .../source/samples/port-create-response.json | 1 + .../samples/port-list-detail-response.json | 1 + .../source/samples/port-update-response.json | 1 + .../contributor/webapi-version-history.rst | 5 +++ ironic/api/controllers/v1/port.py | 8 +++++ ironic/api/controllers/v1/utils.py | 10 +++++- ironic/api/controllers/v1/versions.py | 4 ++- ironic/common/release_mappings.py | 4 +-- ...b_add_category_attribute_to_port_object.py | 31 +++++++++++++++++++ ironic/db/sqlalchemy/models.py | 1 + ironic/objects/port.py | 15 +++++++-- .../unit/api/controllers/v1/test_port.py | 1 + ironic/tests/unit/db/test_ports.py | 26 ++++++++++++++++ ironic/tests/unit/db/utils.py | 1 + ironic/tests/unit/objects/test_objects.py | 4 +-- .../notes/port-category-9935c6006d243bc3.yaml | 6 ++++ 21 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 ironic/db/sqlalchemy/alembic/versions/3ef27505c9fb_add_category_attribute_to_port_object.py create mode 100644 releasenotes/notes/port-category-9935c6006d243bc3.yaml diff --git a/api-ref/source/baremetal-api-v1-portgroups-ports.inc b/api-ref/source/baremetal-api-v1-portgroups-ports.inc index 5cd32ed417..0148aef20e 100644 --- a/api-ref/source/baremetal-api-v1-portgroups-ports.inc +++ b/api-ref/source/baremetal-api-v1-portgroups-ports.inc @@ -37,6 +37,9 @@ Response to include only the specified fields, rather than the default set. .. versionadded:: 1.100 Added the ``vendor`` field. +.. versionadded:: 1.101 + Added the ``category`` field. + Normal response code: 200 Error codes: 400,401,403,404 @@ -90,6 +93,9 @@ Return a detailed list of bare metal Ports associated with ``portgroup_ident``. .. versionadded:: 1.100 Added the ``vendor`` field. +.. versionadded:: 1.101 + Added the ``category`` field. + Normal response code: 200 Error codes: 400,401,403,404 @@ -127,6 +133,7 @@ Response - name: port_name - description: port_description - vendor: port_vendor + - category: port_category **Example details of a Portgroup's Ports:** diff --git a/api-ref/source/baremetal-api-v1-ports.inc b/api-ref/source/baremetal-api-v1-ports.inc index 15716016e8..1848071d1f 100644 --- a/api-ref/source/baremetal-api-v1-ports.inc +++ b/api-ref/source/baremetal-api-v1-ports.inc @@ -59,6 +59,9 @@ By default, this query will return the uuid and address for each Port. .. versionadded:: 1.100 Added the ``vendor`` field. +.. versionadded:: 1.101 + Added the ``category`` field. + Normal response code: 200 Request @@ -134,6 +137,9 @@ This method requires a Node UUID and the physical hardware address for the Port .. versionadded:: 1.100 Added the ``vendor`` field. +.. versionadded:: 1.101 + Added the ``category`` field. + Normal response code: 201 Request @@ -153,6 +159,7 @@ Request - uuid: req_uuid - description: req_port_description - vendor: req_port_vendor + - category: req_port_category .. note:: Either `node_ident` or `node_uuid` is a valid parameter. @@ -183,6 +190,7 @@ Response - is_smartnic: is_smartnic - description: port_description - vendor: port_vendor + - category: port_category **Example Port creation response:** @@ -227,6 +235,9 @@ Return a list of bare metal Ports, with detailed information. .. versionadded:: 1.100 Added the ``vendor`` field. +.. versionadded:: 1.101 + Added the ``category`` field. + Normal response code: 200 Request @@ -266,6 +277,7 @@ Response - is_smartnic: is_smartnic - description: port_description - vendor: port_vendor + - category: port_category **Example detailed Port list response:** diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 02fabf9215..77edac1836 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -1570,6 +1570,12 @@ port_address: in: body required: true type: string +port_category: + description: | + Category of the network Port. Helps to further differentiate the Port. + in: body + required: false + type: string port_description: description: | Descriptive text about the network Port. @@ -1977,6 +1983,12 @@ req_port_address: in: body required: true type: string +req_port_category: + description: | + Category of the network Port. Helps to further differentiate the Port. + in: body + required: false + type: string req_port_description: description: | Descriptive text about the network Port. diff --git a/api-ref/source/samples/node-port-detail-response.json b/api-ref/source/samples/node-port-detail-response.json index 0ac38ed7d3..e992deaeb8 100644 --- a/api-ref/source/samples/node-port-detail-response.json +++ b/api-ref/source/samples/node-port-detail-response.json @@ -24,6 +24,7 @@ "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", "physical_network": "physnet1", "vendor": "splitrock", + "category": "hupernet", "portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a", "pxe_enabled": true, "updated_at": "2016-08-18T22:28:49.653974+00:00", diff --git a/api-ref/source/samples/port-create-request.json b/api-ref/source/samples/port-create-request.json index 719a93482f..9721dee367 100644 --- a/api-ref/source/samples/port-create-request.json +++ b/api-ref/source/samples/port-create-request.json @@ -4,6 +4,7 @@ "name": "port1", "description": "Physical Network", "vendor": "splitrock", + "category": "hypernet", "address": "11:11:11:11:11:11", "is_smartnic": true, "local_link_connection": { diff --git a/api-ref/source/samples/port-create-response.json b/api-ref/source/samples/port-create-response.json index 5d8b1c1028..6a5018782f 100644 --- a/api-ref/source/samples/port-create-response.json +++ b/api-ref/source/samples/port-create-response.json @@ -22,6 +22,7 @@ "name": "port1", "description": "Physical Network", "vendor": "splitrock", + "category": "hypernet", "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", "physical_network": "physnet1", "portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a", diff --git a/api-ref/source/samples/port-list-detail-response.json b/api-ref/source/samples/port-list-detail-response.json index 3c0572f314..f44c14cfb3 100644 --- a/api-ref/source/samples/port-list-detail-response.json +++ b/api-ref/source/samples/port-list-detail-response.json @@ -24,6 +24,7 @@ "name": "port1", "description": "Physical Network", "vendor": "splitrock", + "category": "hypernet", "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", "physical_network": "physnet1", "portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a", diff --git a/api-ref/source/samples/port-update-response.json b/api-ref/source/samples/port-update-response.json index 8687835ed7..bba4fea8d6 100644 --- a/api-ref/source/samples/port-update-response.json +++ b/api-ref/source/samples/port-update-response.json @@ -22,6 +22,7 @@ "name": "port1", "description": "Physical Network", "vendor": "splitrock", + "category": "hypernet", "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", "physical_network": "physnet1", "portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a", diff --git a/doc/source/contributor/webapi-version-history.rst b/doc/source/contributor/webapi-version-history.rst index ed75f818c3..fe74f83582 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.101 (Flamingo) +----------------------- + +Add a 'category' field to the Port object. + 1.100 (Flamingo) ----------------------- diff --git a/ironic/api/controllers/v1/port.py b/ironic/api/controllers/v1/port.py index 888c12ee7c..5b35fb7cc6 100644 --- a/ironic/api/controllers/v1/port.py +++ b/ironic/api/controllers/v1/port.py @@ -55,6 +55,7 @@ PORT_SCHEMA = { 'name': {'type': ['string', 'null']}, 'description': {'type': ['string', 'null'], 'maxLength': 255}, 'vendor': {'type': ['string', 'null'], 'maxLength': 32}, + 'category': {'type': ['string', 'null'], 'maxLength': 80}, }, 'required': ['address'], 'oneOf': [ @@ -80,6 +81,7 @@ PATCH_ALLOWED_FIELDS = [ 'name', 'description', 'vendor', + 'category', ] PORT_VALIDATOR_EXTRA = args.dict_valid( @@ -144,6 +146,9 @@ def hide_fields_in_newer_versions(port): # if requested version is < 1.100, hide vendor field. if not api_utils.allow_port_vendor(): port.pop('vendor', None) + # if requested version is < 1.101, hide category field. + if not api_utils.allow_port_category(): + port.pop('category', None) def convert_with_links(rpc_port, fields=None, sanitize=True): @@ -413,6 +418,9 @@ class PortsController(rest.RestController): if ('vendor' in fields and not api_utils.allow_port_vendor()): raise exception.NotAcceptable() + if ('category' in fields + and not api_utils.allow_port_category()): + raise exception.NotAcceptable() @METRICS.timer('PortsController.get_all') @method.expose() diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py index 291f85d7bc..c657b40303 100644 --- a/ironic/api/controllers/v1/utils.py +++ b/ironic/api/controllers/v1/utils.py @@ -2248,6 +2248,14 @@ def allow_port_description(): def allow_port_vendor(): """Check if vendor is allowed for ports. - Version 1.100 of the API added description field to the port object. + Version 1.100 of the API added vendor field to the port object. """ return api.request.version.minor >= versions.MINOR_100_PORT_VENDOR + + +def allow_port_category(): + """Check if category is allowed for ports. + + Version 1.101 of the API added category field to the port object. + """ + return api.request.version.minor >= versions.MINOR_101_PORT_CATEGORY diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py index 739e2af58a..04886a7a2e 100644 --- a/ironic/api/controllers/v1/versions.py +++ b/ironic/api/controllers/v1/versions.py @@ -138,6 +138,7 @@ BASE_VERSION = 1 # v1.98: Add support for object attributes with keys containing ~ or /. # v1.99: Add conductor group filtering to port and portgroup list # v1.100: Add vendor field to port. +# v1.101: Add category field to port. MINOR_0_JUNO = 0 MINOR_1_INITIAL_VERSION = 1 @@ -240,6 +241,7 @@ MINOR_97_PORT_DESCRIPTION = 97 MINOR_98_SUPPORT_SPECIAL_CHAR_IN_ATTRIBUTES = 98 MINOR_99_PORT_PORTGROUP_CONDUCTOR_GROUP_FILTER = 99 MINOR_100_PORT_VENDOR = 100 +MINOR_101_PORT_CATEGORY = 101 # When adding another version, update: # - MINOR_MAX_VERSION @@ -248,7 +250,7 @@ MINOR_100_PORT_VENDOR = 100 # - common/release_mappings.py, RELEASE_MAPPING['master']['api'] -MINOR_MAX_VERSION = MINOR_100_PORT_VENDOR +MINOR_MAX_VERSION = MINOR_101_PORT_CATEGORY # 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 36a0bdb621..b24785371b 100644 --- a/ironic/common/release_mappings.py +++ b/ironic/common/release_mappings.py @@ -896,7 +896,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.100', + 'api': '1.101', 'rpc': '1.61', 'objects': { 'Allocation': ['1.3', '1.2', '1.1'], @@ -908,7 +908,7 @@ RELEASE_MAPPING = { 'Chassis': ['1.4', '1.3'], 'Deployment': ['1.1', '1.0'], 'DeployTemplate': ['1.2', '1.1'], - 'Port': ['1.14', '1.13', '1.12'], + 'Port': ['1.15', '1.14', '1.13', '1.12'], 'Portgroup': ['1.6', '1.5'], 'Trait': ['1.1', '1.0'], 'TraitList': ['1.1', '1.0'], diff --git a/ironic/db/sqlalchemy/alembic/versions/3ef27505c9fb_add_category_attribute_to_port_object.py b/ironic/db/sqlalchemy/alembic/versions/3ef27505c9fb_add_category_attribute_to_port_object.py new file mode 100644 index 0000000000..983c0ec733 --- /dev/null +++ b/ironic/db/sqlalchemy/alembic/versions/3ef27505c9fb_add_category_attribute_to_port_object.py @@ -0,0 +1,31 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""add category attribute to port object + +Revision ID: 3ef27505c9fb +Revises: e4827561979d +Create Date: 2025-07-21 01:33:47.215396 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '3ef27505c9fb' +down_revision = 'e4827561979d' + + +def upgrade(): + op.add_column('ports', sa.Column('category', sa.String(length=80), + nullable=True)) diff --git a/ironic/db/sqlalchemy/models.py b/ironic/db/sqlalchemy/models.py index 131cf21d11..d41f0191c1 100644 --- a/ironic/db/sqlalchemy/models.py +++ b/ironic/db/sqlalchemy/models.py @@ -271,6 +271,7 @@ class Port(Base): name = Column(String(255), nullable=True) description = Column(String(255), nullable=True) vendor = Column(String(32), nullable=True) + category = Column(String(80), nullable=True) _node_uuid = orm.relationship( "Node", diff --git a/ironic/objects/port.py b/ironic/objects/port.py index db5462af96..26e183f636 100644 --- a/ironic/objects/port.py +++ b/ironic/objects/port.py @@ -48,7 +48,8 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat): # Version 1.12: Add description field # Version 1.13: Add vendor field # Version 1.14: Mark multiple methods as remotable methods. - VERSION = '1.14' + # Version 1.15: Add category field + VERSION = '1.15' dbapi = dbapi.get_instance() @@ -70,6 +71,7 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat): 'name': object_fields.StringField(nullable=True), 'description': object_fields.StringField(nullable=True), 'vendor': object_fields.StringField(nullable=True), + 'category': object_fields.StringField(nullable=True), } def _convert_field_by_version(self, field_name, introduced_version, @@ -129,6 +131,9 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat): Version 1.13: remove vendor for unsupported versions if remove_unavailable_fields is True. + Version 1.15: remove category for unsupported versions if + remove_unavailable_fields is True. + :param target_version: the desired version of the object :param remove_unavailable_fields: True to remove fields that are unavailable in the target version; set this to True when @@ -163,6 +168,9 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat): # Convert the vendor field. self._convert_field_by_version('vendor', (1, 13), target_version, remove_unavailable_fields) + # Convert the category field. + self._convert_field_by_version('category', (1, 15), target_version, + remove_unavailable_fields) @object_base.remotable_classmethod def get(cls, context, port_id): @@ -495,7 +503,8 @@ class PortCRUDPayload(notification.NotificationPayloadBase): # Version 1.4: Add "name" field # Version 1.5: Add "description" field # Version 1.6: Add "vendor" field - VERSION = '1.6' + # Version 1.7: Add "category" field + VERSION = '1.7' SCHEMA = { 'address': ('port', 'address'), @@ -510,6 +519,7 @@ class PortCRUDPayload(notification.NotificationPayloadBase): 'name': ('port', 'name'), 'description': ('port', 'description'), 'vendor': ('port', 'vendor'), + 'category': ('port', 'category'), } fields = { @@ -529,6 +539,7 @@ class PortCRUDPayload(notification.NotificationPayloadBase): 'name': object_fields.StringField(nullable=True), 'description': object_fields.StringField(nullable=True), 'vendor': object_fields.StringField(nullable=True), + 'category': object_fields.StringField(nullable=True), } def __init__(self, port, node_uuid, portgroup_uuid): diff --git a/ironic/tests/unit/api/controllers/v1/test_port.py b/ironic/tests/unit/api/controllers/v1/test_port.py index 318e711fb1..180b60f392 100644 --- a/ironic/tests/unit/api/controllers/v1/test_port.py +++ b/ironic/tests/unit/api/controllers/v1/test_port.py @@ -2001,6 +2001,7 @@ class TestPost(test_api_base.BaseApiTest): pdict.pop('name') pdict.pop('description') pdict.pop('vendor') + pdict.pop('category') headers = {api_base.Version.string: str(api_v1.min_version())} response = self.post_json('/ports', pdict, headers=headers) self.assertEqual('application/json', response.content_type) diff --git a/ironic/tests/unit/db/test_ports.py b/ironic/tests/unit/db/test_ports.py index 9d80c4df09..5f3343da8a 100644 --- a/ironic/tests/unit/db/test_ports.py +++ b/ironic/tests/unit/db/test_ports.py @@ -368,3 +368,29 @@ class DbPortTestCase(base.DbTestCase): retrieved_port1 = self.dbapi.get_port_by_uuid(port1.uuid) self.assertEqual(new_vendor, retrieved_port1.vendor) + + def test_create_port_with_category(self): + category = 'hypernet' + port1 = db_utils.create_test_port( + uuid=uuidutils.generate_uuid(), + node_id=self.node.id, + address='52:54:00:cf:2d:42', + category=category) + + port2 = db_utils.create_test_port( + uuid=uuidutils.generate_uuid(), + node_id=self.node.id, + address='52:54:00:cf:2d:45', + category=category) + + self.assertEqual(category, port1.category) + self.assertEqual(category, port2.category) + + new_category = 'ultranet' + updated_port = self.dbapi.update_port( + port1.id, {'category': new_category}) + + self.assertEqual(new_category, updated_port.category) + + retrieved_port1 = self.dbapi.get_port_by_uuid(port1.uuid) + self.assertEqual(new_category, retrieved_port1.category) diff --git a/ironic/tests/unit/db/utils.py b/ironic/tests/unit/db/utils.py index a6fed6bde6..906efe62dd 100644 --- a/ironic/tests/unit/db/utils.py +++ b/ironic/tests/unit/db/utils.py @@ -290,6 +290,7 @@ def get_test_port(**kw): 'name': kw.get('name'), 'description': kw.get('description'), 'vendor': kw.get('vendor'), + 'category': kw.get('category'), } diff --git a/ironic/tests/unit/objects/test_objects.py b/ironic/tests/unit/objects/test_objects.py index 3deb6ec329..1d04ab8be2 100644 --- a/ironic/tests/unit/objects/test_objects.py +++ b/ironic/tests/unit/objects/test_objects.py @@ -678,7 +678,7 @@ expected_object_fingerprints = { 'Node': '1.42-a1d3e6011e3cdb27aafa9353b7c0b6d4', 'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6', 'Chassis': '1.4-fe427272d8bad232a8d46e996a5ca42a', - 'Port': '1.14-684faad7173c1d9e8a2d630381c51903', + 'Port': '1.15-013610c0fe2e370b14f4304e0d8aeb3a', 'Portgroup': '1.6-ada5300518c2262766121a4333d92df3', 'Conductor': '1.6-ed00540fae97aa1c9982f9017c6e8b68', 'EventType': '1.1-aa2ba1afd38553e3880c267404e8d370', @@ -699,7 +699,7 @@ expected_object_fingerprints = { 'NodeCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', 'NodeCRUDPayload': '1.15-9168946f843edd5859464aaa40ad70e0', 'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', - 'PortCRUDPayload': '1.6-72323673fb8b869200d91bcd6886acae', + 'PortCRUDPayload': '1.7-aaefef8ba3a94030753c1e3b9a29741b', 'NodeMaintenanceNotification': '1.0-59acc533c11d306f149846f922739c15', 'NodeConsoleNotification': '1.0-59acc533c11d306f149846f922739c15', 'PortgroupCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', diff --git a/releasenotes/notes/port-category-9935c6006d243bc3.yaml b/releasenotes/notes/port-category-9935c6006d243bc3.yaml new file mode 100644 index 0000000000..386bcd5fc3 --- /dev/null +++ b/releasenotes/notes/port-category-9935c6006d243bc3.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + A new "category" field has been added to the Port object. This field is meant + to help distinguish between different types of Ports. Relevant to trait + based port scheduling feature.