# 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