Repository: cloud-custodian/cloud-custodian

Issue:
aws.backup-plan - add new resource
Potentially use in conjunction with the missing filter to ensure that backup plans are set in the account, and enable or set backup plans

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/backup.html

https://aws.amazon.com/blogs/aws/aws-backup-automate-and-centrally-manage-your-backups/


Discussion:
i see a few interesting integrations, where this could be exposed.

Resolving patch:
diff --git a/c7n/resources/__init__.py b/c7n/resources/__init__.py
--- a/c7n/resources/__init__.py
+++ b/c7n/resources/__init__.py
@@ -35,6 +35,7 @@ def load_resources():
     import c7n.resources.appelb
     import c7n.resources.asg
     import c7n.resources.awslambda
+    import c7n.resources.backup
     import c7n.resources.batch
     import c7n.resources.cfn
     import c7n.resources.cloudfront
diff --git a/c7n/resources/backup.py b/c7n/resources/backup.py
new file mode 100644
--- /dev/null
+++ b/c7n/resources/backup.py
@@ -0,0 +1,61 @@
+# Copyright 2019 Capital One Services, LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from c7n.manager import resources
+from c7n.query import QueryResourceManager
+from c7n.utils import local_session
+
+
+@resources.register('backup-plan')
+class BackupPlan(QueryResourceManager):
+
+    class resource_type(object):
+        service = 'backup'
+        enum_spec = ('list_backup_plans', 'BackupPlansList', None)
+        detail_spec = ('get_backup_plan', 'BackupPlanId', 'BackupPlanId', 'BackupPlan')
+        id = 'BackupPlanName'
+        name = 'BackupPlanId'
+        dimension = None
+        filter_name = None
+        filter_type = None
+
+    def get_arns(self, resources):
+        return [r['BackupPlanArn'] for r in resources]
+
+    def augment(self, resources):
+        super(BackupPlan, self).augment(resources)
+        client = local_session(self.session_factory).client('backup')
+        results = []
+        for r in resources:
+            try:
+                tags = client.list_tags(ResourceArn=r['BackupPlanArn']).get('Tags', {})
+            except client.exceptions.ResourceNotFoundException:
+                continue
+            r['Tags'] = [{'Name': k, 'Value': v} for k, v in tags.items()]
+            results.append(r)
+
+        return results
+
+    def get_resources(self, resource_ids, cache=True):
+        client = local_session(self.session_factory).client('backup')
+        resources = []
+
+        for rid in resource_ids:
+            try:
+                resources.append(
+                    client.get_backup_plan(BackupPlanId=rid)['BackupPlan'])
+            except client.exceptions.ResourceNotFoundException:
+                continue
+        return resources


Test patch:
diff --git a/tests/data/placebo/test_backup_augment/backup.GetBackupPlan_1.json b/tests/data/placebo/test_backup_augment/backup.GetBackupPlan_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_backup_augment/backup.GetBackupPlan_1.json
@@ -0,0 +1,61 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "412eeec5-de22-4b29-aeb8-428e1c55bbaa",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "application/json",
+                "date": "Thu, 17 Jan 2019 15:40:40 GMT",
+                "x-amzn-requestid": "412eeec5-de22-4b29-aeb8-428e1c55bbaa",
+                "content-length": "988",
+                "connection": "keep-alive"
+            },
+            "RetryAttempts": 0
+        },
+        "BackupPlan": {
+            "BackupPlanName": "test",
+            "Rules": [
+                {
+                    "RuleName": "dev",
+                    "TargetBackupVaultName": "Default",
+                    "ScheduleExpression": "cron(0 5 ? * * *)",
+                    "StartWindowMinutes": 480,
+                    "CompletionWindowMinutes": 10080,
+                    "Lifecycle": {
+                        "DeleteAfterDays": 21
+                    },
+                    "RecoveryPointTags": {
+                        "CostCenter": "Backup"
+                    },
+                    "RuleId": "1d5ef03b-d787-4a05-9e0a-04fdb2d4793c"
+                },
+                {
+                    "RuleName": "weekly-archive",
+                    "TargetBackupVaultName": "Default",
+                    "ScheduleExpression": "cron(0 5 ? * 7 *)",
+                    "StartWindowMinutes": 480,
+                    "CompletionWindowMinutes": 10080,
+                    "Lifecycle": {
+                        "MoveToColdStorageAfterDays": 90,
+                        "DeleteAfterDays": 180
+                    },
+                    "RuleId": "f9f067cb-c58d-4b8b-b38e-f2aa8e0a47b4"
+                }
+            ]
+        },
+        "BackupPlanId": "14c4daac-b199-45ce-8644-c541734ebd14",
+        "BackupPlanArn": "arn:aws:backup:us-east-1:644160558196:backup-plan:14c4daac-b199-45ce-8644-c541734ebd14",
+        "VersionId": "NTNlNjNkOGMtM2M4NS00NWMwLTk3YjMtNzk1NzNhMjRjMTZj",
+        "CreationDate": {
+            "__class__": "datetime",
+            "year": 2019,
+            "month": 1,
+            "day": 17,
+            "hour": 6,
+            "minute": 1,
+            "second": 58,
+            "microsecond": 667000
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_backup_augment/backup.GetBackupPlan_2.json b/tests/data/placebo/test_backup_augment/backup.GetBackupPlan_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_backup_augment/backup.GetBackupPlan_2.json
@@ -0,0 +1,61 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "2396f0eb-2867-45a4-8f85-4ca021855d22",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "application/json",
+                "date": "Thu, 17 Jan 2019 15:40:41 GMT",
+                "x-amzn-requestid": "2396f0eb-2867-45a4-8f85-4ca021855d22",
+                "content-length": "988",
+                "connection": "keep-alive"
+            },
+            "RetryAttempts": 0
+        },
+        "BackupPlan": {
+            "BackupPlanName": "test",
+            "Rules": [
+                {
+                    "RuleName": "dev",
+                    "TargetBackupVaultName": "Default",
+                    "ScheduleExpression": "cron(0 5 ? * * *)",
+                    "StartWindowMinutes": 480,
+                    "CompletionWindowMinutes": 10080,
+                    "Lifecycle": {
+                        "DeleteAfterDays": 21
+                    },
+                    "RecoveryPointTags": {
+                        "CostCenter": "Backup"
+                    },
+                    "RuleId": "1d5ef03b-d787-4a05-9e0a-04fdb2d4793c"
+                },
+                {
+                    "RuleName": "weekly-archive",
+                    "TargetBackupVaultName": "Default",
+                    "ScheduleExpression": "cron(0 5 ? * 7 *)",
+                    "StartWindowMinutes": 480,
+                    "CompletionWindowMinutes": 10080,
+                    "Lifecycle": {
+                        "MoveToColdStorageAfterDays": 90,
+                        "DeleteAfterDays": 180
+                    },
+                    "RuleId": "f9f067cb-c58d-4b8b-b38e-f2aa8e0a47b4"
+                }
+            ]
+        },
+        "BackupPlanId": "14c4daac-b199-45ce-8644-c541734ebd14",
+        "BackupPlanArn": "arn:aws:backup:us-east-1:644160558196:backup-plan:14c4daac-b199-45ce-8644-c541734ebd14",
+        "VersionId": "NTNlNjNkOGMtM2M4NS00NWMwLTk3YjMtNzk1NzNhMjRjMTZj",
+        "CreationDate": {
+            "__class__": "datetime",
+            "year": 2019,
+            "month": 1,
+            "day": 17,
+            "hour": 6,
+            "minute": 1,
+            "second": 58,
+            "microsecond": 667000
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_backup_augment/backup.ListBackupPlans_1.json b/tests/data/placebo/test_backup_augment/backup.ListBackupPlans_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_backup_augment/backup.ListBackupPlans_1.json
@@ -0,0 +1,35 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "443e5682-fd75-440d-9332-35afecd21cf8",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "application/json",
+                "date": "Thu, 17 Jan 2019 15:40:40 GMT",
+                "x-amzn-requestid": "443e5682-fd75-440d-9332-35afecd21cf8",
+                "content-length": "387",
+                "connection": "keep-alive"
+            },
+            "RetryAttempts": 0
+        },
+        "BackupPlansList": [
+            {
+                "BackupPlanArn": "arn:aws:backup:us-east-1:644160558196:backup-plan:14c4daac-b199-45ce-8644-c541734ebd14",
+                "BackupPlanId": "14c4daac-b199-45ce-8644-c541734ebd14",
+                "CreationDate": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 1,
+                    "day": 17,
+                    "hour": 6,
+                    "minute": 1,
+                    "second": 58,
+                    "microsecond": 667000
+                },
+                "VersionId": "NTNlNjNkOGMtM2M4NS00NWMwLTk3YjMtNzk1NzNhMjRjMTZj",
+                "BackupPlanName": "test"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_backup_augment/backup.ListTags_1.json b/tests/data/placebo/test_backup_augment/backup.ListTags_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_backup_augment/backup.ListTags_1.json
@@ -0,0 +1,20 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "9fa6209d-a14a-4d64-bb60-fb1258054575",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "application/json",
+                "date": "Thu, 17 Jan 2019 15:40:40 GMT",
+                "x-amzn-requestid": "9fa6209d-a14a-4d64-bb60-fb1258054575",
+                "content-length": "43",
+                "connection": "keep-alive"
+            },
+            "RetryAttempts": 0
+        },
+        "Tags": {
+            "App": "Backups"
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/test_backup.py b/tests/test_backup.py
new file mode 100644
--- /dev/null
+++ b/tests/test_backup.py
@@ -0,0 +1,38 @@
+# Copyright 2019 Capital One Services, LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from .common import BaseTest
+
+
+class BackupTest(BaseTest):
+
+    def test_augment(self):
+        factory = self.replay_flight_data("test_backup_augment")
+        p = self.load_policy({
+            'name': 'all-backup',
+            'resource': 'aws.backup-plan'}, session_factory=factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        plan = resources.pop()
+        self.assertEqual(
+            plan['Tags'],
+            [{'Name': 'App', 'Value': 'Backups'}])
+        self.assertTrue('Rules' in plan)
+
+        self.assertEqual(
+            p.resource_manager.get_arns([plan]),
+            [plan['BackupPlanArn']])
+        resources = p.resource_manager.get_resources([plan['BackupPlanId']])
+        self.assertEqual(len(resources), 1)