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:
parent
abb7527633
commit
77b4012a02
doc/api_samples
all_extensions
os-user-quotas
nova
api/openstack/compute
cmd
compute
conductor
db
exception.pyquota.pytests
api/openstack/compute
compute
conductor
db
integrated
test_quota.py@ -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.",
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
15
doc/api_samples/os-user-quotas/user-quotas-show-get-resp.xml
Normal file
15
doc/api_samples/os-user-quotas/user-quotas-show-get-resp.xml
Normal 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>
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"quota_set": {
|
||||
"force": "True",
|
||||
"instances": 9
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<quota_set id="fake_tenant">
|
||||
<force>True</force>
|
||||
<instances>9</instances>
|
||||
</quota_set>
|
@ -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
|
||||
}
|
||||
}
|
@ -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>
|
@ -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()
|
||||
|
27
nova/api/openstack/compute/contrib/user_quotas.py
Normal file
27
nova/api/openstack/compute/contrib/user_quotas.py
Normal 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"
|
@ -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()
|
||||
|
@ -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'],
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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 = {}
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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
|
@ -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)
|
||||
|
@ -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.")
|
||||
|
||||
|
422
nova/quota.py
422
nova/quota.py
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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>
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"quota_set": {
|
||||
"force": "True",
|
||||
"instances": 9
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<quota_set id="fake_tenant">
|
||||
<force>True</force>
|
||||
<instances>9</instances>
|
||||
</quota_set>
|
16
nova/tests/integrated/api_samples/os-user-quotas/user-quotas-update-post-resp.json.tpl
Normal file
16
nova/tests/integrated/api_samples/os-user-quotas/user-quotas-update-post-resp.json.tpl
Normal 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
|
||||
}
|
||||
}
|
15
nova/tests/integrated/api_samples/os-user-quotas/user-quotas-update-post-resp.xml.tpl
Normal file
15
nova/tests/integrated/api_samples/os-user-quotas/user-quotas-update-post-resp.xml.tpl
Normal 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>
|
@ -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
Loading…
x
Reference in New Issue
Block a user