From d78db35f41ebc435b842d07f7ca1759c6bd00afb Mon Sep 17 00:00:00 2001 From: Jean-Frederic Mainville <35815402+jfmainville@users.noreply.github.com> Date: Sat, 2 Sep 2023 09:00:25 -0400 Subject: [PATCH] ElastiCache: Add the `create_cache_cluster`, `delete_cache_cluster` and `describe_cache_clusters` methods support (#6754) --- docs/docs/services/elasticache.rst | 10 +- moto/__init__.py | 5 +- moto/elasticache/exceptions.py | 23 + moto/elasticache/models.py | 242 ++++++++++- moto/elasticache/responses.py | 463 ++++++++++++++++++++- tests/test_elasticache/test_elasticache.py | 214 +++++++++- 6 files changed, 928 insertions(+), 29 deletions(-) diff --git a/docs/docs/services/elasticache.rst b/docs/docs/services/elasticache.rst index 8ede5a9f94d0..2c5a64e3bd06 100644 --- a/docs/docs/services/elasticache.rst +++ b/docs/docs/services/elasticache.rst @@ -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 diff --git a/moto/__init__.py b/moto/__init__.py index cb1527a08901..29bae381b72e 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -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 @@ def f(*args: Any, **kwargs: Any) -> Any: 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): diff --git a/moto/elasticache/exceptions.py b/moto/elasticache/exceptions.py index fe14cb05011f..f5e7f5b65413 100644 --- a/moto/elasticache/exceptions.py +++ b/moto/elasticache/exceptions.py @@ -1,4 +1,5 @@ from typing import Any + from moto.core.exceptions import RESTError EXCEPTION_RESPONSE = """ @@ -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.", + ) diff --git a/moto/elasticache/models.py b/moto/elasticache/models.py index 198d75dee8d7..25e37807c2b2 100644 --- a/moto/elasticache/models.py +++ b/moto/elasticache/models.py @@ -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 @@ def __init__( 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 @@ def __init__(self, region_name: str, account_id: str): 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 @@ def describe_users(self, user_id: Optional[str]) -> List[User]: 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") diff --git a/moto/elasticache/responses.py b/moto/elasticache/responses.py index 273fa002d7b5..66a78a8e69bc 100644 --- a/moto/elasticache/responses.py +++ b/moto/elasticache/responses.py @@ -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 @@ def describe_users(self) -> str: 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 = """{{ user.id }} {{ user.name }} @@ -74,14 +166,13 @@ def describe_users(self) -> str: {{ user.arn }}""" - CREATE_USER_TEMPLATE = ( """ - - 1549581b-12b7-11e3-895e-1334aEXAMPLE - - - """ + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + + + """ + USER_TEMPLATE + """ @@ -90,11 +181,11 @@ def describe_users(self) -> str: DELETE_USER_TEMPLATE = ( """ - - 1549581b-12b7-11e3-895e-1334aEXAMPLE - - - """ + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + + + """ + USER_TEMPLATE + """ @@ -103,14 +194,14 @@ def describe_users(self) -> str: DESCRIBE_USERS_TEMPLATE = ( """ - - 1549581b-12b7-11e3-895e-1334aEXAMPLE - - - -{% for user in users %} - - """ + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + + + + {% for user in users %} + + """ + USER_TEMPLATE + """ @@ -120,3 +211,337 @@ def describe_users(self) -> str: """ ) + +CREATE_CACHE_CLUSTER_TEMPLATE = """ + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + + + + {{ cache_cluster.cache_cluster_id }} + +
example.cache.amazonaws.com
+ {{ cache_cluster.port }} +
+ + {{ cache_cluster.cache_node_type }} + {{ cache_cluster.engine }} + {{ cache_cluster.engine_version }} + available + {{ cache_cluster.num_cache_nodes }} + {{ cache_cluster.preferred_availability_zone }} + {{ cache_cluster.preferred_outpost_arn }} + {{ cache_cluster.cache_cluster_create_time }} + {{ cache_cluster.preferred_maintenance_window }} + {% if cache_cluster.cache_node_ids_to_remove != [] %} + + {{ cache_cluster.num_cache_nodes }} + {% for cache_node_id_to_remove in cache_cluster.cache_node_ids_to_remove %} + {{ cache_node_id_to_remove }} + {% endfor %} + {{ cache_cluster.engine_version }} + {{ cache_cluster.cache_node_type }} + SETTING + + {% for log_delivery_configuration in cache_cluster.log_delivery_configurations %} + {{ log_delivery_configuration.LogType }} + {{ log_delivery_configuration.DestinationType }} + + + {{ log_delivery_configuration.LogGroup }} + + + {{ log_delivery_configuration.DeliveryStream }} + + + {{ log_delivery_configuration.LogFormat }} + {% endfor %} + + {{ cache_cluster.transit_encryption_enabled }} + preferred + + {% endif %} + + {{ cache_cluster.notification_topic_arn }} + active + + + {% for cache_security_group_name in cache_cluster.cache_security_group_names %} + {{ cache_security_group_name }} + {% endfor %} + active + + + {{ cache_cluster.cache_parameter_group_name }} + active + {% for cache_node_id_to_reboot in cache_cluster.cache_node_ids_to_reboot %} + + {{ cache_node_id_to_reboot }} + + {% endfor %} + + {{ cache_cluster.cache_subnet_group_name }} + + {{ cache_cluster.cache_node_id }} + {{ cache_cluster.cache_node_status }} + {{ cache_cluster.cache_cluster_create_time }} + +
{{ cache_cluster.address }}
+ {{ cache_cluster.port }} +
+ active + {{ cache_cluster.cache_node_id }} + {{ cache_cluster.preferred_availability_zone }} + {{ cache_cluster.preferred_output_arn }} +
+ {{ cache_cluster.auto_minor_version_upgrade }} + + {% for security_group_id in cache_cluster.security_group_ids %} + {{ security_group_id }} + active + {% endfor %} + + {% if cache_cluster.engine == "redis" %} + {{ cache_cluster.replication_group_id }} + {{ cache_cluster.snapshot_retention_limit }} + {{ cache_cluster.snapshot_window }} + {% endif %} + true + {{ cache_cluster.cache_cluster_create_time }} + {{ cache_cluster.transit_encryption_enabled }} + true + {{ cache_cluster.arn }} + true + + {% for log_delivery_configuration in cache_cluster.log_delivery_configurations %} + {{ log_delivery_configuration.LogType }} + {{ log_delivery_configuration.DestinationType }} + + + {{ log_delivery_configuration.LogGroup }} + + + {{ log_delivery_configuration.DeliveryStream }} + + + {{ log_delivery_configuration.LogFormat }} + active + + {% endfor %} + + {{ cache_cluster.network_type }} + {{ cache_cluster.ip_discovery }} + preferred +
+
+
""" + +DESCRIBE_CACHE_CLUSTERS_TEMPLATE = """ + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + + + {{ marker }} + +{% for cache_cluster in cache_clusters %} + + {{ cache_cluster.cache_cluster_id }} + {{ cache_cluster.configuration_endpoint }} + {{ cache_cluster.client_download_landing_page }} + {{ cache_cluster.cache_node_type }} + {{ cache_cluster.engine }} + {{ cache_cluster.engine_version }} + {{ cache_cluster.cache_cluster_status }} + {{ cache_cluster.num_cache_nodes }} + {{ cache_cluster.preferred_availability_zone }} + {{ cache_cluster.preferred_outpost_arn }} + {{ cache_cluster.cache_cluster_create_time }} + {{ cache_cluster.preferred_maintenance_window }} + {{ cache_cluster.pending_modified_values }} + {{ cache_cluster.notification_configuration }} + +{% for cache_security_group in cache_cluster.cache_security_groups %} + + {{ cache_security_group.cache_security_group_name }} + {{ cache_security_group.status }} + +{% endfor %} + + {{ cache_cluster.cache_parameter_group }} + {{ cache_cluster.cache_subnet_group_name }} + +{% for cache_node in cache_cluster.cache_nodes %} + + {{ cache_node.cache_node_id }} + {{ cache_node.cache_node_status }} + {{ cache_node.cache_node_create_time }} + {{ cache_node.endpoint }} + {{ cache_node.parameter_group_status }} + {{ cache_node.source_cache_node_id }} + {{ cache_node.customer_availability_zone }} + {{ cache_node.customer_outpost_arn }} + +{% endfor %} + + {{ cache_cluster.auto_minor_version_upgrade }} + +{% for security_group in cache_cluster.security_groups %} + + {{ security_group.security_group_id }} + {{ security_group.status }} + +{% endfor %} + + {{ cache_cluster.replication_group_id }} + {{ cache_cluster.snapshot_retention_limit }} + {{ cache_cluster.snapshot_window }} + {{ cache_cluster.auth_token_enabled }} + {{ cache_cluster.auth_token_last_modified_date }} + {{ cache_cluster.transit_encryption_enabled }} + {{ cache_cluster.at_rest_encryption_enabled }} + {{ cache_cluster.arn }} + {{ cache_cluster.replication_group_log_delivery_enabled }} + +{% for log_delivery_configuration in cache_cluster.log_delivery_configurations %} + + {{ log_delivery_configuration.log_type }} + {{ log_delivery_configuration.destination_type }} + {{ log_delivery_configuration.destination_details }} + {{ log_delivery_configuration.log_format }} + {{ log_delivery_configuration.status }} + {{ log_delivery_configuration.message }} + +{% endfor %} + + {{ cache_cluster.network_type }} + {{ cache_cluster.ip_discovery }} + {{ cache_cluster.transit_encryption_mode }} + +{% endfor %} + + +""" + +DELETE_CACHE_CLUSTER_TEMPLATE = """ + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + + + + {{ cache_cluster.cache_cluster_id }} + +
example.cache.amazonaws.com
+ {{ cache_cluster.port }} +
+ + {{ cache_cluster.cache_node_type }} + {{ cache_cluster.engine }} + {{ cache_cluster.engine_version }} + available + {{ cache_cluster.num_cache_nodes }} + {{ cache_cluster.preferred_availability_zone }} + {{ cache_cluster.preferred_outpost_arn }} + {{ cache_cluster.cache_cluster_create_time }} + {{ cache_cluster.preferred_maintenance_window }} + {% if cache_cluster.cache_node_ids_to_remove != [] %} + + {{ cache_cluster.num_cache_nodes }} + {% for cache_node_id_to_remove in cache_cluster.cache_node_ids_to_remove %} + {{ cache_node_id_to_remove }} + {% endfor %} + {{ cache_cluster.engine_version }} + {{ cache_cluster.cache_node_type }} + SETTING + + {% for log_delivery_configuration in cache_cluster.log_delivery_configurations %} + {{ log_delivery_configuration.LogType }} + {{ log_delivery_configuration.DestinationType }} + + + {{ log_delivery_configuration.LogGroup }} + + + {{ log_delivery_configuration.DeliveryStream }} + + + {{ log_delivery_configuration.LogFormat }} + {% endfor %} + + {{ cache_cluster.transit_encryption_enabled }} + preferred + + {% endif %} + + {{ cache_cluster.notification_topic_arn }} + active + + + {% for cache_security_group_name in cache_cluster.cache_security_group_names %} + {{ cache_security_group_name }} + {% endfor %} + active + + + {{ cache_cluster.cache_parameter_group_name }} + active + {% for cache_node_id_to_reboot in cache_cluster.cache_node_ids_to_reboot %} + + {{ cache_node_id_to_reboot }} + + {% endfor %} + + {{ cache_cluster.cache_subnet_group_name }} + + {{ cache_cluster.cache_node_id }} + {{ cache_cluster.cache_node_status }} + {{ cache_cluster.cache_cluster_create_time }} + +
{{ cache_cluster.address }}
+ {{ cache_cluster.port }} +
+ active + {{ cache_cluster.cache_node_id }} + {{ cache_cluster.preferred_availability_zone }} + {{ cache_cluster.preferred_output_arn }} +
+ {{ cache_cluster.auto_minor_version_upgrade }} + + {% for security_group_id in cache_cluster.security_group_ids %} + {{ security_group_id }} + active + {% endfor %} + + {% if cache_cluster.engine == "redis" %} + {{ cache_cluster.replication_group_id }} + {{ cache_cluster.snapshot_retention_limit }} + {{ cache_cluster.snapshot_window }} + {% endif %} + true + {{ cache_cluster.cache_cluster_create_time }} + {{ cache_cluster.transit_encryption_enabled }} + true + {{ cache_cluster.arn }} + true + + {% for log_delivery_configuration in cache_cluster.log_delivery_configurations %} + {{ log_delivery_configuration.LogType }} + {{ log_delivery_configuration.DestinationType }} + + + {{ log_delivery_configuration.LogGroup }} + + + {{ log_delivery_configuration.DeliveryStream }} + + + {{ log_delivery_configuration.LogFormat }} + active + + {% endfor %} + + {{ cache_cluster.network_type }} + {{ cache_cluster.ip_discovery }} + preferred +
+
+
""" diff --git a/tests/test_elasticache/test_elasticache.py b/tests/test_elasticache/test_elasticache.py index ecc18679abe8..e119fe1a4329 100644 --- a/tests/test_elasticache/test_elasticache.py +++ b/tests/test_elasticache/test_elasticache.py @@ -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."