From dbe3eb5459e4263c1c6f402329c0039010a1e4f3 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 8 Jan 2015 22:18:06 -0500 Subject: [PATCH] Add database CRUD. --- moto/__init__.py | 1 + moto/backends.py | 2 + moto/rds/__init__.py | 12 ++++ moto/rds/exceptions.py | 24 +++++++ moto/rds/models.py | 114 ++++++++++++++++++++++++++++++++++ moto/rds/responses.py | 91 +++++++++++++++++++++++++++ moto/rds/urls.py | 10 +++ tests/test_rds/test_rds.py | 62 ++++++++++++++++++ tests/test_rds/test_server.py | 20 ++++++ 9 files changed, 336 insertions(+) create mode 100644 moto/rds/__init__.py create mode 100644 moto/rds/exceptions.py create mode 100644 moto/rds/models.py create mode 100644 moto/rds/responses.py create mode 100644 moto/rds/urls.py create mode 100644 tests/test_rds/test_rds.py create mode 100644 tests/test_rds/test_server.py diff --git a/moto/__init__.py b/moto/__init__.py index 8041f0856..965eaf4ee 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -12,6 +12,7 @@ from .elb import mock_elb # flake8: noqa from .emr import mock_emr # flake8: noqa from .iam import mock_iam # flake8: noqa from .kinesis import mock_kinesis # flake8: noqa +from .rds import mock_rds # flake8: noqa from .redshift import mock_redshift # flake8: noqa from .s3 import mock_s3 # flake8: noqa from .s3bucket_path import mock_s3bucket_path # flake8: noqa diff --git a/moto/backends.py b/moto/backends.py index cf6759d99..460ac028f 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -7,6 +7,7 @@ from moto.ec2 import ec2_backend from moto.elb import elb_backend from moto.emr import emr_backend from moto.kinesis import kinesis_backend +from moto.rds import rds_backend from moto.redshift import redshift_backend from moto.s3 import s3_backend from moto.s3bucket_path import s3bucket_path_backend @@ -25,6 +26,7 @@ BACKENDS = { 'emr': emr_backend, 'kinesis': kinesis_backend, 'redshift': redshift_backend, + 'rds': rds_backend, 's3': s3_backend, 's3bucket_path': s3bucket_path_backend, 'ses': ses_backend, diff --git a/moto/rds/__init__.py b/moto/rds/__init__.py new file mode 100644 index 000000000..407f1680c --- /dev/null +++ b/moto/rds/__init__.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +from .models import rds_backends +from ..core.models import MockAWS + +rds_backend = rds_backends['us-east-1'] + + +def mock_rds(func=None): + if func: + return MockAWS(rds_backends)(func) + else: + return MockAWS(rds_backends) diff --git a/moto/rds/exceptions.py b/moto/rds/exceptions.py new file mode 100644 index 000000000..487162a8a --- /dev/null +++ b/moto/rds/exceptions.py @@ -0,0 +1,24 @@ +from __future__ import unicode_literals + +import json +from werkzeug.exceptions import BadRequest + + +class RDSClientError(BadRequest): + def __init__(self, code, message): + super(RDSClientError, self).__init__() + self.description = json.dumps({ + "Error": { + "Code": code, + "Message": message, + 'Type': 'Sender', + }, + 'RequestId': '6876f774-7273-11e4-85dc-39e55ca848d1', + }) + + +class DBInstanceNotFoundError(RDSClientError): + def __init__(self, database_identifier): + super(DBInstanceNotFoundError, self).__init__( + 'DBInstanceNotFound', + "Database {0} not found.".format(database_identifier)) diff --git a/moto/rds/models.py b/moto/rds/models.py new file mode 100644 index 000000000..27f4d10aa --- /dev/null +++ b/moto/rds/models.py @@ -0,0 +1,114 @@ +from __future__ import unicode_literals + +import boto.rds +from jinja2 import Template + +from moto.core import BaseBackend +from .exceptions import DBInstanceNotFoundError + + +class Database(object): + def __init__(self, **kwargs): + self.status = "available" + + self.region = kwargs.get('region') + self.engine = kwargs.get("engine") + self.engine_version = kwargs.get("engine_version") + self.iops = kwargs.get("iops") + self.storage_type = kwargs.get("storage_type") + self.master_username = kwargs.get('master_username') + self.master_password = kwargs.get('master_password') + self.auto_minor_version_upgrade = kwargs.get('auto_minor_version_upgrade') + self.allocated_storage = kwargs.get('allocated_storage') + self.db_instance_identifier = kwargs.get('db_instance_identifier') + self.db_instance_class = kwargs.get('db_instance_class') + self.port = kwargs.get('port') + self.db_instance_identifier = kwargs.get('db_instance_identifier') + self.db_name = kwargs.get("db_name") + self.publicly_accessible = kwargs.get("publicly_accessible") + + self.backup_retention_period = kwargs.get("backup_retention_period") + if self.backup_retention_period is None: + self.backup_retention_period = 1 + + self.availability_zone = kwargs.get("availability_zone") + self.multi_az = kwargs.get("multi_az") + self.db_subnet_group_name = kwargs.get("db_subnet_group_name") + + # PreferredBackupWindow + # PreferredMaintenanceWindow + # backup_retention_period = self._get_param("BackupRetentionPeriod") + # OptionGroupName + # DBParameterGroupName + # DBSecurityGroups.member.N + # VpcSecurityGroupIds.member.N + + @property + def address(self): + return "{}.aaaaaaaaaa.{}.rds.amazonaws.com".format(self.db_instance_identifier, self.region) + + def to_xml(self): + template = Template(""" + {{ database.backup_retention_period }} + {{ database.status }} + {{ database.multi_az }} + + {{ database.db_instance_identifier }} + 03:50-04:20 + wed:06:38-wed:07:08 + + {{ database.engine }} + general-public-license + {{ database.engine_version }} + + + + + + + active + default + + + {{ database.publicly_accessible }} + {{ database.auto_minor_version_upgrade }} + {{ database.allocated_storage }} + {{ database.db_instance_class }} + {{ database.master_username }} + +
{{ database.address }}
+ {{ database.port }} +
+
""") + return template.render(database=self) + + +class RDSBackend(BaseBackend): + + def __init__(self): + self.databases = {} + + def create_database(self, db_kwargs): + database_id = db_kwargs['db_instance_identifier'] + database = Database(**db_kwargs) + self.databases[database_id] = database + return database + + def describe_databases(self, db_instance_identifier=None): + if db_instance_identifier: + if db_instance_identifier in self.databases: + return [self.databases[db_instance_identifier]] + else: + raise DBInstanceNotFoundError(db_instance_identifier) + return self.databases.values() + + def delete_database(self, db_instance_identifier): + if db_instance_identifier in self.databases: + return self.databases.pop(db_instance_identifier) + else: + raise DBInstanceNotFoundError(db_instance_identifier) + + +rds_backends = {} +for region in boto.rds.regions(): + rds_backends[region.name] = RDSBackend() diff --git a/moto/rds/responses.py b/moto/rds/responses.py new file mode 100644 index 000000000..c6ed9707b --- /dev/null +++ b/moto/rds/responses.py @@ -0,0 +1,91 @@ +from __future__ import unicode_literals + +from moto.core.responses import BaseResponse +from .models import rds_backends + + +class RDSResponse(BaseResponse): + + @property + def backend(self): + return rds_backends[self.region] + + def create_dbinstance(self): + db_kwargs = { + "engine": self._get_param("Engine"), + "engine_version": self._get_param("EngineVersion"), + "region": self.region, + "iops": self._get_int_param("Iops"), + "storage_type": self._get_param("StorageType"), + + "master_username": self._get_param('MasterUsername'), + "master_password": self._get_param('MasterUserPassword'), + "auto_minor_version_upgrade": self._get_param('AutoMinorVersionUpgrade'), + "allocated_storage": self._get_int_param('AllocatedStorage'), + "db_instance_class": self._get_param('DBInstanceClass'), + "port": self._get_param('Port'), + "db_instance_identifier": self._get_param('DBInstanceIdentifier'), + "db_name": self._get_param("DBName"), + "publicly_accessible": self._get_param("PubliclyAccessible"), + + # PreferredBackupWindow + # PreferredMaintenanceWindow + "backup_retention_period": self._get_param("BackupRetentionPeriod"), + + # OptionGroupName + # DBParameterGroupName + # DBSecurityGroups.member.N + # VpcSecurityGroupIds.member.N + + "availability_zone": self._get_param("AvailabilityZone"), + "multi_az": self._get_bool_param("MultiAZ"), + "db_subnet_group_name": self._get_param("DBSubnetGroupName"), + } + + database = self.backend.create_database(db_kwargs) + template = self.response_template(CREATE_DATABASE_TEMPLATE) + return template.render(database=database) + + def describe_dbinstances(self): + db_instance_identifier = self._get_param('DBInstanceIdentifier') + databases = self.backend.describe_databases(db_instance_identifier) + template = self.response_template(DESCRIBE_DATABASES_TEMPLATE) + return template.render(databases=databases) + + def delete_dbinstance(self): + db_instance_identifier = self._get_param('DBInstanceIdentifier') + database = self.backend.delete_database(db_instance_identifier) + template = self.response_template(DELETE_DATABASE_TEMPLATE) + return template.render(database=database) + + +CREATE_DATABASE_TEMPLATE = """ + + {{ database.to_xml() }} + + + 523e3218-afc7-11c3-90f5-f90431260ab4 + +""" + +DESCRIBE_DATABASES_TEMPLATE = """ + + + {% for database in databases %} + {{ database.to_xml() }} + {% endfor %} + + + + 01b2685a-b978-11d3-f272-7cd6cce12cc5 + +""" + +DELETE_DATABASE_TEMPLATE = """ + + {{ database.to_xml() }} + + + 7369556f-b70d-11c3-faca-6ba18376ea1b + +""" diff --git a/moto/rds/urls.py b/moto/rds/urls.py new file mode 100644 index 000000000..e2e5b86ce --- /dev/null +++ b/moto/rds/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from .responses import RDSResponse + +url_bases = [ + "https?://rds.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': RDSResponse().dispatch, +} diff --git a/tests/test_rds/test_rds.py b/tests/test_rds/test_rds.py new file mode 100644 index 000000000..518f698c4 --- /dev/null +++ b/tests/test_rds/test_rds.py @@ -0,0 +1,62 @@ +from __future__ import unicode_literals + +import boto.rds +from boto.exception import BotoServerError +import sure # noqa + +from moto import mock_rds + + +@mock_rds +def test_create_database(): + conn = boto.rds.connect_to_region("us-west-2") + + database = conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2') + + database.status.should.equal('available') + database.id.should.equal("db-master-1") + database.allocated_storage.should.equal(10) + database.instance_class.should.equal("db.m1.small") + database.master_username.should.equal("root") + database.endpoint.should.equal(('db-master-1.aaaaaaaaaa.us-west-2.rds.amazonaws.com', 3306)) + + +@mock_rds +def test_get_databases(): + conn = boto.rds.connect_to_region("us-west-2") + + list(conn.get_all_dbinstances()).should.have.length_of(0) + + conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2') + conn.create_dbinstance("db-master-2", 10, 'db.m1.small', 'root', 'hunter2') + + list(conn.get_all_dbinstances()).should.have.length_of(2) + + databases = conn.get_all_dbinstances("db-master-1") + list(databases).should.have.length_of(1) + + databases[0].id.should.equal("db-master-1") + + +@mock_rds +def test_describe_non_existant_database(): + conn = boto.rds.connect_to_region("us-west-2") + conn.get_all_dbinstances.when.called_with("not-a-db").should.throw(BotoServerError) + + +@mock_rds +def test_delete_database(): + conn = boto.rds.connect_to_region("us-west-2") + list(conn.get_all_dbinstances()).should.have.length_of(0) + + conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2') + list(conn.get_all_dbinstances()).should.have.length_of(1) + + conn.delete_dbinstance("db-master-1") + list(conn.get_all_dbinstances()).should.have.length_of(0) + + +@mock_rds +def test_delete_non_existant_database(): + conn = boto.rds.connect_to_region("us-west-2") + conn.delete_dbinstance.when.called_with("not-a-db").should.throw(BotoServerError) diff --git a/tests/test_rds/test_server.py b/tests/test_rds/test_server.py new file mode 100644 index 000000000..224704a0b --- /dev/null +++ b/tests/test_rds/test_server.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +import sure # noqa + +import moto.server as server +from moto import mock_rds + +''' +Test the different server responses +''' + + +@mock_rds +def test_list_databases(): + backend = server.create_backend_app("rds") + test_client = backend.test_client() + + res = test_client.get('/?Action=DescribeDBInstances') + + res.data.decode("utf-8").should.contain("")