if stacks is None:
return res
for s in stacks:
- ps = parser.Stack(context, s.name,
- s.raw_template.template,
- s.id, s.parameters)
+ stack = parser.Stack.load(context, s.id)
mem = {}
mem['StackId'] = "/".join([s.name, str(s.id)])
mem['StackName'] = s.name
mem['CreationTime'] = heat_utils.strtime(s.created_at)
- mem['TemplateDescription'] = ps.t.get('Description',
- 'No description')
+ mem['TemplateDescription'] = stack.t[parser.DESCRIPTION]
mem['StackStatus'] = s.status
res['stacks'].append(mem)
logging.debug("Processing show_stack for %s" % stack)
s = db_api.stack_get_by_name(context, stack)
if s:
- ps = parser.Stack(context, s.name,
- s.raw_template.template,
- s.id, s.parameters)
+ stack = parser.Stack.load(context, s.id)
mem = {}
mem['StackId'] = "/".join([s.name, str(s.id)])
mem['StackName'] = s.name
mem['CreationTime'] = heat_utils.strtime(s.created_at)
mem['LastUpdatedTimestamp'] = heat_utils.strtime(s.updated_at)
mem['NotificationARNs'] = 'TODO'
- mem['Parameters'] = ps.t['Parameters']
- mem['Description'] = ps.t.get('Description',
- 'No description')
+ mem['Parameters'] = stack.t[parser.PARAMETERS]
+ mem['Description'] = stack.t[parser.DESCRIPTION]
mem['StackStatus'] = s.status
mem['StackStatusReason'] = s.status_reason
# only show the outputs on a completely created stack
- if s.status == ps.CREATE_COMPLETE:
- mem['Outputs'] = ps.get_outputs()
+ if s.status == stack.CREATE_COMPLETE:
+ mem['Outputs'] = stack.get_outputs()
res['stacks'].append(mem)
if db_api.stack_get_by_name(None, stack_name):
return {'Error': 'Stack already exists with that name.'}
- user_params = _extract_user_params(params)
- # We don't want to reset the stack template, so we are making
- # an instance just for validation.
- template_copy = deepcopy(template)
- stack_validator = parser.Stack(context, stack_name,
- template_copy, 0,
- user_params)
- response = stack_validator.validate()
- stack_validator = None
- template_copy = None
- if 'Malformed Query Response' in \
- response['ValidateTemplateResult']['Description']:
- return response
-
- stack = parser.Stack(context, stack_name, template, 0, user_params)
- rt = {}
- rt['template'] = template
- rt['StackName'] = stack_name
- new_rt = db_api.raw_template_create(None, rt)
-
- new_creds = db_api.user_creds_create(context.to_dict())
+ tmpl = parser.Template(template)
+ user_params = parser.Parameters(stack_name, tmpl,
+ _extract_user_params(params))
+ stack = parser.Stack(context, stack_name, tmpl, user_params)
- s = {}
- s['name'] = stack_name
- s['raw_template_id'] = new_rt.id
- s['user_creds_id'] = new_creds.id
- s['username'] = context.username
- s['parameters'] = user_params
- new_s = db_api.stack_create(context, s)
- stack.id = new_s.id
+ response = stack.validate()
+ if response['Description'] != 'Successfully validated':
+ return response
+ stack_id = stack.store()
greenpool.spawn_n(stack.create, **_extract_args(params))
- return {'StackId': "/".join([new_s.name, str(new_s.id)])}
+ return {'StackId': "/".join([stack.name, str(stack.id)])}
def validate_template(self, context, template, params):
"""
msg = _("No Template provided.")
return webob.exc.HTTPBadRequest(explanation=msg)
+ stack_name = 'validate'
try:
- s = parser.Stack(context, 'validate', template, 0,
- _extract_user_params(params))
+ tmpl = parser.Template(template)
+ user_params = parser.Parameters(stack_name, tmpl,
+ _extract_user_params(params))
+ s = parser.Stack(context, stack_name, tmpl, user_params)
except KeyError as ex:
res = ('A Fn::FindInMap operation referenced '
'a non-existent map [%s]' % str(ex))
- response = {'ValidateTemplateResult': {
- 'Description': 'Malformed Query Response [%s]' % (res),
- 'Parameters': []}}
- return response
-
- res = s.validate()
+ result = {'Description': 'Malformed Query Response [%s]' % (res),
+ 'Parameters': []}
+ else:
+ result = s.validate()
- return res
+ return {'ValidateTemplateResult': result}
def get_template(self, context, stack_name, params):
"""
logger.info('deleting stack %s' % stack_name)
- ps = parser.Stack(context, st.name,
- st.raw_template.template,
- st.id, st.parameters)
- greenpool.spawn_n(ps.delete)
+ stack = parser.Stack.load(context, st.id)
+ greenpool.spawn_n(stack.delete)
return None
# Helper for list_events. It's here so we can use it in tests.
def describe_stack_resource(self, context, stack_name, resource_name):
auth.authenticate(context)
- stack = db_api.stack_get_by_name(context, stack_name)
- if not stack:
+ s = db_api.stack_get_by_name(context, stack_name)
+ if not s:
raise AttributeError('Unknown stack name')
- resource = db_api.resource_get_by_name_and_stack(context,
- resource_name,
- stack.id)
- if not resource:
+
+ stack = parser.Stack.load(context, s.id)
+ if resource_name not in stack:
raise AttributeError('Unknown resource name')
- return format_resource_attributes(stack, resource)
+
+ resource = stack[resource_name]
+ if resource.id is None:
+ raise AttributeError('Resource not created')
+
+ return format_stack_resource(stack[resource_name])
def describe_stack_resources(self, context, stack_name,
physical_resource_id, logical_resource_id):
auth.authenticate(context)
- if stack_name:
- stack = db_api.stack_get_by_name(context, stack_name)
+ if stack_name is not None:
+ s = db_api.stack_get_by_name(context, stack_name)
else:
- resource = db_api.resource_get_by_physical_resource_id(context,
+ rs = db_api.resource_get_by_physical_resource_id(context,
physical_resource_id)
- if not resource:
+ if not rs:
msg = "The specified PhysicalResourceId doesn't exist"
raise AttributeError(msg)
- stack = resource.stack
+ s = rs.stack
- if not stack:
+ if not s:
raise AttributeError("The specified stack doesn't exist")
+ stack = parser.Stack.load(context, s.id)
resources = []
- for r in stack.resources:
- if logical_resource_id and r.name != logical_resource_id:
+ for resource in stack:
+ if logical_resource_id and resource.name != logical_resource_id:
continue
- formatted = format_resource_attributes(stack, r)
+ formatted = format_stack_resource(resource)
# this API call uses Timestamp instead of LastUpdatedTimestamp
formatted['Timestamp'] = formatted['LastUpdatedTimestamp']
del formatted['LastUpdatedTimestamp']
def list_stack_resources(self, context, stack_name):
auth.authenticate(context)
- stack = db_api.stack_get_by_name(context, stack_name)
- if not stack:
+ s = db_api.stack_get_by_name(context, stack_name)
+ if not s:
raise AttributeError('Unknown stack name')
+ stack = parser.Stack.load(context, s.id)
+
resources = []
response_keys = ('ResourceStatus', 'LogicalResourceId',
'LastUpdatedTimestamp', 'PhysicalResourceId',
'ResourceType')
- for r in stack.resources:
- formatted = format_resource_attributes(stack, r)
+ for resource in stack:
+ formatted = format_stack_resource(resource)
for key in formatted.keys():
if not key in response_keys:
del formatted[key]
if s:
user_creds = db_api.user_creds_get(s.user_creds_id)
ctxt = ctxtlib.RequestContext.from_dict(dict(user_creds))
- ps = parser.Stack(ctxt, s.name,
- s.raw_template.template,
- s.id, s.parameters)
+ stack = parser.Stack.load(ctxt, s.id)
for a in wr.rule[action_map[new_state]]:
- greenpool.spawn_n(ps[a].alarm)
+ greenpool.spawn_n(stack[a].alarm)
wr.last_evaluated = now
return [None, wd.data]
-def format_resource_attributes(stack, resource):
+def format_stack_resource(resource):
"""
Return a representation of the given resource that mathes the API output
expectations.
"""
- template = resource.parsed_template.template
- template_resources = template.get('Resources', {})
- resource_type = template_resources.get(resource.name, {}).get('Type', '')
- last_updated_time = resource.updated_at or resource.created_at
+ rs = db_api.resource_get(resource.stack.context, resource.id)
+ last_updated_time = rs.updated_at or rs.created_at
return {
- 'StackId': stack.id,
- 'StackName': stack.name,
+ 'StackId': resource.stack.id,
+ 'StackName': resource.stack.name,
'LogicalResourceId': resource.name,
- 'PhysicalResourceId': resource.nova_instance or '',
- 'ResourceType': resource_type,
+ 'PhysicalResourceId': resource.instance_id or '',
+ 'ResourceType': resource.t['Type'],
'LastUpdatedTimestamp': heat_utils.strtime(last_updated_time),
- 'ResourceStatus': resource.state,
- 'ResourceStatusReason': resource.state_description,
+ 'ResourceStatus': rs.state,
+ 'ResourceStatusReason': rs.state_description,
}
import eventlet
import json
-import itertools
+import functools
+import copy
import logging
from heat.common import exception
from heat.engine.resources import Resource
from heat.db import api as db_api
+
logger = logging.getLogger('heat.engine.parser')
+SECTIONS = (VERSION, DESCRIPTION, MAPPINGS,
+ PARAMETERS, RESOURCES, OUTPUTS) = \
+ ('AWSTemplateFormatVersion', 'Description', 'Mappings',
+ 'Parameters', 'Resources', 'Outputs')
+
+(PARAM_STACK_NAME, PARAM_REGION) = ('AWS::StackName', 'AWS::Region')
+
+
+class Parameters(checkeddict.CheckedDict):
+ '''
+ The parameters of a stack, with type checking, defaults &c. specified by
+ the stack's template.
+ '''
+
+ def __init__(self, stack_name, template, user_params={}):
+ '''
+ Create the parameter container for a stack from the stack name and
+ template, optionally setting the initial set of parameters.
+ '''
+ checkeddict.CheckedDict.__init__(self, PARAMETERS)
+ self._init_schemata(template[PARAMETERS])
+
+ self[PARAM_STACK_NAME] = stack_name
+ self.update(user_params)
+
+ def _init_schemata(self, schemata):
+ '''
+ Initialise the parameter schemata with the pseudo-parameters and the
+ list of schemata obtained from the template.
+ '''
+ self.addschema(PARAM_STACK_NAME, {"Description": "AWS StackName",
+ "Type": "String"})
+ self.addschema(PARAM_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.",
+ })
+
+ for param, schema in schemata.items():
+ self.addschema(param, copy.deepcopy(schema))
+
+ def user_parameters(self):
+ '''
+ Return a dictionary of all the parameters passed in by the user
+ '''
+ return dict((k, v['Value']) for k, v in self.data.iteritems()
+ if 'Value' in v)
+
+
+class Template(object):
+ '''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 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)
+
class Stack(object):
IN_PROGRESS = 'IN_PROGRESS'
DELETE_FAILED = 'DELETE_FAILED'
DELETE_COMPLETE = 'DELETE_COMPLETE'
- def __init__(self, context, stack_name, template, stack_id=0, parms=None):
+ def __init__(self, context, stack_name, template, parameters=None,
+ stack_id=None):
+ '''
+ Initialise from a context, name, Template object and (optionally)
+ Parameters object. The database ID may also be initialised, if the
+ stack is already in the database.
+ '''
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
- # 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)
+ if parameters is None:
+ parameters = Parameters(stack_name, template)
+ self.parameters = parameters
- self.outputs = self.resolve_static_data(self.t.get('Outputs', {}))
+ self.outputs = self.resolve_static_data(self.t[OUTPUTS])
self.resources = dict((name,
Resource(name, data, self))
- for (name, data) in self.t['Resources'].items())
+ for (name, data) in self.t[RESOURCES].items())
+
+ self.dependencies = self._get_dependencies(self.resources.itervalues())
+
+ @staticmethod
+ def _get_dependencies(resources):
+ '''Return the dependency graph for a list of resources'''
+ deps = dependencies.Dependencies()
+ for resource in resources:
+ resource.add_dependencies(deps)
+
+ return deps
+
+ @classmethod
+ def load(cls, context, stack_id):
+ '''Retrieve a Stack from the database'''
+ s = db_api.stack_get(context, stack_id)
+
+ template = Template.load(context, s.raw_template_id)
+ params = Parameters(s.name, template, s.parameters)
+ stack = cls(context, s.name, template, params, stack_id)
+
+ return stack
- self.dependencies = dependencies.Dependencies()
- for resource in self.resources.values():
- resource.add_dependencies(self.dependencies)
+ def store(self, owner=None):
+ '''Store the stack in the database and return its ID'''
+ if self.id is None:
+ new_creds = db_api.user_creds_create(self.context.to_dict())
+
+ s = {'name': self.name,
+ 'raw_template_id': self.t.store(),
+ 'parameters': self.parameters.user_parameters(),
+ 'owner_id': owner and owner.id,
+ 'user_creds_id': new_creds.id,
+ 'username': self.context.username}
+ new_s = db_api.stack_create(self.context, s)
+ self.id = new_s.id
+
+ return self.id
def __iter__(self):
'''
return key in self.resources
def keys(self):
+ '''Return a list of resource keys for the stack'''
return self.resources.keys()
def __str__(self):
+ '''Return a human-readable string representation of the stack'''
return 'Stack "%s"' % self.name
def validate(self):
'''
# TODO(sdake) Should return line number of invalid reference
- response = None
-
for res in self:
try:
result = res.validate()
if result:
err_str = 'Malformed Query Response %s' % result
- response = {'ValidateTemplateResult': {
- 'Description': err_str,
- 'Parameters': []}}
+ response = {'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 format_param(p):
+ return {'NoEcho': 'false',
+ 'ParameterKey': p,
+ 'Description': self.parameters.get_attr(p, 'Description'),
+ 'DefaultValue': self.parameters.get_attr(p, 'Default')}
- 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)
+ response = {'Description': 'Successfully validated',
+ 'Parameters': [format_param(p) for p in self.parameters]}
- if stack is None:
+ return response
+
+ def state_set(self, new_status, reason):
+ '''Update the stack state in the database'''
+ if self.id is None:
return
- self.id = stack.id
+ stack = db_api.stack_get(self.context, self.id)
stack.update_and_save({'status': new_status,
'status_reason': reason})
'''
Delete all of the resources, and then the stack itself.
'''
- self.state_set(self.DELETE_IN_PROGRESS)
+ self.state_set(self.DELETE_IN_PROGRESS, 'Stack deletion started')
failures = []
logger.exception('create')
failed = True
else:
- res.state_set(res.CREATE_FAILED)
+ res.state_set(res.CREATE_FAILED, 'Resource restart aborted')
# 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_availability_zones(self, s):
- '''
- looking for { "Fn::GetAZs" : "str" }
- '''
- def match(key, value):
- return (key == 'Fn::GetAZs' and
- isinstance(value, basestring))
-
- def handle(ref):
- return ['nova']
-
- 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_availability_zones,
- self._resolve_find_in_map])
+ return transform(snippet,
+ [functools.partial(self.t.resolve_param_refs,
+ parameters=self.parameters),
+ self.t.resolve_availability_zones,
+ self.t.resolve_find_in_map])
def resolve_runtime_data(self, snippet):
- return transform(snippet, [self._resolve_attributes,
- self._resolve_joins,
- self._resolve_base64])
+ return transform(snippet,
+ [functools.partial(self.t.resolve_resource_refs,
+ resources=self.resources),
+ functools.partial(self.t.resolve_attributes,
+ resources=self.resources),
+ self.t.resolve_joins,
+ self.t.resolve_base64])
def transform(data, transformations):
return p
def nested(self):
- if self._nested is None:
- if self.instance_id is None:
- return None
+ if self._nested is None and self.instance_id is not None:
+ self._nested = parser.Stack.load(self.stack.context,
+ self.instance_id)
- st = db_api.stack_get(self.stack.context, self.instance_id)
- if not st:
+ if self._nested is None:
raise exception.NotFound('Nested stack not found in DB')
- n = parser.Stack(self.stack.context, st.name,
- st.raw_template.template,
- self.instance_id, self._params())
- self._nested = n
-
return self._nested
def create_with_template(self, child_template):
'''
Handle the creation of the nested stack from a given JSON template.
'''
+ template = parser.Template(child_template)
+ params = parser.Parameters(self.name, template, self._params())
+
self._nested = parser.Stack(self.stack.context,
self.name,
- child_template,
- parms=self._params())
-
- rt = {'template': child_template, 'stack_name': self.name}
- new_rt = db_api.raw_template_create(None, rt)
-
- parent_stack = db_api.stack_get(self.stack.context, self.stack.id)
-
- s = {'name': self.name,
- 'owner_id': self.stack.id,
- 'raw_template_id': new_rt.id,
- 'user_creds_id': parent_stack.user_creds_id,
- 'username': self.stack.context.username}
- new_s = db_api.stack_create(None, s)
- self._nested.id = new_s.id
+ template,
+ params)
+ nested_id = self._nested.store(self.stack)
+ self.instance_id_set(nested_id)
self._nested.create()
- self.instance_id_set(self._nested.id)
def handle_create(self):
response = urllib2.urlopen(self.properties[PROP_TEMPLATE_URL])
if stack is None:
# This seems like a hack, to get past validation
return ''
- if op not in self.nested().outputs:
+ if op not in stack.outputs:
raise exception.InvalidTemplateAttribute(resource=self.name,
key=key)
t = json.loads(f.read())
f.close()
- params = {}
- params['KeyStoneCreds'] = None
- t['Parameters']['KeyName']['Value'] = keyname
- t['Parameters']['DBUsername']['Value'] = dbusername
- t['Parameters']['DBPassword']['Value'] = creds['password']
+ template = parser.Template(t)
+ params = parser.Parameters('test', t,
+ {'KeyName': keyname,
+ 'DBUsername': dbusername,
+ 'DBPassword': creds['password']})
- stack = parser.Stack('test', t, 0, params)
+ stack = parser.Stack(None, 'test', template, params)
parsed_t = stack.resolve_static_refs(t)
remote_file = sftp.open('/var/lib/cloud/instance/scripts/startup')
remote_file_list = remote_file.read().split('\n')
import nose
import unittest
from nose.plugins.attrib import attr
+import mox
-from heat.engine.parser import _resolve as resolve
+import json
+from heat.common import exception
+from heat.engine import parser
+from heat.engine import checkeddict
+from heat.engine.resources import Resource
def join(raw):
delim, strs = args
return delim.join(strs)
- return resolve(lambda k, v: k == 'Fn::Join', handle_join, raw)
+ return parser._resolve(lambda k, v: k == 'Fn::Join', handle_join, raw)
@attr(tag=['unit', 'parser'])
self.assertEqual(join(raw), 'foo bar\nbaz')
+mapping_template = json.loads('''{
+ "Mappings" : {
+ "ValidMapping" : {
+ "TestKey" : { "TestValue" : "wibble" }
+ },
+ "InvalidMapping" : {
+ "ValueList" : [ "foo", "bar" ],
+ "ValueString" : "baz"
+ },
+ "MapList": [ "foo", { "bar" : "baz" } ],
+ "MapString": "foobar"
+ }
+}''')
+
+
+@attr(tag=['unit', 'parser', 'template'])
+@attr(speed='fast')
+class TemplateTest(unittest.TestCase):
+ def setUp(self):
+ self.m = mox.Mox()
+
+ def tearDown(self):
+ self.m.UnsetStubs()
+
+ def test_defaults(self):
+ empty = parser.Template({})
+ try:
+ empty[parser.VERSION]
+ except KeyError:
+ pass
+ else:
+ self.fail('Expected KeyError for version not present')
+ self.assertEqual(empty[parser.DESCRIPTION], 'No description')
+ self.assertEqual(empty[parser.MAPPINGS], {})
+ self.assertEqual(empty[parser.PARAMETERS], {})
+ self.assertEqual(empty[parser.RESOURCES], {})
+ self.assertEqual(empty[parser.OUTPUTS], {})
+
+ def test_invalid_section(self):
+ tmpl = parser.Template({'Foo': ['Bar']})
+ try:
+ tmpl['Foo']
+ except KeyError:
+ pass
+ else:
+ self.fail('Expected KeyError for invalid template key')
+
+ def test_find_in_map(self):
+ tmpl = parser.Template(mapping_template)
+ find = {'Fn::FindInMap': ["ValidMapping", "TestKey", "TestValue"]}
+ self.assertEqual(tmpl.resolve_find_in_map(find), "wibble")
+
+ def test_find_in_invalid_map(self):
+ tmpl = parser.Template(mapping_template)
+ finds = ({'Fn::FindInMap': ["InvalidMapping", "ValueList", "foo"]},
+ {'Fn::FindInMap': ["InvalidMapping", "ValueString", "baz"]},
+ {'Fn::FindInMap': ["MapList", "foo", "bar"]},
+ {'Fn::FindInMap': ["MapString", "foo", "bar"]})
+
+ for find in finds:
+ self.assertRaises(KeyError, tmpl.resolve_find_in_map, find)
+
+ def test_bad_find_in_map(self):
+ tmpl = parser.Template(mapping_template)
+ finds = ({'Fn::FindInMap': "String"},
+ {'Fn::FindInMap': {"Dict": "String"}},
+ {'Fn::FindInMap': ["ShortList", "foo"]},
+ {'Fn::FindInMap': ["ReallyShortList"]})
+
+ for find in finds:
+ self.assertRaises(KeyError, tmpl.resolve_find_in_map, find)
+
+ def test_param_refs(self):
+ params = {'foo': 'bar', 'blarg': 'wibble'}
+ p_snippet = {"Ref": "foo"}
+ self.assertEqual(parser.Template.resolve_param_refs(p_snippet, params),
+ "bar")
+
+ def test_param_refs_resource(self):
+ params = {'foo': 'bar', 'blarg': 'wibble'}
+ r_snippet = {"Ref": "baz"}
+ self.assertEqual(parser.Template.resolve_param_refs(r_snippet, params),
+ r_snippet)
+
+ def test_param_ref_missing(self):
+ params = checkeddict.CheckedDict("test")
+ params.addschema('foo', {"Required": True})
+ snippet = {"Ref": "foo"}
+ self.assertRaises(exception.UserParameterMissing,
+ parser.Template.resolve_param_refs,
+ snippet, params)
+
+ def test_resource_refs(self):
+ resources = {'foo': self.m.CreateMock(Resource),
+ 'blarg': self.m.CreateMock(Resource)}
+ resources['foo'].FnGetRefId().AndReturn('bar')
+ self.m.ReplayAll()
+
+ r_snippet = {"Ref": "foo"}
+ self.assertEqual(parser.Template.resolve_resource_refs(r_snippet,
+ resources),
+ "bar")
+ self.m.VerifyAll()
+
+ def test_resource_refs_param(self):
+ resources = {'foo': 'bar', 'blarg': 'wibble'}
+ p_snippet = {"Ref": "baz"}
+ self.assertEqual(parser.Template.resolve_resource_refs(p_snippet,
+ resources),
+ p_snippet)
+
+ def test_join(self):
+ join = {"Fn::Join": [" ", ["foo", "bar"]]}
+ self.assertEqual(parser.Template.resolve_joins(join), "foo bar")
+
+ def test_join_string(self):
+ join = {"Fn::Join": [" ", "foo"]}
+ self.assertRaises(TypeError, parser.Template.resolve_joins,
+ join)
+
+ def test_join_dict(self):
+ join = {"Fn::Join": [" ", {"foo": "bar"}]}
+ self.assertRaises(TypeError, parser.Template.resolve_joins,
+ join)
+
+ def test_join_wrong_num_args(self):
+ join0 = {"Fn::Join": []}
+ self.assertRaises(ValueError, parser.Template.resolve_joins,
+ join0)
+ join1 = {"Fn::Join": [" "]}
+ self.assertRaises(ValueError, parser.Template.resolve_joins,
+ join1)
+ join3 = {"Fn::Join": [" ", {"foo": "bar"}, ""]}
+ self.assertRaises(ValueError, parser.Template.resolve_joins,
+ join3)
+
+ def test_join_string_nodelim(self):
+ join1 = {"Fn::Join": "o"}
+ self.assertRaises(TypeError, parser.Template.resolve_joins,
+ join1)
+ join2 = {"Fn::Join": "oh"}
+ self.assertRaises(TypeError, parser.Template.resolve_joins,
+ join2)
+ join3 = {"Fn::Join": "ohh"}
+ self.assertRaises(TypeError, parser.Template.resolve_joins,
+ join3)
+
+ def test_join_dict_nodelim(self):
+ join1 = {"Fn::Join": {"foo": "bar"}}
+ self.assertRaises(TypeError, parser.Template.resolve_joins,
+ join1)
+ join2 = {"Fn::Join": {"foo": "bar", "blarg": "wibble"}}
+ self.assertRaises(TypeError, parser.Template.resolve_joins,
+ join2)
+ join3 = {"Fn::Join": {"foo": "bar", "blarg": "wibble", "baz": "quux"}}
+ self.assertRaises(TypeError, parser.Template.resolve_joins,
+ join3)
+
+ def test_base64(self):
+ snippet = {"Fn::Base64": "foobar"}
+ # For now, the Base64 function just returns the original text, and
+ # does not convert to base64 (see issue #133)
+ self.assertEqual(parser.Template.resolve_base64(snippet), "foobar")
+
+ def test_base64_list(self):
+ list_snippet = {"Fn::Base64": ["foobar"]}
+ self.assertRaises(TypeError, parser.Template.resolve_base64,
+ list_snippet)
+
+ def test_base64_dict(self):
+ dict_snippet = {"Fn::Base64": {"foo": "bar"}}
+ self.assertRaises(TypeError, parser.Template.resolve_base64,
+ dict_snippet)
+
+
+params_schema = json.loads('''{
+ "Parameters" : {
+ "User" : { "Type": "String" },
+ "Defaulted" : {
+ "Type": "String",
+ "Default": "foobar"
+ }
+ }
+}''')
+
+
+@attr(tag=['unit', 'parser', 'parameters'])
+@attr(speed='fast')
+class ParametersTest(unittest.TestCase):
+ def test_pseudo_params(self):
+ params = parser.Parameters('test_stack', {"Parameters": {}})
+
+ self.assertEqual(params['AWS::StackName'], 'test_stack')
+ self.assertTrue('AWS::Region' in params)
+
+ def test_user_param(self):
+ params = parser.Parameters('test', params_schema, {'User': 'wibble'})
+ user_params = params.user_parameters()
+ self.assertEqual(user_params['User'], 'wibble')
+
+ def test_user_param_default(self):
+ params = parser.Parameters('test', params_schema)
+ user_params = params.user_parameters()
+ self.assertTrue('Defaulted' not in user_params)
+
+ def test_user_param_nonexist(self):
+ params = parser.Parameters('test', params_schema)
+ user_params = params.user_parameters()
+ self.assertTrue('User' not in user_params)
+
+ def test_schema_invariance(self):
+ params1 = parser.Parameters('test', params_schema)
+ params1['Defaulted'] = "wibble"
+ self.assertEqual(params1['Defaulted'], 'wibble')
+
+ params2 = parser.Parameters('test', params_schema)
+ self.assertEqual(params2['Defaulted'], 'foobar')
+
+
# allows testing of the test directly, shown below
if __name__ == '__main__':
sys.argv.append(__file__)
t = json.loads(f.read())
f.close()
- parameters = {}
t['Parameters']['KeyName']['Value'] = 'test'
- stack = parser.Stack(None, 'test_stack', t, 0)
+ stack = parser.Stack(None, 'test_stack', parser.Template(t),
+ stack_id=-1)
self.m.StubOutWithMock(db_api, 'resource_get_by_name_and_stack')
db_api.resource_get_by_name_and_stack(None, 'test_resource_name',
stack).AndReturn(None)
self.m.StubOutWithMock(instances.Instance, 'nova')
- instances.Instance.nova().AndReturn(self.fc)
- instances.Instance.nova().AndReturn(self.fc)
- instances.Instance.nova().AndReturn(self.fc)
- instances.Instance.nova().AndReturn(self.fc)
+ instances.Instance.nova().MultipleTimes().AndReturn(self.fc)
self.m.ReplayAll()
t = json.loads(f.read())
f.close()
- parameters = {}
t['Parameters']['KeyName']['Value'] = 'test'
- stack = parser.Stack(None, 'test_stack', t, 0)
+ stack = parser.Stack(None, 'test_stack', parser.Template(t),
+ stack_id=-1)
self.m.StubOutWithMock(db_api, 'resource_get_by_name_and_stack')
db_api.resource_get_by_name_and_stack(None, 'test_resource_name',
def setUp(self):
self.m = mox.Mox()
self.fc = fakes.FakeClient()
- self.path = os.path.dirname(os.path.realpath(__file__)).\
- replace('heat/tests', 'templates')
+ path = os.path.dirname(os.path.realpath(__file__))
+ self.path = path.replace(os.path.join('heat', 'tests'), 'templates')
def tearDown(self):
self.m.UnsetStubs()
print "stackTest teardown complete"
+ def create_context(self, user='stacks_test_user'):
+ ctx = context.get_admin_context()
+ self.m.StubOutWithMock(ctx, 'username')
+ ctx.username = user
+ self.m.StubOutWithMock(auth, 'authenticate')
+ return ctx
+
# We use this in a number of tests so it's factored out here.
- def start_wordpress_stack(self, stack_name):
- f = open("%s/WordPress_Single_Instance_gold.template" % self.path)
- t = json.loads(f.read())
- f.close()
- params = {}
- parameters = {}
- t['Parameters']['KeyName']['Value'] = 'test'
- stack = parser.Stack(None, stack_name, t, 0, params)
+ def get_wordpress_stack(self, stack_name, ctx=None):
+ tmpl_path = os.path.join(self.path,
+ 'WordPress_Single_Instance_gold.template')
+ with open(tmpl_path) as f:
+ t = json.load(f)
+
+ template = parser.Template(t)
+ parameters = parser.Parameters(stack_name, template,
+ {'KeyName': 'test'})
+
+ stack = parser.Stack(ctx or self.create_context(),
+ stack_name, template, parameters)
+
self.m.StubOutWithMock(instances.Instance, 'nova')
- instances.Instance.nova().AndReturn(self.fc)
- instances.Instance.nova().AndReturn(self.fc)
- instances.Instance.nova().AndReturn(self.fc)
- instances.Instance.nova().AndReturn(self.fc)
+ instances.Instance.nova().MultipleTimes().AndReturn(self.fc)
+
instance = stack.resources['WebServer']
instance.itype_oflavor['m1.large'] = 'm1.large'
instance.calculate_properties()
name='WebServer', security_groups=None,
userdata=server_userdata).\
AndReturn(self.fc.servers.list()[-1])
+
return stack
def test_wordpress_single_instance_stack_create(self):
- stack = self.start_wordpress_stack('test_stack')
+ stack = self.get_wordpress_stack('test_stack')
self.m.ReplayAll()
stack.create()
self.assertNotEqual(stack.resources['WebServer'].ipaddress, '0.0.0.0')
def test_wordpress_single_instance_stack_delete(self):
- stack = self.start_wordpress_stack('test_stack')
+ ctx = self.create_context()
+ stack = self.get_wordpress_stack('test_stack', ctx)
self.m.ReplayAll()
- rt = {}
- rt['template'] = stack.t
- rt['StackName'] = stack.name
- new_rt = db_api.raw_template_create(None, rt)
- ct = {'username': 'fred',
- 'password': 'mentions_fruit'}
- new_creds = db_api.user_creds_create(ct)
- s = {}
- s['name'] = stack.name
- s['raw_template_id'] = new_rt.id
- s['user_creds_id'] = new_creds.id
- s['username'] = ct['username']
- new_s = db_api.stack_create(None, s)
- stack.id = new_s.id
+ stack_id = stack.store()
stack.create()
+
+ db_s = db_api.stack_get(ctx, stack_id)
+ self.assertNotEqual(db_s, None)
+
self.assertNotEqual(stack.resources['WebServer'], None)
self.assertTrue(stack.resources['WebServer'].instance_id > 0)
stack.delete()
self.assertEqual(stack.resources['WebServer'].state, 'DELETE_COMPLETE')
- self.assertEqual(new_s.status, 'DELETE_COMPLETE')
+ self.assertEqual(db_api.stack_get(ctx, stack_id), None)
+ self.assertEqual(db_s.status, 'DELETE_COMPLETE')
def test_stack_event_list(self):
- stack = self.start_wordpress_stack('test_event_list_stack')
+ stack = self.get_wordpress_stack('test_event_list_stack')
self.m.ReplayAll()
- rt = {}
- rt['template'] = stack.t
- rt['StackName'] = stack.name
- new_rt = db_api.raw_template_create(None, rt)
- ct = {'username': 'fred',
- 'password': 'mentions_fruit'}
- new_creds = db_api.user_creds_create(ct)
- s = {}
- s['name'] = stack.name
- s['raw_template_id'] = new_rt.id
- s['user_creds_id'] = new_creds.id
- s['username'] = ct['username']
- new_s = db_api.stack_create(None, s)
- stack.id = new_s.id
+ stack.store()
stack.create()
self.assertNotEqual(stack.resources['WebServer'], None)
'm1.large')
def test_stack_list(self):
- stack = self.start_wordpress_stack('test_stack_list')
- rt = {}
- rt['template'] = stack.t
- rt['StackName'] = stack.name
- new_rt = db_api.raw_template_create(None, rt)
- ct = {'username': 'fred',
- 'password': 'mentions_fruit'}
- new_creds = db_api.user_creds_create(ct)
+ ctx = self.create_context()
+ auth.authenticate(ctx).AndReturn(True)
- ctx = context.get_admin_context()
- self.m.StubOutWithMock(ctx, 'username')
- ctx.username = 'fred'
- self.m.StubOutWithMock(auth, 'authenticate')
+ stack = self.get_wordpress_stack('test_stack_list', ctx)
+
+ self.m.ReplayAll()
+ stack.store()
+ stack.create()
+
+ man = manager.EngineManager()
+ sl = man.list_stacks(ctx, {})
+
+ self.assertTrue(len(sl['stacks']) > 0)
+ for s in sl['stacks']:
+ self.assertNotEqual(s['StackId'], None)
+ self.assertNotEqual(s['TemplateDescription'].find('WordPress'), -1)
+
+ def test_stack_describe_all(self):
+ ctx = self.create_context('stack_describe_all')
auth.authenticate(ctx).AndReturn(True)
- s = {}
- s['name'] = stack.name
- s['raw_template_id'] = new_rt.id
- s['user_creds_id'] = new_creds.id
- s['username'] = ct['username']
- new_s = db_api.stack_create(ctx, s)
- stack.id = new_s.id
- instances.Instance.nova().AndReturn(self.fc)
+ stack = self.get_wordpress_stack('test_stack_desc_all', ctx)
+
self.m.ReplayAll()
+ stack.store()
stack.create()
- f = open("%s/WordPress_Single_Instance_gold.template" % self.path)
- t = json.loads(f.read())
- params = {}
- parameters = {}
- t['Parameters']['KeyName']['Value'] = 'test'
- stack = parser.Stack(ctx, 'test_stack_list', t, 0, params)
+ man = manager.EngineManager()
+ sl = man.show_stack(ctx, None, {})
+
+ self.assertEqual(len(sl['stacks']), 1)
+ for s in sl['stacks']:
+ self.assertNotEqual(s['StackId'], None)
+ self.assertNotEqual(s['Description'].find('WordPress'), -1)
+
+ def test_stack_describe_all_empty(self):
+ ctx = self.create_context('stack_describe_all_empty')
+ auth.authenticate(ctx).AndReturn(True)
+
+ self.m.ReplayAll()
man = manager.EngineManager()
- sl = man.list_stacks(ctx, params)
+ sl = man.show_stack(ctx, None, {})
+
+ self.assertEqual(len(sl['stacks']), 0)
+
+ def test_stack_describe_nonexistent(self):
+ ctx = self.create_context()
+ auth.authenticate(ctx).AndReturn(True)
+
+ self.m.ReplayAll()
+
+ man = manager.EngineManager()
+ sl = man.show_stack(ctx, 'wibble', {})
+
+ self.assertEqual(len(sl['stacks']), 0)
+
+ def test_stack_describe(self):
+ ctx = self.create_context('stack_describe')
+ auth.authenticate(ctx).AndReturn(True)
+
+ stack = self.get_wordpress_stack('test_stack_desc', ctx)
+
+ self.m.ReplayAll()
+ stack.store()
+ stack.create()
+
+ man = manager.EngineManager()
+ sl = man.show_stack(ctx, 'test_stack_desc', {})
self.assertTrue(len(sl['stacks']) > 0)
for s in sl['stacks']:
- self.assertTrue(s['StackId'] > 0)
- self.assertNotEqual(s['TemplateDescription'].find('WordPress'), -1)
+ self.assertEqual(s['StackName'], 'test_stack_desc')
+ self.assertTrue('CreationTime' in s)
+ self.assertNotEqual(s['StackId'], None)
+ self.assertNotEqual(s['Description'].find('WordPress'), -1)
# allows testing of the test directly
if __name__ == '__main__':
t = json.loads(test_template_volumeattach % 'vdq')
self.m.StubOutWithMock(auth, 'authenticate')
auth.authenticate(None).AndReturn(True)
- params = {}
- stack = parser.Stack(None, 'test_stack', t, 0, params)
+ stack = parser.Stack(None, 'test_stack', parser.Template(t))
self.m.StubOutWithMock(db_api, 'resource_get_by_name_and_stack')
db_api.resource_get_by_name_and_stack(None, 'test_resource_name',
self.m.ReplayAll()
volumeattach = stack.resources['MountPoint']
- assert(volumeattach.validate() is None)
+ self.assertTrue(volumeattach.validate() is None)
def test_validate_volumeattach_invalid(self):
t = json.loads(test_template_volumeattach % 'sda')
self.m.StubOutWithMock(auth, 'authenticate')
auth.authenticate(None).AndReturn(True)
- params = {}
- stack = parser.Stack(None, 'test_stack', t, 0, params)
+ stack = parser.Stack(None, 'test_stack', parser.Template(t))
self.m.StubOutWithMock(db_api, 'resource_get_by_name_and_stack')
db_api.resource_get_by_name_and_stack(None, 'test_resource_name',
import nose
import unittest
-import mox
from nose.plugins.attrib import attr
from nose import with_setup
import heat.db as db_api
from heat.engine import parser
+from heat.common import context
logger = logging.getLogger('test_waitcondition')
@attr(speed='slow')
class stacksTest(unittest.TestCase):
def setUp(self):
- self.m = mox.Mox()
self.greenpool = eventlet.GreenPool()
- def tearDown(self):
- self.m.UnsetStubs()
-
def create_stack(self, stack_name, temp, params):
- stack = parser.Stack(None, stack_name, temp, 0, params)
-
- rt = {}
- rt['template'] = temp
- rt['StackName'] = stack_name
- new_rt = db_api.raw_template_create(None, rt)
-
- ct = {'username': 'fred',
- 'password': 'mentions_fruit'}
- new_creds = db_api.user_creds_create(ct)
-
- s = {}
- s['name'] = stack_name
- s['raw_template_id'] = new_rt.id
- s['user_creds_id'] = new_creds.id
- s['username'] = ct['username']
- new_s = db_api.stack_create(None, s)
- stack.id = new_s.id
+ template = parser.Template(temp)
+ parameters = parser.Parameters(stack_name, template, params)
+ stack = parser.Stack(context.get_admin_context(), stack_name,
+ template, parameters)
+ stack.store()
return stack
def test_post_success_to_handle(self):
t = json.loads(test_template_waitcondition)
stack = self.create_stack('test_stack', t, params)
- self.m.ReplayAll()
self.greenpool.spawn_n(stack.create)
eventlet.sleep(1)
self.assertEqual(stack.resources['WaitForTheHandle'].state,