From e048bb246e167707115d1141e5840297f19c777e Mon Sep 17 00:00:00 2001 From: Tim Schnell Date: Thu, 12 Dec 2013 22:52:50 +0000 Subject: [PATCH] Updates template_validate call to validate parameter_groups. When calling template_validate, the parameter_groups will now be checked for valid parameter references and parameter integrity across groups. Change-Id: I0cbca0d443b68b8932a155a637a9aedc8eb0c2f9 Implements: blueprint parameter-grouping-ordering --- heat/engine/parameter_groups.py | 68 +++++++++++ heat/engine/parser.py | 5 + heat/engine/service.py | 6 + heat/tests/test_validate.py | 198 ++++++++++++++++++++++++++++++++ 4 files changed, 277 insertions(+) create mode 100644 heat/engine/parameter_groups.py diff --git a/heat/engine/parameter_groups.py b/heat/engine/parameter_groups.py new file mode 100644 index 0000000000..fb6ef8a3d0 --- /dev/null +++ b/heat/engine/parameter_groups.py @@ -0,0 +1,68 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from heat.openstack.common import log as logging +from heat.openstack.common.gettextutils import _ + +from heat.common.exception import StackValidationFailed + +logger = logging.getLogger(__name__) + +PARAMETER_GROUPS = 'parameter_groups' +PARAMETERS = 'parameters' + + +class ParameterGroups(object): + ''' + The ParameterGroups specified by the stack's template. + ''' + def __init__(self, tmpl): + self.tmpl = tmpl + self.parameters = tmpl.parameters(None, {}, validate_value=False) + logger.debug(self.tmpl) + logger.debug(self.parameters) + self.parameter_names = [] + if self.parameters: + self.parameter_names = [param for param in self.parameters] + self.parameter_groups = tmpl.get(PARAMETER_GROUPS) + + def validate(self): + ''' + Validate that a parameter belongs to only one Parameter Group + and that each parameter name references a valid parameter. + ''' + logger.debug(_('Validating Parameter Groups.')) + logger.debug(self.parameter_names) + if self.parameter_groups is not None: + #Loop through groups and validate parameters + grouped_parameters = [] + for group in self.parameter_groups: + parameters = group.get(PARAMETERS) + + if parameters is None: + raise StackValidationFailed(message=_( + 'Parameters must be provided for ' + 'each Parameter Group.')) + + for param in parameters: + #Check if param has been added to a previous group + if param in grouped_parameters: + raise StackValidationFailed(message=_( + 'The %s parameter must be assigned to one ' + 'Parameter Group only.') % param) + else: + grouped_parameters.append(param) + + #Check that grouped parameter references a valid Parameter + if param not in self.parameter_names: + raise StackValidationFailed(message=_( + 'The Parameter name (%s) does not reference ' + 'an existing parameter.') % param) diff --git a/heat/engine/parser.py b/heat/engine/parser.py index 10188ec9da..050b80c48e 100644 --- a/heat/engine/parser.py +++ b/heat/engine/parser.py @@ -31,6 +31,7 @@ from heat.engine import scheduler from heat.engine import timestamp from heat.engine import update from heat.engine.notification import stack as notification +from heat.engine.parameter_groups import ParameterGroups from heat.engine.template import Template from heat.engine.clients import Clients from heat.db import api as db_api @@ -319,6 +320,10 @@ class Stack(collections.Mapping): ''' # TODO(sdake) Should return line number of invalid reference + # Validate Parameter Groups + parameter_groups = ParameterGroups(self.t) + parameter_groups.validate() + # Check duplicate names between parameters and resources dup_names = set(self.parameters.keys()) & set(self.keys()) diff --git a/heat/engine/service.py b/heat/engine/service.py index 8586688245..92e2510149 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -34,6 +34,7 @@ from heat.engine import environment from heat.common import exception from heat.common import identifier from heat.common import heat_keystoneclient as hkc +from heat.engine import parameter_groups from heat.engine import parser from heat.engine import properties from heat.engine import resource @@ -505,11 +506,16 @@ class EngineService(service.Service): tmpl_params = tmpl.parameters(None, {}, validate_value=False) is_real_param = lambda p: p.name not in tmpl_params.PSEUDO_PARAMETERS params = tmpl_params.map(api.format_validate_parameter, is_real_param) + param_groups = parameter_groups.ParameterGroups(tmpl) result = { 'Description': tmpl.get('Description', ''), 'Parameters': params, } + + if param_groups.parameter_groups: + result['ParameterGroups'] = param_groups.parameter_groups + return result @request_context diff --git a/heat/tests/test_validate.py b/heat/tests/test_validate.py index 80d791a50a..c36f05f5e5 100644 --- a/heat/tests/test_validate.py +++ b/heat/tests/test_validate.py @@ -25,6 +25,7 @@ from heat.engine.resources import instance as instances from heat.engine import service from heat.openstack.common.importutils import try_import from heat.engine import parser +from heat.engine.hot import HOTemplate from heat.tests.common import HeatTestCase from heat.tests import utils @@ -537,6 +538,163 @@ test_template_unique_logical_name = ''' } ''' +test_template_duplicate_parameters = ''' +# This is a hello world HOT template just defining a single compute instance +heat_template_version: 2013-05-23 + +description: > + Hello world HOT template that just defines a single compute instance. + Contains just base features to verify base HOT support. + +parameter_groups: + - label: Server Group + description: A group of parameters for the server + parameters: + - InstanceType + - KeyName + - ImageId + - label: Database Group + description: A group of parameters for the database + parameters: + - db_password + - db_port + - InstanceType + +parameters: + KeyName: + type: string + description: Name of an existing key pair to use for the instance + InstanceType: + type: string + description: Instance type for the instance to be created + default: m1.small + constraints: + - allowed_values: [m1.tiny, m1.small, m1.large] + description: Value must be one of 'm1.tiny', 'm1.small' or 'm1.large' + ImageId: + type: string + description: ID of the image to use for the instance + # parameters below are not used in template, but are for verifying parameter + # validation support in HOT + db_password: + type: string + description: Database password + hidden: true + constraints: + - length: { min: 6, max: 8 } + description: Password length must be between 6 and 8 characters + - allowed_pattern: "[a-zA-Z0-9]+" + description: Password must consist of characters and numbers only + - allowed_pattern: "[A-Z]+[a-zA-Z0-9]*" + description: Password must start with an uppercase character + db_port: + type: number + description: Database port number + default: 50000 + constraints: + - range: { min: 40000, max: 60000 } + description: Port number must be between 40000 and 60000 + +resources: + my_instance: + # Use an AWS resource type since this exists; so why use other name here? + type: AWS::EC2::Instance + properties: + KeyName: { get_param: KeyName } + ImageId: { get_param: ImageId } + InstanceType: { get_param: InstanceType } + +outputs: + instance_ip: + description: The IP address of the deployed instance + value: { get_attr: [my_instance, PublicIp] } +''' + +test_template_invalid_parameter_name = ''' +# This is a hello world HOT template just defining a single compute instance +heat_template_version: 2013-05-23 + +description: > + Hello world HOT template that just defines a single compute instance. + Contains just base features to verify base HOT support. + +parameter_groups: + - label: Server Group + description: A group of parameters for the server + parameters: + - InstanceType + - KeyName + - ImageId + - label: Database Group + description: A group of parameters for the database + parameters: + - db_password + - db_port + - SomethingNotHere + +parameters: + KeyName: + type: string + description: Name of an existing key pair to use for the instance + InstanceType: + type: string + description: Instance type for the instance to be created + default: m1.small + constraints: + - allowed_values: [m1.tiny, m1.small, m1.large] + description: Value must be one of 'm1.tiny', 'm1.small' or 'm1.large' + ImageId: + type: string + description: ID of the image to use for the instance + # parameters below are not used in template, but are for verifying parameter + # validation support in HOT + db_password: + type: string + description: Database password + hidden: true + constraints: + - length: { min: 6, max: 8 } + description: Password length must be between 6 and 8 characters + - allowed_pattern: "[a-zA-Z0-9]+" + description: Password must consist of characters and numbers only + - allowed_pattern: "[A-Z]+[a-zA-Z0-9]*" + description: Password must start with an uppercase character + db_port: + type: number + description: Database port number + default: 50000 + constraints: + - range: { min: 40000, max: 60000 } + description: Port number must be between 40000 and 60000 + +resources: + my_instance: + # Use an AWS resource type since this exists; so why use other name here? + type: AWS::EC2::Instance + properties: + KeyName: { get_param: KeyName } + ImageId: { get_param: ImageId } + InstanceType: { get_param: InstanceType } + +outputs: + instance_ip: + description: The IP address of the deployed instance + value: { get_attr: [my_instance, PublicIp] } +''' +test_template_no_parameters = ''' +heat_template_version: 2013-05-23 + +description: > + Hello world HOT template that just defines a single compute instance. + Contains just base features to verify base HOT support. + +parameter_groups: + - label: Server Group + description: A group of parameters for the server + - label: Database Group + description: A group of parameters for the database +''' + class validateTest(HeatTestCase): def setUp(self): @@ -849,3 +1007,43 @@ class validateTest(HeatTestCase): 'KeyName': 'test'})) self.assertRaises(exception.StackValidationFailed, stack.validate) + + def test_validate_duplicate_parameters_in_group(self): + t = template_format.parse(test_template_duplicate_parameters) + template = HOTemplate(t) + stack = parser.Stack(self.ctx, 'test_stack', template, + environment.Environment({ + 'KeyName': 'test', + 'ImageId': 'sometestid', + 'db_password': 'Pass123' + })) + exc = self.assertRaises(exception.StackValidationFailed, + stack.validate) + + self.assertEqual(_('The InstanceType parameter must be assigned to ' + 'one Parameter Group only.'), str(exc)) + + def test_validate_invalid_parameter_in_group(self): + t = template_format.parse(test_template_invalid_parameter_name) + template = HOTemplate(t) + stack = parser.Stack(self.ctx, 'test_stack', template, + environment.Environment({ + 'KeyName': 'test', + 'ImageId': 'sometestid', + 'db_password': 'Pass123'})) + + exc = self.assertRaises(exception.StackValidationFailed, + stack.validate) + + self.assertEqual(_('The Parameter name (SomethingNotHere) does not ' + 'reference an existing parameter.'), str(exc)) + + def test_validate_no_parameters_in_group(self): + t = template_format.parse(test_template_no_parameters) + template = HOTemplate(t) + stack = parser.Stack(self.ctx, 'test_stack', template) + exc = self.assertRaises(exception.StackValidationFailed, + stack.validate) + + self.assertEqual(_('Parameters must be provided for each Parameter ' + 'Group.'), str(exc))