From d806f727dbebe6bc1616ed4723f967c94f1349a8 Mon Sep 17 00:00:00 2001 From: Zane Bitter Date: Wed, 31 Oct 2012 20:18:42 +0100 Subject: [PATCH] Put the Template class in its own file Change-Id: I5e3a9ef79c1c4f893720cfa529066d49a7c83ad1 Signed-off-by: Zane Bitter --- heat/engine/api.py | 5 +- heat/engine/parser.py | 194 +++--------------------------------- heat/engine/template.py | 200 ++++++++++++++++++++++++++++++++++++++ heat/tests/test_parser.py | 15 +-- 4 files changed, 224 insertions(+), 190 deletions(-) create mode 100644 heat/engine/template.py diff --git a/heat/engine/api.py b/heat/engine/api.py index a794f4d4..ac9250d1 100644 --- a/heat/engine/api.py +++ b/heat/engine/api.py @@ -15,6 +15,7 @@ import re from heat.openstack.common import timeutils from heat.engine import parser +from heat.engine import template from heat.engine import watchrule from heat.openstack.common import log as logging @@ -94,8 +95,8 @@ def format_stack(stack): STACK_UPDATED_TIME: timeutils.isotime(stack.updated_time), STACK_NOTIFICATION_TOPICS: [], # TODO Not implemented yet STACK_PARAMETERS: dict(stack.parameters), - STACK_DESCRIPTION: stack.t[parser.DESCRIPTION], - STACK_TMPL_DESCRIPTION: stack.t[parser.DESCRIPTION], + STACK_DESCRIPTION: stack.t[template.DESCRIPTION], + STACK_TMPL_DESCRIPTION: stack.t[template.DESCRIPTION], STACK_STATUS: stack.state, STACK_STATUS_DATA: stack.state_description, STACK_CAPABILITIES: [], # TODO Not implemented yet diff --git a/heat/engine/parser.py b/heat/engine/parser.py index 7e82c131..7c01818a 100644 --- a/heat/engine/parser.py +++ b/heat/engine/parser.py @@ -23,18 +23,15 @@ from heat.engine import checkeddict from heat.engine import dependencies from heat.engine import identifier from heat.engine import resources +from heat.engine import template from heat.engine import timestamp +from heat.engine.template import Template from heat.db import api as db_api from heat.openstack.common import log as logging logger = logging.getLogger('heat.engine.parser') -SECTIONS = (VERSION, DESCRIPTION, MAPPINGS, - PARAMETERS, RESOURCES, OUTPUTS) = \ - ('AWSTemplateFormatVersion', 'Description', 'Mappings', - 'Parameters', 'Resources', 'Outputs') - (PARAM_STACK_NAME, PARAM_REGION) = ('AWS::StackName', 'AWS::Region') @@ -44,13 +41,13 @@ class Parameters(checkeddict.CheckedDict): the stack's template. ''' - def __init__(self, stack_name, template, user_params={}): + def __init__(self, stack_name, tmpl, user_params={}): ''' Create the parameter container for a stack from the stack name and template, optionally setting the initial set of parameters. ''' - checkeddict.CheckedDict.__init__(self, PARAMETERS) - self._init_schemata(template[PARAMETERS]) + checkeddict.CheckedDict.__init__(self, template.PARAMETERS) + self._init_schemata(tmpl[template.PARAMETERS]) self[PARAM_STACK_NAME] = stack_name self.update(user_params) @@ -83,151 +80,6 @@ class Parameters(checkeddict.CheckedDict): if 'Value' in v) -class Template(object): - '''A stack template.''' - - def __init__(self, template, template_id=None): - ''' - Initialise the template with a JSON object and a set of Parameters - ''' - self.id = template_id - self.t = template - self.maps = self[MAPPINGS] - - @classmethod - def load(cls, context, template_id): - '''Retrieve a Template with the given ID from the database''' - t = db_api.raw_template_get(context, template_id) - return cls(t.template, template_id) - - def store(self, context=None): - '''Store the Template in the database and return its ID''' - if self.id is None: - rt = {'template': self.t} - new_rt = db_api.raw_template_create(context, rt) - self.id = new_rt.id - return self.id - - def __getitem__(self, section): - '''Get the relevant section in the template''' - if section not in SECTIONS: - raise KeyError('"%s" is not a valid template section' % section) - if section == VERSION: - return self.t[section] - - if section == DESCRIPTION: - default = 'No description' - else: - default = {} - - return self.t.get(section, default) - - def resolve_find_in_map(self, s): - ''' - Resolve constructs of the form { "Fn::FindInMap" : [ "mapping", - "key", - "value" ] } - ''' - def handle_find_in_map(args): - try: - name, key, value = args - return self.maps[name][key][value] - except (ValueError, TypeError) as ex: - raise KeyError(str(ex)) - - return _resolve(lambda k, v: k == 'Fn::FindInMap', - handle_find_in_map, s) - - @staticmethod - def resolve_availability_zones(s): - ''' - looking for { "Fn::GetAZs" : "str" } - ''' - def match_get_az(key, value): - return (key == 'Fn::GetAZs' and - isinstance(value, basestring)) - - def handle_get_az(ref): - return ['nova'] - - return _resolve(match_get_az, handle_get_az, s) - - @staticmethod - def resolve_param_refs(s, parameters): - ''' - Resolve constructs of the form { "Ref" : "string" } - ''' - def match_param_ref(key, value): - return (key == 'Ref' and - isinstance(value, basestring) and - value in parameters) - - def handle_param_ref(ref): - try: - return parameters[ref] - except (KeyError, ValueError): - raise exception.UserParameterMissing(key=ref) - - return _resolve(match_param_ref, handle_param_ref, s) - - @staticmethod - def resolve_resource_refs(s, resources): - ''' - Resolve constructs of the form { "Ref" : "resource" } - ''' - def match_resource_ref(key, value): - return key == 'Ref' and value in resources - - def handle_resource_ref(arg): - return resources[arg].FnGetRefId() - - return _resolve(match_resource_ref, handle_resource_ref, s) - - @staticmethod - def resolve_attributes(s, resources): - ''' - Resolve constructs of the form { "Fn::GetAtt" : [ "WebServer", - "PublicIp" ] } - ''' - def handle_getatt(args): - resource, att = args - try: - return resources[resource].FnGetAtt(att) - except KeyError: - raise exception.InvalidTemplateAttribute(resource=resource, - key=att) - - return _resolve(lambda k, v: k == 'Fn::GetAtt', handle_getatt, s) - - @staticmethod - def resolve_joins(s): - ''' - Resolve constructs of the form { "Fn::Join" : [ "delim", [ "str1", - "str2" ] } - ''' - def handle_join(args): - if not isinstance(args, (list, tuple)): - raise TypeError('Arguments to "Fn::Join" must be a list') - delim, strings = args - if not isinstance(strings, (list, tuple)): - raise TypeError('Arguments to "Fn::Join" not fully resolved') - return delim.join(strings) - - return _resolve(lambda k, v: k == 'Fn::Join', handle_join, s) - - @staticmethod - def resolve_base64(s): - ''' - Resolve constructs of the form { "Fn::Base64" : "string" } - ''' - def handle_base64(string): - if not isinstance(string, basestring): - raise TypeError('Arguments to "Fn::Base64" not fully resolved') - return string - - return _resolve(lambda k, v: k == 'Fn::Base64', handle_base64, s) - - class Stack(object): CREATE_IN_PROGRESS = 'CREATE_IN_PROGRESS' CREATE_FAILED = 'CREATE_FAILED' @@ -244,7 +96,7 @@ class Stack(object): created_time = timestamp.Timestamp(db_api.stack_get, 'created_at') updated_time = timestamp.Timestamp(db_api.stack_get, 'updated_at') - def __init__(self, context, stack_name, template, parameters=None, + def __init__(self, context, stack_name, tmpl, parameters=None, stack_id=None, state=None, state_description='', timeout_mins=60): ''' @@ -254,21 +106,22 @@ class Stack(object): ''' self.id = stack_id self.context = context - self.t = template + self.t = tmpl self.name = stack_name self.state = state self.state_description = state_description self.timeout_mins = timeout_mins if parameters is None: - parameters = Parameters(stack_name, template) + parameters = Parameters(self.name, self.t) self.parameters = parameters - self.outputs = self.resolve_static_data(self.t[OUTPUTS]) + self.outputs = self.resolve_static_data(self.t[template.OUTPUTS]) + template_resources = self.t[template.RESOURCES] self.resources = dict((name, resources.Resource(name, data, self)) - for (name, data) in self.t[RESOURCES].items()) + for (name, data) in template_resources.items()) self.dependencies = self._get_dependencies(self.resources.itervalues()) @@ -544,7 +397,8 @@ class Stack(object): # flip the template & parameters to the newstack values self.t = newstack.t self.parameters = newstack.parameters - self.outputs = self.resolve_static_data(self.t[OUTPUTS]) + template_outputs = self.t[template.OUTPUTS] + self.outputs = self.resolve_static_data(template_outputs) self.dependencies = self._get_dependencies( self.resources.itervalues()) self.store() @@ -669,25 +523,3 @@ def transform(data, transformations): for t in transformations: data = t(data) return data - - -def _resolve(match, handle, snippet): - ''' - Resolve constructs in a snippet of a template. The supplied match function - should return True if a particular key-value pair should be substituted, - and the handle function should return the correct substitution when passed - the argument list as parameters. - - Returns a copy of the original snippet with the substitutions performed. - ''' - recurse = lambda s: _resolve(match, handle, s) - - if isinstance(snippet, dict): - if len(snippet) == 1: - k, v = snippet.items()[0] - if match(k, v): - return handle(recurse(v)) - return dict((k, recurse(v)) for k, v in snippet.items()) - elif isinstance(snippet, list): - return [recurse(v) for v in snippet] - return snippet diff --git a/heat/engine/template.py b/heat/engine/template.py new file mode 100644 index 00000000..63924f70 --- /dev/null +++ b/heat/engine/template.py @@ -0,0 +1,200 @@ +# 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 collections + +from heat.db import api as db_api +from heat.common import exception + + +SECTIONS = (VERSION, DESCRIPTION, MAPPINGS, + PARAMETERS, RESOURCES, OUTPUTS) = \ + ('AWSTemplateFormatVersion', 'Description', 'Mappings', + 'Parameters', 'Resources', 'Outputs') + + +class Template(collections.Mapping): + '''A stack template.''' + + def __init__(self, template, template_id=None): + ''' + Initialise the template with a JSON object and a set of Parameters + ''' + self.id = template_id + self.t = template + self.maps = self[MAPPINGS] + + @classmethod + def load(cls, context, template_id): + '''Retrieve a Template with the given ID from the database''' + t = db_api.raw_template_get(context, template_id) + return cls(t.template, template_id) + + def store(self, context=None): + '''Store the Template in the database and return its ID''' + if self.id is None: + rt = {'template': self.t} + new_rt = db_api.raw_template_create(context, rt) + self.id = new_rt.id + return self.id + + def __getitem__(self, section): + '''Get the relevant section in the template''' + if section not in SECTIONS: + raise KeyError('"%s" is not a valid template section' % section) + if section == VERSION: + return self.t[section] + + if section == DESCRIPTION: + default = 'No description' + else: + default = {} + + return self.t.get(section, default) + + def __iter__(self): + '''Return an iterator over the section names''' + return iter(SECTIONS) + + def __len__(self): + '''Return the number of sections''' + return len(SECTIONS) + + def resolve_find_in_map(self, s): + ''' + Resolve constructs of the form { "Fn::FindInMap" : [ "mapping", + "key", + "value" ] } + ''' + def handle_find_in_map(args): + try: + name, key, value = args + return self.maps[name][key][value] + except (ValueError, TypeError) as ex: + raise KeyError(str(ex)) + + return _resolve(lambda k, v: k == 'Fn::FindInMap', + handle_find_in_map, s) + + @staticmethod + def resolve_availability_zones(s): + ''' + looking for { "Fn::GetAZs" : "str" } + ''' + def match_get_az(key, value): + return (key == 'Fn::GetAZs' and + isinstance(value, basestring)) + + def handle_get_az(ref): + return ['nova'] + + return _resolve(match_get_az, handle_get_az, s) + + @staticmethod + def resolve_param_refs(s, parameters): + ''' + Resolve constructs of the form { "Ref" : "string" } + ''' + def match_param_ref(key, value): + return (key == 'Ref' and + isinstance(value, basestring) and + value in parameters) + + def handle_param_ref(ref): + try: + return parameters[ref] + except (KeyError, ValueError): + raise exception.UserParameterMissing(key=ref) + + return _resolve(match_param_ref, handle_param_ref, s) + + @staticmethod + def resolve_resource_refs(s, resources): + ''' + Resolve constructs of the form { "Ref" : "resource" } + ''' + def match_resource_ref(key, value): + return key == 'Ref' and value in resources + + def handle_resource_ref(arg): + return resources[arg].FnGetRefId() + + return _resolve(match_resource_ref, handle_resource_ref, s) + + @staticmethod + def resolve_attributes(s, resources): + ''' + Resolve constructs of the form { "Fn::GetAtt" : [ "WebServer", + "PublicIp" ] } + ''' + def handle_getatt(args): + resource, att = args + try: + return resources[resource].FnGetAtt(att) + except KeyError: + raise exception.InvalidTemplateAttribute(resource=resource, + key=att) + + return _resolve(lambda k, v: k == 'Fn::GetAtt', handle_getatt, s) + + @staticmethod + def resolve_joins(s): + ''' + Resolve constructs of the form { "Fn::Join" : [ "delim", [ "str1", + "str2" ] } + ''' + def handle_join(args): + if not isinstance(args, (list, tuple)): + raise TypeError('Arguments to "Fn::Join" must be a list') + delim, strings = args + if not isinstance(strings, (list, tuple)): + raise TypeError('Arguments to "Fn::Join" not fully resolved') + return delim.join(strings) + + return _resolve(lambda k, v: k == 'Fn::Join', handle_join, s) + + @staticmethod + def resolve_base64(s): + ''' + Resolve constructs of the form { "Fn::Base64" : "string" } + ''' + def handle_base64(string): + if not isinstance(string, basestring): + raise TypeError('Arguments to "Fn::Base64" not fully resolved') + return string + + return _resolve(lambda k, v: k == 'Fn::Base64', handle_base64, s) + + +def _resolve(match, handle, snippet): + ''' + Resolve constructs in a snippet of a template. The supplied match function + should return True if a particular key-value pair should be substituted, + and the handle function should return the correct substitution when passed + the argument list as parameters. + + Returns a copy of the original snippet with the substitutions performed. + ''' + recurse = lambda s: _resolve(match, handle, s) + + if isinstance(snippet, dict): + if len(snippet) == 1: + k, v = snippet.items()[0] + if match(k, v): + return handle(recurse(v)) + return dict((k, recurse(v)) for k, v in snippet.items()) + elif isinstance(snippet, list): + return [recurse(v) for v in snippet] + return snippet diff --git a/heat/tests/test_parser.py b/heat/tests/test_parser.py index fb4b73f8..4f56c781 100644 --- a/heat/tests/test_parser.py +++ b/heat/tests/test_parser.py @@ -22,6 +22,7 @@ import json from heat.common import context from heat.common import exception from heat.engine import parser +from heat.engine import template from heat.engine import checkeddict from heat.engine.resources import Resource @@ -31,7 +32,7 @@ def join(raw): delim, strs = args return delim.join(strs) - return parser._resolve(lambda k, v: k == 'Fn::Join', handle_join, raw) + return template._resolve(lambda k, v: k == 'Fn::Join', handle_join, raw) @attr(tag=['unit', 'parser']) @@ -124,16 +125,16 @@ class TemplateTest(unittest.TestCase): def test_defaults(self): empty = parser.Template({}) try: - empty[parser.VERSION] + empty[template.VERSION] except KeyError: pass else: self.fail('Expected KeyError for version not present') - self.assertEqual(empty[parser.DESCRIPTION], 'No description') - self.assertEqual(empty[parser.MAPPINGS], {}) - self.assertEqual(empty[parser.PARAMETERS], {}) - self.assertEqual(empty[parser.RESOURCES], {}) - self.assertEqual(empty[parser.OUTPUTS], {}) + self.assertEqual(empty[template.DESCRIPTION], 'No description') + self.assertEqual(empty[template.MAPPINGS], {}) + self.assertEqual(empty[template.PARAMETERS], {}) + self.assertEqual(empty[template.RESOURCES], {}) + self.assertEqual(empty[template.OUTPUTS], {}) def test_invalid_section(self): tmpl = parser.Template({'Foo': ['Bar']}) -- 2.45.2