Merge "Add a new 'category' field to the Port object"

This commit is contained in:
Zuul
2025-08-22 19:58:19 +00:00
committed by Gerrit Code Review
21 changed files with 144 additions and 8 deletions

View File

@@ -37,6 +37,9 @@ Response to include only the specified fields, rather than the default set.
.. versionadded:: 1.100 .. versionadded:: 1.100
Added the ``vendor`` field. Added the ``vendor`` field.
.. versionadded:: 1.101
Added the ``category`` field.
Normal response code: 200 Normal response code: 200
Error codes: 400,401,403,404 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 .. versionadded:: 1.100
Added the ``vendor`` field. Added the ``vendor`` field.
.. versionadded:: 1.101
Added the ``category`` field.
Normal response code: 200 Normal response code: 200
Error codes: 400,401,403,404 Error codes: 400,401,403,404
@@ -127,6 +133,7 @@ Response
- name: port_name - name: port_name
- description: port_description - description: port_description
- vendor: port_vendor - vendor: port_vendor
- category: port_category
**Example details of a Portgroup's Ports:** **Example details of a Portgroup's Ports:**

View File

@@ -59,6 +59,9 @@ By default, this query will return the uuid and address for each Port.
.. versionadded:: 1.100 .. versionadded:: 1.100
Added the ``vendor`` field. Added the ``vendor`` field.
.. versionadded:: 1.101
Added the ``category`` field.
Normal response code: 200 Normal response code: 200
Request Request
@@ -134,6 +137,9 @@ This method requires a Node UUID and the physical hardware address for the Port
.. versionadded:: 1.100 .. versionadded:: 1.100
Added the ``vendor`` field. Added the ``vendor`` field.
.. versionadded:: 1.101
Added the ``category`` field.
Normal response code: 201 Normal response code: 201
Request Request
@@ -153,6 +159,7 @@ Request
- uuid: req_uuid - uuid: req_uuid
- description: req_port_description - description: req_port_description
- vendor: req_port_vendor - vendor: req_port_vendor
- category: req_port_category
.. note:: .. note::
Either `node_ident` or `node_uuid` is a valid parameter. Either `node_ident` or `node_uuid` is a valid parameter.
@@ -183,6 +190,7 @@ Response
- is_smartnic: is_smartnic - is_smartnic: is_smartnic
- description: port_description - description: port_description
- vendor: port_vendor - vendor: port_vendor
- category: port_category
**Example Port creation response:** **Example Port creation response:**
@@ -227,6 +235,9 @@ Return a list of bare metal Ports, with detailed information.
.. versionadded:: 1.100 .. versionadded:: 1.100
Added the ``vendor`` field. Added the ``vendor`` field.
.. versionadded:: 1.101
Added the ``category`` field.
Normal response code: 200 Normal response code: 200
Request Request
@@ -266,6 +277,7 @@ Response
- is_smartnic: is_smartnic - is_smartnic: is_smartnic
- description: port_description - description: port_description
- vendor: port_vendor - vendor: port_vendor
- category: port_category
**Example detailed Port list response:** **Example detailed Port list response:**

View File

@@ -1570,6 +1570,12 @@ port_address:
in: body in: body
required: true required: true
type: string type: string
port_category:
description: |
Category of the network Port. Helps to further differentiate the Port.
in: body
required: false
type: string
port_description: port_description:
description: | description: |
Descriptive text about the network Port. Descriptive text about the network Port.
@@ -1977,6 +1983,12 @@ req_port_address:
in: body in: body
required: true required: true
type: string 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: req_port_description:
description: | description: |
Descriptive text about the network Port. Descriptive text about the network Port.

View File

@@ -24,6 +24,7 @@
"node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
"physical_network": "physnet1", "physical_network": "physnet1",
"vendor": "splitrock", "vendor": "splitrock",
"category": "hupernet",
"portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a", "portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a",
"pxe_enabled": true, "pxe_enabled": true,
"updated_at": "2016-08-18T22:28:49.653974+00:00", "updated_at": "2016-08-18T22:28:49.653974+00:00",

View File

@@ -4,6 +4,7 @@
"name": "port1", "name": "port1",
"description": "Physical Network", "description": "Physical Network",
"vendor": "splitrock", "vendor": "splitrock",
"category": "hypernet",
"address": "11:11:11:11:11:11", "address": "11:11:11:11:11:11",
"is_smartnic": true, "is_smartnic": true,
"local_link_connection": { "local_link_connection": {

View File

@@ -22,6 +22,7 @@
"name": "port1", "name": "port1",
"description": "Physical Network", "description": "Physical Network",
"vendor": "splitrock", "vendor": "splitrock",
"category": "hypernet",
"node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
"physical_network": "physnet1", "physical_network": "physnet1",
"portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a", "portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a",

View File

@@ -24,6 +24,7 @@
"name": "port1", "name": "port1",
"description": "Physical Network", "description": "Physical Network",
"vendor": "splitrock", "vendor": "splitrock",
"category": "hypernet",
"node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
"physical_network": "physnet1", "physical_network": "physnet1",
"portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a", "portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a",

View File

@@ -22,6 +22,7 @@
"name": "port1", "name": "port1",
"description": "Physical Network", "description": "Physical Network",
"vendor": "splitrock", "vendor": "splitrock",
"category": "hypernet",
"node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
"physical_network": "physnet1", "physical_network": "physnet1",
"portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a", "portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a",

View File

@@ -2,6 +2,11 @@
REST API Version History REST API Version History
======================== ========================
1.101 (Flamingo)
-----------------------
Add a 'category' field to the Port object.
1.100 (Flamingo) 1.100 (Flamingo)
----------------------- -----------------------

View File

@@ -55,6 +55,7 @@ PORT_SCHEMA = {
'name': {'type': ['string', 'null']}, 'name': {'type': ['string', 'null']},
'description': {'type': ['string', 'null'], 'maxLength': 255}, 'description': {'type': ['string', 'null'], 'maxLength': 255},
'vendor': {'type': ['string', 'null'], 'maxLength': 32}, 'vendor': {'type': ['string', 'null'], 'maxLength': 32},
'category': {'type': ['string', 'null'], 'maxLength': 80},
}, },
'required': ['address'], 'required': ['address'],
'oneOf': [ 'oneOf': [
@@ -80,6 +81,7 @@ PATCH_ALLOWED_FIELDS = [
'name', 'name',
'description', 'description',
'vendor', 'vendor',
'category',
] ]
PORT_VALIDATOR_EXTRA = args.dict_valid( 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 requested version is < 1.100, hide vendor field.
if not api_utils.allow_port_vendor(): if not api_utils.allow_port_vendor():
port.pop('vendor', None) 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): def convert_with_links(rpc_port, fields=None, sanitize=True):
@@ -413,6 +418,9 @@ class PortsController(rest.RestController):
if ('vendor' in fields if ('vendor' in fields
and not api_utils.allow_port_vendor()): and not api_utils.allow_port_vendor()):
raise exception.NotAcceptable() raise exception.NotAcceptable()
if ('category' in fields
and not api_utils.allow_port_category()):
raise exception.NotAcceptable()
@METRICS.timer('PortsController.get_all') @METRICS.timer('PortsController.get_all')
@method.expose() @method.expose()

View File

@@ -2248,6 +2248,14 @@ def allow_port_description():
def allow_port_vendor(): def allow_port_vendor():
"""Check if vendor is allowed for ports. """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 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

View File

@@ -138,6 +138,7 @@ BASE_VERSION = 1
# v1.98: Add support for object attributes with keys containing ~ or /. # v1.98: Add support for object attributes with keys containing ~ or /.
# v1.99: Add conductor group filtering to port and portgroup list # v1.99: Add conductor group filtering to port and portgroup list
# v1.100: Add vendor field to port. # v1.100: Add vendor field to port.
# v1.101: Add category field to port.
MINOR_0_JUNO = 0 MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1 MINOR_1_INITIAL_VERSION = 1
@@ -240,6 +241,7 @@ MINOR_97_PORT_DESCRIPTION = 97
MINOR_98_SUPPORT_SPECIAL_CHAR_IN_ATTRIBUTES = 98 MINOR_98_SUPPORT_SPECIAL_CHAR_IN_ATTRIBUTES = 98
MINOR_99_PORT_PORTGROUP_CONDUCTOR_GROUP_FILTER = 99 MINOR_99_PORT_PORTGROUP_CONDUCTOR_GROUP_FILTER = 99
MINOR_100_PORT_VENDOR = 100 MINOR_100_PORT_VENDOR = 100
MINOR_101_PORT_CATEGORY = 101
# When adding another version, update: # When adding another version, update:
# - MINOR_MAX_VERSION # - MINOR_MAX_VERSION
@@ -248,7 +250,7 @@ MINOR_100_PORT_VENDOR = 100
# - common/release_mappings.py, RELEASE_MAPPING['master']['api'] # - 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 # 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

@@ -896,7 +896,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.100', 'api': '1.101',
'rpc': '1.61', 'rpc': '1.61',
'objects': { 'objects': {
'Allocation': ['1.3', '1.2', '1.1'], 'Allocation': ['1.3', '1.2', '1.1'],
@@ -908,7 +908,7 @@ RELEASE_MAPPING = {
'Chassis': ['1.4', '1.3'], 'Chassis': ['1.4', '1.3'],
'Deployment': ['1.1', '1.0'], 'Deployment': ['1.1', '1.0'],
'DeployTemplate': ['1.2', '1.1'], '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'], 'Portgroup': ['1.6', '1.5'],
'Trait': ['1.1', '1.0'], 'Trait': ['1.1', '1.0'],
'TraitList': ['1.1', '1.0'], 'TraitList': ['1.1', '1.0'],

View File

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

View File

@@ -271,6 +271,7 @@ class Port(Base):
name = Column(String(255), nullable=True) name = Column(String(255), nullable=True)
description = Column(String(255), nullable=True) description = Column(String(255), nullable=True)
vendor = Column(String(32), nullable=True) vendor = Column(String(32), nullable=True)
category = Column(String(80), nullable=True)
_node_uuid = orm.relationship( _node_uuid = orm.relationship(
"Node", "Node",

View File

@@ -48,7 +48,8 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat):
# Version 1.12: Add description field # Version 1.12: Add description field
# Version 1.13: Add vendor field # Version 1.13: Add vendor field
# Version 1.14: Mark multiple methods as remotable methods. # 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() dbapi = dbapi.get_instance()
@@ -70,6 +71,7 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat):
'name': object_fields.StringField(nullable=True), 'name': object_fields.StringField(nullable=True),
'description': object_fields.StringField(nullable=True), 'description': object_fields.StringField(nullable=True),
'vendor': 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, 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 Version 1.13: remove vendor for unsupported versions if
remove_unavailable_fields is True. 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 target_version: the desired version of the object
:param remove_unavailable_fields: True to remove fields that are :param remove_unavailable_fields: True to remove fields that are
unavailable in the target version; set this to True when 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. # Convert the vendor field.
self._convert_field_by_version('vendor', (1, 13), target_version, self._convert_field_by_version('vendor', (1, 13), target_version,
remove_unavailable_fields) remove_unavailable_fields)
# Convert the category field.
self._convert_field_by_version('category', (1, 15), target_version,
remove_unavailable_fields)
@object_base.remotable_classmethod @object_base.remotable_classmethod
def get(cls, context, port_id): def get(cls, context, port_id):
@@ -495,7 +503,8 @@ class PortCRUDPayload(notification.NotificationPayloadBase):
# Version 1.4: Add "name" field # Version 1.4: Add "name" field
# Version 1.5: Add "description" field # Version 1.5: Add "description" field
# Version 1.6: Add "vendor" field # Version 1.6: Add "vendor" field
VERSION = '1.6' # Version 1.7: Add "category" field
VERSION = '1.7'
SCHEMA = { SCHEMA = {
'address': ('port', 'address'), 'address': ('port', 'address'),
@@ -510,6 +519,7 @@ class PortCRUDPayload(notification.NotificationPayloadBase):
'name': ('port', 'name'), 'name': ('port', 'name'),
'description': ('port', 'description'), 'description': ('port', 'description'),
'vendor': ('port', 'vendor'), 'vendor': ('port', 'vendor'),
'category': ('port', 'category'),
} }
fields = { fields = {
@@ -529,6 +539,7 @@ class PortCRUDPayload(notification.NotificationPayloadBase):
'name': object_fields.StringField(nullable=True), 'name': object_fields.StringField(nullable=True),
'description': object_fields.StringField(nullable=True), 'description': object_fields.StringField(nullable=True),
'vendor': 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): def __init__(self, port, node_uuid, portgroup_uuid):

View File

@@ -2001,6 +2001,7 @@ class TestPost(test_api_base.BaseApiTest):
pdict.pop('name') pdict.pop('name')
pdict.pop('description') pdict.pop('description')
pdict.pop('vendor') pdict.pop('vendor')
pdict.pop('category')
headers = {api_base.Version.string: str(api_v1.min_version())} headers = {api_base.Version.string: str(api_v1.min_version())}
response = self.post_json('/ports', pdict, headers=headers) response = self.post_json('/ports', pdict, headers=headers)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)

View File

@@ -368,3 +368,29 @@ class DbPortTestCase(base.DbTestCase):
retrieved_port1 = self.dbapi.get_port_by_uuid(port1.uuid) retrieved_port1 = self.dbapi.get_port_by_uuid(port1.uuid)
self.assertEqual(new_vendor, retrieved_port1.vendor) 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)

View File

@@ -290,6 +290,7 @@ def get_test_port(**kw):
'name': kw.get('name'), 'name': kw.get('name'),
'description': kw.get('description'), 'description': kw.get('description'),
'vendor': kw.get('vendor'), 'vendor': kw.get('vendor'),
'category': kw.get('category'),
} }

View File

@@ -678,7 +678,7 @@ expected_object_fingerprints = {
'Node': '1.42-a1d3e6011e3cdb27aafa9353b7c0b6d4', 'Node': '1.42-a1d3e6011e3cdb27aafa9353b7c0b6d4',
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6', 'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
'Chassis': '1.4-fe427272d8bad232a8d46e996a5ca42a', 'Chassis': '1.4-fe427272d8bad232a8d46e996a5ca42a',
'Port': '1.14-684faad7173c1d9e8a2d630381c51903', 'Port': '1.15-013610c0fe2e370b14f4304e0d8aeb3a',
'Portgroup': '1.6-ada5300518c2262766121a4333d92df3', 'Portgroup': '1.6-ada5300518c2262766121a4333d92df3',
'Conductor': '1.6-ed00540fae97aa1c9982f9017c6e8b68', 'Conductor': '1.6-ed00540fae97aa1c9982f9017c6e8b68',
'EventType': '1.1-aa2ba1afd38553e3880c267404e8d370', 'EventType': '1.1-aa2ba1afd38553e3880c267404e8d370',
@@ -699,7 +699,7 @@ expected_object_fingerprints = {
'NodeCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', 'NodeCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'NodeCRUDPayload': '1.15-9168946f843edd5859464aaa40ad70e0', 'NodeCRUDPayload': '1.15-9168946f843edd5859464aaa40ad70e0',
'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', 'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'PortCRUDPayload': '1.6-72323673fb8b869200d91bcd6886acae', 'PortCRUDPayload': '1.7-aaefef8ba3a94030753c1e3b9a29741b',
'NodeMaintenanceNotification': '1.0-59acc533c11d306f149846f922739c15', 'NodeMaintenanceNotification': '1.0-59acc533c11d306f149846f922739c15',
'NodeConsoleNotification': '1.0-59acc533c11d306f149846f922739c15', 'NodeConsoleNotification': '1.0-59acc533c11d306f149846f922739c15',
'PortgroupCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', 'PortgroupCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',

View File

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