diff --git a/doc/source/contributor/webapi-version-history.rst b/doc/source/contributor/webapi-version-history.rst index 65f97ef6c7..398215e52f 100644 --- a/doc/source/contributor/webapi-version-history.rst +++ b/doc/source/contributor/webapi-version-history.rst @@ -2,6 +2,14 @@ REST API Version History ======================== +1.111 (Gazpacho) +---------------------- + +Add ``available_for_dynamic_portgroup`` boolean field to the port object +(defaults to ``True``) and ``dynamic_portgroup`` boolean field to the +portgroup object. The port field is user-editable via POST and PATCH +requests. The portgroup field is read-only to API consumers. + 1.110 (Gazpacho) ---------------------- diff --git a/ironic/api/controllers/v1/port.py b/ironic/api/controllers/v1/port.py index b28acb4602..0a43d62e92 100644 --- a/ironic/api/controllers/v1/port.py +++ b/ironic/api/controllers/v1/port.py @@ -56,6 +56,7 @@ PORT_SCHEMA = { 'description': {'type': ['string', 'null'], 'maxLength': 255}, 'vendor': {'type': ['string', 'null'], 'maxLength': 32}, 'category': {'type': ['string', 'null'], 'maxLength': 80}, + 'available_for_dynamic_portgroup': {'type': 'boolean'}, }, 'required': ['address'], 'oneOf': [ @@ -82,6 +83,7 @@ PATCH_ALLOWED_FIELDS = [ 'description', 'vendor', 'category', + 'available_for_dynamic_portgroup', ] PORT_VALIDATOR_EXTRA = args.dict_valid( @@ -92,6 +94,7 @@ PORT_VALIDATOR_EXTRA = args.dict_valid( portgroup_uuid=args.uuid, pxe_enabled=args.boolean, uuid=args.uuid, + available_for_dynamic_portgroup=args.boolean, ) PORT_VALIDATOR = args.and_valid( @@ -149,6 +152,10 @@ def hide_fields_in_newer_versions(port): # if requested version is < 1.101, hide category field. if not api_utils.allow_port_category(): port.pop('category', None) + # if requested version is < 1.111, hide + # available_for_dynamic_portgroup field. + if not api_utils.allow_port_available_for_dynamic_portgroup(): + port.pop('available_for_dynamic_portgroup', None) def convert_with_links(rpc_port, fields=None, sanitize=True): @@ -168,6 +175,7 @@ def convert_with_links(rpc_port, fields=None, sanitize=True): 'description', 'vendor', 'category', + 'available_for_dynamic_portgroup', ) ) if rpc_port.portgroup_id: @@ -423,6 +431,10 @@ class PortsController(rest.RestController): if ('category' in fields and not api_utils.allow_port_category()): raise exception.NotAcceptable() + allow_dynpg = api_utils.allow_port_available_for_dynamic_portgroup + if ('available_for_dynamic_portgroup' in fields + and not allow_dynpg()): + raise exception.NotAcceptable() @METRICS.timer('PortsController.get_all') @method.expose() diff --git a/ironic/api/controllers/v1/portgroup.py b/ironic/api/controllers/v1/portgroup.py index 75d62bfd24..52226fdcba 100644 --- a/ironic/api/controllers/v1/portgroup.py +++ b/ironic/api/controllers/v1/portgroup.py @@ -93,6 +93,9 @@ def hide_fields_in_newer_versions(portgroup): # if requested version is < 1.103, hide category if not api_utils.allow_portgroup_category(): portgroup.pop('category', None) + # if requested version is < 1.111, hide dynamic_portgroup + if not api_utils.allow_portgroup_dynamic_portgroup(): + portgroup.pop('dynamic_portgroup', None) def portgroup_sanitize(portgroup, fields=None): @@ -123,7 +126,8 @@ def convert_with_links(rpc_portgroup, fields=None, sanitize=True): 'standalone_ports_supported', 'node_uuid', 'physical_network', - 'category' + 'category', + 'dynamic_portgroup', ) ) url = api.request.public_url diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py index 2e5942c549..2ead120f91 100644 --- a/ironic/api/controllers/v1/utils.py +++ b/ironic/api/controllers/v1/utils.py @@ -946,6 +946,9 @@ def check_allowed_portgroup_fields(fields): if (('mode' in fields or 'properties' in fields) and not allow_portgroup_mode_properties()): raise exception.NotAcceptable() + if ('dynamic_portgroup' in fields + and not allow_portgroup_dynamic_portgroup()): + raise exception.NotAcceptable() def check_allow_management_verbs(verb): @@ -2345,3 +2348,23 @@ def check_allow_filter_by_instance_name(instance_name): if instance_name is not None and not allow_node_instance_name(): raise exception.NotAcceptable( _("instance_name is not acceptable in this API version")) + + +def allow_port_available_for_dynamic_portgroup(): + """Check if available_for_dynamic_portgroup is allowed for ports. + + Version 1.111 of the API added available_for_dynamic_portgroup + field to the port object. + """ + return (api.request.version.minor + >= versions.MINOR_111_DYNAMIC_PORTGROUP) + + +def allow_portgroup_dynamic_portgroup(): + """Check if dynamic_portgroup is allowed for portgroups. + + Version 1.111 of the API added dynamic_portgroup field to the + portgroup object. + """ + return (api.request.version.minor + >= versions.MINOR_111_DYNAMIC_PORTGROUP) diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py index de4d52de76..24a7d9d319 100644 --- a/ironic/api/controllers/v1/versions.py +++ b/ironic/api/controllers/v1/versions.py @@ -148,6 +148,8 @@ BASE_VERSION = 1 # v1.108: Add disable_ramdisk support for servicing # v1.109: Add health field to node object. # v1.110: Add support for aborting deployment in DEPLOYWAIT state +# v1.111: Add available_for_dynamic_portgroup to port, +# dynamic_portgroup to portgroup MINOR_0_JUNO = 0 MINOR_1_INITIAL_VERSION = 1 @@ -260,6 +262,7 @@ MINOR_107_X_OPENSTACK_REQUEST_ID = 107 MINOR_108_SERVICE_DISABLE_RAMDISK = 108 MINOR_109_NODE_HEALTH = 109 MINOR_110_DEPLOYWAIT_ABORT = 110 +MINOR_111_DYNAMIC_PORTGROUP = 111 # When adding another version, update: # - MINOR_MAX_VERSION @@ -269,7 +272,7 @@ MINOR_110_DEPLOYWAIT_ABORT = 110 # - Add a comment describing the change above the list of consts -MINOR_MAX_VERSION = MINOR_110_DEPLOYWAIT_ABORT +MINOR_MAX_VERSION = MINOR_111_DYNAMIC_PORTGROUP # 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 e11becd0d3..1e48dd46ed 100644 --- a/ironic/common/release_mappings.py +++ b/ironic/common/release_mappings.py @@ -944,7 +944,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.110', + 'api': '1.111', 'rpc': '1.62', 'networking_rpc': '1.0', 'objects': { @@ -957,8 +957,8 @@ RELEASE_MAPPING = { 'Chassis': ['1.4', '1.3'], 'Deployment': ['1.1', '1.0'], 'DeployTemplate': ['1.2', '1.1'], - 'Port': ['1.15', '1.14', '1.13', '1.12'], - 'Portgroup': ['1.8', '1.7', '1.6', '1.5'], + 'Port': ['1.16', '1.15', '1.14', '1.13', '1.12'], + 'Portgroup': ['1.9', '1.8', '1.7', '1.6', '1.5'], 'Trait': ['1.1', '1.0'], 'TraitList': ['1.2', '1.1', '1.0'], 'VolumeConnector': ['1.1', '1.0'], diff --git a/ironic/db/sqlalchemy/alembic/versions/2a3b4c5d6e7f_add_dynamic_portgroup_fields.py b/ironic/db/sqlalchemy/alembic/versions/2a3b4c5d6e7f_add_dynamic_portgroup_fields.py new file mode 100644 index 0000000000..57ff6214b4 --- /dev/null +++ b/ironic/db/sqlalchemy/alembic/versions/2a3b4c5d6e7f_add_dynamic_portgroup_fields.py @@ -0,0 +1,38 @@ +# 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 dynamic portgroup fields + +Revision ID: 2a3b4c5d6e7f +Revises: c1fd28861bb9 +Create Date: 2026-03-04 00:00:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '2a3b4c5d6e7f' +down_revision = 'c1fd28861bb9' + + +def upgrade(): + op.add_column('ports', + sa.Column('available_for_dynamic_portgroup', + sa.Boolean(), + server_default=sa.true(), + nullable=False)) + op.add_column('portgroups', + sa.Column('dynamic_portgroup', + sa.Boolean(), + nullable=True)) diff --git a/ironic/db/sqlalchemy/models.py b/ironic/db/sqlalchemy/models.py index c4f1ca8b0f..bdb123fcd9 100644 --- a/ironic/db/sqlalchemy/models.py +++ b/ironic/db/sqlalchemy/models.py @@ -25,7 +25,7 @@ from oslo_db import options as db_options from oslo_db.sqlalchemy import models from oslo_db.sqlalchemy import types as db_types from sqlalchemy.ext.associationproxy import association_proxy -from sqlalchemy import Boolean, Column, DateTime, false, Index +from sqlalchemy import Boolean, Column, DateTime, false, Index, true from sqlalchemy import BigInteger, ForeignKey, Integer from sqlalchemy import schema, String, Text from sqlalchemy import orm @@ -273,6 +273,8 @@ class Port(Base): description = Column(String(255), nullable=True) vendor = Column(String(32), nullable=True) category = Column(String(80), nullable=True) + available_for_dynamic_portgroup = Column(Boolean, server_default=true(), + nullable=False) _node_uuid = orm.relationship( "Node", @@ -305,6 +307,7 @@ class Portgroup(Base): properties = Column(db_types.JsonEncodedDict) physical_network = Column(String(64), nullable=True) category = Column(String(80), nullable=True) + dynamic_portgroup = Column(Boolean, nullable=True, default=False) _node_uuid = orm.relationship( "Node", diff --git a/ironic/objects/port.py b/ironic/objects/port.py index 26e183f636..2e1a236e9e 100644 --- a/ironic/objects/port.py +++ b/ironic/objects/port.py @@ -49,7 +49,8 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat): # Version 1.13: Add vendor field # Version 1.14: Mark multiple methods as remotable methods. # Version 1.15: Add category field - VERSION = '1.15' + # Version 1.16: Add available_for_dynamic_portgroup field + VERSION = '1.16' dbapi = dbapi.get_instance() @@ -72,6 +73,8 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat): 'description': object_fields.StringField(nullable=True), 'vendor': object_fields.StringField(nullable=True), 'category': object_fields.StringField(nullable=True), + 'available_for_dynamic_portgroup': object_fields.BooleanField( + default=True), } def _convert_field_by_version(self, field_name, introduced_version, @@ -134,6 +137,9 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat): Version 1.15: remove category for unsupported versions if remove_unavailable_fields is True. + Version 1.16: remove available_for_dynamic_portgroup 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 @@ -171,6 +177,10 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat): # Convert the category field. self._convert_field_by_version('category', (1, 15), target_version, remove_unavailable_fields) + # Convert the available_for_dynamic_portgroup field. + self._convert_field_by_version( + 'available_for_dynamic_portgroup', (1, 16), target_version, + remove_unavailable_fields, True) @object_base.remotable_classmethod def get(cls, context, port_id): @@ -504,7 +514,8 @@ class PortCRUDPayload(notification.NotificationPayloadBase): # Version 1.5: Add "description" field # Version 1.6: Add "vendor" field # Version 1.7: Add "category" field - VERSION = '1.7' + # Version 1.8: Add "available_for_dynamic_portgroup" field + VERSION = '1.8' SCHEMA = { 'address': ('port', 'address'), @@ -520,6 +531,8 @@ class PortCRUDPayload(notification.NotificationPayloadBase): 'description': ('port', 'description'), 'vendor': ('port', 'vendor'), 'category': ('port', 'category'), + 'available_for_dynamic_portgroup': ( + 'port', 'available_for_dynamic_portgroup'), } fields = { @@ -540,6 +553,8 @@ class PortCRUDPayload(notification.NotificationPayloadBase): 'description': object_fields.StringField(nullable=True), 'vendor': object_fields.StringField(nullable=True), 'category': object_fields.StringField(nullable=True), + 'available_for_dynamic_portgroup': object_fields.BooleanField( + default=True), } def __init__(self, port, node_uuid, portgroup_uuid): diff --git a/ironic/objects/portgroup.py b/ironic/objects/portgroup.py index 9036443c33..e3d0452670 100644 --- a/ironic/objects/portgroup.py +++ b/ironic/objects/portgroup.py @@ -41,7 +41,8 @@ class Portgroup(base.IronicObject, object_base.VersionedObjectDictCompat): # Version 1.6: Relevant methods changed to be remotable methods. # Version 1.7: Add physical_network field # Version 1.8: Add category field - VERSION = '1.8' + # Version 1.9: Add dynamic_portgroup field + VERSION = '1.9' dbapi = dbapi.get_instance() @@ -59,6 +60,8 @@ class Portgroup(base.IronicObject, object_base.VersionedObjectDictCompat): 'properties': object_fields.FlexibleDictField(nullable=True), 'physical_network': object_fields.StringField(nullable=True), 'category': object_fields.StringField(nullable=True), + 'dynamic_portgroup': object_fields.BooleanField( + nullable=True, default=False), } # TODO(clif): Abstract this, already exists in Port object. @@ -112,6 +115,9 @@ class Portgroup(base.IronicObject, object_base.VersionedObjectDictCompat): Version 1.8: remove category for unsupported versions if remove_unavailable_fields is True. + Version 1.9: remove dynamic_portgroup 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 @@ -142,6 +148,10 @@ class Portgroup(base.IronicObject, object_base.VersionedObjectDictCompat): # Convert the category field. self._convert_field_by_version('category', (1, 8), target_version, remove_unavailable_fields) + # Convert the dynamic_portgroup field. + self._convert_field_by_version( + 'dynamic_portgroup', (1, 9), target_version, + remove_unavailable_fields, False) @object_base.remotable_classmethod def get(cls, context, portgroup_ident): @@ -418,7 +428,8 @@ class PortgroupCRUDPayload(notification.NotificationPayloadBase): # Version 1.0: Initial version # Version 1.1: Add physical_network field # Version 1.2: Add category field - VERSION = '1.2' + # Version 1.3: Add dynamic_portgroup field + VERSION = '1.3' SCHEMA = { 'address': ('portgroup', 'address'), @@ -432,7 +443,8 @@ class PortgroupCRUDPayload(notification.NotificationPayloadBase): 'standalone_ports_supported'), 'created_at': ('portgroup', 'created_at'), 'updated_at': ('portgroup', 'updated_at'), - 'uuid': ('portgroup', 'uuid') + 'uuid': ('portgroup', 'uuid'), + 'dynamic_portgroup': ('portgroup', 'dynamic_portgroup'), } fields = { @@ -448,7 +460,9 @@ class PortgroupCRUDPayload(notification.NotificationPayloadBase): nullable=True), 'created_at': object_fields.DateTimeField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), - 'uuid': object_fields.UUIDField() + 'uuid': object_fields.UUIDField(), + 'dynamic_portgroup': object_fields.BooleanField( + nullable=True, default=False), } def __init__(self, portgroup, node_uuid): diff --git a/ironic/tests/unit/api/controllers/v1/test_port.py b/ironic/tests/unit/api/controllers/v1/test_port.py index 1bbb9c53d3..7547ff02f3 100644 --- a/ironic/tests/unit/api/controllers/v1/test_port.py +++ b/ironic/tests/unit/api/controllers/v1/test_port.py @@ -2025,6 +2025,7 @@ class TestPost(test_api_base.BaseApiTest): pdict.pop('description') pdict.pop('vendor') pdict.pop('category') + pdict.pop('available_for_dynamic_portgroup') 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/api/controllers/v1/test_portgroup.py b/ironic/tests/unit/api/controllers/v1/test_portgroup.py index ecb3aff417..7eb82cf0e2 100644 --- a/ironic/tests/unit/api/controllers/v1/test_portgroup.py +++ b/ironic/tests/unit/api/controllers/v1/test_portgroup.py @@ -1141,6 +1141,21 @@ class TestPatch(test_api_base.BaseApiTest): self.assertTrue(response.json['error_message']) self.assertFalse(mock_upd.called) + def test_update_portgroup_dynamic_portgroup_not_allowed( + self, mock_upd): + mock_upd.return_value = self.portgroup + response = self.patch_json( + '/portgroups/%s' % self.portgroup.uuid, + [{'path': '/dynamic_portgroup', + 'value': True, + 'op': 'replace'}], + expect_errors=True, + headers=self.headers) + self.assertEqual('application/json', response.content_type) + self.assertEqual(http_client.BAD_REQUEST, response.status_int) + self.assertTrue(response.json['error_message']) + self.assertFalse(mock_upd.called) + def test_update_portgroup_mode_properties(self, mock_upd): mock_upd.return_value = self.portgroup mock_upd.return_value.mode = '802.3ad' @@ -1461,6 +1476,16 @@ class TestPost(test_api_base.BaseApiTest): self.assertEqual('application/json', response.content_type) self.assertTrue(response.json['error_message']) + def test_create_portgroup_dynamic_portgroup_not_allowed(self): + pdict = apiutils.post_get_test_portgroup() + pdict['dynamic_portgroup'] = True + response = self.post_json('/portgroups', pdict, + expect_errors=True, + headers=self.headers) + self.assertEqual(http_client.BAD_REQUEST, response.status_int) + self.assertEqual('application/json', response.content_type) + self.assertTrue(response.json['error_message']) + def test_create_portgroup_mode_old_api_version(self): for kwarg in [{'mode': '802.3ad'}, {'properties': {'bond_prop': 123}}]: pdict = apiutils.post_get_test_portgroup(**kwarg) diff --git a/ironic/tests/unit/db/utils.py b/ironic/tests/unit/db/utils.py index 0eaed19b45..ad39a8fc6b 100644 --- a/ironic/tests/unit/db/utils.py +++ b/ironic/tests/unit/db/utils.py @@ -292,7 +292,9 @@ def get_test_port(**kw): 'name': kw.get('name'), 'description': kw.get('description'), 'vendor': kw.get('vendor'), - 'category': kw.get('category') + 'category': kw.get('category'), + 'available_for_dynamic_portgroup': kw.get( + 'available_for_dynamic_portgroup', True), } @@ -470,7 +472,8 @@ def get_test_portgroup(**kw): 'mode': kw.get('mode'), 'properties': kw.get('properties', {}), 'physical_network': kw.get('physical_network'), - 'category': kw.get('category') + 'category': kw.get('category'), + 'dynamic_portgroup': kw.get('dynamic_portgroup', False), } diff --git a/ironic/tests/unit/objects/test_objects.py b/ironic/tests/unit/objects/test_objects.py index 22f85e1884..66c84a059f 100644 --- a/ironic/tests/unit/objects/test_objects.py +++ b/ironic/tests/unit/objects/test_objects.py @@ -678,8 +678,8 @@ expected_object_fingerprints = { 'Node': '1.44-c9cd729566ab4c6fd69aaa4eaa1386bf', 'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6', 'Chassis': '1.4-fe427272d8bad232a8d46e996a5ca42a', - 'Port': '1.15-013610c0fe2e370b14f4304e0d8aeb3a', - 'Portgroup': '1.8-c931f147ebe450b7c91cc51acfc59472', + 'Port': '1.16-f66d781ac2c9e2906531cc523be56141', + 'Portgroup': '1.9-0c02f589c88ef5a8f60248454783fbbc', 'Conductor': '1.6-ed00540fae97aa1c9982f9017c6e8b68', 'EventType': '1.1-aa2ba1afd38553e3880c267404e8d370', 'NotificationPublisher': '1.0-51a09397d6c0687771fb5be9a999605d', @@ -699,11 +699,11 @@ expected_object_fingerprints = { 'NodeCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', 'NodeCRUDPayload': '1.15-9168946f843edd5859464aaa40ad70e0', 'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', - 'PortCRUDPayload': '1.7-aaefef8ba3a94030753c1e3b9a29741b', + 'PortCRUDPayload': '1.8-e6234f3aafda0dae874feb6f9ec8715f', 'NodeMaintenanceNotification': '1.0-59acc533c11d306f149846f922739c15', 'NodeConsoleNotification': '1.0-59acc533c11d306f149846f922739c15', 'PortgroupCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', - 'PortgroupCRUDPayload': '1.2-ea41e2dfcd5817ec4d3f9768e3f26c6e', + 'PortgroupCRUDPayload': '1.3-0d2b8747f09f71ed84a804aa6888657b', 'VolumeConnectorCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', 'VolumeConnectorCRUDPayload': '1.0-5e8dbb41e05b6149d8f7bfd4daff9339', 'VolumeTargetCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', diff --git a/ironic/tests/unit/objects/test_port.py b/ironic/tests/unit/objects/test_port.py index eddea00b59..06a5aac2b9 100644 --- a/ironic/tests/unit/objects/test_port.py +++ b/ironic/tests/unit/objects/test_port.py @@ -424,3 +424,60 @@ class TestConvertToVersion(db_base.DbTestCase): port._convert_to_version("1.9", False) self.assertIsNone(port.name) self.assertNotIn('name', port.obj_get_changes()) + + def test_available_for_dynamic_portgroup_supported_missing(self): + port = objects.Port(self.context, **self.fake_port) + delattr(port, 'available_for_dynamic_portgroup') + port.obj_reset_changes() + port._convert_to_version("1.16") + self.assertTrue(port.available_for_dynamic_portgroup) + self.assertIn('available_for_dynamic_portgroup', + port.obj_get_changes()) + + def test_available_for_dynamic_portgroup_supported_set(self): + port = objects.Port(self.context, **self.fake_port) + port.available_for_dynamic_portgroup = False + port.obj_reset_changes() + port._convert_to_version("1.16") + self.assertFalse(port.available_for_dynamic_portgroup) + self.assertNotIn('available_for_dynamic_portgroup', + port.obj_get_changes()) + + def test_available_for_dynamic_portgroup_unsupported(self): + port = objects.Port(self.context, **self.fake_port) + port._convert_to_version("1.15") + self.assertNotIn('available_for_dynamic_portgroup', port) + + def test_available_for_dynamic_portgroup_unsupported_missing(self): + port = objects.Port(self.context, **self.fake_port) + delattr(port, 'available_for_dynamic_portgroup') + port.obj_reset_changes() + port._convert_to_version("1.15") + self.assertNotIn('available_for_dynamic_portgroup', port) + + def test_available_for_dynamic_portgroup_unsupported_set_remove(self): + port = objects.Port(self.context, **self.fake_port) + port.available_for_dynamic_portgroup = True + port.obj_reset_changes() + port._convert_to_version("1.15") + self.assertNotIn('available_for_dynamic_portgroup', port) + + def test_available_for_dynamic_portgroup_unsupported_no_remove_non_default( + self): + port = objects.Port(self.context, **self.fake_port) + port.available_for_dynamic_portgroup = False + port.obj_reset_changes() + port._convert_to_version("1.15", False) + self.assertTrue(port.available_for_dynamic_portgroup) + self.assertIn('available_for_dynamic_portgroup', + port.obj_get_changes()) + + def test_available_for_dynamic_portgroup_unsupported_no_remove_default( + self): + port = objects.Port(self.context, **self.fake_port) + port.available_for_dynamic_portgroup = True + port.obj_reset_changes() + port._convert_to_version("1.15", False) + self.assertTrue(port.available_for_dynamic_portgroup) + self.assertNotIn('available_for_dynamic_portgroup', + port.obj_get_changes()) diff --git a/ironic/tests/unit/objects/test_portgroup.py b/ironic/tests/unit/objects/test_portgroup.py index 42e17e23f2..4d6810db30 100644 --- a/ironic/tests/unit/objects/test_portgroup.py +++ b/ironic/tests/unit/objects/test_portgroup.py @@ -245,3 +245,54 @@ class TestConvertToVersion(db_base.DbTestCase): portgroup._convert_to_version('1.4', False) # no change self.assertEqual(vif2, portgroup.internal_info['tenant_vif_port_id']) + + def test_dynamic_portgroup_supported_missing(self): + portgroup = objects.Portgroup(self.context, **self.fake_portgroup) + delattr(portgroup, 'dynamic_portgroup') + portgroup.obj_reset_changes() + portgroup._convert_to_version("1.9") + self.assertFalse(portgroup.dynamic_portgroup) + self.assertIn('dynamic_portgroup', portgroup.obj_get_changes()) + + def test_dynamic_portgroup_supported_set(self): + portgroup = objects.Portgroup(self.context, **self.fake_portgroup) + portgroup.dynamic_portgroup = True + portgroup.obj_reset_changes() + portgroup._convert_to_version("1.9") + self.assertTrue(portgroup.dynamic_portgroup) + self.assertNotIn('dynamic_portgroup', portgroup.obj_get_changes()) + + def test_dynamic_portgroup_unsupported(self): + portgroup = objects.Portgroup(self.context, **self.fake_portgroup) + portgroup._convert_to_version("1.8") + self.assertNotIn('dynamic_portgroup', portgroup) + + def test_dynamic_portgroup_unsupported_missing(self): + portgroup = objects.Portgroup(self.context, **self.fake_portgroup) + delattr(portgroup, 'dynamic_portgroup') + portgroup.obj_reset_changes() + portgroup._convert_to_version("1.8") + self.assertNotIn('dynamic_portgroup', portgroup) + + def test_dynamic_portgroup_unsupported_set_remove(self): + portgroup = objects.Portgroup(self.context, **self.fake_portgroup) + portgroup.dynamic_portgroup = False + portgroup.obj_reset_changes() + portgroup._convert_to_version("1.8") + self.assertNotIn('dynamic_portgroup', portgroup) + + def test_dynamic_portgroup_unsupported_no_remove_non_default(self): + portgroup = objects.Portgroup(self.context, **self.fake_portgroup) + portgroup.dynamic_portgroup = True + portgroup.obj_reset_changes() + portgroup._convert_to_version("1.8", False) + self.assertFalse(portgroup.dynamic_portgroup) + self.assertIn('dynamic_portgroup', portgroup.obj_get_changes()) + + def test_dynamic_portgroup_unsupported_no_remove_default(self): + portgroup = objects.Portgroup(self.context, **self.fake_portgroup) + portgroup.dynamic_portgroup = False + portgroup.obj_reset_changes() + portgroup._convert_to_version("1.8", False) + self.assertFalse(portgroup.dynamic_portgroup) + self.assertNotIn('dynamic_portgroup', portgroup.obj_get_changes()) diff --git a/releasenotes/notes/tbn-port-portgroup-fields-ae7fbd3d5095fc06.yaml b/releasenotes/notes/tbn-port-portgroup-fields-ae7fbd3d5095fc06.yaml new file mode 100644 index 0000000000..bd0d7a2775 --- /dev/null +++ b/releasenotes/notes/tbn-port-portgroup-fields-ae7fbd3d5095fc06.yaml @@ -0,0 +1,21 @@ +--- +features: + - | + Adds two new fields for trait-based networking in REST API version + *1.111*: + + * ``Port.available_for_dynamic_portgroup``: a boolean field (default + True) indicating whether the port should be considered for dynamic + portgroup assignment. This field is operator-editable via POST and + PATCH. + * ``Portgroup.dynamic_portgroup``: a read-only boolean field (default + False) that indicates whether the portgroup was dynamically created + by the trait-based networking subsystem. This field is managed + internally by Ironic and cannot be modified by API consumers. +upgrade: + - | + Adds ``available_for_dynamic_portgroup`` and ``dynamic_portgroup`` + fields to the port and portgroup objects respectively in REST API + version *1.111*. Upgrading to this release will set + ``available_for_dynamic_portgroup`` to True for all existing ports + and ``dynamic_portgroup`` to False for all existing portgroups.