Merge "Add trusted vif api extension for the port"

This commit is contained in:
Zuul 2024-09-06 01:27:44 +00:00 committed by Gerrit Code Review
commit 19a6e8e626
23 changed files with 641 additions and 20 deletions

View File

@ -0,0 +1,3 @@
function configure_port_trusted_ml2_extension {
neutron_ml2_extension_driver_add "port_trusted"
}

View File

@ -18,6 +18,7 @@ source $LIBDIR/tag_ports_during_bulk_creation
source $LIBDIR/octavia
source $LIBDIR/loki
source $LIBDIR/local_ip
source $LIBDIR/port_trusted_vif
# source the OVS/OVN compilation helper methods
source $TOP_DIR/lib/neutron_plugins/ovs_source
@ -98,6 +99,9 @@ if [[ "$1" == "stack" ]]; then
fi
configure_l3_agent
fi
if is_service_enabled q-port-trusted-vif neutron-port-trusted-vif; then
configure_port_trusted_ml2_extension
fi
if [ $NEUTRON_CORE_PLUGIN = ml2 ]; then
configure_ml2_extension_drivers
fi

View File

@ -96,6 +96,7 @@ from neutron_lib.api.definitions import vpn
from neutron_lib.api.definitions import vpn_endpoint_groups
from neutron_lib import constants
from neutron.extensions import port_trusted_vif
from neutron.extensions import quotasv2_detail
from neutron.extensions import security_groups_default_rules
@ -158,6 +159,7 @@ ML2_SUPPORTED_API_EXTENSIONS = [
port_numa_affinity_policy.ALIAS,
port_numa_affinity_policy_socket.ALIAS,
port_security.ALIAS,
port_trusted_vif.ALIAS,
provider_net.ALIAS,
port_resource_request.ALIAS,
qos.ALIAS,

View File

@ -297,6 +297,15 @@ rules = [
),
operations=ACTION_POST,
),
policy.DocumentedRuleDefault(
name='create_port:trusted',
check_str=base.ADMIN,
scope_types=['project'],
description=(
'Specify ``trusted`` attribute when creating a port'
),
operations=ACTION_POST,
),
policy.DocumentedRuleDefault(
name='get_port',
@ -383,6 +392,13 @@ rules = [
description='Get ``hints`` attribute of a port',
operations=ACTION_GET,
),
policy.DocumentedRuleDefault(
name='get_port:trusted',
check_str=base.ADMIN,
scope_types=['project'],
description='Get ``trusted`` attribute of a port',
operations=ACTION_GET,
),
policy.DocumentedRuleDefault(
name='get_ports_tags',
check_str=neutron_policy.policy_or(
@ -641,6 +657,13 @@ rules = [
description='Update ``hints`` attribute of a port',
operations=ACTION_PUT,
),
policy.DocumentedRuleDefault(
name='update_port:trusted',
check_str=base.ADMIN,
scope_types=['project'],
description='Update ``trusted`` attribute of a port',
operations=ACTION_PUT,
),
policy.DocumentedRuleDefault(
name='update_ports_tags',
check_str=neutron_policy.policy_or(

View File

@ -0,0 +1,70 @@
# Copyright 2024 OpenStack Foundation
#
# 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.
#
import json
from alembic import op
from oslo_serialization import jsonutils
from oslo_utils import strutils
import sqlalchemy as sa
# Add port trusted attribute
#
# Revision ID: 5bcb7b31ec7d
# Revises: 175fa80908e1
# Create Date: 2024-08-06 12:44:37.193211
# revision identifiers, used by Alembic.
revision = '5bcb7b31ec7d'
down_revision = '175fa80908e1'
def upgrade():
port_trusted_table = op.create_table(
'porttrusted',
sa.Column('port_id',
sa.String(36),
sa.ForeignKey('ports.id',
ondelete="CASCADE"),
primary_key=True),
sa.Column('trusted',
sa.Boolean,
nullable=True))
# A simple model of the ml2_port_bindings table, just to get and update
# binding:profile fields where needed
port_binding_table = sa.Table(
'ml2_port_bindings', sa.MetaData(),
sa.Column('port_id', sa.String(length=36), nullable=False),
sa.Column('profile', sa.String(length=4095)))
session = sa.orm.Session(bind=op.get_bind())
for row in session.query(port_binding_table).all():
if len(row[1]) == 0:
continue
try:
profile = jsonutils.loads(row[1])
except json.JSONDecodeError:
continue
trusted = profile.pop('trusted', None)
if trusted is None:
continue
session.execute(port_trusted_table.insert().values(
port_id=row[0],
trusted=strutils.bool_from_string(trusted)))
session.execute(port_binding_table.update().values(
profile=jsonutils.dumps(profile) if profile else '').where(
port_binding_table.c.port_id == row[0]))
session.commit()

View File

@ -1 +1 @@
175fa80908e1
5bcb7b31ec7d

View File

@ -0,0 +1,37 @@
# Copyright 2024 Red Hat, Inc.
# All Rights Reserved.
#
# 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.
from neutron_lib.db import model_base
import sqlalchemy as sa
from sqlalchemy import orm
from neutron.db import models_v2
class PortTrusted(model_base.BASEV2):
__tablename__ = 'porttrusted'
port_id = sa.Column(sa.String(36),
sa.ForeignKey('ports.id', ondelete='CASCADE'),
primary_key=True)
trusted = sa.Column(sa.Boolean, nullable=True)
port = orm.relationship(
models_v2.Port,
load_on_pending=True,
backref=orm.backref('trusted',
lazy='subquery',
uselist=False,
cascade='delete'))
revises_on_change = ('port', )

View File

@ -0,0 +1,66 @@
# Copyright (c) 2024 Red Hat, Inc.
#
# 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.
from neutron_lib.api.definitions import portbindings
from neutron_lib import constants as n_const
from neutron.extensions import port_trusted_vif
from neutron.objects.port.extensions import port_trusted as trusted_obj
class PortTrustedDbMixin(object):
"""Mixin class to add trusted extension to a port"""
@staticmethod
def _set_portbinding_profile(data, trusted):
try:
data[portbindings.PROFILE]['trusted'] = trusted
except (AttributeError, KeyError):
data[portbindings.PROFILE] = {'trusted': trusted}
def _process_create_port(self, context, data, result):
trusted = data.get(port_trusted_vif.TRUSTED_VIF)
if trusted is n_const.ATTR_NOT_SPECIFIED:
result[port_trusted_vif.TRUSTED_VIF] = None
return
obj = trusted_obj.PortTrusted(
context, port_id=result['id'], trusted=trusted)
obj.create()
result[port_trusted_vif.TRUSTED_VIF] = trusted
self._set_portbinding_profile(result, trusted)
def _process_update_port(self, context, data, result):
trusted = data.get(port_trusted_vif.TRUSTED_VIF)
if trusted is None or trusted is n_const.ATTR_NOT_SPECIFIED:
result[port_trusted_vif.TRUSTED_VIF] = None
return
obj = trusted_obj.PortTrusted.get_object(
context, port_id=result['id'])
if obj:
obj.trusted = trusted
obj.update()
result[port_trusted_vif.TRUSTED_VIF] = trusted
self._set_portbinding_profile(result, trusted)
else:
self._process_create_port(context, data, result)
def _extend_port_dict(self, response_data, db_data):
if db_data.trusted is not None:
trusted = db_data.trusted.trusted
response_data[port_trusted_vif.TRUSTED_VIF] = trusted
self._set_portbinding_profile(response_data, trusted)
else:
response_data[port_trusted_vif.TRUSTED_VIF] = None

View File

@ -0,0 +1,77 @@
# Copyright (c) 2024 Red Hat, Inc.
#
# 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.
from neutron_lib.api import converters
from neutron_lib.api.definitions import port
from neutron_lib.api.definitions import portbindings
from neutron_lib.api import extensions as api_extensions
from neutron_lib import constants
# TODO(slaweq): use api definition from neutron-lib once
# https://review.opendev.org/c/openstack/neutron-lib/+/923860
# will be merged and released
ALIAS = 'port-trusted-vif'
NAME = "Port trusted vif"
DESCRIPTION = "Expose port 'trusted' attribute in the API"
UPDATED_TIMESTAMP = "2024-07-10T10:00:00-00:00"
RESOURCE_NAME = port.RESOURCE_NAME
COLLECTION_NAME = port.COLLECTION_NAME
TRUSTED_VIF = 'trusted'
RESOURCE_ATTRIBUTE_MAP = {
COLLECTION_NAME: {
TRUSTED_VIF: {
'allow_post': True,
'allow_put': True,
'convert_to': converters.convert_to_boolean,
'enforce_policy': True,
'required_by_policy': False,
'default': constants.ATTR_NOT_SPECIFIED,
'is_visible': True,
'validate': {'type:boolean': None}
}
},
}
REQUIRED_EXTENSIONS = [portbindings.ALIAS]
OPTIONAL_EXTENSIONS = []
class Port_trusted_vif(api_extensions.ExtensionDescriptor):
@classmethod
def get_name(cls):
return NAME
@classmethod
def get_alias(cls):
return ALIAS
@classmethod
def get_description(cls):
return DESCRIPTION
@classmethod
def get_updated(cls):
return UPDATED_TIMESTAMP
def get_required_extensions(self):
return REQUIRED_EXTENSIONS
def get_extended_resources(self, version):
if version == "2.0":
return RESOURCE_ATTRIBUTE_MAP
else:
return {}

View File

@ -0,0 +1,36 @@
# Copyright (c) 2023 Red Hat, Inc.
#
# 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.
from neutron_lib.objects import common_types
from oslo_versionedobjects import fields as obj_fields
from neutron.db.models import port_trusted
from neutron.objects import base
@base.NeutronObjectRegistry.register
class PortTrusted(base.NeutronDbObject):
# Version 1.0: Initial version
VERSION = '1.0'
db_model = port_trusted.PortTrusted
primary_keys = ['port_id']
fields = {
'port_id': common_types.UUIDField(),
'trusted': obj_fields.BooleanField(nullable=True),
}
foreign_keys = {'Port': {'port_id': 'id'}}

View File

@ -338,7 +338,8 @@ class Port(base.NeutronDbObject):
# Version 1.7: Added port_device field
# Version 1.8: Added hints field
# Version 1.9: Added hardware_offload_type field
VERSION = '1.9'
# Version 1.10: Added trusted field
VERSION = '1.10'
db_model = models_v2.Port
@ -394,6 +395,7 @@ class Port(base.NeutronDbObject):
'numa_affinity_policy': obj_fields.StringField(nullable=True),
'device_profile': obj_fields.StringField(nullable=True),
'hardware_offload_type': obj_fields.StringField(nullable=True),
'trusted': obj_fields.BooleanField(nullable=True),
# TODO(ihrachys): consider adding a 'dns_assignment' fully synthetic
# field in later object iterations
@ -420,6 +422,7 @@ class Port(base.NeutronDbObject):
'qos_network_policy_id',
'security',
'security_group_ids',
'trusted',
]
fields_need_translation = {
@ -591,6 +594,10 @@ class Port(base.NeutronDbObject):
db_obj.hardware_offload_type.hardware_offload_type)
fields_to_change.append('hardware_offload_type')
if db_obj.get('trusted') is not None:
self.trusted = db_obj.trusted.trusted
fields_to_change.append('trusted')
self.obj_reset_changes(fields_to_change)
def obj_make_compatible(self, primitive, target_version):
@ -627,6 +634,8 @@ class Port(base.NeutronDbObject):
primitive.pop('hints', None)
if _target_version < (1, 9):
primitive.pop('hardware_offload_type', None)
if _target_version < (1, 10):
primitive.pop('trusted', None)
@classmethod
@db_api.CONTEXT_READER

View File

@ -0,0 +1,46 @@
# Copyright 2023 Red Hat, Inc.
# All Rights Reserved.
#
# 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.
from neutron_lib.plugins.ml2 import api
from oslo_log import log as logging
from neutron.db import port_trusted_db
from neutron.extensions import port_trusted_vif
LOG = logging.getLogger(__name__)
class PortTrustedExtensionDriver(
api.ExtensionDriver,
port_trusted_db.PortTrustedDbMixin):
_supported_extension_alias = port_trusted_vif.ALIAS
def initialize(self):
LOG.info('PortTrustedExtensionDriver initialization complete')
@property
def extension_alias(self):
return self._supported_extension_alias
def process_create_port(self, context, data, result):
self._process_create_port(context, data, result)
def process_update_port(self, context, data, result):
self._process_update_port(context, data, result)
def extend_port_dict(self, session, port_db, result):
self._extend_port_dict(result, port_db)

View File

@ -561,7 +561,17 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
if profile not in (None, const.ATTR_NOT_SPECIFIED,
self._get_profile(binding)):
binding.profile = jsonutils.dumps(profile)
profile_json = jsonutils.dumps(profile)
# TODO(slaweq): Remove warning and raise InvalidInput exception
# instead in the 2026.1 release
if 'trusted' in profile_json:
LOG.warning("Marking VIF as 'trusted' directly through the "
"'binding:profile' field of the port is "
"deprecated and will be forbidden in future. "
"Please enable 'port_trusted' ML2 plugin's "
"extension and use 'trusted' field of the port "
"instead")
binding.profile = profile_json
if len(binding.profile) > models.BINDING_PROFILE_LEN:
msg = _("binding:profile value too large")
raise exc.InvalidInput(error_message=msg)
@ -897,6 +907,9 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
port[portbindings.HOST_ID] = binding.host
port[portbindings.VIF_TYPE] = binding.vif_type
port[portbindings.VIF_DETAILS] = self._get_vif_details(binding)
port_trusted = port.get('trusted')
if port_trusted is not None:
port[portbindings.PROFILE]['trusted'] = port_trusted
def _update_port_dict_bound_drivers(self, port, binding_levels):
levels = {str(bl.level): bl.driver for bl in binding_levels}
@ -2680,7 +2693,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
'and shared filesystem ports') % port['id']
raise exc.BadRequest(resource='port', msg=msg)
def _make_port_binding_dict(self, binding, fields=None):
def _make_port_binding_dict(self, binding, fields=None, port=None):
res = {key: binding[key] for key in (
pbe_ext.HOST, pbe_ext.VIF_TYPE, pbe_ext.VNIC_TYPE, pbe_ext.STATUS)}
if isinstance(binding, ports_obj.PortBinding):
@ -2689,8 +2702,19 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
else:
res[pbe_ext.PROFILE] = self._get_profile(binding)
res[pbe_ext.VIF_DETAILS] = self._get_vif_details(binding)
# If port object was passed, get e.g. trusted field from it and add it
# to the binding:profile
if port:
self._extend_port_binding_dict_with_synthetic_fields(res, port)
return db_utils.resource_fields(res, fields)
def _extend_port_binding_dict_with_synthetic_fields(self, binding, port):
if binding[pbe_ext.VIF_TYPE] == portbindings.VIF_TYPE_UNBOUND:
# For unbound port there is no need to extend binding dict
return
if port.trusted is not None:
binding[pbe_ext.PROFILE]['trusted'] = port.trusted
def _get_port_binding_attrs(self, binding, host=None):
return {portbindings.VNIC_TYPE: binding.get(pbe_ext.VNIC_TYPE),
portbindings.HOST_ID: binding.get(pbe_ext.HOST) or host,
@ -2716,7 +2740,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
def create_port_binding(self, context, port_id, binding):
attrs = binding[pbe_ext.RESOURCE_NAME]
with db_api.CONTEXT_WRITER.using(context):
port_db = self._get_port(context, port_id)
port = ports_obj.Port.get_object(context, id=port_id)
port_db = port.db_obj
self._validate_port_supports_multiple_bindings(port_db)
if self._get_binding_for_host(port_db.port_bindings,
attrs[pbe_ext.HOST]):
@ -2755,7 +2780,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
with db_api.CONTEXT_WRITER.using(context):
bind_context._binding.persist_state_to_session(context.session)
db.set_binding_levels(context, bind_context._binding_levels)
return self._make_port_binding_dict(bind_context._binding)
return self._make_port_binding_dict(bind_context._binding, port=port)
@utils.transaction_guard
@db_api.retry_if_session_inactive()
@ -2771,7 +2796,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
bindings = ports_obj.PortBinding.get_objects(
context, _pager=pager, port_id=port_id, **filters)
return [self._make_port_binding_dict(binding, fields)
return [self._make_port_binding_dict(binding, fields, port)
for binding in bindings]
@utils.transaction_guard
@ -2785,7 +2810,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
port_id=port_id)
if not binding:
raise exc.PortBindingNotFound(port_id=port_id, host=host)
return self._make_port_binding_dict(binding, fields)
return self._make_port_binding_dict(binding, fields, port)
def _get_binding_for_host(self, bindings, host):
for binding in bindings:
@ -2797,7 +2822,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
def update_port_binding(self, context, host, port_id, binding):
attrs = binding[pbe_ext.RESOURCE_NAME]
with db_api.CONTEXT_WRITER.using(context):
port_db = self._get_port(context, port_id)
port = ports_obj.Port.get_object(context, id=port_id)
port_db = port.db_obj
self._validate_port_supports_multiple_bindings(port_db)
original_binding = self._get_binding_for_host(
port_db.port_bindings, host)
@ -2824,7 +2850,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
with db_api.CONTEXT_WRITER.using(context):
bind_context._binding.persist_state_to_session(context.session)
db.set_binding_levels(context, bind_context._binding_levels)
return self._make_port_binding_dict(bind_context._binding)
return self._make_port_binding_dict(bind_context._binding, port=port)
@utils.transaction_guard
@db_api.retry_if_session_inactive()
@ -2834,7 +2860,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
# fixed
if isinstance(port_id, dict):
port_id = port_id['port_id']
port_db = self._get_port(context, port_id)
port = ports_obj.Port.get_object(context, id=port_id)
port_db = port.db_obj
self._validate_port_supports_multiple_bindings(port_db)
active_binding = p_utils.get_port_binding_by_status_and_host(
port_db.port_bindings, const.ACTIVE)
@ -2874,7 +2901,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
network['id'])
self.notifier.binding_activate(context, port_id,
inactive_binding.host)
return self._make_port_binding_dict(cur_context._binding)
return self._make_port_binding_dict(cur_context._binding,
port=port)
raise exc.PortBindingError(port_id=port_id, host=host)
@utils.transaction_guard

View File

@ -65,6 +65,7 @@ NETWORK_API_EXTENSIONS+=",port-mac-address-regenerate"
NETWORK_API_EXTENSIONS+=",port-numa-affinity-policy"
NETWORK_API_EXTENSIONS+=",port-numa-affinity-policy-socket"
NETWORK_API_EXTENSIONS+=",port-security-groups-filtering"
NETWORK_API_EXTENSIONS+=",port-trusted-vif"
NETWORK_API_EXTENSIONS+=",segment"
NETWORK_API_EXTENSIONS+=",segments-peer-subnet-host-routes"
NETWORK_API_EXTENSIONS+=",service-type"

View File

@ -552,6 +552,16 @@ class AdminTests(PortAPITestCase):
'create_port:hints',
self.alt_target))
def test_create_port_with_trusted_field(self):
self.assertTrue(
policy.enforce(self.context,
'create_port:trusted',
self.target))
self.assertTrue(
policy.enforce(self.context,
'create_port:trusted',
self.alt_target))
def test_get_port(self):
self.assertTrue(
policy.enforce(self.context, 'get_port', self.target))
@ -606,6 +616,14 @@ class AdminTests(PortAPITestCase):
policy.enforce(
self.context, 'get_port:hints', self.alt_target))
def test_get_port_trusted_field(self):
self.assertTrue(
policy.enforce(
self.context, 'get_port:trusted', self.target))
self.assertTrue(
policy.enforce(
self.context, 'get_port:trusted', self.alt_target))
def test_get_ports_tags(self):
self.assertTrue(
policy.enforce(self.context, 'get_ports_tags', self.target))
@ -747,6 +765,16 @@ class AdminTests(PortAPITestCase):
'update_port:hints',
self.alt_target))
def test_update_port_trusted_field(self):
self.assertTrue(
policy.enforce(self.context,
'update_port:trusted',
self.target))
self.assertTrue(
policy.enforce(self.context,
'update_port:trusted',
self.alt_target))
def test_delete_port(self):
self.assertTrue(
policy.enforce(self.context, 'delete_port', self.target))
@ -899,6 +927,18 @@ class ProjectManagerTests(AdminTests):
self.context, 'create_port:hints',
self.alt_target)
def test_create_port_with_trusted_field(self):
self.assertRaises(
base_policy.PolicyNotAuthorized,
policy.enforce,
self.context, 'create_port:trusted',
self.target)
self.assertRaises(
base_policy.PolicyNotAuthorized,
policy.enforce,
self.context, 'create_port:trusted',
self.alt_target)
def test_get_port(self):
self.assertTrue(
policy.enforce(self.context, 'get_port', self.target))
@ -966,6 +1006,16 @@ class ProjectManagerTests(AdminTests):
policy.enforce, self.context, 'get_port:hints',
self.alt_target)
def test_get_port_trusted_field(self):
self.assertRaises(
base_policy.PolicyNotAuthorized,
policy.enforce, self.context, 'get_port:trusted',
self.target)
self.assertRaises(
base_policy.PolicyNotAuthorized,
policy.enforce, self.context, 'get_port:trusted',
self.alt_target)
def test_get_ports_tags(self):
self.assertTrue(
policy.enforce(self.context, 'get_ports_tags', self.target))
@ -1120,6 +1170,16 @@ class ProjectManagerTests(AdminTests):
policy.enforce,
self.context, 'update_port:hints', self.alt_target)
def test_update_port_trusted_field(self):
self.assertRaises(
base_policy.PolicyNotAuthorized,
policy.enforce,
self.context, 'update_port:trusted', self.target)
self.assertRaises(
base_policy.PolicyNotAuthorized,
policy.enforce,
self.context, 'update_port:trusted', self.alt_target)
def test_update_ports_tags(self):
self.assertTrue(
policy.enforce(self.context, 'update_ports_tags', self.target))

View File

@ -538,8 +538,8 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
'mac_address', 'name', 'fixed_ips',
'tenant_id', 'device_owner', 'security_groups',
'propagate_uplink_status', 'numa_affinity_policy',
'device_profile', 'hints', 'hardware_offload_type') +
(arg_list or ())):
'device_profile', 'hints', 'hardware_offload_type',
'trusted') + (arg_list or ())):
# Arg must be present
if arg in kwargs:
data['port'][arg] = kwargs[arg]

View File

@ -0,0 +1,70 @@
# Copyright (c) 2023 Red Hat, Inc.
#
# 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.
import ddt
from neutron_lib.api.definitions import portbindings
from neutron_lib import context
from oslo_config import cfg
from neutron.db import port_trusted_db
from neutron.extensions import port_trusted_vif as apidef
from neutron.plugins.ml2 import plugin
from neutron.tests.unit.db import test_db_base_plugin_v2
class PortTrustedExtensionTestPlugin(
plugin.Ml2Plugin,
port_trusted_db.PortTrustedDbMixin):
"""Test plugin to mixin the port trusted extension."""
supported_extension_aliases = [apidef.ALIAS, portbindings.ALIAS]
@ddt.ddt
class PortTrustedExtensionTestCase(
test_db_base_plugin_v2.NeutronDbPluginV2TestCase):
"""Test API extension port-trusted-vif attributes."""
def setUp(self, *args):
plugin = ('neutron.tests.unit.extensions.test_port_trusted_vif.'
'PortTrustedExtensionTestPlugin')
extension_drivers = ['port_trusted']
cfg.CONF.set_override('extension_drivers', extension_drivers, 'ml2')
super().setUp(plugin=plugin)
self.ctx = context.get_admin_context()
def _create_and_check_port_with_trusted_field(self, trusted):
name = 'port-trusted-vif'
keys = [('name', name),
('admin_state_up', True),
('trusted', trusted)]
port_args = {'name': name}
if trusted is not None:
port_args['trusted'] = trusted
with self.port(is_admin=True, **port_args) as port:
for k, v in keys:
self.assertEqual(v, port['port'][k])
if trusted is not None:
self.assertEqual(trusted,
port['port']['binding:profile']['trusted'])
else:
self.assertNotIn('trusted',
port['port']['binding:profile'].keys())
def test_create_port_with_trusted_field(self):
self._create_and_check_port_with_trusted_field(True)
self._create_and_check_port_with_trusted_field(False)
def test_create_port_with_trusted_field_not_set(self):
self._create_and_check_port_with_trusted_field(None)

View File

@ -0,0 +1,36 @@
# Copyright (c) 2024 Red Hat, Inc.
#
# 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.
from neutron.objects.port.extensions import port_trusted
from neutron.tests.unit.objects import test_base as obj_test_base
from neutron.tests.unit import testlib_api
class PortTrustedIfaceObjectTestCase(
obj_test_base.BaseObjectIfaceTestCase):
_test_class = port_trusted.PortTrusted
class PortTrustedDbObjectTestCase(
obj_test_base.BaseDbObjectTestCase,
testlib_api.SqlTestCase):
_test_class = port_trusted.PortTrusted
def setUp(self):
super().setUp()
self.update_obj_fields(
{'port_id': lambda: self._create_test_port_id(),
'trusted': True})

View File

@ -73,7 +73,7 @@ object_data = {
'NetworkSegment': '1.0-57b7f2960971e3b95ded20cbc59244a8',
'NetworkSegmentRange': '1.0-bdec1fffc9058ea676089b1f2f2b3cf3',
'NetworkSubnetLock': '1.0-140de39d4b86ae346dc3d70b885bea53',
'Port': '1.9-25f8da7ed95f1538f9e08657b0b450c1',
'Port': '1.10-ae84f686bfc3deb4017495134da6ef04',
'PortHardwareOffloadType': '1.0-5f424d02b144fd1832ac3e6b03662674',
'PortDeviceProfile': '1.0-b98c7083cc3e93d176fd7a91ae13af32',
'PortHints': '1.0-9ebf6e12fa427809476a92c7432352b8',
@ -85,6 +85,7 @@ object_data = {
'PortForwarding': '1.3-402b1fb5a754808b82a966c95f468113',
'PortSecurity': '1.0-b30802391a87945ee9c07582b4ff95e3',
'PortUplinkStatusPropagation': '1.1-f0a4ca451a941910376c33616dea5de2',
'PortTrusted': '1.0-8312fb91937412cdeb92c3279059c7ce',
'ProviderResourceAssociation': '1.0-05ab2d5a3017e5ce9dd381328f285f34',
'ProvisioningBlock': '1.0-c19d6d05bfa8143533471c1296066125',
'QosBandwidthLimitRule': '1.5-51b662b12a8d1dfa89288d826c6d26d3',

View File

@ -535,6 +535,12 @@ class PortDbObjectTestCase(obj_test_base.BaseDbObjectTestCase,
self.assertNotIn('hints',
port_v1_7['versioned_object.data'])
def test_v1_10_to_v1_9_drops_trusted(self):
port_new = self._create_test_port(trusted=True)
port_v1_9 = port_new.obj_to_primitive(target_version='1.9')
self.assertNotIn('trusted',
port_v1_9['versioned_object.data'])
def test_get_ports_ids_by_security_groups_except_router(self):
sg_id = self._create_test_security_group_id()
filter_owner = constants.ROUTER_INTERFACE_OWNERS_SNAT

View File

@ -354,6 +354,7 @@ class ExtendedPortBindingTestCase(test_plugin.NeutronDbPluginV2TestCase):
cfg.CONF.set_override('mechanism_drivers',
['logger', 'test'],
'ml2')
cfg.CONF.set_override('extension_drivers', ['port_trusted'], 'ml2')
driver_type.register_ml2_drivers_vlan_opts()
cfg.CONF.set_override('network_vlan_ranges',
@ -431,9 +432,15 @@ class ExtendedPortBindingTestCase(test_plugin.NeutronDbPluginV2TestCase):
subresource='bindings', sub_id=host).get_response(self.api)
return response
def _create_port_and_binding(self, **kwargs):
device_owner = '%s%s' % (const.DEVICE_OWNER_COMPUTE_PREFIX, 'nova')
with self.port(device_owner=device_owner) as port:
# todo(slaweq): here I can add trusted to be checked too
def _create_port_and_binding(self, trusted=None, **kwargs):
port_kwargs = {
'device_owner': '%s%s' % (
const.DEVICE_OWNER_COMPUTE_PREFIX, 'nova')}
if trusted is not None:
port_kwargs['trusted'] = trusted
port_kwargs['is_admin'] = True
with self.port(**port_kwargs) as port:
port_id = port['port']['id']
binding = self._make_port_binding(self.fmt, port_id, self.host,
**kwargs)['binding']
@ -447,12 +454,13 @@ class ExtendedPortBindingTestCase(test_plugin.NeutronDbPluginV2TestCase):
self.assertEqual({'port_filter': False},
binding[pbe_ext.VIF_DETAILS])
def _assert_unbound_port_binding(self, binding):
def _assert_unbound_port_binding(self, binding, expected_profile=None):
self.assertFalse(binding[pbe_ext.HOST])
self.assertEqual(portbindings.VIF_TYPE_UNBOUND,
binding[pbe_ext.VIF_TYPE])
self.assertEqual({}, binding[pbe_ext.VIF_DETAILS])
self.assertEqual({}, binding[pbe_ext.PROFILE])
expected_profile = expected_profile or {}
self.assertEqual(expected_profile, binding[pbe_ext.PROFILE])
def test_create_port_binding(self):
profile = {'key1': 'value1'}
@ -641,12 +649,32 @@ class ExtendedPortBindingTestCase(test_plugin.NeutronDbPluginV2TestCase):
self.assertEqual(1, len(retrieved_bindings))
self._assert_bound_port_binding(retrieved_bindings[0])
def test_list_port_bindings_with_trusted_field_set(self):
port, new_binding = self._create_port_and_binding(trusted=True)
retrieved_bindings = self._list_port_bindings(
port['id'], raw_response=False)['bindings']
self.assertEqual(2, len(retrieved_bindings))
self._assert_unbound_port_binding(
utils.get_port_binding_by_status_and_host(
retrieved_bindings, const.ACTIVE))
bound_port_binding = utils.get_port_binding_by_status_and_host(
retrieved_bindings, const.INACTIVE, host=self.host)
self._assert_bound_port_binding(bound_port_binding)
self.assertTrue(bound_port_binding['profile']['trusted'])
def test_show_port_binding(self):
port, new_binding = self._create_port_and_binding()
retrieved_binding = self._show_port_binding(
port['id'], self.host, raw_response=False)['binding']
self._assert_bound_port_binding(retrieved_binding)
def test_show_port_binding_with_trusted_field_set(self):
port, new_binding = self._create_port_and_binding(trusted=True)
retrieved_binding = self._show_port_binding(
port['id'], self.host, raw_response=False)['binding']
self._assert_bound_port_binding(retrieved_binding)
self.assertTrue(retrieved_binding['profile']['trusted'])
def test_show_port_binding_with_fields(self):
port, new_binding = self._create_port_and_binding()
fields = 'fields=%s' % pbe_ext.HOST

View File

@ -0,0 +1,17 @@
---
features:
- |
New ML2 plugin extension ``port_trusted`` is now available. This extension
implements the ``port_trusted_vif`` API extension which adds to the port resource
a new boolean field called ``trusted``. This field should be used by admin users
to set the port as trusted what was previously possible only through the port's
``binding:profile`` dictionary. Value of the ``trusted`` field is still visible
in the port's ``binding:profile`` dictionary so that for example Nova still has
it where it is expected to be.
deprecations:
- |
Setting ``trusted`` key directly in the port's ``binding:profile`` is
deprecated and will be forbidden in future releases. Dedicated port's
attribute ``trusted``, added by the API extension ``port_trusted_vif``
should be used instead.

View File

@ -125,6 +125,7 @@ neutron.ml2.extension_drivers =
port_device_profile = neutron.plugins.ml2.extensions.port_device_profile:PortDeviceProfileExtensionDriver
port_hardware_offload_type = neutron.plugins.ml2.extensions.port_hardware_offload_type:PortHardwareOffloadTypeExtensionDriver
port_numa_affinity_policy = neutron.plugins.ml2.extensions.port_numa_affinity_policy:PortNumaAffinityPolicyExtensionDriver
port_trusted = neutron.plugins.ml2.extensions.port_trusted:PortTrustedExtensionDriver
uplink_status_propagation = neutron.plugins.ml2.extensions.uplink_status_propagation:UplinkStatusPropagationExtensionDriver
tag_ports_during_bulk_creation = neutron.plugins.ml2.extensions.tag_ports_during_bulk_creation:TagPortsDuringBulkCreationExtensionDriver
subnet_dns_publish_fixed_ip = neutron.plugins.ml2.extensions.subnet_dns_publish_fixed_ip:SubnetDNSPublishFixedIPExtensionDriver