From 4b3469292a27ff3e6d6ff99fc5ef92d17b928250 Mon Sep 17 00:00:00 2001 From: Rob Walker Date: Thu, 22 Mar 2018 02:10:38 +1000 Subject: [PATCH] Enable Extended CIDR Associations on VPC (#1511) * Enable Extended CIDR Associations on VPC * Ooops missed the utils, try to be more flakey?, remove unnecessary part in tests * try to be even more flakey --- moto/ec2/exceptions.py | 28 ++++++ moto/ec2/models.py | 130 +++++++++++++++++++------- moto/ec2/responses/vpcs.py | 147 +++++++++++++++++++++++++++-- moto/ec2/utils.py | 24 +++-- tests/test_ec2/test_vpcs.py | 181 +++++++++++++++++++++++++++++++++++- 5 files changed, 455 insertions(+), 55 deletions(-) diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py index 5cff527b..f747c9cd 100644 --- a/moto/ec2/exceptions.py +++ b/moto/ec2/exceptions.py @@ -280,6 +280,15 @@ class InvalidAssociationIdError(EC2ClientError): .format(association_id)) +class InvalidVpcCidrBlockAssociationIdError(EC2ClientError): + + def __init__(self, association_id): + super(InvalidVpcCidrBlockAssociationIdError, self).__init__( + "InvalidVpcCidrBlockAssociationIdError.NotFound", + "The vpc CIDR block association ID '{0}' does not exist" + .format(association_id)) + + class InvalidVPCPeeringConnectionIdError(EC2ClientError): def __init__(self, vpc_peering_connection_id): @@ -392,3 +401,22 @@ class FilterNotImplementedError(MotoNotImplementedError): super(FilterNotImplementedError, self).__init__( "The filter '{0}' for {1}".format( filter_name, method_name)) + + +class CidrLimitExceeded(EC2ClientError): + + def __init__(self, vpc_id, max_cidr_limit): + super(CidrLimitExceeded, self).__init__( + "CidrLimitExceeded", + "This network '{0}' has met its maximum number of allowed CIDRs: {1}".format(vpc_id, max_cidr_limit) + ) + + +class OperationNotPermitted(EC2ClientError): + + def __init__(self, association_id): + super(OperationNotPermitted, self).__init__( + "OperationNotPermitted", + "The vpc CIDR block with association ID {} may not be disassociated. " + "It is the primary IPv4 CIDR block of the VPC".format(association_id) + ) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index c885dac9..c94752ef 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -24,51 +24,54 @@ from moto.core import BaseBackend from moto.core.models import Model, BaseModel from moto.core.utils import iso_8601_datetime_with_milliseconds, camelcase_to_underscores from .exceptions import ( - EC2ClientError, + CidrLimitExceeded, DependencyViolationError, - MissingParameterError, + EC2ClientError, + FilterNotImplementedError, + GatewayNotAttachedError, + InvalidAddressError, + InvalidAllocationIdError, + InvalidAMIIdError, + InvalidAMIAttributeItemValueError, + InvalidAssociationIdError, + InvalidCIDRSubnetError, + InvalidCustomerGatewayIdError, + InvalidDHCPOptionsIdError, + InvalidDomainError, + InvalidID, + InvalidInstanceIdError, + InvalidInternetGatewayIdError, + InvalidKeyPairDuplicateError, + InvalidKeyPairNameError, + InvalidNetworkAclIdError, + InvalidNetworkAttachmentIdError, + InvalidNetworkInterfaceIdError, InvalidParameterValueError, InvalidParameterValueErrorTagNull, - InvalidDHCPOptionsIdError, - MalformedDHCPOptionsIdError, - InvalidKeyPairNameError, - InvalidKeyPairDuplicateError, - InvalidInternetGatewayIdError, - GatewayNotAttachedError, - ResourceAlreadyAssociatedError, - InvalidVPCIdError, - InvalidSubnetIdError, - InvalidNetworkInterfaceIdError, - InvalidNetworkAttachmentIdError, - InvalidSecurityGroupDuplicateError, - InvalidSecurityGroupNotFoundError, InvalidPermissionNotFoundError, InvalidPermissionDuplicateError, InvalidRouteTableIdError, InvalidRouteError, - InvalidInstanceIdError, - InvalidAMIIdError, - InvalidAMIAttributeItemValueError, + InvalidSecurityGroupDuplicateError, + InvalidSecurityGroupNotFoundError, InvalidSnapshotIdError, + InvalidSubnetIdError, InvalidVolumeIdError, InvalidVolumeAttachmentError, - InvalidDomainError, - InvalidAddressError, - InvalidAllocationIdError, - InvalidAssociationIdError, + InvalidVpcCidrBlockAssociationIdError, InvalidVPCPeeringConnectionIdError, InvalidVPCPeeringConnectionStateTransitionError, - TagLimitExceeded, - InvalidID, - InvalidCIDRSubnetError, - InvalidNetworkAclIdError, + InvalidVPCIdError, InvalidVpnGatewayIdError, InvalidVpnConnectionIdError, - InvalidCustomerGatewayIdError, - RulesPerSecurityGroupLimitExceededError, + MalformedAMIIdError, + MalformedDHCPOptionsIdError, + MissingParameterError, MotoNotImplementedError, - FilterNotImplementedError, - MalformedAMIIdError) + OperationNotPermitted, + ResourceAlreadyAssociatedError, + RulesPerSecurityGroupLimitExceededError, + TagLimitExceeded) from .utils import ( EC2_RESOURCE_TO_PREFIX, EC2_PREFIX_TO_RESOURCE, @@ -81,6 +84,7 @@ from .utils import ( random_instance_id, random_internet_gateway_id, random_ip, + random_ipv6_cidr, random_nat_gateway_id, random_key_pair, random_private_ip, @@ -97,6 +101,7 @@ from .utils import ( random_subnet_association_id, random_volume_id, random_vpc_id, + random_vpc_cidr_association_id, random_vpc_peering_connection_id, generic_filter, is_valid_resource_id, @@ -2005,10 +2010,13 @@ class EBSBackend(object): class VPC(TaggedEC2Resource): - def __init__(self, ec2_backend, vpc_id, cidr_block, is_default, instance_tenancy='default'): + def __init__(self, ec2_backend, vpc_id, cidr_block, is_default, instance_tenancy='default', + amazon_provided_ipv6_cidr_block=False): + self.ec2_backend = ec2_backend self.id = vpc_id self.cidr_block = cidr_block + self.cidr_block_association_set = {} self.dhcp_options = None self.state = 'available' self.instance_tenancy = instance_tenancy @@ -2018,6 +2026,10 @@ class VPC(TaggedEC2Resource): # or VPCs created using the wizard of the VPC console self.enable_dns_hostnames = 'true' if is_default else 'false' + self.associate_vpc_cidr_block(cidr_block) + if amazon_provided_ipv6_cidr_block: + self.associate_vpc_cidr_block(cidr_block, amazon_provided_ipv6_cidr_block=amazon_provided_ipv6_cidr_block) + @classmethod def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] @@ -2043,6 +2055,12 @@ class VPC(TaggedEC2Resource): return self.id elif filter_name in ('cidr', 'cidr-block', 'cidrBlock'): return self.cidr_block + elif filter_name in ('cidr-block-association.cidr-block', 'ipv6-cidr-block-association.ipv6-cidr-block'): + return [c['cidr_block'] for c in self.get_cidr_block_association_set(ipv6='ipv6' in filter_name)] + elif filter_name in ('cidr-block-association.association-id', 'ipv6-cidr-block-association.association-id'): + return self.cidr_block_association_set.keys() + elif filter_name in ('cidr-block-association.state', 'ipv6-cidr-block-association.state'): + return [c['cidr_block_state']['state'] for c in self.get_cidr_block_association_set(ipv6='ipv6' in filter_name)] elif filter_name in ('instance_tenancy', 'InstanceTenancy'): return self.instance_tenancy elif filter_name in ('is-default', 'isDefault'): @@ -2054,8 +2072,37 @@ class VPC(TaggedEC2Resource): return None return self.dhcp_options.id else: - return super(VPC, self).get_filter_value( - filter_name, 'DescribeVpcs') + return super(VPC, self).get_filter_value(filter_name, 'DescribeVpcs') + + def associate_vpc_cidr_block(self, cidr_block, amazon_provided_ipv6_cidr_block=False): + max_associations = 5 if not amazon_provided_ipv6_cidr_block else 1 + + if len(self.get_cidr_block_association_set(amazon_provided_ipv6_cidr_block)) >= max_associations: + raise CidrLimitExceeded(self.id, max_associations) + + association_id = random_vpc_cidr_association_id() + + association_set = { + 'association_id': association_id, + 'cidr_block_state': {'state': 'associated', 'StatusMessage': ''} + } + + association_set['cidr_block'] = random_ipv6_cidr() if amazon_provided_ipv6_cidr_block else cidr_block + self.cidr_block_association_set[association_id] = association_set + return association_set + + def disassociate_vpc_cidr_block(self, association_id): + if self.cidr_block == self.cidr_block_association_set.get(association_id, {}).get('cidr_block'): + raise OperationNotPermitted(association_id) + + response = self.cidr_block_association_set.pop(association_id, {}) + if response: + response['vpc_id'] = self.id + response['cidr_block_state']['state'] = 'disassociating' + return response + + def get_cidr_block_association_set(self, ipv6=False): + return [c for c in self.cidr_block_association_set.values() if ('::/' if ipv6 else '.') in c.get('cidr_block')] class VPCBackend(object): @@ -2063,10 +2110,9 @@ class VPCBackend(object): self.vpcs = {} super(VPCBackend, self).__init__() - def create_vpc(self, cidr_block, instance_tenancy='default'): + def create_vpc(self, cidr_block, instance_tenancy='default', amazon_provided_ipv6_cidr_block=False): vpc_id = random_vpc_id() - vpc = VPC(self, vpc_id, cidr_block, len( - self.vpcs) == 0, instance_tenancy) + vpc = VPC(self, vpc_id, cidr_block, len(self.vpcs) == 0, instance_tenancy, amazon_provided_ipv6_cidr_block) self.vpcs[vpc_id] = vpc # AWS creates a default main route table and security group. @@ -2139,6 +2185,18 @@ class VPCBackend(object): else: raise InvalidParameterValueError(attr_name) + def disassociate_vpc_cidr_block(self, association_id): + for vpc in self.vpcs.values(): + response = vpc.disassociate_vpc_cidr_block(association_id) + if response: + return response + else: + raise InvalidVpcCidrBlockAssociationIdError(association_id) + + def associate_vpc_cidr_block(self, vpc_id, cidr_block, amazon_provided_ipv6_cidr_block): + vpc = self.get_vpc(vpc_id) + return vpc.associate_vpc_cidr_block(cidr_block, amazon_provided_ipv6_cidr_block) + class VPCPeeringConnectionStatus(object): def __init__(self, code='initiating-request', message=''): diff --git a/moto/ec2/responses/vpcs.py b/moto/ec2/responses/vpcs.py index 8a53151e..88673d86 100644 --- a/moto/ec2/responses/vpcs.py +++ b/moto/ec2/responses/vpcs.py @@ -9,9 +9,12 @@ class VPCs(BaseResponse): def create_vpc(self): cidr_block = self._get_param('CidrBlock') instance_tenancy = self._get_param('InstanceTenancy', if_none='default') - vpc = self.ec2_backend.create_vpc(cidr_block, instance_tenancy) + amazon_provided_ipv6_cidr_blocks = self._get_param('AmazonProvidedIpv6CidrBlock') + vpc = self.ec2_backend.create_vpc(cidr_block, instance_tenancy, + amazon_provided_ipv6_cidr_block=amazon_provided_ipv6_cidr_blocks) + doc_date = '2013-10-15' if 'Boto/' in self.headers.get('user-agent', '') else '2016-11-15' template = self.response_template(CREATE_VPC_RESPONSE) - return template.render(vpc=vpc) + return template.render(vpc=vpc, doc_date=doc_date) def delete_vpc(self): vpc_id = self._get_param('VpcId') @@ -23,8 +26,9 @@ class VPCs(BaseResponse): vpc_ids = self._get_multi_param('VpcId') filters = filters_from_querystring(self.querystring) vpcs = self.ec2_backend.get_all_vpcs(vpc_ids=vpc_ids, filters=filters) + doc_date = '2013-10-15' if 'Boto/' in self.headers.get('user-agent', '') else '2016-11-15' template = self.response_template(DESCRIBE_VPCS_RESPONSE) - return template.render(vpcs=vpcs) + return template.render(vpcs=vpcs, doc_date=doc_date) def describe_vpc_attribute(self): vpc_id = self._get_param('VpcId') @@ -45,14 +49,63 @@ class VPCs(BaseResponse): vpc_id, attr_name, attr_value) return MODIFY_VPC_ATTRIBUTE_RESPONSE + def associate_vpc_cidr_block(self): + vpc_id = self._get_param('VpcId') + amazon_provided_ipv6_cidr_blocks = self._get_param('AmazonProvidedIpv6CidrBlock') + # todo test on AWS if can create an association for IPV4 and IPV6 in the same call? + cidr_block = self._get_param('CidrBlock') if not amazon_provided_ipv6_cidr_blocks else None + value = self.ec2_backend.associate_vpc_cidr_block(vpc_id, cidr_block, amazon_provided_ipv6_cidr_blocks) + if not amazon_provided_ipv6_cidr_blocks: + render_template = ASSOCIATE_VPC_CIDR_BLOCK_RESPONSE + else: + render_template = IPV6_ASSOCIATE_VPC_CIDR_BLOCK_RESPONSE + template = self.response_template(render_template) + return template.render(vpc_id=vpc_id, value=value, cidr_block=value['cidr_block'], + association_id=value['association_id'], cidr_block_state='associating') + + def disassociate_vpc_cidr_block(self): + association_id = self._get_param('AssociationId') + value = self.ec2_backend.disassociate_vpc_cidr_block(association_id) + if "::" in value.get('cidr_block', ''): + render_template = IPV6_DISASSOCIATE_VPC_CIDR_BLOCK_RESPONSE + else: + render_template = DISASSOCIATE_VPC_CIDR_BLOCK_RESPONSE + template = self.response_template(render_template) + return template.render(vpc_id=value['vpc_id'], cidr_block=value['cidr_block'], + association_id=value['association_id'], cidr_block_state='disassociating') + CREATE_VPC_RESPONSE = """ - + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE {{ vpc.id }} pending {{ vpc.cidr_block }} + {% if doc_date == "2016-11-15" %} + + {% for assoc in vpc.get_cidr_block_association_set() %} + + {{assoc.cidr_block}} + {{ assoc.association_id }} + + {{assoc.cidr_block_state.state}} + + + {% endfor %} + + + {% for assoc in vpc.get_cidr_block_association_set(ipv6=True) %} + + {{assoc.cidr_block}} + {{ assoc.association_id }} + + {{assoc.cidr_block_state.state}} + + + {% endfor %} + + {% endif %} {% if vpc.dhcp_options %}{{ vpc.dhcp_options.id }}{% else %}dopt-1a2b3c4d2{% endif %} {{ vpc.instance_tenancy }} @@ -69,14 +122,38 @@ CREATE_VPC_RESPONSE = """ """ DESCRIBE_VPCS_RESPONSE = """ - - 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + + 7a62c442-3484-4f42-9342-6942EXAMPLE {% for vpc in vpcs %} {{ vpc.id }} {{ vpc.state }} {{ vpc.cidr_block }} + {% if doc_date == "2016-11-15" %} + + {% for assoc in vpc.get_cidr_block_association_set() %} + + {{assoc.cidr_block}} + {{ assoc.association_id }} + + {{assoc.cidr_block_state.state}} + + + {% endfor %} + + + {% for assoc in vpc.get_cidr_block_association_set(ipv6=True) %} + + {{assoc.cidr_block}} + {{ assoc.association_id }} + + {{assoc.cidr_block_state.state}} + + + {% endfor %} + + {% endif %} {% if vpc.dhcp_options %}{{ vpc.dhcp_options.id }}{% else %}dopt-7a8b9c2d{% endif %} {{ vpc.instance_tenancy }} {{ vpc.is_default }} @@ -96,14 +173,14 @@ DESCRIBE_VPCS_RESPONSE = """ """ DELETE_VPC_RESPONSE = """ - + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE true """ DESCRIBE_VPC_ATTRIBUTE_RESPONSE = """ - + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE {{ vpc_id }} <{{ attribute }}> @@ -112,7 +189,59 @@ DESCRIBE_VPC_ATTRIBUTE_RESPONSE = """ """ MODIFY_VPC_ATTRIBUTE_RESPONSE = """ - + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE true """ + +ASSOCIATE_VPC_CIDR_BLOCK_RESPONSE = """ + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + {{vpc_id}} + + {{association_id}} + {{cidr_block}} + + {{cidr_block_state}} + + +""" + +DISASSOCIATE_VPC_CIDR_BLOCK_RESPONSE = """ + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + {{vpc_id}} + + {{association_id}} + {{cidr_block}} + + {{cidr_block_state}} + + +""" + +IPV6_ASSOCIATE_VPC_CIDR_BLOCK_RESPONSE = """ + + 33af6c54-1139-4d50-b4f7-15a8example + {{vpc_id}} + + {{association_id}} + {{cidr_block}} + + {{cidr_block_state}} + + +""" + +IPV6_DISASSOCIATE_VPC_CIDR_BLOCK_RESPONSE = """ + + 33af6c54-1139-4d50-b4f7-15a8example + {{vpc_id}} + + {{association_id}} + {{cidr_block}} + + {{cidr_block_state}} + + +""" diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index 32122c76..f5c9b851 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -27,6 +27,7 @@ EC2_RESOURCE_TO_PREFIX = { 'reservation': 'r', 'volume': 'vol', 'vpc': 'vpc', + 'vpc-cidr-association-id': 'vpc-cidr-assoc', 'vpc-elastic-ip': 'eipalloc', 'vpc-elastic-ip-association': 'eipassoc', 'vpc-peering-connection': 'pcx', @@ -34,16 +35,17 @@ EC2_RESOURCE_TO_PREFIX = { 'vpn-gateway': 'vgw'} -EC2_PREFIX_TO_RESOURCE = dict((v, k) - for (k, v) in EC2_RESOURCE_TO_PREFIX.items()) +EC2_PREFIX_TO_RESOURCE = dict((v, k) for (k, v) in EC2_RESOURCE_TO_PREFIX.items()) + + +def random_resource_id(size=8): + chars = list(range(10)) + ['a', 'b', 'c', 'd', 'e', 'f'] + resource_id = ''.join(six.text_type(random.choice(chars)) for x in range(size)) + return resource_id def random_id(prefix='', size=8): - chars = list(range(10)) + ['a', 'b', 'c', 'd', 'e', 'f'] - - resource_id = ''.join(six.text_type(random.choice(chars)) - for x in range(size)) - return '{0}-{1}'.format(prefix, resource_id) + return '{0}-{1}'.format(prefix, random_resource_id(size)) def random_ami_id(): @@ -110,6 +112,10 @@ def random_vpc_id(): return random_id(prefix=EC2_RESOURCE_TO_PREFIX['vpc']) +def random_vpc_cidr_association_id(): + return random_id(prefix=EC2_RESOURCE_TO_PREFIX['vpc-cidr-association-id']) + + def random_vpc_peering_connection_id(): return random_id(prefix=EC2_RESOURCE_TO_PREFIX['vpc-peering-connection']) @@ -165,6 +171,10 @@ def random_ip(): ) +def random_ipv6_cidr(): + return "2400:6500:{}:{}::/56".format(random_resource_id(4), random_resource_id(4)) + + def generate_route_id(route_table_id, cidr_block): return "%s~%s" % (route_table_id, cidr_block) diff --git a/tests/test_ec2/test_vpcs.py b/tests/test_ec2/test_vpcs.py index fc0a93cb..318491b4 100644 --- a/tests/test_ec2/test_vpcs.py +++ b/tests/test_ec2/test_vpcs.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals # Ensure 'assert_raises' context manager support for Python 2.6 import tests.backport_assert_raises # flake8: noqa from nose.tools import assert_raises +from moto.ec2.exceptions import EC2ClientError +from botocore.exceptions import ClientError import boto3 import boto @@ -275,8 +277,8 @@ def test_default_vpc(): def test_non_default_vpc(): ec2 = boto3.resource('ec2', region_name='us-west-1') - # Create the default VPC - ec2.create_vpc(CidrBlock='172.31.0.0/16') + # Create the default VPC - this already exists when backend instantiated! + #ec2.create_vpc(CidrBlock='172.31.0.0/16') # Create the non default VPC vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16') @@ -295,6 +297,12 @@ def test_non_default_vpc(): attr = response.get('EnableDnsHostnames') attr.get('Value').shouldnt.be.ok + # Check Primary CIDR Block Associations + cidr_block_association_set = next(iter(vpc.cidr_block_association_set), None) + cidr_block_association_set['CidrBlockState']['State'].should.equal('associated') + cidr_block_association_set['CidrBlock'].should.equal(vpc.cidr_block) + cidr_block_association_set['AssociationId'].should.contain('vpc-cidr-assoc') + @mock_ec2 def test_vpc_dedicated_tenancy(): @@ -340,7 +348,6 @@ def test_vpc_modify_enable_dns_hostnames(): ec2.create_vpc(CidrBlock='172.31.0.0/16') vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16') - # Test default values for VPC attributes response = vpc.describe_attribute(Attribute='enableDnsHostnames') attr = response.get('EnableDnsHostnames') @@ -364,3 +371,171 @@ def test_vpc_associate_dhcp_options(): vpc.update() dhcp_options.id.should.equal(vpc.dhcp_options_id) + + +@mock_ec2 +def test_associate_vpc_ipv4_cidr_block(): + ec2 = boto3.resource('ec2', region_name='us-west-1') + + vpc = ec2.create_vpc(CidrBlock='10.10.42.0/24') + + # Associate/Extend vpc CIDR range up to 5 ciders + for i in range(43, 47): + response = ec2.meta.client.associate_vpc_cidr_block(VpcId=vpc.id, CidrBlock='10.10.{}.0/24'.format(i)) + response['CidrBlockAssociation']['CidrBlockState']['State'].should.equal('associating') + response['CidrBlockAssociation']['CidrBlock'].should.equal('10.10.{}.0/24'.format(i)) + response['CidrBlockAssociation']['AssociationId'].should.contain('vpc-cidr-assoc') + + # Check all associations exist + vpc = ec2.Vpc(vpc.id) + vpc.cidr_block_association_set.should.have.length_of(5) + vpc.cidr_block_association_set[2]['CidrBlockState']['State'].should.equal('associated') + vpc.cidr_block_association_set[4]['CidrBlockState']['State'].should.equal('associated') + + # Check error on adding 6th association. + with assert_raises(ClientError) as ex: + response = ec2.meta.client.associate_vpc_cidr_block(VpcId=vpc.id, CidrBlock='10.10.50.0/22') + str(ex.exception).should.equal( + "An error occurred (CidrLimitExceeded) when calling the AssociateVpcCidrBlock " + "operation: This network '{}' has met its maximum number of allowed CIDRs: 5".format(vpc.id)) + +@mock_ec2 +def test_disassociate_vpc_ipv4_cidr_block(): + ec2 = boto3.resource('ec2', region_name='us-west-1') + + vpc = ec2.create_vpc(CidrBlock='10.10.42.0/24') + ec2.meta.client.associate_vpc_cidr_block(VpcId=vpc.id, CidrBlock='10.10.43.0/24') + + # Remove an extended cidr block + vpc = ec2.Vpc(vpc.id) + non_default_assoc_cidr_block = next(iter([x for x in vpc.cidr_block_association_set if vpc.cidr_block != x['CidrBlock']]), None) + response = ec2.meta.client.disassociate_vpc_cidr_block(AssociationId=non_default_assoc_cidr_block['AssociationId']) + response['CidrBlockAssociation']['CidrBlockState']['State'].should.equal('disassociating') + response['CidrBlockAssociation']['CidrBlock'].should.equal(non_default_assoc_cidr_block['CidrBlock']) + response['CidrBlockAssociation']['AssociationId'].should.equal(non_default_assoc_cidr_block['AssociationId']) + + # Error attempting to delete a non-existent CIDR_BLOCK association + with assert_raises(ClientError) as ex: + response = ec2.meta.client.disassociate_vpc_cidr_block(AssociationId='vpc-cidr-assoc-BORING123') + str(ex.exception).should.equal( + "An error occurred (InvalidVpcCidrBlockAssociationIdError.NotFound) when calling the " + "DisassociateVpcCidrBlock operation: The vpc CIDR block association ID " + "'vpc-cidr-assoc-BORING123' does not exist") + + # Error attempting to delete Primary CIDR BLOCK association + vpc_base_cidr_assoc_id = next(iter([x for x in vpc.cidr_block_association_set + if vpc.cidr_block == x['CidrBlock']]), {})['AssociationId'] + + with assert_raises(ClientError) as ex: + response = ec2.meta.client.disassociate_vpc_cidr_block(AssociationId=vpc_base_cidr_assoc_id) + str(ex.exception).should.equal( + "An error occurred (OperationNotPermitted) when calling the DisassociateVpcCidrBlock operation: " + "The vpc CIDR block with association ID {} may not be disassociated. It is the primary " + "IPv4 CIDR block of the VPC".format(vpc_base_cidr_assoc_id)) + +@mock_ec2 +def test_cidr_block_association_filters(): + ec2 = boto3.resource('ec2', region_name='us-west-1') + vpc1 = ec2.create_vpc(CidrBlock='10.90.0.0/16') + vpc2 = ec2.create_vpc(CidrBlock='10.91.0.0/16') + ec2.meta.client.associate_vpc_cidr_block(VpcId=vpc2.id, CidrBlock='10.10.0.0/19') + vpc3 = ec2.create_vpc(CidrBlock='10.92.0.0/24') + ec2.meta.client.associate_vpc_cidr_block(VpcId=vpc3.id, CidrBlock='10.92.1.0/24') + ec2.meta.client.associate_vpc_cidr_block(VpcId=vpc3.id, CidrBlock='10.92.2.0/24') + vpc3_assoc_response = ec2.meta.client.associate_vpc_cidr_block(VpcId=vpc3.id, CidrBlock='10.92.3.0/24') + + # Test filters for a cidr-block in all VPCs cidr-block-associations + filtered_vpcs = list(ec2.vpcs.filter(Filters=[{'Name': 'cidr-block-association.cidr-block', + 'Values': ['10.10.0.0/19']}])) + filtered_vpcs.should.be.length_of(1) + filtered_vpcs[0].id.should.equal(vpc2.id) + + # Test filter for association id in VPCs + association_id = vpc3_assoc_response['CidrBlockAssociation']['AssociationId'] + filtered_vpcs = list(ec2.vpcs.filter(Filters=[{'Name': 'cidr-block-association.association-id', + 'Values': [association_id]}])) + filtered_vpcs.should.be.length_of(1) + filtered_vpcs[0].id.should.equal(vpc3.id) + + # Test filter for association state in VPC - this will never show anything in this test + filtered_vpcs = list(ec2.vpcs.filter(Filters=[{'Name': 'cidr-block-association.association-id', + 'Values': ['failing']}])) + filtered_vpcs.should.be.length_of(0) + +@mock_ec2 +def test_vpc_associate_ipv6_cidr_block(): + ec2 = boto3.resource('ec2', region_name='us-west-1') + + # Test create VPC with IPV6 cidr range + vpc = ec2.create_vpc(CidrBlock='10.10.42.0/24', AmazonProvidedIpv6CidrBlock=True) + ipv6_cidr_block_association_set = next(iter(vpc.ipv6_cidr_block_association_set), None) + ipv6_cidr_block_association_set['Ipv6CidrBlockState']['State'].should.equal('associated') + ipv6_cidr_block_association_set['Ipv6CidrBlock'].should.contain('::/56') + ipv6_cidr_block_association_set['AssociationId'].should.contain('vpc-cidr-assoc') + + # Test Fail on adding 2nd IPV6 association - AWS only allows 1 at this time! + with assert_raises(ClientError) as ex: + response = ec2.meta.client.associate_vpc_cidr_block(VpcId=vpc.id, AmazonProvidedIpv6CidrBlock=True) + str(ex.exception).should.equal( + "An error occurred (CidrLimitExceeded) when calling the AssociateVpcCidrBlock " + "operation: This network '{}' has met its maximum number of allowed CIDRs: 1".format(vpc.id)) + + # Test associate ipv6 cidr block after vpc created + vpc = ec2.create_vpc(CidrBlock='10.10.50.0/24') + response = ec2.meta.client.associate_vpc_cidr_block(VpcId=vpc.id, AmazonProvidedIpv6CidrBlock=True) + response['Ipv6CidrBlockAssociation']['Ipv6CidrBlockState']['State'].should.equal('associating') + response['Ipv6CidrBlockAssociation']['Ipv6CidrBlock'].should.contain('::/56') + response['Ipv6CidrBlockAssociation']['AssociationId'].should.contain('vpc-cidr-assoc-') + + # Check on describe vpc that has ipv6 cidr block association + vpc = ec2.Vpc(vpc.id) + vpc.ipv6_cidr_block_association_set.should.be.length_of(1) + + +@mock_ec2 +def test_vpc_disassociate_ipv6_cidr_block(): + ec2 = boto3.resource('ec2', region_name='us-west-1') + + # Test create VPC with IPV6 cidr range + vpc = ec2.create_vpc(CidrBlock='10.10.42.0/24', AmazonProvidedIpv6CidrBlock=True) + # Test disassociating the only IPV6 + assoc_id = vpc.ipv6_cidr_block_association_set[0]['AssociationId'] + response = ec2.meta.client.disassociate_vpc_cidr_block(AssociationId=assoc_id) + response['Ipv6CidrBlockAssociation']['Ipv6CidrBlockState']['State'].should.equal('disassociating') + response['Ipv6CidrBlockAssociation']['Ipv6CidrBlock'].should.contain('::/56') + response['Ipv6CidrBlockAssociation']['AssociationId'].should.equal(assoc_id) + + +@mock_ec2 +def test_ipv6_cidr_block_association_filters(): + ec2 = boto3.resource('ec2', region_name='us-west-1') + vpc1 = ec2.create_vpc(CidrBlock='10.90.0.0/16') + + vpc2 = ec2.create_vpc(CidrBlock='10.91.0.0/16', AmazonProvidedIpv6CidrBlock=True) + vpc2_assoc_ipv6_assoc_id = vpc2.ipv6_cidr_block_association_set[0]['AssociationId'] + ec2.meta.client.associate_vpc_cidr_block(VpcId=vpc2.id, CidrBlock='10.10.0.0/19') + + vpc3 = ec2.create_vpc(CidrBlock='10.92.0.0/24') + ec2.meta.client.associate_vpc_cidr_block(VpcId=vpc3.id, CidrBlock='10.92.1.0/24') + ec2.meta.client.associate_vpc_cidr_block(VpcId=vpc3.id, CidrBlock='10.92.2.0/24') + response = ec2.meta.client.associate_vpc_cidr_block(VpcId=vpc3.id, AmazonProvidedIpv6CidrBlock=True) + vpc3_ipv6_cidr_block = response['Ipv6CidrBlockAssociation']['Ipv6CidrBlock'] + + vpc4 = ec2.create_vpc(CidrBlock='10.95.0.0/16') # Here for its looks + + # Test filters for an ipv6 cidr-block in all VPCs cidr-block-associations + filtered_vpcs = list(ec2.vpcs.filter(Filters=[{'Name': 'ipv6-cidr-block-association.ipv6-cidr-block', + 'Values': [vpc3_ipv6_cidr_block]}])) + filtered_vpcs.should.be.length_of(1) + filtered_vpcs[0].id.should.equal(vpc3.id) + + # Test filter for association id in VPCs + filtered_vpcs = list(ec2.vpcs.filter(Filters=[{'Name': 'ipv6-cidr-block-association.association-id', + 'Values': [vpc2_assoc_ipv6_assoc_id]}])) + filtered_vpcs.should.be.length_of(1) + filtered_vpcs[0].id.should.equal(vpc2.id) + + # Test filter for association state in VPC - this will never show anything in this test + filtered_vpcs = list(ec2.vpcs.filter(Filters=[{'Name': 'ipv6-cidr-block-association.state', + 'Values': ['associated']}])) + filtered_vpcs.should.be.length_of(2) # 2 of 4 VPCs