Spaces:
Runtime error
Runtime error
from pathlib import Path | |
from typing import Optional, Any, Dict, List | |
from typing_extensions import Literal | |
from phi.aws.api_client import AwsApiClient | |
from phi.aws.resource.base import AwsResource | |
from phi.aws.resource.ec2.security_group import SecurityGroup | |
from phi.aws.resource.elasticache.subnet_group import CacheSubnetGroup | |
from phi.cli.console import print_info | |
from phi.utils.log import logger | |
class CacheCluster(AwsResource): | |
""" | |
Reference: | |
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/elasticache.html | |
""" | |
resource_type: Optional[str] = "CacheCluster" | |
service_name: str = "elasticache" | |
# Name of the cluster. | |
name: str | |
# The node group (shard) identifier. This parameter is stored as a lowercase string. | |
# If None, use the name as the cache_cluster_id | |
# Constraints: | |
# A name must contain from 1 to 50 alphanumeric characters or hyphens. | |
# The first character must be a letter. | |
# A name cannot end with a hyphen or contain two consecutive hyphens. | |
cache_cluster_id: Optional[str] = None | |
# The name of the cache engine to be used for this cluster. | |
engine: Literal["memcached", "redis"] | |
# Compute and memory capacity of the nodes in the node group (shard). | |
cache_node_type: str | |
# The initial number of cache nodes that the cluster has. | |
# For clusters running Redis, this value must be 1. | |
# For clusters running Memcached, this value must be between 1 and 40. | |
num_cache_nodes: int | |
# The ID of the replication group to which this cluster should belong. | |
# If this parameter is specified, the cluster is added to the specified replication group as a read replica; | |
# otherwise, the cluster is a standalone primary that is not part of any replication group. | |
replication_group_id: Optional[str] = None | |
# Specifies whether the nodes in this Memcached cluster are created in a single Availability Zone or | |
# created across multiple Availability Zones in the cluster's region. | |
# This parameter is only supported for Memcached clusters. | |
az_mode: Optional[Literal["single-az", "cross-az"]] = None | |
# The EC2 Availability Zone in which the cluster is created. | |
# All nodes belonging to this cluster are placed in the preferred Availability Zone. If you want to create your | |
# nodes across multiple Availability Zones, use PreferredAvailabilityZones . | |
# Default: System chosen Availability Zone. | |
preferred_availability_zone: Optional[str] = None | |
# A list of the Availability Zones in which cache nodes are created. The order of the zones is not important. | |
# This option is only supported on Memcached. | |
preferred_availability_zones: Optional[List[str]] = None | |
# The version number of the cache engine to be used for this cluster. | |
engine_version: Optional[str] = None | |
cache_parameter_group_name: Optional[str] = None | |
# The name of the subnet group to be used for the cluster. | |
cache_subnet_group_name: Optional[str] = None | |
# If cache_subnet_group_name is None, | |
# Read the cache_subnet_group_name from cache_subnet_group | |
cache_subnet_group: Optional[CacheSubnetGroup] = None | |
# A list of security group names to associate with this cluster. | |
# Use this parameter only when you are creating a cluster outside of an Amazon Virtual Private Cloud (Amazon VPC). | |
cache_security_group_names: Optional[List[str]] = None | |
# One or more VPC security groups associated with the cluster. | |
# Use this parameter only when you are creating a cluster in an Amazon Virtual Private Cloud (Amazon VPC). | |
cache_security_group_ids: Optional[List[str]] = None | |
# If cache_security_group_ids is None | |
# Read the security_group_id from cache_security_groups | |
cache_security_groups: Optional[List[SecurityGroup]] = None | |
tags: Optional[List[Dict[str, str]]] = None | |
snapshot_arns: Optional[List[str]] = None | |
snapshot_name: Optional[str] = None | |
preferred_maintenance_window: Optional[str] = None | |
# The version number of the cache engine to be used for this cluster. | |
port: Optional[int] = None | |
notification_topic_arn: Optional[str] = None | |
auto_minor_version_upgrade: Optional[bool] = None | |
snapshot_retention_limit: Optional[int] = None | |
snapshot_window: Optional[str] = None | |
# The password used to access a password protected server. | |
# Password constraints: | |
# - Must be only printable ASCII characters. | |
# - Must be at least 16 characters and no more than 128 characters in length. | |
# - The only permitted printable special characters are !, &, #, $, ^, <, >, and -. | |
# Other printable special characters cannot be used in the AUTH token. | |
# - For more information, see AUTH password at http://redis.io/commands/AUTH. | |
# Provide AUTH_TOKEN here or as AUTH_TOKEN in secrets_file | |
auth_token: Optional[str] = None | |
outpost_mode: Optional[Literal["single-outpost", "cross-outpost"]] = None | |
preferred_outpost_arn: Optional[str] = None | |
preferred_outpost_arns: Optional[List[str]] = None | |
log_delivery_configurations: Optional[List[Dict[str, Any]]] = None | |
transit_encryption_enabled: Optional[bool] = None | |
network_type: Optional[Literal["ipv4", "ipv6", "dual_stack"]] = None | |
ip_discovery: Optional[Literal["ipv4", "ipv6"]] = None | |
# The user-supplied name of a final cluster snapshot | |
final_snapshot_identifier: Optional[str] = None | |
# Read secrets from a file in yaml format | |
secrets_file: Optional[Path] = None | |
# The follwing attributes are used for update function | |
cache_node_ids_to_remove: Optional[List[str]] = None | |
new_availability_zone: Optional[List[str]] = None | |
security_group_ids: Optional[List[str]] = None | |
notification_topic_status: Optional[str] = None | |
apply_immediately: Optional[bool] = None | |
auth_token_update_strategy: Optional[Literal["SET", "ROTATE", "DELETE"]] = None | |
def get_cache_cluster_id(self): | |
return self.cache_cluster_id or self.name | |
def get_auth_token(self) -> Optional[str]: | |
auth_token = self.auth_token | |
if auth_token is None and self.secrets_file is not None: | |
# read from secrets_file | |
secret_data = self.get_secret_file_data() | |
if secret_data is not None: | |
auth_token = secret_data.get("AUTH_TOKEN", auth_token) | |
return auth_token | |
def _create(self, aws_client: AwsApiClient) -> bool: | |
"""Creates the CacheCluster | |
Args: | |
aws_client: The AwsApiClient for the current cluster | |
""" | |
print_info(f"Creating {self.get_resource_type()}: {self.get_resource_name()}") | |
# create a dict of args which are not null, otherwise aws type validation fails | |
not_null_args: Dict[str, Any] = {} | |
# Get the CacheSubnetGroupName | |
cache_subnet_group_name = self.cache_subnet_group_name | |
if cache_subnet_group_name is None and self.cache_subnet_group is not None: | |
cache_subnet_group_name = self.cache_subnet_group.name | |
logger.debug(f"Using CacheSubnetGroup: {cache_subnet_group_name}") | |
if cache_subnet_group_name is not None: | |
not_null_args["CacheSubnetGroupName"] = cache_subnet_group_name | |
cache_security_group_ids = self.cache_security_group_ids | |
if cache_security_group_ids is None and self.cache_security_groups is not None: | |
sg_ids = [] | |
for sg in self.cache_security_groups: | |
sg_id = sg.get_security_group_id(aws_client) | |
if sg_id is not None: | |
sg_ids.append(sg_id) | |
if len(sg_ids) > 0: | |
cache_security_group_ids = sg_ids | |
logger.debug(f"Using SecurityGroups: {cache_security_group_ids}") | |
if cache_security_group_ids is not None: | |
not_null_args["SecurityGroupIds"] = cache_security_group_ids | |
if self.replication_group_id is not None: | |
not_null_args["ReplicationGroupId"] = self.replication_group_id | |
if self.az_mode is not None: | |
not_null_args["AZMode"] = self.az_mode | |
if self.preferred_availability_zone is not None: | |
not_null_args["PreferredAvailabilityZone"] = self.preferred_availability_zone | |
if self.preferred_availability_zones is not None: | |
not_null_args["PreferredAvailabilityZones"] = self.preferred_availability_zones | |
if self.num_cache_nodes is not None: | |
not_null_args["NumCacheNodes"] = self.num_cache_nodes | |
if self.cache_node_type is not None: | |
not_null_args["CacheNodeType"] = self.cache_node_type | |
if self.engine is not None: | |
not_null_args["Engine"] = self.engine | |
if self.engine_version is not None: | |
not_null_args["EngineVersion"] = self.engine_version | |
if self.cache_parameter_group_name is not None: | |
not_null_args["CacheParameterGroupName"] = self.cache_parameter_group_name | |
if self.cache_security_group_names is not None: | |
not_null_args["CacheSecurityGroupNames"] = self.cache_security_group_names | |
if self.tags is not None: | |
not_null_args["Tags"] = self.tags | |
if self.snapshot_arns is not None: | |
not_null_args["SnapshotArns"] = self.snapshot_arns | |
if self.snapshot_name is not None: | |
not_null_args["SnapshotName"] = self.snapshot_name | |
if self.preferred_maintenance_window is not None: | |
not_null_args["PreferredMaintenanceWindow"] = self.preferred_maintenance_window | |
if self.port is not None: | |
not_null_args["Port"] = self.port | |
if self.notification_topic_arn is not None: | |
not_null_args["NotificationTopicArn"] = self.notification_topic_arn | |
if self.auto_minor_version_upgrade is not None: | |
not_null_args["AutoMinorVersionUpgrade"] = self.auto_minor_version_upgrade | |
if self.snapshot_retention_limit is not None: | |
not_null_args["SnapshotRetentionLimit"] = self.snapshot_retention_limit | |
if self.snapshot_window is not None: | |
not_null_args["SnapshotWindow"] = self.snapshot_window | |
if self.auth_token is not None: | |
not_null_args["AuthToken"] = self.get_auth_token() | |
if self.outpost_mode is not None: | |
not_null_args["OutpostMode"] = self.outpost_mode | |
if self.preferred_outpost_arn is not None: | |
not_null_args["PreferredOutpostArn"] = self.preferred_outpost_arn | |
if self.preferred_outpost_arns is not None: | |
not_null_args["PreferredOutpostArns"] = self.preferred_outpost_arns | |
if self.log_delivery_configurations is not None: | |
not_null_args["LogDeliveryConfigurations"] = self.log_delivery_configurations | |
if self.transit_encryption_enabled is not None: | |
not_null_args["TransitEncryptionEnabled"] = self.transit_encryption_enabled | |
if self.network_type is not None: | |
not_null_args["NetworkType"] = self.network_type | |
if self.ip_discovery is not None: | |
not_null_args["IpDiscovery"] = self.ip_discovery | |
# Create CacheCluster | |
service_client = self.get_service_client(aws_client) | |
try: | |
create_response = service_client.create_cache_cluster( | |
CacheClusterId=self.get_cache_cluster_id(), | |
**not_null_args, | |
) | |
logger.debug(f"CacheCluster: {create_response}") | |
resource_dict = create_response.get("CacheCluster", {}) | |
# Validate resource creation | |
if resource_dict is not None: | |
print_info(f"CacheCluster created: {self.get_cache_cluster_id()}") | |
self.active_resource = create_response | |
return True | |
except Exception as e: | |
logger.error(f"{self.get_resource_type()} could not be created.") | |
logger.error(e) | |
return False | |
def post_create(self, aws_client: AwsApiClient) -> bool: | |
# Wait for CacheCluster to be created | |
if self.wait_for_create: | |
try: | |
print_info(f"Waiting for {self.get_resource_type()} to be active.") | |
waiter = self.get_service_client(aws_client).get_waiter("cache_cluster_available") | |
waiter.wait( | |
CacheClusterId=self.get_cache_cluster_id(), | |
WaiterConfig={ | |
"Delay": self.waiter_delay, | |
"MaxAttempts": self.waiter_max_attempts, | |
}, | |
) | |
except Exception as e: | |
logger.error("Waiter failed.") | |
logger.error(e) | |
return True | |
def _read(self, aws_client: AwsApiClient) -> Optional[Any]: | |
"""Returns the CacheCluster | |
Args: | |
aws_client: The AwsApiClient for the current cluster | |
""" | |
logger.debug(f"Reading {self.get_resource_type()}: {self.get_resource_name()}") | |
from botocore.exceptions import ClientError | |
service_client = self.get_service_client(aws_client) | |
try: | |
cache_cluster_id = self.get_cache_cluster_id() | |
describe_response = service_client.describe_cache_clusters(CacheClusterId=cache_cluster_id) | |
logger.debug(f"CacheCluster: {describe_response}") | |
resource_list = describe_response.get("CacheClusters", None) | |
if resource_list is not None and isinstance(resource_list, list): | |
for resource in resource_list: | |
_cluster_identifier = resource.get("CacheClusterId", None) | |
if _cluster_identifier == cache_cluster_id: | |
self.active_resource = resource | |
break | |
except ClientError as ce: | |
logger.debug(f"ClientError: {ce}") | |
except Exception as e: | |
logger.error(f"Error reading {self.get_resource_type()}.") | |
logger.error(e) | |
return self.active_resource | |
def _update(self, aws_client: AwsApiClient) -> bool: | |
"""Updates the CacheCluster | |
Args: | |
aws_client: The AwsApiClient for the current cluster | |
""" | |
logger.debug(f"Updating {self.get_resource_type()}: {self.get_resource_name()}") | |
cache_cluster_id = self.get_cache_cluster_id() | |
if cache_cluster_id is None: | |
logger.error("CacheClusterId is None") | |
return False | |
# create a dict of args which are not null, otherwise aws type validation fails | |
not_null_args: Dict[str, Any] = {} | |
if self.num_cache_nodes is not None: | |
not_null_args["NumCacheNodes"] = self.num_cache_nodes | |
if self.cache_node_ids_to_remove is not None: | |
not_null_args["CacheNodeIdsToRemove"] = self.cache_node_ids_to_remove | |
if self.az_mode is not None: | |
not_null_args["AZMode"] = self.az_mode | |
if self.new_availability_zone is not None: | |
not_null_args["NewAvailabilityZone"] = self.new_availability_zone | |
if self.cache_security_group_names is not None: | |
not_null_args["CacheSecurityGroupNames"] = self.cache_security_group_names | |
if self.security_group_ids is not None: | |
not_null_args["SecurityGroupIds"] = self.security_group_ids | |
if self.preferred_maintenance_window is not None: | |
not_null_args["PreferredMaintenanceWindow"] = self.preferred_maintenance_window | |
if self.notification_topic_arn is not None: | |
not_null_args["NotificationTopicArn"] = self.notification_topic_arn | |
if self.cache_parameter_group_name is not None: | |
not_null_args["CacheParameterGroupName"] = self.cache_parameter_group_name | |
if self.notification_topic_status is not None: | |
not_null_args["NotificationTopicStatus"] = self.notification_topic_status | |
if self.apply_immediately is not None: | |
not_null_args["ApplyImmediately"] = self.apply_immediately | |
if self.engine_version is not None: | |
not_null_args["EngineVersion"] = self.engine_version | |
if self.auto_minor_version_upgrade is not None: | |
not_null_args["AutoMinorVersionUpgrade"] = self.auto_minor_version_upgrade | |
if self.snapshot_retention_limit is not None: | |
not_null_args["SnapshotRetentionLimit"] = self.snapshot_retention_limit | |
if self.snapshot_window is not None: | |
not_null_args["SnapshotWindow"] = self.snapshot_window | |
if self.cache_node_type is not None: | |
not_null_args["CacheNodeType"] = self.cache_node_type | |
if self.auth_token is not None: | |
not_null_args["AuthToken"] = self.get_auth_token() | |
if self.auth_token_update_strategy is not None: | |
not_null_args["AuthTokenUpdateStrategy"] = self.auth_token_update_strategy | |
if self.log_delivery_configurations is not None: | |
not_null_args["LogDeliveryConfigurations"] = self.log_delivery_configurations | |
service_client = self.get_service_client(aws_client) | |
try: | |
modify_response = service_client.modify_cache_cluster( | |
CacheClusterId=cache_cluster_id, | |
**not_null_args, | |
) | |
logger.debug(f"CacheCluster: {modify_response}") | |
resource_dict = modify_response.get("CacheCluster", {}) | |
# Validate resource creation | |
if resource_dict is not None: | |
print_info(f"CacheCluster updated: {self.get_cache_cluster_id()}") | |
self.active_resource = modify_response | |
return True | |
except Exception as e: | |
logger.error(f"{self.get_resource_type()} could not be updated.") | |
logger.error(e) | |
return False | |
def _delete(self, aws_client: AwsApiClient) -> bool: | |
"""Deletes the CacheCluster | |
Args: | |
aws_client: The AwsApiClient for the current cluster | |
""" | |
print_info(f"Deleting {self.get_resource_type()}: {self.get_resource_name()}") | |
# create a dict of args which are not null, otherwise aws type validation fails | |
not_null_args: Dict[str, Any] = {} | |
if self.final_snapshot_identifier: | |
not_null_args["FinalSnapshotIdentifier"] = self.final_snapshot_identifier | |
service_client = self.get_service_client(aws_client) | |
self.active_resource = None | |
try: | |
delete_response = service_client.delete_cache_cluster( | |
CacheClusterId=self.get_cache_cluster_id(), | |
**not_null_args, | |
) | |
logger.debug(f"CacheCluster: {delete_response}") | |
return True | |
except Exception as e: | |
logger.error(f"{self.get_resource_type()} could not be deleted.") | |
logger.error("Please try again or delete resources manually.") | |
logger.error(e) | |
return False | |
def post_delete(self, aws_client: AwsApiClient) -> bool: | |
# Wait for CacheCluster to be deleted | |
if self.wait_for_delete: | |
try: | |
print_info(f"Waiting for {self.get_resource_type()} to be deleted.") | |
waiter = self.get_service_client(aws_client).get_waiter("cache_cluster_deleted") | |
waiter.wait( | |
CacheClusterId=self.get_cache_cluster_id(), | |
WaiterConfig={ | |
"Delay": self.waiter_delay, | |
"MaxAttempts": self.waiter_max_attempts, | |
}, | |
) | |
except Exception as e: | |
logger.error("Waiter failed.") | |
logger.error(e) | |
return True | |
def get_cache_endpoint(self, aws_client: Optional[AwsApiClient] = None) -> Optional[str]: | |
"""Returns the CacheCluster endpoint | |
Args: | |
aws_client: The AwsApiClient for the current cluster | |
""" | |
cache_endpoint = None | |
try: | |
client: AwsApiClient = aws_client or self.get_aws_client() | |
cache_cluster_id = self.get_cache_cluster_id() | |
describe_response = self.get_service_client(client).describe_cache_clusters( | |
CacheClusterId=cache_cluster_id, ShowCacheNodeInfo=True | |
) | |
# logger.debug(f"CacheCluster: {describe_response}") | |
resource_list = describe_response.get("CacheClusters", None) | |
if resource_list is not None and isinstance(resource_list, list): | |
for resource in resource_list: | |
_cluster_identifier = resource.get("CacheClusterId", None) | |
if _cluster_identifier == cache_cluster_id: | |
for node in resource.get("CacheNodes", []): | |
cache_endpoint = node.get("Endpoint", {}).get("Address", None) | |
if cache_endpoint is not None and isinstance(cache_endpoint, str): | |
return cache_endpoint | |
break | |
except Exception as e: | |
logger.error(f"Error reading {self.get_resource_type()}.") | |
logger.error(e) | |
return cache_endpoint | |
def get_cache_port(self, aws_client: Optional[AwsApiClient] = None) -> Optional[int]: | |
"""Returns the CacheCluster port | |
Args: | |
aws_client: The AwsApiClient for the current cluster | |
""" | |
cache_port = None | |
try: | |
client: AwsApiClient = aws_client or self.get_aws_client() | |
cache_cluster_id = self.get_cache_cluster_id() | |
describe_response = self.get_service_client(client).describe_cache_clusters( | |
CacheClusterId=cache_cluster_id, ShowCacheNodeInfo=True | |
) | |
# logger.debug(f"CacheCluster: {describe_response}") | |
resource_list = describe_response.get("CacheClusters", None) | |
if resource_list is not None and isinstance(resource_list, list): | |
for resource in resource_list: | |
_cluster_identifier = resource.get("CacheClusterId", None) | |
if _cluster_identifier == cache_cluster_id: | |
for node in resource.get("CacheNodes", []): | |
cache_port = node.get("Endpoint", {}).get("Port", None) | |
if cache_port is not None and isinstance(cache_port, int): | |
return cache_port | |
break | |
except Exception as e: | |
logger.error(f"Error reading {self.get_resource_type()}.") | |
logger.error(e) | |
return cache_port | |