From a1bfb6a48e7d84f5dd795a181790f970582f1423 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Fri, 24 Dec 2021 14:12:57 -0100 Subject: [PATCH] EC2 - SubnetId and NetworkInterfaces cannot both be specified when calling run_instances (#4719) --- moto/ec2/exceptions.py | 5 +++++ moto/ec2/models.py | 8 +++++++- moto/ec2/responses/instances.py | 6 +++++- tests/test_ec2/test_instances.py | 35 +++++++++++++++++++++++++------- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py index 9b8f595f8..80b6471c0 100644 --- a/moto/ec2/exceptions.py +++ b/moto/ec2/exceptions.py @@ -49,6 +49,11 @@ class InvalidDHCPOptionsIdError(EC2ClientError): ) +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 index 84d008b3c..b354952d5 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -677,6 +677,8 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel): 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 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel): 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 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel): 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 index 2742a28bc..44b646cd3 100644 --- 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 @@ class InstanceResponse(BaseResponse): ), "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: diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index 9e84e389a..196ec3c5f 100644 --- 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")