diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 75aade56f..100bb57f1 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -8,6 +8,7 @@ from moto.ec2 import models as ec2_models from moto.elb import models as elb_models from moto.iam import models as iam_models from moto.rds import models as rds_models +from moto.route53 import models as route53_models from moto.sqs import models as sqs_models from .utils import random_suffix from .exceptions import MissingParameterError, UnformattedGetAttTemplateException @@ -36,6 +37,8 @@ MODEL_MAP = { "AWS::RDS::DBInstance": rds_models.Database, "AWS::RDS::DBSecurityGroup": rds_models.SecurityGroup, "AWS::RDS::DBSubnetGroup": rds_models.SubnetGroup, + "AWS::Route53::HostedZone": route53_models.FakeZone, + "AWS::Route53::RecordSetGroup": route53_models.RecordSetGroup, "AWS::SQS::Queue": sqs_models.Queue, } diff --git a/moto/route53/models.py b/moto/route53/models.py index bfc1dde51..f87b32820 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -8,13 +8,43 @@ class FakeZone(object): def __init__(self, name, id_): self.name = name self.id = id_ - self.rrsets = {} + self.rrsets = [] def add_rrset(self, name, rrset): - self.rrsets[name] = rrset + self.rrsets.append(rrset) def delete_rrset(self, name): - self.rrsets.pop(name, None) + self.rrsets = [record_set for record_set in self.rrsets if record_set['Name'] != name] + + @property + def physical_resource_id(self): + return self.name + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + name = properties["Name"] + + hosted_zone = route53_backend.create_hosted_zone(name) + return hosted_zone + + +class RecordSetGroup(object): + def __init__(self, record_sets): + self.record_sets = record_sets + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + + zone_name = properties["HostedZoneName"] + hosted_zone = route53_backend.get_hosted_zone_by_name(zone_name) + record_sets = properties["RecordSets"] + for record_set in record_sets: + hosted_zone.add_rrset(record_set["Name"], record_set) + + record_set_group = RecordSetGroup(record_sets) + return record_set_group class Route53Backend(BaseBackend): @@ -34,12 +64,16 @@ class Route53Backend(BaseBackend): def get_hosted_zone(self, id_): return self.zones.get(id_) + def get_hosted_zone_by_name(self, name): + for zone in self.get_all_hosted_zones(): + if zone.name == name: + return zone + def delete_hosted_zone(self, id_): zone = self.zones.get(id_) if zone: del self.zones[id_] return zone - return None route53_backend = Route53Backend() diff --git a/moto/route53/responses.py b/moto/route53/responses.py index 82a870df7..71a2acd7d 100644 --- a/moto/route53/responses.py +++ b/moto/route53/responses.py @@ -66,12 +66,12 @@ def rrset_response(request, full_url, headers): querystring = parse_qs(parsed_url.query) template = Template(LIST_RRSET_REPONSE) rrset_list = [] - for key, value in the_zone.rrsets.items(): - if 'type' in querystring and querystring["type"][0] != value["Type"]: + for record_set in the_zone.rrsets: + if 'type' in querystring and querystring["type"][0] != record_set["Type"]: continue - if 'name' in querystring and querystring["name"][0] != value["Name"]: + if 'name' in querystring and querystring["name"][0] != record_set["Name"]: continue - rrset_list.append(dicttoxml.dicttoxml({"ResourceRecordSet": value}, root=False)) + rrset_list.append(dicttoxml.dicttoxml({"ResourceRecordSet": record_set}, root=False)) return 200, headers, template.render(rrsets=rrset_list) @@ -79,7 +79,7 @@ def rrset_response(request, full_url, headers): LIST_RRSET_REPONSE = """ {% for rrset in rrsets %} - {{ rrset }} + {{ rrset }} {% endfor %} """ diff --git a/tests/test_cloudformation/fixtures/route53_roundrobin.py b/tests/test_cloudformation/fixtures/route53_roundrobin.py new file mode 100644 index 000000000..d985623bb --- /dev/null +++ b/tests/test_cloudformation/fixtures/route53_roundrobin.py @@ -0,0 +1,47 @@ +from __future__ import unicode_literals + +template = { + "AWSTemplateFormatVersion" : "2010-09-09", + + "Description" : "AWS CloudFormation Sample Template Route53_RoundRobin: Sample template showing how to use weighted round robin (WRR) DNS entried via Amazon Route 53. This contrived sample uses weighted CNAME records to illustrate that the weighting influences the return records. It assumes that you already have a Hosted Zone registered with Amazon Route 53. **WARNING** This template creates one or more AWS resources. You will be billed for the AWS resources used if you create a stack from this template.", + + "Resources" : { + + "MyZone": { + "Type" : "AWS::Route53::HostedZone", + "Properties" : { + "Name" : "my_zone" + } + }, + + "MyDNSRecord" : { + "Type" : "AWS::Route53::RecordSetGroup", + "Properties" : { + "HostedZoneName" : {"Ref": "MyZone"}, + "Comment" : "Contrived example to redirect to aws.amazon.com 75% of the time and www.amazon.com 25% of the time.", + "RecordSets" : [{ + "SetIdentifier" : { "Fn::Join" : [ " ", [{"Ref" : "AWS::StackName"}, "AWS" ]]}, + "Name" : { "Fn::Join" : [ "", [{"Ref" : "AWS::StackName"}, ".", {"Ref" : "AWS::Region"}, ".", {"Ref" : "MyZone"}, "."]]}, + "Type" : "CNAME", + "TTL" : "900", + "ResourceRecords" : ["aws.amazon.com"], + "Weight" : "3" + },{ + "SetIdentifier" : { "Fn::Join" : [ " ", [{"Ref" : "AWS::StackName"}, "Amazon" ]]}, + "Name" : { "Fn::Join" : [ "", [{"Ref" : "AWS::StackName"}, ".", {"Ref" : "AWS::Region"}, ".", {"Ref" : "MyZone"}, "."]]}, + "Type" : "CNAME", + "TTL" : "900", + "ResourceRecords" : ["www.amazon.com"], + "Weight" : "1" + }] + } + } + }, + + "Outputs" : { + "DomainName" : { + "Description" : "Fully qualified domain name", + "Value" : { "Ref" : "MyDNSRecord" } + } + } +} \ No newline at end of file diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 60353f205..a138a4963 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -19,6 +19,7 @@ from moto import ( mock_elb, mock_iam, mock_rds, + mock_route53, mock_sqs, ) @@ -26,6 +27,7 @@ from .fixtures import ( ec2_classic_eip, fn_join, rds_mysql_with_read_replica, + route53_roundrobin, single_instance_with_ebs_volume, vpc_eip, vpc_single_instance_in_subnet, @@ -769,3 +771,39 @@ def test_cloudformation_mapping(): reservation = ec2_conn.get_all_instances()[0] ec2_instance = reservation.instances[0] ec2_instance.image_id.should.equal("ami-c9c7978c") + + +@mock_cloudformation() +@mock_route53() +def test_route53_roundrobin(): + route53_conn = boto.connect_route53() + + template_json = json.dumps(route53_roundrobin.template) + conn = boto.cloudformation.connect_to_region("us-west-1") + conn.create_stack( + "test_stack", + template_body=template_json, + ) + + zones = route53_conn.get_all_hosted_zones()['ListHostedZonesResponse']['HostedZones'] + list(zones).should.have.length_of(1) + zone_id = zones[0]['Id'] + + rrsets = route53_conn.get_all_rrsets(zone_id) + rrsets.hosted_zone_id.should.equal(zone_id) + rrsets.should.have.length_of(2) + record_set1 = rrsets[0] + record_set1.name.should.equal('test_stack.us-west-1.my_zone.') + record_set1.identifier.should.equal("test_stack AWS") + record_set1.type.should.equal('CNAME') + record_set1.ttl.should.equal('900') + record_set1.weight.should.equal('3') + # FIXME record_set1.resource_records[0].should.equal("aws.amazon.com") + + record_set2 = rrsets[1] + record_set2.name.should.equal('test_stack.us-west-1.my_zone.') + record_set2.identifier.should.equal("test_stack Amazon") + record_set2.type.should.equal('CNAME') + record_set2.ttl.should.equal('900') + record_set2.weight.should.equal('1') + # FIXME record_set2.resource_records[0].should.equal("www.amazon.com")