# (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.
--- /dev/null
+# 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')
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',
LOG = log.getLogger(__name__)
+TYPE_GRE = 'gre'
+
gre_opts = [
cfg.ListOpt('tunnel_id_ranges',
default=[],
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):
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}
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."""
TUNNEL = 'tunnel'
-TYPE_GRE = 'gre'
-
class TunnelTypeDriver(object):
"""Define stable abstract interface for ML2 type drivers.
"""
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):
--- /dev/null
+# 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 <vni_min>:<vni_max> 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 "<VxlanTunnelEndpoint(%s)>" % 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
--- /dev/null
+# 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'])
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