Add quotas for Server Groups (V2 API compatibility & V2.1 support)

Server groups can be used to control the affinity and anti-affinity
scheduling policy for a group of servers (instances).  Whilst this is
a useful mechanism for users such scheduling decisions need to be
balanced by a deployers requirements to make effective use of the
available capacity.

This change adds quota values to constrain the number and size of
server groups a user can create.

Two new quota values are be introduced to limit the number of server
groups and the number of servers in a server group. These will follow
the existing pattern for quotas in that:

* They are defined by config values, which also include the default
  value

* They can be defined per project or per user within a project

* A value of -1 for either quota will be treated as unlimited

* Defaults can be set via the quota groups API

* Values may be changed at any time but will only take effect at the next
  server group or server create.   Reducing the quota will not affect any
  existing groups, but new servers will not be allowed into group
  that have become over quota.

This is part one of a linked sequences of changes that implement the
new quotas - split to make the reviews easier.

This part adds the definition of the new quota values, but leaves the
V2 API unchanged.  The V2.1 API is updated as it shows all quota values.

The second part adds the new V2 API extension to make the new quota
values visible and changeable.  At this point a Tempest change is
required to get a clean run as it checks for a specific set of
values.

The third part implements the quota checks themselves.

Thanks to Cyril Roelandt for supplying some of the unit tests.

Co-authored-by: Cyril Roelandt <cyril.roelandt@enovance.com>

Implements: blueprint server-group-quotas
DocImpact

Change-Id: Ib281e43eabfbd176454bde7f0622d46fb04fcb79
This commit is contained in:
Phil Day 2014-07-04 21:03:48 +00:00
parent 989f054a05
commit 597c46fde1
33 changed files with 491 additions and 92 deletions

View File

@ -12,6 +12,8 @@
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 10
"security_groups": 10,
"server_groups": 10,
"server_group_members": 10
}
}

View File

@ -12,6 +12,8 @@
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 10
"security_groups": 10,
"server_groups": 10,
"server_group_members": 10
}
}

View File

@ -12,6 +12,8 @@
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 10
"security_groups": 10,
"server_groups": 10,
"server_group_members": 10
}
}

View File

@ -12,6 +12,8 @@
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 45
"security_groups": 45,
"server_groups": 10,
"server_group_members": 10
}
}

View File

@ -12,6 +12,8 @@
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 10
"security_groups": 10,
"server_groups": 10,
"server_group_members": 10
}
}

View File

@ -12,6 +12,8 @@
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 10
"security_groups": 10,
"server_groups": 10,
"server_group_members": 10
}
}

View File

@ -12,11 +12,14 @@
"maxTotalInstances": 10,
"maxTotalKeypairs": 100,
"maxTotalRAMSize": 51200,
"maxServerGroups": 10,
"maxServerGroupMembers": 10,
"totalCoresUsed": 0,
"totalInstancesUsed": 0,
"totalRAMUsed": 0,
"totalSecurityGroupsUsed": 0,
"totalFloatingIpsUsed": 0
"totalFloatingIpsUsed": 0,
"totalServerGroupsUsed": 0
},
"rate": []
}

View File

@ -27,6 +27,9 @@ from nova import utils
QUOTAS = quota.QUOTAS
# Quotas that are only enabled by specific extensions
EXTENDED_QUOTAS = {'server_groups': 'os-server-group-quotas',
'server_group_members': 'os-server-group-quotas'}
authorize = extensions.extension_authorizer('compute', 'quota_classes')
@ -39,21 +42,35 @@ class QuotaClassTemplate(xmlutil.TemplateBuilder):
root.set('id')
for resource in QUOTAS.resources:
elem = xmlutil.SubTemplateElement(root, resource)
elem.text = resource
if resource not in EXTENDED_QUOTAS:
elem = xmlutil.SubTemplateElement(root, resource)
elem.text = resource
return xmlutil.MasterTemplate(root, 1)
class QuotaClassSetsController(wsgi.Controller):
supported_quotas = []
def __init__(self, ext_mgr):
self.ext_mgr = ext_mgr
self.supported_quotas = QUOTAS.resources
for resource, extension in EXTENDED_QUOTAS.items():
if not self.ext_mgr.is_loaded(extension):
self.supported_quotas.remove(resource)
def _format_quota_set(self, quota_class, quota_set):
"""Convert the quota object to a result dict."""
result = dict(id=str(quota_class))
if quota_class:
result = dict(id=str(quota_class))
else:
result = {}
for resource in QUOTAS.resources:
result[resource] = quota_set[resource]
for resource in self.supported_quotas:
if resource in quota_set:
result[resource] = quota_set[resource]
return dict(quota_class_set=result)
@ -63,8 +80,8 @@ class QuotaClassSetsController(wsgi.Controller):
authorize(context)
try:
nova.context.authorize_quota_class_context(context, id)
return self._format_quota_set(id,
QUOTAS.get_class_quotas(context, id))
values = QUOTAS.get_class_quotas(context, id)
return self._format_quota_set(id, values)
except exception.Forbidden:
raise webob.exc.HTTPForbidden()
@ -73,27 +90,39 @@ class QuotaClassSetsController(wsgi.Controller):
context = req.environ['nova.context']
authorize(context)
quota_class = id
bad_keys = []
if not self.is_valid_body(body, 'quota_class_set'):
msg = _("quota_class_set not specified")
raise webob.exc.HTTPBadRequest(explanation=msg)
quota_class_set = body['quota_class_set']
for key in quota_class_set.keys():
if key in QUOTAS:
try:
value = utils.validate_integer(
if key not in self.supported_quotas:
bad_keys.append(key)
continue
try:
value = utils.validate_integer(
body['quota_class_set'][key], key)
except exception.InvalidInput as e:
raise webob.exc.HTTPBadRequest(
explanation=e.format_message())
try:
db.quota_class_update(context, quota_class, key, value)
except exception.QuotaClassNotFound:
db.quota_class_create(context, quota_class, key, value)
except exception.AdminRequired:
raise webob.exc.HTTPForbidden()
return {'quota_class_set': QUOTAS.get_class_quotas(context,
quota_class)}
except exception.InvalidInput as e:
raise webob.exc.HTTPBadRequest(
explanation=e.format_message())
if bad_keys:
msg = _("Bad key(s) %s in quota_set") % ",".join(bad_keys)
raise webob.exc.HTTPBadRequest(explanation=msg)
for key in quota_class_set.keys():
value = utils.validate_integer(
body['quota_class_set'][key], key)
try:
db.quota_class_update(context, quota_class, key, value)
except exception.QuotaClassNotFound:
db.quota_class_create(context, quota_class, key, value)
except exception.AdminRequired:
raise webob.exc.HTTPForbidden()
values = QUOTAS.get_class_quotas(context, quota_class)
return self._format_quota_set(None, values)
class Quota_classes(extensions.ExtensionDescriptor):
@ -109,7 +138,7 @@ class Quota_classes(extensions.ExtensionDescriptor):
resources = []
res = extensions.ResourceExtension('os-quota-class-sets',
QuotaClassSetsController())
QuotaClassSetsController(self.ext_mgr))
resources.append(res)
return resources

View File

@ -30,9 +30,14 @@ from nova import utils
QUOTAS = quota.QUOTAS
LOG = logging.getLogger(__name__)
NON_QUOTA_KEYS = ['tenant_id', 'id', 'force']
# Quotas that are only enabled by specific extensions
EXTENDED_QUOTAS = {'server_groups': 'os-server-group-quotas',
'server_group_members': 'os-server-group-quotas'}
LOG = logging.getLogger(__name__)
authorize_update = extensions.extension_authorizer('compute', 'quotas:update')
authorize_show = extensions.extension_authorizer('compute', 'quotas:show')
@ -45,24 +50,35 @@ class QuotaTemplate(xmlutil.TemplateBuilder):
root.set('id')
for resource in QUOTAS.resources:
elem = xmlutil.SubTemplateElement(root, resource)
elem.text = resource
if resource not in EXTENDED_QUOTAS:
elem = xmlutil.SubTemplateElement(root, resource)
elem.text = resource
return xmlutil.MasterTemplate(root, 1)
class QuotaSetsController(wsgi.Controller):
supported_quotas = []
def __init__(self, ext_mgr):
self.ext_mgr = ext_mgr
self.supported_quotas = QUOTAS.resources
for resource, extension in EXTENDED_QUOTAS.items():
if not self.ext_mgr.is_loaded(extension):
self.supported_quotas.remove(resource)
def _format_quota_set(self, project_id, quota_set):
"""Convert the quota object to a result dict."""
result = dict(id=str(project_id))
if project_id:
result = dict(id=str(project_id))
else:
result = {}
for resource in QUOTAS.resources:
result[resource] = quota_set[resource]
for resource in self.supported_quotas:
if resource in quota_set:
result[resource] = quota_set[resource]
return dict(quota_set=result)
@ -140,9 +156,10 @@ class QuotaSetsController(wsgi.Controller):
msg = _("quota_set not specified")
raise webob.exc.HTTPBadRequest(explanation=msg)
quota_set = body['quota_set']
for key, value in quota_set.items():
if (key not in QUOTAS and
key not in NON_QUOTA_KEYS):
if (key not in self.supported_quotas
and key not in NON_QUOTA_KEYS):
bad_keys.append(key)
continue
if key == 'force' and extended_loaded:
@ -158,7 +175,7 @@ class QuotaSetsController(wsgi.Controller):
LOG.debug("force update quotas: %s", force_update)
if len(bad_keys) > 0:
if bad_keys:
msg = _("Bad key(s) %s in quota_set") % ",".join(bad_keys)
raise webob.exc.HTTPBadRequest(explanation=msg)
@ -203,13 +220,15 @@ class QuotaSetsController(wsgi.Controller):
key, value, user_id=user_id)
except exception.AdminRequired:
raise webob.exc.HTTPForbidden()
return {'quota_set': self._get_quotas(context, id, user_id=user_id)}
values = self._get_quotas(context, id, user_id=user_id)
return self._format_quota_set(None, values)
@wsgi.serializers(xml=QuotaTemplate)
def defaults(self, req, id):
context = req.environ['nova.context']
authorize_show(context)
return self._format_quota_set(id, QUOTAS.get_defaults(context))
values = QUOTAS.get_defaults(context)
return self._format_quota_set(id, values)
def delete(self, req, id):
if self.ext_mgr.is_loaded('os-extended-quotas'):

View File

@ -60,6 +60,9 @@ class UsedLimitsController(wsgi.Controller):
'totalFloatingIpsUsed': 'floating_ips',
'totalSecurityGroupsUsed': 'security_groups',
}
if self.ext_mgr.is_loaded('os-server-group-quotas'):
quota_map['totalServerGroupsUsed'] = 'server_groups'
used_limits = {}
for display_name, key in quota_map.iteritems():
if key in quotas:

View File

@ -40,7 +40,7 @@ class LimitsController(object):
return builder.build(rate_limits, abs_limits)
def _get_view_builder(self, req):
return limits_views.ViewBuilder()
return limits_views.ViewBuilderV3()
class Limits(extensions.V3APIExtensionBase):

View File

@ -45,6 +45,7 @@ class UsedLimitsController(wsgi.Controller):
'totalInstancesUsed': 'instances',
'totalFloatingIpsUsed': 'floating_ips',
'totalSecurityGroupsUsed': 'security_groups',
'totalServerGroupsUsed': 'server_groups',
}
used_limits = {}
for display_name, key in quota_map.iteritems():
@ -81,4 +82,4 @@ class UsedLimits(extensions.V3APIExtensionBase):
return [limits_ext]
def get_resources(self):
return []
return []

View File

@ -39,6 +39,8 @@ update = {
'injected_files': common_quota,
'injected_file_content_bytes': common_quota,
'injected_file_path_bytes': common_quota,
'server_groups': common_quota,
'server_group_members': common_quota,
'force': parameter_types.boolean,
},
'additionalProperties': False,

View File

@ -21,6 +21,22 @@ from nova.openstack.common import timeutils
class ViewBuilder(object):
"""OpenStack API base limits view builder."""
limit_names = {}
def __init__(self):
self.limit_names = {
"ram": ["maxTotalRAMSize"],
"instances": ["maxTotalInstances"],
"cores": ["maxTotalCores"],
"key_pairs": ["maxTotalKeypairs"],
"floating_ips": ["maxTotalFloatingIps"],
"metadata_items": ["maxServerMeta", "maxImageMeta"],
"injected_files": ["maxPersonality"],
"injected_file_content_bytes": ["maxPersonalitySize"],
"security_groups": ["maxSecurityGroups"],
"security_group_rules": ["maxSecurityGroupRules"],
}
def build(self, rate_limits, absolute_limits):
rate_limits = self._build_rate_limits(rate_limits)
absolute_limits = self._build_absolute_limits(absolute_limits)
@ -41,22 +57,10 @@ class ViewBuilder(object):
For example: {"ram": 512, "gigabytes": 1024}.
"""
limit_names = {
"ram": ["maxTotalRAMSize"],
"instances": ["maxTotalInstances"],
"cores": ["maxTotalCores"],
"key_pairs": ["maxTotalKeypairs"],
"floating_ips": ["maxTotalFloatingIps"],
"metadata_items": ["maxServerMeta", "maxImageMeta"],
"injected_files": ["maxPersonality"],
"injected_file_content_bytes": ["maxPersonalitySize"],
"security_groups": ["maxSecurityGroups"],
"security_group_rules": ["maxSecurityGroupRules"],
}
limits = {}
for name, value in absolute_limits.iteritems():
if name in limit_names and value is not None:
for name in limit_names[name]:
if name in self.limit_names and value is not None:
for name in self.limit_names[name]:
limits[name] = value
return limits
@ -100,6 +104,8 @@ class ViewBuilder(object):
class ViewBuilderV3(ViewBuilder):
def build(self, rate_limits):
rate_limits = self._build_rate_limits(rate_limits)
return {"limits": {"rate": rate_limits}}
def __init__(self):
super(ViewBuilderV3, self).__init__()
# NOTE In v2.0 these are added by a specific extension
self.limit_names["server_groups"] = ["maxServerGroups"]
self.limit_names["server_group_members"] = ["maxServerGroupMembers"]

View File

@ -333,11 +333,17 @@ 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))
def _sync_server_groups(context, project_id, user_id, session):
return dict(server_groups=_instance_group_count_by_project_and_user(
context, project_id, user_id, session))
QUOTA_SYNC_FUNCTIONS = {
'_sync_instances': _sync_instances,
'_sync_floating_ips': _sync_floating_ips,
'_sync_fixed_ips': _sync_fixed_ips,
'_sync_security_groups': _sync_security_groups,
'_sync_server_groups': _sync_server_groups,
}
###################
@ -5882,6 +5888,15 @@ def instance_group_get_all_by_project_id(context, project_id):
all()
def _instance_group_count_by_project_and_user(context, project_id,
user_id, session=None):
return model_query(context, models.InstanceGroup, read_deleted="no",
session=session).\
filter_by(project_id=project_id).\
filter_by(user_id=user_id).\
count()
def _instance_group_model_get_query(context, model_class, group_id,
session=None, read_deleted='no'):
return model_query(context,

View File

@ -30,7 +30,8 @@ class InstanceGroup(base.NovaPersistentObject, base.NovaObject):
# Version 1.5: Add get_hosts()
# Version 1.6: Add get_by_name()
# Version 1.7: Deprecate metadetails
VERSION = '1.7'
# Version 1.8: Add count_members_by_user()
VERSION = '1.8'
fields = {
'id': fields.IntegerField(),
@ -160,6 +161,15 @@ class InstanceGroup(base.NovaPersistentObject, base.NovaObject):
return list(set([instance.host for instance in instances
if instance.host]))
@base.remotable
def count_members_by_user(self, context, user_id):
"""Count the number of instances in a group belonging to a user."""
filter_uuids = self.members
filters = {'uuid': filter_uuids, 'user_id': user_id, 'deleted': False}
instances = objects.InstanceList.get_by_filters(context,
filters=filters)
return len(instances)
class InstanceGroupList(base.ObjectListBase, base.NovaObject):
# Version 1.0: Initial version
@ -168,7 +178,8 @@ class InstanceGroupList(base.ObjectListBase, base.NovaObject):
# Version 1.2: InstanceGroup <= version 1.5
# Version 1.3: InstanceGroup <= version 1.6
# Version 1.4: InstanceGroup <= version 1.7
VERSION = '1.2'
# Version 1.5: InstanceGroup <= version 1.8
VERSION = '1.5'
fields = {
'objects': fields.ListOfObjectsField('InstanceGroup'),
@ -180,6 +191,7 @@ class InstanceGroupList(base.ObjectListBase, base.NovaObject):
'1.2': '1.5',
'1.3': '1.6',
'1.4': '1.7',
'1.5': '1.8',
}
@base.remotable_classmethod

View File

@ -39,6 +39,13 @@ def ids_from_security_group(context, security_group):
return ids_from_instance(context, security_group)
# TODO(PhilD): This method needs to be cleaned up once the
# ids_from_instance helper method is renamed or some common
# method is added for objects.quotas.
def ids_from_server_group(context, server_group):
return ids_from_instance(context, server_group)
class Quotas(base.NovaObject):
# Version 1.0: initial version
# Version 1.1: Added create_limit() and update_limit()

View File

@ -72,6 +72,12 @@ quota_opts = [
cfg.IntOpt('quota_key_pairs',
default=100,
help='Number of key pairs per user'),
cfg.IntOpt('quota_server_groups',
default=10,
help='Number of server groups per project'),
cfg.IntOpt('quota_server_group_members',
default=10,
help='Number of servers per server group'),
cfg.IntOpt('reservation_expire',
default=86400,
help='Number of seconds until a reservation expires'),
@ -1416,6 +1422,11 @@ def _keypair_get_count_by_user(*args, **kwargs):
return objects.KeyPairList.get_count_by_user(*args, **kwargs)
def _server_group_count_members_by_user(*args, **kwargs):
"""Helper method to avoid referencing objects.InstanceGroup on import."""
return objects.InstanceGroup.count_members_by_user(*args, **kwargs)
QUOTAS = QuotaEngine()
@ -1439,6 +1450,11 @@ resources = [
'quota_security_group_rules'),
CountableResource('key_pairs', _keypair_get_count_by_user,
'quota_key_pairs'),
ReservableResource('server_groups', '_sync_server_groups',
'quota_server_groups'),
CountableResource('server_group_members',
_server_group_count_members_by_user,
'quota_server_group_members'),
]

View File

@ -17,6 +17,7 @@ from lxml import etree
import webob
from nova.api.openstack.compute.contrib import quota_classes
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova import test
from nova.tests.api.openstack import fakes
@ -37,7 +38,9 @@ class QuotaClassSetsTest(test.TestCase):
def setUp(self):
super(QuotaClassSetsTest, self).setUp()
self.controller = quota_classes.QuotaClassSetsController()
self.ext_mgr = extensions.ExtensionManager()
self.ext_mgr.extensions = {}
self.controller = quota_classes.QuotaClassSetsController(self.ext_mgr)
def test_format_quota_set(self):
raw_quota_set = {

View File

@ -30,13 +30,17 @@ from nova import test
from nova.tests.api.openstack import fakes
def quota_set(id):
return {'quota_set': {'id': id, 'metadata_items': 128,
'ram': 51200, 'floating_ips': 10, 'fixed_ips': -1,
'instances': 10, 'injected_files': 5, 'cores': 20,
'injected_file_content_bytes': 10240,
'security_groups': 10, 'security_group_rules': 20,
'key_pairs': 100, 'injected_file_path_bytes': 255}}
def quota_set(id, include_server_group_quotas=True):
res = {'quota_set': {'id': id, 'metadata_items': 128,
'ram': 51200, 'floating_ips': 10, 'fixed_ips': -1,
'instances': 10, 'injected_files': 5, 'cores': 20,
'injected_file_content_bytes': 10240,
'security_groups': 10, 'security_group_rules': 20,
'key_pairs': 100, 'injected_file_path_bytes': 255}}
if include_server_group_quotas:
res['quota_set']['server_groups'] = 10
res['quota_set']['server_group_members'] = 10
return res
class BaseQuotaSetsTest(test.TestCase):
@ -81,11 +85,11 @@ class BaseQuotaSetsTest(test.TestCase):
class QuotaSetsTestV21(BaseQuotaSetsTest):
plugin = quotas_v21
validation_error = exception.ValidationError
include_server_group_quotas = True
def setUp(self):
super(QuotaSetsTestV21, self).setUp()
self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
self._setup_controller()
self.default_quotas = {
'instances': 10,
'cores': 20,
@ -98,8 +102,15 @@ class QuotaSetsTestV21(BaseQuotaSetsTest):
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20,
'key_pairs': 100
'key_pairs': 100,
}
if self.include_server_group_quotas:
self.default_quotas['server_groups'] = 10
self.default_quotas['server_group_members'] = 10
def _setup_controller(self):
self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
def test_format_quota_set(self):
quota_set = self.controller._format_quota_set('1234',
@ -119,6 +130,9 @@ class QuotaSetsTestV21(BaseQuotaSetsTest):
self.assertEqual(qs['security_groups'], 10)
self.assertEqual(qs['security_group_rules'], 20)
self.assertEqual(qs['key_pairs'], 100)
if self.include_server_group_quotas:
self.assertEqual(qs['server_groups'], 10)
self.assertEqual(qs['server_group_members'], 10)
def test_quotas_defaults(self):
uri = '/v2/fake_tenant/os-quota-sets/fake_tenant/defaults'
@ -136,7 +150,8 @@ class QuotaSetsTestV21(BaseQuotaSetsTest):
use_admin_context=True)
res_dict = self.controller.show(req, 1234)
self.assertEqual(res_dict, quota_set('1234'))
ref_quota_set = quota_set('1234', self.include_server_group_quotas)
self.assertEqual(res_dict, ref_quota_set)
def test_quotas_show_as_unauthorized_user(self):
self.setup_mock_for_show()
@ -169,6 +184,9 @@ class QuotaSetsTestV21(BaseQuotaSetsTest):
'security_groups': 0,
'security_group_rules': 0,
'key_pairs': 100, 'fixed_ips': -1}}
if self.include_server_group_quotas:
body['quota_set']['server_groups'] = 10
body['quota_set']['server_group_members'] = 10
expected_body = self.get_update_expected_response(body)
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
@ -330,8 +348,7 @@ class ExtendedQuotasTestV21(BaseQuotaSetsTest):
def setUp(self):
super(ExtendedQuotasTestV21, self).setUp()
self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
self._setup_controller()
self.setup_mock_for_update()
fake_quotas = {'ram': {'limit': 51200,
@ -344,6 +361,10 @@ class ExtendedQuotasTestV21(BaseQuotaSetsTest):
'in_use': 0,
'reserved': 0}}
def _setup_controller(self):
self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
def fake_get_quotas(self, context, id, user_id=None, usages=False):
if usages:
return self.fake_quotas
@ -384,9 +405,13 @@ class ExtendedQuotasTestV21(BaseQuotaSetsTest):
class UserQuotasTestV21(BaseQuotaSetsTest):
plugin = quotas_v21
include_server_group_quotas = True
def setUp(self):
super(UserQuotasTestV21, self).setUp()
self._setup_controller()
def _setup_controller(self):
self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
@ -395,8 +420,8 @@ class UserQuotasTestV21(BaseQuotaSetsTest):
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'))
ref_quota_set = quota_set('1234', self.include_server_group_quotas)
self.assertEqual(res_dict, ref_quota_set)
def test_user_quotas_show_as_unauthorized_user(self):
self.setup_mock_for_show()
@ -415,6 +440,10 @@ class UserQuotasTestV21(BaseQuotaSetsTest):
'security_groups': 10,
'security_group_rules': 20,
'key_pairs': 100}}
if self.include_server_group_quotas:
body['quota_set']['server_groups'] = 10
body['quota_set']['server_group_members'] = 10
expected_body = self.get_update_expected_response(body)
url = '/v2/fake4/os-quota-sets/update_me?user_id=1'
@ -432,7 +461,9 @@ class UserQuotasTestV21(BaseQuotaSetsTest):
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20,
'key_pairs': 100}}
'key_pairs': 100,
'server_groups': 10,
'server_group_members': 10}}
url = '/v2/fake4/os-quota-sets/update_me?user_id=1'
req = fakes.HTTPRequest.blank(url)
@ -474,6 +505,15 @@ class UserQuotasTestV21(BaseQuotaSetsTest):
class QuotaSetsTestV2(QuotaSetsTestV21):
plugin = quotas_v2
validation_error = webob.exc.HTTPBadRequest
include_server_group_quotas = False
def _setup_controller(self):
self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
self.ext_mgr.is_loaded('os-server-group-quotas').MultipleTimes().\
AndReturn(self.include_server_group_quotas)
self.mox.ReplayAll()
self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
self.mox.ResetAll()
# NOTE: The following tests are tricky and v2.1 API does not allow
# this kind of input by strong input validation. Just for test coverage,
@ -528,10 +568,63 @@ class QuotaSetsTestV2(QuotaSetsTestV21):
self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
req, 1234)
# NOTE: os-server-group-quotas is only for v2.0. On v2.1 this feature
# is always enabled, so this test is only needed for v2.0
def test_quotas_update_without_server_group_quotas_extenstion(self):
self.setup_mock_for_update()
self.default_quotas.update({
'server_groups': 50,
'sever_group_members': 50
})
body = {'quota_set': self.default_quotas}
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
use_admin_context=True)
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
req, 'update_me', body=body)
class ExtendedQuotasTestV2(ExtendedQuotasTestV21):
plugin = quotas_v2
def _setup_controller(self):
self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
self.ext_mgr.is_loaded('os-server-group-quotas').MultipleTimes().\
AndReturn(False)
self.mox.ReplayAll()
self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
self.mox.ResetAll()
class UserQuotasTestV2(UserQuotasTestV21):
plugin = quotas_v2
include_server_group_quotas = False
def _setup_controller(self):
self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
self.ext_mgr.is_loaded('os-server-group-quotas').MultipleTimes().\
AndReturn(self.include_server_group_quotas)
self.mox.ReplayAll()
self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
self.mox.ResetAll()
# NOTE: os-server-group-quotas is only for v2.0. On v2.1 this feature
# is always enabled, so this test is only needed for v2.0
def test_user_quotas_update_as_admin_without_sg_quota_extension(self):
self.setup_mock_for_update()
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,
'server_groups': 100,
'server_group_members': 200}}
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=body)

View File

@ -34,6 +34,7 @@ class FakeRequest(object):
class UsedLimitsTestCaseV21(test.NoDBTestCase):
used_limit_extension = "compute_extension:v3:os-used-limits:used_limits"
include_server_group_quotas = True
def setUp(self):
"""Run before each test."""
@ -62,12 +63,17 @@ class UsedLimitsTestCaseV21(test.NoDBTestCase):
'totalInstancesUsed': 'instances',
'totalFloatingIpsUsed': 'floating_ips',
'totalSecurityGroupsUsed': 'security_groups',
'totalServerGroupsUsed': 'server_groups',
}
limits = {}
expected_abs_limits = []
for display_name, q in quota_map.iteritems():
limits[q] = {'limit': len(display_name),
'in_use': len(display_name) / 2,
'reserved': len(display_name) / 3}
if (self.include_server_group_quotas or
display_name != 'totalServerGroupsUsed'):
expected_abs_limits.append(display_name)
def stub_get_project_quotas(context, project_id, usages=True):
return limits
@ -76,14 +82,17 @@ class UsedLimitsTestCaseV21(test.NoDBTestCase):
stub_get_project_quotas)
if self.ext_mgr is not None:
self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False)
self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(
self.include_server_group_quotas)
self.mox.ReplayAll()
self.controller.index(fake_req, res)
abs_limits = res.obj['limits']['absolute']
for used_limit, value in abs_limits.iteritems():
r = limits[quota_map[used_limit]]['reserved'] if reserved else 0
for limit in expected_abs_limits:
value = abs_limits[limit]
r = limits[quota_map[limit]]['reserved'] if reserved else 0
self.assertEqual(value,
limits[quota_map[used_limit]]['in_use'] + r)
limits[quota_map[limit]]['in_use'] + r)
def test_used_limits_basic(self):
self._do_test_used_limits(False)
@ -111,6 +120,8 @@ class UsedLimitsTestCaseV21(test.NoDBTestCase):
fake_req.GET = {'tenant_id': tenant_id}
if self.ext_mgr is not None:
self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(True)
self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(
self.include_server_group_quotas)
self.authorize(self.fake_context, target=target)
self.mox.StubOutWithMock(quota.QUOTAS, 'get_project_quotas')
quota.QUOTAS.get_project_quotas(self.fake_context, '%s' % tenant_id,
@ -134,6 +145,8 @@ class UsedLimitsTestCaseV21(test.NoDBTestCase):
fake_req.GET = {}
if self.ext_mgr is not None:
self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(True)
self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(
self.include_server_group_quotas)
self.mox.StubOutWithMock(extensions, 'extension_authorizer')
self.mox.StubOutWithMock(quota.QUOTAS, 'get_project_quotas')
quota.QUOTAS.get_project_quotas(self.fake_context, '%s' % project_id,
@ -182,6 +195,8 @@ class UsedLimitsTestCaseV21(test.NoDBTestCase):
fake_req = FakeRequest(self.fake_context)
if self.ext_mgr is not None:
self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False)
self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(
self.include_server_group_quotas)
self.mox.StubOutWithMock(quota.QUOTAS, 'get_project_quotas')
quota.QUOTAS.get_project_quotas(self.fake_context, project_id,
usages=True).AndReturn({})
@ -206,6 +221,8 @@ class UsedLimitsTestCaseV21(test.NoDBTestCase):
if self.ext_mgr is not None:
self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False)
self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(
self.include_server_group_quotas)
self.stubs.Set(quota.QUOTAS, "get_project_quotas",
stub_get_project_quotas)
self.mox.ReplayAll()
@ -230,6 +247,8 @@ class UsedLimitsTestCaseV21(test.NoDBTestCase):
if self.ext_mgr is not None:
self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False)
self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(
self.include_server_group_quotas)
self.stubs.Set(quota.QUOTAS, "get_project_quotas",
stub_get_project_quotas)
self.mox.ReplayAll()
@ -241,6 +260,7 @@ class UsedLimitsTestCaseV21(test.NoDBTestCase):
class UsedLimitsTestCaseV2(UsedLimitsTestCaseV21):
used_limit_extension = "compute_extension:used_limits_for_admin"
include_server_group_quotas = False
def _set_up_controller(self):
self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
@ -274,6 +294,7 @@ class UsedLimitsTestCaseXml(test.NoDBTestCase):
self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False)
self.stubs.Set(quota.QUOTAS, "get_project_quotas",
stub_get_project_quotas)
self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(False)
self.mox.ReplayAll()
self.controller.index(fake_req, res)

View File

@ -5463,6 +5463,11 @@ class QuotaTestCase(test.TestCase, ModelsObjectComparatorMixin):
for i in range(3):
db.security_group_create(self.ctxt, {'project_id': 'project1'})
usages['server_groups'] = 4
for i in range(4):
db.instance_group_create(self.ctxt, {'uuid': str(i),
'project_id': 'project1'})
reservations_uuids = db.quota_reserve(self.ctxt, reservable_resources,
quotas, quotas, deltas, None,
None, None, 'project1')

View File

@ -12,6 +12,8 @@
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 10
"security_groups": 10,
"server_groups": 10,
"server_group_members": 10
}
}

View File

@ -12,6 +12,8 @@
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 10
"security_groups": 10,
"server_groups": 10,
"server_group_members": 10
}
}

View File

@ -12,6 +12,8 @@
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 10
"security_groups": 10,
"server_groups": 10,
"server_group_members": 10
}
}

View File

@ -12,6 +12,8 @@
"ram": 51200,
"security_group_rules": 20,
"security_groups": 10,
"server_groups": 10,
"server_group_members": 10,
"id": "fake_tenant"
}
}

View File

@ -12,6 +12,8 @@
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 45
"security_groups": 45,
"server_groups": 10,
"server_group_members": 10
}
}

View File

@ -12,6 +12,8 @@
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 10
"security_groups": 10,
"server_groups": 10,
"server_group_members": 10
}
}

View File

@ -12,6 +12,8 @@
"metadata_items": 128,
"ram": 51200,
"security_group_rules": 20,
"security_groups": 10
"security_groups": 10,
"server_groups": 10,
"server_group_members": 10
}
}

View File

@ -12,11 +12,14 @@
"maxTotalInstances": 10,
"maxTotalKeypairs": 100,
"maxTotalRAMSize": 51200,
"maxServerGroups": 10,
"maxServerGroupMembers": 10,
"totalCoresUsed": 0,
"totalInstancesUsed": 0,
"totalRAMUsed": 0,
"totalSecurityGroupsUsed": 0,
"totalFloatingIpsUsed": 0
"totalFloatingIpsUsed": 0,
"totalServerGroupsUsed": 0
},
"rate": []
}

View File

@ -277,6 +277,35 @@ class _TestInstanceGroupObjects(test.TestCase):
group.obj_make_compatible(group_primitive, '1.6')
self.assertEqual({}, group_primitive['metadetails'])
def test_count_members_by_user(self):
instance1 = tests_utils.get_test_instance(self.context,
flavor=flavors.get_default_flavor(), obj=True)
instance1.user_id = 'user1'
instance1.save()
instance2 = tests_utils.get_test_instance(self.context,
flavor=flavors.get_default_flavor(), obj=True)
instance2.user_id = 'user2'
instance2.save()
instance3 = tests_utils.get_test_instance(self.context,
flavor=flavors.get_default_flavor(), obj=True)
instance3.user_id = 'user2'
instance3.save()
instance_ids = [instance1.uuid, instance2.uuid, instance3.uuid]
values = self._get_default_values()
group = self._create_instance_group(self.context, values)
instance_group.InstanceGroup.add_members(self.context, group.uuid,
instance_ids)
group = instance_group.InstanceGroup.get_by_uuid(self.context,
group.uuid)
count_user1 = group.count_members_by_user(self.context, 'user1')
count_user2 = group.count_members_by_user(self.context, 'user2')
count_user3 = group.count_members_by_user(self.context, 'user3')
self.assertEqual(1, count_user1)
self.assertEqual(2, count_user2)
self.assertEqual(0, count_user3)
class TestInstanceGroupObject(test_objects._LocalTest,
_TestInstanceGroupObjects):

View File

@ -958,8 +958,8 @@ object_data = {
'InstanceExternalEvent': '1.0-f1134523654407a875fd59b80f759ee7',
'InstanceFault': '1.2-313438e37e9d358f3566c85f6ddb2d3e',
'InstanceFaultList': '1.1-aeb598ffd0cd6aa61fca7adf0f5e900d',
'InstanceGroup': '1.7-b31ea31fdb452ab7810adbe789244f91',
'InstanceGroupList': '1.2-a474822eebc3e090012e581adcc1fa09',
'InstanceGroup': '1.8-9f3ef6ee21e424f817f76a63d35eb803',
'InstanceGroupList': '1.5-b507229896d60fad117cb3223dbaa0cc',
'InstanceInfoCache': '1.5-ef64b604498bfa505a8c93747a9d8b2f',
'InstanceList': '1.8-16db4c93fe5b80564413b9a4f547e0d1',
'InstanceNUMACell': '1.0-17e6ee0a24cb6651d1b084efa3027bda',

View File

@ -810,6 +810,8 @@ class DbQuotaDriverTestCase(test.TestCase):
quota_injected_file_path_length=255,
quota_security_groups=10,
quota_security_group_rules=20,
quota_server_groups=10,
quota_server_group_members=10,
reservation_expire=86400,
until_refresh=0,
max_age=0,
@ -839,6 +841,8 @@ class DbQuotaDriverTestCase(test.TestCase):
security_groups=10,
security_group_rules=20,
key_pairs=100,
server_groups=10,
server_group_members=10,
))
def _stub_quota_class_get_default(self):
@ -885,6 +889,8 @@ class DbQuotaDriverTestCase(test.TestCase):
security_groups=10,
security_group_rules=20,
key_pairs=100,
server_groups=10,
server_group_members=10,
))
def test_get_class_quotas_no_defaults(self):
@ -1016,6 +1022,16 @@ class DbQuotaDriverTestCase(test.TestCase):
in_use=0,
reserved=0,
),
server_groups=dict(
limit=10,
in_use=0,
reserved=0,
),
server_group_members=dict(
limit=10,
in_use=0,
reserved=0,
),
))
def _stub_get_by_project_and_user_specific(self):
@ -1144,6 +1160,16 @@ class DbQuotaDriverTestCase(test.TestCase):
in_use=0,
reserved=0,
),
server_groups=dict(
limit=10,
in_use=0,
reserved=0,
),
server_group_members=dict(
limit=10,
in_use=0,
reserved=0,
),
))
def test_get_user_quotas_alt_context_no_class(self):
@ -1219,6 +1245,16 @@ class DbQuotaDriverTestCase(test.TestCase):
in_use=0,
reserved=0,
),
server_groups=dict(
limit=10,
in_use=0,
reserved=0,
),
server_group_members=dict(
limit=10,
in_use=0,
reserved=0,
),
))
def test_get_project_quotas_alt_context_no_class(self):
@ -1294,6 +1330,16 @@ class DbQuotaDriverTestCase(test.TestCase):
in_use=0,
reserved=0,
),
server_groups=dict(
limit=10,
in_use=0,
reserved=0,
),
server_group_members=dict(
limit=10,
in_use=0,
reserved=0,
),
))
def test_get_user_quotas_alt_context_with_class(self):
@ -1371,6 +1417,16 @@ class DbQuotaDriverTestCase(test.TestCase):
in_use=0,
reserved=0,
),
server_groups=dict(
limit=10,
in_use=0,
reserved=0,
),
server_group_members=dict(
limit=10,
in_use=0,
reserved=0,
),
))
def test_get_project_quotas_alt_context_with_class(self):
@ -1447,6 +1503,16 @@ class DbQuotaDriverTestCase(test.TestCase):
in_use=0,
reserved=0,
),
server_groups=dict(
limit=10,
in_use=0,
reserved=0,
),
server_group_members=dict(
limit=10,
in_use=0,
reserved=0,
),
))
def test_get_user_quotas_no_defaults(self):
@ -1558,6 +1624,12 @@ class DbQuotaDriverTestCase(test.TestCase):
key_pairs=dict(
limit=100,
),
server_groups=dict(
limit=10,
),
server_group_members=dict(
limit=10,
),
))
def test_get_project_quotas_no_usages(self):
@ -1608,6 +1680,12 @@ class DbQuotaDriverTestCase(test.TestCase):
key_pairs=dict(
limit=100,
),
server_groups=dict(
limit=10,
),
server_group_members=dict(
limit=10,
),
))
def _stub_get_settable_quotas(self):
@ -1724,6 +1802,14 @@ class DbQuotaDriverTestCase(test.TestCase):
'minimum': 0,
'maximum': 100,
},
'server_groups': {
'minimum': 0,
'maximum': 10,
},
'server_group_members': {
'minimum': 0,
'maximum': 10,
},
})
def test_get_settable_quotas_without_user(self):
@ -1784,6 +1870,14 @@ class DbQuotaDriverTestCase(test.TestCase):
'minimum': 0,
'maximum': -1,
},
'server_groups': {
'minimum': 0,
'maximum': -1,
},
'server_group_members': {
'minimum': 0,
'maximum': -1,
},
})
def test_get_settable_quotas_by_user_with_unlimited_value(self):
@ -1846,6 +1940,14 @@ class DbQuotaDriverTestCase(test.TestCase):
'minimum': 0,
'maximum': 100,
},
'server_groups': {
'minimum': 0,
'maximum': 10,
},
'server_group_members': {
'minimum': 0,
'maximum': 10,
},
})
def _stub_get_project_quotas(self):
@ -1898,7 +2000,8 @@ class DbQuotaDriverTestCase(test.TestCase):
'test_class'),
quota.QUOTAS._resources,
['instances', 'cores', 'ram',
'floating_ips', 'security_groups'],
'floating_ips', 'security_groups',
'server_groups'],
True,
project_id='test_project')
@ -1909,6 +2012,7 @@ class DbQuotaDriverTestCase(test.TestCase):
ram=50 * 1024,
floating_ips=10,
security_groups=10,
server_groups=10,
))
def test_get_quotas_no_sync(self):
@ -1919,7 +2023,8 @@ class DbQuotaDriverTestCase(test.TestCase):
['metadata_items', 'injected_files',
'injected_file_content_bytes',
'injected_file_path_bytes',
'security_group_rules'], False,
'security_group_rules',
'server_group_members'], False,
project_id='test_project')
self.assertEqual(self.calls, ['get_project_quotas'])
@ -1929,6 +2034,7 @@ class DbQuotaDriverTestCase(test.TestCase):
injected_file_content_bytes=10 * 1024,
injected_file_path_bytes=255,
security_group_rules=20,
server_group_members=10,
))
def test_limit_check_under(self):