Repository: cloud-custodian/cloud-custodian

Issue:
Ability to manage RDS DB snapshots
I'd love to be able to manage RDS DB snapshots as we do EBS snapshots:
- A new resource type (e.g. `rds-snapshot`)
- An associated `age` filter
- An associated `delete` action



Discussion:

Resolving patch:
diff --git a/c7n/resources/rds.py b/c7n/resources/rds.py
--- a/c7n/resources/rds.py
+++ b/c7n/resources/rds.py
@@ -65,11 +65,11 @@
 from concurrent.futures import as_completed
 
 from c7n.actions import ActionRegistry, BaseAction
-from c7n.filters import FilterRegistry, Filter
+from c7n.filters import FilterRegistry, Filter, AgeFilter
 from c7n.manager import resources
 from c7n.query import QueryResourceManager
 from c7n import tags
-from c7n.utils import local_session, type_schema, get_account_id
+from c7n.utils import local_session, type_schema, get_account_id, chunks
 
 from skew.resources.aws import rds
 
@@ -299,3 +299,56 @@ def set_retention_window(self, resource):
         c.modify_db_instance(
             DBInstanceIdentifier=resource['DBInstanceIdentifier'],
             BackupRetentionPeriod=self.data['days'])
+
+
+@resources.register('rds-snapshot')
+class RDSSnapshot(QueryResourceManager):
+
+    class Meta(object):
+
+        service = 'rds'
+        type = 'rds-snapshot'
+        enum_spec = ('describe_db_snapshots', 'DBSnapshots', None)
+        name = id = 'DBSnapshotIdentifier'
+        filter_name = None
+        filter_type = None
+        dimension = None
+        date = 'SnapshotCreateTime'
+
+    resource_type = Meta
+
+    filter_registry = FilterRegistry('rds-snapshot.filters')
+    action_registry = ActionRegistry('rds-snapshot.actions')
+
+
+@RDSSnapshot.filter_registry.register('age')
+class RDSSnapshotAge(AgeFilter):
+
+    schema = type_schema('age', days={'type': 'number'})
+    date_attribute = 'SnapshotCreateTime'
+
+@RDSSnapshot.action_registry.register('delete')
+class RDSSnapshotDelete(BaseAction):
+
+    def process(self, snapshots):
+        log.info("Deleting %d rds snapshots", len(snapshots))
+        with self.executor_factory(max_workers=3) as w:
+            futures = []
+            for snapshot_set in chunks(reversed(snapshots), size=50):
+                futures.append(
+                    w.submit(self.process_snapshot_set, snapshot_set))
+                for f in as_completed(futures):
+                    if f.exception():
+                        self.log.error(
+                            "Exception deleting snapshot set \n %s" % (
+                                f.exception()))
+        return snapshots
+
+    def process_snapshot_set(self, snapshots_set):
+        c = local_session(self.manager.session_factory).client('rds')
+        for s in snapshots_set:
+            try:
+                c.delete_db_snapshot(
+                    DBSnapshotIdentifier=s['DBSnapshotIdentifier'])
+            except ClientError as e:
+                raise


Test patch:
diff --git a/tests/data/placebo/test_rds_snapshot_delete/rds.DeleteDBSnapshot_1.json b/tests/data/placebo/test_rds_snapshot_delete/rds.DeleteDBSnapshot_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_delete/rds.DeleteDBSnapshot_1.json
@@ -0,0 +1,47 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBSnapshot": {
+          "Engine": "postgres",
+          "SnapshotCreateTime": {
+              "hour": 19,
+              "__class__": "datetime",
+              "month": 4,
+              "second": 52,
+              "microsecond": 776000,
+              "year": 2016,
+              "day": 21,
+              "minute": 4
+          },
+          "AvailabilityZone": "us-east-1b",
+          "PercentProgress": 100,
+          "MasterUsername": "dbadmin",
+          "Encrypted": true,
+          "LicenseModel": "postgresql-license",
+          "StorageType": "gp2",
+          "Status": "available",
+          "VpcId": "vpc-f3581498",
+          "DBSnapshotIdentifier": "abc-restore-final-snapshot",
+          "InstanceCreateTime": {
+              "hour": 3,
+              "__class__": "datetime",
+              "month": 3,
+              "second": 42,
+              "microsecond": 265000,
+              "year": 2016,
+              "day": 14,
+              "minute": 33
+          },
+          "OptionGroupName": "default:postgres-9-4",
+          "AllocatedStorage": 100,
+          "EngineVersion": "9.4.5",
+          "SnapshotType": "manual",
+          "Port": 5432,
+          "DBInstanceIdentifier": "abc-restore"
+        },
+        "ResponseMetadata": {
+            "HTTPStatusCode": 200,
+            "RequestId": "fde3d219-2cce-11e6-8bae-372c75b0cc26"
+        }
+    }
+}
diff --git a/tests/data/placebo/test_rds_snapshot_delete/rds.DescribeDBSnapshots_1.json b/tests/data/placebo/test_rds_snapshot_delete/rds.DescribeDBSnapshots_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_delete/rds.DescribeDBSnapshots_1.json
@@ -0,0 +1,49 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "HTTPStatusCode": 200,
+            "RequestId": "9ec20898-2cce-11e6-9e53-995121fda0e7"
+        },
+        "DBSnapshots": [
+            {
+                "Engine": "postgres",
+                "SnapshotCreateTime": {
+                    "hour": 19,
+                    "__class__": "datetime",
+                    "month": 4,
+                    "second": 52,
+                    "microsecond": 776000,
+                    "year": 2016,
+                    "day": 21,
+                    "minute": 4
+                },
+                "AvailabilityZone": "us-east-1b",
+                "PercentProgress": 100,
+                "MasterUsername": "dbadmin",
+                "Encrypted": true,
+                "LicenseModel": "postgresql-license",
+                "StorageType": "gp2",
+                "Status": "available",
+                "VpcId": "vpc-f3581498",
+                "DBSnapshotIdentifier": "abc-restore-final-snapshot",
+                "InstanceCreateTime": {
+                    "hour": 3,
+                    "__class__": "datetime",
+                    "month": 3,
+                    "second": 42,
+                    "microsecond": 265000,
+                    "year": 2016,
+                    "day": 14,
+                    "minute": 33
+                },
+                "OptionGroupName": "default:postgres-9-4",
+                "AllocatedStorage": 100,
+                "EngineVersion": "9.4.5",
+                "SnapshotType": "manual",
+                "Port": 5432,
+                "DBInstanceIdentifier": "abc-restore"
+            }
+        ]
+    }
+}
diff --git a/tests/test_rds.py b/tests/test_rds.py
--- a/tests/test_rds.py
+++ b/tests/test_rds.py
@@ -18,7 +18,7 @@ def test_rds_tags(self):
         self.assertEqual(len(resources), 1)
 
     def test_rds_tag_and_remove(self):
-        self.patch(rds.RDS, 'executor_factory', MainThreadExecutor)        
+        self.patch(rds.RDS, 'executor_factory', MainThreadExecutor)
         session_factory = self.replay_flight_data('test_rds_tag_and_remove')
         client = session_factory().client('rds')
 
@@ -81,7 +81,7 @@ def test_rds_mark_and_match(self):
         self.assertEqual(len(resources), 1)
 
     def test_rds_default_vpc(self):
-        session_factory = self.replay_flight_data('test_rds_default_vpc')            
+        session_factory = self.replay_flight_data('test_rds_default_vpc')
         p = self.load_policy(
             {'name': 'rds-default-filters',
              'resource': 'rds',
@@ -94,7 +94,7 @@ def test_rds_default_vpc(self):
         self.assertEqual(len(resources), 1)
 
     def test_rds_snapshot(self):
-        session_factory = self.replay_flight_data('test_rds_snapshot')            
+        session_factory = self.replay_flight_data('test_rds_snapshot')
         p = self.load_policy(
             {'name': 'rds-snapshot',
              'resource': 'rds',
@@ -116,7 +116,7 @@ def test_rds_retention(self):
             session_factory=session_factory)
         resources = p.run()
         self.assertEqual(len(resources), 1)
-        
+
     def test_rds_delete(self):
         session_factory = self.replay_flight_data('test_rds_delete')
         p = self.load_policy(
@@ -130,4 +130,17 @@ def test_rds_delete(self):
             config={'region': 'us-west-2'},
             session_factory=session_factory)
         resources = p.run()
-        self.assertEqual(len(resources), 1)        
+        self.assertEqual(len(resources), 1)
+
+
+class RDSSnapshotTrimTest(BaseTest):
+
+    def test_rds_snapshot_trim(self):
+        factory = self.replay_flight_data('test_rds_snapshot_delete')
+        p = self.load_policy({
+            'name': 'rds-snapshot-trim',
+            'resource': 'rds-snapshot',
+            'actions': ['delete']},
+            session_factory=factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)