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@487: import pvl.hosts.host tero@458: tero@487: class HostZoneError(pvl.hosts.host.HostError): tero@487: pass tero@458: tero@471: # TODO: generate location alias CNAMEs even if host itself is outside origin? tero@496: 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@487: label = pvl.dns.relative(origin, host.domain, host.name) tero@487: except ValueError 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 terom@733: for sublabel, (ip4, ip6) in host.ip.iteritems(): terom@733: if sublabel: terom@733: sublabel = pvl.dns.join(sublabel, label) terom@733: else: terom@733: sublabel = label terom@733: terom@733: if ip4: terom@733: log.info("%s: ip: %s@%s A %s", host, sublabel, origin, ip4) tero@458: terom@733: yield pvl.dns.ZoneRecord.A(sublabel, ip4) tero@467: terom@733: if ip6: terom@733: log.info("%s: ip6: %s@%s AAAA %s", host, label, origin, ip6) tero@467: terom@733: yield pvl.dns.ZoneRecord.AAAA(sublabel, 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@487: try: tero@487: yield pvl.dns.ZoneRecord.CNAME(pvl.dns.relative(origin, location_domain, location_alias), label) tero@487: except ValueError as error: tero@487: raise HostZoneError(host, error) tero@458: tero@466: for alias in host.alias: tero@495: yield pvl.dns.ZoneRecord.CNAME(pvl.dns.relative(origin, host.domain, alias), label) tero@458: tero@458: for alias in host.alias4: terom@733: if not host.ip4: terom@733: raise HostZoneError(host, "alias4={host.alias4} without ip4=".format(host=host)) tero@468: terom@733: yield pvl.dns.ZoneRecord.A(pvl.dns.relative(origin, host.domain, alias), host.ip4) 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@495: yield pvl.dns.ZoneRecord.AAAA(pvl.dns.relative(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: """ terom@733: terom@733: for sublabel, (ip4, ip6) in host.ip.iteritems(): terom@733: if prefix.version == 4: terom@733: ip = ip4 terom@733: terom@733: # reverse= is IPv4-only terom@733: reverse = host.reverse tero@458: terom@733: elif prefix.version == 6: terom@733: ip = ip6 terom@733: terom@733: # if reverse= is set, always omit, for lack of reverse6= terom@733: reverse = None if host.reverse is None else False tero@458: terom@733: else: terom@733: raise ValueError("%s: unknown ip version: %s" % (prefix, prefix.version)) tero@458: terom@733: if not ip: terom@733: log.debug("%s: no ip%d", host, prefix.version) terom@733: continue tero@458: terom@733: if ip not in prefix: terom@733: log.debug("%s: %s out of prefix: %s", host, ip, prefix) terom@733: continue terom@733: terom@733: # relative label terom@733: label = pvl.dns.reverse_label(prefix, ip) terom@733: terom@733: if reverse: terom@733: alias = pvl.dns.fqdn(reverse) terom@733: terom@733: log.info("%s %s[%s]: CNAME %s", host, prefix, ip, alias) tero@458: terom@733: yield ip, pvl.dns.zone.ZoneRecord.CNAME(label, alias) terom@733: terom@733: elif reverse is None: terom@733: fqdn = host.fqdn() terom@733: terom@733: if sublabel: terom@733: fqdn = pvl.dns.join(sublabel, fqdn) terom@733: terom@733: log.info("%s %s[%s]: PTR %s", host, prefix, ip, fqdn) terom@733: terom@733: yield ip, pvl.dns.zone.ZoneRecord.PTR(label, fqdn) terom@733: terom@733: else: terom@733: log.info("%s %s[%s]: omit", host, prefix, ip) terom@733: tero@489: def apply_hosts_forward (hosts, origin, tero@687: add_origin = False, tero@687: check_conflicts = False, tero@489: ) : 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@489: hosts: [Host] - Host's to render tero@489: origin: str - generate records relative to given zone origin tero@489: add_origin: bool - generate an explicit $ORIGIN directive tero@687: check_conflits: bool- raise HostZoneError on dupliate records for the same name/type tero@687: overlapping CNAME records will always raise tero@489: tero@458: Yields ZoneRecords in Host-order tero@458: """ tero@458: tero@489: if add_origin: terom@658: yield pvl.dns.ZoneDirective.build('ORIGIN', pvl.dns.fqdn(origin)) tero@458: tero@458: by_name = dict() tero@472: by_name_type = dict() tero@458: tero@458: for host in hosts: tero@458: for rr in host_forward(host, origin) : tero@472: if (rr.name, 'CNAME') in by_name_type: tero@686: raise HostZoneError(host, u"{cname} CNAME conflict with {other}".format(cname=rr.name, other=by_name_type[rr.name, 'CNAME'])) tero@458: elif rr.type == 'CNAME' and rr.name in by_name: tero@686: raise HostZoneError(host, u"{cname} CNAME conflict with {other}".format(cname=rr.name, other=by_name[rr.name])) tero@687: elif check_conflicts and (rr.name, rr.type) in by_name_type: tero@686: raise HostZoneError(host, u"{name} {type} conflict with {other}".format(type=rr.type, name=rr.name, other=by_name_type[rr.name, rr.type])) tero@458: tero@686: by_name[rr.name] = host tero@686: by_name_type[rr.name, rr.type] = host tero@458: tero@458: # preserve ordering tero@458: yield rr tero@458: tero@489: def apply_hosts_reverse (hosts, prefix, tero@489: unknown_host = None, tero@489: unknown_domain = None, tero@489: ) : tero@458: """ tero@458: Generate DNS ZoneRecords within the given prefix's reverse-dns zone for hosts. tero@458: tero@489: hosts: [Host] - Host's to render PTRs for tero@489: prefix: ipaddr.IPNetwork - IPv4/IPv6 prefix to render PTRs for within associated in-addr.arpa/ip6.arpa zone tero@489: unknown_host: str - render a PTR to the given host @unknown_domain for unassigned IPs within prefix tero@489: unknown_domain: str - required if unknown_host is given tero@489: tero@458: Yields ZoneRecords in IPAddress-order tero@458: """ tero@474: tero@489: if unknown_host and not unknown_domain: tero@489: raise ValueError("unknown_host requires unknown_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@489: if 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@489: elif unknown_host: tero@458: # synthesize a record tero@458: label = pvl.dns.reverse_label(prefix, ip) tero@489: fqdn = pvl.dns.fqdn(unknown_host, unknown_domain) tero@458: tero@489: log.info("%s %s[%s]: unused PTR %s", 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: