QinQ API extension implementation in the Neutron server

This patch implements 'qinq' API extension in the Neutron server. It
means that this new attribute is now added for the "vlan" networks to
the network dict and returned through the API.
This patch also adds validation that both "vlan_transparent" and "qinq"
aren't enabled for the same network at the same time as this is not
supported.
This patch adds all the necessary bits to store value of the new
attribute in the Neutron DB and to add support for it to the Network
OVO.
Finally it also adds check_vlan_qinq() method to all mechanism drivers
which are in-tree. For now all of them declare that QinQ vlans are not
supported.

Related-Bug: #1915151
Change-Id: I427edfd580eb06aa4f6904f90ff28cf8b5267397
This commit is contained in:
Slawek Kaplonski 2024-12-09 15:13:45 +01:00
parent 0632996b17
commit e20ef3fa86
18 changed files with 322 additions and 7 deletions

View File

@ -128,6 +128,10 @@ core_opts = [
cfg.BoolOpt('vlan_transparent', default=False,
help=_('If True, then allow plugins that support it to '
'create VLAN transparent networks.')),
cfg.BoolOpt('vlan_qinq', default=False,
help=_('If True, then allow plugins that support it to '
'create VLAN transparent networks using 0x8a88 '
'ethertype.')),
cfg.BoolOpt('filter_validation', default=True,
help=_('If True, then allow plugins to decide '
'whether to perform validations on filter parameters. '

View File

@ -0,0 +1,35 @@
# 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.
#
from alembic import op
import sqlalchemy as sa
# Add qinq column to the Network table
#
# Revision ID: ad80a9f07c5c
# Revises: 5bcb7b31ec7d
# Create Date: 2024-12-09 11:27:41.108660
# revision identifiers, used by Alembic.
revision = 'ad80a9f07c5c'
down_revision = '5bcb7b31ec7d'
def upgrade():
op.add_column(
'networks',
sa.Column('qinq', sa.Boolean(), server_default=None)
)

View File

@ -1 +1 @@
5bcb7b31ec7d
ad80a9f07c5c

View File

@ -321,6 +321,7 @@ class Network(standard_attr.HasStandardAttributes, model_base.BASEV2,
status = sa.Column(sa.String(16))
admin_state_up = sa.Column(sa.Boolean)
vlan_transparent = sa.Column(sa.Boolean, nullable=True)
qinq = sa.Column(sa.Boolean, nullable=True)
rbac_entries = orm.relationship(rbac_db_models.NetworkRBAC,
backref=orm.backref('network',
load_on_pending=True),

27
neutron/db/qinq_db.py Normal file
View File

@ -0,0 +1,27 @@
# 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 network as net_def
from neutron_lib.api.definitions import qinq as qinq_def
from neutron_lib.db import resource_extend
@resource_extend.has_resource_extenders
class Vlanqinq_db_mixin:
"""Mixin class to add vlan QinQ methods to db_base_plugin_v2."""
@staticmethod
@resource_extend.extends([net_def.COLLECTION_NAME])
def _extend_network_dict_vlan_qinq(network_res, network_db):
network_res[qinq_def.QINQ_FIELD] = network_db.qinq
return network_res

View File

@ -0,0 +1,48 @@
# 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 qinq as apidef
from neutron_lib.api import extensions as api_extensions
from neutron_lib.api import validators
from oslo_config import cfg
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
def _disable_extension_by_config(aliases):
if not cfg.CONF.vlan_qinq:
if apidef.ALIAS in aliases:
aliases.remove(apidef.ALIAS)
LOG.info('Disabled VLAN QinQ extension.')
def get_qinq(network):
"""Get the value of vlan_qinq from a network if set.
:param network: The network dict to retrieve the value of vlan_qinq
from.
:returns: The value of vlan_qinq from the network dict if set in
the dict, otherwise False is returned.
"""
return (network[apidef.QINQ_FIELD]
if (apidef.QINQ_FIELD in network and
validators.is_attr_set(network[apidef.QINQ_FIELD]))
else False)
class Qinq(api_extensions.APIExtensionDescriptor):
"""Extension class supporting vlan QinQ networks."""
api_definition = apidef

View File

@ -200,7 +200,8 @@ class ExternalNetwork(base.NeutronDbObject):
class Network(rbac_db.NeutronRbacObject):
# Version 1.0: Initial version
# Version 1.1: Changed 'mtu' to be not nullable
VERSION = '1.1'
# Version 1.2: Added 'qinq' field
VERSION = '1.2'
rbac_db_cls = NetworkRBAC
db_model = models_v2.Network
@ -212,6 +213,7 @@ class Network(rbac_db.NeutronRbacObject):
'status': obj_fields.StringField(nullable=True),
'admin_state_up': obj_fields.BooleanField(nullable=True),
'vlan_transparent': obj_fields.BooleanField(nullable=True),
'qinq': obj_fields.BooleanField(nullable=True),
# TODO(ihrachys): consider converting to a field of stricter type
'availability_zone_hints': obj_fields.ListOfStringsField(
nullable=True),
@ -337,6 +339,8 @@ class Network(rbac_db.NeutronRbacObject):
# mtu will not be nullable after
raise exception.IncompatibleObjectVersion(
objver=target_version, objname=self.__class__.__name__)
if _target_version < (1, 2):
primitive.pop('qinq', None)
@base.NeutronObjectRegistry.register

View File

@ -60,6 +60,10 @@ class L2populationMechanismDriver(api.MechanismDriver):
"""L2population driver vlan transparency support."""
return True
def check_vlan_qinq(self, context):
"""L2population driver doesn't support vlan transparency."""
return False
def _get_ha_port_agents_fdb(
self, context, network_id, router_id):
other_fdb_ports = {}

View File

@ -60,6 +60,10 @@ class MacvtapMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase):
"""Macvtap driver vlan transparency support."""
return False
def check_vlan_qinq(self, context):
"""Currently Macvtap driver doesn't support QinQ vlan."""
return False
def _is_live_migration(self, context):
# We cannot just check if
# context.original['host_id'] != context.current['host_id']

View File

@ -201,6 +201,10 @@ class SriovNicSwitchMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase):
"""SR-IOV driver vlan transparency support."""
return True
def check_vlan_qinq(self, context):
"""Currently SR-IOV driver doesn't support QinQ vlan."""
return False
def _get_vif_details(self, segment):
network_type = segment[api.NETWORK_TYPE]
if network_type == constants.TYPE_FLAT:

View File

@ -113,6 +113,10 @@ class OpenvswitchMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase):
"""Currently Openvswitch driver doesn't support vlan transparency."""
return False
def check_vlan_qinq(self, context):
"""Currently Openvswitch driver doesn't support QinQ vlan."""
return False
def bind_port(self, context):
vnic_type = context.current.get(portbindings.VNIC_TYPE,
portbindings.VNIC_NORMAL)

View File

@ -225,6 +225,10 @@ class OVNMechanismDriver(api.MechanismDriver):
return (context.current.get(provider_net.NETWORK_TYPE)
in vlan_transparency_network_types)
def check_vlan_qinq(self, context):
"""OVN driver vlan QinQ support."""
return False
def _setup_vif_port_bindings(self):
self.supported_vnic_types = ovn_const.OVN_SUPPORTED_VNIC_TYPES
self.vif_details = {

View File

@ -23,6 +23,7 @@ from neutron_lib.db import api as db_api
from neutron_lib import exceptions as exc
from neutron_lib.exceptions import multiprovidernet as mpnet_exc
from neutron_lib.exceptions import placement as place_exc
from neutron_lib.exceptions import vlanqinq as qinq_exc
from neutron_lib.exceptions import vlantransparent as vlan_exc
from neutron_lib.plugins.ml2 import api
from oslo_config import cfg
@ -469,6 +470,19 @@ class MechanismManager(stevedore.named.NamedExtensionManager):
if not driver.obj.check_vlan_transparency(context):
raise vlan_exc.VlanTransparencyDriverError()
def _check_vlan_qinq(self, context):
"""Helper method for checking vlan qinq support.
:param context: context parameter to pass to each method call
:raises: neutron_lib.exceptions.qinq.
VlanQinqDriverError if any mechanism driver doesn't
support vlan transparency.
"""
if context.current.get('qinq'):
for driver in self.ordered_mech_drivers:
if not driver.obj.check_vlan_qinq(context):
raise qinq_exc.VlanQinqDriverError()
def start_driver_rpc_listeners(self):
servers = []
for driver in self.ordered_mech_drivers:
@ -529,6 +543,7 @@ class MechanismManager(stevedore.named.NamedExtensionManager):
that all mechanism drivers are called in this case.
"""
self._check_vlan_transparency(context)
self._check_vlan_qinq(context)
self._call_on_drivers("create_network_precommit", context,
raise_db_retriable=True)

View File

@ -52,6 +52,7 @@ from neutron_lib.api.definitions import port_security as psec
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import portbindings_extended as pbe_ext
from neutron_lib.api.definitions import provider_net
from neutron_lib.api.definitions import qinq as qinq_apidef
from neutron_lib.api.definitions import quota_check_limit
from neutron_lib.api.definitions import rbac_address_groups as rbac_ag_apidef
from neutron_lib.api.definitions import rbac_address_scope
@ -128,12 +129,14 @@ from neutron.db import extradhcpopt_db
from neutron.db.models import securitygroup as sg_models
from neutron.db import models_v2
from neutron.db import provisioning_blocks
from neutron.db import qinq_db
from neutron.db import securitygroups_rpc_base as sg_db_rpc
from neutron.db import segments_db
from neutron.db import subnet_service_type_mixin
from neutron.db import vlantransparent_db
from neutron.extensions import dhcpagentscheduler as dhcp_ext
from neutron.extensions import filter_validation
from neutron.extensions import qinq
from neutron.extensions import quota_check_limit_default
from neutron.extensions import security_groups_default_rules as \
sg_default_rules_ext
@ -186,7 +189,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
extradhcpopt_db.ExtraDhcpOptMixin,
address_scope_db.AddressScopeDbMixin,
subnet_service_type_mixin.SubnetServiceTypeMixin,
address_group_db.AddressGroupDbMixin):
address_group_db.AddressGroupDbMixin,
qinq_db.Vlanqinq_db_mixin):
"""Implement the Neutron L2 abstractions using modules.
@ -253,6 +257,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
sg_default_rules_ext.ALIAS,
sg_rules_default_sg.ALIAS,
subnet_ext_net_def.ALIAS,
qinq_apidef.ALIAS,
]
# List of agent types for which all binding_failed ports should try to be
@ -268,6 +273,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
vlantransparent._disable_extension_by_config(aliases)
filter_validation._disable_extension_by_config(aliases)
dhcp_ext.disable_extension_by_config(aliases)
qinq._disable_extension_by_config(aliases)
self._aliases = self._filter_extensions_by_mech_driver(aliases)
return self._aliases
@ -1212,10 +1218,23 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
self.type_manager.extend_network_dict_provider(context, result)
# Update the transparent vlan if configured
is_vlan_transparent = None
if extensions.is_extension_supported(self, 'vlan-transparent'):
vlt = vlan_apidef.get_vlan_transparent(net_data)
net_db['vlan_transparent'] = vlt
result['vlan_transparent'] = vlt
is_vlan_transparent = vlan_apidef.get_vlan_transparent(
net_data)
net_db['vlan_transparent'] = is_vlan_transparent
result['vlan_transparent'] = is_vlan_transparent
# Update the vlan QinQ if configured
qinq_value = None
if extensions.is_extension_supported(self, qinq_apidef.ALIAS):
qinq_value = qinq.get_qinq(net_data)
net_db['qinq'] = qinq_value
result['qinq'] = qinq_value
# QinQ and vlan_transparent can't be both set to True
if is_vlan_transparent and qinq_value:
msg = _("Attributes 'vlan_transparent' and 'qinq' can not be "
"set to True for the same network.")
raise exc.BadRequest(resource='network', msg=msg)
az_hints = utils.get_az_hints(net_data)
if az_hints:
self.validate_availability_zones(context, 'network', az_hints)

View File

@ -0,0 +1,132 @@
# 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 provider_net
from neutron_lib.api.definitions import qinq as qinq_apidef
from neutron_lib.api.definitions import vlantransparent as vlan_apidef
from oslo_config import cfg
from webob import exc as web_exc
from neutron.db import qinq_db
from neutron.plugins.ml2 import plugin as ml2_plugin
from neutron.tests.common import test_db_base_plugin_v2
from neutron.tests.unit import testlib_api
class QinqExtensionTestPlugin(ml2_plugin.Ml2Plugin,
qinq_db.Vlanqinq_db_mixin):
"""Test plugin to mixin the VLAN transparent extensions."""
supported_extension_aliases = [provider_net.ALIAS,
qinq_apidef.ALIAS,
vlan_apidef.ALIAS]
class QinqExtensionTestCase(test_db_base_plugin_v2.TestNetworksV2):
fmt = 'json'
def setUp(self):
plugin = ('neutron.tests.unit.extensions.test_qinq.'
'QinqExtensionTestPlugin')
cfg.CONF.set_override('network_vlan_ranges', 'datacentre',
group='ml2_type_vlan')
super().setUp(plugin=plugin)
def test_create_network_with_qinq_attr(self):
arg_list = (
qinq_apidef.QINQ_FIELD),
net_kwargs = {
qinq_apidef.QINQ_FIELD: True
}
with self.network(name='net1', as_admin=True,
arg_list=arg_list, **net_kwargs) as net:
req = self.new_show_request('networks', net['network']['id'])
res = self.deserialize(self.fmt, req.get_response(self.api))
self.assertEqual(net['network']['name'],
res['network']['name'])
self.assertTrue(res['network'][qinq_apidef.QINQ_FIELD])
def test_create_network_with_bad_qinq_attr(self):
arg_list = (
qinq_apidef.QINQ_FIELD),
net_kwargs = {
qinq_apidef.QINQ_FIELD: 'this is not boolean value',
}
with testlib_api.ExpectedException(
web_exc.HTTPClientError) as ctx_manager:
with self.network(name='net1', as_admin=True,
arg_list=arg_list, **net_kwargs):
pass
self.assertEqual(web_exc.HTTPClientError.code,
ctx_manager.exception.code)
def test_network_update_with_qinq_exception(self):
arg_list = (
qinq_apidef.QINQ_FIELD),
net_kwargs = {
qinq_apidef.QINQ_FIELD: False,
}
with self.network(name='net1', as_admin=True,
arg_list=arg_list, **net_kwargs) as net:
self._update('networks', net['network']['id'],
{'network': {qinq_apidef.QINQ_FIELD: True}},
web_exc.HTTPBadRequest.code)
req = self.new_show_request('networks', net['network']['id'])
res = self.deserialize(self.fmt, req.get_response(self.api))
self.assertEqual(net['network']['name'],
res['network']['name'])
self.assertFalse(res['network'][qinq_apidef.QINQ_FIELD])
def _test_create_network_qinq_and_transparent_vlan(self, qinq_value, vlt):
arg_list = (
qinq_apidef.QINQ_FIELD,
vlan_apidef.VLANTRANSPARENT)
net_kwargs = {
qinq_apidef.QINQ_FIELD: qinq_value,
vlan_apidef.VLANTRANSPARENT: vlt,
}
# Both vlan_transparent and qinq can't be set for the same network
if qinq_value and vlt:
with testlib_api.ExpectedException(
web_exc.HTTPClientError) as ctx_manager:
with self.network(name='net1', as_admin=True,
arg_list=arg_list, **net_kwargs):
pass
self.assertEqual(web_exc.HTTPBadRequest.code,
ctx_manager.exception.code)
return
# In any other case it should work fine
with self.network(name='net1', as_admin=True,
arg_list=arg_list, **net_kwargs) as net:
req = self.new_show_request('networks', net['network']['id'])
res = self.deserialize(self.fmt, req.get_response(self.api))
self.assertEqual(net['network']['name'],
res['network']['name'])
self.assertEqual(qinq_value,
res['network'][qinq_apidef.QINQ_FIELD])
self.assertEqual(vlt, res['network'][vlan_apidef.VLANTRANSPARENT])
def test_create_network_qinq_disabled_transparent_vlan_enabled(self):
self._test_create_network_qinq_and_transparent_vlan(False, True)
def test_create_network_qinq_disabled_transparent_vlan_disabled(self):
self._test_create_network_qinq_and_transparent_vlan(False, False)
def test_create_network_qinq_enabled_transparent_vlan_disabled(self):
self._test_create_network_qinq_and_transparent_vlan(True, False)
def test_create_network_qinq_enabled_transparent_vlan_enabled(self):
self._test_create_network_qinq_and_transparent_vlan(True, True)

View File

@ -260,6 +260,11 @@ class NetworkDbObjectTestCase(obj_test_base.BaseDbObjectTestCase,
obj = network.Network.get_object(self.context, id=obj.id)
self.assertEqual('bar.com', obj.dns_domain)
def test_v1_2_to_v1_1_drops_qinq_attribute(self):
network_obj = self._make_object(self.obj_fields[0])
network_v1_1 = network_obj.obj_to_primitive(target_version='1.1')
self.assertNotIn('qinq', network_v1_1['versioned_object.data'])
class SegmentHostMappingIfaceObjectTestCase(
obj_test_base.BaseObjectIfaceTestCase):

View File

@ -65,7 +65,7 @@ object_data = {
'MeteringLabelRule': '2.0-0ad09894c62e1ce6e868f725158959ba',
'Log': '1.0-6391351c0f34ed34375a19202f361d24',
'NDPProxy': '1.0-a6597d9caac3bb0d63f943f82e4dda8c',
'Network': '1.1-c3e9ecc0618ee934181d91b143a48901',
'Network': '1.2-0221c921b40f11b237e6a274984f238a',
'NetworkDhcpAgentBinding': '1.1-d9443c88809ffa4c45a0a5a48134b54a',
'NetworkDNSDomain': '1.0-420db7910294608534c1e2e30d6d8319',
'NetworkPortSecurity': '1.0-b30802391a87945ee9c07582b4ff95e3',

View File

@ -96,6 +96,11 @@ class LoggerMechanismDriver(api.MechanismDriver):
self._log_diff_call("check_vlan_transparency", context)
return True
def check_vlan_qinq(self, context):
self._log_network_call("check_vlan_qinq", context)
self._log_diff_call("check_vlan_qinq", context)
return True
def _log_subnet_call(self, method_name, context):
LOG.info("%(method)s called with subnet settings %(current)s "
"(original settings %(original)s)",