Repository: getmoto/moto

Issue:
AssociatePublicIpAddress = False on primary network interface is ignored
When creating an ec2 instance and a primary network interface with `"AssociatePublicIpAddress": False`, an instance is created with a public IP address.

I am expecting no public IP address to be created.

Example request:

```
kwargs = {
        "ImageId": ami,
        "MaxCount": 1,
        "MinCount": 1,
        "KeyName": "test-key",
        "InstanceType": "t2.medium",
        "EbsOptimized": False,
        "NetworkInterfaces": [
            {
                "DeviceIndex": 0,
                "Description": "Primary network interface",
                "DeleteOnTermination": True,
                "SubnetId": "subnet-317243a9",
                "Ipv6AddressCount": 0,
                "Groups": "default",
                "AssociatePublicIpAddress": False,
            }
        ]
    }
```

I'm using moto 1.3.14


Discussion:
Thanks for the fix @bblommers I've tested it but it still seems like a PublicIpAddress and PublicDnsName are being created when `AssociatePublicIpAddress` = `False`
You're right @tekumara - I just ran the test against AWS, and realized the mistake. My test case worked when providing an instance-level SubnetId, but I didn't realize that the SubnetId has to be provided as part of the NIC. A followup PR will be out soon.

Resolving patch:
diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py
--- a/moto/ec2/exceptions.py
+++ b/moto/ec2/exceptions.py
@@ -49,6 +49,11 @@ def __init__(self, dhcp_options_id):
         )
 
 
+class InvalidParameterCombination(EC2ClientError):
+    def __init__(self, msg):
+        super().__init__("InvalidParameterCombination", msg)
+
+
 class MalformedDHCPOptionsIdError(EC2ClientError):
     def __init__(self, dhcp_options_id):
         super().__init__(
diff --git a/moto/ec2/models.py b/moto/ec2/models.py
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -677,6 +677,8 @@ def __init__(self, ec2_backend, image_id, user_data, security_groups, **kwargs):
         self.id = random_instance_id()
         self.lifecycle = kwargs.get("lifecycle")
 
+        nics = kwargs.get("nics", {})
+
         launch_template_arg = kwargs.get("launch_template", {})
         if launch_template_arg and not image_id:
             # the image id from the template should be used
@@ -703,6 +705,10 @@ def __init__(self, ec2_backend, image_id, user_data, security_groups, **kwargs):
         self.region_name = kwargs.get("region_name", "us-east-1")
         placement = kwargs.get("placement", None)
         self.subnet_id = kwargs.get("subnet_id")
+        if not self.subnet_id:
+            self.subnet_id = next(
+                (n["SubnetId"] for n in nics.values() if "SubnetId" in n), None
+            )
         in_ec2_classic = not bool(self.subnet_id)
         self.key_name = kwargs.get("key_name")
         self.ebs_optimized = kwargs.get("ebs_optimized", False)
@@ -759,7 +765,7 @@ def __init__(self, ec2_backend, image_id, user_data, security_groups, **kwargs):
 
         self._private_ips = set()
         self.prep_nics(
-            kwargs.get("nics", {}),
+            nics,
             private_ip=kwargs.get("private_ip"),
             associate_public_ip=self.associate_public_ip,
             security_groups=self.security_groups,
diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py
--- a/moto/ec2/responses/instances.py
+++ b/moto/ec2/responses/instances.py
@@ -1,7 +1,7 @@
 from moto.autoscaling import autoscaling_backends
 from moto.core.responses import BaseResponse
 from moto.core.utils import camelcase_to_underscores
-from moto.ec2.exceptions import MissingParameterError
+from moto.ec2.exceptions import MissingParameterError, InvalidParameterCombination
 from moto.ec2.utils import (
     filters_from_querystring,
     dict_from_querystring,
@@ -70,6 +70,10 @@ def run_instances(self):
             ),
             "launch_template": self._get_multi_param_dict("LaunchTemplate"),
         }
+        if len(kwargs["nics"]) and kwargs["subnet_id"]:
+            raise InvalidParameterCombination(
+                msg="Network interfaces and an instance-level subnet ID may not be specified on the same request"
+            )
 
         mappings = self._parse_block_device_mapping()
         if mappings:


Test patch:
diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py
--- a/tests/test_ec2/test_instances.py
+++ b/tests/test_ec2/test_instances.py
@@ -2116,7 +2116,6 @@ def test_run_instance_with_nic_preexisting_boto3():
         MinCount=1,
         MaxCount=1,
         NetworkInterfaces=[{"DeviceIndex": 0, "NetworkInterfaceId": eni.id,}],
-        SubnetId=subnet.id,
         SecurityGroupIds=[security_group2.group_id],
     )[0]
 
@@ -3053,8 +3052,7 @@ def test_run_instance_and_associate_public_ip():
         ImageId=EXAMPLE_AMI_ID,
         MinCount=1,
         MaxCount=1,
-        NetworkInterfaces=[{"DeviceIndex": 0}],
-        SubnetId=subnet.id,
+        NetworkInterfaces=[{"DeviceIndex": 0, "SubnetId": subnet.id}],
     )[0]
     interfaces = instance.network_interfaces_attribute
     addresses = interfaces[0]["PrivateIpAddresses"][0]
@@ -3067,8 +3065,9 @@ def test_run_instance_and_associate_public_ip():
         ImageId=EXAMPLE_AMI_ID,
         MinCount=1,
         MaxCount=1,
-        NetworkInterfaces=[{"DeviceIndex": 0, "AssociatePublicIpAddress": False}],
-        SubnetId=subnet.id,
+        NetworkInterfaces=[
+            {"DeviceIndex": 0, "SubnetId": subnet.id, "AssociatePublicIpAddress": False}
+        ],
     )[0]
     interfaces = instance.network_interfaces_attribute
     addresses = interfaces[0]["PrivateIpAddresses"][0]
@@ -3081,8 +3080,9 @@ def test_run_instance_and_associate_public_ip():
         ImageId=EXAMPLE_AMI_ID,
         MinCount=1,
         MaxCount=1,
-        NetworkInterfaces=[{"DeviceIndex": 0, "AssociatePublicIpAddress": True}],
-        SubnetId=subnet.id,
+        NetworkInterfaces=[
+            {"DeviceIndex": 0, "SubnetId": subnet.id, "AssociatePublicIpAddress": True}
+        ],
     )[0]
     interfaces = instance.network_interfaces_attribute
     addresses = interfaces[0]["PrivateIpAddresses"][0]
@@ -3094,6 +3094,27 @@ def test_run_instance_and_associate_public_ip():
     addresses["Association"].should.have.key("PublicIp")
 
 
+@mock_ec2
+def test_run_instance_cannot_have_subnet_and_networkinterface_parameter():
+    ec2 = boto3.resource("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")
+
+    with pytest.raises(ClientError) as exc:
+        ec2.create_instances(
+            ImageId=EXAMPLE_AMI_ID,
+            MinCount=1,
+            MaxCount=1,
+            SubnetId=subnet.id,
+            NetworkInterfaces=[{"DeviceIndex": 0}],
+        )
+    err = exc.value.response["Error"]
+    err["Code"].should.equal("InvalidParameterCombination")
+    err["Message"].should.equal(
+        "Network interfaces and an instance-level subnet ID may not be specified on the same request"
+    )
+
+
 @mock_ec2
 def test_describe_instances_dryrun():
     client = boto3.client("ec2", region_name="us-east-1")