bin/pvl.hosts-dns
author Tero Marttila <terom@paivola.fi>
Sun, 07 Sep 2014 14:21:56 +0300
changeset 424 e77e967d59b0
parent 421 585eadaed270
permissions -rwxr-xr-x
hgignore: use glob; ignore snmp mibs
#!/usr/bin/env python

import pvl.args
import pvl.hosts
import pvl.dns.zone

import fnmatch
import ipaddr
import logging; log = logging.getLogger('pvl.hosts-dns')
import optparse 

def process_hosts_alias (options, origin, host_domain, alias, host) :
    """
        Resolve alias@domain within given zone.
    """

    if host_domain :
        alias = pvl.dns.join(alias, host_domain)
    else :
        raise ValueError("no domain given for host %s alias %s" % (host, alias, ))

    if alias.endswith('.' + origin) :
        # strip
        alias = alias[:(len(alias) - len(origin) - 1)]
    else:
        raise ValueError("alias domain outside of origin: %s / %s" % (alias, origin))

    return pvl.dns.zone.ZoneRecord.CNAME(alias, host)

def process_hosts_names (options, hosts, origin) :
    """
        Yield ZoneRecords for hosts within the given zone.
    """

    for host in hosts :
        # determine label within zone
        if not origin :
            label = pvl.dns.join(host, host.domain)
        elif host.domain == origin :
            label = str(host)
        elif host.domain and host.domain.endswith('.' + origin) :
            fqdn = pvl.dns.join(host, host.domain)
            label = fqdn[:(len(fqdn) - len(origin) - 1)]
        elif host.domain :
            log.debug("%s: domain %s out of zone: %s", host, host.domain, origin)
            continue
        else :
            log.debug("%s: fqdn out of zone: %s", host, origin)
            continue
        
        if host.forward is None  :
            pass
        elif host.forward :
            forward = pvl.dns.zone.fqdn(host.forward)

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

            yield pvl.dns.zone.ZoneRecord.CNAME(label, forward)
            continue
        else :
            log.info("%s: skip forward", host)
            continue

        if host.ip :
            yield pvl.dns.zone.ZoneRecord.A(label, host.ip)
        
        if host.alias4 :
            yield pvl.dns.zone.ZoneRecord.A(host.ALIAS4_FMT.format(host=label), host.ip)

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

        if host.alias6 :
            yield pvl.dns.zone.ZoneRecord.AAAA(label.ALIAS6_FMT.format(host=host), host.ip6)

        if host.location and host.location_domain:
            yield process_hosts_alias(options, origin, host.location_domain, host.location, label)
        elif host.location:
            yield process_hosts_alias(options, origin, host.domain, host.location, label)

        for alias in host.alias :
            yield process_hosts_alias(options, origin, host.domain, alias, label)

        for alias4 in host.alias4 :
            yield process_hosts_alias(options, origin, host.domain, alias4, host.ALIAS4_FMT.format(host=label))

        for alias6 in host.alias6 :
            yield process_hosts_alias(options, origin, host.domain, alias6, host.ALIAS6_FMT.format(host=label))

def process_hosts_forward (options, hosts, origin) :
    """
        Generate DNS ZoneRecords for for hosts within the given zone origin.
    """

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

    by_name = dict()
    by_name_type = dict()
    
    # list of types thare are allowed to be present for a host
    MULTI_TYPES = ('A', 'AAAA')

    for rr in process_hosts_names(options, hosts, origin) :
        if (rr.name, rr.type) in by_name_type :
            raise ValueError("%s: duplicate name/type: %s: %s" % (rr.name, rr, by_name_type[(rr.name, rr.type)]))
        elif rr.type in MULTI_TYPES :
            by_name_type[(rr.name, rr.type)] = rr
        elif rr.name in by_name :
            raise ValueError("%s: duplicate name: %s: %s" % (rr.name, rr, by_name[rr.name]))
        
        # always check these
        by_name[rr.name] = rr
        
        # preserve ordering
        yield rr

def split_ipv6_parts (prefix) :
    for hextet in prefix.rstrip(':').split(':') :
        for nibble in hextet.rjust(4, '0') :
            yield nibble

def build_ipv6_parts (parts) :
    for i in xrange(0, len(parts), 4) :
        yield ''.join(parts[i:i+4])
    
    # suffix ::
    if len(parts) < 32 :
        yield ''
        yield ''

def parse_prefix (prefix) :
    """
        Return an ipaddr.IPNetwork from given IPv4/IPv6 prefix.

        >>> parse_prefix('127.0.0.0/8')
        IPv4Network('127.0.0.0/8')
        >>> parse_prefix('192.0.2.128/26')
        IPv4Network('192.0.2.128/26')
        >>> parse_prefix('192.0.2.128-26')
        IPv4Network('192.0.2.128-26')
        >>> parse_prefix('127.')
        IPv4Network('127.0.0.0/8')
        >>> parse_prefix('10')
        IPv4Network('10.0.0.0/8')
        >>> parse_prefix('192.168')
        IPv4Network('192.168.0.0/16')
        >>> parse_prefix('fe80:')
        IPv6Network('fe80::/16')
        >>> parse_prefix('2001:db8::')
        IPv6Network('2001:db8::/32')
        >>> parse_prefix('2001:db8:1:2')
        IPv6Network('2001:db8:1:2::/64')
    """

    if '/' in prefix :
        return ipaddr.IPNetwork(prefix)
    
    elif '-' in prefix :
        return ipaddr.IPNetwork(prefix.replace('-', '/'))

    elif '.' in prefix or prefix.isdigit() :
        parts = prefix.rstrip('.').split('.')
        prefixlen = len(parts) * 8
        
        return ipaddr.IPv4Network('{prefix}/{prefixlen}'.format(
            prefix      = '.'.join(parts + ['0' for i in xrange(4 - len(parts))]),
            prefixlen   = prefixlen,
        ))
   
    elif ':' in prefix :
        parts = list(split_ipv6_parts(prefix))
        prefixlen = len(parts) * 4

        return ipaddr.IPv6Network('{prefix}/{prefixlen}'.format(
            prefix      = ':'.join(build_ipv6_parts(parts)),
            prefixlen   = prefixlen,
        ))

    else :
        raise ValueError("Unrecognized IP prefix string: %s" % (prefix, ))

def process_hosts_ips (options, hosts, prefix) :
    """
        Yield (ip, fqnd) for hosts within given prefix.
    """

    for host in hosts :
        if prefix.version == 4 :
            ip = host.ip
        elif prefix.version == 6 :
            ip = host.ip6
        else :
            raise ValueError("%s: unknown ip version: %s" % (prefix, prefix.version))

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

        if ip not in prefix :
            log.debug("%s: %s out of prefix: %s", host, ip, prefix)
            continue
        
        label = pvl.dns.zone.reverse_label(prefix, ip)
       
        if host.reverse is None :
            fqdn = host.fqdn()

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

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

        elif host.reverse :
            alias = pvl.dns.zone.fqdn(host.reverse)
            
            log.info("%s %s[%s]: CNAME %s", host, prefix, ip, alias)

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

        else :
            log.info("%s %s[%s]: omit", host, prefix, ip)
            continue

 
def process_hosts_reverse (options, hosts, prefix) :
    """
        Generate DNS ZoneRecords within the given prefix's reverse-dns zone for hosts.
    """
    
    # collect data for records
    by_ip = dict()
    for host, ip, rr in process_hosts_ips(options, hosts, prefix) :
        if ip in by_ip :
            raise ValueError("%s: duplicate ip: %s: %s" % (host, ip, by_ip[ip]))
        else :
            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 :
            label = pvl.dns.zone.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

def apply_zone (options, zone) :
    """
        Output given ZoneRecord's
    """

    for record in zone :
        print unicode(record)

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

    parser = optparse.OptionParser(main.__doc__)
    parser.add_option_group(pvl.args.parser(parser))
    parser.add_option_group(pvl.hosts.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")

    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")

    options, args = parser.parse_args(argv[1:])
    pvl.args.apply(options)

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

    # process
    if options.forward_zone :
        apply_zone(options, 
                process_hosts_forward(options, hosts, options.forward_zone),
        )
    
    if options.reverse_zone :
        apply_zone(options, 
                process_hosts_reverse(options, hosts, parse_prefix(options.reverse_zone)),
        )

if __name__ == '__main__':
    pvl.args.main(main)