Add dynamic portgroup fields (API 1.111)

Add two new boolean fields in API microversion 1.111:
- Port.available_for_dynamic_portgroup: user-editable via
  POST/PATCH, default True
- Portgroup.dynamic_portgroup: read-only to API consumers,
  default False, settable internally by Ironic

These fields support dynamic portgroup assignment where ports
can be marked as available for automatic portgroup membership
and portgroups can be flagged as dynamically managed.

Assisted-By: claude-code
Change-Id: Ib10f8bd5ba9e25b60cffd7d67d1e2c45be8f8533
Signed-off-by: Jay Faulkner <jay@jvf.cc>
This commit is contained in:
Jay Faulkner
2026-03-04 12:58:25 -08:00
committed by Clif Houck
parent 674e56d951
commit 63fefc3ceb
17 changed files with 296 additions and 18 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'],

View File

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

View File

@@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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',

View File

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

View File

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

View File

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