ElastiCache: Add the create_cache_cluster, delete_cache_cluster and describe_cache_clusters methods support (#6754)

This commit is contained in:
Jean-Frederic Mainville 2023-09-02 09:00:25 -04:00 committed by GitHub
parent 4bc41ae6f2
commit d78db35f41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 928 additions and 29 deletions

View File

@ -33,7 +33,7 @@ elasticache
- [ ] batch_stop_update_action
- [ ] complete_migration
- [ ] copy_snapshot
- [ ] create_cache_cluster
- [x] create_cache_cluster
- [ ] create_cache_parameter_group
- [ ] create_cache_security_group
- [ ] create_cache_subnet_group
@ -44,7 +44,7 @@ elasticache
- [ ] create_user_group
- [ ] decrease_node_groups_in_global_replication_group
- [ ] decrease_replica_count
- [ ] delete_cache_cluster
- [x] delete_cache_cluster
- [ ] delete_cache_parameter_group
- [ ] delete_cache_security_group
- [ ] delete_cache_subnet_group
@ -53,7 +53,7 @@ elasticache
- [ ] delete_snapshot
- [X] delete_user
- [ ] delete_user_group
- [ ] describe_cache_clusters
- [x] describe_cache_clusters
- [ ] describe_cache_engine_versions
- [ ] describe_cache_parameter_groups
- [ ] describe_cache_parameters
@ -70,10 +70,10 @@ elasticache
- [ ] describe_update_actions
- [ ] describe_user_groups
- [X] describe_users
Only the `user_id` parameter is currently supported.
Pagination is not yet implemented.
- [ ] disassociate_global_replication_group
- [ ] failover_global_replication_group

View File

@ -1,9 +1,9 @@
import importlib
import sys
from contextlib import ContextDecorator
from moto.core.models import BaseMockAWS
from typing import Any, Callable, List, Optional, TypeVar
from moto.core.models import BaseMockAWS
TEST_METHOD = TypeVar("TEST_METHOD", bound=Callable[..., Any])
@ -189,6 +189,9 @@ mock_xray = lazy_load(".xray", "mock_xray")
mock_xray_client = lazy_load(".xray", "mock_xray_client")
mock_wafv2 = lazy_load(".wafv2", "mock_wafv2")
mock_textract = lazy_load(".textract", "mock_textract")
mock_elasticache = lazy_load(
".elasticache", "mock_elasticache", boto3_name="elasticache"
)
class MockAll(ContextDecorator):

View File

@ -1,4 +1,5 @@
from typing import Any
from moto.core.exceptions import RESTError
EXCEPTION_RESPONSE = """<?xml version="1.0"?>
@ -58,3 +59,25 @@ class UserNotFound(ElastiCacheException):
def __init__(self, user_id: str):
super().__init__("UserNotFound", message=f"User {user_id} not found.")
class CacheClusterAlreadyExists(ElastiCacheException):
code = 404
def __init__(self, cache_cluster_id: str):
super().__init__(
"CacheClusterAlreadyExists",
message=f"Cache cluster {cache_cluster_id} already exists.",
),
class CacheClusterNotFound(ElastiCacheException):
code = 404
def __init__(self, cache_cluster_id: str):
super().__init__(
"CacheClusterNotFound",
message=f"Cache cluster {cache_cluster_id} not found.",
)

View File

@ -1,7 +1,15 @@
from typing import List, Optional
from datetime import datetime
from typing import List, Optional, Dict, Any, Tuple
from moto.core import BaseBackend, BackendDict, BaseModel
from .exceptions import UserAlreadyExists, UserNotFound
from .exceptions import (
UserAlreadyExists,
UserNotFound,
CacheClusterAlreadyExists,
CacheClusterNotFound,
)
from ..moto_api._internal import mock_random
class User(BaseModel):
@ -29,6 +37,93 @@ class User(BaseModel):
self.arn = f"arn:aws:elasticache:{self.region}:{account_id}:user:{self.id}"
class CacheCluster(BaseModel):
def __init__(
self,
account_id: str,
region_name: str,
cache_cluster_id: str,
replication_group_id: Optional[str],
az_mode: Optional[str],
preferred_availability_zone: Optional[str],
num_cache_nodes: Optional[int],
cache_node_type: Optional[str],
engine: Optional[str],
engine_version: Optional[str],
cache_parameter_group_name: Optional[str],
cache_subnet_group_name: Optional[str],
transit_encryption_enabled: Optional[bool],
network_type: Optional[str],
ip_discovery: Optional[str],
snapshot_name: Optional[str],
preferred_maintenance_window: Optional[str],
port: Optional[int],
notification_topic_arn: Optional[str],
auto_minor_version_upgrade: Optional[bool],
snapshot_retention_limit: Optional[int],
snapshot_window: Optional[str],
auth_token: Optional[str],
outpost_mode: Optional[str],
preferred_outpost_arn: Optional[str],
preferred_availability_zones: Optional[List[str]],
cache_security_group_names: Optional[List[str]],
security_group_ids: Optional[List[str]],
tags: Optional[List[Dict[str, str]]],
snapshot_arns: Optional[List[str]],
preferred_outpost_arns: Optional[List[str]],
log_delivery_configurations: List[Dict[str, Any]],
cache_node_ids_to_remove: Optional[List[str]],
cache_node_ids_to_reboot: Optional[List[str]],
):
if tags is None:
tags = []
self.cache_cluster_id = cache_cluster_id
self.az_mode = az_mode
self.preferred_availability_zone = preferred_availability_zone
self.preferred_availability_zones = preferred_availability_zones or []
self.engine = engine or "redis"
self.engine_version = engine_version
if engine == "redis":
self.num_cache_nodes = 1
self.replication_group_id = replication_group_id
self.snapshot_arns = snapshot_arns or []
self.snapshot_name = snapshot_name
self.snapshot_window = snapshot_window
if engine == "memcached":
if num_cache_nodes is None:
self.num_cache_nodes = 1
elif 1 <= num_cache_nodes <= 40:
self.num_cache_nodes = num_cache_nodes
self.cache_node_type = cache_node_type
self.cache_parameter_group_name = cache_parameter_group_name
self.cache_subnet_group_name = cache_subnet_group_name
self.cache_security_group_names = cache_security_group_names or []
self.security_group_ids = security_group_ids or []
self.tags = tags
self.preferred_maintenance_window = preferred_maintenance_window
self.port = port or 6379
self.notification_topic_arn = notification_topic_arn
self.auto_minor_version_upgrade = auto_minor_version_upgrade
self.snapshot_retention_limit = snapshot_retention_limit or 0
self.auth_token = auth_token
self.outpost_mode = outpost_mode
self.preferred_outpost_arn = preferred_outpost_arn
self.preferred_outpost_arns = preferred_outpost_arns or []
self.log_delivery_configurations = log_delivery_configurations or []
self.transit_encryption_enabled = transit_encryption_enabled
self.network_type = network_type
self.ip_discovery = ip_discovery
self.cache_node_ids_to_remove = cache_node_ids_to_remove
self.cache_node_ids_to_reboot = cache_node_ids_to_reboot
self.cache_cluster_create_time = datetime.utcnow()
self.auth_token_last_modified_date = datetime.utcnow()
self.cache_cluster_status = "available"
self.arn = f"arn:aws:elasticache:{region_name}:{account_id}:{cache_cluster_id}"
self.cache_node_id = str(mock_random.uuid4())
class ElastiCacheBackend(BaseBackend):
"""Implementation of ElastiCache APIs."""
@ -45,6 +140,45 @@ class ElastiCacheBackend(BaseBackend):
no_password_required=True,
)
# Define the cache_clusters dictionary to detect duplicates
self.cache_clusters = dict()
self.cache_clusters["default"] = CacheCluster(
account_id=self.account_id,
region_name=self.region_name,
cache_cluster_id="default",
replication_group_id=None,
az_mode=None,
preferred_availability_zone=None,
num_cache_nodes=1,
cache_node_type=None,
engine="redis",
engine_version=None,
cache_parameter_group_name=None,
cache_subnet_group_name=None,
transit_encryption_enabled=True,
network_type=None,
ip_discovery=None,
snapshot_name=None,
preferred_maintenance_window=None,
port=6379,
notification_topic_arn=None,
auto_minor_version_upgrade=True,
snapshot_retention_limit=0,
snapshot_window=None,
auth_token=None,
outpost_mode=None,
preferred_outpost_arn=None,
preferred_availability_zones=[],
cache_security_group_names=[],
security_group_ids=[],
tags=[],
snapshot_arns=[],
preferred_outpost_arns=[],
log_delivery_configurations=[],
cache_node_ids_to_remove=[],
cache_node_ids_to_reboot=[],
)
def create_user(
self,
user_id: str,
@ -92,5 +226,109 @@ class ElastiCacheBackend(BaseBackend):
raise UserNotFound(user_id)
return list(self.users.values())
def create_cache_cluster(
self,
cache_cluster_id: str,
replication_group_id: str,
az_mode: str,
preferred_availability_zone: str,
num_cache_nodes: int,
cache_node_type: str,
engine: str,
engine_version: str,
cache_parameter_group_name: str,
cache_subnet_group_name: str,
transit_encryption_enabled: bool,
network_type: str,
ip_discovery: str,
snapshot_name: str,
preferred_maintenance_window: str,
port: int,
notification_topic_arn: str,
auto_minor_version_upgrade: bool,
snapshot_retention_limit: int,
snapshot_window: str,
auth_token: str,
outpost_mode: str,
preferred_outpost_arn: str,
preferred_availability_zones: List[str],
cache_security_group_names: List[str],
security_group_ids: List[str],
tags: List[Dict[str, str]],
snapshot_arns: List[str],
preferred_outpost_arns: List[str],
log_delivery_configurations: List[Dict[str, Any]],
cache_node_ids_to_remove: List[str],
cache_node_ids_to_reboot: List[str],
) -> CacheCluster:
if cache_cluster_id in self.cache_clusters:
raise CacheClusterAlreadyExists(cache_cluster_id)
cache_cluster = CacheCluster(
account_id=self.account_id,
region_name=self.region_name,
cache_cluster_id=cache_cluster_id,
replication_group_id=replication_group_id,
az_mode=az_mode,
preferred_availability_zone=preferred_availability_zone,
preferred_availability_zones=preferred_availability_zones,
num_cache_nodes=num_cache_nodes,
cache_node_type=cache_node_type,
engine=engine,
engine_version=engine_version,
cache_parameter_group_name=cache_parameter_group_name,
cache_subnet_group_name=cache_subnet_group_name,
cache_security_group_names=cache_security_group_names,
security_group_ids=security_group_ids,
tags=tags,
snapshot_arns=snapshot_arns,
snapshot_name=snapshot_name,
preferred_maintenance_window=preferred_maintenance_window,
port=port,
notification_topic_arn=notification_topic_arn,
auto_minor_version_upgrade=auto_minor_version_upgrade,
snapshot_retention_limit=snapshot_retention_limit,
snapshot_window=snapshot_window,
auth_token=auth_token,
outpost_mode=outpost_mode,
preferred_outpost_arn=preferred_outpost_arn,
preferred_outpost_arns=preferred_outpost_arns,
log_delivery_configurations=log_delivery_configurations,
transit_encryption_enabled=transit_encryption_enabled,
network_type=network_type,
ip_discovery=ip_discovery,
cache_node_ids_to_remove=cache_node_ids_to_remove,
cache_node_ids_to_reboot=cache_node_ids_to_reboot,
)
self.cache_clusters[cache_cluster_id] = cache_cluster
return cache_cluster
def describe_cache_clusters(
self,
cache_cluster_id: str,
max_records: int,
marker: str,
) -> Tuple[str, List[CacheCluster]]:
if marker is None:
marker = str(mock_random.uuid4())
if max_records is None:
max_records = 100
if cache_cluster_id:
if cache_cluster_id in self.cache_clusters:
cache_cluster = self.cache_clusters[cache_cluster_id]
return marker, [cache_cluster]
else:
raise CacheClusterNotFound(cache_cluster_id)
cache_clusters = list(self.cache_clusters.values())[:max_records]
return marker, cache_clusters
def delete_cache_cluster(self, cache_cluster_id: str) -> CacheCluster:
if cache_cluster_id:
if cache_cluster_id in self.cache_clusters:
cache_cluster = self.cache_clusters[cache_cluster_id]
cache_cluster.cache_cluster_status = "deleting"
return cache_cluster
raise CacheClusterNotFound(cache_cluster_id)
elasticache_backends = BackendDict(ElastiCacheBackend, "elasticache")

View File

@ -1,4 +1,5 @@
from moto.core.responses import BaseResponse
from .exceptions import PasswordTooShort, PasswordRequired
from .models import elasticache_backends, ElastiCacheBackend
@ -52,6 +53,97 @@ class ElastiCacheResponse(BaseResponse):
template = self.response_template(DESCRIBE_USERS_TEMPLATE)
return template.render(users=users)
def create_cache_cluster(self) -> str:
cache_cluster_id = self._get_param("CacheClusterId")
replication_group_id = self._get_param("ReplicationGroupId")
az_mode = self._get_param("AZMode")
preferred_availability_zone = self._get_param("PreferredAvailabilityZone")
preferred_availability_zones = self._get_param("PreferredAvailabilityZones")
num_cache_nodes = self._get_int_param("NumCacheNodes")
cache_node_type = self._get_param("CacheNodeType")
engine = self._get_param("Engine")
engine_version = self._get_param("EngineVersion")
cache_parameter_group_name = self._get_param("CacheParameterGroupName")
cache_subnet_group_name = self._get_param("CacheSubnetGroupName")
cache_security_group_names = self._get_param("CacheSecurityGroupNames")
security_group_ids = self._get_param("SecurityGroupIds")
tags = self._get_param("Tags")
snapshot_arns = self._get_param("SnapshotArns")
snapshot_name = self._get_param("SnapshotName")
preferred_maintenance_window = self._get_param("PreferredMaintenanceWindow")
port = self._get_param("Port")
notification_topic_arn = self._get_param("NotificationTopicArn")
auto_minor_version_upgrade = self._get_bool_param("AutoMinorVersionUpgrade")
snapshot_retention_limit = self._get_int_param("SnapshotRetentionLimit")
snapshot_window = self._get_param("SnapshotWindow")
auth_token = self._get_param("AuthToken")
outpost_mode = self._get_param("OutpostMode")
preferred_outpost_arn = self._get_param("PreferredOutpostArn")
preferred_outpost_arns = self._get_param("PreferredOutpostArns")
log_delivery_configurations = self._get_param("LogDeliveryConfigurations")
transit_encryption_enabled = self._get_bool_param("TransitEncryptionEnabled")
network_type = self._get_param("NetworkType")
ip_discovery = self._get_param("IpDiscovery")
# Define the following attributes as they're included in the response even during creation of a cache cluster
cache_node_ids_to_remove = self._get_param("CacheNodeIdsToRemove", [])
cache_node_ids_to_reboot = self._get_param("CacheNodeIdsToReboot", [])
cache_cluster = self.elasticache_backend.create_cache_cluster(
cache_cluster_id=cache_cluster_id,
replication_group_id=replication_group_id,
az_mode=az_mode,
preferred_availability_zone=preferred_availability_zone,
preferred_availability_zones=preferred_availability_zones,
num_cache_nodes=num_cache_nodes,
cache_node_type=cache_node_type,
engine=engine,
engine_version=engine_version,
cache_parameter_group_name=cache_parameter_group_name,
cache_subnet_group_name=cache_subnet_group_name,
cache_security_group_names=cache_security_group_names,
security_group_ids=security_group_ids,
tags=tags,
snapshot_arns=snapshot_arns,
snapshot_name=snapshot_name,
preferred_maintenance_window=preferred_maintenance_window,
port=port,
notification_topic_arn=notification_topic_arn,
auto_minor_version_upgrade=auto_minor_version_upgrade,
snapshot_retention_limit=snapshot_retention_limit,
snapshot_window=snapshot_window,
auth_token=auth_token,
outpost_mode=outpost_mode,
preferred_outpost_arn=preferred_outpost_arn,
preferred_outpost_arns=preferred_outpost_arns,
log_delivery_configurations=log_delivery_configurations,
transit_encryption_enabled=transit_encryption_enabled,
network_type=network_type,
ip_discovery=ip_discovery,
cache_node_ids_to_remove=cache_node_ids_to_remove,
cache_node_ids_to_reboot=cache_node_ids_to_reboot,
)
template = self.response_template(CREATE_CACHE_CLUSTER_TEMPLATE)
return template.render(cache_cluster=cache_cluster)
def describe_cache_clusters(self) -> str:
cache_cluster_id = self._get_param("CacheClusterId")
max_records = self._get_int_param("MaxRecords")
marker = self._get_param("Marker")
marker, cache_clusters = self.elasticache_backend.describe_cache_clusters(
cache_cluster_id=cache_cluster_id,
marker=marker,
max_records=max_records,
)
template = self.response_template(DESCRIBE_CACHE_CLUSTERS_TEMPLATE)
return template.render(marker=marker, cache_clusters=cache_clusters)
def delete_cache_cluster(self) -> str:
cache_cluster_id = self._get_param("CacheClusterId")
cache_cluster = self.elasticache_backend.delete_cache_cluster(
cache_cluster_id=cache_cluster_id,
)
template = self.response_template(DELETE_CACHE_CLUSTER_TEMPLATE)
return template.render(cache_cluster=cache_cluster)
USER_TEMPLATE = """<UserId>{{ user.id }}</UserId>
<UserName>{{ user.name }}</UserName>
@ -74,14 +166,13 @@ USER_TEMPLATE = """<UserId>{{ user.id }}</UserId>
</Authentication>
<ARN>{{ user.arn }}</ARN>"""
CREATE_USER_TEMPLATE = (
"""<CreateUserResponse xmlns="http://elasticache.amazonaws.com/doc/2015-02-02/">
<ResponseMetadata>
<RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
</ResponseMetadata>
<CreateUserResult>
"""
<ResponseMetadata>
<RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
</ResponseMetadata>
<CreateUserResult>
"""
+ USER_TEMPLATE
+ """
</CreateUserResult>
@ -90,11 +181,11 @@ CREATE_USER_TEMPLATE = (
DELETE_USER_TEMPLATE = (
"""<DeleteUserResponse xmlns="http://elasticache.amazonaws.com/doc/2015-02-02/">
<ResponseMetadata>
<RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
</ResponseMetadata>
<DeleteUserResult>
"""
<ResponseMetadata>
<RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
</ResponseMetadata>
<DeleteUserResult>
"""
+ USER_TEMPLATE
+ """
</DeleteUserResult>
@ -103,14 +194,14 @@ DELETE_USER_TEMPLATE = (
DESCRIBE_USERS_TEMPLATE = (
"""<DescribeUsersResponse xmlns="http://elasticache.amazonaws.com/doc/2015-02-02/">
<ResponseMetadata>
<RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
</ResponseMetadata>
<DescribeUsersResult>
<Users>
{% for user in users %}
<member>
"""
<ResponseMetadata>
<RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
</ResponseMetadata>
<DescribeUsersResult>
<Users>
{% for user in users %}
<member>
"""
+ USER_TEMPLATE
+ """
</member>
@ -120,3 +211,337 @@ DESCRIBE_USERS_TEMPLATE = (
</DescribeUsersResult>
</DescribeUsersResponse>"""
)
CREATE_CACHE_CLUSTER_TEMPLATE = """<CreateCacheClusterResponse xmlns="http://elasticache.amazonaws.com/doc/2015-02-02/">
<ResponseMetadata>
<RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
</ResponseMetadata>
<CreateCacheClusterResult>
<CacheCluster>
<CacheClusterId>{{ cache_cluster.cache_cluster_id }}</CacheClusterId>
<ConfigurationEndpoint>
<Address>example.cache.amazonaws.com</Address>
<Port>{{ cache_cluster.port }}</Port>
</ConfigurationEndpoint>
<ClientDownloadLandingPage></ClientDownloadLandingPage>
<CacheNodeType>{{ cache_cluster.cache_node_type }}</CacheNodeType>
<Engine>{{ cache_cluster.engine }}</Engine>
<EngineVersion>{{ cache_cluster.engine_version }}</EngineVersion>
<CacheClusterStatus>available</CacheClusterStatus>
<NumCacheNodes>{{ cache_cluster.num_cache_nodes }}</NumCacheNodes>
<PreferredAvailabilityZone>{{ cache_cluster.preferred_availability_zone }}</PreferredAvailabilityZone>
<PreferredOutpostArn>{{ cache_cluster.preferred_outpost_arn }}</PreferredOutpostArn>
<CacheClusterCreateTime>{{ cache_cluster.cache_cluster_create_time }}</CacheClusterCreateTime>
<PreferredMaintenanceWindow>{{ cache_cluster.preferred_maintenance_window }}</PreferredMaintenanceWindow>
{% if cache_cluster.cache_node_ids_to_remove != [] %}
<PendingModifiedValues>
<NumCacheNodes>{{ cache_cluster.num_cache_nodes }}</NumCacheNodes>
{% for cache_node_id_to_remove in cache_cluster.cache_node_ids_to_remove %}
<CacheNodeIdsToRemove>{{ cache_node_id_to_remove }}</CacheNodeIdsToRemove>
{% endfor %}
<EngineVersion>{{ cache_cluster.engine_version }}</EngineVersion>
<CacheNodeType>{{ cache_cluster.cache_node_type }}</CacheNodeType>
<AuthTokenStatus>SETTING</AuthTokenStatus>
<LogDeliveryConfigurations>
{% for log_delivery_configuration in cache_cluster.log_delivery_configurations %}
<LogType>{{ log_delivery_configuration.LogType }}</LogType>
<DestinationType>{{ log_delivery_configuration.DestinationType }}</DestinationType>
<DestinationDetails>
<CloudWatchLogsDetails>
<LogGroup>{{ log_delivery_configuration.LogGroup }}</LogGroup>
</CloudWatchLogsDetails>
<KinesisFirehoseDetails>
<DeliveryStream>{{ log_delivery_configuration.DeliveryStream }}</DeliveryStream>
</KinesisFirehoseDetails>
</DestinationDetails>
<LogFormat>{{ log_delivery_configuration.LogFormat }}</LogFormat>
{% endfor %}
</LogDeliveryConfigurations>
<TransitEncryptionEnabled>{{ cache_cluster.transit_encryption_enabled }}</TransitEncryptionEnabled>
<TransitEncryptionMode>preferred</TransitEncryptionMode>
</PendingModifiedValues>
{% endif %}
<NotificationConfiguration>
<TopicArn>{{ cache_cluster.notification_topic_arn }}</TopicArn>
<TopicStatus>active</TopicStatus>
</NotificationConfiguration>
<CacheSecurityGroups>
{% for cache_security_group_name in cache_cluster.cache_security_group_names %}
<CacheSecurityGroupName>{{ cache_security_group_name }}</CacheSecurityGroupName>
{% endfor %}
<Status>active</Status>
</CacheSecurityGroups>
<CacheParameterGroup>
<CacheParameterGroupName>{{ cache_cluster.cache_parameter_group_name }}</CacheParameterGroupName>
<ParameterApplyStatus>active</ParameterApplyStatus>
{% for cache_node_id_to_reboot in cache_cluster.cache_node_ids_to_reboot %}
<CacheNodeIdsToReboot>
{{ cache_node_id_to_reboot }}
</CacheNodeIdsToReboot>
{% endfor %}
</CacheParameterGroup>
<CacheSubnetGroupName>{{ cache_cluster.cache_subnet_group_name }}</CacheSubnetGroupName>
<CacheNodes>
<CacheNodeId>{{ cache_cluster.cache_node_id }}</CacheNodeId>
<CacheNodeStatus>{{ cache_cluster.cache_node_status }}</CacheNodeStatus>
<CacheNodeCreateTime>{{ cache_cluster.cache_cluster_create_time }}</CacheNodeCreateTime>
<Endpoint>
<Address>{{ cache_cluster.address }}</Address>
<Port>{{ cache_cluster.port }}</Port>
</Endpoint>
<ParameterGroupStatus>active</ParameterGroupStatus>
<SourceCacheNodeId>{{ cache_cluster.cache_node_id }}</SourceCacheNodeId>
<CustomerAvailabilityZone>{{ cache_cluster.preferred_availability_zone }}</CustomerAvailabilityZone>
<CustomerOutpostArn>{{ cache_cluster.preferred_output_arn }}</CustomerOutpostArn>
</CacheNodes>
<AutoMinorVersionUpgrade>{{ cache_cluster.auto_minor_version_upgrade }}</AutoMinorVersionUpgrade>
<SecurityGroups>
{% for security_group_id in cache_cluster.security_group_ids %}
<SecurityGroupId>{{ security_group_id }}</SecurityGroupId>
<Status>active</Status>
{% endfor %}
</SecurityGroups>
{% if cache_cluster.engine == "redis" %}
<ReplicationGroupId>{{ cache_cluster.replication_group_id }}</ReplicationGroupId>
<SnapshotRetentionLimit>{{ cache_cluster.snapshot_retention_limit }}</SnapshotRetentionLimit>
<SnapshotWindow>{{ cache_cluster.snapshot_window }}</SnapshotWindow>
{% endif %}
<AuthTokenEnabled>true</AuthTokenEnabled>
<AuthTokenLastModifiedDate>{{ cache_cluster.cache_cluster_create_time }}</AuthTokenLastModifiedDate>
<TransitEncryptionEnabled>{{ cache_cluster.transit_encryption_enabled }}</TransitEncryptionEnabled>
<AtRestEncryptionEnabled>true</AtRestEncryptionEnabled>
<ARN>{{ cache_cluster.arn }}</ARN>
<ReplicationGroupLogDeliveryEnabled>true</ReplicationGroupLogDeliveryEnabled>
<LogDeliveryConfigurations>
{% for log_delivery_configuration in cache_cluster.log_delivery_configurations %}
<LogType>{{ log_delivery_configuration.LogType }}</LogType>
<DestinationType>{{ log_delivery_configuration.DestinationType }}</DestinationType>
<DestinationDetails>
<CloudWatchLogsDetails>
<LogGroup>{{ log_delivery_configuration.LogGroup }}</LogGroup>
</CloudWatchLogsDetails>
<KinesisFirehoseDetails>
<DeliveryStream>{{ log_delivery_configuration.DeliveryStream }}</DeliveryStream>
</KinesisFirehoseDetails>
</DestinationDetails>
<LogFormat>{{ log_delivery_configuration.LogFormat }}</LogFormat>
<Status>active</Status>
<Message></Message>
{% endfor %}
</LogDeliveryConfigurations>
<NetworkType>{{ cache_cluster.network_type }}</NetworkType>
<IpDiscovery>{{ cache_cluster.ip_discovery }}</IpDiscovery>
<TransitEncryptionMode>preferred</TransitEncryptionMode>
</CacheCluster>
</CreateCacheClusterResult>
</CreateCacheClusterResponse>"""
DESCRIBE_CACHE_CLUSTERS_TEMPLATE = """<DescribeCacheClustersResponse xmlns="http://elasticache.amazonaws.com/doc/2015-02-02/">
<ResponseMetadata>
<RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
</ResponseMetadata>
<DescribeCacheClustersResult>
<Marker>{{ marker }}</Marker>
<CacheClusters>
{% for cache_cluster in cache_clusters %}
<member>
<CacheClusterId>{{ cache_cluster.cache_cluster_id }}</CacheClusterId>
<ConfigurationEndpoint>{{ cache_cluster.configuration_endpoint }}</ConfigurationEndpoint>
<ClientDownloadLandingPage>{{ cache_cluster.client_download_landing_page }}</ClientDownloadLandingPage>
<CacheNodeType>{{ cache_cluster.cache_node_type }}</CacheNodeType>
<Engine>{{ cache_cluster.engine }}</Engine>
<EngineVersion>{{ cache_cluster.engine_version }}</EngineVersion>
<CacheClusterStatus>{{ cache_cluster.cache_cluster_status }}</CacheClusterStatus>
<NumCacheNodes>{{ cache_cluster.num_cache_nodes }}</NumCacheNodes>
<PreferredAvailabilityZone>{{ cache_cluster.preferred_availability_zone }}</PreferredAvailabilityZone>
<PreferredOutpostArn>{{ cache_cluster.preferred_outpost_arn }}</PreferredOutpostArn>
<CacheClusterCreateTime>{{ cache_cluster.cache_cluster_create_time }}</CacheClusterCreateTime>
<PreferredMaintenanceWindow>{{ cache_cluster.preferred_maintenance_window }}</PreferredMaintenanceWindow>
<PendingModifiedValues>{{ cache_cluster.pending_modified_values }}</PendingModifiedValues>
<NotificationConfiguration>{{ cache_cluster.notification_configuration }}</NotificationConfiguration>
<CacheSecurityGroups>
{% for cache_security_group in cache_cluster.cache_security_groups %}
<member>
<CacheSecurityGroupName>{{ cache_security_group.cache_security_group_name }}</CacheSecurityGroupName>
<Status>{{ cache_security_group.status }}</Status>
</member>
{% endfor %}
</CacheSecurityGroups>
<CacheParameterGroup>{{ cache_cluster.cache_parameter_group }}</CacheParameterGroup>
<CacheSubnetGroupName>{{ cache_cluster.cache_subnet_group_name }}</CacheSubnetGroupName>
<CacheNodes>
{% for cache_node in cache_cluster.cache_nodes %}
<member>
<CacheNodeId>{{ cache_node.cache_node_id }}</CacheNodeId>
<CacheNodeStatus>{{ cache_node.cache_node_status }}</CacheNodeStatus>
<CacheNodeCreateTime>{{ cache_node.cache_node_create_time }}</CacheNodeCreateTime>
<Endpoint>{{ cache_node.endpoint }}</Endpoint>
<ParameterGroupStatus>{{ cache_node.parameter_group_status }}</ParameterGroupStatus>
<SourceCacheNodeId>{{ cache_node.source_cache_node_id }}</SourceCacheNodeId>
<CustomerAvailabilityZone>{{ cache_node.customer_availability_zone }}</CustomerAvailabilityZone>
<CustomerOutpostArn>{{ cache_node.customer_outpost_arn }}</CustomerOutpostArn>
</member>
{% endfor %}
</CacheNodes>
<AutoMinorVersionUpgrade>{{ cache_cluster.auto_minor_version_upgrade }}</AutoMinorVersionUpgrade>
<SecurityGroups>
{% for security_group in cache_cluster.security_groups %}
<member>
<SecurityGroupId>{{ security_group.security_group_id }}</SecurityGroupId>
<Status>{{ security_group.status }}</Status>
</member>
{% endfor %}
</SecurityGroups>
<ReplicationGroupId>{{ cache_cluster.replication_group_id }}</ReplicationGroupId>
<SnapshotRetentionLimit>{{ cache_cluster.snapshot_retention_limit }}</SnapshotRetentionLimit>
<SnapshotWindow>{{ cache_cluster.snapshot_window }}</SnapshotWindow>
<AuthTokenEnabled>{{ cache_cluster.auth_token_enabled }}</AuthTokenEnabled>
<AuthTokenLastModifiedDate>{{ cache_cluster.auth_token_last_modified_date }}</AuthTokenLastModifiedDate>
<TransitEncryptionEnabled>{{ cache_cluster.transit_encryption_enabled }}</TransitEncryptionEnabled>
<AtRestEncryptionEnabled>{{ cache_cluster.at_rest_encryption_enabled }}</AtRestEncryptionEnabled>
<ARN>{{ cache_cluster.arn }}</ARN>
<ReplicationGroupLogDeliveryEnabled>{{ cache_cluster.replication_group_log_delivery_enabled }}</ReplicationGroupLogDeliveryEnabled>
<LogDeliveryConfigurations>
{% for log_delivery_configuration in cache_cluster.log_delivery_configurations %}
<member>
<LogType>{{ log_delivery_configuration.log_type }}</LogType>
<DestinationType>{{ log_delivery_configuration.destination_type }}</DestinationType>
<DestinationDetails>{{ log_delivery_configuration.destination_details }}</DestinationDetails>
<LogFormat>{{ log_delivery_configuration.log_format }}</LogFormat>
<Status>{{ log_delivery_configuration.status }}</Status>
<Message>{{ log_delivery_configuration.message }}</Message>
</member>
{% endfor %}
</LogDeliveryConfigurations>
<NetworkType>{{ cache_cluster.network_type }}</NetworkType>
<IpDiscovery>{{ cache_cluster.ip_discovery }}</IpDiscovery>
<TransitEncryptionMode>{{ cache_cluster.transit_encryption_mode }}</TransitEncryptionMode>
</member>
{% endfor %}
</CacheClusters>
</DescribeCacheClustersResult>
</DescribeCacheClustersResponse>"""
DELETE_CACHE_CLUSTER_TEMPLATE = """<DeleteCacheClusterResponse xmlns="http://elasticache.amazonaws.com/doc/2015-02-02/">
<ResponseMetadata>
<RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
</ResponseMetadata>
<DeleteCacheClusterResult>
<CacheCluster>
<CacheClusterId>{{ cache_cluster.cache_cluster_id }}</CacheClusterId>
<ConfigurationEndpoint>
<Address>example.cache.amazonaws.com</Address>
<Port>{{ cache_cluster.port }}</Port>
</ConfigurationEndpoint>
<ClientDownloadLandingPage></ClientDownloadLandingPage>
<CacheNodeType>{{ cache_cluster.cache_node_type }}</CacheNodeType>
<Engine>{{ cache_cluster.engine }}</Engine>
<EngineVersion>{{ cache_cluster.engine_version }}</EngineVersion>
<CacheClusterStatus>available</CacheClusterStatus>
<NumCacheNodes>{{ cache_cluster.num_cache_nodes }}</NumCacheNodes>
<PreferredAvailabilityZone>{{ cache_cluster.preferred_availability_zone }}</PreferredAvailabilityZone>
<PreferredOutpostArn>{{ cache_cluster.preferred_outpost_arn }}</PreferredOutpostArn>
<CacheClusterCreateTime>{{ cache_cluster.cache_cluster_create_time }}</CacheClusterCreateTime>
<PreferredMaintenanceWindow>{{ cache_cluster.preferred_maintenance_window }}</PreferredMaintenanceWindow>
{% if cache_cluster.cache_node_ids_to_remove != [] %}
<PendingModifiedValues>
<NumCacheNodes>{{ cache_cluster.num_cache_nodes }}</NumCacheNodes>
{% for cache_node_id_to_remove in cache_cluster.cache_node_ids_to_remove %}
<CacheNodeIdsToRemove>{{ cache_node_id_to_remove }}</CacheNodeIdsToRemove>
{% endfor %}
<EngineVersion>{{ cache_cluster.engine_version }}</EngineVersion>
<CacheNodeType>{{ cache_cluster.cache_node_type }}</CacheNodeType>
<AuthTokenStatus>SETTING</AuthTokenStatus>
<LogDeliveryConfigurations>
{% for log_delivery_configuration in cache_cluster.log_delivery_configurations %}
<LogType>{{ log_delivery_configuration.LogType }}</LogType>
<DestinationType>{{ log_delivery_configuration.DestinationType }}</DestinationType>
<DestinationDetails>
<CloudWatchLogsDetails>
<LogGroup>{{ log_delivery_configuration.LogGroup }}</LogGroup>
</CloudWatchLogsDetails>
<KinesisFirehoseDetails>
<DeliveryStream>{{ log_delivery_configuration.DeliveryStream }}</DeliveryStream>
</KinesisFirehoseDetails>
</DestinationDetails>
<LogFormat>{{ log_delivery_configuration.LogFormat }}</LogFormat>
{% endfor %}
</LogDeliveryConfigurations>
<TransitEncryptionEnabled>{{ cache_cluster.transit_encryption_enabled }}</TransitEncryptionEnabled>
<TransitEncryptionMode>preferred</TransitEncryptionMode>
</PendingModifiedValues>
{% endif %}
<NotificationConfiguration>
<TopicArn>{{ cache_cluster.notification_topic_arn }}</TopicArn>
<TopicStatus>active</TopicStatus>
</NotificationConfiguration>
<CacheSecurityGroups>
{% for cache_security_group_name in cache_cluster.cache_security_group_names %}
<CacheSecurityGroupName>{{ cache_security_group_name }}</CacheSecurityGroupName>
{% endfor %}
<Status>active</Status>
</CacheSecurityGroups>
<CacheParameterGroup>
<CacheParameterGroupName>{{ cache_cluster.cache_parameter_group_name }}</CacheParameterGroupName>
<ParameterApplyStatus>active</ParameterApplyStatus>
{% for cache_node_id_to_reboot in cache_cluster.cache_node_ids_to_reboot %}
<CacheNodeIdsToReboot>
{{ cache_node_id_to_reboot }}
</CacheNodeIdsToReboot>
{% endfor %}
</CacheParameterGroup>
<CacheSubnetGroupName>{{ cache_cluster.cache_subnet_group_name }}</CacheSubnetGroupName>
<CacheNodes>
<CacheNodeId>{{ cache_cluster.cache_node_id }}</CacheNodeId>
<CacheNodeStatus>{{ cache_cluster.cache_node_status }}</CacheNodeStatus>
<CacheNodeCreateTime>{{ cache_cluster.cache_cluster_create_time }}</CacheNodeCreateTime>
<Endpoint>
<Address>{{ cache_cluster.address }}</Address>
<Port>{{ cache_cluster.port }}</Port>
</Endpoint>
<ParameterGroupStatus>active</ParameterGroupStatus>
<SourceCacheNodeId>{{ cache_cluster.cache_node_id }}</SourceCacheNodeId>
<CustomerAvailabilityZone>{{ cache_cluster.preferred_availability_zone }}</CustomerAvailabilityZone>
<CustomerOutpostArn>{{ cache_cluster.preferred_output_arn }}</CustomerOutpostArn>
</CacheNodes>
<AutoMinorVersionUpgrade>{{ cache_cluster.auto_minor_version_upgrade }}</AutoMinorVersionUpgrade>
<SecurityGroups>
{% for security_group_id in cache_cluster.security_group_ids %}
<SecurityGroupId>{{ security_group_id }}</SecurityGroupId>
<Status>active</Status>
{% endfor %}
</SecurityGroups>
{% if cache_cluster.engine == "redis" %}
<ReplicationGroupId>{{ cache_cluster.replication_group_id }}</ReplicationGroupId>
<SnapshotRetentionLimit>{{ cache_cluster.snapshot_retention_limit }}</SnapshotRetentionLimit>
<SnapshotWindow>{{ cache_cluster.snapshot_window }}</SnapshotWindow>
{% endif %}
<AuthTokenEnabled>true</AuthTokenEnabled>
<AuthTokenLastModifiedDate>{{ cache_cluster.cache_cluster_create_time }}</AuthTokenLastModifiedDate>
<TransitEncryptionEnabled>{{ cache_cluster.transit_encryption_enabled }}</TransitEncryptionEnabled>
<AtRestEncryptionEnabled>true</AtRestEncryptionEnabled>
<ARN>{{ cache_cluster.arn }}</ARN>
<ReplicationGroupLogDeliveryEnabled>true</ReplicationGroupLogDeliveryEnabled>
<LogDeliveryConfigurations>
{% for log_delivery_configuration in cache_cluster.log_delivery_configurations %}
<LogType>{{ log_delivery_configuration.LogType }}</LogType>
<DestinationType>{{ log_delivery_configuration.DestinationType }}</DestinationType>
<DestinationDetails>
<CloudWatchLogsDetails>
<LogGroup>{{ log_delivery_configuration.LogGroup }}</LogGroup>
</CloudWatchLogsDetails>
<KinesisFirehoseDetails>
<DeliveryStream>{{ log_delivery_configuration.DeliveryStream }}</DeliveryStream>
</KinesisFirehoseDetails>
</DestinationDetails>
<LogFormat>{{ log_delivery_configuration.LogFormat }}</LogFormat>
<Status>active</Status>
<Message></Message>
{% endfor %}
</LogDeliveryConfigurations>
<NetworkType>{{ cache_cluster.network_type }}</NetworkType>
<IpDiscovery>{{ cache_cluster.ip_discovery }}</IpDiscovery>
<TransitEncryptionMode>preferred</TransitEncryptionMode>
</CacheCluster>
</DeleteCacheClusterResult>
</DeleteCacheClusterResponse>"""

View File

@ -1,10 +1,11 @@
import boto3
import pytest
from botocore.exceptions import ClientError
from moto import mock_elasticache
from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
from moto import mock_elasticache
# See our Development Tips on writing tests for hints on how to write good tests:
# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
@ -208,3 +209,212 @@ def test_describe_users_unknown_userid():
err = exc.value.response["Error"]
assert err["Code"] == "UserNotFound"
assert err["Message"] == "User unknown not found."
@mock_elasticache
def test_create_redis_cache_cluster():
client = boto3.client("elasticache", region_name="us-east-2")
test_redis_cache_cluster_exist = False
cache_cluster_id = "test-cache-cluster"
cache_cluster_engine = "redis"
cache_cluster_num_cache_nodes = 5
resp = client.create_cache_cluster(
CacheClusterId=cache_cluster_id,
Engine=cache_cluster_engine,
NumCacheNodes=cache_cluster_num_cache_nodes,
)
cache_cluster = resp["CacheCluster"]
if cache_cluster["CacheClusterId"] == cache_cluster_id:
if cache_cluster["Engine"] == cache_cluster_engine:
if cache_cluster["NumCacheNodes"] == 1:
test_redis_cache_cluster_exist = True
assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
assert test_redis_cache_cluster_exist
@mock_elasticache
def test_create_memcached_cache_cluster():
client = boto3.client("elasticache", region_name="us-east-2")
test_memcached_cache_cluster_exist = False
cache_cluster_id = "test-cache-cluster"
cache_cluster_engine = "memcached"
cache_cluster_num_cache_nodes = 5
resp = client.create_cache_cluster(
CacheClusterId=cache_cluster_id,
Engine=cache_cluster_engine,
NumCacheNodes=cache_cluster_num_cache_nodes,
)
cache_cluster = resp["CacheCluster"]
if cache_cluster["CacheClusterId"] == cache_cluster_id:
if cache_cluster["Engine"] == cache_cluster_engine:
if cache_cluster["NumCacheNodes"] == 5:
test_memcached_cache_cluster_exist = True
assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
assert test_memcached_cache_cluster_exist
@mock_elasticache
def test_create_duplicate_cache_cluster():
client = boto3.client("elasticache", region_name="us-east-2")
cache_cluster_id = "test-cache-cluster"
cache_cluster_engine = "memcached"
cache_cluster_num_cache_nodes = 5
client.create_cache_cluster(
CacheClusterId=cache_cluster_id,
Engine=cache_cluster_engine,
NumCacheNodes=cache_cluster_num_cache_nodes,
)
with pytest.raises(ClientError) as exc:
client.create_cache_cluster(
CacheClusterId=cache_cluster_id,
Engine=cache_cluster_engine,
NumCacheNodes=cache_cluster_num_cache_nodes,
)
err = exc.value.response["Error"]
assert err["Code"] == "CacheClusterAlreadyExists"
assert err["Message"] == f"Cache cluster {cache_cluster_id} already exists."
@mock_elasticache
def test_describe_all_cache_clusters():
client = boto3.client("elasticache", region_name="us-east-2")
test_memcached_cache_cluster_exist = False
cache_cluster_id = "test-cache-cluster"
cache_cluster_engine = "memcached"
cache_cluster_num_cache_nodes = 5
client.create_cache_cluster(
CacheClusterId=cache_cluster_id,
Engine=cache_cluster_engine,
NumCacheNodes=cache_cluster_num_cache_nodes,
)
resp = client.describe_cache_clusters()
cache_clusters = resp["CacheClusters"]
for cache_cluster in cache_clusters:
if cache_cluster["CacheClusterId"] == cache_cluster_id:
test_memcached_cache_cluster_exist = True
assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
assert test_memcached_cache_cluster_exist
@mock_elasticache
def test_describe_specific_cache_clusters():
client = boto3.client("elasticache", region_name="us-east-2")
test_memcached_cache_cluster_exist = False
cache_cluster_id = "test-cache-cluster"
cache_cluster_engine = "memcached"
cache_cluster_num_cache_nodes = 5
client.create_cache_cluster(
CacheClusterId=cache_cluster_id,
Engine=cache_cluster_engine,
NumCacheNodes=cache_cluster_num_cache_nodes,
)
client.create_cache_cluster(
CacheClusterId="test-cache-cluster-2",
Engine=cache_cluster_engine,
NumCacheNodes=cache_cluster_num_cache_nodes,
)
resp = client.describe_cache_clusters(CacheClusterId=cache_cluster_id)
cache_clusters = resp["CacheClusters"]
for cache_cluster in cache_clusters:
if cache_cluster["CacheClusterId"] == cache_cluster_id:
test_memcached_cache_cluster_exist = True
assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
assert test_memcached_cache_cluster_exist
@mock_elasticache
def test_describe_unknown_cache_cluster():
client = boto3.client("elasticache", region_name="us-east-2")
cache_cluster_id = "test-cache-cluster"
cache_cluster_id_unknown = "unknown-cache-cluster"
cache_cluster_engine = "memcached"
cache_cluster_num_cache_nodes = 5
client.create_cache_cluster(
CacheClusterId=cache_cluster_id,
Engine=cache_cluster_engine,
NumCacheNodes=cache_cluster_num_cache_nodes,
)
with pytest.raises(ClientError) as exc:
client.describe_cache_clusters(
CacheClusterId=cache_cluster_id_unknown,
)
err = exc.value.response["Error"]
assert err["Code"] == "CacheClusterNotFound"
assert err["Message"] == f"Cache cluster {cache_cluster_id_unknown} not found."
@mock_elasticache
def test_delete_cache_cluster():
client = boto3.client("elasticache", region_name="us-east-2")
cache_cluster_id = "test-cache-cluster"
cache_cluster_engine = "memcached"
cache_cluster_num_cache_nodes = 5
client.create_cache_cluster(
CacheClusterId=cache_cluster_id,
Engine=cache_cluster_engine,
NumCacheNodes=cache_cluster_num_cache_nodes,
)
client.delete_cache_cluster(CacheClusterId=cache_cluster_id)
resp = client.describe_cache_clusters(CacheClusterId=cache_cluster_id)
assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
assert resp["CacheClusters"][0]["CacheClusterStatus"] == "deleting"
@mock_elasticache
def test_delete_unknown_cache_cluster():
client = boto3.client("elasticache", region_name="us-east-2")
cache_cluster_id = "test-cache-cluster"
cache_cluster_id_unknown = "unknown-cache-cluster"
cache_cluster_engine = "memcached"
cache_cluster_num_cache_nodes = 5
client.create_cache_cluster(
CacheClusterId=cache_cluster_id,
Engine=cache_cluster_engine,
NumCacheNodes=cache_cluster_num_cache_nodes,
)
with pytest.raises(ClientError) as exc:
client.delete_cache_cluster(
CacheClusterId=cache_cluster_id_unknown,
)
err = exc.value.response["Error"]
assert err["Code"] == "CacheClusterNotFound"
assert err["Message"] == f"Cache cluster {cache_cluster_id_unknown} not found."