pvl/hosts/zone.py
author Tero Marttila <tero.marttila@aalto.fi>
Thu, 26 Feb 2015 16:05:26 +0200
changeset 496 530f22575889
parent 495 629fc999cc33
child 497 0082d2092d1f
permissions -rw-r--r--
pvl.hosts.zone: label=relative() check handles FQDNs
"""
    Generate zonefile records from hosts
"""

import ipaddr
import logging; log = logging.getLogger('pvl.hosts.zone')
import pvl.dns
import pvl.hosts.host

class HostZoneError(pvl.hosts.host.HostError):
    pass

# TODO: generate location alias CNAMEs even if host itself is outside origin?
def host_forward (host, origin):
    """
        Yield ZoneRecords for hosts within the given zone origin
    """

    try:
        label = pvl.dns.relative(origin, host.domain, host.name)
    except ValueError as error:
        log.info("%s: skip: %s", host, error)
        return

    if host.forward:
        forward = pvl.dns.fqdn(host.forward)

        log.info("%s: forward: %s", host, forward)

        yield pvl.dns.ZoneRecord.CNAME(label, forward)
    
    elif host.forward is None:
        # forward
        if host.ip :
            log.info("%s: forward %s[%s]: A %s", host, origin, label, host.ip)

            yield pvl.dns.ZoneRecord.A(label, host.ip)

        if host.ip6 :
            log.info("%s: forward %s[%s]: AAAA %s", host, origin, label, host.ip6)

            yield pvl.dns.ZoneRecord.AAAA(label, host.ip6)

    else:
        log.info("%s: skip forward", host)
        return

    if host.location:
        location_alias, location_domain = host.location
        
        try:
            yield pvl.dns.ZoneRecord.CNAME(pvl.dns.relative(origin, location_domain, location_alias), label)
        except ValueError as error:
            raise HostZoneError(host, error)

    for alias in host.alias:
        yield pvl.dns.ZoneRecord.CNAME(pvl.dns.relative(origin, host.domain, alias), label)

    for alias in host.alias4:
        if not host.ip:
            raise HostZoneError(host, "alias4={host.alias4} without ip=".format(host=host))

        yield pvl.dns.ZoneRecord.A(pvl.dns.relative(origin, host.domain, alias), host.ip)

    for alias in host.alias6:
        if not host.ip6:
            raise HostZoneError(host, "alias6={host.alias6} without ip6=".format(host=host))

        yield pvl.dns.ZoneRecord.AAAA(pvl.dns.relative(origin, host.domain, alias), host.ip6)

def host_reverse (host, prefix) :
    """
        Yield (ipaddr.IPAddress, ZoneRecord) tuples for host within given prefix's reverse-dns zone.
    """

    if prefix.version == 4 :
        ip = host.ip
        
        # reverse= is IPv4-only
        reverse = host.reverse

    elif prefix.version == 6 :
        ip = host.ip6
        
        # if reverse= is set, always omit, for lack of reverse6=
        reverse = None if host.reverse is None else False

    else :
        raise ValueError("%s: unknown ip version: %s" % (prefix, prefix.version))

    if not ip :
        log.debug("%s: no ip%d", host, prefix.version)
        return

    if ip not in prefix :
        log.debug("%s: %s out of prefix: %s", host, ip, prefix)
        return
    
    # relative label
    label = pvl.dns.reverse_label(prefix, ip)
   
    if reverse:
        alias = pvl.dns.fqdn(reverse)
        
        log.info("%s %s[%s]: CNAME %s", host, prefix, ip, alias)

        yield ip, pvl.dns.zone.ZoneRecord.CNAME(label, alias)

    elif reverse is None :
        fqdn = host.fqdn()

        log.info("%s %s[%s]: PTR %s", host, prefix, ip, fqdn)

        yield ip, pvl.dns.zone.ZoneRecord.PTR(label, fqdn)

    else:
        log.info("%s %s[%s]: omit", host, prefix, ip)
 
def apply_hosts_forward (hosts, origin,
        add_origin  = False,
) :
    """
        Generate DNS ZoneRecords for for hosts within the given zone origin.

        Verifies that there are no overlapping name/type records, or CNAME records from aliases.

            hosts: [Host]       - Host's to render
            origin: str         - generate records relative to given zone origin
            add_origin: bool    - generate an explicit $ORIGIN directive

        Yields ZoneRecords in Host-order
    """

    if add_origin:
        yield pvl.dns.ZoneDirective.build(None, 'ORIGIN', origin)

    by_name = dict()
    by_name_type = dict()
    
    for host in hosts:
        for rr in host_forward(host, origin) :
            if (rr.name, 'CNAME') in by_name_type:
                raise HostZoneError(host, "CNAME {cname} conflict: {rr}".format(cname=by_name_type[rr.name, 'CNAME'].name, rr=rr))
            elif rr.type == 'CNAME' and rr.name in by_name:
                raise HostZoneError(host, "CNAME {cname} conflict: {rr}".format(cname=rr.name, rr=by_name[rr.name]))
            elif (rr.name, rr.type) in by_name_type:
                raise HostZoneError(host, "{type} {name} conflict: {rr}".format(type=rr.type, name=rr.name, rr=by_name_type[rr.name, rr.type]))
            
            by_name[rr.name] = rr
            by_name_type[rr.name, rr.type] = rr
            
            # preserve ordering
            yield rr

def apply_hosts_reverse (hosts, prefix,
        unknown_host    = None,
        unknown_domain  = None,
) :
    """
        Generate DNS ZoneRecords within the given prefix's reverse-dns zone for hosts.

            hosts: [Host]               - Host's to render PTRs for
            prefix: ipaddr.IPNetwork    - IPv4/IPv6 prefix to render PTRs for within associated in-addr.arpa/ip6.arpa zone
            unknown_host: str           - render a PTR to the given host @unknown_domain for unassigned IPs within prefix
            unknown_domain: str         - required if unknown_host is given

        Yields ZoneRecords in IPAddress-order
    """
        
    if unknown_host and not unknown_domain:
        raise ValueError("unknown_host requires unknown_domain=")
    
    # collect data for records
    by_ip = dict()

    for host in hosts:
        for ip, rr in host_reverse(host, prefix) :
            if ip in by_ip :
                raise HostZoneError(host, "{host}: IP {ip} conflict: {other}".format(host=host, ip=ip, other=by_ip[ip]))
            
            # do not retain order
            by_ip[ip] = rr

    if unknown_host :
        # enumerate all of them
        iter_ips = prefix.iterhosts()
    else :
        iter_ips = sorted(by_ip)

    for ip in iter_ips :
        if ip in by_ip :
            yield by_ip[ip]

        elif unknown_host:
            # synthesize a record
            label = pvl.dns.reverse_label(prefix, ip)
            fqdn = pvl.dns.fqdn(unknown_host, unknown_domain)

            log.info("%s %s[%s]: unused PTR %s", unknown_host, ip, prefix, fqdn)

            yield pvl.dns.ZoneRecord.PTR(label, fqdn)

        else:
            continue