From: Bob Kukura Date: Fri, 15 Jun 2012 14:20:05 +0000 (-0400) Subject: Initial V2 implementation of provider extension. X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=f2e37172ef0ee319de171894bd61fd18f9c55a34;p=openstack-build%2Fneutron-build.git Initial V2 implementation of provider extension. Initial provider extension implementation. Specify vlan_id using the CLI with admin rights via "net-create --tenant_id --provider:vlan_id ". Also includes provider:vlan_id in reply messages for admins. The extension is supported in the linuxbridge and openvswitch plugins. Partially implements blueprint provider-networks. Change-Id: I2fff64c4247b1a3091c28c7a2cd632afda192c3d --- diff --git a/etc/policy.json b/etc/policy.json index 41a5cafbe..1fcc3306e 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -2,6 +2,10 @@ "admin_or_owner": [["role:admin"], ["tenant_id:%(tenant_id)s"]], "default": [["rule:admin_or_owner"]], + "admin_api": [["role:admin"]], + "extension:provider_network:view": [["rule:admin_api"]], + "extension:provider_network:set": [["rule:admin_api"]], + "create_subnet": [], "get_subnet": [["rule:admin_or_owner"]], "update_subnet": [["rule:admin_or_owner"]], diff --git a/quantum/api/v2/base.py b/quantum/api/v2/base.py index 64786d342..c093a8e9e 100644 --- a/quantum/api/v2/base.py +++ b/quantum/api/v2/base.py @@ -346,7 +346,8 @@ class Controller(object): for attr, attr_vals in self._attr_info.iteritems(): # Convert values if necessary if ('convert_to' in attr_vals and - attr in res_dict): + attr in res_dict and + res_dict[attr] != attributes.ATTR_NOT_SPECIFIED): res_dict[attr] = attr_vals['convert_to'](res_dict[attr]) # Check that configured values are correct diff --git a/quantum/common/exceptions.py b/quantum/common/exceptions.py index 88e548927..04595c61c 100644 --- a/quantum/common/exceptions.py +++ b/quantum/common/exceptions.py @@ -105,6 +105,11 @@ class IpAddressInUse(InUse): "The IP address %(ip_address)s is in use.") +class VlanIdInUse(InUse): + message = _("Unable to complete operation for network %(net_id)s. " + "The VLAN %(vlan_id)s is in use.") + + 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/extensions/providernet.py b/quantum/extensions/providernet.py new file mode 100644 index 000000000..6ed2c295d --- /dev/null +++ b/quantum/extensions/providernet.py @@ -0,0 +1,66 @@ +# 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. + +from quantum.api.v2 import attributes + +EXTENDED_ATTRIBUTES_2_0 = { + 'networks': { + # TODO(rkukura): specify validation + 'provider:vlan_id': {'allow_post': True, 'allow_put': False, + 'convert_to': int, + 'default': attributes.ATTR_NOT_SPECIFIED, + 'is_visible': True}, + } +} + + +class Providernet(object): + """Extension class supporting provider networks. + + This class is used by quantum's extension framework to make + metadata about the provider network extension available to + clients. No new resources are defined by this extension. Instead, + the existing network resource's request and response messages are + extended with attributes in the provider namespace. + + To create a provider VLAN network using the CLI with admin rights: + + (shell) net-create --tenant_id \ + --provider:vlan_id + + With admin rights, network dictionaries returned from CLI commands + will also include provider attributes. + """ + + def get_name(cls): + return "Provider Network" + + def get_alias(cls): + return "provider" + + def get_description(cls): + return "Expose mapping of virtual networks to VLANs and flat networks" + + def get_namespace(cls): + return "http://docs.openstack.org/ext/provider/api/v1.0" + + def get_updated(cls): + return "2012-07-23T10:00:00-00:00" + + def get_extended_attributes(self, version): + if version == "2.0": + return EXTENDED_ATTRIBUTES_2_0 + else: + return {} diff --git a/quantum/plugins/linuxbridge/db/l2network_db.py b/quantum/plugins/linuxbridge/db/l2network_db.py index 8cd4014bd..b2e87bf70 100644 --- a/quantum/plugins/linuxbridge/db/l2network_db.py +++ b/quantum/plugins/linuxbridge/db/l2network_db.py @@ -50,7 +50,7 @@ def initialize(base=None): def create_vlanids(): - """Prepopulates the vlan_bindings table""" + """Prepopulate the vlan_bindings table""" LOG.debug("create_vlanids() called") session = db.get_session() start = CONF.VLANS.vlan_start @@ -87,7 +87,7 @@ def create_vlanids(): def get_all_vlanids(): - """Gets all the vlanids""" + """Get all the vlanids""" LOG.debug("get_all_vlanids() called") session = db.get_session() try: @@ -99,7 +99,7 @@ def get_all_vlanids(): def is_vlanid_used(vlan_id): - """Checks if a vlanid is in use""" + """Check if a vlanid is in use""" LOG.debug("is_vlanid_used() called") session = db.get_session() try: @@ -112,7 +112,7 @@ def is_vlanid_used(vlan_id): def release_vlanid(vlan_id): - """Sets the vlanid state to be unused""" + """Set the vlanid state to be unused, and delete if not in range""" LOG.debug("release_vlanid() called") session = db.get_session() try: @@ -120,7 +120,10 @@ def release_vlanid(vlan_id): filter_by(vlan_id=vlan_id). one()) vlanid["vlan_used"] = False - session.merge(vlanid) + if vlan_id >= CONF.VLANS.vlan_start and vlan_id <= CONF.VLANS.vlan_end: + session.merge(vlanid) + else: + session.delete(vlanid) session.flush() return vlanid["vlan_used"] except exc.NoResultFound: @@ -129,7 +132,7 @@ def release_vlanid(vlan_id): def delete_vlanid(vlan_id): - """Deletes a vlanid entry from db""" + """Delete a vlanid entry from db""" LOG.debug("delete_vlanid() called") session = db.get_session() try: @@ -144,7 +147,7 @@ def delete_vlanid(vlan_id): def reserve_vlanid(): - """Reserves the first unused vlanid""" + """Reserve the first unused vlanid""" LOG.debug("reserve_vlanid() called") session = db.get_session() try: @@ -170,8 +173,32 @@ def reserve_vlanid(): raise c_exc.VlanIDNotAvailable() +def reserve_specific_vlanid(vlan_id, net_id): + """Reserve a specific vlanid""" + LOG.debug("reserve_specific_vlanid() called") + 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() + try: + rvlanid = (session.query(l2network_models.VlanID). + filter_by(vlan_id=vlan_id). + one()) + if rvlanid["vlan_used"]: + raise q_exc.VlanIdInUse(net_id=net_id, vlan_id=vlan_id) + LOG.debug("reserving dynamic vlanid %s" % vlan_id) + rvlanid["vlan_used"] = True + session.merge(rvlanid) + except exc.NoResultFound: + rvlanid = l2network_models.VlanID(vlan_id) + LOG.debug("reserving non-dynamic vlanid %s" % vlan_id) + rvlanid["vlan_used"] = True + session.add(rvlanid) + session.flush() + + def get_all_vlanids_used(): - """Gets all the vlanids used""" + """Get all the vlanids used""" LOG.debug("get_all_vlanids() called") session = db.get_session() try: @@ -184,7 +211,7 @@ def get_all_vlanids_used(): def get_all_vlan_bindings(): - """Lists all the vlan to network associations""" + """List all the vlan to network associations""" LOG.debug("get_all_vlan_bindings() called") session = db.get_session() try: @@ -196,7 +223,7 @@ def get_all_vlan_bindings(): def get_vlan_binding(netid): - """Lists the vlan given a network_id""" + """List the vlan given a network_id""" LOG.debug("get_vlan_binding() called") session = db.get_session() try: @@ -209,7 +236,7 @@ def get_vlan_binding(netid): def add_vlan_binding(vlanid, netid): - """Adds a vlan to network association""" + """Add a vlan to network association""" LOG.debug("add_vlan_binding() called") session = db.get_session() try: @@ -226,7 +253,7 @@ def add_vlan_binding(vlanid, netid): def remove_vlan_binding(netid): - """Removes a vlan to network association""" + """Remove a vlan to network association""" LOG.debug("remove_vlan_binding() called") session = db.get_session() try: @@ -241,7 +268,7 @@ def remove_vlan_binding(netid): def update_vlan_binding(netid, newvlanid=None): - """Updates a vlan to network association""" + """Update a vlan to network association""" LOG.debug("update_vlan_binding() called") session = db.get_session() try: diff --git a/quantum/plugins/linuxbridge/lb_quantum_plugin.py b/quantum/plugins/linuxbridge/lb_quantum_plugin.py index ffb52ba71..573c7368e 100644 --- a/quantum/plugins/linuxbridge/lb_quantum_plugin.py +++ b/quantum/plugins/linuxbridge/lb_quantum_plugin.py @@ -15,40 +15,94 @@ import logging +from quantum.api.v2 import attributes from quantum.db import db_base_plugin_v2 from quantum.db import models_v2 from quantum.plugins.linuxbridge.db import l2network_db as cdb +from quantum import policy LOG = logging.getLogger(__name__) class LinuxBridgePluginV2(db_base_plugin_v2.QuantumDbPluginV2): - """ - LinuxBridgePlugin provides support for Quantum abstractions - using LinuxBridge. A new VLAN is created for each network. - It relies on an agent to perform the actual bridge configuration - on each host. + """Implement the Quantum abstractions using Linux bridging. + + A new VLAN is created for each network. An agent is relied upon + to perform the actual Linux bridge configuration on each host. + + The provider extension is also supported. As discussed in + https://bugs.launchpad.net/quantum/+bug/1023156, this class could + be simplified, and filtering on extended attributes could be + handled, by adding support for extended attributes to the + QuantumDbPluginV2 base class. When that occurs, this class should + be updated to take advantage of it. """ + supported_extension_aliases = ["provider"] + def __init__(self): cdb.initialize(base=models_v2.model_base.BASEV2) LOG.debug("Linux Bridge Plugin initialization complete") + # TODO(rkukura) Use core mechanism for attribute authorization + # when available. + + def _check_provider_view_auth(self, context, network): + return policy.check(context, + "extension:provider_network:view", + network) + + def _enforce_provider_set_auth(self, context, network): + return policy.enforce(context, + "extension:provider_network:set", + network) + + def _extend_network_dict(self, context, network): + if self._check_provider_view_auth(context, network): + vlan_binding = cdb.get_vlan_binding(network['id']) + network['provider:vlan_id'] = vlan_binding['vlan_id'] + def create_network(self, context, network): - new_network = super(LinuxBridgePluginV2, self).create_network(context, - network) + net = super(LinuxBridgePluginV2, self).create_network(context, + network) try: - vlan_id = cdb.reserve_vlanid() - cdb.add_vlan_binding(vlan_id, new_network['id']) + 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']) + else: + vlan_id = cdb.reserve_vlanid() + cdb.add_vlan_binding(vlan_id, net['id']) + self._extend_network_dict(context, net) except: super(LinuxBridgePluginV2, self).delete_network(context, - new_network['id']) + net['id']) raise - return new_network + return net + + def update_network(self, context, id, network): + net = super(LinuxBridgePluginV2, self).update_network(context, id, + network) + self._extend_network_dict(context, net) + return net def delete_network(self, context, id): vlan_binding = cdb.get_vlan_binding(id) cdb.release_vlanid(vlan_binding['vlan_id']) cdb.remove_vlan_binding(id) return super(LinuxBridgePluginV2, self).delete_network(context, id) + + def get_network(self, context, id, fields=None, verbose=None): + net = super(LinuxBridgePluginV2, self).get_network(context, id, + None, verbose) + self._extend_network_dict(context, net) + return self._fields(net, fields) + + def get_networks(self, context, filters=None, fields=None, verbose=None): + nets = super(LinuxBridgePluginV2, self).get_networks(context, filters, + None, verbose) + for net in nets: + self._extend_network_dict(context, net) + # TODO(rkukura): Filter on extended attributes. + return [self._fields(net, fields) for net in nets] diff --git a/quantum/plugins/linuxbridge/tests/unit/test_database.py b/quantum/plugins/linuxbridge/tests/unit/test_database.py index ee9c8fe64..230315390 100644 --- a/quantum/plugins/linuxbridge/tests/unit/test_database.py +++ b/quantum/plugins/linuxbridge/tests/unit/test_database.py @@ -21,9 +21,10 @@ that tests the database api method calls """ import logging -import unittest +import unittest2 as unittest import quantum.db.api as db +import quantum.plugins.linuxbridge.common.exceptions as c_exc import quantum.plugins.linuxbridge.db.l2network_db as l2network_db @@ -157,16 +158,14 @@ class L2networkDBTest(unittest.TestCase): """Tear Down""" db.clear_db() - def testa_create_vlanbinding(self): - """test add vlan binding""" + def test_create_vlanbinding(self): net1 = self.quantum.create_network("t1", "netid1") vlan1 = self.dbtest.create_vlan_binding(10, net1["net-id"]) self.assertTrue(vlan1["vlan-id"] == "10") self.teardown_vlanbinding() self.teardown_network() - def testb_getall_vlanbindings(self): - """test get all vlan binding""" + def test_getall_vlanbindings(self): net1 = self.quantum.create_network("t1", "netid1") net2 = self.quantum.create_network("t1", "netid2") vlan1 = self.dbtest.create_vlan_binding(10, net1["net-id"]) @@ -178,8 +177,7 @@ class L2networkDBTest(unittest.TestCase): self.teardown_vlanbinding() self.teardown_network() - def testc_delete_vlanbinding(self): - """test delete vlan binding""" + def test_delete_vlanbinding(self): net1 = self.quantum.create_network("t1", "netid1") vlan1 = self.dbtest.create_vlan_binding(10, net1["net-id"]) self.assertTrue(vlan1["vlan-id"] == "10") @@ -193,8 +191,7 @@ class L2networkDBTest(unittest.TestCase): self.teardown_vlanbinding() self.teardown_network() - def testd_update_vlanbinding(self): - """test update vlan binding""" + def test_update_vlanbinding(self): net1 = self.quantum.create_network("t1", "netid1") vlan1 = self.dbtest.create_vlan_binding(10, net1["net-id"]) self.assertTrue(vlan1["vlan-id"] == "10") @@ -203,17 +200,55 @@ class L2networkDBTest(unittest.TestCase): self.teardown_vlanbinding() self.teardown_network() - def teste_test_vlanids(self): - """test vlanid methods""" + def test_vlanids(self): l2network_db.create_vlanids() vlanids = l2network_db.get_all_vlanids() - self.assertTrue(len(vlanids) > 0) + self.assertGreater(len(vlanids), 0) vlanid = l2network_db.reserve_vlanid() used = l2network_db.is_vlanid_used(vlanid) self.assertTrue(used) used = l2network_db.release_vlanid(vlanid) self.assertFalse(used) - #counting on default teardown here to clear db + self.teardown_vlanbinding() + self.teardown_network() + + def test_specific_vlanid_outside(self): + l2network_db.create_vlanids() + orig_count = len(l2network_db.get_all_vlanids()) + self.assertGreater(orig_count, 0) + 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") + self.assertTrue(l2network_db.is_vlanid_used(vlan_id)) + count = len(l2network_db.get_all_vlanids()) + self.assertEqual(count, orig_count + 1) + used = l2network_db.release_vlanid(vlan_id) + self.assertFalse(used) + with self.assertRaises(c_exc.VlanIDNotFound): + l2network_db.is_vlanid_used(vlan_id) + count = len(l2network_db.get_all_vlanids()) + self.assertEqual(count, orig_count) + self.teardown_vlanbinding() + self.teardown_network() + + def test_specific_vlanid_inside(self): + l2network_db.create_vlanids() + orig_count = len(l2network_db.get_all_vlanids()) + 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") + self.assertTrue(l2network_db.is_vlanid_used(vlan_id)) + count = len(l2network_db.get_all_vlanids()) + self.assertEqual(count, orig_count) + used = l2network_db.release_vlanid(vlan_id) + self.assertFalse(used) + self.assertFalse(l2network_db.is_vlanid_used(vlan_id)) + count = len(l2network_db.get_all_vlanids()) + self.assertEqual(count, orig_count) + self.teardown_vlanbinding() + self.teardown_network() def teardown_network(self): """tearDown Network table""" diff --git a/quantum/plugins/openvswitch/ovs_db_v2.py b/quantum/plugins/openvswitch/ovs_db_v2.py index 9cdaff666..bab6d4f3a 100644 --- a/quantum/plugins/openvswitch/ovs_db_v2.py +++ b/quantum/plugins/openvswitch/ovs_db_v2.py @@ -31,6 +31,17 @@ def get_vlans(): return [(binding.vlan_id, binding.network_id) for binding in bindings] +def get_vlan(net_id): + session = db.get_session() + try: + binding = (session.query(ovs_models_v2.VlanBinding). + filter_by(network_id=net_id). + one()) + except exc.NoResultFound: + return + return binding.vlan_id + + def add_vlan_binding(vlan_id, net_id): session = db.get_session() binding = ovs_models_v2.VlanBinding(vlan_id, net_id) diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py index 13d0a45d9..7ae993015 100644 --- a/quantum/plugins/openvswitch/ovs_quantum_plugin.py +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -18,11 +18,13 @@ # @author: Dan Wendlandt, Nicira Networks, Inc. # @author: Dave Lapsley, Nicira Networks, Inc. # @author: Aaron Rosen, Nicira Networks, Inc. +# @author: Bob Kukura, Red Hat, Inc. import logging import os from quantum.api.api_common import OperationalStatus +from quantum.api.v2 import attributes from quantum.common import exceptions as q_exc from quantum.common.utils import find_config_file from quantum.db import api as db @@ -32,6 +34,7 @@ from quantum.plugins.openvswitch.common import config from quantum.plugins.openvswitch import ovs_db from quantum.plugins.openvswitch import ovs_db_v2 from quantum.quantum_plugin_base import QuantumPluginBase +from quantum import policy LOG = logging.getLogger("ovs_quantum_plugin") @@ -80,10 +83,23 @@ class VlanMap(object): raise NoFreeVLANException("No VLAN free for network %s" % network_id) + def acquire_specific(self, vlan_id, network_id): + LOG.debug("Allocating specific VLAN %s for network %s" + % (vlan_id, network_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) + if self.vlans.get(vlan_id): + raise q_exc.VlanIdInUse(net_id=network_id, + vlan_id=vlan_id) + self.free_vlans.discard(vlan_id) + self.set_vlan(vlan_id, network_id) + def release(self, network_id): vlan = self.net_ids.get(network_id, None) if vlan is not None: - self.free_vlans.add(vlan) + if vlan >= self.vlan_min and vlan <= self.vlan_max: + self.free_vlans.add(vlan) del self.vlans[vlan] del self.net_ids[network_id] LOG.debug("Deallocated VLAN %s (used by network %s)" % @@ -234,8 +250,26 @@ class OVSQuantumPlugin(QuantumPluginBase): class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2): + """Implement the Quantum abstractions using Open vSwitch. + + Depending on whether tunneling is enabled, either a GRE tunnel or + a new VLAN is created for each network. An agent is relied upon to + perform the actual OVS configuration on each host. + + The provider extension is also supported. As discussed in + https://bugs.launchpad.net/quantum/+bug/1023156, this class could + be simplified, and filtering on extended attributes could be + handled, by adding support for extended attributes to the + QuantumDbPluginV2 base class. When that occurs, this class should + be updated to take advantage of it. + """ + + supported_extension_aliases = ["provider"] + def __init__(self, configfile=None): conf = config.parse(CONF_FILE) + self.enable_tunneling = conf.OVS.enable_tunneling + options = {"sql_connection": conf.DATABASE.sql_connection} options.update({'base': models_v2.model_base.BASEV2}) sql_max_retries = conf.DATABASE.sql_max_retries @@ -247,19 +281,63 @@ class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2): self.vmap = VlanMap(conf.OVS.vlan_min, conf.OVS.vlan_max) self.vmap.populate_already_used(ovs_db_v2.get_vlans()) + # TODO(rkukura) Use core mechanism for attribute authorization + # when available. + + def _check_provider_view_auth(self, context, network): + return policy.check(context, + "extension:provider_network:view", + network) + + def _enforce_provider_set_auth(self, context, network): + return policy.enforce(context, + "extension:provider_network:set", + network) + + def _extend_network_dict(self, context, network): + if self._check_provider_view_auth(context, network): + if not self.enable_tunneling: + network['provider:vlan_id'] = ovs_db_v2.get_vlan(network['id']) + def create_network(self, context, network): net = super(OVSQuantumPluginV2, self).create_network(context, network) try: - vlan_id = self.vmap.acquire(str(net['id'])) - except NoFreeVLANException: + 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'])) + else: + vlan_id = self.vmap.acquire(str(net['id'])) + except Exception: super(OVSQuantumPluginV2, self).delete_network(context, net['id']) raise LOG.debug("Created network: %s" % net['id']) ovs_db_v2.add_vlan_binding(vlan_id, str(net['id'])) + self._extend_network_dict(context, net) + return net + + def update_network(self, context, id, network): + net = super(OVSQuantumPluginV2, self).update_network(context, id, + network) + self._extend_network_dict(context, net) return net def delete_network(self, context, id): ovs_db_v2.remove_vlan_binding(id) self.vmap.release(id) return super(OVSQuantumPluginV2, self).delete_network(context, id) + + def get_network(self, context, id, fields=None, verbose=None): + net = super(OVSQuantumPluginV2, self).get_network(context, id, + None, verbose) + self._extend_network_dict(context, net) + return self._fields(net, fields) + + def get_networks(self, context, filters=None, fields=None, verbose=None): + nets = super(OVSQuantumPluginV2, self).get_networks(context, filters, + None, verbose) + for net in nets: + self._extend_network_dict(context, net) + # TODO(rkukura): Filter on extended attributes. + return [self._fields(net, fields) for net in nets]