diff options
-rwxr-xr-x | bin/nova-manage | 51 | ||||
-rw-r--r-- | etc/nova/policy.json | 4 | ||||
-rw-r--r-- | nova/api/openstack/compute/contrib/quotas.py | 78 | ||||
-rw-r--r-- | nova/api/openstack/compute/limits.py | 15 | ||||
-rw-r--r-- | nova/db/api.py | 105 | ||||
-rw-r--r-- | nova/db/sqlalchemy/api.py | 250 | ||||
-rw-r--r-- | nova/db/sqlalchemy/migrate_repo/versions/115_make_user_quotas_key_and_value.py | 94 | ||||
-rw-r--r-- | nova/db/sqlalchemy/models.py | 15 | ||||
-rw-r--r-- | nova/exception.py | 5 | ||||
-rw-r--r-- | nova/quota.py | 219 | ||||
-rw-r--r-- | nova/tests/policy.json | 3 | ||||
-rw-r--r-- | nova/tests/test_quota.py | 607 |
12 files changed, 1289 insertions, 157 deletions
diff --git a/bin/nova-manage b/bin/nova-manage index 1bd0691f4..e581c8fd6 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -224,6 +224,10 @@ def _db_error(caught_exception): class ProjectCommands(object): """Class for managing projects.""" + @args('--project', dest="project_id", metavar='<Project name>', + help='Project name') + @args('--key', dest="key", metavar='<key>', help='Key') + @args('--value', dest="value", metavar='<value>', help='Value') def quota(self, project_id, key=None, value=None): """Set or display quotas for project""" ctxt = context.get_admin_context() @@ -256,6 +260,52 @@ class ProjectCommands(object): AccountCommands = ProjectCommands +class QuotaCommands(object): + """Class for managing quotas.""" + + @args('--project', dest="project_id", metavar='<Project name>', + help='Project name') + @args('--key', dest="key", metavar='<key>', help='Key') + @args('--value', dest="value", metavar='<value>', help='Value') + def project(self, project_id, key=None, value=None): + """Set or display quotas for project""" + ctxt = context.get_admin_context() + if key: + if value.lower() == 'unlimited': + value = None + try: + db.quota_update(ctxt, project_id, key, value) + except exception.ProjectQuotaNotFound: + db.quota_create(ctxt, project_id, key, value) + project_quota = QUOTAS.get_project_quotas(ctxt, project_id) + for key, value in project_quota.iteritems(): + if value['limit'] < 0 or value['limit'] is None: + value['limit'] = 'unlimited' + print '%s: %s' % (key, value['limit']) + + @args('--user', dest="user_id", metavar='<User name>', + help='User name') + @args('--project', dest="project_id", metavar='<Project name>', + help='Project name') + @args('--key', dest="key", metavar='<key>', help='Key') + @args('--value', dest="value", metavar='<value>', help='Value') + def user(self, user_id, project_id, key=None, value=None): + """Set or display quotas for user""" + ctxt = context.get_admin_context() + if key: + if value.lower() == 'unlimited': + value = None + try: + db.quota_update_for_user(ctxt, user_id, project_id, key, value) + except exception.UserQuotaNotFound: + db.quota_create_for_user(ctxt, user_id, project_id, key, value) + user_quota = QUOTAS.get_user_quotas(ctxt, user_id, project_id) + for key, value in user_quota.iteritems(): + if value['limit'] < 0 or value['limit'] is None: + value['limit'] = 'unlimited' + print '%s: %s' % (key, value['limit']) + + class FixedIpCommands(object): """Class for managing fixed ip.""" @@ -1319,6 +1369,7 @@ CATEGORIES = [ ('logs', GetLogCommands), ('network', NetworkCommands), ('project', ProjectCommands), + ('quota', QuotaCommands), ('service', ServiceCommands), ('shell', ShellCommands), ('sm', StorageManagerCommands), diff --git a/etc/nova/policy.json b/etc/nova/policy.json index dc8ed5b8f..b0a20ee94 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -1,5 +1,6 @@ { "admin_or_owner": [["role:admin"], ["project_id:%(project_id)s"]], + "admin_or_projectadmin": [["role:projectadmin"], ["role:admin"]], "default": [["rule:admin_or_owner"]], @@ -48,7 +49,8 @@ "compute_extension:networks": [["rule:admin_api"]], "compute_extension:networks:view": [], "compute_extension:quotas:show": [], - "compute_extension:quotas:update": [["rule:admin_api"]], + "compute_extension:quotas:update_for_project": [["rule:admin_api"]], + "compute_extension:quotas:update_for_user": [["rule:admin_or_projectadmin"]], "compute_extension:quota_classes": [], "compute_extension:rescue": [], "compute_extension:security_groups": [], diff --git a/nova/api/openstack/compute/contrib/quotas.py b/nova/api/openstack/compute/contrib/quotas.py index 33584badc..56583ff79 100644 --- a/nova/api/openstack/compute/contrib/quotas.py +++ b/nova/api/openstack/compute/contrib/quotas.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import urlparse import webob from nova.api.openstack import extensions @@ -24,13 +25,15 @@ from nova import db from nova.db.sqlalchemy import api as sqlalchemy_api from nova import exception from nova import quota +from nova import utils QUOTAS = quota.QUOTAS -authorize_update = extensions.extension_authorizer('compute', 'quotas:update') -authorize_show = extensions.extension_authorizer('compute', 'quotas:show') +def authorize_action(context, action_name): + action = 'quotas:%s' % action_name + extensions.extension_authorizer('compute', action)(context) class QuotaTemplate(xmlutil.TemplateBuilder): @@ -57,51 +60,102 @@ class QuotaSetsController(object): return dict(quota_set=result) - def _validate_quota_limit(self, limit): + def _validate_quota_limit(self, limit, remain, quota): # NOTE: -1 is a flag value for unlimited if limit < -1: msg = _("Quota limit must be -1 or greater.") raise webob.exc.HTTPBadRequest(explanation=msg) - def _get_quotas(self, context, id, usages=False): - values = QUOTAS.get_project_quotas(context, id, usages=usages) + # Quota limit must be less than the remains of the project. + if remain != -1 and remain < limit - quota: + msg = _("Quota limit exceed the remains of the project.") + raise webob.exc.HTTPBadRequest(explanation=msg) + + def _get_quotas(self, context, id, user_id=None, remaining=False, + usages=False): + # Get the remaining quotas for a project. + if remaining: + values = QUOTAS.get_remaining_quotas(context, id) + return values + + if user_id: + # If user_id, return quotas for the given user. + values = QUOTAS.get_user_quotas(context, user_id, id, + usages=usages) + else: + values = QUOTAS.get_project_quotas(context, id, usages=usages) if usages: return values else: return dict((k, v['limit']) for k, v in values.items()) + def _request_params(self, req): + qs = req.environ.get('QUERY_STRING', '') + return urlparse.parse_qs(qs) + @wsgi.serializers(xml=QuotaTemplate) def show(self, req, id): context = req.environ['nova.context'] - authorize_show(context) + authorize_action(context, 'show') + params = self._request_params(req) + remaining = False + if 'remaining' in params: + remaining = utils.bool_from_str(params["remaining"][0]) + user_id = None + if 'user_id' in params: + user_id = params["user_id"][0] try: sqlalchemy_api.authorize_project_context(context, id) - return self._format_quota_set(id, self._get_quotas(context, id)) + return self._format_quota_set(id, + self._get_quotas(context, id, user_id, remaining)) except exception.NotAuthorized: raise webob.exc.HTTPForbidden() @wsgi.serializers(xml=QuotaTemplate) def update(self, req, id, body): context = req.environ['nova.context'] - authorize_update(context) + params = self._request_params(req) project_id = id + user_id = None + remains = {} + quotas = {} + if 'user_id' in params: + # Project admins are able to modify per-user quotas. + authorize_action(context, 'update_for_user') + user_id = params["user_id"][0] + remains = self._get_quotas(context, project_id, remaining=True) + quotas = db.quota_get_all_by_user(context, user_id, project_id) + else: + # Only admins are able to modify per-project quotas. + authorize_action(context, 'update_for_project') + for key in body['quota_set'].keys(): if key in QUOTAS: value = int(body['quota_set'][key]) - self._validate_quota_limit(value) try: - db.quota_update(context, project_id, key, value) + if user_id: + self._validate_quota_limit(value, remains.get(key, 0), + quotas.get(key, 0)) + db.quota_update_for_user(context, user_id, + project_id, key, value) + else: + self._validate_quota_limit(value, remains.get(key, -1), + quotas.get(key, 0)) + db.quota_update(context, project_id, key, value) except exception.ProjectQuotaNotFound: db.quota_create(context, project_id, key, value) + except exception.UserQuotaNotFound: + db.quota_create_for_user(context, user_id, + project_id, key, value) except exception.AdminRequired: raise webob.exc.HTTPForbidden() - return {'quota_set': self._get_quotas(context, id)} + return {'quota_set': self._get_quotas(context, id, user_id)} @wsgi.serializers(xml=QuotaTemplate) def defaults(self, req, id): context = req.environ['nova.context'] - authorize_show(context) + authorize_action(context, 'show') return self._format_quota_set(id, QUOTAS.get_defaults(context)) diff --git a/nova/api/openstack/compute/limits.py b/nova/api/openstack/compute/limits.py index c0ef65670..990c08a10 100644 --- a/nova/api/openstack/compute/limits.py +++ b/nova/api/openstack/compute/limits.py @@ -23,6 +23,7 @@ import httplib import math import re import time +import urlparse import webob.dec import webob.exc @@ -85,8 +86,18 @@ class LimitsController(object): Return all global and rate limit information. """ context = req.environ['nova.context'] - quotas = QUOTAS.get_project_quotas(context, context.project_id, - usages=False) + qs = req.environ.get('QUERY_STRING', '') + params = urlparse.parse_qs(qs) + if 'user_id' in params: + user_id = params["user_id"][0] + quotas = QUOTAS.get_user_quotas(context, user_id, + context.project_id, + usages=False) + else: + quotas = QUOTAS.get_project_quotas(context, + context.project_id, + usages=False) + abs_limits = dict((k, v['limit']) for k, v in quotas.items()) rate_limits = req.environ.get("nova.limits", []) diff --git a/nova/db/api.py b/nova/db/api.py index 5e390d9f9..e6ebecbdf 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -563,6 +563,12 @@ def instance_create(context, values): return IMPL.instance_create(context, values) +def instance_data_get_for_user(context, user_id, project_id, session=None): + """Get (instance_count, total_cores, total_ram) for user.""" + return IMPL.instance_data_get_for_user(context, user_id, project_id, + session=session) + + def instance_data_get_for_project(context, project_id, session=None): """Get (instance_count, total_cores, total_ram) for project.""" return IMPL.instance_data_get_for_project(context, project_id, @@ -944,6 +950,42 @@ def quota_destroy(context, project_id, resource): ################### +def quota_create_for_user(context, user_id, project_id, resource, limit): + """Create a quota for the given user and project.""" + return IMPL.quota_create_for_user(context, user_id, + project_id, resource, limit) + + +def quota_get_for_user(context, user_id, project_id, resource): + """Retrieve a quota or raise if it does not exist.""" + return IMPL.quota_get_for_user(context, user_id, + project_id, resource) + + +def quota_get_all_by_user(context, user_id, project_id): + """Retrieve all quotas associated with a given user and project.""" + return IMPL.quota_get_all_by_user(context, user_id, project_id) + + +def quota_get_remaining(context, project_id): + """Retrieve the remaining quotas associated with a given project.""" + return IMPL.quota_get_remaining(context, project_id) + + +def quota_update_for_user(context, user_id, project_id, resource, limit): + """Update a quota or raise if it does not exist.""" + return IMPL.quota_update_for_user(context, user_id, + project_id, resource, limit) + + +def quota_destroy_for_user(context, user_id, project_id, resource): + """Destroy the quota or raise if it does not exist.""" + return IMPL.quota_destroy_for_user(context, user_id, project_id, resource) + + +################### + + def quota_class_create(context, class_name, resource, limit): """Create a quota class for the given name and resource.""" return IMPL.quota_class_create(context, class_name, resource, limit) @@ -977,16 +1019,21 @@ def quota_class_destroy_all_by_name(context, class_name): ################### -def quota_usage_create(context, project_id, resource, in_use, reserved, - until_refresh): - """Create a quota usage for the given project and resource.""" - return IMPL.quota_usage_create(context, project_id, resource, +def quota_usage_create(context, user_id, project_id, resource, in_use, + reserved, until_refresh): + """Create a quota usage for the given user and resource.""" + return IMPL.quota_usage_create(context, user_id, project_id, resource, in_use, reserved, until_refresh) -def quota_usage_get(context, project_id, resource): +def quota_usage_get(context, user_id, project_id, resource): """Retrieve a quota usage or raise if it does not exist.""" - return IMPL.quota_usage_get(context, project_id, resource) + return IMPL.quota_usage_get(context, user_id, project_id, resource) + + +def quota_usage_get_all_by_user(context, user_id, project_id): + """Retrieve all usage associated with a given user.""" + return IMPL.quota_usage_get_all_by_user(context, user_id, project_id) def quota_usage_get_all_by_project(context, project_id): @@ -994,25 +1041,25 @@ def quota_usage_get_all_by_project(context, project_id): return IMPL.quota_usage_get_all_by_project(context, project_id) -def quota_usage_update(context, class_name, resource, in_use, reserved, - until_refresh): +def quota_usage_update(context, user_id, project_id, resource, in_use, + reserved, until_refresh): """Update a quota usage or raise if it does not exist.""" - return IMPL.quota_usage_update(context, project_id, resource, + return IMPL.quota_usage_update(context, user_id, project_id, resource, in_use, reserved, until_refresh) -def quota_usage_destroy(context, project_id, resource): +def quota_usage_destroy(context, user_id, project_id, resource): """Destroy the quota usage or raise if it does not exist.""" - return IMPL.quota_usage_destroy(context, project_id, resource) + return IMPL.quota_usage_destroy(context, user_id, project_id, resource) ################### -def reservation_create(context, uuid, usage, project_id, resource, delta, - expire): - """Create a reservation for the given project and resource.""" - return IMPL.reservation_create(context, uuid, usage, project_id, +def reservation_create(context, uuid, usage, user_id, project_id, resource, + delta, expire): + """Create a reservation for the given user and resource.""" + return IMPL.reservation_create(context, uuid, usage, user_id, project_id, resource, delta, expire) @@ -1021,9 +1068,9 @@ def reservation_get(context, uuid): return IMPL.reservation_get(context, uuid) -def reservation_get_all_by_project(context, project_id): - """Retrieve all reservations associated with a given project.""" - return IMPL.reservation_get_all_by_project(context, project_id) +def reservation_get_all_by_user(context, user_id, project_id): + """Retrieve all reservations associated with a given user.""" + return IMPL.reservation_get_all_by_user(context, user_id, project_id) def reservation_destroy(context, uuid): @@ -1051,6 +1098,11 @@ def reservation_rollback(context, reservations): return IMPL.reservation_rollback(context, reservations) +def quota_destroy_all_by_user(context, user_id, project_id): + """Destroy all quotas associated with a given user.""" + return IMPL.quota_destroy_all_by_user(context, user_id, project_id) + + def quota_destroy_all_by_project(context, project_id): """Destroy all quotas associated with a given project.""" return IMPL.quota_destroy_all_by_project(context, project_id) @@ -1079,6 +1131,12 @@ def volume_create(context, values): return IMPL.volume_create(context, values) +def volume_data_get_for_user(context, user_id, project_id, session=None): + """Get (volume_count, gigabytes) for user.""" + return IMPL.volume_data_get_for_user(context, user_id, project_id, + session=session) + + def volume_data_get_for_project(context, project_id, session=None): """Get (volume_count, gigabytes) for project.""" return IMPL.volume_data_get_for_project(context, project_id, @@ -1260,6 +1318,11 @@ def security_group_get_by_name(context, project_id, group_name): return IMPL.security_group_get_by_name(context, project_id, group_name) +def security_group_get_by_user(context, user_id, project_id): + """Get all security groups belonging to a user.""" + return IMPL.security_group_get_by_user(context, user_id, project_id) + + def security_group_get_by_project(context, project_id): """Get all security groups belonging to a project.""" return IMPL.security_group_get_by_project(context, project_id) @@ -1295,6 +1358,12 @@ def security_group_destroy(context, security_group_id): return IMPL.security_group_destroy(context, security_group_id) +def security_group_count_by_user(context, user_id, project_id, session=None): + """Count number of security groups for a user in specific project.""" + return IMPL.security_group_count_by_user(context, user_id, project_id, + session=session) + + def security_group_count_by_project(context, project_id, session=None): """Count number of security groups in a project.""" return IMPL.security_group_count_by_project(context, project_id, diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 1f5b07643..2993aaaf3 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1408,20 +1408,33 @@ def instance_create(context, values): return instance_ref -@require_admin_context -def instance_data_get_for_project(context, project_id, session=None): +def _get_instance_data(context, project_id, user_id=None, session=None): result = model_query(context, func.count(models.Instance.id), func.sum(models.Instance.vcpus), func.sum(models.Instance.memory_mb), read_deleted="no", session=session).\ - filter_by(project_id=project_id).\ - first() + filter_by(project_id=project_id) + if user_id: + result = result.filter_by(user_id=user_id).first() + else: + result = result.first() # NOTE(vish): convert None to 0 return (result[0] or 0, result[1] or 0, result[2] or 0) +@require_admin_context +def instance_data_get_for_project(context, project_id, session=None): + return _get_instance_data(context, project_id, session=session) + + +@require_admin_context +def instance_data_get_for_user(context, user_id, project_id, session=None): + return _get_instance_data(context, project_id, user_id=user_id, + session=session) + + @require_context def instance_destroy(context, instance_uuid, constraint=None): session = get_session() @@ -2342,10 +2355,11 @@ def quota_get(context, project_id, resource, session=None): @require_context -def quota_get_all_by_project(context, project_id): +def quota_get_all_by_project(context, project_id, session=None): authorize_project_context(context, project_id) - rows = model_query(context, models.Quota, read_deleted="no").\ + rows = model_query(context, models.Quota, session=session, + read_deleted="no").\ filter_by(project_id=project_id).\ all() @@ -2387,6 +2401,97 @@ def quota_destroy(context, project_id, resource): @require_context +def quota_get_for_user(context, user_id, project_id, resource, session=None): + authorize_project_context(context, project_id) + result = model_query(context, models.UserQuota, session=session, + read_deleted="no").\ + filter_by(user_id=user_id).\ + filter_by(project_id=project_id).\ + filter_by(resource=resource).\ + first() + + if not result: + raise exception.UserQuotaNotFound(project_id=project_id, + user_id=user_id) + + return result + + +@require_context +def quota_get_all_by_user(context, user_id, project_id): + authorize_project_context(context, project_id) + + rows = model_query(context, models.UserQuota, read_deleted="no").\ + filter_by(user_id=user_id).\ + filter_by(project_id=project_id).\ + all() + + result = {'user_id': user_id, 'project_id': project_id} + for row in rows: + result[row.resource] = row.hard_limit + + return result + + +@require_context +def quota_get_remaining(context, project_id): + authorize_project_context(context, project_id) + + session = get_session() + with session.begin(): + rows = model_query(context, models.UserQuota, session=session, + read_deleted="no").\ + filter_by(project_id=project_id).\ + all() + + result = quota_get_all_by_project(context, project_id, session=session) + + for row in rows: + if row.resource in result: + result[row.resource] -= row.hard_limit + + result['project_id'] = project_id + + return result + + +@require_context +def quota_create_for_user(context, user_id, project_id, resource, limit): + authorize_project_context(context, project_id) + quota_ref = models.UserQuota() + quota_ref.user_id = user_id + quota_ref.project_id = project_id + quota_ref.resource = resource + quota_ref.hard_limit = limit + quota_ref.save() + return quota_ref + + +@require_context +def quota_update_for_user(context, user_id, project_id, resource, limit): + authorize_project_context(context, project_id) + session = get_session() + with session.begin(): + quota_ref = quota_get_for_user(context, user_id, project_id, resource, + session=session) + quota_ref.hard_limit = limit + quota_ref.save(session=session) + + +@require_context +def quota_destroy_for_user(context, user_id, project_id, resource): + authorize_project_context(context, project_id) + session = get_session() + with session.begin(): + quota_ref = quota_get_for_user(context, user_id, project_id, resource, + session=session) + quota_ref.delete(session=session) + + +################### + + +@require_context def quota_class_get(context, class_name, resource, session=None): result = model_query(context, models.QuotaClass, session=session, read_deleted="no").\ @@ -2461,9 +2566,10 @@ def quota_class_destroy_all_by_name(context, class_name): @require_context -def quota_usage_get(context, project_id, resource, session=None): +def quota_usage_get(context, user_id, project_id, resource, session=None): result = model_query(context, models.QuotaUsage, session=session, read_deleted="no").\ + filter_by(user_id=user_id).\ filter_by(project_id=project_id).\ filter_by(resource=resource).\ first() @@ -2475,6 +2581,22 @@ def quota_usage_get(context, project_id, resource, session=None): @require_context +def quota_usage_get_all_by_user(context, user_id, project_id): + authorize_project_context(context, project_id) + + rows = model_query(context, models.QuotaUsage, read_deleted="no").\ + filter_by(user_id=user_id).\ + filter_by(project_id=project_id).\ + all() + + result = {'user_id': user_id, 'project_id': project_id} + for row in rows: + result[row.resource] = dict(in_use=row.in_use, reserved=row.reserved) + + return result + + +@require_context def quota_usage_get_all_by_project(context, project_id): authorize_project_context(context, project_id) @@ -2483,16 +2605,20 @@ def quota_usage_get_all_by_project(context, project_id): all() result = {'project_id': project_id} + in_use = 0 + reserved = 0 for row in rows: - result[row.resource] = dict(in_use=row.in_use, reserved=row.reserved) + result[row.resource] = dict(in_use=in_use + row.in_use, + reserved=reserved + row.reserved) return result @require_admin_context -def quota_usage_create(context, project_id, resource, in_use, reserved, - until_refresh, session=None): +def quota_usage_create(context, user_id, project_id, resource, in_use, + reserved, until_refresh, session=None): quota_usage_ref = models.QuotaUsage() + quota_usage_ref.user_id = user_id quota_usage_ref.project_id = project_id quota_usage_ref.resource = resource quota_usage_ref.in_use = in_use @@ -2504,11 +2630,11 @@ def quota_usage_create(context, project_id, resource, in_use, reserved, @require_admin_context -def quota_usage_update(context, project_id, resource, in_use, reserved, - until_refresh, session=None): +def quota_usage_update(context, user_id, project_id, resource, in_use, + reserved, until_refresh, session=None): def do_update(session): - quota_usage_ref = quota_usage_get(context, project_id, resource, - session=session) + quota_usage_ref = quota_usage_get(context, user_id, project_id, + resource, session=session) quota_usage_ref.in_use = in_use quota_usage_ref.reserved = reserved quota_usage_ref.until_refresh = until_refresh @@ -2524,11 +2650,11 @@ def quota_usage_update(context, project_id, resource, in_use, reserved, @require_admin_context -def quota_usage_destroy(context, project_id, resource): +def quota_usage_destroy(context, user_id, project_id, resource): session = get_session() with session.begin(): - quota_usage_ref = quota_usage_get(context, project_id, resource, - session=session) + quota_usage_ref = quota_usage_get(context, user_id, project_id, + resource, session=session) quota_usage_ref.delete(session=session) @@ -2549,14 +2675,15 @@ def reservation_get(context, uuid, session=None): @require_context -def reservation_get_all_by_project(context, project_id): +def reservation_get_all_by_user(context, user_id, project_id): authorize_project_context(context, project_id) rows = model_query(context, models.QuotaUsage, read_deleted="no").\ + filter_by(user_id=user_id).\ filter_by(project_id=project_id).\ all() - result = {'project_id': project_id} + result = {'user_id': user_id, 'project_id': project_id} for row in rows: result.setdefault(row.resource, {}) result[row.resource][row.uuid] = row.delta @@ -2565,11 +2692,12 @@ def reservation_get_all_by_project(context, project_id): @require_admin_context -def reservation_create(context, uuid, usage, project_id, resource, delta, - expire, session=None): +def reservation_create(context, uuid, usage, user_id, project_id, resource, + delta, expire, session=None): reservation_ref = models.Reservation() reservation_ref.uuid = uuid reservation_ref.usage_id = usage['id'] + reservation_ref.user_id = user_id reservation_ref.project_id = project_id reservation_ref.resource = resource reservation_ref.delta = delta @@ -2599,6 +2727,7 @@ def _get_quota_usages(context, session): rows = model_query(context, models.QuotaUsage, read_deleted="no", session=session).\ + filter_by(user_id=context.user_id).\ filter_by(project_id=context.project_id).\ with_lockmode('update').\ all() @@ -2623,6 +2752,7 @@ def quota_reserve(context, resources, quotas, deltas, expire, refresh = False if resource not in usages: usages[resource] = quota_usage_create(elevated, + context.user_id, context.project_id, resource, 0, 0, @@ -2646,11 +2776,13 @@ def quota_reserve(context, resources, quotas, deltas, expire, # Grab the sync routine sync = resources[resource].sync - updates = sync(elevated, context.project_id, session) + updates = sync(elevated, context.user_id, + context.project_id, session) for res, in_use in updates.items(): # Make sure we have a destination for the usage! if res not in usages: usages[res] = quota_usage_create(elevated, + context.user_id, context.project_id, res, 0, 0, @@ -2701,6 +2833,7 @@ def quota_reserve(context, resources, quotas, deltas, expire, reservation = reservation_create(elevated, str(utils.gen_uuid()), usages[resource], + context.user_id, context.project_id, resource, delta, expire, session=session) @@ -2785,6 +2918,38 @@ def reservation_rollback(context, reservations): @require_admin_context +def quota_destroy_all_by_user(context, user_id, project_id): + session = get_session() + with session.begin(): + quotas = model_query(context, models.UserQuota, session=session, + read_deleted="no").\ + filter_by(user_id=user_id).\ + filter_by(project_id=project_id).\ + all() + + for quota_ref in quotas: + quota_ref.delete(session=session) + + quota_usages = model_query(context, models.QuotaUsage, + session=session, read_deleted="no").\ + filter_by(user_id=user_id).\ + filter_by(project_id=project_id).\ + all() + + for quota_usage_ref in quota_usages: + quota_usage_ref.delete(session=session) + + reservations = model_query(context, models.Reservation, + session=session, read_deleted="no").\ + filter_by(user_id=user_id).\ + filter_by(project_id=project_id).\ + all() + + for reservation_ref in reservations: + reservation_ref.delete(session=session) + + +@require_admin_context def quota_destroy_all_by_project(context, project_id): session = get_session() with session.begin(): @@ -2889,21 +3054,34 @@ def volume_create(context, values): return volume_ref -@require_admin_context -def volume_data_get_for_project(context, project_id, session=None): +def _get_volume_data(context, project_id, user_id=None, session=None): result = model_query(context, func.count(models.Volume.id), func.sum(models.Volume.size), read_deleted="no", session=session).\ - filter_by(project_id=project_id).\ - first() + filter_by(project_id=project_id) + + if user_id: + result = result.filter_by(user_id=user_id).first() + else: + result = result.first() - # NOTE(vish): convert None to 0 return (result[0] or 0, result[1] or 0) @require_admin_context +def volume_data_get_for_user(context, user_id, project_id, session=None): + return _get_volume_data(context, project_id, user_id=user_id, + session=session) + + +@require_admin_context +def volume_data_get_for_project(context, project_id, session=None): + return _get_volume_data(context, project_id, session=session) + + +@require_admin_context def volume_destroy(context, volume_id): session = get_session() with session.begin(): @@ -3412,6 +3590,14 @@ def security_group_get_by_name(context, project_id, group_name, @require_context +def security_group_get_by_user(context, user_id, project_id): + return _security_group_get_query(context, read_deleted="no").\ + filter_by(user_id=user_id).\ + filter_by(project_id=project_id).\ + all() + + +@require_context def security_group_get_by_project(context, project_id): return _security_group_get_query(context, read_deleted="no").\ filter_by(project_id=project_id).\ @@ -3507,6 +3693,16 @@ def security_group_destroy(context, security_group_id): @require_context +def security_group_count_by_user(context, user_id, project_id, session=None): + authorize_project_context(context, project_id) + return model_query(context, models.SecurityGroup, read_deleted="no", + session=session).\ + filter_by(user_id=user_id).\ + filter_by(project_id=project_id).\ + count() + + +@require_context def security_group_count_by_project(context, project_id, session=None): authorize_project_context(context, project_id) return model_query(context, models.SecurityGroup, read_deleted="no", diff --git a/nova/db/sqlalchemy/migrate_repo/versions/115_make_user_quotas_key_and_value.py b/nova/db/sqlalchemy/migrate_repo/versions/115_make_user_quotas_key_and_value.py new file mode 100644 index 000000000..447307952 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/115_make_user_quotas_key_and_value.py @@ -0,0 +1,94 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC. +# +# 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. + +from nova.openstack.common import log as logging +from sqlalchemy import Boolean, Column, DateTime, Integer +from sqlalchemy import MetaData, String, Table + +LOG = logging.getLogger(__name__) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta = MetaData() + meta.bind = migrate_engine + + # Add 'user_id' column to quota_usages table. + quota_usages = Table('quota_usages', meta, autoload=True) + user_id = Column('user_id', + String(length=255, convert_unicode=False, + assert_unicode=None, unicode_error=None, + _warn_on_bytestring=False)) + quota_usages.create_column(user_id) + + # Add 'user_id' column to reservations table. + reservations = Table('reservations', meta, autoload=True) + user_id = Column('user_id', + String(length=255, convert_unicode=False, + assert_unicode=None, unicode_error=None, + _warn_on_bytestring=False)) + reservations.create_column(user_id) + + # New table. + user_quotas = Table('user_quotas', meta, + Column('id', Integer(), primary_key=True), + Column('created_at', DateTime(timezone=False)), + Column('updated_at', DateTime(timezone=False)), + Column('deleted_at', DateTime(timezone=False)), + Column('deleted', Boolean(), default=False), + Column('user_id', + String(length=255, convert_unicode=False, + assert_unicode=None, unicode_error=None, + _warn_on_bytestring=False)), + Column('project_id', + String(length=255, convert_unicode=False, + assert_unicode=None, unicode_error=None, + _warn_on_bytestring=False)), + Column('resource', + String(length=255, convert_unicode=False, + assert_unicode=None, unicode_error=None, + _warn_on_bytestring=False), + nullable=False), + Column('hard_limit', Integer(), nullable=True), + mysql_engine='InnoDB', + mysql_charset='utf8', + ) + + try: + user_quotas.create() + except Exception: + LOG.error(_("Table |%s| not created!"), repr(user_quotas)) + raise + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + meta = MetaData() + meta.bind = migrate_engine + + quota_usages = Table('quota_usages', meta, autoload=True) + quota_usages.drop_column('user_id') + + reservations = Table('reservations', meta, autoload=True) + reservations.drop_column('user_id') + + user_quotas = Table('user_quotas', meta, autoload=True) + try: + user_quotas.drop() + except Exception: + LOG.error(_("user_quotas table not dropped")) + raise diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 7d4435a7a..fa3ebe8f0 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -428,6 +428,19 @@ class Quota(BASE, NovaBase): hard_limit = Column(Integer, nullable=True) +class UserQuota(BASE, NovaBase): + """Represents a single quota override for a user.""" + + __tablename__ = 'user_quotas' + id = Column(Integer, primary_key=True) + + user_id = Column(String(255), index=True) + project_id = Column(String(255), index=True) + + resource = Column(String(255)) + hard_limit = Column(Integer, nullable=True) + + class QuotaClass(BASE, NovaBase): """Represents a single quota override for a quota class. @@ -451,6 +464,7 @@ class QuotaUsage(BASE, NovaBase): __tablename__ = 'quota_usages' id = Column(Integer, primary_key=True) + user_id = Column(String(255), index=True) project_id = Column(String(255), index=True) resource = Column(String(255)) @@ -473,6 +487,7 @@ class Reservation(BASE, NovaBase): usage_id = Column(Integer, ForeignKey('quota_usages.id'), nullable=False) + user_id = Column(String(255), index=True) project_id = Column(String(255), index=True) resource = Column(String(255)) diff --git a/nova/exception.py b/nova/exception.py index e4e738e85..9c579322b 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -721,6 +721,11 @@ class QuotaResourceUnknown(QuotaNotFound): message = _("Unknown quota resources %(unknown)s.") +class UserQuotaNotFound(QuotaNotFound): + message = _("Quota for user %(user_id)s in project %(project_id)s " + "could not be found.") + + class ProjectQuotaNotFound(QuotaNotFound): message = _("Quota for project %(project_id)s could not be found.") diff --git a/nova/quota.py b/nova/quota.py index d3ba0aa02..44e3c593d 100644 --- a/nova/quota.py +++ b/nova/quota.py @@ -101,6 +101,11 @@ class DbQuotaDriver(object): return db.quota_get(context, project_id, resource) + def get_by_user(self, context, user_id, project_id, resource): + """Get a specific quota by user.""" + + return db.quota_get_for_user(context, user_id, project_id, resource) + def get_by_class(self, context, quota_class, resource): """Get a specific quota by quota class.""" @@ -143,16 +148,16 @@ class DbQuotaDriver(object): return quotas - def get_project_quotas(self, context, resources, project_id, - quota_class=None, defaults=True, - usages=True): + def _process_quotas(self, context, resources, project_id, quotas, + quota_class=None, defaults=True, usages=None): """ - Given a list of resources, retrieve the quotas for the given - project. + Given a list of resources, process the quotas for the given + quotas and usages. :param context: The request context, for access checks. :param resources: A dictionary of the registered resources. :param project_id: The ID of the project to return quotas for. + :param quotas: The quotas dictionary need to be processed. :param quota_class: If project_id != context.project_id, the quota class cannot be determined. This parameter allows it to be specified. It @@ -162,16 +167,11 @@ class DbQuotaDriver(object): default value, if there is no value from the quota class) will be reported if there is no specific value for the resource. - :param usages: If True, the current in_use and reserved counts + :param usages: If not None, the current in_use and reserved counts will also be returned. """ - quotas = {} - project_quotas = db.quota_get_all_by_project(context, project_id) - if usages: - project_usages = db.quota_usage_get_all_by_project(context, - project_id) - + modified_quotas = {} # Get the quotas for the appropriate class. If the project ID # matches the one in the context, we use the quota_class from # the context, otherwise, we use the provided quota_class (if @@ -185,11 +185,11 @@ class DbQuotaDriver(object): for resource in resources.values(): # Omit default/quota class values - if not defaults and resource.name not in project_quotas: + if not defaults and resource.name not in quotas: continue - quotas[resource.name] = dict( - limit=project_quotas.get(resource.name, class_quotas.get( + modified_quotas[resource.name] = dict( + limit=quotas.get(resource.name, class_quotas.get( resource.name, resource.default)), ) @@ -197,13 +197,96 @@ class DbQuotaDriver(object): # internal consumer of this interface wants to access the # usages directly from inside a transaction. if usages: - usage = project_usages.get(resource.name, {}) - quotas[resource.name].update( + usage = usages.get(resource.name, {}) + modified_quotas[resource.name].update( in_use=usage.get('in_use', 0), reserved=usage.get('reserved', 0), ) - return quotas + return modified_quotas + + def get_project_quotas(self, context, resources, project_id, + quota_class=None, defaults=True, + usages=True): + """ + Given a list of resources, retrieve the quotas for the given + project. + + :param context: The request context, for access checks. + :param resources: A dictionary of the registered resources. + :param project_id: The ID of the project to return quotas for. + :param quota_class: If project_id != context.project_id, the + quota class cannot be determined. This + parameter allows it to be specified. It + will be ignored if project_id == + context.project_id. + :param defaults: If True, the quota class value (or the + default value, if there is no value from the + quota class) will be reported if there is no + specific value for the resource. + :param usages: If True, the current in_use and reserved counts + will also be returned. + """ + + project_quotas = db.quota_get_all_by_project(context, project_id) + + project_usages = None + if usages: + project_usages = db.quota_usage_get_all_by_project(context, + project_id) + + return self._process_quotas(context, resources, + project_id, project_quotas, + quota_class=quota_class, + defaults=defaults, + usages=project_usages) + + def get_user_quotas(self, context, resources, user_id, project_id, + quota_class=None, defaults=True, + usages=True): + """ + Given a list of resources, retrieve the quotas for the given + user. + + :param context: The request context, for access checks. + :param resources: A dictionary of the registered resources. + :param project_id: The ID of the project to return quotas for. + :param user_id: The ID of the user to return quotas for. + :param quota_class: If project_id != context.project_id, the + quota class cannot be determined. This + parameter allows it to be specified. It + will be ignored if project_id == + context.project_id. + :param defaults: If True, the quota class value (or the + default value, if there is no value from the + quota class) will be reported if there is no + specific value for the resource. + :param usages: If True, the current in_use and reserved counts + will also be returned. + """ + + user_quotas = db.quota_get_all_by_user(context, user_id, project_id) + + user_usages = None + if usages: + user_usages = db.quota_usage_get_all_by_user(context, + user_id, + project_id) + + return self._process_quotas(context, resources, + project_id, user_quotas, + quota_class=quota_class, + defaults=defaults, + usages=user_usages) + + def get_remaining_quotas(self, context, project_id, resources): + """Get the remaining quotas for the given project.""" + defaults = self.get_defaults(context, resources) + quotas = db.quota_get_remaining(context, project_id) + for key in defaults.keys(): + if key in quotas: + defaults[key] = quotas[key] + return defaults def _get_quotas(self, context, resources, keys, has_sync): """ @@ -235,9 +318,10 @@ class DbQuotaDriver(object): raise exception.QuotaResourceUnknown(unknown=sorted(unknown)) # Grab and return the quotas (without usages) - quotas = self.get_project_quotas(context, sub_resources, - context.project_id, - context.quota_class, usages=False) + quotas = self.get_user_quotas(context, sub_resources, + context.user_id, + context.project_id, + context.quota_class, usages=False) return dict((k, v['limit']) for k, v in quotas.items()) @@ -368,6 +452,18 @@ class DbQuotaDriver(object): db.quota_destroy_all_by_project(context, project_id) + def destroy_all_by_user(self, context, user_id, project_id): + """ + Destroy all quotas, usages, and reservations associated with a + user. + + :param context: The request context, for access checks. + :param user_id: The ID of the user being deleted. + :param project_id: The ID of the project being deleted. + """ + + db.quota_destroy_all_by_user(context, user_id, project_id) + def expire(self, context): """Expire reservations. @@ -566,6 +662,11 @@ class QuotaEngine(object): return self._driver.get_by_project(context, project_id, resource) + def get_by_user(self, context, user_id, project_id, resource): + """Get a specific quota by user.""" + + return self._driver.get_by_user(context, user_id, project_id, resource) + def get_by_class(self, context, quota_class, resource): """Get a specific quota by quota class.""" @@ -611,10 +712,46 @@ class QuotaEngine(object): """ return self._driver.get_project_quotas(context, self._resources, - project_id, - quota_class=quota_class, - defaults=defaults, - usages=usages) + project_id, + quota_class=quota_class, + defaults=defaults, + usages=usages) + + def get_user_quotas(self, context, user_id, project_id, + quota_class=None, defaults=True, + usages=True): + """Retrieve the quotas for the given user. + + :param context: The request context, for access checks. + :param user_id: The ID of the user to return quotas for. + :param project_id: The ID of the project to return quotas for. + :param quota_class: If project_id != context.project_id, the + quota class cannot be determined. This + parameter allows it to be specified. + :param defaults: If True, the quota class value (or the + default value, if there is no value from the + quota class) will be reported if there is no + specific value for the resource. + :param usages: If True, the current in_use and reserved counts + will also be returned. + """ + + return self._driver.get_user_quotas(context, self._resources, + user_id, + project_id, + quota_class=quota_class, + defaults=defaults, + usages=usages) + + def get_remaining_quotas(self, context, project_id): + """Retrieve the remaining quotas for the given project. + + :param context: The request context, for access checks. + :param project_id: The ID of the project to return quotas for. + """ + + return self._driver.get_remaining_quotas(context, project_id, + self._resources) def count(self, context, resource, *args, **kwargs): """Count a resource. @@ -745,6 +882,18 @@ class QuotaEngine(object): self._driver.destroy_all_by_project(context, project_id) + def destroy_all_by_user(self, context, user_id, project_id): + """ + Destroy all quotas, usages, and reservations associated with a + user. + + :param context: The request context, for access checks. + :param user_id: The ID of the user being deleted. + :param project_id: The ID of the project being deleted. + """ + + self._driver.destroy_all_by_user(context, user_id, project_id) + def expire(self, context): """Expire reservations. @@ -761,26 +910,26 @@ class QuotaEngine(object): return sorted(self._resources.keys()) -def _sync_instances(context, project_id, session): +def _sync_instances(context, user_id, project_id, session): return dict(zip(('instances', 'cores', 'ram'), - db.instance_data_get_for_project( - context, project_id, session=session))) + db.instance_data_get_for_user( + context, user_id, project_id, session=session))) -def _sync_volumes(context, project_id, session): +def _sync_volumes(context, user_id, project_id, session): return dict(zip(('volumes', 'gigabytes'), - db.volume_data_get_for_project( - context, project_id, session=session))) + db.volume_data_get_for_user( + context, user_id, project_id, session=session))) -def _sync_floating_ips(context, project_id, session): +def _sync_floating_ips(context, user_id, project_id, session): return dict(floating_ips=db.floating_ip_count_by_project( context, project_id, session=session)) -def _sync_security_groups(context, project_id, session): - return dict(security_groups=db.security_group_count_by_project( - context, project_id, session=session)) +def _sync_security_groups(context, user_id, project_id, session): + return dict(security_groups=db.security_group_count_by_user( + context, user_id, project_id, session=session)) QUOTAS = QuotaEngine() diff --git a/nova/tests/policy.json b/nova/tests/policy.json index 3e4d166a3..4836a33b4 100644 --- a/nova/tests/policy.json +++ b/nova/tests/policy.json @@ -105,7 +105,8 @@ "compute_extension:networks": [], "compute_extension:networks:view": [], "compute_extension:quotas:show": [], - "compute_extension:quotas:update": [], + "compute_extension:quotas:update_for_project": [], + "compute_extension:quotas:update_for_user": [], "compute_extension:quota_classes": [], "compute_extension:rescue": [], "compute_extension:security_groups": [], diff --git a/nova/tests/test_quota.py b/nova/tests/test_quota.py index 794c578d6..1fcff0e4a 100644 --- a/nova/tests/test_quota.py +++ b/nova/tests/test_quota.py @@ -243,8 +243,10 @@ class FakeContext(object): class FakeDriver(object): - def __init__(self, by_project=None, by_class=None, reservations=None): + def __init__(self, by_user=None, by_project=None, by_class=None, + reservations=None): self.called = [] + self.by_user = by_user or {} self.by_project = by_project or {} self.by_class = by_class or {} self.reservations = reservations or [] @@ -256,6 +258,15 @@ class FakeDriver(object): except KeyError: raise exception.ProjectQuotaNotFound(project_id=project_id) + def get_by_user(self, context, user_id, project_id, resource): + self.called.append(('get_by_user', context, user_id, project_id, + resource)) + try: + return self.by_user[user_id][resource] + except KeyError: + raise exception.UserQuotaNotFound(project_id=project_id, + user_id=user_id) + def get_by_class(self, context, quota_class, resource): self.called.append(('get_by_class', context, quota_class, resource)) try: @@ -279,6 +290,13 @@ class FakeDriver(object): project_id, quota_class, defaults, usages)) return resources + def get_user_quotas(self, context, resources, user_id, project_id, + quota_class=None, defaults=True, usages=True): + self.called.append(('get_user_quotas', context, resources, + user_id, project_id, quota_class, defaults, + usages)) + return resources + def limit_check(self, context, resources, values): self.called.append(('limit_check', context, resources, values)) @@ -295,6 +313,10 @@ class FakeDriver(object): def destroy_all_by_project(self, context, project_id): self.called.append(('destroy_all_by_project', context, project_id)) + def destroy_all_by_user(self, context, user_id, project_id,): + self.called.append(('destroy_all_by_user', context, user_id, + project_id)) + def expire(self, context): self.called.append(('expire', context)) @@ -481,6 +503,19 @@ class QuotaEngineTestCase(test.TestCase): ]) self.assertEqual(result, 42) + def test_get_by_user(self): + context = FakeContext('test_project', 'test_class') + driver = FakeDriver(by_user=dict( + fake_user=dict(test_resource=42))) + quota_obj = quota.QuotaEngine(quota_driver_class=driver) + result = quota_obj.get_by_user(context, 'fake_user', + 'test_project', 'test_resource') + + self.assertEqual(driver.called, [ + ('get_by_user', context, 'fake_user', 'test_project', + 'test_resource'), ]) + self.assertEqual(result, 42) + def test_get_by_class(self): context = FakeContext('test_project', 'test_class') driver = FakeDriver(by_class=dict( @@ -551,6 +586,27 @@ class QuotaEngineTestCase(test.TestCase): self.assertEqual(result1, quota_obj._resources) self.assertEqual(result2, quota_obj._resources) + def test_get_user_quotas(self): + context = FakeContext(None, None) + driver = FakeDriver() + quota_obj = self._make_quota_obj(driver) + result1 = quota_obj.get_user_quotas(context, 'fake_user', + 'test_project') + result2 = quota_obj.get_user_quotas(context, 'fake_user', + 'test_project', + quota_class='test_class', + defaults=False, + usages=False) + + self.assertEqual(driver.called, [ + ('get_user_quotas', context, quota_obj._resources, + 'fake_user', 'test_project', None, True, True), + ('get_user_quotas', context, quota_obj._resources, + 'fake_user', 'test_project', 'test_class', False, False), + ]) + self.assertEqual(result1, quota_obj._resources) + self.assertEqual(result2, quota_obj._resources) + def test_count_no_resource(self): context = FakeContext(None, None) driver = FakeDriver() @@ -662,6 +718,16 @@ class QuotaEngineTestCase(test.TestCase): ('destroy_all_by_project', context, 'test_project'), ]) + def test_destroy_all_by_user(self): + context = FakeContext(None, None) + driver = FakeDriver() + quota_obj = self._make_quota_obj(driver) + quota_obj.destroy_all_by_user(context, 'fake_user', 'test_project') + + self.assertEqual(driver.called, [ + ('destroy_all_by_user', context, 'fake_user', 'test_project'), + ]) + def test_expire(self): context = FakeContext(None, None) driver = FakeDriver() @@ -1146,8 +1212,378 @@ class DbQuotaDriverTestCase(test.TestCase): self.stubs.Set(self.driver, 'get_project_quotas', fake_get_project_quotas) + def _stub_get_by_user(self): + def fake_qgabp(context, user_id, project_id): + self.calls.append('quota_get_all_by_user') + self.assertEqual(project_id, 'test_project') + self.assertEqual(user_id, 'fake_user') + return dict( + cores=10, + gigabytes=50, + injected_files=2, + injected_file_path_bytes=127, + ) + + def fake_qugabp(context, user_id, project_id): + self.calls.append('quota_usage_get_all_by_user') + self.assertEqual(project_id, 'test_project') + self.assertEqual(user_id, 'fake_user') + return dict( + instances=dict(in_use=2, reserved=2), + cores=dict(in_use=4, reserved=4), + ram=dict(in_use=10 * 1024, reserved=0), + volumes=dict(in_use=2, reserved=0), + gigabytes=dict(in_use=10, reserved=0), + floating_ips=dict(in_use=2, reserved=0), + metadata_items=dict(in_use=0, reserved=0), + injected_files=dict(in_use=0, reserved=0), + injected_file_content_bytes=dict(in_use=0, reserved=0), + injected_file_path_bytes=dict(in_use=0, reserved=0), + ) + + self.stubs.Set(db, 'quota_get_all_by_user', fake_qgabp) + self.stubs.Set(db, 'quota_usage_get_all_by_user', fake_qugabp) + + self._stub_quota_class_get_all_by_name() + + def test_get_user_quotas(self): + self._stub_get_by_user() + result = self.driver.get_user_quotas( + FakeContext('test_project', 'test_class'), + quota.QUOTAS._resources, 'fake_user', 'test_project') + + self.assertEqual(self.calls, [ + 'quota_get_all_by_user', + 'quota_usage_get_all_by_user', + 'quota_class_get_all_by_name', + ]) + self.assertEqual(result, dict( + instances=dict( + limit=5, + in_use=2, + reserved=2, + ), + cores=dict( + limit=10, + in_use=4, + reserved=4, + ), + ram=dict( + limit=25 * 1024, + in_use=10 * 1024, + reserved=0, + ), + volumes=dict( + limit=10, + in_use=2, + reserved=0, + ), + gigabytes=dict( + limit=50, + in_use=10, + reserved=0, + ), + floating_ips=dict( + limit=10, + in_use=2, + reserved=0, + ), + metadata_items=dict( + limit=64, + in_use=0, + reserved=0, + ), + injected_files=dict( + limit=2, + in_use=0, + reserved=0, + ), + injected_file_content_bytes=dict( + limit=5 * 1024, + in_use=0, + reserved=0, + ), + injected_file_path_bytes=dict( + limit=127, + in_use=0, + reserved=0, + ), + security_groups=dict( + limit=10, + in_use=0, + reserved=0, + ), + security_group_rules=dict( + limit=20, + in_use=0, + reserved=0, + ), + key_pairs=dict( + limit=100, + in_use=0, + reserved=0, + ), + )) + + def test_get_user_quotas_alt_context_no_class(self): + self._stub_get_by_user() + result = self.driver.get_user_quotas( + FakeContext('other_project', 'other_class'), + quota.QUOTAS._resources, 'fake_user', 'test_project') + + self.assertEqual(self.calls, [ + 'quota_get_all_by_user', + 'quota_usage_get_all_by_user', + ]) + self.assertEqual(result, dict( + instances=dict( + limit=10, + in_use=2, + reserved=2, + ), + cores=dict( + limit=10, + in_use=4, + reserved=4, + ), + ram=dict( + limit=50 * 1024, + in_use=10 * 1024, + reserved=0, + ), + volumes=dict( + limit=10, + in_use=2, + reserved=0, + ), + gigabytes=dict( + limit=50, + in_use=10, + reserved=0, + ), + floating_ips=dict( + limit=10, + in_use=2, + reserved=0, + ), + metadata_items=dict( + limit=128, + in_use=0, + reserved=0, + ), + injected_files=dict( + limit=2, + in_use=0, + reserved=0, + ), + injected_file_content_bytes=dict( + limit=10 * 1024, + in_use=0, + reserved=0, + ), + injected_file_path_bytes=dict( + limit=127, + in_use=0, + reserved=0, + ), + security_groups=dict( + limit=10, + in_use=0, + reserved=0, + ), + security_group_rules=dict( + limit=20, + in_use=0, + reserved=0, + ), + key_pairs=dict( + limit=100, + in_use=0, + reserved=0, + ), + )) + + def test_get_user_quotas_alt_context_with_class(self): + self._stub_get_by_user() + result = self.driver.get_user_quotas( + FakeContext('other_project', 'other_class'), + quota.QUOTAS._resources, 'fake_user', 'test_project', + quota_class='test_class') + + self.assertEqual(self.calls, [ + 'quota_get_all_by_user', + 'quota_usage_get_all_by_user', + 'quota_class_get_all_by_name', + ]) + self.assertEqual(result, dict( + instances=dict( + limit=5, + in_use=2, + reserved=2, + ), + cores=dict( + limit=10, + in_use=4, + reserved=4, + ), + ram=dict( + limit=25 * 1024, + in_use=10 * 1024, + reserved=0, + ), + volumes=dict( + limit=10, + in_use=2, + reserved=0, + ), + gigabytes=dict( + limit=50, + in_use=10, + reserved=0, + ), + floating_ips=dict( + limit=10, + in_use=2, + reserved=0, + ), + metadata_items=dict( + limit=64, + in_use=0, + reserved=0, + ), + injected_files=dict( + limit=2, + in_use=0, + reserved=0, + ), + injected_file_content_bytes=dict( + limit=5 * 1024, + in_use=0, + reserved=0, + ), + injected_file_path_bytes=dict( + limit=127, + in_use=0, + reserved=0, + ), + security_groups=dict( + limit=10, + in_use=0, + reserved=0, + ), + security_group_rules=dict( + limit=20, + in_use=0, + reserved=0, + ), + key_pairs=dict( + limit=100, + in_use=0, + reserved=0, + ), + )) + + def test_get_user_quotas_no_defaults(self): + self._stub_get_by_user() + result = self.driver.get_user_quotas( + FakeContext('test_project', 'test_class'), + quota.QUOTAS._resources, 'fake_user', 'test_project', + defaults=False) + + self.assertEqual(self.calls, [ + 'quota_get_all_by_user', + 'quota_usage_get_all_by_user', + 'quota_class_get_all_by_name', + ]) + self.assertEqual(result, dict( + cores=dict( + limit=10, + in_use=4, + reserved=4, + ), + gigabytes=dict( + limit=50, + in_use=10, + reserved=0, + ), + injected_files=dict( + limit=2, + in_use=0, + reserved=0, + ), + injected_file_path_bytes=dict( + limit=127, + in_use=0, + reserved=0, + ), + )) + + def test_get_user_quotas_no_usages(self): + self._stub_get_by_user() + result = self.driver.get_user_quotas( + FakeContext('test_project', 'test_class'), + quota.QUOTAS._resources, 'fake_user', 'test_project', + usages=False) + + self.assertEqual(self.calls, [ + 'quota_get_all_by_user', + 'quota_class_get_all_by_name', + ]) + self.assertEqual(result, dict( + instances=dict( + limit=5, + ), + cores=dict( + limit=10, + ), + ram=dict( + limit=25 * 1024, + ), + volumes=dict( + limit=10, + ), + gigabytes=dict( + limit=50, + ), + floating_ips=dict( + limit=10, + ), + metadata_items=dict( + limit=64, + ), + injected_files=dict( + limit=2, + ), + injected_file_content_bytes=dict( + limit=5 * 1024, + ), + injected_file_path_bytes=dict( + limit=127, + ), + security_groups=dict( + limit=10, + ), + security_group_rules=dict( + limit=20, + ), + key_pairs=dict( + limit=100, + ), + )) + + def _stub_get_user_quotas(self): + def fake_get_user_quotas(context, resources, user_id, project_id, + quota_class=None, defaults=True, + usages=True): + self.calls.append('get_user_quotas') + return dict((k, dict(limit=v.default)) + for k, v in resources.items()) + + self.stubs.Set(self.driver, 'get_user_quotas', + fake_get_user_quotas) + def test_get_quotas_has_sync_unknown(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() self.assertRaises(exception.QuotaResourceUnknown, self.driver._get_quotas, None, quota.QUOTAS._resources, @@ -1155,7 +1591,7 @@ class DbQuotaDriverTestCase(test.TestCase): self.assertEqual(self.calls, []) def test_get_quotas_no_sync_unknown(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() self.assertRaises(exception.QuotaResourceUnknown, self.driver._get_quotas, None, quota.QUOTAS._resources, @@ -1163,7 +1599,7 @@ class DbQuotaDriverTestCase(test.TestCase): self.assertEqual(self.calls, []) def test_get_quotas_has_sync_no_sync_resource(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() self.assertRaises(exception.QuotaResourceUnknown, self.driver._get_quotas, None, quota.QUOTAS._resources, @@ -1171,7 +1607,7 @@ class DbQuotaDriverTestCase(test.TestCase): self.assertEqual(self.calls, []) def test_get_quotas_no_sync_has_sync_resource(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() self.assertRaises(exception.QuotaResourceUnknown, self.driver._get_quotas, None, quota.QUOTAS._resources, @@ -1179,7 +1615,7 @@ class DbQuotaDriverTestCase(test.TestCase): self.assertEqual(self.calls, []) def test_get_quotas_has_sync(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() result = self.driver._get_quotas(FakeContext('test_project', 'test_class'), quota.QUOTAS._resources, @@ -1188,7 +1624,7 @@ class DbQuotaDriverTestCase(test.TestCase): 'floating_ips', 'security_groups'], True) - self.assertEqual(self.calls, ['get_project_quotas']) + self.assertEqual(self.calls, ['get_user_quotas']) self.assertEqual(result, dict( instances=10, cores=20, @@ -1200,7 +1636,7 @@ class DbQuotaDriverTestCase(test.TestCase): )) def test_get_quotas_no_sync(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() result = self.driver._get_quotas(FakeContext('test_project', 'test_class'), quota.QUOTAS._resources, @@ -1209,7 +1645,7 @@ class DbQuotaDriverTestCase(test.TestCase): 'injected_file_path_bytes', 'security_group_rules'], False) - self.assertEqual(self.calls, ['get_project_quotas']) + self.assertEqual(self.calls, ['get_user_quotas']) self.assertEqual(result, dict( metadata_items=128, injected_files=5, @@ -1219,7 +1655,7 @@ class DbQuotaDriverTestCase(test.TestCase): )) def test_limit_check_under(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() self.assertRaises(exception.InvalidQuotaValue, self.driver.limit_check, FakeContext('test_project', 'test_class'), @@ -1227,7 +1663,7 @@ class DbQuotaDriverTestCase(test.TestCase): dict(metadata_items=-1)) def test_limit_check_over(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() self.assertRaises(exception.OverQuota, self.driver.limit_check, FakeContext('test_project', 'test_class'), @@ -1236,13 +1672,13 @@ class DbQuotaDriverTestCase(test.TestCase): def test_limit_check_unlimited(self): self.flags(quota_metadata_items=-1) - self._stub_get_project_quotas() + self._stub_get_user_quotas() self.driver.limit_check(FakeContext('test_project', 'test_class'), quota.QUOTAS._resources, dict(metadata_items=32767)) def test_limit_check(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() self.driver.limit_check(FakeContext('test_project', 'test_class'), quota.QUOTAS._resources, dict(metadata_items=128)) @@ -1256,7 +1692,7 @@ class DbQuotaDriverTestCase(test.TestCase): self.stubs.Set(db, 'quota_reserve', fake_quota_reserve) def test_reserve_bad_expire(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() self._stub_quota_reserve() self.assertRaises(exception.InvalidReservationExpiration, self.driver.reserve, @@ -1266,7 +1702,7 @@ class DbQuotaDriverTestCase(test.TestCase): self.assertEqual(self.calls, []) def test_reserve_default_expire(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() self._stub_quota_reserve() result = self.driver.reserve(FakeContext('test_project', 'test_class'), quota.QUOTAS._resources, @@ -1274,13 +1710,13 @@ class DbQuotaDriverTestCase(test.TestCase): expire = timeutils.utcnow() + datetime.timedelta(seconds=86400) self.assertEqual(self.calls, [ - 'get_project_quotas', + 'get_user_quotas', ('quota_reserve', expire, 0, 0), ]) self.assertEqual(result, ['resv-1', 'resv-2', 'resv-3']) def test_reserve_int_expire(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() self._stub_quota_reserve() result = self.driver.reserve(FakeContext('test_project', 'test_class'), quota.QUOTAS._resources, @@ -1288,13 +1724,13 @@ class DbQuotaDriverTestCase(test.TestCase): expire = timeutils.utcnow() + datetime.timedelta(seconds=3600) self.assertEqual(self.calls, [ - 'get_project_quotas', + 'get_user_quotas', ('quota_reserve', expire, 0, 0), ]) self.assertEqual(result, ['resv-1', 'resv-2', 'resv-3']) def test_reserve_timedelta_expire(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() self._stub_quota_reserve() expire_delta = datetime.timedelta(seconds=60) result = self.driver.reserve(FakeContext('test_project', 'test_class'), @@ -1303,13 +1739,13 @@ class DbQuotaDriverTestCase(test.TestCase): expire = timeutils.utcnow() + expire_delta self.assertEqual(self.calls, [ - 'get_project_quotas', + 'get_user_quotas', ('quota_reserve', expire, 0, 0), ]) self.assertEqual(result, ['resv-1', 'resv-2', 'resv-3']) def test_reserve_datetime_expire(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() self._stub_quota_reserve() expire = timeutils.utcnow() + datetime.timedelta(seconds=120) result = self.driver.reserve(FakeContext('test_project', 'test_class'), @@ -1317,13 +1753,13 @@ class DbQuotaDriverTestCase(test.TestCase): dict(instances=2), expire=expire) self.assertEqual(self.calls, [ - 'get_project_quotas', + 'get_user_quotas', ('quota_reserve', expire, 0, 0), ]) self.assertEqual(result, ['resv-1', 'resv-2', 'resv-3']) def test_reserve_until_refresh(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() self._stub_quota_reserve() self.flags(until_refresh=500) expire = timeutils.utcnow() + datetime.timedelta(seconds=120) @@ -1332,13 +1768,13 @@ class DbQuotaDriverTestCase(test.TestCase): dict(instances=2), expire=expire) self.assertEqual(self.calls, [ - 'get_project_quotas', + 'get_user_quotas', ('quota_reserve', expire, 500, 0), ]) self.assertEqual(result, ['resv-1', 'resv-2', 'resv-3']) def test_reserve_max_age(self): - self._stub_get_project_quotas() + self._stub_get_user_quotas() self._stub_quota_reserve() self.flags(max_age=86400) expire = timeutils.utcnow() + datetime.timedelta(seconds=120) @@ -1347,7 +1783,7 @@ class DbQuotaDriverTestCase(test.TestCase): dict(instances=2), expire=expire) self.assertEqual(self.calls, [ - 'get_project_quotas', + 'get_user_quotas', ('quota_reserve', expire, 0, 86400), ]) self.assertEqual(result, ['resv-1', 'resv-2', 'resv-3']) @@ -1380,7 +1816,7 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.sync_called = set() def make_sync(res_name): - def sync(context, project_id, session): + def sync(context, user_id, project_id, session): self.sync_called.add(res_name) if res_name in self.usages: if self.usages[res_name].in_use < 0: @@ -1407,21 +1843,22 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): def fake_get_quota_usages(context, session): return self.usages.copy() - def fake_quota_usage_create(context, project_id, resource, in_use, - reserved, until_refresh, session=None, - save=True): + def fake_quota_usage_create(context, user_id, project_id, resource, + in_use, reserved, until_refresh, + session=None, save=True): quota_usage_ref = self._make_quota_usage( - project_id, resource, in_use, reserved, until_refresh, - timeutils.utcnow(), timeutils.utcnow()) + user_id, project_id, resource, in_use, reserved, + until_refresh, timeutils.utcnow(), timeutils.utcnow()) self.usages_created[resource] = quota_usage_ref return quota_usage_ref - def fake_reservation_create(context, uuid, usage_id, project_id, - resource, delta, expire, session=None): + def fake_reservation_create(context, uuid, usage_id, user_id, + project_id, resource, delta, expire, + session=None): reservation_ref = self._make_reservation( - uuid, usage_id, project_id, resource, delta, expire, + uuid, usage_id, user_id, project_id, resource, delta, expire, timeutils.utcnow(), timeutils.utcnow()) self.reservations_created[resource] = reservation_ref @@ -1435,10 +1872,11 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): timeutils.set_time_override() - def _make_quota_usage(self, project_id, resource, in_use, reserved, - until_refresh, created_at, updated_at): + def _make_quota_usage(self, user_id, project_id, resource, in_use, + reserved, until_refresh, created_at, updated_at): quota_usage_ref = FakeUsage() quota_usage_ref.id = len(self.usages) + len(self.usages_created) + quota_usage_ref.user_id = user_id quota_usage_ref.project_id = project_id quota_usage_ref.resource = resource quota_usage_ref.in_use = in_use @@ -1451,14 +1889,15 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): return quota_usage_ref - def init_usage(self, project_id, resource, in_use, reserved, + def init_usage(self, user_id, project_id, resource, in_use, reserved, until_refresh=None, created_at=None, updated_at=None): if created_at is None: created_at = timeutils.utcnow() if updated_at is None: updated_at = timeutils.utcnow() - quota_usage_ref = self._make_quota_usage(project_id, resource, in_use, + quota_usage_ref = self._make_quota_usage(user_id, project_id, + resource, in_use, reserved, until_refresh, created_at, updated_at) @@ -1473,12 +1912,13 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): "%s != %s on usage for resource %s" % (actual, value, resource)) - def _make_reservation(self, uuid, usage_id, project_id, resource, + def _make_reservation(self, uuid, usage_id, user_id, project_id, resource, delta, expire, created_at, updated_at): reservation_ref = sqa_models.Reservation() reservation_ref.id = len(self.reservations_created) reservation_ref.uuid = uuid reservation_ref.usage_id = usage_id + reservation_ref.user_id = user_id reservation_ref.project_id = project_id reservation_ref.resource = resource reservation_ref.delta = delta @@ -1525,16 +1965,19 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.assertEqual(self.sync_called, set(['instances', 'cores', 'ram'])) self.compare_usage(self.usages_created, [ dict(resource='instances', + user_id='fake_user', project_id='test_project', in_use=0, reserved=2, until_refresh=None), dict(resource='cores', + user_id='fake_user', project_id='test_project', in_use=0, reserved=4, until_refresh=None), dict(resource='ram', + user_id='fake_user', project_id='test_project', in_use=0, reserved=2 * 1024, @@ -1543,10 +1986,12 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.compare_reservation(result, [ dict(resource='instances', usage_id=self.usages_created['instances'], + user_id='fake_user', project_id='test_project', delta=2), dict(resource='cores', usage_id=self.usages_created['cores'], + user_id='fake_user', project_id='test_project', delta=4), dict(resource='ram', @@ -1555,9 +2000,12 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): ]) def test_quota_reserve_negative_in_use(self): - self.init_usage('test_project', 'instances', -1, 0, until_refresh=1) - self.init_usage('test_project', 'cores', -1, 0, until_refresh=1) - self.init_usage('test_project', 'ram', -1, 0, until_refresh=1) + self.init_usage('fake_user', 'test_project', 'instances', -1, 0, + until_refresh=1) + self.init_usage('fake_user', 'test_project', 'cores', -1, 0, + until_refresh=1) + self.init_usage('fake_user', 'test_project', 'ram', -1, 0, + until_refresh=1) context = FakeContext('test_project', 'test_class') quotas = dict( instances=5, @@ -1575,16 +2023,19 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.assertEqual(self.sync_called, set(['instances', 'cores', 'ram'])) self.compare_usage(self.usages, [ dict(resource='instances', + user_id='fake_user', project_id='test_project', in_use=2, reserved=2, until_refresh=5), dict(resource='cores', + user_id='fake_user', project_id='test_project', in_use=2, reserved=4, until_refresh=5), dict(resource='ram', + user_id='fake_user', project_id='test_project', in_use=2, reserved=2 * 1024, @@ -1594,10 +2045,12 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.compare_reservation(result, [ dict(resource='instances', usage_id=self.usages['instances'], + user_id='fake_user', project_id='test_project', delta=2), dict(resource='cores', usage_id=self.usages['cores'], + user_id='fake_user', project_id='test_project', delta=4), dict(resource='ram', @@ -1606,9 +2059,12 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): ]) def test_quota_reserve_until_refresh(self): - self.init_usage('test_project', 'instances', 3, 0, until_refresh=1) - self.init_usage('test_project', 'cores', 3, 0, until_refresh=1) - self.init_usage('test_project', 'ram', 3, 0, until_refresh=1) + self.init_usage('fake_user', 'test_project', 'instances', 3, 0, + until_refresh=1) + self.init_usage('fake_user', 'test_project', 'cores', 3, 0, + until_refresh=1) + self.init_usage('fake_user', 'test_project', 'ram', 3, 0, + until_refresh=1) context = FakeContext('test_project', 'test_class') quotas = dict( instances=5, @@ -1626,16 +2082,19 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.assertEqual(self.sync_called, set(['instances', 'cores', 'ram'])) self.compare_usage(self.usages, [ dict(resource='instances', + user_id='fake_user', project_id='test_project', in_use=2, reserved=2, until_refresh=5), dict(resource='cores', + user_id='fake_user', project_id='test_project', in_use=2, reserved=4, until_refresh=5), dict(resource='ram', + user_id='fake_user', project_id='test_project', in_use=2, reserved=2 * 1024, @@ -1645,10 +2104,12 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.compare_reservation(result, [ dict(resource='instances', usage_id=self.usages['instances'], + user_id='fake_user', project_id='test_project', delta=2), dict(resource='cores', usage_id=self.usages['cores'], + user_id='fake_user', project_id='test_project', delta=4), dict(resource='ram', @@ -1660,11 +2121,11 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): max_age = 3600 record_created = (timeutils.utcnow() - datetime.timedelta(seconds=max_age)) - self.init_usage('test_project', 'instances', 3, 0, + self.init_usage('fake_user', 'test_project', 'instances', 3, 0, created_at=record_created, updated_at=record_created) - self.init_usage('test_project', 'cores', 3, 0, + self.init_usage('fake_user', 'test_project', 'cores', 3, 0, created_at=record_created, updated_at=record_created) - self.init_usage('test_project', 'ram', 3, 0, + self.init_usage('fake_user', 'test_project', 'ram', 3, 0, created_at=record_created, updated_at=record_created) context = FakeContext('test_project', 'test_class') quotas = dict( @@ -1683,16 +2144,19 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.assertEqual(self.sync_called, set(['instances', 'cores', 'ram'])) self.compare_usage(self.usages, [ dict(resource='instances', + user_id='fake_user', project_id='test_project', in_use=2, reserved=2, until_refresh=None), dict(resource='cores', + user_id='fake_user', project_id='test_project', in_use=2, reserved=4, until_refresh=None), dict(resource='ram', + user_id='fake_user', project_id='test_project', in_use=2, reserved=2 * 1024, @@ -1702,10 +2166,12 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.compare_reservation(result, [ dict(resource='instances', usage_id=self.usages['instances'], + user_id='fake_user', project_id='test_project', delta=2), dict(resource='cores', usage_id=self.usages['cores'], + user_id='fake_user', project_id='test_project', delta=4), dict(resource='ram', @@ -1714,9 +2180,9 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): ]) def test_quota_reserve_no_refresh(self): - self.init_usage('test_project', 'instances', 3, 0) - self.init_usage('test_project', 'cores', 3, 0) - self.init_usage('test_project', 'ram', 3, 0) + self.init_usage('fake_user', 'test_project', 'instances', 3, 0) + self.init_usage('fake_user', 'test_project', 'cores', 3, 0) + self.init_usage('fake_user', 'test_project', 'ram', 3, 0) context = FakeContext('test_project', 'test_class') quotas = dict( instances=5, @@ -1734,16 +2200,19 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.assertEqual(self.sync_called, set([])) self.compare_usage(self.usages, [ dict(resource='instances', + user_id='fake_user', project_id='test_project', in_use=3, reserved=2, until_refresh=None), dict(resource='cores', + user_id='fake_user', project_id='test_project', in_use=3, reserved=4, until_refresh=None), dict(resource='ram', + user_id='fake_user', project_id='test_project', in_use=3, reserved=2 * 1024, @@ -1753,10 +2222,12 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.compare_reservation(result, [ dict(resource='instances', usage_id=self.usages['instances'], + user_id='fake_user', project_id='test_project', delta=2), dict(resource='cores', usage_id=self.usages['cores'], + user_id='fake_user', project_id='test_project', delta=4), dict(resource='ram', @@ -1765,9 +2236,9 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): ]) def test_quota_reserve_unders(self): - self.init_usage('test_project', 'instances', 1, 0) - self.init_usage('test_project', 'cores', 3, 0) - self.init_usage('test_project', 'ram', 1 * 1024, 0) + self.init_usage('fake_user', 'test_project', 'instances', 1, 0) + self.init_usage('fake_user', 'test_project', 'cores', 3, 0) + self.init_usage('fake_user', 'test_project', 'ram', 1 * 1024, 0) context = FakeContext('test_project', 'test_class') quotas = dict( instances=5, @@ -1785,16 +2256,19 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.assertEqual(self.sync_called, set([])) self.compare_usage(self.usages, [ dict(resource='instances', + user_id='fake_user', project_id='test_project', in_use=1, reserved=0, until_refresh=None), dict(resource='cores', + user_id='fake_user', project_id='test_project', in_use=3, reserved=0, until_refresh=None), dict(resource='ram', + user_id='fake_user', project_id='test_project', in_use=1 * 1024, reserved=0, @@ -1804,10 +2278,12 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.compare_reservation(result, [ dict(resource='instances', usage_id=self.usages['instances'], + user_id='fake_user', project_id='test_project', delta=-2), dict(resource='cores', usage_id=self.usages['cores'], + user_id='fake_user', project_id='test_project', delta=-4), dict(resource='ram', @@ -1816,9 +2292,9 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): ]) def test_quota_reserve_overs(self): - self.init_usage('test_project', 'instances', 4, 0) - self.init_usage('test_project', 'cores', 8, 0) - self.init_usage('test_project', 'ram', 10 * 1024, 0) + self.init_usage('fake_user', 'test_project', 'instances', 4, 0) + self.init_usage('fake_user', 'test_project', 'cores', 8, 0) + self.init_usage('fake_user', 'test_project', 'ram', 10 * 1024, 0) context = FakeContext('test_project', 'test_class') quotas = dict( instances=5, @@ -1838,16 +2314,19 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.assertEqual(self.sync_called, set([])) self.compare_usage(self.usages, [ dict(resource='instances', + user_id='fake_user', project_id='test_project', in_use=4, reserved=0, until_refresh=None), dict(resource='cores', + user_id='fake_user', project_id='test_project', in_use=8, reserved=0, until_refresh=None), dict(resource='ram', + user_id='fake_user', project_id='test_project', in_use=10 * 1024, reserved=0, @@ -1857,9 +2336,9 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.assertEqual(self.reservations_created, {}) def test_quota_reserve_reduction(self): - self.init_usage('test_project', 'instances', 10, 0) - self.init_usage('test_project', 'cores', 20, 0) - self.init_usage('test_project', 'ram', 20 * 1024, 0) + self.init_usage('fake_user', 'test_project', 'instances', 10, 0) + self.init_usage('fake_user', 'test_project', 'cores', 20, 0) + self.init_usage('fake_user', 'test_project', 'ram', 20 * 1024, 0) context = FakeContext('test_project', 'test_class') quotas = dict( instances=5, @@ -1877,16 +2356,19 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.assertEqual(self.sync_called, set([])) self.compare_usage(self.usages, [ dict(resource='instances', + user_id='fake_user', project_id='test_project', in_use=10, reserved=0, until_refresh=None), dict(resource='cores', + user_id='fake_user', project_id='test_project', in_use=20, reserved=0, until_refresh=None), dict(resource='ram', + user_id='fake_user', project_id='test_project', in_use=20 * 1024, reserved=0, @@ -1896,14 +2378,17 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.compare_reservation(result, [ dict(resource='instances', usage_id=self.usages['instances'], + user_id='fake_user', project_id='test_project', delta=-2), dict(resource='cores', usage_id=self.usages['cores'], + user_id='fake_user', project_id='test_project', delta=-4), dict(resource='ram', usage_id=self.usages['ram'], + user_id='fake_user', project_id='test_project', delta=-2 * 1024), ]) |