diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py index 358c84b2b..d97bc00a7 100644 --- a/moto/ec2/exceptions.py +++ b/moto/ec2/exceptions.py @@ -72,6 +72,14 @@ class InvalidSubnetIdError(EC2ClientError): .format(subnet_id)) +class InvalidNetworkInterfaceIdError(EC2ClientError): + def __init__(self, eni_id): + super(InvalidNetworkInterfaceIdError, self).__init__( + "InvalidNetworkInterfaceID.NotFound", + "The network interface ID '{0}' does not exist" + .format(eni_id)) + + class InvalidSecurityGroupDuplicateError(EC2ClientError): def __init__(self, name): super(InvalidSecurityGroupDuplicateError, self).__init__( diff --git a/moto/ec2/models.py b/moto/ec2/models.py index bab604890..5245fb098 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -26,6 +26,7 @@ from .exceptions import ( ResourceAlreadyAssociatedError, InvalidVPCIdError, InvalidSubnetIdError, + InvalidNetworkInterfaceIdError, InvalidSecurityGroupDuplicateError, InvalidSecurityGroupNotFoundError, InvalidPermissionNotFoundError, @@ -48,11 +49,14 @@ from .utils import ( random_dhcp_option_id, random_eip_allocation_id, random_eip_association_id, + random_eni_attach_id, + random_eni_id, random_internet_gateway_id, random_instance_id, random_internet_gateway_id, random_ip, random_key_pair, + random_public_ip, random_reservation_id, random_route_table_id, random_security_group_id, @@ -77,6 +81,172 @@ class TaggedEC2Instance(object): return tags +class NetworkInterface(object): + def __init__(self, subnet, private_ip_address, device_index=0, public_ip_auto_assign=True, group_ids=None): + self.id = random_eni_id() + self.device_index = device_index + self.private_ip_address = private_ip_address + self.subnet = subnet + self.instance = None + + self.public_ip = None + self.public_ip_auto_assign = public_ip_auto_assign + self.start() + + self.attachments = [] + self.group_set = [] + + group = None + if group_ids: + for group_id in group_ids: + group = ec2_backend.get_security_group_from_id(group_id) + if not group: + # Create with specific group ID. + 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) + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + properties = cloudformation_json['Properties'] + + security_group_ids = properties.get('SecurityGroups', []) + + subnet_id = properties['SubnetId'] + subnet = ec2_backend.get_subnet(subnet_id) + + private_ip_address = properties.get('PrivateIpAddress', None) + + network_interface = ec2_backend.create_network_interface( + subnet, + private_ip_address, + group_ids=security_group_ids + ) + return network_interface + + def stop(self): + if self.public_ip_auto_assign: + self.public_ip = None + + def start(self): + self.check_auto_public_ip() + + def check_auto_public_ip(self): + 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) + + +class NetworkInterfaceBackend(object): + def __init__(self): + self.enis = {} + super(NetworkInterfaceBackend, self).__init__() + + def create_network_interface(self, subnet, private_ip_address, group_ids=None, **kwargs): + eni = NetworkInterface(subnet, private_ip_address, group_ids=group_ids) + self.enis[eni.id] = eni + return eni + + def get_network_interface(self, eni_id): + for eni in self.enis.values(): + if eni_id == eni.id: + return eni + raise InvalidNetworkInterfaceIdError(eni_id) + + def delete_network_interface(self, eni_id): + deleted = self.enis.pop(eni_id, None) + if not deleted: + raise InvalidNetworkInterfaceIdError(eni_id) + return deleted + + def describe_network_interfaces(self, filters=None): + enis = self.enis.values() + + if filters: + for (_filter, _filter_value) in filters.items(): + if _filter == 'network-interface-id': + _filter = 'id' + enis = [ eni for eni in enis if getattr(eni, _filter) in _filter_value ] + elif _filter == 'group-id': + original_enis = enis + enis = [] + for eni in original_enis: + group_ids = [] + for group in eni.group_set: + if group.id in _filter_value: + enis.append(eni) + break + else: + ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeNetworkInterfaces".format(_filter)) + return enis + + 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 + + class Instance(BotoInstance, TaggedEC2Instance): def __init__(self, image_id, user_data, security_groups, **kwargs): super(Instance, self).__init__() @@ -108,6 +278,12 @@ 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)) + @classmethod def create_from_cloudformation_json(cls, resource_name, cloudformation_json): properties = cloudformation_json['Properties'] @@ -131,14 +307,23 @@ class Instance(BotoInstance, TaggedEC2Instance): return self.id def start(self, *args, **kwargs): + for nic in self.nics.values(): + nic.start() + self._state.name = "running" self._state.code = 16 def stop(self, *args, **kwargs): + for nic in self.nics.values(): + nic.stop() + self._state.name = "stopped" self._state.code = 80 def terminate(self, *args, **kwargs): + for nic in self.nics.values(): + nic.stop() + self._state.name = "terminated" self._state.code = 48 @@ -146,6 +331,21 @@ class Instance(BotoInstance, TaggedEC2Instance): self._state.name = "running" self._state.code = 16 + def get_tags(self): + tags = ec2_backend.describe_tags(self.id) + return tags + + @property + def dynamic_group_list(self): + if self.nics: + groups = [] + for nic in self.nics.values(): + for group in nic.group_set: + groups.append(group) + return groups + else: + return self.security_groups + class InstanceBackend(object): @@ -511,6 +711,7 @@ class SecurityGroup(object): self.description = description self.ingress_rules = [] self.egress_rules = [] + self.enis = {} self.vpc_id = vpc_id @classmethod @@ -569,18 +770,23 @@ class SecurityGroupBackend(object): def describe_security_groups(self): return itertools.chain(*[x.values() for x in self.groups.values()]) + def _delete_security_group(self, vpc_id, group_id): + if self.groups[vpc_id][group_id].enis: + raise DependencyViolationError("{0} is being utilized by {1}".format(group_id, 'ENIs')) + return self.groups[vpc_id].pop(group_id) + def delete_security_group(self, name=None, group_id=None): if group_id: # loop over all the SGs, find the right one - for vpc in self.groups.values(): - if group_id in vpc: - return vpc.pop(group_id) + for vpc_id, groups in self.groups.items(): + if group_id in groups: + return self._delete_security_group(vpc_id, group_id) raise InvalidSecurityGroupNotFoundError(group_id) elif name: # Group Name. Has to be in standard EC2, VPC needs to be identified by group_id group = self.get_security_group_from_name(name) if group: - return self.groups[None].pop(group.id) + return self._delete_security_group(None, group.id) raise InvalidSecurityGroupNotFoundError(name) def get_security_group_from_id(self, group_id): @@ -1003,6 +1209,12 @@ class SubnetBackend(object): self.subnets = {} super(SubnetBackend, self).__init__() + def get_subnet(self, subnet_id): + subnet = self.subnets.get(subnet_id, None) + if not subnet: + raise InvalidSubnetIdError(subnet_id) + return subnet + def create_subnet(self, vpc_id, cidr_block): subnet_id = random_subnet_id() subnet = Subnet(subnet_id, vpc_id, cidr_block) @@ -1289,6 +1501,7 @@ class ElasticAddress(object): self.allocation_id = random_eip_allocation_id() if domain == "vpc" else None self.domain = domain self.instance = None + self.eni = None self.association_id = None @classmethod @@ -1355,7 +1568,7 @@ class ElasticAddressBackend(object): return eips - def associate_address(self, instance, address=None, allocation_id=None, reassociate=False): + def associate_address(self, instance=None, eni=None, address=None, allocation_id=None, reassociate=False): eips = [] if address: eips = self.address_by_ip([address]) @@ -1363,13 +1576,20 @@ class ElasticAddressBackend(object): eips = self.address_by_allocation([allocation_id]) eip = eips[0] - if eip.instance and not reassociate: - raise ResourceAlreadyAssociatedError(eip.public_ip) + new_instance_association = bool(instance and (not eip.instance or eip.instance.id == instance.id)) + new_eni_association = bool(eni and (not eip.eni or eni.id == eip.eni.id)) - eip.instance = instance - if eip.domain == "vpc": - eip.association_id = random_eip_association_id() - return eip + if new_instance_association or new_eni_association or reassociate: + eip.instance = instance + eip.eni = eni + if eip.eni: + eip.eni.public_ip = eip.public_ip + if eip.domain == "vpc": + eip.association_id = random_eip_association_id() + + return eip + + raise ResourceAlreadyAssociatedError(eip.public_ip) def describe_addresses(self): return self.addresses @@ -1382,6 +1602,13 @@ class ElasticAddressBackend(object): eips = self.address_by_association([association_id]) eip = eips[0] + if eip.eni: + if eip.eni.instance and eip.eni.instance._state.name == "running": + eip.eni.check_auto_public_ip() + else: + eip.eni.public_ip = None + eip.eni = None + eip.instance = None eip.association_id = None return True @@ -1474,6 +1701,7 @@ class DHCPOptionsSetBackend(object): class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, RegionsAndZonesBackend, SecurityGroupBackend, EBSBackend, VPCBackend, SubnetBackend, SubnetRouteTableAssociationBackend, + NetworkInterfaceBackend, VPCPeeringConnectionBackend, RouteTableBackend, RouteBackend, InternetGatewayBackend, VPCGatewayAttachmentBackend, SpotRequestBackend, diff --git a/moto/ec2/responses/elastic_ip_addresses.py b/moto/ec2/responses/elastic_ip_addresses.py index 7169eb854..ff7e77be3 100644 --- a/moto/ec2/responses/elastic_ip_addresses.py +++ b/moto/ec2/responses/elastic_ip_addresses.py @@ -17,10 +17,12 @@ class ElasticIPAddresses(BaseResponse): return template.render(address=address) def associate_address(self): + instance = eni = None + if "InstanceId" in self.querystring: instance = ec2_backend.get_instance(self.querystring['InstanceId'][0]) elif "NetworkInterfaceId" in self.querystring: - raise NotImplementedError("Lookup by allocation id not implemented") + eni = ec2_backend.get_network_interface(self.querystring['NetworkInterfaceId'][0]) else: ec2_backend.raise_error("MissingParameter", "Invalid request, expect InstanceId/NetworkId parameter.") @@ -28,12 +30,15 @@ class ElasticIPAddresses(BaseResponse): if "AllowReassociation" in self.querystring: reassociate = self.querystring['AllowReassociation'][0] == "true" - if "PublicIp" in self.querystring: - eip = ec2_backend.associate_address(instance, address=self.querystring['PublicIp'][0], reassociate=reassociate) - elif "AllocationId" in self.querystring: - eip = ec2_backend.associate_address(instance, allocation_id=self.querystring['AllocationId'][0], reassociate=reassociate) + if instance or eni: + if "PublicIp" in self.querystring: + eip = ec2_backend.associate_address(instance=instance, eni=eni, address=self.querystring['PublicIp'][0], reassociate=reassociate) + elif "AllocationId" in self.querystring: + eip = ec2_backend.associate_address(instance=instance, eni=eni, allocation_id=self.querystring['AllocationId'][0], reassociate=reassociate) + else: + ec2_backend.raise_error("MissingParameter", "Invalid request, expect PublicIp/AllocationId parameter.") else: - ec2_backend.raise_error("MissingParameter", "Invalid request, expect PublicIp/AllocationId parameter.") + ec2_backend.raise_error("MissingParameter", "Invalid request, expect either instance or ENI.") template = Template(ASSOCIATE_ADDRESS_RESPONSE) return template.render(address=eip) @@ -103,6 +108,11 @@ DESCRIBE_ADDRESS_RESPONSE = """ + 2c6021ec-d705-445a-9780-420d0c7ab793 + + {{ eni.id }} + {{ eni.subnet.id }} + {{ eni.subnet.vpc_id }} + us-west-2a + + 498654062920 + false + pending + 02:07:a9:b6:12:51 + {% if eni.private_ip_address %} + {{ eni.private_ip_address }} + {% endif %} + true + + {% for group in eni.group_set %} + + {{ group.id }} + {{ group.name }} + + {% endfor %} + + + {% if eni.private_ip_address %} + + + {{ eni.private_ip_address }} + true + + + {% else %} + + {% endif %} + + +""" + +DESCRIBE_NETWORK_INTERFACES_RESPONSE = """ + ddb0aaf1-8b65-4f0a-94fa-654b18b8a204 + + {% for eni in enis %} + + {{ eni.id }} + {{ eni.subnet.id }} + vpc-9367a6f8 + us-west-2a + Primary network interface + 190610284047 + false + in-use + 0e:a3:a7:7b:95:a7 + {% if eni.private_ip_address %} + {{ eni.private_ip_address }} + {% endif %} + ip-10-0-0-134.us-west-2.compute.internal + true + + {% for group in eni.group_set %} + + {{ group.id }} + {{ group.name }} + + {% endfor %} + + {% for attachment in eni.attachments %} + + {{ attachment['attachmentId'] }} + {{ attachment['instanceId'] }} + 190610284047 + {{ attachment['deviceIndex'] }} + attached + 2013-10-04T17:38:53.000Z + true + + {% endfor %} + + {{ eni.public_ip }} + ec2-54-200-86-47.us-west-2.compute.amazonaws.com + amazon + + + {% if eni.private_ip_address %} + + + {{ eni.private_ip_address }} + ip-10-0-0-134.us-west-2.compute.internal + true + {% if eni.public_ip %} + + {{ eni.public_ip }} + ec2-54-200-86-47.us-west-2.compute.amazonaws.com + amazon + + {% endif %} + + + {% else %} + + {% endif %} + + {% endfor %} + +""" + +MODIFY_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true +""" + +DELETE_NETWORK_INTERFACE_RESPONSE = """ + + 34b5b3b4-d0c5-49b9-b5e2-a468ef6adcd8 + true +""" diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py index 863fe6600..c92de014b 100644 --- a/moto/ec2/responses/instances.py +++ b/moto/ec2/responses/instances.py @@ -3,7 +3,7 @@ from jinja2 import Template from moto.core.responses import BaseResponse from moto.core.utils import camelcase_to_underscores -from moto.ec2.utils import instance_ids_from_querystring, filters_from_querystring, filter_reservations +from moto.ec2.utils import instance_ids_from_querystring, filters_from_querystring, filter_reservations, dict_from_querystring class InstanceResponse(BaseResponse): @@ -26,13 +26,19 @@ class InstanceResponse(BaseResponse): user_data = self.querystring.get('UserData') security_group_names = self._get_multi_param('SecurityGroup') security_group_ids = self._get_multi_param('SecurityGroupId') + nics = dict_from_querystring("NetworkInterface", self.querystring) instance_type = self.querystring.get("InstanceType", ["m1.small"])[0] subnet_id = self.querystring.get("SubnetId", [None])[0] + private_ip = self.querystring.get("PrivateIpAddress", [None])[0] + associate_public_ip = self.querystring.get("AssociatePublicIpAddress", [None])[0] key_name = self.querystring.get("KeyName", [None])[0] + new_reservation = self.ec2_backend.add_instances( image_id, min_count, user_data, security_group_names, instance_type=instance_type, subnet_id=subnet_id, - key_name=key_name, security_group_ids=security_group_ids) + key_name=key_name, security_group_ids=security_group_ids, + nics=nics, private_ip=private_ip, associate_public_ip=associate_public_ip) + template = Template(EC2_RUN_INSTANCES) return template.render(reservation=new_reservation) @@ -189,10 +195,19 @@ EC2_RUN_INSTANCES = """ disabled - {{ instance.subnet_id }} - vpc-1a2b3c4d - 10.0.0.12 - 46.51.219.63 + {% if instance.nics %} + {{ instance.nics[0].subnet.id }} + {{ instance.nics[0].subnet.vpc_id }} + {{ instance.nics[0].private_ip_address }} + {% if instance.nics[0].public_ip %} + 46.51.219.63 + {% endif %} + {% endif %} true - {% for group in instance.security_groups %} + {% for group in instance.dynamic_group_list %} {{ group.id }} {{ group.name }} @@ -280,7 +354,54 @@ EC2_DESCRIBE_INSTANCES = """