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
This commit is contained in:
Chris Yeoh 2014-09-16 20:21:27 +09:30
parent b16f7e08c5
commit cc452485c2
13 changed files with 362 additions and 6 deletions

View File

@ -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"
}
]
}

View File

@ -0,0 +1,9 @@
{
"network": {
"label": "public",
"cidr": "172.0.0.0/24",
"vlan_start": 1,
"num_networks": 1,
"network_size": 255
}
}

View File

@ -0,0 +1,7 @@
{
"network": {
"cidr": "172.0.0.0/24",
"id": "5bbcc3c4-1da2-4437-a48a-66f15b1b13f9",
"label": "public"
}
}

View File

@ -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": "",

View File

@ -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'))

View File

@ -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

View File

@ -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

View File

@ -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": "",

View File

@ -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"
}
]
}

View File

@ -0,0 +1,9 @@
{
"network": {
"label": "public",
"cidr": "172.0.0.0/24",
"vlan_start": 1,
"num_networks": 1,
"network_size": 255
}
}

View File

@ -0,0 +1,7 @@
{
"network": {
"cidr": "172.0.0.0/24",
"id": "%(id)s",
"label": "public"
}
}

View File

@ -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, "")

View File

@ -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