diff --git a/etc/neutron/plugins/ml2/ml2_conf.ini b/etc/neutron/plugins/ml2/ml2_conf.ini index 3e5820608..4ad62e3ee 100644 --- a/etc/neutron/plugins/ml2/ml2_conf.ini +++ b/etc/neutron/plugins/ml2/ml2_conf.ini @@ -3,15 +3,22 @@ # (ListOpt) List of network type driver entrypoints to be loaded from # the quantum.ml2.type_drivers namespace. # -# type_drivers = local,flat,vlan,gre -# Example: type_drivers = flat,vlan,gre +# type_drivers = local,flat,vlan,gre,vxlan +# Example: type_drivers = flat,vlan,gre,vxlan # (ListOpt) Ordered list of network_types to allocate as tenant # networks. The default value 'local' is useful for single-box testing # but provides no connectivity between hosts. # # tenant_network_types = local -# Example: tenant_network_types = vlan,gre +# Example: tenant_network_types = vlan,gre,vxlan + +# (StrOpt) Multicast group for the VXLAN interface. When configured, will +# enable sending all broadcast traffic to this multicast group. When left +# unconfigured, will disable multicast VXLAN mode. +# +# vxlan_group = +# Example: vxlan_group = 239.1.1.1 # (ListOpt) Ordered list of networking mechanism driver entrypoints # to be loaded from the neutron.ml2.mechanism_drivers namespace. diff --git a/neutron/db/migration/alembic_migrations/versions/477a4488d3f4_ml2_vxlan_type_driver.py b/neutron/db/migration/alembic_migrations/versions/477a4488d3f4_ml2_vxlan_type_driver.py new file mode 100644 index 000000000..ecb8a5d9a --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/477a4488d3f4_ml2_vxlan_type_driver.py @@ -0,0 +1,69 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 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. +# + +"""DB Migration for ML2 VXLAN Type Driver + +Revision ID: 477a4488d3f4 +Revises: 20ae61555e95 +Create Date: 2013-07-09 14:14:33.158502 + +""" + +# revision identifiers, used by Alembic. +revision = '477a4488d3f4' +down_revision = '20ae61555e95' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = [ + 'neutron.plugins.ml2.plugin.Ml2Plugin' +] + +from alembic import op +import sqlalchemy as sa + + +from neutron.db import migration + + +def upgrade(active_plugin=None, options=None): + if not migration.should_run(active_plugin, migration_for_plugins): + return + + op.create_table( + 'ml2_vxlan_allocations', + sa.Column('vxlan_vni', sa.Integer, nullable=False, + autoincrement=False), + sa.Column('allocated', sa.Boolean, nullable=False), + sa.PrimaryKeyConstraint('vxlan_vni') + ) + + op.create_table( + 'ml2_vxlan_endpoints', + sa.Column('ip_address', sa.String(length=64)), + sa.Column('udp_port', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('ip_address'), + sa.PrimaryKeyConstraint('udp_port') + ) + + +def downgrade(active_plugin=None, options=None): + if not migration.should_run(active_plugin, migration_for_plugins): + return + + op.drop_table('ml2_vxlan_allocations') + op.drop_table('ml2_vxlan_endpoint') diff --git a/neutron/plugins/ml2/config.py b/neutron/plugins/ml2/config.py index a24152523..bbec2ab1d 100644 --- a/neutron/plugins/ml2/config.py +++ b/neutron/plugins/ml2/config.py @@ -20,7 +20,7 @@ from neutron import scheduler ml2_opts = [ cfg.ListOpt('type_drivers', - default=['local', 'flat', 'vlan', 'gre'], + default=['local', 'flat', 'vlan', 'gre', 'vxlan'], help=_("List of network type driver entrypoints to be loaded " "from the neutron.ml2.type_drivers namespace.")), cfg.ListOpt('tenant_network_types', diff --git a/neutron/plugins/ml2/drivers/type_gre.py b/neutron/plugins/ml2/drivers/type_gre.py index ba565ee9a..906d61f86 100644 --- a/neutron/plugins/ml2/drivers/type_gre.py +++ b/neutron/plugins/ml2/drivers/type_gre.py @@ -26,6 +26,8 @@ from neutron.plugins.ml2.drivers import type_tunnel LOG = log.getLogger(__name__) +TYPE_GRE = 'gre' + gre_opts = [ cfg.ListOpt('tunnel_id_ranges', default=[], @@ -60,11 +62,15 @@ class GreTypeDriver(api.TypeDriver, type_tunnel.TunnelTypeDriver): def get_type(self): - return type_tunnel.TYPE_GRE + return TYPE_GRE def initialize(self): self.gre_id_ranges = [] - self._parse_gre_id_ranges() + self._parse_tunnel_ranges( + cfg.CONF.ml2_type_gre.tunnel_id_ranges, + self.gre_id_ranges, + TYPE_GRE + ) self._sync_gre_allocations() def validate_provider_segment(self, segment): @@ -109,7 +115,7 @@ class GreTypeDriver(api.TypeDriver, LOG.debug(_("Allocating gre tunnel id %(gre_id)s"), {'gre_id': alloc.gre_id}) alloc.allocated = True - return {api.NETWORK_TYPE: type_tunnel.TYPE_GRE, + return {api.NETWORK_TYPE: TYPE_GRE, api.PHYSICAL_NETWORK: None, api.SEGMENTATION_ID: alloc.gre_id} @@ -134,21 +140,6 @@ class GreTypeDriver(api.TypeDriver, except sa_exc.NoResultFound: LOG.warning(_("gre_id %s not found"), gre_id) - def _parse_gre_id_ranges(self): - for entry in cfg.CONF.ml2_type_gre.tunnel_id_ranges: - entry = entry.strip() - try: - tun_min, tun_max = entry.split(':') - tun_min = tun_min.strip() - tun_max = tun_max.strip() - self.gre_id_ranges.append((int(tun_min), int(tun_max))) - except ValueError as ex: - LOG.error(_("Invalid tunnel ID range: '%(range)s' - %(e)s. " - "Agent terminated!"), - {'range': cfg.CONF.ml2_type_gre.tunnel_id_ranges, - 'e': ex}) - LOG.info(_("gre ID ranges: %s"), self.gre_id_ranges) - def _sync_gre_allocations(self): """Synchronize gre_allocations table with configured tunnel ranges.""" diff --git a/neutron/plugins/ml2/drivers/type_tunnel.py b/neutron/plugins/ml2/drivers/type_tunnel.py index 4038176aa..b447f5582 100644 --- a/neutron/plugins/ml2/drivers/type_tunnel.py +++ b/neutron/plugins/ml2/drivers/type_tunnel.py @@ -22,8 +22,6 @@ LOG = log.getLogger(__name__) TUNNEL = 'tunnel' -TYPE_GRE = 'gre' - class TunnelTypeDriver(object): """Define stable abstract interface for ML2 type drivers. @@ -50,6 +48,21 @@ class TunnelTypeDriver(object): """ pass + def _parse_tunnel_ranges(self, tunnel_ranges, current_range, tunnel_type): + for entry in tunnel_ranges: + entry = entry.strip() + try: + tun_min, tun_max = entry.split(':') + tun_min = tun_min.strip() + tun_max = tun_max.strip() + current_range.append((int(tun_min), int(tun_max))) + except ValueError as ex: + LOG.error(_("Invalid tunnel ID range: '%(range)s' - %(e)s. " + "Agent terminated!"), + {'range': tunnel_ranges, 'e': ex}) + LOG.info(_("%(type)s ID ranges: %(range)s"), + {'type': tunnel_type, 'range': current_range}) + class TunnelRpcCallbackMixin(object): diff --git a/neutron/plugins/ml2/drivers/type_vxlan.py b/neutron/plugins/ml2/drivers/type_vxlan.py new file mode 100644 index 000000000..949a11b75 --- /dev/null +++ b/neutron/plugins/ml2/drivers/type_vxlan.py @@ -0,0 +1,215 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. +# @author: Kyle Mestery, Cisco Systems, Inc. + +from oslo.config import cfg +import sqlalchemy as sa +from sqlalchemy.orm import exc as sa_exc + +from neutron.common import exceptions as exc +from neutron.db import api as db_api +from neutron.db import model_base +from neutron.openstack.common import log +from neutron.plugins.ml2 import driver_api as api +from neutron.plugins.ml2.drivers import type_tunnel + +LOG = log.getLogger(__name__) + +TYPE_VXLAN = 'vxlan' +VXLAN_UDP_PORT = 4789 +MAX_VXLAN_VNI = 16777215 + +vxlan_opts = [ + cfg.ListOpt('vni_ranges', + default=[], + help=_("Comma-separated list of : tuples " + "enumerating ranges of VXLAN VNI IDs that are " + "available for tenant network allocation")), + cfg.StrOpt('vxlan_group', default=None, + help=_("Multicast group for VXLAN. If unset, disables VXLAN " + "multicast mode.")), +] + +cfg.CONF.register_opts(vxlan_opts, "ml2_type_vxlan") + + +class VxlanAllocation(model_base.BASEV2): + + __tablename__ = 'ml2_vxlan_allocations' + + vxlan_vni = sa.Column(sa.Integer, nullable=False, primary_key=True, + autoincrement=False) + allocated = sa.Column(sa.Boolean, nullable=False, default=False) + + +class VxlanEndpoints(model_base.BASEV2): + """Represents tunnel endpoint in RPC mode.""" + __tablename__ = 'ml2_vxlan_endpoints' + + ip_address = sa.Column(sa.String(64), primary_key=True) + udp_port = sa.Column(sa.Integer, primary_key=True, nullable=False) + + def __repr__(self): + return "" % self.ip_address + + +class VxlanTypeDriver(api.TypeDriver, + type_tunnel.TunnelTypeDriver): + + def get_type(self): + return TYPE_VXLAN + + def initialize(self): + self.vxlan_vni_ranges = [] + self._parse_tunnel_ranges( + cfg.CONF.ml2_type_vxlan.vni_ranges, + self.vxlan_vni_ranges, + TYPE_VXLAN + ) + self._sync_vxlan_allocations() + + def validate_provider_segment(self, segment): + physical_network = segment.get(api.PHYSICAL_NETWORK) + if physical_network: + msg = _("provider:physical_network specified for VXLAN " + "network") + raise exc.InvalidInput(error_message=msg) + + segmentation_id = segment.get(api.SEGMENTATION_ID) + if segmentation_id is None: + msg = _("segmentation_id required for VXLAN provider network") + raise exc.InvalidInput(error_message=msg) + + def reserve_provider_segment(self, session, segment): + segmentation_id = segment.get(api.SEGMENTATION_ID) + with session.begin(subtransactions=True): + try: + alloc = (session.query(VxlanAllocation). + filter_by(vxlan_vni=segmentation_id). + with_lockmode('update'). + one()) + if alloc.allocated: + raise exc.TunnelIdInUse(tunnel_id=segmentation_id) + LOG.debug(_("Reserving specific vxlan tunnel %s from pool"), + segmentation_id) + alloc.allocated = True + except sa_exc.NoResultFound: + LOG.debug(_("Reserving specific vxlan tunnel %s outside pool"), + segmentation_id) + alloc = VxlanAllocation(vxlan_vni=segmentation_id) + alloc.allocated = True + session.add(alloc) + + def allocate_tenant_segment(self, session): + with session.begin(subtransactions=True): + alloc = (session.query(VxlanAllocation). + filter_by(allocated=False). + with_lockmode('update'). + first()) + if alloc: + LOG.debug(_("Allocating vxlan tunnel vni %(vxlan_vni)s"), + {'vxlan_vni': alloc.vxlan_vni}) + alloc.allocated = True + return {api.NETWORK_TYPE: TYPE_VXLAN, + api.PHYSICAL_NETWORK: None, + api.SEGMENTATION_ID: alloc.vxlan_vni} + + def release_segment(self, session, segment): + vxlan_vni = segment[api.SEGMENTATION_ID] + with session.begin(subtransactions=True): + try: + alloc = (session.query(VxlanAllocation). + filter_by(vxlan_vni=vxlan_vni). + with_lockmode('update'). + one()) + alloc.allocated = False + for low, high in self.vxlan_vni_ranges: + if low <= vxlan_vni <= high: + LOG.debug(_("Releasing vxlan tunnel %s to pool"), + vxlan_vni) + break + else: + session.delete(alloc) + LOG.debug(_("Releasing vxlan tunnel %s outside pool"), + vxlan_vni) + except sa_exc.NoResultFound: + LOG.warning(_("vxlan_vni %s not found"), vxlan_vni) + + def _sync_vxlan_allocations(self): + """ + Synchronize vxlan_allocations table with configured tunnel ranges. + """ + + # determine current configured allocatable vnis + vxlan_vnis = set() + for tun_min, tun_max in self.vxlan_vni_ranges: + if tun_max + 1 - tun_min > MAX_VXLAN_VNI: + LOG.error(_("Skipping unreasonable VXLAN VNI range " + "%(tun_min)s:%(tun_max)s"), + {'tun_min': tun_min, 'tun_max': tun_max}) + else: + vxlan_vnis |= set(xrange(tun_min, tun_max + 1)) + + session = db_api.get_session() + with session.begin(subtransactions=True): + # remove from table unallocated tunnels not currently allocatable + allocs = session.query(VxlanAllocation) + for alloc in allocs: + try: + # see if tunnel is allocatable + vxlan_vnis.remove(alloc.vxlan_vni) + except KeyError: + # it's not allocatable, so check if its allocated + if not alloc.allocated: + # it's not, so remove it from table + LOG.debug(_("Removing tunnel %s from pool"), + alloc.vxlan_vni) + session.delete(alloc) + + # add missing allocatable tunnels to table + for vxlan_vni in sorted(vxlan_vnis): + alloc = VxlanAllocation(vxlan_vni=vxlan_vni) + session.add(alloc) + + def get_vxlan_allocation(self, session, vxlan_vni): + with session.begin(subtransactions=True): + return session.query(VxlanAllocation).filter_by( + vxlan_vni=vxlan_vni).first() + + def get_endpoints(self): + """Get every vxlan endpoints from database.""" + + LOG.debug(_("get_vxlan_endpoints() called")) + session = db_api.get_session() + + with session.begin(subtransactions=True): + vxlan_endpoints = session.query(VxlanEndpoints) + return [{'ip_address': vxlan_endpoint.ip_address, + 'udp_port': vxlan_endpoint.udp_port} + for vxlan_endpoint in vxlan_endpoints] + + def add_endpoint(self, ip, udp_port=VXLAN_UDP_PORT): + LOG.debug(_("add_vxlan_endpoint() called for ip %s"), ip) + session = db_api.get_session() + with session.begin(subtransactions=True): + try: + vxlan_endpoint = (session.query(VxlanEndpoints). + filter_by(ip_address=ip). + with_lockmode('update').one()) + except sa_exc.NoResultFound: + vxlan_endpoint = VxlanEndpoints(ip_address=ip, + udp_port=udp_port) + session.add(vxlan_endpoint) + return vxlan_endpoint diff --git a/neutron/tests/unit/ml2/test_type_vxlan.py b/neutron/tests/unit/ml2/test_type_vxlan.py new file mode 100644 index 000000000..7f4fd8baa --- /dev/null +++ b/neutron/tests/unit/ml2/test_type_vxlan.py @@ -0,0 +1,196 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. +# @author: Kyle Mestery, Cisco Systems, Inc. + +from oslo.config import cfg +import testtools +from testtools import matchers + +from neutron.common import exceptions as exc +from neutron.db import api as db +from neutron.plugins.ml2 import db as ml2_db +from neutron.plugins.ml2 import driver_api as api +from neutron.plugins.ml2.drivers import type_vxlan +from neutron.tests import base + + +TUNNEL_IP_ONE = "10.10.10.10" +TUNNEL_IP_TWO = "10.10.10.20" +TUN_MIN = 100 +TUN_MAX = 109 +TUNNEL_RANGES = [(TUN_MIN, TUN_MAX)] +UPDATED_TUNNEL_RANGES = [(TUN_MIN + 5, TUN_MAX + 5)] +INVALID_VXLAN_VNI = 7337 +MULTICAST_GROUP = "239.1.1.1" +VXLAN_UDP_PORT_ONE = 9999 +VXLAN_UDP_PORT_TWO = 8888 + + +class VxlanTypeTest(base.BaseTestCase): + def setUp(self): + super(VxlanTypeTest, self).setUp() + ml2_db.initialize() + cfg.CONF.set_override('vni_ranges', [TUNNEL_RANGES], + group='ml2_type_vxlan') + cfg.CONF.set_override('vxlan_group', MULTICAST_GROUP, + group='ml2_type_vxlan') + self.driver = type_vxlan.VxlanTypeDriver() + self.driver.vxlan_vni_ranges = TUNNEL_RANGES + self.driver._sync_vxlan_allocations() + self.session = db.get_session() + self.addCleanup(cfg.CONF.reset) + self.addCleanup(db.clear_db) + + def test_vxlan_tunnel_type(self): + self.assertEqual(self.driver.get_type(), type_vxlan.TYPE_VXLAN) + + def test_validate_provider_segment(self): + segment = {api.NETWORK_TYPE: 'vxlan', + api.PHYSICAL_NETWORK: 'phys_net', + api.SEGMENTATION_ID: None} + + with testtools.ExpectedException(exc.InvalidInput): + self.driver.validate_provider_segment(segment) + + segment[api.PHYSICAL_NETWORK] = None + with testtools.ExpectedException(exc.InvalidInput): + self.driver.validate_provider_segment(segment) + + def test_sync_tunnel_allocations(self): + self.assertIsNone( + self.driver.get_vxlan_allocation(self.session, + (TUN_MIN - 1)) + ) + self.assertFalse( + self.driver.get_vxlan_allocation(self.session, + (TUN_MIN)).allocated + ) + self.assertFalse( + self.driver.get_vxlan_allocation(self.session, + (TUN_MIN + 1)).allocated + ) + self.assertFalse( + self.driver.get_vxlan_allocation(self.session, + (TUN_MAX - 1)).allocated + ) + self.assertFalse( + self.driver.get_vxlan_allocation(self.session, + (TUN_MAX)).allocated + ) + self.assertIsNone( + self.driver.get_vxlan_allocation(self.session, + (TUN_MAX + 1)) + ) + + self.driver.vxlan_vni_ranges = UPDATED_TUNNEL_RANGES + self.driver._sync_vxlan_allocations() + + self.assertIsNone(self.driver. + get_vxlan_allocation(self.session, + (TUN_MIN + 5 - 1))) + self.assertFalse(self.driver. + get_vxlan_allocation(self.session, (TUN_MIN + 5)). + allocated) + self.assertFalse(self.driver. + get_vxlan_allocation(self.session, (TUN_MIN + 5 + 1)). + allocated) + self.assertFalse(self.driver. + get_vxlan_allocation(self.session, (TUN_MAX + 5 - 1)). + allocated) + self.assertFalse(self.driver. + get_vxlan_allocation(self.session, (TUN_MAX + 5)). + allocated) + self.assertIsNone(self.driver. + get_vxlan_allocation(self.session, + (TUN_MAX + 5 + 1))) + + def test_reserve_provider_segment(self): + segment = {api.NETWORK_TYPE: 'vxlan', + api.PHYSICAL_NETWORK: 'None', + api.SEGMENTATION_ID: 101} + self.driver.reserve_provider_segment(self.session, segment) + alloc = self.driver.get_vxlan_allocation(self.session, + segment[api.SEGMENTATION_ID]) + self.assertTrue(alloc.allocated) + + with testtools.ExpectedException(exc.TunnelIdInUse): + self.driver.reserve_provider_segment(self.session, segment) + + self.driver.release_segment(self.session, segment) + alloc = self.driver.get_vxlan_allocation(self.session, + segment[api.SEGMENTATION_ID]) + self.assertFalse(alloc.allocated) + + segment[api.SEGMENTATION_ID] = 1000 + self.driver.reserve_provider_segment(self.session, segment) + alloc = self.driver.get_vxlan_allocation(self.session, + segment[api.SEGMENTATION_ID]) + self.assertTrue(alloc.allocated) + + self.driver.release_segment(self.session, segment) + alloc = self.driver.get_vxlan_allocation(self.session, + segment[api.SEGMENTATION_ID]) + self.assertIsNone(alloc) + + def test_allocate_tenant_segment(self): + tunnel_ids = set() + for x in xrange(TUN_MIN, TUN_MAX + 1): + segment = self.driver.allocate_tenant_segment(self.session) + self.assertThat(segment[api.SEGMENTATION_ID], + matchers.GreaterThan(TUN_MIN - 1)) + self.assertThat(segment[api.SEGMENTATION_ID], + matchers.LessThan(TUN_MAX + 1)) + tunnel_ids.add(segment[api.SEGMENTATION_ID]) + + segment = self.driver.allocate_tenant_segment(self.session) + self.assertEqual(None, segment) + + segment = {api.NETWORK_TYPE: 'vxlan', + api.PHYSICAL_NETWORK: 'None', + api.SEGMENTATION_ID: tunnel_ids.pop()} + self.driver.release_segment(self.session, segment) + segment = self.driver.allocate_tenant_segment(self.session) + self.assertThat(segment[api.SEGMENTATION_ID], + matchers.GreaterThan(TUN_MIN - 1)) + self.assertThat(segment[api.SEGMENTATION_ID], + matchers.LessThan(TUN_MAX + 1)) + tunnel_ids.add(segment[api.SEGMENTATION_ID]) + + for tunnel_id in tunnel_ids: + segment[api.SEGMENTATION_ID] = tunnel_id + self.driver.release_segment(self.session, segment) + + def test_vxlan_endpoints(self): + """Test VXLAN allocation/de-allocation.""" + + # Set first endpoint, verify it gets VXLAN VNI 1 + vxlan1_endpoint = self.driver.add_endpoint(TUNNEL_IP_ONE, + VXLAN_UDP_PORT_ONE) + self.assertEqual(TUNNEL_IP_ONE, vxlan1_endpoint.ip_address) + self.assertEqual(VXLAN_UDP_PORT_ONE, vxlan1_endpoint.udp_port) + + # Set second endpoint, verify it gets VXLAN VNI 2 + vxlan2_endpoint = self.driver.add_endpoint(TUNNEL_IP_TWO, + VXLAN_UDP_PORT_TWO) + self.assertEqual(TUNNEL_IP_TWO, vxlan2_endpoint.ip_address) + self.assertEqual(VXLAN_UDP_PORT_TWO, vxlan2_endpoint.udp_port) + + # Get all the endpoints + endpoints = self.driver.get_endpoints() + for endpoint in endpoints: + if endpoint['ip_address'] == TUNNEL_IP_ONE: + self.assertEqual(VXLAN_UDP_PORT_ONE, endpoint['udp_port']) + elif endpoint['ip_address'] == TUNNEL_IP_TWO: + self.assertEqual(VXLAN_UDP_PORT_TWO, endpoint['udp_port']) diff --git a/setup.cfg b/setup.cfg index 25dee0e5e..d30b962c5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -110,6 +110,7 @@ neutron.ml2.type_drivers = local = neutron.plugins.ml2.drivers.type_local:LocalTypeDriver vlan = neutron.plugins.ml2.drivers.type_vlan:VlanTypeDriver gre = neutron.plugins.ml2.drivers.type_gre:GreTypeDriver + vxlan = neutron.plugins.ml2.drivers.type_vxlan:VxlanTypeDriver neutron.ml2.mechanism_drivers = logger = neutron.tests.unit.ml2.drivers.mechanism_logger:LoggerMechanismDriver test = neutron.tests.unit.ml2.drivers.mechanism_test:TestMechanismDriver