#!/usr/bin/python # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . DOCUMENTATION = ''' --- module: ec2_vpc_route_table short_description: Configure route tables for AWS virtual private clouds description: - Create or removes route tables from AWS virtual private clouds.''' '''This module has a dependency on python-boto. version_added: "1.8" options: vpc_id: description: - "The VPC in which to create the route table." required: true route_table_id: description: - "The ID of the route table to update or delete." required: false default: null resource_tags: description: - 'A dictionary array of resource tags of the form: { tag1: value1,''' ''' tag2: value2 }. Tags in this list are used to uniquely identify route''' ''' tables within a VPC when the route_table_id is not supplied. required: false default: null aliases: [] version_added: "1.6" routes: 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'. required: true aliases: [] subnets: description: - An array of subnets to add to this route table. Subnets may either''' ''' be specified by subnet ID, Name tag, or by a CIDR such as '10.0.0.0/24'. required: true aliases: [] propagating_vgw_ids: description: - Enables route propagation from virtual gateways specified by ID. required: false aliases: [] wait: description: - wait for the VPC to be in state 'available' before returning required: false default: "no" choices: [ "yes", "no" ] aliases: [] wait_timeout: description: - how long before wait gives up, in seconds default: 300 aliases: [] state: description: - Create or terminate the VPC required: true default: present aliases: [] region: description: - region in which the resource exists. required: false default: null aliases: ['aws_region', 'ec2_region'] aws_secret_key: description: - AWS secret key. If not set then the value of the AWS_SECRET_KEY''' ''' environment variable is used. required: false default: None aliases: ['ec2_secret_key', 'secret_key' ] aws_access_key: description: - AWS access key. If not set then the value of the AWS_ACCESS_KEY''' ''' environment variable is used. required: false default: None aliases: ['ec2_access_key', 'access_key' ] validate_certs: description: - When set to "no", SSL certificates will not be validated for boto''' ''' versions >= 2.6.0. required: false default: "yes" choices: ["yes", "no"] aliases: [] version_added: "1.5" requirements: [ "boto" ] author: Robert Estelle ''' EXAMPLES = ''' # Note: None of these examples set aws_access_key, aws_secret_key, or region. # It is assumed that their matching environment variables are set. # Basic creation example: - name: Set up public subnet route table local_action: module: ec2_vpc_route_table vpc_id: vpc-1245678 region: us-west-1 resource_tags: Name: Public subnets: - '{{jumpbox_subnet.subnet_id}}' - '{{frontend_subnet.subnet_id}}' - '{{vpn_subnet.subnet_id}}' routes: - dest: 0.0.0.0/0 gateway_id: '{{igw.gateway_id}}' register: public_route_table - name: Set up NAT-protected route table local_action: module: ec2_vpc_route_table vpc_id: vpc-1245678 region: us-west-1 resource_tags: - Name: Internal subnets: - '{{application_subnet.subnet_id}}' - 'Database Subnet' - '10.0.0.0/8' routes: - dest: 0.0.0.0/0 instance_id: '{{nat.instance_id}}' register: nat_route_table ''' import sys # noqa import re try: import boto.ec2 import boto.vpc from boto.exception import EC2ResponseError HAS_BOTO = True except ImportError: HAS_BOTO = False if __name__ != '__main__': raise class AnsibleRouteTableException(Exception): pass class AnsibleTagCreationException(AnsibleRouteTableException): pass class AnsibleSubnetSearchException(AnsibleRouteTableException): pass CIDR_RE = re.compile('^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$') SUBNET_RE = re.compile('^subnet-[A-z0-9]+$') ROUTE_TABLE_RE = re.compile('^rtb-[A-z0-9]+$') def find_subnets(vpc_conn, 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. Note that this function is duplicated in other ec2 modules, and should potentially be moved into potentially be moved into a shared module_utils """ subnet_ids = [] subnet_names = [] subnet_cidrs = [] for subnet in (identified_subnets or []): if re.match(SUBNET_RE, subnet): subnet_ids.append(subnet) elif re.match(CIDR_RE, subnet): subnet_cidrs.append(subnet) else: subnet_names.append(subnet) 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)) 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(subnet_cidr)) subnets_by_name = [] if subnet_names: subnets_by_name = vpc_conn.get_all_subnets( filters={'vpc_id': vpc_id, 'tag:Name': subnet_names}) for name in subnet_names: matching = [s.tags.get('Name') == name for s in subnets_by_name] if len(matching) == 0: raise AnsibleSubnetSearchException( 'Subnet named "{0}" does not exist'.format(name)) elif len(matching) > 1: raise AnsibleSubnetSearchException( 'Multiple subnets named "{0}"'.format(name)) return subnets_by_id + subnets_by_cidr + subnets_by_name 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})) def tags_match(match_tags, candidate_tags): return all((k in candidate_tags and candidate_tags[k] == v for k, v in match_tags.iteritems())) def ensure_tags(vpc_conn, resource_id, tags, add_only, check_mode): try: cur_tags = get_resource_tags(vpc_conn, resource_id) if tags == cur_tags: return {'changed': False, 'tags': cur_tags} 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 = 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) latest_tags = get_resource_tags(vpc_conn, resource_id) return {'changed': True, 'tags': latest_tags} except EC2ResponseError as e: raise AnsibleTagCreationException( 'Unable to update tags for {0}, error: {1}'.format(resource_id, e)) def get_route_table_by_id(vpc_conn, vpc_id, route_table_id): route_tables = vpc_conn.get_all_route_tables( route_table_ids=[route_table_id], filters={'vpc_id': vpc_id}) return route_tables[0] if route_tables else None def get_route_table_by_tags(vpc_conn, vpc_id, tags): route_tables = vpc_conn.get_all_route_tables(filters={'vpc_id': vpc_id}) for route_table in route_tables: this_tags = get_resource_tags(vpc_conn, route_table.id) if tags_match(tags, this_tags): 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', } for k in key_attr_map.iterkeys(): if k in route_spec: if route_spec[k] != getattr(route, k): return False return True def rename_key(d, old_key, new_key): d[new_key] = d[old_key] del d[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 def ensure_routes(vpc_conn, route_table, route_specs, check_mode): routes_to_match = list(route_table.routes) route_specs_to_create = [] for route_spec in route_specs: i = index_of_matching_route(route_spec, routes_to_match) if i is None: route_specs_to_create.append(route_spec) else: del routes_to_match[i] routes_to_delete = [r for r in routes_to_match if r.gateway_id != 'local'] changed = routes_to_delete or route_specs_to_create if changed: for route_spec in route_specs_to_create: vpc_conn.create_route(route_table.id, dry_run=check_mode, **route_spec) for route in routes_to_delete: vpc_conn.delete_route(route_table.id, route.destination_cidr_block, dry_run=check_mode) return {'changed': 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} ) for route_table in route_tables: if route_table.id is None: continue for a in route_table.associations: if a.subnet_id == subnet_id: if route_table.id == route_table_id: return {'changed': False, 'association_id': a.id} else: if check_mode: return {'changed': True} vpc_conn.disassociate_route_table(a.id) association_id = vpc_conn.associate_route_table(route_table_id, subnet_id) return {'changed': True, 'association_id': association_id} def ensure_subnet_associations(vpc_conn, vpc_id, route_table, subnets, check_mode): current_association_ids = [a.id for a in route_table.associations] new_association_ids = [] changed = False for subnet in subnets: result = ensure_subnet_association( vpc_conn, vpc_id, route_table.id, subnet.id, check_mode) changed = changed or result['changed'] if changed and check_mode: return {'changed': True} new_association_ids.append(result['association_id']) to_delete = [a_id for a_id in current_association_ids if a_id not in new_association_ids] for a_id in to_delete: changed = True vpc_conn.disassociate_route_table(a_id, dry_run=check_mode) return {'changed': changed} def ensure_propagation(vpc_conn, route_table_id, propagating_vgw_ids, check_mode): # NOTE: As of boto==2.15.0, it is not yet possible to query the existing # propagating gateways. However, EC2 does support this as evidenced by # the describe-route-tables tool. For now, just enable the given VGWs # and do not disable any others. changed = False for vgw_id in propagating_vgw_ids: if vgw_id not in original_association_ids: changed = True vpc_conn.enable_vgw_route_propagation(route_table_id, vgw_id, test_run=check_mode) return {'changed': changed} def ensure_route_table_absent(vpc_conn, vpc_id, route_table_id, resource_tags, check_mode): if route_table_id: route_table = get_route_table_by_id(vpc_conn, vpc_id, route_table_id) elif resource_tags: route_table = get_route_table_by_tags(vpc_conn, vpc_id, resource_tags) else: raise AnsibleRouteTableException( 'must provide route_table_id or resource_tags') if route_table is None: return {'changed': False} vpc_conn.delete_route_table(route_table.id, dry_run=check_mode) return {'changed': True} def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, routes, subnets, propagating_vgw_ids, check_mode): changed = False tags_valid = False if route_table_id: route_table = get_route_table_by_id(vpc_conn, vpc_id, route_table_id) elif resource_tags: route_table = get_route_table_by_tags(vpc_conn, vpc_id, resource_tags) tags_valid = route_table is not None else: raise AnsibleRouteTableException( 'must provide route_table_id or resource_tags') if check_mode and route_table is None: return {'changed': True} if route_table is None: try: route_table = vpc_conn.create_route_table(vpc_id) except EC2ResponseError as e: raise AnsibleRouteTableException( 'Unable to create route table {0}, error: {1}' .format(route_table_id or resource_tags, e) ) if propagating_vgw_ids is not None: result = ensure_propagation(vpc_conn, route_table_id, propagating_vgw_ids, check_mode=check_mode) changed = changed or result['changed'] if not tags_valid and resource_tags is not None: result = ensure_tags(vpc_conn, route_table.id, resource_tags, add_only=True, check_mode=check_mode) changed = changed or result['changed'] if routes is not None: try: result = ensure_routes(vpc_conn, route_table, routes, check_mode) changed = changed or result['changed'] except EC2ResponseError as e: raise AnsibleRouteTableException( 'Unable to ensure routes for route table {0}, error: {1}' .format(route_table, e) ) if subnets: associated_subnets = [] try: associated_subnets = find_subnets(vpc_conn, vpc_id, subnets) except EC2ResponseError as e: raise AnsibleRouteTableException( 'Unable to find subnets for route table {0}, error: {1}' .format(route_table, e) ) try: result = ensure_subnet_associations( vpc_conn, vpc_id, route_table, associated_subnets, check_mode) changed = changed or result['changed'] except EC2ResponseError as e: raise AnsibleRouteTableException( 'Unable to associate subnets for route table {0}, error: {1}' .format(route_table, e) ) return { 'changed': changed, 'route_table_id': route_table.id, } def main(): argument_spec = ec2_argument_spec() argument_spec.update({ 'vpc_id': {'required': True}, 'route_table_id': {'required': False}, 'propagating_vgw_ids': {'type': 'list', 'required': False}, 'resource_tags': {'type': 'dict', 'required': False}, 'routes': {'type': 'list', 'required': False}, 'subnets': {'type': 'list', 'required': False}, 'state': {'choices': ['present', 'absent'], 'default': 'present'}, }) module = AnsibleModule( argument_spec=argument_spec, supports_check_mode=True, ) if not HAS_BOTO: module.fail_json(msg='boto is required for this module') ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module) if not region: module.fail_json(msg='Region must be specified') try: vpc_conn = boto.vpc.connect_to_region( region, aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key ) except boto.exception.NoAuthHandlerFound as e: module.fail_json(msg=str(e)) vpc_id = module.params.get('vpc_id') route_table_id = module.params.get('route_table_id') resource_tags = module.params.get('resource_tags') propagating_vgw_ids = module.params.get('propagating_vgw_ids', []) routes = module.params.get('routes') for route_spec in routes: rename_key(route_spec, 'dest', 'destination_cidr_block') subnets = module.params.get('subnets') state = module.params.get('state', 'present') try: if state == 'present': result = ensure_route_table_present( vpc_conn, vpc_id, route_table_id, resource_tags, routes, subnets, propagating_vgw_ids, module.check_mode ) elif state == 'absent': result = ensure_route_table_absent( vpc_conn, vpc_id, route_table_id, resource_tags, module.check_mode ) except AnsibleRouteTableException as e: module.fail_json(msg=str(e)) module.exit_json(**result) from ansible.module_utils.basic import * # noqa from ansible.module_utils.ec2 import * # noqa if __name__ == '__main__': main()