diff --git a/moto/rds2/exceptions.py b/moto/rds2/exceptions.py
index 8866b9bfd..52e5b91bf 100644
--- a/moto/rds2/exceptions.py
+++ b/moto/rds2/exceptions.py
@@ -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),
+ )
diff --git a/moto/rds2/models.py b/moto/rds2/models.py
index d4ffaca26..f77323ca9 100644
--- a/moto/rds2/models.py
+++ b/moto/rds2/models.py
@@ -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(
+ """
+ 1
+
+ {% for zone in cluster.availability_zones %}
+ {{ zone }}
+ {% endfor %}
+
+ 1
+ {{ cluster.status }}
+ {% if cluster.db_name %}{{ cluster.db_name }}{% endif %}
+ {{ cluster.db_cluster_identifier }}
+ {{ cluster.parameter_group }}
+ {{ cluster.subnet_group }}
+ {{ cluster.cluster_create_time }}
+ {{ cluster.engine }}
+ {{ cluster.status }}
+ {{ cluster.endpoint }}
+ {{ cluster.reader_endpoint }}
+ false
+ {{ cluster.engine_version }}
+ {{ cluster.port }}
+ {{ cluster.master_username }}
+ {{ cluster.preferred_backup_window }}
+ {{ cluster.preferred_maintenance_window }}
+
+
+
+ {% for id in cluster.vpc_security_groups %}
+
+ {{ id }}
+ active
+
+ {% endfor %}
+
+ {{ cluster.hosted_zone_id }}
+ false
+ {{ cluster.resource_id }}
+ {{ cluster.arn }}
+
+ false
+ {{ cluster.engine_mode }}
+ false
+ false
+ false
+ false
+
+
+ """
+ )
+ 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(":")
diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py
index ed1a9a628..fd16da643 100644
--- a/moto/rds2/responses.py
+++ b/moto/rds2/responses.py
@@ -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 = """
@@ -506,6 +562,24 @@ DELETE_DATABASE_TEMPLATE = """
+
+ {{ cluster.to_xml() }}
+
+
+ 7369556f-b70d-11c3-faca-6ba18376ea1b
+
+"""
+
+RESTORE_INSTANCE_FROM_SNAPSHOT_TEMPLATE = """
+
+ {{ database.to_xml() }}
+
+
+ 523e3218-afc7-11c3-90f5-f90431260ab4
+
+"""
+
CREATE_SNAPSHOT_TEMPLATE = """
{{ snapshot.to_xml() }}
@@ -748,3 +822,47 @@ REMOVE_TAGS_FROM_RESOURCE_TEMPLATE = """
+
+ {{ cluster.to_xml() }}
+
+
+ 523e3218-afc7-11c3-90f5-f90431260ab4
+
+"""
+
+DESCRIBE_CLUSTERS_TEMPLATE = """
+
+
+ {%- for cluster in clusters -%}
+ {{ cluster.to_xml() }}
+ {%- endfor -%}
+
+ {% if marker %}
+ {{ marker }}
+ {% endif %}
+
+
+ 523e3218-afc7-11c3-90f5-f90431260ab4
+
+"""
+
+START_CLUSTER_TEMPLATE = """
+
+ {{ cluster.to_xml() }}
+
+
+ 523e3218-afc7-11c3-90f5-f90431260ab9
+
+"""
+
+STOP_CLUSTER_TEMPLATE = """
+
+ {{ cluster.to_xml() }}
+
+
+ 523e3218-afc7-11c3-90f5-f90431260ab8
+
+"""
diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py
index 93fe11380..435e8437d 100644
--- a/tests/test_rds2/test_rds2.py
+++ b/tests/test_rds2/test_rds2.py
@@ -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")
diff --git a/tests/test_rds2/test_rds2_clusters.py b/tests/test_rds2/test_rds2_clusters.py
new file mode 100644
index 000000000..29fbf9486
--- /dev/null
+++ b/tests/test_rds2/test_rds2_clusters.py
@@ -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.")