Repository: cloud-custodian/cloud-custodian

Issue:
ClientError: An error occurred (InvalidParameterValue) when calling the CreateDBSnapshot operation: The specified instance is a member of a cluster and a snapshot cannot be created directly. Please use the CreateDBClusterSnapshot API instead
This happens when the RDS `snapshot` action is called on an Aurora cluster member.



Discussion:
We can reuse the `_db_instance_eligible_for_backup` method that is used for the `retention` action.


Resolving patch:
diff --git a/c7n/resources/rds.py b/c7n/resources/rds.py
--- a/c7n/resources/rds.py
+++ b/c7n/resources/rds.py
@@ -147,6 +147,38 @@ def process_tags(db):
     with executor_factory(max_workers=1) as w:
         return list(w.map(process_tags, dbs))
 
+def _db_instance_eligible_for_backup(resource):
+    db_instance_id = resource['DBInstanceIdentifier']
+
+    # Database instance is not in available state
+    if resource.get('DBInstanceStatus', '') != 'available':
+        log.debug("DB instance %s is not in available state" %
+            (db_instance_id))
+        return False
+    # The specified DB Instance is a member of a cluster and its backup retention should not be modified directly.
+    #   Instead, modify the backup retention of the cluster using the ModifyDbCluster API
+    if resource.get('DBClusterIdentifier', ''):
+        log.debug("DB instance %s is a cluster member" %
+            (db_instance_id))
+        return False
+    # DB Backups not supported on a read replica for engine postgres
+    if (resource.get('ReadReplicaSourceDBInstanceIdentifier', '') and
+            resource.get('Engine', '') == 'postgres'):
+        log.debug("DB instance %s is a postgres read-replica" %
+            (db_instance_id))
+        return False
+    # DB Backups not supported on a read replica running a mysql version before 5.6.
+    if (resource.get('ReadReplicaSourceDBInstanceIdentifier', '') and
+            resource.get('Engine', '') == 'mysql'):
+        engine_version = resource.get('EngineVersion', '')
+        # Assume "<major>.<minor>.<whatever>"
+        match = re.match(r'(?P<major>\d+)\.(?P<minor>\d+)\..*', engine_version)
+        if (match and int(match.group('major')) < 5 or
+            (int(match.group('major')) == 5 and int(match.group('minor')) < 6)):
+            log.debug("DB instance %s is a version %s mysql read-replica" %
+                (db_instance_id, engine_version))
+            return False
+    return True
 
 @filters.register('default-vpc')
 class DefaultVpc(Filter):
@@ -319,6 +351,9 @@ def process(self, resources):
         return resources
 
     def process_rds_snapshot(self, resource):
+        if not _db_instance_eligible_for_backup(resource):
+            return
+
         c = local_session(self.manager.session_factory).client('rds')
         c.create_db_snapshot(
             DBSnapshotIdentifier="Backup-%s-%s" % (
@@ -356,7 +391,7 @@ def process_snapshot_retention(self, resource):
 
         if ((current_retention < new_retention or
                 current_copy_tags != new_copy_tags) and
-                self._db_instance_eligible_for_backup(resource)):
+                _db_instance_eligible_for_backup(resource)):
             self.set_retention_window(
                 resource,
                 max(current_retention, new_retention),
@@ -370,39 +405,6 @@ def set_retention_window(self, resource, retention, copy_tags):
             BackupRetentionPeriod=retention,
             CopyTagsToSnapshot=copy_tags)
 
-    def _db_instance_eligible_for_backup(self, resource):
-        db_instance_id = resource['DBInstanceIdentifier']
-
-        # Database instance is not in available state
-        if resource.get('DBInstanceStatus', '') != 'available':
-            log.debug("DB instance %s is not in available state" %
-                (db_instance_id))
-            return False
-        # The specified DB Instance is a member of a cluster and its backup retention should not be modified directly.
-        #   Instead, modify the backup retention of the cluster using the ModifyDbCluster API
-        if resource.get('DBClusterIdentifier', ''):
-            log.debug("DB instance %s is a cluster member" %
-                (db_instance_id))
-            return False
-        # DB Backups not supported on a read replica for engine postgres
-        if (resource.get('ReadReplicaSourceDBInstanceIdentifier', '') and
-                resource.get('Engine', '') == 'postgres'):
-            log.debug("DB instance %s is a postgres read-replica" %
-                (db_instance_id))
-            return False
-        # DB Backups not supported on a read replica running a mysql version before 5.6.
-        if (resource.get('ReadReplicaSourceDBInstanceIdentifier', '') and
-                resource.get('Engine', '') == 'mysql'):
-            engine_version = resource.get('EngineVersion', '')
-            # Assume "<major>.<minor>.<whatever>"
-            match = re.match(r'(?P<major>\d+)\.(?P<minor>\d+)\..*', engine_version)
-            if (match and int(match.group('major')) < 5 or
-                (int(match.group('major')) == 5 and int(match.group('minor')) < 6)):
-                log.debug("DB instance %s is a version %s mysql read-replica" %
-                    (db_instance_id, engine_version))
-                return False
-        return True
-
 
 @resources.register('rds-snapshot')
 class RDSSnapshot(QueryResourceManager):


Test patch:
diff --git a/tests/test_rds.py b/tests/test_rds.py
--- a/tests/test_rds.py
+++ b/tests/test_rds.py
@@ -181,6 +181,81 @@ def test_rds_delete(self):
         resources = p.run()
         self.assertEqual(len(resources), 1)
 
+    def test_rds_db_instance_eligible_for_backup(self):
+        resource = {
+            'DBInstanceIdentifier': 'ABC'
+        }
+        self.assertFalse(rds._db_instance_eligible_for_backup(resource))
+
+        resource = {
+            'DBInstanceIdentifier': 'ABC',
+            'DBInstanceStatus': 'funky'
+        }
+        self.assertFalse(rds._db_instance_eligible_for_backup(resource))
+
+        resource = {
+            'DBInstanceIdentifier': 'ABC',
+            'DBInstanceStatus': 'available'
+        }
+        self.assertTrue(rds._db_instance_eligible_for_backup(resource))
+
+        resource = {
+            'DBInstanceIdentifier': 'ABC',
+            'DBInstanceStatus': 'available',
+            'DBClusterIdentifier': 'C1'
+        }
+        self.assertFalse(rds._db_instance_eligible_for_backup(resource))
+
+        resource = {
+            'DBInstanceIdentifier': 'ABC',
+            'DBInstanceStatus': 'available',
+            'ReadReplicaSourceDBInstanceIdentifier': 'R1',
+            'Engine': 'postgres'
+        }
+        self.assertFalse(rds._db_instance_eligible_for_backup(resource))
+
+        resource = {
+            'DBInstanceIdentifier': 'ABC',
+            'DBInstanceStatus': 'available',
+            'Engine': 'postgres'
+        }
+        self.assertTrue(rds._db_instance_eligible_for_backup(resource))
+
+        resource = {
+            'DBInstanceIdentifier': 'ABC',
+            'DBInstanceStatus': 'available',
+            'Engine': 'mysql',
+            'EngineVersion': '5.5.1'
+        }
+        self.assertTrue(rds._db_instance_eligible_for_backup(resource))
+
+        resource = {
+            'DBInstanceIdentifier': 'ABC',
+            'DBInstanceStatus': 'available',
+            'ReadReplicaSourceDBInstanceIdentifier': 'R1',
+            'Engine': 'mysql',
+            'EngineVersion': '5.5.1'
+        }
+        self.assertFalse(rds._db_instance_eligible_for_backup(resource))
+
+        resource = {
+            'DBInstanceIdentifier': 'ABC',
+            'DBInstanceStatus': 'available',
+            'ReadReplicaSourceDBInstanceIdentifier': 'R1',
+            'Engine': 'mysql',
+            'EngineVersion': '5.7.1'
+        }
+        self.assertTrue(rds._db_instance_eligible_for_backup(resource))
+
+        resource = {
+            'DBInstanceIdentifier': 'ABC',
+            'DBInstanceStatus': 'available',
+            'ReadReplicaSourceDBInstanceIdentifier': 'R1',
+            'Engine': 'mysql',
+            'EngineVersion': '6.1.1'
+        }
+        self.assertTrue(rds._db_instance_eligible_for_backup(resource))
+
 
 class RDSSnapshotTrimTest(BaseTest):