From bc73fba58f755d0fb82548c289ad4901f4fcf698 Mon Sep 17 00:00:00 2001 From: Sloane Hertel Date: Tue, 15 May 2018 14:51:43 -0400 Subject: [PATCH] backport ec2_vpc_subnet fix and custom waiters from PRs 37534/38473/39171/38960 (#39440) * [cloud] Make ec2_vpc_route_table wait for the route to propagate (#35975) * Stabilize ec2_vpc_route_table Wait for route table to be present before attempting to use it Sleep before getting the final state of the route table in case modifications are incomplete * Conditionally wait if changes were made * Simplify logic (cherry picked from commit 8fb31ac2f01e7c75d5181510290c99aee22be7ef) * Route custom waiter (#36922) This creates a way for us to use boto3's data-driven waiter support to use custom waiters where Boto3 hasn't implemented them yet. The only waiter implemented so far is for VPC Route Tables to check that they exist, and this replaces some custom retry code. (cherry picked from commit a40bce2bcbd5a40aee0de2b6ab5f6197bb1c5237) * Use NormalizedOperationMethod to catch ClientErrors so the waiter can handle them properly (#37356) (cherry picked from commit c9e8aca26cfc7559e7e8c7970acf06cd30cc7629) * [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 (cherry picked from commit ea943e454c783c6b0ffb91b78131f27cd9bce269) * Fix sporadic errors in ec2_vpc_subnet integration tests (#38473) (cherry picked from commit 46f13d343786fa3985cc16cc770762984c7884ac) * [aws] Skip ec2_vpc_subnet waiters for old botocore versions (#39171) Fix ec2_vpc_subnet for botocore versions that do not accept the WaiterConfig parameter (cherry picked from commit 6b91dae21c20006677e1e4adf2a9ff7ad55ca49c) * [aws] Increase possible wait time for nonmonotonic subnet attributes (#38960) (cherry picked from commit c4f010704890581a4974e83af03c2e81fb29e58e) * changelog --- .../fragments/ec2_vpc_subnet_waiters.yaml | 6 + lib/ansible/module_utils/aws/waiters.py | 178 +++++++ .../cloud/amazon/ec2_vpc_route_table.py | 11 + .../modules/cloud/amazon/ec2_vpc_subnet.py | 148 +++++- .../targets/ec2_vpc_subnet/tasks/main.yml | 466 +++++++++++++++--- 5 files changed, 707 insertions(+), 102 deletions(-) create mode 100644 changelogs/fragments/ec2_vpc_subnet_waiters.yaml create mode 100644 lib/ansible/module_utils/aws/waiters.py diff --git a/changelogs/fragments/ec2_vpc_subnet_waiters.yaml b/changelogs/fragments/ec2_vpc_subnet_waiters.yaml new file mode 100644 index 00000000000..bece7149a43 --- /dev/null +++ b/changelogs/fragments/ec2_vpc_subnet_waiters.yaml @@ -0,0 +1,6 @@ +--- +bugfixes: + - Use custom waiters + - Add integration tests for check mode + - Fix non-monotonic AWS behavior by waiting until attributes are the correct value before returning the subnet + - Don't use custom waiter configs for older versions of botocore diff --git a/lib/ansible/module_utils/aws/waiters.py b/lib/ansible/module_utils/aws/waiters.py new file mode 100644 index 00000000000..4d59fa3077e --- /dev/null +++ b/lib/ansible/module_utils/aws/waiters.py @@ -0,0 +1,178 @@ +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +try: + import botocore.waiter as core_waiter +except ImportError: + pass # caught by HAS_BOTO3 + + +ec2_data = { + "version": 2, + "waiters": { + "RouteTableExists": { + "delay": 5, + "maxAttempts": 40, + "operation": "DescribeRouteTables", + "acceptors": [ + { + "matcher": "path", + "expected": True, + "argument": "length(RouteTables[]) > `0`", + "state": "success" + }, + { + "matcher": "error", + "expected": "InvalidRouteTableID.NotFound", + "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" + }, + ] + }, + } +} + + +def model_for(name): + ec2_models = core_waiter.WaiterModel(waiter_config=ec2_data) + return ec2_models.get_waiter(name) + + +waiters_by_name = { + ('EC2', 'route_table_exists'): lambda ec2: core_waiter.Waiter( + 'route_table_exists', + 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 + )), +} + + +def get_waiter(client, waiter_name): + try: + return waiters_by_name[(client.__class__.__name__, waiter_name)](client) + except KeyError: + raise NotImplementedError("Waiter {0} could not be found for client {1}. Available waiters: {2}".format( + waiter_name, type(client), ', '.join(repr(k) for k in waiters_by_name.keys()))) diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_route_table.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_route_table.py index 1169eba529f..9a3eae01da5 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_route_table.py +++ b/lib/ansible/modules/cloud/amazon/ec2_vpc_route_table.py @@ -224,7 +224,9 @@ route_table: ''' import re +from time import sleep from ansible.module_utils.aws.core import AnsibleAWSModule +from ansible.module_utils.aws.waiters import get_waiter from ansible.module_utils.ec2 import ec2_argument_spec, boto3_conn, get_aws_connection_info from ansible.module_utils.ec2 import ansible_dict_to_boto3_filter_list from ansible.module_utils.ec2 import camel_dict_to_snake_dict, snake_dict_to_camel_dict @@ -661,6 +663,12 @@ def ensure_route_table_present(connection, module): if not module.check_mode: try: route_table = connection.create_route_table(VpcId=vpc_id)['RouteTable'] + # try to wait for route table to be present before moving on + get_waiter( + connection, 'route_table_exists' + ).wait( + RouteTableIds=[route_table['RouteTableId']], + ) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Error creating route table") else: @@ -692,6 +700,9 @@ def ensure_route_table_present(connection, module): purge_subnets=purge_subnets) changed = changed or result['changed'] + if changed: + # pause to allow route table routes/subnets/associations to be updated before exiting with final state + sleep(5) module.exit_json(changed=changed, route_table=get_route_table_info(connection, module, route_table)) diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet.py index ea56343e30e..4c9acc624a1 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet.py +++ b/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet.py @@ -216,13 +216,16 @@ subnet: import time import traceback +from distutils.version import LooseVersion 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) @@ -262,7 +265,25 @@ 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 waiter_params(module, params, start_time): + if LooseVersion(botocore.__version__) >= "1.7.0": + remaining_wait_timeout = int(module.params['wait_timeout'] + start_time - time.time()) + params['WaiterConfig'] = {'Delay': 5, 'MaxAttempts': remaining_wait_timeout // 5} + return params + + +def handle_waiter(conn, module, waiter_name, params, start_time): + try: + get_waiter(conn, waiter_name).wait( + **waiter_params(module, params, start_time) + ) + 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'] @@ -284,20 +305,19 @@ 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( + **waiter_params(module, {'SubnetIds': [subnet['id']]}, 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'}) @@ -311,7 +331,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: @@ -324,16 +349,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: @@ -342,19 +375,18 @@ 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): +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") -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']) @@ -362,13 +394,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: @@ -385,7 +427,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: @@ -394,6 +436,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'] @@ -422,9 +472,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: @@ -432,24 +487,31 @@ 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']) + if not module.check_mode and module.params['wait']: + # GET calls are not monotonic for map_public_ip_on_launch and assign_ipv6_address_on_creation + # so we only wait for those if necessary just before returning the subnet + subnet = ensure_final_subnet(conn, module, subnet, start_time) return { 'changed': changed, @@ -457,6 +519,36 @@ def ensure_subnet_present(conn, module): } +def ensure_final_subnet(conn, module, subnet, start_time): + for rewait in range(0, 30): + map_public_correct = False + assign_ipv6_correct = False + + if module.params['map_public'] == subnet['map_public_ip_on_launch']: + map_public_correct = True + else: + if module.params['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) + + if module.params['assign_instances_ipv6'] == subnet.get('assign_ipv6_address_on_creation'): + assign_ipv6_correct = True + else: + if module.params['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) + + if map_public_correct and assign_ipv6_correct: + break + + time.sleep(5) + subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr']) + + return subnet + + def ensure_subnet_absent(conn, module): subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr']) if subnet is None: @@ -465,6 +557,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,6 +589,9 @@ def main(): 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") + if LooseVersion(botocore.__version__) < "1.7.0": + module.warn("botocore >= 1.7.0 is required to use wait_timeout for custom wait times") + 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) @@ -506,8 +603,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/tasks/main.yml b/test/integration/targets/ec2_vpc_subnet/tasks/main.yml index 72636e1cc15..6b537adb3a6 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) (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 + 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: "{{ 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 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) (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 + 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: "{{ 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 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) (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}}' + AnotherTag: SomeValue + <<: *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: "{{ 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 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) (CHECK MODE) + 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 + 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: "{{ 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 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) (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 + 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: "{{ 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 register: vpc_subnet_change_tags @@ -162,15 +285,27 @@ - '"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) (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=true) + assert: + that: + - 'result.changed' + - 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 }}' - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' + <<: *aws_connection_info register: result - name: assert state=absent (expected changed=true) @@ -178,15 +313,55 @@ 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 + <<: *aws_connection_info + check_mode: true + register: subnet_without_az + + - name: check that subnet without AZ works fine + assert: + that: + - 'subnet_without_az.changed' + - name: create subnet without AZ 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 register: subnet_without_az - name: check that subnet without AZ works fine @@ -194,15 +369,27 @@ 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 + <<: *aws_connection_info + check_mode: true + register: result + + - name: assert state=absent (expected changed=true) + assert: + 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 - region: '{{ec2_region}}' - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' + <<: *aws_connection_info register: result - name: assert state=absent (expected changed=true) @@ -210,6 +397,7 @@ 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