Route table boto3 (#32059)
* Allow protection of certain keys during camel_to_snake Create an `ignore_list` parameter that preserves the case of the contents of certain dictionaries. Most valuable for `tags` but other uses might arise. * Port ec2_vpc_route_table to boto3 Update tests to reflect fixes in boto3. * Add RETURN documentation to ec2_vpc_route_table * Update DOCUMENTATION to be valid yaml * Add check mode tests
This commit is contained in:
parent
ecfe177380
commit
a685b621cd
4 changed files with 701 additions and 439 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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'))
|
||||
|
|
Loading…
Reference in a new issue