Move import code to dnsutils

This refactors out the zone import code into designate.dnsutils as a precursor
for Secondary zones.

Change-Id: I2bdad3483f8b58b40c2d816e834dd7477571df7e
This commit is contained in:
Endre Karlson 2014-10-25 02:15:02 +02:00
parent 5c2d83adfe
commit 6cd833daf7
5 changed files with 211 additions and 112 deletions

View File

@ -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

View File

@ -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

75
designate/dnsutils.py Normal file
View File

@ -0,0 +1,75 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Endre Karlson <endre.karlson@hp.com>
#
# 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

View File

@ -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.

View File

@ -0,0 +1,97 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Endre Karlson <endre.karlson@hp.com>
#
# 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)