From cfbe9c8aee7d593a18243257b7965f1fbc5c32d4 Mon Sep 17 00:00:00 2001 From: Daniel Shepherd Date: Thu, 16 Nov 2017 14:58:12 -0500 Subject: [PATCH] [cloud] Add IPv6 support for ec2_vpc_subnet module(#30444) * Add integration test suite for ec2_vpc_subnet * wrap boto3 connection in try/except update module documentation and add RETURN docs add IPv6 support to VPC subnet module rename ipv6cidr to ipv6_cidr, use required_if for parameter testing, update some failure messages to be more descriptive DryRun mode was removed from this function a while ago but exception handling was still checking for it, removed add wait and timeout for subnet creation process fixup the ipv6 cidr disassociation logic a bit per review update RETURN values per review added module parameter check removed DryRun parameter from boto3 call since it would always be false here fix subnet wait loop add a purge_tags parameter, fix the ensure_tags function, update to use compare_aws_tags func fix tags type error per review remove **kwargs use in create_subnet function per review * rebased on #31870, fixed merge conflicts, and updated error messages * fixes to pass tests * add test for failure on invalid ipv6 block and update tags test for purge_tags=true function * fix pylint issue * fix exception handling error when run with python3 * add ipv6 tests and fix module code * Add permissions to hacking/aws_config/testing_policies/ec2-policy.json for adding IPv6 cidr blocks to VPC and subnets * fix type in tests and update assert conditional to check entire returned value * add AWS_SESSION_TOKEN into environment for aws cli commands to work in CI * remove key and value options from call to boto3_tag_list_to_ansible_dict * remove wait loop and use boto3 EC2 waiter * remove unused register: result vars * revert az argument default value to original setting default=None --- .../testing_policies/ec2-policy.json | 2 + .../modules/cloud/amazon/ec2_vpc_subnet.py | 379 ++++++++++++++---- .../targets/ec2_vpc_subnet/tasks/main.yml | 205 +++++++++- 3 files changed, 499 insertions(+), 87 deletions(-) diff --git a/hacking/aws_config/testing_policies/ec2-policy.json b/hacking/aws_config/testing_policies/ec2-policy.json index c5623b62371..2ccababf7f0 100644 --- a/hacking/aws_config/testing_policies/ec2-policy.json +++ b/hacking/aws_config/testing_policies/ec2-policy.json @@ -10,6 +10,8 @@ "ec2:AllocateAddress", "ec2:AssociateAddress", "ec2:AssociateRouteTable", + "ec2:AssociateVpcCidrBlock", + "ec2:AssociateSubnetCidrBlock", "ec2:CreateImage", "ec2:AttachInternetGateway", "ec2:CreateInternetGateway", diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet.py index a09de0eb930..ea56343e30e 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet.py +++ b/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet.py @@ -23,14 +23,21 @@ requirements: [ boto3 ] options: az: description: - - "The availability zone for the subnet. Only required when state=present." + - "The availability zone for the subnet." required: false default: null cidr: description: - - "The CIDR block for the subnet. E.g. 192.0.2.0/24. Only required when state=present." + - "The CIDR block for the subnet. E.g. 192.0.2.0/24." required: false default: null + ipv6_cidr: + description: + - "The IPv6 CIDR block for the subnet. The VPC must have a /56 block assigned and this value must be a valid IPv6 /64 that falls in the VPC range." + - "Required if I(assign_instances_ipv6=true)" + required: false + default: null + version_added: "2.5" tags: description: - "A dict of tags to apply to the subnet. Any tags currently applied to the subnet and not present here will be removed." @@ -45,15 +52,39 @@ options: choices: [ 'present', 'absent' ] vpc_id: description: - - "VPC ID of the VPC in which to create the subnet." - required: false + - "VPC ID of the VPC in which to create or delete the subnet." + required: true default: null map_public: description: - - "Specify true to indicate that instances launched into the subnet should be assigned public IP address by default." + - "Specify true to indicate that instances launched into the subnet should be assigned public IP address by default." required: false default: false version_added: "2.4" + assign_instances_ipv6: + description: + - "Specify true to indicate that instances launched into the subnet should be automatically assigned an IPv6 address." + required: false + default: false + version_added: "2.5" + wait: + description: + - "When specified,I(state=present) module will wait for subnet to be in available state before continuing." + required: false + default: true + version_added: "2.5" + wait_timeout: + description: + - "Number of seconds to wait for subnet to become available I(wait=True)." + required: false + default: 300 + version_added: "2.5" + purge_tags: + description: + - Whether or not to remove tags that do not appear in the I(tags) list. Defaults to true. + required: false + default: true + version_added: "2.5" extends_documentation_fragment: - aws - ec2 @@ -77,8 +108,112 @@ EXAMPLES = ''' vpc_id: vpc-123456 cidr: 10.0.1.16/28 +- name: Create subnet with IPv6 block assigned + ec2_vpc_subnet: + state: present + vpc_id: vpc-123456 + cidr: 10.1.100.0/24 + ipv6_cidr: 2001:db8:0:102::/64 + +- name: Remove IPv6 block assigned to subnet + ec2_vpc_subnet: + state: present + vpc_id: vpc-123456 + cidr: 10.1.100.0/24 + ipv6_cidr: '' ''' +RETURN = ''' +subnet: + description: Dictionary of subnet values + returned: I(state=present) + type: complex + contains: + id: + description: Subnet resource id + returned: I(state=present) + type: string + sample: subnet-b883b2c4 + cidr_block: + description: The IPv4 CIDR of the Subnet + returned: I(state=present) + type: string + sample: "10.0.0.0/16" + ipv6_cidr_block: + description: The IPv6 CIDR block actively associated with the Subnet + returned: I(state=present) + type: string + sample: "2001:db8:0:102::/64" + availability_zone: + description: Availability zone of the Subnet + returned: I(state=present) + type: string + sample: us-east-1a + state: + description: state of the Subnet + returned: I(state=present) + type: string + sample: available + tags: + description: tags attached to the Subnet, includes name + returned: I(state=present) + type: dict + sample: {"Name": "My Subnet", "env": "staging"} + map_public_ip_on_launch: + description: whether public IP is auto-assigned to new instances + returned: I(state=present) + type: boolean + sample: false + assign_ipv6_address_on_creation: + description: whether IPv6 address is auto-assigned to new instances + returned: I(state=present) + type: boolean + sample: false + vpc_id: + description: the id of the VPC where this Subnet exists + returned: I(state=present) + type: string + sample: vpc-67236184 + available_ip_address_count: + description: number of available IPv4 addresses + returned: I(state=present) + type: string + sample: 251 + default_for_az: + description: indicates whether this is the default Subnet for this Availability Zone + returned: I(state=present) + type: boolean + sample: false + ipv6_association_id: + description: The IPv6 association ID for the currently associated CIDR + returned: I(state=present) + type: string + sample: subnet-cidr-assoc-b85c74d2 + ipv6_cidr_block_association_set: + description: An array of IPv6 cidr block association set information. + returned: I(state=present) + type: complex + contains: + association_id: + description: The association ID + returned: always + type: string + ipv6_cidr_block: + description: The IPv6 CIDR block that is associated with the subnet. + returned: always + type: string + ipv6_cidr_block_state: + description: A hash/dict that contains a single item. The state of the cidr block association. + returned: always + type: dict + contains: + state: + description: The CIDR block association state. + returned: always + type: string +''' + + import time import traceback @@ -90,7 +225,7 @@ except ImportError: from ansible.module_utils.aws.core import AnsibleAWSModule from ansible.module_utils.ec2 import (ansible_dict_to_boto3_filter_list, ansible_dict_to_boto3_tag_list, ec2_argument_spec, camel_dict_to_snake_dict, get_aws_connection_info, - boto3_conn, boto3_tag_list_to_ansible_dict, AWSRetry) + boto3_conn, boto3_tag_list_to_ansible_dict, compare_aws_tags, AWSRetry) def get_subnet_info(subnet): @@ -110,6 +245,15 @@ def get_subnet_info(subnet): subnet['id'] = subnet['subnet_id'] del subnet['subnet_id'] + subnet['ipv6_cidr_block'] = '' + subnet['ipv6_association_id'] = '' + ipv6set = subnet.get('ipv6_cidr_block_association_set') + if ipv6set: + for item in ipv6set: + if item.get('ipv6_cidr_block_state', {}).get('state') in ('associated', 'associating'): + subnet['ipv6_cidr_block'] = item['ipv6_cidr_block'] + subnet['ipv6_association_id'] = item['association_id'] + return subnet @@ -118,56 +262,75 @@ def describe_subnets_with_backoff(client, **params): return client.describe_subnets(**params) -def subnet_exists(conn, module, subnet_id): - filters = ansible_dict_to_boto3_filter_list({'subnet-id': subnet_id}) - try: - subnets = get_subnet_info(describe_subnets_with_backoff(conn, Filters=filters)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't check if subnet exists") - if len(subnets) > 0 and 'state' in subnets[0] and subnets[0]['state'] == "available": - return subnets[0] - else: - return False +def create_subnet(conn, module, vpc_id, cidr, ipv6_cidr=None, az=None): + wait = module.params['wait'] + wait_timeout = module.params['wait_timeout'] + params = dict(VpcId=vpc_id, + CidrBlock=cidr) + + if ipv6_cidr: + params['Ipv6CidrBlock'] = ipv6_cidr -def create_subnet(conn, module, vpc_id, cidr, az, check_mode): - if check_mode: - return - params = dict(VpcId=vpc_id, CidrBlock=cidr) if az: params['AvailabilityZone'] = az + try: - new_subnet = get_subnet_info(conn.create_subnet(**params)) + subnet = get_subnet_info(conn.create_subnet(**params)) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Couldn't create subnet") + # Sometimes AWS takes its time to create a subnet and so using # new subnets's id to do things like create tags results in - # exception. boto doesn't seem to refresh 'state' of the newly - # created subnet, i.e.: it's always 'pending'. - subnet = False - while subnet is False: - subnet = subnet_exists(conn, module, new_subnet['id']) - time.sleep(0.1) + # exception. + if wait and subnet.get('state') != 'available': + delay = 5 + max_attempts = wait_timeout / delay + waiter_config = dict(Delay=delay, MaxAttempts=max_attempts) + waiter = conn.get_waiter('subnet_available') + try: + waiter.wait(SubnetIds=[subnet['id']], WaiterConfig=waiter_config) + subnet['state'] = 'available' + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json(msg="Create subnet action timed out waiting for Subnet to become available.") return subnet -def ensure_tags(conn, module, subnet, tags, add_only, check_mode): - cur_tags = subnet['tags'] +def ensure_tags(conn, module, subnet, tags, purge_tags): + changed = False - to_delete = dict((k, cur_tags[k]) for k in cur_tags if k not in tags) - if to_delete and not add_only and not check_mode: + filters = ansible_dict_to_boto3_filter_list({'resource-id': subnet['id'], 'resource-type': 'subnet'}) + try: + cur_tags = conn.describe_tags(Filters=filters) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't describe tags") + + to_update, to_delete = compare_aws_tags(boto3_tag_list_to_ansible_dict(cur_tags.get('Tags')), tags, purge_tags) + + if to_update: try: - conn.delete_tags(Resources=[subnet['id']], Tags=ansible_dict_to_boto3_tag_list(to_delete)) + if not module.check_mode: + conn.create_tags(Resources=[subnet['id']], Tags=ansible_dict_to_boto3_tag_list(to_update)) + + changed = True + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't create tags") + + if to_delete: + try: + if not module.check_mode: + tags_list = [] + for key in to_delete: + tags_list.append({'Key': key}) + + conn.delete_tags(Resources=[subnet['id']], Tags=tags_list) + + changed = True except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Couldn't delete tags") - to_add = dict((k, tags[k]) for k in tags if k not in cur_tags or cur_tags[k] != tags[k]) - if to_add and not check_mode: - try: - conn.create_tags(Resources=[subnet['id']], Tags=ansible_dict_to_boto3_tag_list(to_add)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create tags") + return changed def ensure_map_public(conn, module, subnet, map_public, check_mode): @@ -179,25 +342,89 @@ def ensure_map_public(conn, module, subnet, map_public, check_mode): module.fail_json_aws(e, msg="Couldn't modify subnet attribute") +def ensure_assign_ipv6_on_create(conn, module, subnet, assign_instances_ipv6, check_mode): + if check_mode: + return + + try: + conn.modify_subnet_attribute(SubnetId=subnet['id'], AssignIpv6AddressOnCreation={'Value': assign_instances_ipv6}) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't modify subnet attribute") + + +def disassociate_ipv6_cidr(conn, module, subnet): + if subnet.get('assign_ipv6_address_on_creation'): + ensure_assign_ipv6_on_create(conn, module, subnet, False, False) + + try: + conn.disassociate_subnet_cidr_block(AssociationId=subnet['ipv6_association_id']) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't disassociate ipv6 cidr block id {0} from subnet {1}" + .format(subnet['ipv6_association_id'], subnet['id'])) + + +def ensure_ipv6_cidr_block(conn, module, subnet, ipv6_cidr, check_mode): + changed = False + + if subnet['ipv6_association_id'] and not ipv6_cidr: + if not check_mode: + disassociate_ipv6_cidr(conn, module, subnet) + changed = True + + if ipv6_cidr: + filters = ansible_dict_to_boto3_filter_list({'ipv6-cidr-block-association.ipv6-cidr-block': ipv6_cidr, + 'vpc-id': subnet['vpc_id']}) + + try: + check_subnets = get_subnet_info(describe_subnets_with_backoff(conn, Filters=filters)) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't get subnet info") + + if check_subnets and check_subnets[0]['ipv6_cidr_block']: + module.fail_json(msg="The IPv6 CIDR '{0}' conflicts with another subnet".format(ipv6_cidr)) + + if subnet['ipv6_association_id']: + if not check_mode: + disassociate_ipv6_cidr(conn, module, subnet) + changed = True + + try: + if not check_mode: + associate_resp = conn.associate_subnet_cidr_block(SubnetId=subnet['id'], Ipv6CidrBlock=ipv6_cidr) + changed = True + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't associate ipv6 cidr {0} to {1}".format(ipv6_cidr, subnet['id'])) + + if associate_resp.get('Ipv6CidrBlockAssociation', {}).get('AssociationId'): + subnet['ipv6_association_id'] = associate_resp['Ipv6CidrBlockAssociation']['AssociationId'] + subnet['ipv6_cidr_block'] = associate_resp['Ipv6CidrBlockAssociation']['Ipv6CidrBlock'] + if subnet['ipv6_cidr_block_association_set']: + subnet['ipv6_cidr_block_association_set'][0] = camel_dict_to_snake_dict(associate_resp['Ipv6CidrBlockAssociation']) + else: + subnet['ipv6_cidr_block_association_set'].append(camel_dict_to_snake_dict(associate_resp['Ipv6CidrBlockAssociation'])) + + return changed + + def get_matching_subnet(conn, module, vpc_id, cidr): filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id, 'cidr-block': cidr}) try: - subnets = get_subnet_info(conn.describe_subnets(Filters=filters)) + subnets = get_subnet_info(describe_subnets_with_backoff(conn, Filters=filters)) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Couldn't get matching subnet") - if len(subnets) > 0: + if subnets: return subnets[0] - else: - return None + + return None -def ensure_subnet_present(conn, module, vpc_id, cidr, az, tags, map_public, check_mode): - subnet = get_matching_subnet(conn, module, vpc_id, cidr) +def ensure_subnet_present(conn, module): + subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr']) changed = False if subnet is None: - if not check_mode: - subnet = create_subnet(conn, module, vpc_id, cidr, az, check_mode) + if not module.check_mode: + subnet = create_subnet(conn, module, module.params['vpc_id'], module.params['cidr'], ipv6_cidr=module.params['ipv6_cidr'], az=module.params['az']) changed = True # Subnet will be None when check_mode is true if subnet is None: @@ -205,30 +432,39 @@ def ensure_subnet_present(conn, module, vpc_id, cidr, az, tags, map_public, chec 'changed': changed, 'subnet': {} } - if map_public != subnet['map_public_ip_on_launch']: - ensure_map_public(conn, module, subnet, map_public, check_mode) - subnet['map_public_ip_on_launch'] = map_public + + if module.params['ipv6_cidr'] != subnet.get('ipv6_cidr_block'): + if ensure_ipv6_cidr_block(conn, module, subnet, module.params['ipv6_cidr'], module.check_mode): + changed = True + + if module.params['map_public'] != subnet['map_public_ip_on_launch']: + ensure_map_public(conn, module, subnet, module.params['map_public'], module.check_mode) changed = True - if tags != subnet['tags']: - ensure_tags(conn, module, subnet, tags, False, check_mode) - subnet['tags'] = tags + if module.params['assign_instances_ipv6'] != subnet.get('assign_ipv6_address_on_creation'): + ensure_assign_ipv6_on_create(conn, module, subnet, module.params['assign_instances_ipv6'], module.check_mode) changed = True + if module.params['tags'] != subnet['tags']: + if ensure_tags(conn, module, subnet, module.params['tags'], module.params['purge_tags']): + changed = True + + subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr']) + return { 'changed': changed, 'subnet': subnet } -def ensure_subnet_absent(conn, module, vpc_id, cidr, check_mode): - subnet = get_matching_subnet(conn, module, vpc_id, cidr) +def ensure_subnet_absent(conn, module): + subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr']) if subnet is None: return {'changed': False} try: - if not check_mode: - conn.delete_subnet(SubnetId=subnet['id'], DryRun=check_mode) + if not module.check_mode: + conn.delete_subnet(SubnetId=subnet['id']) return {'changed': True} except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Couldn't delete subnet") @@ -240,36 +476,35 @@ def main(): dict( az=dict(default=None, required=False), cidr=dict(default=None, required=True), + ipv6_cidr=dict(default='', required=False), state=dict(default='present', choices=['present', 'absent']), tags=dict(default={}, required=False, type='dict', aliases=['resource_tags']), vpc_id=dict(default=None, required=True), - map_public=dict(default=False, required=False, type='bool') + map_public=dict(default=False, required=False, type='bool'), + assign_instances_ipv6=dict(default=False, required=False, type='bool'), + wait=dict(type='bool', default=True), + wait_timeout=dict(type='int', default=300, required=False), + purge_tags=dict(default=True, type='bool') ) ) - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) + required_if = [('assign_instances_ipv6', True, ['ipv6_cidr'])] + + module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if) + + if module.params.get('assign_instances_ipv6') and not module.params.get('ipv6_cidr'): + module.fail_json(msg="assign_instances_ipv6 is True but ipv6_cidr is None or an empty string") region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) + connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params) - if region: - connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params) - else: - module.fail_json(msg="region must be specified") - - vpc_id = module.params.get('vpc_id') - tags = module.params.get('tags') - cidr = module.params.get('cidr') - az = module.params.get('az') state = module.params.get('state') - map_public = module.params.get('map_public') try: if state == 'present': - result = ensure_subnet_present(connection, module, vpc_id, cidr, az, tags, map_public, - check_mode=module.check_mode) + result = ensure_subnet_present(connection, module) elif state == 'absent': - result = ensure_subnet_absent(connection, module, vpc_id, cidr, - check_mode=module.check_mode) + result = ensure_subnet_absent(connection, module) except botocore.exceptions.ClientError as e: module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) diff --git a/test/integration/targets/ec2_vpc_subnet/tasks/main.yml b/test/integration/targets/ec2_vpc_subnet/tasks/main.yml index 47854f868d4..72636e1cc15 100644 --- a/test/integration/targets/ec2_vpc_subnet/tasks/main.yml +++ b/test/integration/targets/ec2_vpc_subnet/tasks/main.yml @@ -16,7 +16,7 @@ name: "{{ resource_prefix }}-vpc" state: present cidr_block: "10.232.232.128/26" - region: '{{ec2_region}}' + region: '{{ ec2_region }}' aws_access_key: '{{ aws_access_key }}' aws_secret_key: '{{ aws_secret_key }}' security_token: '{{ security_token }}' @@ -33,7 +33,7 @@ tags: Name: '{{ec2_vpc_subnet_name}}' Description: '{{ec2_vpc_subnet_description}}' - region: '{{ec2_region}}' + region: '{{ ec2_region }}' aws_access_key: '{{ aws_access_key }}' aws_secret_key: '{{ aws_secret_key }}' security_token: '{{ security_token }}' @@ -56,20 +56,41 @@ tags: Name: '{{ec2_vpc_subnet_name}}' Description: '{{ec2_vpc_subnet_description}}' - region: '{{ec2_region}}' + region: '{{ ec2_region }}' aws_access_key: '{{ aws_access_key }}' aws_secret_key: '{{ aws_secret_key }}' security_token: '{{ security_token }}' state: present register: vpc_subnet_recreate - - name: assert recreation changed nothing (expected changed=true) + - name: assert recreation changed nothing (expected changed=false) assert: that: - 'not vpc_subnet_recreate.changed' - - 'vpc_subnet_recreate.subnet.id.startswith("subnet-")' - - '"Name" in vpc_subnet_recreate.subnet.tags and vpc_subnet_recreate.subnet.tags["Name"] == ec2_vpc_subnet_name' - - '"Description" in vpc_subnet_recreate.subnet.tags and vpc_subnet_recreate.subnet.tags["Description"] == ec2_vpc_subnet_description' + - 'vpc_subnet_recreate.subnet == vpc_subnet_create.subnet' + + - name: add invalid ipv6 block to subnet (expected failed) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + az: "{{ ec2_region }}a" + vpc_id: "{{ vpc_result.vpc.id }}" + ipv6_cidr: 2001:db8::/64 + tags: + Name: '{{ec2_vpc_subnet_name}}' + Description: '{{ec2_vpc_subnet_description}}' + region: '{{ec2_region}}' + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + state: present + register: vpc_subnet_ipv6_failed + ignore_errors: yes + + - name: assert failure happened (expected failed) + assert: + that: + - 'vpc_subnet_ipv6_failed.failed' + - "'Couldn\\'t associate ipv6 cidr' in vpc_subnet_ipv6_failed.msg" - name: add a tag (expected changed=true) ec2_vpc_subnet: @@ -80,7 +101,7 @@ Name: '{{ec2_vpc_subnet_name}}' Description: '{{ec2_vpc_subnet_description}}' AnotherTag: SomeValue - region: '{{ec2_region}}' + region: '{{ ec2_region }}' aws_access_key: '{{ aws_access_key }}' aws_secret_key: '{{ aws_secret_key }}' security_token: '{{ security_token }}' @@ -95,16 +116,14 @@ - '"Description" in vpc_subnet_add_a_tag.subnet.tags and vpc_subnet_add_a_tag.subnet.tags["Description"] == ec2_vpc_subnet_description' - '"AnotherTag" in vpc_subnet_add_a_tag.subnet.tags and vpc_subnet_add_a_tag.subnet.tags["AnotherTag"] == "SomeValue"' - # We may want to change this behaviour by adding purge_tags to the module - # and setting it to false by default - - name: remove tags (expected changed=true) + - name: remove tags with default purge_tags=true (expected changed=true) ec2_vpc_subnet: cidr: "10.232.232.128/28" az: "{{ ec2_region }}a" vpc_id: "{{ vpc_result.vpc.id }}" tags: AnotherTag: SomeValue - region: '{{ec2_region}}' + region: '{{ ec2_region }}' aws_access_key: '{{ aws_access_key }}' aws_secret_key: '{{ aws_secret_key }}' security_token: '{{ security_token }}' @@ -119,12 +138,36 @@ - '"Description" not in vpc_subnet_remove_tags.subnet.tags' - '"AnotherTag" in vpc_subnet_remove_tags.subnet.tags and vpc_subnet_remove_tags.subnet.tags["AnotherTag"] == "SomeValue"' + - name: change tags with purge_tags=false (expected changed=true) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + az: "{{ ec2_region }}a" + vpc_id: "{{ vpc_result.vpc.id }}" + tags: + Name: '{{ec2_vpc_subnet_name}}' + Description: '{{ec2_vpc_subnet_description}}' + region: '{{ec2_region}}' + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + state: present + purge_tags: false + register: vpc_subnet_change_tags + + - name: assert tag addition happened (expected changed=true) + assert: + that: + - 'vpc_subnet_change_tags.changed' + - '"Name" in vpc_subnet_change_tags.subnet.tags and vpc_subnet_change_tags.subnet.tags["Name"] == ec2_vpc_subnet_name' + - '"Description" in vpc_subnet_change_tags.subnet.tags and vpc_subnet_change_tags.subnet.tags["Description"] == ec2_vpc_subnet_description' + - '"AnotherTag" in vpc_subnet_change_tags.subnet.tags and vpc_subnet_change_tags.subnet.tags["AnotherTag"] == "SomeValue"' + - name: test state=absent (expected changed=true) ec2_vpc_subnet: cidr: "10.232.232.128/28" vpc_id: "{{ vpc_result.vpc.id }}" state: absent - region: '{{ec2_region}}' + region: '{{ ec2_region }}' aws_access_key: '{{ aws_access_key }}' aws_secret_key: '{{ aws_secret_key }}' security_token: '{{ security_token }}' @@ -166,6 +209,138 @@ assert: that: - 'result.changed' + + # FIXME - Replace by creating IPv6 enabled VPC once ec2_vpc_net module supports it. + - name: install aws cli - FIXME temporary this should go for a lighterweight solution + command: pip install awscli + + - name: Assign an Amazon provided IPv6 CIDR block to the VPC + command: aws ec2 associate-vpc-cidr-block --amazon-provided-ipv6-cidr-block --vpc-id '{{ vpc_result.vpc.id }}' + environment: + AWS_ACCESS_KEY_ID: '{{aws_access_key}}' + AWS_SECRET_ACCESS_KEY: '{{aws_secret_key}}' + AWS_SESSION_TOKEN: '{{security_token}}' + AWS_DEFAULT_REGION: '{{ec2_region}}' + + - name: Get the assigned IPv6 CIDR + command: aws ec2 describe-vpcs --vpc-ids '{{ vpc_result.vpc.id }}' + environment: + AWS_ACCESS_KEY_ID: '{{aws_access_key}}' + AWS_SECRET_ACCESS_KEY: '{{aws_secret_key}}' + AWS_SESSION_TOKEN: '{{security_token}}' + AWS_DEFAULT_REGION: '{{ec2_region}}' + register: vpc_ipv6 + + - set_fact: + vpc_ipv6_cidr: "{{ vpc_ipv6.stdout | from_json | json_query('Vpcs[0].Ipv6CidrBlockAssociationSet[0].Ipv6CidrBlock') }}" + + - name: create subnet with IPv6 (expected changed=true) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + vpc_id: "{{ vpc_result.vpc.id }}" + ipv6_cidr: "{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}" + assign_instances_ipv6: true + state: present + region: '{{ec2_region}}' + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + tags: + Name: '{{ec2_vpc_subnet_name}}' + Description: '{{ec2_vpc_subnet_description}}' + register: vpc_subnet_ipv6_create + + - name: assert creation with IPv6 happened (expected changed=true) + assert: + that: + - 'vpc_subnet_ipv6_create' + - 'vpc_subnet_ipv6_create.subnet.id.startswith("subnet-")' + - "vpc_subnet_ipv6_create.subnet.ipv6_cidr_block == '{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}'" + - '"Name" in vpc_subnet_ipv6_create.subnet.tags and vpc_subnet_ipv6_create.subnet.tags["Name"] == ec2_vpc_subnet_name' + - '"Description" in vpc_subnet_ipv6_create.subnet.tags and vpc_subnet_ipv6_create.subnet.tags["Description"] == ec2_vpc_subnet_description' + - 'vpc_subnet_ipv6_create.subnet.assign_ipv6_address_on_creation' + + - name: recreate subnet (expected changed=false) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + vpc_id: "{{ vpc_result.vpc.id }}" + ipv6_cidr: "{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}" + assign_instances_ipv6: true + region: '{{ec2_region}}' + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + state: present + tags: + Name: '{{ec2_vpc_subnet_name}}' + Description: '{{ec2_vpc_subnet_description}}' + register: vpc_subnet_ipv6_recreate + + - name: assert recreation changed nothing (expected changed=false) + assert: + that: + - 'not vpc_subnet_ipv6_recreate.changed' + - 'vpc_subnet_ipv6_recreate.subnet == vpc_subnet_ipv6_create.subnet' + + - name: change subnet ipv6 attribute (expected changed=true) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + vpc_id: "{{ vpc_result.vpc.id }}" + ipv6_cidr: "{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}" + assign_instances_ipv6: false + region: '{{ec2_region}}' + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + state: present + purge_tags: false + register: vpc_change_attribute + + - name: assert assign_instances_ipv6 attribute changed (expected changed=true) + assert: + that: + - 'vpc_change_attribute.changed' + - 'not vpc_change_attribute.subnet.assign_ipv6_address_on_creation' + + - name: add second subnet with duplicate ipv6 cidr (expected failure) + ec2_vpc_subnet: + cidr: "10.232.232.144/28" + vpc_id: "{{ vpc_result.vpc.id }}" + ipv6_cidr: "{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}" + region: '{{ec2_region}}' + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + state: present + purge_tags: false + register: vpc_add_duplicate_ipv6 + ignore_errors: true + + - name: assert graceful failure (expected failed) + assert: + that: + - 'vpc_add_duplicate_ipv6.failed' + - "'The IPv6 CIDR \\'{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}\\' conflicts with another subnet' in vpc_add_duplicate_ipv6.msg" + + - name: remove subnet ipv6 cidr (expected changed=true) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + vpc_id: "{{ vpc_result.vpc.id }}" + region: '{{ec2_region}}' + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + state: present + purge_tags: false + register: vpc_remove_ipv6_cidr + + - name: assert subnet ipv6 cidr removed (expected changed=true) + assert: + that: + - 'vpc_remove_ipv6_cidr.changed' + - "vpc_remove_ipv6_cidr.subnet.ipv6_cidr_block == ''" + - 'not vpc_remove_ipv6_cidr.subnet.assign_ipv6_address_on_creation' + always: ################################################ @@ -177,7 +352,7 @@ cidr: "10.232.232.128/28" vpc_id: "{{ vpc_result.vpc.id }}" state: absent - region: '{{ec2_region}}' + region: '{{ ec2_region }}' aws_access_key: '{{ aws_access_key }}' aws_secret_key: '{{ aws_secret_key }}' security_token: '{{ security_token }}' @@ -187,7 +362,7 @@ name: "{{ resource_prefix }}-vpc" state: absent cidr_block: "10.232.232.128/26" - region: '{{ec2_region}}' + region: '{{ ec2_region }}' aws_access_key: '{{ aws_access_key }}' aws_secret_key: '{{ aws_secret_key }}' security_token: '{{ security_token }}'