Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ec2_eni: Add check_mode support to ec2_eni #534

Merged
merged 7 commits into from
Oct 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/534-ec2_eni_add_check_mode_support.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- ec2_eni - add check mode support (https://github.com/ansible-collections/amazon.aws/pull/534).
168 changes: 99 additions & 69 deletions plugins/modules/ec2_eni.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@
'''

import time
from ipaddress import ip_address
from ipaddress import ip_network

try:
import botocore.exceptions
Expand All @@ -302,6 +304,7 @@
from ..module_utils.core import AnsibleAWSModule
from ..module_utils.core import is_boto3_error_code
from ..module_utils.ec2 import AWSRetry
from ..module_utils.ec2 import ansible_dict_to_boto3_tag_list
from ..module_utils.ec2 import get_ec2_security_group_ids_from_names
from ..module_utils.tagging import boto3_tag_list_to_ansible_dict
from ..module_utils.tagging import boto3_tag_specifications
Expand Down Expand Up @@ -441,6 +444,14 @@ def create_eni(connection, vpc_id, module):
if tags:
args["TagSpecifications"] = boto3_tag_specifications(tags, types='network-interface')

# check if provided private_ip_address is within the subnet's address range
cidr_block = connection.describe_subnets(SubnetIds=[str(subnet_id)])['Subnets'][0]['CidrBlock']
valid_private_ip = ip_address(private_ip_address) in ip_network(cidr_block)
if not valid_private_ip:
module.fail_json(changed=False, msg="Error: cannot create ENI - Address does not fall within the subnet's address range.")
if module.check_mode:
module.exit_json(changed=True, msg="Would have created ENI if not in check mode.")

eni_dict = connection.create_network_interface(aws_retry=True, **args)
eni = eni_dict["NetworkInterface"]
# Once we have an ID make sure we're always modifying the same object
Expand Down Expand Up @@ -521,43 +532,47 @@ def modify_eni(connection, module, eni):
try:
if description is not None:
if "Description" not in eni or eni["Description"] != description:
connection.modify_network_interface_attribute(
aws_retry=True,
NetworkInterfaceId=eni_id,
Description={'Value': description}
)
if not module.check_mode:
connection.modify_network_interface_attribute(
aws_retry=True,
NetworkInterfaceId=eni_id,
Description={'Value': description}
)
changed = True
if len(security_groups) > 0:
groups = get_ec2_security_group_ids_from_names(security_groups, connection, vpc_id=eni["VpcId"], boto3=True)
if sorted(get_sec_group_list(eni["Groups"])) != sorted(groups):
connection.modify_network_interface_attribute(
aws_retry=True,
NetworkInterfaceId=eni_id,
Groups=groups
)
if not module.check_mode:
connection.modify_network_interface_attribute(
aws_retry=True,
NetworkInterfaceId=eni_id,
Groups=groups
)
changed = True
if source_dest_check is not None:
if "SourceDestCheck" not in eni or eni["SourceDestCheck"] != source_dest_check:
connection.modify_network_interface_attribute(
aws_retry=True,
NetworkInterfaceId=eni_id,
SourceDestCheck={'Value': source_dest_check}
)
if not module.check_mode:
connection.modify_network_interface_attribute(
aws_retry=True,
NetworkInterfaceId=eni_id,
SourceDestCheck={'Value': source_dest_check}
)
changed = True
if delete_on_termination is not None and "Attachment" in eni:
if eni["Attachment"]["DeleteOnTermination"] is not delete_on_termination:
connection.modify_network_interface_attribute(
aws_retry=True,
NetworkInterfaceId=eni_id,
Attachment={'AttachmentId': eni["Attachment"]["AttachmentId"],
'DeleteOnTermination': delete_on_termination}
)
if not module.check_mode:
connection.modify_network_interface_attribute(
aws_retry=True,
NetworkInterfaceId=eni_id,
Attachment={'AttachmentId': eni["Attachment"]["AttachmentId"],
'DeleteOnTermination': delete_on_termination}
)
if delete_on_termination:
waiter = "network_interface_delete_on_terminate"
else:
waiter = "network_interface_no_delete_on_terminate"
get_waiter(connection, waiter).wait(NetworkInterfaceIds=[eni_id])
changed = True
if delete_on_termination:
waiter = "network_interface_delete_on_terminate"
else:
waiter = "network_interface_no_delete_on_terminate"
get_waiter(connection, waiter).wait(NetworkInterfaceIds=[eni_id])

current_secondary_addresses = []
if "PrivateIpAddresses" in eni:
Expand All @@ -566,65 +581,71 @@ def modify_eni(connection, module, eni):
if secondary_private_ip_addresses is not None:
secondary_addresses_to_remove = list(set(current_secondary_addresses) - set(secondary_private_ip_addresses))
if secondary_addresses_to_remove and purge_secondary_private_ip_addresses:
connection.unassign_private_ip_addresses(
aws_retry=True,
NetworkInterfaceId=eni_id,
PrivateIpAddresses=list(set(current_secondary_addresses) - set(secondary_private_ip_addresses)),
)
wait_for(absent_ips, connection, secondary_addresses_to_remove, module, eni_id)
if not module.check_mode:
connection.unassign_private_ip_addresses(
aws_retry=True,
NetworkInterfaceId=eni_id,
PrivateIpAddresses=list(set(current_secondary_addresses) - set(secondary_private_ip_addresses)),
)
wait_for(absent_ips, connection, secondary_addresses_to_remove, module, eni_id)
changed = True
secondary_addresses_to_add = list(set(secondary_private_ip_addresses) - set(current_secondary_addresses))
if secondary_addresses_to_add:
connection.assign_private_ip_addresses(
aws_retry=True,
NetworkInterfaceId=eni_id,
PrivateIpAddresses=secondary_addresses_to_add,
AllowReassignment=allow_reassignment
)
wait_for(correct_ips, connection, secondary_addresses_to_add, module, eni_id)
if not module.check_mode:
connection.assign_private_ip_addresses(
aws_retry=True,
NetworkInterfaceId=eni_id,
PrivateIpAddresses=secondary_addresses_to_add,
AllowReassignment=allow_reassignment
)
wait_for(correct_ips, connection, secondary_addresses_to_add, module, eni_id)
changed = True

if secondary_private_ip_address_count is not None:
current_secondary_address_count = len(current_secondary_addresses)
if secondary_private_ip_address_count > current_secondary_address_count:
connection.assign_private_ip_addresses(
aws_retry=True,
NetworkInterfaceId=eni_id,
SecondaryPrivateIpAddressCount=(secondary_private_ip_address_count - current_secondary_address_count),
AllowReassignment=allow_reassignment
)
wait_for(correct_ip_count, connection, secondary_private_ip_address_count, module, eni_id)
if not module.check_mode:
connection.assign_private_ip_addresses(
aws_retry=True,
NetworkInterfaceId=eni_id,
SecondaryPrivateIpAddressCount=(secondary_private_ip_address_count - current_secondary_address_count),
AllowReassignment=allow_reassignment
)
wait_for(correct_ip_count, connection, secondary_private_ip_address_count, module, eni_id)
changed = True
elif secondary_private_ip_address_count < current_secondary_address_count:
# How many of these addresses do we want to remove
secondary_addresses_to_remove_count = current_secondary_address_count - secondary_private_ip_address_count
connection.unassign_private_ip_addresses(
aws_retry=True,
NetworkInterfaceId=eni_id,
PrivateIpAddresses=current_secondary_addresses[:secondary_addresses_to_remove_count]
)
wait_for(correct_ip_count, connection, secondary_private_ip_address_count, module, eni_id)
if not module.check_mode:
secondary_addresses_to_remove_count = current_secondary_address_count - secondary_private_ip_address_count
connection.unassign_private_ip_addresses(
aws_retry=True,
NetworkInterfaceId=eni_id,
PrivateIpAddresses=current_secondary_addresses[:secondary_addresses_to_remove_count]
)
wait_for(correct_ip_count, connection, secondary_private_ip_address_count, module, eni_id)
changed = True

if attached is True:
if "Attachment" in eni and eni["Attachment"]["InstanceId"] != instance_id:
detach_eni(connection, eni, module)
connection.attach_network_interface(
aws_retry=True,
InstanceId=instance_id,
DeviceIndex=device_index,
NetworkInterfaceId=eni_id,
)
get_waiter(connection, 'network_interface_attached').wait(NetworkInterfaceIds=[eni_id])
if not module.check_mode:
detach_eni(connection, eni, module)
connection.attach_network_interface(
aws_retry=True,
InstanceId=instance_id,
DeviceIndex=device_index,
NetworkInterfaceId=eni_id,
)
get_waiter(connection, 'network_interface_attached').wait(NetworkInterfaceIds=[eni_id])
changed = True
if "Attachment" not in eni:
connection.attach_network_interface(
aws_retry=True,
InstanceId=instance_id,
DeviceIndex=device_index,
NetworkInterfaceId=eni_id,
)
get_waiter(connection, 'network_interface_attached').wait(NetworkInterfaceIds=[eni_id])
if not module.check_mode:
connection.attach_network_interface(
aws_retry=True,
InstanceId=instance_id,
DeviceIndex=device_index,
NetworkInterfaceId=eni_id,
)
get_waiter(connection, 'network_interface_attached').wait(NetworkInterfaceIds=[eni_id])
changed = True

elif attached is False:
Expand All @@ -637,6 +658,8 @@ def modify_eni(connection, module, eni):
module.fail_json_aws(e, "Failed to modify eni {0}".format(eni_id))

eni = describe_eni(connection, module, eni_id)
if module.check_mode and changed:
module.exit_json(changed=changed, msg="Would have modified ENI: {0} if not in check mode".format(eni['NetworkInterfaceId']))
module.exit_json(changed=changed, interface=get_eni_info(eni))


Expand All @@ -656,6 +679,9 @@ def delete_eni(connection, module):
if not eni:
module.exit_json(changed=False)

if module.check_mode:
module.exit_json(changed=True, msg="Would have deleted ENI if not in check mode.")

eni_id = eni["NetworkInterfaceId"]
force_detach = module.params.get("force_detach")

Expand Down Expand Up @@ -683,6 +709,9 @@ def delete_eni(connection, module):

def detach_eni(connection, eni, module):

if module.check_mode:
module.exit_json(changed=True, msg="Would have detached ENI if not in check mode.")

attached = module.params.get("attached")
eni_id = eni["NetworkInterfaceId"]

Expand Down Expand Up @@ -836,7 +865,8 @@ def main():
required_if=([
('attached', True, ['instance_id']),
('purge_secondary_private_ip_addresses', True, ['secondary_private_ip_addresses'])
])
]),
supports_check_mode=True,
)

retry_decorator = AWSRetry.jittered_backoff(
Expand Down
58 changes: 58 additions & 0 deletions tests/integration/targets/ec2_eni/tasks/test_attachment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@
- "{{ instance_id_2 }}"
wait: True

- name: attach the network interface to instance 1 (check mode)
ec2_eni:
instance_id: "{{ instance_id_1 }}"
device_index: 1
private_ip_address: "{{ ip_1 }}"
subnet_id: "{{ vpc_subnet_result.subnet.id }}"
state: present
attached: True
check_mode: true
register: result_check_mode

- assert:
that:
- result_check_mode.changed

- name: attach the network interface to instance 1
ec2_eni:
instance_id: "{{ instance_id_1 }}"
Expand Down Expand Up @@ -84,6 +99,21 @@
_interface_0: '{{ eni_info.network_interfaces[0] }}'

# ============================================================
- name: test attaching the network interface to a different instance (check mode)
ec2_eni:
instance_id: "{{ instance_id_2 }}"
device_index: 1
private_ip_address: "{{ ip_1 }}"
subnet_id: "{{ vpc_subnet_result.subnet.id }}"
state: present
attached: True
check_mode: true
register: result_check_mode

- assert:
that:
- result_check_mode.changed

- name: test attaching the network interface to a different instance
ec2_eni:
instance_id: "{{ instance_id_2 }}"
Expand All @@ -109,6 +139,21 @@
_interface_0: '{{ eni_info.network_interfaces[0] }}'

# ============================================================
- name: detach the network interface (check mode)
ec2_eni:
instance_id: "{{ instance_id_2 }}"
device_index: 1
private_ip_address: "{{ ip_1 }}"
subnet_id: "{{ vpc_subnet_result.subnet.id }}"
state: present
attached: False
check_mode: true
register: result_check_mode

- assert:
that:
- result_check_mode.changed

- name: detach the network interface
ec2_eni:
instance_id: "{{ instance_id_2 }}"
Expand Down Expand Up @@ -182,6 +227,19 @@
- "{{ instance_id_2 }}"
wait: True

- name: delete an attached network interface with force_detach (check mode)
ec2_eni:
force_detach: True
eni_id: "{{ eni_id_1 }}"
state: absent
check_mode: true
register: result_check_mode
ignore_errors: True

- assert:
that:
- result_check_mode.changed

- name: delete an attached network interface with force_detach
ec2_eni:
force_detach: True
Expand Down
26 changes: 26 additions & 0 deletions tests/integration/targets/ec2_eni/tasks/test_deletion.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
---
# ============================================================
- name: test deleting the unattached network interface by using the ID (check mode)
ec2_eni:
eni_id: "{{ eni_id_1 }}"
name: "{{ resource_prefix }}"
subnet_id: "{{ vpc_subnet_id }}"
state: absent
check_mode: True
register: result_check_mode

- assert:
that:
- result_check_mode.changed

- name: test deleting the unattached network interface by using the ID
ec2_eni:
eni_id: "{{ eni_id_1 }}"
Expand All @@ -18,6 +31,19 @@
- '"network_interfaces" in eni_info'
- eni_id_1 not in ( eni_info.network_interfaces | selectattr('id') | map(attribute='id') | list )

- name: test removing the network interface by ID is idempotent (check mode)
ec2_eni:
eni_id: "{{ eni_id_1 }}"
name: "{{ resource_prefix }}"
subnet_id: "{{ vpc_subnet_id }}"
state: absent
check_mode: True
register: result_check_mode

- assert:
that:
- not result_check_mode.changed

- name: test removing the network interface by ID is idempotent
ec2_eni:
eni_id: "{{ eni_id_1 }}"
Expand Down
Loading