Support ram quota

Set default to -1 so this is backwards compatible. Existing
installations will need to manully backfill quote usage for this
to work as expected.

Story: 2008293
Task: 41172

Change-Id: I455477a2e7a00f0d132971a2a684352967ac19b9
This commit is contained in:
Sam Morrison 2020-10-29 15:32:28 +11:00 committed by Lingxian Kong
parent 78772cef68
commit 5be23d1b20
7 changed files with 50 additions and 19 deletions

View File

@ -0,0 +1,7 @@
---
features:
- |
Added the ability to quota on total amount of RAM in MB used per project.
Set ``quota.max_ram_per_tenant`` to enable. Default is -1 (unlimited)
to be backwards compatible. Existing installations will need to manually
backfill quote usage for this to work as expected.

View File

@ -221,6 +221,9 @@ common_opts = [
default=10,
help='Default maximum number of instances per tenant.',
deprecated_name='max_instances_per_user'),
cfg.IntOpt('max_ram_per_tenant',
default=-1,
help='Default maximum total amount of RAM in MB per tenant.'),
cfg.IntOpt('max_accepted_volume_size', default=10,
help='Default maximum volume size (in GB) for an instance.'),
cfg.IntOpt('max_volumes_per_tenant', default=40,

View File

@ -716,8 +716,8 @@ class BaseInstance(SimpleInstance):
self.update_db(task_status=InstanceTasks.DELETING,
configuration_id=None)
task_api.API(self.context).delete_instance(self.id)
deltas = {'instances': -1}
flavor = self.get_flavor()
deltas = {'instances': -1, 'ram': -flavor.ram}
if self.volume_support:
deltas['volumes'] = -self.volume_size
return run_with_quotas(self.tenant_id,
@ -913,6 +913,9 @@ class BaseInstance(SimpleInstance):
except exception.ModelNotFoundError:
pass
def get_flavor(self):
return self.nova_client.flavors.get(self.flavor_id)
@property
def volume_client(self):
if not self._volume_client:
@ -1153,7 +1156,7 @@ class Instance(BuiltInstance):
cls._validate_remote_datastore(context, region_name, flavor,
datastore, datastore_version)
deltas = {'instances': 1}
deltas = {'instances': 1, 'ram': flavor.ram}
if volume_support:
if replica_source:
try:
@ -1351,9 +1354,6 @@ class Instance(BuiltInstance):
module_models.InstanceModule.create(
context, instance_id, module.id, module.md5)
def get_flavor(self):
return self.nova_client.flavors.get(self.flavor_id)
def get_default_configuration_template(self):
flavor = self.get_flavor()
LOG.debug("Getting default config template for datastore version "
@ -1371,13 +1371,13 @@ class Instance(BuiltInstance):
if self.db_info.cluster_id is not None:
raise exception.ClusterInstanceOperationNotSupported()
# Validate that the old and new flavor IDs are not the same, new flavor
# can be found and has ephemeral/volume support if required by the
# current flavor.
# Validate that the old and new flavor IDs are not the same, new
# flavor can be found and has ephemeral/volume support if required
# by the current flavor.
if self.flavor_id == new_flavor_id:
raise exception.BadRequest(_("The new flavor id must be different "
"than the current flavor id of '%s'.")
% self.flavor_id)
raise exception.BadRequest(
_("The new flavor id must be different "
"than the current flavor id of '%s'.") % self.flavor_id)
try:
new_flavor = self.nova_client.flavors.get(new_flavor_id)
except nova_exceptions.NotFound:
@ -1390,13 +1390,20 @@ class Instance(BuiltInstance):
elif self.device_path is not None:
# ephemeral support enabled
if new_flavor.ephemeral == 0:
raise exception.LocalStorageNotSpecified(flavor=new_flavor_id)
raise exception.LocalStorageNotSpecified(
flavor=new_flavor_id)
# Set the task to RESIZING and begin the async call before returning.
self.update_db(task_status=InstanceTasks.RESIZING)
LOG.debug("Instance %s set to RESIZING.", self.id)
task_api.API(self.context).resize_flavor(self.id, old_flavor,
new_flavor)
def _resize_flavor():
# Set the task to RESIZING and begin the async call before
# returning.
self.update_db(task_status=InstanceTasks.RESIZING)
LOG.debug("Instance %s set to RESIZING.", self.id)
task_api.API(self.context).resize_flavor(self.id, old_flavor,
new_flavor)
return run_with_quotas(self.tenant_id,
{'ram': new_flavor.ram - old_flavor.ram},
_resize_flavor)
def resize_volume(self, new_size):
"""Resize instance volume.

View File

@ -75,6 +75,7 @@ class Resource(object):
"""Describe a single resource for quota checking."""
INSTANCES = 'instances'
RAM = 'ram'
VOLUMES = 'volumes'
BACKUPS = 'backups'

View File

@ -349,6 +349,7 @@ QUOTAS = QuotaEngine()
''' Define all kind of resources here '''
resources = [Resource(Resource.INSTANCES, 'max_instances_per_tenant'),
Resource(Resource.RAM, 'max_ram_per_tenant'),
Resource(Resource.BACKUPS, 'max_backups_per_tenant'),
Resource(Resource.VOLUMES, 'max_volumes_per_tenant')]

View File

@ -39,6 +39,7 @@ DEFAULT_RATE = CONF.http_get_rate
DEFAULT_MAX_VOLUMES = CONF.max_volumes_per_tenant
DEFAULT_MAX_INSTANCES = CONF.max_instances_per_tenant
DEFAULT_MAX_BACKUPS = CONF.max_backups_per_tenant
DEFAULT_MAX_RAM = CONF.max_ram_per_tenant
def ensure_limits_are_not_faked(func):
@ -109,6 +110,7 @@ class Limits(object):
assert_equal(int(abs_limits.max_instances), DEFAULT_MAX_INSTANCES)
assert_equal(int(abs_limits.max_backups), DEFAULT_MAX_BACKUPS)
assert_equal(int(abs_limits.max_volumes), DEFAULT_MAX_VOLUMES)
assert_equal(int(abs_limits.max_ram), DEFAULT_MAX_RAM)
for k in d:
assert_equal(d[k].verb, k)
@ -132,6 +134,7 @@ class Limits(object):
assert_equal(int(abs_limits.max_instances), DEFAULT_MAX_INSTANCES)
assert_equal(int(abs_limits.max_backups), DEFAULT_MAX_BACKUPS)
assert_equal(int(abs_limits.max_volumes), DEFAULT_MAX_VOLUMES)
assert_equal(int(abs_limits.max_ram), DEFAULT_MAX_RAM)
assert_equal(get.verb, "GET")
assert_equal(get.unit, "MINUTE")
assert_true(int(get.remaining) <= DEFAULT_RATE - 5)
@ -163,6 +166,8 @@ class Limits(object):
DEFAULT_MAX_BACKUPS)
assert_equal(int(abs_limits.max_volumes),
DEFAULT_MAX_VOLUMES)
assert_equal(int(abs_limits.max_ram,),
DEFAULT_MAX_RAM)
except exceptions.OverLimit:
encountered = True

View File

@ -48,6 +48,7 @@ class BaseLimitTestSuite(trove_testtools.TestCase):
self.context = trove_testtools.TroveTestContext(self)
self.absolute_limits = {"max_instances": 55,
"max_volumes": 100,
"max_ram": 200,
"max_backups": 40}
@ -114,6 +115,10 @@ class LimitsControllerTest(BaseLimitTestSuite):
resource="instances",
hard_limit=100),
"ram": Quota(tenant_id=tenant_id,
resource="ram",
hard_limit=200),
"backups": Quota(tenant_id=tenant_id,
resource="backups",
hard_limit=40),
@ -135,6 +140,7 @@ class LimitsControllerTest(BaseLimitTestSuite):
{
'max_instances': 100,
'max_backups': 40,
'max_ram': 200,
'verb': 'ABSOLUTE',
'max_volumes': 55
},
@ -798,7 +804,7 @@ class LimitsViewsTest(trove_testtools.TestCase):
"resetTime": 1311272226
}
]
abs_view = {"instances": 55, "volumes": 100, "backups": 40}
abs_view = {"instances": 55, "volumes": 100, "backups": 40, 'ram': 200}
view_data = views.LimitViews(abs_view, rate_limits)
self.assertIsNotNone(view_data)
@ -806,6 +812,7 @@ class LimitsViewsTest(trove_testtools.TestCase):
data = view_data.data()
expected = {'limits': [{'max_instances': 55,
'max_backups': 40,
'max_ram': 200,
'verb': 'ABSOLUTE',
'max_volumes': 100},
{'regex': '.*',