diff --git a/awslimitchecker/services/vpc.py b/awslimitchecker/services/vpc.py index bed40336..d6981b16 100644 --- a/awslimitchecker/services/vpc.py +++ b/awslimitchecker/services/vpc.py @@ -48,6 +48,8 @@ logger = logging.getLogger(__name__) +DEFAULT_ENI_LIMIT = 350 + class _VpcService(_AwsService): @@ -71,6 +73,7 @@ def find_usage(self): self._find_usage_gateways() self._find_usage_nat_gateways(subnet_to_az) self._find_usages_vpn_gateways() + self._find_usage_network_interfaces() self._have_usage = True logger.debug("Done checking usage.") @@ -210,6 +213,20 @@ def _find_usages_vpn_gateways(self): aws_type='AWS::EC2::VPNGateway' ) + def _find_usage_network_interfaces(self): + """find usage of network interfaces""" + enis = paginate_dict( + self.conn.describe_network_interfaces, + alc_marker_path=['NextToken'], + alc_data_path=['NetworkInterfaces'], + alc_marker_param='NextToken' + ) + + self.limits['Network interfaces per Region']._add_current_usage( + len(enis), + aws_type='AWS::EC2::NetworkInterface' + ) + def get_limits(self): """ Return all known limits for this service, as a dict of their names @@ -307,9 +324,42 @@ def get_limits(self): self.critical_threshold, limit_type='AWS::EC2::VPNGateway' ) + + limits['Network interfaces per Region'] = AwsLimit( + 'Network interfaces per Region', + self, + DEFAULT_ENI_LIMIT, + self.warning_threshold, + self.critical_threshold, + limit_type='AWS::EC2::NetworkInterface' + ) self.limits = limits return limits + def _update_limits_from_api(self): + """ + Query EC2's DescribeAccountAttributes API action and + update the network interface limit, as needed. Updates ``self.limits``. + + More info on the network interface limit, from the docs: + 'This limit is the greater of either the default limit (350) or your + On-Demand Instance limit multiplied by 5. + The default limit for On-Demand Instances is 20.' + """ + self.connect() + self.connect_resource() + logger.info("Querying EC2 DescribeAccountAttributes for limits") + attribs = self.conn.describe_account_attributes() + for attrib in attribs['AccountAttributes']: + if attrib['AttributeName'] == 'max-instances': + val = attrib['AttributeValues'][0]['AttributeValue'] + if int(val) * 5 > DEFAULT_ENI_LIMIT: + limit_name = 'Network interfaces per Region' + self.limits[limit_name]._set_api_limit(int(val)) + else: + continue + logger.debug("Done setting limits from API") + def required_iam_permissions(self): """ Return a list of IAM Actions required for this Service to function @@ -326,4 +376,5 @@ def required_iam_permissions(self): 'ec2:DescribeSubnets', 'ec2:DescribeVpcs', 'ec2:DescribeVpnGateways', + 'ec2:DescribeNetworkInterfaces', ] diff --git a/awslimitchecker/tests/services/result_fixtures.py b/awslimitchecker/tests/services/result_fixtures.py index 9497e75b..2d034837 100644 --- a/awslimitchecker/tests/services/result_fixtures.py +++ b/awslimitchecker/tests/services/result_fixtures.py @@ -551,6 +551,104 @@ class VPC(object): ] } + test_find_usage_network_interfaces = { + 'NetworkInterfaces': [ + { + 'Association': { + 'AllocationId': 'allocation-123', + 'AssociationId': 'association-123', + 'IpOwnerId': '123456789012', + 'PublicDnsName': 'string', + 'PublicIp': '50.0.0.0' + }, + 'Attachment': { + 'AttachTime': datetime(2015, 1, 1), + 'AttachmentId': 'eni-123', + 'DeleteOnTermination': True, + 'DeviceIndex': 123, + 'InstanceId': 'i-123', + 'InstanceOwnerId': '123456789012', + 'Status': 'attaching' + }, + 'AvailabilityZone': 'us-east-2a', + 'Description': 'NetworkInface', + 'Groups': [ + { + 'GroupName': 'groupName', + 'GroupId': 'sg-123' + }, + ], + 'InterfaceType': 'interface', + 'Ipv6Addresses': [], + 'MacAddress': 'address', + 'NetworkInterfaceId': 'eni-123', + 'OwnerId': 'string', + 'PrivateDnsName': 'string', + 'PrivateIpAddress': 'string', + 'PrivateIpAddresses': [ + { + 'Association': { + 'AllocationId': 'allocation-123', + 'AssociationId': 'association-123', + 'IpOwnerId': '123456789012', + 'PublicDnsName': 'string', + 'PublicIp': '10.0.0.0' + }, + 'Primary': True, + 'PrivateDnsName': 'string', + 'PrivateIpAddress': 'string' + }, + ], + 'RequesterId': '123456789012', + 'RequesterManaged': True, + 'SourceDestCheck': True, + 'Status': 'available', + 'SubnetId': 'subnet-123', + 'TagSet': [ + { + 'Key': 'tagkey', + 'Value': 'tagvalue' + }, + ], + 'VpcId': 'string' + }, + ] + } + + test_update_limits_from_api_high_max_instances = { + 'ResponseMetadata': { + 'HTTPStatusCode': 200, + 'RequestId': '16b85906-ab0d-4134-b8bb-df3e6120c6c7' + }, + 'AccountAttributes': [ + { + 'AttributeName': 'max-instances', + 'AttributeValues': [ + { + 'AttributeValue': '400' + } + ] + } + ] + } + + test_update_limits_from_api_low_max_instances = { + 'ResponseMetadata': { + 'HTTPStatusCode': 200, + 'RequestId': '16b85906-ab0d-4134-b8bb-df3e6120c6c7' + }, + 'AccountAttributes': [ + { + 'AttributeName': 'max-instances', + 'AttributeValues': [ + { + 'AttributeValue': '50' + } + ] + } + ] + } + class RDS(object): test_find_usage_instances = [] diff --git a/awslimitchecker/tests/services/test_vpc.py b/awslimitchecker/tests/services/test_vpc.py index 13463d73..54227883 100644 --- a/awslimitchecker/tests/services/test_vpc.py +++ b/awslimitchecker/tests/services/test_vpc.py @@ -39,7 +39,7 @@ import sys from awslimitchecker.tests.services import result_fixtures -from awslimitchecker.services.vpc import _VpcService +from awslimitchecker.services.vpc import _VpcService, DEFAULT_ENI_LIMIT from botocore.exceptions import ClientError @@ -81,6 +81,7 @@ def test_get_limits(self): 'Rules per network ACL', 'Route tables per VPC', 'Virtual private gateways', + 'Network interfaces per Region', ]) for name, limit in res.items(): assert limit.service == cls @@ -111,7 +112,8 @@ def test_find_usage(self): _find_usage_route_tables=DEFAULT, _find_usage_gateways=DEFAULT, _find_usage_nat_gateways=DEFAULT, - _find_usages_vpn_gateways=DEFAULT + _find_usages_vpn_gateways=DEFAULT, + _find_usage_network_interfaces=DEFAULT, ) as mocks: mocks['_find_usage_subnets'].return_value = sn cls = _VpcService(21, 43) @@ -128,6 +130,7 @@ def test_find_usage(self): '_find_usage_route_tables', '_find_usage_gateways', '_find_usages_vpn_gateways', + '_find_usage_network_interfaces', ]: assert mocks[x].mock_calls == [call()] assert mocks['_find_usage_nat_gateways'].mock_calls == [call(sn)] @@ -339,6 +342,75 @@ def test_find_usages_vpn_gateways(self): ]), ] + def test_find_usage_network_interfaces(self): + response = result_fixtures.VPC.test_find_usage_network_interfaces + + mock_conn = Mock() + mock_conn.describe_network_interfaces.return_value = response + + cls = _VpcService(21, 43) + cls.conn = mock_conn + + cls._find_usage_network_interfaces() + + assert len(cls.limits['Network interfaces per Region'] + .get_current_usage()) == 1 + assert cls.limits['Network interfaces per Region'].get_current_usage()[ + 0].get_value() == 1 + assert mock_conn.mock_calls == [ + call.describe_network_interfaces(), + ] + + def test_update_limits_from_api_high_max_instances(self): + fixtures = result_fixtures.VPC() + response = fixtures.test_update_limits_from_api_high_max_instances + + mock_conn = Mock() + mock_client_conn = Mock() + mock_client_conn.describe_account_attributes.return_value = response + + cls = _VpcService(21, 43) + cls.resource_conn = mock_conn + cls.conn = mock_client_conn + with patch('awslimitchecker.services.vpc.logger') as mock_logger: + cls._update_limits_from_api() + assert mock_conn.mock_calls == [] + assert mock_client_conn.mock_calls == [ + call.describe_account_attributes() + ] + assert mock_logger.mock_calls == [ + call.info("Querying EC2 DescribeAccountAttributes for limits"), + call.debug('Done setting limits from API') + ] + assert cls.limits['Network interfaces per Region'].api_limit == 400 + assert cls.limits['Network interfaces per Region'].get_limit() == 400 + + def test_update_limits_from_api_low_max_instances(self): + fixtures = result_fixtures.VPC() + response = fixtures.test_update_limits_from_api_low_max_instances + + mock_conn = Mock() + mock_client_conn = Mock() + mock_client_conn.describe_account_attributes.return_value = response + + cls = _VpcService(21, 43) + cls.resource_conn = mock_conn + cls.conn = mock_client_conn + with patch('awslimitchecker.services.vpc.logger') as mock_logger: + cls._update_limits_from_api() + assert mock_conn.mock_calls == [] + assert mock_client_conn.mock_calls == [ + call.describe_account_attributes() + ] + assert mock_logger.mock_calls == [ + call.info("Querying EC2 DescribeAccountAttributes for limits"), + call.debug('Done setting limits from API') + ] + + limit_name = 'Network interfaces per Region' + assert cls.limits[limit_name].api_limit is None + assert cls.limits[limit_name].get_limit() == DEFAULT_ENI_LIMIT + def test_required_iam_permissions(self): cls = _VpcService(21, 43) assert cls.required_iam_permissions() == [ @@ -348,4 +420,5 @@ def test_required_iam_permissions(self): 'ec2:DescribeSubnets', 'ec2:DescribeVpcs', 'ec2:DescribeVpnGateways', + 'ec2:DescribeNetworkInterfaces', ] diff --git a/default) b/default) new file mode 100644 index 00000000..e69de29b