tero@458: """ tero@458: Generate zonefile records from hosts tero@458: """ tero@458: tero@458: import ipaddr tero@458: import logging; log = logging.getLogger('pvl.hosts.zone') tero@458: import pvl.dns tero@458: tero@458: class HostZoneError(Exception): tero@458: pass tero@458: tero@458: def resolve (origin, domain, name) : tero@458: """ tero@458: Resolve relative CNAME for label@origin -> alias@domain tero@458: """ tero@458: tero@462: if origin: tero@462: origin = pvl.dns.fqdn(origin) tero@462: tero@462: if domain: tero@462: fqdn = pvl.dns.fqdn(name, domain) tero@462: else: tero@462: fqdn = pvl.dns.fqdn(name) tero@458: tero@458: if not origin: tero@458: return fqdn tero@458: tero@458: elif domain == origin: tero@458: return name tero@458: tero@458: elif fqdn.endswith('.' + origin): tero@458: return pvl.dns.relative(origin, fqdn) tero@458: tero@458: elif domain: tero@458: raise HostZoneError("{name}: domain {domain} out of zone {origin}".format(name=name, domain=domain, origin=origin)) tero@458: tero@458: else: tero@458: raise HostZoneError("{name}: fqdn {fqdn} out of zone {origin}".format(name=name, fqdn=fqdn, origin=origin)) tero@458: tero@471: # TODO: generate location alias CNAMEs even if host itself is outside origin? tero@458: def host_forward (host, origin) : tero@458: """ tero@458: Yield ZoneRecords for hosts within the given zone origin tero@458: """ tero@458: tero@458: try: tero@458: label = resolve(origin, host.domain, host.name) tero@458: except HostZoneError as error: tero@458: log.info("%s: skip: %s", host, error) tero@458: return tero@458: tero@458: if host.forward: tero@463: forward = pvl.dns.fqdn(host.forward) tero@458: tero@458: log.info("%s: forward: %s", host, forward) tero@458: tero@458: yield pvl.dns.ZoneRecord.CNAME(label, forward) tero@467: tero@467: elif host.forward is None: tero@467: # forward tero@467: if host.ip : tero@467: log.info("%s: forward %s[%s]: A %s", host, origin, label, host.ip) tero@458: tero@467: yield pvl.dns.ZoneRecord.A(label, host.ip) tero@467: tero@467: if host.ip6 : tero@467: log.info("%s: forward %s[%s]: AAAA %s", host, origin, label, host.ip6) tero@467: tero@467: yield pvl.dns.ZoneRecord.AAAA(label, host.ip6) tero@467: tero@467: else: tero@458: log.info("%s: skip forward", host) tero@458: return tero@458: tero@458: if host.location: tero@458: location_alias, location_domain = host.location tero@471: tero@458: yield pvl.dns.ZoneRecord.CNAME(resolve(origin, location_domain, location_alias), label) tero@458: tero@466: for alias in host.alias: tero@458: yield pvl.dns.ZoneRecord.CNAME(resolve(origin, host.domain, alias), label) tero@458: tero@458: for alias in host.alias4: tero@468: if not host.ip: tero@468: raise HostZoneError(host, "alias4={host.alias4} without ip=".format(host=host)) tero@468: tero@458: yield pvl.dns.ZoneRecord.A(resolve(origin, host.domain, alias), host.ip) tero@458: tero@458: for alias in host.alias6: tero@468: if not host.ip6: tero@468: raise HostZoneError(host, "alias6={host.alias6} without ip6=".format(host=host)) tero@468: tero@468: yield pvl.dns.ZoneRecord.AAAA(resolve(origin, host.domain, alias), host.ip6) tero@458: tero@458: def host_reverse (host, prefix) : tero@458: """ tero@464: Yield (ipaddr.IPAddress, ZoneRecord) tuples for host within given prefix's reverse-dns zone. tero@458: """ tero@458: tero@458: if prefix.version == 4 : tero@458: ip = host.ip tero@464: tero@464: # reverse= is IPv4-only tero@464: reverse = host.reverse tero@464: tero@458: elif prefix.version == 6 : tero@458: ip = host.ip6 tero@464: tero@464: # if reverse= is set, always omit, for lack of reverse6= tero@464: reverse = None if host.reverse is None else False tero@464: tero@458: else : tero@458: raise ValueError("%s: unknown ip version: %s" % (prefix, prefix.version)) tero@458: tero@458: if not ip : tero@458: log.debug("%s: no ip%d", host, prefix.version) tero@458: return tero@458: tero@458: if ip not in prefix : tero@458: log.debug("%s: %s out of prefix: %s", host, ip, prefix) tero@458: return tero@458: tero@458: # relative label tero@458: label = pvl.dns.reverse_label(prefix, ip) tero@458: tero@464: if reverse: tero@464: alias = pvl.dns.fqdn(reverse) tero@458: tero@458: log.info("%s %s[%s]: CNAME %s", host, prefix, ip, alias) tero@458: tero@458: yield ip, pvl.dns.zone.ZoneRecord.CNAME(label, alias) tero@458: tero@464: elif reverse is None : tero@458: fqdn = host.fqdn() tero@458: tero@458: log.info("%s %s[%s]: PTR %s", host, prefix, ip, fqdn) tero@458: tero@458: yield ip, pvl.dns.zone.ZoneRecord.PTR(label, fqdn) tero@458: tero@464: else: tero@458: log.info("%s %s[%s]: omit", host, prefix, ip) tero@458: tero@458: def apply_hosts_forward (options, hosts, origin) : tero@458: """ tero@458: Generate DNS ZoneRecords for for hosts within the given zone origin. tero@458: tero@472: Verifies that there are no overlapping name/type records, or CNAME records from aliases. tero@472: tero@458: Yields ZoneRecords in Host-order tero@458: """ tero@458: tero@458: if options.add_origin : tero@458: yield pvl.dns.ZoneDirective.build(None, 'ORIGIN', origin) tero@458: tero@458: by_name = dict() tero@472: by_name_type = dict() tero@458: tero@458: for host in hosts: tero@458: if not host.domain: tero@458: log.debug("%s: skip without domain", host) tero@458: tero@458: for rr in host_forward(host, origin) : tero@472: if (rr.name, 'CNAME') in by_name_type: tero@472: raise HostZoneError(host, "{host}: CNAME {cname} conflict: {rr}".format(host=host, cname=by_name_type[rr.name, 'CNAME'].name, rr=rr)) tero@458: elif rr.type == 'CNAME' and rr.name in by_name: tero@458: raise HostZoneError(host, "{host}: CNAME {cname} conflict: {rr}".format(host=host, cname=rr.name, rr=by_name[rr.name])) tero@472: elif (rr.name, rr.type) in by_name_type: tero@472: raise HostZoneError(host, "{host}: {type} {name} conflict: {rr}".format(host=host, type=rr.type, name=rr.name, rr=by_name_type[rr.name, rr.type])) tero@458: tero@458: by_name[rr.name] = rr tero@472: by_name_type[rr.name, rr.type] = rr tero@458: tero@458: # preserve ordering tero@458: yield rr tero@458: tero@458: def apply_hosts_reverse (options, hosts, prefix) : tero@458: """ tero@458: Generate DNS ZoneRecords within the given prefix's reverse-dns zone for hosts. tero@458: tero@458: Yields ZoneRecords in IPAddress-order tero@458: """ tero@474: tero@474: if options.unknown_host and not options.hosts_domain: tero@474: raise Exception("--unknown-host requires --hosts-domain") tero@458: tero@458: # collect data for records tero@458: by_ip = dict() tero@458: tero@458: for host in hosts: tero@458: for ip, rr in host_reverse(host, prefix) : tero@458: if ip in by_ip : tero@458: raise HostZoneError(host, "{host}: IP {ip} conflict: {other}".format(host=host, ip=ip, other=by_ip[ip])) tero@458: tero@458: # do not retain order tero@458: by_ip[ip] = rr tero@458: tero@458: if options.unknown_host : tero@458: # enumerate all of them tero@458: iter_ips = prefix.iterhosts() tero@458: else : tero@458: iter_ips = sorted(by_ip) tero@458: tero@458: for ip in iter_ips : tero@458: if ip in by_ip : tero@458: yield by_ip[ip] tero@458: tero@458: elif options.unknown_host: tero@458: # synthesize a record tero@458: label = pvl.dns.reverse_label(prefix, ip) tero@474: fqdn = pvl.dns.fqdn(options.unknown_host, options.hosts_domain) tero@458: tero@458: log.info("%s %s[%s]: unused PTR %s", options.unknown_host, ip, prefix, fqdn) tero@458: tero@474: yield pvl.dns.ZoneRecord.PTR(label, fqdn) tero@458: tero@474: else: tero@458: continue tero@458: