Move dbinstance into a TemplateResource

The old dbinstance will soon be overtaken by the trove resource.
- The only reason for keeping this around is for people that don't
  have trove installed and want to use the AWS resource.
- Seperating it out into a TemplateResource really makes it easier
  for deployers and users to customise it.
- The old dbinstance did nothing "special" in python, and was really
  one of the first "TemplateResources" as it attempted to convert properties
  into parameters. Since this in now done a lot better in the TemplateResource
  lets just make use of that.

This will make it easier to migrate to other distros and versions.

Partial-Bug: #1215797
Change-Id: If72e1f40f67dc831551e0db8df8caaa002aaaeda
This commit is contained in:
Angus Salkeld 2013-09-05 20:47:07 +10:00 committed by Steve Baker
parent 5fc4d75cf5
commit 13b7f54ad8
4 changed files with 162 additions and 304 deletions

View File

@ -6,3 +6,4 @@ resource_registry:
#"AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml" #"AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml"
"AWS::CloudWatch::Alarm": "OS::Heat::CWLiteAlarm" "AWS::CloudWatch::Alarm": "OS::Heat::CWLiteAlarm"
"OS::Metering::Alarm": "OS::Ceilometer::Alarm" "OS::Metering::Alarm": "OS::Ceilometer::Alarm"
"AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml"

View File

@ -0,0 +1,98 @@
HeatTemplateFormatVersion: '2012-12-12'
Description: 'Builtin AWS::RDS::DBInstance'
Parameters:
AllocatedStorage:
Type: String
DBInstanceClass:
Type: String
DBName:
Type: String
DBSecurityGroups:
Type: CommaDelimitedList
Default: ''
Engine:
Type: String
AllowedValues: ['MySQL']
MasterUsername:
Type: String
MasterUserPassword:
Type: String
Port:
Type: String
Default: '3306'
KeyName:
Type: String
Default: ''
Mappings:
DBInstanceToInstance:
db.m1.small: {Instance: m1.small}
db.m1.large: {Instance: m1.large}
db.m1.xlarge: {Instance: m1.xlarge}
db.m2.xlarge: {Instance: m2.xlarge}
db.m2.2xlarge: {Instance: m2.2xlarge}
db.m2.4xlarge: {Instance: m2.4xlarge}
Resources:
DatabaseInstance:
Type: AWS::EC2::Instance
Metadata:
AWS::CloudFormation::Init:
config:
packages:
yum:
mysql : []
mysql-server : []
services:
systemd:
mysqld:
enabled: true
ensureRunning: true
Properties:
ImageId: F17-x86_64-cfntools
InstanceType: {'Fn::FindInMap': [DBInstanceToInstance,
{Ref: DBInstanceClass}, Instance]}
KeyName: {Ref: KeyName}
UserData:
Fn::Base64:
Fn::Replace:
- 'AWS::StackName': {Ref: 'AWS::StackName'}
'AWS::Region': {Ref: 'AWS::Region'}
MasterUsername: {Ref: MasterUsername}
MasterUserPassword: {Ref: MasterUserPassword}
DBName: {Ref: DBName}
WaitHandle: {Ref: WaitHandle}
- |
#!/bin/bash -v
# Helper function
function error_exit
{
/opt/aws/bin/cfn-signal -e 1 -r \"$1\" 'WaitHandle'
exit 1
}
/opt/aws/bin/cfn-init -s AWS::StackName -r DatabaseInstance --region AWS::Region || error_exit 'Failed to run cfn-init'
# Setup MySQL root password and create a user
mysqladmin -u root password 'MasterUserPassword'
cat << EOF | mysql -u root --password='MasterUserPassword'
CREATE DATABASE DBName;
GRANT ALL PRIVILEGES ON DBName.* TO "MasterUsername"@"%"
IDENTIFIED BY "MasterUserPassword";
FLUSH PRIVILEGES;
EXIT
EOF
# Database setup completed, signal success
/opt/aws/bin/cfn-signal -e 0 -r "MySQL server setup complete" 'WaitHandle'
WaitHandle:
Type: AWS::CloudFormation::WaitConditionHandle
WaitCondition:
Type: AWS::CloudFormation::WaitCondition
DependsOn: DatabaseInstance
Properties:
Handle: {Ref: WaitHandle}
Timeout: "600"
Outputs:
Endpoint.Address: {'Fn::GetAtt': [DatabaseInstance, PublicIp]}
Endpoint.Port: {Ref: Port}

View File

@ -1,244 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from heat.common import template_format
from heat.engine import stack_resource
from heat.openstack.common import log as logging
logger = logging.getLogger(__name__)
mysql_template = r'''
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Builtin RDS::DBInstance",
"Parameters" : {
"DBInstanceClass" : {
"Type": "String"
},
"DBName" : {
"Type": "String"
},
"MasterUsername" : {
"Type": "String"
},
"MasterUserPassword" : {
"Type": "String"
},
"AllocatedStorage" : {
"Type": "String"
},
"DBSecurityGroups" : {
"Type": "CommaDelimitedList",
"Default": ""
},
"Port" : {
"Type": "String"
},
"KeyName" : {
"Type" : "String"
}
},
"Mappings" : {
"DBInstanceToInstance" : {
"db.m1.small": {"Instance": "m1.small"},
"db.m1.large": {"Instance": "m1.large"},
"db.m1.xlarge": {"Instance": "m1.xlarge"},
"db.m2.xlarge": {"Instance": "m2.xlarge"},
"db.m2.2xlarge": {"Instance": "m2.2xlarge"},
"db.m2.4xlarge": {"Instance": "m2.4xlarge"}
}
},
"Resources": {
"DatabaseInstance": {
"Type": "AWS::EC2::Instance",
"Metadata": {
"AWS::CloudFormation::Init": {
"config": {
"packages": {
"yum": {
"mysql" : [],
"mysql-server" : []
}
},
"services": {
"systemd": {
"mysqld" : { "enabled" : "true", "ensureRunning" : "true" }
}
}
}
}
},
"Properties": {
"ImageId": "F17-x86_64-cfntools",
"InstanceType": { "Fn::FindInMap": [ "DBInstanceToInstance",
{ "Ref": "DBInstanceClass" },
"Instance" ] },
"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 DatabaseInstance",
" --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":"MasterUserPassword"},"'\n",
"cat << EOF | mysql -u root --password='",
{ "Ref" : "MasterUserPassword" }, "'\n",
"CREATE DATABASE ", { "Ref" : "DBName" }, ";\n",
"GRANT ALL PRIVILEGES ON ", { "Ref" : "DBName" },
".* TO \"", { "Ref" : "MasterUsername" }, "\"@\"%\"\n",
"IDENTIFIED BY \"", { "Ref" : "MasterUserPassword" }, "\";\n",
"FLUSH PRIVILEGES;\n",
"EXIT\n",
"EOF\n",
"# Database setup completed, signal success\n",
"/opt/aws/bin/cfn-signal -e 0 -r \"MySQL server setup complete\" '",
{ "Ref" : "WaitHandle" }, "'\n"
]]}}
}
},
"WaitHandle" : {
"Type" : "AWS::CloudFormation::WaitConditionHandle"
},
"WaitCondition" : {
"Type" : "AWS::CloudFormation::WaitCondition",
"DependsOn" : "DatabaseInstance",
"Properties" : {
"Handle" : {"Ref" : "WaitHandle"},
"Timeout" : "600"
}
}
},
"Outputs": {
}
}
'''
class DBInstance(stack_resource.StackResource):
properties_schema = {
'DBSnapshotIdentifier': {'Type': 'String',
'Implemented': False},
'AllocatedStorage': {'Type': 'String',
'Required': True},
'AvailabilityZone': {'Type': 'String',
'Implemented': False},
'BackupRetentionPeriod': {'Type': 'String',
'Implemented': False},
'DBInstanceClass': {'Type': 'String',
'Required': True},
'DBName': {'Type': 'String',
'Required': False},
'DBParameterGroupName': {'Type': 'String',
'Implemented': False},
'DBSecurityGroups': {'Type': 'List',
'Required': False, 'Default': []},
'DBSubnetGroupName': {'Type': 'String',
'Implemented': False},
'Engine': {'Type': 'String',
'AllowedValues': ['MySQL'],
'Required': True},
'EngineVersion': {'Type': 'String',
'Implemented': False},
'LicenseModel': {'Type': 'String',
'Implemented': False},
'MasterUsername': {'Type': 'String',
'Required': True},
'MasterUserPassword': {'Type': 'String',
'Required': True},
'Port': {'Type': 'String',
'Default': '3306',
'Required': False},
'PreferredBackupWindow': {'Type': 'String',
'Implemented': False},
'PreferredMaintenanceWindow': {'Type': 'String',
'Implemented': False},
'MultiAZ': {'Type': 'Boolean',
'Implemented': False},
}
# We only support a couple of the attributes right now
attributes_schema = {
"Endpoint.Address": "Connection endpoint for the database.",
"Endpoint.Port": ("The port number on which the database accepts "
"connections.")
}
def _params(self):
params = {
'KeyName': {'Ref': 'KeyName'},
}
# Add the DBInstance parameters specified in the user's template
# Ignore the not implemented ones
for key, value in self.properties_schema.items():
if value.get('Implemented', True) and key != 'Engine':
# There is a mismatch between the properties "List" format
# and the parameters "CommaDelimitedList" format, so we need
# to translate lists into the expected comma-delimited form
if isinstance(self.properties[key], list):
params[key] = ','.join(self.properties[key])
else:
params[key] = self.properties[key]
p = self.stack.resolve_static_data(params)
return p
def handle_create(self):
templ = template_format.parse(mysql_template)
return self.create_with_template(templ, self._params())
def handle_delete(self):
return self.delete_nested()
def _resolve_attribute(self, name):
'''
We don't really support any of these yet.
'''
if name == 'Endpoint.Address':
if self.nested() and 'DatabaseInstance' in self.nested().resources:
return self.nested().resources['DatabaseInstance']._ipaddress()
else:
return '0.0.0.0'
elif name == 'Endpoint.Port':
return self.properties['Port']
def resource_mapping():
return {
'AWS::RDS::DBInstance': DBInstance,
}

View File

@ -12,15 +12,11 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import mox
from heat.common import exception
from heat.common import template_format from heat.common import template_format
from heat.engine import scheduler from heat.engine import resource
from heat.engine.resources import dbinstance as dbi
from heat.tests.common import HeatTestCase from heat.tests.common import HeatTestCase
from heat.tests import utils from heat.tests import utils
from heat.engine import parser
rds_template = ''' rds_template = '''
@ -52,64 +48,71 @@ rds_template = '''
''' '''
class DBInstance(resource.Resource):
"""This is copied from the old DBInstance
to verify the schema of the new TemplateResource.
"""
properties_schema = {
'DBSnapshotIdentifier': {'Type': 'String',
'Implemented': False},
'AllocatedStorage': {'Type': 'String',
'Required': True},
'AvailabilityZone': {'Type': 'String',
'Implemented': False},
'BackupRetentionPeriod': {'Type': 'String',
'Implemented': False},
'DBInstanceClass': {'Type': 'String',
'Required': True},
'DBName': {'Type': 'String',
'Required': False},
'DBParameterGroupName': {'Type': 'String',
'Implemented': False},
'DBSecurityGroups': {'Type': 'List',
'Required': False, 'Default': []},
'DBSubnetGroupName': {'Type': 'String',
'Implemented': False},
'Engine': {'Type': 'String',
'AllowedValues': ['MySQL'],
'Required': True},
'EngineVersion': {'Type': 'String',
'Implemented': False},
'LicenseModel': {'Type': 'String',
'Implemented': False},
'MasterUsername': {'Type': 'String',
'Required': True},
'MasterUserPassword': {'Type': 'String',
'Required': True},
'Port': {'Type': 'String',
'Default': '3306',
'Required': False},
'PreferredBackupWindow': {'Type': 'String',
'Implemented': False},
'PreferredMaintenanceWindow': {'Type': 'String',
'Implemented': False},
'MultiAZ': {'Type': 'Boolean',
'Implemented': False},
}
# We only support a couple of the attributes right now
attributes_schema = {
"Endpoint.Address": "Connection endpoint for the database.",
"Endpoint.Port": ("The port number on which the database accepts "
"connections.")
}
class DBInstanceTest(HeatTestCase): class DBInstanceTest(HeatTestCase):
def setUp(self): def setUp(self):
super(DBInstanceTest, self).setUp() super(DBInstanceTest, self).setUp()
utils.setup_dummy_db() utils.setup_dummy_db()
self.m.StubOutWithMock(dbi.DBInstance, 'create_with_template')
self.m.StubOutWithMock(dbi.DBInstance, 'check_create_complete')
self.m.StubOutWithMock(dbi.DBInstance, 'nested')
def create_dbinstance(self, t, stack, resource_name):
resource = dbi.DBInstance(resource_name,
t['Resources'][resource_name],
stack)
self.assertEqual(None, resource.validate())
scheduler.TaskRunner(resource.create)()
self.assertEqual((resource.CREATE, resource.COMPLETE), resource.state)
return resource
def test_dbinstance(self): def test_dbinstance(self):
"""test that the Template is parsable and
publishes the correct properties.
"""
templ = parser.Template(template_format.parse(rds_template))
stack = parser.Stack(utils.dummy_context(), 'test_stack',
templ)
class FakeDatabaseInstance(object): res = stack['DatabaseServer']
def _ipaddress(self): self.assertEquals(None, res._validate_against_facade(DBInstance))
return '10.0.0.1'
class FakeNested(object):
resources = {'DatabaseInstance': FakeDatabaseInstance()}
params = {'DBSecurityGroups': '',
'MasterUsername': u'admin',
'MasterUserPassword': u'admin',
'DBName': u'wordpress',
'KeyName': u'test',
'AllocatedStorage': u'5',
'DBInstanceClass': u'db.m1.small',
'Port': '3306'}
dbi.DBInstance.create_with_template(mox.IgnoreArg(),
params).AndReturn(None)
dbi.DBInstance.check_create_complete(mox.IgnoreArg()).AndReturn(True)
fn = FakeNested()
dbi.DBInstance.nested().AndReturn(None)
dbi.DBInstance.nested().MultipleTimes().AndReturn(fn)
self.m.ReplayAll()
t = template_format.parse(rds_template)
s = utils.parse_stack(t)
resource = self.create_dbinstance(t, s, 'DatabaseServer')
self.assertEqual('0.0.0.0', resource.FnGetAtt('Endpoint.Address'))
self.assertEqual('10.0.0.1', resource.FnGetAtt('Endpoint.Address'))
self.assertEqual('3306', resource.FnGetAtt('Endpoint.Port'))
try:
resource.FnGetAtt('foo')
except exception.InvalidTemplateAttribute:
pass
else:
raise Exception('Expected InvalidTemplateAttribute')
self.m.VerifyAll()