bin/pvl.hosts-dns
changeset 458 600ad9eb6f25
parent 457 1e925a1cc8de
child 459 927b9caf3be6
equal deleted inserted replaced
457:1e925a1cc8de 458:600ad9eb6f25
     1 #!/usr/bin/env python
       
     2 
       
     3 import pvl.args
       
     4 import pvl.hosts
       
     5 import pvl.dns.zone
       
     6 
       
     7 import ipaddr
       
     8 import logging; log = logging.getLogger('pvl.hosts-dns')
       
     9 import optparse 
       
    10 
       
    11 def process_hosts_alias (options, origin, host_domain, alias, host) :
       
    12     """
       
    13         Resolve alias@domain within given zone.
       
    14     """
       
    15 
       
    16     if host_domain :
       
    17         alias = pvl.dns.join(alias, host_domain)
       
    18     else :
       
    19         raise ValueError("no domain given for host %s alias %s" % (host, alias, ))
       
    20 
       
    21     if alias.endswith('.' + origin) :
       
    22         # strip
       
    23         alias = alias[:(len(alias) - len(origin) - 1)]
       
    24     else:
       
    25         raise ValueError("alias domain outside of origin: %s / %s" % (alias, origin))
       
    26 
       
    27     return pvl.dns.zone.ZoneRecord.CNAME(alias, host)
       
    28 
       
    29 def process_hosts_names (options, hosts, origin) :
       
    30     """
       
    31         Yield ZoneRecords for hosts within the given zone.
       
    32     """
       
    33 
       
    34     for host in hosts :
       
    35         # determine label within zone
       
    36         if not origin :
       
    37             label = pvl.dns.join(host.name, host.domain)
       
    38         elif host.domain == origin :
       
    39             label = host.name
       
    40         elif host.domain and host.domain.endswith('.' + origin) :
       
    41             fqdn = pvl.dns.join(host.name, host.domain)
       
    42             label = fqdn[:(len(fqdn) - len(origin) - 1)]
       
    43         elif host.domain :
       
    44             log.debug("%s: domain out of zone: %s", host, origin)
       
    45             continue
       
    46         else :
       
    47             log.debug("%s: fqdn out of zone: %s", host, origin)
       
    48             continue
       
    49         
       
    50         if host.forward is None  :
       
    51             pass
       
    52         elif host.forward :
       
    53             forward = pvl.dns.zone.fqdn(host.forward)
       
    54 
       
    55             log.info("%s: forward: %s", host, forward)
       
    56 
       
    57             yield pvl.dns.zone.ZoneRecord.CNAME(label, forward)
       
    58             continue
       
    59         else :
       
    60             log.info("%s: skip forward", host)
       
    61             continue
       
    62 
       
    63         if host.ip :
       
    64             yield pvl.dns.zone.ZoneRecord.A(label, host.ip)
       
    65         
       
    66         if host.alias4 :
       
    67             yield pvl.dns.zone.ZoneRecord.A(host.ALIAS4_FMT.format(host=label), host.ip)
       
    68 
       
    69         if host.ip6 :
       
    70             yield pvl.dns.zone.ZoneRecord.AAAA(label, host.ip6)
       
    71 
       
    72         if host.alias6 :
       
    73             yield pvl.dns.zone.ZoneRecord.AAAA(label.ALIAS6_FMT.format(host=host), host.ip6)
       
    74 
       
    75         if host.location and host.location_domain:
       
    76             yield process_hosts_alias(options, origin, host.location_domain, host.location, label)
       
    77         elif host.location:
       
    78             yield process_hosts_alias(options, origin, host.domain, host.location, label)
       
    79 
       
    80         for alias in host.alias :
       
    81             yield process_hosts_alias(options, origin, host.domain, alias, label)
       
    82 
       
    83         for alias4 in host.alias4 :
       
    84             yield process_hosts_alias(options, origin, host.domain, alias4, host.ALIAS4_FMT.format(host=label))
       
    85 
       
    86         for alias6 in host.alias6 :
       
    87             yield process_hosts_alias(options, origin, host.domain, alias6, host.ALIAS6_FMT.format(host=label))
       
    88 
       
    89 def process_hosts_forward (options, hosts, origin) :
       
    90     """
       
    91         Generate DNS ZoneRecords for for hosts within the given zone origin.
       
    92     """
       
    93 
       
    94     if options.add_origin :
       
    95         yield pvl.dns.zone.ZoneDirective.build(None, 'ORIGIN', origin)
       
    96 
       
    97     by_name = dict()
       
    98     by_name_type = dict()
       
    99     
       
   100     # list of types thare are allowed to be present for a host
       
   101     MULTI_TYPES = ('A', 'AAAA')
       
   102 
       
   103     for rr in process_hosts_names(options, hosts, origin) :
       
   104         if (rr.name, rr.type) in by_name_type :
       
   105             raise ValueError("%s: duplicate name/type: %s: %s" % (rr.name, rr, by_name_type[(rr.name, rr.type)]))
       
   106         elif rr.type in MULTI_TYPES :
       
   107             by_name_type[(rr.name, rr.type)] = rr
       
   108         elif rr.name in by_name :
       
   109             raise ValueError("%s: duplicate name: %s: %s" % (rr.name, rr, by_name[rr.name]))
       
   110         
       
   111         # always check these
       
   112         by_name[rr.name] = rr
       
   113         
       
   114         # preserve ordering
       
   115         yield rr
       
   116 
       
   117 def split_ipv6_parts (prefix) :
       
   118     for hextet in prefix.rstrip(':').split(':') :
       
   119         for nibble in hextet.rjust(4, '0') :
       
   120             yield nibble
       
   121 
       
   122 def build_ipv6_parts (parts) :
       
   123     for i in xrange(0, len(parts), 4) :
       
   124         yield ''.join(parts[i:i+4])
       
   125     
       
   126     # suffix ::
       
   127     if len(parts) < 32 :
       
   128         yield ''
       
   129         yield ''
       
   130 
       
   131 def parse_prefix (prefix) :
       
   132     """
       
   133         Return an ipaddr.IPNetwork from given IPv4/IPv6 prefix.
       
   134 
       
   135         >>> parse_prefix('127.0.0.0/8')
       
   136         IPv4Network('127.0.0.0/8')
       
   137         >>> parse_prefix('192.0.2.128/26')
       
   138         IPv4Network('192.0.2.128/26')
       
   139         >>> parse_prefix('192.0.2.128-26')
       
   140         IPv4Network('192.0.2.128-26')
       
   141         >>> parse_prefix('127.')
       
   142         IPv4Network('127.0.0.0/8')
       
   143         >>> parse_prefix('10')
       
   144         IPv4Network('10.0.0.0/8')
       
   145         >>> parse_prefix('192.168')
       
   146         IPv4Network('192.168.0.0/16')
       
   147         >>> parse_prefix('fe80:')
       
   148         IPv6Network('fe80::/16')
       
   149         >>> parse_prefix('2001:db8::')
       
   150         IPv6Network('2001:db8::/32')
       
   151         >>> parse_prefix('2001:db8:1:2')
       
   152         IPv6Network('2001:db8:1:2::/64')
       
   153     """
       
   154 
       
   155     if '/' in prefix :
       
   156         return ipaddr.IPNetwork(prefix)
       
   157     
       
   158     elif '-' in prefix :
       
   159         return ipaddr.IPNetwork(prefix.replace('-', '/'))
       
   160 
       
   161     elif '.' in prefix or prefix.isdigit() :
       
   162         parts = prefix.rstrip('.').split('.')
       
   163         prefixlen = len(parts) * 8
       
   164         
       
   165         return ipaddr.IPv4Network('{prefix}/{prefixlen}'.format(
       
   166             prefix      = '.'.join(parts + ['0' for i in xrange(4 - len(parts))]),
       
   167             prefixlen   = prefixlen,
       
   168         ))
       
   169    
       
   170     elif ':' in prefix :
       
   171         parts = list(split_ipv6_parts(prefix))
       
   172         prefixlen = len(parts) * 4
       
   173 
       
   174         return ipaddr.IPv6Network('{prefix}/{prefixlen}'.format(
       
   175             prefix      = ':'.join(build_ipv6_parts(parts)),
       
   176             prefixlen   = prefixlen,
       
   177         ))
       
   178 
       
   179     else :
       
   180         raise ValueError("Unrecognized IP prefix string: %s" % (prefix, ))
       
   181 
       
   182 def process_hosts_ips (options, hosts, prefix) :
       
   183     """
       
   184         Yield (ip, fqnd) for hosts within given prefix.
       
   185     """
       
   186 
       
   187     for host in hosts :
       
   188         if prefix.version == 4 :
       
   189             ip = host.ip
       
   190         elif prefix.version == 6 :
       
   191             ip = host.ip6
       
   192         else :
       
   193             raise ValueError("%s: unknown ip version: %s" % (prefix, prefix.version))
       
   194 
       
   195         if not ip :
       
   196             log.debug("%s: no ip%d", host, prefix.version)
       
   197             continue
       
   198 
       
   199         if ip not in prefix :
       
   200             log.debug("%s: %s out of prefix: %s", host, ip, prefix)
       
   201             continue
       
   202         
       
   203         label = pvl.dns.zone.reverse_label(prefix, ip)
       
   204        
       
   205         if host.reverse is None :
       
   206             fqdn = host.fqdn()
       
   207 
       
   208             log.info("%s %s[%s]: PTR %s", host, prefix, ip, fqdn)
       
   209 
       
   210             yield host, ip, pvl.dns.zone.ZoneRecord.PTR(label, fqdn)
       
   211 
       
   212         elif host.reverse :
       
   213             alias = pvl.dns.zone.fqdn(host.reverse)
       
   214             
       
   215             log.info("%s %s[%s]: CNAME %s", host, prefix, ip, alias)
       
   216 
       
   217             yield host, ip, pvl.dns.zone.ZoneRecord.CNAME(label, alias)
       
   218 
       
   219         else :
       
   220             log.info("%s %s[%s]: omit", host, prefix, ip)
       
   221             continue
       
   222 
       
   223  
       
   224 def process_hosts_reverse (options, hosts, prefix) :
       
   225     """
       
   226         Generate DNS ZoneRecords within the given prefix's reverse-dns zone for hosts.
       
   227     """
       
   228     
       
   229     # collect data for records
       
   230     by_ip = dict()
       
   231     for host, ip, rr in process_hosts_ips(options, hosts, prefix) :
       
   232         if ip in by_ip :
       
   233             raise ValueError("%s: duplicate ip: %s: %s" % (host, ip, by_ip[ip]))
       
   234         else :
       
   235             by_ip[ip] = rr
       
   236 
       
   237     if options.unknown_host :
       
   238         # enumerate all of them
       
   239         iter_ips = prefix.iterhosts()
       
   240     else :
       
   241         iter_ips = sorted(by_ip)
       
   242 
       
   243     for ip in iter_ips :
       
   244         if ip in by_ip :
       
   245             yield by_ip[ip]
       
   246         elif options.unknown_host :
       
   247             label = pvl.dns.zone.reverse_label(prefix, ip)
       
   248             fqdn = pvl.dns.zone.fqdn(options.unknown_host, options.hosts_domain)
       
   249 
       
   250             log.info("%s %s[%s]: unused PTR %s", options.unknown_host, ip, prefix, fqdn)
       
   251 
       
   252             yield pvl.dns.zone.ZoneRecord.PTR(label, fqdn)
       
   253         else :
       
   254             continue
       
   255 
       
   256 def apply_zone (options, zone) :
       
   257     """
       
   258         Output given ZoneRecord's
       
   259     """
       
   260 
       
   261     for record in zone :
       
   262         print unicode(record)
       
   263 
       
   264 def main (argv) :
       
   265     """
       
   266         Generate bind zonefiles from host definitions.
       
   267     """
       
   268 
       
   269     parser = optparse.OptionParser(main.__doc__)
       
   270     parser.add_option_group(pvl.args.parser(parser))
       
   271     parser.add_option_group(pvl.hosts.optparser(parser))
       
   272 
       
   273     parser.add_option('--add-origin',           action='store_true',
       
   274             help="Include $ORIGIN directive in zone")
       
   275 
       
   276     parser.add_option('--forward-zone',         metavar='DOMAIN',
       
   277             help="Generate forward zone for domain")
       
   278 
       
   279     parser.add_option('--reverse-zone',         metavar='PREFIX',
       
   280             help="Generate reverse zone for prefix")
       
   281 
       
   282     parser.add_option('--unknown-host',         metavar='NAME',
       
   283             help="Generate records for unused IPs")
       
   284 
       
   285     options, args = parser.parse_args(argv[1:])
       
   286     pvl.args.apply(options)
       
   287 
       
   288     # input
       
   289     hosts = pvl.hosts.apply(options, args)
       
   290 
       
   291     # process
       
   292     if options.forward_zone :
       
   293         apply_zone(options, 
       
   294                 process_hosts_forward(options, hosts, options.forward_zone),
       
   295         )
       
   296     
       
   297     if options.reverse_zone :
       
   298         apply_zone(options, 
       
   299                 process_hosts_reverse(options, hosts, parse_prefix(options.reverse_zone)),
       
   300         )
       
   301 
       
   302 if __name__ == '__main__':
       
   303     pvl.args.main(main)