From: Clint Byrum Date: Thu, 5 Sep 2013 22:07:47 +0000 (-0700) Subject: Provide config option to limit resources per stack X-Git-Tag: 2014.1~20^2 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=ecf3954d2320fc79797d83873805168b8c837a8a;p=openstack-build%2Fheat-build.git Provide config option to limit resources per stack This provides an upper bounds on the number of resources a root level stack can contain. The limitation is only applied to the engine creation point so that existing stacks that are over the limit in the database will not cause problems. Nested stacks will be addressed in a follow-up patch. Partial-Bug: #1215100 Change-Id: I1adcb22cf9bd5750b4ae3f219dd3264d1d02c1fc --- diff --git a/etc/heat/heat.conf.sample b/etc/heat/heat.conf.sample index 50867b80..2608bf8b 100644 --- a/etc/heat/heat.conf.sample +++ b/etc/heat/heat.conf.sample @@ -31,6 +31,10 @@ # Subset of trustor roles to be delegated to heat (list value) #trusts_delegated_roles=heat_stack_owner +# Maximum resources allowed per top-level stack. (integer +# value) +#max_resources_per_stack=1000 + # Name of the engine node. This can be an opaque identifier.It # is not necessarily a hostname, FQDN, or IP address. (string # value) diff --git a/heat/common/config.py b/heat/common/config.py index 53d26b51..27f75532 100644 --- a/heat/common/config.py +++ b/heat/common/config.py @@ -97,8 +97,10 @@ engine_opts = [ 'stored password or trusts')), cfg.ListOpt('trusts_delegated_roles', default=['heat_stack_owner'], - help=_('Subset of trustor roles to be delegated to heat'))] - + help=_('Subset of trustor roles to be delegated to heat')), + cfg.IntOpt('max_resources_per_stack', + default=1000, + help='Maximum resources allowed per top-level stack.')] rpc_opts = [ cfg.StrOpt('host', diff --git a/heat/common/exception.py b/heat/common/exception.py index 22bc4f8e..7a00ec9c 100644 --- a/heat/common/exception.py +++ b/heat/common/exception.py @@ -322,3 +322,7 @@ class InvalidContentType(HeatException): class RequestLimitExceeded(HeatException): message = _('Request limit exceeded: %(message)s') + + +class StackResourceLimitExceeded(HeatException): + message = _('Maximum resources per stack exceeded.') diff --git a/heat/engine/service.py b/heat/engine/service.py index 61a414b7..9241c8d2 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -19,6 +19,8 @@ import json from oslo.config import cfg import webob +cfg.CONF.import_opt('max_resources_per_stack', 'heat.common.config') + from heat.openstack.common import timeutils from heat.common import context from heat.db import api as db_api @@ -36,6 +38,7 @@ from heat.engine import parser from heat.engine import properties from heat.engine import resource from heat.engine import resources +from heat.engine import template as tpl from heat.engine import watchrule from heat.openstack.common import log as logging @@ -249,6 +252,9 @@ class EngineService(service.Service): tmpl = parser.Template(template, files=files) + if len(tmpl[tpl.RESOURCES]) > cfg.CONF.max_resources_per_stack: + raise exception.StackResourceLimitExceeded() + # Extract the common query parameters common_params = api.extract_args(args) env = environment.Environment(params) diff --git a/heat/tests/test_engine_service.py b/heat/tests/test_engine_service.py index ea40369e..46829da4 100644 --- a/heat/tests/test_engine_service.py +++ b/heat/tests/test_engine_service.py @@ -34,6 +34,7 @@ from heat.engine import parser from heat.engine.resource import _register_class from heat.engine import service from heat.engine.properties import Properties +from heat.engine import resource as res from heat.engine.resources import instance as instances from heat.engine.resources import nova_utils from heat.engine import resource as rsrs @@ -413,6 +414,65 @@ class StackServiceCreateUpdateDeleteTest(HeatTestCase): self.assertEqual( 'Missing required credential: X-Auth-User', ex.message) + def test_stack_create_total_resources_equals_max(self): + stack_name = 'service_create_stack_total_resources_equals_max' + params = {} + res._register_class('GenericResourceType', + generic_rsrc.GenericResource) + tpl = {'Resources': { + 'A': {'Type': 'GenericResourceType'}, + 'B': {'Type': 'GenericResourceType'}, + 'C': {'Type': 'GenericResourceType'}}} + + template = parser.Template(tpl) + stack = parser.Stack(self.ctx, stack_name, template, + environment.Environment({})) + + self.m.StubOutWithMock(parser, 'Template') + self.m.StubOutWithMock(environment, 'Environment') + self.m.StubOutWithMock(parser, 'Stack') + + parser.Template(template, files=None).AndReturn(stack.t) + environment.Environment(params).AndReturn(stack.env) + parser.Stack(self.ctx, stack.name, + stack.t, + stack.env).AndReturn(stack) + + self.m.StubOutClassWithMocks(hkc.kc, "Client") + mock_ks_client = hkc.kc.Client( + auth_url=mox.IgnoreArg(), + tenant_name='test_tenant', + token='abcd1234') + mock_ks_client.authenticate().AndReturn(True) + + self.m.StubOutWithMock(hkc.KeystoneClient, 'create_trust_context') + hkc.KeystoneClient.create_trust_context().AndReturn(None) + + self.m.ReplayAll() + + cfg.CONF.set_override('max_resources_per_stack', 3) + + result = self.man.create_stack(self.ctx, stack_name, template, params, + None, {}) + self.m.VerifyAll() + self.assertEquals(stack.identifier(), result) + self.assertEquals(3, stack.total_resources()) + + def test_stack_create_total_resources_exceeds_max(self): + stack_name = 'service_create_stack_total_resources_exceeds_max' + params = {} + res._register_class('GenericResourceType', + generic_rsrc.GenericResource) + tpl = {'Resources': { + 'A': {'Type': 'GenericResourceType'}, + 'B': {'Type': 'GenericResourceType'}, + 'C': {'Type': 'GenericResourceType'}}} + template = parser.Template(tpl) + cfg.CONF.set_override('max_resources_per_stack', 2) + self.assertRaises(exception.StackResourceLimitExceeded, + self.man.create_stack, self.ctx, stack_name, + template, params, None, {}) + def test_stack_validate(self): stack_name = 'service_create_test_validate' stack = get_wordpress_stack(stack_name, self.ctx)