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