# Copyright (c) 2013 MetaMetrics, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the 'Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
"""Core functionality and all required components of a working CFN template.
These are all available without preamble in a pyplate's global namespace.
"""
import inspect
import json
import traceback
from collections import OrderedDict
from cfn_pyplates.exceptions import AddRemoveError, Error
import functions
aws_template_format_version = '2010-09-09'
__all__ = [
'JSONableDict',
'CloudFormationTemplate',
'Parameters',
'Mappings',
'Resources',
'Outputs',
'Conditions',
'Properties',
'Mapping',
'Resource',
'Parameter',
'Output',
'DependsOn',
'DeletionPolicy',
'UpdatePolicy',
'Metadata',
'Condition',
'ec2_tags',
]
[docs]class JSONableDict(OrderedDict):
"""A dictionary that knows how to turn itself into JSON
Args:
update_dict: A dictionary of values for prepopulating the JSONableDict
at instantiation
name: An optional name. If left out, the class's (or subclass's) name
will be used.
The most common use-case of any JSON entry in a CFN Template is the
``{"Name": {"Key1": "Value1", "Key2": Value2"} }`` pattern. The
significance of a JSONableDict's subclass name, or explicitly passing
a 'name' argument is accomodating this pattern. All JSONableDicts have
names.
To create the pyplate equivalent of the above JSON, contruct a
JSONableDict accordingly::
JSONableDict({'Key1': 'Value1', 'Key2', 'Value2'}, 'Name'})
Based on :class:`ordereddict.OrderedDict`, the order of keys is significant.
"""
def __init__(self, update_dict=None, name=None):
super(JSONableDict, self).__init__()
self._name = name
if update_dict:
self.update(update_dict)
def __unicode__(self):
# Indenting to keep things readable
# Trailing whitespace after commas removed
# (The space after colons is cool, though. He can stay.)
return unicode(self.json)
def __str__(self):
return unicode(self).encode('utf-8')
def __setattr__(self, name, value):
# This makes it simple to bind child dictionaries to an
# attribute while still making sure they wind up in the output
# dictionary, see usage example in CloudFormationTemplate init
if isinstance(value, JSONableDict):
self.add(value)
super(JSONableDict, self).__setattr__(name, value)
def __delattr__(self, name):
attr = getattr(self, name)
if isinstance(attr, JSONableDict):
try:
self.remove(attr)
except KeyError:
# Key already deleted, somehow.
# Everything's fine here now. How're you?
pass
super(JSONableDict, self).__delattr__(name)
def _get_name(self):
if self._name is not None:
return self._name
else:
# Default to the class name if _name is None
return self.__class__.__name__
def _set_name(self, name):
self._name = name
def _del_name(self):
self._name = None
name = property(_get_name, _set_name, _del_name)
"""Accessor to the ``name`` internals;
Allows getting, settings, and deleting the name
"""
@property
[docs] def json(self):
'Accessor to the canonical JSON representation of a JSONableDict'
return self.to_json(indent=2, separators=(',', ': '))
[docs] def add(self, child):
"""Add a child node
Args:
child: An instance of JSONableDict
Raises:
AddRemoveError: :exc:`cfn_pyplates.exceptions.AddRemoveError`
"""
if isinstance(child, JSONableDict):
self.update(
{child.name: child}
)
else:
raise AddRemoveError
return child
[docs] def remove(self, child):
"""Remove a child node
Args:
child: An instance of JSONableDict
Raises:
AddRemoveError: :exc:`cfn_pyplates.exceptions.AddRemoveError`
"""
if isinstance(child, JSONableDict):
del(self[child.name])
else:
raise AddRemoveError
[docs] def to_json(self, *args, **kwargs):
"""Thin wrapper around the :func:`json.dumps` method.
Allows for passing any arguments that json.dumps would accept to
completely customize the JSON output if desired.
"""
return json.dumps(self, *args, **kwargs)
[docs]class Parameters(JSONableDict):
"""The base Container for parameters used at stack creation
Attached to a :class:`cfn_pyplates.core.CloudFormationTemplate`
For more information, see `the AWS docs <cfn-parameters_>`_
"""
pass
[docs]class Mappings(JSONableDict):
"""The base Container for stack option mappings
.. note::
Since most lookups can be done inside a pyplate using python,
this is normally unused.
Attached to a :class:`cfn_pyplates.core.CloudFormationTemplate`
For more information, see `the AWS docs <cfn-mappings_>`_
"""
pass
[docs]class Resources(JSONableDict):
"""The base Container for stack resources
Attached to a :class:`cfn_pyplates.core.CloudFormationTemplate`
For more information, see `the AWS docs <cfn-resources_>`_
"""
pass
[docs]class Outputs(JSONableDict):
"""The base Container for stack outputs
Attached to a :class:`cfn_pyplates.core.CloudFormationTemplate`
For more information, see `the AWS docs <cfn-outputs_>`_
"""
pass
[docs]class Conditions(JSONableDict):
"""The base Container for stack conditions used at stack creation
Attached to a :class:`cfn_pyplates.core.CloudFormationTemplate`
For more information, see `the AWS docs <cfn-conditions_>`_
"""
pass
# Other 'named' JSONableDicts
[docs]class Properties(JSONableDict):
"""A properties mapping, used by various CFN declarations
Can be found in:
- :class:`cfn_pyplates.core.Parameters`
- :class:`cfn_pyplates.core.Outputs`
- :class:`cfn_pyplates.core.Resource`
Properties will be most commonly found in Resources
For more information, see `the AWS docs <cfn-properties_>`_
"""
pass
[docs]class Resource(JSONableDict):
"""A generic CFN Resource
Used in the :class:`cfn_pyplates.core.Resources` container.
All resources have a name, and most have a 'Type' and 'Properties' dict.
Thus, this class takes those as arguments and makes a generic resource.
The 'name' parameter must follow CFN's guidelines for naming
The 'type' parameter must be `one of these <cfn-resource-types_>`_
The optional 'properties' parameter is a dictionary of properties as
defined by the resource type, see documentation related to each resource
type
Args:
name: The unique name of the resource to add
type: The type of this resource
properties: Optional properties mapping to apply to this resource,
can be an instance of ``JSONableDict`` or just plain old ``dict``
attributes: Optional (one of 'Condition', 'DependsOn', 'DeletionPolicy',
'Metadata', 'UpdatePolicy' or a list of 2 or more)
For more information, see `the AWS docs <cfn-resources_>`_
"""
def __init__(self, name, type, properties=None, attributes=[]):
update_dict = {'Type': type}
super(Resource, self).__init__(update_dict, name)
if properties:
try:
# Assume we've got a JSONableDict
self.add(properties)
except AddRemoveError:
# If not, coerce it
self.add(Properties(properties))
if attributes:
if self._is_attribute(attributes):
self.add(attributes)
elif isinstance(attributes, list):
for i in attributes:
if isinstance(i, JSONableDict) and self._is_attribute(i):
self.add(i)
def _is_attribute(self, attribute):
"""Is the Object a valid Resource Attribute?
:param attribute: the object under test
"""
if isinstance(attribute, list):
for i in attribute:
self._is_attribute(i)
elif attribute.__class__.__name__ in ['Metadata', 'UpdatePolicy']:
self.add(attribute)
elif attribute.__class__.__name__ in ['DependsOn', 'DeletionPolicy', 'Condition']:
self.update({attribute.__class__.__name__: attribute.value})
[docs]class Parameter(JSONableDict):
"""A CFN Parameter
Used in the :class:`cfn_pyplates.core.Parameters` container, a Parameter
will be used when the template is processed by CloudFormation to prompt the
user for any additional input.
For more information, see `the AWS docs <cfn-parameters_>`_
Args:
name: The unique name of the parameter to add
type: The type of this parameter
properties: Optional properties mapping to apply to this parameter
"""
def __init__(self, name, type, properties=None):
# Just like a Resource, except the properties go in the
# update_dict, not a named key.
update_dict = {'Type': type}
if properties is not None:
update_dict.update(properties)
super(Parameter, self).__init__(update_dict, name)
[docs]class Mapping(JSONableDict):
"""A CFN Mapping
Used in the :class:`cfn_pyplates.core.Mappings` container, a Mapping
defines mappings used within the Cloudformation template and is not
the same as a PyPlates options mapping.
For more information, see `the AWS docs <cfn-mappings_>`_
Args:
name: The unique name of the mapping to add
mappings: The dictionary of mappings
"""
def __init__(self, name, mappings=None):
update_dict = {}
if mappings is not None:
update_dict.update(mappings)
super(Mapping, self).__init__(update_dict, name)
[docs]class Output(JSONableDict):
"""A CFN Output
Used in the :class:`cfn_pyplates.core.Outputs`, an Output entry describes
a value to be shown when describe this stack using CFN API tools.
For more information, see `the AWS docs <cfn-outputs_>`_
Args:
name: The unique name of the output
value: The value the output should return
description: An optional description of this output
"""
def __init__(self, name, value, description=None):
update_dict = {'Value': value}
if description is not None:
update_dict['Description'] = description
super(Output, self).__init__(update_dict, name)
[docs]class DependsOn(object):
"""A CFN Resource Dependency
Used in the :class:`cfn_pyplates.core.Resource`, The DependsOn attribute enables you to specify
that the creation of a specific resource follows another
For more information, see `the AWS docs <cfn-dependson_>`_
Args:
properties: The unique name of the output
"""
def __init__(self, policy=None):
if policy:
self.value = policy
[docs]class DeletionPolicy(object):
"""A CFN Resource Deletion Policy
Used in the :class:`cfn_pyplates.core.Resource`, The DeletionPolicy attribute enables you to
specify how AWS CloudFormation handles the resource deletion.
For more information, see `the AWS docs <cfn-deletionpolicy_>`_
Args:
properties: The unique name of the output
"""
def __init__(self, policy=None):
if policy:
self.value = str(policy)
[docs]class UpdatePolicy(JSONableDict):
"""A CFN Resource Update Policy
Used in the :class:`cfn_pyplates.core.Resource`, The UpdatePolicy attribute enables you to
specify how AWS CloudFormation handles rolling updates for a particular resource.
For more information, see `the AWS docs <cfn-updatepolicy_>`_
Args:
properties: The unique name of the output
"""
def __init__(self, properties=None):
super(UpdatePolicy, self).__init__(properties, 'UpdatePolicy')
[docs]class Condition(JSONableDict):
"""A CFN Condition Item
Used in the :class:`cfn_pyplates.core.Condition` container, a ConditionItem
will be used when the template is processed by CloudFormation so you can define
which resources are created and how they're configured for each environment type.
Conditions are made up of instrinsic functions for conditions found in
:mod:`cfn_pyplates.functions`, or a :func:`ref <cfn_pyplates.functions.ref>`
to a :class:`Parameter` or :class:`Mapping`.
For more information, see `the AWS docs <cfn-conditions_>`_
Args:
name: The unique name of the ConditionItem to add
type: The type of this parameter
properties: The Intrinsic Conditional function
"""
def __init__(self, name, condition):
super(Condition, self).__init__(condition, name)
self.value = name
def generate_pyplate(pyplate, options=None):
"""Generate CloudFormation JSON Template based on a Pyplate
Arguments:
pyplate
input pyplate file, can be a path or a file object
options
a mapping of some kind (probably a dict),
to be used at this pyplate's options mapping
Returns the output string of the compiled pyplate
"""
try:
if not isinstance(pyplate, file):
pyplate = open(pyplate)
pyplate = _load_pyplate(pyplate, options)
cft = _find_cloudformationtemplate(pyplate)
output = unicode(cft)
except Exception:
print 'Error processing the pyplate:'
print traceback.format_exc()
return None
return output
def _load_pyplate(pyplate, options_mapping=None):
'Load a pyplate file object, and return a dict of its globals'
# Inject all the useful stuff into the template namespace
exec_namespace = {
'options': options_mapping,
}
for entry in __all__:
exec_namespace[entry] = globals().get(entry)
for entry in functions.__all__:
exec_namespace[entry] = getattr(functions, entry)
# Do the needful.
exec pyplate in exec_namespace
return exec_namespace
def _find_cloudformationtemplate(pyplate):
"""Find a CloudFormationTemplate in a pyplate
Goes through a pyplate namespace dict and returns the first
CloudFormationTemplate it finds.
"""
for key, value in pyplate.iteritems():
if isinstance(value, CloudFormationTemplate):
return value
# If we haven't returned something, it's an Error
raise Error('No CloudFormationTemplate found in pyplate')