Allow more options to limit number of resources

This commit adds the configuration options related to resource limits
in the Heat project. The `max_software_configs_per_tenant`,
`max_software_deployments_per_tenant`, and `max_snapshots_per_stack`
options have been added to control the maximum limits for software
configs, software deployments, stack snapshots.

Story: 2011006
Task: 49401
Change-Id: If33a1c6f3eb9e93f586931bc5c05104439c92bf9
This commit is contained in:
lujiefsi 2024-01-22 22:04:25 +08:00 committed by Takashi Kajinami
parent 13ec108b0d
commit 7d86fc6d84
11 changed files with 202 additions and 0 deletions

View File

@ -152,6 +152,19 @@ engine_opts = [
default=512,
help=_('Maximum number of stacks any one tenant may have '
'active at one time. -1 stands for unlimited.')),
cfg.IntOpt('max_software_configs_per_tenant',
default=4096,
help=_('Maximum number of software configs any one tenant may '
'have active at one time. -1 stands for unlimited.')),
cfg.IntOpt('max_software_deployments_per_tenant',
default=4096,
help=_('Maximum number of software deployments any one tenant '
'may have active at one time.'
'-1 stands for unlimited.')),
cfg.IntOpt('max_snapshots_per_stack',
default=32,
help=_('Maximum number of snapshot any one stack may have '
'active at one time. -1 stands for unlimited.')),
cfg.IntOpt('action_retry_limit',
default=5,
help=_('Number of times to retry to bring a '

View File

@ -1431,6 +1431,14 @@ def software_config_get_all(context, limit=None, marker=None):
limit=limit, marker=marker).all()
@context_manager.reader
def software_config_count_all(context):
query = context.session.query(models.SoftwareConfig)
if not context.is_admin:
query = query.filter_by(tenant=context.tenant_id)
return query.count()
@context_manager.writer
def software_config_delete(context, config_id):
config = _software_config_get(context, config_id)
@ -1510,6 +1518,21 @@ def software_deployment_get_all(context, server_id=None):
return query.all()
@context_manager.reader
def software_deployment_count_all(context):
sd = models.SoftwareDeployment
query = context.session.query(sd)
if not context.is_admin:
query = query.filter(
sqlalchemy.or_(
sd.tenant == context.tenant_id,
sd.stack_user_project_id == context.tenant_id,
)
)
return query.count()
@context_manager.writer
def software_deployment_update(context, deployment_id, values):
deployment = _software_deployment_get(context, deployment_id)
@ -1587,6 +1610,12 @@ def snapshot_get_all_by_stack(context, stack_id):
stack_id=stack_id, tenant=context.tenant_id)
@context_manager.reader
def snapshot_count_all_by_stack(context, stack_id):
return context.session.query(models.Snapshot).filter_by(
stack_id=stack_id, tenant=context.tenant_id).count()
# service

View File

@ -73,6 +73,10 @@ from heat.rpc import worker_api as rpc_worker_api
cfg.CONF.import_opt('engine_life_check_timeout', 'heat.common.config')
cfg.CONF.import_opt('max_resources_per_stack', 'heat.common.config')
cfg.CONF.import_opt('max_stacks_per_tenant', 'heat.common.config')
cfg.CONF.import_opt('max_snapshots_per_stack', 'heat.common.config')
cfg.CONF.import_opt('max_software_configs_per_tenant', 'heat.common.config')
cfg.CONF.import_opt('max_software_deployments_per_tenant',
'heat.common.config')
cfg.CONF.import_opt('enable_stack_abandon', 'heat.common.config')
cfg.CONF.import_opt('enable_stack_adopt', 'heat.common.config')
cfg.CONF.import_opt('convergence_engine', 'heat.common.config')
@ -2124,6 +2128,17 @@ class EngineService(service.ServiceBase):
raise exception.ActionInProgress(stack_name=stack.name,
action=stack.action)
# Do not enforce the limit, following the stack limit
if not cnxt.is_admin:
stack_limit = cfg.CONF.max_snapshots_per_stack
count_all = snapshot_object.Snapshot.count_all_by_stack(cnxt,
stack.id)
if (stack_limit >= 0 and count_all >= stack_limit):
message = _("You have reached the maximum snapshots "
"per stack, %d. Please delete some "
"snapshots.") % stack_limit
raise exception.RequestLimitExceeded(message=message)
lock = stack_lock.StackLock(cnxt, stack.id, self.engine_id)
with lock.thread_lock():
@ -2219,6 +2234,15 @@ class EngineService(service.ServiceBase):
@context.request_context
def create_software_config(self, cnxt, group, name, config,
inputs, outputs, options):
# Do not enforce the limit, following the stack limit
if not cnxt.is_admin:
tenant_limit = cfg.CONF.max_software_configs_per_tenant
count_all = self.software_config.count_software_config(cnxt)
if (tenant_limit >= 0 and count_all >= tenant_limit):
message = _("You have reached the maximum software configs "
"per tenant, %d. "
"Please delete some configs.") % tenant_limit
raise exception.RequestLimitExceeded(message=message)
return self.software_config.create_software_config(
cnxt,
group=group,
@ -2257,6 +2281,16 @@ class EngineService(service.ServiceBase):
input_values, action, status,
status_reason, stack_user_project_id,
deployment_id=None):
# Do not enforce the limit, following the stack limit
if not cnxt.is_admin:
tenant_limit = cfg.CONF.max_software_deployments_per_tenant
count_all = self.software_config.count_software_deployment(cnxt)
if (tenant_limit >= 0 and
count_all >= tenant_limit):
message = _("You have reached the maximum software "
"deployments per tenant, %d. "
"Please delete some deployments.") % tenant_limit
raise exception.RequestLimitExceeded(message=message)
return self.software_config.create_software_deployment(
cnxt, server_id=server_id,
config_id=config_id,

View File

@ -52,6 +52,9 @@ class SoftwareConfigService(object):
for sc in scs]
return result
def count_software_config(self, cnxt):
return software_config_object.SoftwareConfig.count_all(cnxt)
def create_software_config(self, cnxt, group, name, config,
inputs, outputs, options):
@ -81,6 +84,10 @@ class SoftwareConfigService(object):
result = [api.format_software_deployment(sd) for sd in all_sd]
return result
def count_software_deployment(self, cnxt):
return software_deployment_object.SoftwareDeployment.count_all(
cnxt)
def metadata_software_deployments(self, cnxt, server_id):
if not server_id:
raise ValueError(_('server_id must be specified'))

View File

@ -74,3 +74,7 @@ class Snapshot(
return [cls._from_db_object(context, cls(), db_snapshot)
for db_snapshot
in db_api.snapshot_get_all_by_stack(context, stack_id)]
@classmethod
def count_all_by_stack(cls, context, stack_id):
return db_api.snapshot_count_all_by_stack(context, stack_id)

View File

@ -65,6 +65,10 @@ class SoftwareConfig(
scs = db_api.software_config_get_all(context, **kwargs)
return [cls._from_db_object(context, cls(), sc) for sc in scs]
@classmethod
def count_all(cls, context, **kwargs):
return db_api.software_config_count_all(context, **kwargs)
@classmethod
def delete(cls, context, config_id):
db_api.software_config_delete(context, config_id)

View File

@ -77,6 +77,11 @@ class SoftwareDeployment(
for db_deployment in db_api.software_deployment_get_all(
context, server_id)]
@classmethod
def count_all(cls, context):
return db_api.software_deployment_count_all(
context)
@classmethod
def update_by_id(cls, context, deployment_id, values):
"""Note this is a bit unusual as it returns the object.

View File

@ -1121,6 +1121,13 @@ class SqlAlchemyTest(common.HeatTestCase):
tenant_id='admin_tenant')
self._test_software_config_get_all(get_ctx=admin_ctx)
def test_software_config_count_all(self):
self.assertEqual(0, db_api.software_config_count_all(self.ctx))
self._create_software_config_record()
self._create_software_config_record()
self._create_software_config_record()
self.assertEqual(3, db_api.software_config_count_all(self.ctx))
def test_software_config_delete(self):
scf_id = self._create_software_config_record()
@ -1250,6 +1257,17 @@ class SqlAlchemyTest(common.HeatTestCase):
deployments = db_api.software_deployment_get_all(admin_ctx)
self.assertEqual(1, len(deployments))
def test_software_deployment_count_all(self):
self.assertEqual(0, db_api.software_deployment_count_all(self.ctx))
values = self._deployment_values()
deployment = db_api.software_deployment_create(self.ctx, values)
self.assertIsNotNone(deployment)
deployment = db_api.software_deployment_create(self.ctx, values)
self.assertIsNotNone(deployment)
deployment = db_api.software_deployment_create(self.ctx, values)
self.assertIsNotNone(deployment)
self.assertEqual(3, db_api.software_deployment_count_all(self.ctx))
def test_software_deployment_update(self):
deployment_id = str(uuid.uuid4())
err = self.assertRaises(exception.NotFound,
@ -1435,6 +1453,38 @@ class SqlAlchemyTest(common.HeatTestCase):
self.assertEqual(values['status'], snapshot.status)
self.assertIsNotNone(snapshot.created_at)
def test_snapshot_count_all_by_stack(self):
template = create_raw_template(self.ctx)
user_creds = create_user_creds(self.ctx)
stack1 = create_stack(self.ctx, template, user_creds)
stack2 = create_stack(self.ctx, template, user_creds)
values = [
{
'tenant': self.ctx.tenant_id,
'status': 'IN_PROGRESS',
'stack_id': stack1.id,
'name': 'snp1'
},
{
'tenant': self.ctx.tenant_id,
'status': 'IN_PROGRESS',
'stack_id': stack1.id,
'name': 'snp1'
},
{
'tenant': self.ctx.tenant_id,
'status': 'IN_PROGRESS',
'stack_id': stack2.id,
'name': 'snp2'
}
]
for val in values:
self.assertIsNotNone(db_api.snapshot_create(self.ctx, val))
self.assertEqual(2, db_api.snapshot_count_all_by_stack(self.ctx,
stack1.id))
self.assertEqual(1, db_api.snapshot_count_all_by_stack(self.ctx,
stack2.id))
def create_raw_template(context, **kwargs):
t = template_format.parse(wp_template)

View File

@ -15,6 +15,7 @@ import datetime
from unittest import mock
import uuid
from oslo_config import cfg
from oslo_messaging.rpc import dispatcher
from oslo_serialization import jsonutils as json
from oslo_utils import timeutils
@ -169,6 +170,14 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
config['outputs'])
self.assertEqual(kwargs['options'], config['options'])
def test_create_config_exceeds_max_per_tenant(self):
cfg.CONF.set_override('max_software_configs_per_tenant', 0)
ex = self.assertRaises(dispatcher.ExpectedException,
self._create_software_config)
self.assertEqual(exception.RequestLimitExceeded, ex.exc_info[0])
self.assertIn("You have reached the maximum software configs "
"per tenant", str(ex.exc_info[1]))
def test_create_software_config_structured(self):
kwargs = {
'group': 'json-file',
@ -504,6 +513,14 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
self.assertEqual(deployment_id, deployment['id'])
self.assertEqual(kwargs['input_values'], deployment['input_values'])
def test_create_deployment_exceeds_max_per_tenant(self):
cfg.CONF.set_override('max_software_deployments_per_tenant', 0)
ex = self.assertRaises(dispatcher.ExpectedException,
self._create_software_deployment)
self.assertEqual(exception.RequestLimitExceeded, ex.exc_info[0])
self.assertIn("You have reached the maximum software deployments"
" per tenant", str(ex.exc_info[1]))
def test_create_software_deployment_invalid_stack_user_project_id(self):
sc_kwargs = {
'group': 'Heat::Chef',

View File

@ -98,6 +98,19 @@ class SnapshotServiceTest(common.HeatTestCase):
self.assertIsNotNone(snapshot['creation_time'])
mock_load.assert_called_once_with(self.ctx, stack=mock.ANY)
@mock.patch.object(stack.Stack, 'load')
def test_create_snapshot_exceeds_max_per_stack(self, mock_load):
stk = self._create_stack('stack_snapshot_exceeds_max')
mock_load.return_value = stk
cfg.CONF.set_override('max_snapshots_per_stack', 0)
ex = self.assertRaises(dispatcher.ExpectedException,
self.engine.stack_snapshot,
self.ctx, stk.identifier(), 'snap_none')
self.assertEqual(exception.RequestLimitExceeded, ex.exc_info[0])
self.assertIn("You have reached the maximum snapshots per stack",
str(ex.exc_info[1]))
@mock.patch.object(stack.Stack, 'load')
def test_create_snapshot_action_in_progress(self, mock_load):
stack_name = 'stack_snapshot_action_in_progress'

View File

@ -0,0 +1,26 @@
---
features:
- |
Heat now supports limiting number of software configs, software
deployments, stack snapshots which users can create, by the following
config options. These limits are not enforced for users with admin role.
- ``[DEFAULT] max_software_configis_per_tenant``
- ``[DEFAULT] max_software_deployments_per_tenant``
- ``[DEFAULT] max_snapshots_per_stack``
upgrade:
- |
Now the following limits are enforced by default, unless a request user
has admin role.
- Maximum number of software configs per project is 4096
- Maximum number of software deployments per project is 4096
- Maximum number of stack snapshots per tenant is 32
Set the following options in case the limits should be increased. Limits
can be disabled by setting -1 to these options.
- ``[DEFAULT] max_software_configis_per_tenant``
- ``[DEFAULT] max_software_deployments_per_tenant``
- ``[DEFAULT] max_snapshots_per_stack``