'WatchRuleNotFound',
'StackExists',
)
+ denied_errors = ('Forbidden', 'NotAuthorized')
if ex.exc_type in inval_param_errors:
return HeatInvalidParameterValueError(detail=ex.value)
+ elif ex.exc_type in denied_errors:
+ return HeatAccessDeniedError(detail=ex.value)
else:
# Map everything else to internal server error for now
return HeatInternalFailureError(detail=ex.value)
logger = logging.getLogger(__name__)
#
-# We are ignoring Policies and Groups as keystone does not support them.
-#
-# For now support users and accesskeys.
+# We are ignoring Groups as keystone does not support them.
+# For now support users and accesskeys,
+# We also now support a limited heat-native Policy implementation
#
def __init__(self, name, json_snippet, stack):
super(User, self).__init__(name, json_snippet, stack)
+ def _validate_policies(self, policies):
+ for policy in policies:
+ # When we support AWS IAM style policies, we will have to accept
+ # either a ref to an AWS::IAM::Policy defined in the stack, or
+ # and embedded dict describing the policy directly, but for now
+ # we only expect this list to contain strings, which must map
+ # to an OS::Heat::AccessPolicy in this stack
+ # If a non-string (e.g embedded IAM dict policy) is passed, we
+ # ignore the policy (don't reject it because we previously ignored
+ # and we don't want to break templates which previously worked
+ if not isinstance(policy, basestring):
+ logger.warning("Ignoring policy %s, " % policy
+ + "must be string resource name")
+ continue
+
+ try:
+ policy_rsrc = self.stack.resources[policy]
+ except KeyError:
+ logger.error("Policy %s does not exist in stack %s" %
+ (policy, self.stack.name))
+ return False
+
+ if not callable(getattr(policy_rsrc, 'access_allowed', None)):
+ logger.error("Policy %s is not an AccessPolicy resource" %
+ policy)
+ return False
+
+ return True
+
def handle_create(self):
passwd = ''
if self.properties['LoginProfile'] and \
'Password' in self.properties['LoginProfile']:
passwd = self.properties['LoginProfile']['Password']
+ if self.properties['Policies']:
+ if not self._validate_policies(self.properties['Policies']):
+ raise exception.InvalidTemplateAttribute(resource=self.name,
+ key='Policies')
+
uid = self.keystone().create_stack_user(self.physical_resource_name(),
passwd)
self.resource_id_set(uid)
raise exception.InvalidTemplateAttribute(
resource=self.physical_resource_name(), key=key)
+ def access_allowed(self, resource_name):
+ policies = self.properties['Policies']
+ for policy in policies:
+ if not isinstance(policy, basestring):
+ logger.warning("Ignoring policy %s, " % policy
+ + "must be string resource name")
+ continue
+ policy_rsrc = self.stack.resources[policy]
+ if not policy_rsrc.access_allowed(resource_name):
+ return False
+ return True
+
class AccessKey(resource.Resource):
properties_schema = {'Serial': {'Type': 'Integer',
super(AccessKey, self).__init__(name, json_snippet, stack)
self._secret = None
- def _get_userid(self):
+ def _get_user(self):
"""
Helper function to derive the keystone userid, which is stored in the
resource_id of the User associated with this key. We want to avoid
for r in self.stack.resources:
refid = self.stack.resources[r].FnGetRefId()
if refid == self.properties['UserName']:
- return self.stack.resources[r].resource_id
+ return self.stack.resources[r]
def handle_create(self):
- user_id = self._get_userid()
- if user_id is None:
+ try:
+ user_id = self._get_user().resource_id
+ except AttributeError:
raise exception.NotFound('could not find user %s' %
self.properties['UserName'])
def handle_delete(self):
self.resource_id_set(None)
self._secret = None
- user_id = self._get_userid()
+ user_id = self._get_user().resource_id
if user_id and self.resource_id:
self.keystone().delete_ec2_keypair(user_id, self.resource_id)
'''
Return the user's access key, fetching it from keystone if necessary
'''
- user_id = self._get_userid()
+ user_id = self._get_user().resource_id
if self._secret is None:
if not self.resource_id:
logger.warn('could not get secret for %s Error:%s' %
key, log_res))
return unicode(res)
+ def access_allowed(self, resource_name):
+ return self._get_user().access_allowed(resource_name)
+
+
+class AccessPolicy(resource.Resource):
+ properties_schema = {'AllowedResources': {'Type': 'List',
+ 'Required': True}}
+
+ def __init__(self, name, json_snippet, stack):
+ super(AccessPolicy, self).__init__(name, json_snippet, stack)
+
+ def handle_create(self):
+ resources = self.properties['AllowedResources']
+ # All of the provided resource names must exist in this stack
+ for resource in resources:
+ if resource not in self.stack:
+ logger.error("AccessPolicy resource %s not in stack" %
+ resource)
+ raise exception.ResourceNotFound(resource_name=resource,
+ stack_name=self.stack.name)
+
+ def handle_update(self, json_snippet):
+ return self.UPDATE_REPLACE
+
+ def access_allowed(self, resource_name):
+ return resource_name in self.properties['AllowedResources']
+
def resource_mapping():
return {
'AWS::IAM::User': User,
'AWS::IAM::AccessKey': AccessKey,
+ 'OS::Heat::AccessPolicy': AccessPolicy,
}
import functools
import webob
+import json
from heat.common import context
from heat.db import api as db_api
return [api.format_event(Event.load(context, e.id)) for e in events]
+ def _authorize_stack_user(self, context, stack, resource_name):
+ '''
+ Filter access to describe_stack_resource for stack in-instance users
+ - The user must map to a User resource defined in the requested stack
+ - The user resource must validate OK against any Policy specified
+ '''
+ # We're expecting EC2 credentials because all in-instance credentials
+ # are deployed as ec2 keypairs
+ try:
+ ec2_creds = json.loads(context.aws_creds).get('ec2Credentials')
+ except TypeError, AttributeError:
+ ec2_creds = None
+
+ if ec2_creds:
+ access_key = ec2_creds.get('access')
+ # Then we look up the AccessKey resource and check the stack
+ try:
+ akey_rsrc = self.find_physical_resource(context, access_key)
+ except exception.PhysicalResourceNotFound:
+ logger.warning("access_key % not found!" % access_key)
+ return False
+
+ akey_rsrc_id = identifier.ResourceIdentifier(**akey_rsrc)
+ if stack.identifier() == akey_rsrc_id.stack():
+ # The stack matches, so check if access is allowed to this
+ # resource via the AccessKey resource access_allowed()
+ ak_akey_rsrc = stack[akey_rsrc_id.resource_name]
+ return ak_akey_rsrc.access_allowed(resource_name)
+ else:
+ logger.warning("Cannot access resource from wrong stack!")
+ else:
+ logger.warning("Cannot access resource, invalid credentials!")
+
+ return False
+
@request_context
def describe_stack_resource(self, context, stack_identity, resource_name):
s = self._get_stack(context, stack_identity)
-
stack = parser.Stack.load(context, stack=s)
+
+ if cfg.CONF.heat_stack_user_role in context.roles:
+ if not self._authorize_stack_user(context, stack, resource_name):
+ logger.warning("Access denied to resource %s" % resource_name)
+ raise exception.Forbidden()
+
if resource_name not in stack:
raise exception.ResourceNotFound(resource_name=resource_name,
stack_name=stack.name)
from heat.engine.resources import instance as instances
from heat.engine import watchrule
from heat.openstack.common import threadgroup
+from heat.openstack.common import cfg
tests_dir = os.path.dirname(os.path.realpath(__file__))
ctx = create_context(m, cls.username, cls.tenant)
cls.stack_name = 'service_test_stack'
+ cfg.CONF.set_default('heat_stack_user_role', 'stack_user_role')
+
stack = get_wordpress_stack(cls.stack_name, ctx)
setup_mocks(m, stack)
self.man.describe_stack_resource,
self.ctx, self.stack_identity, 'foo')
+ def test_stack_resource_describe_stack_user_deny(self):
+ self.ctx.roles = [cfg.CONF.heat_stack_user_role]
+ self.m.StubOutWithMock(service.EngineService, '_authorize_stack_user')
+ service.EngineService._authorize_stack_user(self.ctx, mox.IgnoreArg(),
+ 'foo').AndReturn(False)
+ self.m.ReplayAll()
+ self.assertRaises(exception.Forbidden,
+ self.man.describe_stack_resource,
+ self.ctx, self.stack_identity, 'foo')
+
+ def test_stack_authorize_stack_user_nocreds(self):
+ self.assertFalse(self.man._authorize_stack_user(self.ctx,
+ self.stack_identity,
+ 'foo'))
+
def test_stack_resources_describe(self):
resources = self.man.describe_stack_resources(self.ctx,
self.stack_identity,
self.m.UnsetStubs()
print "UserTest teardown complete"
- def load_template(self):
+ def load_template(self, template_name='Rails_Single_Instance.template'):
self.path = os.path.dirname(os.path.realpath(__file__)).\
replace('heat/tests', 'templates')
- f = open("%s/Rails_Single_Instance.template" % self.path)
+ f = open("%s/%s" % (self.path, template_name))
t = template_format.parse(f.read())
f.close()
return t
self.assertEqual('DELETE_COMPLETE', resource.state)
self.m.VerifyAll()
+ def test_user_validate_policies(self):
+
+ self.m.StubOutWithMock(user.User, 'keystone')
+ user.User.keystone().MultipleTimes().AndReturn(self.fc)
+
+ self.m.ReplayAll()
+
+ tmpl = 'WordPress_Single_Instance_With_HA_AccessPolicy.template'
+ t = self.load_template(template_name=tmpl)
+ stack = self.parse_stack(t)
+
+ resource = self.create_user(t, stack, 'CfnUser')
+ self.assertEqual(self.fc.user_id, resource.resource_id)
+ self.assertEqual('test_stack.CfnUser', resource.FnGetRefId())
+ self.assertEqual('CREATE_COMPLETE', resource.state)
+
+ self.assertEqual([u'WebServerAccessPolicy'],
+ resource.properties['Policies'])
+
+ # OK
+ self.assertTrue(
+ resource._validate_policies([u'WebServerAccessPolicy']))
+
+ # Resource name doesn't exist in the stack
+ self.assertFalse(resource._validate_policies([u'NoExistAccessPolicy']))
+
+ # Resource name is wrong Resource type
+ self.assertFalse(resource._validate_policies([u'NoExistAccessPolicy',
+ u'WikiDatabase']))
+
+ # Wrong type (AWS embedded policy format, not yet supported)
+ dict_policy = {"PolicyName": "AccessForCFNInit",
+ "PolicyDocument":
+ {"Statement": [{"Effect": "Allow",
+ "Action":
+ "cloudformation:DescribeStackResource",
+ "Resource": "*"}]}}
+
+ # However we should just ignore it to avoid breaking existing templates
+ self.assertTrue(resource._validate_policies([dict_policy]))
+
+ self.m.VerifyAll()
+
+ def test_user_create_bad_policies(self):
+ self.m.ReplayAll()
+
+ tmpl = 'WordPress_Single_Instance_With_HA_AccessPolicy.template'
+ t = self.load_template(template_name=tmpl)
+ t['Resources']['CfnUser']['Properties']['Policies'] = ['NoExistBad']
+ stack = self.parse_stack(t)
+ resource_name = 'CfnUser'
+ resource = user.User(resource_name,
+ t['Resources'][resource_name],
+ stack)
+ self.assertRaises(exception.InvalidTemplateAttribute,
+ resource.handle_create)
+ self.m.VerifyAll()
+
+ def test_user_access_allowed(self):
+
+ self.m.StubOutWithMock(user.User, 'keystone')
+ user.User.keystone().MultipleTimes().AndReturn(self.fc)
+
+ self.m.StubOutWithMock(user.AccessPolicy, 'access_allowed')
+ user.AccessPolicy.access_allowed('a_resource').AndReturn(True)
+ user.AccessPolicy.access_allowed('b_resource').AndReturn(False)
+
+ self.m.ReplayAll()
+
+ tmpl = 'WordPress_Single_Instance_With_HA_AccessPolicy.template'
+ t = self.load_template(template_name=tmpl)
+ stack = self.parse_stack(t)
+
+ resource = self.create_user(t, stack, 'CfnUser')
+ self.assertEqual(self.fc.user_id, resource.resource_id)
+ self.assertEqual('test_stack.CfnUser', resource.FnGetRefId())
+ self.assertEqual('CREATE_COMPLETE', resource.state)
+
+ self.assertTrue(resource.access_allowed('a_resource'))
+ self.assertFalse(resource.access_allowed('b_resource'))
+ self.m.VerifyAll()
+
+ def test_user_access_allowed_ignorepolicy(self):
+
+ self.m.StubOutWithMock(user.User, 'keystone')
+ user.User.keystone().MultipleTimes().AndReturn(self.fc)
+
+ self.m.StubOutWithMock(user.AccessPolicy, 'access_allowed')
+ user.AccessPolicy.access_allowed('a_resource').AndReturn(True)
+ user.AccessPolicy.access_allowed('b_resource').AndReturn(False)
+
+ self.m.ReplayAll()
+
+ tmpl = 'WordPress_Single_Instance_With_HA_AccessPolicy.template'
+ t = self.load_template(template_name=tmpl)
+ t['Resources']['CfnUser']['Properties']['Policies'] = [
+ 'WebServerAccessPolicy', {'an_ignored': 'policy'}]
+ stack = self.parse_stack(t)
+
+ resource = self.create_user(t, stack, 'CfnUser')
+ self.assertEqual(self.fc.user_id, resource.resource_id)
+ self.assertEqual('test_stack.CfnUser', resource.FnGetRefId())
+ self.assertEqual('CREATE_COMPLETE', resource.state)
+
+ self.assertTrue(resource.access_allowed('a_resource'))
+ self.assertFalse(resource.access_allowed('b_resource'))
+ self.m.VerifyAll()
+
@attr(tag=['unit', 'resource', 'AccessKey'])
@attr(speed='fast')
resource._secret = None
self.assertEqual(resource.FnGetAtt('SecretAccessKey'),
self.fc.secret)
- try:
- resource.FnGetAtt('Foo')
- except exception.InvalidTemplateAttribute:
- pass
- else:
- raise Exception('Expected InvalidTemplateAttribute')
+ self.assertRaises(exception.InvalidTemplateAttribute,
+ resource.FnGetAtt, 'Foo')
self.assertEqual(None, resource.delete())
self.m.VerifyAll()
resource.state)
self.m.VerifyAll()
+
+
+@attr(tag=['unit', 'resource', 'AccessPolicy'])
+@attr(speed='fast')
+class AccessPolicyTest(unittest.TestCase):
+ def setUp(self):
+ self.m = mox.Mox()
+ self.fc = fakes.FakeKeystoneClient(username='test_stack.CfnUser')
+ cfg.CONF.set_default('heat_stack_user_role', 'stack_user_role')
+
+ def tearDown(self):
+ self.m.UnsetStubs()
+ print "UserTest teardown complete"
+
+ def load_template(self):
+ template_name =\
+ 'WordPress_Single_Instance_With_HA_AccessPolicy.template'
+ self.path = os.path.dirname(os.path.realpath(__file__)).\
+ replace('heat/tests', 'templates')
+ f = open("%s/%s" % (self.path, template_name))
+ t = template_format.parse(f.read())
+ f.close()
+ return t
+
+ def parse_stack(self, t):
+ ctx = context.RequestContext.from_dict({
+ 'tenant_id': 'test_tenant',
+ 'username': 'test_username',
+ 'password': 'password',
+ 'auth_url': 'http://localhost:5000/v2.0'})
+ template = parser.Template(t)
+ params = parser.Parameters('test_stack',
+ template,
+ {'KeyName': 'test',
+ 'DBRootPassword': 'test',
+ 'DBUsername': 'test',
+ 'DBPassword': 'test'})
+ stack = parser.Stack(ctx, 'test_stack', template, params)
+
+ return stack
+
+ def test_accesspolicy_create_ok(self):
+ t = self.load_template()
+ stack = self.parse_stack(t)
+
+ resource_name = 'WebServerAccessPolicy'
+ resource = user.AccessPolicy(resource_name,
+ t['Resources'][resource_name],
+ stack)
+ self.assertEqual(None, resource.create())
+ self.assertEqual(user.User.CREATE_COMPLETE, resource.state)
+
+ def test_accesspolicy_create_ok_empty(self):
+ t = self.load_template()
+ resource_name = 'WebServerAccessPolicy'
+ t['Resources'][resource_name]['Properties']['AllowedResources'] = []
+ stack = self.parse_stack(t)
+
+ resource = user.AccessPolicy(resource_name,
+ t['Resources'][resource_name],
+ stack)
+ self.assertEqual(None, resource.create())
+ self.assertEqual(user.User.CREATE_COMPLETE, resource.state)
+
+ def test_accesspolicy_create_err_notfound(self):
+ t = self.load_template()
+ resource_name = 'WebServerAccessPolicy'
+ t['Resources'][resource_name]['Properties']['AllowedResources'] = [
+ 'NoExistResource']
+ stack = self.parse_stack(t)
+
+ resource = user.AccessPolicy(resource_name,
+ t['Resources'][resource_name],
+ stack)
+ self.assertRaises(exception.ResourceNotFound, resource.handle_create)
+
+ def test_accesspolicy_update(self):
+ t = self.load_template()
+ resource_name = 'WebServerAccessPolicy'
+ stack = self.parse_stack(t)
+
+ resource = user.AccessPolicy(resource_name,
+ t['Resources'][resource_name],
+ stack)
+ self.assertEqual(user.AccessPolicy.UPDATE_REPLACE,
+ resource.handle_update({}))
+
+ def test_accesspolicy_access_allowed(self):
+ t = self.load_template()
+ resource_name = 'WebServerAccessPolicy'
+ stack = self.parse_stack(t)
+
+ resource = user.AccessPolicy(resource_name,
+ t['Resources'][resource_name],
+ stack)
+ self.assertTrue(resource.access_allowed('WikiDatabase'))
+ self.assertFalse(resource.access_allowed('NotWikiDatabase'))
+ self.assertFalse(resource.access_allowed(None))
--- /dev/null
+{
+ "AWSTemplateFormatVersion" : "2010-09-09",
+
+ "Description" : "AWS CloudFormation Sample Template WordPress_Multi_Instance: WordPress is web software you can use to create a beautiful website or blog. This template installs two instances: one running a WordPress deployment and the other using a local MySQL database to store the data.",
+
+ "Parameters" : {
+
+ "KeyName" : {
+ "Description" : "Name of an existing EC2 KeyPair to enable SSH access to the instances",
+ "Type" : "String"
+ },
+
+ "InstanceType" : {
+ "Description" : "WebServer EC2 instance type",
+ "Type" : "String",
+ "Default" : "m1.large",
+ "AllowedValues" : [ "t1.micro", "m1.small", "m1.large", "m1.xlarge", "m2.xlarge", "m2.2xlarge", "m2.4xlarge", "c1.medium", "c1.xlarge", "cc1.4xlarge" ],
+ "ConstraintDescription" : "must be a valid EC2 instance type."
+ },
+
+ "DBName": {
+ "Default": "wordpress",
+ "Description" : "The WordPress database name",
+ "Type": "String",
+ "MinLength": "1",
+ "MaxLength": "64",
+ "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9]*",
+ "ConstraintDescription" : "must begin with a letter and contain only alphanumeric characters."
+ },
+
+ "DBUsername": {
+ "Default": "admin",
+ "NoEcho": "true",
+ "Description" : "The WordPress database admin account username",
+ "Type": "String",
+ "MinLength": "1",
+ "MaxLength": "16",
+ "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9]*",
+ "ConstraintDescription" : "must begin with a letter and contain only alphanumeric characters."
+ },
+
+ "DBPassword": {
+ "Default": "admin",
+ "NoEcho": "true",
+ "Description" : "The WordPress database admin account password",
+ "Type": "String",
+ "MinLength": "1",
+ "MaxLength": "41",
+ "AllowedPattern" : "[a-zA-Z0-9]*",
+ "ConstraintDescription" : "must contain only alphanumeric characters."
+ },
+
+ "DBRootPassword": {
+ "Default": "admin",
+ "NoEcho": "true",
+ "Description" : "Root password for MySQL",
+ "Type": "String",
+ "MinLength": "1",
+ "MaxLength": "41",
+ "AllowedPattern" : "[a-zA-Z0-9]*",
+ "ConstraintDescription" : "must contain only alphanumeric characters."
+ },
+ "LinuxDistribution": {
+ "Default": "F17",
+ "Description" : "Distribution of choice",
+ "Type": "String",
+ "AllowedValues" : [ "F16", "F17", "U10", "RHEL-6.1", "RHEL-6.2", "RHEL-6.3" ]
+ },
+ "HupPollInterval": {
+ "Default": "1",
+ "Description" : "Interval for cfn-hup",
+ "Type": "String"
+ }
+ },
+
+ "Mappings" : {
+ "AWSInstanceType2Arch" : {
+ "t1.micro" : { "Arch" : "32" },
+ "m1.small" : { "Arch" : "32" },
+ "m1.large" : { "Arch" : "64" },
+ "m1.xlarge" : { "Arch" : "64" },
+ "m2.xlarge" : { "Arch" : "64" },
+ "m2.2xlarge" : { "Arch" : "64" },
+ "m2.4xlarge" : { "Arch" : "64" },
+ "c1.medium" : { "Arch" : "32" },
+ "c1.xlarge" : { "Arch" : "64" },
+ "cc1.4xlarge" : { "Arch" : "64" }
+ },
+ "DistroArch2AMI": {
+ "F16" : { "32" : "F16-i386-cfntools", "64" : "F16-x86_64-cfntools" },
+ "F17" : { "32" : "F17-i386-cfntools", "64" : "F17-x86_64-cfntools" },
+ "U10" : { "32" : "U10-i386-cfntools", "64" : "U10-x86_64-cfntools" },
+ "RHEL-6.1" : { "32" : "rhel61-i386-cfntools", "64" : "rhel61-x86_64-cfntools" },
+ "RHEL-6.2" : { "32" : "rhel62-i386-cfntools", "64" : "rhel62-x86_64-cfntools" },
+ "RHEL-6.3" : { "32" : "rhel63-i386-cfntools", "64" : "rhel63-x86_64-cfntools" }
+ }
+ },
+
+ "Resources" : {
+ "CfnUser" : {
+ "Type" : "AWS::IAM::User",
+ "Properties" : {
+ "Policies" : [ { "Ref": "WebServerAccessPolicy"} ]
+ }
+ },
+ "WebServerAccessPolicy" : {
+ "Type" : "OS::Heat::AccessPolicy",
+ "Properties" : {
+ "AllowedResources" : [ "WikiDatabase" ]
+ }
+ },
+ "WebServerKeys" : {
+ "Type" : "AWS::IAM::AccessKey",
+ "Properties" : {
+ "UserName" : {"Ref": "CfnUser"}
+ }
+ },
+ "WebServerRestartPolicy" : {
+ "Type" : "OS::Heat::HARestarter",
+ "Properties" : {
+ "InstanceId" : { "Ref" : "WikiDatabase" }
+ }
+ },
+ "HttpFailureAlarm": {
+ "Type": "AWS::CloudWatch::Alarm",
+ "Properties": {
+ "AlarmDescription": "Restart the WikiDatabase if httpd fails > 3 times in 10 minutes",
+ "MetricName": "ServiceFailure",
+ "Namespace": "system/linux",
+ "Statistic": "SampleCount",
+ "Period": "300",
+ "EvaluationPeriods": "1",
+ "Threshold": "2",
+ "AlarmActions": [ { "Ref": "WebServerRestartPolicy" } ],
+ "ComparisonOperator": "GreaterThanThreshold"
+ }
+ },
+ "WikiDatabase": {
+ "Type": "AWS::EC2::Instance",
+ "Metadata" : {
+ "AWS::CloudFormation::Init" : {
+ "config" : {
+ "files" : {
+ "/etc/cfn/cfn-credentials" : {
+ "content" : { "Fn::Join" : ["", [
+ "AWSAccessKeyId=", { "Ref" : "WebServerKeys" }, "\n",
+ "AWSSecretKey=", {"Fn::GetAtt": ["WebServerKeys",
+ "SecretAccessKey"]}, "\n"
+ ]]},
+ "mode" : "000400",
+ "owner" : "root",
+ "group" : "root"
+ },
+
+ "/etc/cfn/cfn-hup.conf" : {
+ "content" : { "Fn::Join" : ["", [
+ "[main]\n",
+ "stack=", { "Ref" : "AWS::StackName" }, "\n",
+ "credential-file=/etc/cfn/cfn-credentials\n",
+ "region=", { "Ref" : "AWS::Region" }, "\n",
+ "interval=", { "Ref" : "HupPollInterval" }, "\n"
+ ]]},
+ "mode" : "000400",
+ "owner" : "root",
+ "group" : "root"
+ },
+
+ "/etc/cfn/notify-on-httpd-restarted" : {
+ "content" : { "Fn::Join" : ["", [
+ "#!/bin/sh\n",
+ "/opt/aws/bin/cfn-push-stats --watch ",
+ { "Ref" : "HttpFailureAlarm" },
+ " --service-failure\n"
+ ]]},
+ "mode" : "000700",
+ "owner" : "root",
+ "group" : "root"
+ },
+
+ "/tmp/cfn-hup-crontab.txt" : {
+ "content" : { "Fn::Join" : ["", [
+ "MAIL=\"\"\n",
+ "\n",
+ "* * * * * /opt/aws/bin/cfn-hup -f\n"
+ ]]},
+ "mode" : "000600",
+ "owner" : "root",
+ "group" : "root"
+ },
+
+ "/tmp/setup.mysql" : {
+ "content" : { "Fn::Join" : ["", [
+ "CREATE DATABASE ", { "Ref" : "DBName" }, ";\n",
+ "GRANT ALL PRIVILEGES ON ", { "Ref" : "DBName" },
+ ".* TO '", { "Ref" : "DBUsername" }, "'@'localhost'\n",
+ "IDENTIFIED BY '", { "Ref" : "DBPassword" }, "';\n",
+ "FLUSH PRIVILEGES;\n",
+ "EXIT\n"
+ ]]},
+ "mode" : "000644",
+ "owner" : "root",
+ "group" : "root"
+ },
+
+ "/etc/cfn/hooks.conf" : {
+ "content": { "Fn::Join" : ["", [
+ "[cfn-http-restarted]\n",
+ "triggers=service.restarted\n",
+ "path=Resources.WikiDatabase.Metadata\n",
+ "action=/etc/cfn/notify-on-httpd-restarted\n",
+ "runas=root\n"
+ ]]},
+ "mode" : "000400",
+ "owner" : "root",
+ "group" : "root"
+ }
+ },
+ "packages" : {
+ "yum" : {
+ "cronie" : [],
+ "mysql" : [],
+ "mysql-server" : [],
+ "httpd" : [],
+ "wordpress" : []
+ }
+ },
+ "services" : {
+ "systemd" : {
+ "mysqld" : { "enabled" : "true", "ensureRunning" : "true" },
+ "httpd" : { "enabled" : "true", "ensureRunning" : "true" },
+ "crond" : { "enabled" : "true", "ensureRunning" : "true" }
+ }
+ }
+ }
+ }
+ },
+ "Properties": {
+ "ImageId" : { "Fn::FindInMap" : [ "DistroArch2AMI", { "Ref" : "LinuxDistribution" },
+ { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } ] },
+ "InstanceType" : { "Ref" : "InstanceType" },
+ "KeyName" : { "Ref" : "KeyName" },
+ "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
+ "#!/bin/bash -v\n",
+ "# Helper function\n",
+ "function error_exit\n",
+ "{\n",
+ " /opt/aws/bin/cfn-signal -e 1 -r \"$1\" '", { "Ref" : "WaitHandle" }, "'\n",
+ " exit 1\n",
+ "}\n",
+
+ "/opt/aws/bin/cfn-init -s ", { "Ref" : "AWS::StackName" },
+ " -r WikiDatabase ",
+ " --access-key ", { "Ref" : "WebServerKeys" },
+ " --secret-key ", {"Fn::GetAtt": ["WebServerKeys", "SecretAccessKey"]},
+ " --region ", { "Ref" : "AWS::Region" },
+ " || error_exit 'Failed to run cfn-init'\n",
+
+ "# Setup MySQL root password and create a user\n",
+ "mysqladmin -u root password '", { "Ref" : "DBRootPassword" },
+ "' || error_exit 'Failed to initialize root password'\n",
+
+ "mysql -u root --password='", { "Ref" : "DBRootPassword" },
+ "' < /tmp/setup.mysql || error_exit 'Failed to create database.'\n",
+
+ "sed --in-place --e s/database_name_here/", { "Ref" : "DBName" },
+ "/ --e s/username_here/", { "Ref" : "DBUsername" },
+ "/ --e s/password_here/", { "Ref" : "DBPassword" },
+ "/ /usr/share/wordpress/wp-config.php\n",
+
+ "# install cfn-hup crontab\n",
+ "crontab /tmp/cfn-hup-crontab.txt\n",
+
+ "# All is well so signal success\n",
+ "/opt/aws/bin/cfn-signal -e 0 -r \"Wiki server setup complete\" '",
+ { "Ref" : "WaitHandle" }, "'\n"
+ ]]}}
+ }
+ },
+
+ "WaitHandle" : {
+ "Type" : "AWS::CloudFormation::WaitConditionHandle"
+ },
+
+ "WaitCondition" : {
+ "Type" : "AWS::CloudFormation::WaitCondition",
+ "DependsOn" : "WikiDatabase",
+ "Properties" : {
+ "Handle" : {"Ref" : "WaitHandle"},
+ "Count" : "1",
+ "Timeout" : "600"
+ }
+ }
+ },
+
+ "Outputs" : {
+ "WebsiteURL" : {
+ "Value" : { "Fn::Join" : ["", ["http://", { "Fn::GetAtt" : [ "WikiDatabase", "PublicIp" ]}, "/wordpress"]] },
+ "Description" : "URL for Wordpress wiki"
+ }
+ }
+}