From e7a8dc0ce6e812ebd3874be2f18cf33e46095edb Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Fri, 14 Jun 2013 14:53:24 +0930 Subject: Port quota classes extension to v3 API Part 1 This changeset only copies the v2 files (implementation and test) into the appropriate v3 directories unchanged. The copy as-is will not be loaded by either the v2 or v3 extension loaders. The second changeset will then make the changes required for it to work as a v3 extension. This is being done in order to make reviewing of extension porting easier as gerrit will display only what is actually changed for v3 rather than entirely new files Partially implements blueprint nova-v3-api Change-Id: I328b32aee29021bae42cba5d97e0735e7b691ac9 --- .../openstack/compute/plugins/v3/quota_classes.py | 103 +++++++++++ .../compute/plugins/v3/test_quota_classes.py | 188 +++++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 nova/api/openstack/compute/plugins/v3/quota_classes.py create mode 100644 nova/tests/api/openstack/compute/plugins/v3/test_quota_classes.py diff --git a/nova/api/openstack/compute/plugins/v3/quota_classes.py b/nova/api/openstack/compute/plugins/v3/quota_classes.py new file mode 100644 index 000000000..361748df8 --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/quota_classes.py @@ -0,0 +1,103 @@ +# Copyright 2012 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 webob + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +import nova.context +from nova import db +from nova import exception +from nova import quota + + +QUOTAS = quota.QUOTAS + + +authorize = extensions.extension_authorizer('compute', 'quota_classes') + + +class QuotaClassTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('quota_class_set', + selector='quota_class_set') + root.set('id') + + for resource in QUOTAS.resources: + elem = xmlutil.SubTemplateElement(root, resource) + elem.text = resource + + return xmlutil.MasterTemplate(root, 1) + + +class QuotaClassSetsController(object): + + def _format_quota_set(self, quota_class, quota_set): + """Convert the quota object to a result dict.""" + + result = dict(id=str(quota_class)) + + for resource in QUOTAS.resources: + result[resource] = quota_set[resource] + + return dict(quota_class_set=result) + + @wsgi.serializers(xml=QuotaClassTemplate) + def show(self, req, id): + context = req.environ['nova.context'] + authorize(context) + try: + nova.context.authorize_quota_class_context(context, id) + return self._format_quota_set(id, + QUOTAS.get_class_quotas(context, id)) + except exception.NotAuthorized: + raise webob.exc.HTTPForbidden() + + @wsgi.serializers(xml=QuotaClassTemplate) + def update(self, req, id, body): + context = req.environ['nova.context'] + authorize(context) + quota_class = id + for key in body['quota_class_set'].keys(): + if key in QUOTAS: + value = int(body['quota_class_set'][key]) + try: + db.quota_class_update(context, quota_class, key, value) + except exception.QuotaClassNotFound: + db.quota_class_create(context, quota_class, key, value) + except exception.AdminRequired: + raise webob.exc.HTTPForbidden() + return {'quota_class_set': QUOTAS.get_class_quotas(context, + quota_class)} + + +class Quota_classes(extensions.ExtensionDescriptor): + """Quota classes management support.""" + + name = "QuotaClasses" + alias = "os-quota-class-sets" + namespace = ("http://docs.openstack.org/compute/ext/" + "quota-classes-sets/api/v1.1") + updated = "2012-03-12T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension('os-quota-class-sets', + QuotaClassSetsController()) + resources.append(res) + + return resources diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_quota_classes.py b/nova/tests/api/openstack/compute/plugins/v3/test_quota_classes.py new file mode 100644 index 000000000..c4f79118e --- /dev/null +++ b/nova/tests/api/openstack/compute/plugins/v3/test_quota_classes.py @@ -0,0 +1,188 @@ +# Copyright 2012 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. + +from lxml import etree +import webob + +from nova.api.openstack.compute.contrib import quota_classes +from nova.api.openstack import wsgi +from nova import test +from nova.tests.api.openstack import fakes + + +def quota_set(class_name): + return {'quota_class_set': {'id': class_name, 'metadata_items': 128, + 'ram': 51200, 'floating_ips': 10, + 'fixed_ips': -1, 'instances': 10, + 'injected_files': 5, 'cores': 20, + 'injected_file_content_bytes': 10240, + 'security_groups': 10, + 'security_group_rules': 20, 'key_pairs': 100, + 'injected_file_path_bytes': 255}} + + +class QuotaClassSetsTest(test.TestCase): + + def setUp(self): + super(QuotaClassSetsTest, self).setUp() + self.controller = quota_classes.QuotaClassSetsController() + + def test_format_quota_set(self): + raw_quota_set = { + 'instances': 10, + 'cores': 20, + 'ram': 51200, + 'floating_ips': 10, + 'fixed_ips': -1, + 'metadata_items': 128, + 'injected_files': 5, + 'injected_file_path_bytes': 255, + 'injected_file_content_bytes': 10240, + 'security_groups': 10, + 'security_group_rules': 20, + 'key_pairs': 100, + } + + quota_set = self.controller._format_quota_set('test_class', + raw_quota_set) + qs = quota_set['quota_class_set'] + + self.assertEqual(qs['id'], 'test_class') + self.assertEqual(qs['instances'], 10) + self.assertEqual(qs['cores'], 20) + self.assertEqual(qs['ram'], 51200) + self.assertEqual(qs['floating_ips'], 10) + self.assertEqual(qs['fixed_ips'], -1) + self.assertEqual(qs['metadata_items'], 128) + self.assertEqual(qs['injected_files'], 5) + self.assertEqual(qs['injected_file_path_bytes'], 255) + self.assertEqual(qs['injected_file_content_bytes'], 10240) + self.assertEqual(qs['security_groups'], 10) + self.assertEqual(qs['security_group_rules'], 20) + self.assertEqual(qs['key_pairs'], 100) + + def test_quotas_show_as_admin(self): + req = fakes.HTTPRequest.blank( + '/v2/fake4/os-quota-class-sets/test_class', + use_admin_context=True) + res_dict = self.controller.show(req, 'test_class') + + self.assertEqual(res_dict, quota_set('test_class')) + + def test_quotas_show_as_unauthorized_user(self): + req = fakes.HTTPRequest.blank( + '/v2/fake4/os-quota-class-sets/test_class') + self.assertRaises(webob.exc.HTTPForbidden, self.controller.show, + req, 'test_class') + + def test_quotas_update_as_admin(self): + body = {'quota_class_set': {'instances': 50, 'cores': 50, + 'ram': 51200, 'floating_ips': 10, + 'fixed_ips': -1, 'metadata_items': 128, + 'injected_files': 5, + 'injected_file_content_bytes': 10240, + 'injected_file_path_bytes': 255, + 'security_groups': 10, + 'security_group_rules': 20, + 'key_pairs': 100}} + + req = fakes.HTTPRequest.blank( + '/v2/fake4/os-quota-class-sets/test_class', + use_admin_context=True) + res_dict = self.controller.update(req, 'test_class', body) + + self.assertEqual(res_dict, body) + + def test_quotas_update_as_user(self): + body = {'quota_class_set': {'instances': 50, 'cores': 50, + 'ram': 51200, 'floating_ips': 10, + 'fixed_ips': -1, 'metadata_items': 128, + 'injected_files': 5, + 'injected_file_content_bytes': 10240, + 'security_groups': 10, + 'security_group_rules': 20, + 'key_pairs': 100, + }} + + req = fakes.HTTPRequest.blank( + '/v2/fake4/os-quota-class-sets/test_class') + self.assertRaises(webob.exc.HTTPForbidden, self.controller.update, + req, 'test_class', body) + + +class QuotaTemplateXMLSerializerTest(test.TestCase): + def setUp(self): + super(QuotaTemplateXMLSerializerTest, self).setUp() + self.serializer = quota_classes.QuotaClassTemplate() + self.deserializer = wsgi.XMLDeserializer() + + def test_serializer(self): + exemplar = dict(quota_class_set=dict( + id='test_class', + metadata_items=10, + injected_file_path_bytes=255, + injected_file_content_bytes=20, + ram=50, + floating_ips=60, + fixed_ips=-1, + instances=70, + injected_files=80, + security_groups=10, + security_group_rules=20, + key_pairs=100, + cores=90)) + text = self.serializer.serialize(exemplar) + + tree = etree.fromstring(text) + + self.assertEqual('quota_class_set', tree.tag) + self.assertEqual('test_class', tree.get('id')) + self.assertEqual(len(exemplar['quota_class_set']) - 1, len(tree)) + for child in tree: + self.assertTrue(child.tag in exemplar['quota_class_set']) + self.assertEqual(int(child.text), + exemplar['quota_class_set'][child.tag]) + + def test_deserializer(self): + exemplar = dict(quota_class_set=dict( + metadata_items='10', + injected_file_content_bytes='20', + ram='50', + floating_ips='60', + fixed_ips='-1', + instances='70', + injected_files='80', + security_groups='10', + security_group_rules='20', + key_pairs='100', + cores='90')) + intext = ("\n" + '' + '10' + '20' + '' + '50' + '60' + '-1' + '70' + '80' + '90' + '10' + '20' + '100' + '') + + result = self.deserializer.deserialize(intext)['body'] + self.assertEqual(result, exemplar) -- cgit