Add VXLAN tunneling support for the ML2 plugin

Add an ML2 Type Driver for VXLAN networks to allow the creation
of VXLAN networks with the OVS agent.

Change-Id: I7fcc384cc44d5adc752510847d8ba5f46bbb79fb
Implements: blueprint ml2-vxlan
This commit is contained in:
Kyle Mestery 2013-06-26 07:13:32 +00:00
parent ba948fe6c1
commit c95d3441e1
8 changed files with 516 additions and 24 deletions

View File

@ -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.

View File

@ -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')

View File

@ -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',

View File

@ -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."""

View File

@ -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):

View File

@ -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 <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

View File

@ -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'])

View File

@ -108,6 +108,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