diff --git a/moto/route53/exceptions.py b/moto/route53/exceptions.py index cb2456b36..1a765cb2e 100644 --- a/moto/route53/exceptions.py +++ b/moto/route53/exceptions.py @@ -33,6 +33,17 @@ class InvalidPaginationToken(Route53ClientError): super().__init__("InvalidPaginationToken", message) +class InvalidVPCId(Route53ClientError): + """Missing/Invalid VPC ID""" + + code = 400 + + def __init__(self): + message = "Invalid or missing VPC Id." + super().__init__("InvalidVPCId", message) + self.content_type = "text/xml" + + class NoSuchCloudWatchLogsLogGroup(Route53ClientError): """CloudWatch LogGroup has a permissions policy, but does not exist.""" diff --git a/moto/route53/models.py b/moto/route53/models.py index 52c6620ad..a0cd35e72 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -212,11 +212,17 @@ def reverse_domain_name(domain_name): class FakeZone(CloudFormationModel): - def __init__(self, name, id_, private_zone, comment=None): + def __init__( + self, name, id_, private_zone, vpcid=None, vpcregion=None, comment=None + ): self.name = name self.id = id_ if comment is not None: self.comment = comment + if vpcid is not None: + self.vpcid = vpcid + if vpcregion is not None: + self.vpcregion = vpcregion self.private_zone = private_zone self.rrsets = [] @@ -365,9 +371,18 @@ class Route53Backend(BaseBackend): self.resource_tags = defaultdict(dict) self.query_logging_configs = {} - def create_hosted_zone(self, name, private_zone, comment=None): + def create_hosted_zone( + self, name, private_zone, vpcid=None, vpcregion=None, comment=None + ): new_id = create_route53_zone_id() - new_zone = FakeZone(name, new_id, private_zone=private_zone, comment=comment) + new_zone = FakeZone( + name, + new_id, + private_zone=private_zone, + vpcid=vpcid, + vpcregion=vpcregion, + comment=comment, + ) self.zones[new_id] = new_zone return new_zone diff --git a/moto/route53/responses.py b/moto/route53/responses.py index daf673c25..7fc94a943 100644 --- a/moto/route53/responses.py +++ b/moto/route53/responses.py @@ -6,7 +6,7 @@ from jinja2 import Template import xmltodict from moto.core.responses import BaseResponse -from moto.route53.exceptions import Route53ClientError, InvalidChangeBatch +from moto.route53.exceptions import Route53ClientError, InvalidChangeBatch, InvalidVPCId from moto.route53.models import route53_backend XMLNS = "https://route53.amazonaws.com/doc/2013-04-01/" @@ -30,32 +30,39 @@ class Route53(BaseResponse): def list_or_create_hostzone_response(self, request, full_url, headers): self.setup_class(request, full_url, headers) + # Set these here outside the scope of the try/except + # so they're defined later when we call create_hosted_zone() + vpcid = None + vpcregion = None if request.method == "POST": elements = xmltodict.parse(self.body) - if "HostedZoneConfig" in elements["CreateHostedZoneRequest"]: - comment = elements["CreateHostedZoneRequest"]["HostedZoneConfig"][ - "Comment" - ] - try: - # in boto3, this field is set directly in the xml - private_zone = elements["CreateHostedZoneRequest"][ - "HostedZoneConfig" - ]["PrivateZone"] - except KeyError: - # if a VPC subsection is only included in xmls params when private_zone=True, - # see boto: boto/route53/connection.py - private_zone = "VPC" in elements["CreateHostedZoneRequest"] + zone_request = elements["CreateHostedZoneRequest"] + if "HostedZoneConfig" in zone_request: + zone_config = zone_request["HostedZoneConfig"] + comment = zone_config["Comment"] + private_zone = zone_config.get("PrivateZone", False) else: comment = None private_zone = False - name = elements["CreateHostedZoneRequest"]["Name"] + if private_zone == "true": + try: + vpcid = zone_request["VPC"]["VPCId"] + vpcregion = zone_request["VPC"]["VPCRegion"] + except KeyError: + raise InvalidVPCId() + + name = zone_request["Name"] if name[-1] != ".": name += "." new_zone = route53_backend.create_hosted_zone( - name, comment=comment, private_zone=private_zone + name, + comment=comment, + private_zone=private_zone, + vpcid=vpcid, + vpcregion=vpcregion, ) template = Template(CREATE_HOSTED_ZONE_RESPONSE) return 201, headers, template.render(zone=new_zone) @@ -414,6 +421,13 @@ GET_HOSTED_ZONE_RESPONSE = """ @@ -433,6 +447,10 @@ CREATE_HOSTED_ZONE_RESPONSE = """ diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py index 3516b64fd..3fdd4a889 100644 --- a/tests/test_route53/test_route53.py +++ b/tests/test_route53/test_route53.py @@ -6,7 +6,7 @@ import sure # noqa # pylint: disable=unused-import import botocore import pytest -from moto import mock_route53 +from moto import mock_ec2, mock_route53 @mock_route53 @@ -367,21 +367,34 @@ def test_deleting_latency_route_boto3(): cnames[0]["Region"].should.equal("us-west-1") +@mock_ec2 @mock_route53 def test_hosted_zone_private_zone_preserved_boto3(): - conn = boto3.client("route53", region_name="us-east-1") - # TODO: actually create_hosted_zone statements with PrivateZone=True, but without - # a _valid_ vpc-id should fail. - firstzone = conn.create_hosted_zone( + # Create mock VPC so we can get a VPC ID + region = "us-east-1" + ec2c = boto3.client("ec2", region_name=region) + vpc_id = ec2c.create_vpc(CidrBlock="10.1.0.0/16").get("Vpc").get("VpcId") + + # Create hosted_zone as a Private VPC Hosted Zone + conn = boto3.client("route53", region_name=region) + new_zone = conn.create_hosted_zone( Name="testdns.aws.com.", CallerReference=str(hash("foo")), HostedZoneConfig=dict(PrivateZone=True, Comment="Test"), + VPC={"VPCRegion": region, "VPCId": vpc_id}, ) - zone_id = firstzone["HostedZone"]["Id"].split("/")[-1] - + zone_id = new_zone["HostedZone"]["Id"].split("/")[-1] hosted_zone = conn.get_hosted_zone(Id=zone_id) hosted_zone["HostedZone"]["Config"]["PrivateZone"].should.equal(True) + hosted_zone.should.have.key("VPCs") + hosted_zone["VPCs"].should.have.length_of(1) + hosted_zone["VPCs"][0].should.have.key("VPCId") + hosted_zone["VPCs"][0].should.have.key("VPCRegion") + hosted_zone["VPCs"][0]["VPCId"].should_not.be.empty + hosted_zone["VPCs"][0]["VPCRegion"].should_not.be.empty + hosted_zone["VPCs"][0]["VPCId"].should.be.equal(vpc_id) + hosted_zone["VPCs"][0]["VPCRegion"].should.be.equal(region) hosted_zones = conn.list_hosted_zones() hosted_zones["HostedZones"][0]["Config"]["PrivateZone"].should.equal(True) @@ -390,6 +403,21 @@ def test_hosted_zone_private_zone_preserved_boto3(): len(hosted_zones["HostedZones"]).should.equal(1) hosted_zones["HostedZones"][0]["Config"]["PrivateZone"].should.equal(True) + # create_hosted_zone statements with PrivateZone=True, + # but without a _valid_ vpc-id should fail. + conn = boto3.client("route53", region_name=region) + with pytest.raises(ClientError) as exc: + conn.create_hosted_zone( + Name="testdns.aws.com.", + CallerReference=str(hash("foo")), + HostedZoneConfig=dict(PrivateZone=True, Comment="Test"), + ) + err = exc.value.response["Error"] + err["Code"].should.equal("InvalidVPCId") + err["Message"].should.equal("Invalid or missing VPC Id.") + + return + @mock_route53 def test_list_or_change_tags_for_resource_request(): @@ -479,33 +507,77 @@ def test_list_or_change_tags_for_resource_request(): response["ResourceTagSet"]["Tags"].should.be.empty +@mock_ec2 @mock_route53 def test_list_hosted_zones_by_name(): - conn = boto3.client("route53", region_name="us-east-1") - conn.create_hosted_zone( + + # Create mock VPC so we can get a VPC ID + ec2c = boto3.client("ec2", region_name="us-east-1") + vpc_id = ec2c.create_vpc(CidrBlock="10.1.0.0/16").get("Vpc").get("VpcId") + region = "us-east-1" + + conn = boto3.client("route53", region_name=region) + zone_b = conn.create_hosted_zone( Name="test.b.com.", CallerReference=str(hash("foo")), HostedZoneConfig=dict(PrivateZone=True, Comment="test com"), - ) - conn.create_hosted_zone( - Name="test.a.org.", - CallerReference=str(hash("bar")), - HostedZoneConfig=dict(PrivateZone=True, Comment="test org"), - ) - conn.create_hosted_zone( - Name="test.a.org.", - CallerReference=str(hash("bar")), - HostedZoneConfig=dict(PrivateZone=True, Comment="test org 2"), + VPC={"VPCRegion": region, "VPCId": vpc_id}, ) - # test lookup - zones = conn.list_hosted_zones_by_name(DNSName="test.b.com.") - len(zones["HostedZones"]).should.equal(1) - zones["HostedZones"][0]["Name"].should.equal("test.b.com.") + zone_b = conn.list_hosted_zones_by_name(DNSName="test.b.com.") + len(zone_b["HostedZones"]).should.equal(1) + zone_b["HostedZones"][0]["Name"].should.equal("test.b.com.") + zone_b["HostedZones"][0].should.have.key("Config") + zone_b["HostedZones"][0]["Config"].should.have.key("PrivateZone") + zone_b["HostedZones"][0]["Config"]["PrivateZone"].should.be.equal(True) + + # We declared this a a private hosted zone above, so let's make + # sure it really is! + zone_b_id = zone_b["HostedZones"][0]["Id"].split("/")[-1] + b_hosted_zone = conn.get_hosted_zone(Id=zone_b_id) + + # Pull the HostedZone block out and test it. + b_hosted_zone.should.have.key("HostedZone") + b_hz = b_hosted_zone["HostedZone"] + b_hz.should.have.key("Config") + b_hz["Config"].should.have.key("PrivateZone") + b_hz["Config"]["PrivateZone"].should.be.equal(True) + + # Check for the VPCs block since this *should* be a VPC-Private Zone + b_hosted_zone.should.have.key("VPCs") + b_hosted_zone["VPCs"].should.have.length_of(1) + b_hz_vpcs = b_hosted_zone["VPCs"][0] + b_hz_vpcs.should.have.key("VPCId") + b_hz_vpcs.should.have.key("VPCRegion") + b_hz_vpcs["VPCId"].should_not.be.empty + b_hz_vpcs["VPCRegion"].should_not.be.empty + b_hz_vpcs["VPCId"].should.be.equal(vpc_id) + b_hz_vpcs["VPCRegion"].should.be.equal(region) + + # Now create other zones and test them. + conn.create_hosted_zone( + Name="test.a.org.", + CallerReference=str(hash("bar")), + HostedZoneConfig=dict(PrivateZone=False, Comment="test org"), + ) + conn.create_hosted_zone( + Name="test.a.org.", + CallerReference=str(hash("bar")), + HostedZoneConfig=dict(PrivateZone=False, Comment="test org 2"), + ) + + # Now makes sure the other zones we created above are NOT private... zones = conn.list_hosted_zones_by_name(DNSName="test.a.org.") len(zones["HostedZones"]).should.equal(2) zones["HostedZones"][0]["Name"].should.equal("test.a.org.") + zones["HostedZones"][0].should.have.key("Config") + zones["HostedZones"][0]["Config"].should.have.key("PrivateZone") + zones["HostedZones"][0]["Config"]["PrivateZone"].should.be.equal(False) + zones["HostedZones"][1]["Name"].should.equal("test.a.org.") + zones["HostedZones"][1].should.have.key("Config") + zones["HostedZones"][1]["Config"].should.have.key("PrivateZone") + zones["HostedZones"][1]["Config"]["PrivateZone"].should.be.equal(False) # test sort order zones = conn.list_hosted_zones_by_name() @@ -521,17 +593,17 @@ def test_list_hosted_zones_by_dns_name(): conn.create_hosted_zone( Name="test.b.com.", CallerReference=str(hash("foo")), - HostedZoneConfig=dict(PrivateZone=True, Comment="test com"), + HostedZoneConfig=dict(PrivateZone=False, Comment="test com"), ) conn.create_hosted_zone( Name="test.a.org.", CallerReference=str(hash("bar")), - HostedZoneConfig=dict(PrivateZone=True, Comment="test org"), + HostedZoneConfig=dict(PrivateZone=False, Comment="test org"), ) conn.create_hosted_zone( Name="test.a.org.", CallerReference=str(hash("bar")), - HostedZoneConfig=dict(PrivateZone=True, Comment="test org 2"), + HostedZoneConfig=dict(PrivateZone=False, Comment="test org 2"), ) conn.create_hosted_zone( Name="my.test.net.", @@ -569,7 +641,7 @@ def test_change_resource_record_sets_crud_valid(): conn.create_hosted_zone( Name="db.", CallerReference=str(hash("foo")), - HostedZoneConfig=dict(PrivateZone=True, Comment="db"), + HostedZoneConfig=dict(PrivateZone=False, Comment="db"), ) zones = conn.list_hosted_zones_by_name(DNSName="db.") @@ -707,7 +779,7 @@ def test_change_resource_record_sets_crud_valid_with_special_xml_chars(): conn.create_hosted_zone( Name="db.", CallerReference=str(hash("foo")), - HostedZoneConfig=dict(PrivateZone=True, Comment="db"), + HostedZoneConfig=dict(PrivateZone=False, Comment="db"), ) zones = conn.list_hosted_zones_by_name(DNSName="db.") @@ -977,7 +1049,7 @@ def test_change_resource_record_invalid(): conn.create_hosted_zone( Name="db.", CallerReference=str(hash("foo")), - HostedZoneConfig=dict(PrivateZone=True, Comment="db"), + HostedZoneConfig=dict(PrivateZone=False, Comment="db"), ) zones = conn.list_hosted_zones_by_name(DNSName="db.") @@ -1038,7 +1110,7 @@ def test_list_resource_record_sets_name_type_filters(): create_hosted_zone_response = conn.create_hosted_zone( Name="db.", CallerReference=str(hash("foo")), - HostedZoneConfig=dict(PrivateZone=True, Comment="db"), + HostedZoneConfig=dict(PrivateZone=False, Comment="db"), ) hosted_zone_id = create_hosted_zone_response["HostedZone"]["Id"] @@ -1107,7 +1179,7 @@ def test_change_resource_record_sets_records_limit(): conn.create_hosted_zone( Name="db.", CallerReference=str(hash("foo")), - HostedZoneConfig=dict(PrivateZone=True, Comment="db"), + HostedZoneConfig=dict(PrivateZone=False, Comment="db"), ) zones = conn.list_hosted_zones_by_name(DNSName="db.") @@ -1159,7 +1231,6 @@ def test_change_resource_record_sets_records_limit(): "Comment": "Create four records with 250 resource records each, plus one more", "Changes": too_many_changes, } - with pytest.raises(ClientError) as exc: conn.change_resource_record_sets( HostedZoneId=hosted_zone_id,