diff --git a/quantum/api/v2/base.py b/quantum/api/v2/base.py index f43bb470ab..5fdd07bd0e 100644 --- a/quantum/api/v2/base.py +++ b/quantum/api/v2/base.py @@ -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, diff --git a/quantum/common/exceptions.py b/quantum/common/exceptions.py index 04595c61ce..22eac42593 100644 --- a/quantum/common/exceptions.py +++ b/quantum/common/exceptions.py @@ -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 " diff --git a/quantum/plugins/linuxbridge/db/l2network_db.py b/quantum/plugins/linuxbridge/db/l2network_db.py index ff5b0b0cb7..80e8c9d8d6 100644 --- a/quantum/plugins/linuxbridge/db/l2network_db.py +++ b/quantum/plugins/linuxbridge/db/l2network_db.py @@ -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) diff --git a/quantum/plugins/linuxbridge/lb_quantum_plugin.py b/quantum/plugins/linuxbridge/lb_quantum_plugin.py index 9d5ea28b20..80f2e407fc 100644 --- a/quantum/plugins/linuxbridge/lb_quantum_plugin.py +++ b/quantum/plugins/linuxbridge/lb_quantum_plugin.py @@ -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 diff --git a/quantum/plugins/linuxbridge/tests/unit/test_database.py b/quantum/plugins/linuxbridge/tests/unit/test_database.py index 8a80ef9800..af574bd4bf 100644 --- a/quantum/plugins/linuxbridge/tests/unit/test_database.py +++ b/quantum/plugins/linuxbridge/tests/unit/test_database.py @@ -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) diff --git a/quantum/plugins/openvswitch/ovs_db_v2.py b/quantum/plugins/openvswitch/ovs_db_v2.py index bab6d4f3a2..45a391165a 100644 --- a/quantum/plugins/openvswitch/ovs_db_v2.py +++ b/quantum/plugins/openvswitch/ovs_db_v2.py @@ -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) diff --git a/quantum/plugins/openvswitch/ovs_models_v2.py b/quantum/plugins/openvswitch/ovs_models_v2.py index cbdabdd4e3..755dc8ee20 100644 --- a/quantum/plugins/openvswitch/ovs_models_v2.py +++ b/quantum/plugins/openvswitch/ovs_models_v2.py @@ -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 "" % (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 "" % (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' diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py index 97f71944d1..a0156c81ba 100644 --- a/quantum/plugins/openvswitch/ovs_quantum_plugin.py +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -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 index 0000000000..707bb52505 --- /dev/null +++ b/quantum/plugins/openvswitch/tests/unit/test_ovs_db.py @@ -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)) diff --git a/quantum/plugins/openvswitch/tests/unit/test_vlan_map.py b/quantum/plugins/openvswitch/tests/unit/test_vlan_map.py index 30d75cc826..baa5e63211 100644 --- a/quantum/plugins/openvswitch/tests/unit/test_vlan_map.py +++ b/quantum/plugins/openvswitch/tests/unit/test_vlan_map.py @@ -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()