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