diff --git a/barbican/api/controllers/quotas.py b/barbican/api/controllers/quotas.py index eda53c06d..67b789be9 100644 --- a/barbican/api/controllers/quotas.py +++ b/barbican/api/controllers/quotas.py @@ -16,20 +16,28 @@ import pecan from barbican import api from barbican.api import controllers +from barbican.common import exception from barbican.common import quota +from barbican.common import resources as res from barbican.common import utils from barbican.common import validators from barbican import i18n as u + LOG = utils.getLogger(__name__) +def _project_quotas_not_found(): + """Throw exception indicating project quotas not found.""" + pecan.abort(404, u._('Not Found. Sorry but your project quotas are in ' + 'another castle.')) + + class QuotasController(controllers.ACLMixin): """Handles quota retrieval requests.""" - def __init__(self, quota_repo=None): + def __init__(self): LOG.debug('=== Creating QuotasController ===') - self.repo = quota_repo self.quota_driver = quota.QuotaDriver() @pecan.expose(generic=True) @@ -40,18 +48,18 @@ class QuotasController(controllers.ACLMixin): @controllers.handle_exceptions(u._('Quotas')) @controllers.enforce_rbac('quotas:get') def on_get(self, external_project_id, **kwargs): - # TODO(dave) implement - resp = {'quotas': self.quota_driver.get_defaults()} + LOG.debug('=== QuotasController GET ===') + project = res.get_or_create_project(external_project_id) + resp = self.quota_driver.get_quotas(project.id) return resp class ProjectQuotasController(controllers.ACLMixin): """Handles project quota requests.""" - def __init__(self, project_id, project_quota_repo=None): + def __init__(self, project_id): LOG.debug('=== Creating ProjectQuotasController ===') self.passed_project_id = project_id - self.repo = project_quota_repo self.validator = validators.ProjectQuotaValidator() self.quota_driver = quota.QuotaDriver() @@ -63,29 +71,26 @@ class ProjectQuotasController(controllers.ACLMixin): @controllers.handle_exceptions(u._('Project Quotas')) @controllers.enforce_rbac('project_quotas:get') def on_get(self, external_project_id, **kwargs): - # TODO(dave) implement LOG.debug('=== ProjectQuotasController GET ===') - resp = {'project_quotas': self.quota_driver.get_defaults()} + resp = self.quota_driver.get_project_quotas(self.passed_project_id) + if resp: + return resp + else: + _project_quotas_not_found() - return resp - - @index.when(method='POST', template='json') + @index.when(method='PUT', template='json') @controllers.handle_exceptions(u._('Project Quotas')) - @controllers.enforce_rbac('project_quotas:post') - def on_post(self, external_project_id, **kwargs): - LOG.debug('=== ProjectQuotasController POST ===') + @controllers.enforce_rbac('project_quotas:put') + def on_put(self, external_project_id, **kwargs): + LOG.debug('=== ProjectQuotasController PUT ===') + if not pecan.request.body: + raise exception.NoDataToProcess() api.load_body(pecan.request, validator=self.validator) - # TODO(dave) implement - resp = {'project_quotas': { - 'secrets': 10, - 'orders': 20, - 'containers': 10, - 'transport_keys': 10, - 'consumers': -1} - } - LOG.info(u._LI('Post Project Quotas')) - return resp + self.quota_driver.set_project_quotas(self.passed_project_id, + kwargs['project_quotas']) + LOG.info(u._LI('Put Project Quotas')) + pecan.response.status = 204 @index.when(method='DELETE', template='json') @utils.allow_all_content_types @@ -93,23 +98,26 @@ class ProjectQuotasController(controllers.ACLMixin): @controllers.enforce_rbac('project_quotas:delete') def on_delete(self, external_project_id, **kwargs): LOG.debug('=== ProjectQuotasController DELETE ===') - # TODO(dave) implement - LOG.info(u._LI('Delete Project Quotas')) - pecan.response.status = 204 + try: + self.quota_driver.delete_project_quotas(self.passed_project_id) + except exception.NotFound: + LOG.info(u._LI('Delete Project Quotas - Project not found')) + _project_quotas_not_found() + else: + LOG.info(u._LI('Delete Project Quotas')) + pecan.response.status = 204 class ProjectsQuotasController(controllers.ACLMixin): """Handles projects quota retrieval requests.""" - def __init__(self, project_quota_repo=None): + def __init__(self): LOG.debug('=== Creating ProjectsQuotaController ===') - self.repo = project_quota_repo self.quota_driver = quota.QuotaDriver() @pecan.expose() def _lookup(self, project_id, *remainder): - return ProjectQuotasController(project_id, - project_quota_repo=self.repo), remainder + return ProjectQuotasController(project_id), remainder @pecan.expose(generic=True) def index(self, **kwargs): @@ -119,13 +127,8 @@ class ProjectsQuotasController(controllers.ACLMixin): @controllers.handle_exceptions(u._('Project Quotas')) @controllers.enforce_rbac('project_quotas:get') def on_get(self, external_project_id, **kwargs): - - # TODO(dave) implement - project1 = {'project_id': "1234", - 'project_quotas': self.quota_driver.get_defaults()} - project2 = {'project_id': "5678", - 'project_quotas': self.quota_driver.get_defaults()} - project_quotas = {"project_quotas": [project1, project2]} - resp = project_quotas - + resp = self.quota_driver.get_project_quotas_list( + offset_arg=kwargs.get('offset', 0), + limit_arg=kwargs.get('limit', None) + ) return resp diff --git a/barbican/common/quota.py b/barbican/common/quota.py index 8c36acdce..e24a79655 100644 --- a/barbican/common/quota.py +++ b/barbican/common/quota.py @@ -17,6 +17,10 @@ from oslo_config import cfg from oslo_log import log as logging +from barbican.common import exception +from barbican.common import hrefs +from barbican.model import repositories as repo + LOG = logging.getLogger(__name__) UNLIMITED_VALUE = -1 @@ -26,23 +30,20 @@ quota_opt_group = cfg.OptGroup(name='quotas', title='Quota Options') quota_opts = [ - cfg.BoolOpt('enabled', - default=False, - help='When True, quotas are enforced.'), cfg.IntOpt('quota_secrets', - default=500, + default=-1, help='Number of secrets allowed per project'), cfg.IntOpt('quota_orders', - default=100, + default=-1, help='Number of orders allowed per project'), cfg.IntOpt('quota_containers', default=-1, help='Number of containers allowed per project'), cfg.IntOpt('quota_transport_keys', - default=100, + default=-1, help='Number of transport keys allowed per project'), cfg.IntOpt('quota_consumers', - default=100, + default=-1, help='Number of consumers allowed per project'), ] @@ -54,7 +55,15 @@ CONF.register_opts(quota_opts, group=quota_opt_group) class QuotaDriver(object): """Driver to enforce quotas and obtain quota information.""" - def get_defaults(self): + def __init__(self): + self.repo = repo.get_project_quotas_repository() + + def _get_resources(self): + """List of resources that can be constrained by a quota""" + return ['secrets', 'orders', 'containers', 'transport_keys', + 'consumers'] + + def _get_defaults(self): """Return list of default quotas""" quotas = { 'secrets': CONF.quotas.quota_secrets, @@ -65,7 +74,113 @@ class QuotaDriver(object): } return quotas + def _extract_project_quotas(self, project_quotas_model): + """Convert project quotas model to Python dict + + :param project_quotas_model: Model containing quota information + :return: Python dict containing quota information + """ + resp_quotas = {} + for resource in self._get_resources(): + resp_quotas[resource] = getattr(project_quotas_model, resource) + return resp_quotas + + def _compute_effective_quotas(self, configured_quotas): + """Merge configured and default quota information + + When a quota value is not set, use the default value + :param configured_quotas: configured quota values + :return: effective quotas + """ + default_quotas = self._get_defaults() + resp_quotas = dict(configured_quotas) + for resource, quota in resp_quotas.iteritems(): + if quota is None: + resp_quotas[resource] = default_quotas[resource] + return resp_quotas + def _is_unlimited_value(self, v): """A helper method to check for unlimited value.""" + return v is not None and v <= UNLIMITED_VALUE - return v <= UNLIMITED_VALUE + def set_project_quotas(self, project_id, parsed_project_quotas): + """Create a new database entry, or update existing one + + :param project_id: ID of project whose quotas are to be set + :param parsed_project_quotas: quota values to save in database + :return: None + """ + session = self.repo.get_session() + self.repo.create_or_update_by_project_id( + project_id, parsed_project_quotas, session=session) + session.commit() + + def get_project_quotas(self, project_id): + """Retrieve configured quota information from database + + :param project_id: ID of project for whose value are wanted + :return: the values + """ + session = self.repo.get_session() + try: + retrieved_project_quotas =\ + self.repo.get_by_project_id(project_id, session=session) + except exception.NotFound: + return None + resp_quotas = self._extract_project_quotas(retrieved_project_quotas) + resp = {'project_quotas': resp_quotas} + return resp + + def get_project_quotas_list(self, offset_arg=None, limit_arg=None): + """Return a dict and list of all configured quota information + + :return: a dict and list of a page of quota config info + """ + session = self.repo.get_session() + retrieved_project_quotas, offset, limit, total =\ + self.repo.get_by_create_date(session=session, + offset_arg=offset_arg, + limit_arg=limit_arg, + suppress_exception=True) + resp_quotas = [] + for quotas in retrieved_project_quotas: + list_item = {'project_id': quotas.project_id, + 'project_quotas': + self._extract_project_quotas(quotas)} + resp_quotas.append(list_item) + resp = {'project_quotas': resp_quotas} + resp_overall = hrefs.add_nav_hrefs( + 'project_quotas', offset, limit, total, resp) + resp_overall.update({'total': total}) + return resp_overall + + def delete_project_quotas(self, project_id): + """Remove configured quota information from database + + :param project_id: ID of project whose quota config will be deleted + :raises NotFound: if project has no configured values + :return: None + """ + session = self.repo.get_session() + self.repo.delete_by_project_id(project_id, + session=session) + + def get_quotas(self, project_id): + """Get the effective quotas for a project + + Effective quotas are based on both configured and default values + :param project_id: ID of project for which to get effective quotas + :return: dict of effective quota values + """ + session = self.repo.get_session() + try: + retrieved_project_quotas =\ + self.repo.get_by_project_id(project_id, + session=session) + except exception.NotFound: + resp_quotas = self._get_defaults() + else: + resp_quotas = self._compute_effective_quotas( + self._extract_project_quotas(retrieved_project_quotas)) + resp = {'quotas': resp_quotas} + return resp diff --git a/barbican/model/models.py b/barbican/model/models.py index c12c8f05e..cb115f091 100644 --- a/barbican/model/models.py +++ b/barbican/model/models.py @@ -1242,3 +1242,76 @@ class ContainerACLUser(BASE, ModelBase): """Sub-class hook method: return dict of fields.""" return {'acl_id': self.acl_id, 'user_id': self.user_id} + + +class ProjectQuotas(BASE, ModelBase): + """Stores Project Quotas. + + Class to define project specific resource quotas. + + Project quota deletes are not soft-deletes. + """ + + __tablename__ = 'project_quotas' + + project_id = sa.Column( + sa.String(36), + # TODO(dave): enforce project exists + # sa.ForeignKey('projects.id', name='project_quotas_fk'), + index=True, + nullable=False) + secrets = sa.Column(sa.Integer, nullable=True) + orders = sa.Column(sa.Integer, nullable=True) + containers = sa.Column(sa.Integer, nullable=True) + transport_keys = sa.Column(sa.Integer, nullable=True) + consumers = sa.Column(sa.Integer, nullable=True) + + __table_args__ = (sa.UniqueConstraint('project_id', + name='project_quotas_uc'),) + + def __init__(self, project_id=None, parsed_project_quotas=None): + """Creates Project Quotas entity from a project and a dict. + + :param project_id: the id of the project whose quotas are to be stored + :param parsed_project_quotas: a dict with the keys matching the + resources for which quotas are to be set, and the values containing + the quota value to be set for this project and that resource. + :return: None + """ + super(ProjectQuotas, self).__init__() + + msg = u._("Must supply non-None {0} argument for ProjectQuotas entry.") + + if project_id is None: + raise exception.MissingArgumentError(msg.format("project_id")) + self.project_id = project_id + + if parsed_project_quotas is None: + self.secrets = None + self.orders = None + self.containers = None + self.transport_keys = None + self.consumers = None + else: + self.secrets = parsed_project_quotas.get('secrets') + self.orders = parsed_project_quotas.get('orders') + self.containers = parsed_project_quotas.get('containers') + self.transport_keys = parsed_project_quotas.get('transport_keys') + self.consumers = parsed_project_quotas.get('consumers') + + def _do_extra_dict_fields(self): + """Sub-class hook method: return dict of fields.""" + ret = { + 'project_id': self.project_id, + } + if self.secrets: + ret['secrets'] = self.secrets + if self.orders: + ret['orders'] = self.orders + if self.containers: + ret['containers'] = self.containers + if self.transport_keys: + ret['transport_keys'] = self.transport_keys + if self.consumers: + ret['consumers'] = self.consumers + return ret diff --git a/barbican/model/repositories.py b/barbican/model/repositories.py index 3de65b8d0..c29ea820a 100755 --- a/barbican/model/repositories.py +++ b/barbican/model/repositories.py @@ -61,6 +61,7 @@ _ORDER_RETRY_TASK_REPOSITORY = None _PREFERRED_CA_REPOSITORY = None _PROJECT_REPOSITORY = None _PROJECT_CA_REPOSITORY = None +_PROJECT_QUOTAS_REPOSITORY = None _SECRET_ACL_REPOSITORY = None _SECRET_META_REPOSITORY = None _SECRET_REPOSITORY = None @@ -1836,6 +1837,126 @@ class ContainerACLRepo(BaseRepo): entity.delete(session=session) +class ProjectQuotasRepo(BaseRepo): + """Repository for the ProjectQuotas entity.""" + def _do_entity_name(self): + """Sub-class hook: return entity name, such as for debugging.""" + return "ProjectQuotas" + + def _do_build_get_query(self, entity_id, external_project_id, session): + """Sub-class hook: build a retrieve query.""" + return session.query(models.ProjectQuotas).filter_by(id=entity_id) + + def _do_validate(self, values): + """Sub-class hook: validate values.""" + pass + + def get_by_create_date(self, offset_arg=None, limit_arg=None, + suppress_exception=False, session=None): + """Returns a list of ProjectQuotas + + The list is ordered by the date they were created at and paged + based on the offset and limit fields. + + :param offset_arg: The entity number where the query result should + start. + :param limit_arg: The maximum amount of entities in the result set. + :param suppress_exception: Whether NoResultFound exceptions should be + suppressed. + :param session: SQLAlchemy session object. + :raises NotFound: if no quota config is found for the project + :returns: Tuple consisting of (list_of_entities, offset, limit, total). + """ + + offset, limit = clean_paging_values(offset_arg, limit_arg) + + session = self.get_session(session) + + query = session.query(models.ProjectQuotas) + query = query.order_by(models.ProjectQuotas.created_at) + query = query.filter_by(deleted=False) + + start = offset + end = offset + limit + LOG.debug('Retrieving from %s to %s', start, end) + total = query.count() + entities = query.offset(start).limit(limit).all() + LOG.debug('Number entities retrieved: %s out of %s', + len(entities), total) + + if total <= 0 and not suppress_exception: + _raise_no_entities_found(self._do_entity_name()) + + return entities, offset, limit, total + + def create_or_update_by_project_id(self, project_id, parsed_project_quotas, + session=None): + """Create or update Project Quotas config for a project by project_id. + + :param project_id: ID of project whose quota config will be saved + :param parsed_project_quotas: Python dict with quota definition + :param session: SQLAlchemy session object. + :return: None + """ + session = self.get_session(session) + query = session.query(models.ProjectQuotas) + query = query.filter_by(project_id=project_id) + try: + entity = query.one() + except sa_orm.exc.NoResultFound: + self.create_from( + models.ProjectQuotas(project_id, parsed_project_quotas), + session=session) + else: + self._update_values(entity, parsed_project_quotas) + + def get_by_project_id(self, project_id, + suppress_exception=False, session=None): + """Return configured Project Quotas for a project by project_id. + + :param project_id: ID of project whose quota config will be deleted + :param suppress_exception: when True, NotFound is not raised + :param session: SQLAlchemy session object. + :raises NotFound: if no quota config is found for the project + :return: None or Python dict of project quotas for project + """ + + session = self.get_session(session) + query = session.query(models.ProjectQuotas) + query = query.filter_by(project_id=project_id) + try: + entity = query.one() + except sa_orm.exc.NoResultFound: + if suppress_exception: + return None + else: + _raise_no_entities_found(self._do_entity_name()) + return entity + + def delete_by_project_id(self, project_id, + suppress_exception=False, session=None): + """Remove configured Project Quotas for a project by project_id. + + :param project_id: ID of project whose quota config will be deleted + :param suppress_exception: when True, NotFound is not raised + :param session: SQLAlchemy session object. + :raises NotFound: if no quota config is found for the project + :return: None + """ + + session = self.get_session(session) + query = session.query(models.ProjectQuotas) + query = query.filter_by(project_id=project_id) + try: + entity = query.one() + except sa_orm.exc.NoResultFound: + if suppress_exception: + return + else: + _raise_no_entities_found(self._do_entity_name()) + entity.delete(session=session) + + def get_ca_repository(): """Returns a singleton Secret repository instance.""" global _CA_REPOSITORY @@ -1925,6 +2046,13 @@ def get_project_ca_repository(): ProjectCertificateAuthorityRepo) +def get_project_quotas_repository(): + """Returns a singleton Project Quotas repository instance.""" + global _PROJECT_QUOTAS_REPOSITORY + return _get_repository(_PROJECT_QUOTAS_REPOSITORY, + ProjectQuotasRepo) + + def get_secret_acl_repository(): """Returns a singleton Secret ACL repository instance.""" global _SECRET_ACL_REPOSITORY diff --git a/barbican/tests/api/controllers/test_quotas.py b/barbican/tests/api/controllers/test_quotas.py index 9be7c93ef..8b7949100 100644 --- a/barbican/tests/api/controllers/test_quotas.py +++ b/barbican/tests/api/controllers/test_quotas.py @@ -23,46 +23,129 @@ class WhenTestingQuotas(utils.BarbicanAPIBaseTestCase): def test_should_get_quotas(self): params = {} resp = self.app.get('/quotas', params) - self.assertIn('quotas', resp.namespace) + self.assertEqual(200, resp.status_int) + quotas_list = resp.json.get('quotas') + self.assertEqual({'consumers': -1, 'containers': -1, 'orders': -1, + 'secrets': -1, 'transport_keys': -1}, + quotas_list) def test_should_get_specific_project_quotas(self): params = {} + self.create_a_project_quotas() resp = self.app.get( - '/project-quotas/{0}'.format(self.project_id), + '/project-quotas/{0}'.format(self.get_test_project_id()), params) self.assertEqual(200, resp.status_int) - self.assertIn('project_quotas', resp.namespace) + project_quotas = resp.json.get('project_quotas') + self.assertEqual({'consumers': 105, 'containers': 103, 'orders': 102, + 'secrets': 101, 'transport_keys': 104}, + project_quotas) + + def test_should_return_not_found_get_specific_project_quotas(self): + params = {} + resp = self.app.get( + '/project-quotas/{0}'.format(self.get_test_project_id()), + params, expect_errors=True) + self.assertEqual(404, resp.status_int) def test_should_get_project_quotas_list(self): + self.create_project_quotas() params = {} resp = self.app.get('/project-quotas', params) self.assertEqual(200, resp.status_int) - self.assertIn('project_quotas', resp.namespace) + project_quotas_list = resp.json.get('project_quotas') + self.assertEqual(3, len(project_quotas_list)) + self.assertIn('total', resp.json) - def test_should_post_project_quotas(self): - request = {'project_quotas': {}} - resp = self.app.post_json( - '/project-quotas/{0}'.format(self.project_id), request) - self.assertEqual(200, resp.status_int) - - def test_should_delete_specific_project_quotas(self): + def test_should_get_empty_project_quotas_list(self): params = {} - resp = self.app.delete( - '/project-quotas/{0}'.format(self.project_id), params) + resp = self.app.get('/project-quotas', params) + self.assertEqual(200, resp.status_int) + project_quotas_list = resp.json.get('project_quotas') + self.assertEqual([], project_quotas_list) + self.assertIn('total', resp.json) + + def test_pagination_attributes(self): + for index in range(11): + self.create_a_project_quotas(index) + + params = {'limit': '2', 'offset': '2'} + resp = self.app.get('/project-quotas', params) + + self.assertEqual(200, resp.status_int) + self.assertIn('previous', resp.json) + self.assertIn('next', resp.json) + + previous_ref = resp.json.get('previous') + next_ref = resp.json.get('next') + + self.assertIn('offset=0', previous_ref) + self.assertIn('offset=4', next_ref) + + def test_should_put_project_quotas(self): + request = {'project_quotas': {}} + resp = self.app.put_json( + '/project-quotas/{0}'.format(self.project_id), request) self.assertEqual(204, resp.status_int) - def test_check_post_quotas_not_allowed(self): - """POST not allowed operation for /quotas""" - params = {} - resp = self.app.post('/quotas/', params, expect_errors=True) - self.assertEqual(405, resp.status_int) + def test_should_return_bad_value_put_project_quotas(self): + request = '{"project_quotas": {"secrets": "foo"}}' + resp = self.app.put( + '/project-quotas/{0}'.format(self.project_id), + request, + headers={'Content-Type': 'application/json'}, + expect_errors=True) + self.assertEqual(400, resp.status_int) - def test_check_put_project_quotas_not_allowed(self): + def test_should_return_bad_data_put_project_quotas(self): + """PUT not allowed operation for /project-quotas/{project-id}""" + params = {'bad data'} + resp = self.app.put( + '/project-quotas/{0}'.format(self.project_id), + params, expect_errors=True) + self.assertEqual(400, resp.status_int) + + def test_should_return_no_payload_for_put_project_quotas(self): """PUT not allowed operation for /project-quotas/{project-id}""" params = {} resp = self.app.put( '/project-quotas/{0}'.format(self.project_id), params, expect_errors=True) + self.assertEqual(400, resp.status_int) + + def test_should_delete_specific_project_quotas(self): + params = {} + self.create_a_project_quotas() + resp = self.app.delete( + '/project-quotas/{0}'.format(self.get_test_project_id()), + params) + self.assertEqual(204, resp.status_int) + + def test_should_return_not_found_delete_specific_project_quotas(self): + params = {} + resp = self.app.delete( + '/project-quotas/{0}'.format('dummy'), + params, expect_errors=True) + self.assertEqual(404, resp.status_int) + + def test_check_put_quotas_not_allowed(self): + """PuT not allowed operation for /quotas""" + params = {} + resp = self.app.put('/quotas/', params, expect_errors=True) + self.assertEqual(405, resp.status_int) + + def test_check_put_project_quotas_list_not_allowed(self): + """PUT not allowed operation for /project-quotas""" + params = {} + resp = self.app.put('/project-quotas', params, expect_errors=True) + self.assertEqual(405, resp.status_int) + + def test_check_post_project_quotas_not_allowed(self): + """POST not allowed operation for /project-quotas/{project-id}""" + params = {} + resp = self.app.post( + '/project-quotas/{0}'.format(self.project_id), + params, expect_errors=True) self.assertEqual(405, resp.status_int) def test_check_post_project_quotas_list_not_allowed(self): @@ -71,6 +154,27 @@ class WhenTestingQuotas(utils.BarbicanAPIBaseTestCase): resp = self.app.post('/project-quotas', params, expect_errors=True) self.assertEqual(405, resp.status_int) + # ----------------------- Helper Functions --------------------------- + def get_test_project_id(self, index=1): + return 'project' + str(index) + + def create_a_project_quotas(self, index=1): + project_id = self.get_test_project_id(index) + parsed_project_quotas = { + 'secrets': index * 100 + 1, + 'orders': index * 100 + 2, + 'containers': index * 100 + 3, + 'transport_keys': index * 100 + 4, + 'consumers': index * 100 + 5} + request = {'project_quotas': parsed_project_quotas} + resp = self.app.put_json( + '/project-quotas/{0}'.format(project_id), request) + self.assertEqual(204, resp.status_int) + + def create_project_quotas(self): + for index in [1, 2, 3]: + self.create_a_project_quotas(index) + if __name__ == '__main__': unittest.main() diff --git a/barbican/tests/common/test_quota.py b/barbican/tests/common/test_quota.py index f656ca50a..31434d15d 100644 --- a/barbican/tests/common/test_quota.py +++ b/barbican/tests/common/test_quota.py @@ -15,23 +15,44 @@ import unittest +from barbican.common import exception from barbican.common import quota -from barbican.tests import utils +from barbican.tests import database_utils -class WhenTestingQuotaFunctions(utils.BaseTestCase): +class WhenTestingQuotaDriverFunctions(database_utils.RepositoryTestCase): def setUp(self): - super(WhenTestingQuotaFunctions, self).setUp() + super(WhenTestingQuotaDriverFunctions, self).setUp() self.quota_driver = quota.QuotaDriver() def test_get_defaults(self): - quotas = self.quota_driver.get_defaults() - self.assertEqual(500, quotas['secrets']) - self.assertEqual(100, quotas['orders']) + quotas = self.quota_driver._get_defaults() + self.assertEqual(-1, quotas['secrets']) + self.assertEqual(-1, quotas['orders']) self.assertEqual(-1, quotas['containers']) - self.assertEqual(100, quotas['transport_keys']) - self.assertEqual(100, quotas['consumers']) + self.assertEqual(-1, quotas['transport_keys']) + self.assertEqual(-1, quotas['consumers']) + + def test_compute_effective_quotas_using_some_defaults(self): + configured_quotas = {'consumers': None, 'containers': 66, + 'orders': None, 'secrets': 55, + 'transport_keys': None} + quotas = self.quota_driver._compute_effective_quotas(configured_quotas) + expected_quotas = {'consumers': -1, 'containers': 66, + 'orders': -1, 'secrets': 55, + 'transport_keys': -1} + self.assertEqual(expected_quotas, quotas) + + def test_compute_effective_quotas_using_all_defaults(self): + configured_quotas = {'consumers': None, 'containers': None, + 'orders': None, 'secrets': None, + 'transport_keys': None} + quotas = self.quota_driver._compute_effective_quotas(configured_quotas) + expected_quotas = {'consumers': -1, 'containers': -1, + 'orders': -1, 'secrets': -1, + 'transport_keys': -1} + self.assertEqual(expected_quotas, quotas) def test_is_unlimited_true(self): self.assertTrue(self.quota_driver._is_unlimited_value(-1)) @@ -39,6 +60,137 @@ class WhenTestingQuotaFunctions(utils.BaseTestCase): def test_is_unlimited_false(self): self.assertFalse(self.quota_driver._is_unlimited_value(1)) + def test_is_unlimited_none_is_false(self): + self.assertFalse(self.quota_driver._is_unlimited_value(None)) + + def test_should_get_project_quotas(self): + self.create_a_test_project_quotas() + project_quotas = self.quota_driver.get_project_quotas( + self.get_test_project_id()) + self.assertEqual({'project_quotas': + self.get_test_parsed_project_quotas()}, + project_quotas) + + def test_should_return_not_found_get_project_quotas(self): + project_quotas = self.quota_driver.get_project_quotas('dummy') + self.assertIsNone(project_quotas) + + def test_should_get_project_quotas_list(self): + self.create_a_test_project_quotas() + project_quotas = self.quota_driver.get_project_quotas_list() + self.assertEqual({'project_quotas': [{ + 'project_id': u'project1', + 'project_quotas': {'consumers': 105, + 'containers': 103, + 'orders': 102, + 'secrets': 101, + 'transport_keys': 104}}], 'total': 1}, + project_quotas) + + def test_should_get_empty_project_quotas_list(self): + project_quotas = self.quota_driver.get_project_quotas_list() + self.assertEqual({'total': 0, 'project_quotas': []}, project_quotas) + + def test_should_delete_project_quotas(self): + self.create_a_test_project_quotas() + self.quota_driver.delete_project_quotas( + self.get_test_project_id()) + + def test_should_raise_not_found_delete_project_quotas(self): + self.assertRaises( + exception.NotFound, + self.quota_driver.delete_project_quotas, + 'dummy') + + def test_get_project_quotas_with_partial_definition(self): + self.create_a_test_project_quotas('partial') + project_quotas = self.quota_driver.get_project_quotas( + self.get_test_project_id('partial')) + self.assertEqual({'project_quotas': + self.get_test_response_project_quotas('partial')}, + project_quotas) + + def test_get_project_quotas_using_empty_definition(self): + self.create_a_test_project_quotas('none') + project_quotas = self.quota_driver.get_project_quotas( + self.get_test_project_id('none')) + self.assertEqual({'project_quotas': + self.get_test_response_project_quotas('none')}, + project_quotas) + + def test_get_quotas_using_some_defaults(self): + self.create_a_test_project_quotas('partial') + quotas = self.quota_driver.get_quotas( + self.get_test_project_id('partial')) + expected_quotas = {'quotas': {'consumers': -1, 'containers': 66, + 'orders': -1, 'secrets': 55, + 'transport_keys': -1}} + self.assertEqual(expected_quotas, quotas) + + def test_get_quotas_using_all_defaults(self): + quotas = self.quota_driver.get_quotas('not_configured') + expected_quotas = {'quotas': {'consumers': -1, 'containers': -1, + 'orders': -1, 'secrets': -1, + 'transport_keys': -1}} + self.assertEqual(expected_quotas, quotas) + + # ----------------------- Helper Functions --------------------------- + def get_test_project_id(self, index=1): + if index == 'partial': + return 'project_partial' + elif index == 'none': + return 'project_none' + else: + return 'project' + str(index) + + def get_test_parsed_project_quotas(self, index=1): + if index == 'partial': + parsed_project_quotas = { + 'secrets': 55, + 'containers': 66} + elif index == 'none': + parsed_project_quotas = {} + else: + parsed_project_quotas = { + 'secrets': index * 100 + 1, + 'orders': index * 100 + 2, + 'containers': index * 100 + 3, + 'transport_keys': index * 100 + 4, + 'consumers': index * 100 + 5} + return parsed_project_quotas + + def get_test_response_project_quotas(self, index=1): + if index == 'partial': + response_project_quotas = { + 'secrets': 55, + 'orders': None, + 'containers': 66, + 'transport_keys': None, + 'consumers': None} + elif index == 'none': + response_project_quotas = { + 'secrets': None, + 'orders': None, + 'containers': None, + 'transport_keys': None, + 'consumers': None} + else: + response_project_quotas = { + 'secrets': index * 100 + 1, + 'orders': index * 100 + 2, + 'containers': index * 100 + 3, + 'transport_keys': index * 100 + 4, + 'consumers': index * 100 + 5} + return response_project_quotas + + def create_a_test_project_quotas(self, index=1): + project_id = self.get_test_project_id(index) + parsed_project_quotas = self.get_test_parsed_project_quotas(index) + self.quota_driver.set_project_quotas(project_id, parsed_project_quotas) + + def create_project_quotas(self): + for index in [1, 2, 3]: + self.create_a_test_project_quotas(index) if __name__ == '__main__': unittest.main() diff --git a/barbican/tests/model/repositories/test_repositories_quotas.py b/barbican/tests/model/repositories/test_repositories_quotas.py new file mode 100644 index 000000000..5464a785f --- /dev/null +++ b/barbican/tests/model/repositories/test_repositories_quotas.py @@ -0,0 +1,249 @@ +# Copyright (c) 2015 Cisco Systems +# +# 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 unittest + +from barbican.common import exception +from barbican.model import models +from barbican.model import repositories +from barbican.tests import database_utils + + +class WhenTestingProjectQuotasRepo(database_utils.RepositoryTestCase): + + def setUp(self): + super(WhenTestingProjectQuotasRepo, self).setUp() + self.project_quotas_repo = repositories.ProjectQuotasRepo() + + self.project_id_1 = '11111' + self.project_id_2 = '22222' + self.project_id_3 = '33333' + self.parsed_project_quotas_1 = { + 'secrets': 101, + 'orders': 102, + 'containers': 103, + 'transport_keys': 104, + 'consumers': 105} + self.parsed_project_quotas_2 = { + 'secrets': 201, + 'orders': 202, + 'containers': 203, + 'transport_keys': 204, + 'consumers': 205} + self.parsed_project_quotas_3 = { + 'secrets': 301, + 'containers': 303, + 'consumers': 305} + + def test_get_list_of_one_project_quotas(self): + session = self.project_quotas_repo.get_session() + self.project_quotas_repo.create_or_update_by_project_id( + self.project_id_1, + self.parsed_project_quotas_1, + session) + session.commit() + retrieved_project_quotas, offset, limit, total =\ + self.project_quotas_repo.get_by_create_date(session=session) + self.assertEqual(0, offset) + self.assertEqual(10, limit) + self.assertEqual(1, total) + self.assertEqual([self.project_id_1], + [s.project_id for s in retrieved_project_quotas]) + self.assertEqual([101], + [s.secrets for s in retrieved_project_quotas]) + self.assertEqual([102], + [s.orders for s in retrieved_project_quotas]) + self.assertEqual([103], + [s.containers for s in retrieved_project_quotas]) + self.assertEqual([104], + [s.transport_keys for s in retrieved_project_quotas]) + self.assertEqual([105], + [s.consumers for s in retrieved_project_quotas]) + + def test_get_list_of_two_project_quotas(self): + session = self.project_quotas_repo.get_session() + self.project_quotas_repo.create_or_update_by_project_id( + self.project_id_1, + self.parsed_project_quotas_1, + session) + self.project_quotas_repo.create_or_update_by_project_id( + self.project_id_2, + self.parsed_project_quotas_2, + session) + session.commit() + retrieved_project_quotas, offset, limit, total =\ + self.project_quotas_repo.get_by_create_date(session=session) + self.assertEqual(0, offset) + self.assertEqual(10, limit) + self.assertEqual(2, total) + self.assertItemsEqual([self.project_id_1, self.project_id_2], + [s.project_id for s in retrieved_project_quotas]) + self.assertItemsEqual([101, 201], + [s.secrets for s in retrieved_project_quotas]) + self.assertItemsEqual([102, 202], + [s.orders for s in retrieved_project_quotas]) + self.assertItemsEqual([103, 203], + [s.containers for s in retrieved_project_quotas]) + self.assertItemsEqual([104, 204], + [s.transport_keys for s in + retrieved_project_quotas]) + self.assertItemsEqual([105, 205], + [s.consumers for s in retrieved_project_quotas]) + + def test_should_raise_get_list_of_zero_project_quotas(self): + session = self.project_quotas_repo.get_session() + self.assertRaises( + exception.NotFound, + self.project_quotas_repo.get_by_create_date, + session=session, + suppress_exception=False) + + def test_should_suppress_get_list_of_zero_project_quotas(self): + session = self.project_quotas_repo.get_session() + retrieved_project_quotas, offset, limit, total =\ + self.project_quotas_repo.get_by_create_date( + session=session, suppress_exception=True) + self.assertEqual(0, offset) + self.assertEqual(10, limit) + self.assertEqual(0, total) + + def test_get_specific_project_quotas(self): + session = self.project_quotas_repo.get_session() + self.project_quotas_repo.create_or_update_by_project_id( + self.project_id_1, self.parsed_project_quotas_1, session) + session.commit() + retrieved_project_quotas =\ + self.project_quotas_repo.get_by_project_id(self.project_id_1, + session=session) + self.assertEqual(self.project_id_1, + retrieved_project_quotas.project_id) + self.assertEqual(101, retrieved_project_quotas.secrets) + self.assertEqual(102, retrieved_project_quotas.orders) + self.assertEqual(103, retrieved_project_quotas.containers) + self.assertEqual(104, retrieved_project_quotas.transport_keys) + self.assertEqual(105, retrieved_project_quotas.consumers) + + def test_project_quotas_with_some_defaults(self): + session = self.project_quotas_repo.get_session() + self.project_quotas_repo.create_or_update_by_project_id( + self.project_id_3, self.parsed_project_quotas_3, session) + session.commit() + retrieved_project_quotas =\ + self.project_quotas_repo.get_by_project_id(self.project_id_3, + session=session) + self.assertEqual(self.project_id_3, + retrieved_project_quotas.project_id) + self.assertEqual(301, retrieved_project_quotas.secrets) + self.assertIsNone(retrieved_project_quotas.orders) + self.assertEqual(303, retrieved_project_quotas.containers) + self.assertIsNone(retrieved_project_quotas.transport_keys) + self.assertEqual(305, retrieved_project_quotas.consumers) + + def test_update_specific_project_quotas(self): + session = self.project_quotas_repo.get_session() + + self.project_quotas_repo.create_or_update_by_project_id( + self.project_id_1, self.parsed_project_quotas_1, session) + session.commit() + self.project_quotas_repo.create_or_update_by_project_id( + self.project_id_1, self.parsed_project_quotas_2, session) + session.commit() + retrieved_project_quotas =\ + self.project_quotas_repo.get_by_project_id(self.project_id_1, + session=session) + self.assertEqual(self.project_id_1, + retrieved_project_quotas.project_id) + self.assertEqual(201, retrieved_project_quotas.secrets) + self.assertEqual(202, retrieved_project_quotas.orders) + self.assertEqual(203, retrieved_project_quotas.containers) + self.assertEqual(204, retrieved_project_quotas.transport_keys) + self.assertEqual(205, retrieved_project_quotas.consumers) + + def test_should_raise_get_missing_specific_project_quotas(self): + session = self.project_quotas_repo.get_session() + self.assertRaises( + exception.NotFound, + self.project_quotas_repo.get_by_project_id, + "dummy", + suppress_exception=False, + session=session) + + def test_should_suppress_get_missing_specific_project_quotas(self): + session = self.project_quotas_repo.get_session() + retrieved_project_quotas =\ + self.project_quotas_repo.get_by_project_id(self.project_id_1, + suppress_exception=True, + session=session) + self.assertIsNone(retrieved_project_quotas) + + def test_get_by_create_date_nothing(self): + session = self.project_quotas_repo.get_session() + retrieved_project_quotas, offset, limit, total =\ + self.project_quotas_repo.get_by_create_date( + session=session, suppress_exception=True) + self.assertEqual([], retrieved_project_quotas) + self.assertEqual(0, offset) + self.assertEqual(10, limit) + self.assertEqual(0, total) + + def test_should_raise_add_duplicate_project_id(self): + session = self.project_quotas_repo.get_session() + self.project_quotas_repo.create_or_update_by_project_id( + self.project_id_1, self.parsed_project_quotas_1, session) + session.commit() + project_quotas = models.ProjectQuotas( + self.project_id_1, self.parsed_project_quotas_2) + self.assertRaises( + exception.Duplicate, + self.project_quotas_repo.create_from, + project_quotas, + session) + + def test_should_delete(self): + session = self.project_quotas_repo.get_session() + self.project_quotas_repo.create_or_update_by_project_id( + self.project_id_1, self.parsed_project_quotas_1, session) + session.commit() + self.project_quotas_repo.delete_by_project_id(self.project_id_1, + session=session) + + def test_should_raise_delete_not_found(self): + session = self.project_quotas_repo.get_session() + self.assertRaises( + exception.NotFound, + self.project_quotas_repo.delete_by_project_id, + "dummy", + session=session) + + def test_should_suppress_delete_not_found(self): + session = self.project_quotas_repo.get_session() + self.project_quotas_repo.delete_by_project_id('dummy', + suppress_exception=True, + session=session) + + def test_do_entity_name(self): + self.assertEqual("ProjectQuotas", + self.project_quotas_repo._do_entity_name()) + + def test_should_raise_not_found_get_by_entity_id(self): + session = self.project_quotas_repo.get_session() + self.assertRaises( + exception.NotFound, + self.project_quotas_repo.get, + "dummy", + session=session) + + +if __name__ == '__main__': + unittest.main() diff --git a/barbican/tests/model/test_models.py b/barbican/tests/model/test_models.py index c3371ac78..3dd70d453 100644 --- a/barbican/tests/model/test_models.py +++ b/barbican/tests/model/test_models.py @@ -14,6 +14,7 @@ # limitations under the License. import datetime +import unittest from barbican.common import exception from barbican.model import models @@ -506,3 +507,86 @@ class WhenCreatingNewContainerACLUser(utils.BaseTestCase): self.assertRaises(exception.MissingArgumentError, models.ContainerACLUser, self.container_acl_id, None) + + +class WhenCreatingNewProjectQuotas(utils.BaseTestCase): + def setUp(self): + super(WhenCreatingNewProjectQuotas, self).setUp() + + def test_create_new_project_quotas(self): + project_id = '12345' + parsed_project_quotas = { + 'secrets': 101, + 'orders': 102, + 'containers': 103, + 'transport_keys': 104, + 'consumers': 105} + project_quotas = models.ProjectQuotas(project_id, + parsed_project_quotas) + + self.assertEqual('12345', project_quotas.project_id) + self.assertEqual(101, project_quotas.secrets) + self.assertEqual(102, project_quotas.orders) + self.assertEqual(103, project_quotas.containers) + self.assertEqual(104, project_quotas.transport_keys) + self.assertEqual(105, project_quotas.consumers) + + def test_create_new_project_quotas_with_all_default_quotas(self): + project_id = '12345' + project_quotas = models.ProjectQuotas(project_id, + None) + + self.assertEqual('12345', project_quotas.project_id) + self.assertEqual(None, project_quotas.secrets) + self.assertEqual(None, project_quotas.orders) + self.assertEqual(None, project_quotas.containers) + self.assertEqual(None, project_quotas.transport_keys) + self.assertEqual(None, project_quotas.consumers) + + def test_create_new_project_quotas_with_some_default_quotas(self): + project_id = '12345' + parsed_project_quotas = { + 'secrets': 101, + 'containers': 103, + 'consumers': 105} + project_quotas = models.ProjectQuotas(project_id, + parsed_project_quotas) + + self.assertEqual('12345', project_quotas.project_id) + self.assertEqual(101, project_quotas.secrets) + self.assertEqual(None, project_quotas.orders) + self.assertEqual(103, project_quotas.containers) + self.assertEqual(None, project_quotas.transport_keys) + self.assertEqual(105, project_quotas.consumers) + + def test_should_throw_exception_missing_project_id(self): + self.assertRaises(exception.MissingArgumentError, + models.ProjectQuotas, None, + None) + + def test_project_quotas_check_to_dict_fields(self): + project_id = '12345' + parsed_project_quotas = { + 'secrets': 101, + 'orders': 102, + 'containers': 103, + 'transport_keys': 104, + 'consumers': 105} + project_quotas = models.ProjectQuotas(project_id, + parsed_project_quotas) + self.assertEqual(project_id, + project_quotas.to_dict_fields()['project_id']) + self.assertEqual(101, + project_quotas.to_dict_fields()['secrets']) + self.assertEqual(102, + project_quotas.to_dict_fields()['orders']) + self.assertEqual(103, + project_quotas.to_dict_fields()['containers']) + self.assertEqual(104, + project_quotas.to_dict_fields()['transport_keys']) + self.assertEqual(105, + project_quotas.to_dict_fields()['consumers']) + + +if __name__ == '__main__': + unittest.main() diff --git a/etc/barbican/barbican.conf b/etc/barbican/barbican.conf index ca114a3c4..d0f58bfcd 100644 --- a/etc/barbican/barbican.conf +++ b/etc/barbican/barbican.conf @@ -195,28 +195,26 @@ periodic_interval_max_seconds = 10.0 # ====================== Quota Options =============================== [quotas] -enabled = true -# True enforces quotas for the number of resources used by each project. # For each resource, the default maximum number that can be used for # a project is set below. This value can be overridden for each # project through the API. A negative value means no limit. A zero # value effectively disables the resource. # default number of secrets allowed per project -quota_secrets = 500 +quota_secrets = -1 # default number of orders allowed per project -quota_orders = 100 +quota_orders = -1 # default number of containers allowed per project quota_containers = -1 # Note, a negative value signifies unlimited # default number of transport_keys allowed per project -quota_transport_keys = 100 +quota_transport_keys = -1 # default number of consumers allowed per project -quota_consumers = 100 +quota_consumers = -1 # ================= Keystone Notification Options - Application =============== diff --git a/etc/barbican/policy.json b/etc/barbican/policy.json index 9b09c2ce9..dbbbb4bc8 100644 --- a/etc/barbican/policy.json +++ b/etc/barbican/policy.json @@ -70,6 +70,6 @@ "container_acls:get": "rule:all_but_audit and rule:container_project_match", "quotas:get": "rule:all_users", "project_quotas:get": "rule:service_admin", - "project_quotas:post": "rule:service_admin", + "project_quotas:put": "rule:service_admin", "project_quotas:delete": "rule:service_admin" } diff --git a/functionaltests/api/v1/behaviors/quota_behaviors.py b/functionaltests/api/v1/behaviors/quota_behaviors.py index 23d3319e6..7054d9a55 100644 --- a/functionaltests/api/v1/behaviors/quota_behaviors.py +++ b/functionaltests/api/v1/behaviors/quota_behaviors.py @@ -28,10 +28,10 @@ class QuotaBehaviors(base_behaviors.BaseBehaviors): :param user_name: The user name used for REST command :return: a request Response object """ - resp = self.client.get('quotas', - response_model_type=quota_models.QuotaModel, - extra_headers=extra_headers, - use_auth=use_auth, user_name=user_name) + resp = self.client.get( + 'quotas', response_model_type=quota_models.QuotasResponseModel, + extra_headers=extra_headers, + use_auth=use_auth, user_name=user_name) return resp def get_project_quotas_list(self, limit=10, offset=0, extra_headers=None, @@ -44,21 +44,25 @@ class QuotaBehaviors(base_behaviors.BaseBehaviors): :param extra_headers: extra HTTP headers for the REST request :param use_auth: Boolean for whether to send authentication headers :param user_name: The user name used for REST command - :return: a request Response object + :return: the response, a list of project quotas and the next/prev refs """ params = {'limit': limit, 'offset': offset} resp = self.client.get( 'project-quotas', - response_model_type=quota_models.ProjectQuotaModel, + response_model_type=quota_models.ProjectQuotaListModel, params=params, extra_headers=extra_headers, use_auth=use_auth, user_name=user_name) - response = self.get_json(resp) - project_quotas, next_ref, prev_ref = self.client.get_list_of_models( - response, quota_models.ProjectQuotaModel) + # handle expected JSON parsing errors for unauthenticated requests + if resp.status_code == 401 and not use_auth: + return resp, None, None, None - return resp, project_quotas + project_quotas_list = self.get_json(resp) + project_quotas, next_ref, prev_ref = self.client.get_list_of_models( + project_quotas_list, quota_models.ProjectQuotaListItemModel) + + return resp, project_quotas, next_ref, prev_ref def get_project_quotas(self, project_id, extra_headers=None, use_auth=True, user_name=None): @@ -71,9 +75,14 @@ class QuotaBehaviors(base_behaviors.BaseBehaviors): """ resp = self.client.get( 'project-quotas/' + project_id, - response_model_type=quota_models.ProjectQuotaModel, + response_model_type=quota_models.ProjectQuotaOneModel, extra_headers=extra_headers, use_auth=use_auth, user_name=user_name) + + # handle expected JSON parsing errors for unauthenticated requests + if resp.status_code == 401 and not use_auth: + return resp, None, None, None + return resp def set_project_quotas(self, project_id, request_model, extra_headers=None, @@ -86,15 +95,18 @@ class QuotaBehaviors(base_behaviors.BaseBehaviors): :param user_name: The user name used for REST command :return: a request Response object """ - resp = self.client.post( + resp = self.client.put( 'project-quotas/' + project_id, request_model=request_model, - response_model_type=quota_models.ProjectQuotaModel, extra_headers=extra_headers, use_auth=use_auth, user_name=user_name) + + self.created_entities.append((project_id, user_name)) + return resp def delete_project_quotas(self, project_id, extra_headers=None, + expected_fail=False, use_auth=True, user_name=None): """Handles deleting project quotas @@ -107,4 +119,16 @@ class QuotaBehaviors(base_behaviors.BaseBehaviors): resp = self.client.delete('project-quotas/' + project_id, extra_headers=extra_headers, use_auth=use_auth, user_name=user_name) + + if not expected_fail: + for item in self.created_entities: + if item[0] == project_id: + self.created_entities.remove(item) + return resp + + def delete_all_created_quotas(self): + """Delete all of the project_quotas that we have created.""" + entities = list(self.created_entities) + for (acl_ref, user_name) in entities: + self.delete_project_quotas(acl_ref, user_name=user_name) diff --git a/functionaltests/api/v1/functional/test_quotas.py b/functionaltests/api/v1/functional/test_quotas.py index 318ea13dd..d72d6a3ec 100644 --- a/functionaltests/api/v1/functional/test_quotas.py +++ b/functionaltests/api/v1/functional/test_quotas.py @@ -35,64 +35,140 @@ class QuotasTestCase(base.TestCase): def setUp(self): super(QuotasTestCase, self).setUp() self.behaviors = quota_behaviors.QuotaBehaviors(self.client) - self.project_id = self.behaviors.get_project_id_from_name('admin') + self.project_id = self.behaviors.get_project_id_from_name( + CONF.identity.username) def tearDown(self): super(QuotasTestCase, self).tearDown() + self.behaviors.delete_all_created_quotas() - def test_get_quotas(self): - """Get quota information""" + def test_get_quotas_with_defaults(self): + """Get effective quota information for own project""" resp = self.behaviors.get_quotas() self.assertEqual(200, resp.status_code) - self.assertEqual(500, resp.model.quotas['secrets']) - self.assertEqual(100, resp.model.quotas['transport_keys']) - self.assertEqual(100, resp.model.quotas['orders']) - self.assertEqual(-1, resp.model.quotas['containers']) - self.assertEqual(100, resp.model.quotas['consumers']) + self.assertEqual(-1, resp.model.quotas.secrets) + self.assertEqual(-1, resp.model.quotas.transport_keys) + self.assertEqual(-1, resp.model.quotas.orders) + self.assertEqual(-1, resp.model.quotas.containers) + self.assertEqual(-1, resp.model.quotas.consumers) - def test_get_project_quota_list(self): - """Get list of all project quotas""" - - resp, project_quotas_list = self.behaviors.get_project_quotas_list( - user_name=service_admin) - - self.assertEqual(200, resp.status_code) - for project_quotas in project_quotas_list: - self.assertEqual(500, project_quotas.project_quotas['secrets']) - self.assertEqual(100, - project_quotas.project_quotas['transport_keys']) - self.assertEqual(100, project_quotas.project_quotas['orders']) - self.assertEqual(-1, project_quotas.project_quotas['containers']) - self.assertEqual(100, project_quotas.project_quotas['consumers']) - - def test_get_one_project_quotas(self): + def test_get_project_quotas_by_project_id(self): """Get project quota information for specific project""" - resp = self.behaviors.get_project_quotas(self.project_id, - user_name=service_admin) - - self.assertEqual(200, resp.status_code) - self.assertEqual(500, resp.model.project_quotas['secrets']) - self.assertEqual(100, resp.model.project_quotas['transport_keys']) - self.assertEqual(100, resp.model.project_quotas['orders']) - self.assertEqual(-1, resp.model.project_quotas['containers']) - self.assertEqual(100, resp.model.project_quotas['consumers']) - - def test_set_project_quotas(self): - """Set project quota information""" - request_model = quota_models.ProjectQuotaRequestModel( **get_set_project_quotas_request()) - resp = self.behaviors.set_project_quotas(self.project_id, + resp = self.behaviors.set_project_quotas('44444', request_model, user_name=service_admin) + self.assertEqual(204, resp.status_code) + + resp = self.behaviors.get_project_quotas('44444', + user_name=service_admin) + self.assertEqual(200, resp.status_code) + self.assertEqual(50, resp.model.project_quotas.secrets) + self.assertIsNone(resp.model.project_quotas.transport_keys) + self.assertEqual(10, resp.model.project_quotas.orders) + self.assertEqual(20, resp.model.project_quotas.containers) + self.assertIsNone(resp.model.project_quotas.consumers) + + def test_get_project_quotas_by_project_id_not_found(self): + """Get project quota information for specific project""" + resp = self.behaviors.get_project_quotas('dummy', + user_name=service_admin) + self.assertEqual(404, resp.status_code) def test_delete_project_quotas(self): """Delete project quota information""" + request_model = quota_models.ProjectQuotaRequestModel( + **get_set_project_quotas_request()) + resp = self.behaviors.set_project_quotas('55555', + request_model, + user_name=service_admin) + self.assertEqual(204, resp.status_code) - resp = self.behaviors.delete_project_quotas(self.project_id, + resp = self.behaviors.delete_project_quotas('55555', user_name=service_admin) self.assertEqual(204, resp.status_code) + + def test_delete_project_quotas_not_found(self): + """Get project quota information""" + resp = self.behaviors.delete_project_quotas('dummy', + user_name=service_admin) + self.assertEqual(404, resp.status_code) + + +class ProjectQuotasPagingTestCase(base.PagingTestCase): + + def setUp(self): + super(ProjectQuotasPagingTestCase, self).setUp() + self.behaviors = quota_behaviors.QuotaBehaviors(self.client) + + def tearDown(self): + self.behaviors.delete_all_created_quotas() + super(ProjectQuotasPagingTestCase, self).tearDown() + + def create_model(self): + request_model = quota_models.ProjectQuotaRequestModel( + **get_set_project_quotas_request()) + return request_model + + def create_resources(self, count=0, model=None): + for x in range(0, count): + self.behaviors.set_project_quotas(str(x), model, + user_name=service_admin) + + def get_resources(self, limit=10, offset=0, filter=None): + return self.behaviors.get_project_quotas_list( + limit=limit, offset=offset, user_name=service_admin) + + def set_filter_field(self, unique_str, model): + """ProjectQuotas API does not support filter """ + pass + + def test_get_project_quota_list_none(self): + """Get list of all project quotas, when there are none""" + + resp, project_quotas_list, _, _ =\ + self.behaviors.get_project_quotas_list(user_name=service_admin) + + self.assertEqual(200, resp.status_code) + self.assertEqual([], project_quotas_list) + + def test_get_project_quota_list_one(self): + """Get list of all project quotas, when there is one""" + + request_model = quota_models.ProjectQuotaRequestModel( + **get_set_project_quotas_request()) + resp = self.behaviors.set_project_quotas('11111', + request_model, + user_name=service_admin) + self.assertEqual(204, resp.status_code) + + resp, project_quotas_list, _, _ =\ + self.behaviors.get_project_quotas_list(user_name=service_admin) + + self.assertEqual(200, resp.status_code) + self.assertEqual(1, len(project_quotas_list)) + + def test_get_project_quota_list_two(self): + """Get list of all project quotas, when there is one""" + + request_model = quota_models.ProjectQuotaRequestModel( + **get_set_project_quotas_request()) + resp = self.behaviors.set_project_quotas('22222', + request_model, + user_name=service_admin) + self.assertEqual(204, resp.status_code) + resp = self.behaviors.set_project_quotas('33333', + request_model, + user_name=service_admin) + self.assertEqual(204, resp.status_code) + + resp, project_quotas_list, _, _ =\ + self.behaviors.get_project_quotas_list(user_name=service_admin) + + self.assertEqual(200, resp.status_code) + self.assertEqual(2, len(project_quotas_list)) diff --git a/functionaltests/api/v1/functional/test_quotas_rbac.py b/functionaltests/api/v1/functional/test_quotas_rbac.py index e8ec8eb86..dcc7b9c98 100644 --- a/functionaltests/api/v1/functional/test_quotas_rbac.py +++ b/functionaltests/api/v1/functional/test_quotas_rbac.py @@ -56,7 +56,7 @@ test_data_rbac_get_project_quotas = { test_data_rbac_set_project_quotas = { 'with_service_admin': {'user': service_admin, 'admin': service_admin, - 'expected_return': 200}, + 'expected_return': 204}, 'with_admin_a': {'user': admin_a, 'admin': admin_a, 'expected_return': 403}, 'with_creator_a': {'user': creator_a, 'admin': admin_a, @@ -125,7 +125,7 @@ class RBACQuotasTestCase(base.TestCase): :param admin: the admin of the group owning quotas :param expected_return: the expected http return code """ - resp, _ = self.behaviors.get_project_quotas_list(user_name=user) + resp, _, _, _ = self.behaviors.get_project_quotas_list(user_name=user) self.assertEqual(expected_return, resp.status_code) @utils.parameterized_dataset(test_data_rbac_set_project_quotas) diff --git a/functionaltests/api/v1/models/quota_models.py b/functionaltests/api/v1/models/quota_models.py index 9b8d43b90..6446a16ed 100644 --- a/functionaltests/api/v1/models/quota_models.py +++ b/functionaltests/api/v1/models/quota_models.py @@ -17,19 +17,28 @@ limitations under the License. from functionaltests.api.v1.models.base_models import BaseModel -class QuotaModel(BaseModel): +class QuotasModel(BaseModel): + + def __init__(self, secrets=None, orders=None, containers=None, + transport_keys=None, consumers=None): + super(QuotasModel, self).__init__() + self.secrets = secrets + self.orders = orders + self.containers = containers + self.transport_keys = transport_keys + self.consumers = consumers + + +class QuotasResponseModel(BaseModel): def __init__(self, quotas=None): - super(QuotaModel, self).__init__() + super(QuotasResponseModel, self).__init__() self.quotas = quotas - -class ProjectQuotaModel(BaseModel): - - def __init__(self, project_quotas=None, project_id=None): - super(ProjectQuotaModel, self).__init__() - self.project_quotas = project_quotas - self.project_id = project_id + @classmethod + def dict_to_obj(cls, input_dict): + quotas = QuotasModel(**input_dict.get('quotas')) + return cls(quotas=quotas) class ProjectQuotaRequestModel(BaseModel): @@ -37,3 +46,37 @@ class ProjectQuotaRequestModel(BaseModel): def __init__(self, project_quotas=None): super(ProjectQuotaRequestModel, self).__init__() self.project_quotas = project_quotas + + @classmethod + def dict_to_obj(cls, input_dict): + project_quotas = QuotasModel(**input_dict.get('project_quotas')) + return cls(project_quotas=project_quotas) + + +class ProjectQuotaOneModel(BaseModel): + + def __init__(self, project_quotas=None): + super(ProjectQuotaOneModel, self).__init__() + self.project_quotas = QuotasModel(**project_quotas) + + +class ProjectQuotaListItemModel(BaseModel): + + def __init__(self, project_id=None, project_quotas=None): + super(ProjectQuotaListItemModel, self).__init__() + self.project_id = project_id + self.project_quotas = QuotasModel(**project_quotas) + + +class ProjectQuotaListModel(BaseModel): + + def __init__(self, project_quotas=None): + super(ProjectQuotaListModel, self).__init__() + self.project_quotas = project_quotas + + @classmethod + def dict_to_obj(cls, input_dict): + project_quotas = [ProjectQuotaListItemModel(**project_quotas_item) + for project_quotas_item in + input_dict.get('project_quotas', [])] + return cls(project_quotas=project_quotas) diff --git a/functionaltests/run_tests.sh b/functionaltests/run_tests.sh index 1ffefbae5..faa58735d 100755 --- a/functionaltests/run_tests.sh +++ b/functionaltests/run_tests.sh @@ -35,8 +35,9 @@ retval=$? testr slowest # run the tests in parallel +SKIP=^\(\?\!\.\*ProjectQuotasPagingTestCase\) testr init -testr run --parallel --subunit | subunit-trace --no-failure-debug -f +testr run $SKIP --parallel --subunit | subunit-trace --no-failure-debug -f retval=$? testr slowest