Per-project-user-quotas for more granularity

Implements blueprint per-user-quotas.

Fixes bug 968175

Based on the original quotas structure.

NOTE:
quota_instances, quota_cores, quota_ram, quota_key_pairs and
quota_security_groups are supported per user.

Add CRUD methods for project user quotas API. DocImpact
 - Shows quotas for a user.
   GET v2/{tenant_id}/os-quota-sets/{tenant_id}?user_id={user_id}
 - Updates quotas for a user.
   POST v2/{tenant_id}/os-quota-sets/{tenant_id}?user_id={user_id}

Add commands for project user quotas management.
 - Show user quotas:
   nova-manage project quota --project <Project name> --user <User name>
 - Update/Create user quotas:
   nova-manage project quota --project <Project name> --user <User name>
   --key <key> --value <value>

Change-Id: I24af1f6bc439d5d740303c6fe176a9bffe754579
This commit is contained in:
liyingjun 2013-04-29 16:39:22 +08:00
parent abb7527633
commit 77b4012a02
40 changed files with 2123 additions and 353 deletions

View File

@ -448,6 +448,14 @@
"namespace": "http://docs.openstack.org/compute/ext/quotas-sets/api/v1.1",
"updated": "2011-08-08T00:00:00+00:00"
},
{
"alias": "os-user-quotas",
"description": "Project user quota support.",
"links": [],
"name": "UserQuotas",
"namespace": "http://docs.openstack.org/compute/ext/user_quotas/api/v1.1",
"updated": "2013-07-18T00:00:00+00:00"
},
{
"alias": "os-rescue",
"description": "Instance rescue mode.",

View File

@ -186,6 +186,9 @@
<extension alias="os-quota-sets" updated="2011-08-08T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/quotas-sets/api/v1.1" name="Quotas">
<description>Quotas management support.</description>
</extension>
<extension alias="os-user-quotas" updated="2013-07-18T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/user_quotas/api/v1.1" name="UserQuotas">
<description>Project user quota support.</description>
</extension>
<extension alias="os-rescue" updated="2011-08-18T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/rescue/api/v1.1" name="Rescue">
<description>Instance rescue mode.</description>
</extension>

View File

@ -0,0 +1,17 @@
{
"quota_set": {
"cores": 20,
"fixed_ips": -1,
"floating_ips": 10,
"id": "fake_tenant",
"injected_file_content_bytes": 10240,
"injected_file_path_bytes": 255,
"injected_files": 5,
"instances": 10,
"key_pairs": 100,
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 10
}
}

View File

@ -0,0 +1,15 @@
<?xml version='1.0' encoding='UTF-8'?>
<quota_set id="fake_tenant">
<cores>20</cores>
<fixed_ips>-1</fixed_ips>
<floating_ips>10</floating_ips>
<injected_file_content_bytes>10240</injected_file_content_bytes>
<injected_file_path_bytes>255</injected_file_path_bytes>
<injected_files>5</injected_files>
<instances>10</instances>
<key_pairs>100</key_pairs>
<metadata_items>128</metadata_items>
<ram>51200</ram>
<security_group_rules>20</security_group_rules>
<security_groups>10</security_groups>
</quota_set>

View File

@ -0,0 +1,6 @@
{
"quota_set": {
"force": "True",
"instances": 9
}
}

View File

@ -0,0 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
<quota_set id="fake_tenant">
<force>True</force>
<instances>9</instances>
</quota_set>

View File

@ -0,0 +1,16 @@
{
"quota_set": {
"cores": 20,
"floating_ips": 10,
"fixed_ips": -1,
"injected_file_content_bytes": 10240,
"injected_file_path_bytes": 255,
"injected_files": 5,
"instances": 9,
"key_pairs": 100,
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 10
}
}

View File

@ -0,0 +1,15 @@
<?xml version='1.0' encoding='UTF-8'?>
<quota_set>
<cores>20</cores>
<floating_ips>10</floating_ips>
<fixed_ips>-1</fixed_ips>
<injected_file_content_bytes>10240</injected_file_content_bytes>
<injected_file_path_bytes>255</injected_file_path_bytes>
<injected_files>5</injected_files>
<instances>9</instances>
<key_pairs>100</key_pairs>
<metadata_items>128</metadata_items>
<ram>51200</ram>
<security_group_rules>20</security_group_rules>
<security_groups>10</security_groups>
</quota_set>

View File

@ -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
@ -65,14 +66,25 @@ class QuotaSetsController(object):
return dict(quota_set=result)
def _validate_quota_limit(self, limit):
def _validate_quota_limit(self, limit, minimum, maximum):
# 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)
if ((limit < minimum) and
(maximum != -1 or (maximum == -1 and limit != -1))):
msg = _("Quota limit must greater than %s.") % minimum
raise webob.exc.HTTPBadRequest(explanation=msg)
if maximum != -1 and limit > maximum:
msg = _("Quota limit must less than %s.") % maximum
raise webob.exc.HTTPBadRequest(explanation=msg)
def _get_quotas(self, context, id, usages=False):
values = QUOTAS.get_project_quotas(context, id, usages=usages)
def _get_quotas(self, context, id, user_id=None, usages=False):
if user_id:
values = QUOTAS.get_user_quotas(context, id, user_id,
usages=usages)
else:
values = QUOTAS.get_project_quotas(context, id, usages=usages)
if usages:
return values
@ -83,9 +95,14 @@ class QuotaSetsController(object):
def show(self, req, id):
context = req.environ['nova.context']
authorize_show(context)
params = urlparse.parse_qs(req.environ.get('QUERY_STRING', ''))
user_id = None
if self.ext_mgr.is_loaded('os-user-quotas'):
user_id = params.get('user_id', [None])[0]
try:
nova.context.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=user_id))
except exception.NotAuthorized:
raise webob.exc.HTTPForbidden()
@ -107,6 +124,18 @@ class QuotaSetsController(object):
extended_loaded = True
force_update = False
user_id = None
if self.ext_mgr.is_loaded('os-user-quotas'):
# Update user quotas only if the extended is loaded
params = urlparse.parse_qs(req.environ.get('QUERY_STRING', ''))
user_id = params.get('user_id', [None])[0]
try:
settable_quotas = QUOTAS.get_settable_quotas(context, project_id,
user_id=user_id)
except exception.NotAuthorized:
raise webob.exc.HTTPForbidden()
for key, value in body['quota_set'].items():
if (key not in QUOTAS and
key not in NON_QUOTA_KEYS):
@ -124,7 +153,6 @@ class QuotaSetsController(object):
"integer.") % {'value': value, 'key': key}
LOG.warn(msg)
raise webob.exc.HTTPBadRequest(explanation=msg)
self._validate_quota_limit(value)
LOG.debug(_("force update quotas: %s") % force_update)
@ -133,7 +161,8 @@ class QuotaSetsController(object):
raise webob.exc.HTTPBadRequest(explanation=msg)
try:
project_quota = self._get_quotas(context, id, True)
quotas = self._get_quotas(context, id, user_id=user_id,
usages=True)
except exception.NotAuthorized:
raise webob.exc.HTTPForbidden()
@ -145,7 +174,7 @@ class QuotaSetsController(object):
# update
value = int(value)
if force_update is not True and value >= 0:
quota_value = project_quota.get(key)
quota_value = quotas.get(key)
if quota_value and quota_value['limit'] >= 0:
quota_used = (quota_value['in_use'] +
quota_value['reserved'])
@ -161,13 +190,18 @@ class QuotaSetsController(object):
'quota_used': quota_used})
raise webob.exc.HTTPBadRequest(explanation=msg)
minimum = settable_quotas[key]['minimum']
maximum = settable_quotas[key]['maximum']
self._validate_quota_limit(value, minimum, maximum)
try:
db.quota_create(context, project_id, key, value)
db.quota_create(context, project_id, key, value,
user_id=user_id)
except exception.QuotaExists:
db.quota_update(context, project_id, key, value)
db.quota_update(context, project_id, key, value,
user_id=user_id)
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=user_id)}
@wsgi.serializers(xml=QuotaTemplate)
def defaults(self, req, id):
@ -179,9 +213,17 @@ class QuotaSetsController(object):
if self.ext_mgr.is_loaded('os-extended-quotas'):
context = req.environ['nova.context']
authorize_delete(context)
params = urlparse.parse_qs(req.environ.get('QUERY_STRING', ''))
user_id = params.get('user_id', [None])[0]
if user_id and not self.ext_mgr.is_loaded('os-user-quotas'):
raise webob.exc.HTTPNotFound()
try:
nova.context.authorize_project_context(context, id)
QUOTAS.destroy_all_by_project(context, id)
if user_id:
QUOTAS.destroy_all_by_project_and_user(context,
id, user_id)
else:
QUOTAS.destroy_all_by_project(context, id)
return webob.Response(status_int=202)
except exception.NotAuthorized:
raise webob.exc.HTTPForbidden()

View File

@ -0,0 +1,27 @@
# Copyright 2013 OpenStack Foundation
# Author: Yingjun Li <liyingjun1988@gmail.com>
# All Rights Reserved.
#
# 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.api.openstack import extensions
class User_quotas(extensions.ExtensionDescriptor):
"""Project user quota support."""
name = "UserQuotas"
alias = "os-user-quotas"
namespace = ("http://docs.openstack.org/compute/ext/user_quotas"
"/api/v1.1")
updated = "2013-07-18T00:00:00+00:00"

View File

@ -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
@ -66,14 +67,25 @@ class QuotaSetsController(object):
return dict(quota_set=result)
def _validate_quota_limit(self, limit):
def _validate_quota_limit(self, limit, minimum, maximum):
# 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)
if ((limit < minimum) and
(maximum != -1 or (maximum == -1 and limit != -1))):
msg = _("Quota limit must greater than %s.") % minimum
raise webob.exc.HTTPBadRequest(explanation=msg)
if maximum != -1 and limit > maximum:
msg = _("Quota limit must less than %s.") % maximum
raise webob.exc.HTTPBadRequest(explanation=msg)
def _get_quotas(self, context, id, usages=False):
values = QUOTAS.get_project_quotas(context, id, usages=usages)
def _get_quotas(self, context, id, user_id=None, usages=False):
if user_id:
values = QUOTAS.get_user_quotas(context, id, user_id,
usages=usages)
else:
values = QUOTAS.get_project_quotas(context, id, usages=usages)
if usages:
return values
@ -84,9 +96,12 @@ class QuotaSetsController(object):
def show(self, req, id):
context = req.environ['nova.context']
authorize_show(context)
params = urlparse.parse_qs(req.environ.get('QUERY_STRING', ''))
user_id = params.get('user_id', [None])[0]
try:
nova.context.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=user_id))
except exception.NotAuthorized:
raise webob.exc.HTTPForbidden()
@ -95,6 +110,8 @@ class QuotaSetsController(object):
context = req.environ['nova.context']
authorize_update(context)
project_id = id
params = urlparse.parse_qs(req.environ.get('QUERY_STRING', ''))
user_id = params.get('user_id', [None])[0]
bad_keys = []
force_update = False
@ -114,7 +131,6 @@ class QuotaSetsController(object):
"integer.") % {'value': value, 'key': key}
LOG.warn(msg)
raise webob.exc.HTTPBadRequest(explanation=msg)
self._validate_quota_limit(value)
LOG.debug(_("force update quotas: %s") % force_update)
@ -123,7 +139,14 @@ class QuotaSetsController(object):
raise webob.exc.HTTPBadRequest(explanation=msg)
try:
project_quota = self._get_quotas(context, id, True)
settable_quotas = QUOTAS.get_settable_quotas(context, project_id,
user_id=user_id)
except exception.NotAuthorized:
raise webob.exc.HTTPForbidden()
try:
quotas = self._get_quotas(context, id, user_id=user_id,
usages=True)
except exception.NotAuthorized:
raise webob.exc.HTTPForbidden()
@ -135,7 +158,7 @@ class QuotaSetsController(object):
# update
value = int(value)
if force_update is not True and value >= 0:
quota_value = project_quota.get(key)
quota_value = quotas.get(key)
if quota_value and quota_value['limit'] >= 0:
quota_used = (quota_value['in_use'] +
quota_value['reserved'])
@ -151,13 +174,18 @@ class QuotaSetsController(object):
'quota_used': quota_used})
raise webob.exc.HTTPBadRequest(explanation=msg)
minimum = settable_quotas[key]['minimum']
maximum = settable_quotas[key]['maximum']
self._validate_quota_limit(value, minimum, maximum)
try:
db.quota_update(context, project_id, key, value)
except exception.ProjectQuotaNotFound:
db.quota_create(context, project_id, key, value)
db.quota_create(context, project_id, key, value,
user_id=user_id)
except exception.QuotaExists:
db.quota_update(context, project_id, key, value,
user_id=user_id)
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=user_id)}
@wsgi.serializers(xml=QuotaTemplate)
def defaults(self, req, id):
@ -168,9 +196,15 @@ class QuotaSetsController(object):
def delete(self, req, id):
context = req.environ['nova.context']
authorize_delete(context)
params = urlparse.parse_qs(req.environ.get('QUERY_STRING', ''))
user_id = params.get('user_id', [None])[0]
try:
nova.context.authorize_project_context(context, id)
QUOTAS.destroy_all_by_project(context, id)
if user_id:
QUOTAS.destroy_all_by_project_and_user(context,
id, user_id)
else:
QUOTAS.destroy_all_by_project(context, id)
return webob.Response(status_int=202)
except exception.NotAuthorized:
raise webob.exc.HTTPForbidden()

View File

@ -215,11 +215,13 @@ class ProjectCommands(object):
@args('--project', dest='project_id', metavar='<Project name>',
help='Project name')
@args('--user', dest='user_id', metavar='<User name>',
help='User name')
@args('--key', metavar='<key>', help='Key')
@args('--value', metavar='<value>', help='Value')
def quota(self, project_id, key=None, value=None):
def quota(self, project_id, user_id=None, key=None, value=None):
"""
Create, update or display quotas for project
Create, update or display quotas for project/user
If no quota key is provided, the quota will be displayed.
If a valid quota key is provided and it does not exist,
@ -227,21 +229,42 @@ class ProjectCommands(object):
"""
ctxt = context.get_admin_context()
project_quota = QUOTAS.get_project_quotas(ctxt, project_id)
if user_id:
quota = QUOTAS.get_user_quotas(ctxt, project_id, user_id)
else:
user_id = None
quota = QUOTAS.get_project_quotas(ctxt, project_id)
# if key is None, that means we need to show the quotas instead
# of updating them
if key:
if key in project_quota:
settable_quotas = QUOTAS.get_settable_quotas(ctxt,
project_id,
user_id=user_id)
if key in quota:
minimum = settable_quotas[key]['minimum']
maximum = settable_quotas[key]['maximum']
if value.lower() == 'unlimited':
value = -1
if int(value) < -1:
print _('Quota limit must be -1 or greater.')
return(2)
if ((int(value) < minimum) and
(maximum != -1 or (maximum == -1 and int(value) != -1))):
print _('Quota limit must greater than %s.') % minimum
return(2)
if maximum != -1 and int(value) > maximum:
print _('Quota limit must less than %s.') % maximum
return(2)
try:
db.quota_create(ctxt, project_id, key, value)
db.quota_create(ctxt, project_id, key, value,
user_id=user_id)
except exception.QuotaExists:
db.quota_update(ctxt, project_id, key, value)
db.quota_update(ctxt, project_id, key, value,
user_id=user_id)
else:
print _('%(key)s is not a valid quota key. Valid options are: '
'%(options)s.') % {'key': key,
'options': ', '.join(project_quota)}
'options': ', '.join(quota)}
return(2)
print_format = "%-36s %-10s %-10s %-10s"
print print_format % (
@ -250,8 +273,11 @@ class ProjectCommands(object):
_('In Use'),
_('Reserved'))
# Retrieve the quota after update
project_quota = QUOTAS.get_project_quotas(ctxt, project_id)
for key, value in project_quota.iteritems():
if user_id:
quota = QUOTAS.get_user_quotas(ctxt, project_id, user_id)
else:
quota = QUOTAS.get_project_quotas(ctxt, project_id)
for key, value in quota.iteritems():
if value['limit'] < 0 or value['limit'] is None:
value['limit'] = 'unlimited'
print print_format % (key, value['limit'], value['in_use'],

View File

@ -1164,6 +1164,10 @@ class API(base.Base):
project_id = instance['project_id']
else:
project_id = context.project_id
if context.user_id != instance['user_id']:
user_id = instance['user_id']
else:
user_id = context.user_id
try:
# NOTE(maoy): no expected_task_state needs to be set
@ -1179,7 +1183,8 @@ class API(base.Base):
reservations = self._create_reservations(context,
old,
updated,
project_id)
project_id,
user_id)
if not host:
# Just update database, nothing else we can do
@ -1190,7 +1195,8 @@ class API(base.Base):
if reservations:
QUOTAS.commit(context,
reservations,
project_id=project_id)
project_id=project_id,
user_id=user_id)
return
except exception.ConstraintNotMet:
# Refresh to get new host information
@ -1248,23 +1254,27 @@ class API(base.Base):
if reservations:
QUOTAS.commit(context,
reservations,
project_id=project_id)
project_id=project_id,
user_id=user_id)
reservations = None
except exception.InstanceNotFound:
# NOTE(comstud): Race condition. Instance already gone.
if reservations:
QUOTAS.rollback(context,
reservations,
project_id=project_id)
project_id=project_id,
user_id=user_id)
except Exception:
with excutils.save_and_reraise_exception():
if reservations:
QUOTAS.rollback(context,
reservations,
project_id=project_id)
project_id=project_id,
user_id=user_id)
def _create_reservations(self, context, old_instance, new_instance,
project_id):
project_id,
user_id):
instance_vcpus = old_instance['vcpus']
instance_memory_mb = old_instance['memory_mb']
# NOTE(wangpan): if the instance is resizing, and the resources
@ -1296,6 +1306,7 @@ class API(base.Base):
reservations = QUOTAS.reserve(context,
project_id=project_id,
user_id=user_id,
instances=-1,
cores=-instance_vcpus,
ram=-instance_memory_mb)

View File

@ -543,12 +543,18 @@ class ComputeManager(manager.SchedulerDependentManager):
"""
self.conductor_api.instance_destroy(context, instance)
project_id = instance['project_id']
if (instance.get('user_id', None) and
(context.user_id != instance['user_id'])):
user_id = instance['user_id']
else:
user_id = context.user_id
system_meta = utils.metadata_to_dict(instance['system_metadata'])
bdms = self._get_instance_volume_bdms(context, instance)
instance_vcpus = instance['vcpus']
instance_memory_mb = instance['memory_mb']
reservations = quota.QUOTAS.reserve(context,
project_id=project_id,
user_id=user_id,
instances=-1,
cores=-instance_vcpus,
ram=-instance_memory_mb)
@ -557,12 +563,14 @@ class ComputeManager(manager.SchedulerDependentManager):
bdms,
reservations,
project_id,
system_meta)
system_meta,
user_id=user_id)
def _complete_deletion(self, context, instance, bdms,
reservations, prj_id, system_meta):
reservations, prj_id, system_meta, user_id=None):
self._quota_commit(context, reservations, project_id=prj_id)
self._quota_commit(context, reservations, project_id=prj_id,
user_id=user_id)
# ensure block device mappings are not leaked
self.conductor_api.block_device_mapping_destroy(context, bdms)
@ -1542,6 +1550,10 @@ class ComputeManager(manager.SchedulerDependentManager):
project_id = instance['project_id']
else:
project_id = context.project_id
if context.user_id != instance['user_id']:
user_id = instance['user_id']
else:
user_id = context.user_id
was_soft_deleted = instance['vm_state'] == vm_states.SOFT_DELETED
if was_soft_deleted:
@ -1549,7 +1561,8 @@ class ComputeManager(manager.SchedulerDependentManager):
# decremented.
try:
self._quota_rollback(context, reservations,
project_id=project_id)
project_id=project_id,
user_id=user_id)
except Exception:
pass
reservations = None
@ -1585,14 +1598,16 @@ class ComputeManager(manager.SchedulerDependentManager):
except Exception:
with excutils.save_and_reraise_exception():
self._quota_rollback(context, reservations,
project_id=project_id)
project_id=project_id,
user_id=user_id)
self._complete_deletion(context,
instance,
bdms,
reservations,
project_id,
system_meta)
system_meta,
user_id=user_id)
@exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@wrap_instance_event
@ -1683,6 +1698,10 @@ class ComputeManager(manager.SchedulerDependentManager):
project_id = instance['project_id']
else:
project_id = context.project_id
if context.user_id != instance['user_id']:
user_id = instance['user_id']
else:
user_id = context.user_id
try:
self._notify_about_instance_usage(context, instance,
@ -1702,8 +1721,10 @@ class ComputeManager(manager.SchedulerDependentManager):
except Exception:
with excutils.save_and_reraise_exception():
self._quota_rollback(context, reservations,
project_id=project_id)
self._quota_commit(context, reservations, project_id=project_id)
project_id=project_id,
user_id=user_id)
self._quota_commit(context, reservations, project_id=project_id,
user_id=user_id)
self._notify_about_instance_usage(context, instance, "soft_delete.end")
@exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@ -2603,15 +2624,19 @@ class ComputeManager(manager.SchedulerDependentManager):
self._quota_commit(context, reservations)
def _quota_commit(self, context, reservations, project_id=None):
def _quota_commit(self, context, reservations, project_id=None,
user_id=None):
if reservations:
self.conductor_api.quota_commit(context, reservations,
project_id=project_id)
project_id=project_id,
user_id=user_id)
def _quota_rollback(self, context, reservations, project_id=None):
def _quota_rollback(self, context, reservations, project_id=None,
user_id=None):
if reservations:
self.conductor_api.quota_rollback(context, reservations,
project_id=project_id)
project_id=project_id,
user_id=user_id)
def _prep_resize(self, context, image, instance, instance_type,
reservations, request_spec, filter_properties, node):

View File

@ -318,13 +318,17 @@ class LocalAPI(object):
instance,
migration)
def quota_commit(self, context, reservations, project_id=None):
def quota_commit(self, context, reservations, project_id=None,
user_id=None):
return self._manager.quota_commit(context, reservations,
project_id=project_id)
project_id=project_id,
user_id=user_id)
def quota_rollback(self, context, reservations, project_id=None):
def quota_rollback(self, context, reservations, project_id=None,
user_id=None):
return self._manager.quota_rollback(context, reservations,
project_id=project_id)
project_id=project_id,
user_id=user_id)
def get_ec2_ids(self, context, instance):
return self._manager.get_ec2_ids(context, instance)

View File

@ -492,11 +492,15 @@ class ConductorManager(manager.Manager):
def network_migrate_instance_finish(self, context, instance, migration):
self.network_api.migrate_instance_finish(context, instance, migration)
def quota_commit(self, context, reservations, project_id=None):
quota.QUOTAS.commit(context, reservations, project_id=project_id)
def quota_commit(self, context, reservations, project_id=None,
user_id=None):
quota.QUOTAS.commit(context, reservations, project_id=project_id,
user_id=user_id)
def quota_rollback(self, context, reservations, project_id=None):
quota.QUOTAS.rollback(context, reservations, project_id=project_id)
def quota_rollback(self, context, reservations, project_id=None,
user_id=None):
quota.QUOTAS.rollback(context, reservations, project_id=project_id,
user_id=user_id)
def get_ec2_ids(self, context, instance):
ec2_ids = {}

View File

@ -460,16 +460,18 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy):
instance=instance_p, migration=migration_p)
return self.call(context, msg, version='1.41')
def quota_commit(self, context, reservations, project_id=None):
def quota_commit(self, context, reservations, project_id=None,
user_id=None):
reservations_p = jsonutils.to_primitive(reservations)
msg = self.make_msg('quota_commit', reservations=reservations_p,
project_id=project_id)
project_id=project_id, user_id=user_id)
return self.call(context, msg, version='1.45')
def quota_rollback(self, context, reservations, project_id=None):
def quota_rollback(self, context, reservations, project_id=None,
user_id=None):
reservations_p = jsonutils.to_primitive(reservations)
msg = self.make_msg('quota_rollback', reservations=reservations_p,
project_id=project_id)
project_id=project_id, user_id=user_id)
return self.call(context, msg, version='1.45')
def get_ec2_ids(self, context, instance):

View File

@ -951,14 +951,20 @@ def network_update(context, network_id, values):
###############
def quota_create(context, project_id, resource, limit):
def quota_create(context, project_id, resource, limit, user_id=None):
"""Create a quota for the given project and resource."""
return IMPL.quota_create(context, project_id, resource, limit)
return IMPL.quota_create(context, project_id, resource, limit,
user_id=user_id)
def quota_get(context, project_id, resource):
def quota_get(context, project_id, resource, user_id=None):
"""Retrieve a quota or raise if it does not exist."""
return IMPL.quota_get(context, project_id, resource)
return IMPL.quota_get(context, project_id, resource, user_id=user_id)
def quota_get_all_by_project_and_user(context, project_id, user_id):
"""Retrieve all quotas associated with a given project and user."""
return IMPL.quota_get_all_by_project_and_user(context, project_id, user_id)
def quota_get_all_by_project(context, project_id):
@ -966,9 +972,15 @@ def quota_get_all_by_project(context, project_id):
return IMPL.quota_get_all_by_project(context, project_id)
def quota_update(context, project_id, resource, limit):
def quota_get_all(context, project_id):
"""Retrieve all user quotas associated with a given project."""
return IMPL.quota_get_all(context, project_id)
def quota_update(context, project_id, resource, limit, user_id=None):
"""Update a quota or raise if it does not exist."""
return IMPL.quota_update(context, project_id, resource, limit)
return IMPL.quota_update(context, project_id, resource, limit,
user_id=user_id)
###################
@ -1002,9 +1014,15 @@ def quota_class_update(context, class_name, resource, limit):
###################
def quota_usage_get(context, project_id, resource):
def quota_usage_get(context, project_id, resource, user_id=None):
"""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, project_id, resource, user_id=user_id)
def quota_usage_get_all_by_project_and_user(context, project_id, user_id):
"""Retrieve all usage associated with a given resource."""
return IMPL.quota_usage_get_all_by_project_and_user(context,
project_id, user_id)
def quota_usage_get_all_by_project(context, project_id):
@ -1012,19 +1030,20 @@ 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, project_id, resource, **kwargs):
def quota_usage_update(context, project_id, user_id, resource, **kwargs):
"""Update a quota usage or raise if it does not exist."""
return IMPL.quota_usage_update(context, project_id, resource, **kwargs)
return IMPL.quota_usage_update(context, project_id, user_id, resource,
**kwargs)
###################
def reservation_create(context, uuid, usage, project_id, resource, delta,
expire):
def reservation_create(context, uuid, usage, project_id, user_id, resource,
delta, expire):
"""Create a reservation for the given project and resource."""
return IMPL.reservation_create(context, uuid, usage, project_id,
resource, delta, expire)
user_id, resource, delta, expire)
def reservation_get(context, uuid):
@ -1035,23 +1054,32 @@ def reservation_get(context, uuid):
###################
def quota_reserve(context, resources, quotas, deltas, expire,
until_refresh, max_age, project_id=None):
def quota_reserve(context, resources, quotas, user_quotas, deltas, expire,
until_refresh, max_age, project_id=None, user_id=None):
"""Check quotas and create appropriate reservations."""
return IMPL.quota_reserve(context, resources, quotas, deltas, expire,
until_refresh, max_age, project_id=project_id)
return IMPL.quota_reserve(context, resources, quotas, user_quotas, deltas,
expire, until_refresh, max_age,
project_id=project_id, user_id=user_id)
def reservation_commit(context, reservations, project_id=None):
def reservation_commit(context, reservations, project_id=None, user_id=None):
"""Commit quota reservations."""
return IMPL.reservation_commit(context, reservations,
project_id=project_id)
project_id=project_id,
user_id=user_id)
def reservation_rollback(context, reservations, project_id=None):
def reservation_rollback(context, reservations, project_id=None, user_id=None):
"""Roll back quota reservations."""
return IMPL.reservation_rollback(context, reservations,
project_id=project_id)
project_id=project_id,
user_id=user_id)
def quota_destroy_all_by_project_and_user(context, project_id, user_id):
"""Destroy all quotas associated with a given project and user."""
return IMPL.quota_destroy_all_by_project_and_user(context,
project_id, user_id)
def quota_destroy_all_by_project(context, project_id):

View File

@ -281,25 +281,25 @@ def convert_datetimes(values, *datetime_keys):
return values
def _sync_instances(context, project_id, session):
def _sync_instances(context, project_id, user_id, session):
return dict(zip(('instances', 'cores', 'ram'),
_instance_data_get_for_project(
context, project_id, session)))
_instance_data_get_for_user(
context, project_id, user_id, session)))
def _sync_floating_ips(context, project_id, session):
def _sync_floating_ips(context, project_id, user_id, session):
return dict(floating_ips=_floating_ip_count_by_project(
context, project_id, session))
def _sync_fixed_ips(context, project_id, session):
def _sync_fixed_ips(context, project_id, user_id, session):
return dict(fixed_ips=_fixed_ip_count_by_project(
context, project_id, session))
def _sync_security_groups(context, project_id, session):
return dict(security_groups=_security_group_count_by_project(
context, project_id, session))
def _sync_security_groups(context, project_id, user_id, session):
return dict(security_groups=_security_group_count_by_project_and_user(
context, project_id, user_id, session))
QUOTA_SYNC_FUNCTIONS = {
'_sync_instances': _sync_instances,
@ -1526,15 +1526,18 @@ def instance_create(context, values):
return instance_ref
def _instance_data_get_for_project(context, project_id, session=None):
def _instance_data_get_for_user(context, project_id, user_id, session=None):
result = model_query(context,
func.count(models.Instance.id),
func.sum(models.Instance.vcpus),
func.sum(models.Instance.memory_mb),
base_model=models.Instance,
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)
@ -2601,14 +2604,39 @@ def network_update(context, network_id, values):
@require_context
def quota_get(context, project_id, resource):
result = model_query(context, models.Quota, read_deleted="no").\
filter_by(project_id=project_id).\
filter_by(resource=resource).\
first()
def quota_get(context, project_id, resource, user_id=None):
model = models.ProjectUserQuota if user_id else models.Quota
query = model_query(context, model).\
filter_by(project_id=project_id).\
filter_by(resource=resource)
if user_id:
query = query.filter_by(user_id=user_id)
result = query.first()
if not result:
raise exception.ProjectQuotaNotFound(project_id=project_id)
if user_id:
raise exception.ProjectUserQuotaNotFound(project_id=project_id,
user_id=user_id)
else:
raise exception.ProjectQuotaNotFound(project_id=project_id)
return result
@require_context
def quota_get_all_by_project_and_user(context, project_id, user_id):
nova.context.authorize_project_context(context, project_id)
rows = model_query(context, models.ProjectUserQuota.resource,
models.ProjectUserQuota.hard_limit,
base_model=models.ProjectUserQuota).\
filter_by(project_id=project_id).\
filter_by(user_id=user_id).\
all()
result = {'project_id': project_id, 'user_id': user_id}
for row in rows:
result[row.resource] = row.hard_limit
return result
@ -2628,9 +2656,22 @@ def quota_get_all_by_project(context, project_id):
return result
@require_context
def quota_get_all(context, project_id):
nova.context.authorize_project_context(context, project_id)
result = model_query(context, models.ProjectUserQuota).\
filter_by(project_id=project_id).\
all()
return result
@require_admin_context
def quota_create(context, project_id, resource, limit):
quota_ref = models.Quota()
def quota_create(context, project_id, resource, limit, user_id=None):
quota_ref = models.ProjectUserQuota() if user_id else models.Quota()
if user_id:
quota_ref.user_id = user_id
quota_ref.project_id = project_id
quota_ref.resource = resource
quota_ref.hard_limit = limit
@ -2642,14 +2683,21 @@ def quota_create(context, project_id, resource, limit):
@require_admin_context
def quota_update(context, project_id, resource, limit):
result = model_query(context, models.Quota, read_deleted="no").\
filter_by(project_id=project_id).\
filter_by(resource=resource).\
update({'hard_limit': limit})
def quota_update(context, project_id, resource, limit, user_id=None):
model = models.ProjectUserQuota if user_id else models.Quota
query = model_query(context, model).\
filter_by(project_id=project_id).\
filter_by(resource=resource)
if user_id:
query = query.filter_by(user_id=user_id)
result = query.update({'hard_limit': limit})
if not result:
raise exception.ProjectQuotaNotFound(project_id=project_id)
if user_id:
raise exception.ProjectUserQuotaNotFound(project_id=project_id,
user_id=user_id)
else:
raise exception.ProjectQuotaNotFound(project_id=project_id)
###################
@ -2720,11 +2768,14 @@ def quota_class_update(context, class_name, resource, limit):
@require_context
def quota_usage_get(context, project_id, resource):
result = model_query(context, models.QuotaUsage, read_deleted="no").\
def quota_usage_get(context, project_id, resource, user_id=None):
query = model_query(context, models.QuotaUsage, read_deleted="no").\
filter_by(project_id=project_id).\
filter_by(resource=resource).\
first()
filter_by(resource=resource)
if user_id:
result = query.filter_by(user_id=user_id).first()
else:
result = query.first()
if not result:
raise exception.QuotaUsageNotFound(project_id=project_id)
@ -2732,30 +2783,49 @@ def quota_usage_get(context, project_id, resource):
return result
@require_context
def quota_usage_get_all_by_project(context, project_id):
def _quota_usage_get_all(context, project_id, user_id=None):
nova.context.authorize_project_context(context, project_id)
rows = model_query(context, models.QuotaUsage, read_deleted="no").\
filter_by(project_id=project_id).\
all()
query = model_query(context, models.QuotaUsage, read_deleted="no").\
filter_by(project_id=project_id)
result = {'project_id': project_id}
if user_id:
query = query.filter_by(user_id=user_id)
result['user_id'] = user_id
rows = query.all()
for row in rows:
result[row.resource] = dict(in_use=row.in_use, reserved=row.reserved)
if row.resource in result:
result[row.resource]['in_use'] += row.in_use
result[row.resource]['reserved'] += row.reserved
else:
result[row.resource] = dict(in_use=row.in_use,
reserved=row.reserved)
return result
@require_context
def quota_usage_get_all_by_project_and_user(context, project_id, user_id):
return _quota_usage_get_all(context, project_id, user_id=user_id)
@require_context
def quota_usage_get_all_by_project(context, project_id):
return _quota_usage_get_all(context, project_id)
@require_admin_context
def _quota_usage_create(context, project_id, resource, in_use, reserved,
until_refresh, session=None):
def _quota_usage_create(context, project_id, user_id, resource, in_use,
reserved, until_refresh, session=None):
quota_usage_ref = models.QuotaUsage()
quota_usage_ref.project_id = project_id
quota_usage_ref.user_id = user_id
quota_usage_ref.resource = resource
quota_usage_ref.in_use = in_use
quota_usage_ref.reserved = reserved
quota_usage_ref.until_refresh = until_refresh
# updated_at is needed for judgement of max_age
quota_usage_ref.updated_at = timeutils.utcnow()
quota_usage_ref.save(session=session)
@ -2763,17 +2833,16 @@ def _quota_usage_create(context, project_id, resource, in_use, reserved,
@require_admin_context
def quota_usage_update(context, project_id, resource, **kwargs):
def quota_usage_update(context, project_id, user_id, resource, **kwargs):
updates = {}
if 'in_use' in kwargs:
updates['in_use'] = kwargs['in_use']
if 'reserved' in kwargs:
updates['reserved'] = kwargs['reserved']
if 'until_refresh' in kwargs:
updates['until_refresh'] = kwargs['until_refresh']
for key in ['in_use', 'reserved', 'until_refresh']:
if key in kwargs:
updates[key] = kwargs[key]
result = model_query(context, models.QuotaUsage, read_deleted="no").\
filter_by(project_id=project_id).\
filter_by(user_id=user_id).\
filter_by(resource=resource).\
update(updates)
@ -2797,19 +2866,20 @@ def reservation_get(context, uuid):
@require_admin_context
def reservation_create(context, uuid, usage, project_id, resource, delta,
expire):
return _reservation_create(context, uuid, usage, project_id,
def reservation_create(context, uuid, usage, project_id, user_id, resource,
delta, expire):
return _reservation_create(context, uuid, usage, project_id, user_id,
resource, delta, expire)
@require_admin_context
def _reservation_create(context, uuid, usage, project_id, resource, delta,
expire, session=None):
def _reservation_create(context, uuid, usage, project_id, user_id, resource,
delta, expire, session=None):
reservation_ref = models.Reservation()
reservation_ref.uuid = uuid
reservation_ref.usage_id = usage['id']
reservation_ref.project_id = project_id
reservation_ref.user_id = user_id
reservation_ref.resource = resource
reservation_ref.delta = delta
reservation_ref.expire = expire
@ -2825,30 +2895,58 @@ def _reservation_create(context, uuid, usage, project_id, resource, delta,
# code always acquires the lock on quota_usages before acquiring the lock
# on reservations.
def _get_quota_usages(context, session, project_id):
def _get_user_quota_usages(context, session, project_id, user_id):
# Broken out for testability
rows = model_query(context, models.QuotaUsage,
read_deleted="no",
session=session).\
filter_by(project_id=project_id).\
filter_by(user_id=user_id).\
with_lockmode('update').\
all()
return dict((row.resource, row) for row in rows)
def _get_project_quota_usages(context, session, project_id):
rows = model_query(context, models.QuotaUsage,
read_deleted="no",
session=session).\
filter_by(project_id=project_id).\
with_lockmode('update').\
all()
return dict((row.resource, row) for row in rows)
result = dict()
# Get the total count of in_use,reserved
for row in rows:
if row.resource in result:
result[row.resource]['in_use'] += row.in_use
result[row.resource]['reserved'] += row.reserved
result[row.resource]['total'] += (row.in_use + row.reserved)
else:
result[row.resource] = dict(in_use=row.in_use,
reserved=row.reserved,
total=row.in_use + row.reserved)
return result
@require_context
@_retry_on_deadlock
def quota_reserve(context, resources, quotas, deltas, expire,
until_refresh, max_age, project_id=None):
def quota_reserve(context, resources, project_quotas, user_quotas, deltas,
expire, until_refresh, max_age, project_id=None,
user_id=None):
elevated = context.elevated()
session = get_session()
with session.begin():
if project_id is None:
project_id = context.project_id
if user_id is None:
user_id = context.user_id
# Get the current usages
usages = _get_quota_usages(context, session, project_id)
user_usages = _get_user_quota_usages(context, session,
project_id, user_id)
project_usages = _get_project_quota_usages(context, session,
project_id)
# Handle usage refresh
work = set(deltas.keys())
@ -2857,23 +2955,24 @@ def quota_reserve(context, resources, quotas, deltas, expire,
# Do we need to refresh the usage?
refresh = False
if resource not in usages:
usages[resource] = _quota_usage_create(elevated,
if resource not in user_usages:
user_usages[resource] = _quota_usage_create(elevated,
project_id,
user_id,
resource,
0, 0,
until_refresh or None,
session=session)
refresh = True
elif usages[resource].in_use < 0:
elif user_usages[resource].in_use < 0:
# Negative in_use count indicates a desync, so try to
# heal from that...
refresh = True
elif usages[resource].until_refresh is not None:
usages[resource].until_refresh -= 1
if usages[resource].until_refresh <= 0:
elif user_usages[resource].until_refresh is not None:
user_usages[resource].until_refresh -= 1
if user_usages[resource].until_refresh <= 0:
refresh = True
elif max_age and (usages[resource].updated_at -
elif max_age and (user_usages[resource].updated_at -
timeutils.utcnow()).seconds >= max_age:
refresh = True
@ -2882,20 +2981,21 @@ def quota_reserve(context, resources, quotas, deltas, expire,
# Grab the sync routine
sync = QUOTA_SYNC_FUNCTIONS[resources[resource].sync]
updates = sync(elevated, project_id, session)
updates = sync(elevated, project_id, user_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,
if res not in user_usages:
user_usages[res] = _quota_usage_create(elevated,
project_id,
user_id,
res,
0, 0,
until_refresh or None,
session=session)
# Update the usage
usages[res].in_use = in_use
usages[res].until_refresh = until_refresh or None
user_usages[res].in_use = in_use
user_usages[res].until_refresh = until_refresh or None
# Because more than one resource may be refreshed
# by the call to the sync routine, and we don't
@ -2912,16 +3012,22 @@ def quota_reserve(context, resources, quotas, deltas, expire,
# Check for deltas that would go negative
unders = [res for res, delta in deltas.items()
if delta < 0 and
delta + usages[res].in_use < 0]
delta + user_usages[res].in_use < 0]
# Now, let's check the quotas
# NOTE(Vek): We're only concerned about positive increments.
# If a project has gone over quota, we want them to
# be able to reduce their usage without any
# problems.
for key, value in user_usages.items():
if key not in project_usages:
project_usages[key] = value
overs = [res for res, delta in deltas.items()
if quotas[res] >= 0 and delta >= 0 and
quotas[res] < delta + usages[res].total]
if user_quotas[res] >= 0 and delta >= 0 and
(project_quotas[res] < delta +
project_usages[res]['total'] or
user_quotas[res] < delta +
user_usages[res].total)]
# NOTE(Vek): The quota check needs to be in the transaction,
# but the transaction doesn't fail just because
@ -2936,8 +3042,9 @@ def quota_reserve(context, resources, quotas, deltas, expire,
for res, delta in deltas.items():
reservation = _reservation_create(elevated,
str(uuid.uuid4()),
usages[res],
user_usages[res],
project_id,
user_id,
res, delta, expire,
session=session)
reservations.append(reservation.uuid)
@ -2955,19 +3062,23 @@ def quota_reserve(context, resources, quotas, deltas, expire,
# To prevent this, we only update the
# reserved value if the delta is positive.
if delta > 0:
usages[res].reserved += delta
user_usages[res].reserved += delta
# Apply updates to the usages table
for usage_ref in usages.values():
for usage_ref in user_usages.values():
usage_ref.save(session=session)
if unders:
LOG.warning(_("Change will make usage less than 0 for the following "
"resources: %s"), unders)
if overs:
if project_quotas == user_quotas:
usages = project_usages
else:
usages = user_usages
usages = dict((k, dict(in_use=v['in_use'], reserved=v['reserved']))
for k, v in usages.items())
raise exception.OverQuota(overs=sorted(overs), quotas=quotas,
raise exception.OverQuota(overs=sorted(overs), quotas=user_quotas,
usages=usages)
return reservations
@ -2985,10 +3096,10 @@ def _quota_reservations_query(session, context, reservations):
@require_context
def reservation_commit(context, reservations, project_id=None):
def reservation_commit(context, reservations, project_id=None, user_id=None):
session = get_session()
with session.begin():
usages = _get_quota_usages(context, session, project_id)
usages = _get_user_quota_usages(context, session, project_id, user_id)
reservation_query = _quota_reservations_query(session, context,
reservations)
for reservation in reservation_query.all():
@ -3000,10 +3111,10 @@ def reservation_commit(context, reservations, project_id=None):
@require_context
def reservation_rollback(context, reservations, project_id=None):
def reservation_rollback(context, reservations, project_id=None, user_id=None):
session = get_session()
with session.begin():
usages = _get_quota_usages(context, session, project_id)
usages = _get_user_quota_usages(context, session, project_id, user_id)
reservation_query = _quota_reservations_query(session, context,
reservations)
for reservation in reservation_query.all():
@ -3013,6 +3124,29 @@ def reservation_rollback(context, reservations, project_id=None):
reservation_query.soft_delete(synchronize_session=False)
@require_admin_context
def quota_destroy_all_by_project_and_user(context, project_id, user_id):
session = get_session()
with session.begin():
model_query(context, models.ProjectUserQuota, session=session,
read_deleted="no").\
filter_by(project_id=project_id).\
filter_by(user_id=user_id).\
soft_delete(synchronize_session=False)
model_query(context, models.QuotaUsage,
session=session, read_deleted="no").\
filter_by(project_id=project_id).\
filter_by(user_id=user_id).\
soft_delete(synchronize_session=False)
model_query(context, models.Reservation,
session=session, read_deleted="no").\
filter_by(project_id=project_id).\
filter_by(user_id=user_id).\
soft_delete(synchronize_session=False)
@require_admin_context
def quota_destroy_all_by_project(context, project_id):
session = get_session()
@ -3469,11 +3603,13 @@ def security_group_destroy(context, security_group_id):
soft_delete()
def _security_group_count_by_project(context, project_id, session=None):
def _security_group_count_by_project_and_user(context, project_id, user_id,
session=None):
nova.context.authorize_project_context(context, project_id)
return model_query(context, models.SecurityGroup, read_deleted="no",
session=session).\
filter_by(project_id=project_id).\
filter_by(user_id=user_id).\
count()

View File

@ -0,0 +1,145 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 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 sqlalchemy import Column, DateTime, Integer
from sqlalchemy import Index, UniqueConstraint, MetaData, String, Table
from nova.db.sqlalchemy import api as db
from nova.db.sqlalchemy import utils
from nova.openstack.common import log as logging
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 and its shadow table.
quota_usages = utils.get_table(migrate_engine, 'quota_usages')
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)
shadow_quota_usages = utils.get_table(migrate_engine,
db._SHADOW_TABLE_PREFIX + 'quota_usages')
user_id = Column('user_id',
String(length=255, convert_unicode=False,
assert_unicode=None, unicode_error=None,
_warn_on_bytestring=False))
shadow_quota_usages.create_column(user_id)
# Add 'user_id' column to reservations table and its shadow table.
reservations = utils.get_table(migrate_engine, 'reservations')
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)
shadow_reservations = utils.get_table(migrate_engine,
db._SHADOW_TABLE_PREFIX + 'reservations')
user_id = Column('user_id',
String(length=255, convert_unicode=False,
assert_unicode=None, unicode_error=None,
_warn_on_bytestring=False))
shadow_reservations.create_column(user_id)
indexes = [
Index('ix_quota_usages_user_id_deleted',
quota_usages.c.user_id, quota_usages.c.deleted),
Index('ix_reservations_user_id_deleted',
reservations.c.user_id, reservations.c.deleted)
]
if migrate_engine.name == 'mysql' or migrate_engine.name == 'postgresql':
for index in indexes:
index.create(migrate_engine)
uniq_name = "uniq_project_user_quotas0user_id0project_id0resource0deleted"
project_user_quotas = Table('project_user_quotas', meta,
Column('id', Integer, primary_key=True,
nullable=False),
Column('created_at', DateTime),
Column('updated_at', DateTime),
Column('deleted_at', DateTime),
Column('deleted', Integer),
Column('user_id',
String(length=255, convert_unicode=False,
assert_unicode=None, unicode_error=None,
_warn_on_bytestring=False),
nullable=False),
Column('project_id',
String(length=255, convert_unicode=False,
assert_unicode=None, unicode_error=None,
_warn_on_bytestring=False),
nullable=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),
UniqueConstraint('user_id', 'project_id', 'resource',
'deleted', name=uniq_name),
Index('project_user_quotas_project_id_deleted_idx',
'project_id', 'deleted'),
Index('project_user_quotas_user_id_deleted_idx',
'project_id', 'deleted'),
mysql_engine='InnoDB',
mysql_charset='utf8',
)
try:
project_user_quotas.create()
utils.create_shadow_table(migrate_engine, table=project_user_quotas)
except Exception:
LOG.exception("Exception while creating table 'project_user_quotas'")
meta.drop_all(tables=[project_user_quotas])
raise
def downgrade(migrate_engine):
quota_usages = utils.get_table(migrate_engine, 'quota_usages')
quota_usages.drop_column('user_id')
shadow_quota_usages = utils.get_table(migrate_engine,
db._SHADOW_TABLE_PREFIX + 'quota_usages')
shadow_quota_usages.drop_column('user_id')
reservations = utils.get_table(migrate_engine, 'reservations')
reservations.drop_column('user_id')
shadow_reservations = utils.get_table(migrate_engine,
db._SHADOW_TABLE_PREFIX + 'reservations')
shadow_reservations.drop_column('user_id')
project_user_quotas = utils.get_table(migrate_engine,
'project_user_quotas')
try:
project_user_quotas.drop()
except Exception:
LOG.error(_("project_user_quotas table not dropped"))
raise
shadow_table_name = db._SHADOW_TABLE_PREFIX + 'project_user_quotas'
shadow_table = utils.get_table(migrate_engine, shadow_table_name)
try:
shadow_table.drop()
except Exception:
LOG.error(_("%s table not dropped") % shadow_table_name)
raise

View File

@ -399,6 +399,28 @@ class Quota(BASE, NovaBase):
hard_limit = Column(Integer, nullable=True)
class ProjectUserQuota(BASE, NovaBase):
"""Represents a single quota override for a user with in a project."""
__tablename__ = 'project_user_quotas'
uniq_name = "uniq_project_user_quotas0user_id0project_id0resource0deleted"
__table_args__ = (
schema.UniqueConstraint("user_id", "project_id", "resource", "deleted",
name=uniq_name),
Index('project_user_quotas_project_id_deleted_idx',
'project_id', 'deleted'),
Index('project_user_quotas_user_id_deleted_idx',
'user_id', 'deleted')
)
id = Column(Integer, primary_key=True, nullable=False)
project_id = Column(String(255), nullable=False)
user_id = Column(String(255), nullable=False)
resource = Column(String(255), nullable=False)
hard_limit = Column(Integer, nullable=True)
class QuotaClass(BASE, NovaBase):
"""Represents a single quota override for a quota class.
@ -429,7 +451,8 @@ class QuotaUsage(BASE, NovaBase):
id = Column(Integer, primary_key=True)
project_id = Column(String(255), nullable=True)
resource = Column(String(255), nullable=True)
user_id = Column(String(255), nullable=True)
resource = Column(String(255), nullable=False)
in_use = Column(Integer, nullable=False)
reserved = Column(Integer, nullable=False)
@ -454,6 +477,7 @@ class Reservation(BASE, NovaBase):
usage_id = Column(Integer, ForeignKey('quota_usages.id'), nullable=False)
project_id = Column(String(255))
user_id = Column(String(255))
resource = Column(String(255))
delta = Column(Integer, nullable=False)

View File

@ -735,6 +735,11 @@ class QuotaResourceUnknown(QuotaNotFound):
msg_fmt = _("Unknown quota resources %(unknown)s.")
class ProjectUserQuotaNotFound(QuotaNotFound):
message = _("Quota for user %(user_id)s in project %(project_id)s "
"could not be found.")
class ProjectQuotaNotFound(QuotaNotFound):
msg_fmt = _("Quota for project %(project_id)s could not be found.")

View File

@ -92,6 +92,10 @@ class DbQuotaDriver(object):
quota information. The default driver utilizes the local
database.
"""
def get_by_project_and_user(self, context, project_id, user_id, resource):
"""Get a specific quota by project and user."""
return db.quota_get(context, project_id, user_id, resource)
def get_by_project(self, context, project_id, resource):
"""Get a specific quota by project."""
@ -144,9 +148,91 @@ class DbQuotaDriver(object):
return quotas
def _process_quotas(self, context, resources, project_id, quotas,
quota_class=None, defaults=True, usages=None,
remains=False):
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
# any)
if project_id == context.project_id:
quota_class = context.quota_class
if quota_class:
class_quotas = db.quota_class_get_all_by_name(context, quota_class)
else:
class_quotas = {}
default_quotas = self.get_defaults(context, resources)
for resource in resources.values():
# Omit default/quota class values
if not defaults and resource.name not in quotas:
continue
limit = quotas.get(resource.name, class_quotas.get(
resource.name, default_quotas[resource.name]))
modified_quotas[resource.name] = dict(limit=limit)
# Include usages if desired. This is optional because one
# internal consumer of this interface wants to access the
# usages directly from inside a transaction.
if usages:
usage = usages.get(resource.name, {})
modified_quotas[resource.name].update(
in_use=usage.get('in_use', 0),
reserved=usage.get('reserved', 0),
)
# Initialize remains quotas.
if remains:
modified_quotas[resource.name].update(remains=limit)
if remains:
all_quotas = db.quota_get_all(context, project_id)
for quota in all_quotas:
if quota.resource in modified_quotas:
modified_quotas[quota.resource]['remains'] -= \
quota.hard_limit
return modified_quotas
def get_user_quotas(self, context, resources, project_id, user_id,
quota_class=None, defaults=True,
usages=True):
"""
Given a list of resources, retrieve the quotas for the given
user and 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 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_project_and_user(context,
project_id, user_id)
user_usages = None
if usages:
user_usages = db.quota_usage_get_all_by_project_and_user(context,
project_id,
user_id)
return self._process_quotas(context, resources, project_id,
user_quotas, quota_class,
defaults=defaults, usages=user_usages)
def get_project_quotas(self, context, resources, project_id,
quota_class=None, defaults=True,
usages=True):
usages=True, remains=False):
"""
Given a list of resources, retrieve the quotas for the given
project.
@ -165,50 +251,55 @@ class DbQuotaDriver(object):
specific value for the resource.
:param usages: If True, the current in_use and reserved counts
will also be returned.
:param remains: If True, the current remains of the project will
will be returned.
"""
quotas = {}
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,
defaults=defaults, usages=project_usages,
remains=remains)
# 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
# any)
if project_id == context.project_id:
quota_class = context.quota_class
if quota_class:
class_quotas = db.quota_class_get_all_by_name(context, quota_class)
def get_settable_quotas(self, context, resources, project_id,
user_id=None):
"""
Given a list of resources, retrieve the range of settable quotas for
the given user or 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 user_id: The ID of the user to return quotas for.
"""
settable_quotas = {}
project_quotas = self.get_project_quotas(context, resources,
project_id, remains=True)
if user_id:
user_quotas = self.get_user_quotas(context, resources,
project_id, user_id)
setted_quotas = db.quota_get_all_by_project_and_user(context,
project_id,
user_id)
for key, value in user_quotas.items():
maximum = project_quotas[key]['remains'] +\
setted_quotas.get(key, 0)
settable_quotas[key] = dict(
minimum=value['in_use'] + value['reserved'],
maximum=maximum
)
else:
class_quotas = {}
for key, value in project_quotas.items():
minimum = max(int(value['limit'] - value['remains']),
int(value['in_use'] + value['reserved']))
settable_quotas[key] = dict(minimum=minimum, maximum=-1)
return settable_quotas
default_quotas = self.get_defaults(context, resources)
for resource in resources.values():
# Omit default/quota class values
if not defaults and resource.name not in project_quotas:
continue
quotas[resource.name] = dict(
limit=project_quotas.get(resource.name, class_quotas.get(
resource.name, default_quotas[resource.name])),
)
# Include usages if desired. This is optional because one
# 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(
in_use=usage.get('in_use', 0),
reserved=usage.get('reserved', 0),
)
return quotas
def _get_quotas(self, context, resources, keys, has_sync, project_id=None):
def _get_quotas(self, context, resources, keys, has_sync, project_id=None,
user_id=None):
"""
A helper method which retrieves the quotas for the specific
resources identified by keys, and which apply to the current
@ -224,6 +315,9 @@ class DbQuotaDriver(object):
:param project_id: Specify the project_id if current context
is admin and admin wants to impact on
common user's tenant.
:param user_id: Specify the user_id if current context
is admin and admin wants to impact on
common user.
"""
# Filter resources
@ -240,14 +334,22 @@ class DbQuotaDriver(object):
unknown = desired - set(sub_resources.keys())
raise exception.QuotaResourceUnknown(unknown=sorted(unknown))
# Grab and return the quotas (without usages)
quotas = self.get_project_quotas(context, sub_resources,
project_id,
context.quota_class, usages=False)
if user_id:
# Grab and return the quotas (without usages)
quotas = self.get_user_quotas(context, sub_resources,
project_id, user_id,
context.quota_class, usages=False)
else:
# Grab and return the quotas (without usages)
quotas = self.get_project_quotas(context, sub_resources,
project_id,
context.quota_class,
usages=False)
return dict((k, v['limit']) for k, v in quotas.items())
def limit_check(self, context, resources, values, project_id=None):
def limit_check(self, context, resources, values, project_id=None,
user_id=None):
"""Check simple quota limits.
For limits--those quotas for which there is no usage
@ -270,6 +372,9 @@ class DbQuotaDriver(object):
:param project_id: Specify the project_id if current context
is admin and admin wants to impact on
common user's tenant.
:param user_id: Specify the user_id if current context
is admin and admin wants to impact on
common user.
"""
# Ensure no value is less than zero
@ -280,21 +385,28 @@ class DbQuotaDriver(object):
# If project_id is None, then we use the project_id in context
if project_id is None:
project_id = context.project_id
# If user id is None, then we use the user_id in context
if user_id is None:
user_id = context.user_id
# Get the applicable quotas
quotas = self._get_quotas(context, resources, values.keys(),
has_sync=False, project_id=project_id)
user_quotas = self._get_quotas(context, resources, values.keys(),
has_sync=False, project_id=project_id,
user_id=user_id)
# Check the quotas and construct a list of the resources that
# would be put over limit by the desired values
overs = [key for key, val in values.items()
if quotas[key] >= 0 and quotas[key] < val]
if (quotas[key] >= 0 and quotas[key] < val) or
(user_quotas[key] >= 0 and user_quotas[key] < val)]
if overs:
raise exception.OverQuota(overs=sorted(overs), quotas=quotas,
usages={})
def reserve(self, context, resources, deltas, expire=None,
project_id=None):
project_id=None, user_id=None):
"""Check quotas and reserve resources.
For counting quotas--those quotas for which there is a usage
@ -327,6 +439,9 @@ class DbQuotaDriver(object):
:param project_id: Specify the project_id if current context
is admin and admin wants to impact on
common user's tenant.
:param user_id: Specify the user_id if current context
is admin and admin wants to impact on
common user.
"""
# Set up the reservation expiration
@ -342,6 +457,9 @@ class DbQuotaDriver(object):
# If project_id is None, then we use the project_id in context
if project_id is None:
project_id = context.project_id
# If user_id is None, then we use the project_id in context
if user_id is None:
user_id = context.user_id
# Get the applicable quotas.
# NOTE(Vek): We're not worried about races at this point.
@ -349,17 +467,21 @@ class DbQuotaDriver(object):
# quotas, but that's a pretty rare thing.
quotas = self._get_quotas(context, resources, deltas.keys(),
has_sync=True, project_id=project_id)
user_quotas = self._get_quotas(context, resources, deltas.keys(),
has_sync=True, project_id=project_id,
user_id=user_id)
# NOTE(Vek): Most of the work here has to be done in the DB
# API, because we have to do it in a transaction,
# which means access to the session. Since the
# session isn't available outside the DBAPI, we
# have to do the work there.
return db.quota_reserve(context, resources, quotas, deltas, expire,
return db.quota_reserve(context, resources, quotas, user_quotas,
deltas, expire,
CONF.until_refresh, CONF.max_age,
project_id=project_id)
project_id=project_id, user_id=user_id)
def commit(self, context, reservations, project_id=None):
def commit(self, context, reservations, project_id=None, user_id=None):
"""Commit reservations.
:param context: The request context, for access checks.
@ -368,14 +490,21 @@ class DbQuotaDriver(object):
:param project_id: Specify the project_id if current context
is admin and admin wants to impact on
common user's tenant.
:param user_id: Specify the user_id if current context
is admin and admin wants to impact on
common user.
"""
# If project_id is None, then we use the project_id in context
if project_id is None:
project_id = context.project_id
# If user_id is None, then we use the user_id in context
if user_id is None:
user_id = context.user_id
db.reservation_commit(context, reservations, project_id=project_id)
db.reservation_commit(context, reservations, project_id=project_id,
user_id=user_id)
def rollback(self, context, reservations, project_id=None):
def rollback(self, context, reservations, project_id=None, user_id=None):
"""Roll back reservations.
:param context: The request context, for access checks.
@ -384,12 +513,19 @@ class DbQuotaDriver(object):
:param project_id: Specify the project_id if current context
is admin and admin wants to impact on
common user's tenant.
:param user_id: Specify the user_id if current context
is admin and admin wants to impact on
common user.
"""
# If project_id is None, then we use the project_id in context
if project_id is None:
project_id = context.project_id
# If user_id is None, then we use the user_id in context
if user_id is None:
user_id = context.user_id
db.reservation_rollback(context, reservations, project_id=project_id)
db.reservation_rollback(context, reservations, project_id=project_id,
user_id=user_id)
def usage_reset(self, context, resources):
"""
@ -415,11 +551,24 @@ class DbQuotaDriver(object):
# Reset the usage to -1, which will force it to be
# refreshed
db.quota_usage_update(elevated, context.project_id,
context.user_id,
resource, in_use=-1)
except exception.QuotaUsageNotFound:
# That means it'll be refreshed anyway
pass
def destroy_all_by_project_and_user(self, context, project_id, user_id):
"""
Destroy all quotas, usages, and reservations associated with a
project and user.
:param context: The request context, for access checks.
:param project_id: The ID of the project being deleted.
:param user_id: The ID of the user being deleted.
"""
db.quota_destroy_all_by_project_and_user(context, project_id, user_id)
def destroy_all_by_project(self, context, project_id):
"""
Destroy all quotas, usages, and reservations associated with a
@ -451,6 +600,11 @@ class NoopQuotaDriver(object):
should not.
"""
def get_by_project_and_user(self, context, project_id, user_id, resource):
"""Get a specific quota by project and user."""
# Unlimited
return -1
def get_by_project(self, context, project_id, resource):
"""Get a specific quota by project."""
# Unlimited
@ -491,9 +645,37 @@ class NoopQuotaDriver(object):
quotas[resource.name] = -1
return quotas
def get_user_quotas(self, context, resources, project_id, user_id,
quota_class=None, defaults=True,
usages=True):
"""
Given a list of resources, retrieve the quotas for the given
user and 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 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.
"""
quotas = {}
for resource in resources.values():
quotas[resource.name] = -1
return quotas
def get_project_quotas(self, context, resources, project_id,
quota_class=None, defaults=True,
usages=True):
usages=True, remains=False):
"""
Given a list of resources, retrieve the quotas for the given
project.
@ -512,13 +694,32 @@ class NoopQuotaDriver(object):
specific value for the resource.
:param usages: If True, the current in_use and reserved counts
will also be returned.
:param remains: If True, the current remains of the project will
will be returned.
"""
quotas = {}
for resource in resources.values():
quotas[resource.name] = -1
return quotas
def limit_check(self, context, resources, values, project_id=None):
def get_settable_quotas(self, context, resources, project_id,
user_id=None):
"""
Given a list of resources, retrieve the range of settable quotas for
the given user or 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 user_id: The ID of the user to return quotas for.
"""
quotas = {}
for resource in resources.values():
quotas[resource.name].update(minimum=0, maximum=-1)
return quotas
def limit_check(self, context, resources, values, project_id=None,
user_id=None):
"""Check simple quota limits.
For limits--those quotas for which there is no usage
@ -541,11 +742,14 @@ class NoopQuotaDriver(object):
:param project_id: Specify the project_id if current context
is admin and admin wants to impact on
common user's tenant.
:param user_id: Specify the user_id if current context
is admin and admin wants to impact on
common user.
"""
pass
def reserve(self, context, resources, deltas, expire=None,
project_id=None):
project_id=None, user_id=None):
"""Check quotas and reserve resources.
For counting quotas--those quotas for which there is a usage
@ -578,10 +782,13 @@ class NoopQuotaDriver(object):
:param project_id: Specify the project_id if current context
is admin and admin wants to impact on
common user's tenant.
:param user_id: Specify the user_id if current context
is admin and admin wants to impact on
common user.
"""
return []
def commit(self, context, reservations, project_id=None):
def commit(self, context, reservations, project_id=None, user_id=None):
"""Commit reservations.
:param context: The request context, for access checks.
@ -590,10 +797,13 @@ class NoopQuotaDriver(object):
:param project_id: Specify the project_id if current context
is admin and admin wants to impact on
common user's tenant.
:param user_id: Specify the user_id if current context
is admin and admin wants to impact on
common user.
"""
pass
def rollback(self, context, reservations, project_id=None):
def rollback(self, context, reservations, project_id=None, user_id=None):
"""Roll back reservations.
:param context: The request context, for access checks.
@ -602,6 +812,9 @@ class NoopQuotaDriver(object):
:param project_id: Specify the project_id if current context
is admin and admin wants to impact on
common user's tenant.
:param user_id: Specify the user_id if current context
is admin and admin wants to impact on
common user.
"""
pass
@ -621,6 +834,17 @@ class NoopQuotaDriver(object):
"""
pass
def destroy_all_by_project_and_user(self, context, project_id, user_id):
"""
Destroy all quotas, usages, and reservations associated with a
project and user.
:param context: The request context, for access checks.
:param project_id: The ID of the project being deleted.
:param user_id: The ID of the user being deleted.
"""
pass
def destroy_all_by_project(self, context, project_id):
"""
Destroy all quotas, usages, and reservations associated with a
@ -829,6 +1053,12 @@ class QuotaEngine(object):
for resource in resources:
self.register_resource(resource)
def get_by_project_and_user(self, context, project_id, user_id, resource):
"""Get a specific quota by project and user."""
return self._driver.get_by_project_and_user(context, project_id,
user_id, resource)
def get_by_project(self, context, project_id, resource):
"""Get a specific quota by project."""
@ -861,8 +1091,32 @@ class QuotaEngine(object):
return self._driver.get_class_quotas(context, self._resources,
quota_class, defaults=defaults)
def get_user_quotas(self, context, project_id, user_id, quota_class=None,
defaults=True, usages=True):
"""Retrieve the quotas for the given user and project.
:param context: The request context, for access checks.
: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.
: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,
project_id, user_id,
quota_class=quota_class,
defaults=defaults,
usages=usages)
def get_project_quotas(self, context, project_id, quota_class=None,
defaults=True, usages=True):
defaults=True, usages=True, remains=False):
"""Retrieve the quotas for the given project.
:param context: The request context, for access checks.
@ -876,13 +1130,31 @@ class QuotaEngine(object):
specific value for the resource.
:param usages: If True, the current in_use and reserved counts
will also be returned.
:param remains: If True, the current remains of the project will
will be returned.
"""
return self._driver.get_project_quotas(context, self._resources,
project_id,
quota_class=quota_class,
defaults=defaults,
usages=usages)
usages=usages,
remains=remains)
def get_settable_quotas(self, context, project_id, user_id=None):
"""
Given a list of resources, retrieve the range of settable quotas for
the given user or 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 user_id: The ID of the user to return quotas for.
"""
return self._driver.get_settable_quotas(context, self._resources,
project_id,
user_id=user_id)
def count(self, context, resource, *args, **kwargs):
"""Count a resource.
@ -903,7 +1175,7 @@ class QuotaEngine(object):
return res.count(context, *args, **kwargs)
def limit_check(self, context, project_id=None, **values):
def limit_check(self, context, project_id=None, user_id=None, **values):
"""Check simple quota limits.
For limits--those quotas for which there is no usage
@ -926,12 +1198,16 @@ class QuotaEngine(object):
:param project_id: Specify the project_id if current context
is admin and admin wants to impact on
common user's tenant.
:param user_id: Specify the user_id if current context
is admin and admin wants to impact on
common user.
"""
return self._driver.limit_check(context, self._resources, values,
project_id=project_id)
project_id=project_id, user_id=user_id)
def reserve(self, context, expire=None, project_id=None, **deltas):
def reserve(self, context, expire=None, project_id=None, user_id=None,
**deltas):
"""Check quotas and reserve resources.
For counting quotas--those quotas for which there is a usage
@ -968,13 +1244,14 @@ class QuotaEngine(object):
reservations = self._driver.reserve(context, self._resources, deltas,
expire=expire,
project_id=project_id)
project_id=project_id,
user_id=user_id)
LOG.debug(_("Created reservations %s"), reservations)
return reservations
def commit(self, context, reservations, project_id=None):
def commit(self, context, reservations, project_id=None, user_id=None):
"""Commit reservations.
:param context: The request context, for access checks.
@ -986,7 +1263,8 @@ class QuotaEngine(object):
"""
try:
self._driver.commit(context, reservations, project_id=project_id)
self._driver.commit(context, reservations, project_id=project_id,
user_id=user_id)
except Exception:
# NOTE(Vek): Ignoring exceptions here is safe, because the
# usage resynchronization and the reservation expiration
@ -996,7 +1274,7 @@ class QuotaEngine(object):
return
LOG.debug(_("Committed reservations %s"), reservations)
def rollback(self, context, reservations, project_id=None):
def rollback(self, context, reservations, project_id=None, user_id=None):
"""Roll back reservations.
:param context: The request context, for access checks.
@ -1008,7 +1286,8 @@ class QuotaEngine(object):
"""
try:
self._driver.rollback(context, reservations, project_id=project_id)
self._driver.rollback(context, reservations, project_id=project_id,
user_id=user_id)
except Exception:
# NOTE(Vek): Ignoring exceptions here is safe, because the
# usage resynchronization and the reservation expiration
@ -1036,6 +1315,19 @@ class QuotaEngine(object):
self._driver.usage_reset(context, resources)
def destroy_all_by_project_and_user(self, context, project_id, user_id):
"""
Destroy all quotas, usages, and reservations associated with a
project and user.
:param context: The request context, for access checks.
:param project_id: The ID of the project being deleted.
:param user_id: The ID of the user being deleted.
"""
self._driver.destroy_all_by_project_and_user(context,
project_id, user_id)
def destroy_all_by_project(self, context, project_id):
"""
Destroy all quotas, usages, and reservations associated with a

View File

@ -100,6 +100,8 @@ class QuotaSetsTest(test.TestCase):
self.assertEqual(res_dict, expected)
def test_quotas_show_as_admin(self):
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234',
use_admin_context=True)
res_dict = self.controller.show(req, 1234)
@ -107,12 +109,15 @@ class QuotaSetsTest(test.TestCase):
self.assertEqual(res_dict, quota_set('1234'))
def test_quotas_show_as_unauthorized_user(self):
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234')
self.assertRaises(webob.exc.HTTPForbidden, self.controller.show,
req, 1234)
def test_quotas_update_as_admin(self):
self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
body = {'quota_set': {'instances': 50, 'cores': 50,
'ram': 51200, 'floating_ips': 10,
@ -132,6 +137,7 @@ class QuotaSetsTest(test.TestCase):
def test_quotas_update_as_user(self):
self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
body = {'quota_set': {'instances': 50, 'cores': 50,
'ram': 51200, 'floating_ips': 10,
@ -148,6 +154,7 @@ class QuotaSetsTest(test.TestCase):
def test_quotas_update_invalid_key(self):
self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
body = {'quota_set': {'instances2': -2, 'cores': -2,
'ram': -2, 'floating_ips': -2,
@ -161,6 +168,7 @@ class QuotaSetsTest(test.TestCase):
def test_quotas_update_invalid_limit(self):
self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
body = {'quota_set': {'instances': -2, 'cores': -2,
'ram': -2, 'floating_ips': -2, 'fixed_ips': -2,
@ -197,6 +205,7 @@ class QuotaSetsTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
use_admin_context=True)
self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
res_dict = self.controller.update(req, 'update_me', body)
self.assertEqual(res_dict, expected_resp)
@ -225,6 +234,7 @@ class QuotaSetsTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
use_admin_context=True)
self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
res_dict = self.controller.update(req, 'update_me', body)
self.assertEqual(res_dict, expected_resp)
@ -243,6 +253,7 @@ class QuotaSetsTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
use_admin_context=True)
self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
req, 'update_me', body)
@ -351,7 +362,7 @@ fake_quotas = {'ram': {'limit': 51200,
'reserved': 0}}
def fake_get_quotas(self, context, id, usages=False):
def fake_get_quotas(self, context, id, user_id=None, usages=False):
if usages:
return fake_quotas
else:
@ -374,6 +385,7 @@ class ExtendedQuotasTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
use_admin_context=True)
self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
@ -394,6 +406,111 @@ class ExtendedQuotasTest(test.TestCase):
fake_quotas.get('instances')['limit'] = 200
self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
res_dict = self.controller.update(req, 'update_me', body)
self.assertEqual(res_dict, expected)
class UserQuotasTest(test.TestCase):
def setUp(self):
super(UserQuotasTest, self).setUp()
self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
self.controller = quotas.QuotaSetsController(self.ext_mgr)
def test_user_quotas_show_as_admin(self):
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1',
use_admin_context=True)
res_dict = self.controller.show(req, 1234)
self.assertEqual(res_dict, quota_set('1234'))
def test_user_quotas_show_as_unauthorized_user(self):
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1')
self.assertRaises(webob.exc.HTTPForbidden, self.controller.show,
req, 1234)
def test_user_quotas_update_as_admin(self):
self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
body = {'quota_set': {'instances': 10, 'cores': 20,
'ram': 51200, 'floating_ips': 10,
'fixed_ips': -1, 'metadata_items': 128,
'injected_files': 5,
'injected_file_content_bytes': 10240,
'injected_file_path_bytes': 255,
'security_groups': 10,
'security_group_rules': 20,
'key_pairs': 100}}
url = '/v2/fake4/os-quota-sets/update_me?user_id=1'
req = fakes.HTTPRequest.blank(url, use_admin_context=True)
res_dict = self.controller.update(req, 'update_me', body)
self.assertEqual(res_dict, body)
def test_user_quotas_update_as_user(self):
self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
body = {'quota_set': {'instances': 10, 'cores': 20,
'ram': 51200, 'floating_ips': 10,
'fixed_ips': -1, 'metadata_items': 128,
'injected_files': 5,
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20,
'key_pairs': 100}}
url = '/v2/fake4/os-quota-sets/update_me?user_id=1'
req = fakes.HTTPRequest.blank(url)
self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
req, 'update_me', body)
def test_user_quotas_update_exceed_project(self):
self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
body = {'quota_set': {'instances': 20}}
url = '/v2/fake4/os-quota-sets/update_me?user_id=1'
req = fakes.HTTPRequest.blank(url, use_admin_context=True)
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
req, 'update_me', body)
def test_delete_user_quotas_when_extension_not_loaded(self):
self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(False)
self.mox.ReplayAll()
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1')
self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
req, 1234)
def test_user_quotas_delete_as_unauthorized_user(self):
self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.ReplayAll()
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1')
self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete,
req, 1234)
def test_user_quotas_delete_as_admin(self):
context = context_maker.get_admin_context()
url = '/v2/fake4/os-quota-sets/1234?user_id=1'
self.req = fakes.HTTPRequest.blank(url)
self.req.environ['nova.context'] = context
self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
self.mox.StubOutWithMock(quota.QUOTAS,
"destroy_all_by_project_and_user")
quota.QUOTAS.destroy_all_by_project_and_user(context, 1234, '1')
self.mox.ReplayAll()
res = self.controller.delete(self.req, 1234)
self.mox.VerifyAll()
self.assertEqual(res.status_int, 202)

View File

@ -276,6 +276,75 @@ class QuotaSetsTest(test.TestCase):
res_dict = self.controller.update(req, 'update_me', body)
self.assertEqual(res_dict, expected)
def test_user_quotas_show_as_admin(self):
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1',
use_admin_context=True)
res_dict = self.controller.show(req, 1234)
self.assertEqual(res_dict, quota_set('1234'))
def test_user_quotas_show_as_unauthorized_user(self):
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1')
self.assertRaises(webob.exc.HTTPForbidden, self.controller.show,
req, 1234)
def test_user_quotas_update_as_admin(self):
body = {'quota_set': {'instances': 10, 'cores': 20,
'ram': 51200, 'floating_ips': 10,
'fixed_ips': -1, 'metadata_items': 128,
'injected_files': 5,
'injected_file_content_bytes': 10240,
'injected_file_path_bytes': 255,
'security_groups': 10,
'security_group_rules': 20,
'key_pairs': 100, 'fixed_ips': -1}}
url = '/v2/fake4/os-quota-sets/update_me?user_id=1'
req = fakes.HTTPRequest.blank(url, use_admin_context=True)
res_dict = self.controller.update(req, 'update_me', body)
self.assertEqual(res_dict, body)
def test_user_quotas_update_as_user(self):
body = {'quota_set': {'instances': 10, 'cores': 20,
'ram': 51200, 'floating_ips': 10,
'fixed_ips': -1, 'metadata_items': 128,
'injected_files': 5,
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20,
'key_pairs': 100}}
url = '/v2/fake4/os-quota-sets/update_me?user_id=1'
req = fakes.HTTPRequest.blank(url)
self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
req, 'update_me', body)
def test_user_quotas_update_exceed_project(self):
body = {'quota_set': {'instances': 20}}
url = '/v2/fake4/os-quota-sets/update_me?user_id=1'
req = fakes.HTTPRequest.blank(url, use_admin_context=True)
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
req, 'update_me', body)
def test_user_quotas_delete_as_unauthorized_user(self):
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1')
self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete,
req, 1234)
def test_user_quotas_delete_as_admin(self):
context = context_maker.get_admin_context()
url = '/v2/fake4/os-quota-sets/1234?user_id=1'
self.req = fakes.HTTPRequest.blank(url)
self.req.environ['nova.context'] = context
self.mox.StubOutWithMock(quota.QUOTAS,
"destroy_all_by_project_and_user")
quota.QUOTAS.destroy_all_by_project_and_user(context, 1234, '1')
self.mox.ReplayAll()
res = self.controller.delete(self.req, 1234)
self.mox.VerifyAll()
self.assertEqual(res.status_int, 202)
class QuotaXMLSerializerTest(test.TestCase):
def setUp(self):
@ -353,7 +422,7 @@ fake_quotas = {'ram': {'limit': 51200,
'reserved': 0}}
def fake_get_quotas(self, context, id, usages=False):
def fake_get_quotas(self, context, id, user_id=None, usages=False):
if usages:
return fake_quotas
else:

View File

@ -2770,31 +2770,39 @@ class ComputeTestCase(BaseTestCase):
for operation in actions:
self._test_state_revert(instance, *operation)
def _ensure_quota_reservations_committed(self, expect_project=False):
def _ensure_quota_reservations_committed(self, expect_project=False,
expect_user=False):
"""Mock up commit of quota reservations."""
reservations = list('fake_res')
self.mox.StubOutWithMock(nova.quota.QUOTAS, 'commit')
nova.quota.QUOTAS.commit(mox.IgnoreArg(), reservations,
project_id=(expect_project and
self.context.project_id or
None))
None),
user_id=(expect_user and
self.context.user_id or
None))
self.mox.ReplayAll()
return reservations
def _ensure_quota_reservations_rolledback(self, expect_project=False):
def _ensure_quota_reservations_rolledback(self, expect_project=False,
expect_user=False):
"""Mock up rollback of quota reservations."""
reservations = list('fake_res')
self.mox.StubOutWithMock(nova.quota.QUOTAS, 'rollback')
nova.quota.QUOTAS.rollback(mox.IgnoreArg(), reservations,
project_id=(expect_project and
self.context.project_id or
None))
None),
user_id=(expect_user and
self.context.user_id or
None))
self.mox.ReplayAll()
return reservations
def test_quotas_succesful_delete(self):
instance = jsonutils.to_primitive(self._create_fake_instance())
resvs = self._ensure_quota_reservations_committed(True)
resvs = self._ensure_quota_reservations_committed(True, True)
self.compute.terminate_instance(self.context, instance,
bdms=None, reservations=resvs)
@ -2807,7 +2815,7 @@ class ComputeTestCase(BaseTestCase):
self.stubs.Set(self.compute, '_shutdown_instance',
fake_shutdown_instance)
resvs = self._ensure_quota_reservations_rolledback(True)
resvs = self._ensure_quota_reservations_rolledback(True, True)
self.assertRaises(test.TestingException,
self.compute.terminate_instance,
self.context, instance,
@ -2816,7 +2824,7 @@ class ComputeTestCase(BaseTestCase):
def test_quotas_succesful_soft_delete(self):
instance = jsonutils.to_primitive(self._create_fake_instance(
params=dict(task_state=task_states.SOFT_DELETING)))
resvs = self._ensure_quota_reservations_committed(True)
resvs = self._ensure_quota_reservations_committed(True, True)
self.compute.soft_delete_instance(self.context, instance,
reservations=resvs)
@ -2830,7 +2838,7 @@ class ComputeTestCase(BaseTestCase):
self.stubs.Set(self.compute.driver, 'soft_delete',
fake_soft_delete)
resvs = self._ensure_quota_reservations_rolledback(True)
resvs = self._ensure_quota_reservations_rolledback(True, True)
self.assertRaises(test.TestingException,
self.compute.soft_delete_instance,
self.context, instance,

View File

@ -388,7 +388,7 @@ class _ComputeAPIUnitTestMixIn(object):
db.instance_update_and_get_original(
self.context, inst.uuid, updates).AndReturn((db_inst, new_inst))
self.compute_api._create_reservations(
self.context, db_inst, new_inst, inst.project_id
self.context, db_inst, new_inst, inst.project_id, inst.user_id
).AndReturn('fake-resv')
if inst.vm_state == vm_states.RESIZED:
@ -422,7 +422,8 @@ class _ComputeAPIUnitTestMixIn(object):
system_metadata='sys-meta')
if inst.host == 'down-host':
quota.QUOTAS.commit(self.context, 'fake-resv',
project_id=inst.project_id)
project_id=inst.project_id,
user_id=inst.user_id)
elif delete_type == 'soft_delete':
self.compute_api._record_action_start(self.context, db_inst,
instance_actions.DELETE)
@ -485,7 +486,8 @@ class _ComputeAPIUnitTestMixIn(object):
self.context, inst.uuid, updates).AndReturn((db_inst, new_inst))
self.compute_api._create_reservations(self.context,
db_inst, new_inst,
inst.project_id).AndReturn(None)
inst.project_id,
inst.user_id).AndReturn(None)
db.constraint(host=mox.IgnoreArg()).AndReturn('constraint')
db.instance_destroy(self.context, inst.uuid, 'constraint')

View File

@ -540,19 +540,25 @@ class _BaseTestCase(object):
def test_quota_commit(self):
self.mox.StubOutWithMock(quota.QUOTAS, 'commit')
quota.QUOTAS.commit(self.context, 'reservations', project_id=None)
quota.QUOTAS.commit(self.context, 'reservations', project_id='proj')
quota.QUOTAS.commit(self.context, 'reservations', project_id=None,
user_id=None)
quota.QUOTAS.commit(self.context, 'reservations', project_id='proj',
user_id='user')
self.mox.ReplayAll()
self.conductor.quota_commit(self.context, 'reservations')
self.conductor.quota_commit(self.context, 'reservations', 'proj')
self.conductor.quota_commit(self.context, 'reservations', 'proj',
'user')
def test_quota_rollback(self):
self.mox.StubOutWithMock(quota.QUOTAS, 'rollback')
quota.QUOTAS.rollback(self.context, 'reservations', project_id=None)
quota.QUOTAS.rollback(self.context, 'reservations', project_id='proj')
quota.QUOTAS.rollback(self.context, 'reservations', project_id=None,
user_id=None)
quota.QUOTAS.rollback(self.context, 'reservations', project_id='proj',
user_id='user')
self.mox.ReplayAll()
self.conductor.quota_rollback(self.context, 'reservations')
self.conductor.quota_rollback(self.context, 'reservations', 'proj')
self.conductor.quota_rollback(self.context, 'reservations', 'proj',
'user')
def test_get_ec2_ids(self):
expected = {

View File

@ -62,7 +62,7 @@ get_engine = db_session.get_engine
get_session = db_session.get_session
def _quota_reserve(context, project_id):
def _quota_reserve(context, project_id, user_id):
"""Create sample Quota, QuotaUsage and Reservation objects.
There is no method db.quota_usage_create(), so we have to use
@ -72,25 +72,28 @@ def _quota_reserve(context, project_id):
"""
def get_sync(resource, usage):
def sync(elevated, project_id, session):
def sync(elevated, project_id, user_id, session):
return {resource: usage}
return sync
quotas = {}
user_quotas = {}
resources = {}
deltas = {}
for i in range(3):
resource = 'resource%d' % i
sync_name = '_sync_%s' % resource
quotas[resource] = db.quota_create(context, project_id, resource, i)
user_quotas[resource] = db.quota_create(context, project_id,
resource, i, user_id=user_id)
resources[resource] = ReservableResource(
resource, sync_name, 'quota_res_%d' % i)
deltas[resource] = i
setattr(sqlalchemy_api, sync_name, get_sync(resource, i))
sqlalchemy_api.QUOTA_SYNC_FUNCTIONS[sync_name] = getattr(
sqlalchemy_api, sync_name)
return db.quota_reserve(context, resources, quotas, deltas,
return db.quota_reserve(context, resources, quotas, user_quotas, deltas,
timeutils.utcnow(), timeutils.utcnow(),
datetime.timedelta(days=1), project_id)
datetime.timedelta(days=1), project_id, user_id)
class DbTestCase(test.TestCase):
@ -834,6 +837,7 @@ class ReservationTestCase(test.TestCase, ModelsObjectComparatorMixin):
self.ctxt = context.get_admin_context()
self.values = {'uuid': 'sample-uuid',
'project_id': 'project1',
'user_id': 'user1',
'resource': 'resource',
'delta': 42,
'expire': timeutils.utcnow() + datetime.timedelta(days=1),
@ -858,55 +862,54 @@ class ReservationTestCase(test.TestCase, ModelsObjectComparatorMixin):
self.ctxt, 'non-exitent-resevation-uuid')
def test_reservation_commit(self):
reservations = _quota_reserve(self.ctxt, 'project1')
expected = {'project_id': 'project1',
reservations = _quota_reserve(self.ctxt, 'project1', 'user1')
expected = {'project_id': 'project1', 'user_id': 'user1',
'resource0': {'reserved': 0, 'in_use': 0},
'resource1': {'reserved': 1, 'in_use': 1},
'resource2': {'reserved': 2, 'in_use': 2}}
self.assertEqual(expected, db.quota_usage_get_all_by_project(
self.ctxt, 'project1'))
self.assertEqual(expected, db.quota_usage_get_all_by_project_and_user(
self.ctxt, 'project1', 'user1'))
db.reservation_get(self.ctxt, reservations[0])
db.reservation_commit(self.ctxt, reservations, 'project1')
db.reservation_commit(self.ctxt, reservations, 'project1', 'user1')
self.assertRaises(exception.ReservationNotFound,
db.reservation_get, self.ctxt, reservations[0])
expected = {'project_id': 'project1',
expected = {'project_id': 'project1', 'user_id': 'user1',
'resource0': {'reserved': 0, 'in_use': 0},
'resource1': {'reserved': 0, 'in_use': 2},
'resource2': {'reserved': 0, 'in_use': 4}}
self.assertEqual(expected, db.quota_usage_get_all_by_project(
self.ctxt, 'project1'))
self.assertEqual(expected, db.quota_usage_get_all_by_project_and_user(
self.ctxt, 'project1', 'user1'))
def test_reservation_rollback(self):
reservations = _quota_reserve(self.ctxt, 'project1')
expected = {'project_id': 'project1',
reservations = _quota_reserve(self.ctxt, 'project1', 'user1')
expected = {'project_id': 'project1', 'user_id': 'user1',
'resource0': {'reserved': 0, 'in_use': 0},
'resource1': {'reserved': 1, 'in_use': 1},
'resource2': {'reserved': 2, 'in_use': 2}}
self.assertEqual(expected, db.quota_usage_get_all_by_project(
self.ctxt, 'project1'))
self.assertEqual(expected, db.quota_usage_get_all_by_project_and_user(
self.ctxt, 'project1', 'user1'))
db.reservation_get(self.ctxt, reservations[0])
db.reservation_rollback(self.ctxt, reservations, 'project1')
db.reservation_rollback(self.ctxt, reservations, 'project1', 'user1')
self.assertRaises(exception.ReservationNotFound,
db.reservation_get, self.ctxt, reservations[0])
expected = {'project_id': 'project1',
expected = {'project_id': 'project1', 'user_id': 'user1',
'resource0': {'reserved': 0, 'in_use': 0},
'resource1': {'reserved': 0, 'in_use': 1},
'resource2': {'reserved': 0, 'in_use': 2}}
self.assertEqual(expected, db.quota_usage_get_all_by_project(
self.ctxt, 'project1'))
self.assertEqual(expected, db.quota_usage_get_all_by_project_and_user(
self.ctxt, 'project1', 'user1'))
def test_reservation_expire(self):
self.values['expire'] = timeutils.utcnow() + datetime.timedelta(days=1)
_quota_reserve(self.ctxt, 'project1')
_quota_reserve(self.ctxt, 'project1', 'user1')
db.reservation_expire(self.ctxt)
expected = {'project_id': 'project1',
expected = {'project_id': 'project1', 'user_id': 'user1',
'resource0': {'reserved': 0, 'in_use': 0},
'resource1': {'reserved': 0, 'in_use': 1},
'resource2': {'reserved': 0, 'in_use': 2}}
self.assertEqual(expected, db.quota_usage_get_all_by_project(
self.ctxt, 'project1'))
self.assertEqual(expected, db.quota_usage_get_all_by_project_and_user(
self.ctxt, 'project1', 'user1'))
class SecurityGroupRuleTestCase(test.TestCase, ModelsObjectComparatorMixin):
@ -4715,7 +4718,7 @@ class QuotaTestCase(test.TestCase, ModelsObjectComparatorMixin):
db.security_group_create(self.ctxt, {'project_id': 'project1'})
reservations_uuids = db.quota_reserve(self.ctxt, reservable_resources,
quotas, deltas, None,
quotas, quotas, deltas, None,
None, None, 'project1')
resources_names = reservable_resources.keys()
for reservation_uuid in reservations_uuids:
@ -4730,7 +4733,7 @@ class QuotaTestCase(test.TestCase, ModelsObjectComparatorMixin):
self.assertEqual(len(resources_names), 0)
def test_quota_destroy_all_by_project(self):
reservations = _quota_reserve(self.ctxt, 'project1')
reservations = _quota_reserve(self.ctxt, 'project1', 'user1')
db.quota_destroy_all_by_project(self.ctxt, 'project1')
self.assertEqual(db.quota_get_all_by_project(self.ctxt, 'project1'),
{'project_id': 'project1'})
@ -4746,7 +4749,7 @@ class QuotaTestCase(test.TestCase, ModelsObjectComparatorMixin):
self.ctxt, 'p1', 'nonexitent_resource')
def test_quota_usage_get(self):
_quota_reserve(self.ctxt, 'p1')
_quota_reserve(self.ctxt, 'p1', 'u1')
quota_usage = db.quota_usage_get(self.ctxt, 'p1', 'resource0')
expected = {'resource': 'resource0', 'project_id': 'p1',
'in_use': 0, 'reserved': 0, 'total': 0}
@ -4754,7 +4757,7 @@ class QuotaTestCase(test.TestCase, ModelsObjectComparatorMixin):
self.assertEqual(value, quota_usage[key])
def test_quota_usage_get_all_by_project(self):
_quota_reserve(self.ctxt, 'p1')
_quota_reserve(self.ctxt, 'p1', 'u1')
expected = {'project_id': 'p1',
'resource0': {'in_use': 0, 'reserved': 0},
'resource1': {'in_use': 1, 'reserved': 1},
@ -4764,15 +4767,15 @@ class QuotaTestCase(test.TestCase, ModelsObjectComparatorMixin):
def test_quota_usage_update_nonexistent(self):
self.assertRaises(exception.QuotaUsageNotFound, db.quota_usage_update,
self.ctxt, 'p1', 'resource', in_use=42)
self.ctxt, 'p1', 'u1', 'resource', in_use=42)
def test_quota_usage_update(self):
_quota_reserve(self.ctxt, 'p1')
db.quota_usage_update(self.ctxt, 'p1', 'resource0', in_use=42,
_quota_reserve(self.ctxt, 'p1', 'u1')
db.quota_usage_update(self.ctxt, 'p1', 'u1', 'resource0', in_use=42,
reserved=43)
quota_usage = db.quota_usage_get(self.ctxt, 'p1', 'resource0')
quota_usage = db.quota_usage_get(self.ctxt, 'p1', 'resource0', 'u1')
expected = {'resource': 'resource0', 'project_id': 'p1',
'in_use': 42, 'reserved': 43, 'total': 85}
'user_id': 'u1', 'in_use': 42, 'reserved': 43, 'total': 85}
for key, value in expected.iteritems():
self.assertEqual(value, quota_usage[key])

View File

@ -2332,6 +2332,63 @@ class TestNovaMigrations(BaseMigrationTestCase, CommonTestsMixIn):
{'instance_type_id': 35, 'key': 'key1',
'deleted': 0})
# migration 203 - make user quotas key and value
def _pre_upgrade_203(self, engine):
quota_usages = db_utils.get_table(engine, 'quota_usages')
reservations = db_utils.get_table(engine, 'reservations')
fake_quota_usages = {'id': 5,
'resource': 'instances',
'in_use': 1,
'reserved': 1}
fake_reservations = {'id': 6,
'uuid': 'fake_reservationo_uuid',
'usage_id': 5,
'resource': 'instances',
'delta': 1,
'expire': timeutils.utcnow()}
quota_usages.insert().execute(fake_quota_usages)
reservations.insert().execute(fake_reservations)
def _check_203(self, engine, data):
project_user_quotas = db_utils.get_table(engine, 'project_user_quotas')
fake_quotas = {'id': 4,
'project_id': 'fake_project',
'user_id': 'fake_user',
'resource': 'instances',
'hard_limit': 10}
project_user_quotas.insert().execute(fake_quotas)
quota_usages = db_utils.get_table(engine, 'quota_usages')
reservations = db_utils.get_table(engine, 'reservations')
# Get the record
quota = project_user_quotas.select().execute().first()
quota_usage = quota_usages.select().execute().first()
reservation = reservations.select().execute().first()
self.assertEqual(quota['id'], 4)
self.assertEqual(quota['project_id'], 'fake_project')
self.assertEqual(quota['user_id'], 'fake_user')
self.assertEqual(quota['resource'], 'instances')
self.assertEqual(quota['hard_limit'], 10)
self.assertEqual(quota_usage['user_id'], None)
self.assertEqual(reservation['user_id'], None)
def _post_downgrade_203(self, engine):
try:
table_exist = True
db_utils.get_table(engine, 'project_user_quotas')
except Exception:
table_exist = False
quota_usages = db_utils.get_table(engine, 'quota_usages')
reservations = db_utils.get_table(engine, 'reservations')
# Get the record
quota_usage = quota_usages.select().execute().first()
reservation = reservations.select().execute().first()
self.assertFalse('user_id' in quota_usage)
self.assertFalse('user_id' in reservation)
self.assertFalse(table_exist)
class TestBaremetalMigrations(BaseMigrationTestCase, CommonTestsMixIn):
"""Test sqlalchemy-migrate migrations."""

View File

@ -464,6 +464,14 @@
"namespace": "http://docs.openstack.org/compute/ext/quotas-sets/api/v1.1",
"updated": "%(timestamp)s"
},
{
"alias": "os-user-quotas",
"description": "%(text)s",
"links": [],
"name": "UserQuotas",
"namespace": "http://docs.openstack.org/compute/ext/user_quotas/api/v1.1",
"updated": "%(timestamp)s"
},
{
"alias": "os-rescue",
"description": "%(text)s",

View File

@ -174,6 +174,9 @@
<extension alias="os-quota-sets" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/quotas-sets/api/v1.1" name="Quotas">
<description>%(text)s</description>
</extension>
<extension alias="os-user-quotas" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/user_quotas/api/v1.1" name="UserQuotas">
<description>%(text)s</description>
</extension>
<extension alias="os-rescue" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/rescue/api/v1.1" name="Rescue">
<description>%(text)s</description>
</extension>

View File

@ -0,0 +1,17 @@
{
"quota_set": {
"cores": 20,
"floating_ips": 10,
"fixed_ips": -1,
"id": "fake_tenant",
"injected_file_content_bytes": 10240,
"injected_file_path_bytes": 255,
"injected_files": 5,
"instances": 10,
"key_pairs": 100,
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 10
}
}

View File

@ -0,0 +1,15 @@
<?xml version='1.0' encoding='UTF-8'?>
<quota_set id="fake_tenant">
<cores>20</cores>
<floating_ips>10</floating_ips>
<fixed_ips>-1</fixed_ips>
<injected_file_content_bytes>10240</injected_file_content_bytes>
<injected_file_path_bytes>255</injected_file_path_bytes>
<injected_files>5</injected_files>
<instances>10</instances>
<key_pairs>100</key_pairs>
<metadata_items>128</metadata_items>
<ram>51200</ram>
<security_group_rules>20</security_group_rules>
<security_groups>10</security_groups>
</quota_set>

View File

@ -0,0 +1,6 @@
{
"quota_set": {
"force": "True",
"instances": 9
}
}

View File

@ -0,0 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
<quota_set id="fake_tenant">
<force>True</force>
<instances>9</instances>
</quota_set>

View File

@ -0,0 +1,16 @@
{
"quota_set": {
"cores": 20,
"floating_ips": 10,
"fixed_ips": -1,
"injected_file_content_bytes": 10240,
"injected_file_path_bytes": 255,
"injected_files": 5,
"instances": 9,
"key_pairs": 100,
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 10
}
}

View File

@ -0,0 +1,15 @@
<?xml version='1.0' encoding='UTF-8'?>
<quota_set>
<cores>20</cores>
<floating_ips>10</floating_ips>
<fixed_ips>-1</fixed_ips>
<injected_file_content_bytes>10240</injected_file_content_bytes>
<injected_file_path_bytes>255</injected_file_path_bytes>
<injected_files>5</injected_files>
<instances>9</instances>
<key_pairs>100</key_pairs>
<metadata_items>128</metadata_items>
<ram>51200</ram>
<security_group_rules>20</security_group_rules>
<security_groups>10</security_groups>
</quota_set>

View File

@ -2403,6 +2403,39 @@ class ExtendedQuotasSampleXmlTests(ExtendedQuotasSampleJsonTests):
ctype = "xml"
class UserQuotasSampleJsonTests(ApiSampleTestBase):
extends_name = "nova.api.openstack.compute.contrib.quotas.Quotas"
extension_name = ("nova.api.openstack.compute.contrib"
".user_quotas.User_quotas")
def fake_load(self, *args):
return True
def test_show_quotas_for_user(self):
# Get api sample to show quotas for user.
response = self._do_get('os-quota-sets/fake_tenant?user_id=1')
self._verify_response('user-quotas-show-get-resp', {}, response, 200)
def test_delete_quotas_for_user(self):
# Get api sample to delete quota for user.
self.stubs.Set(ext_mgr, "is_loaded", self.fake_load)
response = self._do_delete('os-quota-sets/fake_tenant?user_id=1')
self.assertEqual(response.status, 202)
self.assertEqual(response.read(), '')
def test_update_quotas_for_user(self):
# Get api sample to update quotas for user.
response = self._do_put('os-quota-sets/fake_tenant?user_id=1',
'user-quotas-update-post-req',
{})
return self._verify_response('user-quotas-update-post-resp', {},
response, 200)
class UserQuotasSampleXmlTests(UserQuotasSampleJsonTests):
ctype = "xml"
class ExtendedIpsSampleJsonTests(ServersSampleBase):
extension_name = ("nova.api.openstack.compute.contrib"
".extended_ips.Extended_ips")

File diff suppressed because it is too large Load Diff