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:
parent
d326ec0088
commit
8807825002
@ -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,
|
||||
|
@ -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 "
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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):
|
||||
|
108
quantum/plugins/openvswitch/tests/unit/test_ovs_db.py
Normal file
108
quantum/plugins/openvswitch/tests/unit/test_ovs_db.py
Normal 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))
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user