diff --git a/config.yaml b/config.yaml index 1b7f39af..2a0f2341 100644 --- a/config.yaml +++ b/config.yaml @@ -411,3 +411,12 @@ options: description: | List of filter class names to use for filtering hosts when not specified in the request. + api-rate-limit-rules: + type: string + default: + description: | + The API rate-limit rules to use for the deployed nova API, if any. + Contents of this config options will be inserted in the api-paste.ini file + under the "filter:ratelimit" section as "limits". The syntax for these + rules is documented at + http://docs.openstack.org/kilo/config-reference/content/configuring-compute-API.html diff --git a/hooks/nova_cc_context.py b/hooks/nova_cc_context.py index 06138fc1..ff471899 100644 --- a/hooks/nova_cc_context.py +++ b/hooks/nova_cc_context.py @@ -409,3 +409,11 @@ class ConsoleSSLContext(context.OSContextGenerator): ctxt['html5proxy_base_url'] = url return ctxt + +class APIRateLimitingContext(context.OSContextGenerator): + def __call__(self): + ctxt = {} + rate_rules = config('api-rate-limit-rules') + if rate_rules: + ctxt['api_rate_limit_rules'] = rate_rules + return ctxt diff --git a/hooks/nova_cc_utils.py b/hooks/nova_cc_utils.py index b959ca6f..0d571ce0 100644 --- a/hooks/nova_cc_utils.py +++ b/hooks/nova_cc_utils.py @@ -199,7 +199,8 @@ BASE_RESOURCE_MAP = OrderedDict([ }), (NOVA_API_PASTE, { 'services': [s for s in BASE_SERVICES if 'api' in s], - 'contexts': [nova_cc_context.IdentityServiceContext()], + 'contexts': [nova_cc_context.IdentityServiceContext(), + nova_cc_context.APIRateLimitingContext()], }), (QUANTUM_CONF, { 'services': ['quantum-server'], diff --git a/templates/icehouse/etc_nova_api-paste.ini b/templates/icehouse/etc_nova_api-paste.ini index aad3d0f3..16211daa 100644 --- a/templates/icehouse/etc_nova_api-paste.ini +++ b/templates/icehouse/etc_nova_api-paste.ini @@ -85,6 +85,9 @@ paste.filter_factory = nova.api.openstack.auth:NoAuthMiddlewareV3.factory [filter:ratelimit] paste.filter_factory = nova.api.openstack.compute.limits:RateLimitingMiddleware.factory +{% if api_rate_limit_rules -%} +limits = {{ api_rate_limit_rules }} +{% endif -%} [filter:sizelimit] paste.filter_factory = nova.api.sizelimit:RequestBodySizeLimiter.factory diff --git a/templates/kilo/etc_nova_api-paste.ini b/templates/kilo/etc_nova_api-paste.ini index 6c69251d..8ba8f4c3 100644 --- a/templates/kilo/etc_nova_api-paste.ini +++ b/templates/kilo/etc_nova_api-paste.ini @@ -104,6 +104,9 @@ paste.filter_factory = nova.api.openstack.auth:NoAuthMiddlewareV3.factory [filter:ratelimit] paste.filter_factory = nova.api.openstack.compute.limits:RateLimitingMiddleware.factory +{% if api_rate_limit_rules -%} +limits = {{ api_rate_limit_rules }} +{% endif -%} [filter:sizelimit] paste.filter_factory = oslo.middleware:RequestBodySizeLimiter.factory diff --git a/tests/basic_deployment.py b/tests/basic_deployment.py index 47edc50b..453e0b72 100644 --- a/tests/basic_deployment.py +++ b/tests/basic_deployment.py @@ -98,6 +98,10 @@ class NovaCCBasicDeployment(OpenStackAmuletDeployment): } nova_cc_config['openstack-origin-git'] = yaml.dump(openstack_origin_git) nova_config['openstack-origin-git'] = yaml.dump(openstack_origin_git) + + # Add some rate-limiting options to the charm. These will noop before + # icehouse. + nova_cc_config['api-rate-limit-rules'] = "( POST, '*', .*, 9999, MINUTE )" keystone_config = {'admin-password': 'openstack', 'admin-token': 'ubuntutesting'} configs = {'nova-cloud-controller': nova_cc_config, @@ -647,3 +651,25 @@ class NovaCCBasicDeployment(OpenStackAmuletDeployment): u.delete_image(self.glance, image) u.delete_instance(self.nova_demo, instance) + + def test_api_rate_limiting_is_enabled_for_icehouse_or_more(self): + """ + The API rate limiting is enabled for icehouse or more. Otherwise the + api-paste.ini file is left untouched. + """ + unit = self.nova_cc_sentry + conf = '/etc/nova/api-paste.ini' + section = "filter:ratelimit" + factory = ("nova.api.openstack.compute.limits:RateLimitingMiddleware" + ".factory") + + if self._get_openstack_release() >= self.precise_icehouse: + expected = {"paste.filter_factory": factory, + "limits": "( POST, *, .*, 9999, MINUTE );"} + else: + expected = {"paste.filter_factory": factory} + + ret = u.validate_config_data(unit, conf, section, expected) + if ret: + message = "api paste config error: {}".format(ret) + amulet.raise_status(amulet.FAIL, msg=message)