Repository: getmoto/moto
Pull request #8415 (2024-12-17 16:33:19)

EC2: Remove internal Network Interface traces from the Subnet after deletion
Hello! Issue #7896 was not completely fixed, the test code in the bug report did not run completely through. The fixed IP address of the deleted instance was still kept in an internal field of the Subnet, so that another instance with the same IP address could not get created.
I tried fixing this, and added a test. All EC2 tests pass, except two that seem unrelated and also fail for me on current master (one in test_availability_zones_and_regions.py and one in test_security_groups.py).
Thanks!

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8415?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.59%. Comparing base [(`3c4c72e`)](https://app.codecov.io/gh/getmoto/moto/commit/3c4c72e1c9282e920022afaa7ad818472e9fbf50?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`687b9dd`)](https://app.codecov.io/gh/getmoto/moto/commit/687b9ddef70e08993bac253e0823ddc427f69f2b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #8415   +/-   ##
=======================================
  Coverage   94.59%   94.59%           
=======================================
  Files        1163     1163           
  Lines      101636   101638    +2     
=======================================
+ Hits        96147    96149    +2     
  Misses       5489     5489           
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8415/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/8415/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.83% <0.00%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8415/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.57% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/8415?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/moto/ec2/models/elastic_network_interfaces.py b/moto/ec2/models/elastic_network_interfaces.py
index 8b6b05437..78b8e1e8e 100644
--- a/moto/ec2/models/elastic_network_interfaces.py
+++ b/moto/ec2/models/elastic_network_interfaces.py
@@ -204,6 +204,8 @@ class NetworkInterface(TaggedEC2Resource, CloudFormationModel):
 
     def delete(self) -> None:
         self.ec2_backend.delete_network_interface(eni_id=self.id)
+        for priv_ip in self.private_ip_addresses:
+            self.subnet.del_subnet_ip(priv_ip["PrivateIpAddress"])
 
     def stop(self) -> None:
         if self.public_ip_auto_assign:


Test patch:
diff --git a/tests/test_ec2/test_elastic_network_interfaces.py b/tests/test_ec2/test_elastic_network_interfaces.py
index cd68efca3..2980fbdcb 100644
--- a/tests/test_ec2/test_elastic_network_interfaces.py
+++ b/tests/test_ec2/test_elastic_network_interfaces.py
@@ -1020,6 +1020,37 @@ def test_create_instance__termination_deletes_eni(delete_eni):
         ec2_client.delete_network_interface(NetworkInterfaceId=eni_id)
 
 
+@mock_aws
+def test_recreate_instance_with_same_ip_address():
+    ssm = boto3.client("ssm", "us-east-1")
+    kernel_61 = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64"
+    ami_id = ssm.get_parameter(Name=kernel_61)["Parameter"]["Value"]
+
+    ec2_client = boto3.client("ec2", "us-east-1")
+    existing_subnet_id = ec2_client.describe_subnets()["Subnets"][0]["SubnetId"]
+
+    for _ in range(2):
+        # Second attempt must not throw an exception about a used IP
+        instance_id = ec2_client.run_instances(
+            MaxCount=1,
+            MinCount=1,
+            ImageId=ami_id,
+            InstanceType="t3a.small",
+            NetworkInterfaces=[
+                {
+                    "PrivateIpAddress": "172.31.0.5",
+                    "DeleteOnTermination": True,
+                    "DeviceIndex": 0,
+                    "SubnetId": existing_subnet_id,
+                }
+            ],
+        )["Instances"][0]["InstanceId"]
+        ec2_client.get_waiter("instance_running").wait(InstanceIds=[instance_id])
+
+        ec2_client.terminate_instances(InstanceIds=[instance_id])
+        ec2_client.get_waiter("instance_terminated").wait(InstanceIds=[instance_id])
+
+
 def setup_vpc(boto3):  # pylint: disable=W0621
     ec2resource = boto3.resource("ec2", region_name="us-east-1")
     ec2client = boto3.client("ec2", "us-east-1")