# 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 logging import sys from heat.common import exception from heat.engine import resources from heat.engine import instance from heat.engine import volume from heat.engine import eip from heat.engine import security_group from heat.engine import wait_condition 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, stack_name, template, stack_id=0, parms=None, metadata_server=None): self.id = stack_id self.t = template self.parms = self.t.get('Parameters', {}) 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 self.parms['AWS::StackName'] = {"Description": "AWS StackName", "Type": "String", "Value": stack_name} self.parms['AWS::Region'] = {"Description": "AWS Regions", "Type": "String", "Default": "ap-southeast-1", "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."} if parms != None: self._apply_user_parameters(parms) if isinstance(parms['KeyStoneCreds'], (basestring, unicode)): self.creds = eval(parms['KeyStoneCreds']) else: self.creds = parms['KeyStoneCreds'] self.resources = {} for r in self.t['Resources']: type = self.t['Resources'][r]['Type'] if type == 'AWS::EC2::Instance': self.resources[r] = instance.Instance(r, self.t['Resources'][r], self) elif type == 'AWS::EC2::Volume': self.resources[r] = volume.Volume(r, self.t['Resources'][r], self) elif type == 'AWS::EC2::VolumeAttachment': self.resources[r] = volume.VolumeAttachment(r, self.t['Resources'][r], self) elif type == 'AWS::EC2::EIP': self.resources[r] = eip.ElasticIp(r, self.t['Resources'][r], self) elif type == 'AWS::EC2::EIPAssociation': self.resources[r] = eip.ElasticIpAssociation(r, self.t['Resources'][r], self) elif type == 'AWS::EC2::SecurityGroup': self.resources[r] = security_group.SecurityGroup(r, self.t['Resources'][r], self) elif type == 'AWS::CloudFormation::WaitConditionHandle': self.resources[r] = wait_condition.WaitConditionHandle(r, self.t['Resources'][r], self) elif type == 'AWS::CloudFormation::WaitCondition': self.resources[r] = wait_condition.WaitCondition(r, self.t['Resources'][r], self) else: self.resources[r] = resources.GenericResource(r, self.t['Resources'][r], self) self.calulate_dependencies(self.t['Resources'][r], self.resources[r]) 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: res = 'A Ref operation referenced a non-existent key [%s]' % sys.exc_value response = {'ValidateTemplateResult': { 'Description': 'Malformed Query Response [%s]' % (res), 'Parameters': []}} return response for r in order: try: res = self.resources[r].validate() if res: err_str = 'Malformed Query Response [%s]' % (res) response = {'ValidateTemplateResult': { 'Description': err_str, 'Parameters': []}} return response except Exception as ex: logger.exception('validate') failed = True if response == 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[p].get('Description', '') res['DefaultValue'] = self.parms[p].get('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(None, self.name) if stack: self.parsed_template_id = stack.raw_template.parsed_template.id else: return pt = db_api.parsed_template_get(None, self.parsed_template_id) if pt: pt.template = self.t pt.save() 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: failed_str = self.resources[r].CREATE_FAILED if not failed: try: self.resources[r].create() except Exception as ex: logger.exception('create') failed = True self.resources[r].state_set(failed_str, str(ex)) try: self.update_parsed_template() except Exception as ex: logger.exception('update_parsed_template') else: self.resources[r].state_set(failed_str) if failed: self.status_set(self.CREATE_FAILED) else: self.status_set(self.CREATE_COMPLETE) self.update_parsed_template() 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(). ''' self.status_set(self.DELETE_IN_PROGRESS) order = self.get_create_order() order.reverse() for r in order: try: self.resources[r].delete() db_api.resource_get(None, self.resources[r].id).delete() except Exception as ex: logger.error('delete: %s' % str(ex)) db_api.stack_delete(None, self.name) self.status_set(self.DELETE_COMPLETE) def delete(self): pool = eventlet.GreenPool() pool.spawn_n(self.delete_blocking) def get_outputs(self): for r in self.resources: self.resources[r].reload() self.resolve_attributes(self.outputs) self.resolve_joins(self.outputs) outs = [] for o in self.outputs: out = {} out['Description'] = self.outputs[o].get('Description', 'No description given') out['OutputKey'] = o out['OutputValue'] = self.outputs[o].get('Value', '') outs.append(out) return outs 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]) r.depends_on.append(s[i]) elif i == 'DependsOn' or i == 'Ref': #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_parameter(self, key, value): logger.debug('appling user parameter %s=%s ' % (key, value)) if not key in self.parms: self.parms[key] = {} self.parms[key]['Value'] = value 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] self._apply_user_parameter(parms[key_name], parms[value_name]) except Exception: logger.error('Could not apply parameter %s' % p) def parameter_get(self, key): if self.parms[key] == None: raise exception.UserParameterMissing(key=key) elif 'Value' in self.parms[key]: return self.parms[key]['Value'] elif 'Default' in self.parms[key]: return self.parms[key]['Default'] else: raise exception.UserParameterMissing(key=key) def resolve_static_refs(self, s): ''' looking for { "Ref": "str" } ''' if isinstance(s, dict): for i in s: if i == 'Ref' and \ isinstance(s[i], (basestring, unicode)) and \ s[i] in self.parms: return self.parameter_get(s[i]) else: s[i] = self.resolve_static_refs(s[i]) elif isinstance(s, list): for index, item in enumerate(s): #print 'resolve_static_refs %d %s' % (index, item) s[index] = self.resolve_static_refs(item) return s def resolve_find_in_map(self, s): ''' looking for { "Fn::FindInMap": ["str", "str"] } ''' if isinstance(s, dict): for i in s: if i == 'Fn::FindInMap': obj = self.maps if isinstance(s[i], list): #print 'map list: %s' % s[i] for index, item in enumerate(s[i]): if isinstance(item, dict): item = self.resolve_find_in_map(item) #print 'map item dict: %s' % (item) else: pass #print 'map item str: %s' % (item) obj = obj[item] else: obj = obj[s[i]] return obj else: s[i] = self.resolve_find_in_map(s[i]) elif isinstance(s, list): for index, item in enumerate(s): s[index] = self.resolve_find_in_map(item) return s def resolve_attributes(self, s): ''' looking for something like: {"Fn::GetAtt" : ["DBInstance", "Endpoint.Address"]} ''' if isinstance(s, dict): for i in s: if i == 'Ref' and s[i] in self.resources: return self.resources[s[i]].FnGetRefId() elif i == 'Fn::GetAtt': resource_name = s[i][0] key_name = s[i][1] res = self.resources.get(resource_name) rc = None if res: return res.FnGetAtt(key_name) else: raise exception.InvalidTemplateAttribute( resource=resource_name, key=key_name) return rc else: s[i] = self.resolve_attributes(s[i]) elif isinstance(s, list): for index, item in enumerate(s): s[index] = self.resolve_attributes(item) return s def resolve_joins(self, s): ''' looking for { "Fn::join": []} ''' if isinstance(s, dict): for i in s: if i == 'Fn::Join': j = None try: j = s[i][0].join(s[i][1]) except Exception: logger.error('Could not join %s' % str(s[i])) return j else: s[i] = self.resolve_joins(s[i]) elif isinstance(s, list): for index, item in enumerate(s): s[index] = self.resolve_joins(item) return s def resolve_base64(self, s): ''' looking for { "Fn::join": [] } ''' if isinstance(s, dict): for i in s: if i == 'Fn::Base64': return s[i] else: s[i] = self.resolve_base64(s[i]) elif isinstance(s, list): for index, item in enumerate(s): s[index] = self.resolve_base64(item) return s