Merge pull request #1053 from rocky4570/rds-stop-start

Rds stop start
This commit is contained in:
Jack Danger 2017-08-14 17:53:32 -07:00 committed by GitHub
commit f6fc9c7d54
4 changed files with 212 additions and 5 deletions

View File

@ -58,3 +58,36 @@ 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',
'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))

View File

@ -2,6 +2,7 @@ from __future__ import unicode_literals
import copy
import datetime
import os
from collections import defaultdict
import boto.rds2
@ -18,7 +19,11 @@ from .exceptions import (RDSClientError,
DBSnapshotNotFoundError,
DBSecurityGroupNotFoundError,
DBSubnetGroupNotFoundError,
DBParameterGroupNotFoundError)
DBParameterGroupNotFoundError,
InvalidDBClusterStateFaultError,
InvalidDBInstanceStateError,
SnapshotQuotaExceededError,
DBSnapshotAlreadyExistsError)
class Database(BaseModel):
@ -674,10 +679,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
@ -733,6 +742,27 @@ class RDS2Backend(BaseBackend):
database = self.describe_databases(db_instance_identifier)[0]
return database
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.is_replica or database.multi_az:
# todo: more db types not supported by stop/start instance api
raise InvalidDBClusterStateFaultError(db_instance_identifier)
if database.status != 'available':
raise InvalidDBInstanceStateError(db_instance_identifier, 'stop')
if db_snapshot_identifier:
self.create_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]
# 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 database
def find_db_from_id(self, db_id):
if self.arn_regex.match(db_id):
arn_breakdown = db_id.split(':')

View File

@ -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('DBSnapshotIdentifier')
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,6 +424,23 @@ REBOOT_DATABASE_TEMPLATE = """<RebootDBInstanceResponse xmlns="http://rds.amazon
</ResponseMetadata>
</RebootDBInstanceResponse>"""
START_DATABASE_TEMPLATE = """<StartDBInstanceResponse xmlns="http://rds.amazonaws.com/doc/2014-10-31/">
<StartDBInstanceResult>
{{ database.to_xml() }}
</StartDBInstanceResult>
<ResponseMetadata>
<RequestId>523e3218-afc7-11c3-90f5-f90431260ab9</RequestId>
</ResponseMetadata>
</StartDBInstanceResponse>"""
STOP_DATABASE_TEMPLATE = """<StopDBInstanceResponse xmlns="http://rds.amazonaws.com/doc/2014-10-31/">
<StopDBInstanceResult>
{{ database.to_xml() }}
</StopDBInstanceResult>
<ResponseMetadata>
<RequestId>523e3218-afc7-11c3-90f5-f90431260ab8</RequestId>
</ResponseMetadata>
</StopDBInstanceResponse>"""
DELETE_DATABASE_TEMPLATE = """<DeleteDBInstanceResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/">
<DeleteDBInstanceResult>

View File

@ -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,6 +27,122 @@ 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 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():
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 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, 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():
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