]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Replace openvswitch plugin's VlanMap with vlan_ids DB table.
authorBob Kukura <rkukura@redhat.com>
Mon, 30 Jul 2012 18:52:06 +0000 (14:52 -0400)
committerBob Kukura <rkukura@redhat.com>
Fri, 3 Aug 2012 21:29:45 +0000 (17:29 -0400)
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

quantum/api/v2/base.py
quantum/common/exceptions.py
quantum/plugins/linuxbridge/db/l2network_db.py
quantum/plugins/linuxbridge/lb_quantum_plugin.py
quantum/plugins/linuxbridge/tests/unit/test_database.py
quantum/plugins/openvswitch/ovs_db_v2.py
quantum/plugins/openvswitch/ovs_models_v2.py
quantum/plugins/openvswitch/ovs_quantum_plugin.py
quantum/plugins/openvswitch/tests/unit/test_ovs_db.py [new file with mode: 0644]
quantum/plugins/openvswitch/tests/unit/test_vlan_map.py

index f43bb470abe0db12b495d47f84cd37215c264930..5fdd07bd0ecfd202557b093249a79453610a7aa6 100644 (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,
index 04595c61cead93b233f6191f1d5493b2ce3526e1..22eac42593e870621c6df922b18cab7603a8b0ee 100644 (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 "
index ff5b0b0cb77661a013044a6b4a133b039d7a99ea..80e8c9d8d6f1917b5f042aa28f592f283232df6b 100644 (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)
index 9d5ea28b20e3f9a688da984e4f68d1d9ee4e34bf..80f2e407fc6edf10c956c368b315d762d96e9f18 100644 (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
 
index 8a80ef98005b56cd7c5d8d05269bd289a8271248..af574bd4bffff43b69ed8af3c37b4a98b1a036b0 100644 (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)
index bab6d4f3a24779acc7014d7bb3f50be06b525cc9..45a391165ab082bcfae28e8e9edc0792bce12452 100644 (file)
 #    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)
index cbdabdd4e30b51b2ef2b71d739d08acfa22a3480..755dc8ee20a8a394aac495f0bd0f8ef62fd6d21b 100644 (file)
 #    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'
 
index 97f71944d14f5233da7e7bd2e09fa5010dc8566a..a0156c81ba0ae0237297a3a84a47b343648337b9 100644 (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):
diff --git a/quantum/plugins/openvswitch/tests/unit/test_ovs_db.py b/quantum/plugins/openvswitch/tests/unit/test_ovs_db.py
new file mode 100644 (file)
index 0000000..707bb52
--- /dev/null
@@ -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))
index 30d75cc82698c4880a5c1057bdba01952a23df8a..baa5e63211d86123d2a365c515916edde91d3c0e 100644 (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()