From 3945b24062ead44f5ce95ed22f57d0cdf1e7147a Mon Sep 17 00:00:00 2001 From: Paulo Ewerton Gomes Fragoso Date: Tue, 16 Jun 2015 18:21:23 +0000 Subject: [PATCH] Adding Identity NGProjects API This patch adds some projects-related endpoints to the Nova, Cinder and Neutron REST/Angular APIs. These changes are needed for the angular Identity Projects actions that handle mainly project quota updates. Partially-Implements: blueprint angularize-identity-projects Change-Id: I98b8eb9e5e7bfb0d49a77c00115fe62d412abe21 --- openstack_dashboard/api/rest/cinder.py | 29 +++++ openstack_dashboard/api/rest/neutron.py | 61 ++++++++++ openstack_dashboard/api/rest/nova.py | 48 ++++++++ .../openstack-service-api/cinder.service.js | 25 ++++- .../cinder.service.spec.js | 7 ++ .../openstack-service-api/neutron.service.js | 41 ++++++- .../neutron.service.spec.js | 21 ++++ .../openstack-service-api/nova.service.js | 42 ++++++- .../nova.service.spec.js | 21 ++++ .../test/api_tests/cinder_rest_tests.py | 52 +++++++++ .../test/api_tests/neutron_rest_tests.py | 104 ++++++++++++++++++ .../test/api_tests/nova_rest_tests.py | 76 +++++++++++++ 12 files changed, 520 insertions(+), 7 deletions(-) diff --git a/openstack_dashboard/api/rest/cinder.py b/openstack_dashboard/api/rest/cinder.py index 523d1fc506..e724ae15de 100644 --- a/openstack_dashboard/api/rest/cinder.py +++ b/openstack_dashboard/api/rest/cinder.py @@ -300,3 +300,32 @@ class DefaultQuotaSets(generic.View): api.cinder.default_quota_update(request, **cinder_data) else: raise rest_utils.AjaxError(501, _('Service Cinder is disabled.')) + + +@urls.register +class QuotaSets(generic.View): + """API for setting quotas for a given project. + """ + url_regex = r'cinder/quota-sets/(?P[0-9a-f]+)$' + + @rest_utils.ajax(data_required=True) + def patch(self, request, project_id): + """Update a single project quota data. + + The PATCH data should be an application/json object with the + attributes to set to new quota values. + + This method returns HTTP 204 (no content) on success. + """ + # Filters cinder quota fields + disabled_quotas = quotas.get_disabled_quotas(request) + + if api.cinder.is_volume_service_enabled(): + cinder_data = { + key: request.DATA[key] for key in quotas.CINDER_QUOTA_FIELDS + if key not in disabled_quotas + } + + api.cinder.tenant_quota_update(request, project_id, **cinder_data) + else: + raise rest_utils.AjaxError(501, _('Service Cinder is disabled.')) diff --git a/openstack_dashboard/api/rest/neutron.py b/openstack_dashboard/api/rest/neutron.py index 75c14922c7..8098d58c6a 100644 --- a/openstack_dashboard/api/rest/neutron.py +++ b/openstack_dashboard/api/rest/neutron.py @@ -15,11 +15,13 @@ """API over the neutron service. """ +from django.utils.translation import ugettext_lazy as _ from django.views import generic from openstack_dashboard import api from openstack_dashboard.api.rest import urls from openstack_dashboard.api.rest import utils as rest_utils +from openstack_dashboard.usage import quotas @urls.register @@ -173,3 +175,62 @@ class Extensions(generic.View): """ result = api.neutron.list_extensions(request) return {'items': [e for e in result]} + + +class DefaultQuotaSets(generic.View): + """API for getting default quotas for neutron + """ + url_regex = r'neutron/quota-sets/defaults/$' + + @rest_utils.ajax() + def get(self, request): + if api.base.is_service_enabled(request, 'network'): + quota_set = api.neutron.tenant_quota_get( + request, request.user.tenant_id) + + result = [{ + 'display_name': quotas.QUOTA_NAMES.get( + quota.name, + quota.name.replace('_', ' ').title() + ) + '', + 'name': quota.name, + 'limit': quota.limit + } for quota in quota_set] + + return {'items': result} + else: + raise rest_utils.AjaxError(501, _('Service Neutron is disabled.')) + + +@urls.register +class QuotasSets(generic.View): + """API for setting quotas of a given project. + """ + url_regex = r'neutron/quotas-sets/(?P[0-9a-f]+)$' + + @rest_utils.ajax(data_required=True) + def patch(self, request, project_id): + """Update a single project quota data. + + The PATCH data should be an application/json object with the + attributes to set to new quota values. + + This method returns HTTP 204 (no content) on success. + """ + # Filters only neutron quota fields + disabled_quotas = quotas.get_disabled_quotas(request) + + if api.base.is_service_enabled(request, 'network') and \ + api.neutron.is_extension_supported(request, 'quotas'): + neutron_data = { + key: request.DATA[key] for key in quotas.NEUTRON_QUOTA_FIELDS + if key not in disabled_quotas + } + + api.neutron.tenant_quota_update(request, + project_id, + **neutron_data) + else: + message = _('Service Neutron is disabled or quotas extension not ' + 'available.') + raise rest_utils.AjaxError(501, message) diff --git a/openstack_dashboard/api/rest/nova.py b/openstack_dashboard/api/rest/nova.py index c9ff2eb8ac..67ea2dab9b 100644 --- a/openstack_dashboard/api/rest/nova.py +++ b/openstack_dashboard/api/rest/nova.py @@ -572,3 +572,51 @@ class DefaultQuotaSets(generic.View): api.nova.default_quota_update(request, **nova_data) else: raise rest_utils.AjaxError(501, _('Service Nova is disabled.')) + + +@urls.register +class EditableQuotaSets(generic.View): + """API for editable quotas. + """ + url_regex = r'nova/quota-sets/editable/$' + + @rest_utils.ajax() + def get(self, request): + """Get a list of editable quota fields. + + The listing result is an object with property "items". Each item + is an editable quota. Returns an empty list in case no editable + quota is found. + """ + disabled_quotas = quotas.get_disabled_quotas(request) + editable_quotas = [quota for quota in quotas.QUOTA_FIELDS + if quota not in disabled_quotas] + return {'items': editable_quotas} + + +@urls.register +class QuotaSets(generic.View): + """API for setting quotas for a given project. + """ + url_regex = r'nova/quota-sets/(?P[0-9a-f]+)$' + + @rest_utils.ajax(data_required=True) + def patch(self, request, project_id): + """Update a single project quota data. + + The PATCH data should be an application/json object with the + attributes to set to new quota values. + + This method returns HTTP 204 (no content) on success. + """ + disabled_quotas = quotas.get_disabled_quotas(request) + + if api.base.is_service_enabled(request, 'compute'): + nova_data = { + key: request.DATA[key] for key in quotas.NOVA_QUOTA_FIELDS + if key not in disabled_quotas + } + + api.nova.tenant_quota_update(request, project_id, **nova_data) + else: + raise rest_utils.AjaxError(501, _('Service Nova is disabled.')) diff --git a/openstack_dashboard/static/app/core/openstack-service-api/cinder.service.js b/openstack_dashboard/static/app/core/openstack-service-api/cinder.service.js index 547927748d..333b82bf5a 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/cinder.service.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/cinder.service.js @@ -47,7 +47,8 @@ getAbsoluteLimits: getAbsoluteLimits, getServices: getServices, getDefaultQuotaSets: getDefaultQuotaSets, - setDefaultQuotaSets: setDefaultQuotaSets + setDefaultQuotaSets: setDefaultQuotaSets, + updateProjectQuota: updateProjectQuota }; return service; @@ -199,8 +200,7 @@ var config = params ? {'params': params} : {}; return apiService.get('/api/cinder/volumesnapshots/', config) .error(function () { - toastService.add('error', - gettext('Unable to retrieve the volume snapshots.')); + toastService.add('error', gettext('Unable to retrieve the volume snapshots.')); }); } @@ -322,5 +322,24 @@ }); } + // Quota Sets + + /** + * @name updateProjectQuota + * @description + * Update a single project quota data. + * @param {application/json} quota + * A JSON object with the atributes to set to new quota values. + * @param {string} projectId + * Specifies the id of the project that'll have the quota data updated. + */ + function updateProjectQuota(quota, projectId) { + var url = '/api/cinder/quota-sets/' + projectId; + return apiService.patch(url, quota) + .error(function() { + toastService.add('error', gettext('Unable to update project quota data.')); + }); + } } + }()); diff --git a/openstack_dashboard/static/app/core/openstack-service-api/cinder.service.spec.js b/openstack_dashboard/static/app/core/openstack-service-api/cinder.service.spec.js index 542831c1bc..f7951735fc 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/cinder.service.spec.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/cinder.service.spec.js @@ -158,6 +158,13 @@ method: 'patch', path: '/api/cinder/quota-sets/defaults/', error: 'Unable to set the default quotas.' + }, + { func: 'updateProjectQuota', + method: 'patch', + path: '/api/cinder/quota-sets/42', + data: {'volumes': 42}, + error: 'Unable to update project quota data.', + testInput: [{'volumes': 42}, 42] } ]; diff --git a/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.js b/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.js index 8ed3323c17..7864796618 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.js @@ -41,7 +41,9 @@ createSubnet: createSubnet, getPorts: getPorts, getAgents: getAgents, - getExtensions: getExtensions + getExtensions: getExtensions, + getDefaultQuotaSets: getDefaultQuotaSets, + updateProjectQuota: updateProjectQuota }; return service; @@ -297,6 +299,43 @@ toastService.add('error', gettext('Unable to retrieve the extensions.')); }); } + + // Default Quota Sets + + /** + * @name getDefaultQuotaSets + * @description + * Get default quotasets + * + * The listing result is an object with property "items." Each item is + * a quota. + * + */ + function getDefaultQuotaSets() { + return apiService.get('/api/neutron/quota-sets/defaults/') + .error(function() { + toastService.add('error', gettext('Unable to retrieve the default quotas.')); + }); + } + + // Quotas Extension + + /** + * @name updateProjectQuota + * @description + * Update a single project quota data. + * @param {application/json} quota + * A JSON object with the atributes to set to new quota values. + * @param {string} projectId + * Specifies the id of the project that'll have the quota data updated. + */ + function updateProjectQuota(quota, projectId) { + var url = '/api/neutron/quotas-sets/' + projectId; + return apiService.patch(url, quota) + .error(function() { + toastService.add('error', gettext('Unable to update project quota data.')); + }); + } } }()); diff --git a/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.spec.js b/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.spec.js index cf65a21d15..9959a85f36 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.spec.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.spec.js @@ -104,6 +104,27 @@ "method": "get", "path": "/api/neutron/extensions/", "error": "Unable to retrieve the extensions." + }, + { + "func": "getDefaultQuotaSets", + "method": "get", + "path": "/api/neutron/quota-sets/defaults/", + "error": "Unable to retrieve the default quotas." + }, + { + "func": "updateProjectQuota", + "method": "patch", + "path": "/api/neutron/quotas-sets/42", + "data": { + "network": 42 + }, + "error": "Unable to update project quota data.", + "testInput": [ + { + "network": 42 + }, + 42 + ] } ]; diff --git a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js index 515ea05176..1340367d01 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js @@ -62,7 +62,9 @@ updateFlavor: updateFlavor, deleteFlavor: deleteFlavor, getDefaultQuotaSets: getDefaultQuotaSets, - setDefaultQuotaSets: setDefaultQuotaSets + setDefaultQuotaSets: setDefaultQuotaSets, + getEditableQuotas: getEditableQuotas, + updateProjectQuota: updateProjectQuota }; return service; @@ -516,7 +518,7 @@ // Default Quota Sets /** - * @name horizon.app.core.openstack-service-api.nova.getDefaultQuotaSets + * @name getDefaultQuotaSets * @description * Get default quotasets * @@ -532,7 +534,7 @@ } /** - * @name horizon.app.core.openstack-service-api.nova.setDefaultQuotaSets + * @name setDefaultQuotaSets * @description * Set default quotasets * @@ -544,6 +546,40 @@ }); } + // Quota Sets + + /** + * @name getEditableQuotas + * @description + * Get a list of editable quota fields. + * The listing result is an object with property "items." Each item is + * an editable quota field. + * + */ + function getEditableQuotas() { + return apiService.get('/api/nova/quota-sets/editable/') + .error(function() { + toastService.add('error', gettext('Unable to retrieve the editable quotas.')); + }); + } + + /** + * @name updateProjectQuota + * @description + * Update a single project quota data. + * @param {application/json} quota + * A JSON object with the atributes to set to new quota values. + * @param {string} projectId + * Specifies the id of the project that'll have the quota data updated. + */ + function updateProjectQuota(quota, projectId) { + var url = '/api/nova/quota-sets/' + projectId; + return apiService.patch(url, quota) + .error(function() { + toastService.add('error', gettext('Unable to update project quota data.')); + }); + } + /** * @ngdoc function * @name getCreateKeypairUrl diff --git a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js index b56855572f..f13dabe198 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js @@ -343,6 +343,27 @@ ], "path": "/api/nova/quota-sets/defaults/", "error": "Unable to set the default quotas." + }, + { + "func": "getEditableQuotas", + "method": "get", + "path": "/api/nova/quota-sets/editable/", + "error": "Unable to retrieve the editable quotas." + }, + { + "func": "updateProjectQuota", + "method": "patch", + "path": "/api/nova/quota-sets/42", + "data": { + "cores": 42 + }, + "error": "Unable to update project quota data.", + "testInput": [ + { + "cores": 42 + }, + 42 + ] } ]; diff --git a/openstack_dashboard/test/api_tests/cinder_rest_tests.py b/openstack_dashboard/test/api_tests/cinder_rest_tests.py index 791b5819d3..1cbed49401 100644 --- a/openstack_dashboard/test/api_tests/cinder_rest_tests.py +++ b/openstack_dashboard/test/api_tests/cinder_rest_tests.py @@ -346,3 +346,55 @@ class CinderRestTestCase(test.TestCase): '"Service Cinder is disabled."') cc.default_quota_update.assert_not_called() + + @mock.patch.object(cinder.api, 'cinder') + @mock.patch.object(cinder, 'quotas') + def test_quota_sets_patch(self, qc, cc): + quota_set = self.cinder_quotas.list()[0] + quota_data = {} + + for quota in quota_set: + quota_data[quota.name] = quota.limit + + request = self.mock_rest_request(body=''' + {"volumes": "15", "snapshots": "5000", + "gigabytes": "5", "cores": "10"} + ''') + + qc.get_disabled_quotas.return_value = [] + qc.CINDER_QUOTA_FIELDS = (n for n in quota_data) + cc.is_volume_service_enabled.return_value = True + + response = cinder.QuotaSets().patch(request, 'spam123') + + self.assertStatusCode(response, 204) + self.assertEqual(response.content.decode('utf-8'), '') + cc.tenant_quota_update.assert_called_once_with(request, 'spam123', + volumes='15', + snapshots='5000', + gigabytes='5') + + @mock.patch.object(cinder.api, 'cinder') + @mock.patch.object(cinder, 'quotas') + def test_quota_sets_when_service_is_disabled(self, qc, cc): + quota_set = self.cinder_quotas.list()[0] + quota_data = {} + + for quota in quota_set: + quota_data[quota.name] = quota.limit + + request = self.mock_rest_request(body=''' + {"volumes": "15", "snapshots": "5000", + "gigabytes": "5", "cores": "10"} + ''') + + qc.get_disabled_quotas.return_value = [] + qc.CINDER_QUOTA_FIELDS = (n for n in quota_data) + cc.is_volume_service_enabled.return_value = False + + response = cinder.QuotaSets().patch(request, 'spam123') + + self.assertStatusCode(response, 501) + self.assertEqual(response.content.decode('utf-8'), + '"Service Cinder is disabled."') + cc.tenant_quota_update.assert_not_called() diff --git a/openstack_dashboard/test/api_tests/neutron_rest_tests.py b/openstack_dashboard/test/api_tests/neutron_rest_tests.py index 536ae47695..22dd64b0b9 100644 --- a/openstack_dashboard/test/api_tests/neutron_rest_tests.py +++ b/openstack_dashboard/test/api_tests/neutron_rest_tests.py @@ -16,6 +16,7 @@ import mock from openstack_dashboard import api +from openstack_dashboard.api import base from openstack_dashboard.api.rest import neutron from openstack_dashboard.test import helpers as test from openstack_dashboard.test.test_data import neutron_data @@ -174,6 +175,109 @@ class NeutronExtensionsTestCase(test.TestCase): nc.list_extensions.assert_called_once_with(request) +class NeutronDefaultQuotasTestCase(test.TestCase): + @test.create_stubs({base: ('is_service_enabled',)}) + @mock.patch.object(neutron.api, 'neutron') + def test_quotas_sets_defaults_get_when_service_is_enabled(self, client): + filters = {'user': {'tenant_id': 'tenant'}} + request = self.mock_rest_request(**{'GET': dict(filters)}) + + base.is_service_enabled(request, 'network').AndReturn(True) + + client.tenant_quota_get.return_value = [ + base.Quota("network", 100), + base.Quota("q2", 101)] + + self.mox.ReplayAll() + + response = neutron.DefaultQuotaSets().get(request) + self.assertStatusCode(response, 200) + self.assertItemsCollectionEqual(response, [ + {'limit': 100, 'display_name': 'Networks', 'name': 'network'}, + {'limit': 101, 'display_name': 'Q2', 'name': 'q2'}]) + + client.tenant_quota_get.assert_called_once_with( + request, + request.user.tenant_id) + + @test.create_stubs({neutron.api.base: ('is_service_enabled',)}) + @mock.patch.object(neutron.api, 'neutron') + def test_quota_sets_defaults_get_when_service_is_disabled(self, client): + filters = {'user': {'tenant_id': 'tenant'}} + request = self.mock_rest_request(**{'GET': dict(filters)}) + + base.is_service_enabled(request, 'network').AndReturn(False) + + self.mox.ReplayAll() + + response = neutron.DefaultQuotaSets().get(request) + self.assertStatusCode(response, 501) + self.assertEqual(response.content.decode('utf-8'), + '"Service Neutron is disabled."') + + client.tenant_quota_get.assert_not_called() + + +class NeutronQuotaSetsTestCase(test.TestCase): + def setUp(self): + super(NeutronQuotaSetsTestCase, self).setUp() + + quota_set = self.neutron_quotas.list()[0] + self._quota_data = {} + + for quota in quota_set: + self._quota_data[quota.name] = quota.limit + + @mock.patch.object(neutron, 'quotas') + @mock.patch.object(neutron.api, 'neutron') + @mock.patch.object(neutron.api, 'base') + def test_quotas_sets_patch(self, bc, nc, qc): + request = self.mock_rest_request(body=''' + {"network": "5", "subnet": "5", "port": "50", + "router": "5", "floatingip": "50", + "security_group": "5", "security_group_rule": "50", + "volumes": "5", "cores": "50"} + ''') + + qc.get_disabled_quotas.return_value = [] + qc.NEUTRON_QUOTA_FIELDS = (n for n in self._quota_data) + bc.is_service_enabled.return_value = True + nc.is_extension_supported.return_value = True + + response = neutron.QuotasSets().patch(request, 'spam123') + + self.assertStatusCode(response, 204) + self.assertEqual(response.content.decode('utf-8'), '') + nc.tenant_quota_update.assert_called_once_with( + request, 'spam123', network='5', + subnet='5', port='50', router='5', + floatingip='50', security_group='5', + security_group_rule='50') + + @mock.patch.object(neutron, 'quotas') + @mock.patch.object(neutron.api, 'neutron') + @mock.patch.object(neutron.api, 'base') + def test_quotas_sets_patch_when_service_is_disabled(self, bc, nc, qc): + request = self.mock_rest_request(body=''' + {"network": "5", "subnet": "5", "port": "50", + "router": "5", "floatingip": "50", + "security_group": "5", "security_group_rule": "50", + "volumes": "5", "cores": "50"} + ''') + + qc.get_disabled_quotas.return_value = [] + qc.NEUTRON_QUOTA_FIELDS = (n for n in self._quota_data) + bc.is_service_enabled.return_value = False + + response = neutron.QuotasSets().patch(request, 'spam123') + message = \ + '"Service Neutron is disabled or quotas extension not available."' + + self.assertStatusCode(response, 501) + self.assertEqual(response.content.decode('utf-8'), message) + nc.tenant_quota_update.assert_not_called() + + def mock_obj_to_dict(r): return mock.Mock(**{'to_dict.return_value': r}) diff --git a/openstack_dashboard/test/api_tests/nova_rest_tests.py b/openstack_dashboard/test/api_tests/nova_rest_tests.py index 7813c2e5ca..35e40187aa 100644 --- a/openstack_dashboard/test/api_tests/nova_rest_tests.py +++ b/openstack_dashboard/test/api_tests/nova_rest_tests.py @@ -769,3 +769,79 @@ class NovaRestTestCase(test.TestCase): '"Service Nova is disabled."') nc.default_quota_update.assert_not_called() + + @mock.patch.object(nova, 'quotas') + @mock.patch.object(nova.api, 'nova') + def test_editable_quotas_get(self, nc, qc): + disabled_quotas = ['floating_ips', 'fixed_ips', + 'security_groups', 'security_group_rules'] + editable_quotas = ['cores', 'volumes', 'network', 'fixed_ips'] + qc.get_disabled_quotas.return_value = disabled_quotas + qc.QUOTA_FIELDS = editable_quotas + request = self.mock_rest_request() + response = nova.EditableQuotaSets().get(request) + self.assertStatusCode(response, 200) + self.assertItemsCollectionEqual(response, + ['cores', 'volumes', 'network']) + + @mock.patch.object(nova.api, 'nova') + @mock.patch.object(nova.api, 'base') + @mock.patch.object(nova, 'quotas') + def test_quota_sets_patch(self, qc, bc, nc): + quota_data = dict(cores='15', instances='5', + ram='50000', metadata_items='150', + injected_files='5', + injected_file_content_bytes='10240', + floating_ips='50', fixed_ips='5', + security_groups='10', + security_group_rules='100') + + request = self.mock_rest_request(body=''' + {"cores": "15", "ram": "50000", "instances": "5", + "metadata_items": "150", "injected_files": "5", + "injected_file_content_bytes": "10240", "floating_ips": "50", + "fixed_ips": "5", "security_groups": "10" , + "security_group_rules": "100", "volumes": "10"} + ''') + + qc.get_disabled_quotas.return_value = [] + qc.NOVA_QUOTA_FIELDS = (n for n in quota_data) + bc.is_service_enabled.return_value = True + + response = nova.QuotaSets().patch(request, 'spam123') + + self.assertStatusCode(response, 204) + self.assertEqual(response.content.decode('utf-8'), '') + nc.tenant_quota_update.assert_called_once_with( + request, 'spam123', **quota_data) + + @mock.patch.object(nova.api, 'nova') + @mock.patch.object(nova.api, 'base') + @mock.patch.object(nova, 'quotas') + def test_quota_sets_patch_when_service_is_disabled(self, qc, bc, nc): + quota_data = dict(cores='15', instances='5', + ram='50000', metadata_items='150', + injected_files='5', + injected_file_content_bytes='10240', + floating_ips='50', fixed_ips='5', + security_groups='10', + security_group_rules='100') + + request = self.mock_rest_request(body=''' + {"cores": "15", "ram": "50000", "instances": "5", + "metadata_items": "150", "injected_files": "5", + "injected_file_content_bytes": "10240", "floating_ips": "50", + "fixed_ips": "5", "security_groups": "10" , + "security_group_rules": "100", "volumes": "10"} + ''') + + qc.get_disabled_quotas.return_value = [] + qc.NOVA_QUOTA_FIELDS = (n for n in quota_data) + bc.is_service_enabled.return_value = False + + response = nova.QuotaSets().patch(request, 'spam123') + + self.assertStatusCode(response, 501) + self.assertEqual(response.content.decode('utf-8'), + '"Service Nova is disabled."') + nc.tenant_quota_update.assert_not_called()