Implement simple AccessPolicy Resource

Implement a simple AccessPolicy resource, which can be used
to restrict in-instance users to specific resources when they
call the DescribeStackResource API action

Fixes bug 1115758

Signed-off-by: Steven Hardy <shardy@redhat.com>
Change-Id: Idc98531388e535ce16308fd5aab5ceecda1de682
This commit is contained in:
Steven Hardy 2013-02-05 14:07:14 +00:00
parent b85c442741
commit 46defc819b
6 changed files with 658 additions and 18 deletions

View File

@ -254,9 +254,12 @@ def map_remote_error(ex):
'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)

View File

@ -21,9 +21,9 @@ from heat.openstack.common import log as logging
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
#
@ -39,12 +39,46 @@ class User(resource.Resource):
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)
@ -66,6 +100,18 @@ class User(resource.Resource):
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',
@ -80,7 +126,7 @@ class AccessKey(resource.Resource):
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
@ -95,11 +141,12 @@ class AccessKey(resource.Resource):
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'])
@ -117,7 +164,7 @@ class AccessKey(resource.Resource):
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)
@ -125,7 +172,7 @@ class AccessKey(resource.Resource):
'''
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' %
@ -165,9 +212,37 @@ class AccessKey(resource.Resource):
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,
}

View File

@ -15,6 +15,7 @@
import functools
import webob
import json
from heat.common import context
from heat.db import api as db_api
@ -363,11 +364,51 @@ class EngineService(service.Service):
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)

View File

@ -31,6 +31,7 @@ from heat.engine import service
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__))
@ -368,6 +369,8 @@ class stackServiceTest(unittest.TestCase):
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)
@ -591,6 +594,21 @@ class stackServiceTest(unittest.TestCase):
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,

View File

@ -41,10 +41,10 @@ class UserTest(unittest.TestCase):
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
@ -111,6 +111,114 @@ class UserTest(unittest.TestCase):
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')
@ -187,13 +295,9 @@ class AccessKeyTest(unittest.TestCase):
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()
@ -216,3 +320,101 @@ class AccessKeyTest(unittest.TestCase):
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))

View File

@ -0,0 +1,301 @@
{
"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"
}
}
}