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:
parent
b85c442741
commit
46defc819b
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue