File size: 22,631 Bytes
105b369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
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