Replace openvswitch plugin's VlanMap with vlan_ids DB table.

Fixes bug 1023167.

The openswitch plugin's in-memory VlanMap is replaced with a vlan_ids
DB table similar to that used by the linuxbridge plugin. This will
prevent conflicting VLAN assignments if multiple server replicas are
run, and also sets the stage for phase 2 of the provider-networks BP
implementation that will add support for multiple physical
networks.

Unlike with the current linuxbridge plugin, the contents of the
openvswitch plugin's vlan_ids table are properly updated at startup in
case the vlan_min or vlan_max configuration variables have changed.

A new test_ovs_db test case has been added.

The primary key of the vlan_bindings table is changed from the vlan_id
to the network_id, which is now a foreign key, and network deletion is
now properly handled.

The net_id has been removed from the VlanIdInUse exception, requiring
a minor update to the linuxbridge plugin. The new NoNetworksAvailable
exception, with ResourceExhausted as its base class, is returned when
no more VLANs are available.

Change-Id: I65a2347dea5366cc0d15d98a88f40e42e1a45f4c
This commit is contained in:
Bob Kukura 2012-07-30 14:52:06 -04:00
parent d326ec0088
commit 8807825002
10 changed files with 269 additions and 21 deletions

View File

@ -32,6 +32,7 @@ XML_NS_V20 = 'http://openstack.org/quantum/api/v2.0'
FAULT_MAP = {exceptions.NotFound: webob.exc.HTTPNotFound,
exceptions.InUse: webob.exc.HTTPConflict,
exceptions.ResourceExhausted: webob.exc.HTTPServiceUnavailable,
exceptions.MacAddressGenerationFailure:
webob.exc.HTTPServiceUnavailable,
exceptions.StateInvalid: webob.exc.HTTPBadRequest,

View File

@ -106,10 +106,19 @@ class IpAddressInUse(InUse):
class VlanIdInUse(InUse):
message = _("Unable to complete operation for network %(net_id)s. "
message = _("Unable to create the network. "
"The VLAN %(vlan_id)s is in use.")
class ResourceExhausted(QuantumException):
pass
class NoNetworkAvailable(ResourceExhausted):
message = _("Unable to create the network. "
"No virtual network is available.")
class AlreadyAttached(QuantumException):
message = _("Unable to plug the attachment %(att_id)s into port "
"%(port_id)s for network %(net_id)s. The attachment is "

View File

@ -172,7 +172,7 @@ def reserve_vlanid():
raise c_exc.VlanIDNotAvailable()
def reserve_specific_vlanid(vlan_id, net_id):
def reserve_specific_vlanid(vlan_id):
"""Reserve a specific vlanid"""
LOG.debug("reserve_specific_vlanid() called")
if vlan_id < 1 or vlan_id > 4094:
@ -184,7 +184,7 @@ def reserve_specific_vlanid(vlan_id, net_id):
filter_by(vlan_id=vlan_id).
one())
if rvlanid["vlan_used"]:
raise q_exc.VlanIdInUse(net_id=net_id, vlan_id=vlan_id)
raise q_exc.VlanIdInUse(vlan_id=vlan_id)
LOG.debug("reserving dynamic vlanid %s" % vlan_id)
rvlanid["vlan_used"] = True
session.merge(rvlanid)

View File

@ -69,7 +69,7 @@ class LinuxBridgePluginV2(db_base_plugin_v2.QuantumDbPluginV2):
vlan_id = network['network'].get('provider:vlan_id')
if vlan_id not in (None, attributes.ATTR_NOT_SPECIFIED):
self._enforce_provider_set_auth(context, net)
cdb.reserve_specific_vlanid(int(vlan_id), net['id'])
cdb.reserve_specific_vlanid(int(vlan_id))
else:
vlan_id = cdb.reserve_vlanid()
cdb.add_vlan_binding(vlan_id, net['id'])
@ -88,8 +88,8 @@ class LinuxBridgePluginV2(db_base_plugin_v2.QuantumDbPluginV2):
return net
def delete_network(self, context, id):
result = super(LinuxBridgePluginV2, self).delete_network(context, id)
vlan_binding = cdb.get_vlan_binding(id)
result = super(LinuxBridgePluginV2, self).delete_network(context, id)
cdb.release_vlanid(vlan_binding['vlan_id'])
return result

View File

@ -220,7 +220,7 @@ class L2networkDBTest(unittest.TestCase):
vlan_id = 7 # outside range dynamically allocated
with self.assertRaises(c_exc.VlanIDNotFound):
l2network_db.is_vlanid_used(vlan_id)
l2network_db.reserve_specific_vlanid(vlan_id, "net-id")
l2network_db.reserve_specific_vlanid(vlan_id)
self.assertTrue(l2network_db.is_vlanid_used(vlan_id))
count = len(l2network_db.get_all_vlanids())
self.assertEqual(count, orig_count + 1)
@ -239,7 +239,7 @@ class L2networkDBTest(unittest.TestCase):
self.assertGreater(orig_count, 0)
vlan_id = 1007 # inside range dynamically allocated
self.assertFalse(l2network_db.is_vlanid_used(vlan_id))
l2network_db.reserve_specific_vlanid(vlan_id, "net-id")
l2network_db.reserve_specific_vlanid(vlan_id)
self.assertTrue(l2network_db.is_vlanid_used(vlan_id))
count = len(l2network_db.get_all_vlanids())
self.assertEqual(count, orig_count)

View File

@ -14,12 +14,19 @@
# License for the specific language governing permissions and limitations
# under the License.
# @author: Aaron Rosen, Nicira Networks, Inc.
# @author: Bob Kukura, Red Hat, Inc.
import logging
from sqlalchemy.orm import exc
from quantum.common import exceptions as q_exc
import quantum.db.api as db
from quantum.openstack.common import cfg
from quantum.plugins.openvswitch import ovs_models_v2
LOG = logging.getLogger(__name__)
def get_vlans():
session = db.get_session()
@ -60,3 +67,105 @@ def remove_vlan_binding(net_id):
except exc.NoResultFound:
pass
session.flush()
def update_vlan_id_pool():
"""Update vlan_ids based on current configuration."""
# determine current dynamically-allocated range
vlans = set(xrange(cfg.CONF.OVS.vlan_min,
cfg.CONF.OVS.vlan_max + 1))
session = db.get_session()
with session.begin(subtransactions=True):
# remove unused vlan_ids outside current range
try:
records = (session.query(ovs_models_v2.VlanID).
all())
for record in records:
try:
vlans.remove(record.vlan_id)
except KeyError:
if not record.vlan_used:
LOG.debug("removing vlan %s from pool"
% record.vlan_id)
session.delete(record)
except exc.NoResultFound:
pass
# add missing vlan_ids
for vlan in vlans:
record = ovs_models_v2.VlanID(vlan)
session.add(record)
def get_vlan_id(vlan_id):
"""Get state of specified vlan"""
session = db.get_session()
try:
record = (session.query(ovs_models_v2.VlanID).
filter_by(vlan_id=vlan_id).
one())
return record
except exc.NoResultFound:
return None
def reserve_vlan_id():
"""Reserve an unused vlan_id"""
session = db.get_session()
with session.begin(subtransactions=True):
record = (session.query(ovs_models_v2.VlanID).
filter_by(vlan_used=False).
first())
if not record:
raise q_exc.NoNetworkAvailable()
LOG.debug("reserving vlan %s from pool" % record.vlan_id)
record.vlan_used = True
return record.vlan_id
def reserve_specific_vlan_id(vlan_id):
"""Reserve a specific vlan_id"""
if vlan_id < 1 or vlan_id > 4094:
msg = _("Specified VLAN %s outside legal range (1-4094)") % vlan_id
raise q_exc.InvalidInput(error_message=msg)
session = db.get_session()
with session.begin(subtransactions=True):
try:
record = (session.query(ovs_models_v2.VlanID).
filter_by(vlan_id=vlan_id).
one())
if record.vlan_used:
raise q_exc.VlanIdInUse(vlan_id=vlan_id)
LOG.debug("reserving specific vlan %s from pool" % vlan_id)
record.vlan_used = True
except exc.NoResultFound:
LOG.debug("reserving specific vlan %s outside pool" % vlan_id)
record = ovs_models_v2.VlanID(vlan_id)
record.vlan_used = True
session.add(record)
def release_vlan_id(vlan_id):
"""Set the vlan state to be unused, and delete if not in range"""
session = db.get_session()
with session.begin(subtransactions=True):
try:
record = (session.query(ovs_models_v2.VlanID).
filter_by(vlan_id=vlan_id).
one())
record.vlan_used = False
if (vlan_id >= cfg.CONF.OVS.vlan_min and
vlan_id <= cfg.CONF.OVS.vlan_max):
LOG.debug("releasing vlan %s to pool" % vlan_id)
else:
LOG.debug("removing vlan %s outside pool" % vlan_id)
session.delete(record)
except exc.NoResultFound:
LOG.error("vlan id %s not found in release_vlan_id" % vlan_id)

View File

@ -14,19 +14,37 @@
# License for the specific language governing permissions and limitations
# under the License.
# @author: Aaron Rosen, Nicira Networks, Inc.
# @author: Bob Kukura, Red Hat, Inc.
from sqlalchemy import Column, Integer, String
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
from quantum.db import models_v2
from quantum.db.models_v2 import model_base
class VlanBinding(models_v2.model_base.BASEV2):
class VlanID(model_base.BASEV2):
"""Represents a vlan_id usage"""
__tablename__ = 'vlan_ids'
vlan_id = Column(Integer, nullable=False, primary_key=True)
vlan_used = Column(Boolean, nullable=False)
def __init__(self, vlan_id):
self.vlan_id = vlan_id
self.vlan_used = False
def __repr__(self):
return "<VlanID(%d,%s)>" % (self.vlan_id, self.vlan_used)
class VlanBinding(model_base.BASEV2):
"""Represents a binding of network_id to vlan_id."""
__tablename__ = 'vlan_bindings'
vlan_id = Column(Integer, primary_key=True)
network_id = Column(String(255))
network_id = Column(String(36), ForeignKey('networks.id',
ondelete="CASCADE"),
primary_key=True)
vlan_id = Column(Integer, nullable=False)
def __init__(self, vlan_id, network_id):
self.network_id = network_id
@ -36,7 +54,7 @@ class VlanBinding(models_v2.model_base.BASEV2):
return "<VlanBinding(%s,%s)>" % (self.vlan_id, self.network_id)
class TunnelIP(models_v2.model_base.BASEV2):
class TunnelIP(model_base.BASEV2):
"""Represents a remote IP in tunnel mode."""
__tablename__ = 'tunnel_ips'

View File

@ -43,10 +43,12 @@ LOG = logging.getLogger("ovs_quantum_plugin")
# Exception thrown if no more VLANs are available
class NoFreeVLANException(Exception):
# TODO(rkukura) Remove this class when removing V1 API
pass
class VlanMap(object):
# TODO(rkukura) Remove this class when removing V1 API
vlans = {}
net_ids = {}
free_vlans = set()
@ -89,8 +91,7 @@ class VlanMap(object):
msg = _("Specified VLAN %s outside legal range (1-4094)") % vlan_id
raise q_exc.InvalidInput(error_message=msg)
if self.vlans.get(vlan_id):
raise q_exc.VlanIdInUse(net_id=network_id,
vlan_id=vlan_id)
raise q_exc.VlanIdInUse(vlan_id=vlan_id)
self.free_vlans.discard(vlan_id)
self.set_vlan(vlan_id, network_id)
@ -114,6 +115,7 @@ class VlanMap(object):
class OVSQuantumPlugin(QuantumPluginBase):
# TODO(rkukura) Remove this class when removing V1 API
def __init__(self, configfile=None):
options = {"sql_connection": cfg.CONF.DATABASE.sql_connection}
@ -274,8 +276,8 @@ class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
options.update({"reconnect_interval": reconnect_interval})
db.configure_db(options)
self.vmap = VlanMap(cfg.CONF.OVS.vlan_min, cfg.CONF.OVS.vlan_max)
self.vmap.populate_already_used(ovs_db_v2.get_vlans())
# update the vlan_id table based on current configuration
ovs_db_v2.update_vlan_id_pool()
# TODO(rkukura) Use core mechanism for attribute authorization
# when available.
@ -301,9 +303,9 @@ class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
vlan_id = network['network'].get('provider:vlan_id')
if vlan_id not in (None, attributes.ATTR_NOT_SPECIFIED):
self._enforce_provider_set_auth(context, net)
self.vmap.acquire_specific(int(vlan_id), str(net['id']))
ovs_db_v2.reserve_specific_vlan_id(vlan_id)
else:
vlan_id = self.vmap.acquire(str(net['id']))
vlan_id = ovs_db_v2.reserve_vlan_id()
except Exception:
super(OVSQuantumPluginV2, self).delete_network(context, net['id'])
raise
@ -320,9 +322,9 @@ class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
return net
def delete_network(self, context, id):
vlan_id = ovs_db_v2.get_vlan(id)
result = super(OVSQuantumPluginV2, self).delete_network(context, id)
ovs_db_v2.remove_vlan_binding(id)
self.vmap.release(id)
ovs_db_v2.release_vlan_id(vlan_id)
return result
def get_network(self, context, id, fields=None, verbose=None):

View File

@ -0,0 +1,108 @@
# Copyright (c) 2012 OpenStack, LLC.
#
# 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 unittest2
from quantum.common import exceptions as q_exc
from quantum.db import api as db
from quantum.db import models_v2
from quantum.plugins.openvswitch.common import config
from quantum.openstack.common import cfg
from quantum.plugins.openvswitch import ovs_db_v2
VLAN_MIN = 10
VLAN_MAX = 19
class OVSVlanIdsTest(unittest2.TestCase):
def setUp(self):
cfg.CONF.set_override('vlan_min', VLAN_MIN, group='OVS')
cfg.CONF.set_override('vlan_max', VLAN_MAX, group='OVS')
options = {"sql_connection": cfg.CONF.DATABASE.sql_connection}
options.update({'base': models_v2.model_base.BASEV2})
sql_max_retries = cfg.CONF.DATABASE.sql_max_retries
options.update({"sql_max_retries": sql_max_retries})
reconnect_interval = cfg.CONF.DATABASE.reconnect_interval
options.update({"reconnect_interval": reconnect_interval})
db.configure_db(options)
ovs_db_v2.update_vlan_id_pool()
def tearDown(self):
db.clear_db()
cfg.CONF.reset()
def test_update_vlan_id_pool(self):
self.assertIsNone(ovs_db_v2.get_vlan_id(VLAN_MIN - 1))
self.assertFalse(ovs_db_v2.get_vlan_id(VLAN_MIN).vlan_used)
self.assertFalse(ovs_db_v2.get_vlan_id(VLAN_MIN + 1).vlan_used)
self.assertFalse(ovs_db_v2.get_vlan_id(VLAN_MAX).vlan_used)
self.assertIsNone(ovs_db_v2.get_vlan_id(VLAN_MAX + 1))
cfg.CONF.set_override('vlan_min', VLAN_MIN + 5, group='OVS')
cfg.CONF.set_override('vlan_max', VLAN_MAX + 5, group='OVS')
ovs_db_v2.update_vlan_id_pool()
self.assertIsNone(ovs_db_v2.get_vlan_id(VLAN_MIN + 5 - 1))
self.assertFalse(ovs_db_v2.get_vlan_id(VLAN_MIN + 5).vlan_used)
self.assertFalse(ovs_db_v2.get_vlan_id(VLAN_MIN + 5 + 1).vlan_used)
self.assertFalse(ovs_db_v2.get_vlan_id(VLAN_MAX + 5).vlan_used)
self.assertIsNone(ovs_db_v2.get_vlan_id(VLAN_MAX + 5 + 1))
def test_vlan_id_pool(self):
vlan_ids = set()
for x in xrange(VLAN_MIN, VLAN_MAX + 1):
vlan_id = ovs_db_v2.reserve_vlan_id()
self.assertGreaterEqual(vlan_id, VLAN_MIN)
self.assertLessEqual(vlan_id, VLAN_MAX)
vlan_ids.add(vlan_id)
with self.assertRaises(q_exc.NoNetworkAvailable):
vlan_id = ovs_db_v2.reserve_vlan_id()
for vlan_id in vlan_ids:
ovs_db_v2.release_vlan_id(vlan_id)
def test_invalid_specific_vlan_id(self):
with self.assertRaises(q_exc.InvalidInput):
vlan_id = ovs_db_v2.reserve_specific_vlan_id(0)
with self.assertRaises(q_exc.InvalidInput):
vlan_id = ovs_db_v2.reserve_specific_vlan_id(4095)
def test_specific_vlan_id_inside_pool(self):
vlan_id = VLAN_MIN + 5
self.assertFalse(ovs_db_v2.get_vlan_id(vlan_id).vlan_used)
ovs_db_v2.reserve_specific_vlan_id(vlan_id)
self.assertTrue(ovs_db_v2.get_vlan_id(vlan_id).vlan_used)
with self.assertRaises(q_exc.VlanIdInUse):
ovs_db_v2.reserve_specific_vlan_id(vlan_id)
ovs_db_v2.release_vlan_id(vlan_id)
self.assertFalse(ovs_db_v2.get_vlan_id(vlan_id).vlan_used)
def test_specific_vlan_id_outside_pool(self):
vlan_id = VLAN_MAX + 5
self.assertIsNone(ovs_db_v2.get_vlan_id(vlan_id))
ovs_db_v2.reserve_specific_vlan_id(vlan_id)
self.assertTrue(ovs_db_v2.get_vlan_id(vlan_id).vlan_used)
with self.assertRaises(q_exc.VlanIdInUse):
ovs_db_v2.reserve_specific_vlan_id(vlan_id)
ovs_db_v2.release_vlan_id(vlan_id)
self.assertIsNone(ovs_db_v2.get_vlan_id(vlan_id))

View File

@ -23,6 +23,7 @@ from quantum.plugins.openvswitch.ovs_quantum_plugin import (
class VlanMapTest(unittest.TestCase):
# TODO(rkukura) Remove this class when removing V1 API
def setUp(self):
self.vmap = VlanMap()