Repository: getmoto/moto
Pull request #5887 (2023-01-31 17:59:07)

EC2: Allow providing NIC template when creating several instances
AWS ec2 API allows providing a NIC "template" when creating instances, which does not contain any IP. ec2 assign an IP address automatically for each instance.

With moto, almost everything was made to reproduce the same behaviour, except that the dict containing the params was directly updated with the assigned IP address, and was producing an error the second time when checking if the IP was available, because it was checking it against the previously assigned IP.

The solution is very simple: duplicating the parameter to avoid updating the original entry.


codecov[bot]: # [Codecov](https://codecov.io/gh/getmoto/moto/pull/5887?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#5887](https://codecov.io/gh/getmoto/moto/pull/5887?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (3a1ef02) into [master](https://codecov.io/gh/getmoto/moto/commit/ad016112fe382149f204f7191ba679a6f57a2417?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (ad01611) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##           master    #5887   +/-   ##
=======================================
  Coverage   96.20%   96.20%           
=======================================
  Files         759      759           
  Lines       74726    74740   +14     
=======================================
+ Hits        71887    71901   +14     
  Misses       2839     2839           
```

| Flag | Coverage Δ | |
|---|---|---|
| servertests | `36.93% <100.00%> (+<0.01%)` | :arrow_up: |
| unittests | `96.14% <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=None#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/getmoto/moto/pull/5887?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [moto/ec2/models/instances.py](https://codecov.io/gh/getmoto/moto/pull/5887?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-bW90by9lYzIvbW9kZWxzL2luc3RhbmNlcy5weQ==) | `97.86% <100.00%> (ø)` | |
| [moto/eks/models.py](https://codecov.io/gh/getmoto/moto/pull/5887?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-bW90by9la3MvbW9kZWxzLnB5) | `98.20% <0.00%> (+0.07%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)


Patch:
diff --git a/moto/ec2/models/instances.py b/moto/ec2/models/instances.py
index 6094773fd..ff1bf4294 100644
--- a/moto/ec2/models/instances.py
+++ b/moto/ec2/models/instances.py
@@ -74,7 +74,7 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
         self.owner_id = ec2_backend.account_id
         self.lifecycle = kwargs.get("lifecycle")
 
-        nics = kwargs.get("nics", {})
+        nics = copy.deepcopy(kwargs.get("nics", []))
 
         launch_template_arg = kwargs.get("launch_template", {})
         if launch_template_arg and not image_id:


Test patch:
diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py
index a1e67456a..5c0c5c45e 100644
--- a/tests/test_ec2/test_instances.py
+++ b/tests/test_ec2/test_instances.py
@@ -2563,3 +2563,61 @@ def retrieve_all_reservations(client, filters=[]):  # pylint: disable=W0102
 def retrieve_all_instances(client, filters=[]):  # pylint: disable=W0102
     reservations = retrieve_all_reservations(client, filters)
     return [i for r in reservations for i in r["Instances"]]
+
+
+@mock_ec2
+def test_run_multiple_instances_with_single_nic_template():
+    ec2 = boto3.resource("ec2", "us-west-1")
+    client = boto3.client("ec2", "us-west-1")
+    vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
+    subnet = ec2.create_subnet(VpcId=vpc.id, CidrBlock="10.0.0.0/18")
+    security_group1 = ec2.create_security_group(
+        GroupName=str(uuid4()), Description="n/a"
+    )
+
+    instances = ec2.create_instances(
+        ImageId=EXAMPLE_AMI_ID,
+        MinCount=2,
+        MaxCount=2,
+        NetworkInterfaces=[
+            {
+                "AssociatePublicIpAddress": False,
+                "DeleteOnTermination": True,
+                "DeviceIndex": 0,
+                "Groups": [security_group1.group_id],
+                "SubnetId": subnet.id,
+                "InterfaceType": "interface",
+            }
+        ],
+    )
+
+    enis = []
+
+    for instance in instances:
+        instance_eni = instance.network_interfaces_attribute
+        instance_eni.should.have.length_of(1)
+
+        nii = instance_eni[0]["NetworkInterfaceId"]
+
+        my_enis = client.describe_network_interfaces(NetworkInterfaceIds=[nii])[
+            "NetworkInterfaces"
+        ]
+        my_enis.should.have.length_of(1)
+        eni = my_enis[0]
+
+        instance.subnet_id.should.equal(subnet.id)
+
+        eni["SubnetId"].should.equal(subnet.id)
+        eni["Groups"].should.have.length_of(1)
+        set([group["GroupId"] for group in eni["Groups"]]).should.equal(
+            set([security_group1.id])
+        )
+        eni["PrivateIpAddresses"].should.have.length_of(1)
+        eni["PrivateIpAddresses"][0]["PrivateIpAddress"].should_not.equal(None)
+
+        enis.append(eni)
+
+    instance_0_ip = enis[0]["PrivateIpAddresses"][0]["PrivateIpAddress"]
+    instance_1_ip = enis[1]["PrivateIpAddresses"][0]["PrivateIpAddress"]
+
+    instance_0_ip.should_not.equal(instance_1_ip)