diff --git a/magnum/common/exception.py b/magnum/common/exception.py index db6e94f2da..fd847b76a7 100644 --- a/magnum/common/exception.py +++ b/magnum/common/exception.py @@ -383,6 +383,10 @@ class QuotaAlreadyExists(Conflict): "for resource %(resource)s.") +class QuotaNotFound(ResourceNotFound): + message = _("Quota could not be found: %(msg)s") + + class RegionsListFailed(MagnumException): message = _("Failed to list regions.") diff --git a/magnum/db/api.py b/magnum/db/api.py index 58f80a43ff..ade4afb174 100644 --- a/magnum/db/api.py +++ b/magnum/db/api.py @@ -358,6 +358,62 @@ class Connection(object): :returns: A quota record. """ + @abc.abstractmethod + def update_quota(self, project_id, values): + """Update quota record. + + :param project_id: The project id. + :param values: A dict containing several items used to identify + and track quota for a resource in a project. + + :: + + { + 'id': uuidutils.generate_uuid(), + 'project_id': 'fake_project', + 'resource': 'fake_resource', + 'hard_limit': 'fake_hardlimit', + } + :returns: A quota record. + """ + + @abc.abstractmethod + def delete_quota(self, project_id, resource): + """Delete a quota. + + :param project_id: Project id. + :param resource: resource name. + """ + + @abc.abstractmethod + def get_quota_by_id(self, context, quota_id): + """Return a quota. + + :param context: The security context + :param quota_id: The id of a quota. + :returns: A quota. + """ + + @abc.abstractmethod + def get_quota_list(self, context, filters=None, limit=None, + marker=None, sort_key=None, sort_dir=None): + """Get quota list. + + Return a list of the specified columns for all quotas that match the + specified filters. + + :param context: The security context + :param filters: Filters to apply. Defaults to None. + + :param limit: Maximum number of clusters to return. + :param marker: the last item of the previous page; we return the next + result set. + :param sort_key: Attribute by which results should be sorted. + :param sort_dir: direction in which results should be sorted. + (asc, desc) + :returns: A list of tuples of the specified columns. + """ + @abc.abstractmethod def quota_get_all_by_project_id(self, project_id): """Gets Quota record for all the resources in a project. @@ -366,3 +422,13 @@ class Connection(object): :returns: Quota record for all resources in a project. """ + + @abc.abstractmethod + def get_quota_by_project_id_resource(self, project_id, resource): + """Gets quota record for the given quota id. + + :param project_id: project id. + :param resource: resource name. + + :returns: Quota record. + """ diff --git a/magnum/db/sqlalchemy/api.py b/magnum/db/sqlalchemy/api.py index 7f75a91da6..93db8f1431 100644 --- a/magnum/db/sqlalchemy/api.py +++ b/magnum/db/sqlalchemy/api.py @@ -527,8 +527,81 @@ class Connection(api.Connection): resource=values['resource']) return quotas + def _add_quota_filters(self, query, filters): + if filters is None: + filters = {} + + possible_filters = ["resource", "project_id"] + + filter_names = set(filters).intersection(possible_filters) + filter_dict = {filter_name: filters[filter_name] + for filter_name in filter_names} + + query = query.filter_by(**filter_dict) + return query + + def get_quota_list(self, context, filters=None, limit=None, marker=None, + sort_key=None, sort_dir=None): + query = model_query(models.Quota) + query = self._add_quota_filters(query, filters) + return _paginate_query(models.Quota, limit, marker, + sort_key, sort_dir, query) + + def update_quota(self, project_id, values): + session = get_session() + with session.begin(): + query = model_query(models.Quota, session=session) + resource = values['resource'] + try: + query = query.filter_by(project_id=project_id).filter_by( + resource=resource) + ref = query.with_lockmode('update').one() + except NoResultFound: + msg = (_('project_id %(project_id)s resource %(resource)s.') % + {'project_id': project_id, 'resource': resource}) + raise exception.QuotaNotFound(msg=msg) + + ref.update(values) + return ref + + def delete_quota(self, project_id, resource): + session = get_session() + with session.begin(): + query = model_query(models.Quota, session=session) + + try: + query.filter_by(project_id=project_id).filter_by( + resource=resource).one() + except NoResultFound: + msg = (_('project_id %(project_id)s resource %(resource)s.') % + {'project_id': project_id, 'resource': resource}) + raise exception.QuotaNotFound(msg=msg) + + query.delete() + + def get_quota_by_id(self, context, quota_id): + query = model_query(models.Quota) + query = query.filter_by(id=quota_id) + try: + return query.one() + except NoResultFound: + msg = _('quota id %s .') % quota_id + raise exception.QuotaNotFound(msg=msg) + def quota_get_all_by_project_id(self, project_id): query = model_query(models.Quota) result = query.filter_by(project_id=project_id).all() return result + + def get_quota_by_project_id_resource(self, project_id, resource): + query = model_query(models.Quota) + query = query.filter_by(project_id=project_id).filter_by( + resource=resource) + + try: + return query.one() + except NoResultFound: + msg = (_('project_id %(project_id)s resource %(resource)s.') % + {'project_id': project_id, 'resource': resource}) + raise exception.QuotaNotFound(msg=msg) diff --git a/magnum/tests/unit/db/test_quota.py b/magnum/tests/unit/db/test_quota.py index 417b7a1a4b..d599b5577c 100644 --- a/magnum/tests/unit/db/test_quota.py +++ b/magnum/tests/unit/db/test_quota.py @@ -39,3 +39,123 @@ class DbQuotaTestCase(base.DbTestCase): self.assertEqual(q.hard_limit, r.hard_limit) self.assertEqual(q.project_id, r.project_id) self.assertEqual(q.resource, r.resource) + + def test_get_quota_by_project_id_resource(self): + q = utils.create_test_quotas(project_id='123', + resource='test-res', + hard_limit=5) + res = self.dbapi.get_quota_by_project_id_resource('123', 'test-res') + self.assertEqual(q.hard_limit, res.hard_limit) + self.assertEqual(q.project_id, res.project_id) + self.assertEqual(q.resource, res.resource) + + def test_get_quota_by_project_id_resource_not_found(self): + utils.create_test_quotas(project_id='123', + resource='test-res', + hard_limit=5) + self.assertRaises(exception.QuotaNotFound, + self.dbapi.get_quota_by_project_id_resource, + project_id='123', + resource='bad-res') + + def test_get_quota_list(self): + project_ids = [] + for i in range(1, 6): + project_id = 'proj-'+str(i) + utils.create_test_quotas(project_id=project_id) + project_ids.append(project_id) + res = self.dbapi.get_quota_list(self.context) + res_proj_ids = [r.project_id for r in res] + self.assertEqual(sorted(project_ids), sorted(res_proj_ids)) + + def test_get_quota_list_sorted(self): + project_ids = [] + for i in range(1, 6): + project_id = 'proj-'+str(i) + utils.create_test_quotas(project_id=project_id) + project_ids.append(project_id) + res = self.dbapi.get_quota_list(self.context, sort_key='project_id') + res_proj_ids = [r.project_id for r in res] + self.assertEqual(sorted(project_ids), res_proj_ids) + + def test_get_quota_list_invalid_sort_key(self): + project_ids = [] + for i in range(1, 6): + project_id = 'proj-'+str(i) + utils.create_test_quotas(project_id=project_id) + project_ids.append(project_id) + + self.assertRaises(exception.InvalidParameterValue, + self.dbapi.get_quota_list, + self.context, + sort_key='invalid') + + def test_get_quota_list_with_filters(self): + quota1 = utils.create_test_quotas(project_id='proj-1', resource='res1') + quota2 = utils.create_test_quotas(project_id='proj-1', resource='res2') + quota3 = utils.create_test_quotas(project_id='proj-2', resource='res1') + + res = self.dbapi.get_quota_list( + self.context, filters={'resource': 'res2'}) + self.assertEqual(quota2.project_id, res[0].project_id) + + res = self.dbapi.get_quota_list( + self.context, filters={'project_id': 'proj-2'}) + self.assertEqual(quota3.project_id, res[0].project_id) + + res = self.dbapi.get_quota_list( + self.context, filters={'project_id': 'proj-1'}) + self.assertEqual(sorted([quota1.project_id, quota2.project_id]), + sorted([r.project_id for r in res])) + + def test_update_quota(self): + q = utils.create_test_quotas(hard_limit=5, + project_id='1234', + resource='Cluster') + + res = self.dbapi.get_quota_by_project_id_resource('1234', 'Cluster') + self.assertEqual(q.hard_limit, res.hard_limit) + self.assertEqual(q.project_id, res.project_id) + self.assertEqual(q.resource, res.resource) + quota_dict = {'resource': 'Cluster', 'hard_limit': 15} + self.dbapi.update_quota('1234', quota_dict) + res = self.dbapi.get_quota_by_project_id_resource('1234', 'Cluster') + self.assertEqual(quota_dict['hard_limit'], res.hard_limit) + self.assertEqual(quota_dict['resource'], res.resource) + + def test_update_quota_not_found(self): + utils.create_test_quotas(hard_limit=5, + project_id='1234', + resource='Cluster') + quota_dict = {'resource': 'Cluster', 'hard_limit': 15} + self.assertRaises(exception.QuotaNotFound, + self.dbapi.update_quota, + 'invalid_proj', + quota_dict) + + def test_delete_quota(self): + q = utils.create_test_quotas(project_id='123', + resource='test-res', + hard_limit=5) + res = self.dbapi.get_quota_by_project_id_resource('123', 'test-res') + self.assertEqual(q.hard_limit, res.hard_limit) + self.assertEqual(q.project_id, res.project_id) + self.assertEqual(q.resource, res.resource) + self.dbapi.delete_quota(q.project_id, q.resource) + self.assertRaises(exception.QuotaNotFound, + self.dbapi.get_quota_by_project_id_resource, + project_id='123', + resource='bad-res') + + def test_delete_quota_that_does_not_exist(self): + # Make sure that quota does not exist + self.assertRaises(exception.QuotaNotFound, + self.dbapi.get_quota_by_project_id_resource, + project_id='123', + resource='bad-res') + + # Now try to delete non-existing quota + self.assertRaises(exception.QuotaNotFound, + self.dbapi.delete_quota, + project_id='123', + resource='bad-res') diff --git a/magnum/tests/unit/db/utils.py b/magnum/tests/unit/db/utils.py index 8e025d1ce6..c1abf2c41e 100644 --- a/magnum/tests/unit/db/utils.py +++ b/magnum/tests/unit/db/utils.py @@ -122,6 +122,32 @@ def create_test_cluster(**kw): return dbapi.create_cluster(cluster) +def get_test_quota(**kw): + attrs = { + 'id': kw.get('id', 42), + 'project_id': kw.get('project_id', 'fake_project'), + 'resource': kw.get('resource', 'Cluster'), + 'hard_limit': kw.get('hard_limit', 10) + } + + return attrs + + +def create_test_quota(**kw): + """Create test quota entry in DB and return Quota DB object. + + Function to be used to create test Quota objects in the database. + :param kw: kwargs with overriding values for quota's attributes. + :returns: Test Quota DB object. + """ + quota = get_test_quota(**kw) + # Let DB generate ID if it isn't specified explicitly + if 'id' not in kw: + del quota['id'] + dbapi = db_api.get_instance() + return dbapi.create_quota(quota) + + def get_test_x509keypair(**kw): return { 'id': kw.get('id', 42), @@ -187,7 +213,7 @@ def get_test_quotas(**kw): return { 'id': kw.get('', 18), 'project_id': kw.get('project_id', 'fake_project'), - 'resource': kw.get('resource', 'fake_resource'), + 'resource': kw.get('resource', 'Cluster'), 'hard_limit': kw.get('hard_limit', 10), 'created_at': kw.get('created_at'), 'updated_at': kw.get('updated_at'),