diff --git a/AUTHORS.md b/AUTHORS.md
index fbca08368..01b000182 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -54,5 +54,6 @@ Moto is written by Steve Pulec with contributions from:
* [William Richard](https://github.com/william-richard)
* [Alex Casalboni](https://github.com/alexcasalboni)
* [Jon Beilke](https://github.com/jrbeilke)
+* [Bendeguz Acs](https://github.com/acsbendi)
* [Craig Anderson](https://github.com/craiga)
* [Robert Lewis](https://github.com/ralewis85)
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index e03eaabe1..c2fec6ece 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -1473,7 +1473,7 @@
- [X] describe_spot_instance_requests
- [ ] describe_spot_price_history
- [ ] describe_stale_security_groups
-- [ ] describe_subnets
+- [X] describe_subnets
- [X] describe_tags
- [ ] describe_volume_attribute
- [ ] describe_volume_status
diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py
index 1357d49e2..259e84bc3 100644
--- a/moto/ec2/exceptions.py
+++ b/moto/ec2/exceptions.py
@@ -430,6 +430,16 @@ class OperationNotPermitted(EC2ClientError):
)
+class InvalidAvailabilityZoneError(EC2ClientError):
+
+ def __init__(self, availability_zone_value, valid_availability_zones):
+ super(InvalidAvailabilityZoneError, self).__init__(
+ "InvalidParameterValue",
+ "Value ({0}) for parameter availabilityZone is invalid. "
+ "Subnets can currently only be created in the following availability zones: {1}.".format(availability_zone_value, valid_availability_zones)
+ )
+
+
class NetworkAclEntryAlreadyExistsError(EC2ClientError):
def __init__(self, rule_number):
diff --git a/moto/ec2/models.py b/moto/ec2/models.py
index b894853d2..811283fe8 100644
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -36,6 +36,7 @@ from .exceptions import (
InvalidAMIIdError,
InvalidAMIAttributeItemValueError,
InvalidAssociationIdError,
+ InvalidAvailabilityZoneError,
InvalidCIDRBlockParameterError,
InvalidCIDRSubnetError,
InvalidCustomerGatewayIdError,
@@ -1288,17 +1289,107 @@ class Region(object):
class Zone(object):
- def __init__(self, name, region_name):
+ def __init__(self, name, region_name, zone_id):
self.name = name
self.region_name = region_name
+ self.zone_id = zone_id
class RegionsAndZonesBackend(object):
regions = [Region(ri.name, ri.endpoint) for ri in boto.ec2.regions()]
- zones = dict(
- (region, [Zone(region + c, region) for c in 'abc'])
- for region in [r.name for r in regions])
+ zones = {
+ 'ap-south-1': [
+ Zone(region_name="ap-south-1", name="ap-south-1a", zone_id="aps1-az1"),
+ Zone(region_name="ap-south-1", name="ap-south-1b", zone_id="aps1-az3")
+ ],
+ 'eu-west-3': [
+ Zone(region_name="eu-west-3", name="eu-west-3a", zone_id="euw3-az1"),
+ Zone(region_name="eu-west-3", name="eu-west-3b", zone_id="euw3-az2"),
+ Zone(region_name="eu-west-3", name="eu-west-3c", zone_id="euw3-az3")
+ ],
+ 'eu-north-1': [
+ Zone(region_name="eu-north-1", name="eu-north-1a", zone_id="eun1-az1"),
+ Zone(region_name="eu-north-1", name="eu-north-1b", zone_id="eun1-az2"),
+ Zone(region_name="eu-north-1", name="eu-north-1c", zone_id="eun1-az3")
+ ],
+ 'eu-west-2': [
+ Zone(region_name="eu-west-2", name="eu-west-2a", zone_id="euw2-az2"),
+ Zone(region_name="eu-west-2", name="eu-west-2b", zone_id="euw2-az3"),
+ Zone(region_name="eu-west-2", name="eu-west-2c", zone_id="euw2-az1")
+ ],
+ 'eu-west-1': [
+ Zone(region_name="eu-west-1", name="eu-west-1a", zone_id="euw1-az3"),
+ Zone(region_name="eu-west-1", name="eu-west-1b", zone_id="euw1-az1"),
+ Zone(region_name="eu-west-1", name="eu-west-1c", zone_id="euw1-az2")
+ ],
+ 'ap-northeast-3': [
+ Zone(region_name="ap-northeast-3", name="ap-northeast-2a", zone_id="apne3-az1")
+ ],
+ 'ap-northeast-2': [
+ Zone(region_name="ap-northeast-2", name="ap-northeast-2a", zone_id="apne2-az1"),
+ Zone(region_name="ap-northeast-2", name="ap-northeast-2c", zone_id="apne2-az3")
+ ],
+ 'ap-northeast-1': [
+ Zone(region_name="ap-northeast-1", name="ap-northeast-1a", zone_id="apne1-az4"),
+ Zone(region_name="ap-northeast-1", name="ap-northeast-1c", zone_id="apne1-az1"),
+ Zone(region_name="ap-northeast-1", name="ap-northeast-1d", zone_id="apne1-az2")
+ ],
+ 'sa-east-1': [
+ Zone(region_name="sa-east-1", name="sa-east-1a", zone_id="sae1-az1"),
+ Zone(region_name="sa-east-1", name="sa-east-1c", zone_id="sae1-az3")
+ ],
+ 'ca-central-1': [
+ Zone(region_name="ca-central-1", name="ca-central-1a", zone_id="cac1-az1"),
+ Zone(region_name="ca-central-1", name="ca-central-1b", zone_id="cac1-az2")
+ ],
+ 'ap-southeast-1': [
+ Zone(region_name="ap-southeast-1", name="ap-southeast-1a", zone_id="apse1-az1"),
+ Zone(region_name="ap-southeast-1", name="ap-southeast-1b", zone_id="apse1-az2"),
+ Zone(region_name="ap-southeast-1", name="ap-southeast-1c", zone_id="apse1-az3")
+ ],
+ 'ap-southeast-2': [
+ Zone(region_name="ap-southeast-2", name="ap-southeast-2a", zone_id="apse2-az1"),
+ Zone(region_name="ap-southeast-2", name="ap-southeast-2b", zone_id="apse2-az3"),
+ Zone(region_name="ap-southeast-2", name="ap-southeast-2c", zone_id="apse2-az2")
+ ],
+ 'eu-central-1': [
+ Zone(region_name="eu-central-1", name="eu-central-1a", zone_id="euc1-az2"),
+ Zone(region_name="eu-central-1", name="eu-central-1b", zone_id="euc1-az3"),
+ Zone(region_name="eu-central-1", name="eu-central-1c", zone_id="euc1-az1")
+ ],
+ 'us-east-1': [
+ Zone(region_name="us-east-1", name="us-east-1a", zone_id="use1-az6"),
+ Zone(region_name="us-east-1", name="us-east-1b", zone_id="use1-az1"),
+ Zone(region_name="us-east-1", name="us-east-1c", zone_id="use1-az2"),
+ Zone(region_name="us-east-1", name="us-east-1d", zone_id="use1-az4"),
+ Zone(region_name="us-east-1", name="us-east-1e", zone_id="use1-az3"),
+ Zone(region_name="us-east-1", name="us-east-1f", zone_id="use1-az5")
+ ],
+ 'us-east-2': [
+ Zone(region_name="us-east-2", name="us-east-2a", zone_id="use2-az1"),
+ Zone(region_name="us-east-2", name="us-east-2b", zone_id="use2-az2"),
+ Zone(region_name="us-east-2", name="us-east-2c", zone_id="use2-az3")
+ ],
+ 'us-west-1': [
+ Zone(region_name="us-west-1", name="us-west-1a", zone_id="usw1-az3"),
+ Zone(region_name="us-west-1", name="us-west-1b", zone_id="usw1-az1")
+ ],
+ 'us-west-2': [
+ Zone(region_name="us-west-2", name="us-west-2a", zone_id="usw2-az2"),
+ Zone(region_name="us-west-2", name="us-west-2b", zone_id="usw2-az1"),
+ Zone(region_name="us-west-2", name="us-west-2c", zone_id="usw2-az3")
+ ],
+ 'cn-north-1': [
+ Zone(region_name="cn-north-1", name="cn-north-1a", zone_id="cnn1-az1"),
+ Zone(region_name="cn-north-1", name="cn-north-1b", zone_id="cnn1-az2")
+ ],
+ 'us-gov-west-1': [
+ Zone(region_name="us-gov-west-1", name="us-gov-west-1a", zone_id="usgw1-az1"),
+ Zone(region_name="us-gov-west-1", name="us-gov-west-1b", zone_id="usgw1-az2"),
+ Zone(region_name="us-gov-west-1", name="us-gov-west-1c", zone_id="usgw1-az3")
+ ]
+ }
def describe_regions(self, region_names=[]):
if len(region_names) == 0:
@@ -2374,7 +2465,7 @@ class VPCPeeringConnectionBackend(object):
class Subnet(TaggedEC2Resource):
def __init__(self, ec2_backend, subnet_id, vpc_id, cidr_block, availability_zone, default_for_az,
- map_public_ip_on_launch):
+ map_public_ip_on_launch, owner_id=111122223333, assign_ipv6_address_on_creation=False):
self.ec2_backend = ec2_backend
self.id = subnet_id
self.vpc_id = vpc_id
@@ -2383,6 +2474,9 @@ class Subnet(TaggedEC2Resource):
self._availability_zone = availability_zone
self.default_for_az = default_for_az
self.map_public_ip_on_launch = map_public_ip_on_launch
+ self.owner_id = owner_id
+ self.assign_ipv6_address_on_creation = assign_ipv6_address_on_creation
+ self.ipv6_cidr_block_associations = []
# 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()
@@ -2412,7 +2506,7 @@ class Subnet(TaggedEC2Resource):
@property
def availability_zone(self):
- return self._availability_zone
+ return self._availability_zone.name
@property
def physical_resource_id(self):
@@ -2509,7 +2603,7 @@ class SubnetBackend(object):
return subnets[subnet_id]
raise InvalidSubnetIdError(subnet_id)
- def create_subnet(self, vpc_id, cidr_block, availability_zone):
+ def create_subnet(self, vpc_id, cidr_block, availability_zone, context=None):
subnet_id = random_subnet_id()
vpc = self.get_vpc(vpc_id) # Validate VPC exists and the supplied CIDR block is a subnet of the VPC's
vpc_cidr_block = ipaddress.IPv4Network(six.text_type(vpc.cidr_block), strict=False)
@@ -2529,8 +2623,15 @@ class SubnetBackend(object):
# consider it the default
default_for_az = str(availability_zone not in self.subnets).lower()
map_public_ip_on_launch = default_for_az
- subnet = Subnet(self, subnet_id, vpc_id, cidr_block, availability_zone,
- default_for_az, map_public_ip_on_launch)
+ if availability_zone is None:
+ availability_zone = 'us-east-1a'
+ try:
+ availability_zone_data = next(zone for zones in RegionsAndZonesBackend.zones.values() for zone in zones if zone.name == availability_zone)
+ except StopIteration:
+ raise InvalidAvailabilityZoneError(availability_zone, ", ".join([zone.name for zones in RegionsAndZonesBackend.zones.values() for zone in zones]))
+ subnet = Subnet(self, subnet_id, vpc_id, cidr_block, availability_zone_data,
+ default_for_az, map_public_ip_on_launch,
+ owner_id=context.get_current_user() if context else '111122223333', assign_ipv6_address_on_creation=False)
# AWS associates a new subnet with the default Network ACL
self.associate_default_network_acl_with_subnet(subnet_id, vpc_id)
@@ -2558,11 +2659,12 @@ class SubnetBackend(object):
return subnets.pop(subnet_id, None)
raise InvalidSubnetIdError(subnet_id)
- def modify_subnet_attribute(self, subnet_id, map_public_ip):
+ def modify_subnet_attribute(self, subnet_id, attr_name, attr_value):
subnet = self.get_subnet(subnet_id)
- if map_public_ip not in ('true', 'false'):
- raise InvalidParameterValueError(map_public_ip)
- subnet.map_public_ip_on_launch = map_public_ip
+ if attr_name in ('map_public_ip_on_launch', 'assign_ipv6_address_on_creation'):
+ setattr(subnet, attr_name, attr_value)
+ else:
+ raise InvalidParameterValueError(attr_name)
class SubnetRouteTableAssociation(object):
diff --git a/moto/ec2/responses/subnets.py b/moto/ec2/responses/subnets.py
index ba4f78a5e..0412d9e8b 100644
--- a/moto/ec2/responses/subnets.py
+++ b/moto/ec2/responses/subnets.py
@@ -1,6 +1,7 @@
from __future__ import unicode_literals
import random
from moto.core.responses import BaseResponse
+from moto.core.utils import camelcase_to_underscores
from moto.ec2.utils import filters_from_querystring
@@ -16,6 +17,7 @@ class Subnets(BaseResponse):
vpc_id,
cidr_block,
availability_zone,
+ context=self,
)
template = self.response_template(CREATE_SUBNET_RESPONSE)
return template.render(subnet=subnet)
@@ -35,9 +37,14 @@ class Subnets(BaseResponse):
def modify_subnet_attribute(self):
subnet_id = self._get_param('SubnetId')
- map_public_ip = self._get_param('MapPublicIpOnLaunch.Value')
- self.ec2_backend.modify_subnet_attribute(subnet_id, map_public_ip)
- return MODIFY_SUBNET_ATTRIBUTE_RESPONSE
+
+ for attribute in ('MapPublicIpOnLaunch', 'AssignIpv6AddressOnCreation'):
+ if self.querystring.get('%s.Value' % attribute):
+ attr_name = camelcase_to_underscores(attribute)
+ attr_value = self.querystring.get('%s.Value' % attribute)[0]
+ self.ec2_backend.modify_subnet_attribute(
+ subnet_id, attr_name, attr_value)
+ return MODIFY_SUBNET_ATTRIBUTE_RESPONSE
CREATE_SUBNET_RESPONSE = """
@@ -49,17 +56,14 @@ CREATE_SUBNET_RESPONSE = """
{{ subnet.vpc_id }}
{{ subnet.cidr_block }}
251
- {{ subnet.availability_zone }}
-
- {% for tag in subnet.get_tags() %}
- -
- {{ tag.resource_id }}
- {{ tag.resource_type }}
- {{ tag.key }}
- {{ tag.value }}
-
- {% endfor %}
-
+ {{ subnet._availability_zone.name }}
+ {{ subnet._availability_zone.zone_id }}
+ {{ subnet.default_for_az }}
+ {{ subnet.map_public_ip_on_launch }}
+ {{ subnet.owner_id }}
+ {{ subnet.assign_ipv6_address_on_creation }}
+ {{ subnet.ipv6_cidr_block_associations }}
+ arn:aws:ec2:{{ subnet._availability_zone.name[0:-1] }}:{{ subnet.owner_id }}:subnet/{{ subnet.id }}
"""
@@ -80,19 +84,26 @@ DESCRIBE_SUBNETS_RESPONSE = """
{{ subnet.vpc_id }}
{{ subnet.cidr_block }}
251
- {{ subnet.availability_zone }}
+ {{ subnet._availability_zone.name }}
+ {{ subnet._availability_zone.zone_id }}
{{ subnet.default_for_az }}
{{ subnet.map_public_ip_on_launch }}
-
- {% for tag in subnet.get_tags() %}
- -
- {{ tag.resource_id }}
- {{ tag.resource_type }}
- {{ tag.key }}
- {{ tag.value }}
-
- {% endfor %}
-
+ {{ subnet.owner_id }}
+ {{ subnet.assign_ipv6_address_on_creation }}
+ {{ subnet.ipv6_cidr_block_associations }}
+ arn:aws:ec2:{{ subnet._availability_zone.name[0:-1] }}:{{ subnet.owner_id }}:subnet/{{ subnet.id }}
+ {% if subnet.get_tags() %}
+
+ {% for tag in subnet.get_tags() %}
+ -
+ {{ tag.resource_id }}
+ {{ tag.resource_type }}
+ {{ tag.key }}
+ {{ tag.value }}
+
+ {% endfor %}
+
+ {% endif %}
{% endfor %}
diff --git a/tests/test_ec2/test_network_acls.py b/tests/test_ec2/test_network_acls.py
index d4c330f00..1c69624bf 100644
--- a/tests/test_ec2/test_network_acls.py
+++ b/tests/test_ec2/test_network_acls.py
@@ -30,12 +30,12 @@ def test_new_subnet_associates_with_default_network_acl():
conn = boto.connect_vpc('the_key', 'the secret')
vpc = conn.get_all_vpcs()[0]
- subnet = conn.create_subnet(vpc.id, "172.31.48.0/20")
+ subnet = conn.create_subnet(vpc.id, "172.31.112.0/20")
all_network_acls = conn.get_all_network_acls()
all_network_acls.should.have.length_of(1)
acl = all_network_acls[0]
- acl.associations.should.have.length_of(4)
+ acl.associations.should.have.length_of(7)
[a.subnet_id for a in acl.associations].should.contain(subnet.id)
diff --git a/tests/test_ec2/test_subnets.py b/tests/test_ec2/test_subnets.py
index 38571b285..38c36f682 100644
--- a/tests/test_ec2/test_subnets.py
+++ b/tests/test_ec2/test_subnets.py
@@ -118,7 +118,7 @@ def test_boto3_non_default_subnet():
@mock_ec2
-def test_modify_subnet_attribute():
+def test_modify_subnet_attribute_public_ip_on_launch():
ec2 = boto3.resource('ec2', region_name='us-west-1')
client = boto3.client('ec2', region_name='us-west-1')
@@ -145,6 +145,34 @@ def test_modify_subnet_attribute():
subnet.map_public_ip_on_launch.should.be.ok
+@mock_ec2
+def test_modify_subnet_attribute_assign_ipv6_address_on_creation():
+ ec2 = boto3.resource('ec2', region_name='us-west-1')
+ client = boto3.client('ec2', region_name='us-west-1')
+
+ # Get the default VPC
+ vpc = list(ec2.vpcs.all())[0]
+
+ subnet = ec2.create_subnet(
+ VpcId=vpc.id, CidrBlock='172.31.112.0/20', AvailabilityZone='us-west-1a')
+
+ # 'map_public_ip_on_launch' is set when calling 'DescribeSubnets' action
+ subnet.reload()
+
+ # For non default subnet, attribute value should be 'False'
+ subnet.assign_ipv6_address_on_creation.shouldnt.be.ok
+
+ client.modify_subnet_attribute(
+ SubnetId=subnet.id, AssignIpv6AddressOnCreation={'Value': False})
+ subnet.reload()
+ subnet.assign_ipv6_address_on_creation.shouldnt.be.ok
+
+ client.modify_subnet_attribute(
+ SubnetId=subnet.id, AssignIpv6AddressOnCreation={'Value': True})
+ subnet.reload()
+ subnet.assign_ipv6_address_on_creation.should.be.ok
+
+
@mock_ec2
def test_modify_subnet_attribute_validation():
ec2 = boto3.resource('ec2', region_name='us-west-1')
@@ -291,6 +319,84 @@ def test_subnet_tags_through_cloudformation():
subnet.tags["blah"].should.equal("baz")
+@mock_ec2
+def test_create_subnet_response_fields():
+ ec2 = boto3.resource('ec2', region_name='us-west-1')
+ client = boto3.client('ec2', region_name='us-west-1')
+
+ vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16')
+ subnet = client.create_subnet(
+ VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-1a')['Subnet']
+
+ subnet.should.have.key('AvailabilityZone')
+ subnet.should.have.key('AvailabilityZoneId')
+ subnet.should.have.key('AvailableIpAddressCount')
+ subnet.should.have.key('CidrBlock')
+ subnet.should.have.key('State')
+ subnet.should.have.key('SubnetId')
+ subnet.should.have.key('VpcId')
+ subnet.shouldnt.have.key('Tags')
+ subnet.should.have.key('DefaultForAz').which.should.equal(False)
+ subnet.should.have.key('MapPublicIpOnLaunch').which.should.equal(False)
+ subnet.should.have.key('OwnerId')
+ subnet.should.have.key('AssignIpv6AddressOnCreation').which.should.equal(False)
+
+ subnet_arn = "arn:aws:ec2:{region}:{owner_id}:subnet/{subnet_id}".format(region=subnet['AvailabilityZone'][0:-1],
+ owner_id=subnet['OwnerId'],
+ subnet_id=subnet['SubnetId'])
+ subnet.should.have.key('SubnetArn').which.should.equal(subnet_arn)
+ subnet.should.have.key('Ipv6CidrBlockAssociationSet').which.should.equal([])
+
+
+@mock_ec2
+def test_describe_subnet_response_fields():
+ ec2 = boto3.resource('ec2', region_name='us-west-1')
+ client = boto3.client('ec2', region_name='us-west-1')
+
+ vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16')
+ subnet_object = ec2.create_subnet(
+ VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-1a')
+
+ subnets = client.describe_subnets(SubnetIds=[subnet_object.id])['Subnets']
+ subnets.should.have.length_of(1)
+ subnet = subnets[0]
+
+ subnet.should.have.key('AvailabilityZone')
+ subnet.should.have.key('AvailabilityZoneId')
+ subnet.should.have.key('AvailableIpAddressCount')
+ subnet.should.have.key('CidrBlock')
+ subnet.should.have.key('State')
+ subnet.should.have.key('SubnetId')
+ subnet.should.have.key('VpcId')
+ subnet.shouldnt.have.key('Tags')
+ subnet.should.have.key('DefaultForAz').which.should.equal(False)
+ subnet.should.have.key('MapPublicIpOnLaunch').which.should.equal(False)
+ subnet.should.have.key('OwnerId')
+ subnet.should.have.key('AssignIpv6AddressOnCreation').which.should.equal(False)
+
+ subnet_arn = "arn:aws:ec2:{region}:{owner_id}:subnet/{subnet_id}".format(region=subnet['AvailabilityZone'][0:-1],
+ owner_id=subnet['OwnerId'],
+ subnet_id=subnet['SubnetId'])
+ subnet.should.have.key('SubnetArn').which.should.equal(subnet_arn)
+ subnet.should.have.key('Ipv6CidrBlockAssociationSet').which.should.equal([])
+
+
+@mock_ec2
+def test_create_subnet_with_invalid_availability_zone():
+ ec2 = boto3.resource('ec2', region_name='us-west-1')
+ client = boto3.client('ec2', region_name='us-west-1')
+
+ vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16')
+
+ subnet_availability_zone = 'asfasfas'
+ with assert_raises(ClientError) as ex:
+ subnet = client.create_subnet(
+ VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone=subnet_availability_zone)
+ assert str(ex.exception).startswith(
+ "An error occurred (InvalidParameterValue) when calling the CreateSubnet "
+ "operation: Value ({}) for parameter availabilityZone is invalid. Subnets can currently only be created in the following availability zones: ".format(subnet_availability_zone))
+
+
@mock_ec2
def test_create_subnet_with_invalid_cidr_range():
ec2 = boto3.resource('ec2', region_name='us-west-1')