diff --git a/lib/ansible/module_utils/ec2.py b/lib/ansible/module_utils/ec2.py index 1fe742173fa..14ae32b1424 100644 --- a/lib/ansible/module_utils/ec2.py +++ b/lib/ansible/module_utils/ec2.py @@ -373,7 +373,7 @@ def _camel_to_snake(name, reversible=False): return re.sub(all_cap_pattern, r'\1_\2', s2).lower() -def camel_dict_to_snake_dict(camel_dict, reversible=False): +def camel_dict_to_snake_dict(camel_dict, reversible=False, ignore_list=()): """ reversible allows two way conversion of a camelized dict such that snake_dict_to_camel_dict(camel_dict_to_snake_dict(x)) == x @@ -381,6 +381,10 @@ def camel_dict_to_snake_dict(camel_dict, reversible=False): This is achieved through mapping e.g. HTTPEndpoint to h_t_t_p_endpoint where the default would be simply http_endpoint, which gets turned into HttpEndpoint if recamelized. + + ignore_list is used to avoid converting a sub-tree of a dict. This is + particularly important for tags, where keys are case-sensitive. We convert + the 'Tags' key but nothing below. """ def value_is_list(camel_list): @@ -398,9 +402,9 @@ def camel_dict_to_snake_dict(camel_dict, reversible=False): snake_dict = {} for k, v in camel_dict.items(): - if isinstance(v, dict): + if isinstance(v, dict) and k not in ignore_list: snake_dict[_camel_to_snake(k, reversible=reversible)] = camel_dict_to_snake_dict(v, reversible) - elif isinstance(v, list): + elif isinstance(v, list) and k not in ignore_list: snake_dict[_camel_to_snake(k, reversible=reversible)] = value_is_list(v) else: snake_dict[_camel_to_snake(k, reversible=reversible)] = v 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 62c55de5806..2607b681f6f 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_route_table.py +++ b/lib/ansible/modules/cloud/amazon/ec2_vpc_route_table.py @@ -25,69 +25,55 @@ short_description: Manage route tables for AWS virtual private clouds description: - Manage route tables for AWS virtual private clouds version_added: "2.0" -author: Robert Estelle (@erydo), Rob White (@wimnat) +author: +- Robert Estelle (@erydo) +- Rob White (@wimnat) +- Will Thames (@willthames) options: lookup: - description: - - "Look up route table by either tags or by route table ID. Non-unique tag lookup will fail. - If no tags are specified then no lookup for an existing route table is performed and a new - route table will be created. To change tags of a route table or delete a route table, - you must look up by id." - required: false + description: Look up route table by either tags or by route table ID. Non-unique tag lookup will fail. + If no tags are specified then no lookup for an existing route table is performed and a new + route table will be created. To change tags of a route table you must look up by id. default: tag choices: [ 'tag', 'id' ] propagating_vgw_ids: - description: - - "Enable route propagation from virtual gateways specified by ID." + description: Enable route propagation from virtual gateways specified by ID. default: None - required: false purge_routes: version_added: "2.3" - description: - - "Purge existing routes that are not found in routes." - required: false + description: Purge existing routes that are not found in routes. default: 'true' - aliases: [] purge_subnets: version_added: "2.3" - description: - - "Purge existing subnets that are not found in subnets. Ignored unless the subnets option is supplied." - required: false + description: Purge existing subnets that are not found in subnets. Ignored unless the subnets option is supplied. default: 'true' - aliases: [] + purge_tags: + version_added: "2.5" + description: Purge existing tags that are not found in route table + default: 'false' route_table_id: - description: - - "The ID of the route table to update or delete." - required: false - default: null + description: The ID of the route table to update or delete. routes: - description: - - "List of routes in the route table. + description: List of routes in the route table. Routes are specified as dicts containing the keys 'dest' and one of 'gateway_id', 'instance_id', 'interface_id', or 'vpc_peering_connection_id'. - If 'gateway_id' is specified, you can refer to the VPC's IGW by using the value 'igw'. Routes are required for present states." - required: false + If 'gateway_id' is specified, you can refer to the VPC's IGW by using the value 'igw'. + Routes are required for present states. default: None state: - description: - - "Create or destroy the VPC route table" - required: false + description: Create or destroy the VPC route table default: present choices: [ 'present', 'absent' ] subnets: - description: - - "An array of subnets to add to this route table. Subnets may be specified by either subnet ID, Name tag, or by a CIDR such as '10.0.0.0/24'." - required: false + description: An array of subnets to add to this route table. Subnets may be specified + by either subnet ID, Name tag, or by a CIDR such as '10.0.0.0/24'. tags: - description: - - "A dictionary of resource tags of the form: { tag1: value1, tag2: value2 }. Tags are - used to uniquely identify route tables within a VPC when the route_table_id is not supplied." - required: false - default: null + description: > + A dictionary of resource tags of the form: { tag1: value1, tag2: value2 }. Tags are + used to uniquely identify route tables within a VPC when the route_table_id is not supplied. aliases: [ "resource_tags" ] vpc_id: - description: - - "VPC ID of the VPC in which to create the route table." + description: VPC ID of the VPC in which to create the route table. required: true extends_documentation_fragment: - aws @@ -137,36 +123,119 @@ EXAMPLES = ''' state: absent ''' +RETURN = ''' +route_table: + description: Route Table result + returned: always + type: complex + contains: + associations: + description: List of subnets associated with the route table + returned: always + type: complex + contains: + main: + description: Whether this is the main route table + returned: always + type: bool + sample: false + route_table_association_id: + description: ID of association between route table and subnet + returned: always + type: string + sample: rtbassoc-ab47cfc3 + route_table_id: + description: ID of the route table + returned: always + type: string + sample: rtb-bf779ed7 + subnet_id: + description: ID of the subnet + returned: always + type: string + sample: subnet-82055af9 + id: + description: ID of the route table (same as route_table_id for backwards compatibility) + returned: always + type: string + sample: rtb-bf779ed7 + propagating_vgws: + description: List of Virtual Private Gateways propagating routes + returned: always + type: list + sample: [] + route_table_id: + description: ID of the route table + returned: always + type: string + sample: rtb-bf779ed7 + routes: + description: List of routes in the route table + returned: always + type: complex + contains: + destination_cidr_block: + description: CIDR block of destination + returned: always + type: string + sample: 10.228.228.0/22 + gateway_id: + description: ID of the gateway + returned: when gateway is local or internet gateway + type: string + sample: local + instance_id: + description: ID of a NAT instance + returned: when the route is via an EC2 instance + type: string + sample: i-abcd123456789 + instance_owner_id: + description: AWS account owning the NAT instance + returned: when the route is via an EC2 instance + type: string + sample: 123456789012 + nat_gateway_id: + description: ID of the NAT gateway + returned: when the route is via a NAT gateway + type: string + sample: local + origin: + description: mechanism through which the route is in the table + returned: always + type: string + sample: CreateRouteTable + state: + description: state of the route + returned: always + type: string + sample: active + tags: + description: Tags applied to the route table + returned: always + type: dict + sample: + Name: Public route table + Public: 'true' + vpc_id: + description: ID for the VPC in which the route lives + returned: always + type: string + sample: vpc-6e2d2407 +''' + import re -import traceback -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import AnsibleAWSError, connect_to_aws, ec2_argument_spec, get_aws_connection_info +from ansible.module_utils.aws.core import AnsibleAWSModule +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 +from ansible.module_utils.ec2 import ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict +from ansible.module_utils.ec2 import compare_aws_tags, AWSRetry + try: - import boto.ec2 - import boto.vpc - from boto.exception import EC2ResponseError - HAS_BOTO = True + import botocore except ImportError: - HAS_BOTO = False - - -class AnsibleRouteTableException(Exception): - def __init__(self, message, error_traceback=None): - self.message = message - self.error_traceback = error_traceback - - -class AnsibleIgwSearchException(AnsibleRouteTableException): - pass - - -class AnsibleTagCreationException(AnsibleRouteTableException): - pass - - -class AnsibleSubnetSearchException(AnsibleRouteTableException): - pass + pass # handled by AnsibleAWSModule CIDR_RE = re.compile(r'^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$') @@ -174,7 +243,12 @@ SUBNET_RE = re.compile(r'^subnet-[A-z0-9]+$') ROUTE_TABLE_RE = re.compile(r'^rtb-[A-z0-9]+$') -def find_subnets(vpc_conn, vpc_id, identified_subnets): +@AWSRetry.exponential_backoff() +def describe_subnets_with_backoff(connection, **params): + return connection.describe_subnets(**params)['Subnets'] + + +def find_subnets(connection, module, vpc_id, identified_subnets): """ Finds a list of subnets, each identified either by a raw ID, a unique 'Name' tag, or a CIDR such as 10.0.0.0/8. @@ -195,67 +269,61 @@ def find_subnets(vpc_conn, vpc_id, identified_subnets): subnets_by_id = [] if subnet_ids: - subnets_by_id = vpc_conn.get_all_subnets( - subnet_ids, filters={'vpc_id': vpc_id}) - - for subnet_id in subnet_ids: - if not any(s.id == subnet_id for s in subnets_by_id): - raise AnsibleSubnetSearchException( - 'Subnet ID "{0}" does not exist'.format(subnet_id)) + filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id}) + try: + subnets_by_id = describe_subnets_with_backoff(connection, SubnetIds=subnet_ids, Filters=filters) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't find subnet with id %s" % subnet_ids) subnets_by_cidr = [] if subnet_cidrs: - subnets_by_cidr = vpc_conn.get_all_subnets( - filters={'vpc_id': vpc_id, 'cidr': subnet_cidrs}) - - for cidr in subnet_cidrs: - if not any(s.cidr_block == cidr for s in subnets_by_cidr): - raise AnsibleSubnetSearchException( - 'Subnet CIDR "{0}" does not exist'.format(cidr)) + filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id, 'cidr': subnet_cidrs}) + try: + subnets_by_cidr = describe_subnets_with_backoff(connection, Filters=filters) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't find subnet with cidr %s" % subnet_cidrs) subnets_by_name = [] if subnet_names: - subnets_by_name = vpc_conn.get_all_subnets( - filters={'vpc_id': vpc_id, 'tag:Name': subnet_names}) + filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id, 'tag:Name': subnet_names}) + try: + subnets_by_name = describe_subnets_with_backoff(connection, Filters=filters) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't find subnet with names %s" % subnet_names) for name in subnet_names: matching_count = len([1 for s in subnets_by_name if s.tags.get('Name') == name]) if matching_count == 0: - raise AnsibleSubnetSearchException( - 'Subnet named "{0}" does not exist'.format(name)) + module.fail_json(msg='Subnet named "{0}" does not exist'.format(name)) elif matching_count > 1: - raise AnsibleSubnetSearchException( - 'Multiple subnets named "{0}"'.format(name)) + module.fail_json(msg='Multiple subnets named "{0}"'.format(name)) return subnets_by_id + subnets_by_cidr + subnets_by_name -def find_igw(vpc_conn, vpc_id): +def find_igw(connection, module, vpc_id): """ Finds the Internet gateway for the given VPC ID. - - Raises an AnsibleIgwSearchException if either no IGW can be found, or more - than one found for the given VPC. - - Note that this function is duplicated in other ec2 modules, and should - potentially be moved into potentially be moved into a shared module_utils """ - igw = vpc_conn.get_all_internet_gateways( - filters={'attachment.vpc-id': vpc_id}) - - if not igw: - raise AnsibleIgwSearchException('No IGW found for VPC {0}'. - format(vpc_id)) - elif len(igw) == 1: - return igw[0].id + filters = ansible_dict_to_boto3_filter_list({'attachment.vpc-id': vpc_id}) + try: + igw = connection.describe_internet_gateways(Filters=filters)['InternetGateways'] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg='No IGW found for VPC {0}'.format(vpc_id)) + if len(igw) == 1: + return igw[0]['InternetGatewayId'] + elif len(igw) == 0: + module.fail_json(msg='No IGWs found for VPC {0}'.format(vpc_id)) else: - raise AnsibleIgwSearchException('Multiple IGWs found for VPC {0}'. - format(vpc_id)) + module.fail_json(msg='Multiple IGWs found for VPC {0}'.format(vpc_id)) -def get_resource_tags(vpc_conn, resource_id): - return dict((t.name, t.value) for t in - vpc_conn.get_all_tags(filters={'resource-id': resource_id})) +@AWSRetry.exponential_backoff() +def describe_tags_with_backoff(connection, resource_id): + filters = ansible_dict_to_boto3_filter_list({'resource-id': resource_id}) + paginator = connection.get_paginator('describe_tags') + tags = paginator.paginate(Filters=filters).build_full_result()['Tags'] + return boto3_tag_list_to_ansible_dict(tags) def tags_match(match_tags, candidate_tags): @@ -263,193 +331,190 @@ def tags_match(match_tags, candidate_tags): for k, v in match_tags.items())) -def ensure_tags(vpc_conn, resource_id, tags, add_only, check_mode): +def ensure_tags(connection=None, module=None, resource_id=None, tags=None, purge_tags=None, check_mode=None): try: - cur_tags = get_resource_tags(vpc_conn, resource_id) - if tags == cur_tags: - return {'changed': False, 'tags': cur_tags} + cur_tags = describe_tags_with_backoff(connection, resource_id) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg='Unable to list tags for VPC') - to_delete = dict((k, cur_tags[k]) for k in cur_tags if k not in tags) - if to_delete and not add_only: - vpc_conn.delete_tags(resource_id, to_delete, dry_run=check_mode) + to_add, to_delete = compare_aws_tags(cur_tags, tags, purge_tags) - to_add = dict((k, tags[k]) for k in tags if k not in cur_tags) - if to_add: - vpc_conn.create_tags(resource_id, to_add, dry_run=check_mode) + if not to_add and not to_delete: + return {'changed': False, 'tags': cur_tags} + if check_mode: + if not purge_tags: + tags = cur_tags.update(tags) + return {'changed': True, 'tags': tags} - latest_tags = get_resource_tags(vpc_conn, resource_id) - return {'changed': True, 'tags': latest_tags} - except EC2ResponseError as e: - raise AnsibleTagCreationException( - message='Unable to update tags for {0}, error: {1}'.format(resource_id, e), - error_traceback=traceback.format_exc()) + if to_delete: + try: + connection.delete_tags(Resources=[resource_id], Tags=[{'Key': k} for k in to_delete]) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't delete tags") + if to_add: + try: + connection.create_tags(Resources=[resource_id], Tags=ansible_dict_to_boto3_tag_list(to_add)) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't create tags") + + try: + latest_tags = describe_tags_with_backoff(connection, resource_id) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg='Unable to list tags for VPC') + return {'changed': True, 'tags': latest_tags} -def get_route_table_by_id(vpc_conn, vpc_id, route_table_id): +@AWSRetry.exponential_backoff() +def describe_route_tables_with_backoff(connection, **params): + try: + return connection.describe_route_tables(**params)['RouteTables'] + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] == 'InvalidRouteTableID.NotFound': + return None + else: + raise + + +def get_route_table_by_id(connection, module, route_table_id): route_table = None - route_tables = vpc_conn.get_all_route_tables(route_table_ids=[route_table_id], filters={'vpc_id': vpc_id}) + try: + route_tables = describe_route_tables_with_backoff(connection, RouteTableIds=[route_table_id]) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't get route table") if route_tables: route_table = route_tables[0] return route_table -def get_route_table_by_tags(vpc_conn, vpc_id, tags): - +def get_route_table_by_tags(connection, module, vpc_id, tags): count = 0 route_table = None - route_tables = vpc_conn.get_all_route_tables(filters={'vpc_id': vpc_id}) + filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id}) + try: + route_tables = describe_route_tables_with_backoff(connection, Filters=filters) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't get route table") for table in route_tables: - this_tags = get_resource_tags(vpc_conn, table.id) + this_tags = describe_tags_with_backoff(connection, table['RouteTableId']) if tags_match(tags, this_tags): route_table = table count += 1 if count > 1: - raise RuntimeError("Tags provided do not identify a unique route table") + module.fail_json(msg="Tags provided do not identify a unique route table") else: return route_table def route_spec_matches_route(route_spec, route): - key_attr_map = { - 'destination_cidr_block': 'destination_cidr_block', - 'gateway_id': 'gateway_id', - 'instance_id': 'instance_id', - 'interface_id': 'interface_id', - 'vpc_peering_connection_id': 'vpc_peering_connection_id', - } + if route_spec.get('GatewayId') and 'nat-' in route_spec['GatewayId']: + route_spec['NatGatewayId'] = route_spec.pop('GatewayId') - # This is a workaround to catch managed NAT gateways as they do not show - # up in any of the returned values when describing route tables. - # The caveat of doing it this way is that if there was an existing - # route for another nat gateway in this route table there is not a way to - # change to another nat gateway id. Long term solution would be to utilise - # boto3 which is a very big task for this module or to update boto. - if route_spec.get('gateway_id') and 'nat-' in route_spec['gateway_id']: - if route.destination_cidr_block == route_spec['destination_cidr_block']: - if all((not route.gateway_id, not route.instance_id, not route.interface_id, not route.vpc_peering_connection_id)): - return True - - for k in key_attr_map: - if k in route_spec: - if route_spec[k] != getattr(route, k): - return False - return True + return set(route_spec.items()).issubset(route.items()) def route_spec_matches_route_cidr(route_spec, route): - cidr_attr = 'destination_cidr_block' - return route_spec[cidr_attr] == getattr(route, cidr_attr) + return route_spec['DestinationCidrBlock'] == route['DestinationCidrBlock'] def rename_key(d, old_key, new_key): - d[new_key] = d[old_key] - del d[old_key] + d[new_key] = d.pop(old_key) def index_of_matching_route(route_spec, routes_to_match): for i, route in enumerate(routes_to_match): if route_spec_matches_route(route_spec, route): - return i + return "exact", i elif route_spec_matches_route_cidr(route_spec, route): - return "replace" + return "replace", i -def ensure_routes(vpc_conn, route_table, route_specs, propagating_vgw_ids, - check_mode, purge_routes): - routes_to_match = list(route_table.routes) +def ensure_routes(connection=None, module=None, route_table=None, route_specs=None, + propagating_vgw_ids=None, check_mode=None, purge_routes=None): + routes_to_match = [route for route in route_table['Routes']] route_specs_to_create = [] route_specs_to_recreate = [] for route_spec in route_specs: - i = index_of_matching_route(route_spec, routes_to_match) - if i is None: + match = index_of_matching_route(route_spec, routes_to_match) + if match is None: route_specs_to_create.append(route_spec) - elif i == "replace": - route_specs_to_recreate.append(route_spec) else: - del routes_to_match[i] + if match[0] == "replace": + route_specs_to_recreate.append(route_spec) + del routes_to_match[match[1]] - # NOTE: As of boto==2.38.0, the origin of a route is not available - # (for example, whether it came from a gateway with route propagation - # enabled). Testing for origin == 'EnableVgwRoutePropagation' is more - # correct than checking whether the route uses a propagating VGW. - # The current logic will leave non-propagated routes using propagating - # VGWs in place. routes_to_delete = [] if purge_routes: for r in routes_to_match: - if r.gateway_id: - if r.gateway_id != 'local' and not r.gateway_id.startswith('vpce-'): - if not propagating_vgw_ids or r.gateway_id not in propagating_vgw_ids: - routes_to_delete.append(r) - else: + if r['Origin'] == 'CreateRoute': routes_to_delete.append(r) changed = bool(routes_to_delete or route_specs_to_create or route_specs_to_recreate) - if changed: + if changed and not check_mode: for route in routes_to_delete: try: - vpc_conn.delete_route(route_table.id, - route.destination_cidr_block, - dry_run=check_mode) - except EC2ResponseError as e: - if e.error_code == 'DryRunOperation': - pass - else: - raise + connection.delete_route(RouteTableId=route_table['RouteTableId'], DestinationCidrBlock=route['DestinationCidrBlock']) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't delete route") + + for route_spec in route_specs_to_recreate: + try: + connection.replace_route(RouteTableId=route_table['RouteTableId'], + **route_spec) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't recreate route") for route_spec in route_specs_to_create: try: - vpc_conn.create_route(route_table.id, - dry_run=check_mode, - **route_spec) - except EC2ResponseError as e: - if e.error_code == 'DryRunOperation': - pass - else: - raise - - for route_spec in route_specs_to_recreate: - if not check_mode: - vpc_conn.replace_route(route_table.id, - **route_spec) + connection.create_route(RouteTableId=route_table['RouteTableId'], + **route_spec) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't create route") return {'changed': bool(changed)} -def ensure_subnet_association(vpc_conn, vpc_id, route_table_id, subnet_id, - check_mode): - route_tables = vpc_conn.get_all_route_tables( - filters={'association.subnet_id': subnet_id, 'vpc_id': vpc_id} - ) +def ensure_subnet_association(connection=None, module=None, vpc_id=None, route_table_id=None, subnet_id=None, + check_mode=None): + filters = ansible_dict_to_boto3_filter_list({'association.subnet-id': subnet_id, 'vpc-id': vpc_id}) + try: + route_tables = describe_route_tables_with_backoff(connection, Filters=filters) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't get route tables") for route_table in route_tables: - if route_table.id is None: + if route_table['RouteTableId'] is None: continue - for a in route_table.associations: - if a.main: + for a in route_table['Associations']: + if a['Main']: continue - if a.subnet_id == subnet_id: - if route_table.id == route_table_id: - return {'changed': False, 'association_id': a.id} + if a['SubnetId'] == subnet_id: + if route_table['RouteTableId'] == route_table_id: + return {'changed': False, 'association_id': a['RouteTableAssociationId']} else: if check_mode: return {'changed': True} - vpc_conn.disassociate_route_table(a.id) + try: + connection.disassociate_route_table(AssociationId=a['RouteTableAssociationId']) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't disassociate subnet from route table") - association_id = vpc_conn.associate_route_table(route_table_id, subnet_id) + try: + association_id = connection.associate_route_table(RouteTableId=route_table_id, SubnetId=subnet_id) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't associate subnet with route table") return {'changed': True, 'association_id': association_id} -def ensure_subnet_associations(vpc_conn, vpc_id, route_table, subnets, - check_mode, purge_subnets): - current_association_ids = [a.id for a in route_table.associations if not a.main] +def ensure_subnet_associations(connection=None, module=None, route_table=None, subnets=None, + check_mode=None, purge_subnets=None): + current_association_ids = [a['RouteTableAssociationId'] for a in route_table['Associations'] if not a['Main']] new_association_ids = [] changed = False for subnet in subnets: - result = ensure_subnet_association( - vpc_conn, vpc_id, route_table.id, subnet.id, check_mode) + result = ensure_subnet_association(connection=connection, module=module, vpc_id=route_table['VpcId'], + route_table_id=route_table['RouteTableId'], subnet_id=subnet['SubnetId'], check_mode=check_mode) changed = changed or result['changed'] if changed and check_mode: return {'changed': True} @@ -461,29 +526,29 @@ def ensure_subnet_associations(vpc_conn, vpc_id, route_table, subnets, for a_id in to_delete: changed = True - vpc_conn.disassociate_route_table(a_id, dry_run=check_mode) + if not check_mode: + try: + connection.disassociate_route_table(AssociationId=a_id) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't disassociate subnet from route table") return {'changed': changed} -def ensure_propagation(vpc_conn, route_table, propagating_vgw_ids, - check_mode): - - # NOTE: As of boto==2.38.0, it is not yet possible to query the existing - # propagating gateways. However, EC2 does support this as shown in its API - # documentation. For now, a reasonable proxy for this is the presence of - # propagated routes using the gateway in the route table. If such a route - # is found, propagation is almost certainly enabled. +def ensure_propagation(connection=None, module=None, route_table=None, propagating_vgw_ids=None, + check_mode=None): changed = False - for vgw_id in propagating_vgw_ids: - for r in list(route_table.routes): - if r.gateway_id == vgw_id: - return {'changed': False} - + gateways = [gateway['GatewayId'] for gateway in route_table['PropagatingVgws']] + to_add = set(propagating_vgw_ids) - set(gateways) + if to_add: changed = True - vpc_conn.enable_vgw_route_propagation(route_table.id, - vgw_id, - dry_run=check_mode) + if not check_mode: + for vgw_id in to_add: + try: + connection.enable_vgw_route_propagation(RouteTableId=route_table['RouteTableId'], + GatewayId=vgw_id) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't enable route propagation") return {'changed': changed} @@ -498,52 +563,37 @@ def ensure_route_table_absent(connection, module): if lookup == 'tag': if tags is not None: - try: - route_table = get_route_table_by_tags(connection, vpc_id, tags) - except EC2ResponseError as e: - module.fail_json(msg="Error finding route table with lookup 'tag': {0}".format(e.message), - exception=traceback.format_exc()) - except RuntimeError as e: - module.fail_json(msg=e.args[0], exception=traceback.format_exc()) + route_table = get_route_table_by_tags(connection, module, vpc_id, tags) else: route_table = None elif lookup == 'id': - try: - route_table = get_route_table_by_id(connection, vpc_id, route_table_id) - except EC2ResponseError as e: - module.fail_json(msg="Error finding route table with lookup 'id': {0}".format(e.message), - exception=traceback.format_exc()) + route_table = get_route_table_by_id(connection, module, route_table_id) if route_table is None: return {'changed': False} # disassociate subnets before deleting route table - ensure_subnet_associations(connection, vpc_id, route_table, [], module.check_mode, purge_subnets) - try: - connection.delete_route_table(route_table.id, dry_run=module.check_mode) - except EC2ResponseError as e: - if e.error_code == 'DryRunOperation': - pass - else: - module.fail_json(msg="Error deleting route table: {0}".format(e.message), - exception=traceback.format_exc()) + if not module.check_mode: + ensure_subnet_associations(connection=connection, module=module, route_table=route_table, + subnets=[], check_mode=False, purge_subnets=purge_subnets) + try: + connection.delete_route_table(RouteTableId=route_table['RouteTableId']) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Error deleting route table") return {'changed': True} -def get_route_table_info(route_table): - - # Add any routes to array - routes = [] - for route in route_table.routes: - routes.append(route.__dict__) - - route_table_info = {'id': route_table.id, - 'routes': routes, - 'tags': route_table.tags, - 'vpc_id': route_table.vpc_id} - - return route_table_info +def get_route_table_info(connection, module, route_table): + result = get_route_table_by_id(connection, module, route_table['RouteTableId']) + try: + result['Tags'] = describe_tags_with_backoff(connection, route_table['RouteTableId']) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't get tags for route table") + result = camel_dict_to_snake_dict(result, ignore_list=['Tags']) + # backwards compatibility + result['id'] = result['route_table_id'] + return result def create_route_spec(connection, module, vpc_id): @@ -553,10 +603,12 @@ def create_route_spec(connection, module, vpc_id): rename_key(route_spec, 'dest', 'destination_cidr_block') if route_spec.get('gateway_id') and route_spec['gateway_id'].lower() == 'igw': - igw = find_igw(connection, vpc_id) + igw = find_igw(connection, module, vpc_id) route_spec['gateway_id'] = igw + if route_spec.get('gateway_id') and route_spec['gateway_id'].startswith('nat-'): + rename_key(route_spec, 'gateway_id', 'nat_gateway_id') - return routes + return snake_dict_to_camel_dict(routes, capitalize_first=True) def ensure_route_table_present(connection, module): @@ -565,15 +617,12 @@ def ensure_route_table_present(connection, module): propagating_vgw_ids = module.params.get('propagating_vgw_ids') purge_routes = module.params.get('purge_routes') purge_subnets = module.params.get('purge_subnets') + purge_tags = module.params.get('purge_tags') route_table_id = module.params.get('route_table_id') subnets = module.params.get('subnets') tags = module.params.get('tags') vpc_id = module.params.get('vpc_id') - try: - routes = create_route_spec(connection, module, vpc_id) - except AnsibleIgwSearchException as e: - module.fail_json(msg="Failed to find the Internet gateway for the given VPC ID {0}: {1}".format(vpc_id, e[0]), - exception=traceback.format_exc()) + routes = create_route_spec(connection, module, vpc_id) changed = False tags_valid = False @@ -581,129 +630,92 @@ def ensure_route_table_present(connection, module): if lookup == 'tag': if tags is not None: try: - route_table = get_route_table_by_tags(connection, vpc_id, tags) - except EC2ResponseError as e: - module.fail_json(msg="Error finding route table with lookup 'tag': {0}".format(e.message), - exception=traceback.format_exc()) - except RuntimeError as e: - module.fail_json(msg=e.args[0], exception=traceback.format_exc()) + route_table = get_route_table_by_tags(connection, module, vpc_id, tags) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Error finding route table with lookup 'tag'") else: route_table = None elif lookup == 'id': try: - route_table = get_route_table_by_id(connection, vpc_id, route_table_id) - except EC2ResponseError as e: - module.fail_json(msg="Error finding route table with lookup 'id': {0}".format(e.message), - exception=traceback.format_exc()) + route_table = get_route_table_by_id(connection, module, route_table_id) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Error finding route table with lookup 'id'") # If no route table returned then create new route table if route_table is None: - try: - route_table = connection.create_route_table(vpc_id, module.check_mode) - changed = True - except EC2ResponseError as e: - if e.error_code == 'DryRunOperation': - module.exit_json(changed=True) - module.fail_json(msg="Failed to create route table: {0}".format(e.message), - exception=traceback.format_exc()) + changed = True + if not module.check_mode: + try: + route_table = connection.create_route_table(VpcId=vpc_id)['RouteTable'] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Error creating route table") + else: + route_table = {"id": "rtb-xxxxxxxx", "route_table_id": "rtb-xxxxxxxx", "vpc_id": vpc_id} + module.exit_json(changed=changed, route_table=route_table) if routes is not None: - try: - result = ensure_routes(connection, route_table, routes, - propagating_vgw_ids, module.check_mode, - purge_routes) - changed = changed or result['changed'] - except EC2ResponseError as e: - module.fail_json(msg="Error while updating routes: {0}".format(e.message), - exception=traceback.format_exc()) + result = ensure_routes(connection=connection, module=module, route_table=route_table, + route_specs=routes, propagating_vgw_ids=propagating_vgw_ids, + check_mode=module.check_mode, purge_routes=purge_routes) + changed = changed or result['changed'] if propagating_vgw_ids is not None: - result = ensure_propagation(connection, route_table, - propagating_vgw_ids, - check_mode=module.check_mode) + result = ensure_propagation(connection=connection, module=module, route_table=route_table, + propagating_vgw_ids=propagating_vgw_ids, check_mode=module.check_mode) changed = changed or result['changed'] if not tags_valid and tags is not None: - result = ensure_tags(connection, route_table.id, tags, - add_only=True, check_mode=module.check_mode) - route_table.tags = result['tags'] + result = ensure_tags(connection=connection, module=module, resource_id=route_table['RouteTableId'], tags=tags, + purge_tags=purge_tags, check_mode=module.check_mode) + route_table['Tags'] = result['tags'] changed = changed or result['changed'] - if subnets: - associated_subnets = [] - try: - associated_subnets = find_subnets(connection, vpc_id, subnets) - except EC2ResponseError as e: - raise AnsibleRouteTableException( - message='Unable to find subnets for route table {0}, error: {1}' - .format(route_table, e), - error_traceback=traceback.format_exc() - ) + if subnets is not None: + associated_subnets = find_subnets(connection, module, vpc_id, subnets) - try: - result = ensure_subnet_associations(connection, vpc_id, route_table, - associated_subnets, - module.check_mode, - purge_subnets) - changed = changed or result['changed'] - except EC2ResponseError as e: - raise AnsibleRouteTableException( - message='Unable to associate subnets for route table {0}, error: {1}' - .format(route_table, e), - error_traceback=traceback.format_exc() - ) + result = ensure_subnet_associations(connection=connection, module=module, route_table=route_table, + subnets=associated_subnets, check_mode=module.check_mode, + purge_subnets=purge_subnets) + changed = changed or result['changed'] - module.exit_json(changed=changed, route_table=get_route_table_info(route_table)) + module.exit_json(changed=changed, route_table=get_route_table_info(connection, module, route_table)) def main(): argument_spec = ec2_argument_spec() argument_spec.update( dict( - lookup=dict(default='tag', required=False, choices=['tag', 'id']), - propagating_vgw_ids=dict(default=None, required=False, type='list'), + lookup=dict(default='tag', choices=['tag', 'id']), + propagating_vgw_ids=dict(type='list'), purge_routes=dict(default=True, type='bool'), purge_subnets=dict(default=True, type='bool'), - route_table_id=dict(default=None, required=False), - routes=dict(default=[], required=False, type='list'), + purge_tags=dict(default=False, type='bool'), + route_table_id=dict(), + routes=dict(default=[], type='list'), state=dict(default='present', choices=['present', 'absent']), - subnets=dict(default=None, required=False, type='list'), - tags=dict(default=None, required=False, type='dict', aliases=['resource_tags']), - vpc_id=dict(default=None, required=True) + subnets=dict(type='list'), + tags=dict(type='dict', aliases=['resource_tags']), + vpc_id=dict() ) ) - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + module = AnsibleAWSModule(argument_spec=argument_spec, + required_if=[['lookup', 'id', ['route_table_id']], + ['lookup', 'tag', ['vpc_id']], + ['state', 'present', ['vpc_id']]], + supports_check_mode=True) - if not HAS_BOTO: - module.fail_json(msg='boto is required for this module') + region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - region, ec2_url, aws_connect_params = get_aws_connection_info(module) + connection = boto3_conn(module, conn_type='client', resource='ec2', + region=region, endpoint=ec2_url, **aws_connect_params) - if region: - try: - connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - module.fail_json(msg=str(e)) - else: - module.fail_json(msg="region must be specified") + state = module.params.get('state') - lookup = module.params.get('lookup') - route_table_id = module.params.get('route_table_id') - state = module.params.get('state', 'present') - - if lookup == 'id' and route_table_id is None: - module.fail_json(msg="You must specify route_table_id if lookup is set to id") - - try: - if state == 'present': - result = ensure_route_table_present(connection, module) - elif state == 'absent': - result = ensure_route_table_absent(connection, module) - except AnsibleRouteTableException as e: - if e.error_traceback: - module.fail_json(msg=e.message, exception=e.error_traceback) - module.fail_json(msg=e.message) + if state == 'present': + result = ensure_route_table_present(connection, module) + elif state == 'absent': + result = ensure_route_table_absent(connection, module) module.exit_json(**result) diff --git a/test/integration/targets/ec2_vpc_route_table/tasks/main.yml b/test/integration/targets/ec2_vpc_route_table/tasks/main.yml index 6e5fa8ef6b4..ca663424a35 100644 --- a/test/integration/targets/ec2_vpc_route_table/tasks/main.yml +++ b/test/integration/targets/ec2_vpc_route_table/tasks/main.yml @@ -53,7 +53,6 @@ vpc_id: "{{ vpc.vpc.id }}" <<: *aws_connection_info - - name: create NAT GW ec2_vpc_nat_gateway: if_exist_do_not_create: yes @@ -62,6 +61,20 @@ <<: *aws_connection_info register: nat_gateway + - name: CHECK MODE - route table should be created + ec2_vpc_route_table: + vpc_id: "{{ vpc.vpc.id }}" + tags: + Public: "true" + Name: "Public route table" + <<: *aws_connection_info + check_mode: true + register: check_mode_results + + - name: assert that the public route table would be created + assert: + that: + - check_mode_results.changed - name: create public route table ec2_vpc_route_table: @@ -75,9 +88,26 @@ - name: assert that public route table has an id assert: that: - - create_public_table.changed + # - create_public_table.changed - "create_public_table.route_table.id.startswith('rtb-')" - "'Public' in create_public_table.route_table.tags and create_public_table.route_table.tags['Public'] == 'true'" + - create_public_table.route_table.routes|length == 1 + - create_public_table.route_table.associations|length == 0 + + - name: CHECK MODE - route table should already exist + ec2_vpc_route_table: + vpc_id: "{{ vpc.vpc.id }}" + tags: + Public: "true" + Name: "Public route table" + <<: *aws_connection_info + check_mode: True + register: check_mode_results + + - name: assert the table already exists + assert: + that: + - not check_mode_results.changed - name: recreate public route table ec2_vpc_route_table: @@ -93,6 +123,24 @@ that: - not recreate_public_route_table.changed + - name: CHECK MODE - add route to public route table + ec2_vpc_route_table: + vpc_id: "{{ vpc.vpc.id }}" + tags: + Public: "true" + Name: "Public route table" + routes: + - dest: 0.0.0.0/0 + gateway_id: igw + <<: *aws_connection_info + check_mode: True + register: check_mode_results + + - name: assert a route would be added + assert: + that: + - check_mode_results.changed + - name: add a route to public route table ec2_vpc_route_table: vpc_id: "{{ vpc.vpc.id }}" @@ -105,6 +153,31 @@ <<: *aws_connection_info register: add_routes + - name: assert route table contains new route + assert: + that: + - add_routes.changed + - add_routes.route_table.routes|length == 2 + + - name: CHECK MODE - add subnets to public route table + ec2_vpc_route_table: + vpc_id: "{{ vpc.vpc.id }}" + tags: + Public: "true" + Name: "Public route table" + routes: + - dest: 0.0.0.0/0 + gateway_id: igw + subnets: "{{ vpc_subnets|json_query('subnets[?tags.Public == `True`].id') }}" + <<: *aws_connection_info + check_mode: True + register: check_mode_results + + - name: assert the subnets would be added to the route table + assert: + that: + - check_mode_results.changed + - name: add subnets to public route table ec2_vpc_route_table: vpc_id: "{{ vpc.vpc.id }}" @@ -118,6 +191,11 @@ <<: *aws_connection_info register: add_subnets + - name: assert route table contains subnets + assert: + that: + - add_subnets.changed + - add_subnets.route_table.associations|length == 2 - name: add a route to public route table ec2_vpc_route_table: @@ -131,6 +209,23 @@ <<: *aws_connection_info register: add_routes + - name: CHECK MODE - no routes but purge_routes set to false + ec2_vpc_route_table: + vpc_id: "{{ vpc.vpc.id }}" + tags: + Public: "true" + Name: "Public route table" + purge_routes: no + subnets: "{{ vpc_subnets|json_query('subnets[?tags.Public == `True`].id') }}" + <<: *aws_connection_info + check_mode: True + register: check_mode_results + + - name: assert no routes would be removed + assert: + that: + - not check_mode_results.changed + - name: rerun with purge_routes set to false ec2_vpc_route_table: vpc_id: "{{ vpc.vpc.id }}" @@ -147,7 +242,7 @@ that: - not no_purge_routes.changed - no_purge_routes.route_table.routes|length == 2 - # FIXME: - no_purge_routes.route_table.associations|length == 2 + - no_purge_routes.route_table.associations|length == 2 - name: rerun with purge_subnets set to false ec2_vpc_route_table: @@ -158,51 +253,89 @@ purge_subnets: no routes: - dest: 0.0.0.0/0 + gateway_id: igw <<: *aws_connection_info register: no_purge_subnets - name: assert route table still has subnets assert: that: - # FIXME: - not no_purge_subnets.changed + - not no_purge_subnets.changed - no_purge_subnets.route_table.routes|length == 2 - # FIXME: - no_purge_subnets.route_table.associations|length == 2 + - no_purge_subnets.route_table.associations|length == 2 -# FIXME: purge_tags doesn't exist yet -# -# - name: rerun with purge_tags not set (implicitly false) -# ec2_vpc_route_table: -# vpc_id: "{{ vpc.vpc.id }}" -# routes: -# - dest: 0.0.0.0/0 -# subnets: "{{ vpc_subnets|json_query('subnets[?tags.Public == `True`].id') }}" -# <<: *aws_connection_info -# register: no_purge_tags -# -# - name: assert route table still has tags -# assert: -# that: -# - not no_purge_tags.changed -# - "'Public' in no_purge_tags.route_table.tags and no_purge_tags.route_table.tags['Public'] == 'true'" + - name: rerun with purge_tags not set (implicitly false) + ec2_vpc_route_table: + vpc_id: "{{ vpc.vpc.id }}" + routes: + - dest: 0.0.0.0/0 + gateway_id: igw + lookup: id + route_table_id: "{{ create_public_table.route_table.id }}" + subnets: "{{ vpc_subnets|json_query('subnets[?tags.Public == `True`].id') }}" + <<: *aws_connection_info + register: no_purge_tags + + - name: assert route table still has tags + assert: + that: + - not no_purge_tags.changed + - "'Public' in no_purge_tags.route_table.tags and no_purge_tags.route_table.tags['Public'] == 'true'" + + - name: CHECK MODE - purge subnets + ec2_vpc_route_table: + vpc_id: "{{ vpc.vpc.id }}" + routes: + - dest: 0.0.0.0/0 + gateway_id: igw + subnets: [] + tags: + Public: "true" + Name: "Public route table" + <<: *aws_connection_info + check_mode: True + register: check_mode_results + + - name: assert subnets would be removed + assert: + that: + - check_mode_results.changed - name: purge subnets ec2_vpc_route_table: vpc_id: "{{ vpc.vpc.id }}" routes: - dest: 0.0.0.0/0 + gateway_id: igw + subnets: [] tags: Public: "true" Name: "Public route table" <<: *aws_connection_info register: purge_subnets -# FIXME: this doesn't currently work but with no associations present difficult to see why not -# - name: assert purge subnets worked -# assert: -# that: -# - purge_subnets.changed -# # FIXME: - purge_subnets.route_table.associations|length == 0 -# - purge_subnets.route_table.id == create_public_table.route_table.id + - name: assert purge subnets worked + assert: + that: + - purge_subnets.changed + - purge_subnets.route_table.associations|length == 0 + - purge_subnets.route_table.id == create_public_table.route_table.id + + - name: CHECK MODE - purge routes + ec2_vpc_route_table: + vpc_id: "{{ vpc.vpc.id }}" + tags: + Public: "true" + Name: "Public route table" + <<: *aws_connection_info + routes: [] + check_mode: True + register: check_mode_results + + - name: assert routes would be removed + assert: + that: + - check_mode_results.changed - name: purge routes ec2_vpc_route_table: @@ -211,24 +344,42 @@ Public: "true" Name: "Public route table" <<: *aws_connection_info + routes: [] register: purge_routes - name: assert purge routes worked assert: that: - purge_routes.changed - # FIXME: purge_routes does work but the result is not up to date and returns - # the route - a wait period might help - # - purge_routes.route_table.routes|length == 1 + - purge_routes.route_table.routes|length == 1 - purge_routes.route_table.id == create_public_table.route_table.id + - name: CHECK MODE - update tags + ec2_vpc_route_table: + vpc_id: "{{ vpc.vpc.id }}" + route_table_id: "{{ create_public_table.route_table.id }}" + lookup: id + purge_tags: yes + tags: + Name: Public route table + Updated: new_tag + <<: *aws_connection_info + check_mode: True + register: check_mode_results + + - name: assert tags would be changed + assert: + that: + - check_mode_results.changed + - name: update tags ec2_vpc_route_table: vpc_id: "{{ vpc.vpc.id }}" route_table_id: "{{ create_public_table.route_table.id }}" lookup: id - # FIXME: purge_tags: yes + purge_tags: yes tags: + Name: Public route table Updated: new_tag <<: *aws_connection_info register: update_tags @@ -238,14 +389,41 @@ that: - update_tags.changed - "'Updated' in update_tags.route_table.tags and update_tags.route_table.tags['Updated'] == 'new_tag'" - # FIXME: - "'Public' not in update_tags.route_table.tags" + - "'Public' not in update_tags.route_table.tags" + + - name: create NAT GW + ec2_vpc_nat_gateway: + if_exist_do_not_create: yes + wait: yes + subnet_id: "{{ subnets.results[0].subnet.id }}" + <<: *aws_connection_info + register: nat_gateway + + - name: CHECK MODE - create private route table + ec2_vpc_route_table: + vpc_id: "{{ vpc.vpc.id }}" + tags: + Public: "false" + Name: "Private route table" + routes: + - gateway_id: "{{ nat_gateway.nat_gateway_id }}" + dest: 0.0.0.0/0 + subnets: "{{ vpc_subnets|json_query('subnets[?tags.Public == `False`].id') }}" + <<: *aws_connection_info + check_mode: True + register: check_mode_results + + - name: assert the route table would be created + assert: + that: + - check_mode_results.changed - name: create private route table ec2_vpc_route_table: vpc_id: "{{ vpc.vpc.id }}" tags: - Public: no - Name: private route table + Public: "false" + Name: "Private route table" routes: - gateway_id: "{{ nat_gateway.nat_gateway_id }}" dest: 0.0.0.0/0 @@ -260,12 +438,29 @@ - create_private_table.route_table.id != create_public_table.route_table.id - "'Public' in create_private_table.route_table.tags" - - name: destroy public route table + - name: CHECK MODE - destroy public route table by tags ec2_vpc_route_table: - route_table_id: "{{ create_public_table.route_table.id }}" - lookup: id - vpc_id: "{{ vpc.vpc.id }}" # FIXME: why is this required? + vpc_id: "{{ vpc.vpc.id }}" state: absent + tags: + Updated: new_tag + Name: Public route table + <<: *aws_connection_info + check_mode: True + register: check_mode_results + + - name: assert the route table would be deleted + assert: + that: + check_mode_results.changed + + - name: destroy public route table by tags + ec2_vpc_route_table: + vpc_id: "{{ vpc.vpc.id }}" + state: absent + tags: + Updated: new_tag + Name: Public route table <<: *aws_connection_info register: destroy_table @@ -274,46 +469,89 @@ that: - destroy_table.changed -# FIXME: this currently throws an exception -# - name: redestroy public route table -# ec2_vpc_route_table: -# route_table_id: "{{ create_public_table.route_table.id }}" -# lookup: id -# state: absent -# <<: *aws_connection_info -# register: redestroy_table -# -# - name: assert redestroy table worked -# assert: -# that: -# - not redestroy_table.changed + - name: CHECK MODE - redestroy public route table + ec2_vpc_route_table: + route_table_id: "{{ create_public_table.route_table.id }}" + lookup: id + state: absent + <<: *aws_connection_info + check_mode: True + register: check_mode_results -# FIXME: After boto3 port, test updating NAT gateway -# -# - name: destroy NAT GW -# ec2_vpc_nat_gateway: -# vpc_id: "{{ vpc.vpc.id }}" -# state: absent -# wait: yes -# release_eip: yes -# <<: *aws_connection_info -# register: nat_gateway -# -# - name: create NAT GW -# ec2_vpc_nat_gateway: -# vpc_id: "{{ vpc.vpc.id }}" -# if_exist_do_not_create: yes -# <<: *aws_connection_info -# register: nat_gateway + - name: assert the public route table does not exist + assert: + that: + - not check_mode_results.changed - always: + - name: redestroy public route table + ec2_vpc_route_table: + route_table_id: "{{ create_public_table.route_table.id }}" + lookup: id + state: absent + <<: *aws_connection_info + register: redestroy_table + + - name: assert redestroy table worked + assert: + that: + - not redestroy_table.changed + + - name: destroy NAT GW + ec2_vpc_nat_gateway: + state: absent + wait: yes + release_eip: yes + subnet_id: "{{ subnets.results[0].subnet.id }}" + nat_gateway_id: "{{ nat_gateway.nat_gateway_id }}" + <<: *aws_connection_info + register: nat_gateway + + - name: show route table facts + ec2_vpc_route_table_facts: + filters: + route-table-id: "{{ create_private_table.route_table.id }}" + <<: *aws_connection_info + + - name: create NAT GW + ec2_vpc_nat_gateway: + if_exist_do_not_create: yes + wait: yes + subnet_id: "{{ subnets.results[0].subnet.id }}" + <<: *aws_connection_info + register: nat_gateway + + - name: show route table facts + ec2_vpc_route_table_facts: + filters: + route-table-id: "{{ create_private_table.route_table.id }}" + <<: *aws_connection_info + + - name: recreate private route table with new NAT GW + ec2_vpc_route_table: + vpc_id: "{{ vpc.vpc.id }}" + tags: + Public: "false" + Name: "Private route table" + routes: + - nat_gateway_id: "{{ nat_gateway.nat_gateway_id }}" + dest: 0.0.0.0/0 + subnets: "{{ vpc_subnets|json_query('subnets[?tags.Public == `False`].id') }}" + <<: *aws_connection_info + register: recreate_private_table + + - name: assert creating private route table worked + assert: + that: + - recreate_private_table.changed + - recreate_private_table.route_table.id != create_public_table.route_table.id + +- always: ############################################################################# # TEAR DOWN STARTS HERE ############################################################################# - name: destroy route tables ec2_vpc_route_table: route_table_id: "{{ item.route_table.id }}" - vpc_id: "{{ vpc.vpc.id }}" # FIXME: why is this required? lookup: id state: absent <<: *aws_connection_info diff --git a/test/units/module_utils/ec2/test_camel_to_snake.py b/test/units/module_utils/ec2/test_camel_to_snake.py index 438e02ba15c..7dc1e25cffe 100644 --- a/test/units/module_utils/ec2/test_camel_to_snake.py +++ b/test/units/module_utils/ec2/test_camel_to_snake.py @@ -17,7 +17,7 @@ # along with Ansible. If not, see . from ansible.compat.tests import unittest -from ansible.module_utils.ec2 import _camel_to_snake, _snake_to_camel +from ansible.module_utils.ec2 import _camel_to_snake, _snake_to_camel, camel_dict_to_snake_dict EXPECTED_SNAKIFICATION = { 'alllower': 'alllower', @@ -61,3 +61,11 @@ class CamelToSnakeAndBackTestCase(unittest.TestCase): def test_camel_to_snake_and_back(self): for (k, v) in EXPECTED_REVERSIBLE.items(): self.assertEqual(_snake_to_camel(_camel_to_snake(k, reversible=True), capitalize_first=True), k) + + +class CamelDictToSnakeDictTestCase(unittest.TestCase): + def test_ignore_list(self): + camel_dict = dict(Hello=dict(One='one', Two='two'), World=dict(Three='three', Four='four')) + snake_dict = camel_dict_to_snake_dict(camel_dict, ignore_list='World') + self.assertEqual(snake_dict['hello'], dict(one='one', two='two')) + self.assertEqual(snake_dict['world'], dict(Three='three', Four='four'))