Repository: ansible-collections/amazon.aws

Issue:
ec2_instance - attach network interfaces issue when using check_mode
### Summary

When trying to attach a new network interface to a running instance with check_mode, the module performs the update instead of just validating if this will be done.

### Issue Type

Bug Report

### Component Name

ec2_instance

### Ansible Version

```console (paste below)
$ ansible --version

```


### Collection Versions

```console (paste below)
$ ansible-galaxy collection list
```


### AWS SDK versions

```console (paste below)
$ pip show boto boto3 botocore
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed

```


### OS / Environment

_No response_

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
- hosts: localhost
  gather_facts: false

  collections:
    - amazon.aws
 
  vars:
    vpc_name: "aubin-test-ec2-0303-2"
    vpc_cidr: "10.1.0.0/16"
    subnet_cidr: "10.1.1.0/24"
    security_group_name: "aubin-test-sg-01"
    ec2_name: "aubin-0303-ec2"
    ec2_instance_type: "t3.nano"
    ec2_ami_id: "ami-0b1236a14f61e87b5"

  tasks:
    - name: "Create VPC for use in testing"
      ec2_vpc_net:
        state: present
        name: "{{ vpc_name }}"
        cidr_block: "{{ vpc_cidr }}"
        tags:
          Name: "{{ vpc_name }}"
        tenancy: default
      register: testing_vpc

    - name: "Create default subnet"
      ec2_vpc_subnet:
        state: present
        vpc_id: "{{ testing_vpc.vpc.id }}"
        cidr: "{{ subnet_cidr }}"
      register: testing_subnet
    
    - name: "create a security group with the vpc"
      ec2_security_group:
        name: "{{ security_group_name }}"
        description: security group for ansible tests
        vpc_id: "{{ testing_vpc.vpc.id }}"
        rules:
          - proto: tcp
            from_port: 22
            to_port: 22
            cidr_ip: 0.0.0.0/0
          - proto: tcp
            from_port: 80
            to_port: 80
            cidr_ip: 0.0.0.0/0
      register: sg

    - name: create an Elastic network interface
      ec2_eni:
        state: present
        delete_on_termination: true
        subnet_id: "{{ testing_subnet.subnet.id }}"
        security_groups:
          - "{{ sg.group_id }}"
      register: enis
      with_items: "{{ range(2) }}"
    
    - name: Create instance
      ec2_instance:
        state: running
        name: "{{ ec2_name }}"
        image_id: "{{ ec2_ami_id }}"
        vpc_subnet_id: "{{ testing_subnet.subnet.id }}"
        instance_type: "{{ ec2_instance_type }}"
        network:
          interfaces:
            - id: "{{ enis.results.0.interface.id }}"
        wait: true
      register: create_instance
    
    - assert:
        that:
          - create_instance is changed
    
    - name: "Gather instance info"
      ec2_instance_info:
        filters:
          "tag:Name": "{{ ec2_name }}"
          instance-state-name: running
      register: _instances_info
    
    - assert:
        that:
          - _instances_info.instances | length == 1

    - name: Attach network interface(s) to the instance
      ec2_instance:
        state: running
        name: "{{ ec2_name }}"
        image_id: "{{ ec2_ami_id }}"
        vpc_subnet_id: "{{ testing_subnet.subnet.id }}"
        network:
          interfaces:
            - id: "{{ enis.results.0.interface.id }}"
            - id: "{{ enis.results.1.interface.id }}"
        instance_type: "{{ ec2_instance_type }}"
        wait: true
      register: create_with_eni
      check_mode: true
    
    - assert:
        that:
          - create_with_eni is changed
    
    - name: "Gather instance info"
      ec2_instance_info:
        filters:
          "tag:Name": "{{ ec2_name }}"
          instance-state-name: running
      register: _instances_info
    
    - assert:
        that:
          - _instances_info.instances | length == 1
          - _instances_info.instances.0.network_interfaces | length == 1

```


### Expected Results

module should not attach interface to the instance

### Actual Results

```console (paste below)

```
module attach the network interface to the instance

### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/ec2_instance.py`](https://github.com/['ansible-collections/amazon.aws', 'ansible-collections/community.aws', 'ansible-collections/community.vmware']/blob/main/plugins/modules/ec2_instance.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @jillr @ryansb @s-hertel @tremble
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/plugins/modules/ec2_instance.py b/plugins/modules/ec2_instance.py
--- a/plugins/modules/ec2_instance.py
+++ b/plugins/modules/ec2_instance.py
@@ -1502,16 +1502,19 @@ def change_network_attachments(instance, params):
         # network.interfaces can create the need to attach new interfaces
         old_ids = [inty['NetworkInterfaceId'] for inty in instance['NetworkInterfaces']]
         to_attach = set(new_ids) - set(old_ids)
-        for eni_id in to_attach:
-            try:
-                client.attach_network_interface(
-                    aws_retry=True,
-                    DeviceIndex=new_ids.index(eni_id),
-                    InstanceId=instance['InstanceId'],
-                    NetworkInterfaceId=eni_id,
-                )
-            except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
-                module.fail_json_aws(e, msg="Could not attach interface {0} to instance {1}".format(eni_id, instance['InstanceId']))
+        if not module.check_mode:
+            for eni_id in to_attach:
+                try:
+                    client.attach_network_interface(
+                        aws_retry=True,
+                        DeviceIndex=new_ids.index(eni_id),
+                        InstanceId=instance["InstanceId"],
+                        NetworkInterfaceId=eni_id,
+                    )
+                except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
+                    module.fail_json_aws(
+                        e, msg=f"Could not attach interface {eni_id} to instance {instance['InstanceId']}"
+                    )
         return bool(len(to_attach))
     return False
 


Test patch:
diff --git a/tests/integration/targets/ec2_instance_external_resource_attach/tasks/main.yml b/tests/integration/targets/ec2_instance_external_resource_attach/tasks/main.yml
--- a/tests/integration/targets/ec2_instance_external_resource_attach/tasks/main.yml
+++ b/tests/integration/targets/ec2_instance_external_resource_attach/tasks/main.yml
@@ -60,6 +60,39 @@
         - 'in_test_vpc_instance.instances.0.key_name == "{{ resource_prefix }}_test_key"'
         - '(in_test_vpc_instance.instances.0.network_interfaces | length) == 1'
 
+  - name: "Add a second interface (check_mode=true)"
+    ec2_instance:
+      state: present
+      name: "{{ resource_prefix }}-test-eni-vpc"
+      network:
+        interfaces:
+          - id: "{{ eni_a.interface.id }}"
+          - id: "{{ eni_b.interface.id }}"
+      image_id: "{{ ec2_ami_id }}"
+      tags:
+        TestId: "{{ ec2_instance_tag_TestId }}"
+      instance_type: "{{ ec2_instance_type }}"
+      wait: false
+    register: add_interface_check_mode
+    check_mode: true
+
+  - name: Validate task reported changed
+    assert:
+      that:
+        - add_interface_check_mode is changed
+
+  - name: "Gather {{ resource_prefix }}-test-eni-vpc info"
+    ec2_instance_info:
+      filters:
+        "tag:Name": '{{ resource_prefix }}-test-eni-vpc'
+    register: in_test_vpc_instance
+
+  - name: Validate that only 1 ENI is attached to instance as we run using check_mode=true
+    assert:
+      that:
+        - 'in_test_vpc_instance.instances.0.key_name == "{{ resource_prefix }}_test_key"'
+        - '(in_test_vpc_instance.instances.0.network_interfaces | length) == 1'
+
   - name: "Add a second interface"
     ec2_instance:
       state: present
@@ -75,9 +108,25 @@
       wait: false
     register: add_interface
     until: add_interface is not failed
-    ignore_errors: yes
+    ignore_errors: true
     retries: 10
 
+  - name: Validate that the instance has now 2 interfaces attached
+    block:
+      - name: "Gather {{ resource_prefix }}-test-eni-vpc info"
+        ec2_instance_info:
+          filters:
+            "tag:Name": '{{ resource_prefix }}-test-eni-vpc'
+        register: in_test_vpc_instance
+
+      - name: Validate that only 1 ENI is attached to instance as we run using check_mode=true
+        assert:
+          that:
+            - 'in_test_vpc_instance.instances.0.key_name == "{{ resource_prefix }}_test_key"'
+            - '(in_test_vpc_instance.instances.0.network_interfaces | length) == 2'
+
+    when: add_interface is successful
+
   - name: "Make instance in the testing subnet created in the test VPC(check mode)"
     ec2_instance:
       state: present