1#!/usr/bin/python
2#
3# This is a free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This Ansible library is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this library.  If not, see <http://www.gnu.org/licenses/>.
15
16ANSIBLE_METADATA = {'metadata_version': '1.1',
17                    'status': ['stableinterface'],
18                    'supported_by': 'community'}
19
20
21DOCUMENTATION = '''
22---
23module: ec2_vpc_route_table
24short_description: Manage route tables for AWS virtual private clouds
25description:
26    - Manage route tables for AWS virtual private clouds
27version_added: "2.0"
28author:
29- Robert Estelle (@erydo)
30- Rob White (@wimnat)
31- Will Thames (@willthames)
32options:
33  lookup:
34    description: Look up route table by either tags or by route table ID. Non-unique tag lookup will fail.
35      If no tags are specified then no lookup for an existing route table is performed and a new
36      route table will be created. To change tags of a route table you must look up by id.
37    default: tag
38    choices: [ 'tag', 'id' ]
39  propagating_vgw_ids:
40    description: Enable route propagation from virtual gateways specified by ID.
41  purge_routes:
42    version_added: "2.3"
43    description: Purge existing routes that are not found in routes.
44    type: bool
45    default: 'yes'
46  purge_subnets:
47    version_added: "2.3"
48    description: Purge existing subnets that are not found in subnets. Ignored unless the subnets option is supplied.
49    default: 'true'
50    type: bool
51  purge_tags:
52    version_added: "2.5"
53    description: Purge existing tags that are not found in route table
54    type: bool
55    default: 'no'
56  route_table_id:
57    description: The ID of the route table to update or delete.
58  routes:
59    description: List of routes in the route table.
60        Routes are specified as dicts containing the keys 'dest' and one of 'gateway_id',
61        'instance_id', 'network_interface_id', or 'vpc_peering_connection_id'.
62        If 'gateway_id' is specified, you can refer to the VPC's IGW by using the value 'igw'.
63        Routes are required for present states.
64  state:
65    description: Create or destroy the VPC route table
66    default: present
67    choices: [ 'present', 'absent' ]
68  subnets:
69    description: An array of subnets to add to this route table. Subnets may be specified
70      by either subnet ID, Name tag, or by a CIDR such as '10.0.0.0/24'.
71  tags:
72    description: >
73      A dictionary of resource tags of the form: { tag1: value1, tag2: value2 }. Tags are
74      used to uniquely identify route tables within a VPC when the route_table_id is not supplied.
75    aliases: [ "resource_tags" ]
76  vpc_id:
77    description: VPC ID of the VPC in which to create the route table.
78    required: true
79extends_documentation_fragment:
80    - aws
81    - ec2
82'''
83
84EXAMPLES = '''
85# Note: These examples do not set authentication details, see the AWS Guide for details.
86
87# Basic creation example:
88- name: Set up public subnet route table
89  ec2_vpc_route_table:
90    vpc_id: vpc-1245678
91    region: us-west-1
92    tags:
93      Name: Public
94    subnets:
95      - "{{ jumpbox_subnet.subnet.id }}"
96      - "{{ frontend_subnet.subnet.id }}"
97      - "{{ vpn_subnet.subnet_id }}"
98    routes:
99      - dest: 0.0.0.0/0
100        gateway_id: "{{ igw.gateway_id }}"
101  register: public_route_table
102
103- name: Set up NAT-protected route table
104  ec2_vpc_route_table:
105    vpc_id: vpc-1245678
106    region: us-west-1
107    tags:
108      Name: Internal
109    subnets:
110      - "{{ application_subnet.subnet.id }}"
111      - 'Database Subnet'
112      - '10.0.0.0/8'
113    routes:
114      - dest: 0.0.0.0/0
115        instance_id: "{{ nat.instance_id }}"
116  register: nat_route_table
117
118- name: delete route table
119  ec2_vpc_route_table:
120    vpc_id: vpc-1245678
121    region: us-west-1
122    route_table_id: "{{ route_table.id }}"
123    lookup: id
124    state: absent
125'''
126
127RETURN = '''
128route_table:
129  description: Route Table result
130  returned: always
131  type: complex
132  contains:
133    associations:
134      description: List of subnets associated with the route table
135      returned: always
136      type: complex
137      contains:
138        main:
139          description: Whether this is the main route table
140          returned: always
141          type: bool
142          sample: false
143        route_table_association_id:
144          description: ID of association between route table and subnet
145          returned: always
146          type: str
147          sample: rtbassoc-ab47cfc3
148        route_table_id:
149          description: ID of the route table
150          returned: always
151          type: str
152          sample: rtb-bf779ed7
153        subnet_id:
154          description: ID of the subnet
155          returned: always
156          type: str
157          sample: subnet-82055af9
158    id:
159      description: ID of the route table (same as route_table_id for backwards compatibility)
160      returned: always
161      type: str
162      sample: rtb-bf779ed7
163    propagating_vgws:
164      description: List of Virtual Private Gateways propagating routes
165      returned: always
166      type: list
167      sample: []
168    route_table_id:
169      description: ID of the route table
170      returned: always
171      type: str
172      sample: rtb-bf779ed7
173    routes:
174      description: List of routes in the route table
175      returned: always
176      type: complex
177      contains:
178        destination_cidr_block:
179          description: CIDR block of destination
180          returned: always
181          type: str
182          sample: 10.228.228.0/22
183        gateway_id:
184          description: ID of the gateway
185          returned: when gateway is local or internet gateway
186          type: str
187          sample: local
188        instance_id:
189          description: ID of a NAT instance
190          returned: when the route is via an EC2 instance
191          type: str
192          sample: i-abcd123456789
193        instance_owner_id:
194          description: AWS account owning the NAT instance
195          returned: when the route is via an EC2 instance
196          type: str
197          sample: 123456789012
198        nat_gateway_id:
199          description: ID of the NAT gateway
200          returned: when the route is via a NAT gateway
201          type: str
202          sample: local
203        origin:
204          description: mechanism through which the route is in the table
205          returned: always
206          type: str
207          sample: CreateRouteTable
208        state:
209          description: state of the route
210          returned: always
211          type: str
212          sample: active
213    tags:
214      description: Tags applied to the route table
215      returned: always
216      type: dict
217      sample:
218        Name: Public route table
219        Public: 'true'
220    vpc_id:
221      description: ID for the VPC in which the route lives
222      returned: always
223      type: str
224      sample: vpc-6e2d2407
225'''
226
227import re
228from time import sleep
229from ansible.module_utils.aws.core import AnsibleAWSModule
230from ansible.module_utils.aws.waiters import get_waiter
231from ansible.module_utils.ec2 import ec2_argument_spec, boto3_conn, get_aws_connection_info
232from ansible.module_utils.ec2 import ansible_dict_to_boto3_filter_list
233from ansible.module_utils.ec2 import camel_dict_to_snake_dict, snake_dict_to_camel_dict
234from ansible.module_utils.ec2 import ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict
235from ansible.module_utils.ec2 import compare_aws_tags, AWSRetry
236
237
238try:
239    import botocore
240except ImportError:
241    pass  # handled by AnsibleAWSModule
242
243
244CIDR_RE = re.compile(r'^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$')
245SUBNET_RE = re.compile(r'^subnet-[A-z0-9]+$')
246ROUTE_TABLE_RE = re.compile(r'^rtb-[A-z0-9]+$')
247
248
249@AWSRetry.exponential_backoff()
250def describe_subnets_with_backoff(connection, **params):
251    return connection.describe_subnets(**params)['Subnets']
252
253
254def find_subnets(connection, module, vpc_id, identified_subnets):
255    """
256    Finds a list of subnets, each identified either by a raw ID, a unique
257    'Name' tag, or a CIDR such as 10.0.0.0/8.
258
259    Note that this function is duplicated in other ec2 modules, and should
260    potentially be moved into a shared module_utils
261    """
262    subnet_ids = []
263    subnet_names = []
264    subnet_cidrs = []
265    for subnet in (identified_subnets or []):
266        if re.match(SUBNET_RE, subnet):
267            subnet_ids.append(subnet)
268        elif re.match(CIDR_RE, subnet):
269            subnet_cidrs.append(subnet)
270        else:
271            subnet_names.append(subnet)
272
273    subnets_by_id = []
274    if subnet_ids:
275        filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id})
276        try:
277            subnets_by_id = describe_subnets_with_backoff(connection, SubnetIds=subnet_ids, Filters=filters)
278        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
279            module.fail_json_aws(e, msg="Couldn't find subnet with id %s" % subnet_ids)
280
281    subnets_by_cidr = []
282    if subnet_cidrs:
283        filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id, 'cidr': subnet_cidrs})
284        try:
285            subnets_by_cidr = describe_subnets_with_backoff(connection, Filters=filters)
286        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
287            module.fail_json_aws(e, msg="Couldn't find subnet with cidr %s" % subnet_cidrs)
288
289    subnets_by_name = []
290    if subnet_names:
291        filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id, 'tag:Name': subnet_names})
292        try:
293            subnets_by_name = describe_subnets_with_backoff(connection, Filters=filters)
294        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
295            module.fail_json_aws(e, msg="Couldn't find subnet with names %s" % subnet_names)
296
297        for name in subnet_names:
298            matching_count = len([1 for s in subnets_by_name for t in s.get('Tags', []) if t['Key'] == 'Name' and t['Value'] == name])
299            if matching_count == 0:
300                module.fail_json(msg='Subnet named "{0}" does not exist'.format(name))
301            elif matching_count > 1:
302                module.fail_json(msg='Multiple subnets named "{0}"'.format(name))
303
304    return subnets_by_id + subnets_by_cidr + subnets_by_name
305
306
307def find_igw(connection, module, vpc_id):
308    """
309    Finds the Internet gateway for the given VPC ID.
310    """
311    filters = ansible_dict_to_boto3_filter_list({'attachment.vpc-id': vpc_id})
312    try:
313        igw = connection.describe_internet_gateways(Filters=filters)['InternetGateways']
314    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
315        module.fail_json_aws(e, msg='No IGW found for VPC {0}'.format(vpc_id))
316    if len(igw) == 1:
317        return igw[0]['InternetGatewayId']
318    elif len(igw) == 0:
319        module.fail_json(msg='No IGWs found for VPC {0}'.format(vpc_id))
320    else:
321        module.fail_json(msg='Multiple IGWs found for VPC {0}'.format(vpc_id))
322
323
324@AWSRetry.exponential_backoff()
325def describe_tags_with_backoff(connection, resource_id):
326    filters = ansible_dict_to_boto3_filter_list({'resource-id': resource_id})
327    paginator = connection.get_paginator('describe_tags')
328    tags = paginator.paginate(Filters=filters).build_full_result()['Tags']
329    return boto3_tag_list_to_ansible_dict(tags)
330
331
332def tags_match(match_tags, candidate_tags):
333    return all((k in candidate_tags and candidate_tags[k] == v
334                for k, v in match_tags.items()))
335
336
337def ensure_tags(connection=None, module=None, resource_id=None, tags=None, purge_tags=None, check_mode=None):
338    try:
339        cur_tags = describe_tags_with_backoff(connection, resource_id)
340    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
341        module.fail_json_aws(e, msg='Unable to list tags for VPC')
342
343    to_add, to_delete = compare_aws_tags(cur_tags, tags, purge_tags)
344
345    if not to_add and not to_delete:
346        return {'changed': False, 'tags': cur_tags}
347    if check_mode:
348        if not purge_tags:
349            tags = cur_tags.update(tags)
350        return {'changed': True, 'tags': tags}
351
352    if to_delete:
353        try:
354            connection.delete_tags(Resources=[resource_id], Tags=[{'Key': k} for k in to_delete])
355        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
356            module.fail_json_aws(e, msg="Couldn't delete tags")
357    if to_add:
358        try:
359            connection.create_tags(Resources=[resource_id], Tags=ansible_dict_to_boto3_tag_list(to_add))
360        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
361            module.fail_json_aws(e, msg="Couldn't create tags")
362
363    try:
364        latest_tags = describe_tags_with_backoff(connection, resource_id)
365    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
366        module.fail_json_aws(e, msg='Unable to list tags for VPC')
367    return {'changed': True, 'tags': latest_tags}
368
369
370@AWSRetry.exponential_backoff()
371def describe_route_tables_with_backoff(connection, **params):
372    try:
373        return connection.describe_route_tables(**params)['RouteTables']
374    except botocore.exceptions.ClientError as e:
375        if e.response['Error']['Code'] == 'InvalidRouteTableID.NotFound':
376            return None
377        else:
378            raise
379
380
381def get_route_table_by_id(connection, module, route_table_id):
382
383    route_table = None
384    try:
385        route_tables = describe_route_tables_with_backoff(connection, RouteTableIds=[route_table_id])
386    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
387        module.fail_json_aws(e, msg="Couldn't get route table")
388    if route_tables:
389        route_table = route_tables[0]
390
391    return route_table
392
393
394def get_route_table_by_tags(connection, module, vpc_id, tags):
395    count = 0
396    route_table = None
397    filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id})
398    try:
399        route_tables = describe_route_tables_with_backoff(connection, Filters=filters)
400    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
401        module.fail_json_aws(e, msg="Couldn't get route table")
402    for table in route_tables:
403        this_tags = describe_tags_with_backoff(connection, table['RouteTableId'])
404        if tags_match(tags, this_tags):
405            route_table = table
406            count += 1
407
408    if count > 1:
409        module.fail_json(msg="Tags provided do not identify a unique route table")
410    else:
411        return route_table
412
413
414def route_spec_matches_route(route_spec, route):
415    if route_spec.get('GatewayId') and 'nat-' in route_spec['GatewayId']:
416        route_spec['NatGatewayId'] = route_spec.pop('GatewayId')
417    if route_spec.get('GatewayId') and 'vpce-' in route_spec['GatewayId']:
418        if route_spec.get('DestinationCidrBlock', '').startswith('pl-'):
419            route_spec['DestinationPrefixListId'] = route_spec.pop('DestinationCidrBlock')
420
421    return set(route_spec.items()).issubset(route.items())
422
423
424def route_spec_matches_route_cidr(route_spec, route):
425    return route_spec['DestinationCidrBlock'] == route.get('DestinationCidrBlock')
426
427
428def rename_key(d, old_key, new_key):
429    d[new_key] = d.pop(old_key)
430
431
432def index_of_matching_route(route_spec, routes_to_match):
433    for i, route in enumerate(routes_to_match):
434        if route_spec_matches_route(route_spec, route):
435            return "exact", i
436        elif 'Origin' in route_spec and route_spec['Origin'] != 'EnableVgwRoutePropagation':
437            if route_spec_matches_route_cidr(route_spec, route):
438                return "replace", i
439
440
441def ensure_routes(connection=None, module=None, route_table=None, route_specs=None,
442                  propagating_vgw_ids=None, check_mode=None, purge_routes=None):
443    routes_to_match = [route for route in route_table['Routes']]
444    route_specs_to_create = []
445    route_specs_to_recreate = []
446    for route_spec in route_specs:
447        match = index_of_matching_route(route_spec, routes_to_match)
448        if match is None:
449            if route_spec.get('DestinationCidrBlock'):
450                route_specs_to_create.append(route_spec)
451            else:
452                module.warn("Skipping creating {0} because it has no destination cidr block. "
453                            "To add VPC endpoints to route tables use the ec2_vpc_endpoint module.".format(route_spec))
454        else:
455            if match[0] == "replace":
456                if route_spec.get('DestinationCidrBlock'):
457                    route_specs_to_recreate.append(route_spec)
458                else:
459                    module.warn("Skipping recreating route {0} because it has no destination cidr block.".format(route_spec))
460            del routes_to_match[match[1]]
461
462    routes_to_delete = []
463    if purge_routes:
464        for r in routes_to_match:
465            if not r.get('DestinationCidrBlock'):
466                module.warn("Skipping purging route {0} because it has no destination cidr block. "
467                            "To remove VPC endpoints from route tables use the ec2_vpc_endpoint module.".format(r))
468                continue
469            if r['Origin'] == 'CreateRoute':
470                routes_to_delete.append(r)
471
472    changed = bool(routes_to_delete or route_specs_to_create or route_specs_to_recreate)
473    if changed and not check_mode:
474        for route in routes_to_delete:
475            try:
476                connection.delete_route(RouteTableId=route_table['RouteTableId'], DestinationCidrBlock=route['DestinationCidrBlock'])
477            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
478                module.fail_json_aws(e, msg="Couldn't delete route")
479
480        for route_spec in route_specs_to_recreate:
481            try:
482                connection.replace_route(RouteTableId=route_table['RouteTableId'],
483                                         **route_spec)
484            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
485                module.fail_json_aws(e, msg="Couldn't recreate route")
486
487        for route_spec in route_specs_to_create:
488            try:
489                connection.create_route(RouteTableId=route_table['RouteTableId'],
490                                        **route_spec)
491            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
492                module.fail_json_aws(e, msg="Couldn't create route")
493
494    return {'changed': bool(changed)}
495
496
497def ensure_subnet_association(connection=None, module=None, vpc_id=None, route_table_id=None, subnet_id=None,
498                              check_mode=None):
499    filters = ansible_dict_to_boto3_filter_list({'association.subnet-id': subnet_id, 'vpc-id': vpc_id})
500    try:
501        route_tables = describe_route_tables_with_backoff(connection, Filters=filters)
502    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
503        module.fail_json_aws(e, msg="Couldn't get route tables")
504    for route_table in route_tables:
505        if route_table['RouteTableId'] is None:
506            continue
507        for a in route_table['Associations']:
508            if a['Main']:
509                continue
510            if a['SubnetId'] == subnet_id:
511                if route_table['RouteTableId'] == route_table_id:
512                    return {'changed': False, 'association_id': a['RouteTableAssociationId']}
513                else:
514                    if check_mode:
515                        return {'changed': True}
516                    try:
517                        connection.disassociate_route_table(AssociationId=a['RouteTableAssociationId'])
518                    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
519                        module.fail_json_aws(e, msg="Couldn't disassociate subnet from route table")
520
521    try:
522        association_id = connection.associate_route_table(RouteTableId=route_table_id, SubnetId=subnet_id)
523    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
524        module.fail_json_aws(e, msg="Couldn't associate subnet with route table")
525    return {'changed': True, 'association_id': association_id}
526
527
528def ensure_subnet_associations(connection=None, module=None, route_table=None, subnets=None,
529                               check_mode=None, purge_subnets=None):
530    current_association_ids = [a['RouteTableAssociationId'] for a in route_table['Associations'] if not a['Main']]
531    new_association_ids = []
532    changed = False
533    for subnet in subnets:
534        result = ensure_subnet_association(connection=connection, module=module, vpc_id=route_table['VpcId'],
535                                           route_table_id=route_table['RouteTableId'], subnet_id=subnet['SubnetId'], check_mode=check_mode)
536        changed = changed or result['changed']
537        if changed and check_mode:
538            return {'changed': True}
539        new_association_ids.append(result['association_id'])
540
541    if purge_subnets:
542        to_delete = [a_id for a_id in current_association_ids
543                     if a_id not in new_association_ids]
544
545        for a_id in to_delete:
546            changed = True
547            if not check_mode:
548                try:
549                    connection.disassociate_route_table(AssociationId=a_id)
550                except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
551                    module.fail_json_aws(e, msg="Couldn't disassociate subnet from route table")
552
553    return {'changed': changed}
554
555
556def ensure_propagation(connection=None, module=None, route_table=None, propagating_vgw_ids=None,
557                       check_mode=None):
558    changed = False
559    gateways = [gateway['GatewayId'] for gateway in route_table['PropagatingVgws']]
560    to_add = set(propagating_vgw_ids) - set(gateways)
561    if to_add:
562        changed = True
563        if not check_mode:
564            for vgw_id in to_add:
565                try:
566                    connection.enable_vgw_route_propagation(RouteTableId=route_table['RouteTableId'],
567                                                            GatewayId=vgw_id)
568                except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
569                    module.fail_json_aws(e, msg="Couldn't enable route propagation")
570
571    return {'changed': changed}
572
573
574def ensure_route_table_absent(connection, module):
575
576    lookup = module.params.get('lookup')
577    route_table_id = module.params.get('route_table_id')
578    tags = module.params.get('tags')
579    vpc_id = module.params.get('vpc_id')
580    purge_subnets = module.params.get('purge_subnets')
581
582    if lookup == 'tag':
583        if tags is not None:
584            route_table = get_route_table_by_tags(connection, module, vpc_id, tags)
585        else:
586            route_table = None
587    elif lookup == 'id':
588        route_table = get_route_table_by_id(connection, module, route_table_id)
589
590    if route_table is None:
591        return {'changed': False}
592
593    # disassociate subnets before deleting route table
594    if not module.check_mode:
595        ensure_subnet_associations(connection=connection, module=module, route_table=route_table,
596                                   subnets=[], check_mode=False, purge_subnets=purge_subnets)
597        try:
598            connection.delete_route_table(RouteTableId=route_table['RouteTableId'])
599        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
600            module.fail_json_aws(e, msg="Error deleting route table")
601
602    return {'changed': True}
603
604
605def get_route_table_info(connection, module, route_table):
606    result = get_route_table_by_id(connection, module, route_table['RouteTableId'])
607    try:
608        result['Tags'] = describe_tags_with_backoff(connection, route_table['RouteTableId'])
609    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
610        module.fail_json_aws(e, msg="Couldn't get tags for route table")
611    result = camel_dict_to_snake_dict(result, ignore_list=['Tags'])
612    # backwards compatibility
613    result['id'] = result['route_table_id']
614    return result
615
616
617def create_route_spec(connection, module, vpc_id):
618    routes = module.params.get('routes')
619
620    for route_spec in routes:
621        rename_key(route_spec, 'dest', 'destination_cidr_block')
622
623        if route_spec.get('gateway_id') and route_spec['gateway_id'].lower() == 'igw':
624            igw = find_igw(connection, module, vpc_id)
625            route_spec['gateway_id'] = igw
626        if route_spec.get('gateway_id') and route_spec['gateway_id'].startswith('nat-'):
627            rename_key(route_spec, 'gateway_id', 'nat_gateway_id')
628
629    return snake_dict_to_camel_dict(routes, capitalize_first=True)
630
631
632def ensure_route_table_present(connection, module):
633
634    lookup = module.params.get('lookup')
635    propagating_vgw_ids = module.params.get('propagating_vgw_ids')
636    purge_routes = module.params.get('purge_routes')
637    purge_subnets = module.params.get('purge_subnets')
638    purge_tags = module.params.get('purge_tags')
639    route_table_id = module.params.get('route_table_id')
640    subnets = module.params.get('subnets')
641    tags = module.params.get('tags')
642    vpc_id = module.params.get('vpc_id')
643    routes = create_route_spec(connection, module, vpc_id)
644
645    changed = False
646    tags_valid = False
647
648    if lookup == 'tag':
649        if tags is not None:
650            try:
651                route_table = get_route_table_by_tags(connection, module, vpc_id, tags)
652            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
653                module.fail_json_aws(e, msg="Error finding route table with lookup 'tag'")
654        else:
655            route_table = None
656    elif lookup == 'id':
657        try:
658            route_table = get_route_table_by_id(connection, module, route_table_id)
659        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
660            module.fail_json_aws(e, msg="Error finding route table with lookup 'id'")
661
662    # If no route table returned then create new route table
663    if route_table is None:
664        changed = True
665        if not module.check_mode:
666            try:
667                route_table = connection.create_route_table(VpcId=vpc_id)['RouteTable']
668                # try to wait for route table to be present before moving on
669                get_waiter(
670                    connection, 'route_table_exists'
671                ).wait(
672                    RouteTableIds=[route_table['RouteTableId']],
673                )
674            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
675                module.fail_json_aws(e, msg="Error creating route table")
676        else:
677            route_table = {"id": "rtb-xxxxxxxx", "route_table_id": "rtb-xxxxxxxx", "vpc_id": vpc_id}
678            module.exit_json(changed=changed, route_table=route_table)
679
680    if routes is not None:
681        result = ensure_routes(connection=connection, module=module, route_table=route_table,
682                               route_specs=routes, propagating_vgw_ids=propagating_vgw_ids,
683                               check_mode=module.check_mode, purge_routes=purge_routes)
684        changed = changed or result['changed']
685
686    if propagating_vgw_ids is not None:
687        result = ensure_propagation(connection=connection, module=module, route_table=route_table,
688                                    propagating_vgw_ids=propagating_vgw_ids, check_mode=module.check_mode)
689        changed = changed or result['changed']
690
691    if not tags_valid and tags is not None:
692        result = ensure_tags(connection=connection, module=module, resource_id=route_table['RouteTableId'], tags=tags,
693                             purge_tags=purge_tags, check_mode=module.check_mode)
694        route_table['Tags'] = result['tags']
695        changed = changed or result['changed']
696
697    if subnets is not None:
698        associated_subnets = find_subnets(connection, module, vpc_id, subnets)
699
700        result = ensure_subnet_associations(connection=connection, module=module, route_table=route_table,
701                                            subnets=associated_subnets, check_mode=module.check_mode,
702                                            purge_subnets=purge_subnets)
703        changed = changed or result['changed']
704
705    if changed:
706        # pause to allow route table routes/subnets/associations to be updated before exiting with final state
707        sleep(5)
708    module.exit_json(changed=changed, route_table=get_route_table_info(connection, module, route_table))
709
710
711def main():
712    argument_spec = ec2_argument_spec()
713    argument_spec.update(
714        dict(
715            lookup=dict(default='tag', choices=['tag', 'id']),
716            propagating_vgw_ids=dict(type='list'),
717            purge_routes=dict(default=True, type='bool'),
718            purge_subnets=dict(default=True, type='bool'),
719            purge_tags=dict(default=False, type='bool'),
720            route_table_id=dict(),
721            routes=dict(default=[], type='list'),
722            state=dict(default='present', choices=['present', 'absent']),
723            subnets=dict(type='list'),
724            tags=dict(type='dict', aliases=['resource_tags']),
725            vpc_id=dict()
726        )
727    )
728
729    module = AnsibleAWSModule(argument_spec=argument_spec,
730                              required_if=[['lookup', 'id', ['route_table_id']],
731                                           ['lookup', 'tag', ['vpc_id']],
732                                           ['state', 'present', ['vpc_id']]],
733                              supports_check_mode=True)
734
735    region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
736
737    connection = boto3_conn(module, conn_type='client', resource='ec2',
738                            region=region, endpoint=ec2_url, **aws_connect_params)
739
740    state = module.params.get('state')
741
742    if state == 'present':
743        result = ensure_route_table_present(connection, module)
744    elif state == 'absent':
745        result = ensure_route_table_absent(connection, module)
746
747    module.exit_json(**result)
748
749
750if __name__ == '__main__':
751    main()
752