440 lines
15 KiB
Python
440 lines
15 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 eventlet
|
|
import json
|
|
import itertools
|
|
import logging
|
|
from heat.common import exception
|
|
from heat.engine import checkeddict
|
|
from heat.engine.resources import Resource
|
|
from heat.db import api as db_api
|
|
|
|
logger = logging.getLogger(__file__)
|
|
|
|
|
|
class Stack(object):
|
|
IN_PROGRESS = 'IN_PROGRESS'
|
|
CREATE_FAILED = 'CREATE_FAILED'
|
|
CREATE_COMPLETE = 'CREATE_COMPLETE'
|
|
DELETE_IN_PROGRESS = 'DELETE_IN_PROGRESS'
|
|
DELETE_FAILED = 'DELETE_FAILED'
|
|
DELETE_COMPLETE = 'DELETE_COMPLETE'
|
|
|
|
def __init__(self, context, stack_name, template, stack_id=0, parms=None,
|
|
metadata_server=None):
|
|
self.id = stack_id
|
|
self.context = context
|
|
self.t = template
|
|
self.maps = self.t.get('Mappings', {})
|
|
self.outputs = self.t.get('Outputs', {})
|
|
self.res = {}
|
|
self.doc = None
|
|
self.name = stack_name
|
|
self.parsed_template_id = 0
|
|
self.metadata_server = metadata_server
|
|
|
|
# Default Parameters
|
|
self.parms = checkeddict.CheckedDict('Parameters')
|
|
self.parms.addschema('AWS::StackName', {"Description": "AWS StackName",
|
|
"Type": "String"})
|
|
self.parms['AWS::StackName'] = stack_name
|
|
self.parms.addschema('AWS::Region', {"Description": "AWS Regions",
|
|
"Default": "ap-southeast-1",
|
|
"Type": "String",
|
|
"AllowedValues": ["us-east-1", "us-west-1", "us-west-2",
|
|
"sa-east-1", "eu-west-1", "ap-southeast-1",
|
|
"ap-northeast-1"],
|
|
"ConstraintDescription": "must be a valid EC2 instance type."})
|
|
|
|
# template Parameters
|
|
ps = self.t.get('Parameters', {})
|
|
for p in ps:
|
|
self.parms.addschema(p, ps[p])
|
|
|
|
# user Parameters
|
|
if parms is not None:
|
|
self._apply_user_parameters(parms)
|
|
|
|
self.resources = {}
|
|
for rname, rdesc in self.t['Resources'].items():
|
|
res = Resource(rname, rdesc, self)
|
|
self.resources[rname] = res
|
|
|
|
self.calulate_dependencies(res.t, res)
|
|
|
|
def validate(self):
|
|
'''
|
|
http://docs.amazonwebservices.com/AWSCloudFormation/latest/ \
|
|
APIReference/API_ValidateTemplate.html
|
|
'''
|
|
# TODO(sdake) Should return line number of invalid reference
|
|
|
|
response = None
|
|
try:
|
|
order = self.get_create_order()
|
|
except KeyError as ex:
|
|
res = 'A Ref operation referenced a non-existent key '\
|
|
'[%s]' % str(ex)
|
|
|
|
response = {'ValidateTemplateResult': {
|
|
'Description': 'Malformed Query Response [%s]' % (res),
|
|
'Parameters': []}}
|
|
return response
|
|
|
|
for r in order:
|
|
try:
|
|
res = self.resources[r].validate()
|
|
except Exception as ex:
|
|
logger.exception('validate')
|
|
res = str(ex)
|
|
finally:
|
|
if res:
|
|
err_str = 'Malformed Query Response [%s]' % (res)
|
|
response = {'ValidateTemplateResult': {
|
|
'Description': err_str,
|
|
'Parameters': []}}
|
|
return response
|
|
|
|
if response is None:
|
|
response = {'ValidateTemplateResult': {
|
|
'Description': 'Successfully validated',
|
|
'Parameters': []}}
|
|
for p in self.parms:
|
|
jp = {'member': {}}
|
|
res = jp['member']
|
|
res['NoEcho'] = 'false'
|
|
res['ParameterKey'] = p
|
|
res['Description'] = self.parms.get_attr(p, 'Description')
|
|
res['DefaultValue'] = self.parms.get_attr(p, 'Default')
|
|
response['ValidateTemplateResult']['Parameters'].append(res)
|
|
return response
|
|
|
|
def resource_append_deps(self, resource, order_list):
|
|
'''
|
|
For the given resource first append it's dependancies then
|
|
it's self to order_list.
|
|
'''
|
|
for r in resource.depends_on:
|
|
self.resource_append_deps(self.resources[r], order_list)
|
|
if not resource.name in order_list:
|
|
order_list.append(resource.name)
|
|
|
|
def get_create_order(self):
|
|
'''
|
|
return a list of Resource names in the correct order
|
|
for startup.
|
|
'''
|
|
order = []
|
|
for r in self.t['Resources']:
|
|
if self.t['Resources'][r]['Type'] == 'AWS::EC2::Volume' or \
|
|
self.t['Resources'][r]['Type'] == 'AWS::EC2::EIP':
|
|
if len(self.resources[r].depends_on) == 0:
|
|
order.append(r)
|
|
|
|
for r in self.t['Resources']:
|
|
self.resource_append_deps(self.resources[r], order)
|
|
|
|
return order
|
|
|
|
def update_parsed_template(self):
|
|
'''
|
|
Update the parsed template after each resource has been
|
|
created, so commands like describe will work.
|
|
'''
|
|
if self.parsed_template_id == 0:
|
|
stack = db_api.stack_get(self.context, self.name)
|
|
if stack:
|
|
self.parsed_template_id = stack.raw_template.parsed_template.id
|
|
else:
|
|
return
|
|
|
|
pt = db_api.parsed_template_get(self.context, self.parsed_template_id)
|
|
if pt:
|
|
template = self.t.copy()
|
|
template['Resources'] = dict((k, r.parsed_template())
|
|
for (k, r) in self.resources.items())
|
|
pt.update_and_save({'template': template})
|
|
else:
|
|
logger.warn('Cant find parsed template to update %d' %
|
|
self.parsed_template_id)
|
|
|
|
def status_set(self, new_status, reason='change in resource state'):
|
|
|
|
self.t['stack_status'] = new_status
|
|
self.update_parsed_template()
|
|
|
|
def create_blocking(self):
|
|
'''
|
|
create all the resources in the order specified by get_create_order
|
|
'''
|
|
order = self.get_create_order()
|
|
failed = False
|
|
self.status_set(self.IN_PROGRESS)
|
|
|
|
for r in order:
|
|
res = self.resources[r]
|
|
if not failed:
|
|
try:
|
|
res.create()
|
|
except Exception as ex:
|
|
logger.exception('create')
|
|
failed = True
|
|
res.state_set(res.CREATE_FAILED, str(ex))
|
|
|
|
try:
|
|
self.update_parsed_template()
|
|
except Exception as ex:
|
|
logger.exception('update_parsed_template')
|
|
|
|
else:
|
|
res.state_set(res.CREATE_FAILED)
|
|
|
|
self.status_set(failed and self.CREATE_FAILED or self.CREATE_COMPLETE)
|
|
|
|
def create(self):
|
|
|
|
pool = eventlet.GreenPool()
|
|
pool.spawn_n(self.create_blocking)
|
|
|
|
def delete_blocking(self):
|
|
'''
|
|
delete all the resources in the reverse order specified by
|
|
get_create_order().
|
|
'''
|
|
order = self.get_create_order()
|
|
failed = False
|
|
self.status_set(self.DELETE_IN_PROGRESS)
|
|
|
|
for r in reversed(order):
|
|
res = self.resources[r]
|
|
try:
|
|
res.delete()
|
|
re = db_api.resource_get(self.context, self.resources[r].id)
|
|
re.delete()
|
|
except Exception as ex:
|
|
failed = True
|
|
res.state_set(res.DELETE_FAILED)
|
|
logger.error('delete: %s' % str(ex))
|
|
|
|
self.status_set(failed and self.DELETE_FAILED or self.DELETE_COMPLETE)
|
|
if not failed:
|
|
db_api.stack_delete(self.context, self.name)
|
|
|
|
def delete(self):
|
|
pool = eventlet.GreenPool()
|
|
pool.spawn_n(self.delete_blocking)
|
|
|
|
def get_outputs(self):
|
|
outputs = self.resolve_runtime_data(self.outputs)
|
|
|
|
def output_dict(k):
|
|
return {'Description': outputs[k].get('Description',
|
|
'No description given'),
|
|
'OutputKey': k,
|
|
'OutputValue': outputs[k].get('Value', '')}
|
|
|
|
return [output_dict(key) for key in outputs]
|
|
|
|
def restart_resource_blocking(self, resource_name):
|
|
'''
|
|
stop resource_name and all that depend on it
|
|
start resource_name and all that depend on it
|
|
'''
|
|
order = []
|
|
self.resource_append_deps(self.resources[resource_name], order)
|
|
failed = False
|
|
|
|
for r in reversed(order):
|
|
res = self.resources[r]
|
|
try:
|
|
res.delete()
|
|
re = db_api.resource_get(self.context, self.resources[r].id)
|
|
re.delete()
|
|
except Exception as ex:
|
|
failed = True
|
|
res.state_set(res.DELETE_FAILED)
|
|
logger.error('delete: %s' % str(ex))
|
|
|
|
for r in order:
|
|
res = self.resources[r]
|
|
if not failed:
|
|
try:
|
|
res.create()
|
|
except Exception as ex:
|
|
logger.exception('create')
|
|
failed = True
|
|
res.state_set(res.CREATE_FAILED, str(ex))
|
|
|
|
try:
|
|
self.update_parsed_template()
|
|
except Exception as ex:
|
|
logger.exception('update_parsed_template')
|
|
|
|
else:
|
|
res.state_set(res.CREATE_FAILED)
|
|
# TODO(asalkeld) if any of this fails we Should
|
|
# restart the whole stack
|
|
|
|
def restart_resource(self, resource_name):
|
|
pool = eventlet.GreenPool()
|
|
pool.spawn_n(self.restart_resource_blocking, resource_name)
|
|
|
|
def calulate_dependencies(self, s, r):
|
|
if isinstance(s, dict):
|
|
for i in s:
|
|
if i == 'Fn::GetAtt':
|
|
#print '%s seems to depend on %s' % (r.name, s[i][0])
|
|
#r.depends_on.append(s[i][0])
|
|
pass
|
|
elif i == 'Ref':
|
|
#print '%s Refences %s' % (r.name, s[i])
|
|
if r.strict_dependency():
|
|
r.depends_on.append(s[i])
|
|
elif i == 'DependsOn':
|
|
#print '%s DependsOn on %s' % (r.name, s[i])
|
|
r.depends_on.append(s[i])
|
|
else:
|
|
self.calulate_dependencies(s[i], r)
|
|
elif isinstance(s, list):
|
|
for index, item in enumerate(s):
|
|
self.calulate_dependencies(item, r)
|
|
|
|
def _apply_user_parameters(self, parms):
|
|
for p in parms:
|
|
if 'Parameters.member.' in p and 'ParameterKey' in p:
|
|
s = p.split('.')
|
|
try:
|
|
key_name = 'Parameters.member.%s.ParameterKey' % s[2]
|
|
value_name = 'Parameters.member.%s.ParameterValue' % s[2]
|
|
logger.debug('appling user parameter %s=%s' %
|
|
(key_name, value_name))
|
|
self.parms[parms[key_name]] = parms[value_name]
|
|
except Exception:
|
|
logger.error('Could not apply parameter %s' % p)
|
|
|
|
def parameter_get(self, key):
|
|
if not key in self.parms:
|
|
raise exception.UserParameterMissing(key=key)
|
|
try:
|
|
return self.parms[key]
|
|
except ValueError:
|
|
raise exception.UserParameterMissing(key=key)
|
|
|
|
def _resolve_static_refs(self, s):
|
|
'''
|
|
looking for { "Ref" : "str" }
|
|
'''
|
|
def match(key, value):
|
|
return (key == 'Ref' and
|
|
isinstance(value, basestring) and
|
|
value in self.parms)
|
|
|
|
def handle(ref):
|
|
return self.parameter_get(ref)
|
|
|
|
return _resolve(match, handle, s)
|
|
|
|
def _resolve_find_in_map(self, s):
|
|
def handle(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, s)
|
|
|
|
def _resolve_attributes(self, s):
|
|
'''
|
|
looking for something like:
|
|
{ "Fn::GetAtt" : [ "DBInstance", "Endpoint.Address" ] }
|
|
'''
|
|
def match_ref(key, value):
|
|
return key == 'Ref' and value in self.resources
|
|
|
|
def handle_ref(arg):
|
|
return self.resources[arg].FnGetRefId()
|
|
|
|
def handle_getatt(args):
|
|
resource, att = args
|
|
try:
|
|
return self.resources[resource].FnGetAtt(att)
|
|
except KeyError:
|
|
raise exception.InvalidTemplateAttribute(resource=resource,
|
|
key=att)
|
|
|
|
return _resolve(lambda k, v: k == 'Fn::GetAtt', handle_getatt,
|
|
_resolve(match_ref, handle_ref, s))
|
|
|
|
@staticmethod
|
|
def _resolve_joins(s):
|
|
'''
|
|
looking for { "Fn::Join" : [] }
|
|
'''
|
|
def handle(args):
|
|
delim, strings = args
|
|
return delim.join(strings)
|
|
|
|
return _resolve(lambda k, v: k == 'Fn::Join', handle, s)
|
|
|
|
@staticmethod
|
|
def _resolve_base64(s):
|
|
'''
|
|
looking for { "Fn::Base64" : "" }
|
|
'''
|
|
return _resolve(lambda k, v: k == 'Fn::Base64', lambda d: d, s)
|
|
|
|
def resolve_static_data(self, snippet):
|
|
return transform(snippet, [self._resolve_static_refs,
|
|
self._resolve_find_in_map])
|
|
|
|
def resolve_runtime_data(self, snippet):
|
|
return transform(snippet, [self._resolve_attributes,
|
|
self._resolve_joins,
|
|
self._resolve_base64])
|
|
|
|
|
|
def transform(data, transformations):
|
|
'''
|
|
Apply each of the transformation functions in the supplied list to the data
|
|
in turn.
|
|
'''
|
|
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
|