terom@269: #!/usr/bin/env python terom@269: terom@269: import pvl.args terom@270: import pvl.hosts terom@269: import pvl.dns.zone terom@269: terom@289: import fnmatch terom@269: import ipaddr terom@269: import logging; log = logging.getLogger('pvl.hosts-dns') terom@324: import optparse terom@269: terom@310: def process_hosts_alias (options, origin, host_domain, alias, host) : terom@269: """ terom@310: Resolve alias@domain within given zone. terom@310: """ terom@310: terom@325: if host_domain : terom@310: alias = pvl.dns.join(alias, host_domain) terom@310: else : terom@325: raise ValueError("no domain given for host %s alias %s" % (host, alias, )) terom@310: terom@310: if alias.endswith('.' + origin) : terom@310: # strip terom@310: alias = alias[:(len(alias) - len(origin) - 1)] terom@310: else: terom@310: raise ValueError("alias domain outside of origin: %s / %s" % (alias, origin)) terom@310: terom@310: return pvl.dns.zone.ZoneRecord.CNAME(alias, host) terom@310: terom@310: def process_hosts_names (options, hosts, origin) : terom@310: """ terom@310: Yield ZoneRecords for hosts within the given zone. terom@269: """ terom@269: terom@270: for host in hosts : terom@310: # determine label within zone terom@310: if not origin : terom@310: label = pvl.dns.join(host, host.domain) terom@310: elif host.domain == origin : terom@310: label = str(host) terom@310: elif host.domain.endswith('.' + origin) : terom@310: fqdn = pvl.dns.join(host, host.domain) terom@310: label = fqdn[:(len(fqdn) - len(origin) - 1)] terom@289: else : terom@331: log.debug("%s: domain %s out of zone: %s", host, host.domain, origin) terom@331: continue terom@331: terom@331: if host.forward is None : terom@331: pass terom@331: elif host.forward : terom@335: forward = pvl.dns.zone.fqdn(host, host.forward, host.domain) terom@331: terom@331: log.info("%s: forward: %s", host, forward) terom@331: terom@331: yield pvl.dns.zone.ZoneRecord.CNAME(label, forward) terom@331: continue terom@331: else : terom@331: log.info("%s: skip forward", host) terom@269: continue terom@310: terom@270: if host.ip : terom@310: yield pvl.dns.zone.ZoneRecord.A(label, host.ip) terom@269: terom@308: if host.alias4 : terom@310: yield pvl.dns.zone.ZoneRecord.A(host.ALIAS4_FMT.format(host=label), host.ip) terom@308: terom@308: if host.ip6 : terom@310: yield pvl.dns.zone.ZoneRecord.AAAA(label, host.ip6) terom@308: terom@308: if host.alias6 : terom@310: yield pvl.dns.zone.ZoneRecord.AAAA(label.ALIAS6_FMT.format(host=host), host.ip6) terom@308: terom@270: for alias in host.alias : terom@310: yield process_hosts_alias(options, origin, host.domain, alias, label) terom@269: terom@308: for alias4 in host.alias4 : terom@310: yield process_hosts_alias(options, origin, host.domain, alias4, host.ALIAS4_FMT.format(host=label)) terom@308: terom@308: for alias6 in host.alias6 : terom@310: yield process_hosts_alias(options, origin, host.domain, alias6, host.ALIAS6_FMT.format(host=label)) terom@308: terom@324: def process_hosts_forward (options, hosts, origin) : terom@273: """ terom@324: Generate DNS ZoneRecords for for hosts within the given zone origin. terom@273: """ terom@273: terom@324: if options.add_origin : terom@324: yield pvl.dns.zone.ZoneDirective.build(None, 'ORIGIN', origin) terom@324: terom@273: by_name = dict() terom@308: by_name_type = dict() terom@308: terom@308: # list of types thare are allowed to be present for a host terom@308: MULTI_TYPES = ('A', 'AAAA') terom@273: terom@324: for rr in process_hosts_names(options, hosts, origin) : terom@308: if (rr.name, rr.type) in by_name_type : terom@308: raise ValueError("%s: duplicate name/type: %s: %s" % (rr.name, rr, by_name_type[(rr.name, rr.type)])) terom@308: elif rr.type in MULTI_TYPES : terom@308: by_name_type[(rr.name, rr.type)] = rr terom@308: elif rr.name in by_name : terom@273: raise ValueError("%s: duplicate name: %s: %s" % (rr.name, rr, by_name[rr.name])) terom@308: terom@308: # always check these terom@308: by_name[rr.name] = rr terom@273: terom@273: # preserve ordering terom@273: yield rr terom@273: terom@312: def split_ipv6_parts (prefix) : terom@312: for hextet in prefix.rstrip(':').split(':') : terom@312: for nibble in hextet.rjust(4, '0') : terom@312: yield nibble terom@312: terom@312: def build_ipv6_parts (parts) : terom@312: for i in xrange(0, len(parts), 4) : terom@312: yield ''.join(parts[i:i+4]) terom@312: terom@312: # suffix :: terom@312: if len(parts) < 32 : terom@312: yield '' terom@312: yield '' terom@312: terom@312: def parse_prefix (prefix) : terom@312: """ terom@312: Return an ipaddr.IPNetwork from given IPv4/IPv6 prefix. terom@312: terom@312: >>> parse_prefix('127.0.0.0/8') terom@312: IPv4Network('127.0.0.0/8') terom@312: >>> parse_prefix('127.') terom@312: IPv4Network('127.0.0.0/8') terom@312: >>> parse_prefix('10') terom@312: IPv4Network('10.0.0.0/8') terom@312: >>> parse_prefix('192.168') terom@312: IPv4Network('192.168.0.0/16') terom@312: >>> parse_prefix('fe80:') terom@312: IPv6Network('fe80::/16') terom@312: >>> parse_prefix('2001:db8::') terom@312: IPv6Network('2001:db8::/32') terom@312: >>> parse_prefix('2001:db8:1:2') terom@312: IPv6Network('2001:db8:1:2::/64') terom@312: """ terom@312: terom@312: if '/' in prefix : terom@312: return ipaddr.IPNetwork(prefix) terom@312: terom@312: elif '.' in prefix or prefix.isdigit() : terom@312: parts = prefix.rstrip('.').split('.') terom@312: prefixlen = len(parts) * 8 terom@312: terom@312: return ipaddr.IPv4Network('{prefix}/{prefixlen}'.format( terom@312: prefix = '.'.join(parts + ['0' for i in xrange(4 - len(parts))]), terom@312: prefixlen = prefixlen, terom@312: )) terom@312: terom@312: elif ':' in prefix : terom@312: parts = list(split_ipv6_parts(prefix)) terom@312: prefixlen = len(parts) * 4 terom@312: terom@312: return ipaddr.IPv6Network('{prefix}/{prefixlen}'.format( terom@312: prefix = ':'.join(build_ipv6_parts(parts)), terom@312: prefixlen = prefixlen, terom@312: )) terom@312: terom@312: else : terom@312: raise ValueError("Unrecognized IP prefix string: %s" % (prefix, )) terom@312: terom@272: def process_hosts_ips (options, hosts, prefix) : terom@269: """ terom@272: Yield (ip, fqnd) for hosts within given prefix. terom@269: """ terom@269: terom@270: for host in hosts : terom@283: if prefix.version == 4 : terom@283: ip = host.ip terom@283: elif prefix.version == 6 : terom@283: ip = host.ip6 terom@283: else : terom@283: raise ValueError("%s: unknown ip version: %s" % (prefix, prefix.version)) terom@283: terom@283: if not ip : terom@296: log.debug("%s: no ip%d", host, prefix.version) terom@270: continue terom@269: terom@283: if ip not in prefix : terom@296: log.debug("%s: %s out of prefix: %s", host, ip, prefix) terom@269: continue terom@296: terom@331: label = pvl.dns.zone.reverse_label(prefix, ip) terom@331: terom@331: if host.reverse is None : terom@331: fqdn = host.fqdn() terom@272: terom@331: log.info("%s %s[%s]: PTR %s", host, prefix, ip, fqdn) terom@296: terom@331: yield host, ip, pvl.dns.zone.ZoneRecord.PTR(label, fqdn) terom@331: terom@331: elif host.reverse : terom@335: alias = pvl.dns.zone.fqdn(label, host.reverse, host.domain) terom@331: terom@331: log.info("%s %s[%s]: CNAME %s", host, prefix, ip, alias) terom@331: terom@331: yield host, ip, pvl.dns.zone.ZoneRecord.CNAME(label, alias) terom@331: terom@331: else : terom@331: log.info("%s %s[%s]: omit", host, prefix, ip) terom@331: continue terom@331: terom@272: terom@272: def process_hosts_reverse (options, hosts, prefix) : terom@272: """ terom@272: Generate DNS ZoneRecords within the given prefix's reverse-dns zone for hosts. terom@272: """ terom@272: terom@272: # collect data for records terom@272: by_ip = dict() terom@331: for host, ip, rr in process_hosts_ips(options, hosts, prefix) : terom@272: if ip in by_ip : terom@331: raise ValueError("%s: duplicate ip: %s: %s" % (host, ip, by_ip[ip])) terom@272: else : terom@331: by_ip[ip] = rr terom@272: terom@278: if options.unknown_host : terom@278: # enumerate all of them terom@278: iter_ips = prefix.iterhosts() terom@278: else : terom@278: iter_ips = sorted(by_ip) terom@278: terom@278: for ip in iter_ips : terom@272: if ip in by_ip : terom@331: yield by_ip[ip] terom@272: elif options.unknown_host : terom@331: label = pvl.dns.zone.reverse_label(prefix, ip) terom@272: fqdn = pvl.dns.zone.fqdn(options.unknown_host, options.hosts_domain) terom@331: terom@331: log.info("%s %s[%s]: unused PTR %s", options.unknown_host, ip, prefix, fqdn) terom@331: terom@331: yield pvl.dns.zone.ZoneRecord.PTR(label, fqdn) terom@272: else : terom@331: continue terom@270: terom@270: def apply_zone (options, zone) : terom@270: """ terom@270: Output given ZoneRecord's terom@270: """ terom@270: terom@270: for record in zone : terom@270: print unicode(record) terom@269: terom@269: def main (argv) : terom@269: """ terom@269: Generate bind zonefiles from host definitions. terom@269: """ terom@269: terom@269: parser = optparse.OptionParser(main.__doc__) terom@269: parser.add_option_group(pvl.args.parser(parser)) terom@270: parser.add_option_group(pvl.hosts.optparser(parser)) terom@269: terom@324: parser.add_option('--add-origin', action='store_true', terom@324: help="Include $ORIGIN directive in zone") terom@324: terom@269: parser.add_option('--forward-zone', metavar='DOMAIN', terom@269: help="Generate forward zone for domain") terom@269: terom@269: parser.add_option('--reverse-zone', metavar='PREFIX', terom@312: help="Generate reverse zone for prefix") terom@269: terom@272: parser.add_option('--unknown-host', metavar='NAME', terom@272: help="Generate records for unused IPs") terom@272: terom@269: options, args = parser.parse_args(argv[1:]) terom@269: pvl.args.apply(options) terom@269: terom@269: # input terom@270: hosts = pvl.hosts.apply(options, args) terom@269: terom@270: # process terom@269: if options.forward_zone : terom@270: apply_zone(options, terom@270: process_hosts_forward(options, hosts, options.forward_zone), terom@270: ) terom@269: terom@270: if options.reverse_zone : terom@270: apply_zone(options, terom@312: process_hosts_reverse(options, hosts, parse_prefix(options.reverse_zone)), terom@270: ) terom@269: terom@269: if __name__ == '__main__': terom@269: pvl.args.main(main)