diff --git a/etc/heat/environment.d/default.yaml b/etc/heat/environment.d/default.yaml index 1f4c848a6b..dae1e37b21 100644 --- a/etc/heat/environment.d/default.yaml +++ b/etc/heat/environment.d/default.yaml @@ -6,3 +6,4 @@ resource_registry: #"AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml" "AWS::CloudWatch::Alarm": "OS::Heat::CWLiteAlarm" "OS::Metering::Alarm": "OS::Ceilometer::Alarm" + "AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml" diff --git a/etc/heat/templates/AWS_RDS_DBInstance.yaml b/etc/heat/templates/AWS_RDS_DBInstance.yaml new file mode 100644 index 0000000000..6f2dd6b980 --- /dev/null +++ b/etc/heat/templates/AWS_RDS_DBInstance.yaml @@ -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} diff --git a/heat/engine/resources/dbinstance.py b/heat/engine/resources/dbinstance.py deleted file mode 100644 index f602282167..0000000000 --- a/heat/engine/resources/dbinstance.py +++ /dev/null @@ -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, - } diff --git a/heat/tests/test_dbinstance.py b/heat/tests/test_dbinstance.py index fb88aa80bc..2eaa60f80a 100644 --- a/heat/tests/test_dbinstance.py +++ b/heat/tests/test_dbinstance.py @@ -12,15 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. - -import mox - -from heat.common import exception from heat.common import template_format -from heat.engine import scheduler -from heat.engine.resources import dbinstance as dbi +from heat.engine import resource from heat.tests.common import HeatTestCase from heat.tests import utils +from heat.engine import parser 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): def setUp(self): super(DBInstanceTest, self).setUp() 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): + """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): - def _ipaddress(self): - 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() + res = stack['DatabaseServer'] + self.assertEquals(None, res._validate_against_facade(DBInstance))