Merge pull request #207 from DreadPirateShawn/NetworkInterfacesAttachDetach
Network Interfaces: Added attach/detach support.
This commit is contained in:
commit
579a6fc398
@ -80,6 +80,14 @@ class InvalidNetworkInterfaceIdError(EC2ClientError):
|
||||
.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):
|
||||
def __init__(self, name):
|
||||
super(InvalidSecurityGroupDuplicateError, self).__init__(
|
||||
|
@ -27,6 +27,7 @@ from .exceptions import (
|
||||
InvalidVPCIdError,
|
||||
InvalidSubnetIdError,
|
||||
InvalidNetworkInterfaceIdError,
|
||||
InvalidNetworkAttachmentIdError,
|
||||
InvalidSecurityGroupDuplicateError,
|
||||
InvalidSecurityGroupNotFoundError,
|
||||
InvalidPermissionNotFoundError,
|
||||
@ -91,13 +92,17 @@ class NetworkInterface(object):
|
||||
self.private_ip_address = private_ip_address
|
||||
self.subnet = subnet
|
||||
self.instance = None
|
||||
self.attachment_id = None
|
||||
|
||||
self.public_ip = None
|
||||
self.public_ip_auto_assign = public_ip_auto_assign
|
||||
self.start()
|
||||
|
||||
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
|
||||
if group_ids:
|
||||
@ -108,7 +113,7 @@ class NetworkInterface(object):
|
||||
group = SecurityGroup(group_id, group_id, group_id, vpc_id=subnet.vpc_id)
|
||||
ec2_backend.groups[subnet.vpc_id][group_id] = group
|
||||
if group:
|
||||
self.group_set.append(group)
|
||||
self._group_set.append(group)
|
||||
|
||||
@classmethod
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json):
|
||||
@ -139,11 +144,12 @@ class NetworkInterface(object):
|
||||
if self.public_ip_auto_assign:
|
||||
self.public_ip = random_public_ip()
|
||||
|
||||
def attach(self, instance_id, device_index):
|
||||
attachment = {'attachmentId': random_eni_attach_id(),
|
||||
'instanceId': instance_id,
|
||||
'deviceIndex': device_index}
|
||||
self.attachments.append(attachment)
|
||||
@property
|
||||
def group_set(self):
|
||||
if self.instance and self.instance.security_groups:
|
||||
return set(self._group_set) | set(self.instance.security_groups)
|
||||
else:
|
||||
return self._group_set
|
||||
|
||||
|
||||
class NetworkInterfaceBackend(object):
|
||||
@ -189,65 +195,27 @@ class NetworkInterfaceBackend(object):
|
||||
ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeNetworkInterfaces".format(_filter))
|
||||
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):
|
||||
eni = self.get_network_interface(eni_id)
|
||||
group = self.get_security_group_from_id(group_id)
|
||||
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
|
||||
eni._group_set = [group]
|
||||
|
||||
|
||||
class Instance(BotoInstance, TaggedEC2Instance):
|
||||
@ -281,11 +249,10 @@ class Instance(BotoInstance, TaggedEC2Instance):
|
||||
# string will have a "u" prefix -- need to get rid of it
|
||||
self.user_data[0] = self.user_data[0].encode('utf-8')
|
||||
|
||||
self.nics = ec2_backend.prep_nics_for_instance(self,
|
||||
kwargs.get("nics", {}),
|
||||
subnet_id=kwargs.get("subnet_id",None),
|
||||
private_ip=kwargs.get("private_ip",None),
|
||||
associate_public_ip=kwargs.get("associate_public_ip",None))
|
||||
self.prep_nics(kwargs.get("nics", {}),
|
||||
subnet_id=kwargs.get("subnet_id",None),
|
||||
private_ip=kwargs.get("private_ip",None),
|
||||
associate_public_ip=kwargs.get("associate_public_ip",None))
|
||||
|
||||
@classmethod
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json):
|
||||
@ -349,6 +316,68 @@ class Instance(BotoInstance, TaggedEC2Instance):
|
||||
else:
|
||||
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):
|
||||
|
||||
|
@ -7,9 +7,6 @@ from moto.ec2.utils import sequence_from_querystring, filters_from_querystring
|
||||
|
||||
|
||||
class ElasticNetworkInterfaces(BaseResponse):
|
||||
def attach_network_interface(self):
|
||||
raise NotImplementedError('ElasticNetworkInterfaces(AmazonVPC).attach_network_interface is not yet implemented')
|
||||
|
||||
def create_network_interface(self):
|
||||
subnet_id = self.querystring.get('SubnetId')[0]
|
||||
private_ip_address = self.querystring.get('PrivateIpAddress', [None])[0]
|
||||
@ -35,8 +32,19 @@ class ElasticNetworkInterfaces(BaseResponse):
|
||||
template = Template(DESCRIBE_NETWORK_INTERFACES_RESPONSE)
|
||||
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):
|
||||
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):
|
||||
#Currently supports modifying one and only one security group
|
||||
@ -115,17 +123,17 @@ DESCRIBE_NETWORK_INTERFACES_RESPONSE = """<DescribeNetworkInterfacesResponse xml
|
||||
</item>
|
||||
{% endfor %}
|
||||
</groupSet>
|
||||
{% for attachment in eni.attachments %}
|
||||
<attachment>
|
||||
<attachmentId>{{ attachment['attachmentId'] }}</attachmentId>
|
||||
<instanceId>{{ attachment['instanceId'] }}</instanceId>
|
||||
<instanceOwnerId>190610284047</instanceOwnerId>
|
||||
<deviceIndex>{{ attachment['deviceIndex'] }}</deviceIndex>
|
||||
<status>attached</status>
|
||||
<attachTime>2013-10-04T17:38:53.000Z</attachTime>
|
||||
<deleteOnTermination>true</deleteOnTermination>
|
||||
</attachment>
|
||||
{% endfor %}
|
||||
{% if eni.instance %}
|
||||
<attachment>
|
||||
<attachmentId>{{ eni.attachment_id }}</attachmentId>
|
||||
<instanceId>{{ eni.instance.id }}</instanceId>
|
||||
<instanceOwnerId>190610284047</instanceOwnerId>
|
||||
<deviceIndex>{{ eni.device_index }}</deviceIndex>
|
||||
<status>attached</status>
|
||||
<attachTime>2013-10-04T17:38:53.000Z</attachTime>
|
||||
<deleteOnTermination>true</deleteOnTermination>
|
||||
</attachment>
|
||||
{% endif %}
|
||||
<association>
|
||||
<publicIp>{{ eni.public_ip }}</publicIp>
|
||||
<publicDnsName>ec2-54-200-86-47.us-west-2.compute.amazonaws.com</publicDnsName>
|
||||
@ -155,6 +163,16 @@ DESCRIBE_NETWORK_INTERFACES_RESPONSE = """<DescribeNetworkInterfacesResponse xml
|
||||
</networkInterfaceSet>
|
||||
</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/">
|
||||
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
|
||||
<return>true</return>
|
||||
|
@ -244,7 +244,7 @@ EC2_RUN_INSTANCES = """<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc
|
||||
{% endfor %}
|
||||
</groupSet>
|
||||
<attachment>
|
||||
<attachmentId>eni-attach-1a2b3c4d</attachmentId>
|
||||
<attachmentId>{{ nic.attachment_id }}</attachmentId>
|
||||
<deviceIndex>{{ nic.device_index }}</deviceIndex>
|
||||
<status>attached</status>
|
||||
<attachTime>YYYY-MM-DDTHH:MM:SS+0000</attachTime>
|
||||
@ -385,7 +385,7 @@ EC2_DESCRIBE_INSTANCES = """<DescribeInstancesResponse xmlns='http://ec2.amazona
|
||||
{% endfor %}
|
||||
</groupSet>
|
||||
<attachment>
|
||||
<attachmentId>eni-attach-1a2b3c4d</attachmentId>
|
||||
<attachmentId>{{ nic.attachment_id }}</attachmentId>
|
||||
<deviceIndex>{{ nic.device_index }}</deviceIndex>
|
||||
<status>attached</status>
|
||||
<attachTime>YYYY-MM-DDTHH:MM:SS+0000</attachTime>
|
||||
|
@ -11,6 +11,7 @@ from boto.exception import EC2ResponseError
|
||||
import sure # noqa
|
||||
|
||||
from moto import mock_ec2
|
||||
from tests.helpers import requires_boto_gte
|
||||
|
||||
|
||||
################ 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)
|
||||
|
||||
|
||||
@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
|
||||
def test_run_instance_with_keypair():
|
||||
conn = boto.connect_ec2('the_key', 'the_secret')
|
||||
|
Loading…
Reference in New Issue
Block a user