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:
parent
5c2d83adfe
commit
6cd833daf7
@ -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
|
||||
|
@ -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
75
designate/dnsutils.py
Normal 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
|
@ -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.
|
||||
|
97
designate/tests/test_dnsutils.py
Normal file
97
designate/tests/test_dnsutils.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user