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