use FQDN typed params in api, remove unused parse_name (which was problematic anyway)

This commit is contained in:
Thomas Waldmann 2014-08-30 18:27:21 +02:00
parent f13d985f4a
commit cf2c46e612
7 changed files with 61 additions and 107 deletions

View File

@ -32,7 +32,7 @@ SECURE = False # TLS/SNI support on python 2.x sucks :(
from django.utils.translation import activate
from nsupdate.main.dnstools import update_ns
from nsupdate.main.dnstools import update_ns, FQDN
@pytest.yield_fixture(scope="function")
@ -43,13 +43,14 @@ def ddns_hostname():
"""
hostname = "test%d" % randint(1000000000, 2000000000)
yield hostname
update_ns(hostname + '.' + TESTDOMAIN, 'A', action='del')
update_ns(hostname + '.' + TESTDOMAIN, 'AAAA', action='del')
fqdn = FQDN(hostname, TESTDOMAIN)
update_ns(fqdn, 'A', action='del')
update_ns(fqdn, 'AAAA', action='del')
@pytest.yield_fixture(scope="function")
def ddns_fqdn(ddns_hostname):
yield ddns_hostname + '.' + TESTDOMAIN
yield FQDN(ddns_hostname, TESTDOMAIN)
# Note: fixture must be "function" scope (default), see https://github.com/pelme/pytest_django/issues/33

View File

@ -4,7 +4,7 @@ Tests for api package.
from django.core.urlresolvers import reverse
from nsupdate.main.dnstools import query_ns
from nsupdate.main.dnstools import query_ns, FQDN
from nsupdate.main.models import Domain
from conftest import TESTDOMAIN, TEST_HOST, TEST_HOST2, TEST_SECRET, TEST_SECRET2
@ -13,7 +13,7 @@ USERNAME = 'test'
PASSWORD = 'pass'
BASEDOMAIN = "nsupdate.info"
HOSTNAME = 'nsupdate-ddns-client-unittest.' + BASEDOMAIN
TEST_HOST_OTHER = FQDN('nsupdate-ddns-client-unittest', BASEDOMAIN)
def test_myip(client):
@ -124,14 +124,14 @@ def test_nic_update_authorized_update_other_services(client):
# must be good (was different IP)
assert response.content == b'good 1.2.3.4'
# now check if it updated the other service also:
assert query_ns(HOSTNAME, 'A') == '1.2.3.4'
assert query_ns(TEST_HOST_OTHER, 'A') == '1.2.3.4'
response = client.get(reverse('nic_update') + '?myip=2.3.4.5',
HTTP_AUTHORIZATION=make_basic_auth_header(TEST_HOST, TEST_SECRET))
assert response.status_code == 200
# must be good (was different IP)
assert response.content == b'good 2.3.4.5'
# now check if it updated the other service also:
assert query_ns(HOSTNAME, 'A') == '2.3.4.5'
assert query_ns(TEST_HOST_OTHER, 'A') == '2.3.4.5'
def test_nic_update_authorized_badagent(client, settings):

View File

@ -224,9 +224,9 @@ class NicUpdateView(View):
ipaddr = request.META.get('REMOTE_ADDR')
secure = request.is_secure()
if delete:
return _delete(host, hostname, ipaddr, secure, logger=logger)
return _delete(host, ipaddr, secure, logger=logger)
else:
return _update(host, hostname, ipaddr, secure, logger=logger)
return _update(host, ipaddr, secure, logger=logger)
class NicDeleteView(NicUpdateView):
@ -282,9 +282,9 @@ class AuthorizedNicUpdateView(View):
ipaddr = request.META.get('REMOTE_ADDR')
secure = request.is_secure()
if delete:
return _delete(host, hostname, ipaddr, secure, logger=logger)
return _delete(host, ipaddr, secure, logger=logger)
else:
return _update(host, hostname, ipaddr, secure, logger=logger)
return _update(host, ipaddr, secure, logger=logger)
class AuthorizedNicDeleteView(AuthorizedNicUpdateView):
@ -300,12 +300,11 @@ class AuthorizedNicDeleteView(AuthorizedNicUpdateView):
return super(AuthorizedNicDeleteView, self).get(request, logger=logger, delete=delete)
def _update(host, hostname, ipaddr, secure=False, logger=None):
def _update(host, ipaddr, secure=False, logger=None):
"""
common code shared by the 2 update views
:param host: host object
:param hostname: hostname (fqdn)
:param ipaddr: new ip addr (v4 or v6)
:param secure: True if we use TLS/https
:param logger: a logger object
@ -330,9 +329,10 @@ def _update(host, hostname, ipaddr, secure=False, logger=None):
# some people manage to even give a non-ascii string instead of an ip addr
return Response('dnserr') # there should be a better response code for this
host.poke(kind, secure)
fqdn = host.get_fqdn()
try:
update(hostname, ipaddr)
logger.info('%s - received good update -> ip: %s tls: %r' % (hostname, ipaddr, secure))
update(fqdn, ipaddr)
logger.info('%s - received good update -> ip: %s tls: %r' % (fqdn, ipaddr, secure))
# now check if there are other services we shall relay updates to:
for hc in host.serviceupdaterhostconfigs.all():
if (kind == 'ipv4' and hc.give_ipv4 and hc.service.accept_ipv4
@ -351,23 +351,22 @@ def _update(host, hostname, ipaddr, secure=False, logger=None):
logger.exception("the dyndns2 updater raised an exception [%r]" % kwargs)
return Response('good %s' % ipaddr)
except SameIpError:
logger.warning('%s - received no-change update, ip: %s tls: %r' % (hostname, ipaddr, secure))
logger.warning('%s - received no-change update, ip: %s tls: %r' % (fqdn, ipaddr, secure))
host.register_client_fault()
return Response('nochg %s' % ipaddr)
except (DnsUpdateError, NameServerNotAvailable) as e:
msg = str(e)
logger.error('%s - received update that resulted in a dns error [%s], ip: %s tls: %r' % (
hostname, msg, ipaddr, secure))
fqdn, msg, ipaddr, secure))
host.register_server_fault()
return Response('dnserr')
def _delete(host, hostname, ipaddr, secure=False, logger=None):
def _delete(host, ipaddr, secure=False, logger=None):
"""
common code shared by the 2 delete views
:param host: host object
:param hostname: hostname (fqdn)
:param ipaddr: ip addr (to determine record type A or AAAA)
:param secure: True if we use TLS/https
:param logger: a logger object
@ -392,15 +391,16 @@ def _delete(host, hostname, ipaddr, secure=False, logger=None):
# some people manage to even give a non-ascii string instead of an ip addr
return Response('dnserr') # there should be a better response code for this
host.poke(kind, secure)
fqdn = host.get_fqdn()
try:
rdtype = 'A' if kind == 'ipv4' else 'AAAA'
delete(hostname, rdtype)
logger.info('%s - received delete for record %s, tls: %r' % (hostname, rdtype, secure))
delete(fqdn, rdtype)
logger.info('%s - received delete for record %s, tls: %r' % (fqdn, rdtype, secure))
# XXX unclear what to do for "other services" we relay updates to
return Response('deleted %s' % rdtype)
except (DnsUpdateError, NameServerNotAvailable) as e:
msg = str(e)
logger.error('%s - received delete for record %s that resulted in a dns error [%s], tls: %r' % (
hostname, rdtype, msg, secure))
fqdn, rdtype, msg, secure))
host.register_server_fault()
return Response('dnserr')

View File

@ -10,12 +10,12 @@ pytestmark = pytest.mark.django_db
from dns.resolver import NXDOMAIN, NoAnswer
from ..dnstools import (add, delete, update, query_ns, rev_lookup, parse_name, update_ns,
from ..dnstools import (add, delete, update, query_ns, rev_lookup, update_ns,
SameIpError, DnsUpdateError, FQDN)
# see also conftest.py
BASEDOMAIN = 'nsupdate.info'
INVALID_HOST = 'test999.' + BASEDOMAIN # this can't get updated
INVALID_HOST = FQDN('test999', BASEDOMAIN) # this can't get updated
class TestFQDN(object):
@ -128,23 +128,6 @@ class TestReverseLookup(object):
class TestUpdate(object):
def test_parse1(self):
host, domain = 'test', BASEDOMAIN
origin, relname = parse_name(host + '.' + domain)
assert str(origin) == domain + '.'
assert str(relname) == host
def test_parse2(self):
host, domain = 'prefix.test', BASEDOMAIN
origin, relname = parse_name(host + '.' + domain)
assert str(origin) == domain + '.'
assert str(relname) == 'prefix.test'
def test_parse_with_origin(self):
origin, relname = parse_name('foo.bar.baz.org', 'bar.baz.org')
assert str(origin) == 'bar.baz.org' + '.'
assert str(relname) == 'foo'
def test_add_del_v4(self, ddns_fqdn):
host, ip = ddns_fqdn, '1.1.1.1'
remove_records(host)

View File

@ -98,22 +98,22 @@ def check_ip(ipaddr, keys=('ipv4', 'ipv6')):
return keys[af == dns.inet.AF_INET6]
def add(fqdn, ipaddr, ttl=60, origin=None):
def add(fqdn, ipaddr, ttl=60):
"""
intelligent dns adder - first does a lookup on the master server to find
the current ip and only sends an 'add' if there is no such entry.
otherwise send an 'upd' if the if we have a different ip.
:param fqdn: fully qualified domain name (str)
:param fqdn: fully qualified domain name (FQDN)
:param ipaddr: new ip address
:param ttl: time to live, default 60s (int)
:param origin: origin zone (optional, str)
:raises: SameIpError if new and old IP is the same
:raises: ValueError if ipaddr is no valid ip address string
"""
assert isinstance(fqdn, FQDN)
rdtype = check_ip(ipaddr, keys=('A', 'AAAA'))
try:
current_ipaddr = query_ns(fqdn, rdtype, origin=origin)
current_ipaddr = query_ns(fqdn, rdtype)
# check if ip really changed
ok = ipaddr != current_ipaddr
action = 'upd'
@ -125,43 +125,43 @@ def add(fqdn, ipaddr, ttl=60, origin=None):
# only send an add/update if the ip really changed as the update
# causes write I/O on the nameserver and also traffic to the
# dns slaves (they get a notify if we update the zone).
update_ns(fqdn, rdtype, ipaddr, action=action, ttl=ttl, origin=origin)
update_ns(fqdn, rdtype, ipaddr, action=action, ttl=ttl)
else:
raise SameIpError
def delete(fqdn, rdtype=None, origin=None):
def delete(fqdn, rdtype=None):
"""
dns deleter
:param fqdn: fully qualified domain name (str)
:param fqdn: fully qualified domain name (FQDN)
:param rdtype: 'A', 'AAAA' or None (deletes 'A' and 'AAAA')
:param origin: origin zone (optional, str)
"""
assert isinstance(fqdn, FQDN)
if rdtype is not None:
assert rdtype in ['A', 'AAAA', ]
rdtypes = [rdtype, ]
else:
rdtypes = ['A', 'AAAA']
for rdtype in rdtypes:
update_ns(fqdn, rdtype, action='del', origin=origin)
update_ns(fqdn, rdtype, action='del')
def update(fqdn, ipaddr, ttl=60, origin=None):
def update(fqdn, ipaddr, ttl=60):
"""
intelligent dns updater - first does a lookup on the master server to find
the current ip and only sends a dynamic update if we have a different ip.
:param fqdn: fully qualified domain name (str)
:param fqdn: fully qualified domain name (FQDN)
:param ipaddr: new ip address
:param ttl: time to live, default 60s (int)
:param origin: origin zone (optional, str)
:raises: SameIpError if new and old IP is the same
:raises: ValueError if ipaddr is no valid ip address string
"""
assert isinstance(fqdn, FQDN)
rdtype = check_ip(ipaddr, keys=('A', 'AAAA'))
try:
current_ipaddr = query_ns(fqdn, rdtype, origin=origin)
current_ipaddr = query_ns(fqdn, rdtype)
# check if ip really changed
ok = ipaddr != current_ipaddr
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
@ -171,27 +171,23 @@ def update(fqdn, ipaddr, ttl=60, origin=None):
# only send an update if the ip really changed as the update
# causes write I/O on the nameserver and also traffic to the
# dns slaves (they get a notify if we update the zone).
update_ns(fqdn, rdtype, ipaddr, action='upd', ttl=ttl, origin=origin)
update_ns(fqdn, rdtype, ipaddr, action='upd', ttl=ttl)
else:
raise SameIpError
def query_ns(qname, rdtype, origin=None):
def query_ns(fqdn, rdtype):
"""
query a dns name from our master server
:param qname: the query name
:type qname: dns.name.Name object or str
:param fqdn: fqdn to query the name server for
:type fqdn: dnstools.FQDN
:param rdtype: the query type
:type rdtype: int or str
:param origin: origin zone
:type origin: str or None
:return: IP (as str)
:raises: see dns.resolver.Resolver.query
"""
origin, name = parse_name(qname, origin)
fqdn = name + origin
assert fqdn.is_absolute()
assert isinstance(fqdn, FQDN)
nameserver, origin = get_ns_info(fqdn)[0:2]
resolver = dns.resolver.Resolver(configure=False)
# we do not configure it from resolv.conf, but patch in the values we
@ -204,13 +200,13 @@ def query_ns(qname, rdtype, origin=None):
# (used if flags = None is given). Thus, we explicitly give flags (all off):
resolver.flags = 0
try:
answer = resolver.query(fqdn, rdtype)
answer = resolver.query(str(fqdn), rdtype)
ip = str(list(answer)[0])
logger.debug("query: %s answer: %s" % (fqdn.to_text(), ip))
logger.debug("query: %s answer: %s" % (fqdn, ip))
return ip
except (dns.resolver.Timeout, dns.resolver.NoNameservers): # socket.error also?
logger.warning("timeout when querying for name '%s' in zone '%s' with rdtype '%s'." % (
name, origin, rdtype))
fqdn.host, origin, rdtype))
set_ns_availability(origin, False)
raise
@ -233,53 +229,27 @@ def rev_lookup(ipaddr):
return name
def parse_name(fqdn, origin=None):
def get_ns_info(fqdn):
"""
Parse a fully qualified domain name into a relative name
and a origin zone. Please note that the origin return value will
have a trailing dot.
:param fqdn: fully qualified domain name (str)
:param origin: origin zone (optional, str)
:return: origin, relative name (both dns.name.Name)
"""
fqdn = dns.name.from_text(fqdn)
if origin is None:
origin = dns.resolver.zone_for_name(fqdn)
rel_name = fqdn.relativize(origin)
else:
origin = dns.name.from_text(origin)
rel_name = fqdn - origin
return origin, rel_name
def get_ns_info(fqdn, origin=None):
"""
Get the master nameserver for the <origin> zone, the key secret needed
to update the zone and the key algorithm used.
Get the master nameserver for fqdn, the key secret needed to update the zone and the key algorithm used.
:param fqdn: the fully qualified hostname we are dealing with (str)
:param origin: zone we are dealing with, must be with trailing dot (default:autodetect) (str)
:return: master nameserver, origin, domain, update keyname, update secret, update algo
:raises: NameServerNotAvailable if ns was flagged unavailable in the db
"""
fqdn_str = str(fqdn)
origin, name = parse_name(fqdn_str, origin)
origin_str = str(origin)
assert isinstance(fqdn, FQDN)
from .models import Domain
try:
# first we check if we have an entry for the fqdn
# single-host update secret use case
# XXX we need 2 DB accesses for the usual case just to support this rare case
domain = fqdn_str.rstrip('.')
domain = str(fqdn)
d = Domain.objects.get(domain=domain)
keyname = fqdn_str
except Domain.DoesNotExist:
# now check the base zone, the usual case
# zone update secret use case
domain = origin_str.rstrip('.')
domain = fqdn.domain
d = Domain.objects.get(domain=domain)
keyname = origin_str
if not d.available:
if d.last_update + timedelta(seconds=UNAVAILABLE_RETRY) > now():
# if there are troubles with a nameserver, we set available=False
@ -290,24 +260,24 @@ def get_ns_info(fqdn, origin=None):
# retry timeout is over, set it available again
set_ns_availability(domain, True)
algorithm = getattr(dns.tsig, d.nameserver_update_algorithm)
return d.nameserver_ip, origin, domain, name, keyname, d.nameserver_update_secret, algorithm
return d.nameserver_ip, fqdn.domain, domain, fqdn.host, domain, d.nameserver_update_secret, algorithm
def update_ns(fqdn, rdtype='A', ipaddr=None, origin=None, action='upd', ttl=60):
def update_ns(fqdn, rdtype='A', ipaddr=None, action='upd', ttl=60):
"""
update the master server
:param fqdn: the fully qualified domain name to update (str)
:param fqdn: the fully qualified domain name to update (FQDN)
:param rdtype: the record type (default: 'A') (str)
:param ipaddr: ip address (v4 or v6), if needed (str)
:param origin: the origin zone to update (default; autodetect) (str)
:param action: 'add', 'del' or 'upd'
:param ttl: time to live for the added/updated resource, default 60s (int)
:return: dns response
:raises: DnsUpdateError, Timeout
"""
assert isinstance(fqdn, FQDN)
assert action in ['add', 'del', 'upd', ]
nameserver, origin, domain, name, keyname, key, algo = get_ns_info(fqdn, origin)
nameserver, origin, domain, name, keyname, key, algo = get_ns_info(fqdn)
upd = dns.update.Update(origin,
keyring=dns.tsigkeyring.from_text({keyname: key}),
keyalgorithm=algo)

View File

@ -181,7 +181,7 @@ class Host(models.Model):
index_together = (('subdomain', 'domain'), )
def get_fqdn(self):
return '%s.%s' % (self.subdomain, self.domain.domain)
return dnstools.FQDN(self.subdomain, self.domain.domain)
@classmethod
def get_by_fqdn(cls, fqdn, **kwargs):
@ -202,7 +202,7 @@ class Host(models.Model):
def get_ip(self, kind):
record = 'A' if kind == 'ipv4' else 'AAAA'
try:
return dnstools.query_ns(self.get_fqdn(), record, origin=self.domain.domain)
return dnstools.query_ns(self.get_fqdn(), record)
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
return 'none'
except (dns.resolver.NoNameservers, dns.resolver.Timeout, dnstools.NameServerNotAvailable):
@ -250,7 +250,7 @@ class Host(models.Model):
def pre_delete_host(sender, **kwargs):
obj = kwargs['instance']
try:
dnstools.delete(obj.get_fqdn(), origin=obj.domain.domain)
dnstools.delete(obj.get_fqdn())
except (dnstools.Timeout, dnstools.NameServerNotAvailable):
# well, we tried to clean up, but we didn't reach the nameserver
pass

View File

@ -194,7 +194,7 @@ class OverviewView(CreateView):
def form_valid(self, form):
self.object = form.save(commit=False)
try:
dnstools.add(self.object.get_fqdn(), self.request.META['REMOTE_ADDR'], origin=self.object.domain.domain)
dnstools.add(self.object.get_fqdn(), self.request.META['REMOTE_ADDR'])
except dnstools.Timeout:
success, level, msg = False, messages.ERROR, 'Timeout - communicating to name server failed.'
except dnstools.NameServerNotAvailable: