RDS - Cluster-methods + restore_db_from_snapshot (#4247)
This commit is contained in:
parent
4e45152d86
commit
4795888fda
@ -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),
|
||||
)
|
||||
|
@ -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(":")
|
||||
|
@ -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>"""
|
||||
|
@ -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")
|
||||
|
305
tests/test_rds2/test_rds2_clusters.py
Normal file
305
tests/test_rds2/test_rds2_clusters.py
Normal 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.")
|
Loading…
Reference in New Issue
Block a user