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("")