Repository: cloud-custodian/cloud-custodian

Issue:
Support for EFS Lifecycles
According to this, it's not an available action: https://cloudcustodian.io/docs/aws/resources/efs.html

Console and CLI instructions from AWS are here: https://docs.aws.amazon.com/efs/latest/ug/enable-lifecycle-management.html


Discussion:

Resolving patch:
diff --git a/c7n/resources/efs.py b/c7n/resources/efs.py
--- a/c7n/resources/efs.py
+++ b/c7n/resources/efs.py
@@ -13,13 +13,15 @@
 # limitations under the License.
 from __future__ import absolute_import, division, print_function, unicode_literals
 
-from c7n.actions import Action
+from c7n.actions import Action, BaseAction
+from c7n.exceptions import PolicyValidationError
 from c7n.filters.kms import KmsRelatedFilter
 from c7n.manager import resources
 from c7n.filters.vpc import SecurityGroupFilter, SubnetFilter
 from c7n.query import QueryResourceManager, ChildResourceManager, TypeInfo
 from c7n.tags import universal_augment
 from c7n.utils import local_session, type_schema, get_retry
+from .aws import shape_validate
 
 
 @resources.register('efs')
@@ -137,3 +139,57 @@ def unmount_filesystems(self, resources):
             for t in client.describe_mount_targets(
                     FileSystemId=r['FileSystemId'])['MountTargets']:
                 client.delete_mount_target(MountTargetId=t['MountTargetId'])
+
+
+@ElasticFileSystem.action_registry.register('configure-lifecycle-policy')
+class ConfigureLifecycle(BaseAction):
+    """Enable/disable lifecycle policy for efs.
+
+    :example:
+
+      .. code-block:: yaml
+
+            policies:
+              - name: efs-apply-lifecycle
+                resource: efs
+                actions:
+                  - type: configure-lifecycle-policy
+                    state: enable
+                    rules:
+                      - 'TransitionToIA': 'AFTER_7_DAYS'
+
+    """
+    schema = type_schema(
+        'configure-lifecycle-policy',
+        state={'enum': ['enable', 'disable']},
+        rules={
+            'type': 'array',
+            'items': {'type': 'object'}},
+        required=['state'])
+
+    permissions = ('elasticfilesystem:PutLifecycleConfiguration',)
+    shape = 'PutLifecycleConfigurationRequest'
+
+    def validate(self):
+        if self.data.get('state') == 'enable' and 'rules' not in self.data:
+            raise PolicyValidationError(
+                'rules are required to enable lifecycle configuration %s' % (self.manager.data))
+        if self.data.get('state') == 'disable' and 'rules' in self.data:
+            raise PolicyValidationError(
+                'rules not required to disable lifecycle configuration %s' % (self.manager.data))
+        if self.data.get('rules'):
+            attrs = {}
+            attrs['LifecyclePolicies'] = self.data['rules']
+            attrs['FileSystemId'] = 'PolicyValidator'
+            return shape_validate(attrs, self.shape, 'efs')
+
+    def process(self, resources):
+        client = local_session(self.manager.session_factory).client('efs')
+        op_map = {'enable': self.data.get('rules'), 'disable': []}
+        for r in resources:
+            try:
+                client.put_lifecycle_configuration(
+                    FileSystemId=r['FileSystemId'],
+                    LifecyclePolicies=op_map.get(self.data.get('state')))
+            except client.exceptions.FileSystemNotFound:
+                continue


Test patch:
diff --git a/tests/data/placebo/test_disable_lifecycle_policy/elasticfilesystem.DescribeFileSystems_1.json b/tests/data/placebo/test_disable_lifecycle_policy/elasticfilesystem.DescribeFileSystems_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_disable_lifecycle_policy/elasticfilesystem.DescribeFileSystems_1.json
@@ -0,0 +1,50 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "c911eebe-3bc1-4360-9e40-58a0a5d06554",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "c911eebe-3bc1-4360-9e40-58a0a5d06554",
+                "content-type": "application/json",
+                "content-length": "528",
+                "date": "Sun, 26 Jan 2020 00:35:38 GMT"
+            },
+            "RetryAttempts": 0
+        },
+        "FileSystems": [
+            {
+                "OwnerId": "644160558196",
+                "CreationToken": "console-13ad1ab9-b969-4df9-9dd4-f4fa152d45f3",
+                "FileSystemId": "fs-fac23c7a",
+                "CreationTime": {
+                    "__class__": "datetime",
+                    "year": 2020,
+                    "month": 1,
+                    "day": 25,
+                    "hour": 19,
+                    "minute": 11,
+                    "second": 48,
+                    "microsecond": 0
+                },
+                "LifeCycleState": "available",
+                "Name": "c7n-test",
+                "NumberOfMountTargets": 1,
+                "SizeInBytes": {
+                    "Value": 6144,
+                    "ValueInIA": 0,
+                    "ValueInStandard": 6144
+                },
+                "PerformanceMode": "generalPurpose",
+                "Encrypted": false,
+                "ThroughputMode": "bursting",
+                "Tags": [
+                    {
+                        "Key": "Name",
+                        "Value": "c7n-test"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_disable_lifecycle_policy/elasticfilesystem.DescribeLifecycleConfiguration_1.json b/tests/data/placebo/test_disable_lifecycle_policy/elasticfilesystem.DescribeLifecycleConfiguration_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_disable_lifecycle_policy/elasticfilesystem.DescribeLifecycleConfiguration_1.json
@@ -0,0 +1,21 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "fbe81164-98c9-4e60-8544-72aabf22d715",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "fbe81164-98c9-4e60-8544-72aabf22d715",
+                "content-type": "application/json",
+                "content-length": "57",
+                "date": "Sun, 26 Jan 2020 00:35:38 GMT"
+            },
+            "RetryAttempts": 0
+        },
+        "LifecyclePolicies": [
+            {
+                "TransitionToIA": "AFTER_7_DAYS"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_disable_lifecycle_policy/elasticfilesystem.DescribeLifecycleConfiguration_2.json b/tests/data/placebo/test_disable_lifecycle_policy/elasticfilesystem.DescribeLifecycleConfiguration_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_disable_lifecycle_policy/elasticfilesystem.DescribeLifecycleConfiguration_2.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "b391d7d2-4011-42c1-bab1-7ea97f61e81a",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "b391d7d2-4011-42c1-bab1-7ea97f61e81a",
+                "content-type": "application/json",
+                "content-length": "24",
+                "date": "Sun, 26 Jan 2020 00:35:39 GMT"
+            },
+            "RetryAttempts": 0
+        },
+        "LifecyclePolicies": []
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_disable_lifecycle_policy/elasticfilesystem.PutLifecycleConfiguration_1.json b/tests/data/placebo/test_disable_lifecycle_policy/elasticfilesystem.PutLifecycleConfiguration_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_disable_lifecycle_policy/elasticfilesystem.PutLifecycleConfiguration_1.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "de17947e-f8c4-4d88-a2f9-0197fdedd8bf",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "de17947e-f8c4-4d88-a2f9-0197fdedd8bf",
+                "content-type": "application/json",
+                "content-length": "24",
+                "date": "Sun, 26 Jan 2020 00:35:39 GMT"
+            },
+            "RetryAttempts": 0
+        },
+        "LifecyclePolicies": []
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_disable_lifecycle_policy/tagging.GetResources_1.json b/tests/data/placebo/test_disable_lifecycle_policy/tagging.GetResources_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_disable_lifecycle_policy/tagging.GetResources_1.json
@@ -0,0 +1,28 @@
+{
+    "status_code": 200,
+    "data": {
+        "PaginationToken": "",
+        "ResourceTagMappingList": [
+            {
+                "ResourceARN": "arn:aws:elasticfilesystem:us-east-1:644160558196:file-system/fs-fac23c7a",
+                "Tags": [
+                    {
+                        "Key": "Name",
+                        "Value": "c7n-test"
+                    }
+                ]
+            }
+        ],
+        "ResponseMetadata": {
+            "RequestId": "ac875717-fc8b-4d4b-ae3a-793f6b675848",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "ac875717-fc8b-4d4b-ae3a-793f6b675848",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "183",
+                "date": "Sun, 26 Jan 2020 00:35:38 GMT"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_enable_lifecycle_policy/elasticfilesystem.DescribeFileSystems_1.json b/tests/data/placebo/test_enable_lifecycle_policy/elasticfilesystem.DescribeFileSystems_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_enable_lifecycle_policy/elasticfilesystem.DescribeFileSystems_1.json
@@ -0,0 +1,50 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "cec1f785-0f33-46e4-b74b-ca4c00012112",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "cec1f785-0f33-46e4-b74b-ca4c00012112",
+                "content-type": "application/json",
+                "content-length": "528",
+                "date": "Sun, 26 Jan 2020 00:32:19 GMT"
+            },
+            "RetryAttempts": 0
+        },
+        "FileSystems": [
+            {
+                "OwnerId": "644160558196",
+                "CreationToken": "console-13ad1ab9-b969-4df9-9dd4-f4fa152d45f3",
+                "FileSystemId": "fs-fac23c7a",
+                "CreationTime": {
+                    "__class__": "datetime",
+                    "year": 2020,
+                    "month": 1,
+                    "day": 25,
+                    "hour": 19,
+                    "minute": 11,
+                    "second": 48,
+                    "microsecond": 0
+                },
+                "LifeCycleState": "available",
+                "Name": "c7n-test",
+                "NumberOfMountTargets": 1,
+                "SizeInBytes": {
+                    "Value": 6144,
+                    "ValueInIA": 0,
+                    "ValueInStandard": 6144
+                },
+                "PerformanceMode": "generalPurpose",
+                "Encrypted": false,
+                "ThroughputMode": "bursting",
+                "Tags": [
+                    {
+                        "Key": "Name",
+                        "Value": "c7n-test"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_enable_lifecycle_policy/elasticfilesystem.DescribeLifecycleConfiguration_1.json b/tests/data/placebo/test_enable_lifecycle_policy/elasticfilesystem.DescribeLifecycleConfiguration_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_enable_lifecycle_policy/elasticfilesystem.DescribeLifecycleConfiguration_1.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "ea2a214e-7494-4118-be84-6ca7caed654f",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "ea2a214e-7494-4118-be84-6ca7caed654f",
+                "content-type": "application/json",
+                "content-length": "24",
+                "date": "Sun, 26 Jan 2020 00:32:18 GMT"
+            },
+            "RetryAttempts": 0
+        },
+        "LifecyclePolicies": []
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_enable_lifecycle_policy/elasticfilesystem.DescribeLifecycleConfiguration_2.json b/tests/data/placebo/test_enable_lifecycle_policy/elasticfilesystem.DescribeLifecycleConfiguration_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_enable_lifecycle_policy/elasticfilesystem.DescribeLifecycleConfiguration_2.json
@@ -0,0 +1,21 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "5fcc0e8e-d703-4d90-b0f4-80dee4d23140",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "5fcc0e8e-d703-4d90-b0f4-80dee4d23140",
+                "content-type": "application/json",
+                "content-length": "57",
+                "date": "Sun, 26 Jan 2020 00:32:19 GMT"
+            },
+            "RetryAttempts": 0
+        },
+        "LifecyclePolicies": [
+            {
+                "TransitionToIA": "AFTER_7_DAYS"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_enable_lifecycle_policy/elasticfilesystem.PutLifecycleConfiguration_1.json b/tests/data/placebo/test_enable_lifecycle_policy/elasticfilesystem.PutLifecycleConfiguration_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_enable_lifecycle_policy/elasticfilesystem.PutLifecycleConfiguration_1.json
@@ -0,0 +1,21 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "61d9b746-327c-4775-9499-d51f6b175f23",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "61d9b746-327c-4775-9499-d51f6b175f23",
+                "content-type": "application/json",
+                "content-length": "57",
+                "date": "Sun, 26 Jan 2020 00:32:19 GMT"
+            },
+            "RetryAttempts": 0
+        },
+        "LifecyclePolicies": [
+            {
+                "TransitionToIA": "AFTER_7_DAYS"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_enable_lifecycle_policy/tagging.GetResources_1.json b/tests/data/placebo/test_enable_lifecycle_policy/tagging.GetResources_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_enable_lifecycle_policy/tagging.GetResources_1.json
@@ -0,0 +1,28 @@
+{
+    "status_code": 200,
+    "data": {
+        "PaginationToken": "",
+        "ResourceTagMappingList": [
+            {
+                "ResourceARN": "arn:aws:elasticfilesystem:us-east-1:644160558196:file-system/fs-fac23c7a",
+                "Tags": [
+                    {
+                        "Key": "Name",
+                        "Value": "c7n-test"
+                    }
+                ]
+            }
+        ],
+        "ResponseMetadata": {
+            "RequestId": "01fe86a5-fa47-40a0-8f86-426b7ffe9ede",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "01fe86a5-fa47-40a0-8f86-426b7ffe9ede",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "183",
+                "date": "Sun, 26 Jan 2020 00:32:19 GMT"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/test_efs.py b/tests/test_efs.py
--- a/tests/test_efs.py
+++ b/tests/test_efs.py
@@ -13,6 +13,8 @@
 # limitations under the License.
 from __future__ import absolute_import, division, print_function, unicode_literals
 
+from c7n.exceptions import PolicyValidationError
+
 from .common import BaseTest, functional
 
 import uuid
@@ -123,3 +125,68 @@ def test_kms_alias(self):
         self.assertEqual(
             resources[0]['KmsKeyId'],
             'arn:aws:kms:us-east-1:644160558196:key/8785aeb9-a616-4e2b-bbd3-df3cde76bcc5') # NOQA
+
+    def test_enable_lifecycle_policy(self):
+        factory = self.replay_flight_data("test_enable_lifecycle_policy")
+        client = factory().client("efs")
+        res = client.describe_lifecycle_configuration(FileSystemId="fs-fac23c7a")
+        self.assertEqual(res.get('LifecyclePolicies'), [])
+        p = self.load_policy(
+            {
+                "name": "efs-lifecycle-policy",
+                "resource": "efs",
+                "filters": [{"Name": "c7n-test"}],
+                "actions": [
+                    {
+                        "type": "configure-lifecycle-policy",
+                        "state": "enable",
+                        "rules": [{'TransitionToIA': 'AFTER_7_DAYS'}],
+                    }
+                ]
+            },
+            session_factory=factory,
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(resources[0]["Name"], "c7n-test")
+        self.assertEqual(resources[0]["FileSystemId"], "fs-fac23c7a")
+        response = client.describe_lifecycle_configuration(FileSystemId="fs-fac23c7a")
+        self.assertEqual(response.get('LifecyclePolicies'), [{'TransitionToIA': 'AFTER_7_DAYS'}])
+
+    def test_disable_lifecycle_policy(self):
+        factory = self.replay_flight_data("test_disable_lifecycle_policy")
+        client = factory().client("efs")
+        res = client.describe_lifecycle_configuration(FileSystemId="fs-fac23c7a")
+        self.assertEqual(res.get('LifecyclePolicies'), [{'TransitionToIA': 'AFTER_7_DAYS'}])
+        p = self.load_policy(
+            {
+                "name": "efs-lifecycle-policy-disable",
+                "resource": "efs",
+                "filters": [{"Name": "c7n-test"}],
+                "actions": [
+                    {
+                        "type": "configure-lifecycle-policy",
+                        "state": "disable",
+                    }
+                ]
+            },
+            session_factory=factory,
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(resources[0]["Name"], "c7n-test")
+        self.assertEqual(resources[0]["FileSystemId"], "fs-fac23c7a")
+        response = client.describe_lifecycle_configuration(FileSystemId="fs-fac23c7a")
+        self.assertEqual(response.get('LifecyclePolicies'), [])
+
+    def test_lifecycle_policy_validation_error(self):
+        self.assertRaises(
+            PolicyValidationError,
+            self.load_policy,
+            {
+                "name": "efs-lifecycle",
+                "resource": "efs",
+                "filters": [{"Name": "c7n-test"}],
+                "actions": [{"type": "configure-lifecycle-policy", "state": "enable"}],
+            }
+        )