From 5b0e463554c9afb5919e006ee73f6ef476b277fe Mon Sep 17 00:00:00 2001 From: Terry Cain Date: Tue, 24 Oct 2017 23:57:43 +0100 Subject: [PATCH] Fixed a few ip issues Fixes #1175 --- moto/ec2/models.py | 83 ++++++++++++++++++++-- tests/test_ec2/test_instances.py | 116 ++++++++++++++++++++++++++++--- 2 files changed, 183 insertions(+), 16 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 1835f4df..c5e93c8f 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import copy import itertools +import ipaddress import json import os import re @@ -402,6 +403,10 @@ class Instance(TaggedEC2Resource, BotoInstance): subnet = ec2_backend.get_subnet(self.subnet_id) self.vpc_id = subnet.vpc_id self._placement.zone = subnet.availability_zone + + if associate_public_ip is None: + # Mapping public ip hasnt been explicitly enabled or disabled + associate_public_ip = subnet.map_public_ip_on_launch == 'true' elif placement: self._placement.zone = placement else: @@ -409,10 +414,22 @@ class Instance(TaggedEC2Resource, BotoInstance): self.block_device_mapping = BlockDeviceMapping() - self.prep_nics(kwargs.get("nics", {}), - subnet_id=self.subnet_id, - private_ip=kwargs.get("private_ip"), - associate_public_ip=associate_public_ip) + self._private_ips = set() + self.prep_nics( + kwargs.get("nics", {}), + private_ip=kwargs.get("private_ip"), + associate_public_ip=associate_public_ip + ) + + def __del__(self): + try: + subnet = self.ec2_backend.get_subnet(self.subnet_id) + for ip in self._private_ips: + subnet.del_subnet_ip(ip) + except Exception: + # Its not "super" critical we clean this up, as reset will do this + # worst case we'll get IP address exaustion... rarely + pass def setup_defaults(self): # Default have an instance with root volume should you not wish to @@ -547,14 +564,19 @@ class Instance(TaggedEC2Resource, BotoInstance): else: return self.security_groups - def prep_nics(self, nic_spec, subnet_id=None, private_ip=None, associate_public_ip=None): + def prep_nics(self, nic_spec, private_ip=None, associate_public_ip=None): self.nics = {} + subnet = self.ec2_backend.get_subnet(self.subnet_id) if not private_ip: - private_ip = random_private_ip() + private_ip = subnet.get_available_subnet_ip(instance=self) + else: + subnet.request_ip(private_ip, instance=self) + + self._private_ips.add(private_ip) # Primary NIC defaults - primary_nic = {'SubnetId': subnet_id, + primary_nic = {'SubnetId': self.subnet_id, 'PrivateIpAddress': private_ip, 'AssociatePublicIpAddress': associate_public_ip} primary_nic = dict((k, v) for k, v in primary_nic.items() if v) @@ -2114,10 +2136,17 @@ class Subnet(TaggedEC2Resource): self.id = subnet_id self.vpc_id = vpc_id self.cidr_block = cidr_block + self.cidr = ipaddress.ip_network(self.cidr_block) self._availability_zone = availability_zone self.default_for_az = default_for_az self.map_public_ip_on_launch = map_public_ip_on_launch + # Theory is we assign ip's as we go (as 16,777,214 usable IPs in a /8) + self._subnet_ip_generator = self.cidr.hosts() + self.reserved_ips = [six.next(self._subnet_ip_generator) for _ in range(0, 3)] # Reserved by AWS + self._unused_ips = set() # if instance is destroyed hold IP here for reuse + self._subnet_ips = {} # has IP: instance + @classmethod def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] @@ -2184,6 +2213,46 @@ class Subnet(TaggedEC2Resource): '"Fn::GetAtt" : [ "{0}" , "AvailabilityZone" ]"') raise UnformattedGetAttTemplateException() + def get_available_subnet_ip(self, instance): + try: + new_ip = self._unused_ips.pop() + except KeyError: + new_ip = six.next(self._subnet_ip_generator) + + # Skips any IP's if they've been manually specified + while str(new_ip) in self._subnet_ips: + new_ip = six.next(self._subnet_ip_generator) + + if new_ip == self.cidr.broadcast_address: + raise StopIteration() # Broadcast address cant be used obviously + # TODO StopIteration will be raised if no ip's available, not sure how aws handles this. + + new_ip = str(new_ip) + self._subnet_ips[new_ip] = instance + + return new_ip + + def request_ip(self, ip, instance): + if ipaddress.ip_address(ip) not in self.cidr: + raise Exception('IP does not fall in the subnet CIDR of {0}'.format(self.cidr)) + + if ip in self._subnet_ips: + raise Exception('IP already in use') + try: + self._unused_ips.remove(ip) + except KeyError: + pass + + self._subnet_ips[ip] = instance + return ip + + def del_subnet_ip(self, ip): + try: + del self._subnet_ips[ip] + self._unused_ips.add(ip) + except KeyError: + pass # Unknown IP + class SubnetBackend(object): def __init__(self): diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index 46bb34d5..cc8970b0 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -5,6 +5,7 @@ from nose.tools import assert_raises import base64 import datetime +import ipaddress import boto import boto3 @@ -830,18 +831,113 @@ def test_run_instance_with_placement(): instance.placement.should.equal("us-east-1b") -@mock_ec2_deprecated +@mock_ec2 def test_run_instance_with_subnet(): - 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") - reservation = conn.run_instances('ami-1234abcd', subnet_id=subnet.id) - instance = reservation.instances[0] + client = boto3.client('ec2', region_name='eu-central-1') - instance.subnet_id.should.equal(subnet.id) + ip_networks = [ + (ipaddress.ip_network('10.0.0.0/16'), ipaddress.ip_network('10.0.99.0/24')), + (ipaddress.ip_network('192.168.42.0/24'), ipaddress.ip_network('192.168.42.0/25')) + ] - all_enis = conn.get_all_network_interfaces() - all_enis.should.have.length_of(1) + # Tests instances are created with the correct IPs + for vpc_cidr, subnet_cidr in ip_networks: + resp = client.create_vpc( + CidrBlock=str(vpc_cidr), + AmazonProvidedIpv6CidrBlock=False, + DryRun=False, + InstanceTenancy='default' + ) + vpc_id = resp['Vpc']['VpcId'] + + resp = client.create_subnet( + CidrBlock=str(subnet_cidr), + VpcId=vpc_id + ) + subnet_id = resp['Subnet']['SubnetId'] + + resp = client.run_instances( + ImageId='ami-1234abcd', + MaxCount=1, + MinCount=1, + SubnetId=subnet_id + ) + instance = resp['Instances'][0] + instance['SubnetId'].should.equal(subnet_id) + + priv_ipv4 = ipaddress.ip_address(instance['PrivateIpAddress']) + subnet_cidr.should.contain(priv_ipv4) + + +@mock_ec2 +def test_run_instance_with_specified_private_ipv4(): + client = boto3.client('ec2', region_name='eu-central-1') + + vpc_cidr = ipaddress.ip_network('192.168.42.0/24') + subnet_cidr = ipaddress.ip_network('192.168.42.0/25') + + resp = client.create_vpc( + CidrBlock=str(vpc_cidr), + AmazonProvidedIpv6CidrBlock=False, + DryRun=False, + InstanceTenancy='default' + ) + vpc_id = resp['Vpc']['VpcId'] + + resp = client.create_subnet( + CidrBlock=str(subnet_cidr), + VpcId=vpc_id + ) + subnet_id = resp['Subnet']['SubnetId'] + + resp = client.run_instances( + ImageId='ami-1234abcd', + MaxCount=1, + MinCount=1, + SubnetId=subnet_id, + PrivateIpAddress='192.168.42.5' + ) + instance = resp['Instances'][0] + instance['SubnetId'].should.equal(subnet_id) + instance['PrivateIpAddress'].shoud.equal('192.168.42.5') + + +@mock_ec2 +def test_run_instance_mapped_public_ipv4(): + client = boto3.client('ec2', region_name='eu-central-1') + + vpc_cidr = ipaddress.ip_network('192.168.42.0/24') + subnet_cidr = ipaddress.ip_network('192.168.42.0/25') + + resp = client.create_vpc( + CidrBlock=str(vpc_cidr), + AmazonProvidedIpv6CidrBlock=False, + DryRun=False, + InstanceTenancy='default' + ) + vpc_id = resp['Vpc']['VpcId'] + + resp = client.create_subnet( + CidrBlock=str(subnet_cidr), + VpcId=vpc_id + ) + subnet_id = resp['Subnet']['SubnetId'] + client.modify_subnet_attribute( + SubnetId=subnet_id, + MapPublicIpOnLaunch={'Value': True} + ) + + resp = client.run_instances( + ImageId='ami-1234abcd', + MaxCount=1, + MinCount=1, + SubnetId=subnet_id + ) + instance = resp['Instances'][0] + instance.should.contain('PublicDnsName') + instance.should.contain('PublicIpAddress') + len(instance['PublicDnsName']).should.be.greater_than(0) + len(instance['PublicIpAddress']).should.be.greater_than(0) @mock_ec2_deprecated @@ -880,6 +976,7 @@ def test_run_instance_with_nic_autocreated(): eni.private_ip_addresses.should.have.length_of(1) eni.private_ip_addresses[0].private_ip_address.should.equal(private_ip) + @mock_ec2_deprecated def test_run_instance_with_nic_preexisting(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -1012,6 +1109,7 @@ def test_ec2_classic_has_public_ip_address(): instance.private_ip_address.should_not.equal(None) instance.private_dns_name.should.contain(instance.private_ip_address.replace('.', '-')) + @mock_ec2_deprecated def test_run_instance_with_keypair(): conn = boto.connect_ec2('the_key', 'the_secret')