From: Steven Hardy Date: Tue, 5 Feb 2013 14:07:14 +0000 (+0000) Subject: Implement simple AccessPolicy Resource X-Git-Tag: 2014.1~918 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=46defc819b070a40d126ebb190cbb490467d944b;p=openstack-build%2Fheat-build.git 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 Change-Id: Idc98531388e535ce16308fd5aab5ceecda1de682 --- diff --git a/heat/api/aws/exception.py b/heat/api/aws/exception.py index 14c8258a..2a58212f 100644 --- a/heat/api/aws/exception.py +++ b/heat/api/aws/exception.py @@ -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) diff --git a/heat/engine/resources/user.py b/heat/engine/resources/user.py index 5eb23638..226a639b 100644 --- a/heat/engine/resources/user.py +++ b/heat/engine/resources/user.py @@ -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, } diff --git a/heat/engine/service.py b/heat/engine/service.py index 1d397986..66009e91 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -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) diff --git a/heat/tests/test_engine_service.py b/heat/tests/test_engine_service.py index ff9360c6..45ca0632 100644 --- a/heat/tests/test_engine_service.py +++ b/heat/tests/test_engine_service.py @@ -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, diff --git a/heat/tests/test_user.py b/heat/tests/test_user.py index d65e4320..335d741d 100644 --- a/heat/tests/test_user.py +++ b/heat/tests/test_user.py @@ -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)) diff --git a/templates/WordPress_Single_Instance_With_HA_AccessPolicy.template b/templates/WordPress_Single_Instance_With_HA_AccessPolicy.template new file mode 100644 index 00000000..082d9984 --- /dev/null +++ b/templates/WordPress_Single_Instance_With_HA_AccessPolicy.template @@ -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" + } + } +}