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__(
"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 datetime
import os
import random
import string
from collections import defaultdict
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 .exceptions import (
RDSClientError,
DBClusterNotFoundError,
DBInstanceNotFoundError,
DBSnapshotNotFoundError,
DBSecurityGroupNotFoundError,
@ -27,10 +30,122 @@ from .exceptions import (
DBSnapshotAlreadyExistsError,
InvalidParameterValue,
InvalidParameterCombination,
InvalidDBClusterStateFault,
)
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):
SUPPORTED_FILTERS = {
@ -132,6 +247,7 @@ class Database(CloudFormationModel):
)
self.license_model = kwargs.get("license_model", "general-public-license")
self.option_group_name = kwargs.get("option_group_name", None)
self.option_group_supplied = self.option_group_name is not None
if (
self.option_group_name
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(
r"^arn:aws:rds:.*:[0-9]*:(db|es|og|pg|ri|secgrp|snapshot|subgrp):.*$"
)
self.clusters = OrderedDict()
self.databases = OrderedDict()
self.snapshots = OrderedDict()
self.db_parameter_groups = {}
@ -946,6 +1063,23 @@ class RDS2Backend(BaseBackend):
database = self.describe_databases(db_instance_identifier)[0]
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):
database = self.describe_databases(db_instance_identifier)[0]
# todo: certain rds types not allowed to be stopped at this time.
@ -1283,6 +1417,47 @@ class RDS2Backend(BaseBackend):
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):
if self.arn_regex.match(arn):
arn_breakdown = arn.split(":")

View File

@ -87,6 +87,23 @@ class RDS2Response(BaseResponse):
"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):
unpacked_list = list()
count = 1
@ -195,6 +212,15 @@ class RDS2Response(BaseResponse):
template = self.response_template(DELETE_SNAPSHOT_TEMPLATE)
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):
arn = self._get_param("ResourceName")
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)
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/">
<CreateDBInstanceResult>
@ -506,6 +562,24 @@ DELETE_DATABASE_TEMPLATE = """<DeleteDBInstanceResponse xmlns="http://rds.amazon
</ResponseMetadata>
</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/">
<CreateDBSnapshotResult>
{{ snapshot.to_xml() }}
@ -748,3 +822,47 @@ REMOVE_TAGS_FROM_RESOURCE_TEMPLATE = """<RemoveTagsFromResourceResponse xmlns="h
<RequestId>b194d9ca-a664-11e4-b688-194eaf8658fa</RequestId>
</ResponseMetadata>
</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)
@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
def test_create_option_group():
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.")