EC2 - implement network attachments (#4998)

This commit is contained in:
George Lungley 2022-04-04 11:17:22 +01:00 committed by GitHub
parent 3718cde444
commit b5f03b0a0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 161 additions and 10 deletions

View File

@ -266,6 +266,8 @@ class NetworkInterface(TaggedEC2Resource, CloudFormationModel):
self.subnet = self.ec2_backend.get_subnet(subnet) self.subnet = self.ec2_backend.get_subnet(subnet)
self.instance = None self.instance = None
self.attachment_id = None self.attachment_id = None
self.attach_time = None
self.delete_on_termination = False
self.description = description self.description = description
self.source_dest_check = True self.source_dest_check = True
@ -274,7 +276,6 @@ class NetworkInterface(TaggedEC2Resource, CloudFormationModel):
self.start() self.start()
self.add_tags(tags or {}) self.add_tags(tags or {})
self.status = "available" self.status = "available"
self.attachments = []
self.mac_address = random_mac_address() self.mac_address = random_mac_address()
self.interface_type = "interface" self.interface_type = "interface"
# Local set to the ENI. When attached to an instance, @property group_set # Local set to the ENI. When attached to an instance, @property group_set
@ -640,6 +641,7 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
super().__init__() super().__init__()
self.ec2_backend = ec2_backend self.ec2_backend = ec2_backend
self.id = random_instance_id() self.id = random_instance_id()
self.owner_id = OWNER_ID
self.lifecycle = kwargs.get("lifecycle") self.lifecycle = kwargs.get("lifecycle")
nics = kwargs.get("nics", {}) nics = kwargs.get("nics", {})
@ -1047,6 +1049,8 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
# This is used upon associate/disassociate public IP. # This is used upon associate/disassociate public IP.
eni.instance = self eni.instance = self
eni.attachment_id = random_eni_attach_id() eni.attachment_id = random_eni_attach_id()
eni.attach_time = utc_date_and_time()
eni.status = "in-use"
eni.device_index = device_index eni.device_index = device_index
return eni.attachment_id return eni.attachment_id
@ -1055,6 +1059,8 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
self.nics.pop(eni.device_index, None) self.nics.pop(eni.device_index, None)
eni.instance = None eni.instance = None
eni.attachment_id = None eni.attachment_id = None
eni.attach_time = None
eni.status = "available"
eni.device_index = None eni.device_index = None
@classmethod @classmethod

View File

@ -1,3 +1,4 @@
from moto.ec2.exceptions import InvalidParameterValueErrorUnknownAttribute
from moto.ec2.utils import get_attribute_value, add_tag_specification from moto.ec2.utils import get_attribute_value, add_tag_specification
from ._base_response import EC2BaseResponse from ._base_response import EC2BaseResponse
@ -39,16 +40,38 @@ class ElasticNetworkInterfaces(EC2BaseResponse):
return template.render() return template.render()
def describe_network_interface_attribute(self): def describe_network_interface_attribute(self):
raise NotImplementedError( eni_id = self._get_param("NetworkInterfaceId")
"ElasticNetworkInterfaces(AmazonVPC).describe_network_interface_attribute is not yet implemented" 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): def describe_network_interfaces(self):
eni_ids = self._get_multi_param("NetworkInterfaceId") eni_ids = self._get_multi_param("NetworkInterfaceId")
filters = self._filters_from_querystring() filters = self._filters_from_querystring()
enis = self.ec2_backend.get_all_network_interfaces(eni_ids, filters) if self.is_not_dryrun("DescribeNetworkInterfaces"):
template = self.response_template(DESCRIBE_NETWORK_INTERFACES_RESPONSE) enis = self.ec2_backend.get_all_network_interfaces(eni_ids, filters)
return template.render(enis=enis) template = self.response_template(DESCRIBE_NETWORK_INTERFACES_RESPONSE)
return template.render(enis=enis)
def attach_network_interface(self): def attach_network_interface(self):
eni_id = self._get_param("NetworkInterfaceId") eni_id = self._get_param("NetworkInterfaceId")
@ -277,6 +300,18 @@ DESCRIBE_NETWORK_INTERFACES_RESPONSE = """<DescribeNetworkInterfacesResponse xml
<natEnabled>true</natEnabled> <natEnabled>true</natEnabled>
</association> </association>
{% endif %} {% endif %}
{% if eni.attachment_id %}
<attachment>
<attachTime>{{ eni.attach_time }}</attachTime>
<attachmentId>{{ eni.attachment_id }}</attachmentId>
<deleteOnTermination>{{ eni.delete_on_termination }}</deleteOnTermination>
<deviceIndex>{{ eni.device_index }}</deviceIndex>
<networkCardIndex>0</networkCardIndex>
<instanceId>{{ eni.instance.id }}</instanceId>
<instanceOwnerId>{{ eni.instance.owner_id }}</instanceOwnerId>
<status>attached</status>
</attachment>
{% endif %}
<tagSet> <tagSet>
{% for tag in eni.get_tags() %} {% for tag in eni.get_tags() %}
<item> <item>
@ -329,3 +364,49 @@ DELETE_NETWORK_INTERFACE_RESPONSE = """
<requestId>34b5b3b4-d0c5-49b9-b5e2-a468ef6adcd8</requestId> <requestId>34b5b3b4-d0c5-49b9-b5e2-a468ef6adcd8</requestId>
<return>true</return> <return>true</return>
</DeleteNetworkInterfaceResponse>""" </DeleteNetworkInterfaceResponse>"""
DESCRIBE_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE_DESCRIPTION = """
<DescribeNetworkInterfaceAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
<networkInterfaceId>{{ eni.id }}</networkInterfaceId>
<description>
<value>{{ eni.description }}</value>
</description>
</DescribeNetworkInterfaceAttributeResponse>"""
DESCRIBE_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE_GROUPSET = """
<DescribeNetworkInterfaceAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
<networkInterfaceId>{{ eni.id }}</networkInterfaceId>
<groupSet>
{% for group in eni.group_set %}
<item>
<groupId>{{ group.id }}</groupId>
<groupName>{{ group.name }}</groupName>
</item>
{% endfor %}
</groupSet>
</DescribeNetworkInterfaceAttributeResponse>"""
DESCRIBE_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE_SOURCEDESTCHECK = """
<DescribeNetworkInterfaceAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
<networkInterfaceId>{{ eni.id }}</networkInterfaceId>
<sourceDestCheck>
<value>{{ "true" if eni.source_dest_check == True else "false" }}</value>
</sourceDestCheck>
</DescribeNetworkInterfaceAttributeResponse>"""
DESCRIBE_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE_ATTACHMENT = """
<DescribeNetworkInterfaceAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
<networkInterfaceId>{{ eni.id }}</networkInterfaceId>
{% if eni.attachment_id %}
<attachment>
<attachTime>{{ eni.attach_time }}</attachTime>
<attachmentId>{{ eni.attachment_id }}</attachmentId>
<deleteOnTermination>{{ eni.delete_on_termination }}</deleteOnTermination>
<deviceIndex>{{ eni.device_index }}</deviceIndex>
<networkCardIndex>0</networkCardIndex>
<instanceId>{{ eni.instance.id }}</instanceId>
<instanceOwnerId>{{ eni.instance.owner_id }}</instanceOwnerId>
<status>attached</status>
</attachment>
{% endif %}
</DescribeNetworkInterfaceAttributeResponse>"""

View File

@ -120,9 +120,20 @@ def test_elastic_network_interfaces_with_groups_boto3():
all_enis = client.describe_network_interfaces()["NetworkInterfaces"] all_enis = client.describe_network_interfaces()["NetworkInterfaces"]
[eni["NetworkInterfaceId"] for eni in all_enis].should.contain(my_eni.id) [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_description = [
my_eni["Groups"].should.have.length_of(2) eni for eni in all_enis if eni["NetworkInterfaceId"] == my_eni.id
set([group["GroupId"] for group in my_eni["Groups"]]).should.equal( ][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]) 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").length_of(2)
my_eni.should.have.key("Ipv6Addresses").should.contain({"Ipv6Address": ipv6_orig}) my_eni.should.have.key("Ipv6Addresses").should.contain({"Ipv6Address": ipv6_orig})
my_eni.should.have.key("Ipv6Addresses").should.contain({"Ipv6Address": ipv6_3}) 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)