Repository: cloud-custodian/cloud-custodian

Issue:
aurora start/stop support
https://aws.amazon.com/about-aws/whats-new/2018/09/amazon-aurora-stop-and-start/


Discussion:

Resolving patch:
diff --git a/c7n/resources/rdscluster.py b/c7n/resources/rdscluster.py
--- a/c7n/resources/rdscluster.py
+++ b/c7n/resources/rdscluster.py
@@ -16,11 +16,10 @@
 import logging
 import functools
 
-from botocore.exceptions import ClientError
 from concurrent.futures import as_completed
 
-from c7n.actions import ActionRegistry, BaseAction
-from c7n.filters import FilterRegistry, AgeFilter, OPERATORS
+from c7n.actions import BaseAction
+from c7n.filters import AgeFilter, OPERATORS
 import c7n.filters.vpc as net_filters
 from c7n.manager import resources
 from c7n.query import QueryResourceManager
@@ -31,12 +30,6 @@
 
 log = logging.getLogger('custodian.rds-cluster')
 
-filters = FilterRegistry('rds-cluster.filters')
-actions = ActionRegistry('rds-cluster.actions')
-
-filters.register('tag-count', tags.TagCountFilter)
-filters.register('marked-for-op', tags.TagActionFilter)
-
 
 @resources.register('rds-cluster')
 class RDSCluster(QueryResourceManager):
@@ -54,8 +47,6 @@ class resource_type(object):
         dimension = 'DBClusterIdentifier'
         date = None
 
-    filter_registry = filters
-    action_registry = actions
     retry = staticmethod(get_retry(('Throttled',)))
 
     @property
@@ -74,6 +65,10 @@ def augment(self, dbs):
             self.generate_arn, self.retry)))
 
 
+RDSCluster.filter_registry.register('tag-count', tags.TagCountFilter)
+RDSCluster.filter_registry.register('marked-for-op', tags.TagActionFilter)
+
+
 def _rds_cluster_tags(model, dbs, session_factory, generator, retry):
     """Augment rds clusters with their respective tags."""
     client = local_session(session_factory).client('rds')
@@ -91,7 +86,7 @@ def process_tags(db):
     return list(filter(None, map(process_tags, dbs)))
 
 
-@actions.register('mark-for-op')
+@RDSCluster.action_registry.register('mark-for-op')
 class TagDelayedAction(tags.TagDelayedAction):
     """Mark a RDS cluster for specific custodian action
 
@@ -111,8 +106,8 @@ class TagDelayedAction(tags.TagDelayedAction):
     """
 
 
-@actions.register('tag')
-@actions.register('mark')
+@RDSCluster.action_registry.register('tag')
+@RDSCluster.action_registry.register('mark')
 class Tag(tags.Tag):
     """Mark/tag a RDS cluster with a key/value
 
@@ -141,8 +136,8 @@ def process_resource_set(self, client, dbs, ts):
             client.add_tags_to_resource(ResourceName=arn, Tags=ts)
 
 
-@actions.register('remove-tag')
-@actions.register('unmark')
+@RDSCluster.action_registry.register('remove-tag')
+@RDSCluster.action_registry.register('unmark')
 class RemoveTag(tags.RemoveTag):
     """Removes a tag or set of tags from RDS clusters
 
@@ -171,13 +166,13 @@ def process_resource_set(self, client, dbs, tag_keys):
                 TagKeys=tag_keys)
 
 
-@filters.register('security-group')
+@RDSCluster.filter_registry.register('security-group')
 class SecurityGroupFilter(net_filters.SecurityGroupFilter):
 
     RelatedIdsExpression = "VpcSecurityGroups[].VpcSecurityGroupId"
 
 
-@filters.register('subnet')
+@RDSCluster.filter_registry.register('subnet')
 class SubnetFilter(net_filters.SubnetFilter):
 
     RelatedIdsExpression = ""
@@ -201,10 +196,10 @@ def process(self, resources, event=None):
         return super(SubnetFilter, self).process(resources, event)
 
 
-filters.register('network-location', net_filters.NetworkLocation)
+RDSCluster.filter_registry.register('network-location', net_filters.NetworkLocation)
 
 
-@actions.register('delete')
+@RDSCluster.action_registry.register('delete')
 class Delete(BaseAction):
     """Action to delete a RDS cluster
 
@@ -257,22 +252,14 @@ def process(self, clusters):
             else:
                 params['FinalDBSnapshotIdentifier'] = snapshot_identifier(
                     'Final', cluster['DBClusterIdentifier'])
-            try:
-                client.delete_db_cluster(**params)
-            except ClientError as e:
-                if e.response['Error']['Code'] == 'InvalidDBClusterStateFault':
-                    self.log.info(
-                        'RDS cluster in invalid state: %s',
-                        cluster['DBClusterIdentifier'])
-                    continue
-                raise
 
-            self.log.info(
-                'Deleted RDS cluster: %s',
-                cluster['DBClusterIdentifier'])
+            _run_cluster_method(
+                client.delete_db_cluster, params,
+                (client.exceptions.DBClusterNotFoundFault, client.exceptions.ResourceNotFoundFault),
+                client.exceptions.InvalidDBClusterStateFault)
 
 
-@actions.register('retention')
+@RDSCluster.action_registry.register('retention')
 class RetentionWindow(BaseAction):
     """
     Action to set the retention period on rds cluster snapshots,
@@ -306,49 +293,82 @@ class RetentionWindow(BaseAction):
     permissions = ('rds:ModifyDBCluster',)
 
     def process(self, clusters):
-        with self.executor_factory(max_workers=2) as w:
-            futures = []
-            for cluster in clusters:
-                futures.append(w.submit(
-                    self.process_snapshot_retention,
-                    cluster))
-            for f in as_completed(futures):
-                if f.exception():
-                    self.log.error(
-                        "Exception setting RDS cluster retention  \n %s",
-                        f.exception())
+        client = local_session(self.manager.session_factory).client('rds')
+
+        for cluster in clusters:
+            self.process_snapshot_retention(client, cluster)
 
-    def process_snapshot_retention(self, cluster):
+    def process_snapshot_retention(self, client, cluster):
         current_retention = int(cluster.get('BackupRetentionPeriod', 0))
         new_retention = self.data['days']
         retention_type = self.data.get('enforce', 'min').lower()
 
         if retention_type == 'min':
             self.set_retention_window(
-                cluster,
-                max(current_retention, new_retention))
-            return cluster
-
-        if retention_type == 'max':
+                client, cluster, max(current_retention, new_retention))
+        elif retention_type == 'max':
             self.set_retention_window(
-                cluster,
-                min(current_retention, new_retention))
-            return cluster
+                client, cluster, min(current_retention, new_retention))
+        elif retention_type == 'exact':
+            self.set_retention_window(client, cluster, new_retention)
+
+    def set_retention_window(self, client, cluster, retention):
+        _run_cluster_method(
+            client.modify_db_cluster,
+            dict(DBClusterIdentifier=cluster['DBClusterIdentifier'],
+                 BackupRetentionPeriod=retention,
+                 PreferredBackupWindow=cluster['PreferredBackupWindow'],
+                 PreferredMaintenanceWindow=cluster['PreferredMaintenanceWindow']),
+            (client.exceptions.DBClusterNotFoundFault, client.exceptions.ResourceNotFoundFault),
+            client.exceptions.InvalidDBClusterStateFault)
+
+
+@RDSCluster.action_registry.register('stop')
+class Stop(BaseAction):
+    """Stop a running db cluster
+    """
+
+    schema = type_schema('stop')
+    permissions = ('rds:StopDBCluster',)
+
+    def process(self, clusters):
+        client = local_session(self.manager.session_factory).client('rds')
+        for c in clusters:
+            _run_cluster_method(
+                client.stop_db_cluster, dict(DBClusterIdentifier=c['DBClusterIdentifier']),
+                (client.exceptions.DBClusterNotFoundFault, client.exceptions.ResourceNotFoundFault),
+                client.exceptions.InvalidDBClusterStateFault)
+
+
+@RDSCluster.action_registry.register('start')
+class Start(BaseAction):
+    """Start a stopped db cluster
+    """
+
+    schema = type_schema('start')
+    permissions = ('rds:StartDBCluster',)
+
+    def process(self, clusters):
+        client = local_session(self.manager.session_factory).client('rds')
+        for c in clusters:
+            _run_cluster_method(
+                client.start_db_cluster, dict(DBClusterIdentifier=c['DBClusterIdentifier']),
+                (client.exceptions.DBClusterNotFoundFault, client.exceptions.ResourceNotFoundFault),
+                client.exceptions.InvalidDBClusterStateFault)
 
-        if retention_type == 'exact':
-            self.set_retention_window(cluster, new_retention)
-            return cluster
 
-    def set_retention_window(self, cluster, retention):
-        c = local_session(self.manager.session_factory).client('rds')
-        c.modify_db_cluster(
-            DBClusterIdentifier=cluster['DBClusterIdentifier'],
-            BackupRetentionPeriod=retention,
-            PreferredBackupWindow=cluster['PreferredBackupWindow'],
-            PreferredMaintenanceWindow=cluster['PreferredMaintenanceWindow'])
+def _run_cluster_method(method, params, ignore=(), warn=(), method_name=""):
+    try:
+        method(**params)
+    except ignore:
+        pass
+    except warn as e:
+        log.warning(
+            "error %s on cluster %s error %s",
+            method_name or method.__name__, params['DBClusterIdentifier'], e)
 
 
-@actions.register('snapshot')
+@RDSCluster.action_registry.register('snapshot')
 class Snapshot(BaseAction):
     """Action to create a snapshot of a rds cluster
 
@@ -367,26 +387,16 @@ class Snapshot(BaseAction):
     permissions = ('rds:CreateDBClusterSnapshot',)
 
     def process(self, clusters):
-        with self.executor_factory(max_workers=3) as w:
-            futures = []
-            for cluster in clusters:
-                futures.append(w.submit(
-                    self.process_cluster_snapshot,
-                    cluster))
-            for f in as_completed(futures):
-                if f.exception():
-                    self.log.error(
-                        "Exception creating RDS cluster snapshot  \n %s",
-                        f.exception())
-        return clusters
-
-    def process_cluster_snapshot(self, cluster):
-        c = local_session(self.manager.session_factory).client('rds')
-        c.create_db_cluster_snapshot(
-            DBClusterSnapshotIdentifier=snapshot_identifier(
-                'Backup',
-                cluster['DBClusterIdentifier']),
-            DBClusterIdentifier=cluster['DBClusterIdentifier'])
+        client = local_session(self.manager.session_factory).client('rds')
+        for cluster in clusters:
+            _run_cluster_method(
+                client.create_db_cluster_snapshot,
+                dict(
+                    DBClusterSnapshotIdentifier=snapshot_identifier(
+                        'Backup', cluster['DBClusterIdentifier']),
+                    DBClusterIdentifier=cluster['DBClusterIdentifier']),
+                (client.exceptions.DBClusterNotFoundFault, client.exceptions.ResourceNotFoundFault),
+                client.exceptions.InvalidDBClusterStateFault)
 
 
 @resources.register('rds-cluster-snapshot')
@@ -406,9 +416,6 @@ class resource_type(object):
         dimension = None
         date = 'SnapshotCreateTime'
 
-    filter_registry = FilterRegistry('rdscluster-snapshot.filters')
-    action_registry = ActionRegistry('rdscluster-snapshot.actions')
-
 
 @RDSClusterSnapshot.filter_registry.register('age')
 class RDSSnapshotAge(AgeFilter):
@@ -461,20 +468,28 @@ class RDSClusterSnapshotDelete(BaseAction):
 
     def process(self, snapshots):
         log.info("Deleting %d RDS cluster snapshots", len(snapshots))
-        with self.executor_factory(max_workers=3) as w:
+        client = local_session(self.manager.session_factory).client('rds')
+        error = None
+        with self.executor_factory(max_workers=2) as w:
             futures = []
             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():
+                    error = f.exception()
                     self.log.error(
                         "Exception deleting snapshot set \n %s",
                         f.exception())
+        if error:
+            raise error
         return snapshots
 
-    def process_snapshot_set(self, snapshots_set):
-        c = local_session(self.manager.session_factory).client('rds')
+    def process_snapshot_set(self, client, snapshots_set):
         for s in snapshots_set:
-            c.delete_db_cluster_snapshot(
-                DBClusterSnapshotIdentifier=s['DBClusterSnapshotIdentifier'])
+            try:
+                client.delete_db_cluster_snapshot(
+                    DBClusterSnapshotIdentifier=s['DBClusterSnapshotIdentifier'])
+            except (client.exceptions.DBSnapshotNotFoundFault,
+                    client.exceptions.InvalidDBSnapshotStateFault):
+                continue


Test patch:
diff --git a/tests/data/placebo/test_rdscluster_start/rds.DescribeDBClusters_1.json b/tests/data/placebo/test_rdscluster_start/rds.DescribeDBClusters_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_start/rds.DescribeDBClusters_1.json
@@ -0,0 +1,126 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusters": [
+            {
+                "AllocatedStorage": 1,
+                "AvailabilityZones": [
+                    "us-east-1d",
+                    "us-east-1a",
+                    "us-east-1b"
+                ],
+                "BackupRetentionPeriod": 1,
+                "DatabaseName": "mytest",
+                "DBClusterIdentifier": "mytest",
+                "DBClusterParameterGroup": "default.aurora5.6",
+                "DBSubnetGroup": "default",
+                "Status": "stopped",
+                "EarliestRestorableTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 20,
+                    "hour": 9,
+                    "minute": 31,
+                    "second": 30,
+                    "microsecond": 930000
+                },
+                "Endpoint": "mytest.cluster-cz6wy8cci3uf.us-east-1.rds.amazonaws.com",
+                "ReaderEndpoint": "mytest.cluster-ro-cz6wy8cci3uf.us-east-1.rds.amazonaws.com",
+                "MultiAZ": true,
+                "Engine": "aurora",
+                "EngineVersion": "5.6.10a",
+                "LatestRestorableTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 21,
+                    "hour": 12,
+                    "minute": 24,
+                    "second": 43,
+                    "microsecond": 569000
+                },
+                "Port": 3306,
+                "MasterUsername": "mytest",
+                "PreferredBackupWindow": "09:27-09:57",
+                "PreferredMaintenanceWindow": "tue:06:41-tue:07:11",
+                "ReadReplicaIdentifiers": [],
+                "DBClusterMembers": [
+                    {
+                        "DBInstanceIdentifier": "mycluster-us-east-1a",
+                        "IsClusterWriter": false,
+                        "DBClusterParameterGroupStatus": "in-sync",
+                        "PromotionTier": 1
+                    },
+                    {
+                        "DBInstanceIdentifier": "mycluster",
+                        "IsClusterWriter": true,
+                        "DBClusterParameterGroupStatus": "in-sync",
+                        "PromotionTier": 1
+                    }
+                ],
+                "VpcSecurityGroups": [
+                    {
+                        "VpcSecurityGroupId": "sg-02ec691909b2a4753",
+                        "Status": "active"
+                    },
+                    {
+                        "VpcSecurityGroupId": "sg-6c7fa917",
+                        "Status": "active"
+                    },
+                    {
+                        "VpcSecurityGroupId": "sg-08ee699c0c4e168e4",
+                        "Status": "active"
+                    }
+                ],
+                "HostedZoneId": "Z2R2ITUGPM61AM",
+                "StorageEncrypted": true,
+                "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/b10f842a-feb7-4318-92d5-0640a75b7688",
+                "DbClusterResourceId": "cluster-FLECGNGAS7PCRHRT7DHDF24BGU",
+                "DBClusterArn": "arn:aws:rds:us-east-1:644160558196:cluster:mytest",
+                "AssociatedRoles": [],
+                "IAMDatabaseAuthenticationEnabled": true,
+                "ClusterCreateTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 19,
+                    "hour": 15,
+                    "minute": 33,
+                    "second": 56,
+                    "microsecond": 465000
+                },
+                "EarliestBacktrackTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 21,
+                    "hour": 4,
+                    "minute": 16,
+                    "second": 23,
+                    "microsecond": 940000
+                },
+                "BacktrackWindow": 28800,
+                "BacktrackConsumedChangeRecords": 75776,
+                "EnabledCloudwatchLogsExports": [
+                    "slowquery"
+                ],
+                "EngineMode": "provisioned",
+                "DeletionProtection": true,
+                "HttpEndpointEnabled": false
+            }
+        ],
+        "ResponseMetadata": {
+            "RequestId": "f5bcf3fa-dcef-433c-90b2-cf93a9b2b250",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "f5bcf3fa-dcef-433c-90b2-cf93a9b2b250",
+                "content-type": "text/xml",
+                "content-length": "4042",
+                "vary": "Accept-Encoding",
+                "date": "Thu, 21 Feb 2019 12:33:16 GMT"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rdscluster_start/rds.DescribeDBClusters_2.json b/tests/data/placebo/test_rdscluster_start/rds.DescribeDBClusters_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_start/rds.DescribeDBClusters_2.json
@@ -0,0 +1,126 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusters": [
+            {
+                "AllocatedStorage": 1,
+                "AvailabilityZones": [
+                    "us-east-1d",
+                    "us-east-1a",
+                    "us-east-1b"
+                ],
+                "BackupRetentionPeriod": 1,
+                "DatabaseName": "mytest",
+                "DBClusterIdentifier": "mytest",
+                "DBClusterParameterGroup": "default.aurora5.6",
+                "DBSubnetGroup": "default",
+                "Status": "starting",
+                "EarliestRestorableTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 20,
+                    "hour": 9,
+                    "minute": 31,
+                    "second": 30,
+                    "microsecond": 930000
+                },
+                "Endpoint": "mytest.cluster-cz6wy8cci3uf.us-east-1.rds.amazonaws.com",
+                "ReaderEndpoint": "mytest.cluster-ro-cz6wy8cci3uf.us-east-1.rds.amazonaws.com",
+                "MultiAZ": true,
+                "Engine": "aurora",
+                "EngineVersion": "5.6.10a",
+                "LatestRestorableTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 21,
+                    "hour": 12,
+                    "minute": 24,
+                    "second": 43,
+                    "microsecond": 569000
+                },
+                "Port": 3306,
+                "MasterUsername": "mytest",
+                "PreferredBackupWindow": "09:27-09:57",
+                "PreferredMaintenanceWindow": "tue:06:41-tue:07:11",
+                "ReadReplicaIdentifiers": [],
+                "DBClusterMembers": [
+                    {
+                        "DBInstanceIdentifier": "mycluster-us-east-1a",
+                        "IsClusterWriter": false,
+                        "DBClusterParameterGroupStatus": "in-sync",
+                        "PromotionTier": 1
+                    },
+                    {
+                        "DBInstanceIdentifier": "mycluster",
+                        "IsClusterWriter": true,
+                        "DBClusterParameterGroupStatus": "in-sync",
+                        "PromotionTier": 1
+                    }
+                ],
+                "VpcSecurityGroups": [
+                    {
+                        "VpcSecurityGroupId": "sg-02ec691909b2a4753",
+                        "Status": "active"
+                    },
+                    {
+                        "VpcSecurityGroupId": "sg-6c7fa917",
+                        "Status": "active"
+                    },
+                    {
+                        "VpcSecurityGroupId": "sg-08ee699c0c4e168e4",
+                        "Status": "active"
+                    }
+                ],
+                "HostedZoneId": "Z2R2ITUGPM61AM",
+                "StorageEncrypted": true,
+                "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/b10f842a-feb7-4318-92d5-0640a75b7688",
+                "DbClusterResourceId": "cluster-FLECGNGAS7PCRHRT7DHDF24BGU",
+                "DBClusterArn": "arn:aws:rds:us-east-1:644160558196:cluster:mytest",
+                "AssociatedRoles": [],
+                "IAMDatabaseAuthenticationEnabled": true,
+                "ClusterCreateTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 19,
+                    "hour": 15,
+                    "minute": 33,
+                    "second": 56,
+                    "microsecond": 465000
+                },
+                "EarliestBacktrackTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 21,
+                    "hour": 4,
+                    "minute": 16,
+                    "second": 23,
+                    "microsecond": 940000
+                },
+                "BacktrackWindow": 28800,
+                "BacktrackConsumedChangeRecords": 75776,
+                "EnabledCloudwatchLogsExports": [
+                    "slowquery"
+                ],
+                "EngineMode": "provisioned",
+                "DeletionProtection": true,
+                "HttpEndpointEnabled": false
+            }
+        ],
+        "ResponseMetadata": {
+            "RequestId": "43e8e12e-4c08-42b8-9c9f-6e7e74a731f0",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "43e8e12e-4c08-42b8-9c9f-6e7e74a731f0",
+                "content-type": "text/xml",
+                "content-length": "4043",
+                "vary": "Accept-Encoding",
+                "date": "Thu, 21 Feb 2019 12:33:18 GMT"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rdscluster_start/rds.ListTagsForResource_1.json b/tests/data/placebo/test_rdscluster_start/rds.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_start/rds.ListTagsForResource_1.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 200,
+    "data": {
+        "TagList": [],
+        "ResponseMetadata": {
+            "RequestId": "b4b3d599-cb69-40ea-a890-cc74f622c319",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "b4b3d599-cb69-40ea-a890-cc74f622c319",
+                "content-type": "text/xml",
+                "content-length": "293",
+                "date": "Thu, 21 Feb 2019 12:33:16 GMT"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rdscluster_start/rds.StartDBCluster_1.json b/tests/data/placebo/test_rdscluster_start/rds.StartDBCluster_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_start/rds.StartDBCluster_1.json
@@ -0,0 +1,124 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBCluster": {
+            "AllocatedStorage": 1,
+            "AvailabilityZones": [
+                "us-east-1d",
+                "us-east-1a",
+                "us-east-1b"
+            ],
+            "BackupRetentionPeriod": 1,
+            "DatabaseName": "mytest",
+            "DBClusterIdentifier": "mytest",
+            "DBClusterParameterGroup": "default.aurora5.6",
+            "DBSubnetGroup": "default",
+            "Status": "stopped",
+            "EarliestRestorableTime": {
+                "__class__": "datetime",
+                "year": 2019,
+                "month": 2,
+                "day": 20,
+                "hour": 9,
+                "minute": 31,
+                "second": 30,
+                "microsecond": 930000
+            },
+            "Endpoint": "mytest.cluster-cz6wy8cci3uf.us-east-1.rds.amazonaws.com",
+            "ReaderEndpoint": "mytest.cluster-ro-cz6wy8cci3uf.us-east-1.rds.amazonaws.com",
+            "MultiAZ": true,
+            "Engine": "aurora",
+            "EngineVersion": "5.6.10a",
+            "LatestRestorableTime": {
+                "__class__": "datetime",
+                "year": 2019,
+                "month": 2,
+                "day": 21,
+                "hour": 12,
+                "minute": 24,
+                "second": 43,
+                "microsecond": 569000
+            },
+            "Port": 3306,
+            "MasterUsername": "mytest",
+            "PreferredBackupWindow": "09:27-09:57",
+            "PreferredMaintenanceWindow": "tue:06:41-tue:07:11",
+            "ReadReplicaIdentifiers": [],
+            "DBClusterMembers": [
+                {
+                    "DBInstanceIdentifier": "mycluster-us-east-1a",
+                    "IsClusterWriter": false,
+                    "DBClusterParameterGroupStatus": "in-sync",
+                    "PromotionTier": 1
+                },
+                {
+                    "DBInstanceIdentifier": "mycluster",
+                    "IsClusterWriter": true,
+                    "DBClusterParameterGroupStatus": "in-sync",
+                    "PromotionTier": 1
+                }
+            ],
+            "VpcSecurityGroups": [
+                {
+                    "VpcSecurityGroupId": "sg-02ec691909b2a4753",
+                    "Status": "active"
+                },
+                {
+                    "VpcSecurityGroupId": "sg-6c7fa917",
+                    "Status": "active"
+                },
+                {
+                    "VpcSecurityGroupId": "sg-08ee699c0c4e168e4",
+                    "Status": "active"
+                }
+            ],
+            "HostedZoneId": "Z2R2ITUGPM61AM",
+            "StorageEncrypted": true,
+            "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/b10f842a-feb7-4318-92d5-0640a75b7688",
+            "DbClusterResourceId": "cluster-FLECGNGAS7PCRHRT7DHDF24BGU",
+            "DBClusterArn": "arn:aws:rds:us-east-1:644160558196:cluster:mytest",
+            "AssociatedRoles": [],
+            "IAMDatabaseAuthenticationEnabled": true,
+            "ClusterCreateTime": {
+                "__class__": "datetime",
+                "year": 2019,
+                "month": 2,
+                "day": 19,
+                "hour": 15,
+                "minute": 33,
+                "second": 56,
+                "microsecond": 465000
+            },
+            "EarliestBacktrackTime": {
+                "__class__": "datetime",
+                "year": 2019,
+                "month": 2,
+                "day": 21,
+                "hour": 4,
+                "minute": 16,
+                "second": 23,
+                "microsecond": 940000
+            },
+            "BacktrackWindow": 28800,
+            "BacktrackConsumedChangeRecords": 75776,
+            "EnabledCloudwatchLogsExports": [
+                "slowquery"
+            ],
+            "EngineMode": "provisioned",
+            "DeletionProtection": true,
+            "HttpEndpointEnabled": false
+        },
+        "ResponseMetadata": {
+            "RequestId": "c461910e-1068-4164-af22-0e80d9ae899a",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "c461910e-1068-4164-af22-0e80d9ae899a",
+                "content-type": "text/xml",
+                "content-length": "3847",
+                "vary": "Accept-Encoding",
+                "date": "Thu, 21 Feb 2019 12:33:16 GMT"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rdscluster_stop/rds.DescribeDBClusters_1.json b/tests/data/placebo/test_rdscluster_stop/rds.DescribeDBClusters_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_stop/rds.DescribeDBClusters_1.json
@@ -0,0 +1,126 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusters": [
+            {
+                "AllocatedStorage": 1,
+                "AvailabilityZones": [
+                    "us-east-1d",
+                    "us-east-1a",
+                    "us-east-1b"
+                ],
+                "BackupRetentionPeriod": 1,
+                "DatabaseName": "mytest",
+                "DBClusterIdentifier": "mytest",
+                "DBClusterParameterGroup": "default.aurora5.6",
+                "DBSubnetGroup": "default",
+                "Status": "available",
+                "EarliestRestorableTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 20,
+                    "hour": 9,
+                    "minute": 31,
+                    "second": 30,
+                    "microsecond": 930000
+                },
+                "Endpoint": "mytest.cluster-cz6wy8cci3uf.us-east-1.rds.amazonaws.com",
+                "ReaderEndpoint": "mytest.cluster-ro-cz6wy8cci3uf.us-east-1.rds.amazonaws.com",
+                "MultiAZ": true,
+                "Engine": "aurora",
+                "EngineVersion": "5.6.10a",
+                "LatestRestorableTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 21,
+                    "hour": 12,
+                    "minute": 15,
+                    "second": 34,
+                    "microsecond": 118000
+                },
+                "Port": 3306,
+                "MasterUsername": "mytest",
+                "PreferredBackupWindow": "09:27-09:57",
+                "PreferredMaintenanceWindow": "tue:06:41-tue:07:11",
+                "ReadReplicaIdentifiers": [],
+                "DBClusterMembers": [
+                    {
+                        "DBInstanceIdentifier": "mycluster-us-east-1a",
+                        "IsClusterWriter": false,
+                        "DBClusterParameterGroupStatus": "in-sync",
+                        "PromotionTier": 1
+                    },
+                    {
+                        "DBInstanceIdentifier": "mycluster",
+                        "IsClusterWriter": true,
+                        "DBClusterParameterGroupStatus": "in-sync",
+                        "PromotionTier": 1
+                    }
+                ],
+                "VpcSecurityGroups": [
+                    {
+                        "VpcSecurityGroupId": "sg-02ec691909b2a4753",
+                        "Status": "active"
+                    },
+                    {
+                        "VpcSecurityGroupId": "sg-6c7fa917",
+                        "Status": "active"
+                    },
+                    {
+                        "VpcSecurityGroupId": "sg-08ee699c0c4e168e4",
+                        "Status": "active"
+                    }
+                ],
+                "HostedZoneId": "Z2R2ITUGPM61AM",
+                "StorageEncrypted": true,
+                "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/b10f842a-feb7-4318-92d5-0640a75b7688",
+                "DbClusterResourceId": "cluster-FLECGNGAS7PCRHRT7DHDF24BGU",
+                "DBClusterArn": "arn:aws:rds:us-east-1:644160558196:cluster:mytest",
+                "AssociatedRoles": [],
+                "IAMDatabaseAuthenticationEnabled": true,
+                "ClusterCreateTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 19,
+                    "hour": 15,
+                    "minute": 33,
+                    "second": 56,
+                    "microsecond": 465000
+                },
+                "EarliestBacktrackTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 21,
+                    "hour": 4,
+                    "minute": 12,
+                    "second": 23,
+                    "microsecond": 942000
+                },
+                "BacktrackWindow": 28800,
+                "BacktrackConsumedChangeRecords": 75776,
+                "EnabledCloudwatchLogsExports": [
+                    "slowquery"
+                ],
+                "EngineMode": "provisioned",
+                "DeletionProtection": true,
+                "HttpEndpointEnabled": false
+            }
+        ],
+        "ResponseMetadata": {
+            "RequestId": "6bcccda2-6f12-43cc-a4f3-456e911163c0",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "6bcccda2-6f12-43cc-a4f3-456e911163c0",
+                "content-type": "text/xml",
+                "content-length": "4044",
+                "vary": "Accept-Encoding",
+                "date": "Thu, 21 Feb 2019 12:16:34 GMT"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rdscluster_stop/rds.DescribeDBClusters_2.json b/tests/data/placebo/test_rdscluster_stop/rds.DescribeDBClusters_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_stop/rds.DescribeDBClusters_2.json
@@ -0,0 +1,126 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusters": [
+            {
+                "AllocatedStorage": 1,
+                "AvailabilityZones": [
+                    "us-east-1d",
+                    "us-east-1a",
+                    "us-east-1b"
+                ],
+                "BackupRetentionPeriod": 1,
+                "DatabaseName": "mytest",
+                "DBClusterIdentifier": "mytest",
+                "DBClusterParameterGroup": "default.aurora5.6",
+                "DBSubnetGroup": "default",
+                "Status": "stopping",
+                "EarliestRestorableTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 20,
+                    "hour": 9,
+                    "minute": 31,
+                    "second": 30,
+                    "microsecond": 930000
+                },
+                "Endpoint": "mytest.cluster-cz6wy8cci3uf.us-east-1.rds.amazonaws.com",
+                "ReaderEndpoint": "mytest.cluster-ro-cz6wy8cci3uf.us-east-1.rds.amazonaws.com",
+                "MultiAZ": true,
+                "Engine": "aurora",
+                "EngineVersion": "5.6.10a",
+                "LatestRestorableTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 21,
+                    "hour": 12,
+                    "minute": 15,
+                    "second": 34,
+                    "microsecond": 118000
+                },
+                "Port": 3306,
+                "MasterUsername": "mytest",
+                "PreferredBackupWindow": "09:27-09:57",
+                "PreferredMaintenanceWindow": "tue:06:41-tue:07:11",
+                "ReadReplicaIdentifiers": [],
+                "DBClusterMembers": [
+                    {
+                        "DBInstanceIdentifier": "mycluster-us-east-1a",
+                        "IsClusterWriter": false,
+                        "DBClusterParameterGroupStatus": "in-sync",
+                        "PromotionTier": 1
+                    },
+                    {
+                        "DBInstanceIdentifier": "mycluster",
+                        "IsClusterWriter": true,
+                        "DBClusterParameterGroupStatus": "in-sync",
+                        "PromotionTier": 1
+                    }
+                ],
+                "VpcSecurityGroups": [
+                    {
+                        "VpcSecurityGroupId": "sg-02ec691909b2a4753",
+                        "Status": "active"
+                    },
+                    {
+                        "VpcSecurityGroupId": "sg-6c7fa917",
+                        "Status": "active"
+                    },
+                    {
+                        "VpcSecurityGroupId": "sg-08ee699c0c4e168e4",
+                        "Status": "active"
+                    }
+                ],
+                "HostedZoneId": "Z2R2ITUGPM61AM",
+                "StorageEncrypted": true,
+                "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/b10f842a-feb7-4318-92d5-0640a75b7688",
+                "DbClusterResourceId": "cluster-FLECGNGAS7PCRHRT7DHDF24BGU",
+                "DBClusterArn": "arn:aws:rds:us-east-1:644160558196:cluster:mytest",
+                "AssociatedRoles": [],
+                "IAMDatabaseAuthenticationEnabled": true,
+                "ClusterCreateTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 19,
+                    "hour": 15,
+                    "minute": 33,
+                    "second": 56,
+                    "microsecond": 465000
+                },
+                "EarliestBacktrackTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 21,
+                    "hour": 4,
+                    "minute": 12,
+                    "second": 23,
+                    "microsecond": 942000
+                },
+                "BacktrackWindow": 28800,
+                "BacktrackConsumedChangeRecords": 75776,
+                "EnabledCloudwatchLogsExports": [
+                    "slowquery"
+                ],
+                "EngineMode": "provisioned",
+                "DeletionProtection": true,
+                "HttpEndpointEnabled": false
+            }
+        ],
+        "ResponseMetadata": {
+            "RequestId": "20d05bb1-40ec-4237-bf0b-014800fe4b5b",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "20d05bb1-40ec-4237-bf0b-014800fe4b5b",
+                "content-type": "text/xml",
+                "content-length": "4043",
+                "vary": "Accept-Encoding",
+                "date": "Thu, 21 Feb 2019 12:17:11 GMT"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rdscluster_stop/rds.ListTagsForResource_1.json b/tests/data/placebo/test_rdscluster_stop/rds.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_stop/rds.ListTagsForResource_1.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 200,
+    "data": {
+        "TagList": [],
+        "ResponseMetadata": {
+            "RequestId": "56fa1f76-d7f3-432a-a7c4-72dfb4f416db",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "56fa1f76-d7f3-432a-a7c4-72dfb4f416db",
+                "content-type": "text/xml",
+                "content-length": "293",
+                "date": "Thu, 21 Feb 2019 12:16:34 GMT"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rdscluster_stop/rds.StopDBCluster_1.json b/tests/data/placebo/test_rdscluster_stop/rds.StopDBCluster_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_stop/rds.StopDBCluster_1.json
@@ -0,0 +1,124 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBCluster": {
+            "AllocatedStorage": 1,
+            "AvailabilityZones": [
+                "us-east-1d",
+                "us-east-1a",
+                "us-east-1b"
+            ],
+            "BackupRetentionPeriod": 1,
+            "DatabaseName": "mytest",
+            "DBClusterIdentifier": "mytest",
+            "DBClusterParameterGroup": "default.aurora5.6",
+            "DBSubnetGroup": "default",
+            "Status": "available",
+            "EarliestRestorableTime": {
+                "__class__": "datetime",
+                "year": 2019,
+                "month": 2,
+                "day": 20,
+                "hour": 9,
+                "minute": 31,
+                "second": 30,
+                "microsecond": 930000
+            },
+            "Endpoint": "mytest.cluster-cz6wy8cci3uf.us-east-1.rds.amazonaws.com",
+            "ReaderEndpoint": "mytest.cluster-ro-cz6wy8cci3uf.us-east-1.rds.amazonaws.com",
+            "MultiAZ": true,
+            "Engine": "aurora",
+            "EngineVersion": "5.6.10a",
+            "LatestRestorableTime": {
+                "__class__": "datetime",
+                "year": 2019,
+                "month": 2,
+                "day": 21,
+                "hour": 12,
+                "minute": 15,
+                "second": 34,
+                "microsecond": 118000
+            },
+            "Port": 3306,
+            "MasterUsername": "mytest",
+            "PreferredBackupWindow": "09:27-09:57",
+            "PreferredMaintenanceWindow": "tue:06:41-tue:07:11",
+            "ReadReplicaIdentifiers": [],
+            "DBClusterMembers": [
+                {
+                    "DBInstanceIdentifier": "mycluster-us-east-1a",
+                    "IsClusterWriter": false,
+                    "DBClusterParameterGroupStatus": "in-sync",
+                    "PromotionTier": 1
+                },
+                {
+                    "DBInstanceIdentifier": "mycluster",
+                    "IsClusterWriter": true,
+                    "DBClusterParameterGroupStatus": "in-sync",
+                    "PromotionTier": 1
+                }
+            ],
+            "VpcSecurityGroups": [
+                {
+                    "VpcSecurityGroupId": "sg-02ec691909b2a4753",
+                    "Status": "active"
+                },
+                {
+                    "VpcSecurityGroupId": "sg-6c7fa917",
+                    "Status": "active"
+                },
+                {
+                    "VpcSecurityGroupId": "sg-08ee699c0c4e168e4",
+                    "Status": "active"
+                }
+            ],
+            "HostedZoneId": "Z2R2ITUGPM61AM",
+            "StorageEncrypted": true,
+            "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/b10f842a-feb7-4318-92d5-0640a75b7688",
+            "DbClusterResourceId": "cluster-FLECGNGAS7PCRHRT7DHDF24BGU",
+            "DBClusterArn": "arn:aws:rds:us-east-1:644160558196:cluster:mytest",
+            "AssociatedRoles": [],
+            "IAMDatabaseAuthenticationEnabled": true,
+            "ClusterCreateTime": {
+                "__class__": "datetime",
+                "year": 2019,
+                "month": 2,
+                "day": 19,
+                "hour": 15,
+                "minute": 33,
+                "second": 56,
+                "microsecond": 465000
+            },
+            "EarliestBacktrackTime": {
+                "__class__": "datetime",
+                "year": 2019,
+                "month": 2,
+                "day": 21,
+                "hour": 4,
+                "minute": 12,
+                "second": 23,
+                "microsecond": 942000
+            },
+            "BacktrackWindow": 28800,
+            "BacktrackConsumedChangeRecords": 75776,
+            "EnabledCloudwatchLogsExports": [
+                "slowquery"
+            ],
+            "EngineMode": "provisioned",
+            "DeletionProtection": true,
+            "HttpEndpointEnabled": false
+        },
+        "ResponseMetadata": {
+            "RequestId": "671cc0e4-2630-4e8a-aa69-4beda4511ff7",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "671cc0e4-2630-4e8a-aa69-4beda4511ff7",
+                "content-type": "text/xml",
+                "content-length": "3845",
+                "vary": "Accept-Encoding",
+                "date": "Thu, 21 Feb 2019 12:16:34 GMT"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/test_rdscluster.py b/tests/test_rdscluster.py
--- a/tests/test_rdscluster.py
+++ b/tests/test_rdscluster.py
@@ -14,7 +14,7 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 from c7n.executor import MainThreadExecutor
-from c7n.resources.rdscluster import RDSCluster
+from c7n.resources.rdscluster import RDSCluster, _run_cluster_method
 
 from .common import BaseTest
 
@@ -25,7 +25,7 @@ def remove_augments(self):
         # This exists because we added tag augmentation after eight other tests
         # were created and I did not want to re-create the state to re-record
         # them with the extra API call. If those get re-recorded we can remove
-        # this.
+        # this. -scotwk
         self.patch(RDSCluster, "augment", lambda x, y: y)
 
     def test_rdscluster_security_group(self):
@@ -285,6 +285,68 @@ def test_rdscluster_mark_match_unmark(self):
         tag_map = {t["Key"]: t["Value"] for t in tags["TagList"]}
         self.assertFalse("custodian_next" in tag_map)
 
+    def test_run_cluster_method(self):
+
+        output = self.capture_logging('custodian.rds-cluster')
+
+        def foobar(DBClusterIdentifier):
+            raise ValueError("eek")
+
+        # unspecified
+        self.assertRaises(
+            ValueError, _run_cluster_method, foobar, {'DBClusterIdentifier': 'mytest'})
+
+        # ignored
+        try:
+            _run_cluster_method(foobar, {'DBClusterIdentifier': 'mytest'}, ValueError)
+        except ValueError:
+            self.fail("Shouldn't raise")
+        finally:
+            self.assertEqual(output.getvalue(), "")
+
+        # warn
+        try:
+            _run_cluster_method(
+                foobar, {'DBClusterIdentifier': 'mytest'}, warn=(ValueError, KeyError))
+        except ValueError:
+            self.fail("Shouldn't raise")
+        finally:
+            self.assertTrue("eek" in output.getvalue())
+
+    def test_stop(self):
+        factory = self.replay_flight_data("test_rdscluster_stop")
+        p = self.load_policy(
+            {"name": "rdscluster",
+             "resource": "rds-cluster",
+             "filters": [{'DBClusterIdentifier': 'mytest'}],
+             'actions': ['stop']},
+            session_factory=factory, config={'account_id': '644160558196'})
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(resources[0]['Status'], 'available')
+
+        client = factory().client('rds')
+        cluster = client.describe_db_clusters(
+            DBClusterIdentifier='mytest').get('DBClusters')[0]
+        self.assertEqual(cluster['Status'], 'stopping')
+
+    def test_start(self):
+        factory = self.replay_flight_data("test_rdscluster_start")
+        p = self.load_policy(
+            {"name": "rdscluster",
+             "resource": "rds-cluster",
+             "filters": [{'DBClusterIdentifier': 'mytest'}],
+             'actions': ['start']},
+            session_factory=factory, config={'account_id': '644160558196'})
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(resources[0]['Status'], 'stopped')
+
+        client = factory().client('rds')
+        cluster = client.describe_db_clusters(
+            DBClusterIdentifier='mytest').get('DBClusters')[0]
+        self.assertEqual(cluster['Status'], 'starting')
+
 
 class RDSClusterSnapshotTest(BaseTest):