Repository: getmoto/moto

Issue:
Feature Request: RDS - create_db_cluster
Unable to mock a create_db_cluster with Moto.  Noticied it's not covered in the responses.py under rds or rds2.

`self = <moto.rds.responses.RDSResponse object at 0x118ea5048>

    def call_action(self):
        headers = self.response_headers
    
        try:
            self._authenticate_and_authorize_normal_action()
        except HTTPException as http_error:
            response = http_error.description, dict(status=http_error.code)
            return self._send_response(headers, response)
    
        action = camelcase_to_underscores(self._get_action())
        method_names = method_names_from_class(self.__class__)
        if action in method_names:
            method = getattr(self, action)
            try:
                response = method()
            except HTTPException as http_error:
                response = http_error.description, dict(status=http_error.code)
    
            if isinstance(response, six.string_types):
                return 200, headers, response
            else:
                return self._send_response(headers, response)
    
        if not action:
            return 404, headers, ""
    
        raise NotImplementedError(
>           "The {0} action has not been implemented".format(action)
        )
E       NotImplementedError: The create_db_cluster action has not been implemented`


Discussion:
Thanks for raising this @dhammond22222 - marking this as an enhancement

Resolving patch:
diff --git a/moto/rds2/exceptions.py b/moto/rds2/exceptions.py
--- a/moto/rds2/exceptions.py
+++ b/moto/rds2/exceptions.py
@@ -120,3 +120,16 @@ def __init__(self, message):
         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
--- a/moto/rds2/models.py
+++ b/moto/rds2/models.py
@@ -3,6 +3,8 @@
 import copy
 import datetime
 import os
+import random
+import string
 
 from collections import defaultdict
 from boto3 import Session
@@ -15,6 +17,7 @@
 from moto.ec2.models import ec2_backends
 from .exceptions import (
     RDSClientError,
+    DBClusterNotFoundError,
     DBInstanceNotFoundError,
     DBSnapshotNotFoundError,
     DBSecurityGroupNotFoundError,
@@ -27,10 +30,122 @@
     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 @@ def __init__(self, **kwargs):
         )
         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 @@ def __init__(self, region):
         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 = {}
@@ -937,6 +1054,23 @@ def reboot_db_instance(self, db_instance_identifier):
         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.
@@ -1274,6 +1408,47 @@ def modify_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):
         if self.arn_regex.match(arn):
             arn_breakdown = arn.split(":")
diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py
--- a/moto/rds2/responses.py
+++ b/moto/rds2/responses.py
@@ -87,6 +87,23 @@ def _get_db_parameter_group_kwargs(self):
             "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 @@ def delete_db_snapshot(self):
         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 @@ def delete_db_parameter_group(self):
         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 @@ def delete_db_parameter_group(self):
   </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 @@ def delete_db_parameter_group(self):
     <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>"""


Test patch:
diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py
--- 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
--- /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.")