From 597c46fde1ae36023b67061166e6c1c42677cd34 Mon Sep 17 00:00:00 2001 From: Phil Day Date: Fri, 4 Jul 2014 21:03:48 +0000 Subject: [PATCH] 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 Implements: blueprint server-group-quotas DocImpact Change-Id: Ib281e43eabfbd176454bde7f0622d46fb04fcb79 --- .../quotas-show-defaults-get-resp.json | 4 +- .../os-quota-sets/quotas-show-get-resp.json | 4 +- .../quotas-update-force-post-resp.json | 4 +- .../quotas-update-post-resp.json | 4 +- .../user-quotas-show-get-resp.json | 4 +- .../user-quotas-update-post-resp.json | 4 +- .../os-used-limits/usedlimits-get-resp.json | 5 +- .../compute/contrib/quota_classes.py | 73 +++++++--- nova/api/openstack/compute/contrib/quotas.py | 41 ++++-- .../openstack/compute/contrib/used_limits.py | 3 + .../openstack/compute/plugins/v3/limits.py | 2 +- .../compute/plugins/v3/used_limits.py | 3 +- .../compute/schemas/v3/quota_sets.py | 2 + nova/api/openstack/compute/views/limits.py | 40 +++--- nova/db/sqlalchemy/api.py | 15 +++ nova/objects/instance_group.py | 16 ++- nova/objects/quotas.py | 7 + nova/quota.py | 16 +++ .../compute/contrib/test_quota_classes.py | 5 +- .../openstack/compute/contrib/test_quotas.py | 125 +++++++++++++++--- .../compute/contrib/test_used_limits.py | 27 +++- nova/tests/db/test_db_api.py | 5 + .../quotas-show-defaults-get-resp.json.tpl | 4 +- .../quotas-show-get-resp.json.tpl | 4 +- .../quotas-update-force-post-resp.json.tpl | 4 +- .../quotas-update-force-resp.json.tpl | 2 + .../quotas-update-post-resp.json.tpl | 4 +- .../user-quotas-show-get-resp.json.tpl | 4 +- .../user-quotas-update-post-resp.json.tpl | 4 +- .../usedlimits-get-resp.json.tpl | 5 +- nova/tests/objects/test_instance_group.py | 29 ++++ nova/tests/objects/test_objects.py | 4 +- nova/tests/test_quota.py | 110 ++++++++++++++- 33 files changed, 491 insertions(+), 92 deletions(-) diff --git a/doc/v3/api_samples/os-quota-sets/quotas-show-defaults-get-resp.json b/doc/v3/api_samples/os-quota-sets/quotas-show-defaults-get-resp.json index 239c64d23d44..c93f809d598c 100644 --- a/doc/v3/api_samples/os-quota-sets/quotas-show-defaults-get-resp.json +++ b/doc/v3/api_samples/os-quota-sets/quotas-show-defaults-get-resp.json @@ -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 } } diff --git a/doc/v3/api_samples/os-quota-sets/quotas-show-get-resp.json b/doc/v3/api_samples/os-quota-sets/quotas-show-get-resp.json index 239c64d23d44..c93f809d598c 100644 --- a/doc/v3/api_samples/os-quota-sets/quotas-show-get-resp.json +++ b/doc/v3/api_samples/os-quota-sets/quotas-show-get-resp.json @@ -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 } } diff --git a/doc/v3/api_samples/os-quota-sets/quotas-update-force-post-resp.json b/doc/v3/api_samples/os-quota-sets/quotas-update-force-post-resp.json index e4ada84d7745..97c456d4d417 100644 --- a/doc/v3/api_samples/os-quota-sets/quotas-update-force-post-resp.json +++ b/doc/v3/api_samples/os-quota-sets/quotas-update-force-post-resp.json @@ -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 } } diff --git a/doc/v3/api_samples/os-quota-sets/quotas-update-post-resp.json b/doc/v3/api_samples/os-quota-sets/quotas-update-post-resp.json index d8f963bc09fc..60230f23419a 100644 --- a/doc/v3/api_samples/os-quota-sets/quotas-update-post-resp.json +++ b/doc/v3/api_samples/os-quota-sets/quotas-update-post-resp.json @@ -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 } } diff --git a/doc/v3/api_samples/os-quota-sets/user-quotas-show-get-resp.json b/doc/v3/api_samples/os-quota-sets/user-quotas-show-get-resp.json index 239c64d23d44..c93f809d598c 100644 --- a/doc/v3/api_samples/os-quota-sets/user-quotas-show-get-resp.json +++ b/doc/v3/api_samples/os-quota-sets/user-quotas-show-get-resp.json @@ -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 } } diff --git a/doc/v3/api_samples/os-quota-sets/user-quotas-update-post-resp.json b/doc/v3/api_samples/os-quota-sets/user-quotas-update-post-resp.json index 87d83ff6ff04..c7e0da6bf610 100644 --- a/doc/v3/api_samples/os-quota-sets/user-quotas-update-post-resp.json +++ b/doc/v3/api_samples/os-quota-sets/user-quotas-update-post-resp.json @@ -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 } } diff --git a/doc/v3/api_samples/os-used-limits/usedlimits-get-resp.json b/doc/v3/api_samples/os-used-limits/usedlimits-get-resp.json index cb54d20cadf8..28309af04c6c 100644 --- a/doc/v3/api_samples/os-used-limits/usedlimits-get-resp.json +++ b/doc/v3/api_samples/os-used-limits/usedlimits-get-resp.json @@ -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": [] } diff --git a/nova/api/openstack/compute/contrib/quota_classes.py b/nova/api/openstack/compute/contrib/quota_classes.py index bb034a0b4d22..49697a032039 100644 --- a/nova/api/openstack/compute/contrib/quota_classes.py +++ b/nova/api/openstack/compute/contrib/quota_classes.py @@ -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 diff --git a/nova/api/openstack/compute/contrib/quotas.py b/nova/api/openstack/compute/contrib/quotas.py index 8468a41d868f..112f08359d3f 100644 --- a/nova/api/openstack/compute/contrib/quotas.py +++ b/nova/api/openstack/compute/contrib/quotas.py @@ -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'): diff --git a/nova/api/openstack/compute/contrib/used_limits.py b/nova/api/openstack/compute/contrib/used_limits.py index 4cfd3948dcde..49e89f7c0bca 100644 --- a/nova/api/openstack/compute/contrib/used_limits.py +++ b/nova/api/openstack/compute/contrib/used_limits.py @@ -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: diff --git a/nova/api/openstack/compute/plugins/v3/limits.py b/nova/api/openstack/compute/plugins/v3/limits.py index eef0b0f41e59..457f82f823a2 100644 --- a/nova/api/openstack/compute/plugins/v3/limits.py +++ b/nova/api/openstack/compute/plugins/v3/limits.py @@ -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): diff --git a/nova/api/openstack/compute/plugins/v3/used_limits.py b/nova/api/openstack/compute/plugins/v3/used_limits.py index f364c888f764..e6488aa36501 100644 --- a/nova/api/openstack/compute/plugins/v3/used_limits.py +++ b/nova/api/openstack/compute/plugins/v3/used_limits.py @@ -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 [] \ No newline at end of file + return [] diff --git a/nova/api/openstack/compute/schemas/v3/quota_sets.py b/nova/api/openstack/compute/schemas/v3/quota_sets.py index c3a40742fcc5..cba2fc6e8565 100644 --- a/nova/api/openstack/compute/schemas/v3/quota_sets.py +++ b/nova/api/openstack/compute/schemas/v3/quota_sets.py @@ -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, diff --git a/nova/api/openstack/compute/views/limits.py b/nova/api/openstack/compute/views/limits.py index ba686f87a5fc..381b020852ac 100644 --- a/nova/api/openstack/compute/views/limits.py +++ b/nova/api/openstack/compute/views/limits.py @@ -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"] diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index e1dc48d99ece..e3e1cd6bce22 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -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, diff --git a/nova/objects/instance_group.py b/nova/objects/instance_group.py index 842c0ac4a7da..8a519c4764c0 100644 --- a/nova/objects/instance_group.py +++ b/nova/objects/instance_group.py @@ -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 diff --git a/nova/objects/quotas.py b/nova/objects/quotas.py index 2731c53d7e1b..843e8fc19103 100644 --- a/nova/objects/quotas.py +++ b/nova/objects/quotas.py @@ -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() diff --git a/nova/quota.py b/nova/quota.py index b98282ecb1e7..8b6ce06aeb7f 100644 --- a/nova/quota.py +++ b/nova/quota.py @@ -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'), ] diff --git a/nova/tests/api/openstack/compute/contrib/test_quota_classes.py b/nova/tests/api/openstack/compute/contrib/test_quota_classes.py index 998aa2044d15..2767d66518f9 100644 --- a/nova/tests/api/openstack/compute/contrib/test_quota_classes.py +++ b/nova/tests/api/openstack/compute/contrib/test_quota_classes.py @@ -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 = { diff --git a/nova/tests/api/openstack/compute/contrib/test_quotas.py b/nova/tests/api/openstack/compute/contrib/test_quotas.py index f36e1af50629..14480f3b3c7e 100644 --- a/nova/tests/api/openstack/compute/contrib/test_quotas.py +++ b/nova/tests/api/openstack/compute/contrib/test_quotas.py @@ -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) diff --git a/nova/tests/api/openstack/compute/contrib/test_used_limits.py b/nova/tests/api/openstack/compute/contrib/test_used_limits.py index bb62e760a9ea..11878f6ed5d7 100644 --- a/nova/tests/api/openstack/compute/contrib/test_used_limits.py +++ b/nova/tests/api/openstack/compute/contrib/test_used_limits.py @@ -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) diff --git a/nova/tests/db/test_db_api.py b/nova/tests/db/test_db_api.py index 9df4fd771650..26d80b0c20dc 100644 --- a/nova/tests/db/test_db_api.py +++ b/nova/tests/db/test_db_api.py @@ -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') diff --git a/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-show-defaults-get-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-show-defaults-get-resp.json.tpl index 2f0fd9857242..f66f22cd2dfa 100644 --- a/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-show-defaults-get-resp.json.tpl +++ b/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-show-defaults-get-resp.json.tpl @@ -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 } } diff --git a/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-show-get-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-show-get-resp.json.tpl index 2f0fd9857242..f66f22cd2dfa 100644 --- a/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-show-get-resp.json.tpl +++ b/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-show-get-resp.json.tpl @@ -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 } } diff --git a/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-update-force-post-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-update-force-post-resp.json.tpl index e4ada84d7745..97c456d4d417 100644 --- a/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-update-force-post-resp.json.tpl +++ b/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-update-force-post-resp.json.tpl @@ -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 } } diff --git a/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-update-force-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-update-force-resp.json.tpl index 5570e8bfa9e7..ff23ff6ae470 100644 --- a/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-update-force-resp.json.tpl +++ b/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-update-force-resp.json.tpl @@ -12,6 +12,8 @@ "ram": 51200, "security_group_rules": 20, "security_groups": 10, + "server_groups": 10, + "server_group_members": 10, "id": "fake_tenant" } } diff --git a/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-update-post-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-update-post-resp.json.tpl index 6e1aa34184c3..f7c276e3f701 100644 --- a/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-update-post-resp.json.tpl +++ b/nova/tests/integrated/v3/api_samples/os-quota-sets/quotas-update-post-resp.json.tpl @@ -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 } } diff --git a/nova/tests/integrated/v3/api_samples/os-quota-sets/user-quotas-show-get-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-quota-sets/user-quotas-show-get-resp.json.tpl index 2f0fd9857242..f66f22cd2dfa 100644 --- a/nova/tests/integrated/v3/api_samples/os-quota-sets/user-quotas-show-get-resp.json.tpl +++ b/nova/tests/integrated/v3/api_samples/os-quota-sets/user-quotas-show-get-resp.json.tpl @@ -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 } } diff --git a/nova/tests/integrated/v3/api_samples/os-quota-sets/user-quotas-update-post-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-quota-sets/user-quotas-update-post-resp.json.tpl index cea024268e04..a17757aafe34 100644 --- a/nova/tests/integrated/v3/api_samples/os-quota-sets/user-quotas-update-post-resp.json.tpl +++ b/nova/tests/integrated/v3/api_samples/os-quota-sets/user-quotas-update-post-resp.json.tpl @@ -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 } } diff --git a/nova/tests/integrated/v3/api_samples/os-used-limits/usedlimits-get-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-used-limits/usedlimits-get-resp.json.tpl index cb54d20cadf8..28309af04c6c 100644 --- a/nova/tests/integrated/v3/api_samples/os-used-limits/usedlimits-get-resp.json.tpl +++ b/nova/tests/integrated/v3/api_samples/os-used-limits/usedlimits-get-resp.json.tpl @@ -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": [] } diff --git a/nova/tests/objects/test_instance_group.py b/nova/tests/objects/test_instance_group.py index 3099059ff831..27eb3034f877 100644 --- a/nova/tests/objects/test_instance_group.py +++ b/nova/tests/objects/test_instance_group.py @@ -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): diff --git a/nova/tests/objects/test_objects.py b/nova/tests/objects/test_objects.py index d34c9c13c2b7..95691f43a08a 100644 --- a/nova/tests/objects/test_objects.py +++ b/nova/tests/objects/test_objects.py @@ -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', diff --git a/nova/tests/test_quota.py b/nova/tests/test_quota.py index b00ed7e9e127..b8ffce34c08e 100644 --- a/nova/tests/test_quota.py +++ b/nova/tests/test_quota.py @@ -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):