[cloud] Add IPv6 support for ec2_vpc_subnet module(#30444)

* Add integration test suite for ec2_vpc_subnet

* wrap boto3 connection in try/except

update module documentation and add RETURN docs

add IPv6 support to VPC subnet module

rename ipv6cidr to ipv6_cidr, use required_if for parameter testing, update some failure messages to be more descriptive

DryRun mode was removed from this function a while ago but exception handling was still checking for it, removed

add wait and timeout for subnet creation process

fixup the ipv6 cidr disassociation logic a bit per review

update RETURN values per review

added module parameter check

removed DryRun parameter from boto3 call since it would always be false here

fix subnet wait loop

add a purge_tags parameter, fix the ensure_tags function, update to use compare_aws_tags func

fix tags type error per review

remove **kwargs use in create_subnet function per review

* rebased on #31870, fixed merge conflicts, and updated error messages

* fixes to pass tests

* add test for failure on invalid ipv6 block and update tags test for purge_tags=true function

* fix pylint issue

* fix exception handling error when run with python3

* add ipv6 tests and fix module code

* Add permissions to hacking/aws_config/testing_policies/ec2-policy.json for adding IPv6 cidr blocks to VPC and subnets

* fix type in tests and update assert conditional to check entire returned value

* add AWS_SESSION_TOKEN into environment for aws cli commands to work in CI

* remove key and value options from call to boto3_tag_list_to_ansible_dict

* remove wait loop and use boto3 EC2 waiter

* remove unused register: result vars

* revert az argument default value to original setting default=None
This commit is contained in:
Daniel Shepherd 2017-11-16 14:58:12 -05:00 committed by Ryan Brown
parent 23b1dbacaf
commit cfbe9c8aee
3 changed files with 499 additions and 87 deletions

View file

@ -10,6 +10,8 @@
"ec2:AllocateAddress", "ec2:AllocateAddress",
"ec2:AssociateAddress", "ec2:AssociateAddress",
"ec2:AssociateRouteTable", "ec2:AssociateRouteTable",
"ec2:AssociateVpcCidrBlock",
"ec2:AssociateSubnetCidrBlock",
"ec2:CreateImage", "ec2:CreateImage",
"ec2:AttachInternetGateway", "ec2:AttachInternetGateway",
"ec2:CreateInternetGateway", "ec2:CreateInternetGateway",

View file

@ -23,14 +23,21 @@ requirements: [ boto3 ]
options: options:
az: az:
description: description:
- "The availability zone for the subnet. Only required when state=present." - "The availability zone for the subnet."
required: false required: false
default: null default: null
cidr: cidr:
description: description:
- "The CIDR block for the subnet. E.g. 192.0.2.0/24. Only required when state=present." - "The CIDR block for the subnet. E.g. 192.0.2.0/24."
required: false required: false
default: null default: null
ipv6_cidr:
description:
- "The IPv6 CIDR block for the subnet. The VPC must have a /56 block assigned and this value must be a valid IPv6 /64 that falls in the VPC range."
- "Required if I(assign_instances_ipv6=true)"
required: false
default: null
version_added: "2.5"
tags: tags:
description: description:
- "A dict of tags to apply to the subnet. Any tags currently applied to the subnet and not present here will be removed." - "A dict of tags to apply to the subnet. Any tags currently applied to the subnet and not present here will be removed."
@ -45,15 +52,39 @@ options:
choices: [ 'present', 'absent' ] choices: [ 'present', 'absent' ]
vpc_id: vpc_id:
description: description:
- "VPC ID of the VPC in which to create the subnet." - "VPC ID of the VPC in which to create or delete the subnet."
required: false required: true
default: null default: null
map_public: map_public:
description: description:
- "Specify true to indicate that instances launched into the subnet should be assigned public IP address by default." - "Specify true to indicate that instances launched into the subnet should be assigned public IP address by default."
required: false required: false
default: false default: false
version_added: "2.4" version_added: "2.4"
assign_instances_ipv6:
description:
- "Specify true to indicate that instances launched into the subnet should be automatically assigned an IPv6 address."
required: false
default: false
version_added: "2.5"
wait:
description:
- "When specified,I(state=present) module will wait for subnet to be in available state before continuing."
required: false
default: true
version_added: "2.5"
wait_timeout:
description:
- "Number of seconds to wait for subnet to become available I(wait=True)."
required: false
default: 300
version_added: "2.5"
purge_tags:
description:
- Whether or not to remove tags that do not appear in the I(tags) list. Defaults to true.
required: false
default: true
version_added: "2.5"
extends_documentation_fragment: extends_documentation_fragment:
- aws - aws
- ec2 - ec2
@ -77,8 +108,112 @@ EXAMPLES = '''
vpc_id: vpc-123456 vpc_id: vpc-123456
cidr: 10.0.1.16/28 cidr: 10.0.1.16/28
- name: Create subnet with IPv6 block assigned
ec2_vpc_subnet:
state: present
vpc_id: vpc-123456
cidr: 10.1.100.0/24
ipv6_cidr: 2001:db8:0:102::/64
- name: Remove IPv6 block assigned to subnet
ec2_vpc_subnet:
state: present
vpc_id: vpc-123456
cidr: 10.1.100.0/24
ipv6_cidr: ''
''' '''
RETURN = '''
subnet:
description: Dictionary of subnet values
returned: I(state=present)
type: complex
contains:
id:
description: Subnet resource id
returned: I(state=present)
type: string
sample: subnet-b883b2c4
cidr_block:
description: The IPv4 CIDR of the Subnet
returned: I(state=present)
type: string
sample: "10.0.0.0/16"
ipv6_cidr_block:
description: The IPv6 CIDR block actively associated with the Subnet
returned: I(state=present)
type: string
sample: "2001:db8:0:102::/64"
availability_zone:
description: Availability zone of the Subnet
returned: I(state=present)
type: string
sample: us-east-1a
state:
description: state of the Subnet
returned: I(state=present)
type: string
sample: available
tags:
description: tags attached to the Subnet, includes name
returned: I(state=present)
type: dict
sample: {"Name": "My Subnet", "env": "staging"}
map_public_ip_on_launch:
description: whether public IP is auto-assigned to new instances
returned: I(state=present)
type: boolean
sample: false
assign_ipv6_address_on_creation:
description: whether IPv6 address is auto-assigned to new instances
returned: I(state=present)
type: boolean
sample: false
vpc_id:
description: the id of the VPC where this Subnet exists
returned: I(state=present)
type: string
sample: vpc-67236184
available_ip_address_count:
description: number of available IPv4 addresses
returned: I(state=present)
type: string
sample: 251
default_for_az:
description: indicates whether this is the default Subnet for this Availability Zone
returned: I(state=present)
type: boolean
sample: false
ipv6_association_id:
description: The IPv6 association ID for the currently associated CIDR
returned: I(state=present)
type: string
sample: subnet-cidr-assoc-b85c74d2
ipv6_cidr_block_association_set:
description: An array of IPv6 cidr block association set information.
returned: I(state=present)
type: complex
contains:
association_id:
description: The association ID
returned: always
type: string
ipv6_cidr_block:
description: The IPv6 CIDR block that is associated with the subnet.
returned: always
type: string
ipv6_cidr_block_state:
description: A hash/dict that contains a single item. The state of the cidr block association.
returned: always
type: dict
contains:
state:
description: The CIDR block association state.
returned: always
type: string
'''
import time import time
import traceback import traceback
@ -90,7 +225,7 @@ except ImportError:
from ansible.module_utils.aws.core import AnsibleAWSModule from ansible.module_utils.aws.core import AnsibleAWSModule
from ansible.module_utils.ec2 import (ansible_dict_to_boto3_filter_list, ansible_dict_to_boto3_tag_list, from ansible.module_utils.ec2 import (ansible_dict_to_boto3_filter_list, ansible_dict_to_boto3_tag_list,
ec2_argument_spec, camel_dict_to_snake_dict, get_aws_connection_info, ec2_argument_spec, camel_dict_to_snake_dict, get_aws_connection_info,
boto3_conn, boto3_tag_list_to_ansible_dict, AWSRetry) boto3_conn, boto3_tag_list_to_ansible_dict, compare_aws_tags, AWSRetry)
def get_subnet_info(subnet): def get_subnet_info(subnet):
@ -110,6 +245,15 @@ def get_subnet_info(subnet):
subnet['id'] = subnet['subnet_id'] subnet['id'] = subnet['subnet_id']
del subnet['subnet_id'] del subnet['subnet_id']
subnet['ipv6_cidr_block'] = ''
subnet['ipv6_association_id'] = ''
ipv6set = subnet.get('ipv6_cidr_block_association_set')
if ipv6set:
for item in ipv6set:
if item.get('ipv6_cidr_block_state', {}).get('state') in ('associated', 'associating'):
subnet['ipv6_cidr_block'] = item['ipv6_cidr_block']
subnet['ipv6_association_id'] = item['association_id']
return subnet return subnet
@ -118,56 +262,75 @@ def describe_subnets_with_backoff(client, **params):
return client.describe_subnets(**params) return client.describe_subnets(**params)
def subnet_exists(conn, module, subnet_id): def create_subnet(conn, module, vpc_id, cidr, ipv6_cidr=None, az=None):
filters = ansible_dict_to_boto3_filter_list({'subnet-id': subnet_id}) wait = module.params['wait']
try: wait_timeout = module.params['wait_timeout']
subnets = get_subnet_info(describe_subnets_with_backoff(conn, Filters=filters))
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Couldn't check if subnet exists")
if len(subnets) > 0 and 'state' in subnets[0] and subnets[0]['state'] == "available":
return subnets[0]
else:
return False
params = dict(VpcId=vpc_id,
CidrBlock=cidr)
if ipv6_cidr:
params['Ipv6CidrBlock'] = ipv6_cidr
def create_subnet(conn, module, vpc_id, cidr, az, check_mode):
if check_mode:
return
params = dict(VpcId=vpc_id, CidrBlock=cidr)
if az: if az:
params['AvailabilityZone'] = az params['AvailabilityZone'] = az
try: try:
new_subnet = get_subnet_info(conn.create_subnet(**params)) subnet = get_subnet_info(conn.create_subnet(**params))
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Couldn't create subnet") module.fail_json_aws(e, msg="Couldn't create subnet")
# Sometimes AWS takes its time to create a subnet and so using # Sometimes AWS takes its time to create a subnet and so using
# new subnets's id to do things like create tags results in # new subnets's id to do things like create tags results in
# exception. boto doesn't seem to refresh 'state' of the newly # exception.
# created subnet, i.e.: it's always 'pending'. if wait and subnet.get('state') != 'available':
subnet = False delay = 5
while subnet is False: max_attempts = wait_timeout / delay
subnet = subnet_exists(conn, module, new_subnet['id']) waiter_config = dict(Delay=delay, MaxAttempts=max_attempts)
time.sleep(0.1) waiter = conn.get_waiter('subnet_available')
try:
waiter.wait(SubnetIds=[subnet['id']], WaiterConfig=waiter_config)
subnet['state'] = 'available'
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json(msg="Create subnet action timed out waiting for Subnet to become available.")
return subnet return subnet
def ensure_tags(conn, module, subnet, tags, add_only, check_mode): def ensure_tags(conn, module, subnet, tags, purge_tags):
cur_tags = subnet['tags'] changed = False
to_delete = dict((k, cur_tags[k]) for k in cur_tags if k not in tags) filters = ansible_dict_to_boto3_filter_list({'resource-id': subnet['id'], 'resource-type': 'subnet'})
if to_delete and not add_only and not check_mode: try:
cur_tags = conn.describe_tags(Filters=filters)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Couldn't describe tags")
to_update, to_delete = compare_aws_tags(boto3_tag_list_to_ansible_dict(cur_tags.get('Tags')), tags, purge_tags)
if to_update:
try: try:
conn.delete_tags(Resources=[subnet['id']], Tags=ansible_dict_to_boto3_tag_list(to_delete)) if not module.check_mode:
conn.create_tags(Resources=[subnet['id']], Tags=ansible_dict_to_boto3_tag_list(to_update))
changed = True
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Couldn't create tags")
if to_delete:
try:
if not module.check_mode:
tags_list = []
for key in to_delete:
tags_list.append({'Key': key})
conn.delete_tags(Resources=[subnet['id']], Tags=tags_list)
changed = True
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Couldn't delete tags") module.fail_json_aws(e, msg="Couldn't delete tags")
to_add = dict((k, tags[k]) for k in tags if k not in cur_tags or cur_tags[k] != tags[k]) return changed
if to_add and not check_mode:
try:
conn.create_tags(Resources=[subnet['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")
def ensure_map_public(conn, module, subnet, map_public, check_mode): def ensure_map_public(conn, module, subnet, map_public, check_mode):
@ -179,25 +342,89 @@ def ensure_map_public(conn, module, subnet, map_public, check_mode):
module.fail_json_aws(e, msg="Couldn't modify subnet attribute") module.fail_json_aws(e, msg="Couldn't modify subnet attribute")
def ensure_assign_ipv6_on_create(conn, module, subnet, assign_instances_ipv6, check_mode):
if check_mode:
return
try:
conn.modify_subnet_attribute(SubnetId=subnet['id'], AssignIpv6AddressOnCreation={'Value': assign_instances_ipv6})
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Couldn't modify subnet attribute")
def disassociate_ipv6_cidr(conn, module, subnet):
if subnet.get('assign_ipv6_address_on_creation'):
ensure_assign_ipv6_on_create(conn, module, subnet, False, False)
try:
conn.disassociate_subnet_cidr_block(AssociationId=subnet['ipv6_association_id'])
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Couldn't disassociate ipv6 cidr block id {0} from subnet {1}"
.format(subnet['ipv6_association_id'], subnet['id']))
def ensure_ipv6_cidr_block(conn, module, subnet, ipv6_cidr, check_mode):
changed = False
if subnet['ipv6_association_id'] and not ipv6_cidr:
if not check_mode:
disassociate_ipv6_cidr(conn, module, subnet)
changed = True
if ipv6_cidr:
filters = ansible_dict_to_boto3_filter_list({'ipv6-cidr-block-association.ipv6-cidr-block': ipv6_cidr,
'vpc-id': subnet['vpc_id']})
try:
check_subnets = get_subnet_info(describe_subnets_with_backoff(conn, Filters=filters))
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Couldn't get subnet info")
if check_subnets and check_subnets[0]['ipv6_cidr_block']:
module.fail_json(msg="The IPv6 CIDR '{0}' conflicts with another subnet".format(ipv6_cidr))
if subnet['ipv6_association_id']:
if not check_mode:
disassociate_ipv6_cidr(conn, module, subnet)
changed = True
try:
if not check_mode:
associate_resp = conn.associate_subnet_cidr_block(SubnetId=subnet['id'], Ipv6CidrBlock=ipv6_cidr)
changed = True
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Couldn't associate ipv6 cidr {0} to {1}".format(ipv6_cidr, subnet['id']))
if associate_resp.get('Ipv6CidrBlockAssociation', {}).get('AssociationId'):
subnet['ipv6_association_id'] = associate_resp['Ipv6CidrBlockAssociation']['AssociationId']
subnet['ipv6_cidr_block'] = associate_resp['Ipv6CidrBlockAssociation']['Ipv6CidrBlock']
if subnet['ipv6_cidr_block_association_set']:
subnet['ipv6_cidr_block_association_set'][0] = camel_dict_to_snake_dict(associate_resp['Ipv6CidrBlockAssociation'])
else:
subnet['ipv6_cidr_block_association_set'].append(camel_dict_to_snake_dict(associate_resp['Ipv6CidrBlockAssociation']))
return changed
def get_matching_subnet(conn, module, vpc_id, cidr): def get_matching_subnet(conn, module, vpc_id, cidr):
filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id, 'cidr-block': cidr}) filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id, 'cidr-block': cidr})
try: try:
subnets = get_subnet_info(conn.describe_subnets(Filters=filters)) subnets = get_subnet_info(describe_subnets_with_backoff(conn, Filters=filters))
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Couldn't get matching subnet") module.fail_json_aws(e, msg="Couldn't get matching subnet")
if len(subnets) > 0: if subnets:
return subnets[0] return subnets[0]
else:
return None return None
def ensure_subnet_present(conn, module, vpc_id, cidr, az, tags, map_public, check_mode): def ensure_subnet_present(conn, module):
subnet = get_matching_subnet(conn, module, vpc_id, cidr) subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr'])
changed = False changed = False
if subnet is None: if subnet is None:
if not check_mode: if not module.check_mode:
subnet = create_subnet(conn, module, vpc_id, cidr, az, check_mode) subnet = create_subnet(conn, module, module.params['vpc_id'], module.params['cidr'], ipv6_cidr=module.params['ipv6_cidr'], az=module.params['az'])
changed = True changed = True
# Subnet will be None when check_mode is true # Subnet will be None when check_mode is true
if subnet is None: if subnet is None:
@ -205,30 +432,39 @@ def ensure_subnet_present(conn, module, vpc_id, cidr, az, tags, map_public, chec
'changed': changed, 'changed': changed,
'subnet': {} 'subnet': {}
} }
if map_public != subnet['map_public_ip_on_launch']:
ensure_map_public(conn, module, subnet, map_public, check_mode) if module.params['ipv6_cidr'] != subnet.get('ipv6_cidr_block'):
subnet['map_public_ip_on_launch'] = map_public if ensure_ipv6_cidr_block(conn, module, subnet, module.params['ipv6_cidr'], module.check_mode):
changed = True
if module.params['map_public'] != subnet['map_public_ip_on_launch']:
ensure_map_public(conn, module, subnet, module.params['map_public'], module.check_mode)
changed = True changed = True
if tags != subnet['tags']: if module.params['assign_instances_ipv6'] != subnet.get('assign_ipv6_address_on_creation'):
ensure_tags(conn, module, subnet, tags, False, check_mode) ensure_assign_ipv6_on_create(conn, module, subnet, module.params['assign_instances_ipv6'], module.check_mode)
subnet['tags'] = tags
changed = True changed = True
if module.params['tags'] != subnet['tags']:
if ensure_tags(conn, module, subnet, module.params['tags'], module.params['purge_tags']):
changed = True
subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr'])
return { return {
'changed': changed, 'changed': changed,
'subnet': subnet 'subnet': subnet
} }
def ensure_subnet_absent(conn, module, vpc_id, cidr, check_mode): def ensure_subnet_absent(conn, module):
subnet = get_matching_subnet(conn, module, vpc_id, cidr) subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr'])
if subnet is None: if subnet is None:
return {'changed': False} return {'changed': False}
try: try:
if not check_mode: if not module.check_mode:
conn.delete_subnet(SubnetId=subnet['id'], DryRun=check_mode) conn.delete_subnet(SubnetId=subnet['id'])
return {'changed': True} return {'changed': True}
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Couldn't delete subnet") module.fail_json_aws(e, msg="Couldn't delete subnet")
@ -240,36 +476,35 @@ def main():
dict( dict(
az=dict(default=None, required=False), az=dict(default=None, required=False),
cidr=dict(default=None, required=True), cidr=dict(default=None, required=True),
ipv6_cidr=dict(default='', required=False),
state=dict(default='present', choices=['present', 'absent']), state=dict(default='present', choices=['present', 'absent']),
tags=dict(default={}, required=False, type='dict', aliases=['resource_tags']), tags=dict(default={}, required=False, type='dict', aliases=['resource_tags']),
vpc_id=dict(default=None, required=True), vpc_id=dict(default=None, required=True),
map_public=dict(default=False, required=False, type='bool') map_public=dict(default=False, required=False, type='bool'),
assign_instances_ipv6=dict(default=False, required=False, type='bool'),
wait=dict(type='bool', default=True),
wait_timeout=dict(type='int', default=300, required=False),
purge_tags=dict(default=True, type='bool')
) )
) )
module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) required_if = [('assign_instances_ipv6', True, ['ipv6_cidr'])]
module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if)
if module.params.get('assign_instances_ipv6') and not module.params.get('ipv6_cidr'):
module.fail_json(msg="assign_instances_ipv6 is True but ipv6_cidr is None or an empty string")
region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params)
if region:
connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params)
else:
module.fail_json(msg="region must be specified")
vpc_id = module.params.get('vpc_id')
tags = module.params.get('tags')
cidr = module.params.get('cidr')
az = module.params.get('az')
state = module.params.get('state') state = module.params.get('state')
map_public = module.params.get('map_public')
try: try:
if state == 'present': if state == 'present':
result = ensure_subnet_present(connection, module, vpc_id, cidr, az, tags, map_public, result = ensure_subnet_present(connection, module)
check_mode=module.check_mode)
elif state == 'absent': elif state == 'absent':
result = ensure_subnet_absent(connection, module, vpc_id, cidr, result = ensure_subnet_absent(connection, module)
check_mode=module.check_mode)
except botocore.exceptions.ClientError as e: except botocore.exceptions.ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), module.fail_json(msg=e.message, exception=traceback.format_exc(),
**camel_dict_to_snake_dict(e.response)) **camel_dict_to_snake_dict(e.response))

View file

@ -16,7 +16,7 @@
name: "{{ resource_prefix }}-vpc" name: "{{ resource_prefix }}-vpc"
state: present state: present
cidr_block: "10.232.232.128/26" cidr_block: "10.232.232.128/26"
region: '{{ec2_region}}' region: '{{ ec2_region }}'
aws_access_key: '{{ aws_access_key }}' aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}' aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token }}' security_token: '{{ security_token }}'
@ -33,7 +33,7 @@
tags: tags:
Name: '{{ec2_vpc_subnet_name}}' Name: '{{ec2_vpc_subnet_name}}'
Description: '{{ec2_vpc_subnet_description}}' Description: '{{ec2_vpc_subnet_description}}'
region: '{{ec2_region}}' region: '{{ ec2_region }}'
aws_access_key: '{{ aws_access_key }}' aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}' aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token }}' security_token: '{{ security_token }}'
@ -56,20 +56,41 @@
tags: tags:
Name: '{{ec2_vpc_subnet_name}}' Name: '{{ec2_vpc_subnet_name}}'
Description: '{{ec2_vpc_subnet_description}}' Description: '{{ec2_vpc_subnet_description}}'
region: '{{ec2_region}}' region: '{{ ec2_region }}'
aws_access_key: '{{ aws_access_key }}' aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}' aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token }}' security_token: '{{ security_token }}'
state: present state: present
register: vpc_subnet_recreate register: vpc_subnet_recreate
- name: assert recreation changed nothing (expected changed=true) - name: assert recreation changed nothing (expected changed=false)
assert: assert:
that: that:
- 'not vpc_subnet_recreate.changed' - 'not vpc_subnet_recreate.changed'
- 'vpc_subnet_recreate.subnet.id.startswith("subnet-")' - 'vpc_subnet_recreate.subnet == vpc_subnet_create.subnet'
- '"Name" in vpc_subnet_recreate.subnet.tags and vpc_subnet_recreate.subnet.tags["Name"] == ec2_vpc_subnet_name'
- '"Description" in vpc_subnet_recreate.subnet.tags and vpc_subnet_recreate.subnet.tags["Description"] == ec2_vpc_subnet_description' - name: add invalid ipv6 block to subnet (expected failed)
ec2_vpc_subnet:
cidr: "10.232.232.128/28"
az: "{{ ec2_region }}a"
vpc_id: "{{ vpc_result.vpc.id }}"
ipv6_cidr: 2001:db8::/64
tags:
Name: '{{ec2_vpc_subnet_name}}'
Description: '{{ec2_vpc_subnet_description}}'
region: '{{ec2_region}}'
aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token }}'
state: present
register: vpc_subnet_ipv6_failed
ignore_errors: yes
- name: assert failure happened (expected failed)
assert:
that:
- 'vpc_subnet_ipv6_failed.failed'
- "'Couldn\\'t associate ipv6 cidr' in vpc_subnet_ipv6_failed.msg"
- name: add a tag (expected changed=true) - name: add a tag (expected changed=true)
ec2_vpc_subnet: ec2_vpc_subnet:
@ -80,7 +101,7 @@
Name: '{{ec2_vpc_subnet_name}}' Name: '{{ec2_vpc_subnet_name}}'
Description: '{{ec2_vpc_subnet_description}}' Description: '{{ec2_vpc_subnet_description}}'
AnotherTag: SomeValue AnotherTag: SomeValue
region: '{{ec2_region}}' region: '{{ ec2_region }}'
aws_access_key: '{{ aws_access_key }}' aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}' aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token }}' security_token: '{{ security_token }}'
@ -95,16 +116,14 @@
- '"Description" in vpc_subnet_add_a_tag.subnet.tags and vpc_subnet_add_a_tag.subnet.tags["Description"] == ec2_vpc_subnet_description' - '"Description" in vpc_subnet_add_a_tag.subnet.tags and vpc_subnet_add_a_tag.subnet.tags["Description"] == ec2_vpc_subnet_description'
- '"AnotherTag" in vpc_subnet_add_a_tag.subnet.tags and vpc_subnet_add_a_tag.subnet.tags["AnotherTag"] == "SomeValue"' - '"AnotherTag" in vpc_subnet_add_a_tag.subnet.tags and vpc_subnet_add_a_tag.subnet.tags["AnotherTag"] == "SomeValue"'
# We may want to change this behaviour by adding purge_tags to the module - name: remove tags with default purge_tags=true (expected changed=true)
# and setting it to false by default
- name: remove tags (expected changed=true)
ec2_vpc_subnet: ec2_vpc_subnet:
cidr: "10.232.232.128/28" cidr: "10.232.232.128/28"
az: "{{ ec2_region }}a" az: "{{ ec2_region }}a"
vpc_id: "{{ vpc_result.vpc.id }}" vpc_id: "{{ vpc_result.vpc.id }}"
tags: tags:
AnotherTag: SomeValue AnotherTag: SomeValue
region: '{{ec2_region}}' region: '{{ ec2_region }}'
aws_access_key: '{{ aws_access_key }}' aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}' aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token }}' security_token: '{{ security_token }}'
@ -119,12 +138,36 @@
- '"Description" not in vpc_subnet_remove_tags.subnet.tags' - '"Description" not in vpc_subnet_remove_tags.subnet.tags'
- '"AnotherTag" in vpc_subnet_remove_tags.subnet.tags and vpc_subnet_remove_tags.subnet.tags["AnotherTag"] == "SomeValue"' - '"AnotherTag" in vpc_subnet_remove_tags.subnet.tags and vpc_subnet_remove_tags.subnet.tags["AnotherTag"] == "SomeValue"'
- name: change tags with purge_tags=false (expected changed=true)
ec2_vpc_subnet:
cidr: "10.232.232.128/28"
az: "{{ ec2_region }}a"
vpc_id: "{{ vpc_result.vpc.id }}"
tags:
Name: '{{ec2_vpc_subnet_name}}'
Description: '{{ec2_vpc_subnet_description}}'
region: '{{ec2_region}}'
aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token }}'
state: present
purge_tags: false
register: vpc_subnet_change_tags
- name: assert tag addition happened (expected changed=true)
assert:
that:
- 'vpc_subnet_change_tags.changed'
- '"Name" in vpc_subnet_change_tags.subnet.tags and vpc_subnet_change_tags.subnet.tags["Name"] == ec2_vpc_subnet_name'
- '"Description" in vpc_subnet_change_tags.subnet.tags and vpc_subnet_change_tags.subnet.tags["Description"] == ec2_vpc_subnet_description'
- '"AnotherTag" in vpc_subnet_change_tags.subnet.tags and vpc_subnet_change_tags.subnet.tags["AnotherTag"] == "SomeValue"'
- name: test state=absent (expected changed=true) - name: test state=absent (expected changed=true)
ec2_vpc_subnet: ec2_vpc_subnet:
cidr: "10.232.232.128/28" cidr: "10.232.232.128/28"
vpc_id: "{{ vpc_result.vpc.id }}" vpc_id: "{{ vpc_result.vpc.id }}"
state: absent state: absent
region: '{{ec2_region}}' region: '{{ ec2_region }}'
aws_access_key: '{{ aws_access_key }}' aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}' aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token }}' security_token: '{{ security_token }}'
@ -166,6 +209,138 @@
assert: assert:
that: that:
- 'result.changed' - 'result.changed'
# FIXME - Replace by creating IPv6 enabled VPC once ec2_vpc_net module supports it.
- name: install aws cli - FIXME temporary this should go for a lighterweight solution
command: pip install awscli
- name: Assign an Amazon provided IPv6 CIDR block to the VPC
command: aws ec2 associate-vpc-cidr-block --amazon-provided-ipv6-cidr-block --vpc-id '{{ vpc_result.vpc.id }}'
environment:
AWS_ACCESS_KEY_ID: '{{aws_access_key}}'
AWS_SECRET_ACCESS_KEY: '{{aws_secret_key}}'
AWS_SESSION_TOKEN: '{{security_token}}'
AWS_DEFAULT_REGION: '{{ec2_region}}'
- name: Get the assigned IPv6 CIDR
command: aws ec2 describe-vpcs --vpc-ids '{{ vpc_result.vpc.id }}'
environment:
AWS_ACCESS_KEY_ID: '{{aws_access_key}}'
AWS_SECRET_ACCESS_KEY: '{{aws_secret_key}}'
AWS_SESSION_TOKEN: '{{security_token}}'
AWS_DEFAULT_REGION: '{{ec2_region}}'
register: vpc_ipv6
- set_fact:
vpc_ipv6_cidr: "{{ vpc_ipv6.stdout | from_json | json_query('Vpcs[0].Ipv6CidrBlockAssociationSet[0].Ipv6CidrBlock') }}"
- name: create subnet with IPv6 (expected changed=true)
ec2_vpc_subnet:
cidr: "10.232.232.128/28"
vpc_id: "{{ vpc_result.vpc.id }}"
ipv6_cidr: "{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}"
assign_instances_ipv6: true
state: present
region: '{{ec2_region}}'
aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token }}'
tags:
Name: '{{ec2_vpc_subnet_name}}'
Description: '{{ec2_vpc_subnet_description}}'
register: vpc_subnet_ipv6_create
- name: assert creation with IPv6 happened (expected changed=true)
assert:
that:
- 'vpc_subnet_ipv6_create'
- 'vpc_subnet_ipv6_create.subnet.id.startswith("subnet-")'
- "vpc_subnet_ipv6_create.subnet.ipv6_cidr_block == '{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}'"
- '"Name" in vpc_subnet_ipv6_create.subnet.tags and vpc_subnet_ipv6_create.subnet.tags["Name"] == ec2_vpc_subnet_name'
- '"Description" in vpc_subnet_ipv6_create.subnet.tags and vpc_subnet_ipv6_create.subnet.tags["Description"] == ec2_vpc_subnet_description'
- 'vpc_subnet_ipv6_create.subnet.assign_ipv6_address_on_creation'
- name: recreate subnet (expected changed=false)
ec2_vpc_subnet:
cidr: "10.232.232.128/28"
vpc_id: "{{ vpc_result.vpc.id }}"
ipv6_cidr: "{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}"
assign_instances_ipv6: true
region: '{{ec2_region}}'
aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token }}'
state: present
tags:
Name: '{{ec2_vpc_subnet_name}}'
Description: '{{ec2_vpc_subnet_description}}'
register: vpc_subnet_ipv6_recreate
- name: assert recreation changed nothing (expected changed=false)
assert:
that:
- 'not vpc_subnet_ipv6_recreate.changed'
- 'vpc_subnet_ipv6_recreate.subnet == vpc_subnet_ipv6_create.subnet'
- name: change subnet ipv6 attribute (expected changed=true)
ec2_vpc_subnet:
cidr: "10.232.232.128/28"
vpc_id: "{{ vpc_result.vpc.id }}"
ipv6_cidr: "{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}"
assign_instances_ipv6: false
region: '{{ec2_region}}'
aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token }}'
state: present
purge_tags: false
register: vpc_change_attribute
- name: assert assign_instances_ipv6 attribute changed (expected changed=true)
assert:
that:
- 'vpc_change_attribute.changed'
- 'not vpc_change_attribute.subnet.assign_ipv6_address_on_creation'
- name: add second subnet with duplicate ipv6 cidr (expected failure)
ec2_vpc_subnet:
cidr: "10.232.232.144/28"
vpc_id: "{{ vpc_result.vpc.id }}"
ipv6_cidr: "{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}"
region: '{{ec2_region}}'
aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token }}'
state: present
purge_tags: false
register: vpc_add_duplicate_ipv6
ignore_errors: true
- name: assert graceful failure (expected failed)
assert:
that:
- 'vpc_add_duplicate_ipv6.failed'
- "'The IPv6 CIDR \\'{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}\\' conflicts with another subnet' in vpc_add_duplicate_ipv6.msg"
- name: remove subnet ipv6 cidr (expected changed=true)
ec2_vpc_subnet:
cidr: "10.232.232.128/28"
vpc_id: "{{ vpc_result.vpc.id }}"
region: '{{ec2_region}}'
aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token }}'
state: present
purge_tags: false
register: vpc_remove_ipv6_cidr
- name: assert subnet ipv6 cidr removed (expected changed=true)
assert:
that:
- 'vpc_remove_ipv6_cidr.changed'
- "vpc_remove_ipv6_cidr.subnet.ipv6_cidr_block == ''"
- 'not vpc_remove_ipv6_cidr.subnet.assign_ipv6_address_on_creation'
always: always:
################################################ ################################################
@ -177,7 +352,7 @@
cidr: "10.232.232.128/28" cidr: "10.232.232.128/28"
vpc_id: "{{ vpc_result.vpc.id }}" vpc_id: "{{ vpc_result.vpc.id }}"
state: absent state: absent
region: '{{ec2_region}}' region: '{{ ec2_region }}'
aws_access_key: '{{ aws_access_key }}' aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}' aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token }}' security_token: '{{ security_token }}'
@ -187,7 +362,7 @@
name: "{{ resource_prefix }}-vpc" name: "{{ resource_prefix }}-vpc"
state: absent state: absent
cidr_block: "10.232.232.128/26" cidr_block: "10.232.232.128/26"
region: '{{ec2_region}}' region: '{{ ec2_region }}'
aws_access_key: '{{ aws_access_key }}' aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}' aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token }}' security_token: '{{ security_token }}'