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, "metadata_items": 128,
"ram": 51200, "ram": 51200,
"security_group_rules": 20, "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, "metadata_items": 128,
"ram": 51200, "ram": 51200,
"security_group_rules": 20, "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, "metadata_items": 128,
"ram": 51200, "ram": 51200,
"security_group_rules": 20, "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, "metadata_items": 128,
"ram": 51200, "ram": 51200,
"security_group_rules": 20, "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, "metadata_items": 128,
"ram": 51200, "ram": 51200,
"security_group_rules": 20, "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, "metadata_items": 128,
"ram": 51200, "ram": 51200,
"security_group_rules": 20, "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, "maxTotalInstances": 10,
"maxTotalKeypairs": 100, "maxTotalKeypairs": 100,
"maxTotalRAMSize": 51200, "maxTotalRAMSize": 51200,
"maxServerGroups": 10,
"maxServerGroupMembers": 10,
"totalCoresUsed": 0, "totalCoresUsed": 0,
"totalInstancesUsed": 0, "totalInstancesUsed": 0,
"totalRAMUsed": 0, "totalRAMUsed": 0,
"totalSecurityGroupsUsed": 0, "totalSecurityGroupsUsed": 0,
"totalFloatingIpsUsed": 0 "totalFloatingIpsUsed": 0,
"totalServerGroupsUsed": 0
}, },
"rate": [] "rate": []
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,22 @@ from nova.openstack.common import timeutils
class ViewBuilder(object): class ViewBuilder(object):
"""OpenStack API base limits view builder.""" """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): def build(self, rate_limits, absolute_limits):
rate_limits = self._build_rate_limits(rate_limits) rate_limits = self._build_rate_limits(rate_limits)
absolute_limits = self._build_absolute_limits(absolute_limits) absolute_limits = self._build_absolute_limits(absolute_limits)
@ -41,22 +57,10 @@ class ViewBuilder(object):
For example: {"ram": 512, "gigabytes": 1024}. 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 = {} limits = {}
for name, value in absolute_limits.iteritems(): for name, value in absolute_limits.iteritems():
if name in limit_names and value is not None: if name in self.limit_names and value is not None:
for name in limit_names[name]: for name in self.limit_names[name]:
limits[name] = value limits[name] = value
return limits return limits
@ -100,6 +104,8 @@ class ViewBuilder(object):
class ViewBuilderV3(ViewBuilder): class ViewBuilderV3(ViewBuilder):
def build(self, rate_limits): def __init__(self):
rate_limits = self._build_rate_limits(rate_limits) super(ViewBuilderV3, self).__init__()
return {"limits": {"rate": rate_limits}} # 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( return dict(security_groups=_security_group_count_by_project_and_user(
context, project_id, user_id, session)) 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 = { QUOTA_SYNC_FUNCTIONS = {
'_sync_instances': _sync_instances, '_sync_instances': _sync_instances,
'_sync_floating_ips': _sync_floating_ips, '_sync_floating_ips': _sync_floating_ips,
'_sync_fixed_ips': _sync_fixed_ips, '_sync_fixed_ips': _sync_fixed_ips,
'_sync_security_groups': _sync_security_groups, '_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() 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, def _instance_group_model_get_query(context, model_class, group_id,
session=None, read_deleted='no'): session=None, read_deleted='no'):
return model_query(context, return model_query(context,

View File

@ -30,7 +30,8 @@ class InstanceGroup(base.NovaPersistentObject, base.NovaObject):
# Version 1.5: Add get_hosts() # Version 1.5: Add get_hosts()
# Version 1.6: Add get_by_name() # Version 1.6: Add get_by_name()
# Version 1.7: Deprecate metadetails # Version 1.7: Deprecate metadetails
VERSION = '1.7' # Version 1.8: Add count_members_by_user()
VERSION = '1.8'
fields = { fields = {
'id': fields.IntegerField(), 'id': fields.IntegerField(),
@ -160,6 +161,15 @@ class InstanceGroup(base.NovaPersistentObject, base.NovaObject):
return list(set([instance.host for instance in instances return list(set([instance.host for instance in instances
if instance.host])) 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): class InstanceGroupList(base.ObjectListBase, base.NovaObject):
# Version 1.0: Initial version # Version 1.0: Initial version
@ -168,7 +178,8 @@ class InstanceGroupList(base.ObjectListBase, base.NovaObject):
# Version 1.2: InstanceGroup <= version 1.5 # Version 1.2: InstanceGroup <= version 1.5
# Version 1.3: InstanceGroup <= version 1.6 # Version 1.3: InstanceGroup <= version 1.6
# Version 1.4: InstanceGroup <= version 1.7 # Version 1.4: InstanceGroup <= version 1.7
VERSION = '1.2' # Version 1.5: InstanceGroup <= version 1.8
VERSION = '1.5'
fields = { fields = {
'objects': fields.ListOfObjectsField('InstanceGroup'), 'objects': fields.ListOfObjectsField('InstanceGroup'),
@ -180,6 +191,7 @@ class InstanceGroupList(base.ObjectListBase, base.NovaObject):
'1.2': '1.5', '1.2': '1.5',
'1.3': '1.6', '1.3': '1.6',
'1.4': '1.7', '1.4': '1.7',
'1.5': '1.8',
} }
@base.remotable_classmethod @base.remotable_classmethod

View File

@ -39,6 +39,13 @@ def ids_from_security_group(context, security_group):
return ids_from_instance(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): class Quotas(base.NovaObject):
# Version 1.0: initial version # Version 1.0: initial version
# Version 1.1: Added create_limit() and update_limit() # Version 1.1: Added create_limit() and update_limit()

View File

@ -72,6 +72,12 @@ quota_opts = [
cfg.IntOpt('quota_key_pairs', cfg.IntOpt('quota_key_pairs',
default=100, default=100,
help='Number of key pairs per user'), 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', cfg.IntOpt('reservation_expire',
default=86400, default=86400,
help='Number of seconds until a reservation expires'), 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) 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() QUOTAS = QuotaEngine()
@ -1439,6 +1450,11 @@ resources = [
'quota_security_group_rules'), 'quota_security_group_rules'),
CountableResource('key_pairs', _keypair_get_count_by_user, CountableResource('key_pairs', _keypair_get_count_by_user,
'quota_key_pairs'), '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 import webob
from nova.api.openstack.compute.contrib import quota_classes from nova.api.openstack.compute.contrib import quota_classes
from nova.api.openstack import extensions
from nova.api.openstack import wsgi from nova.api.openstack import wsgi
from nova import test from nova import test
from nova.tests.api.openstack import fakes from nova.tests.api.openstack import fakes
@ -37,7 +38,9 @@ class QuotaClassSetsTest(test.TestCase):
def setUp(self): def setUp(self):
super(QuotaClassSetsTest, self).setUp() 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): def test_format_quota_set(self):
raw_quota_set = { raw_quota_set = {

View File

@ -30,13 +30,17 @@ from nova import test
from nova.tests.api.openstack import fakes from nova.tests.api.openstack import fakes
def quota_set(id): def quota_set(id, include_server_group_quotas=True):
return {'quota_set': {'id': id, 'metadata_items': 128, res = {'quota_set': {'id': id, 'metadata_items': 128,
'ram': 51200, 'floating_ips': 10, 'fixed_ips': -1, 'ram': 51200, 'floating_ips': 10, 'fixed_ips': -1,
'instances': 10, 'injected_files': 5, 'cores': 20, 'instances': 10, 'injected_files': 5, 'cores': 20,
'injected_file_content_bytes': 10240, 'injected_file_content_bytes': 10240,
'security_groups': 10, 'security_group_rules': 20, 'security_groups': 10, 'security_group_rules': 20,
'key_pairs': 100, 'injected_file_path_bytes': 255}} '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): class BaseQuotaSetsTest(test.TestCase):
@ -81,11 +85,11 @@ class BaseQuotaSetsTest(test.TestCase):
class QuotaSetsTestV21(BaseQuotaSetsTest): class QuotaSetsTestV21(BaseQuotaSetsTest):
plugin = quotas_v21 plugin = quotas_v21
validation_error = exception.ValidationError validation_error = exception.ValidationError
include_server_group_quotas = True
def setUp(self): def setUp(self):
super(QuotaSetsTestV21, self).setUp() super(QuotaSetsTestV21, self).setUp()
self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) self._setup_controller()
self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
self.default_quotas = { self.default_quotas = {
'instances': 10, 'instances': 10,
'cores': 20, 'cores': 20,
@ -98,8 +102,15 @@ class QuotaSetsTestV21(BaseQuotaSetsTest):
'injected_file_content_bytes': 10240, 'injected_file_content_bytes': 10240,
'security_groups': 10, 'security_groups': 10,
'security_group_rules': 20, '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): def test_format_quota_set(self):
quota_set = self.controller._format_quota_set('1234', 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_groups'], 10)
self.assertEqual(qs['security_group_rules'], 20) self.assertEqual(qs['security_group_rules'], 20)
self.assertEqual(qs['key_pairs'], 100) 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): def test_quotas_defaults(self):
uri = '/v2/fake_tenant/os-quota-sets/fake_tenant/defaults' uri = '/v2/fake_tenant/os-quota-sets/fake_tenant/defaults'
@ -136,7 +150,8 @@ class QuotaSetsTestV21(BaseQuotaSetsTest):
use_admin_context=True) use_admin_context=True)
res_dict = self.controller.show(req, 1234) 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): def test_quotas_show_as_unauthorized_user(self):
self.setup_mock_for_show() self.setup_mock_for_show()
@ -169,6 +184,9 @@ class QuotaSetsTestV21(BaseQuotaSetsTest):
'security_groups': 0, 'security_groups': 0,
'security_group_rules': 0, 'security_group_rules': 0,
'key_pairs': 100, 'fixed_ips': -1}} '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) expected_body = self.get_update_expected_response(body)
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me', req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
@ -330,8 +348,7 @@ class ExtendedQuotasTestV21(BaseQuotaSetsTest):
def setUp(self): def setUp(self):
super(ExtendedQuotasTestV21, self).setUp() super(ExtendedQuotasTestV21, self).setUp()
self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) self._setup_controller()
self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
self.setup_mock_for_update() self.setup_mock_for_update()
fake_quotas = {'ram': {'limit': 51200, fake_quotas = {'ram': {'limit': 51200,
@ -344,6 +361,10 @@ class ExtendedQuotasTestV21(BaseQuotaSetsTest):
'in_use': 0, 'in_use': 0,
'reserved': 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): def fake_get_quotas(self, context, id, user_id=None, usages=False):
if usages: if usages:
return self.fake_quotas return self.fake_quotas
@ -384,9 +405,13 @@ class ExtendedQuotasTestV21(BaseQuotaSetsTest):
class UserQuotasTestV21(BaseQuotaSetsTest): class UserQuotasTestV21(BaseQuotaSetsTest):
plugin = quotas_v21 plugin = quotas_v21
include_server_group_quotas = True
def setUp(self): def setUp(self):
super(UserQuotasTestV21, self).setUp() super(UserQuotasTestV21, self).setUp()
self._setup_controller()
def _setup_controller(self):
self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
self.controller = self.plugin.QuotaSetsController(self.ext_mgr) 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', req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1',
use_admin_context=True) use_admin_context=True)
res_dict = self.controller.show(req, 1234) res_dict = self.controller.show(req, 1234)
ref_quota_set = quota_set('1234', self.include_server_group_quotas)
self.assertEqual(res_dict, quota_set('1234')) self.assertEqual(res_dict, ref_quota_set)
def test_user_quotas_show_as_unauthorized_user(self): def test_user_quotas_show_as_unauthorized_user(self):
self.setup_mock_for_show() self.setup_mock_for_show()
@ -415,6 +440,10 @@ class UserQuotasTestV21(BaseQuotaSetsTest):
'security_groups': 10, 'security_groups': 10,
'security_group_rules': 20, 'security_group_rules': 20,
'key_pairs': 100}} '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) expected_body = self.get_update_expected_response(body)
url = '/v2/fake4/os-quota-sets/update_me?user_id=1' url = '/v2/fake4/os-quota-sets/update_me?user_id=1'
@ -432,7 +461,9 @@ class UserQuotasTestV21(BaseQuotaSetsTest):
'injected_file_content_bytes': 10240, 'injected_file_content_bytes': 10240,
'security_groups': 10, 'security_groups': 10,
'security_group_rules': 20, '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' url = '/v2/fake4/os-quota-sets/update_me?user_id=1'
req = fakes.HTTPRequest.blank(url) req = fakes.HTTPRequest.blank(url)
@ -474,6 +505,15 @@ class UserQuotasTestV21(BaseQuotaSetsTest):
class QuotaSetsTestV2(QuotaSetsTestV21): class QuotaSetsTestV2(QuotaSetsTestV21):
plugin = quotas_v2 plugin = quotas_v2
validation_error = webob.exc.HTTPBadRequest 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 # 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, # 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, self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
req, 1234) 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): class ExtendedQuotasTestV2(ExtendedQuotasTestV21):
plugin = quotas_v2 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): class UserQuotasTestV2(UserQuotasTestV21):
plugin = quotas_v2 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): class UsedLimitsTestCaseV21(test.NoDBTestCase):
used_limit_extension = "compute_extension:v3:os-used-limits:used_limits" used_limit_extension = "compute_extension:v3:os-used-limits:used_limits"
include_server_group_quotas = True
def setUp(self): def setUp(self):
"""Run before each test.""" """Run before each test."""
@ -62,12 +63,17 @@ class UsedLimitsTestCaseV21(test.NoDBTestCase):
'totalInstancesUsed': 'instances', 'totalInstancesUsed': 'instances',
'totalFloatingIpsUsed': 'floating_ips', 'totalFloatingIpsUsed': 'floating_ips',
'totalSecurityGroupsUsed': 'security_groups', 'totalSecurityGroupsUsed': 'security_groups',
'totalServerGroupsUsed': 'server_groups',
} }
limits = {} limits = {}
expected_abs_limits = []
for display_name, q in quota_map.iteritems(): for display_name, q in quota_map.iteritems():
limits[q] = {'limit': len(display_name), limits[q] = {'limit': len(display_name),
'in_use': len(display_name) / 2, 'in_use': len(display_name) / 2,
'reserved': len(display_name) / 3} '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): def stub_get_project_quotas(context, project_id, usages=True):
return limits return limits
@ -76,14 +82,17 @@ class UsedLimitsTestCaseV21(test.NoDBTestCase):
stub_get_project_quotas) stub_get_project_quotas)
if self.ext_mgr is not None: 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-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.mox.ReplayAll()
self.controller.index(fake_req, res) self.controller.index(fake_req, res)
abs_limits = res.obj['limits']['absolute'] abs_limits = res.obj['limits']['absolute']
for used_limit, value in abs_limits.iteritems(): for limit in expected_abs_limits:
r = limits[quota_map[used_limit]]['reserved'] if reserved else 0 value = abs_limits[limit]
r = limits[quota_map[limit]]['reserved'] if reserved else 0
self.assertEqual(value, self.assertEqual(value,
limits[quota_map[used_limit]]['in_use'] + r) limits[quota_map[limit]]['in_use'] + r)
def test_used_limits_basic(self): def test_used_limits_basic(self):
self._do_test_used_limits(False) self._do_test_used_limits(False)
@ -111,6 +120,8 @@ class UsedLimitsTestCaseV21(test.NoDBTestCase):
fake_req.GET = {'tenant_id': tenant_id} fake_req.GET = {'tenant_id': tenant_id}
if self.ext_mgr is not None: 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-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.authorize(self.fake_context, target=target)
self.mox.StubOutWithMock(quota.QUOTAS, 'get_project_quotas') self.mox.StubOutWithMock(quota.QUOTAS, 'get_project_quotas')
quota.QUOTAS.get_project_quotas(self.fake_context, '%s' % tenant_id, quota.QUOTAS.get_project_quotas(self.fake_context, '%s' % tenant_id,
@ -134,6 +145,8 @@ class UsedLimitsTestCaseV21(test.NoDBTestCase):
fake_req.GET = {} fake_req.GET = {}
if self.ext_mgr is not None: 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-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(extensions, 'extension_authorizer')
self.mox.StubOutWithMock(quota.QUOTAS, 'get_project_quotas') self.mox.StubOutWithMock(quota.QUOTAS, 'get_project_quotas')
quota.QUOTAS.get_project_quotas(self.fake_context, '%s' % project_id, 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) fake_req = FakeRequest(self.fake_context)
if self.ext_mgr is not None: 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-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') self.mox.StubOutWithMock(quota.QUOTAS, 'get_project_quotas')
quota.QUOTAS.get_project_quotas(self.fake_context, project_id, quota.QUOTAS.get_project_quotas(self.fake_context, project_id,
usages=True).AndReturn({}) usages=True).AndReturn({})
@ -206,6 +221,8 @@ class UsedLimitsTestCaseV21(test.NoDBTestCase):
if self.ext_mgr is not None: 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-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", self.stubs.Set(quota.QUOTAS, "get_project_quotas",
stub_get_project_quotas) stub_get_project_quotas)
self.mox.ReplayAll() self.mox.ReplayAll()
@ -230,6 +247,8 @@ class UsedLimitsTestCaseV21(test.NoDBTestCase):
if self.ext_mgr is not None: 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-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", self.stubs.Set(quota.QUOTAS, "get_project_quotas",
stub_get_project_quotas) stub_get_project_quotas)
self.mox.ReplayAll() self.mox.ReplayAll()
@ -241,6 +260,7 @@ class UsedLimitsTestCaseV21(test.NoDBTestCase):
class UsedLimitsTestCaseV2(UsedLimitsTestCaseV21): class UsedLimitsTestCaseV2(UsedLimitsTestCaseV21):
used_limit_extension = "compute_extension:used_limits_for_admin" used_limit_extension = "compute_extension:used_limits_for_admin"
include_server_group_quotas = False
def _set_up_controller(self): def _set_up_controller(self):
self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) 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.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False)
self.stubs.Set(quota.QUOTAS, "get_project_quotas", self.stubs.Set(quota.QUOTAS, "get_project_quotas",
stub_get_project_quotas) stub_get_project_quotas)
self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(False)
self.mox.ReplayAll() self.mox.ReplayAll()
self.controller.index(fake_req, res) self.controller.index(fake_req, res)

View File

@ -5463,6 +5463,11 @@ class QuotaTestCase(test.TestCase, ModelsObjectComparatorMixin):
for i in range(3): for i in range(3):
db.security_group_create(self.ctxt, {'project_id': 'project1'}) 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, reservations_uuids = db.quota_reserve(self.ctxt, reservable_resources,
quotas, quotas, deltas, None, quotas, quotas, deltas, None,
None, None, 'project1') None, None, 'project1')

View File

@ -12,6 +12,8 @@
"metadata_items": 128, "metadata_items": 128,
"ram": 51200, "ram": 51200,
"security_group_rules": 20, "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, "metadata_items": 128,
"ram": 51200, "ram": 51200,
"security_group_rules": 20, "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, "metadata_items": 128,
"ram": 51200, "ram": 51200,
"security_group_rules": 20, "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, "ram": 51200,
"security_group_rules": 20, "security_group_rules": 20,
"security_groups": 10, "security_groups": 10,
"server_groups": 10,
"server_group_members": 10,
"id": "fake_tenant" "id": "fake_tenant"
} }
} }

View File

@ -12,6 +12,8 @@
"metadata_items": 128, "metadata_items": 128,
"ram": 51200, "ram": 51200,
"security_group_rules": 20, "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, "metadata_items": 128,
"ram": 51200, "ram": 51200,
"security_group_rules": 20, "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, "metadata_items": 128,
"ram": 51200, "ram": 51200,
"security_group_rules": 20, "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, "maxTotalInstances": 10,
"maxTotalKeypairs": 100, "maxTotalKeypairs": 100,
"maxTotalRAMSize": 51200, "maxTotalRAMSize": 51200,
"maxServerGroups": 10,
"maxServerGroupMembers": 10,
"totalCoresUsed": 0, "totalCoresUsed": 0,
"totalInstancesUsed": 0, "totalInstancesUsed": 0,
"totalRAMUsed": 0, "totalRAMUsed": 0,
"totalSecurityGroupsUsed": 0, "totalSecurityGroupsUsed": 0,
"totalFloatingIpsUsed": 0 "totalFloatingIpsUsed": 0,
"totalServerGroupsUsed": 0
}, },
"rate": [] "rate": []
} }

View File

@ -277,6 +277,35 @@ class _TestInstanceGroupObjects(test.TestCase):
group.obj_make_compatible(group_primitive, '1.6') group.obj_make_compatible(group_primitive, '1.6')
self.assertEqual({}, group_primitive['metadetails']) 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, class TestInstanceGroupObject(test_objects._LocalTest,
_TestInstanceGroupObjects): _TestInstanceGroupObjects):

View File

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

View File

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