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:
parent
b16f7e08c5
commit
cc452485c2
14
doc/v3/api_samples/os-tenant-networks/networks-list-res.json
Normal file
14
doc/v3/api_samples/os-tenant-networks/networks-list-res.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"network": {
|
||||
"label": "public",
|
||||
"cidr": "172.0.0.0/24",
|
||||
"vlan_start": 1,
|
||||
"num_networks": 1,
|
||||
"network_size": 255
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"network": {
|
||||
"cidr": "172.0.0.0/24",
|
||||
"id": "5bbcc3c4-1da2-4437-a48a-66f15b1b13f9",
|
||||
"label": "public"
|
||||
}
|
||||
}
|
@ -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": "",
|
||||
|
217
nova/api/openstack/compute/plugins/v3/tenant_networks.py
Normal file
217
nova/api/openstack/compute/plugins/v3/tenant_networks.py
Normal 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'))
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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": "",
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"network": {
|
||||
"label": "public",
|
||||
"cidr": "172.0.0.0/24",
|
||||
"vlan_start": 1,
|
||||
"num_networks": 1,
|
||||
"network_size": 255
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"network": {
|
||||
"cidr": "172.0.0.0/24",
|
||||
"id": "%(id)s",
|
||||
"label": "public"
|
||||
}
|
||||
}
|
61
nova/tests/integrated/v3/test_tenant_networks.py
Normal file
61
nova/tests/integrated/v3/test_tenant_networks.py
Normal 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, "")
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user