From 12118374bd215104d59bb3ca4546edf163a945d7 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 10 Jan 2015 13:50:37 -0500 Subject: [PATCH] Add database replicas. --- moto/rds/models.py | 78 ++++++++++++++++++++++++++++++++++++-- moto/rds/responses.py | 59 ++++++++++++++++++++-------- tests/test_rds/test_rds.py | 40 ++++++++++++++++++- 3 files changed, 157 insertions(+), 20 deletions(-) diff --git a/moto/rds/models.py b/moto/rds/models.py index b7cb8ef5a..60c009010 100644 --- a/moto/rds/models.py +++ b/moto/rds/models.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import copy + import boto.rds from jinja2 import Template @@ -11,6 +13,9 @@ class Database(object): def __init__(self, **kwargs): self.status = "available" + self.is_replica = False + self.replicas = [] + self.region = kwargs.get('region') self.engine = kwargs.get("engine") self.engine_version = kwargs.get("engine_version") @@ -21,6 +26,7 @@ class Database(object): 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.source_db_identifier = kwargs.get("source_db_identifier") self.db_instance_class = kwargs.get('db_instance_class') self.port = kwargs.get('port') self.db_instance_identifier = kwargs.get('db_instance_identifier') @@ -34,6 +40,10 @@ class Database(object): 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") + if self.db_subnet_group_name: + self.db_subnet_group = rds_backends[self.region].describe_subnet_groups(self.db_subnet_group_name)[0] + else: + self.db_subnet_group = [] self.security_groups = kwargs.get('security_groups', []) @@ -48,6 +58,16 @@ class Database(object): def address(self): return "{}.aaaaaaaaaa.{}.rds.amazonaws.com".format(self.db_instance_identifier, self.region) + def add_replica(self, replica): + self.replicas.append(replica.db_instance_identifier) + + def remove_replica(self, replica): + self.replicas.remove(replica.db_instance_identifier) + + def set_as_replica(self): + self.is_replica = True + self.replicas = [] + def update(self, db_kwargs): for key, value in db_kwargs.items(): if value is not None: @@ -62,7 +82,24 @@ class Database(object): {{ database.db_instance_identifier }} 03:50-04:20 wed:06:38-wed:07:08 - + + {% for replica_id in database.replicas %} + {{ replica_id }} + {% endfor %} + + + {% if database.is_replica %} + + read replication + replicating + true + + + {% endif %} + + {% if database.is_replica %} + {{ database.source_db_identifier }} + {% endif %} {{ database.engine }} general-public-license {{ database.engine_version }} @@ -78,6 +115,24 @@ class Database(object): {% endfor %} + + {{ database.db_subnet_group.subnet_name }} + {{ database.db_subnet_group.description }} + {{ database.db_subnet_group.status }} + + {% for subnet in database.db_subnet_group.subnets %} + + Active + {{ subnet.id }} + + {{ subnet.availability_zone }} + false + + + {% endfor %} + + {{ database.db_subnet_group.vpc_id }} + {{ database.publicly_accessible }} {{ database.auto_minor_version_upgrade }} {{ database.allocated_storage }} @@ -96,6 +151,7 @@ class SecurityGroup(object): self.group_name = group_name self.description = description self.ip_ranges = [] + self.status = "authorized" def to_xml(self): template = Template(""" @@ -123,13 +179,14 @@ class SubnetGroup(object): self.subnet_name = subnet_name self.description = description self.subnets = subnets + self.status = "Complete" self.vpc_id = self.subnets[0].vpc_id def to_xml(self): template = Template(""" {{ subnet_group.vpc_id }} - Complete + {{ subnet_group.status }} {{ subnet_group.description }} {{ subnet_group.subnet_name }} @@ -161,6 +218,17 @@ class RDSBackend(BaseBackend): self.databases[database_id] = database return database + def create_database_replica(self, db_kwargs): + database_id = db_kwargs['db_instance_identifier'] + source_database_id = db_kwargs['source_db_identifier'] + primary = self.describe_databases(source_database_id)[0] + replica = copy.deepcopy(primary) + replica.update(db_kwargs) + replica.set_as_replica() + self.databases[database_id] = replica + primary.add_replica(replica) + return replica + def describe_databases(self, db_instance_identifier=None): if db_instance_identifier: if db_instance_identifier in self.databases: @@ -176,7 +244,11 @@ class RDSBackend(BaseBackend): def delete_database(self, db_instance_identifier): if db_instance_identifier in self.databases: - return self.databases.pop(db_instance_identifier) + database = self.databases.pop(db_instance_identifier) + if database.is_replica: + primary = self.describe_databases(database.source_db_identifier)[0] + primary.remove_replica(database) + return database else: raise DBInstanceNotFoundError(db_instance_identifier) diff --git a/moto/rds/responses.py b/moto/rds/responses.py index b115b60a5..98015e7bb 100644 --- a/moto/rds/responses.py +++ b/moto/rds/responses.py @@ -13,34 +13,45 @@ class RDSResponse(BaseResponse): def _get_db_kwargs(self): return { - "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'), + "availability_zone": self._get_param("AvailabilityZone"), + "backup_retention_period": self._get_param("BackupRetentionPeriod"), "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"), - + # DBParameterGroupName + "db_subnet_group_name": self._get_param("DBSubnetGroupName"), + "engine": self._get_param("Engine"), + "engine_version": self._get_param("EngineVersion"), + "iops": self._get_int_param("Iops"), + "master_password": self._get_param('MasterUserPassword'), + "master_username": self._get_param('MasterUsername'), + "multi_az": self._get_bool_param("MultiAZ"), + # OptionGroupName + "port": self._get_param('Port'), # PreferredBackupWindow # PreferredMaintenanceWindow - "backup_retention_period": self._get_param("BackupRetentionPeriod"), - - # OptionGroupName - # DBParameterGroupName + "publicly_accessible": self._get_param("PubliclyAccessible"), + "region": self.region, "security_groups": self._get_multi_param('DBSecurityGroups.member'), + "storage_type": self._get_param("StorageType"), # VpcSecurityGroupIds.member.N + } + def _get_db_replica_kwargs(self): + return { + "auto_minor_version_upgrade": self._get_param('AutoMinorVersionUpgrade'), "availability_zone": self._get_param("AvailabilityZone"), - "multi_az": self._get_bool_param("MultiAZ"), + "db_instance_class": self._get_param('DBInstanceClass'), + "db_instance_identifier": self._get_param('DBInstanceIdentifier'), "db_subnet_group_name": self._get_param("DBSubnetGroupName"), + "iops": self._get_int_param("Iops"), + # OptionGroupName + "port": self._get_param('Port'), + "publicly_accessible": self._get_param("PubliclyAccessible"), + "source_db_identifier": self._get_param('SourceDBInstanceIdentifier'), + "storage_type": self._get_param("StorageType"), } def create_dbinstance(self): @@ -50,6 +61,13 @@ class RDSResponse(BaseResponse): template = self.response_template(CREATE_DATABASE_TEMPLATE) return template.render(database=database) + def create_dbinstance_read_replica(self): + db_kwargs = self._get_db_replica_kwargs() + + database = self.backend.create_database_replica(db_kwargs) + template = self.response_template(CREATE_DATABASE_REPLICA_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) @@ -126,6 +144,15 @@ CREATE_DATABASE_TEMPLATE = """ + + {{ database.to_xml() }} + + + ba8dedf0-bb9a-11d3-855b-576787000e19 + +""" + DESCRIBE_DATABASES_TEMPLATE = """ diff --git a/tests/test_rds/test_rds.py b/tests/test_rds/test_rds.py index a4c758953..02c644103 100644 --- a/tests/test_rds/test_rds.py +++ b/tests/test_rds/test_rds.py @@ -190,4 +190,42 @@ def test_delete_database_subnet_group(): conn.delete_db_subnet_group.when.called_with("db_subnet1").should.throw(BotoServerError) -# TODO incorporate subnet groups with actual DBs + +@mock_ec2 +@mock_rds +def test_create_database_in_subnet_group(): + vpc_conn = boto.vpc.connect_to_region("us-west-2") + vpc = vpc_conn.create_vpc("10.0.0.0/16") + subnet = vpc_conn.create_subnet(vpc.id, "10.1.0.0/24") + + conn = boto.rds.connect_to_region("us-west-2") + conn.create_db_subnet_group("db_subnet1", "my db subnet", [subnet.id]) + + database = conn.create_dbinstance("db-master-1", 10, 'db.m1.small', + 'root', 'hunter2', db_subnet_group_name="db_subnet1") + + database = conn.get_all_dbinstances("db-master-1")[0] + database.subnet_group.name.should.equal("db_subnet1") + + +@mock_rds +def test_create_database_replica(): + conn = boto.rds.connect_to_region("us-west-2") + + primary = conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2') + + replica = conn.create_dbinstance_read_replica("replica", "db-master-1", "db.m1.small") + replica.id.should.equal("replica") + replica.instance_class.should.equal("db.m1.small") + status_info = replica.status_infos[0] + status_info.normal.should.equal(True) + status_info.status_type.should.equal('read replication') + status_info.status.should.equal('replicating') + + primary = conn.get_all_dbinstances("db-master-1")[0] + primary.read_replica_dbinstance_identifiers[0].should.equal("replica") + + conn.delete_dbinstance("replica") + + primary = conn.get_all_dbinstances("db-master-1")[0] + list(primary.read_replica_dbinstance_identifiers).should.have.length_of(0)