From 56984c1589f0e3e80ed6f1442b588669b36d3c7c Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Mon, 6 Jan 2014 17:17:58 +1300 Subject: [PATCH] Move template_format from heat to heatclient heatclient needs to (YAML) parse the template to allow it to populate the content for get_files function calls. For this reason, template_format (and environment_format) have been moved to heatclient so this code can be shared by both heat and heatclient. Change-Id: I3cc11cd57256ad539efb4dab314ac07547e9e6a2 --- heatclient/common/environment_format.py | 51 +++++++++++++ heatclient/common/template_format.py | 80 +++++++++++++++++++++ heatclient/tests/test_environment_format.py | 69 ++++++++++++++++++ heatclient/tests/test_template_format.py | 42 +++++++++++ 4 files changed, 242 insertions(+) create mode 100644 heatclient/common/environment_format.py create mode 100644 heatclient/common/template_format.py create mode 100644 heatclient/tests/test_environment_format.py create mode 100644 heatclient/tests/test_template_format.py diff --git a/heatclient/common/environment_format.py b/heatclient/common/environment_format.py new file mode 100644 index 00000000..136c0aef --- /dev/null +++ b/heatclient/common/environment_format.py @@ -0,0 +1,51 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# 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 heatclient.common.template_format import yaml_loader + +import yaml + + +SECTIONS = (PARAMETERS, RESOURCE_REGISTRY) = \ + ('parameters', 'resource_registry') + + +def parse(env_str): + '''Takes a string and returns a dict containing the parsed structure. + + This includes determination of whether the string is using the + JSON or YAML format. + ''' + try: + env = yaml.load(env_str, Loader=yaml_loader) + except yaml.YAMLError as yea: + raise ValueError(yea) + else: + if env is None: + env = {} + + for param in env: + if param not in SECTIONS: + raise ValueError('environment has wrong section "%s"' % param) + + return env + + +def default_for_missing(env): + '''Checks a parsed environment for missing sections. + ''' + for param in SECTIONS: + if param not in env: + env[param] = {} diff --git a/heatclient/common/template_format.py b/heatclient/common/template_format.py new file mode 100644 index 00000000..0a7432f1 --- /dev/null +++ b/heatclient/common/template_format.py @@ -0,0 +1,80 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# 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. + +import json +import yaml + +HEAT_VERSIONS = (u'2012-12-12',) +CFN_VERSIONS = (u'2010-09-09',) + +if hasattr(yaml, 'CSafeLoader'): + yaml_loader = yaml.CSafeLoader +else: + yaml_loader = yaml.SafeLoader + +if hasattr(yaml, 'CSafeDumper'): + yaml_dumper = yaml.CSafeDumper +else: + yaml_dumper = yaml.SafeDumper + + +def _construct_yaml_str(self, node): + # Override the default string handling function + # to always return unicode objects + return self.construct_scalar(node) +yaml_loader.add_constructor(u'tag:yaml.org,2002:str', _construct_yaml_str) +# Unquoted dates like 2013-05-23 in yaml files get loaded as objects of type +# datetime.data which causes problems in API layer when being processed by +# openstack.common.jsonutils. Therefore, make unicode string out of timestamps +# until jsonutils can handle dates. +yaml_loader.add_constructor(u'tag:yaml.org,2002:timestamp', + _construct_yaml_str) + + +def parse(tmpl_str): + '''Takes a string and returns a dict containing the parsed structure. + + This includes determination of whether the string is using the + JSON or YAML format. + ''' + if tmpl_str.startswith('{'): + tpl = json.loads(tmpl_str) + else: + try: + tpl = yaml.load(tmpl_str, Loader=yaml_loader) + except yaml.YAMLError as yea: + raise ValueError(yea) + else: + if tpl is None: + tpl = {} + if u'heat_template_version' not in tpl: + default_for_missing(tpl, u'HeatTemplateFormatVersion', + HEAT_VERSIONS) + return tpl + + +def default_for_missing(tpl, version_param, versions): + '''Checks a parsed template for missing version and sections. + + This is currently only applied to YAML templates. + ''' + # if version is missing, implicitly use the lastest one + if version_param not in tpl: + tpl[version_param] = versions[-1] + + # create empty placeholders for any of the main dict sections + for param in (u'Parameters', u'Mappings', u'Resources', u'Outputs'): + if param not in tpl: + tpl[param] = {} diff --git a/heatclient/tests/test_environment_format.py b/heatclient/tests/test_environment_format.py new file mode 100644 index 00000000..0df52d15 --- /dev/null +++ b/heatclient/tests/test_environment_format.py @@ -0,0 +1,69 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 heatclient.common import environment_format + +import mock +import testscenarios +import testtools +import yaml + + +load_tests = testscenarios.load_tests_apply_scenarios + + +class YamlEnvironmentTest(testtools.TestCase): + + def test_minimal_yaml(self): + yaml1 = '' + yaml2 = ''' +parameters: {} +resource_registry: {} +''' + tpl1 = environment_format.parse(yaml1) + environment_format.default_for_missing(tpl1) + tpl2 = environment_format.parse(yaml2) + self.assertEqual(tpl1, tpl2) + + def test_wrong_sections(self): + env = ''' +parameters: {} +resource_regis: {} +''' + self.assertRaises(ValueError, environment_format.parse, env) + + def test_bad_yaml(self): + env = ''' +parameters: } +''' + self.assertRaises(ValueError, environment_format.parse, env) + + +class YamlParseExceptions(testtools.TestCase): + + scenarios = [ + ('scanner', dict(raised_exception=yaml.scanner.ScannerError())), + ('parser', dict(raised_exception=yaml.parser.ParserError())), + ('reader', + dict(raised_exception=yaml.reader.ReaderError('', '', '', '', ''))), + ] + + def test_parse_to_value_exception(self): + text = 'not important' + + with mock.patch.object(yaml, 'load') as yaml_loader: + yaml_loader.side_effect = self.raised_exception + + self.assertRaises(ValueError, + environment_format.parse, text) diff --git a/heatclient/tests/test_template_format.py b/heatclient/tests/test_template_format.py new file mode 100644 index 00000000..8dd50f97 --- /dev/null +++ b/heatclient/tests/test_template_format.py @@ -0,0 +1,42 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +import mock +import testscenarios +import testtools +import yaml + +from heatclient.common import template_format + + +load_tests = testscenarios.load_tests_apply_scenarios + + +class YamlParseExceptions(testtools.TestCase): + + scenarios = [ + ('scanner', dict(raised_exception=yaml.scanner.ScannerError())), + ('parser', dict(raised_exception=yaml.parser.ParserError())), + ('reader', + dict(raised_exception=yaml.reader.ReaderError('', '', '', '', ''))), + ] + + def test_parse_to_value_exception(self): + text = 'not important' + + with mock.patch.object(yaml, 'load') as yaml_loader: + yaml_loader.side_effect = self.raised_exception + + self.assertRaises(ValueError, + template_format.parse, text)