diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 08364c7afd2f..a05537a888ff 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -155,6 +155,7 @@ "compute_extension:v3:os-quota-sets:show": "", "compute_extension:v3:os-quota-sets:update": "rule:admin_api", "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:rescue": "", diff --git a/nova/api/openstack/compute/plugins/v3/quota_sets.py b/nova/api/openstack/compute/plugins/v3/quota_sets.py index 48e753d85189..af0d6f933039 100644 --- a/nova/api/openstack/compute/plugins/v3/quota_sets.py +++ b/nova/api/openstack/compute/plugins/v3/quota_sets.py @@ -39,6 +39,8 @@ authorize_show = extensions.extension_authorizer('compute', 'v3:%s:show' % ALIAS) authorize_delete = extensions.extension_authorizer('compute', 'v3:%s:delete' % ALIAS) +authorize_detail = extensions.extension_authorizer('compute', + 'v3:%s:detail' % ALIAS) class QuotaTemplate(xmlutil.TemplateBuilder): @@ -53,6 +55,21 @@ class QuotaTemplate(xmlutil.TemplateBuilder): return xmlutil.MasterTemplate(root, 1) +class QuotaDetailTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('quota_set', selector='quota_set') + root.set('id') + + for resource in QUOTAS.resources: + elem = xmlutil.SubTemplateElement(root, resource, + selector=resource) + elem.set('in_use') + elem.set('reserved') + elem.set('limit') + + return xmlutil.MasterTemplate(root, 1) + + class QuotaSetsController(object): def _format_quota_set(self, project_id, quota_set): @@ -99,6 +116,20 @@ class QuotaSetsController(object): except exception.NotAuthorized: raise webob.exc.HTTPForbidden() + @extensions.expected_errors(403) + @wsgi.serializers(xml=QuotaDetailTemplate) + def detail(self, req, id): + context = req.environ['nova.context'] + authorize_detail(context) + user_id = req.GET.get('user_id', None) + try: + nova.context.authorize_project_context(context, id) + return self._format_quota_set(id, self._get_quotas(context, id, + user_id=user_id, + usages=True)) + except exception.NotAuthorized: + raise webob.exc.HTTPForbidden() + @extensions.expected_errors((400, 403)) @wsgi.serializers(xml=QuotaTemplate) def update(self, req, id, body): @@ -220,7 +251,8 @@ class QuotaSets(extensions.V3APIExtensionBase): res = extensions.ResourceExtension(ALIAS, QuotaSetsController(), - member_actions={'defaults': 'GET'}) + member_actions={'defaults': 'GET', + 'detail': 'GET'}) resources.append(res) return resources diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_quota_sets.py b/nova/tests/api/openstack/compute/plugins/v3/test_quota_sets.py index 0a5b5e938a78..fe1b36a818f2 100644 --- a/nova/tests/api/openstack/compute/plugins/v3/test_quota_sets.py +++ b/nova/tests/api/openstack/compute/plugins/v3/test_quota_sets.py @@ -51,6 +51,28 @@ class QuotaSetsTest(test.TestCase): quota_set['quota_set'].update(kwargs) return quota_set + def _generate_detail_quota_set(self, **kwargs): + quota_set = { + 'quota_set': + {'cores': {'in_use': 0, 'limit': 20, 'reserved': 0}, + 'fixed_ips': {'in_use': 0, 'limit': -1, 'reserved': 0}, + 'floating_ips': {'in_use': 0, 'limit': 10, 'reserved': 0}, + 'injected_files': {'in_use': 0, 'limit': 5, 'reserved': 0}, + 'instances': {'in_use': 0, 'limit': 10, 'reserved': 0}, + 'key_pairs': {'in_use': 0, 'limit': 100, 'reserved': 0}, + 'metadata_items': {'in_use': 0, 'limit': 128, 'reserved': 0}, + 'ram': {'in_use': 0, 'limit': 51200, 'reserved': 0}, + 'security_groups': {'in_use': 0, 'limit': 10, 'reserved': 0}, + 'injected_file_content_bytes': + {'in_use': 0, 'limit': 10240, 'reserved': 0}, + 'injected_file_path_bytes': + {'in_use': 0, 'limit': 255, 'reserved': 0}, + 'security_group_rules': + {'in_use': 0, 'limit': 20, 'reserved': 0}} + } + quota_set['quota_set'].update(kwargs) + return quota_set + def test_format_quota_set(self): raw_quota_set = self._generate_quota_set()['quota_set'] quota_set = self.controller._format_quota_set('1234', raw_quota_set) @@ -92,6 +114,18 @@ class QuotaSetsTest(test.TestCase): self.assertRaises(webob.exc.HTTPForbidden, self.controller.show, req, '1234') + def test_quotas_detail_as_admin(self): + uri = '/os-quota-sets/1234/detail' + req = fakes.HTTPRequestV3.blank(uri, use_admin_context=True) + res_dict = self.controller.detail(req, '1234') + + self.assertEqual(res_dict, self._generate_detail_quota_set(id='1234')) + + def test_quotas_detail_as_unauthorized_user(self): + req = fakes.HTTPRequestV3.blank('/os-quota-sets/1234/detail') + self.assertRaises(webob.exc.HTTPForbidden, self.controller.detail, + req, 1234) + def test_quotas_update_as_admin(self): id = 'update_me' body = self._generate_quota_set() @@ -214,6 +248,22 @@ class QuotaSetsTest(test.TestCase): self.assertRaises(webob.exc.HTTPForbidden, self.controller.show, req, '1234') + def test_user_quotas_detail_as_admin(self): + req = fakes.HTTPRequestV3.blank( + '/os-quota-sets/1234/detail?user_id=1', + use_admin_context=True + ) + res_dict = self.controller.detail(req, '1234') + + self.assertEqual(res_dict, self._generate_detail_quota_set(id='1234')) + + def test_user_quotas_detail_as_unauthorized_user(self): + req = fakes.HTTPRequestV3.blank( + '/os-quota-sets/1234/detail?user_id=1' + ) + self.assertRaises(webob.exc.HTTPForbidden, self.controller.detail, + req, '1234') + def test_user_quotas_update_as_admin(self): body = self._generate_quota_set() @@ -261,6 +311,7 @@ class QuotaXMLSerializerTest(test.TestCase): super(QuotaXMLSerializerTest, self).setUp() self.serializer = quotas.QuotaTemplate() self.deserializer = wsgi.XMLDeserializer() + self.detail_serializer = quotas.QuotaDetailTemplate() def test_serializer(self): exemplar = dict(quota_set=dict( @@ -288,6 +339,35 @@ class QuotaXMLSerializerTest(test.TestCase): self.assertTrue(child.tag in exemplar['quota_set']) self.assertEqual(int(child.text), exemplar['quota_set'][child.tag]) + def test_detail_serializer(self): + exemplar = dict(quota_set=dict( + id='project_id', + metadata_items=dict(limit=10, in_use=1, reserved=2), + injected_file_path_bytes=dict(limit=255, in_use=25, reserved=1), + injected_file_content_bytes=dict(limit=20, in_use=10, reserved=2), + ram=dict(limit=30, in_use=10, reserved=3), + floating_ips=dict(limit=60, in_use=20, reserved=20), + fixed_ips=dict(limit=-1, in_use=20, reserved=0), + instances=dict(limit=10, in_use=2, reserved=2), + injected_files=dict(limit=80, in_use=20, reserved=30), + security_groups=dict(limit=10, in_use=4, reserved=6), + security_group_rules=dict(limit=20, in_use=10, reserved=8), + key_pairs=dict(limit=20, in_use=10, reserved=11), + cores=dict(limit=20, in_use=10, reserved=2), + )) + text = self.detail_serializer.serialize(exemplar) + + tree = etree.fromstring(text) + + self.assertEqual('quota_set', tree.tag) + self.assertEqual('project_id', tree.get('id')) + self.assertEqual(len(exemplar['quota_set']) - 1, len(tree)) + for child in tree: + self.assertTrue(child.tag in exemplar['quota_set']) + for k in child.attrib.keys(): + self.assertEqual(int(child.attrib[k]), + exemplar['quota_set'][child.tag][k]) + def test_deserializer(self): exemplar = dict(quota_set=dict( metadata_items='10', diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index fa582f045b28..e0c14c4d5ab5 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -234,6 +234,7 @@ policy_data = """ "compute_extension:v3:os-quota-sets:show": "", "compute_extension:v3:os-quota-sets:update": "", "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": "",