From cc452485c2c06d791295520dd70abce90c042d8c Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Tue, 16 Sep 2014 20:21:27 +0930 Subject: [PATCH] Port os-tenant-networks plugin to v2.1(v3) infrastructure Ports os-tenant-networks extension and adapts it to the v2.1/v3 API framework. API behaviour is identical. - unittest code modified to share testing with both v2/v2.1 - Adds expected error decorators for API methods Partially implements blueprint v2-on-v3-api Change-Id: I340c9b1312a3477c63d28f19df9611c95e67cde6 --- .../os-tenant-networks/networks-list-res.json | 14 ++ .../os-tenant-networks/networks-post-req.json | 9 + .../os-tenant-networks/networks-post-res.json | 7 + etc/nova/policy.json | 2 + .../compute/plugins/v3/tenant_networks.py | 217 ++++++++++++++++++ .../compute/contrib/test_networks.py | 13 +- .../compute/contrib/test_tenant_networks.py | 13 +- nova/tests/fake_policy.py | 1 + .../networks-list-res.json.tpl | 14 ++ .../networks-post-req.json.tpl | 9 + .../networks-post-res.json.tpl | 7 + .../integrated/v3/test_tenant_networks.py | 61 +++++ setup.cfg | 1 + 13 files changed, 362 insertions(+), 6 deletions(-) create mode 100644 doc/v3/api_samples/os-tenant-networks/networks-list-res.json create mode 100644 doc/v3/api_samples/os-tenant-networks/networks-post-req.json create mode 100644 doc/v3/api_samples/os-tenant-networks/networks-post-res.json create mode 100644 nova/api/openstack/compute/plugins/v3/tenant_networks.py create mode 100644 nova/tests/integrated/v3/api_samples/os-tenant-networks/networks-list-res.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/os-tenant-networks/networks-post-req.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/os-tenant-networks/networks-post-res.json.tpl create mode 100644 nova/tests/integrated/v3/test_tenant_networks.py diff --git a/doc/v3/api_samples/os-tenant-networks/networks-list-res.json b/doc/v3/api_samples/os-tenant-networks/networks-list-res.json new file mode 100644 index 000000000000..b857e8112af5 --- /dev/null +++ b/doc/v3/api_samples/os-tenant-networks/networks-list-res.json @@ -0,0 +1,14 @@ +{ + "networks": [ + { + "cidr": "10.0.0.0/29", + "id": "616fb98f-46ca-475e-917e-2563e5a8cd19", + "label": "test_0" + }, + { + "cidr": "10.0.0.8/29", + "id": "616fb98f-46ca-475e-917e-2563e5a8cd20", + "label": "test_1" + } + ] +} diff --git a/doc/v3/api_samples/os-tenant-networks/networks-post-req.json b/doc/v3/api_samples/os-tenant-networks/networks-post-req.json new file mode 100644 index 000000000000..f47fc9d20152 --- /dev/null +++ b/doc/v3/api_samples/os-tenant-networks/networks-post-req.json @@ -0,0 +1,9 @@ +{ + "network": { + "label": "public", + "cidr": "172.0.0.0/24", + "vlan_start": 1, + "num_networks": 1, + "network_size": 255 + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/os-tenant-networks/networks-post-res.json b/doc/v3/api_samples/os-tenant-networks/networks-post-res.json new file mode 100644 index 000000000000..536a9a0a4afa --- /dev/null +++ b/doc/v3/api_samples/os-tenant-networks/networks-post-res.json @@ -0,0 +1,7 @@ +{ + "network": { + "cidr": "172.0.0.0/24", + "id": "5bbcc3c4-1da2-4437-a48a-66f15b1b13f9", + "label": "public" + } +} diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 71e9fb728fbe..6718563865d7 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -264,6 +264,8 @@ "compute_extension:v3:os-suspend-server:discoverable": "", "compute_extension:v3:os-suspend-server:suspend": "rule:admin_or_owner", "compute_extension:v3:os-suspend-server:resume": "rule:admin_or_owner", + "compute_extension:v3:os-tenant-networks": "rule:admin_or_owner", + "compute_extension:v3:os-tenant-networks:discoverable": "", "compute_extension:simple_tenant_usage:list": "rule:admin_api", "compute_extension:unshelve": "", "compute_extension:v3:os-shelve:unshelve": "", diff --git a/nova/api/openstack/compute/plugins/v3/tenant_networks.py b/nova/api/openstack/compute/plugins/v3/tenant_networks.py new file mode 100644 index 000000000000..90d3c29b730d --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/tenant_networks.py @@ -0,0 +1,217 @@ +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# 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 netaddr +import netaddr.core as netexc +from oslo.config import cfg +import six +from webob import exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova import context as nova_context +from nova import exception +from nova.i18n import _ +from nova.i18n import _LE +import nova.network +from nova.openstack.common import log as logging +from nova import quota + + +CONF = cfg.CONF +CONF.import_opt('enable_network_quota', + 'nova.api.openstack.compute.contrib.os_tenant_networks') +CONF.import_opt('use_neutron_default_nets', + 'nova.api.openstack.compute.contrib.os_tenant_networks') +CONF.import_opt('neutron_default_tenant_id', + 'nova.api.openstack.compute.contrib.os_tenant_networks') +CONF.import_opt('quota_networks', + 'nova.api.openstack.compute.contrib.os_tenant_networks') + + +ALIAS = 'os-tenant-networks' + +QUOTAS = quota.QUOTAS +LOG = logging.getLogger(__name__) +authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS) + + +def network_dict(network): + return {"id": network.get("uuid") or network.get("id"), + "cidr": str(network.get("cidr")), + "label": network.get("label")} + + +class TenantNetworkController(object): + def __init__(self, network_api=None): + self.network_api = nova.network.API() + self._default_networks = [] + + def _refresh_default_networks(self): + self._default_networks = [] + if CONF.use_neutron_default_nets == "True": + try: + self._default_networks = self._get_default_networks() + except Exception: + LOG.exception(_LE("Failed to get default networks")) + + def _get_default_networks(self): + project_id = CONF.neutron_default_tenant_id + ctx = nova_context.RequestContext(user_id=None, + project_id=project_id) + networks = {} + for n in self.network_api.get_all(ctx): + networks[n['id']] = n['label'] + return [{'id': k, 'label': v} for k, v in networks.iteritems()] + + @extensions.expected_errors(()) + def index(self, req): + context = req.environ['nova.context'] + authorize(context) + networks = list(self.network_api.get_all(context)) + if not self._default_networks: + self._refresh_default_networks() + networks.extend(self._default_networks) + return {'networks': [network_dict(n) for n in networks]} + + @extensions.expected_errors(404) + def show(self, req, id): + context = req.environ['nova.context'] + authorize(context) + try: + network = self.network_api.get(context, id) + except exception.NetworkNotFound: + msg = _("Network not found") + raise exc.HTTPNotFound(explanation=msg) + return {'network': network_dict(network)} + + @extensions.expected_errors((403, 404, 409)) + @wsgi.response(202) + def delete(self, req, id): + context = req.environ['nova.context'] + authorize(context) + reservation = None + try: + if CONF.enable_network_quota: + reservation = QUOTAS.reserve(context, networks=-1) + except Exception: + reservation = None + LOG.exception(_LE("Failed to update usages deallocating " + "network.")) + + def _rollback_quota(reservation): + if CONF.enable_network_quota and reservation: + QUOTAS.rollback(context, reservation) + + try: + self.network_api.delete(context, id) + except exception.PolicyNotAuthorized as e: + _rollback_quota(reservation) + raise exc.HTTPForbidden(explanation=six.text_type(e)) + except exception.NetworkInUse as e: + _rollback_quota(reservation) + raise exc.HTTPConflict(explanation=e.format_message()) + except exception.NetworkNotFound: + _rollback_quota(reservation) + msg = _("Network not found") + raise exc.HTTPNotFound(explanation=msg) + + if CONF.enable_network_quota and reservation: + QUOTAS.commit(context, reservation) + + @extensions.expected_errors((400, 403, 503)) + def create(self, req, body): + if not body: + _msg = _("Missing request body") + raise exc.HTTPBadRequest(explanation=_msg) + + context = req.environ["nova.context"] + authorize(context) + + network = body["network"] + keys = ["cidr", "cidr_v6", "ipam", "vlan_start", "network_size", + "num_networks"] + kwargs = dict((k, network.get(k)) for k in keys) + + label = network["label"] + + if not (kwargs["cidr"] or kwargs["cidr_v6"]): + msg = _("No CIDR requested") + raise exc.HTTPBadRequest(explanation=msg) + if kwargs["cidr"]: + try: + net = netaddr.IPNetwork(kwargs["cidr"]) + if net.size < 4: + msg = _("Requested network does not contain " + "enough (2+) usable hosts") + raise exc.HTTPBadRequest(explanation=msg) + except netexc.AddrFormatError: + msg = _("CIDR is malformed.") + raise exc.HTTPBadRequest(explanation=msg) + except netexc.AddrConversionError: + msg = _("Address could not be converted.") + raise exc.HTTPBadRequest(explanation=msg) + + networks = [] + try: + if CONF.enable_network_quota: + reservation = QUOTAS.reserve(context, networks=1) + except exception.OverQuota: + msg = _("Quota exceeded, too many networks.") + raise exc.HTTPBadRequest(explanation=msg) + + try: + networks = self.network_api.create(context, + label=label, **kwargs) + if CONF.enable_network_quota: + QUOTAS.commit(context, reservation) + except exception.PolicyNotAuthorized as e: + raise exc.HTTPForbidden(explanation=six.text_type(e)) + except Exception: + if CONF.enable_network_quota: + QUOTAS.rollback(context, reservation) + msg = _("Create networks failed") + LOG.exception(msg, extra=network) + raise exc.HTTPServiceUnavailable(explanation=msg) + return {"network": network_dict(networks[0])} + + +class TenantNetworks(extensions.V3APIExtensionBase): + """Tenant-based Network Management Extension.""" + + name = "TenantNetworks" + alias = ALIAS + version = 1 + + def get_resources(self): + ext = extensions.ResourceExtension(ALIAS, TenantNetworkController()) + return [ext] + + def get_controller_extensions(self): + return [] + + +def _sync_networks(context, project_id, session): + ctx = nova_context.RequestContext(user_id=None, project_id=project_id) + ctx = ctx.elevated() + networks = nova.network.api.API().get_all(ctx) + return dict(networks=len(networks)) + + +if CONF.enable_network_quota: + QUOTAS.register_resource(quota.ReservableResource('networks', + _sync_networks, + 'quota_networks')) diff --git a/nova/tests/api/openstack/compute/contrib/test_networks.py b/nova/tests/api/openstack/compute/contrib/test_networks.py index 857403af3ff4..96ebc369e7d4 100644 --- a/nova/tests/api/openstack/compute/contrib/test_networks.py +++ b/nova/tests/api/openstack/compute/contrib/test_networks.py @@ -29,6 +29,7 @@ from nova.api.openstack.compute.contrib import networks_associate from nova.api.openstack.compute.contrib import os_networks as networks from nova.api.openstack.compute.contrib import os_tenant_networks as tnet from nova.api.openstack.compute.plugins.v3 import networks as networks_v21 +from nova.api.openstack.compute.plugins.v3 import tenant_networks as tnet_v21 from nova.api.openstack import extensions import nova.context from nova import exception @@ -562,10 +563,12 @@ class NetworksAssociateTest(test.NoDBTestCase): req, uuid, {'disassociate_host': None}) -class TenantNetworksTest(test.NoDBTestCase): +class TenantNetworksTestV21(test.NoDBTestCase): + ctrlr = tnet_v21.TenantNetworkController + def setUp(self): - super(TenantNetworksTest, self).setUp() - self.controller = tnet.NetworkController() + super(TenantNetworksTestV21, self).setUp() + self.controller = self.ctrlr() self.flags(enable_network_quota=True) @mock.patch('nova.quota.QUOTAS.reserve') @@ -599,3 +602,7 @@ class TenantNetworksTest(test.NoDBTestCase): ex = exception.NetworkInUse(network_id=1) expex = webob.exc.HTTPConflict self._test_network_delete_exception(ex, expex) + + +class TenantNetworksTestV2(TenantNetworksTestV21): + ctrlr = tnet.NetworkController diff --git a/nova/tests/api/openstack/compute/contrib/test_tenant_networks.py b/nova/tests/api/openstack/compute/contrib/test_tenant_networks.py index fcaa8b775bb5..66c5cae7eebb 100644 --- a/nova/tests/api/openstack/compute/contrib/test_tenant_networks.py +++ b/nova/tests/api/openstack/compute/contrib/test_tenant_networks.py @@ -16,16 +16,19 @@ import mock import webob from nova.api.openstack.compute.contrib import os_tenant_networks as networks +from nova.api.openstack.compute.plugins.v3 import tenant_networks \ + as networks_v21 from nova import exception from nova import test from nova.tests.api.openstack import fakes -class NetworksTest(test.NoDBTestCase): +class NetworksTestV21(test.NoDBTestCase): + ctrl_class = networks_v21.TenantNetworkController def setUp(self): - super(NetworksTest, self).setUp() - self.controller = networks.NetworkController() + super(NetworksTestV21, self).setUp() + self.controller = self.ctrl_class() @mock.patch('nova.network.api.API.delete', side_effect=exception.NetworkInUse(network_id=1)) @@ -34,3 +37,7 @@ class NetworksTest(test.NoDBTestCase): self.assertRaises(webob.exc.HTTPConflict, self.controller.delete, req, 1) + + +class NetworksTestV2(NetworksTestV21): + ctrl_class = networks.NetworkController diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index 8a63eef249ec..4fd56c2c563a 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -256,6 +256,7 @@ policy_data = """ "compute_extension:v3:os-networks:view": "", "compute_extension:networks_associate": "", "compute_extension:os-tenant-networks": "", + "compute_extension:v3:os-tenant-networks": "", "compute_extension:v3:os-pause-server:pause": "", "compute_extension:v3:os-pause-server:unpause": "", "compute_extension:v3:os-pci:pci_servers": "", diff --git a/nova/tests/integrated/v3/api_samples/os-tenant-networks/networks-list-res.json.tpl b/nova/tests/integrated/v3/api_samples/os-tenant-networks/networks-list-res.json.tpl new file mode 100644 index 000000000000..757084d2f37b --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-tenant-networks/networks-list-res.json.tpl @@ -0,0 +1,14 @@ +{ + "networks": [ + { + "cidr": "10.0.0.0/29", + "id": "%(id)s", + "label": "test_0" + }, + { + "cidr": "10.0.0.8/29", + "id": "%(id)s", + "label": "test_1" + } + ] +} diff --git a/nova/tests/integrated/v3/api_samples/os-tenant-networks/networks-post-req.json.tpl b/nova/tests/integrated/v3/api_samples/os-tenant-networks/networks-post-req.json.tpl new file mode 100644 index 000000000000..fb1c2d3d062c --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-tenant-networks/networks-post-req.json.tpl @@ -0,0 +1,9 @@ +{ + "network": { + "label": "public", + "cidr": "172.0.0.0/24", + "vlan_start": 1, + "num_networks": 1, + "network_size": 255 + } +} diff --git a/nova/tests/integrated/v3/api_samples/os-tenant-networks/networks-post-res.json.tpl b/nova/tests/integrated/v3/api_samples/os-tenant-networks/networks-post-res.json.tpl new file mode 100644 index 000000000000..ff9e2273d306 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-tenant-networks/networks-post-res.json.tpl @@ -0,0 +1,7 @@ +{ + "network": { + "cidr": "172.0.0.0/24", + "id": "%(id)s", + "label": "public" + } +} diff --git a/nova/tests/integrated/v3/test_tenant_networks.py b/nova/tests/integrated/v3/test_tenant_networks.py new file mode 100644 index 000000000000..6ea3c7d7c6c5 --- /dev/null +++ b/nova/tests/integrated/v3/test_tenant_networks.py @@ -0,0 +1,61 @@ +# Copyright 2012 Nebula, Inc. +# Copyright 2014 IBM Corp. +# +# 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 oslo.config import cfg +from oslo.serialization import jsonutils + +import nova.quota +from nova.tests.integrated.v3 import api_sample_base + +CONF = cfg.CONF +CONF.import_opt('enable_network_quota', + 'nova.api.openstack.compute.contrib.os_tenant_networks') + + +class TenantNetworksJsonTests(api_sample_base.ApiSampleTestBaseV3): + extension_name = "os-tenant-networks" + + def setUp(self): + super(TenantNetworksJsonTests, self).setUp() + CONF.set_override("enable_network_quota", True) + + def fake(*args, **kwargs): + pass + + self.stubs.Set(nova.quota.QUOTAS, "reserve", fake) + self.stubs.Set(nova.quota.QUOTAS, "commit", fake) + self.stubs.Set(nova.quota.QUOTAS, "rollback", fake) + self.stubs.Set(nova.quota.QuotaEngine, "reserve", fake) + self.stubs.Set(nova.quota.QuotaEngine, "commit", fake) + self.stubs.Set(nova.quota.QuotaEngine, "rollback", fake) + + def test_list_networks(self): + response = self._do_get('os-tenant-networks') + subs = self._get_regexes() + self._verify_response('networks-list-res', subs, response, 200) + + def test_create_network(self): + response = self._do_post('os-tenant-networks', "networks-post-req", {}) + subs = self._get_regexes() + self._verify_response('networks-post-res', subs, response, 200) + + def test_delete_network(self): + response = self._do_post('os-tenant-networks', "networks-post-req", {}) + net = jsonutils.loads(response.content) + response = self._do_delete('os-tenant-networks/%s' % + net["network"]["id"]) + self.assertEqual(response.status_code, 202) + self.assertEqual(response.content, "") diff --git a/setup.cfg b/setup.cfg index c7431d0e36df..c6ba4d02dd73 100644 --- a/setup.cfg +++ b/setup.cfg @@ -121,6 +121,7 @@ nova.api.v3.extensions = shelve = nova.api.openstack.compute.plugins.v3.shelve:Shelve simple_tenant_usage = nova.api.openstack.compute.plugins.v3.simple_tenant_usage:SimpleTenantUsage suspend_server = nova.api.openstack.compute.plugins.v3.suspend_server:SuspendServer + tenant_networks = nova.api.openstack.compute.plugins.v3.tenant_networks:TenantNetworks used_limits = nova.api.openstack.compute.plugins.v3.used_limits:UsedLimits versions = nova.api.openstack.compute.plugins.v3.versions:Versions volumes = nova.api.openstack.compute.plugins.v3.volumes:Volumes