Skip to content

Commit

Permalink
vpc_net check mode, IPV6 CIDR assoc/disassoc (#631)
Browse files Browse the repository at this point in the history
vpc_net check mode, IPV6 CIDR assoc/disassoc

SUMMARY

Implement check mode correctly for the ec2_vpc_net module. The module was incorrectly making actual changes when executed in check mode.

In check mode, do not change the configuration. Previously the module was making VPC changes in the following scenarios:

Association with IPv4 CIDR or IPv6 CIDR.
Disassociation from IPv4 CIDR or IPv6 CIDR.


Handle case when Amazon-provided ipv6 block is enabled, then disabled, then enabled again.
Do not disable IPv6 CIDR association (using Amazon pool) if ipv6_cidr property is not present in the task. If the VPC already exists and ipv6_cidr property, retain the current config.
Add integration tests:

Enable, disable, then re-enable Amazon-provided IPv6 CIDR.
When VPC already exists and ipv6_cidr property is not specified, validate this does not disable IPv6 CIDR association.




ISSUE TYPE


Bugfix Pull Request

COMPONENT NAME

ec2_vpc_net
ADDITIONAL INFORMATION

Reviewed-by: Sebastien Rosset <None>
Reviewed-by: Alina Buzachis <None>
Reviewed-by: Joseph Torcasso <None>
Reviewed-by: Markus Bergholz <git@osuv.de>
  • Loading branch information
sebastien-rosset committed Mar 21, 2022
1 parent 1cd572b commit 3e24a37
Show file tree
Hide file tree
Showing 5 changed files with 313 additions and 49 deletions.
6 changes: 6 additions & 0 deletions changelogs/fragments/631-ec2_vpc_net-check_mode.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
bugfixes:
- >-
ec2_vpc_net - In check mode, ensure the module does not change the configuration.
Handle case when Amazon-provided ipv6 block is enabled, then disabled, then enabled again.
Do not disable IPv6 CIDR association (using Amazon pool) if ipv6_cidr property is not present in the task.
If the VPC already exists and ipv6_cidr property, retain the current config (https://github.com/ansible-collections/amazon.aws/pull/631).
120 changes: 101 additions & 19 deletions plugins/modules/ec2_vpc_net.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@
elements: str
ipv6_cidr:
description:
- Request an Amazon-provided IPv6 CIDR block with /56 prefix length. You cannot specify the range of IPv6 addresses,
- Request an Amazon-provided IPv6 CIDR block with /56 prefix length. You cannot specify the range of IPv6 addresses,
or the size of the CIDR block.
default: False
- Default value is C(false) when creating a new VPC.
type: bool
purge_cidrs:
description:
Expand Down Expand Up @@ -331,7 +331,7 @@ def create_vpc(connection, module, cidr_block, tenancy):
if not module.check_mode:
vpc_obj = connection.create_vpc(CidrBlock=cidr_block, InstanceTenancy=tenancy, aws_retry=True)
else:
module.exit_json(changed=True)
module.exit_json(changed=True, msg="VPC would be created if not in check mode")
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, "Failed to create the VPC")

Expand Down Expand Up @@ -369,6 +369,40 @@ def wait_for_vpc_attribute(connection, module, vpc_id, attribute, expected_value
module.fail_json(msg="Failed to wait for {0} to be updated".format(attribute))


def wait_for_vpc_ipv6_state(module, connection, vpc_id, ipv6_assoc_state):
"""
If ipv6_assoc_state is True, wait for VPC to be associated with at least one Amazon-provided IPv6 CIDR block.
If ipv6_assoc_state is False, wait for VPC to be dissociated from all Amazon-provided IPv6 CIDR blocks.
"""
start_time = time()
criteria_match = False
while time() < start_time + 300:
current_value = get_vpc(module, connection, vpc_id)
if current_value:
ipv6_set = current_value.get('Ipv6CidrBlockAssociationSet')
if ipv6_set:
if ipv6_assoc_state:
# At least one 'Amazon' IPv6 CIDR block must be associated.
for val in ipv6_set:
if val.get('Ipv6Pool') == 'Amazon' and val.get("Ipv6CidrBlockState").get("State") == "associated":
criteria_match = True
break
if criteria_match:
break
else:
# All 'Amazon' IPv6 CIDR blocks must be disassociated.
expected_count = sum(
[(val.get("Ipv6Pool") == "Amazon") for val in ipv6_set])
actual_count = sum([(val.get('Ipv6Pool') == 'Amazon' and
val.get("Ipv6CidrBlockState").get("State") == "disassociated") for val in ipv6_set])
if actual_count == expected_count:
criteria_match = True
break
sleep(3)
if not criteria_match:
module.fail_json(msg="Failed to wait for IPv6 CIDR association")


def get_cidr_network_bits(module, cidr_block):
fixed_cidrs = []
for cidr in cidr_block:
Expand All @@ -391,7 +425,7 @@ def main():
argument_spec = dict(
name=dict(required=True),
cidr_block=dict(type='list', required=True, elements='str'),
ipv6_cidr=dict(type='bool', default=False),
ipv6_cidr=dict(type='bool', default=None),
tenancy=dict(choices=['default', 'dedicated'], default='default'),
dns_support=dict(type='bool', default=True),
dns_hostnames=dict(type='bool', default=True),
Expand Down Expand Up @@ -435,12 +469,25 @@ def main():

# Check if VPC exists
vpc_id = vpc_exists(module, connection, name, cidr_block, multi)

is_new_vpc = False
if vpc_id is None:
is_new_vpc = True
vpc_id = create_vpc(connection, module, cidr_block[0], tenancy)
changed = True
if ipv6_cidr is None:
# default value when creating new VPC.
ipv6_cidr = False

vpc_obj = get_vpc(module, connection, vpc_id)
if not is_new_vpc and ipv6_cidr is None:
# 'ipv6_cidr' wasn't specified in the task.
# Retain the value from the existing VPC.
ipv6_cidr = False
if 'Ipv6CidrBlockAssociationSet' in vpc_obj.keys():
for ipv6_assoc in vpc_obj['Ipv6CidrBlockAssociationSet']:
if ipv6_assoc['Ipv6Pool'] == 'Amazon' and ipv6_assoc['Ipv6CidrBlockState']['State'] in ['associated', 'associating']:
ipv6_cidr = True
break

associated_cidrs = dict((cidr['CidrBlock'], cidr['AssociationId']) for cidr in vpc_obj.get('CidrBlockAssociationSet', [])
if cidr['CidrBlockState']['State'] != 'disassociated')
Expand All @@ -451,26 +498,59 @@ def main():
if len(cidr_block) > 1:
for cidr in to_add:
changed = True
try:
connection.associate_vpc_cidr_block(CidrBlock=cidr, VpcId=vpc_id, aws_retry=True)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, "Unable to associate CIDR {0}.".format(ipv6_cidr))
if not module.check_mode:
try:
connection.associate_vpc_cidr_block(CidrBlock=cidr, VpcId=vpc_id, aws_retry=True)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, "Unable to associate CIDR {0}.".format(ipv6_cidr))
if ipv6_cidr:
if 'Ipv6CidrBlockAssociationSet' not in vpc_obj.keys():
try:
connection.associate_vpc_cidr_block(AmazonProvidedIpv6CidrBlock=ipv6_cidr, VpcId=vpc_id, aws_retry=True)
changed = True
if not module.check_mode:
try:
connection.associate_vpc_cidr_block(AmazonProvidedIpv6CidrBlock=ipv6_cidr, VpcId=vpc_id, aws_retry=True)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, "Unable to associate CIDR {0}.".format(ipv6_cidr))
else:
# If the VPC has been created with IPv6 CIDR, and the ipv6 blocks were subsequently
# disassociated, a Amazon-provide block must be associate a new block.
assoc_needed = True
for ipv6_assoc in vpc_obj['Ipv6CidrBlockAssociationSet']:
if ipv6_assoc['Ipv6Pool'] == 'Amazon' and ipv6_assoc['Ipv6CidrBlockState']['State'] in ['associated', 'associating']:
assoc_needed = False
break
if assoc_needed:
changed = True
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, "Unable to associate CIDR {0}.".format(ipv6_cidr))

if not module.check_mode:
try:
connection.associate_vpc_cidr_block(AmazonProvidedIpv6CidrBlock=ipv6_cidr, VpcId=vpc_id, aws_retry=True)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, "Unable to associate CIDR {0}.".format(ipv6_cidr))
wait_for_vpc_ipv6_state(module, connection, vpc_id, True)
else:
# ipv6_cidr is False
if 'Ipv6CidrBlockAssociationSet' in vpc_obj.keys() and len(vpc_obj['Ipv6CidrBlockAssociationSet']) > 0:
assoc_disable = False
for ipv6_assoc in vpc_obj['Ipv6CidrBlockAssociationSet']:
if ipv6_assoc['Ipv6Pool'] == 'Amazon' and ipv6_assoc['Ipv6CidrBlockState']['State'] in ['associated', 'associating']:
assoc_disable = True
changed = True
if not module.check_mode:
try:
connection.disassociate_vpc_cidr_block(AssociationId=ipv6_assoc['AssociationId'], aws_retry=True)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, "Unable to disassociate IPv6 CIDR {0}.".format(ipv6_assoc['AssociationId']))
if assoc_disable and not module.check_mode:
wait_for_vpc_ipv6_state(module, connection, vpc_id, False)
if purge_cidrs:
for association_id in to_remove:
changed = True
try:
connection.disassociate_vpc_cidr_block(AssociationId=association_id, aws_retry=True)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, "Unable to disassociate {0}. You must detach or delete all gateways and resources that "
"are associated with the CIDR block before you can disassociate it.".format(association_id))
if not module.check_mode:
try:
connection.disassociate_vpc_cidr_block(AssociationId=association_id, aws_retry=True)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, "Unable to disassociate {0}. You must detach or delete all gateways and resources that "
"are associated with the CIDR block before you can disassociate it.".format(association_id))

if dhcp_id is not None:
try:
Expand All @@ -495,6 +575,7 @@ def main():
connection.modify_vpc_attribute(VpcId=vpc_id, EnableDnsSupport={'Value': dns_support}, aws_retry=True)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, "Failed to update enabled dns support attribute")

if current_dns_hostnames != dns_hostnames:
changed = True
if not module.check_mode:
Expand Down Expand Up @@ -532,6 +613,7 @@ def main():
if not module.check_mode:
connection.delete_vpc(VpcId=vpc_id, aws_retry=True)
changed = True

except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Failed to delete VPC {0} You may want to use the ec2_vpc_subnet, ec2_vpc_igw, "
"and/or ec2_vpc_route_table modules to ensure the other components are absent.".format(vpc_id))
Expand Down
29 changes: 22 additions & 7 deletions plugins/modules/ec2_vpc_route_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,19 @@
- Required when I(lookup=id).
type: str
routes:
description: List of routes in the route table.
Routes are specified as dicts containing the keys 'dest' and one of 'gateway_id',
'instance_id', 'network_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.
description:
- >
List of routes in the route table.
- >
Routes are specified as dicts containing the keys 'dest' and one of 'gateway_id',
'instance_id', 'network_interface_id', or 'vpc_peering_connection_id'.
- >
The value of 'dest' is used for the destination match. It may be a IPv4 CIDR block
or a IPv6 CIDR block.
- >
If 'gateway_id' is specified, you can refer to the VPC's IGW by using the value 'igw'.
- >
Routes are required for present states.
type: list
elements: dict
state:
Expand All @@ -61,7 +69,7 @@
type: str
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'.
by either subnet ID, Name tag, or by a CIDR such as '10.0.0.0/24' or 'fd00::/8'.
type: list
elements: str
tags:
Expand Down Expand Up @@ -98,6 +106,8 @@
routes:
- dest: 0.0.0.0/0
gateway_id: "{{ igw.gateway_id }}"
- dest: ::/0
gateway_id: "{{ igw.gateway_id }}"
register: public_route_table
- name: Set up NAT-protected route table
Expand Down Expand Up @@ -176,10 +186,15 @@
type: complex
contains:
destination_cidr_block:
description: CIDR block of destination
description: IPv4 CIDR block of destination
returned: always
type: str
sample: 10.228.228.0/22
destination_ipv6_cidr_block:
description: IPv6 CIDR block of destination
returned: when the route includes an IPv6 destination
type: str
sample: 2600:1f1c:1b3:8f00:8000::/65
gateway_id:
description: ID of the gateway
returned: when gateway is local or internet gateway
Expand Down
86 changes: 86 additions & 0 deletions tests/integration/targets/ec2_vpc_net/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,22 @@
- result.vpc.tags.Name == resource_prefix
- result.vpc.id == vpc_1

- name: No-op VPC configuration, missing ipv6_cidr property
ec2_vpc_net:
state: present
cidr_block: "{{ vpc_cidr }}"
name: "{{ resource_prefix }}"
# Intentionaly commenting out 'ipv6_cidr'
# When the 'ipv6_cidr' property is missing, the VPC should retain its configuration.
# That should not cause the module to set default value 'false' and disassociate the IPv6 block.
#ipv6_cidr: True
register: result
- name: assert configuration did not change
assert:
that:
- result is successful
- result is not changed

# ============================================================

- name: VPC info (no filters)
Expand Down Expand Up @@ -1251,6 +1267,76 @@

# ============================================================

- name: Remove IPv6 CIDR association from VPC in check mode
ec2_vpc_net:
state: present
cidr_block: "{{ vpc_cidr }}"
name: "{{ resource_prefix }}"
ipv6_cidr: False
check_mode: true
register: result
- name: assert configuration would change
assert:
that:
- result is successful
- result is changed

- name: Set IPv6 CIDR association to VPC, no change expected
# I.e. assert the previous ec2_vpc_net task in check_mode did not
# mistakenly modify the VPC configuration.
ec2_vpc_net:
state: present
cidr_block: "{{ vpc_cidr }}"
name: "{{ resource_prefix }}"
ipv6_cidr: True
register: result
- name: assert configuration did not change
assert:
that:
- result is successful
- result is not changed

- name: Remove IPv6 CIDR association from VPC
ec2_vpc_net:
state: present
cidr_block: "{{ vpc_cidr }}"
name: "{{ resource_prefix }}"
ipv6_cidr: False
register: result
- name: assert IPv6 CIDR association removed from VPC
assert:
that:
- result is successful
- result is changed
- result.vpc.ipv6_cidr_block_association_set | length == 1
- result.vpc.ipv6_cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-")
- result.vpc.ipv6_cidr_block_association_set[0].ipv6_cidr_block | ansible.netcommon.ipv6
- result.vpc.ipv6_cidr_block_association_set[0].ipv6_cidr_block_state.state in ["disassociated"]

- name: Add IPv6 CIDR association to VPC again
ec2_vpc_net:
state: present
cidr_block: "{{ vpc_cidr }}"
name: "{{ resource_prefix }}"
ipv6_cidr: True
register: result
- name: assert configuration change
assert:
that:
- result is successful
- result is changed
# Because the IPv6 CIDR was associated, then disassociated, then reassociated,
# now there should be one disassociated block and one associated block.
- result.vpc.ipv6_cidr_block_association_set | length == 2
- result.vpc.ipv6_cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-")
- result.vpc.ipv6_cidr_block_association_set[0].ipv6_cidr_block | ansible.netcommon.ipv6
- result.vpc.ipv6_cidr_block_association_set[0].ipv6_cidr_block_state.state in ["disassociated", "disassociating"]
- result.vpc.ipv6_cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-")
- result.vpc.ipv6_cidr_block_association_set[1].ipv6_cidr_block | ansible.netcommon.ipv6
- result.vpc.ipv6_cidr_block_association_set[1].ipv6_cidr_block_state.state in ["associated", "associating"]

# ============================================================

- name: test check mode to delete a VPC
ec2_vpc_net:
cidr_block: "{{ vpc_cidr }}"
Expand Down
Loading

0 comments on commit 3e24a37

Please sign in to comment.