pvl/hosts/zone.py
author Tero Marttila <tero.marttila@aalto.fi>
Wed, 25 Feb 2015 14:56:59 +0200
changeset 466 ad9d512ec1e7
parent 464 f1d3dbf04ca3
child 467 3bb00e5e79d3
permissions -rw-r--r--
pvl.hosts.zone: test and fix alias=
"""
    Generate zonefile records from hosts
"""

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

class HostZoneError(Exception):
    pass

def resolve (origin, domain, name) :
    """
        Resolve relative CNAME for label@origin -> alias@domain
    """

    if origin:
        origin = pvl.dns.fqdn(origin)
    
    if domain:
        fqdn = pvl.dns.fqdn(name, domain)
    else:
        fqdn = pvl.dns.fqdn(name)

    if not origin:
        return fqdn

    elif domain == origin:
         return name

    elif fqdn.endswith('.' + origin):
        return pvl.dns.relative(origin, fqdn)

    elif domain:
        raise HostZoneError("{name}: domain {domain} out of zone {origin}".format(name=name, domain=domain, origin=origin))

    else:
        raise HostZoneError("{name}: fqdn {fqdn} out of zone {origin}".format(name=name, fqdn=fqdn, origin=origin))

def host_forward (host, origin) :
    """
        Yield ZoneRecords for hosts within the given zone origin
    """

    try:
        label = resolve(origin, host.domain, host.name)
    except HostZoneError 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)
        return

    elif host.forward is not None:
        log.info("%s: skip forward", host)
        return
    
    # forward
    if host.ip :
        yield pvl.dns.ZoneRecord.A(label, host.ip)

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

    if host.location:
        location_alias, location_domain = host.location

        if not location_domain:
            location_domain = host.domain

        yield pvl.dns.ZoneRecord.CNAME(resolve(origin, location_domain, location_alias), label)

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

    for alias in host.alias4:
        yield pvl.dns.ZoneRecord.A(resolve(origin, host.domain, alias), host.ip)

    for alias in host.alias6:
         yield pvl.dns.ZoneRecord.AAAA(resolve(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 (options, hosts, origin) :
    """
        Generate DNS ZoneRecords for for hosts within the given zone origin.

        Yields ZoneRecords in Host-order
    """

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

    by_name = dict()
    by_cname = dict()
    
    for host in hosts:
        if not host.domain:
            log.debug("%s: skip without domain", host)

        for rr in host_forward(host, origin) :
            if rr.name in by_cname:
                raise HostZoneError(host, "{host}: CNAME {cname} conflict: {rr}".format(host=host, cname=by_cname[rr.name].name, rr=rr))
            elif rr.type == 'CNAME' and rr.name in by_name:
                raise HostZoneError(host, "{host}: CNAME {cname} conflict: {rr}".format(host=host, cname=rr.name, rr=by_name[rr.name]))
            
            by_name[rr.name] = rr

            if rr.type == 'CNAME':
                by_cname[rr.name] = rr
            
            # preserve ordering
            yield rr

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

        Yields ZoneRecords in IPAddress-order
    """
    
    # 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 options.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 options.unknown_host:
            # synthesize a record
            label = pvl.dns.reverse_label(prefix, ip)
            fqdn = pvl.dns.zone.fqdn(options.unknown_host, options.hosts_domain)

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

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

        else :
            continue

import pvl.args
import pvl.hosts.config

import optparse 

def forward_main () :
    """
        Generate bind zonefiles from host definitions.
    """

    parser = optparse.OptionParser(forward_main.__doc__)
    parser.add_option_group(pvl.args.parser(parser))
    parser.add_option_group(pvl.hosts.config.optparser(parser))

    parser.add_option('--add-origin',           action='store_true',
            help="Include $ORIGIN directive in zone")

    parser.add_option('--forward-zone',         metavar='DOMAIN',
            help="Generate forward zone for domain")

    # input
    options, args = parser.parse_args()
    
    pvl.args.apply(options)

    hosts = pvl.hosts.apply(options, args)

    # process
    for rr in apply_hosts_forward(options, hosts, options.forward_zone):
        print unicode(rr)
    
    return 0

def reverse_main () :
    """
        Generate bind zonefiles from host definitions.
    """

    parser = optparse.OptionParser(reverse_main.__doc__)
    parser.add_option_group(pvl.args.parser(parser))
    parser.add_option_group(pvl.hosts.config.optparser(parser))

    parser.add_option('--reverse-zone',         metavar='PREFIX',
            help="Generate reverse zone for prefix")

    parser.add_option('--unknown-host',         metavar='NAME',
            help="Generate records for unused IPs")

    # input
    options, args = parser.parse_args()
    
    pvl.args.apply(options)

    hosts = pvl.hosts.apply(options, args)

    # process
    for rr in apply_hosts_reverse(options, hosts, pvl.dns.parse_prefix(options.reverse_zone)):
        print unicode(rr)

    return 0