Repository: cloud-custodian/cloud-custodian

Issue:
find unused subnet
and action for delete, subnets report their used/total so this should be easy.



Discussion:
Despite the console showing both the total IPs _and_ available IPs, the APIs actually only return the available IPs. We can easily calculate those numbers by looking at the CIDR though and doing the conversion, keeping in mind that Amazon [reserves 5 IP addresses in each subnet](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Subnets.html#VPC_Sizing).  

Describe call:

```
{
    "Subnets": [
        {
            "VpcId": "vpc-REMOVED",
            "Tags": [
                {
                    "Value": "MY-Sample-Subnet",
                    "Key": "Name"
                }
            ],
            "CidrBlock": "10.20.0.0/24",
            "MapPublicIpOnLaunch": false,
            "DefaultForAz": false,
            "State": "available",
            "AvailabilityZone": "us-east-1b",
            "SubnetId": "subnet-REMOVED",
            "AvailableIpAddressCount": 251
        }
    ]
}
```

We see 10.20.0.0/24, so we know /24 -> 256 IP Addresses. 256-5 reserved = 251. So the above would be an unused subnet. 

Is there a better way that I am missing?

that looks pretty good, the alternative is an exhaustive search through all resources that could be vpc attached, which may actually be what we need to do, since some of those resources (aka lambda) are transient in usage.

I’ve spent some time looking into this and I’m not sure about the feasibility. If we’re going to delete a subnet, we need to be 100% sure it’s “unused”.

As you pointed out, we probably need to search through resources rather than rely on the available number alone, but which resources? There’s a list of [related resources][1] in the docs, but it doesn’t include the one example you cited (Lambda) which makes me wonder what else might be missing.

Querying all of those resources individually would require a decent amount of effort, and I’m not sure it will get us to “100% sure”. Do you think it’s worth it?

[1]: http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Introduction.html#related-services


Resolving patch:
diff --git a/c7n/resources/vpc.py b/c7n/resources/vpc.py
--- a/c7n/resources/vpc.py
+++ b/c7n/resources/vpc.py
@@ -504,6 +504,76 @@ class SubnetVpcFilter(net_filters.VpcFilter):
 
     RelatedIdsExpression = "VpcId"
 
+@Subnet.filter_registry.register('ip-address-usage')
+class SubnetIpAddressUsageFilter(ValueFilter):
+    """Filter subnets based on available IP addresses.
+
+    :example:
+
+    Show subnets with no addresses in use.
+
+    .. code-block:: yaml
+
+            policies:
+              - name: empty-subnets
+                resource: aws.subnet
+                filters:
+                  - type: ip-address-usage
+                    key: NumberUsed
+                    value: 0
+
+    :example:
+
+    Show subnets where 90% or more addresses are in use.
+
+    .. code-block:: yaml
+
+            policies:
+              - name: almost-full-subnets
+                resource: aws.subnet
+                filters:
+                  - type: ip-address-usage
+                    key: PercentUsed
+                    op: greater-than
+                    value: 90
+
+    This filter allows ``key`` to be:
+
+    * ``MaxAvailable``: the number of addresses available based on a subnet's CIDR block size
+      (minus the 5 addresses
+      `reserved by AWS <https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html>`_)
+    * ``NumberUsed``: ``MaxAvailable`` minus the subnet's ``AvailableIpAddressCount`` value
+    * ``PercentUsed``: ``NumberUsed`` divided by ``MaxAvailable``
+    """
+    annotation_key = 'c7n:IpAddressUsage'
+    aws_reserved_addresses = 5
+    schema_alias = False
+    schema = type_schema(
+        'ip-address-usage',
+        key={'enum': ['MaxAvailable', 'NumberUsed', 'PercentUsed']},
+        rinherit=ValueFilter.schema,
+    )
+
+    def augment(self, resource):
+        cidr_block = parse_cidr(resource['CidrBlock'])
+        max_addresses = cidr_block.num_addresses - self.aws_reserved_addresses
+        resource[self.annotation_key] = dict(
+            MaxAvailable=max_addresses,
+            NumberUsed=max_addresses - resource['AvailableIpAddressCount'],
+            PercentUsed=round(
+                (max_addresses - resource['AvailableIpAddressCount']) / max_addresses * 100.0,
+                2
+            ),
+        )
+
+    def process(self, resources, event=None):
+        results = []
+        for r in resources:
+            if self.annotation_key not in r:
+                self.augment(r)
+            if self.match(r[self.annotation_key]):
+                results.append(r)
+        return results
 
 class ConfigSG(query.ConfigSource):
 


Test patch:
diff --git a/tests/data/placebo/test_subnet_ip_address_usage_filter/ec2.DescribeSubnets_1.json b/tests/data/placebo/test_subnet_ip_address_usage_filter/ec2.DescribeSubnets_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_subnet_ip_address_usage_filter/ec2.DescribeSubnets_1.json
@@ -0,0 +1,156 @@
+{
+    "status_code": 200,
+    "data": {
+        "Subnets": [
+            {
+                "AvailabilityZone": "us-east-2b",
+                "AvailabilityZoneId": "use2-az2",
+                "AvailableIpAddressCount": 507,
+                "CidrBlock": "10.0.2.0/23",
+                "DefaultForAz": false,
+                "MapPublicIpOnLaunch": false,
+                "MapCustomerOwnedIpOnLaunch": false,
+                "State": "available",
+                "SubnetId": "subnet-08e48f2f3b110fadc",
+                "VpcId": "vpc-056651ecc06e0c9d3",
+                "OwnerId": "644160558196",
+                "AssignIpv6AddressOnCreation": false,
+                "Ipv6CidrBlockAssociationSet": [],
+                "Tags": [
+                    {
+                        "Key": "Environment",
+                        "Value": "dev"
+                    },
+                    {
+                        "Key": "Owner",
+                        "Value": "aj"
+                    },
+                    {
+                        "Key": "Name",
+                        "Value": "dev-private-us-east-2b"
+                    }
+                ],
+                "SubnetArn": "arn:aws:ec2:us-east-2:644160558196:subnet/subnet-08e48f2f3b110fadc",
+                "EnableDns64": false,
+                "Ipv6Native": false,
+                "PrivateDnsNameOptionsOnLaunch": {
+                    "HostnameType": "ip-name",
+                    "EnableResourceNameDnsARecord": false,
+                    "EnableResourceNameDnsAAAARecord": false
+                }
+            },
+            {
+                "AvailabilityZone": "us-east-2c",
+                "AvailabilityZoneId": "use2-az3",
+                "AvailableIpAddressCount": 212,
+                "CidrBlock": "10.0.103.0/24",
+                "DefaultForAz": false,
+                "MapPublicIpOnLaunch": true,
+                "MapCustomerOwnedIpOnLaunch": false,
+                "State": "available",
+                "SubnetId": "subnet-0e5929766e049d22d",
+                "VpcId": "vpc-056651ecc06e0c9d3",
+                "OwnerId": "644160558196",
+                "AssignIpv6AddressOnCreation": false,
+                "Ipv6CidrBlockAssociationSet": [],
+                "Tags": [
+                    {
+                        "Key": "Owner",
+                        "Value": "aj"
+                    },
+                    {
+                        "Key": "Environment",
+                        "Value": "dev"
+                    },
+                    {
+                        "Key": "Name",
+                        "Value": "dev-public-us-east-2c"
+                    }
+                ],
+                "SubnetArn": "arn:aws:ec2:us-east-2:644160558196:subnet/subnet-0e5929766e049d22d",
+                "EnableDns64": false,
+                "Ipv6Native": false,
+                "PrivateDnsNameOptionsOnLaunch": {
+                    "HostnameType": "ip-name",
+                    "EnableResourceNameDnsARecord": false,
+                    "EnableResourceNameDnsAAAARecord": false
+                }
+            },
+            {
+                "AvailabilityZone": "us-east-2c",
+                "AvailabilityZoneId": "use2-az3",
+                "AvailableIpAddressCount": 20,
+                "CidrBlock": "10.0.4.0/23",
+                "DefaultForAz": false,
+                "MapPublicIpOnLaunch": false,
+                "MapCustomerOwnedIpOnLaunch": false,
+                "State": "available",
+                "SubnetId": "subnet-06b49a4a4f6e2be8f",
+                "VpcId": "vpc-056651ecc06e0c9d3",
+                "OwnerId": "644160558196",
+                "AssignIpv6AddressOnCreation": false,
+                "Ipv6CidrBlockAssociationSet": [],
+                "Tags": [
+                    {
+                        "Key": "Name",
+                        "Value": "dev-private-us-east-2c"
+                    },
+                    {
+                        "Key": "Owner",
+                        "Value": "aj"
+                    },
+                    {
+                        "Key": "Environment",
+                        "Value": "dev"
+                    }
+                ],
+                "SubnetArn": "arn:aws:ec2:us-east-2:644160558196:subnet/subnet-06b49a4a4f6e2be8f",
+                "EnableDns64": false,
+                "Ipv6Native": false,
+                "PrivateDnsNameOptionsOnLaunch": {
+                    "HostnameType": "ip-name",
+                    "EnableResourceNameDnsARecord": false,
+                    "EnableResourceNameDnsAAAARecord": false
+                }
+            },
+            {
+                "AvailabilityZone": "us-east-2b",
+                "AvailabilityZoneId": "use2-az2",
+                "AvailableIpAddressCount": 251,
+                "CidrBlock": "10.0.102.0/24",
+                "DefaultForAz": false,
+                "MapPublicIpOnLaunch": true,
+                "MapCustomerOwnedIpOnLaunch": false,
+                "State": "available",
+                "SubnetId": "subnet-0aa79961010e4a387",
+                "VpcId": "vpc-056651ecc06e0c9d3",
+                "OwnerId": "644160558196",
+                "AssignIpv6AddressOnCreation": false,
+                "Ipv6CidrBlockAssociationSet": [],
+                "Tags": [
+                    {
+                        "Key": "Name",
+                        "Value": "dev-public-us-east-2b"
+                    },
+                    {
+                        "Key": "Environment",
+                        "Value": "dev"
+                    },
+                    {
+                        "Key": "Owner",
+                        "Value": "aj"
+                    }
+                ],
+                "SubnetArn": "arn:aws:ec2:us-east-2:644160558196:subnet/subnet-0aa79961010e4a387",
+                "EnableDns64": false,
+                "Ipv6Native": false,
+                "PrivateDnsNameOptionsOnLaunch": {
+                    "HostnameType": "ip-name",
+                    "EnableResourceNameDnsARecord": false,
+                    "EnableResourceNameDnsAAAARecord": false
+                }
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/test_policy.py b/tests/test_policy.py
--- a/tests/test_policy.py
+++ b/tests/test_policy.py
@@ -825,6 +825,7 @@ def test_resource_permissions(self):
                     "instance-uptime",
                     "dead-letter",
                     "list-item",
+                    "ip-address-usage",
                 ):
                     continue
                 qk = "%s.filters.%s" % (k, n)
diff --git a/tests/test_vpc.py b/tests/test_vpc.py
--- a/tests/test_vpc.py
+++ b/tests/test_vpc.py
@@ -347,6 +347,45 @@ def test_subnet_endpoint_filter(self):
         self.assertEqual(len(resources), 2)
         self.assertTrue("subnet-068dfbf3f275a6ae8" in resources[0]["c7n:matched-vpc-endpoint"])
 
+    def test_subnet_ip_address_usage_filter(self):
+        factory = self.replay_flight_data("test_subnet_ip_address_usage_filter", region="us-east-2")
+        p = self.load_policy(
+            {
+                "name": "subnet-no-ips-used",
+                "resource": "aws.subnet",
+                "filters": [
+                    {
+                        "type": "ip-address-usage",
+                        "key": "NumberUsed",
+                        "value": 0,
+                    }
+                ],
+            },
+            session_factory=factory,
+            config={"region": "us-east-2"},
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 2)
+
+        p = self.load_policy(
+            {
+                "name": "subnet-almost-full",
+                "resource": "aws.subnet",
+                "filters": [
+                    {
+                        "type": "ip-address-usage",
+                        "key": "PercentUsed",
+                        "op": "greater-than",
+                        "value": 90,
+                    }
+                ],
+            },
+            session_factory=factory,
+            config={"region": "us-east-2"},
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+
     def test_endpoint_policy_filter(self):
         factory = self.replay_flight_data("test_endpoint_policy_filter")
         p = self.load_policy(