403 lines
14 KiB
Python
403 lines
14 KiB
Python
# 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
|
|
import json
|
|
|
|
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 __new__(cls, template, *args, **kwargs):
|
|
'''Create a new Template of the appropriate class.'''
|
|
|
|
if cls == Template:
|
|
if 'heat_template_version' in template:
|
|
return HOTemplate(template, *args, **kwargs)
|
|
|
|
return super(Template, cls).__new__(cls)
|
|
|
|
def __init__(self, template, template_id=None, files=None):
|
|
'''
|
|
Initialise the template with a JSON object and a set of Parameters
|
|
'''
|
|
self.id = template_id
|
|
self.t = template
|
|
self.files = files or {}
|
|
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, stack):
|
|
'''
|
|
looking for { "Fn::GetAZs" : "str" }
|
|
'''
|
|
def match_get_az(key, value):
|
|
return (key == 'Fn::GetAZs' and
|
|
isinstance(value, basestring))
|
|
|
|
def handle_get_az(ref):
|
|
if stack is None:
|
|
return ['nova']
|
|
else:
|
|
return stack.get_availability_zones()
|
|
|
|
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:
|
|
r = resources[resource]
|
|
if r.state in (
|
|
(r.CREATE, r.IN_PROGRESS),
|
|
(r.CREATE, r.COMPLETE),
|
|
(r.UPDATE, r.IN_PROGRESS),
|
|
(r.UPDATE, r.COMPLETE)):
|
|
return r.FnGetAtt(att)
|
|
except KeyError:
|
|
raise exception.InvalidTemplateAttribute(resource=resource,
|
|
key=att)
|
|
|
|
return _resolve(lambda k, v: k == 'Fn::GetAtt', handle_getatt, s)
|
|
|
|
@staticmethod
|
|
def reduce_joins(s):
|
|
'''
|
|
Reduces contiguous strings in Fn::Join to a single joined string
|
|
eg the following
|
|
{ "Fn::Join" : [ " ", [ "str1", "str2", {"f": "b"}, "str3", "str4"]}
|
|
is reduced to
|
|
{ "Fn::Join" : [ " ", [ "str1 str2", {"f": "b"}, "str3 str4"]}
|
|
'''
|
|
def handle_join(args):
|
|
if not isinstance(args, (list, tuple)):
|
|
raise TypeError('Arguments to "Fn::Join" must be a list')
|
|
try:
|
|
delim, items = args
|
|
except ValueError as ex:
|
|
example = '"Fn::Join" : [ " ", [ "str1", "str2"]]'
|
|
raise ValueError('Incorrect arguments to "Fn::Join" %s: %s' %
|
|
('should be', example))
|
|
|
|
if not isinstance(items, (list, tuple)):
|
|
raise TypeError('Arguments to "Fn::Join" not fully resolved')
|
|
reduced = []
|
|
contiguous = []
|
|
for item in items:
|
|
if isinstance(item, (str, unicode)):
|
|
contiguous.append(item)
|
|
else:
|
|
if contiguous:
|
|
reduced.append(delim.join(contiguous))
|
|
contiguous = []
|
|
reduced.append(item)
|
|
if contiguous:
|
|
reduced.append(delim.join(contiguous))
|
|
return {'Fn::Join': [delim, reduced]}
|
|
|
|
return _resolve(lambda k, v: k == 'Fn::Join', handle_join, s)
|
|
|
|
@staticmethod
|
|
def resolve_select(s):
|
|
'''
|
|
Resolve constructs of the form:
|
|
(for a list lookup)
|
|
{ "Fn::Select" : [ "2", [ "apples", "grapes", "mangoes" ] ] }
|
|
returns "mangoes"
|
|
|
|
(for a dict lookup)
|
|
{ "Fn::Select" : [ "red", {"red": "a", "flu": "b"} ] }
|
|
returns "a"
|
|
|
|
Note: can raise IndexError, KeyError, ValueError and TypeError
|
|
'''
|
|
def handle_select(args):
|
|
if not isinstance(args, (list, tuple)):
|
|
raise TypeError('Arguments to "Fn::Select" must be a list')
|
|
|
|
try:
|
|
lookup, strings = args
|
|
except ValueError as ex:
|
|
example = '"Fn::Select" : [ "4", [ "str1", "str2"]]'
|
|
raise ValueError('Incorrect arguments to "Fn::Select" %s: %s' %
|
|
('should be', example))
|
|
|
|
try:
|
|
index = int(lookup)
|
|
except ValueError as ex:
|
|
index = lookup
|
|
|
|
if isinstance(strings, basestring):
|
|
# might be serialized json.
|
|
# if not allow it to raise a ValueError
|
|
strings = json.loads(strings)
|
|
|
|
if isinstance(strings, (list, tuple)) and isinstance(index, int):
|
|
return strings[index]
|
|
if isinstance(strings, dict) and isinstance(index, basestring):
|
|
return strings[index]
|
|
if strings is None:
|
|
return ''
|
|
|
|
raise TypeError('Arguments to "Fn::Select" not fully resolved')
|
|
|
|
return _resolve(lambda k, v: k == 'Fn::Select', handle_select, 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')
|
|
|
|
try:
|
|
delim, strings = args
|
|
except ValueError as ex:
|
|
example = '"Fn::Join" : [ " ", [ "str1", "str2"]]'
|
|
raise ValueError('Incorrect arguments to "Fn::Join" %s: %s' %
|
|
('should be', example))
|
|
|
|
if not isinstance(strings, (list, tuple)):
|
|
raise TypeError('Arguments to "Fn::Join" not fully resolved')
|
|
|
|
def empty_for_none(v):
|
|
if v is None:
|
|
return ''
|
|
else:
|
|
return v
|
|
|
|
return delim.join(empty_for_none(value) for value in strings)
|
|
|
|
return _resolve(lambda k, v: k == 'Fn::Join', handle_join, s)
|
|
|
|
@staticmethod
|
|
def resolve_split(s):
|
|
'''
|
|
Split strings in Fn::Split to a list of sub strings
|
|
eg the following
|
|
{ "Fn::Split" : [ ",", "str1,str2,str3,str4"]}
|
|
is reduced to
|
|
{["str1", "str2", "str3", "str4"]}
|
|
'''
|
|
def handle_split(args):
|
|
if not isinstance(args, (list, tuple)):
|
|
raise TypeError('Arguments to "Fn::Split" must be a list')
|
|
|
|
example = '"Fn::Split" : [ ",", "str1, str2"]]'
|
|
try:
|
|
delim, strings = args
|
|
except ValueError as ex:
|
|
raise ValueError('Incorrect arguments to "Fn::Split" %s: %s' %
|
|
('should be', example))
|
|
if not isinstance(strings, basestring):
|
|
raise TypeError('Incorrect arguments to "Fn::Split" %s: %s' %
|
|
('should be', example))
|
|
return strings.split(delim)
|
|
return _resolve(lambda k, v: k == 'Fn::Split', handle_split, s)
|
|
|
|
@staticmethod
|
|
def resolve_replace(s):
|
|
"""
|
|
Resolve constructs of the form.
|
|
{"Fn::Replace": [
|
|
{'$var1': 'foo', '%var2%': 'bar'},
|
|
'$var1 is %var2%'
|
|
]}
|
|
This is implemented using python str.replace on each key
|
|
"""
|
|
def handle_replace(args):
|
|
if not isinstance(args, (list, tuple)):
|
|
raise TypeError('Arguments to "Fn::Replace" must be a list')
|
|
|
|
try:
|
|
mapping, string = args
|
|
except ValueError as ex:
|
|
example = ('{"Fn::Replace": '
|
|
'[ {"$var1": "foo", "%var2%": "bar"}, '
|
|
'"$var1 is %var2%"]}')
|
|
raise ValueError(
|
|
'Incorrect arguments to "Fn::Replace" %s: %s' %
|
|
('should be', example))
|
|
|
|
if not isinstance(mapping, dict):
|
|
raise TypeError(
|
|
'Arguments to "Fn::Replace" not fully resolved')
|
|
if not isinstance(string, basestring):
|
|
raise TypeError(
|
|
'Arguments to "Fn::Replace" not fully resolved')
|
|
|
|
for k, v in mapping.items():
|
|
if v is None:
|
|
v = ''
|
|
string = string.replace(k, v)
|
|
return string
|
|
|
|
return _resolve(lambda k, v: k == 'Fn::Replace', handle_replace, 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 HOTemplate(Template):
|
|
'''
|
|
A Heat Orchestration Template format stack template.
|
|
'''
|
|
|
|
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['heat_template_version']
|
|
|
|
if section == DESCRIPTION:
|
|
default = 'No description'
|
|
else:
|
|
default = {}
|
|
|
|
return self.t.get(section.lower(), default)
|
|
|
|
|
|
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(s) for s in snippet]
|
|
return snippet
|