diff --git a/doc/v3/api_samples/os-quota-class-sets/quota-classes-show-get-resp.json b/doc/v3/api_samples/os-quota-class-sets/quota-classes-show-get-resp.json new file mode 100644 index 000000000000..6e477722dda8 --- /dev/null +++ b/doc/v3/api_samples/os-quota-class-sets/quota-classes-show-get-resp.json @@ -0,0 +1,17 @@ +{ + "quota_class_set": { + "cores": 20, + "fixed_ips": -1, + "floating_ips": 10, + "id": "test_class", + "injected_file_content_bytes": 10240, + "injected_file_path_bytes": 255, + "injected_files": 5, + "instances": 10, + "key_pairs": 100, + "metadata_items": 128, + "ram": 51200, + "security_group_rules": 20, + "security_groups": 10 + } +} diff --git a/doc/v3/api_samples/os-quota-class-sets/quota-classes-update-post-req.json b/doc/v3/api_samples/os-quota-class-sets/quota-classes-update-post-req.json new file mode 100644 index 000000000000..f074c829ffaa --- /dev/null +++ b/doc/v3/api_samples/os-quota-class-sets/quota-classes-update-post-req.json @@ -0,0 +1,15 @@ +{ + "quota_class_set": { + "instances": 50, + "cores": 50, + "ram": 51200, + "floating_ips": 10, + "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 + } +} diff --git a/doc/v3/api_samples/os-quota-class-sets/quota-classes-update-post-resp.json b/doc/v3/api_samples/os-quota-class-sets/quota-classes-update-post-resp.json new file mode 100644 index 000000000000..c86f86063b88 --- /dev/null +++ b/doc/v3/api_samples/os-quota-class-sets/quota-classes-update-post-resp.json @@ -0,0 +1,16 @@ +{ + "quota_class_set": { + "cores": 50, + "fixed_ips": -1, + "floating_ips": 10, + "injected_file_content_bytes": 10240, + "injected_file_path_bytes": 255, + "injected_files": 5, + "instances": 50, + "key_pairs": 100, + "metadata_items": 128, + "ram": 51200, + "security_group_rules": 20, + "security_groups": 10 + } +} diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 6d0a258eeedf..5612444d18cd 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -239,6 +239,8 @@ "compute_extension:v3:os-quota-sets:delete": "rule:admin_api", "compute_extension:v3:os-quota-sets:detail": "rule:admin_api", "compute_extension:quota_classes": "", + "compute_extension:v3:os-quota-class-sets": "", + "compute_extension:v3:os-quota-class-sets:discoverable": "", "compute_extension:rescue": "", "compute_extension:v3:os-rescue": "", "compute_extension:v3:os-rescue:discoverable": "", 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 000000000000..3a200a200382 --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/quota_classes.py @@ -0,0 +1,131 @@ +# 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 +import nova.context +from nova import db +from nova import exception +from nova.i18n import _ +from nova import quota +from nova import utils + + +QUOTAS = quota.QUOTAS +ALIAS = "os-quota-class-sets" + +# Quotas that are only enabled by specific extensions +EXTENDED_QUOTAS = {'server_groups': 'os-server-group-quotas', + 'server_group_members': 'os-server-group-quotas'} + + +authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS) + + +class QuotaClassSetsController(wsgi.Controller): + + supported_quotas = [] + + def __init__(self, **kwargs): + self.supported_quotas = QUOTAS.resources + extension_info = kwargs.pop('extension_info').get_extensions() + for resource, extension in EXTENDED_QUOTAS.items(): + if extension not in extension_info: + self.supported_quotas.remove(resource) + + def _format_quota_set(self, quota_class, quota_set): + """Convert the quota object to a result dict.""" + + if quota_class: + result = dict(id=str(quota_class)) + else: + result = {} + + for resource in self.supported_quotas: + if resource in quota_set: + result[resource] = quota_set[resource] + + return dict(quota_class_set=result) + + @extensions.expected_errors(403) + def show(self, req, id): + context = req.environ['nova.context'] + authorize(context) + try: + nova.context.authorize_quota_class_context(context, id) + values = QUOTAS.get_class_quotas(context, id) + return self._format_quota_set(id, values) + except exception.Forbidden: + raise webob.exc.HTTPForbidden() + + @extensions.expected_errors((400, 403)) + def update(self, req, id, body): + context = req.environ['nova.context'] + authorize(context) + quota_class = id + bad_keys = [] + + if not self.is_valid_body(body, 'quota_class_set'): + msg = _("quota_class_set not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + quota_class_set = body['quota_class_set'] + for key in quota_class_set.keys(): + if key not in self.supported_quotas: + bad_keys.append(key) + continue + try: + value = utils.validate_integer( + body['quota_class_set'][key], key) + except exception.InvalidInput as e: + raise webob.exc.HTTPBadRequest( + explanation=e.format_message()) + + if bad_keys: + msg = _("Bad key(s) %s in quota_set") % ",".join(bad_keys) + raise webob.exc.HTTPBadRequest(explanation=msg) + + for key in quota_class_set.keys(): + value = utils.validate_integer( + body['quota_class_set'][key], 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() + + values = QUOTAS.get_class_quotas(context, quota_class) + return self._format_quota_set(None, values) + + +class QuotaClasses(extensions.V3APIExtensionBase): + """Quota classes management support.""" + + name = "QuotaClasses" + alias = ALIAS + version = 1 + + def get_resources(self): + resources = [] + res = extensions.ResourceExtension( + ALIAS, + QuotaClassSetsController(extension_info=self.extension_info)) + resources.append(res) + return resources + + def get_controller_extensions(self): + return [] diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_quota_classes.py b/nova/tests/unit/api/openstack/compute/contrib/test_quota_classes.py index 228b44f369ce..6305dc06409a 100644 --- a/nova/tests/unit/api/openstack/compute/contrib/test_quota_classes.py +++ b/nova/tests/unit/api/openstack/compute/contrib/test_quota_classes.py @@ -17,6 +17,9 @@ from lxml import etree import webob from nova.api.openstack.compute.contrib import quota_classes +from nova.api.openstack.compute import plugins +from nova.api.openstack.compute.plugins.v3 import quota_classes \ + as quota_classes_v21 from nova.api.openstack import extensions from nova.api.openstack import wsgi from nova import test @@ -34,13 +37,16 @@ def quota_set(class_name): 'injected_file_path_bytes': 255}} -class QuotaClassSetsTest(test.TestCase): +class QuotaClassSetsTestV21(test.TestCase): def setUp(self): - super(QuotaClassSetsTest, self).setUp() - self.ext_mgr = extensions.ExtensionManager() - self.ext_mgr.extensions = {} - self.controller = quota_classes.QuotaClassSetsController(self.ext_mgr) + super(QuotaClassSetsTestV21, self).setUp() + self._setup() + + def _setup(self): + ext_info = plugins.LoadedExtensionInfo() + self.controller = quota_classes_v21.QuotaClassSetsController( + extension_info=ext_info) def test_format_quota_set(self): raw_quota_set = { @@ -156,6 +162,14 @@ class QuotaClassSetsTest(test.TestCase): req, 'test_class', body) +class QuotaClassSetsTestV2(QuotaClassSetsTestV21): + + def _setup(self): + ext_mgr = extensions.ExtensionManager() + ext_mgr.extensions = {} + self.controller = quota_classes.QuotaClassSetsController(ext_mgr) + + class QuotaTemplateXMLSerializerTest(test.TestCase): def setUp(self): super(QuotaTemplateXMLSerializerTest, self).setUp() diff --git a/nova/tests/unit/fake_policy.py b/nova/tests/unit/fake_policy.py index e99919f0c58d..e34a1ebe9c52 100644 --- a/nova/tests/unit/fake_policy.py +++ b/nova/tests/unit/fake_policy.py @@ -277,6 +277,7 @@ policy_data = """ "compute_extension:v3:os-quota-sets:delete": "", "compute_extension:v3:os-quota-sets:detail": "", "compute_extension:quota_classes": "", + "compute_extension:v3:os-quota-class-sets": "", "compute_extension:rescue": "", "compute_extension:v3:os-rescue": "", "compute_extension:security_group_default_rules": "", diff --git a/nova/tests/unit/integrated/v3/api_samples/os-quota-class-sets/quota-classes-show-get-resp.json.tpl b/nova/tests/unit/integrated/v3/api_samples/os-quota-class-sets/quota-classes-show-get-resp.json.tpl new file mode 100644 index 000000000000..f9a94e760af7 --- /dev/null +++ b/nova/tests/unit/integrated/v3/api_samples/os-quota-class-sets/quota-classes-show-get-resp.json.tpl @@ -0,0 +1,17 @@ +{ + "quota_class_set": { + "cores": 20, + "floating_ips": 10, + "fixed_ips": -1, + "id": "%(set_id)s", + "injected_file_content_bytes": 10240, + "injected_file_path_bytes": 255, + "injected_files": 5, + "instances": 10, + "key_pairs": 100, + "metadata_items": 128, + "ram": 51200, + "security_group_rules": 20, + "security_groups": 10 + } +} diff --git a/nova/tests/unit/integrated/v3/api_samples/os-quota-class-sets/quota-classes-update-post-req.json.tpl b/nova/tests/unit/integrated/v3/api_samples/os-quota-class-sets/quota-classes-update-post-req.json.tpl new file mode 100644 index 000000000000..483fda8c53e7 --- /dev/null +++ b/nova/tests/unit/integrated/v3/api_samples/os-quota-class-sets/quota-classes-update-post-req.json.tpl @@ -0,0 +1,16 @@ +{ + "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 + } +} diff --git a/nova/tests/unit/integrated/v3/api_samples/os-quota-class-sets/quota-classes-update-post-resp.json.tpl b/nova/tests/unit/integrated/v3/api_samples/os-quota-class-sets/quota-classes-update-post-resp.json.tpl new file mode 100644 index 000000000000..c36783f2f0e6 --- /dev/null +++ b/nova/tests/unit/integrated/v3/api_samples/os-quota-class-sets/quota-classes-update-post-resp.json.tpl @@ -0,0 +1,16 @@ +{ + "quota_class_set": { + "cores": 50, + "floating_ips": 10, + "fixed_ips": -1, + "injected_file_content_bytes": 10240, + "injected_file_path_bytes": 255, + "injected_files": 5, + "instances": 50, + "key_pairs": 100, + "metadata_items": 128, + "ram": 51200, + "security_group_rules": 20, + "security_groups": 10 + } +} diff --git a/nova/tests/unit/integrated/v3/test_quota_classes.py b/nova/tests/unit/integrated/v3/test_quota_classes.py new file mode 100644 index 000000000000..e5fac619501a --- /dev/null +++ b/nova/tests/unit/integrated/v3/test_quota_classes.py @@ -0,0 +1,36 @@ +# Copyright 2012 Nebula, Inc. +# Copyright 2013 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 nova.tests.unit.integrated.v3 import api_sample_base + + +class QuotaClassesSampleJsonTests(api_sample_base.ApiSampleTestBaseV3): + extension_name = "os-quota-class-sets" + set_id = 'test_class' + + def test_show_quota_classes(self): + # Get api sample to show quota classes. + response = self._do_get('os-quota-class-sets/%s' % self.set_id) + subs = {'set_id': self.set_id} + self._verify_response('quota-classes-show-get-resp', subs, + response, 200) + + def test_update_quota_classes(self): + # Get api sample to update quota classes. + response = self._do_put('os-quota-class-sets/%s' % self.set_id, + 'quota-classes-update-post-req', + {}) + self._verify_response('quota-classes-update-post-resp', + {}, response, 200) diff --git a/setup.cfg b/setup.cfg index fc6c35f1c04d..8d679b5fa728 100644 --- a/setup.cfg +++ b/setup.cfg @@ -111,6 +111,7 @@ nova.api.v3.extensions = networks_associate = nova.api.openstack.compute.plugins.v3.networks_associate:NetworksAssociate pause_server = nova.api.openstack.compute.plugins.v3.pause_server:PauseServer pci = nova.api.openstack.compute.plugins.v3.pci:Pci + quota_classes = nova.api.openstack.compute.plugins.v3.quota_classes:QuotaClasses quota_sets = nova.api.openstack.compute.plugins.v3.quota_sets:QuotaSets remote_consoles = nova.api.openstack.compute.plugins.v3.remote_consoles:RemoteConsoles rescue = nova.api.openstack.compute.plugins.v3.rescue:Rescue