Add trusted vif api extension for the port

This patch adds implementation of the "port_trusted_vif" API extension
as ml2 extension.
With this extension enabled, it is now possible for ADMIN users to set
port as trusted without modifying directly 'binding:profile' field
which is supposed to be just for machine to machine communication.

Value set in the 'trusted' attribute of the port is included in the
port's binding:profile so that it is still in the same place where e.g.
Nova expects it.

For now setting this flag directly in the port's binding:profile field
is not forbidden and only warning is generated in such case but in
future releases it should be forbiden and only allowed to be done using
this new attribute of the port resource.

This patch implements also definition of the new API extension directly
in Neutron. It is temporary and will be removed once patch [1] in
neutron-lib will be merged and released.

[1] https://review.opendev.org/c/openstack/neutron-lib/+/923860

Closes-Bug: #2060916
Change-Id: I69785c5d72a5dc659c5a2f27e043c686790b4d2b
This commit is contained in:
Slawek Kaplonski 2024-08-09 16:47:04 +02:00
parent d73cc2eff6
commit 104cbf9e60
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/octavia
source $LIBDIR/loki source $LIBDIR/loki
source $LIBDIR/local_ip source $LIBDIR/local_ip
source $LIBDIR/port_trusted_vif
# source the OVS/OVN compilation helper methods # source the OVS/OVN compilation helper methods
source $TOP_DIR/lib/neutron_plugins/ovs_source source $TOP_DIR/lib/neutron_plugins/ovs_source
@ -98,6 +99,9 @@ if [[ "$1" == "stack" ]]; then
fi fi
configure_l3_agent configure_l3_agent
fi 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 if [ $NEUTRON_CORE_PLUGIN = ml2 ]; then
configure_ml2_extension_drivers configure_ml2_extension_drivers
fi 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.api.definitions import vpn_endpoint_groups
from neutron_lib import constants from neutron_lib import constants
from neutron.extensions import port_trusted_vif
from neutron.extensions import quotasv2_detail from neutron.extensions import quotasv2_detail
from neutron.extensions import security_groups_default_rules 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.ALIAS,
port_numa_affinity_policy_socket.ALIAS, port_numa_affinity_policy_socket.ALIAS,
port_security.ALIAS, port_security.ALIAS,
port_trusted_vif.ALIAS,
provider_net.ALIAS, provider_net.ALIAS,
port_resource_request.ALIAS, port_resource_request.ALIAS,
qos.ALIAS, qos.ALIAS,

View File

@ -297,6 +297,15 @@ rules = [
), ),
operations=ACTION_POST, 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( policy.DocumentedRuleDefault(
name='get_port', name='get_port',
@ -383,6 +392,13 @@ rules = [
description='Get ``hints`` attribute of a port', description='Get ``hints`` attribute of a port',
operations=ACTION_GET, 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( policy.DocumentedRuleDefault(
name='get_ports_tags', name='get_ports_tags',
check_str=neutron_policy.policy_or( check_str=neutron_policy.policy_or(
@ -641,6 +657,13 @@ rules = [
description='Update ``hints`` attribute of a port', description='Update ``hints`` attribute of a port',
operations=ACTION_PUT, 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( policy.DocumentedRuleDefault(
name='update_ports_tags', name='update_ports_tags',
check_str=neutron_policy.policy_or( 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.7: Added port_device field
# Version 1.8: Added hints field # Version 1.8: Added hints field
# Version 1.9: Added hardware_offload_type 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 db_model = models_v2.Port
@ -394,6 +395,7 @@ class Port(base.NeutronDbObject):
'numa_affinity_policy': obj_fields.StringField(nullable=True), 'numa_affinity_policy': obj_fields.StringField(nullable=True),
'device_profile': obj_fields.StringField(nullable=True), 'device_profile': obj_fields.StringField(nullable=True),
'hardware_offload_type': 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 # TODO(ihrachys): consider adding a 'dns_assignment' fully synthetic
# field in later object iterations # field in later object iterations
@ -420,6 +422,7 @@ class Port(base.NeutronDbObject):
'qos_network_policy_id', 'qos_network_policy_id',
'security', 'security',
'security_group_ids', 'security_group_ids',
'trusted',
] ]
fields_need_translation = { fields_need_translation = {
@ -591,6 +594,10 @@ class Port(base.NeutronDbObject):
db_obj.hardware_offload_type.hardware_offload_type) db_obj.hardware_offload_type.hardware_offload_type)
fields_to_change.append('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) self.obj_reset_changes(fields_to_change)
def obj_make_compatible(self, primitive, target_version): def obj_make_compatible(self, primitive, target_version):
@ -627,6 +634,8 @@ class Port(base.NeutronDbObject):
primitive.pop('hints', None) primitive.pop('hints', None)
if _target_version < (1, 9): if _target_version < (1, 9):
primitive.pop('hardware_offload_type', None) primitive.pop('hardware_offload_type', None)
if _target_version < (1, 10):
primitive.pop('trusted', None)
@classmethod @classmethod
@db_api.CONTEXT_READER @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

@ -563,7 +563,17 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
if profile not in (None, const.ATTR_NOT_SPECIFIED, if profile not in (None, const.ATTR_NOT_SPECIFIED,
self._get_profile(binding)): 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: if len(binding.profile) > models.BINDING_PROFILE_LEN:
msg = _("binding:profile value too large") msg = _("binding:profile value too large")
raise exc.InvalidInput(error_message=msg) 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.HOST_ID] = binding.host
port[portbindings.VIF_TYPE] = binding.vif_type port[portbindings.VIF_TYPE] = binding.vif_type
port[portbindings.VIF_DETAILS] = self._get_vif_details(binding) 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): def _update_port_dict_bound_drivers(self, port, binding_levels):
levels = {str(bl.level): bl.driver for bl in 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'] 'and shared filesystem ports') % port['id']
raise exc.BadRequest(resource='port', msg=msg) 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 ( res = {key: binding[key] for key in (
pbe_ext.HOST, pbe_ext.VIF_TYPE, pbe_ext.VNIC_TYPE, pbe_ext.STATUS)} pbe_ext.HOST, pbe_ext.VIF_TYPE, pbe_ext.VNIC_TYPE, pbe_ext.STATUS)}
if isinstance(binding, ports_obj.PortBinding): if isinstance(binding, ports_obj.PortBinding):
@ -2689,8 +2702,19 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
else: else:
res[pbe_ext.PROFILE] = self._get_profile(binding) res[pbe_ext.PROFILE] = self._get_profile(binding)
res[pbe_ext.VIF_DETAILS] = self._get_vif_details(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) 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): def _get_port_binding_attrs(self, binding, host=None):
return {portbindings.VNIC_TYPE: binding.get(pbe_ext.VNIC_TYPE), return {portbindings.VNIC_TYPE: binding.get(pbe_ext.VNIC_TYPE),
portbindings.HOST_ID: binding.get(pbe_ext.HOST) or host, 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): def create_port_binding(self, context, port_id, binding):
attrs = binding[pbe_ext.RESOURCE_NAME] attrs = binding[pbe_ext.RESOURCE_NAME]
with db_api.CONTEXT_WRITER.using(context): 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) self._validate_port_supports_multiple_bindings(port_db)
if self._get_binding_for_host(port_db.port_bindings, if self._get_binding_for_host(port_db.port_bindings,
attrs[pbe_ext.HOST]): attrs[pbe_ext.HOST]):
@ -2755,7 +2780,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
with db_api.CONTEXT_WRITER.using(context): with db_api.CONTEXT_WRITER.using(context):
bind_context._binding.persist_state_to_session(context.session) bind_context._binding.persist_state_to_session(context.session)
db.set_binding_levels(context, bind_context._binding_levels) 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 @utils.transaction_guard
@db_api.retry_if_session_inactive() @db_api.retry_if_session_inactive()
@ -2771,7 +2796,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
bindings = ports_obj.PortBinding.get_objects( bindings = ports_obj.PortBinding.get_objects(
context, _pager=pager, port_id=port_id, **filters) 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] for binding in bindings]
@utils.transaction_guard @utils.transaction_guard
@ -2785,7 +2810,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
port_id=port_id) port_id=port_id)
if not binding: if not binding:
raise exc.PortBindingNotFound(port_id=port_id, host=host) 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): def _get_binding_for_host(self, bindings, host):
for binding in bindings: 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): def update_port_binding(self, context, host, port_id, binding):
attrs = binding[pbe_ext.RESOURCE_NAME] attrs = binding[pbe_ext.RESOURCE_NAME]
with db_api.CONTEXT_WRITER.using(context): 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) self._validate_port_supports_multiple_bindings(port_db)
original_binding = self._get_binding_for_host( original_binding = self._get_binding_for_host(
port_db.port_bindings, host) port_db.port_bindings, host)
@ -2824,7 +2850,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
with db_api.CONTEXT_WRITER.using(context): with db_api.CONTEXT_WRITER.using(context):
bind_context._binding.persist_state_to_session(context.session) bind_context._binding.persist_state_to_session(context.session)
db.set_binding_levels(context, bind_context._binding_levels) 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 @utils.transaction_guard
@db_api.retry_if_session_inactive() @db_api.retry_if_session_inactive()
@ -2834,7 +2860,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
# fixed # fixed
if isinstance(port_id, dict): if isinstance(port_id, dict):
port_id = port_id['port_id'] 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) self._validate_port_supports_multiple_bindings(port_db)
active_binding = p_utils.get_port_binding_by_status_and_host( active_binding = p_utils.get_port_binding_by_status_and_host(
port_db.port_bindings, const.ACTIVE) port_db.port_bindings, const.ACTIVE)
@ -2874,7 +2901,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
network['id']) network['id'])
self.notifier.binding_activate(context, port_id, self.notifier.binding_activate(context, port_id,
inactive_binding.host) 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) raise exc.PortBindingError(port_id=port_id, host=host)
@utils.transaction_guard @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"
NETWORK_API_EXTENSIONS+=",port-numa-affinity-policy-socket" NETWORK_API_EXTENSIONS+=",port-numa-affinity-policy-socket"
NETWORK_API_EXTENSIONS+=",port-security-groups-filtering" NETWORK_API_EXTENSIONS+=",port-security-groups-filtering"
NETWORK_API_EXTENSIONS+=",port-trusted-vif"
NETWORK_API_EXTENSIONS+=",segment" NETWORK_API_EXTENSIONS+=",segment"
NETWORK_API_EXTENSIONS+=",segments-peer-subnet-host-routes" NETWORK_API_EXTENSIONS+=",segments-peer-subnet-host-routes"
NETWORK_API_EXTENSIONS+=",service-type" NETWORK_API_EXTENSIONS+=",service-type"

View File

@ -552,6 +552,16 @@ class AdminTests(PortAPITestCase):
'create_port:hints', 'create_port:hints',
self.alt_target)) 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): def test_get_port(self):
self.assertTrue( self.assertTrue(
policy.enforce(self.context, 'get_port', self.target)) policy.enforce(self.context, 'get_port', self.target))
@ -606,6 +616,14 @@ class AdminTests(PortAPITestCase):
policy.enforce( policy.enforce(
self.context, 'get_port:hints', self.alt_target)) 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): def test_get_ports_tags(self):
self.assertTrue( self.assertTrue(
policy.enforce(self.context, 'get_ports_tags', self.target)) policy.enforce(self.context, 'get_ports_tags', self.target))
@ -747,6 +765,16 @@ class AdminTests(PortAPITestCase):
'update_port:hints', 'update_port:hints',
self.alt_target)) 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): def test_delete_port(self):
self.assertTrue( self.assertTrue(
policy.enforce(self.context, 'delete_port', self.target)) policy.enforce(self.context, 'delete_port', self.target))
@ -899,6 +927,18 @@ class ProjectManagerTests(AdminTests):
self.context, 'create_port:hints', self.context, 'create_port:hints',
self.alt_target) 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): def test_get_port(self):
self.assertTrue( self.assertTrue(
policy.enforce(self.context, 'get_port', self.target)) policy.enforce(self.context, 'get_port', self.target))
@ -966,6 +1006,16 @@ class ProjectManagerTests(AdminTests):
policy.enforce, self.context, 'get_port:hints', policy.enforce, self.context, 'get_port:hints',
self.alt_target) 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): def test_get_ports_tags(self):
self.assertTrue( self.assertTrue(
policy.enforce(self.context, 'get_ports_tags', self.target)) policy.enforce(self.context, 'get_ports_tags', self.target))
@ -1120,6 +1170,16 @@ class ProjectManagerTests(AdminTests):
policy.enforce, policy.enforce,
self.context, 'update_port:hints', self.alt_target) 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): def test_update_ports_tags(self):
self.assertTrue( self.assertTrue(
policy.enforce(self.context, 'update_ports_tags', self.target)) 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', 'mac_address', 'name', 'fixed_ips',
'tenant_id', 'device_owner', 'security_groups', 'tenant_id', 'device_owner', 'security_groups',
'propagate_uplink_status', 'numa_affinity_policy', 'propagate_uplink_status', 'numa_affinity_policy',
'device_profile', 'hints', 'hardware_offload_type') + 'device_profile', 'hints', 'hardware_offload_type',
(arg_list or ())): 'trusted') + (arg_list or ())):
# Arg must be present # Arg must be present
if arg in kwargs: if arg in kwargs:
data['port'][arg] = kwargs[arg] 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', 'NetworkSegment': '1.0-57b7f2960971e3b95ded20cbc59244a8',
'NetworkSegmentRange': '1.0-bdec1fffc9058ea676089b1f2f2b3cf3', 'NetworkSegmentRange': '1.0-bdec1fffc9058ea676089b1f2f2b3cf3',
'NetworkSubnetLock': '1.0-140de39d4b86ae346dc3d70b885bea53', 'NetworkSubnetLock': '1.0-140de39d4b86ae346dc3d70b885bea53',
'Port': '1.9-25f8da7ed95f1538f9e08657b0b450c1', 'Port': '1.10-ae84f686bfc3deb4017495134da6ef04',
'PortHardwareOffloadType': '1.0-5f424d02b144fd1832ac3e6b03662674', 'PortHardwareOffloadType': '1.0-5f424d02b144fd1832ac3e6b03662674',
'PortDeviceProfile': '1.0-b98c7083cc3e93d176fd7a91ae13af32', 'PortDeviceProfile': '1.0-b98c7083cc3e93d176fd7a91ae13af32',
'PortHints': '1.0-9ebf6e12fa427809476a92c7432352b8', 'PortHints': '1.0-9ebf6e12fa427809476a92c7432352b8',
@ -85,6 +85,7 @@ object_data = {
'PortForwarding': '1.3-402b1fb5a754808b82a966c95f468113', 'PortForwarding': '1.3-402b1fb5a754808b82a966c95f468113',
'PortSecurity': '1.0-b30802391a87945ee9c07582b4ff95e3', 'PortSecurity': '1.0-b30802391a87945ee9c07582b4ff95e3',
'PortUplinkStatusPropagation': '1.1-f0a4ca451a941910376c33616dea5de2', 'PortUplinkStatusPropagation': '1.1-f0a4ca451a941910376c33616dea5de2',
'PortTrusted': '1.0-8312fb91937412cdeb92c3279059c7ce',
'ProviderResourceAssociation': '1.0-05ab2d5a3017e5ce9dd381328f285f34', 'ProviderResourceAssociation': '1.0-05ab2d5a3017e5ce9dd381328f285f34',
'ProvisioningBlock': '1.0-c19d6d05bfa8143533471c1296066125', 'ProvisioningBlock': '1.0-c19d6d05bfa8143533471c1296066125',
'QosBandwidthLimitRule': '1.5-51b662b12a8d1dfa89288d826c6d26d3', 'QosBandwidthLimitRule': '1.5-51b662b12a8d1dfa89288d826c6d26d3',

View File

@ -535,6 +535,12 @@ class PortDbObjectTestCase(obj_test_base.BaseDbObjectTestCase,
self.assertNotIn('hints', self.assertNotIn('hints',
port_v1_7['versioned_object.data']) 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): def test_get_ports_ids_by_security_groups_except_router(self):
sg_id = self._create_test_security_group_id() sg_id = self._create_test_security_group_id()
filter_owner = constants.ROUTER_INTERFACE_OWNERS_SNAT filter_owner = constants.ROUTER_INTERFACE_OWNERS_SNAT

View File

@ -354,6 +354,7 @@ class ExtendedPortBindingTestCase(test_plugin.NeutronDbPluginV2TestCase):
cfg.CONF.set_override('mechanism_drivers', cfg.CONF.set_override('mechanism_drivers',
['logger', 'test'], ['logger', 'test'],
'ml2') 'ml2')
cfg.CONF.set_override('extension_drivers', ['port_trusted'], 'ml2')
driver_type.register_ml2_drivers_vlan_opts() driver_type.register_ml2_drivers_vlan_opts()
cfg.CONF.set_override('network_vlan_ranges', 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) subresource='bindings', sub_id=host).get_response(self.api)
return response return response
def _create_port_and_binding(self, **kwargs): # todo(slaweq): here I can add trusted to be checked too
device_owner = '%s%s' % (const.DEVICE_OWNER_COMPUTE_PREFIX, 'nova') def _create_port_and_binding(self, trusted=None, **kwargs):
with self.port(device_owner=device_owner) as port: 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'] port_id = port['port']['id']
binding = self._make_port_binding(self.fmt, port_id, self.host, binding = self._make_port_binding(self.fmt, port_id, self.host,
**kwargs)['binding'] **kwargs)['binding']
@ -447,12 +454,13 @@ class ExtendedPortBindingTestCase(test_plugin.NeutronDbPluginV2TestCase):
self.assertEqual({'port_filter': False}, self.assertEqual({'port_filter': False},
binding[pbe_ext.VIF_DETAILS]) 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.assertFalse(binding[pbe_ext.HOST])
self.assertEqual(portbindings.VIF_TYPE_UNBOUND, self.assertEqual(portbindings.VIF_TYPE_UNBOUND,
binding[pbe_ext.VIF_TYPE]) binding[pbe_ext.VIF_TYPE])
self.assertEqual({}, binding[pbe_ext.VIF_DETAILS]) 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): def test_create_port_binding(self):
profile = {'key1': 'value1'} profile = {'key1': 'value1'}
@ -641,12 +649,32 @@ class ExtendedPortBindingTestCase(test_plugin.NeutronDbPluginV2TestCase):
self.assertEqual(1, len(retrieved_bindings)) self.assertEqual(1, len(retrieved_bindings))
self._assert_bound_port_binding(retrieved_bindings[0]) 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): def test_show_port_binding(self):
port, new_binding = self._create_port_and_binding() port, new_binding = self._create_port_and_binding()
retrieved_binding = self._show_port_binding( retrieved_binding = self._show_port_binding(
port['id'], self.host, raw_response=False)['binding'] port['id'], self.host, raw_response=False)['binding']
self._assert_bound_port_binding(retrieved_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): def test_show_port_binding_with_fields(self):
port, new_binding = self._create_port_and_binding() port, new_binding = self._create_port_and_binding()
fields = 'fields=%s' % pbe_ext.HOST 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_device_profile = neutron.plugins.ml2.extensions.port_device_profile:PortDeviceProfileExtensionDriver
port_hardware_offload_type = neutron.plugins.ml2.extensions.port_hardware_offload_type:PortHardwareOffloadTypeExtensionDriver 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_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 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 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 subnet_dns_publish_fixed_ip = neutron.plugins.ml2.extensions.subnet_dns_publish_fixed_ip:SubnetDNSPublishFixedIPExtensionDriver