diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 025b5ccae..3dc2477f5 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -266,6 +266,8 @@ class NetworkInterface(TaggedEC2Resource, CloudFormationModel): self.subnet = self.ec2_backend.get_subnet(subnet) self.instance = None self.attachment_id = None + self.attach_time = None + self.delete_on_termination = False self.description = description self.source_dest_check = True @@ -274,7 +276,6 @@ class NetworkInterface(TaggedEC2Resource, CloudFormationModel): self.start() self.add_tags(tags or {}) self.status = "available" - self.attachments = [] self.mac_address = random_mac_address() self.interface_type = "interface" # Local set to the ENI. When attached to an instance, @property group_set @@ -640,6 +641,7 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel): super().__init__() self.ec2_backend = ec2_backend self.id = random_instance_id() + self.owner_id = OWNER_ID self.lifecycle = kwargs.get("lifecycle") nics = kwargs.get("nics", {}) @@ -1047,6 +1049,8 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel): # This is used upon associate/disassociate public IP. eni.instance = self eni.attachment_id = random_eni_attach_id() + eni.attach_time = utc_date_and_time() + eni.status = "in-use" eni.device_index = device_index return eni.attachment_id @@ -1055,6 +1059,8 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel): self.nics.pop(eni.device_index, None) eni.instance = None eni.attachment_id = None + eni.attach_time = None + eni.status = "available" eni.device_index = None @classmethod diff --git a/moto/ec2/responses/elastic_network_interfaces.py b/moto/ec2/responses/elastic_network_interfaces.py index 1c4b9aa52..6a4f8cd4e 100644 --- a/moto/ec2/responses/elastic_network_interfaces.py +++ b/moto/ec2/responses/elastic_network_interfaces.py @@ -1,3 +1,4 @@ +from moto.ec2.exceptions import InvalidParameterValueErrorUnknownAttribute from moto.ec2.utils import get_attribute_value, add_tag_specification from ._base_response import EC2BaseResponse @@ -39,16 +40,38 @@ class ElasticNetworkInterfaces(EC2BaseResponse): return template.render() def describe_network_interface_attribute(self): - raise NotImplementedError( - "ElasticNetworkInterfaces(AmazonVPC).describe_network_interface_attribute is not yet implemented" - ) + eni_id = self._get_param("NetworkInterfaceId") + attribute = self._get_param("Attribute") + if self.is_not_dryrun("DescribeNetworkInterfaceAttribute"): + eni = self.ec2_backend.get_all_network_interfaces([eni_id])[0] + + if attribute == "description": + template = self.response_template( + DESCRIBE_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE_DESCRIPTION + ) + elif attribute == "groupSet": + template = self.response_template( + DESCRIBE_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE_GROUPSET + ) + elif attribute == "sourceDestCheck": + template = self.response_template( + DESCRIBE_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE_SOURCEDESTCHECK + ) + elif attribute == "attachment": + template = self.response_template( + DESCRIBE_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE_ATTACHMENT + ) + else: + raise InvalidParameterValueErrorUnknownAttribute(attribute) + return template.render(eni=eni) def describe_network_interfaces(self): eni_ids = self._get_multi_param("NetworkInterfaceId") filters = self._filters_from_querystring() - enis = self.ec2_backend.get_all_network_interfaces(eni_ids, filters) - template = self.response_template(DESCRIBE_NETWORK_INTERFACES_RESPONSE) - return template.render(enis=enis) + if self.is_not_dryrun("DescribeNetworkInterfaces"): + enis = self.ec2_backend.get_all_network_interfaces(eni_ids, filters) + template = self.response_template(DESCRIBE_NETWORK_INTERFACES_RESPONSE) + return template.render(enis=enis) def attach_network_interface(self): eni_id = self._get_param("NetworkInterfaceId") @@ -277,6 +300,18 @@ DESCRIBE_NETWORK_INTERFACES_RESPONSE = """true {% endif %} + {% if eni.attachment_id %} + + {{ eni.attach_time }} + {{ eni.attachment_id }} + {{ eni.delete_on_termination }} + {{ eni.device_index }} + 0 + {{ eni.instance.id }} + {{ eni.instance.owner_id }} + attached + + {% endif %} {% for tag in eni.get_tags() %} @@ -329,3 +364,49 @@ DELETE_NETWORK_INTERFACE_RESPONSE = """ 34b5b3b4-d0c5-49b9-b5e2-a468ef6adcd8 true """ + +DESCRIBE_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE_DESCRIPTION = """ + + {{ eni.id }} + + {{ eni.description }} + +""" + +DESCRIBE_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE_GROUPSET = """ + + {{ eni.id }} + + {% for group in eni.group_set %} + + {{ group.id }} + {{ group.name }} + + {% endfor %} + +""" + +DESCRIBE_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE_SOURCEDESTCHECK = """ + + {{ eni.id }} + + {{ "true" if eni.source_dest_check == True else "false" }} + +""" + +DESCRIBE_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE_ATTACHMENT = """ + + {{ eni.id }} + {% if eni.attachment_id %} + + {{ eni.attach_time }} + {{ eni.attachment_id }} + {{ eni.delete_on_termination }} + {{ eni.device_index }} + 0 + {{ eni.instance.id }} + {{ eni.instance.owner_id }} + attached + + {% endif %} +""" diff --git a/tests/test_ec2/test_elastic_network_interfaces.py b/tests/test_ec2/test_elastic_network_interfaces.py index 6209f85df..fb40ca467 100644 --- a/tests/test_ec2/test_elastic_network_interfaces.py +++ b/tests/test_ec2/test_elastic_network_interfaces.py @@ -120,9 +120,20 @@ def test_elastic_network_interfaces_with_groups_boto3(): all_enis = client.describe_network_interfaces()["NetworkInterfaces"] [eni["NetworkInterfaceId"] for eni in all_enis].should.contain(my_eni.id) - my_eni = [eni for eni in all_enis if eni["NetworkInterfaceId"] == my_eni.id][0] - my_eni["Groups"].should.have.length_of(2) - set([group["GroupId"] for group in my_eni["Groups"]]).should.equal( + my_eni_description = [ + eni for eni in all_enis if eni["NetworkInterfaceId"] == my_eni.id + ][0] + my_eni_description["Groups"].should.have.length_of(2) + set([group["GroupId"] for group in my_eni_description["Groups"]]).should.equal( + set([sec_group1.id, sec_group2.id]) + ) + + eni_groups_attribute = client.describe_network_interface_attribute( + NetworkInterfaceId=my_eni.id, Attribute="groupSet" + ).get("Groups") + + eni_groups_attribute.should.have.length_of(2) + set([group["GroupId"] for group in eni_groups_attribute]).should.equal( set([sec_group1.id, sec_group2.id]) ) @@ -970,3 +981,56 @@ def test_unassign_ipv6_addresses(): my_eni.should.have.key("Ipv6Addresses").length_of(2) my_eni.should.have.key("Ipv6Addresses").should.contain({"Ipv6Address": ipv6_orig}) my_eni.should.have.key("Ipv6Addresses").should.contain({"Ipv6Address": ipv6_3}) + + +@mock_ec2 +def test_elastic_network_interfaces_describe_attachment(): + ec2 = boto3.resource("ec2", region_name="us-east-1") + client = boto3.client("ec2", "us-east-1") + + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + subnet = ec2.create_subnet(VpcId=vpc.id, CidrBlock="10.0.0.0/18") + eni_id = subnet.create_network_interface(Description="A network interface").id + instance_id = client.run_instances(ImageId="ami-12c6146b", MinCount=1, MaxCount=1)[ + "Instances" + ][0]["InstanceId"] + + client.attach_network_interface( + NetworkInterfaceId=eni_id, InstanceId=instance_id, DeviceIndex=1 + ) + + my_eni_attachment = client.describe_network_interface_attribute( + NetworkInterfaceId=eni_id, Attribute="attachment" + ).get("Attachment") + my_eni_attachment["InstanceId"].should.equal(instance_id) + my_eni_attachment["DeleteOnTermination"].should.be.false + + with pytest.raises(ClientError) as ex: + client.describe_network_interface_attribute( + NetworkInterfaceId=eni_id, Attribute="attach" + ) + ex.value.response["Error"]["Code"].should.equal("InvalidParameterValue") + ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.value.response["Error"]["Message"].should.equal( + "Value (attach) for parameter attribute is invalid. Unknown attribute." + ) + + with pytest.raises(ClientError) as ex: + client.describe_network_interface_attribute( + NetworkInterfaceId=eni_id, Attribute="attachment", DryRun=True + ) + ex.value.response["Error"]["Code"].should.equal("DryRunOperation") + ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(412) + ex.value.response["Error"]["Message"].should.equal( + "An error occurred (DryRunOperation) when calling the DescribeNetworkInterfaceAttribute operation: Request would have succeeded, but DryRun flag is set" + ) + + my_eni_description = client.describe_network_interface_attribute( + NetworkInterfaceId=eni_id, Attribute="description" + ).get("Description") + my_eni_description["Value"].should.be.equal("A network interface") + + my_eni_source_dest_check = client.describe_network_interface_attribute( + NetworkInterfaceId=eni_id, Attribute="sourceDestCheck" + ).get("SourceDestCheck") + my_eni_source_dest_check["Value"].should.be.equal(True)