From ecf77d64cdf62eaee6ce108ff2e4e3ef3b0892d4 Mon Sep 17 00:00:00 2001 From: rocky4570fft Date: Sun, 13 Aug 2017 11:52:27 +1000 Subject: [PATCH 01/10] add rds stop-start --- moto/rds2/models.py | 31 +++++++++++++++++++++++++++++++ moto/rds2/responses.py | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 5abd2ed1b..6ae0c98a0 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -733,6 +733,37 @@ class RDS2Backend(BaseBackend): database = self.describe_databases(db_instance_identifier)[0] return database + def _check_can_stop_rds_instance_(self, database=None): + # todo: certain rds types not allowed to be stopped at this time. + if database: + if database.is_replica or database.multi_az: + # should be 400 error + return RDSClientError('InvalidDBClusterStateFault', 'Invalid DB type, when trying to perform StopDBInstance. See AWS RDS documentation on rds.stop_db_instance') + return True + + def stop_database(self, db_instance_identifier, snapshot_name=None): + database = self.describe_databases(db_instance_identifier)[0] + self._check_can_stop_rds_instance_(database) + if database.status != 'available': + return RDSClientError('InvalidDBInstanceState', 'when calling the StopDBInstance operation: Instance testdb is not in available state') + self.create_rds_snapshot(db_instance_identifier, db_instance_identifier) + database.status = 'shutdown' + return database + + def start_database(self, db_instance_identifier): + database = self.describe_databases(db_instance_identifier)[0] + if database.status != 'shutdown': + # should be 400 error + return RDSClientError('InvalidDBInstanceState', 'when calling the StartDBInstance operation: Instance {} is not stopped, it cannot be started.' % db_instance_identifier) + database.status = 'available' + return + + def create_rds_snapshot(self, db_instance_identifier, db_snapshot_identifier): + # todo + # DBSnapshotAlreadyExists + # SnapshotQuotaExceeded + return None + def find_db_from_id(self, db_id): if self.arn_regex.match(db_id): arn_breakdown = db_id.split(':') diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index ef02bfbf1..d8108c7f8 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -23,6 +23,7 @@ class RDS2Response(BaseResponse): "db_instance_identifier": self._get_param('DBInstanceIdentifier'), "db_name": self._get_param("DBName"), "db_parameter_group_name": self._get_param("DBParameterGroupName"), + "db_snapshot_identifier": self._get_param('DBSnapshotIdentifier'), "db_subnet_group_name": self._get_param("DBSubnetGroupName"), "engine": self._get_param("Engine"), "engine_version": self._get_param("EngineVersion"), @@ -193,6 +194,19 @@ class RDS2Response(BaseResponse): template = self.response_template(REMOVE_TAGS_FROM_RESOURCE_TEMPLATE) return template.render() + def stop_db_instance(self): + db_instance_identifier = self._get_param('DBInstanceIdentifier') + db_snapshot_identifier = self._get_param('DBInstanceIdentifier') + database = self.backend.stop_database(db_instance_identifier, db_snapshot_identifier) + template = self.response_template(STOP_DATABASE_TEMPLATE) + return template.render(database=database) + + def start_db_instance(self): + db_instance_identifier = self._get_param('DBInstanceIdentifier') + database = self.backend.start_database(db_instance_identifier) + template = self.response_template(START_DATABASE_TEMPLATE) + return template.render(database=database) + def create_db_security_group(self): group_name = self._get_param('DBSecurityGroupName') description = self._get_param('DBSecurityGroupDescription') @@ -410,8 +424,25 @@ REBOOT_DATABASE_TEMPLATE = """ + + {{ database.to_xml() }} + + + 523e3218-afc7-11c3-90f5-f90431260ab9 + +""" -DELETE_DATABASE_TEMPLATE = """ +STOP_DATABASE_TEMPLATE = """ + + {{ database.to_xml() }} + + + 523e3218-afc7-11c3-90f5-f90431260ab8 + +""" + +DELETE_DATABASE_TEMPLATE = """ {{ database.to_xml() }} @@ -420,7 +451,7 @@ DELETE_DATABASE_TEMPLATE = """ +CREATE_SNAPSHOT_TEMPLATE = """ {{ snapshot.to_xml() }} From 7afd3532c6b39f8eacc6ba0e1ad69472da9609ee Mon Sep 17 00:00:00 2001 From: rocky4570fft Date: Sun, 13 Aug 2017 12:00:21 +1000 Subject: [PATCH 02/10] fixup typos and cleanup code a little --- moto/rds2/models.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 6ae0c98a0..d9c1483ca 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -733,32 +733,27 @@ class RDS2Backend(BaseBackend): database = self.describe_databases(db_instance_identifier)[0] return database - def _check_can_stop_rds_instance_(self, database=None): + def stop_database(self, db_instance_identifier, db_snapshot_identifier=None): + database = self.describe_databases(db_instance_identifier)[0] # todo: certain rds types not allowed to be stopped at this time. - if database: - if database.is_replica or database.multi_az: + if database.is_replica or database.multi_az: # should be 400 error return RDSClientError('InvalidDBClusterStateFault', 'Invalid DB type, when trying to perform StopDBInstance. See AWS RDS documentation on rds.stop_db_instance') - return True - - def stop_database(self, db_instance_identifier, snapshot_name=None): - database = self.describe_databases(db_instance_identifier)[0] - self._check_can_stop_rds_instance_(database) if database.status != 'available': - return RDSClientError('InvalidDBInstanceState', 'when calling the StopDBInstance operation: Instance testdb is not in available state') - self.create_rds_snapshot(db_instance_identifier, db_instance_identifier) + return RDSClientError('InvalidDBInstanceState', 'when calling the StopDBInstance operation: Instance %s is not in available state' % db_instance_identifier) + self.create_rds_snapshot(db_instance_identifier, db_snapshot_identifier) database.status = 'shutdown' return database def start_database(self, db_instance_identifier): database = self.describe_databases(db_instance_identifier)[0] - if database.status != 'shutdown': - # should be 400 error - return RDSClientError('InvalidDBInstanceState', 'when calling the StartDBInstance operation: Instance {} is not stopped, it cannot be started.' % db_instance_identifier) + if database.status != 'shutdown': # should be 400 error + return RDSClientError('InvalidDBInstanceState', 'when calling the StartDBInstance operation: Instance %s is not stopped, it cannot be started.' % db_instance_identifier) database.status = 'available' return def create_rds_snapshot(self, db_instance_identifier, db_snapshot_identifier): + database = self.describe_databases(db_instance_identifier)[0] # todo # DBSnapshotAlreadyExists # SnapshotQuotaExceeded From 6c3c6623bfb666300dc998e51165667c236c140c Mon Sep 17 00:00:00 2001 From: rocky4570fft Date: Sun, 13 Aug 2017 12:02:49 +1000 Subject: [PATCH 03/10] should only call create_snapshot if db_snapshot_identifier is filled --- moto/rds2/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index d9c1483ca..edb46a157 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -741,7 +741,8 @@ class RDS2Backend(BaseBackend): return RDSClientError('InvalidDBClusterStateFault', 'Invalid DB type, when trying to perform StopDBInstance. See AWS RDS documentation on rds.stop_db_instance') if database.status != 'available': return RDSClientError('InvalidDBInstanceState', 'when calling the StopDBInstance operation: Instance %s is not in available state' % db_instance_identifier) - self.create_rds_snapshot(db_instance_identifier, db_snapshot_identifier) + if db_snapshot_identifier: + self.create_rds_snapshot(db_instance_identifier, db_snapshot_identifier) database.status = 'shutdown' return database From 9687b6e03e3985aa2137d5e7c115d084d60752c5 Mon Sep 17 00:00:00 2001 From: rocky4570fft Date: Sun, 13 Aug 2017 12:05:22 +1000 Subject: [PATCH 04/10] get appropritate parameters - doh --- moto/rds2/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index d8108c7f8..e88e4c603 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -196,7 +196,7 @@ class RDS2Response(BaseResponse): def stop_db_instance(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') - db_snapshot_identifier = self._get_param('DBInstanceIdentifier') + db_snapshot_identifier = self._get_param('DBSnapshotIdentifier') database = self.backend.stop_database(db_instance_identifier, db_snapshot_identifier) template = self.response_template(STOP_DATABASE_TEMPLATE) return template.render(database=database) From ed39c2ea4e5e51611791ced8461498c2768c5a66 Mon Sep 17 00:00:00 2001 From: rocky4570fft Date: Mon, 14 Aug 2017 00:27:15 +1000 Subject: [PATCH 05/10] fix up some issues found in unittests - comment out snapshotting until later --- moto/rds2/exceptions.py | 15 ++++++ moto/rds2/models.py | 34 +++++++------ tests/test_rds2/test_rds2.py | 98 ++++++++++++++++++++++++++++++++++-- 3 files changed, 129 insertions(+), 18 deletions(-) diff --git a/moto/rds2/exceptions.py b/moto/rds2/exceptions.py index 057a13ba2..5e4b38ef7 100644 --- a/moto/rds2/exceptions.py +++ b/moto/rds2/exceptions.py @@ -58,3 +58,18 @@ class DBParameterGroupNotFoundError(RDSClientError): super(DBParameterGroupNotFoundError, self).__init__( 'DBParameterGroupNotFound', 'DB Parameter Group {0} not found.'.format(db_parameter_group_name)) + +class InvalidDBClusterStateFaultError(RDSClientError): + + def __init__(self, database_identifier): + super(InvalidDBClusterStateFaultError, self).__init__( + 'InvalidDBClusterStateFault', + 'Invalid DB type, when trying to perform StopDBInstance on {0}e. See AWS RDS documentation on rds.stop_db_instance'.format(database_identifier)) + +class InvalidDBInstanceStateError(RDSClientError): + + def __init__(self, database_identifier, istate): + estate = "in available state" if istate == 'stop' else "stopped, it cannot be started" + super(InvalidDBInstanceStateError, self).__init__( + 'InvalidDBInstanceState', + 'when calling the {}DBInstance operation: Instance {} is not {}.'.format(istate.title(), database_identifier, estate)) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index edb46a157..9fbb79ed9 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -18,7 +18,9 @@ from .exceptions import (RDSClientError, DBSnapshotNotFoundError, DBSecurityGroupNotFoundError, DBSubnetGroupNotFoundError, - DBParameterGroupNotFoundError) + DBParameterGroupNotFoundError, + InvalidDBClusterStateFaultError, + InvalidDBInstanceStateError) class Database(BaseModel): @@ -737,28 +739,30 @@ class RDS2Backend(BaseBackend): database = self.describe_databases(db_instance_identifier)[0] # todo: certain rds types not allowed to be stopped at this time. if database.is_replica or database.multi_az: - # should be 400 error - return RDSClientError('InvalidDBClusterStateFault', 'Invalid DB type, when trying to perform StopDBInstance. See AWS RDS documentation on rds.stop_db_instance') + # todo: more db types not supported by stop/start instance api + raise InvalidDBClusterStateFaultError(db_instance_identifier) if database.status != 'available': - return RDSClientError('InvalidDBInstanceState', 'when calling the StopDBInstance operation: Instance %s is not in available state' % db_instance_identifier) - if db_snapshot_identifier: - self.create_rds_snapshot(db_instance_identifier, db_snapshot_identifier) + raise InvalidDBInstanceStateError(db_instance_identifier, 'stop') + # todo: create rds snapshots + # if db_snapshot_identifier: + # self.create_rds_snapshot(db_instance_identifier, db_snapshot_identifier) database.status = 'shutdown' return database def start_database(self, db_instance_identifier): database = self.describe_databases(db_instance_identifier)[0] - if database.status != 'shutdown': # should be 400 error - return RDSClientError('InvalidDBInstanceState', 'when calling the StartDBInstance operation: Instance %s is not stopped, it cannot be started.' % db_instance_identifier) + # todo: bunch of different error messages to be generated from this api call + if database.status != 'shutdown': + raise InvalidDBInstanceStateError(db_instance_identifier, 'start') database.status = 'available' - return + return database - def create_rds_snapshot(self, db_instance_identifier, db_snapshot_identifier): - database = self.describe_databases(db_instance_identifier)[0] - # todo - # DBSnapshotAlreadyExists - # SnapshotQuotaExceeded - return None + # def create_rds_snapshot(self, db_instance_identifier, db_snapshot_identifier): + # database = self.describe_databases(db_instance_identifier)[0] + # # todo + # # DBSnapshotAlreadyExists + # # SnapshotQuotaExceeded + # return None def find_db_from_id(self, db_id): if self.arn_regex.match(db_id): diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index a50f99868..40e35c9c1 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -19,9 +19,6 @@ def test_create_database(): MasterUserPassword='hunter2', Port=1234, DBSecurityGroups=["my_sg"]) - database['DBInstance']['DBInstanceStatus'].should.equal('available') - database['DBInstance']['DBName'].should.equal('staging-postgres') - database['DBInstance']['DBInstanceIdentifier'].should.equal("db-master-1") database['DBInstance']['AllocatedStorage'].should.equal(10) database['DBInstance']['DBInstanceClass'].should.equal("db.m1.small") database['DBInstance']['LicenseModel'].should.equal("license-included") @@ -30,7 +27,102 @@ def test_create_database(): 'DBSecurityGroupName'].should.equal('my_sg') database['DBInstance']['DBInstanceArn'].should.equal( 'arn:aws:rds:us-west-2:1234567890:db:db-master-1') + database['DBInstance']['DBInstanceStatus'].should.equal('available') + database['DBInstance']['DBName'].should.equal('staging-postgres') + database['DBInstance']['DBInstanceIdentifier'].should.equal("db-master-1") +@mock_rds2 +def test_stop_database(): + conn = boto3.client('rds', region_name='us-west-2') + database = conn.create_db_instance(DBInstanceIdentifier='db-master-1', + AllocatedStorage=10, + Engine='postgres', + DBName='staging-postgres', + DBInstanceClass='db.m1.small', + LicenseModel='license-included', + MasterUsername='root', + MasterUserPassword='hunter2', + Port=1234, + DBSecurityGroups=["my_sg"]) + mydb = conn.describe_db_instances(DBInstanceIdentifier=database['DBInstance']['DBInstanceIdentifier'])['DBInstances'][0] + mydb['DBInstanceStatus'].should.equal('available') + # test stopping database + response = conn.stop_db_instance(DBInstanceIdentifier=mydb['DBInstanceIdentifier']) + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + response['DBInstance']['DBInstanceStatus'].should.equal('shutdown') + # test rdsclient error when trying to stop an already stopped database + conn.stop_db_instance.when.called_with(DBInstanceIdentifier=mydb['DBInstanceIdentifier']).should.throw(ClientError) + +@mock_rds2 +def test_start_database(): + conn = boto3.client('rds', region_name='us-west-2') + database = conn.create_db_instance(DBInstanceIdentifier='db-master-1', + AllocatedStorage=10, + Engine='postgres', + DBName='staging-postgres', + DBInstanceClass='db.m1.small', + LicenseModel='license-included', + MasterUsername='root', + MasterUserPassword='hunter2', + Port=1234, + DBSecurityGroups=["my_sg"]) + mydb = conn.describe_db_instances(DBInstanceIdentifier=database['DBInstance']['DBInstanceIdentifier'])['DBInstances'][0] + mydb['DBInstanceStatus'].should.equal('available') + # test trying to start an already started database + conn.start_db_instance.when.called_with(DBInstanceIdentifier=mydb['DBInstanceIdentifier']).should.throw(ClientError) + # stop and test start - should go from shutdown to available + response = conn.stop_db_instance(DBInstanceIdentifier=mydb['DBInstanceIdentifier']) + response['DBInstance']['DBInstanceStatus'].should.equal('shutdown') + response = conn.start_db_instance(DBInstanceIdentifier=mydb['DBInstanceIdentifier']) + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + response['DBInstance']['DBInstanceStatus'].should.equal('available') + +@mock_rds2 +def test_fail_to_stop_multi_az(): + conn = boto3.client('rds', region_name='us-west-2') + database = conn.create_db_instance(DBInstanceIdentifier='db-master-1', + AllocatedStorage=10, + Engine='postgres', + DBName='staging-postgres', + DBInstanceClass='db.m1.small', + LicenseModel='license-included', + MasterUsername='root', + MasterUserPassword='hunter2', + Port=1234, + DBSecurityGroups=["my_sg"], + MultiAZ=True) + + mydb = conn.describe_db_instances(DBInstanceIdentifier=database['DBInstance']['DBInstanceIdentifier'])['DBInstances'][0] + mydb['DBInstanceStatus'].should.equal('available') + # multi-az databases arent allowed to be shutdown at this time. + conn.stop_db_instance.when.called_with(DBInstanceIdentifier=mydb['DBInstanceIdentifier']).should.throw(ClientError) + # multi-az databases arent allowed to be started up at this time. + conn.start_db_instance.when.called_with(DBInstanceIdentifier=mydb['DBInstanceIdentifier']).should.throw(ClientError) + +@mock_rds2 +def test_fail_to_stop_readreplica(): + conn = boto3.client('rds', region_name='us-west-2') + database = conn.create_db_instance(DBInstanceIdentifier='db-master-1', + AllocatedStorage=10, + Engine='postgres', + DBName='staging-postgres', + DBInstanceClass='db.m1.small', + LicenseModel='license-included', + MasterUsername='root', + MasterUserPassword='hunter2', + Port=1234, + DBSecurityGroups=["my_sg"]) + + replica = conn.create_db_instance_read_replica(DBInstanceIdentifier="db-replica-1", + SourceDBInstanceIdentifier="db-master-1", + DBInstanceClass="db.m1.small") + + mydb = conn.describe_db_instances(DBInstanceIdentifier=replica['DBInstance']['DBInstanceIdentifier'])['DBInstances'][0] + mydb['DBInstanceStatus'].should.equal('available') + # read-replicas are not allowed to be stopped at this time. + conn.stop_db_instance.when.called_with(DBInstanceIdentifier=mydb['DBInstanceIdentifier']).should.throw(ClientError) + # read-replicas are not allowed to be started at this time. + conn.start_db_instance.when.called_with(DBInstanceIdentifier=mydb['DBInstanceIdentifier']).should.throw(ClientError) @mock_rds2 def test_get_databases(): From 85dd2440f42b88644b4f9320c0814a06303ec265 Mon Sep 17 00:00:00 2001 From: rocky4570fft Date: Mon, 14 Aug 2017 00:32:08 +1000 Subject: [PATCH 06/10] oopsie on api version dates for delete and create snapshot_templates, should be on stop/start rds instances --- moto/rds2/responses.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index e88e4c603..bf76660aa 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -424,7 +424,7 @@ REBOOT_DATABASE_TEMPLATE = """ +START_DATABASE_TEMPLATE = """ {{ database.to_xml() }} @@ -433,7 +433,7 @@ START_DATABASE_TEMPLATE = """ +STOP_DATABASE_TEMPLATE = """ {{ database.to_xml() }} @@ -442,7 +442,7 @@ STOP_DATABASE_TEMPLATE = """ +DELETE_DATABASE_TEMPLATE = """ {{ database.to_xml() }} @@ -451,7 +451,7 @@ DELETE_DATABASE_TEMPLATE = """ +CREATE_SNAPSHOT_TEMPLATE = """ {{ snapshot.to_xml() }} From f2cc60b999f994bca5bf003bd70b4feca3259ca1 Mon Sep 17 00:00:00 2001 From: rocky4570fft Date: Mon, 14 Aug 2017 00:41:26 +1000 Subject: [PATCH 07/10] satisfy the flake tests - doesnt like my flakey code :( --- moto/rds2/exceptions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/moto/rds2/exceptions.py b/moto/rds2/exceptions.py index 5e4b38ef7..9c0c0144a 100644 --- a/moto/rds2/exceptions.py +++ b/moto/rds2/exceptions.py @@ -59,6 +59,7 @@ class DBParameterGroupNotFoundError(RDSClientError): 'DBParameterGroupNotFound', 'DB Parameter Group {0} not found.'.format(db_parameter_group_name)) + class InvalidDBClusterStateFaultError(RDSClientError): def __init__(self, database_identifier): @@ -66,6 +67,7 @@ class InvalidDBClusterStateFaultError(RDSClientError): 'InvalidDBClusterStateFault', 'Invalid DB type, when trying to perform StopDBInstance on {0}e. See AWS RDS documentation on rds.stop_db_instance'.format(database_identifier)) + class InvalidDBInstanceStateError(RDSClientError): def __init__(self, database_identifier, istate): From c84e8c86f0c471eab7ce5107508ec835a86ab1e2 Mon Sep 17 00:00:00 2001 From: rocky4570fft Date: Tue, 15 Aug 2017 00:55:09 +1000 Subject: [PATCH 08/10] modify to use create_snapshot, add extra tests for certain error conditions --- moto/rds2/exceptions.py | 18 +++++++++- moto/rds2/models.py | 24 ++++++------- tests/test_rds2/test_rds2.py | 70 +++++++++++++++++++++++++++++++++--- 3 files changed, 95 insertions(+), 17 deletions(-) diff --git a/moto/rds2/exceptions.py b/moto/rds2/exceptions.py index 9c0c0144a..0e716310e 100644 --- a/moto/rds2/exceptions.py +++ b/moto/rds2/exceptions.py @@ -74,4 +74,20 @@ class InvalidDBInstanceStateError(RDSClientError): estate = "in available state" if istate == 'stop' else "stopped, it cannot be started" super(InvalidDBInstanceStateError, self).__init__( 'InvalidDBInstanceState', - 'when calling the {}DBInstance operation: Instance {} is not {}.'.format(istate.title(), database_identifier, estate)) + 'Instance {} is not {}.'.format(database_identifier, estate)) + + +class SnapshotQuotaExceededError(RDSClientError): + + def __init__(self): + super(SnapshotQuotaExceededError, self).__init__( + 'SnapshotQuotaExceeded', + 'The request cannot be processed because it would exceed the maximum number of snapshots.') + + +class DBSnapshotAlreadyExistsError(RDSClientError): + + def __init__(self, database_snapshot_identifier): + super(DBSnapshotAlreadyExistsError, self).__init__( + 'DBSnapshotAlreadyExists', + 'Cannot create the snapshot because a snapshot with the identifier {} already exists.'.format(database_snapshot_identifier)) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 9fbb79ed9..d52cb916d 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import copy import datetime +import os from collections import defaultdict import boto.rds2 @@ -20,7 +21,9 @@ from .exceptions import (RDSClientError, DBSubnetGroupNotFoundError, DBParameterGroupNotFoundError, InvalidDBClusterStateFaultError, - InvalidDBInstanceStateError) + InvalidDBInstanceStateError, + SnapshotQuotaExceededError, + DBSnapshotAlreadyExistsError) class Database(BaseModel): @@ -410,6 +413,7 @@ class Snapshot(BaseModel): self.tags = tags or [] self.created_at = iso_8601_datetime_with_milliseconds(datetime.datetime.now()) + @property def snapshot_arn(self): return "arn:aws:rds:{0}:1234567890:snapshot:{1}".format(self.database.region, self.snapshot_id) @@ -676,10 +680,14 @@ class RDS2Backend(BaseBackend): self.databases[database_id] = database return database - def create_snapshot(self, db_instance_identifier, db_snapshot_identifier, tags): + def create_snapshot(self, db_instance_identifier, db_snapshot_identifier, tags=None): database = self.databases.get(db_instance_identifier) if not database: raise DBInstanceNotFoundError(db_instance_identifier) + if db_snapshot_identifier in self.snapshots: + raise DBSnapshotAlreadyExistsError(db_snapshot_identifier) + if len(self.snapshots) >= int(os.environ.get('MOTO_RDS_SNAPSHOT_LIMIT', '100')): + raise SnapshotQuotaExceededError() snapshot = Snapshot(database, db_snapshot_identifier, tags) self.snapshots[db_snapshot_identifier] = snapshot return snapshot @@ -743,9 +751,8 @@ class RDS2Backend(BaseBackend): raise InvalidDBClusterStateFaultError(db_instance_identifier) if database.status != 'available': raise InvalidDBInstanceStateError(db_instance_identifier, 'stop') - # todo: create rds snapshots - # if db_snapshot_identifier: - # self.create_rds_snapshot(db_instance_identifier, db_snapshot_identifier) + if db_snapshot_identifier: + self.create_snapshot(db_instance_identifier, db_snapshot_identifier) database.status = 'shutdown' return database @@ -757,13 +764,6 @@ class RDS2Backend(BaseBackend): database.status = 'available' return database - # def create_rds_snapshot(self, db_instance_identifier, db_snapshot_identifier): - # database = self.describe_databases(db_instance_identifier)[0] - # # todo - # # DBSnapshotAlreadyExists - # # SnapshotQuotaExceeded - # return None - def find_db_from_id(self, db_id): if self.arn_regex.match(db_id): arn_breakdown = db_id.split(':') diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 40e35c9c1..97914e7e0 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -31,6 +31,7 @@ def test_create_database(): database['DBInstance']['DBName'].should.equal('staging-postgres') database['DBInstance']['DBInstanceIdentifier'].should.equal("db-master-1") + @mock_rds2 def test_stop_database(): conn = boto3.client('rds', region_name='us-west-2') @@ -46,12 +47,17 @@ def test_stop_database(): DBSecurityGroups=["my_sg"]) mydb = conn.describe_db_instances(DBInstanceIdentifier=database['DBInstance']['DBInstanceIdentifier'])['DBInstances'][0] mydb['DBInstanceStatus'].should.equal('available') - # test stopping database + # test stopping database should shutdown response = conn.stop_db_instance(DBInstanceIdentifier=mydb['DBInstanceIdentifier']) response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) response['DBInstance']['DBInstanceStatus'].should.equal('shutdown') # test rdsclient error when trying to stop an already stopped database conn.stop_db_instance.when.called_with(DBInstanceIdentifier=mydb['DBInstanceIdentifier']).should.throw(ClientError) + # test stopping a stopped database with snapshot should error and no snapshot should exist for that call + conn.stop_db_instance.when.called_with(DBInstanceIdentifier=mydb['DBInstanceIdentifier'], DBSnapshotIdentifier='rocky4570-rds-snap').should.throw(ClientError) + response = conn.describe_db_snapshots() + response['DBSnapshots'].should.equal([]) + @mock_rds2 def test_start_database(): @@ -68,14 +74,27 @@ def test_start_database(): DBSecurityGroups=["my_sg"]) mydb = conn.describe_db_instances(DBInstanceIdentifier=database['DBInstance']['DBInstanceIdentifier'])['DBInstances'][0] mydb['DBInstanceStatus'].should.equal('available') - # test trying to start an already started database + # test starting an already started database should error conn.start_db_instance.when.called_with(DBInstanceIdentifier=mydb['DBInstanceIdentifier']).should.throw(ClientError) - # stop and test start - should go from shutdown to available - response = conn.stop_db_instance(DBInstanceIdentifier=mydb['DBInstanceIdentifier']) + # stop and test start - should go from shutdown to available, create snapshot and check snapshot + response = conn.stop_db_instance(DBInstanceIdentifier=mydb['DBInstanceIdentifier'], DBSnapshotIdentifier='rocky4570-rds-snap') + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) response['DBInstance']['DBInstanceStatus'].should.equal('shutdown') + response = conn.describe_db_snapshots() + response['DBSnapshots'][0]['DBSnapshotIdentifier'].should.equal('rocky4570-rds-snap') response = conn.start_db_instance(DBInstanceIdentifier=mydb['DBInstanceIdentifier']) response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) response['DBInstance']['DBInstanceStatus'].should.equal('available') + # starting database should not remove snapshot + response = conn.describe_db_snapshots() + response['DBSnapshots'][0]['DBSnapshotIdentifier'].should.equal('rocky4570-rds-snap') + # test stopping database, create snapshot with existing snapshot already created should throw error + conn.stop_db_instance.when.called_with(DBInstanceIdentifier=mydb['DBInstanceIdentifier'], DBSnapshotIdentifier='rocky4570-rds-snap').should.throw(ClientError) + # test stopping database not invoking snapshot should succeed. + response = conn.stop_db_instance(DBInstanceIdentifier=mydb['DBInstanceIdentifier']) + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + response['DBInstance']['DBInstanceStatus'].should.equal('shutdown') + @mock_rds2 def test_fail_to_stop_multi_az(): @@ -99,6 +118,7 @@ def test_fail_to_stop_multi_az(): # multi-az databases arent allowed to be started up at this time. conn.start_db_instance.when.called_with(DBInstanceIdentifier=mydb['DBInstanceIdentifier']).should.throw(ClientError) + @mock_rds2 def test_fail_to_stop_readreplica(): conn = boto3.client('rds', region_name='us-west-2') @@ -124,6 +144,48 @@ def test_fail_to_stop_readreplica(): # read-replicas are not allowed to be started at this time. conn.start_db_instance.when.called_with(DBInstanceIdentifier=mydb['DBInstanceIdentifier']).should.throw(ClientError) + +@mock_rds2 +def test_snapshotquota_exceeded(): + import os + conn = boto3.client('rds', region_name='us-west-2') + database1 = conn.create_db_instance(DBInstanceIdentifier='db-master-1', + AllocatedStorage=10, + Engine='postgres', + DBName='staging-postgres', + DBInstanceClass='db.m1.small', + LicenseModel='license-included', + MasterUsername='root', + MasterUserPassword='hunter2', + Port=1234, + DBSecurityGroups=["my_sg"]) + database2 = conn.create_db_instance(DBInstanceIdentifier='db-master-2', + AllocatedStorage=10, + Engine='postgres', + DBName='staging-postgres', + DBInstanceClass='db.m1.small', + LicenseModel='license-included', + MasterUsername='root', + MasterUserPassword='hunter2', + Port=1234, + DBSecurityGroups=["my_sg"]) + database3 = conn.create_db_instance(DBInstanceIdentifier='db-master-3', + AllocatedStorage=10, + Engine='postgres', + DBName='staging-postgres', + DBInstanceClass='db.m1.small', + LicenseModel='license-included', + MasterUsername='root', + MasterUserPassword='hunter2', + Port=1234, + DBSecurityGroups=["my_sg"]) + conn.stop_db_instance(DBInstanceIdentifier=database1['DBInstance']['DBInstanceIdentifier'], DBSnapshotIdentifier='rocky4570-rds-snap1') + conn.stop_db_instance(DBInstanceIdentifier=database2['DBInstance']['DBInstanceIdentifier'], DBSnapshotIdentifier='rocky4570-rds-snap2') + os.environ['MOTO_RDS_SNAPSHOT_LIMIT'] = '2' + conn.stop_db_instance.when.called_with(DBInstanceIdentifier=database3['DBInstance']['DBInstanceIdentifier'], DBSnapshotIdentifier='rocky4570-rds-snap3').should.throw(ClientError) + os.unsetenv('MOTO_RDS_SNAPSHOT_LIMIT') + + @mock_rds2 def test_get_databases(): conn = boto3.client('rds', region_name='us-west-2') From 1c1ef9dc25afec99452c8e63f7df2e97d9a85810 Mon Sep 17 00:00:00 2001 From: rocky4570fft Date: Tue, 15 Aug 2017 00:58:33 +1000 Subject: [PATCH 09/10] the linter is too linty --- moto/rds2/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index d52cb916d..bb66ead57 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -413,7 +413,6 @@ class Snapshot(BaseModel): self.tags = tags or [] self.created_at = iso_8601_datetime_with_milliseconds(datetime.datetime.now()) - @property def snapshot_arn(self): return "arn:aws:rds:{0}:1234567890:snapshot:{1}".format(self.database.region, self.snapshot_id) From 592bf868ff01b487465ed4a6969f7068ef74c8ce Mon Sep 17 00:00:00 2001 From: rocky4570fft Date: Tue, 15 Aug 2017 10:34:10 +1000 Subject: [PATCH 10/10] remove test for rds snapshot quota exceed as moto_server doesnt support it --- tests/test_rds2/test_rds2.py | 41 ------------------------------------ 1 file changed, 41 deletions(-) diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 97914e7e0..4ab7dbc60 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -145,47 +145,6 @@ def test_fail_to_stop_readreplica(): conn.start_db_instance.when.called_with(DBInstanceIdentifier=mydb['DBInstanceIdentifier']).should.throw(ClientError) -@mock_rds2 -def test_snapshotquota_exceeded(): - import os - conn = boto3.client('rds', region_name='us-west-2') - database1 = conn.create_db_instance(DBInstanceIdentifier='db-master-1', - AllocatedStorage=10, - Engine='postgres', - DBName='staging-postgres', - DBInstanceClass='db.m1.small', - LicenseModel='license-included', - MasterUsername='root', - MasterUserPassword='hunter2', - Port=1234, - DBSecurityGroups=["my_sg"]) - database2 = conn.create_db_instance(DBInstanceIdentifier='db-master-2', - AllocatedStorage=10, - Engine='postgres', - DBName='staging-postgres', - DBInstanceClass='db.m1.small', - LicenseModel='license-included', - MasterUsername='root', - MasterUserPassword='hunter2', - Port=1234, - DBSecurityGroups=["my_sg"]) - database3 = conn.create_db_instance(DBInstanceIdentifier='db-master-3', - AllocatedStorage=10, - Engine='postgres', - DBName='staging-postgres', - DBInstanceClass='db.m1.small', - LicenseModel='license-included', - MasterUsername='root', - MasterUserPassword='hunter2', - Port=1234, - DBSecurityGroups=["my_sg"]) - conn.stop_db_instance(DBInstanceIdentifier=database1['DBInstance']['DBInstanceIdentifier'], DBSnapshotIdentifier='rocky4570-rds-snap1') - conn.stop_db_instance(DBInstanceIdentifier=database2['DBInstance']['DBInstanceIdentifier'], DBSnapshotIdentifier='rocky4570-rds-snap2') - os.environ['MOTO_RDS_SNAPSHOT_LIMIT'] = '2' - conn.stop_db_instance.when.called_with(DBInstanceIdentifier=database3['DBInstance']['DBInstanceIdentifier'], DBSnapshotIdentifier='rocky4570-rds-snap3').should.throw(ClientError) - os.unsetenv('MOTO_RDS_SNAPSHOT_LIMIT') - - @mock_rds2 def test_get_databases(): conn = boto3.client('rds', region_name='us-west-2')