diff --git a/designate/api/v2/controllers/zones.py b/designate/api/v2/controllers/zones.py index 38f080ed..3a331e49 100644 --- a/designate/api/v2/controllers/zones.py +++ b/designate/api/v2/controllers/zones.py @@ -15,19 +15,17 @@ # under the License. import pecan from dns import zone as dnszone -from dns import rdatatype from dns import exception as dnsexception from designate import exceptions from designate import utils from designate import schema +from designate import dnsutils from designate.api.v2.controllers import rest from designate.api.v2.controllers import nameservers from designate.api.v2.controllers import recordsets from designate.api.v2.views import zones as zones_view from designate.objects import Domain -from designate.objects import Record -from designate.objects import RecordSet class ZonesController(rest.RestController): @@ -159,15 +157,27 @@ class ZonesController(rest.RestController): def _post_zonefile(self, request, response, context): """Import Zone""" - dnspython_zone = self._parse_zonefile(request) - # TODO(artom) This should probably be handled with transactions - zone = self._create_zone(context, dnspython_zone) - try: - self._create_records(context, zone['id'], dnspython_zone) - except exceptions.Base as e: - self.central_api.delete_domain(context, zone['id']) - raise e + dnspython_zone = dnszone.from_text( + request.body, + # Don't relativize, otherwise we end up with '@' record names. + relativize=False, + # Dont check origin, we allow missing NS records (missing SOA + # records are taken care of in _create_zone). + check_origin=False) + domain = dnsutils.from_dnspython_zone(dnspython_zone) + for rrset in domain.recordsets: + if rrset.type in ('NS', 'SOA'): + domain.recordsets.remove(rrset) + + except dnszone.UnknownOrigin: + raise exceptions.BadRequest('The $ORIGIN statement is required and' + ' must be the first statement in the' + ' zonefile.') + except dnsexception.SyntaxError: + raise exceptions.BadRequest('Malformed zonefile.') + + zone = self.central_api.create_domain(context, domain) if zone['status'] == 'PENDING': response.status_int = 202 @@ -247,91 +257,3 @@ class ZonesController(rest.RestController): # NOTE: This is a hack and a half.. But Pecan needs it. return '' - - # TODO(artom) Methods below may be useful elsewhere, consider putting them - # somewhere reusable. - - def _create_zone(self, context, dnspython_zone): - """Creates the initial zone""" - # dnspython never builds a zone with more than one SOA, even if we give - # it a zonefile that contains more than one - soa = dnspython_zone.get_rdataset(dnspython_zone.origin, 'SOA') - if soa is None: - raise exceptions.BadRequest('An SOA record is required') - email = soa[0].rname.to_text().rstrip('.') - email = email.replace('.', '@', 1) - values = { - 'name': dnspython_zone.origin.to_text(), - 'email': email, - 'ttl': soa.ttl - } - return self.central_api.create_domain(context, Domain(**values)) - - def _record2json(self, record_type, rdata): - if record_type == 'MX': - return { - 'data': '%d %s' % (rdata.preference, rdata.exchange.to_text()), - } - elif record_type == 'SRV': - return { - 'data': '%d %d %d %s' % (rdata.priority, rdata.weight, - rdata.port, rdata.target.to_text()), - } - else: - return { - 'data': rdata.to_text() - } - - def _create_records(self, context, zone_id, dnspython_zone): - """Creates the records""" - for record_name in dnspython_zone.nodes.keys(): - for rdataset in dnspython_zone.nodes[record_name]: - record_type = rdatatype.to_text(rdataset.rdtype) - - if (record_type == 'NS') or (record_type == 'SOA'): - # Don't create SOA or NS recordsets, as they are - # created automatically when a domain is - # created - pass - else: - # Create the other recordsets - values = { - 'domain_id': zone_id, - 'name': record_name.to_text(), - 'type': record_type - } - - recordset = self.central_api.create_recordset( - context, zone_id, RecordSet(**values)) - - for rdata in rdataset: - if (record_type == 'NS') or (record_type == 'SOA'): - pass - else: - # Everything else, including delegation NS, gets - # created - values = self._record2json(record_type, rdata) - - self.central_api.create_record( - context, - zone_id, - recordset['id'], - Record(**values)) - - def _parse_zonefile(self, request): - """Parses a POSTed zonefile into a dnspython zone object""" - try: - dnspython_zone = dnszone.from_text( - request.body, - # Don't relativize, otherwise we end up with '@' record names. - relativize=False, - # Dont check origin, we allow missing NS records (missing SOA - # records are taken care of in _create_zone). - check_origin=False) - except dnszone.UnknownOrigin: - raise exceptions.BadRequest('The $ORIGIN statement is required and' - ' must be the first statement in the' - ' zonefile.') - except dnsexception.SyntaxError: - raise exceptions.BadRequest('Malformed zonefile.') - return dnspython_zone diff --git a/designate/central/service.py b/designate/central/service.py index a29e12bf..06ac1cee 100644 --- a/designate/central/service.py +++ b/designate/central/service.py @@ -761,6 +761,11 @@ class Service(service.RPCService): with wrap_backend_call(): self.backend.create_domain(context, created_domain) + if domain.obj_attr_is_set('recordsets'): + for rrset in domain.recordsets: + self.create_recordset(context, created_domain['id'], rrset, + increment_serial=False) + self.notifier.info(context, 'dns.domain.create', created_domain) # If domain is a superdomain, update subdomains diff --git a/designate/dnsutils.py b/designate/dnsutils.py new file mode 100644 index 00000000..070c2b13 --- /dev/null +++ b/designate/dnsutils.py @@ -0,0 +1,75 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# Author: Endre Karlson +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from dns import rdatatype + +from designate import exceptions +from designate import objects + + +def from_dnspython_zone(dnspython_zone): + # dnspython never builds a zone with more than one SOA, even if we give + # it a zonefile that contains more than one + soa = dnspython_zone.get_rdataset(dnspython_zone.origin, 'SOA') + if soa is None: + raise exceptions.BadRequest('An SOA record is required') + email = soa[0].rname.to_text().rstrip('.') + email = email.replace('.', '@', 1) + values = { + 'name': dnspython_zone.origin.to_text(), + 'email': email, + 'ttl': soa.ttl + } + + zone = objects.Domain(**values) + + rrsets = dnspyrecords_to_recordsetlist(dnspython_zone.nodes) + zone.recordsets = rrsets + return zone + + +def dnspyrecords_to_recordsetlist(dnspython_records): + rrsets = objects.RecordList() + + for rname in dnspython_records.keys(): + for rdataset in dnspython_records[rname]: + rrset = dnspythonrecord_to_recordset(rname, rdataset) + + if rrset is None: + continue + + rrsets.append(rrset) + return rrsets + + +def dnspythonrecord_to_recordset(rname, rdataset): + record_type = rdatatype.to_text(rdataset.rdtype) + + # Create the other recordsets + values = { + 'name': rname.to_text(), + 'type': record_type + } + + if rdataset.ttl != 0L: + values['ttl'] = rdataset.ttl + + rrset = objects.RecordSet(**values) + rrset.records = objects.RecordList() + + for rdata in rdataset: + rr = objects.Record(data=rdata.to_text()) + rrset.records.append(rr) + return rrset diff --git a/designate/tests/resources/zonefiles/example.com.zone b/designate/tests/resources/zonefiles/example.com.zone index 7ad78893..06f076a7 100644 --- a/designate/tests/resources/zonefiles/example.com.zone +++ b/designate/tests/resources/zonefiles/example.com.zone @@ -6,16 +6,16 @@ example.com. 600 IN SOA ns1.example.com. nsadmin.example.com. ( 2419200 ; expire 10800 ; minimum ) -ipv4.example.com. 600 IN A 192.0.0.1 -ipv6.example.com. 600 IN AAAA fd00::1 -cname.example.com. 600 IN CNAME example.com. -example.com. 600 IN MX 5 192.0.0.2 -example.com. 600 IN MX 10 192.0.0.3 -_http._tcp.example.com. 600 IN SRV 10 0 80 192.0.0.4 -_http._tcp.example.com. 600 IN SRV 10 5 80 192.0.0.5 -example.com. 600 IN TXT "abc" "def" -example.com. 600 IN SPF "v=spf1 mx a" -example.com. 600 IN NS ns1.example.com. -example.com. 600 IN NS ns2.example.com. -delegation.example.com. 600 IN NS ns1.example.com. -1.0.0.192.in-addr.arpa. 600 IN PTR ipv4.example.com. +ipv4.example.com. 300 IN A 192.0.0.1 +ipv6.example.com. IN AAAA fd00::1 +cname.example.com. IN CNAME example.com. +example.com. IN MX 5 192.0.0.2 +example.com. IN MX 10 192.0.0.3 +_http._tcp.example.com. IN SRV 10 0 80 192.0.0.4 +_http._tcp.example.com. IN SRV 10 5 80 192.0.0.5 +example.com. IN TXT "abc" "def" +example.com. IN SPF "v=spf1 mx a" +example.com. IN NS ns1.example.com. +example.com. IN NS ns2.example.com. +delegation.example.com. IN NS ns1.example.com. +1.0.0.192.in-addr.arpa. IN PTR ipv4.example.com. diff --git a/designate/tests/test_dnsutils.py b/designate/tests/test_dnsutils.py new file mode 100644 index 00000000..dc40e9ea --- /dev/null +++ b/designate/tests/test_dnsutils.py @@ -0,0 +1,97 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# Author: Endre Karlson +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from dns import zone as dnszone + +from designate import dnsutils +from designate.tests import TestCase + +SAMPLES = { + ("cname.example.com.", "CNAME"): { + "records": ["example.com."], + }, + ("_http._tcp.example.com.", "SRV"): { + "records": [ + "10 0 80 192.0.0.4.example.com.", + "10 5 80 192.0.0.5.example.com." + ], + }, + ("ipv4.example.com.", "A"): { + "ttl": 300, + "records": ["192.0.0.1"] + }, + ("delegation.example.com.", "NS"): { + "records": ["ns1.example.com."] + }, + ("ipv6.example.com.", "AAAA"): { + "records": ["fd00::1"], + }, + ("example.com.", "SOA"): { + "records": [ + "ns1.example.com. nsadmin.example.com." + " 2013091101 7200 3600 2419200 10800" + ], + "ttl": 600 + }, + ("example.com.", "MX"): { + "records": [ + "5 192.0.0.2.example.com.", + '10 192.0.0.3.example.com.' + ] + }, + ("example.com.", "TXT"): { + "records": ['"abc" "def"'] + }, + ("example.com.", "SPF"): { + "records": ['"v=spf1 mx a"'] + }, + ("example.com.", "NS"): { + "records": [ + 'ns1.example.com.', + 'ns2.example.com.' + ] + } +} + + +class TestUtils(TestCase): + def test_parse_zone(self): + zone_file = self.get_zonefile_fixture() + + dnspython_zone = dnszone.from_text( + zone_file, + # Don't relativize, otherwise we end up with '@' record names. + relativize=False, + # Dont check origin, we allow missing NS records (missing SOA + # records are taken care of in _create_zone). + check_origin=False) + + zone = dnsutils.from_dnspython_zone(dnspython_zone) + + for rrset in zone.recordsets: + k = (rrset.name, rrset.type) + self.assertIn(k, SAMPLES) + + sample_ttl = SAMPLES[k].get('ttl', None) + if rrset.obj_attr_is_set('ttl') or sample_ttl is not None: + self.assertEqual(rrset.ttl, sample_ttl) + + self.assertEqual(len(SAMPLES[k]['records']), len(rrset.records)) + + for r in rrset.records: + self.assertIn(r.data, SAMPLES[k]['records']) + + self.assertEqual(len(SAMPLES), len(zone.recordsets)) + self.assertEqual('example.com.', zone.name)