Repository: cloud-custodian/cloud-custodian

Issue:
aws - expose vpc metrics filter
### Describe the feature

using these metrics https://docs.aws.amazon.com/vpc/latest/userguide/vpc-cloudwatch.html#nau-cloudwatch to filter vpcs.

### Extra information or context

_No response_


Discussion:

Resolving patch:
diff --git a/c7n/resources/vpc.py b/c7n/resources/vpc.py
--- a/c7n/resources/vpc.py
+++ b/c7n/resources/vpc.py
@@ -44,6 +44,47 @@ class resource_type(query.TypeInfo):
         id_prefix = "vpc-"
 
 
+@Vpc.filter_registry.register('metrics')
+class VpcMetrics(MetricsFilter):
+
+    def get_dimensions(self, resource):
+        return [{"Name": "Per-VPC Metrics",
+                 "Value": resource["VpcId"]}]
+
+
+@Vpc.action_registry.register('modify')
+class ModifyVpc(BaseAction):
+    """Modify vpc settings
+    """
+
+    schema = type_schema(
+        'modify',
+        **{'dnshostnames': {'type': 'boolean'},
+           'dnssupport': {'type': 'boolean'},
+           'addressusage': {'type': 'boolean'}}
+    )
+
+    key_params = (
+        ('dnshostnames', 'EnableDnsHostnames'),
+        ('dnssupport', 'EnableDnsSupport'),
+        ('addressusage', 'EnableNetworkAddressUsageMetrics')
+    )
+
+    permissions = ('ec2:ModifyVpcAttribute',)
+
+    def process(self, resources):
+        client = local_session(self.manager.session_factory).client('ec2')
+
+        for policy_key, param_name in self.key_params:
+            if policy_key not in self.data:
+                continue
+            params = {param_name: {'Value': self.data[policy_key]}}
+            # can only modify one attribute per request
+            for r in resources:
+                params['VpcId'] = r['VpcId']
+                client.modify_vpc_attribute(**params)
+
+
 @Vpc.filter_registry.register('flow-logs')
 class FlowLogFilter(Filter):
     """Are flow logs enabled on the resource.
@@ -324,35 +365,40 @@ class AttributesFilter(Filter):
     schema = type_schema(
         'vpc-attributes',
         dnshostnames={'type': 'boolean'},
+        addressusage={'type': 'boolean'},
         dnssupport={'type': 'boolean'})
+
     permissions = ('ec2:DescribeVpcAttribute',)
 
+    key_params = (
+        ('dnshostnames', 'enableDnsHostnames'),
+        ('dnssupport', 'enableDnsSupport'),
+        ('addressusage', 'enableNetworkAddressUsageMetrics')
+    )
+    annotation_key = 'c7n:attributes'
+
     def process(self, resources, event=None):
         results = []
         client = local_session(self.manager.session_factory).client('ec2')
-        dns_hostname = self.data.get('dnshostnames', None)
-        dns_support = self.data.get('dnssupport', None)
 
         for r in resources:
-            if dns_hostname is not None:
-                hostname = client.describe_vpc_attribute(
-                    VpcId=r['VpcId'],
-                    Attribute='enableDnsHostnames'
-                )['EnableDnsHostnames']['Value']
-            if dns_support is not None:
-                support = client.describe_vpc_attribute(
+            found = True
+            for policy_key, vpc_attr in self.key_params:
+                if policy_key not in self.data:
+                    continue
+                policy_value = self.data[policy_key]
+                response_attr = "%s%s" % (vpc_attr[0].upper(), vpc_attr[1:])
+                value = client.describe_vpc_attribute(
                     VpcId=r['VpcId'],
-                    Attribute='enableDnsSupport'
-                )['EnableDnsSupport']['Value']
-            if dns_hostname is not None and dns_support is not None:
-                if dns_hostname == hostname and dns_support == support:
-                    results.append(r)
-            elif dns_hostname is not None and dns_support is None:
-                if dns_hostname == hostname:
-                    results.append(r)
-            elif dns_support is not None and dns_hostname is None:
-                if dns_support == support:
-                    results.append(r)
+                    Attribute=vpc_attr
+                )
+                value = value[response_attr]['Value']
+                r.setdefault(self.annotation_key, {})[policy_key] = value
+                if policy_value != value:
+                    found = False
+                    break
+            if found:
+                results.append(r)
         return results
 
 
@@ -510,6 +556,7 @@ class SubnetVpcFilter(net_filters.VpcFilter):
 
     RelatedIdsExpression = "VpcId"
 
+
 @Subnet.filter_registry.register('ip-address-usage')
 class SubnetIpAddressUsageFilter(ValueFilter):
     """Filter subnets based on available IP addresses.
diff --git a/tools/ops/logsetup.py b/tools/ops/logsetup.py
--- a/tools/ops/logsetup.py
+++ b/tools/ops/logsetup.py
@@ -86,7 +86,7 @@ def main():
     try:
         manager.publish(func)
     except Exception:
-        import traceback, pdb, sys
+        import traceback, pdb
         traceback.print_exc()
         pdb.post_mortem(sys.exc_info()[-1])
 


Test patch:
diff --git a/tests/data/placebo/test_vpc_attributes/ec2.DescribeVpcAttribute_2.json b/tests/data/placebo/test_vpc_attributes/ec2.DescribeVpcAttribute_2.json
--- a/tests/data/placebo/test_vpc_attributes/ec2.DescribeVpcAttribute_2.json
+++ b/tests/data/placebo/test_vpc_attributes/ec2.DescribeVpcAttribute_2.json
@@ -1,20 +1,20 @@
 {
     "status_code": 200, 
     "data": {
-        "VpcId": "vpc-f1516b97", 
+        "VpcId": "vpc-d2d616b5", 
         "ResponseMetadata": {
             "RetryAttempts": 0, 
             "HTTPStatusCode": 200, 
-            "RequestId": "e53f4689-2fe7-421d-a553-2540ab982ccf", 
+            "RequestId": "911d83c6-f472-4c84-830f-734941fe819b", 
             "HTTPHeaders": {
                 "transfer-encoding": "chunked", 
                 "vary": "Accept-Encoding", 
                 "server": "AmazonEC2", 
                 "content-type": "text/xml;charset=UTF-8", 
-                "date": "Fri, 22 Dec 2017 16:16:40 GMT"
+                "date": "Fri, 22 Dec 2017 16:16:41 GMT"
             }
         }, 
-        "EnableDnsSupport": {
+        "EnableDnsHostnames": {
             "Value": true
         }
     }
diff --git a/tests/data/placebo/test_vpc_attributes/ec2.DescribeVpcAttribute_3.json b/tests/data/placebo/test_vpc_attributes/ec2.DescribeVpcAttribute_3.json
--- a/tests/data/placebo/test_vpc_attributes/ec2.DescribeVpcAttribute_3.json
+++ b/tests/data/placebo/test_vpc_attributes/ec2.DescribeVpcAttribute_3.json
@@ -1,20 +1,20 @@
 {
     "status_code": 200, 
     "data": {
-        "VpcId": "vpc-d2d616b5", 
+        "VpcId": "vpc-f1516b97", 
         "ResponseMetadata": {
             "RetryAttempts": 0, 
             "HTTPStatusCode": 200, 
-            "RequestId": "911d83c6-f472-4c84-830f-734941fe819b", 
+            "RequestId": "e53f4689-2fe7-421d-a553-2540ab982ccf", 
             "HTTPHeaders": {
                 "transfer-encoding": "chunked", 
                 "vary": "Accept-Encoding", 
                 "server": "AmazonEC2", 
                 "content-type": "text/xml;charset=UTF-8", 
-                "date": "Fri, 22 Dec 2017 16:16:41 GMT"
+                "date": "Fri, 22 Dec 2017 16:16:40 GMT"
             }
         }, 
-        "EnableDnsHostnames": {
+        "EnableDnsSupport": {
             "Value": true
         }
     }
diff --git a/tests/data/placebo/test_vpc_usage_detect_set/ec2.DescribeVpcAttribute_1.json b/tests/data/placebo/test_vpc_usage_detect_set/ec2.DescribeVpcAttribute_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_vpc_usage_detect_set/ec2.DescribeVpcAttribute_1.json
@@ -0,0 +1,10 @@
+{
+    "status_code": 200,
+    "data": {
+        "VpcId": "vpc-6d20940b",
+        "EnableNetworkAddressUsageMetrics": {
+            "Value": false
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_vpc_usage_detect_set/ec2.DescribeVpcAttribute_2.json b/tests/data/placebo/test_vpc_usage_detect_set/ec2.DescribeVpcAttribute_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_vpc_usage_detect_set/ec2.DescribeVpcAttribute_2.json
@@ -0,0 +1,10 @@
+{
+    "status_code": 200,
+    "data": {
+        "VpcId": "vpc-6d20940b",
+        "EnableNetworkAddressUsageMetrics": {
+            "Value": true
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_vpc_usage_detect_set/ec2.DescribeVpcs_1.json b/tests/data/placebo/test_vpc_usage_detect_set/ec2.DescribeVpcs_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_vpc_usage_detect_set/ec2.DescribeVpcs_1.json
@@ -0,0 +1,26 @@
+{
+    "status_code": 200,
+    "data": {
+        "Vpcs": [
+            {
+                "CidrBlock": "172.31.0.0/16",
+                "DhcpOptionsId": "dopt-d82384be",
+                "State": "available",
+                "VpcId": "vpc-6d20940b",
+                "OwnerId": "644160558196",
+                "InstanceTenancy": "default",
+                "CidrBlockAssociationSet": [
+                    {
+                        "AssociationId": "vpc-cidr-assoc-6f2bb704",
+                        "CidrBlock": "172.31.0.0/16",
+                        "CidrBlockState": {
+                            "State": "associated"
+                        }
+                    }
+                ],
+                "IsDefault": true
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_vpc_usage_detect_set/ec2.ModifyVpcAttribute_1.json b/tests/data/placebo/test_vpc_usage_detect_set/ec2.ModifyVpcAttribute_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_vpc_usage_detect_set/ec2.ModifyVpcAttribute_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_vpc_usage_metrics/ec2.DescribeVpcs_1.json b/tests/data/placebo/test_vpc_usage_metrics/ec2.DescribeVpcs_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_vpc_usage_metrics/ec2.DescribeVpcs_1.json
@@ -0,0 +1,26 @@
+{
+    "status_code": 200,
+    "data": {
+        "Vpcs": [
+            {
+                "CidrBlock": "172.31.0.0/16",
+                "DhcpOptionsId": "dopt-d82384be",
+                "State": "available",
+                "VpcId": "vpc-6d20940b",
+                "OwnerId": "644160558196",
+                "InstanceTenancy": "default",
+                "CidrBlockAssociationSet": [
+                    {
+                        "AssociationId": "vpc-cidr-assoc-6f2bb704",
+                        "CidrBlock": "172.31.0.0/16",
+                        "CidrBlockState": {
+                            "State": "associated"
+                        }
+                    }
+                ],
+                "IsDefault": true
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_vpc_usage_metrics/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_vpc_usage_metrics/monitoring.GetMetricStatistics_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_vpc_usage_metrics/monitoring.GetMetricStatistics_1.json
@@ -0,0 +1,8 @@
+{
+    "status_code": 200,
+    "data": {
+        "Label": "NetworkAddressUsage",
+        "Datapoints": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/test_vpc.py b/tests/test_vpc.py
--- a/tests/test_vpc.py
+++ b/tests/test_vpc.py
@@ -44,6 +44,47 @@ def test_ec2_igw_subnet(test, ec2_igw_subnet):
     assert expected_instance_ids == result_instance_ids
 
 
+def test_vpc_usage(test):
+    factory = test.replay_flight_data(
+        'test_vpc_usage_detect_set', region='us-west-1')
+    p = test.load_policy({
+        'name': 'vpc-usage',
+        'resource': 'aws.vpc',
+        'filters': [
+            {'type': 'vpc-attributes',
+             'addressusage': False}],
+        'actions': [
+            {'type': 'modify',
+             'addressusage': True}],
+        }, session_factory=factory)
+    resources = p.run()
+    assert len(resources) == 1
+    client = factory().client('ec2')
+    result = client.describe_vpc_attribute(
+        VpcId=resources[0]['VpcId'],
+        Attribute='enableNetworkAddressUsageMetrics')
+    assert result['EnableNetworkAddressUsageMetrics']['Value'] is True
+
+
+def test_vpc_usage_metric(test):
+    factory = test.replay_flight_data(
+        'test_vpc_usage_metrics', region='us-west-1')
+    p = test.load_policy({
+        'name': 'vpc-usage',
+        'resource': 'aws.vpc',
+        'filters': [
+            {'type': 'metrics',
+             'statistics': 'Maximum',
+             'value': 0,
+             'op': 'gte',
+             'missing-value': 0,
+             'days': 2,
+             'name': 'NetworkAddressUsage'}
+        ]}, session_factory=factory)
+    resources = p.run()
+    assert resources[0]['VpcId'] == 'vpc-6d20940b'
+
+
 def test_eni_igw_subnet(test):
     factory = test.replay_flight_data('test_eni_public_subnet')
     p = test.load_policy({
@@ -264,7 +305,9 @@ def test_attributes_filter_all(self):
                 "name": "dns-hostnames-and-support-enabled",
                 "resource": "vpc",
                 "filters": [
-                    {"type": "vpc-attributes", "dnshostnames": True, "dnssupport": True}
+                    {"type": "vpc-attributes",
+                     "dnshostnames": True,
+                     "dnssupport": True}
                 ],
             },
             session_factory=self.session_factory,