# HG changeset patch # User Tero Marttila # Date 1424806692 -7200 # Node ID 600ad9eb6f25d4a41e22c6f8649663329d6f6a82 # Parent 1e925a1cc8ded3ec300fbee456dd837dbb7234da pvl.hosts.zone: cleanup and split pvl.hosts-forward and pvl.hosts-reverse from pvl.hosts-dns diff -r 1e925a1cc8de -r 600ad9eb6f25 bin/pvl.hosts-dns --- a/bin/pvl.hosts-dns Tue Feb 24 21:37:36 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,303 +0,0 @@ -#!/usr/bin/env python - -import pvl.args -import pvl.hosts -import pvl.dns.zone - -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.name, host.domain) - elif host.domain == origin : - label = host.name - elif host.domain and host.domain.endswith('.' + origin) : - fqdn = pvl.dns.join(host.name, host.domain) - label = fqdn[:(len(fqdn) - len(origin) - 1)] - elif host.domain : - log.debug("%s: domain out of zone: %s", host, 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) diff -r 1e925a1cc8de -r 600ad9eb6f25 bin/pvl.hosts-forward --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/pvl.hosts-forward Tue Feb 24 21:38:12 2015 +0200 @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +import pvl.hosts.zone +import sys + +sys.exit(pvl.hosts.zone.forward_main()) diff -r 1e925a1cc8de -r 600ad9eb6f25 bin/pvl.hosts-reverse --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/pvl.hosts-reverse Tue Feb 24 21:38:12 2015 +0200 @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +import pvl.hosts.zone +import sys + +sys.exit(pvl.hosts.zone.reverse_main()) diff -r 1e925a1cc8de -r 600ad9eb6f25 pvl/hosts/zone.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/hosts/zone.py Tue Feb 24 21:38:12 2015 +0200 @@ -0,0 +1,251 @@ +""" + 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 + """ + + fqdn = pvl.dns.join(name, domain) + + 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.zone.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.alias4: + 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 (ip, fqnd) for host within given prefix. + """ + + 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) + 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 host.reverse : + alias = pvl.dns.zone.fqdn(host.reverse) + + log.info("%s %s[%s]: CNAME %s", host, prefix, ip, alias) + + yield ip, pvl.dns.zone.ZoneRecord.CNAME(label, alias) + + elif host.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