From 873a9ddf8dc37086aa8998c248310a23ae518c9d Mon Sep 17 00:00:00 2001 From: Sloane Hertel Date: Tue, 3 Apr 2018 15:25:00 -0400 Subject: [PATCH] [cloud] Add custom waiters to stabilize ec2_vpc_subnet module - Fixes #36083 (#37534) * stabilize ec2_vpc_subnet module * Add waiters for ec2_vpc_subnet Clean up integration tests * Reenable CI for stabilized ec2_vpc_subnet tests * rename waiters * Use module_json_aws where applicable Handle WaiterError first if waiting failed * Fix traceback when tagging with keys/values that look like booleans * Fix check mode with tags * Add integration tests for tags that look like booleans and check mode * Add waiter for deleting subnet * Sleep a few seconds after using aws command line --- lib/ansible/module_utils/aws/waiters.py | 128 ++++- .../modules/cloud/amazon/ec2_vpc_subnet.py | 126 ++++- .../targets/ec2_vpc_subnet/aliases | 1 + .../targets/ec2_vpc_subnet/tasks/main.yml | 484 +++++++++++++++--- 4 files changed, 626 insertions(+), 113 deletions(-) diff --git a/lib/ansible/module_utils/aws/waiters.py b/lib/ansible/module_utils/aws/waiters.py index 4f772fc47f..4d59fa3077 100644 --- a/lib/ansible/module_utils/aws/waiters.py +++ b/lib/ansible/module_utils/aws/waiters.py @@ -26,7 +26,95 @@ ec2_data = { "state": "retry" }, ] - } + }, + "SubnetExists": { + "delay": 5, + "maxAttempts": 40, + "operation": "DescribeSubnets", + "acceptors": [ + { + "matcher": "path", + "expected": True, + "argument": "length(Subnets[]) > `0`", + "state": "success" + }, + { + "matcher": "error", + "expected": "InvalidSubnetID.NotFound", + "state": "retry" + }, + ] + }, + "SubnetHasMapPublic": { + "delay": 5, + "maxAttempts": 40, + "operation": "DescribeSubnets", + "acceptors": [ + { + "matcher": "pathAll", + "expected": True, + "argument": "Subnets[].MapPublicIpOnLaunch", + "state": "success" + }, + ] + }, + "SubnetNoMapPublic": { + "delay": 5, + "maxAttempts": 40, + "operation": "DescribeSubnets", + "acceptors": [ + { + "matcher": "pathAll", + "expected": False, + "argument": "Subnets[].MapPublicIpOnLaunch", + "state": "success" + }, + ] + }, + "SubnetHasAssignIpv6": { + "delay": 5, + "maxAttempts": 40, + "operation": "DescribeSubnets", + "acceptors": [ + { + "matcher": "pathAll", + "expected": True, + "argument": "Subnets[].AssignIpv6AddressOnCreation", + "state": "success" + }, + ] + }, + "SubnetNoAssignIpv6": { + "delay": 5, + "maxAttempts": 40, + "operation": "DescribeSubnets", + "acceptors": [ + { + "matcher": "pathAll", + "expected": False, + "argument": "Subnets[].AssignIpv6AddressOnCreation", + "state": "success" + }, + ] + }, + "SubnetDeleted": { + "delay": 5, + "maxAttempts": 40, + "operation": "DescribeSubnets", + "acceptors": [ + { + "matcher": "path", + "expected": True, + "argument": "length(Subnets[]) > `0`", + "state": "retry" + }, + { + "matcher": "error", + "expected": "InvalidSubnetID.NotFound", + "state": "success" + }, + ] + }, } } @@ -42,7 +130,43 @@ waiters_by_name = { model_for('RouteTableExists'), core_waiter.NormalizedOperationMethod( ec2.describe_route_tables - )) + )), + ('EC2', 'subnet_exists'): lambda ec2: core_waiter.Waiter( + 'subnet_exists', + model_for('SubnetExists'), + core_waiter.NormalizedOperationMethod( + ec2.describe_subnets + )), + ('EC2', 'subnet_has_map_public'): lambda ec2: core_waiter.Waiter( + 'subnet_has_map_public', + model_for('SubnetHasMapPublic'), + core_waiter.NormalizedOperationMethod( + ec2.describe_subnets + )), + ('EC2', 'subnet_no_map_public'): lambda ec2: core_waiter.Waiter( + 'subnet_no_map_public', + model_for('SubnetNoMapPublic'), + core_waiter.NormalizedOperationMethod( + ec2.describe_subnets + )), + ('EC2', 'subnet_has_assign_ipv6'): lambda ec2: core_waiter.Waiter( + 'subnet_has_assign_ipv6', + model_for('SubnetHasAssignIpv6'), + core_waiter.NormalizedOperationMethod( + ec2.describe_subnets + )), + ('EC2', 'subnet_no_assign_ipv6'): lambda ec2: core_waiter.Waiter( + 'subnet_no_assign_ipv6', + model_for('SubnetNoAssignIpv6'), + core_waiter.NormalizedOperationMethod( + ec2.describe_subnets + )), + ('EC2', 'subnet_deleted'): lambda ec2: core_waiter.Waiter( + 'subnet_deleted', + model_for('SubnetDeleted'), + core_waiter.NormalizedOperationMethod( + ec2.describe_subnets + )), } diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet.py index 9d0425f1cd..f1555fc3ce 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet.py +++ b/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet.py @@ -209,9 +209,11 @@ import traceback try: import botocore except ImportError: - pass # caught by imported boto3 + pass # caught by AnsibleAWSModule +from ansible.module_utils._text import to_text from ansible.module_utils.aws.core import AnsibleAWSModule +from ansible.module_utils.aws.waiters import get_waiter 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, compare_aws_tags, AWSRetry) @@ -251,7 +253,24 @@ def describe_subnets_with_backoff(client, **params): return client.describe_subnets(**params) -def create_subnet(conn, module, vpc_id, cidr, ipv6_cidr=None, az=None): +def wait_config(wait_timeout, start_time): + remaining_wait_timeout = int(wait_timeout + start_time - time.time()) + return {'Delay': 5, 'MaxAttempts': remaining_wait_timeout // 5} + + +def handle_waiter(conn, module, waiter_name, params, start_time): + params['WaiterConfig'] = wait_config(module.params['wait_timeout'], start_time) + try: + get_waiter(conn, waiter_name).wait( + **params + ) + except botocore.exceptions.WaiterError as e: + module.fail_json_aws(e, "Failed to wait for updates to complete") + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, "An exception happened while trying to wait for updates") + + +def create_subnet(conn, module, vpc_id, cidr, ipv6_cidr=None, az=None, start_time=None): wait = module.params['wait'] wait_timeout = module.params['wait_timeout'] @@ -273,20 +292,20 @@ def create_subnet(conn, module, vpc_id, cidr, ipv6_cidr=None, az=None): # new subnets's id to do things like create tags results in # 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') + handle_waiter(conn, module, 'subnet_exists', {'SubnetIds': [subnet['id']]}, start_time) try: - waiter.wait(SubnetIds=[subnet['id']], WaiterConfig=waiter_config) + conn.get_waiter('subnet_available').wait( + SubnetIds=[subnet['id']], + WaiterConfig=wait_config(wait_timeout, start_time) + ) 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.") + module.fail_json_aws(e, "Create subnet action timed out waiting for subnet to become available") return subnet -def ensure_tags(conn, module, subnet, tags, purge_tags): +def ensure_tags(conn, module, subnet, tags, purge_tags, start_time): changed = False filters = ansible_dict_to_boto3_filter_list({'resource-id': subnet['id'], 'resource-type': 'subnet'}) @@ -300,7 +319,12 @@ def ensure_tags(conn, module, subnet, tags, purge_tags): if to_update: try: if not module.check_mode: - conn.create_tags(Resources=[subnet['id']], Tags=ansible_dict_to_boto3_tag_list(to_update)) + AWSRetry.exponential_backoff( + catch_extra_error_codes=['InvalidSubnetID.NotFound'] + )(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: @@ -313,16 +337,24 @@ def ensure_tags(conn, module, subnet, tags, purge_tags): for key in to_delete: tags_list.append({'Key': key}) - conn.delete_tags(Resources=[subnet['id']], Tags=tags_list) + AWSRetry.exponential_backoff( + catch_extra_error_codes=['InvalidSubnetID.NotFound'] + )(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") + if module.params['wait'] and not module.check_mode: + # Wait for tags to be updated + filters = [{'Name': 'tag:{0}'.format(k), 'Values': [v]} for k, v in tags.items()] + handle_waiter(conn, module, 'subnet_exists', + {'SubnetIds': [subnet['id']], 'Filters': filters}, start_time) + return changed -def ensure_map_public(conn, module, subnet, map_public, check_mode): +def ensure_map_public(conn, module, subnet, map_public, check_mode, start_time): if check_mode: return try: @@ -330,20 +362,35 @@ def ensure_map_public(conn, module, subnet, map_public, check_mode): except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Couldn't modify subnet attribute") + if module.params['wait']: + if map_public: + handle_waiter(conn, module, 'subnet_has_map_public', + {'SubnetIds': [subnet['id']]}, start_time) + else: + handle_waiter(conn, module, 'subnet_no_map_public', + {'SubnetIds': [subnet['id']]}, start_time) -def ensure_assign_ipv6_on_create(conn, module, subnet, assign_instances_ipv6, check_mode): + +def ensure_assign_ipv6_on_create(conn, module, subnet, assign_instances_ipv6, check_mode, start_time): 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") + if module.params['wait']: + if assign_instances_ipv6: + handle_waiter(conn, module, 'subnet_has_assign_ipv6', + {'SubnetIds': [subnet['id']]}, start_time) + else: + handle_waiter(conn, module, 'subnet_no_assign_ipv6', + {'SubnetIds': [subnet['id']]}, start_time) -def disassociate_ipv6_cidr(conn, module, subnet): + +def disassociate_ipv6_cidr(conn, module, subnet, start_time): if subnet.get('assign_ipv6_address_on_creation'): - ensure_assign_ipv6_on_create(conn, module, subnet, False, False) + ensure_assign_ipv6_on_create(conn, module, subnet, False, False, start_time) try: conn.disassociate_subnet_cidr_block(AssociationId=subnet['ipv6_association_id']) @@ -351,13 +398,23 @@ def disassociate_ipv6_cidr(conn, module, subnet): module.fail_json_aws(e, msg="Couldn't disassociate ipv6 cidr block id {0} from subnet {1}" .format(subnet['ipv6_association_id'], subnet['id'])) + # Wait for cidr block to be disassociated + if module.params['wait']: + filters = ansible_dict_to_boto3_filter_list( + {'ipv6-cidr-block-association.state': ['disassociated'], + 'vpc-id': subnet['vpc_id']} + ) + handle_waiter(conn, module, 'subnet_exists', + {'SubnetIds': [subnet['id']], 'Filters': filters}, start_time) -def ensure_ipv6_cidr_block(conn, module, subnet, ipv6_cidr, check_mode): + +def ensure_ipv6_cidr_block(conn, module, subnet, ipv6_cidr, check_mode, start_time): + wait = module.params['wait'] changed = False if subnet['ipv6_association_id'] and not ipv6_cidr: if not check_mode: - disassociate_ipv6_cidr(conn, module, subnet) + disassociate_ipv6_cidr(conn, module, subnet, start_time) changed = True if ipv6_cidr: @@ -374,7 +431,7 @@ def ensure_ipv6_cidr_block(conn, module, subnet, ipv6_cidr, check_mode): if subnet['ipv6_association_id']: if not check_mode: - disassociate_ipv6_cidr(conn, module, subnet) + disassociate_ipv6_cidr(conn, module, subnet, start_time) changed = True try: @@ -383,6 +440,14 @@ def ensure_ipv6_cidr_block(conn, module, subnet, ipv6_cidr, check_mode): 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'])) + else: + if not check_mode and wait: + filters = ansible_dict_to_boto3_filter_list( + {'ipv6-cidr-block-association.state': ['associated'], + 'vpc-id': subnet['vpc_id']} + ) + handle_waiter(conn, module, 'subnet_exists', + {'SubnetIds': [subnet['id']], 'Filters': filters}, start_time) if associate_resp.get('Ipv6CidrBlockAssociation', {}).get('AssociationId'): subnet['ipv6_association_id'] = associate_resp['Ipv6CidrBlockAssociation']['AssociationId'] @@ -411,9 +476,14 @@ def 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 + + # Initialize start so max time does not exceed the specified wait_timeout for multiple operations + start_time = time.time() + if subnet is None: 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']) + subnet = create_subnet(conn, module, module.params['vpc_id'], module.params['cidr'], + ipv6_cidr=module.params['ipv6_cidr'], az=module.params['az'], start_time=start_time) changed = True # Subnet will be None when check_mode is true if subnet is None: @@ -421,21 +491,24 @@ def ensure_subnet_present(conn, module): 'changed': changed, 'subnet': {} } + if module.params['wait']: + handle_waiter(conn, module, 'subnet_exists', {'SubnetIds': [subnet['id']]}, start_time) 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): + if ensure_ipv6_cidr_block(conn, module, subnet, module.params['ipv6_cidr'], module.check_mode, start_time): 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) + ensure_map_public(conn, module, subnet, module.params['map_public'], module.check_mode, start_time) changed = True 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) + ensure_assign_ipv6_on_create(conn, module, subnet, module.params['assign_instances_ipv6'], module.check_mode, start_time) changed = True if module.params['tags'] != subnet['tags']: - if ensure_tags(conn, module, subnet, module.params['tags'], module.params['purge_tags']): + stringified_tags_dict = dict((to_text(k), to_text(v)) for k, v in module.params['tags'].items()) + if ensure_tags(conn, module, subnet, stringified_tags_dict, module.params['purge_tags'], start_time): changed = True subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr']) @@ -454,6 +527,8 @@ def ensure_subnet_absent(conn, module): try: if not module.check_mode: conn.delete_subnet(SubnetId=subnet['id']) + if module.params['wait']: + handle_waiter(conn, module, 'subnet_deleted', {'SubnetIds': [subnet['id']]}, time.time()) return {'changed': True} except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Couldn't delete subnet") @@ -495,8 +570,7 @@ def main(): elif state == 'absent': 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)) + module.fail_json_aws(e) module.exit_json(**result) diff --git a/test/integration/targets/ec2_vpc_subnet/aliases b/test/integration/targets/ec2_vpc_subnet/aliases index 4ef4b2067d..d6ae2f116b 100644 --- a/test/integration/targets/ec2_vpc_subnet/aliases +++ b/test/integration/targets/ec2_vpc_subnet/aliases @@ -1 +1,2 @@ cloud/aws +posix/ci/cloud/group4/aws diff --git a/test/integration/targets/ec2_vpc_subnet/tasks/main.yml b/test/integration/targets/ec2_vpc_subnet/tasks/main.yml index 72636e1cc1..6b537adb3a 100644 --- a/test/integration/targets/ec2_vpc_subnet/tasks/main.yml +++ b/test/integration/targets/ec2_vpc_subnet/tasks/main.yml @@ -10,33 +10,55 @@ - block: + - name: set up aws connection info + set_fact: + aws_connection_info: &aws_connection_info + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token }}" + region: "{{ aws_region }}" + no_log: yes + # ============================================================ - name: create a VPC ec2_vpc_net: name: "{{ resource_prefix }}-vpc" state: present cidr_block: "10.232.232.128/26" - region: '{{ ec2_region }}' - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' + <<: *aws_connection_info tags: Name: "{{ resource_prefix }}-vpc" Description: "Created by ansible-test" register: vpc_result - - name: create subnet (expected changed=true) + # ============================================================ + - name: create subnet (expected changed=true) (CHECK MODE) ec2_vpc_subnet: cidr: "10.232.232.128/28" - az: "{{ ec2_region }}a" + az: "{{ aws_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 }}' + <<: *aws_connection_info + state: present + check_mode: true + register: vpc_subnet_create + + - name: assert creation would happen + assert: + that: + - vpc_subnet_create.changed + + - name: create subnet (expected changed=true) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + az: "{{ aws_region }}a" + vpc_id: "{{ vpc_result.vpc.id }}" + tags: + Name: '{{ec2_vpc_subnet_name}}' + Description: '{{ec2_vpc_subnet_description}}' + <<: *aws_connection_info state: present register: vpc_subnet_create @@ -47,19 +69,34 @@ - 'vpc_subnet_create.subnet.id.startswith("subnet-")' - '"Name" in vpc_subnet_create.subnet.tags and vpc_subnet_create.subnet.tags["Name"] == ec2_vpc_subnet_name' - '"Description" in vpc_subnet_create.subnet.tags and vpc_subnet_create.subnet.tags["Description"] == ec2_vpc_subnet_description' - - - name: recreate subnet (expected changed=false) + # ============================================================ + - name: recreate subnet (expected changed=false) (CHECK MODE) ec2_vpc_subnet: cidr: "10.232.232.128/28" - az: "{{ ec2_region }}a" + az: "{{ aws_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 }}' + <<: *aws_connection_info + state: present + check_mode: true + register: vpc_subnet_recreate + + - name: assert recreation changed nothing (expected changed=false) + assert: + that: + - 'not vpc_subnet_recreate.changed' + + - name: recreate subnet (expected changed=false) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + az: "{{ aws_region }}a" + vpc_id: "{{ vpc_result.vpc.id }}" + tags: + Name: '{{ec2_vpc_subnet_name}}' + Description: '{{ec2_vpc_subnet_description}}' + <<: *aws_connection_info state: present register: vpc_subnet_recreate @@ -69,19 +106,56 @@ - 'not vpc_subnet_recreate.changed' - 'vpc_subnet_recreate.subnet == vpc_subnet_create.subnet' + # ============================================================ + - name: update subnet so instances launched in it are assigned an IP (CHECK MODE) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + az: "{{ aws_region }}a" + vpc_id: "{{ vpc_result.vpc.id }}" + tags: + Name: '{{ec2_vpc_subnet_name}}' + Description: '{{ec2_vpc_subnet_description}}' + <<: *aws_connection_info + state: present + map_public: true + check_mode: true + register: vpc_subnet_modify + + - name: assert subnet changed + assert: + that: + - vpc_subnet_modify.changed + + - name: update subnet so instances launched in it are assigned an IP + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + az: "{{ aws_region }}a" + vpc_id: "{{ vpc_result.vpc.id }}" + tags: + Name: '{{ec2_vpc_subnet_name}}' + Description: '{{ec2_vpc_subnet_description}}' + <<: *aws_connection_info + state: present + map_public: true + register: vpc_subnet_modify + + - name: assert subnet changed + assert: + that: + - vpc_subnet_modify.changed + - vpc_subnet_modify.subnet.map_public_ip_on_launch + + # ============================================================ - name: add invalid ipv6 block to subnet (expected failed) ec2_vpc_subnet: cidr: "10.232.232.128/28" - az: "{{ ec2_region }}a" + az: "{{ aws_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 }}' + <<: *aws_connection_info state: present register: vpc_subnet_ipv6_failed ignore_errors: yes @@ -92,19 +166,36 @@ - 'vpc_subnet_ipv6_failed.failed' - "'Couldn\\'t associate ipv6 cidr' in vpc_subnet_ipv6_failed.msg" - - name: add a tag (expected changed=true) + # ============================================================ + - name: add a tag (expected changed=true) (CHECK MODE) ec2_vpc_subnet: cidr: "10.232.232.128/28" - az: "{{ ec2_region }}a" + az: "{{ aws_region }}a" vpc_id: "{{ vpc_result.vpc.id }}" tags: Name: '{{ec2_vpc_subnet_name}}' Description: '{{ec2_vpc_subnet_description}}' AnotherTag: SomeValue - region: '{{ ec2_region }}' - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' + <<: *aws_connection_info + state: present + check_mode: true + register: vpc_subnet_add_a_tag + + - name: assert tag addition happened (expected changed=true) + assert: + that: + - 'vpc_subnet_add_a_tag.changed' + + - name: add a tag (expected changed=true) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + az: "{{ aws_region }}a" + vpc_id: "{{ vpc_result.vpc.id }}" + tags: + Name: '{{ec2_vpc_subnet_name}}' + Description: '{{ec2_vpc_subnet_description}}' + AnotherTag: SomeValue + <<: *aws_connection_info state: present register: vpc_subnet_add_a_tag @@ -116,17 +207,32 @@ - '"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"' - - name: remove tags with default purge_tags=true (expected changed=true) + # ============================================================ + - name: remove tags with default purge_tags=true (expected changed=true) (CHECK MODE) ec2_vpc_subnet: cidr: "10.232.232.128/28" - az: "{{ ec2_region }}a" + az: "{{ aws_region }}a" vpc_id: "{{ vpc_result.vpc.id }}" tags: AnotherTag: SomeValue - region: '{{ ec2_region }}' - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' + <<: *aws_connection_info + state: present + check_mode: true + register: vpc_subnet_remove_tags + + - name: assert tag removal happened (expected changed=true) + assert: + that: + - 'vpc_subnet_remove_tags.changed' + + - name: remove tags with default purge_tags=true (expected changed=true) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + az: "{{ aws_region }}a" + vpc_id: "{{ vpc_result.vpc.id }}" + tags: + AnotherTag: SomeValue + <<: *aws_connection_info state: present register: vpc_subnet_remove_tags @@ -138,18 +244,35 @@ - '"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) + # ============================================================ + - name: change tags with purge_tags=false (expected changed=true) (CHECK MODE) ec2_vpc_subnet: cidr: "10.232.232.128/28" - az: "{{ ec2_region }}a" + az: "{{ aws_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 }}' + <<: *aws_connection_info + state: present + purge_tags: false + check_mode: true + register: vpc_subnet_change_tags + + - name: assert tag addition happened (expected changed=true) + assert: + that: + - 'vpc_subnet_change_tags.changed' + + - name: change tags with purge_tags=false (expected changed=true) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + az: "{{ aws_region }}a" + vpc_id: "{{ vpc_result.vpc.id }}" + tags: + Name: '{{ec2_vpc_subnet_name}}' + Description: '{{ec2_vpc_subnet_description}}' + <<: *aws_connection_info state: present purge_tags: false register: vpc_subnet_change_tags @@ -162,15 +285,14 @@ - '"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) + # ============================================================ + - name: test state=absent (expected changed=true) (CHECK MODE) ec2_vpc_subnet: cidr: "10.232.232.128/28" vpc_id: "{{ vpc_result.vpc.id }}" state: absent - region: '{{ ec2_region }}' - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' + <<: *aws_connection_info + check_mode: true register: result - name: assert state=absent (expected changed=true) @@ -178,15 +300,55 @@ that: - 'result.changed' - - name: create subnet without AZ + - name: test state=absent (expected changed=true) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + vpc_id: "{{ vpc_result.vpc.id }}" + state: absent + <<: *aws_connection_info + register: result + + - name: assert state=absent (expected changed=true) + assert: + that: + - 'result.changed' + + # ============================================================ + - name: test state=absent (expected changed=false) (CHECK MODE) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + vpc_id: "{{ vpc_result.vpc.id }}" + state: absent + <<: *aws_connection_info + check_mode: true + register: result + + - name: assert state=absent (expected changed=false) + assert: + that: + - 'not result.changed' + + - name: test state=absent (expected changed=false) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + vpc_id: "{{ vpc_result.vpc.id }}" + state: absent + <<: *aws_connection_info + register: result + + - name: assert state=absent (expected changed=false) + assert: + that: + - 'not result.changed' + + # ============================================================ + - name: create subnet without AZ (CHECK MODE) ec2_vpc_subnet: cidr: "10.232.232.128/28" vpc_id: "{{ vpc_result.vpc.id }}" state: present - region: '{{ec2_region}}' - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' + <<: *aws_connection_info + check_mode: true register: subnet_without_az - name: check that subnet without AZ works fine @@ -194,15 +356,27 @@ that: - 'subnet_without_az.changed' - - name: remove subnet without AZ + - name: create subnet without AZ + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + vpc_id: "{{ vpc_result.vpc.id }}" + state: present + <<: *aws_connection_info + register: subnet_without_az + + - name: check that subnet without AZ works fine + assert: + that: + - 'subnet_without_az.changed' + + # ============================================================ + - name: remove subnet without AZ (CHECK MODE) ec2_vpc_subnet: cidr: "10.232.232.128/28" vpc_id: "{{ vpc_result.vpc.id }}" state: absent - region: '{{ec2_region}}' - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' + <<: *aws_connection_info + check_mode: true register: result - name: assert state=absent (expected changed=true) @@ -210,6 +384,20 @@ that: - 'result.changed' + - name: remove subnet without AZ + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + vpc_id: "{{ vpc_result.vpc.id }}" + state: absent + <<: *aws_connection_info + register: result + + - name: assert state=absent (expected changed=true) + 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 @@ -220,7 +408,10 @@ AWS_ACCESS_KEY_ID: '{{aws_access_key}}' AWS_SECRET_ACCESS_KEY: '{{aws_secret_key}}' AWS_SESSION_TOKEN: '{{security_token}}' - AWS_DEFAULT_REGION: '{{ec2_region}}' + AWS_DEFAULT_REGION: '{{aws_region}}' + + - name: wait for the IPv6 CIDR to be assigned + command: sleep 5 - name: Get the assigned IPv6 CIDR command: aws ec2 describe-vpcs --vpc-ids '{{ vpc_result.vpc.id }}' @@ -228,12 +419,32 @@ AWS_ACCESS_KEY_ID: '{{aws_access_key}}' AWS_SECRET_ACCESS_KEY: '{{aws_secret_key}}' AWS_SESSION_TOKEN: '{{security_token}}' - AWS_DEFAULT_REGION: '{{ec2_region}}' + AWS_DEFAULT_REGION: '{{aws_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) (CHECK MODE) + 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 + <<: *aws_connection_info + tags: + Name: '{{ec2_vpc_subnet_name}}' + Description: '{{ec2_vpc_subnet_description}}' + check_mode: true + register: vpc_subnet_ipv6_create + + - name: assert creation with IPv6 happened (expected changed=true) + assert: + that: + - 'vpc_subnet_ipv6_create.changed' + - name: create subnet with IPv6 (expected changed=true) ec2_vpc_subnet: cidr: "10.232.232.128/28" @@ -241,10 +452,7 @@ 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 }}' + <<: *aws_connection_info tags: Name: '{{ec2_vpc_subnet_name}}' Description: '{{ec2_vpc_subnet_description}}' @@ -260,16 +468,33 @@ - '"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) (CHECK MODE) + 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 + <<: *aws_connection_info + state: present + tags: + Name: '{{ec2_vpc_subnet_name}}' + Description: '{{ec2_vpc_subnet_description}}' + check_mode: true + register: vpc_subnet_ipv6_recreate + + - name: assert recreation changed nothing (expected changed=false) + assert: + that: + - 'not vpc_subnet_ipv6_recreate.changed' + - 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 }}' + <<: *aws_connection_info state: present tags: Name: '{{ec2_vpc_subnet_name}}' @@ -282,16 +507,31 @@ - 'not vpc_subnet_ipv6_recreate.changed' - 'vpc_subnet_ipv6_recreate.subnet == vpc_subnet_ipv6_create.subnet' + # ============================================================ + - name: change subnet ipv6 attribute (expected changed=true) (CHECK MODE) + 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 + <<: *aws_connection_info + state: present + purge_tags: false + check_mode: true + register: vpc_change_attribute + + - name: assert assign_instances_ipv6 attribute changed (expected changed=true) + assert: + that: + - 'vpc_change_attribute.changed' + - 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 }}' + <<: *aws_connection_info state: present purge_tags: false register: vpc_change_attribute @@ -302,15 +542,13 @@ - '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 }}' + <<: *aws_connection_info state: present purge_tags: false register: vpc_add_duplicate_ipv6 @@ -322,14 +560,27 @@ - '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) (CHECK MODE) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + vpc_id: "{{ vpc_result.vpc.id }}" + <<: *aws_connection_info + state: present + purge_tags: false + check_mode: true + register: vpc_remove_ipv6_cidr + + - name: assert subnet ipv6 cidr removed (expected changed=true) + assert: + that: + - 'vpc_remove_ipv6_cidr.changed' + - 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 }}' + <<: *aws_connection_info state: present purge_tags: false register: vpc_remove_ipv6_cidr @@ -341,6 +592,75 @@ - "vpc_remove_ipv6_cidr.subnet.ipv6_cidr_block == ''" - 'not vpc_remove_ipv6_cidr.subnet.assign_ipv6_address_on_creation' + # ============================================================ + - name: test adding a tag that looks like a boolean to the subnet (CHECK MODE) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + vpc_id: "{{ vpc_result.vpc.id }}" + state: present + purge_tags: false + tags: + looks_like_boolean: true + <<: *aws_connection_info + check_mode: true + register: vpc_subnet_info + + - name: assert a tag was added + assert: + that: + - 'vpc_subnet_info.changed' + + - name: test adding a tag that looks like a boolean to the subnet + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + vpc_id: "{{ vpc_result.vpc.id }}" + state: present + purge_tags: false + tags: + looks_like_boolean: true + <<: *aws_connection_info + register: vpc_subnet_info + + - name: assert a tag was added + assert: + that: + - 'vpc_subnet_info.changed' + - 'vpc_subnet_info.subnet.tags.looks_like_boolean == "True"' + + # ============================================================ + - name: test idempotence adding a tag that looks like a boolean (CHECK MODE) + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + vpc_id: "{{ vpc_result.vpc.id }}" + state: present + purge_tags: false + tags: + looks_like_boolean: true + <<: *aws_connection_info + check_mode: true + register: vpc_subnet_info + + - name: assert a tag was added + assert: + that: + - 'not vpc_subnet_info.changed' + + - name: test idempotence adding a tag that looks like a boolean + ec2_vpc_subnet: + cidr: "10.232.232.128/28" + vpc_id: "{{ vpc_result.vpc.id }}" + state: present + purge_tags: false + tags: + looks_like_boolean: true + <<: *aws_connection_info + register: vpc_subnet_info + + - name: assert a tag was added + assert: + that: + - 'not vpc_subnet_info.changed' + always: ################################################ @@ -352,17 +672,11 @@ cidr: "10.232.232.128/28" vpc_id: "{{ vpc_result.vpc.id }}" state: absent - region: '{{ ec2_region }}' - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' + <<: *aws_connection_info - name: tidy up VPC ec2_vpc_net: name: "{{ resource_prefix }}-vpc" state: absent cidr_block: "10.232.232.128/26" - region: '{{ ec2_region }}' - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' + <<: *aws_connection_info