Merge "Add trusted vif api extension for the port"
This commit is contained in:
commit
19a6e8e626
3
devstack/lib/port_trusted_vif
Normal file
3
devstack/lib/port_trusted_vif
Normal file
@ -0,0 +1,3 @@
|
||||
function configure_port_trusted_ml2_extension {
|
||||
neutron_ml2_extension_driver_add "port_trusted"
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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()
|
@ -1 +1 @@
|
||||
175fa80908e1
|
||||
5bcb7b31ec7d
|
||||
|
37
neutron/db/models/port_trusted.py
Normal file
37
neutron/db/models/port_trusted.py
Normal 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', )
|
66
neutron/db/port_trusted_db.py
Normal file
66
neutron/db/port_trusted_db.py
Normal 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
|
77
neutron/extensions/port_trusted_vif.py
Normal file
77
neutron/extensions/port_trusted_vif.py
Normal 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 {}
|
36
neutron/objects/port/extensions/port_trusted.py
Normal file
36
neutron/objects/port/extensions/port_trusted.py
Normal 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'}}
|
@ -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
|
||||
|
46
neutron/plugins/ml2/extensions/port_trusted.py
Normal file
46
neutron/plugins/ml2/extensions/port_trusted.py
Normal 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)
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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))
|
||||
|
@ -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]
|
||||
|
70
neutron/tests/unit/extensions/test_port_trusted_vif.py
Normal file
70
neutron/tests/unit/extensions/test_port_trusted_vif.py
Normal 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)
|
@ -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})
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user