RDS - Cluster-methods + restore_db_from_snapshot (#4247)

This commit is contained in:
Bert Blommers 2021-10-10 19:18:19 +00:00 committed by GitHub
parent 4e45152d86
commit 4795888fda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 693 additions and 0 deletions

View File

@ -120,3 +120,16 @@ class InvalidParameterCombination(RDSClientError):
super(InvalidParameterCombination, self).__init__( super(InvalidParameterCombination, self).__init__(
"InvalidParameterCombination", message "InvalidParameterCombination", message
) )
class InvalidDBClusterStateFault(RDSClientError):
def __init__(self, message):
super().__init__("InvalidDBClusterStateFault", message)
class DBClusterNotFoundError(RDSClientError):
def __init__(self, cluster_identifier):
super(DBClusterNotFoundError, self).__init__(
"DBClusterNotFoundFault",
"DBCluster {} not found.".format(cluster_identifier),
)

View File

@ -3,6 +3,8 @@ from __future__ import unicode_literals
import copy import copy
import datetime import datetime
import os import os
import random
import string
from collections import defaultdict from collections import defaultdict
from boto3 import Session from boto3 import Session
@ -15,6 +17,7 @@ from moto.core.utils import iso_8601_datetime_with_milliseconds
from moto.ec2.models import ec2_backends from moto.ec2.models import ec2_backends
from .exceptions import ( from .exceptions import (
RDSClientError, RDSClientError,
DBClusterNotFoundError,
DBInstanceNotFoundError, DBInstanceNotFoundError,
DBSnapshotNotFoundError, DBSnapshotNotFoundError,
DBSecurityGroupNotFoundError, DBSecurityGroupNotFoundError,
@ -27,10 +30,122 @@ from .exceptions import (
DBSnapshotAlreadyExistsError, DBSnapshotAlreadyExistsError,
InvalidParameterValue, InvalidParameterValue,
InvalidParameterCombination, InvalidParameterCombination,
InvalidDBClusterStateFault,
) )
from .utils import FilterDef, apply_filter, merge_filters, validate_filters from .utils import FilterDef, apply_filter, merge_filters, validate_filters
class Cluster:
def __init__(self, **kwargs):
self.db_name = kwargs.get("db_name")
self.db_cluster_identifier = kwargs.get("db_cluster_identifier")
self.engine = kwargs.get("engine")
self.engine_version = kwargs.get("engine_version")
if not self.engine_version:
# Set default
self.engine_version = "5.6.mysql_aurora.1.22.5" # TODO: depends on engine
self.engine_mode = kwargs.get("engine_mode") or "provisioned"
self.status = "active"
self.region = kwargs.get("region")
self.cluster_create_time = iso_8601_datetime_with_milliseconds(
datetime.datetime.now()
)
self.master_username = kwargs.get("master_username")
if not self.master_username:
raise InvalidParameterValue(
"The parameter MasterUsername must be provided and must not be blank."
)
self.master_user_password = kwargs.get("master_user_password")
if not self.master_user_password:
raise InvalidParameterValue(
"The parameter MasterUserPassword must be provided and must not be blank."
)
if len(self.master_user_password) < 8:
raise InvalidParameterValue(
"The parameter MasterUserPassword is not a valid password because it is shorter than 8 characters."
)
self.availability_zones = kwargs.get("availability_zones")
if not self.availability_zones:
self.availability_zones = [
f"{self.region}a",
f"{self.region}b",
f"{self.region}c",
]
self.parameter_group = kwargs.get("parameter_group") or "default.aurora5.6"
self.subnet_group = "default"
self.status = "creating"
self.url_identifier = "".join(
random.choice(string.ascii_lowercase + string.digits) for _ in range(12)
)
self.endpoint = f"{self.db_cluster_identifier}.cluster-{self.url_identifier}.{self.region}.rds.amazonaws.com"
self.reader_endpoint = f"{self.db_cluster_identifier}.cluster-ro-{self.url_identifier}.{self.region}.rds.amazonaws.com"
self.port = kwargs.get("port") or 3306
self.preferred_backup_window = "01:37-02:07"
self.preferred_maintenance_window = "wed:02:40-wed:03:10"
# This should default to the default security group
self.vpc_security_groups = []
self.hosted_zone_id = "".join(
random.choice(string.ascii_uppercase + string.digits) for _ in range(14)
)
self.resource_id = "cluster-" + "".join(
random.choice(string.ascii_uppercase + string.digits) for _ in range(26)
)
self.arn = f"arn:aws:rds:{self.region}:{ACCOUNT_ID}:cluster:{self.db_cluster_identifier}"
def to_xml(self):
template = Template(
"""<DBCluster>
<AllocatedStorage>1</AllocatedStorage>
<AvailabilityZones>
{% for zone in cluster.availability_zones %}
<AvailabilityZone>{{ zone }}</AvailabilityZone>
{% endfor %}
</AvailabilityZones>
<BackupRetentionPeriod>1</BackupRetentionPeriod>
<DBInstanceStatus>{{ cluster.status }}</DBInstanceStatus>
{% if cluster.db_name %}<DatabaseName>{{ cluster.db_name }}</DatabaseName>{% endif %}
<DBClusterIdentifier>{{ cluster.db_cluster_identifier }}</DBClusterIdentifier>
<DBClusterParameterGroup>{{ cluster.parameter_group }}</DBClusterParameterGroup>
<DBSubnetGroup>{{ cluster.subnet_group }}</DBSubnetGroup>
<ClusterCreateTime>{{ cluster.cluster_create_time }}</ClusterCreateTime>
<Engine>{{ cluster.engine }}</Engine>
<Status>{{ cluster.status }}</Status>
<Endpoint>{{ cluster.endpoint }}</Endpoint>
<ReaderEndpoint>{{ cluster.reader_endpoint }}</ReaderEndpoint>
<MultiAZ>false</MultiAZ>
<EngineVersion>{{ cluster.engine_version }}</EngineVersion>
<Port>{{ cluster.port }}</Port>
<MasterUsername>{{ cluster.master_username }}</MasterUsername>
<PreferredBackupWindow>{{ cluster.preferred_backup_window }}</PreferredBackupWindow>
<PreferredMaintenanceWindow>{{ cluster.preferred_maintenance_window }}</PreferredMaintenanceWindow>
<ReadReplicaIdentifiers></ReadReplicaIdentifiers>
<DBClusterMembers></DBClusterMembers>
<VpcSecurityGroups>
{% for id in cluster.vpc_security_groups %}
<VpcSecurityGroup>
<VpcSecurityGroupId>{{ id }}</VpcSecurityGroupId>
<Status>active</Status>
</VpcSecurityGroup>
{% endfor %}
</VpcSecurityGroups>
<HostedZoneId>{{ cluster.hosted_zone_id }}</HostedZoneId>
<StorageEncrypted>false</StorageEncrypted>
<DbClusterResourceId>{{ cluster.resource_id }}</DbClusterResourceId>
<DBClusterArn>{{ cluster.arn }}</DBClusterArn>
<AssociatedRoles></AssociatedRoles>
<IAMDatabaseAuthenticationEnabled>false</IAMDatabaseAuthenticationEnabled>
<EngineMode>{{ cluster.engine_mode }}</EngineMode>
<DeletionProtection>false</DeletionProtection>
<HttpEndpointEnabled>false</HttpEndpointEnabled>
<CopyTagsToSnapshot>false</CopyTagsToSnapshot>
<CrossAccountClone>false</CrossAccountClone>
<DomainMemberships></DomainMemberships>
<TagList></TagList>
</DBCluster>"""
)
return template.render(cluster=self)
class Database(CloudFormationModel): class Database(CloudFormationModel):
SUPPORTED_FILTERS = { SUPPORTED_FILTERS = {
@ -132,6 +247,7 @@ class Database(CloudFormationModel):
) )
self.license_model = kwargs.get("license_model", "general-public-license") self.license_model = kwargs.get("license_model", "general-public-license")
self.option_group_name = kwargs.get("option_group_name", None) self.option_group_name = kwargs.get("option_group_name", None)
self.option_group_supplied = self.option_group_name is not None
if ( if (
self.option_group_name self.option_group_name
and self.option_group_name not in rds2_backends[self.region].option_groups and self.option_group_name not in rds2_backends[self.region].option_groups
@ -834,6 +950,7 @@ class RDS2Backend(BaseBackend):
self.arn_regex = re_compile( self.arn_regex = re_compile(
r"^arn:aws:rds:.*:[0-9]*:(db|es|og|pg|ri|secgrp|snapshot|subgrp):.*$" r"^arn:aws:rds:.*:[0-9]*:(db|es|og|pg|ri|secgrp|snapshot|subgrp):.*$"
) )
self.clusters = OrderedDict()
self.databases = OrderedDict() self.databases = OrderedDict()
self.snapshots = OrderedDict() self.snapshots = OrderedDict()
self.db_parameter_groups = {} self.db_parameter_groups = {}
@ -946,6 +1063,23 @@ class RDS2Backend(BaseBackend):
database = self.describe_databases(db_instance_identifier)[0] database = self.describe_databases(db_instance_identifier)[0]
return database return database
def restore_db_instance_from_db_snapshot(self, from_snapshot_id, overrides):
snapshot = self.describe_snapshots(
db_instance_identifier=None, db_snapshot_identifier=from_snapshot_id
)[0]
original_database = snapshot.database
new_instance_props = copy.deepcopy(original_database.__dict__)
if not original_database.option_group_supplied:
# If the option group is not supplied originally, the 'option_group_name' will receive a default value
# Force this reconstruction, and prevent any validation on the default value
del new_instance_props["option_group_name"]
for key, value in overrides.items():
if value:
new_instance_props[key] = value
return self.create_database(new_instance_props)
def stop_database(self, db_instance_identifier, db_snapshot_identifier=None): def stop_database(self, db_instance_identifier, db_snapshot_identifier=None):
database = self.describe_databases(db_instance_identifier)[0] database = self.describe_databases(db_instance_identifier)[0]
# todo: certain rds types not allowed to be stopped at this time. # todo: certain rds types not allowed to be stopped at this time.
@ -1283,6 +1417,47 @@ class RDS2Backend(BaseBackend):
return db_parameter_group return db_parameter_group
def create_db_cluster(self, kwargs):
cluster_id = kwargs["db_cluster_identifier"]
cluster = Cluster(**kwargs)
self.clusters[cluster_id] = cluster
initial_state = copy.deepcopy(cluster) # Return status=creating
cluster.status = "available" # Already set the final status in the background
return initial_state
def describe_db_clusters(self, cluster_identifier):
if cluster_identifier:
return [self.clusters[cluster_identifier]]
return self.clusters.values()
def delete_db_cluster(self, cluster_identifier):
return self.clusters.pop(cluster_identifier)
def start_db_cluster(self, cluster_identifier):
if cluster_identifier not in self.clusters:
raise DBClusterNotFoundError(cluster_identifier)
cluster = self.clusters[cluster_identifier]
if cluster.status != "stopped":
raise InvalidDBClusterStateFault(
"DbCluster cluster-id is not in stopped state."
)
temp_state = copy.deepcopy(cluster)
temp_state.status = "started"
cluster.status = "available" # This is the final status - already setting it in the background
return temp_state
def stop_db_cluster(self, cluster_identifier):
if cluster_identifier not in self.clusters:
raise DBClusterNotFoundError(cluster_identifier)
cluster = self.clusters[cluster_identifier]
if cluster.status not in ["available"]:
raise InvalidDBClusterStateFault(
"DbCluster cluster-id is not in available state."
)
previous_state = copy.deepcopy(cluster)
cluster.status = "stopped"
return previous_state
def list_tags_for_resource(self, arn): def list_tags_for_resource(self, arn):
if self.arn_regex.match(arn): if self.arn_regex.match(arn):
arn_breakdown = arn.split(":") arn_breakdown = arn.split(":")

View File

@ -87,6 +87,23 @@ class RDS2Response(BaseResponse):
"tags": self.unpack_complex_list_params("Tags.Tag", ("Key", "Value")), "tags": self.unpack_complex_list_params("Tags.Tag", ("Key", "Value")),
} }
def _get_db_cluster_kwargs(self):
return {
"availability_zones": self._get_multi_param(
"AvailabilityZones.AvailabilityZone"
),
"db_name": self._get_param("DatabaseName"),
"db_cluster_identifier": self._get_param("DBClusterIdentifier"),
"engine": self._get_param("Engine"),
"engine_version": self._get_param("EngineVersion"),
"engine_mode": self._get_param("EngineMode"),
"master_username": self._get_param("MasterUsername"),
"master_user_password": self._get_param("MasterUserPassword"),
"port": self._get_param("Port"),
"parameter_group": self._get_param("DBClusterParameterGroup"),
"region": self.region,
}
def unpack_complex_list_params(self, label, names): def unpack_complex_list_params(self, label, names):
unpacked_list = list() unpacked_list = list()
count = 1 count = 1
@ -195,6 +212,15 @@ class RDS2Response(BaseResponse):
template = self.response_template(DELETE_SNAPSHOT_TEMPLATE) template = self.response_template(DELETE_SNAPSHOT_TEMPLATE)
return template.render(snapshot=snapshot) return template.render(snapshot=snapshot)
def restore_db_instance_from_db_snapshot(self):
db_snapshot_identifier = self._get_param("DBSnapshotIdentifier")
db_kwargs = self._get_db_kwargs()
new_instance = self.backend.restore_db_instance_from_db_snapshot(
db_snapshot_identifier, db_kwargs
)
template = self.response_template(RESTORE_INSTANCE_FROM_SNAPSHOT_TEMPLATE)
return template.render(database=new_instance)
def list_tags_for_resource(self): def list_tags_for_resource(self):
arn = self._get_param("ResourceName") arn = self._get_param("ResourceName")
template = self.response_template(LIST_TAGS_FOR_RESOURCE_TEMPLATE) template = self.response_template(LIST_TAGS_FOR_RESOURCE_TEMPLATE)
@ -426,6 +452,36 @@ class RDS2Response(BaseResponse):
template = self.response_template(DELETE_DB_PARAMETER_GROUP_TEMPLATE) template = self.response_template(DELETE_DB_PARAMETER_GROUP_TEMPLATE)
return template.render(db_parameter_group=db_parameter_group) return template.render(db_parameter_group=db_parameter_group)
def create_db_cluster(self):
kwargs = self._get_db_cluster_kwargs()
cluster = self.backend.create_db_cluster(kwargs)
template = self.response_template(CREATE_DB_CLUSTER_TEMPLATE)
return template.render(cluster=cluster)
def describe_db_clusters(self):
_id = self._get_param("DBClusterIdentifier")
clusters = self.backend.describe_db_clusters(cluster_identifier=_id)
template = self.response_template(DESCRIBE_CLUSTERS_TEMPLATE)
return template.render(clusters=clusters)
def delete_db_cluster(self):
_id = self._get_param("DBClusterIdentifier")
cluster = self.backend.delete_db_cluster(cluster_identifier=_id)
template = self.response_template(DELETE_CLUSTER_TEMPLATE)
return template.render(cluster=cluster)
def start_db_cluster(self):
_id = self._get_param("DBClusterIdentifier")
cluster = self.backend.start_db_cluster(cluster_identifier=_id)
template = self.response_template(START_CLUSTER_TEMPLATE)
return template.render(cluster=cluster)
def stop_db_cluster(self):
_id = self._get_param("DBClusterIdentifier")
cluster = self.backend.stop_db_cluster(cluster_identifier=_id)
template = self.response_template(STOP_CLUSTER_TEMPLATE)
return template.render(cluster=cluster)
CREATE_DATABASE_TEMPLATE = """<CreateDBInstanceResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/"> CREATE_DATABASE_TEMPLATE = """<CreateDBInstanceResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/">
<CreateDBInstanceResult> <CreateDBInstanceResult>
@ -506,6 +562,24 @@ DELETE_DATABASE_TEMPLATE = """<DeleteDBInstanceResponse xmlns="http://rds.amazon
</ResponseMetadata> </ResponseMetadata>
</DeleteDBInstanceResponse>""" </DeleteDBInstanceResponse>"""
DELETE_CLUSTER_TEMPLATE = """<DeleteDBClusterResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/">
<DeleteDBClusterResult>
{{ cluster.to_xml() }}
</DeleteDBClusterResult>
<ResponseMetadata>
<RequestId>7369556f-b70d-11c3-faca-6ba18376ea1b</RequestId>
</ResponseMetadata>
</DeleteDBClusterResponse>"""
RESTORE_INSTANCE_FROM_SNAPSHOT_TEMPLATE = """<RestoreDBInstanceFromDBSnapshotResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/">
<RestoreDBInstanceFromDBSnapshotResult>
{{ database.to_xml() }}
</RestoreDBInstanceFromDBSnapshotResult>
<ResponseMetadata>
<RequestId>523e3218-afc7-11c3-90f5-f90431260ab4</RequestId>
</ResponseMetadata>
</RestoreDBInstanceFromDBSnapshotResponse>"""
CREATE_SNAPSHOT_TEMPLATE = """<CreateDBSnapshotResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/"> CREATE_SNAPSHOT_TEMPLATE = """<CreateDBSnapshotResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/">
<CreateDBSnapshotResult> <CreateDBSnapshotResult>
{{ snapshot.to_xml() }} {{ snapshot.to_xml() }}
@ -748,3 +822,47 @@ REMOVE_TAGS_FROM_RESOURCE_TEMPLATE = """<RemoveTagsFromResourceResponse xmlns="h
<RequestId>b194d9ca-a664-11e4-b688-194eaf8658fa</RequestId> <RequestId>b194d9ca-a664-11e4-b688-194eaf8658fa</RequestId>
</ResponseMetadata> </ResponseMetadata>
</RemoveTagsFromResourceResponse>""" </RemoveTagsFromResourceResponse>"""
CREATE_DB_CLUSTER_TEMPLATE = """<CreateDBClusterResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/">
<CreateDBClusterResult>
{{ cluster.to_xml() }}
</CreateDBClusterResult>
<ResponseMetadata>
<RequestId>523e3218-afc7-11c3-90f5-f90431260ab4</RequestId>
</ResponseMetadata>
</CreateDBClusterResponse>"""
DESCRIBE_CLUSTERS_TEMPLATE = """<DescribeDBClustersResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/">
<DescribeDBClustersResult>
<DBClusters>
{%- for cluster in clusters -%}
{{ cluster.to_xml() }}
{%- endfor -%}
</DBClusters>
{% if marker %}
<Marker>{{ marker }}</Marker>
{% endif %}
</DescribeDBClustersResult>
<ResponseMetadata>
<RequestId>523e3218-afc7-11c3-90f5-f90431260ab4</RequestId>
</ResponseMetadata>
</DescribeDBClustersResponse>"""
START_CLUSTER_TEMPLATE = """<StartDBClusterResponse xmlns="http://rds.amazonaws.com/doc/2014-10-31/">
<StartDBClusterResult>
{{ cluster.to_xml() }}
</StartDBClusterResult>
<ResponseMetadata>
<RequestId>523e3218-afc7-11c3-90f5-f90431260ab9</RequestId>
</ResponseMetadata>
</StartDBClusterResponse>"""
STOP_CLUSTER_TEMPLATE = """<StopDBClusterResponse xmlns="http://rds.amazonaws.com/doc/2014-10-31/">
<StopDBClusterResult>
{{ cluster.to_xml() }}
</StopDBClusterResult>
<ResponseMetadata>
<RequestId>523e3218-afc7-11c3-90f5-f90431260ab8</RequestId>
</ResponseMetadata>
</StopDBClusterResponse>"""

View File

@ -603,6 +603,88 @@ def test_delete_db_snapshot():
).should.throw(ClientError) ).should.throw(ClientError)
@mock_rds2
def test_restore_db_instance_from_db_snapshot():
conn = boto3.client("rds", region_name="us-west-2")
conn.create_db_instance(
DBInstanceIdentifier="db-primary-1",
AllocatedStorage=10,
Engine="postgres",
DBName="staging-postgres",
DBInstanceClass="db.m1.small",
MasterUsername="root",
MasterUserPassword="hunter2",
DBSecurityGroups=["my_sg"],
)
conn.describe_db_instances()["DBInstances"].should.have.length_of(1)
conn.create_db_snapshot(
DBInstanceIdentifier="db-primary-1", DBSnapshotIdentifier="snapshot-1"
)
# restore
new_instance = conn.restore_db_instance_from_db_snapshot(
DBInstanceIdentifier="db-restore-1", DBSnapshotIdentifier="snapshot-1"
)["DBInstance"]
new_instance["DBInstanceIdentifier"].should.equal("db-restore-1")
new_instance["DBInstanceClass"].should.equal("db.m1.small")
new_instance["StorageType"].should.equal("gp2")
new_instance["Engine"].should.equal("postgres")
new_instance["DBName"].should.equal("staging-postgres")
new_instance["DBParameterGroups"][0]["DBParameterGroupName"].should.equal(
"default.postgres9.3"
)
new_instance["DBSecurityGroups"].should.equal(
[{"DBSecurityGroupName": "my_sg", "Status": "active"}]
)
new_instance["Endpoint"]["Port"].should.equal(5432)
# Verify it exists
conn.describe_db_instances()["DBInstances"].should.have.length_of(2)
conn.describe_db_instances(DBInstanceIdentifier="db-restore-1")[
"DBInstances"
].should.have.length_of(1)
@mock_rds2
def test_restore_db_instance_from_db_snapshot_and_override_params():
conn = boto3.client("rds", region_name="us-west-2")
conn.create_db_instance(
DBInstanceIdentifier="db-primary-1",
AllocatedStorage=10,
Engine="postgres",
DBName="staging-postgres",
DBInstanceClass="db.m1.small",
MasterUsername="root",
MasterUserPassword="hunter2",
Port=1234,
DBSecurityGroups=["my_sg"],
)
conn.describe_db_instances()["DBInstances"].should.have.length_of(1)
conn.create_db_snapshot(
DBInstanceIdentifier="db-primary-1", DBSnapshotIdentifier="snapshot-1"
)
# restore with some updated attributes
new_instance = conn.restore_db_instance_from_db_snapshot(
DBInstanceIdentifier="db-restore-1",
DBSnapshotIdentifier="snapshot-1",
Port=10000,
VpcSecurityGroupIds=["new_vpc"],
)["DBInstance"]
new_instance["DBInstanceIdentifier"].should.equal("db-restore-1")
new_instance["DBParameterGroups"][0]["DBParameterGroupName"].should.equal(
"default.postgres9.3"
)
new_instance["DBSecurityGroups"].should.equal(
[{"DBSecurityGroupName": "my_sg", "Status": "active"}]
)
new_instance["VpcSecurityGroups"].should.equal(
[{"VpcSecurityGroupId": "new_vpc", "Status": "active"}]
)
new_instance["Endpoint"]["Port"].should.equal(10000)
@mock_rds2 @mock_rds2
def test_create_option_group(): def test_create_option_group():
conn = boto3.client("rds", region_name="us-west-2") conn = boto3.client("rds", region_name="us-west-2")

View File

@ -0,0 +1,305 @@
import boto3
import pytest
import sure # noqa
from botocore.exceptions import ClientError
from moto import mock_rds2
from moto.core import ACCOUNT_ID
@mock_rds2
def test_describe_db_cluster_initial():
client = boto3.client("rds", region_name="eu-north-1")
resp = client.describe_db_clusters()
resp.should.have.key("DBClusters").should.have.length_of(0)
@mock_rds2
def test_create_db_cluster_needs_master_username():
client = boto3.client("rds", region_name="eu-north-1")
with pytest.raises(ClientError) as ex:
client.create_db_cluster(DBClusterIdentifier="cluster-id", Engine="aurora")
err = ex.value.response["Error"]
err["Code"].should.equal("InvalidParameterValue")
err["Message"].should.equal(
"The parameter MasterUsername must be provided and must not be blank."
)
@mock_rds2
def test_create_db_cluster_needs_master_user_password():
client = boto3.client("rds", region_name="eu-north-1")
with pytest.raises(ClientError) as ex:
client.create_db_cluster(
DBClusterIdentifier="cluster-id", Engine="aurora", MasterUsername="root"
)
err = ex.value.response["Error"]
err["Code"].should.equal("InvalidParameterValue")
err["Message"].should.equal(
"The parameter MasterUserPassword must be provided and must not be blank."
)
@mock_rds2
def test_create_db_cluster_needs_long_master_user_password():
client = boto3.client("rds", region_name="eu-north-1")
with pytest.raises(ClientError) as ex:
client.create_db_cluster(
DBClusterIdentifier="cluster-id",
Engine="aurora",
MasterUsername="root",
MasterUserPassword="hunter2",
)
err = ex.value.response["Error"]
err["Code"].should.equal("InvalidParameterValue")
err["Message"].should.equal(
"The parameter MasterUserPassword is not a valid password because it is shorter than 8 characters."
)
@mock_rds2
def test_create_db_cluster__verify_default_properties():
client = boto3.client("rds", region_name="eu-north-1")
resp = client.create_db_cluster(
DBClusterIdentifier="cluster-id",
Engine="aurora",
MasterUsername="root",
MasterUserPassword="hunter2_",
)
resp.should.have.key("DBCluster")
cluster = resp["DBCluster"]
cluster.shouldnt.have.key(
"DatabaseName"
) # This was not supplied, so should not be returned
cluster.should.have.key("AllocatedStorage").equal(1)
cluster.should.have.key("AvailabilityZones")
set(cluster["AvailabilityZones"]).should.equal(
{"eu-north-1a", "eu-north-1b", "eu-north-1c"}
)
cluster.should.have.key("BackupRetentionPeriod").equal(1)
cluster.should.have.key("DBClusterIdentifier").equal("cluster-id")
cluster.should.have.key("DBClusterParameterGroup").equal("default.aurora5.6")
cluster.should.have.key("DBSubnetGroup").equal("default")
cluster.should.have.key("Status").equal("creating")
cluster.should.have.key("Endpoint").match(
"cluster-id.cluster-[a-z0-9]{12}.eu-north-1.rds.amazonaws.com"
)
endpoint = cluster["Endpoint"]
expected_readonly = endpoint.replace(
"cluster-id.cluster-", "cluster-id.cluster-ro-"
)
cluster.should.have.key("ReaderEndpoint").equal(expected_readonly)
cluster.should.have.key("MultiAZ").equal(False)
cluster.should.have.key("Engine").equal("aurora")
cluster.should.have.key("EngineVersion").equal("5.6.mysql_aurora.1.22.5")
cluster.should.have.key("Port").equal(3306)
cluster.should.have.key("MasterUsername").equal("root")
cluster.should.have.key("PreferredBackupWindow").equal("01:37-02:07")
cluster.should.have.key("PreferredMaintenanceWindow").equal("wed:02:40-wed:03:10")
cluster.should.have.key("ReadReplicaIdentifiers").equal([])
cluster.should.have.key("DBClusterMembers").equal([])
cluster.should.have.key("VpcSecurityGroups")
cluster.should.have.key("HostedZoneId")
cluster.should.have.key("StorageEncrypted").equal(False)
cluster.should.have.key("DbClusterResourceId").match(r"cluster-[A-Z0-9]{26}")
cluster.should.have.key("DBClusterArn").equal(
f"arn:aws:rds:eu-north-1:{ACCOUNT_ID}:cluster:cluster-id"
)
cluster.should.have.key("AssociatedRoles").equal([])
cluster.should.have.key("IAMDatabaseAuthenticationEnabled").equal(False)
cluster.should.have.key("EngineMode").equal("provisioned")
cluster.should.have.key("DeletionProtection").equal(False)
cluster.should.have.key("HttpEndpointEnabled").equal(False)
cluster.should.have.key("CopyTagsToSnapshot").equal(False)
cluster.should.have.key("CrossAccountClone").equal(False)
cluster.should.have.key("DomainMemberships").equal([])
cluster.should.have.key("TagList").equal([])
cluster.should.have.key("ClusterCreateTime")
@mock_rds2
def test_create_db_cluster_with_database_name():
client = boto3.client("rds", region_name="eu-north-1")
resp = client.create_db_cluster(
DBClusterIdentifier="cluster-id",
DatabaseName="users",
Engine="aurora",
MasterUsername="root",
MasterUserPassword="hunter2_",
)
cluster = resp["DBCluster"]
cluster.should.have.key("DatabaseName").equal("users")
cluster.should.have.key("DBClusterIdentifier").equal("cluster-id")
cluster.should.have.key("DBClusterParameterGroup").equal("default.aurora5.6")
@mock_rds2
def test_create_db_cluster_additional_parameters():
client = boto3.client("rds", region_name="eu-north-1")
resp = client.create_db_cluster(
AvailabilityZones=["eu-north-1b"],
DBClusterIdentifier="cluster-id",
Engine="aurora",
EngineVersion="5.6.mysql_aurora.1.19.2",
EngineMode="serverless",
MasterUsername="root",
MasterUserPassword="hunter2_",
Port=1234,
)
cluster = resp["DBCluster"]
cluster.should.have.key("AvailabilityZones").equal(["eu-north-1b"])
cluster.should.have.key("Engine").equal("aurora")
cluster.should.have.key("EngineVersion").equal("5.6.mysql_aurora.1.19.2")
cluster.should.have.key("EngineMode").equal("serverless")
cluster.should.have.key("Port").equal(1234)
@mock_rds2
def test_describe_db_cluster_after_creation():
client = boto3.client("rds", region_name="eu-north-1")
client.create_db_cluster(
DBClusterIdentifier="cluster-id1",
Engine="aurora",
MasterUsername="root",
MasterUserPassword="hunter2_",
)
client.create_db_cluster(
DBClusterIdentifier="cluster-id2",
Engine="aurora",
MasterUsername="root",
MasterUserPassword="hunter2_",
)
client.describe_db_clusters()["DBClusters"].should.have.length_of(2)
client.describe_db_clusters(DBClusterIdentifier="cluster-id2")[
"DBClusters"
].should.have.length_of(1)
@mock_rds2
def test_delete_db_cluster():
client = boto3.client("rds", region_name="eu-north-1")
client.create_db_cluster(
DBClusterIdentifier="cluster-id",
Engine="aurora",
MasterUsername="root",
MasterUserPassword="hunter2_",
)
client.delete_db_cluster(DBClusterIdentifier="cluster-id")
client.describe_db_clusters()["DBClusters"].should.have.length_of(0)
@mock_rds2
def test_start_db_cluster_unknown_cluster():
client = boto3.client("rds", region_name="eu-north-1")
with pytest.raises(ClientError) as ex:
client.start_db_cluster(DBClusterIdentifier="cluster-unknown")
err = ex.value.response["Error"]
err["Code"].should.equal("DBClusterNotFoundFault")
err["Message"].should.equal("DBCluster cluster-unknown not found.")
@mock_rds2
def test_start_db_cluster_after_stopping():
client = boto3.client("rds", region_name="eu-north-1")
client.create_db_cluster(
DBClusterIdentifier="cluster-id",
Engine="aurora",
MasterUsername="root",
MasterUserPassword="hunter2_",
)
client.stop_db_cluster(DBClusterIdentifier="cluster-id")
client.start_db_cluster(DBClusterIdentifier="cluster-id")
cluster = client.describe_db_clusters()["DBClusters"][0]
cluster["Status"].should.equal("available")
@mock_rds2
def test_start_db_cluster_without_stopping():
client = boto3.client("rds", region_name="eu-north-1")
client.create_db_cluster(
DBClusterIdentifier="cluster-id",
Engine="aurora",
MasterUsername="root",
MasterUserPassword="hunter2_",
)
with pytest.raises(ClientError) as ex:
client.start_db_cluster(DBClusterIdentifier="cluster-id")
err = ex.value.response["Error"]
err["Code"].should.equal("InvalidDBClusterStateFault")
err["Message"].should.equal("DbCluster cluster-id is not in stopped state.")
@mock_rds2
def test_stop_db_cluster():
client = boto3.client("rds", region_name="eu-north-1")
client.create_db_cluster(
DBClusterIdentifier="cluster-id",
Engine="aurora",
MasterUsername="root",
MasterUserPassword="hunter2_",
)
resp = client.stop_db_cluster(DBClusterIdentifier="cluster-id")
# Quirk of the AWS implementation - the immediate response show it's still available
cluster = resp["DBCluster"]
cluster["Status"].should.equal("available")
# For some time the status will be 'stopping'
# And finally it will be 'stopped'
cluster = client.describe_db_clusters()["DBClusters"][0]
cluster["Status"].should.equal("stopped")
@mock_rds2
def test_stop_db_cluster_already_stopped():
client = boto3.client("rds", region_name="eu-north-1")
client.create_db_cluster(
DBClusterIdentifier="cluster-id",
Engine="aurora",
MasterUsername="root",
MasterUserPassword="hunter2_",
)
client.stop_db_cluster(DBClusterIdentifier="cluster-id")
# can't call stop on a stopped cluster
with pytest.raises(ClientError) as ex:
client.stop_db_cluster(DBClusterIdentifier="cluster-id")
err = ex.value.response["Error"]
err["Code"].should.equal("InvalidDBClusterStateFault")
err["Message"].should.equal("DbCluster cluster-id is not in available state.")
@mock_rds2
def test_stop_db_cluster_unknown_cluster():
client = boto3.client("rds", region_name="eu-north-1")
with pytest.raises(ClientError) as ex:
client.stop_db_cluster(DBClusterIdentifier="cluster-unknown")
err = ex.value.response["Error"]
err["Code"].should.equal("DBClusterNotFoundFault")
err["Message"].should.equal("DBCluster cluster-unknown not found.")