Repository: cloud-custodian/cloud-custodian

Issue:
aws - elasticache support t2 snapshots
Hi,

When I want to create a snapshot for **t-type** Redis via c7n it can not be created while other types (r and m) can be created. Is there a special reason for that? Because I can create a manual snapshot for all type of Redises but C7N can trigger snapshot just for "r" and "m" type Redises.

In the documentation(https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/backups.html#backups-constraints) it says:

> For Redis (cluster mode disabled) clusters, backup and restore are not supported on cache.t1.micro nodes. All other cache node types are supported.

But there is not a negative definition for t2 types and I test t2 type;

**_Test Redises are totally identical except their types (t vs m)_**
```
1- redis-test-t-type -----> cache.t2.micro --------> can NOT be triggered via c7n
2- redis-test-m-type----> cache.m3.medium ----> can be triggered via c7n
```

**_Result is snapshots just for m-type one but not for t-type one, although I have tested it for several times:_**
```
backup-c7n-redistest-m-type-001-2019-01-10-09-31 | c7n-redistest-m-type-001 | manual | available | 4 MB |  
backup-c7n-redistest-m-type-002-2019-01-10-09-31 | c7n-redistest-m-type-002 | manual | available | 4 MB |  
backup-c7n-redistest-m-type-003-2019-01-10-09-31 | c7n-redistest-m-type-003 | manual | available | 4 MB |
```

**_Policy and Local-Run logs are below for my test Redises:_**

# policy
```
policies:
  - name: elasticache-cluster-snapshot
    resource: cache-cluster
    filters:
      - type: value
        key: CacheClusterStatus
        op: not-in
        value: ["deleted","deleting","creating"]
    actions:
      - snapshot
```

#custodian-run.log
```
2019-01-10 09:31:34,131 - custodian.policy - INFO - policy: elasticache-cluster-snapshot resource:cache-cluster region:us-east-1 count:6 time:2.40
2019-01-10 09:31:36,948 - custodian.policy - INFO - policy: elasticache-cluster-snapshot action: snapshotelasticachecluster resources: 6 execution_time: 2.80
```

*update*: omit json resource files


Discussion:
it looks like the snapshot action does an implicit filter on instance type and is currently catching all t*  instance types, instead of t1. It should be logging any implicit filtering (its not afaics), and it needs to be updated for the check on t2 instance types, per aws recent nov 2018 addition of support for backup/restore/snapshot on t2.

https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/WhatsNew.html



Resolving patch:
diff --git a/c7n/resources/elasticache.py b/c7n/resources/elasticache.py
--- a/c7n/resources/elasticache.py
+++ b/c7n/resources/elasticache.py
@@ -39,7 +39,7 @@
 filters = FilterRegistry('elasticache.filters')
 actions = ActionRegistry('elasticache.actions')
 
-TTYPE = re.compile('cache.t')
+TTYPE = re.compile('cache.t1')
 
 
 @resources.register('cache-cluster')
@@ -216,14 +216,19 @@ class SnapshotElastiCacheCluster(BaseAction):
     permissions = ('elasticache:CreateSnapshot',)
 
     def process(self, clusters):
+        set_size = len(clusters)
+        clusters = [c for c in clusters if _cluster_eligible_for_snapshot(c)]
+        if set_size != len(clusters):
+            self.log.info(
+                "action:snapshot implicitly filtered from %d to %d clusters for snapshot support",
+                set_size, len(clusters))
+
         with self.executor_factory(max_workers=2) as w:
             futures = []
+            client = local_session(self.manager.session_factory).client('elasticache')
             for cluster in clusters:
-                if not _cluster_eligible_for_snapshot(cluster):
-                    continue
-                futures.append(w.submit(
-                    self.process_cluster_snapshot,
-                    cluster))
+                futures.append(
+                    w.submit(self.process_cluster_snapshot, client, cluster))
 
             for f in as_completed(futures):
                 if f.exception():
@@ -232,9 +237,8 @@ def process(self, clusters):
                         f.exception())
         return clusters
 
-    def process_cluster_snapshot(self, cluster):
-        c = local_session(self.manager.session_factory).client('elasticache')
-        c.create_snapshot(
+    def process_cluster_snapshot(self, client, cluster):
+        client.create_snapshot(
             SnapshotName=snapshot_identifier(
                 'Backup',
                 cluster['CacheClusterId']),
@@ -385,9 +389,10 @@ def process(self, snapshots):
         log.info("Deleting %d ElastiCache snapshots", len(snapshots))
         with self.executor_factory(max_workers=3) as w:
             futures = []
+            client = local_session(self.manager.session_factory).client('elasticache')
             for snapshot_set in chunks(reversed(snapshots), size=50):
                 futures.append(
-                    w.submit(self.process_snapshot_set, snapshot_set))
+                    w.submit(self.process_snapshot_set, client, snapshot_set))
                 for f in as_completed(futures):
                     if f.exception():
                         self.log.error(
@@ -395,10 +400,9 @@ def process(self, snapshots):
                             f.exception())
         return snapshots
 
-    def process_snapshot_set(self, snapshots_set):
-        c = local_session(self.manager.session_factory).client('elasticache')
+    def process_snapshot_set(self, client, snapshots_set):
         for s in snapshots_set:
-            c.delete_snapshot(SnapshotName=s['SnapshotName'])
+            client.delete_snapshot(SnapshotName=s['SnapshotName'])
 
 
 @ElastiCacheSnapshot.action_registry.register('copy-cluster-tags')


Test patch:
diff --git a/tests/test_elasticache.py b/tests/test_elasticache.py
--- a/tests/test_elasticache.py
+++ b/tests/test_elasticache.py
@@ -13,11 +13,26 @@
 # limitations under the License.
 from __future__ import absolute_import, division, print_function, unicode_literals
 
+from c7n.resources.elasticache import _cluster_eligible_for_snapshot
+
 from .common import BaseTest, TestConfig as Config
 
 
 class TestElastiCacheCluster(BaseTest):
 
+    def test_eligibility_snapshot(self):
+        # so black box testing, due to use of private interface.
+
+        self.assertTrue(
+            _cluster_eligible_for_snapshot(
+                {'Engine': 'redis', 'CacheNodeType': 'cache.t2.medium'}))
+        self.assertFalse(
+            _cluster_eligible_for_snapshot(
+                {'Engine': 'redis', 'CacheNodeType': 'cache.t1.medium'}))
+        self.assertFalse(
+            _cluster_eligible_for_snapshot(
+                {'Engine': 'memcached', 'CacheNodeType': 'cache.t2.medium'}))
+
     def test_elasticache_security_group(self):
         session_factory = self.replay_flight_data("test_elasticache_security_group")
         p = self.load_policy(