429 lines
14 KiB
Python
429 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 eventlet
|
|
import json
|
|
import itertools
|
|
import logging
|
|
|
|
from heat.common import exception
|
|
from heat.engine import checkeddict
|
|
from heat.engine import dependencies
|
|
from heat.engine.resources import Resource
|
|
from heat.db import api as db_api
|
|
|
|
logger = logging.getLogger('heat.engine.parser')
|
|
|
|
|
|
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.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.parms.update(parms)
|
|
|
|
self.outputs = self.resolve_static_data(self.t.get('Outputs', {}))
|
|
|
|
self.resources = dict((name,
|
|
Resource(name, data, self))
|
|
for (name, data) in self.t['Resources'].items())
|
|
|
|
self.dependencies = dependencies.Dependencies()
|
|
for resource in self.resources.values():
|
|
resource.add_dependencies(self.dependencies)
|
|
|
|
def __iter__(self):
|
|
'''
|
|
Return an iterator over this template's resources in the order that
|
|
they should be started.
|
|
'''
|
|
return iter(self.dependencies)
|
|
|
|
def __reversed__(self):
|
|
'''
|
|
Return an iterator over this template's resources in the order that
|
|
they should be stopped.
|
|
'''
|
|
return reversed(self.dependencies)
|
|
|
|
def __len__(self):
|
|
'''Return the number of resources'''
|
|
return len(self.resources)
|
|
|
|
def __getitem__(self, key):
|
|
'''Get the resource with the specified name.'''
|
|
return self.resources[key]
|
|
|
|
def __contains__(self, key):
|
|
'''Determine whether the stack contains the specified resource'''
|
|
return key in self.resources
|
|
|
|
def keys(self):
|
|
return self.resources.keys()
|
|
|
|
def __str__(self):
|
|
return 'Stack "%s"' % self.name
|
|
|
|
def validate(self):
|
|
'''
|
|
http://docs.amazonwebservices.com/AWSCloudFormation/latest/\
|
|
APIReference/API_ValidateTemplate.html
|
|
'''
|
|
# TODO(sdake) Should return line number of invalid reference
|
|
|
|
response = None
|
|
|
|
for res in self:
|
|
try:
|
|
result = res.validate()
|
|
except Exception as ex:
|
|
logger.exception('validate')
|
|
result = str(ex)
|
|
|
|
if result:
|
|
err_str = 'Malformed Query Response %s' % result
|
|
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 parsed_template_get(self):
|
|
stack = None
|
|
if self.parsed_template_id == 0:
|
|
if self.id == 0:
|
|
stack = db_api.stack_get(self.context, self.id)
|
|
else:
|
|
stack = db_api.stack_get_by_name(self.context, self.name)
|
|
|
|
if stack is None:
|
|
return None
|
|
|
|
self.parsed_template_id = stack.raw_template.parsed_template.id
|
|
return db_api.parsed_template_get(self.context,
|
|
self.parsed_template_id)
|
|
|
|
def update_parsed_template(self):
|
|
'''
|
|
Update the parsed template after each resource has been
|
|
created, so commands like describe will work.
|
|
'''
|
|
pt = self.parsed_template_get()
|
|
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 state_set(self, new_status, reason='change in resource state'):
|
|
if self.id != 0:
|
|
stack = db_api.stack_get(self.context, self.id)
|
|
else:
|
|
stack = db_api.stack_get_by_name(self.context, self.name)
|
|
|
|
if stack is None:
|
|
return
|
|
|
|
self.id = stack.id
|
|
stack.update_and_save({'status': new_status,
|
|
'status_reason': reason})
|
|
|
|
def _timeout(self):
|
|
'''Return the stack creation timeout in seconds'''
|
|
if 'Timeout' in self.t:
|
|
try:
|
|
# Timeout is in minutes
|
|
return int(self.t['Timeout']) * 60
|
|
except ValueError:
|
|
logger.exception('create timeout conversion')
|
|
|
|
# Default to 1 hour
|
|
return 60 * 60
|
|
|
|
def create(self):
|
|
'''
|
|
Create the stack and all of the resources.
|
|
'''
|
|
self.state_set(self.IN_PROGRESS, 'Stack creation started')
|
|
|
|
stack_status = self.CREATE_COMPLETE
|
|
reason = 'Stack successfully created'
|
|
|
|
with eventlet.Timeout(self._timeout()) as tmo:
|
|
try:
|
|
for res in self:
|
|
if stack_status != self.CREATE_FAILED:
|
|
result = res.create()
|
|
if result:
|
|
stack_status = self.CREATE_FAILED
|
|
reason = 'Resource %s failed with: %s' % (str(res),
|
|
result)
|
|
|
|
try:
|
|
self.update_parsed_template()
|
|
except Exception as ex:
|
|
logger.exception('update_parsed_template')
|
|
|
|
else:
|
|
res.state_set(res.CREATE_FAILED,
|
|
'Stack creation aborted')
|
|
|
|
except eventlet.Timeout, t:
|
|
if t is tmo:
|
|
stack_status = self.CREATE_FAILED
|
|
reason = 'Timed out waiting for %s' % (res.name)
|
|
else:
|
|
# not my timeout
|
|
raise
|
|
|
|
self.state_set(stack_status, reason)
|
|
|
|
def delete(self):
|
|
'''
|
|
Delete all of the resources, and then the stack itself.
|
|
'''
|
|
self.state_set(self.DELETE_IN_PROGRESS)
|
|
|
|
failures = []
|
|
|
|
for res in reversed(self):
|
|
result = res.destroy()
|
|
if result:
|
|
failures.append(str(res))
|
|
|
|
if failures:
|
|
self.state_set(self.DELETE_FAILED,
|
|
'Failed to delete ' + ', '.join(failures))
|
|
else:
|
|
self.state_set(self.DELETE_COMPLETE, 'Deleted successfully')
|
|
db_api.stack_delete(self.context, self.id)
|
|
|
|
def output(self, key):
|
|
value = self.outputs[key].get('Value', '')
|
|
return self.resolve_runtime_data(value)
|
|
|
|
def get_outputs(self):
|
|
def output_dict(k):
|
|
return {'Description': self.outputs[k].get('Description',
|
|
'No description given'),
|
|
'OutputKey': k,
|
|
'OutputValue': self.output(k)}
|
|
|
|
return [output_dict(key) for key in self.outputs]
|
|
|
|
def restart_resource(self, resource_name):
|
|
'''
|
|
stop resource_name and all that depend on it
|
|
start resource_name and all that depend on it
|
|
'''
|
|
|
|
if self.parsed_template_id == 0:
|
|
stack = db_api.stack_get(self.context, self.id)
|
|
if stack:
|
|
self.parsed_template_id = stack.raw_template.parsed_template.id
|
|
|
|
deps = self.dependencies[self[resource_name]]
|
|
failed = False
|
|
|
|
for res in reversed(deps):
|
|
try:
|
|
res.delete()
|
|
re = db_api.resource_get(self.context, res.id)
|
|
re.delete()
|
|
except Exception as ex:
|
|
failed = True
|
|
logger.error('delete: %s' % str(ex))
|
|
|
|
for res in deps:
|
|
if not failed:
|
|
try:
|
|
res.create()
|
|
except Exception as ex:
|
|
logger.exception('create')
|
|
failed = True
|
|
|
|
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 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
|
|
|
|
def handle_ref(arg):
|
|
return self[arg].FnGetRefId()
|
|
|
|
def handle_getatt(args):
|
|
resource, att = args
|
|
try:
|
|
return self[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
|