diff --git a/aodh/api/controllers/v2/alarms.py b/aodh/api/controllers/v2/alarms.py index 1b2f1d77c..04be96d10 100644 --- a/aodh/api/controllers/v2/alarms.py +++ b/aodh/api/controllers/v2/alarms.py @@ -105,18 +105,24 @@ def is_over_quota(conn, project_id, user_id): over_quota = False - # Start by checking for user quota - user_alarm_quota = pecan.request.cfg.api.user_alarm_quota - if user_alarm_quota != -1: - user_alarms = conn.get_alarms(user_id=user_id) - over_quota = len(user_alarms) >= user_alarm_quota + project_quotas = conn.get_quotas(project_id) + project_alarms = conn.get_alarms(project_id=project_id) + user_alarms = conn.get_alarms(user_id=user_id) + user_default_alarm_quota = pecan.request.cfg.api.user_alarm_quota + project_default_alarm_quota = pecan.request.cfg.api.project_alarm_quota - # If the user quota isn't reached, we check for the project quota - if not over_quota: - project_alarm_quota = pecan.request.cfg.api.project_alarm_quota - if project_alarm_quota != -1: - project_alarms = conn.get_alarms(project_id=project_id) - over_quota = len(project_alarms) >= project_alarm_quota + # 1. Check project quota + if len(project_quotas) > 0: + for quota in project_quotas: + if quota.resource == 'alarms': + over_quota = len(user_alarms) >= quota.limit + else: + if project_default_alarm_quota != -1: + over_quota = len(project_alarms) >= project_default_alarm_quota + + # 2. Check user quota + if not over_quota and user_default_alarm_quota != -1: + over_quota = len(user_alarms) >= user_default_alarm_quota return over_quota diff --git a/aodh/api/controllers/v2/quotas.py b/aodh/api/controllers/v2/quotas.py index 390d40e07..4586ec3fa 100644 --- a/aodh/api/controllers/v2/quotas.py +++ b/aodh/api/controllers/v2/quotas.py @@ -28,7 +28,7 @@ ALLOWED_RESOURCES = ('alarms',) class Quota(base.Base): resource = wtypes.wsattr(wtypes.Enum(str, *ALLOWED_RESOURCES), mandatory=True) - limit = wtypes.IntegerType(minimum=-1) + limit = wsme.wsattr(wtypes.IntegerType(minimum=-1), mandatory=True) class Quotas(base.Base): @@ -79,6 +79,6 @@ class QuotasController(rest.RestController): input_quotas.append(i.to_dict()) db_quotas = pecan.request.storage.set_quotas(project_id, input_quotas) - quotas = [Quota.from_db_model(i) for i in db_quotas] + return Quotas(project_id=project_id, quotas=quotas) diff --git a/aodh/tests/functional/api/v2/test_alarm_scenarios.py b/aodh/tests/functional/api/v2/test_alarm_scenarios.py index 081c97fbf..128b225c9 100644 --- a/aodh/tests/functional/api/v2/test_alarm_scenarios.py +++ b/aodh/tests/functional/api/v2/test_alarm_scenarios.py @@ -127,8 +127,10 @@ class TestAlarmsBase(v2.FunctionalTest): def setUp(self): super(TestAlarmsBase, self).setUp() - self.auth_headers = {'X-User-Id': uuidutils.generate_uuid(), - 'X-Project-Id': uuidutils.generate_uuid()} + self.project_id = uuidutils.generate_uuid() + self.user_id = uuidutils.generate_uuid() + self.auth_headers = {'X-User-Id': self.user_id, + 'X-Project-Id': self.project_id} c = mock.Mock() c.capabilities.list.return_value = {'aggregation_methods': [ @@ -1955,13 +1957,13 @@ class TestAlarmsHistory(TestAlarmsBase): class TestAlarmsQuotas(TestAlarmsBase): - - def _test_alarm_quota(self): - alarm = { + def setUp(self): + super(TestAlarmsQuotas, self).setUp() + self.alarm = { 'name': 'alarm', 'type': 'gnocchi_aggregation_by_metrics_threshold', - 'user_id': self.auth_headers['X-User-Id'], - 'project_id': self.auth_headers['X-Project-Id'], + 'user_id': self.user_id, + 'project_id': self.project_id, RULE_KEY: { 'metrics': ['41869681-5776-46d6-91ed-cccc43b6e4e3', 'a1fb80f4-c242-4f57-87c6-68f47521059e'], @@ -1973,17 +1975,29 @@ class TestAlarmsQuotas(TestAlarmsBase): } } + def _create_alarm(self, alarm=None): + if not alarm: + alarm = self.alarm + resp = self.post_json('/alarms', params=alarm, - headers=self.auth_headers) - self.assertEqual(201, resp.status_code) + headers=self.auth_headers, + status=201) + + return resp + + def _test_alarm_quota(self): + """Failed on the second creation.""" + resp = self._create_alarm() + alarms = self.get_json('/alarms', headers=self.auth_headers) self.assertEqual(1, len(alarms)) + alarm = copy.copy(self.alarm) alarm['name'] = 'another_user_alarm' resp = self.post_json('/alarms', params=alarm, expect_errors=True, - headers=self.auth_headers) - self.assertEqual(403, resp.status_code) + headers=self.auth_headers, + status=403) faultstring = 'Alarm quota exceeded for user' self.assertIn(faultstring, resp.json['error_message']['faultstring']) @@ -2063,6 +2077,83 @@ class TestAlarmsQuotas(TestAlarmsBase): alarms = self.get_json('/alarms', headers=self.auth_headers) self.assertEqual(1, len(alarms)) + def test_overquota_by_quota_api(self): + auth_headers = copy.copy(self.auth_headers) + auth_headers['X-Roles'] = 'admin' + + # Update project quota. + self.post_json( + '/quotas', + { + "project_id": self.project_id, + "quotas": [ + { + "resource": "alarms", + "limit": 1 + } + ] + }, + headers=auth_headers, + status=201 + ) + + self._test_alarm_quota() + + # Update project quota back + self.post_json( + '/quotas', + { + "project_id": self.project_id, + "quotas": [ + { + "resource": "alarms", + "limit": -1 + } + ] + }, + headers=auth_headers, + status=201 + ) + + def test_overquota_by_user_quota_config(self): + self.CONF.set_override('user_alarm_quota', 1, 'api') + auth_headers = copy.copy(self.auth_headers) + auth_headers['X-Roles'] = 'admin' + + # Update project quota. + self.post_json( + '/quotas', + { + "project_id": self.project_id, + "quotas": [ + { + "resource": "alarms", + "limit": 2 + } + ] + }, + headers=auth_headers, + status=201 + ) + + self._test_alarm_quota() + + # Update project quota back + self.post_json( + '/quotas', + { + "project_id": self.project_id, + "quotas": [ + { + "resource": "alarms", + "limit": -1 + } + ] + }, + headers=auth_headers, + status=201 + ) + class TestAlarmsRuleThreshold(TestAlarmsBase): diff --git a/aodh/tests/functional/api/v2/test_quotas.py b/aodh/tests/functional/api/v2/test_quotas.py index 06aa65757..eec8f0d95 100644 --- a/aodh/tests/functional/api/v2/test_quotas.py +++ b/aodh/tests/functional/api/v2/test_quotas.py @@ -100,3 +100,93 @@ class TestQuotas(v2.FunctionalTest): expect_errors=True, status=403 ) + + def test_post_quotas_no_limit_failed(self): + auth_headers = copy.copy(self.auth_headers) + auth_headers['X-Roles'] = 'admin' + + resp = self.post_json( + '/quotas', + { + "project_id": self.project, + "quotas": [ + { + "resource": "alarms" + } + ] + }, + headers=auth_headers, + expect_errors=True, + status=400 + ) + + self.assertIn('Mandatory field missing', + resp.json['error_message']['faultstring']) + + def test_post_quotas_no_resource_failed(self): + auth_headers = copy.copy(self.auth_headers) + auth_headers['X-Roles'] = 'admin' + + resp = self.post_json( + '/quotas', + { + "project_id": self.project, + "quotas": [ + { + "limit": 1 + } + ] + }, + headers=auth_headers, + expect_errors=True, + status=400 + ) + + self.assertIn('Mandatory field missing', + resp.json['error_message']['faultstring']) + + def test_post_quotas_wrong_limit_failed(self): + auth_headers = copy.copy(self.auth_headers) + auth_headers['X-Roles'] = 'admin' + + resp = self.post_json( + '/quotas', + { + "project_id": self.project, + "quotas": [ + { + "resource": "alarms", + "limit": -5 + } + ] + }, + headers=auth_headers, + expect_errors=True, + status=400 + ) + + self.assertIn('Value should be greater or equal to -1', + resp.json['error_message']['faultstring']) + + def test_post_quotas_unsupported_resource_failed(self): + auth_headers = copy.copy(self.auth_headers) + auth_headers['X-Roles'] = 'admin' + + resp = self.post_json( + '/quotas', + { + "project_id": self.project, + "quotas": [ + { + "resource": "other_resource", + "limit": 1 + } + ] + }, + headers=auth_headers, + expect_errors=True, + status=400 + ) + + self.assertIn('Value should be one of', + resp.json['error_message']['faultstring'])