diff --git a/moto/__init__.py b/moto/__init__.py index 76cc62c55..634daa00e 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -11,3 +11,4 @@ from .s3bucket_path import mock_s3bucket_path from .ses import mock_ses from .sqs import mock_sqs from .sts import mock_sts +from .route53 import mock_route53 diff --git a/moto/backends.py b/moto/backends.py index 0bc766fe3..b11005227 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -8,6 +8,7 @@ from moto.s3bucket_path import s3bucket_path_backend from moto.ses import ses_backend from moto.sqs import sqs_backend from moto.sts import sts_backend +from moto.route53 import route53_backend BACKENDS = { 'autoscaling': autoscaling_backend, @@ -20,4 +21,5 @@ BACKENDS = { 'ses': ses_backend, 'sqs': sqs_backend, 'sts': sts_backend, + 'route53': route53_backend } diff --git a/moto/route53/__init__.py b/moto/route53/__init__.py new file mode 100644 index 000000000..6448c3c39 --- /dev/null +++ b/moto/route53/__init__.py @@ -0,0 +1,2 @@ +from .models import route53_backend +mock_route53 = route53_backend.decorator diff --git a/moto/route53/models.py b/moto/route53/models.py new file mode 100644 index 000000000..0ae3947ea --- /dev/null +++ b/moto/route53/models.py @@ -0,0 +1,57 @@ +from moto.core import BaseBackend +from moto.core.utils import get_random_hex + + +class FakeZone: + + def __init__(self, name, id): + self.name = name + self.id = id + self.rrsets = {} + + def add_rrset(self, name, rrset): + self.rrsets[name] = rrset + + def delete_rrset(self, name): + del self.rrsets[name] + + +class FakeResourceRecord: + def __init__(self, value): + pass + +class FakeResourceRecordSet: + def __init__(self, name, type, ttl, rrlist): + self.name = name + self.type = type + self.ttl = ttl + self.rrList = rrlist + +class Route53Backend(BaseBackend): + + def __init__(self): + self.zones = {} + + def create_hosted_zone(self, name): + new_id = get_random_hex() + new_zone = FakeZone(name, new_id) + self.zones[new_id] = new_zone + return new_zone + + def get_all_hosted_zones(self): + return self.zones.values() + + def get_hosted_zone(self, id): + return self.zones.get(id) + + 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 new file mode 100644 index 000000000..92a4d921c --- /dev/null +++ b/moto/route53/responses.py @@ -0,0 +1,130 @@ +from jinja2 import Template +from urlparse import parse_qs, urlparse +from .models import route53_backend +import xmltodict +import dicttoxml + + +def list_or_create_hostzone_response(request, full_url, headers): + + if request.method == "POST": + r = xmltodict.parse(request.body) + new_zone = route53_backend.create_hosted_zone(r["CreateHostedZoneRequest"]["Name"]) + template = Template(CREATE_HOSTED_ZONE_RESPONSE) + return 201, headers, template.render(zone=new_zone) + + elif request.method == "GET": + all_zones = route53_backend.get_all_hosted_zones() + template = Template(LIST_HOSTED_ZONES_RESPONSE) + return 200, headers, template.render(zones=all_zones) + + +def get_or_delete_hostzone_response(request, full_url, headers): + parsed_url = urlparse(full_url) + zoneid = parsed_url.path.rstrip('/').rsplit('/', 1)[1] + the_zone = route53_backend.get_hosted_zone(zoneid) + if not the_zone: + return 404, headers, "Zone %s not Found" % zoneid + + if request.method == "GET": + template = Template(GET_HOSTED_ZONE_RESPONSE) + return 200, headers, template.render(zone=the_zone) + elif request.method == "DELETE": + route53_backend.delete_hosted_zone(zoneid) + return 200, headers, DELETE_HOSTED_ZONE_RESPONSE + +def rrset_response(request, full_url, headers): + parsed_url = urlparse(full_url) + method = request.method + + zoneid = parsed_url.path.rstrip('/').rsplit('/', 2)[1] + the_zone = route53_backend.get_hosted_zone(zoneid) + if not the_zone: + return 404, headers, "Zone %s Not Found" % zoneid + + if method == "POST": + r = xmltodict.parse(request.body) + for k, v in r['ChangeResourceRecordSetsRequest']['ChangeBatch']['Changes'].items(): + action = v['Action'] + rrset = v['ResourceRecordSet'] + + if action == 'CREATE': + the_zone.add_rrset(rrset["Name"], rrset) + elif action == "DELETE": + the_zone.delete_rrset(rrset["Name"]) + + return 200, headers, CHANGE_RRSET_RESPONSE + + elif method == "GET": + querystring = parse_qs(parsed_url.query) + template = Template(LIST_RRSET_REPONSE) + rrset_list = [] + for key, value in the_zone.rrsets.items(): + if 'type' not in querystring or querystring["type"][0] == value["Type"]: + rrset_list.append(dicttoxml.dicttoxml({"ResourceRecordSet": value}, root=False)) + + return 200, headers, template.render(rrsets=rrset_list) + + + +def not_implemented_response(request, full_url, headers): + parsed_url = urlparse(full_url) + raise NotImplementedError('handling of %s is not yet implemented' % parsed_url.path) + + +LIST_RRSET_REPONSE = """ + + {% for rrset in rrsets %} + {{ rrset }} + {% endfor %} + +""" + +CHANGE_RRSET_RESPONSE = """ + + PENDING + 2010-09-10T01:36:41.958Z + +""" + +DELETE_HOSTED_ZONE_RESPONSE = """ + + +""" + +GET_HOSTED_ZONE_RESPONSE = """ + + /hostedzone/{{ zone.id }} + {{ zone.name }} + {{ zone.rrsets|count }} + + + moto.test.com + +""" + +CREATE_HOSTED_ZONE_RESPONSE = """ + + /hostedzone/{{ zone.id }} + {{ zone.name }} + 0 + + + + moto.test.com + + +""" + +LIST_HOSTED_ZONES_RESPONSE = """ + + {% for zone in zones %} + + {{ zone.id }} + {{ zone.name }} + {{ zone.rrsets|count }} + + {% endfor %} + +""" + diff --git a/moto/route53/urls.py b/moto/route53/urls.py new file mode 100644 index 000000000..6f902e5af --- /dev/null +++ b/moto/route53/urls.py @@ -0,0 +1,12 @@ +import responses + +url_bases = [ + #"https://route53.amazonaws.com/201\d-\d\d-\d\d/hostedzone", + "https://route53.amazonaws.com/201.-..-../hostedzone", +] + +url_paths = { + '{0}$': responses.list_or_create_hostzone_response, + '{0}/.+$': responses.get_or_delete_hostzone_response, + '{0}/.+/rrset$': responses.rrset_response, +} diff --git a/requirements.txt b/requirements.txt index 62f6f0a27..81d8f8e04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,6 @@ mock nose https://github.com/spulec/python-coveralls/tarball/796d9dba34b759664e42ba39e6414209a0f319ad requests -sure \ No newline at end of file +sure +xmltodict +dicttoxml diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py new file mode 100644 index 000000000..465f3b97b --- /dev/null +++ b/tests/test_route53/test_route53.py @@ -0,0 +1,69 @@ +import urllib2 + +import boto +from boto.exception import S3ResponseError +from boto.s3.key import Key +from boto.route53.record import ResourceRecordSets +from freezegun import freeze_time +import requests + +import sure # noqa + +from moto import mock_route53 + + +@mock_route53 +def test_hosted_zone(): + conn = boto.connect_route53('the_key', 'the_secret') + firstzone = conn.create_hosted_zone("testdns.aws.com") + zones = conn.get_all_hosted_zones() + len(zones["ListHostedZonesResponse"]["HostedZones"]).should.equal(1) + + secondzone = conn.create_hosted_zone("testdns1.aws.com") + zones = conn.get_all_hosted_zones() + len(zones["ListHostedZonesResponse"]["HostedZones"]).should.equal(2) + + id1 = firstzone["CreateHostedZoneResponse"]["HostedZone"]["Id"] + zone = conn.get_hosted_zone(id1) + zone["GetHostedZoneResponse"]["HostedZone"]["Name"].should.equal("testdns.aws.com") + + conn.delete_hosted_zone(id1) + zones = conn.get_all_hosted_zones() + len(zones["ListHostedZonesResponse"]["HostedZones"]).should.equal(1) + + conn.get_hosted_zone.when.called_with("abcd").should.throw(boto.route53.exception.DNSServerError, "404 Not Found") + + +@mock_route53 +def test_rrset(): + conn = boto.connect_route53('the_key', 'the_secret') + zone = conn.create_hosted_zone("testdns.aws.com") + zoneid = zone["CreateHostedZoneResponse"]["HostedZone"]["Id"] + + changes = ResourceRecordSets(conn, zoneid) + change = changes.add_change("CREATE", "foo.bar.testdns.aws.com", "A") + change.add_value("1.2.3.4") + changes.commit() + + rrsets = conn.get_all_rrsets(zoneid, type="A") + rrsets.should.have.length_of(1) + rrsets[0].resource_records[0].should.equal('1.2.3.4') + + rrsets = conn.get_all_rrsets(zoneid, type="CNAME") + rrsets.should.have.length_of(0) + + + changes = ResourceRecordSets(conn, zoneid) + changes.add_change("DELETE", "foo.bar.testdns.aws.com", "A") + changes.commit() + + rrsets = conn.get_all_rrsets(zoneid) + rrsets.should.have.length_of(0) + + + + + + + + \ No newline at end of file