Merge pull request #207 from DreadPirateShawn/NetworkInterfacesAttachDetach

Network Interfaces: Added attach/detach support.
This commit is contained in:
Steve Pulec 2014-09-15 20:52:35 -04:00
commit 579a6fc398
5 changed files with 196 additions and 85 deletions

View File

@ -80,6 +80,14 @@ class InvalidNetworkInterfaceIdError(EC2ClientError):
.format(eni_id)) .format(eni_id))
class InvalidNetworkAttachmentIdError(EC2ClientError):
def __init__(self, attachment_id):
super(InvalidNetworkAttachmentIdError, self).__init__(
"InvalidAttachmentID.NotFound",
"The network interface attachment ID '{0}' does not exist"
.format(attachment_id))
class InvalidSecurityGroupDuplicateError(EC2ClientError): class InvalidSecurityGroupDuplicateError(EC2ClientError):
def __init__(self, name): def __init__(self, name):
super(InvalidSecurityGroupDuplicateError, self).__init__( super(InvalidSecurityGroupDuplicateError, self).__init__(

View File

@ -27,6 +27,7 @@ from .exceptions import (
InvalidVPCIdError, InvalidVPCIdError,
InvalidSubnetIdError, InvalidSubnetIdError,
InvalidNetworkInterfaceIdError, InvalidNetworkInterfaceIdError,
InvalidNetworkAttachmentIdError,
InvalidSecurityGroupDuplicateError, InvalidSecurityGroupDuplicateError,
InvalidSecurityGroupNotFoundError, InvalidSecurityGroupNotFoundError,
InvalidPermissionNotFoundError, InvalidPermissionNotFoundError,
@ -91,13 +92,17 @@ class NetworkInterface(object):
self.private_ip_address = private_ip_address self.private_ip_address = private_ip_address
self.subnet = subnet self.subnet = subnet
self.instance = None self.instance = None
self.attachment_id = None
self.public_ip = None self.public_ip = None
self.public_ip_auto_assign = public_ip_auto_assign self.public_ip_auto_assign = public_ip_auto_assign
self.start() self.start()
self.attachments = [] self.attachments = []
self.group_set = []
# Local set to the ENI. When attached to an instance, @property group_set
# returns groups for both self and the attached instance.
self._group_set = []
group = None group = None
if group_ids: if group_ids:
@ -108,7 +113,7 @@ class NetworkInterface(object):
group = SecurityGroup(group_id, group_id, group_id, vpc_id=subnet.vpc_id) group = SecurityGroup(group_id, group_id, group_id, vpc_id=subnet.vpc_id)
ec2_backend.groups[subnet.vpc_id][group_id] = group ec2_backend.groups[subnet.vpc_id][group_id] = group
if group: if group:
self.group_set.append(group) self._group_set.append(group)
@classmethod @classmethod
def create_from_cloudformation_json(cls, resource_name, cloudformation_json): def create_from_cloudformation_json(cls, resource_name, cloudformation_json):
@ -139,11 +144,12 @@ class NetworkInterface(object):
if self.public_ip_auto_assign: if self.public_ip_auto_assign:
self.public_ip = random_public_ip() self.public_ip = random_public_ip()
def attach(self, instance_id, device_index): @property
attachment = {'attachmentId': random_eni_attach_id(), def group_set(self):
'instanceId': instance_id, if self.instance and self.instance.security_groups:
'deviceIndex': device_index} return set(self._group_set) | set(self.instance.security_groups)
self.attachments.append(attachment) else:
return self._group_set
class NetworkInterfaceBackend(object): class NetworkInterfaceBackend(object):
@ -189,65 +195,27 @@ class NetworkInterfaceBackend(object):
ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeNetworkInterfaces".format(_filter)) ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeNetworkInterfaces".format(_filter))
return enis return enis
def attach_network_interface(self, eni_id, instance_id, device_index):
eni = self.get_network_interface(eni_id)
instance = self.get_instance(instance_id)
return instance.attach_eni(eni, device_index)
def detach_network_interface(self, attachment_id):
found_eni = None
for eni in self.enis.values():
if eni.attachment_id == attachment_id:
found_eni = eni
break
else:
raise InvalidNetworkAttachmentIdError(attachment_id)
found_eni.instance.detach_eni(found_eni)
def modify_network_interface_attribute(self, eni_id, group_id): def modify_network_interface_attribute(self, eni_id, group_id):
eni = self.get_network_interface(eni_id) eni = self.get_network_interface(eni_id)
group = self.get_security_group_from_id(group_id) group = self.get_security_group_from_id(group_id)
eni.group_set = [group] eni._group_set = [group]
def prep_nics_for_instance(self, instance, nic_spec, subnet_id=None, private_ip=None, associate_public_ip=None):
nics = {}
# Primary NIC defaults
primary_nic = {'SubnetId': subnet_id,
'PrivateIpAddress': private_ip,
'AssociatePublicIpAddress': associate_public_ip}
primary_nic = dict((k,v) for k, v in primary_nic.items() if v)
# If empty NIC spec but primary NIC values provided, create NIC from them.
if primary_nic and not nic_spec:
nic_spec[0] = primary_nic
nic_spec[0]['DeviceIndex'] = 0
# Flesh out data structures and associations
for nic in nic_spec.values():
use_eni = None
security_group_ids = []
device_index = int(nic.get('DeviceIndex'))
nic_id = nic.get('NetworkInterfaceId', None)
if nic_id:
# If existing NIC found, use it.
use_nic = ec2_backend.get_network_interface(nic_id)
use_nic.device_index = device_index
use_nic.public_ip_auto_assign = False
else:
# If primary NIC values provided, use them for the primary NIC.
if device_index == 0 and primary_nic:
nic.update(primary_nic)
subnet = ec2_backend.get_subnet(nic['SubnetId'])
group_id = nic.get('SecurityGroupId',None)
group_ids = [group_id] if group_id else []
use_nic = ec2_backend.create_network_interface(subnet,
nic.get('PrivateIpAddress',None),
device_index=device_index,
public_ip_auto_assign=nic.get('AssociatePublicIpAddress',False),
group_ids=group_ids)
use_nic.instance = instance # This is used upon associate/disassociate public IP.
if use_nic.instance.security_groups:
use_nic.group_set.extend(use_nic.instance.security_groups)
use_nic.attach(instance.id, device_index)
nics[device_index] = use_nic
return nics
class Instance(BotoInstance, TaggedEC2Instance): class Instance(BotoInstance, TaggedEC2Instance):
@ -281,8 +249,7 @@ class Instance(BotoInstance, TaggedEC2Instance):
# string will have a "u" prefix -- need to get rid of it # string will have a "u" prefix -- need to get rid of it
self.user_data[0] = self.user_data[0].encode('utf-8') self.user_data[0] = self.user_data[0].encode('utf-8')
self.nics = ec2_backend.prep_nics_for_instance(self, self.prep_nics(kwargs.get("nics", {}),
kwargs.get("nics", {}),
subnet_id=kwargs.get("subnet_id",None), subnet_id=kwargs.get("subnet_id",None),
private_ip=kwargs.get("private_ip",None), private_ip=kwargs.get("private_ip",None),
associate_public_ip=kwargs.get("associate_public_ip",None)) associate_public_ip=kwargs.get("associate_public_ip",None))
@ -349,6 +316,68 @@ class Instance(BotoInstance, TaggedEC2Instance):
else: else:
return self.security_groups return self.security_groups
def prep_nics(self, nic_spec, subnet_id=None, private_ip=None, associate_public_ip=None):
self.nics = {}
# Primary NIC defaults
primary_nic = {'SubnetId': subnet_id,
'PrivateIpAddress': private_ip,
'AssociatePublicIpAddress': associate_public_ip}
primary_nic = dict((k,v) for k, v in primary_nic.items() if v)
# If empty NIC spec but primary NIC values provided, create NIC from them.
if primary_nic and not nic_spec:
nic_spec[0] = primary_nic
nic_spec[0]['DeviceIndex'] = 0
# Flesh out data structures and associations
for nic in nic_spec.values():
use_eni = None
security_group_ids = []
device_index = int(nic.get('DeviceIndex'))
nic_id = nic.get('NetworkInterfaceId', None)
if nic_id:
# If existing NIC found, use it.
use_nic = ec2_backend.get_network_interface(nic_id)
use_nic.device_index = device_index
use_nic.public_ip_auto_assign = False
else:
# If primary NIC values provided, use them for the primary NIC.
if device_index == 0 and primary_nic:
nic.update(primary_nic)
subnet = ec2_backend.get_subnet(nic['SubnetId'])
group_id = nic.get('SecurityGroupId',None)
group_ids = [group_id] if group_id else []
use_nic = ec2_backend.create_network_interface(subnet,
nic.get('PrivateIpAddress',None),
device_index=device_index,
public_ip_auto_assign=nic.get('AssociatePublicIpAddress',False),
group_ids=group_ids)
self.attach_eni(use_nic, device_index)
def attach_eni(self, eni, device_index):
device_index = int(device_index)
self.nics[device_index] = eni
eni.instance = self # This is used upon associate/disassociate public IP.
eni.attachment_id = random_eni_attach_id()
eni.device_index = device_index
return eni.attachment_id
def detach_eni(self, eni):
self.nics.pop(eni.device_index,None)
eni.instance = None
eni.attachment_id = None
eni.device_index = None
class InstanceBackend(object): class InstanceBackend(object):

View File

@ -7,9 +7,6 @@ from moto.ec2.utils import sequence_from_querystring, filters_from_querystring
class ElasticNetworkInterfaces(BaseResponse): class ElasticNetworkInterfaces(BaseResponse):
def attach_network_interface(self):
raise NotImplementedError('ElasticNetworkInterfaces(AmazonVPC).attach_network_interface is not yet implemented')
def create_network_interface(self): def create_network_interface(self):
subnet_id = self.querystring.get('SubnetId')[0] subnet_id = self.querystring.get('SubnetId')[0]
private_ip_address = self.querystring.get('PrivateIpAddress', [None])[0] private_ip_address = self.querystring.get('PrivateIpAddress', [None])[0]
@ -35,8 +32,19 @@ class ElasticNetworkInterfaces(BaseResponse):
template = Template(DESCRIBE_NETWORK_INTERFACES_RESPONSE) template = Template(DESCRIBE_NETWORK_INTERFACES_RESPONSE)
return template.render(enis=enis) return template.render(enis=enis)
def attach_network_interface(self):
eni_id = self.querystring.get('NetworkInterfaceId')[0]
instance_id = self.querystring.get('InstanceId')[0]
device_index = self.querystring.get('DeviceIndex')[0]
attachment_id = ec2_backend.attach_network_interface(eni_id, instance_id, device_index)
template = Template(ATTACH_NETWORK_INTERFACE_RESPONSE)
return template.render(attachment_id=attachment_id)
def detach_network_interface(self): def detach_network_interface(self):
raise NotImplementedError('ElasticNetworkInterfaces(AmazonVPC).detach_network_interface is not yet implemented') attachment_id = self.querystring.get('AttachmentId')[0]
ec2_backend.detach_network_interface(attachment_id)
template = Template(DETACH_NETWORK_INTERFACE_RESPONSE)
return template.render()
def modify_network_interface_attribute(self): def modify_network_interface_attribute(self):
#Currently supports modifying one and only one security group #Currently supports modifying one and only one security group
@ -115,17 +123,17 @@ DESCRIBE_NETWORK_INTERFACES_RESPONSE = """<DescribeNetworkInterfacesResponse xml
</item> </item>
{% endfor %} {% endfor %}
</groupSet> </groupSet>
{% for attachment in eni.attachments %} {% if eni.instance %}
<attachment> <attachment>
<attachmentId>{{ attachment['attachmentId'] }}</attachmentId> <attachmentId>{{ eni.attachment_id }}</attachmentId>
<instanceId>{{ attachment['instanceId'] }}</instanceId> <instanceId>{{ eni.instance.id }}</instanceId>
<instanceOwnerId>190610284047</instanceOwnerId> <instanceOwnerId>190610284047</instanceOwnerId>
<deviceIndex>{{ attachment['deviceIndex'] }}</deviceIndex> <deviceIndex>{{ eni.device_index }}</deviceIndex>
<status>attached</status> <status>attached</status>
<attachTime>2013-10-04T17:38:53.000Z</attachTime> <attachTime>2013-10-04T17:38:53.000Z</attachTime>
<deleteOnTermination>true</deleteOnTermination> <deleteOnTermination>true</deleteOnTermination>
</attachment> </attachment>
{% endfor %} {% endif %}
<association> <association>
<publicIp>{{ eni.public_ip }}</publicIp> <publicIp>{{ eni.public_ip }}</publicIp>
<publicDnsName>ec2-54-200-86-47.us-west-2.compute.amazonaws.com</publicDnsName> <publicDnsName>ec2-54-200-86-47.us-west-2.compute.amazonaws.com</publicDnsName>
@ -155,6 +163,16 @@ DESCRIBE_NETWORK_INTERFACES_RESPONSE = """<DescribeNetworkInterfacesResponse xml
</networkInterfaceSet> </networkInterfaceSet>
</DescribeNetworkInterfacesResponse>""" </DescribeNetworkInterfacesResponse>"""
ATTACH_NETWORK_INTERFACE_RESPONSE = """<AttachNetworkInterfaceResponse xmlns="http://ec2.amazonaws.com/doc/2014-06-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<attachmentId>{{ attachment_id }}</attachmentId>
</AttachNetworkInterfaceResponse>"""
DETACH_NETWORK_INTERFACE_RESPONSE = """<DetachNetworkInterfaceResponse xmlns="http://ec2.amazonaws.com/doc/2014-06-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
</DetachNetworkInterfaceResponse>"""
MODIFY_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE = """<ModifyNetworkInterfaceAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2012-12-01/"> MODIFY_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE = """<ModifyNetworkInterfaceAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2012-12-01/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId> <requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return> <return>true</return>

View File

@ -244,7 +244,7 @@ EC2_RUN_INSTANCES = """<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc
{% endfor %} {% endfor %}
</groupSet> </groupSet>
<attachment> <attachment>
<attachmentId>eni-attach-1a2b3c4d</attachmentId> <attachmentId>{{ nic.attachment_id }}</attachmentId>
<deviceIndex>{{ nic.device_index }}</deviceIndex> <deviceIndex>{{ nic.device_index }}</deviceIndex>
<status>attached</status> <status>attached</status>
<attachTime>YYYY-MM-DDTHH:MM:SS+0000</attachTime> <attachTime>YYYY-MM-DDTHH:MM:SS+0000</attachTime>
@ -385,7 +385,7 @@ EC2_DESCRIBE_INSTANCES = """<DescribeInstancesResponse xmlns='http://ec2.amazona
{% endfor %} {% endfor %}
</groupSet> </groupSet>
<attachment> <attachment>
<attachmentId>eni-attach-1a2b3c4d</attachmentId> <attachmentId>{{ nic.attachment_id }}</attachmentId>
<deviceIndex>{{ nic.device_index }}</deviceIndex> <deviceIndex>{{ nic.device_index }}</deviceIndex>
<status>attached</status> <status>attached</status>
<attachTime>YYYY-MM-DDTHH:MM:SS+0000</attachTime> <attachTime>YYYY-MM-DDTHH:MM:SS+0000</attachTime>

View File

@ -11,6 +11,7 @@ from boto.exception import EC2ResponseError
import sure # noqa import sure # noqa
from moto import mock_ec2 from moto import mock_ec2
from tests.helpers import requires_boto_gte
################ Test Readme ############### ################ Test Readme ###############
@ -360,6 +361,61 @@ def test_run_instance_with_nic_preexisting():
instance_eni.private_ip_addresses[0].private_ip_address.should.equal(private_ip) instance_eni.private_ip_addresses[0].private_ip_address.should.equal(private_ip)
@requires_boto_gte("2.32.0")
@mock_ec2
def test_instance_with_nic_attach_detach():
conn = boto.connect_vpc('the_key', 'the_secret')
vpc = conn.create_vpc("10.0.0.0/16")
subnet = conn.create_subnet(vpc.id, "10.0.0.0/18")
security_group1 = conn.create_security_group('test security group #1', 'this is a test security group')
security_group2 = conn.create_security_group('test security group #2', 'this is a test security group')
reservation = conn.run_instances('ami-1234abcd', security_group_ids=[security_group1.id])
instance = reservation.instances[0]
eni = conn.create_network_interface(subnet.id, groups=[security_group2.id])
# Check initial instance and ENI data
instance.interfaces.should.have.length_of(0)
eni.groups.should.have.length_of(1)
set([group.id for group in eni.groups]).should.equal(set([security_group2.id]))
# Attach
conn.attach_network_interface(eni.id, instance.id, device_index=0)
# Check attached instance and ENI data
instance.update()
instance.interfaces.should.have.length_of(1)
instance_eni = instance.interfaces[0]
instance_eni.id.should.equal(eni.id)
instance_eni.groups.should.have.length_of(2)
set([group.id for group in instance_eni.groups]).should.equal(set([security_group1.id,security_group2.id]))
eni = conn.get_all_network_interfaces(eni.id)[0]
eni.groups.should.have.length_of(2)
set([group.id for group in eni.groups]).should.equal(set([security_group1.id,security_group2.id]))
# Detach
conn.detach_network_interface(instance_eni.attachment.id)
# Check detached instance and ENI data
instance.update()
instance.interfaces.should.have.length_of(0)
eni = conn.get_all_network_interfaces(eni.id)[0]
eni.groups.should.have.length_of(1)
set([group.id for group in eni.groups]).should.equal(set([security_group2.id]))
# Detach with invalid attachment ID
with assert_raises(EC2ResponseError) as cm:
conn.detach_network_interface('eni-attach-1234abcd')
cm.exception.code.should.equal('InvalidAttachmentID.NotFound')
cm.exception.status.should.equal(400)
cm.exception.request_id.should_not.be.none
@mock_ec2 @mock_ec2
def test_run_instance_with_keypair(): def test_run_instance_with_keypair():
conn = boto.connect_ec2('the_key', 'the_secret') conn = boto.connect_ec2('the_key', 'the_secret')