From a11c80fe201fe9c24f777e545604848773eac4f0 Mon Sep 17 00:00:00 2001 From: jjofseattle Date: Thu, 14 Nov 2013 11:14:14 -0800 Subject: [PATCH 1/5] add route53 --- moto/__init__.py | 1 + moto/backends.py | 2 + moto/route53/__init__.py | 2 + moto/route53/models.py | 57 +++++++++++++ moto/route53/responses.py | 130 +++++++++++++++++++++++++++++ moto/route53/urls.py | 12 +++ requirements.txt | 4 +- tests/test_route53/test_route53.py | 69 +++++++++++++++ 8 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 moto/route53/__init__.py create mode 100644 moto/route53/models.py create mode 100644 moto/route53/responses.py create mode 100644 moto/route53/urls.py create mode 100644 tests/test_route53/test_route53.py 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 From 8da1d31432f5f0e1a54742ecb0e538bc92b03dfe Mon Sep 17 00:00:00 2001 From: jjofseattle Date: Fri, 15 Nov 2013 15:29:01 -0800 Subject: [PATCH 2/5] fix style issues --- moto/route53/models.py | 4 ---- moto/route53/responses.py | 18 ++++++------------ moto/route53/urls.py | 1 - tests/test_route53/test_route53.py | 8 -------- 4 files changed, 6 insertions(+), 25 deletions(-) diff --git a/moto/route53/models.py b/moto/route53/models.py index 0ae3947ea..39ee4fc6d 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -16,10 +16,6 @@ class FakeZone: del self.rrsets[name] -class FakeResourceRecord: - def __init__(self, value): - pass - class FakeResourceRecordSet: def __init__(self, name, type, ttl, rrlist): self.name = name diff --git a/moto/route53/responses.py b/moto/route53/responses.py index 92a4d921c..c2527320e 100644 --- a/moto/route53/responses.py +++ b/moto/route53/responses.py @@ -8,8 +8,8 @@ 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"]) + elements = xmltodict.parse(request.body) + new_zone = route53_backend.create_hosted_zone(elements["CreateHostedZoneRequest"]["Name"]) template = Template(CREATE_HOSTED_ZONE_RESPONSE) return 201, headers, template.render(zone=new_zone) @@ -43,10 +43,10 @@ def rrset_response(request, full_url, headers): 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'] + elements = xmltodict.parse(request.body) + for key, value in elements['ChangeResourceRecordSetsRequest']['ChangeBatch']['Changes'].items(): + action = value['Action'] + rrset = value['ResourceRecordSet'] if action == 'CREATE': the_zone.add_rrset(rrset["Name"], rrset) @@ -64,12 +64,6 @@ def rrset_response(request, full_url, headers): 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 = """ diff --git a/moto/route53/urls.py b/moto/route53/urls.py index 6f902e5af..7b76e6b23 100644 --- a/moto/route53/urls.py +++ b/moto/route53/urls.py @@ -1,7 +1,6 @@ import responses url_bases = [ - #"https://route53.amazonaws.com/201\d-\d\d-\d\d/hostedzone", "https://route53.amazonaws.com/201.-..-../hostedzone", ] diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py index 465f3b97b..ceffa63e0 100644 --- a/tests/test_route53/test_route53.py +++ b/tests/test_route53/test_route53.py @@ -59,11 +59,3 @@ def test_rrset(): rrsets = conn.get_all_rrsets(zoneid) rrsets.should.have.length_of(0) - - - - - - - - \ No newline at end of file From 08777e4b182378d392bb5307afe85745e01705e4 Mon Sep 17 00:00:00 2001 From: jjofseattle Date: Fri, 15 Nov 2013 15:35:46 -0800 Subject: [PATCH 3/5] pep8 --- moto/route53/models.py | 4 ++-- moto/route53/responses.py | 4 ++-- tests/test_route53/test_route53.py | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/moto/route53/models.py b/moto/route53/models.py index 39ee4fc6d..4afcfe922 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -17,12 +17,14 @@ class FakeZone: 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): @@ -49,5 +51,3 @@ class Route53Backend(BaseBackend): route53_backend = Route53Backend() - - diff --git a/moto/route53/responses.py b/moto/route53/responses.py index c2527320e..55160922e 100644 --- a/moto/route53/responses.py +++ b/moto/route53/responses.py @@ -33,6 +33,7 @@ def get_or_delete_hostzone_response(request, full_url, headers): 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 @@ -120,5 +121,4 @@ LIST_HOSTED_ZONES_RESPONSE = """""" diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py index ceffa63e0..3e6132833 100644 --- a/tests/test_route53/test_route53.py +++ b/tests/test_route53/test_route53.py @@ -23,7 +23,7 @@ def test_hosted_zone(): zones = conn.get_all_hosted_zones() len(zones["ListHostedZonesResponse"]["HostedZones"]).should.equal(2) - id1 = firstzone["CreateHostedZoneResponse"]["HostedZone"]["Id"] + id1 = firstzone["CreateHostedZoneResponse"]["HostedZone"]["Id"] zone = conn.get_hosted_zone(id1) zone["GetHostedZoneResponse"]["HostedZone"]["Name"].should.equal("testdns.aws.com") @@ -52,7 +52,6 @@ def test_rrset(): 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() From 2d6e64924589a4a6eed7ec0c8bdb543a90a7e75b Mon Sep 17 00:00:00 2001 From: jjofseattle Date: Fri, 15 Nov 2013 16:20:25 -0800 Subject: [PATCH 4/5] improve coverage --- moto/route53/models.py | 9 --------- tests/test_route53/test_route53.py | 4 ++++ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/moto/route53/models.py b/moto/route53/models.py index 4afcfe922..0ee7dc446 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -16,15 +16,6 @@ class FakeZone: del self.rrsets[name] -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): diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py index 3e6132833..57da8112a 100644 --- a/tests/test_route53/test_route53.py +++ b/tests/test_route53/test_route53.py @@ -37,6 +37,10 @@ def test_hosted_zone(): @mock_route53 def test_rrset(): conn = boto.connect_route53('the_key', 'the_secret') + + conn.get_all_rrsets.when.called_with("abcd", type="A").\ + should.throw(boto.route53.exception.DNSServerError, "404 Not Found") + zone = conn.create_hosted_zone("testdns.aws.com") zoneid = zone["CreateHostedZoneResponse"]["HostedZone"]["Id"] From 38b26f038fb7855a6b4d80fad897997acd4f0ef2 Mon Sep 17 00:00:00 2001 From: jjofseattle Date: Wed, 20 Nov 2013 14:45:44 -0800 Subject: [PATCH 5/5] handle double deletion caused by httpretty --- moto/route53/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/route53/models.py b/moto/route53/models.py index 0ee7dc446..d901996fa 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -13,7 +13,7 @@ class FakeZone: self.rrsets[name] = rrset def delete_rrset(self, name): - del self.rrsets[name] + self.rrsets.pop(name, None) class Route53Backend(BaseBackend):