Convert validate_template validation path

Currently the validation code for template-validate is different to
that used for create/preview/update, which can lead to inconsistent
results as the implementations have diverged.

So instead align more closely with the actual validation, which
should also enable easier validation of nested stacks which is
currently not possible.

Change-Id: Ibf93a170ab381a42a46ea414c3b134cbe0c3f232
Closes-Bug: #1467573
This commit is contained in:
Steven Hardy 2015-09-15 19:08:50 +01:00
parent 54116830fc
commit ed64822d4d
2 changed files with 63 additions and 85 deletions

View File

@ -559,6 +559,17 @@ class EngineService(service.Service):
raise exception.MissingCredentialError(required='X-Auth-Key') raise exception.MissingCredentialError(required='X-Auth-Key')
def _validate_new_stack(self, cnxt, stack_name, parsed_template): def _validate_new_stack(self, cnxt, stack_name, parsed_template):
if stack_object.Stack.get_by_name(cnxt, stack_name):
raise exception.StackExists(stack_name=stack_name)
tenant_limit = cfg.CONF.max_stacks_per_tenant
if stack_object.Stack.count_all(cnxt) >= tenant_limit:
message = _("You have reached the maximum stacks per tenant, "
"%d. Please delete some stacks.") % tenant_limit
raise exception.RequestLimitExceeded(message=message)
self._validate_template(cnxt, parsed_template)
def _validate_template(self, cnxt, parsed_template):
try: try:
parsed_template.validate() parsed_template.validate()
except AssertionError: except AssertionError:
@ -566,15 +577,6 @@ class EngineService(service.Service):
except Exception as ex: except Exception as ex:
raise exception.StackValidationFailed(message=six.text_type(ex)) raise exception.StackValidationFailed(message=six.text_type(ex))
if stack_object.Stack.get_by_name(cnxt, stack_name):
raise exception.StackExists(stack_name=stack_name)
tenant_limit = cfg.CONF.max_stacks_per_tenant
if stack_object.Stack.count_all(cnxt) >= tenant_limit:
message = _("You have reached the maximum stacks per tenant, %d."
" Please delete some stacks.") % tenant_limit
raise exception.RequestLimitExceeded(message=message)
max_resources = cfg.CONF.max_resources_per_stack max_resources = cfg.CONF.max_resources_per_stack
if max_resources == -1: if max_resources == -1:
return return
@ -973,58 +975,33 @@ class EngineService(service.Service):
msg = _("No Template provided.") msg = _("No Template provided.")
return webob.exc.HTTPBadRequest(explanation=msg) return webob.exc.HTTPBadRequest(explanation=msg)
tmpl = templatem.Template(template, files=files)
# validate overall template
try:
tmpl.validate()
except Exception as ex:
return {'Error': six.text_type(ex)}
# validate resource classes
tmpl_resources = tmpl[tmpl.RESOURCES]
env = environment.Environment(params) env = environment.Environment(params)
tmpl = templatem.Template(template, files=files, env=env)
for name, res in six.iteritems(tmpl_resources):
ResourceClass = env.get_class(res['Type'])
if ResourceClass == resources.template_resource.TemplateResource:
# we can't validate a TemplateResource unless we instantiate
# it as we need to download the template and convert the
# parameters into properties_schema.
continue
if not ResourceClass.is_service_available(cnxt):
raise exception.ResourceTypeUnavailable(
service_name=ResourceClass.default_client_name,
resource_type=res['Type']
)
props = properties.Properties(
ResourceClass.properties_schema,
res.get('Properties', {}),
parent_name=six.text_type(name),
context=cnxt,
section='Properties')
deletion_policy = res.get('DeletionPolicy', 'Delete')
try: try:
ResourceClass.validate_deletion_policy(deletion_policy) self._validate_template(cnxt, tmpl)
props.validate(with_value=False)
except Exception as ex: except Exception as ex:
return {'Error': six.text_type(ex)} return {'Error': six.text_type(ex)}
# validate parameters stack_name = 'dummy'
tmpl_params = tmpl.parameters(None, user_params=env.params) stack = parser.Stack(cnxt, stack_name, tmpl, strict_validate=False)
tmpl_params.validate(validate_value=False, context=cnxt) stack.resource_validate = False
is_real_param = lambda p: p.name not in tmpl_params.PSEUDO_PARAMETERS try:
params = tmpl_params.map(api.format_validate_parameter, is_real_param) stack.validate()
param_groups = parameter_groups.ParameterGroups(tmpl) except exception.StackValidationFailed as ex:
return {'Error': six.text_type(ex)}
def filter_parameter(p):
return p.name not in stack.parameters.PSEUDO_PARAMETERS
params = stack.parameters.map(api.format_validate_parameter,
filter_func=filter_parameter)
result = { result = {
'Description': tmpl.get('Description', ''), 'Description': tmpl.get('Description', ''),
'Parameters': params, 'Parameters': params
} }
param_groups = parameter_groups.ParameterGroups(tmpl)
if param_groups.parameter_groups: if param_groups.parameter_groups:
result['ParameterGroups'] = param_groups.parameter_groups result['ParameterGroups'] = param_groups.parameter_groups

View File

@ -940,7 +940,7 @@ class ValidateTest(common.HeatTestCase):
def test_validate_ref_valid(self): def test_validate_ref_valid(self):
t = template_format.parse(test_template_ref % 'WikiDatabase') t = template_format.parse(test_template_ref % 'WikiDatabase')
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
self.assertEqual('test.', res['Description']) self.assertEqual('test.', res['Description'])
def test_validate_with_environment(self): def test_validate_with_environment(self):
@ -950,7 +950,7 @@ class ValidateTest(common.HeatTestCase):
t = template_format.parse(test_template) t = template_format.parse(test_template)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
params = {'resource_registry': {'My::Instance': 'AWS::EC2::Instance'}} params = {'resource_registry': {'My::Instance': 'AWS::EC2::Instance'}}
res = dict(engine.validate_template(None, t, params)) res = dict(engine.validate_template(self.ctx, t, params))
self.assertEqual('test.', res['Description']) self.assertEqual('test.', res['Description'])
def test_validate_hot_valid(self): def test_validate_hot_valid(self):
@ -963,31 +963,31 @@ class ValidateTest(common.HeatTestCase):
type: AWS::EC2::Instance type: AWS::EC2::Instance
""") """)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
self.assertEqual('test.', res['Description']) self.assertEqual('test.', res['Description'])
def test_validate_ref_invalid(self): def test_validate_ref_invalid(self):
t = template_format.parse(test_template_ref % 'WikiDatabasez') t = template_format.parse(test_template_ref % 'WikiDatabasez')
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
self.assertNotEqual(res['Description'], 'Successfully validated') self.assertNotEqual(res['Description'], 'Successfully validated')
def test_validate_findinmap_valid(self): def test_validate_findinmap_valid(self):
t = template_format.parse(test_template_findinmap_valid) t = template_format.parse(test_template_findinmap_valid)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
self.assertEqual('test.', res['Description']) self.assertEqual('test.', res['Description'])
def test_validate_findinmap_invalid(self): def test_validate_findinmap_invalid(self):
t = template_format.parse(test_template_findinmap_invalid) t = template_format.parse(test_template_findinmap_invalid)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
self.assertNotEqual(res['Description'], 'Successfully validated') self.assertNotEqual(res['Description'], 'Successfully validated')
def test_validate_parameters(self): def test_validate_parameters(self):
t = template_format.parse(test_template_ref % 'WikiDatabase') t = template_format.parse(test_template_ref % 'WikiDatabase')
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
# Note: the assertion below does not expect a CFN dict of the parameter # Note: the assertion below does not expect a CFN dict of the parameter
# but a dict of the parameters.Schema object. # but a dict of the parameters.Schema object.
# For API CFN backward compatibility, formating to CFN is done in the # For API CFN backward compatibility, formating to CFN is done in the
@ -1003,7 +1003,7 @@ class ValidateTest(common.HeatTestCase):
t = template_format.parse(test_template_default_override) t = template_format.parse(test_template_default_override)
env_params = {'net_name': 'betternetname'} env_params = {'net_name': 'betternetname'}
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, env_params)) res = dict(engine.validate_template(self.ctx, t, env_params))
self.assertEqual('defaultnet', self.assertEqual('defaultnet',
res['Parameters']['net_name']['Default']) res['Parameters']['net_name']['Default'])
self.assertEqual('betternetname', self.assertEqual('betternetname',
@ -1013,7 +1013,7 @@ class ValidateTest(common.HeatTestCase):
t = template_format.parse(test_template_no_default) t = template_format.parse(test_template_no_default)
env_params = {'net_name': 'betternetname'} env_params = {'net_name': 'betternetname'}
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, env_params)) res = dict(engine.validate_template(self.ctx, t, env_params))
self.assertEqual('betternetname', self.assertEqual('betternetname',
res['Parameters']['net_name']['Value']) res['Parameters']['net_name']['Value'])
self.assertNotIn('Default', res['Parameters']['net_name']) self.assertNotIn('Default', res['Parameters']['net_name'])
@ -1029,13 +1029,13 @@ class ValidateTest(common.HeatTestCase):
type: AWS::EC2::Instance type: AWS::EC2::Instance
""") """)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
self.assertEqual({}, res['Parameters']) self.assertEqual({}, res['Parameters'])
def test_validate_hot_parameter_label(self): def test_validate_hot_parameter_label(self):
t = template_format.parse(test_template_hot_parameter_label) t = template_format.parse(test_template_hot_parameter_label)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
parameters = res['Parameters'] parameters = res['Parameters']
expected = {'KeyName': { expected = {'KeyName': {
@ -1049,7 +1049,7 @@ class ValidateTest(common.HeatTestCase):
def test_validate_hot_no_parameter_label(self): def test_validate_hot_no_parameter_label(self):
t = template_format.parse(test_template_hot_no_parameter_label) t = template_format.parse(test_template_hot_no_parameter_label)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
parameters = res['Parameters'] parameters = res['Parameters']
expected = {'KeyName': { expected = {'KeyName': {
@ -1063,7 +1063,7 @@ class ValidateTest(common.HeatTestCase):
def test_validate_cfn_parameter_label(self): def test_validate_cfn_parameter_label(self):
t = template_format.parse(test_template_cfn_parameter_label) t = template_format.parse(test_template_cfn_parameter_label)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
parameters = res['Parameters'] parameters = res['Parameters']
expected = {'KeyName': { expected = {'KeyName': {
@ -1090,7 +1090,7 @@ class ValidateTest(common.HeatTestCase):
type: boolean type: boolean
""") """)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
parameters = res['Parameters'] parameters = res['Parameters']
# make sure all the types are reported correctly # make sure all the types are reported correctly
self.assertEqual('String', parameters["param1"]["Type"]) self.assertEqual('String', parameters["param1"]["Type"])
@ -1107,7 +1107,7 @@ class ValidateTest(common.HeatTestCase):
resources: resources:
""") """)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
expected = {"Description": "test.", expected = {"Description": "test.",
"Parameters": {}} "Parameters": {}}
self.assertEqual(expected, res) self.assertEqual(expected, res)
@ -1120,7 +1120,7 @@ class ValidateTest(common.HeatTestCase):
outputs: outputs:
""") """)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
expected = {"Description": "test.", expected = {"Description": "test.",
"Parameters": {}} "Parameters": {}}
self.assertEqual(expected, res) self.assertEqual(expected, res)
@ -1128,14 +1128,15 @@ class ValidateTest(common.HeatTestCase):
def test_validate_properties(self): def test_validate_properties(self):
t = template_format.parse(test_template_invalid_property) t = template_format.parse(test_template_invalid_property)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
self.assertEqual({'Error': 'Property error: WikiDatabase.Properties: ' self.assertEqual(
{'Error': 'Property error: Resources.WikiDatabase.Properties: '
'Unknown Property UnknownProperty'}, res) 'Unknown Property UnknownProperty'}, res)
def test_invalid_resources(self): def test_invalid_resources(self):
t = template_format.parse(test_template_invalid_resources) t = template_format.parse(test_template_invalid_resources)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
self.assertEqual({'Error': 'Resources must contain Resource. ' self.assertEqual({'Error': 'Resources must contain Resource. '
'Found a [%s] instead' % six.text_type}, 'Found a [%s] instead' % six.text_type},
res) res)
@ -1155,7 +1156,7 @@ class ValidateTest(common.HeatTestCase):
""") """)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t)) res = dict(engine.validate_template(self.ctx, t))
self.assertEqual({'Error': 'The template section is invalid: Output'}, self.assertEqual({'Error': 'The template section is invalid: Output'},
res) res)
@ -1170,36 +1171,36 @@ class ValidateTest(common.HeatTestCase):
""") """)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t)) res = dict(engine.validate_template(self.ctx, t))
self.assertEqual({'Error': 'The template section is invalid: output'}, self.assertEqual({'Error': 'The template section is invalid: output'},
res) res)
def test_unimplemented_property(self): def test_unimplemented_property(self):
t = template_format.parse(test_template_unimplemented_property) t = template_format.parse(test_template_unimplemented_property)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
self.assertEqual( self.assertEqual(
{'Error': 'Property error: WikiDatabase.Properties: ' {'Error': 'Property error: Resources.WikiDatabase.Properties: '
'Property SourceDestCheck not implemented yet'}, 'Property SourceDestCheck not implemented yet'},
res) res)
def test_invalid_deletion_policy(self): def test_invalid_deletion_policy(self):
t = template_format.parse(test_template_invalid_deletion_policy) t = template_format.parse(test_template_invalid_deletion_policy)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
self.assertEqual({'Error': 'Invalid deletion policy "Destroy"'}, res) self.assertEqual({'Error': 'Invalid deletion policy "Destroy"'}, res)
def test_snapshot_deletion_policy(self): def test_snapshot_deletion_policy(self):
t = template_format.parse(test_template_snapshot_deletion_policy) t = template_format.parse(test_template_snapshot_deletion_policy)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
self.assertEqual( self.assertEqual(
{'Error': '"Snapshot" deletion policy not supported'}, res) {'Error': '"Snapshot" deletion policy not supported'}, res)
def test_volume_snapshot_deletion_policy(self): def test_volume_snapshot_deletion_policy(self):
t = template_format.parse(test_template_volume_snapshot) t = template_format.parse(test_template_volume_snapshot)
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, t, {})) res = dict(engine.validate_template(self.ctx, t, {}))
self.assertEqual({'Description': u'test.', 'Parameters': {}}, res) self.assertEqual({'Description': u'test.', 'Parameters': {}}, res)
def test_validate_template_without_resources(self): def test_validate_template_without_resources(self):
@ -1208,7 +1209,7 @@ class ValidateTest(common.HeatTestCase):
''') ''')
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, hot_tpl, {})) res = dict(engine.validate_template(self.ctx, hot_tpl, {}))
expected = {'Description': 'No description', 'Parameters': {}} expected = {'Description': 'No description', 'Parameters': {}}
self.assertEqual(expected, res) self.assertEqual(expected, res)
@ -1229,7 +1230,7 @@ class ValidateTest(common.HeatTestCase):
''') ''')
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, hot_tpl, {})) res = dict(engine.validate_template(self.ctx, hot_tpl, {}))
self.assertEqual({'Error': '"Type" is not a valid keyword ' self.assertEqual({'Error': '"Type" is not a valid keyword '
'inside a resource definition'}, res) 'inside a resource definition'}, res)
@ -1250,7 +1251,7 @@ class ValidateTest(common.HeatTestCase):
''') ''')
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, hot_tpl, {})) res = dict(engine.validate_template(self.ctx, hot_tpl, {}))
self.assertEqual({'Error': '"Properties" is not a valid keyword ' self.assertEqual({'Error': '"Properties" is not a valid keyword '
'inside a resource definition'}, res) 'inside a resource definition'}, res)
@ -1271,7 +1272,7 @@ class ValidateTest(common.HeatTestCase):
''') ''')
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, hot_tpl, {})) res = dict(engine.validate_template(self.ctx, hot_tpl, {}))
self.assertEqual({'Error': '"Metadata" is not a valid keyword ' self.assertEqual({'Error': '"Metadata" is not a valid keyword '
'inside a resource definition'}, res) 'inside a resource definition'}, res)
@ -1292,7 +1293,7 @@ class ValidateTest(common.HeatTestCase):
''') ''')
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, hot_tpl, {})) res = dict(engine.validate_template(self.ctx, hot_tpl, {}))
self.assertEqual({'Error': '"DependsOn" is not a valid keyword ' self.assertEqual({'Error': '"DependsOn" is not a valid keyword '
'inside a resource definition'}, res) 'inside a resource definition'}, res)
@ -1313,7 +1314,7 @@ class ValidateTest(common.HeatTestCase):
''') ''')
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, hot_tpl, {})) res = dict(engine.validate_template(self.ctx, hot_tpl, {}))
self.assertEqual({'Error': '"DeletionPolicy" is not a valid ' self.assertEqual({'Error': '"DeletionPolicy" is not a valid '
'keyword inside a resource definition'}, 'keyword inside a resource definition'},
res) res)
@ -1335,7 +1336,7 @@ class ValidateTest(common.HeatTestCase):
''') ''')
engine = service.EngineService('a', 't') engine = service.EngineService('a', 't')
res = dict(engine.validate_template(None, hot_tpl, {})) res = dict(engine.validate_template(self.ctx, hot_tpl, {}))
self.assertEqual({'Error': '"UpdatePolicy" is not a valid ' self.assertEqual({'Error': '"UpdatePolicy" is not a valid '
'keyword inside a resource definition'}, 'keyword inside a resource definition'},
res) res)
@ -1678,7 +1679,7 @@ class ValidateTest(common.HeatTestCase):
self.mock_is_service_available.return_value = False self.mock_is_service_available.return_value = False
ex = self.assertRaises(dispatcher.ExpectedException, ex = self.assertRaises(dispatcher.ExpectedException,
engine.validate_template, engine.validate_template,
None, self.ctx,
t, t,
{}) {})
self.assertEqual(exception.ResourceTypeUnavailable, ex.exc_info[0]) self.assertEqual(exception.ResourceTypeUnavailable, ex.exc_info[0])