diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 1c9e34989..719c5e94f 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -15,6 +15,7 @@ from boto.ec2.launchspecification import LaunchSpecification from moto.core import BaseBackend from moto.core.models import Model +from moto.core.utils import iso_8601_datetime_with_milliseconds from .exceptions import ( EC2ClientError, DependencyViolationError, @@ -70,6 +71,7 @@ from .utils import ( random_instance_id, random_internet_gateway_id, random_ip, + random_nat_gateway_id, random_key_pair, random_private_ip, random_public_ip, @@ -2955,6 +2957,61 @@ class CustomerGatewayBackend(object): return deleted +class NatGateway(object): + + def __init__(self, backend, subnet_id, allocation_id): + # public properties + self.id = random_nat_gateway_id() + self.subnet_id = subnet_id + self.allocation_id = allocation_id + self.state = 'available' + self.private_ip = random_private_ip() + + # protected properties + self._created_at = datetime.utcnow() + self._backend = backend + # NOTE: this is the core of NAT Gateways creation + self._eni = self._backend.create_network_interface(backend.get_subnet(self.subnet_id), self.private_ip) + + # associate allocation with ENI + self._backend.associate_address(eni=self._eni, allocation_id=self.allocation_id) + + @property + def vpc_id(self): + subnet = self._backend.get_subnet(self.subnet_id) + return subnet.vpc_id + + @property + def create_time(self): + return iso_8601_datetime_with_milliseconds(self._created_at) + + @property + def network_interface_id(self): + return self._eni.id + + @property + def public_ip(self): + eips = self._backend.address_by_allocation([self.allocation_id]) + return eips[0].public_ip + + +class NatGatewayBackend(object): + + def __init__(self): + self.nat_gateways = {} + + def get_all_nat_gateways(self, filters): + return self.nat_gateways.values() + + def create_nat_gateway(self, subnet_id, allocation_id): + nat_gateway = NatGateway(self, subnet_id, allocation_id) + self.nat_gateways[nat_gateway.id] = nat_gateway + return nat_gateway + + def delete_nat_gateway(self, nat_gateway_id): + return self.nat_gateways.pop(nat_gateway_id) + + class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, RegionsAndZonesBackend, SecurityGroupBackend, EBSBackend, VPCBackend, SubnetBackend, SubnetRouteTableAssociationBackend, @@ -2963,7 +3020,8 @@ class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, RouteTableBackend, RouteBackend, InternetGatewayBackend, VPCGatewayAttachmentBackend, SpotRequestBackend, ElasticAddressBackend, KeyPairBackend, DHCPOptionsSetBackend, - NetworkAclBackend, VpnGatewayBackend, CustomerGatewayBackend): + NetworkAclBackend, VpnGatewayBackend, CustomerGatewayBackend, + NatGatewayBackend): def __init__(self, region_name): super(EC2Backend, self).__init__() diff --git a/moto/ec2/responses/__init__.py b/moto/ec2/responses/__init__.py index e51992e41..d939178fb 100644 --- a/moto/ec2/responses/__init__.py +++ b/moto/ec2/responses/__init__.py @@ -29,6 +29,7 @@ from .vpcs import VPCs from .vpc_peering_connections import VPCPeeringConnections from .vpn_connections import VPNConnections from .windows import Windows +from .nat_gateways import NatGateways class EC2Response( @@ -61,6 +62,7 @@ class EC2Response( VPCPeeringConnections, VPNConnections, Windows, + NatGateways, ): @property def ec2_backend(self): diff --git a/moto/ec2/responses/nat_gateways.py b/moto/ec2/responses/nat_gateways.py new file mode 100644 index 000000000..e960b990c --- /dev/null +++ b/moto/ec2/responses/nat_gateways.py @@ -0,0 +1,73 @@ +from __future__ import unicode_literals +from moto.core.responses import BaseResponse +from moto.ec2.utils import filters_from_querystring + + +class NatGateways(BaseResponse): + + def create_nat_gateway(self): + subnet_id = self.querystring.get('SubnetId')[0] + allocation_id = self.querystring.get('AllocationId')[0] + nat_gateway = self.ec2_backend.create_nat_gateway(subnet_id=subnet_id, allocation_id=allocation_id) + template = self.response_template(CREATE_NAT_GATEWAY) + return template.render(nat_gateway=nat_gateway) + + def delete_nat_gateway(self): + nat_gateway_id = self.querystring.get('NatGatewayId')[0] + nat_gateway = self.ec2_backend.delete_nat_gateway(nat_gateway_id) + template = self.response_template(DELETE_NAT_GATEWAY_RESPONSE) + return template.render(nat_gateway=nat_gateway) + + def describe_nat_gateways(self): + filters = filters_from_querystring(self.querystring) + nat_gateways = self.ec2_backend.get_all_nat_gateways(filters) + template = self.response_template(DESCRIBE_NAT_GATEWAYS_RESPONSE) + return template.render(nat_gateways=nat_gateways) + + +DESCRIBE_NAT_GATEWAYS_RESPONSE = """ + bfed02c6-dae9-47c0-86a2-example + + {% for nat_gateway in nat_gateways %} + + {{ nat_gateway.subnet_id }} + + + {{ nat_gateway.network_interface_id }} + {{ nat_gateway.public_ip }} + {{ nat_gateway.allocation_id }} + {{ nat_gateway.private_ip }} + + + {{ nat_gateway.create_time }} + {{ nat_gateway.vpc_id }} + {{ nat_gateway.id }} + {{ nat_gateway.state }} + + {% endfor %} + + +""" + +CREATE_NAT_GATEWAY = """ + 1b74dc5c-bcda-403f-867d-example + + {{ nat_gateway.subnet_id }} + + + {{ nat_gateway.allocation_id }} + + + {{ nat_gateway.create_time }} + {{ nat_gateway.vpc_id }} + {{ nat_gateway.id }} + {{ nat_gateway.state }} + + +""" + + +DELETE_NAT_GATEWAY_RESPONSE = """ + 741fc8ab-6ebe-452b-b92b-example + {{ nat_gateway.id }} +""" diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index 5b7743bf4..5b05cafa0 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -9,6 +9,7 @@ EC2_RESOURCE_TO_PREFIX = { 'image': 'ami', 'instance': 'i', 'internet-gateway': 'igw', + 'nat-gateway': 'nat', 'network-acl': 'acl', 'network-acl-subnet-assoc': 'aclassoc', 'network-interface': 'eni', @@ -33,8 +34,7 @@ EC2_RESOURCE_TO_PREFIX = { EC2_PREFIX_TO_RESOURCE = dict((v, k) for (k, v) in EC2_RESOURCE_TO_PREFIX.items()) -def random_id(prefix=''): - size = 8 +def random_id(prefix='', size=8): chars = list(range(10)) + ['a', 'b', 'c', 'd', 'e', 'f'] resource_id = ''.join(six.text_type(random.choice(chars)) for x in range(size)) @@ -133,6 +133,10 @@ def random_eni_attach_id(): return random_id(prefix=EC2_RESOURCE_TO_PREFIX['network-interface-attachment']) +def random_nat_gateway_id(): + return random_id(prefix=EC2_RESOURCE_TO_PREFIX['nat-gateway'], size=17) + + def random_public_ip(): return '54.214.{0}.{1}'.format(random.choice(range(255)), random.choice(range(255))) diff --git a/tests/test_ec2/test_nat_gateway.py b/tests/test_ec2/test_nat_gateway.py new file mode 100644 index 000000000..d073fc5e3 --- /dev/null +++ b/tests/test_ec2/test_nat_gateway.py @@ -0,0 +1,100 @@ +from __future__ import unicode_literals +import boto3 +import sure # noqa +from moto import mock_ec2 + + +@mock_ec2 +def test_describe_nat_gateways(): + conn = boto3.client('ec2', 'us-east-1') + + response = conn.describe_nat_gateways() + + response['NatGateways'].should.have.length_of(0) + + +@mock_ec2 +def test_create_nat_gateway(): + conn = boto3.client('ec2', 'us-east-1') + vpc = conn.create_vpc(CidrBlock='10.0.0.0/16') + vpc_id = vpc['Vpc']['VpcId'] + subnet = conn.create_subnet( + VpcId=vpc_id, + CidrBlock='10.0.1.0/27', + AvailabilityZone='us-east-1a', + ) + allocation_id = conn.allocate_address(Domain='vpc')['AllocationId'] + subnet_id = subnet['Subnet']['SubnetId'] + + response = conn.create_nat_gateway( + SubnetId=subnet_id, + AllocationId=allocation_id, + ) + + response['NatGateway']['VpcId'].should.equal(vpc_id) + response['NatGateway']['SubnetId'].should.equal(subnet_id) + response['NatGateway']['State'].should.equal('available') + + +@mock_ec2 +def test_delete_nat_gateway(): + conn = boto3.client('ec2', 'us-east-1') + vpc = conn.create_vpc(CidrBlock='10.0.0.0/16') + vpc_id = vpc['Vpc']['VpcId'] + subnet = conn.create_subnet( + VpcId=vpc_id, + CidrBlock='10.0.1.0/27', + AvailabilityZone='us-east-1a', + ) + allocation_id = conn.allocate_address(Domain='vpc')['AllocationId'] + subnet_id = subnet['Subnet']['SubnetId'] + + nat_gateway = conn.create_nat_gateway( + SubnetId=subnet_id, + AllocationId=allocation_id, + ) + nat_gateway_id = nat_gateway['NatGateway']['NatGatewayId'] + response = conn.delete_nat_gateway(NatGatewayId=nat_gateway_id) + + response.should.equal({ + 'NatGatewayId': nat_gateway_id, + 'ResponseMetadata': { + 'HTTPStatusCode': 200, + 'RequestId': '741fc8ab-6ebe-452b-b92b-example' + } + }) + + +@mock_ec2 +def test_create_and_describe_nat_gateway(): + conn = boto3.client('ec2', 'us-east-1') + vpc = conn.create_vpc(CidrBlock='10.0.0.0/16') + vpc_id = vpc['Vpc']['VpcId'] + subnet = conn.create_subnet( + VpcId=vpc_id, + CidrBlock='10.0.1.0/27', + AvailabilityZone='us-east-1a', + ) + allocation_id = conn.allocate_address(Domain='vpc')['AllocationId'] + subnet_id = subnet['Subnet']['SubnetId'] + + create_response = conn.create_nat_gateway( + SubnetId=subnet_id, + AllocationId=allocation_id, + ) + nat_gateway_id = create_response['NatGateway']['NatGatewayId'] + describe_response = conn.describe_nat_gateways() + + enis = conn.describe_network_interfaces()['NetworkInterfaces'] + eni_id = enis[0]['NetworkInterfaceId'] + public_ip = conn.describe_addresses(AllocationIds=[allocation_id])['Addresses'][0]['PublicIp'] + + describe_response['NatGateways'].should.have.length_of(1) + describe_response['NatGateways'][0]['NatGatewayId'].should.equal(nat_gateway_id) + describe_response['NatGateways'][0]['State'].should.equal('available') + describe_response['NatGateways'][0]['SubnetId'].should.equal(subnet_id) + describe_response['NatGateways'][0]['VpcId'].should.equal(vpc_id) + describe_response['NatGateways'][0]['NatGatewayAddresses'][0]['AllocationId'].should.equal(allocation_id) + describe_response['NatGateways'][0]['NatGatewayAddresses'][0]['NetworkInterfaceId'].should.equal(eni_id) + assert describe_response['NatGateways'][0]['NatGatewayAddresses'][0]['PrivateIp'].startswith('10.') + describe_response['NatGateways'][0]['NatGatewayAddresses'][0]['PublicIp'].should.equal(public_ip)