terom@233: #!/usr/bin/env python terom@233: terom@233: """ terom@247: Process bind zonefiles. terom@233: """ terom@233: terom@233: import codecs terom@247: import optparse terom@233: terom@247: import pvl.args terom@247: import pvl.dns.zone terom@247: from pvl.dns import __version__ terom@247: from pvl.dns.zone import ZoneRecord, reverse_ipv4, reverse_ipv6, fqdn terom@233: terom@247: import logging; log = logging.getLogger('main') terom@233: terom@233: terom@233: def parse_options (argv) : terom@233: """ terom@233: Parse command-line arguments. terom@233: """ terom@233: terom@233: prog = argv[0] terom@233: terom@233: parser = optparse.OptionParser( terom@233: prog = prog, terom@233: usage = '%prog: [options]', terom@233: version = __version__, terom@233: terom@233: # module docstring terom@233: description = __doc__, terom@233: ) terom@233: terom@233: # logging terom@247: parser.add_option_group(pvl.args.parser(parser)) terom@233: terom@233: # input/output terom@233: parser.add_option('-c', '--input-charset', metavar='CHARSET', default='utf-8', terom@233: help="Encoding used for input files") terom@233: terom@233: parser.add_option('-o', '--output', metavar='FILE', default='-', terom@233: help="Write to output file; default stdout") terom@233: terom@233: parser.add_option('--output-charset', metavar='CHARSET', default='utf-8', terom@233: help="Encoding used for output files") terom@233: terom@233: # check stage terom@233: parser.add_option('--check-hosts', action='store_true', terom@233: help="Check that host/IPs are unique. Use --quiet to silence warnings, and test exit status") terom@233: terom@233: parser.add_option('--check-exempt', metavar='HOST', action='append', terom@233: help="Allow given names to have multiple records") terom@233: terom@233: # meta stage terom@233: parser.add_option('--meta-zone', action='store_true', terom@233: help="Generate host metadata zone; requires --input-line-date") terom@233: terom@233: parser.add_option('--meta-ignore', metavar='HOST', action='append', terom@233: help="Ignore given hostnames in metadata output") terom@233: terom@233: parser.add_option('--input-line-date', action='store_true', terom@233: help="Parse timestamp prefix from each input line (e.g. `hg blame | ...`)") terom@233: terom@233: # forward stage terom@233: parser.add_option('--forward-zone', action='store_true', terom@233: help="Generate forward zone") terom@233: terom@233: parser.add_option('--forward-txt', action='store_true', terom@233: help="Generate TXT records for forward zone") terom@233: terom@233: parser.add_option('--forward-mx', metavar='MX', terom@233: help="Generate MX records for forward zone") terom@233: terom@233: # reverse stage terom@233: parser.add_option('--reverse-domain', metavar='DOMAIN', terom@233: help="Domain to use for hosts in reverse zone") terom@233: terom@233: parser.add_option('--reverse-zone', metavar='NET', terom@233: help="Generate forward zone for given subnet (x.z.y | a:b:c:d)") terom@233: terom@252: # other terom@252: parser.add_option('--serial', metavar='YYMMDDXX', terom@252: help="Set serial for SOA record") terom@252: terom@233: # defaults terom@233: parser.set_defaults( terom@233: # XXX: combine terom@233: check_exempt = [], terom@233: meta_ignore = [], terom@233: ) terom@233: terom@233: # parse terom@233: options, args = parser.parse_args(argv[1:]) terom@233: terom@247: # apply terom@247: pvl.args.apply(options, prog) terom@233: terom@233: return options, args terom@233: terom@233: def check_zone_hosts (zone, whitelist=None, whitelist_types=set(['TXT'])) : terom@233: """ terom@233: Parse host/IP pairs from the zone, and verify that they are unique. terom@233: terom@233: As an exception, names listed in the given whitelist may have multiple IPs. terom@233: """ terom@233: terom@233: by_name = {} terom@233: by_ip = {} terom@233: terom@233: fail = None terom@233: terom@233: last_name = None terom@233: terom@233: for r in zone : terom@233: name = r.name or last_name terom@233: terom@233: name = (r.origin, name) terom@233: terom@233: # name terom@233: if r.type not in whitelist_types : terom@233: if name not in by_name : terom@233: by_name[name] = r terom@233: terom@233: elif r.name in whitelist : terom@233: log.debug("Duplicate whitelist entry: %s", r) terom@233: terom@233: else : terom@233: # fail! terom@233: log.warn("%s: Duplicate name: %s <-> %s", r.line, r, by_name[name]) terom@233: fail = True terom@233: terom@233: # ip terom@233: if r.type == 'A' : terom@233: ip, = r.data terom@233: terom@233: if ip not in by_ip : terom@233: by_ip[ip] = r terom@233: terom@233: else : terom@233: # fail! terom@233: log.warn("%s: Duplicate IP: %s <-> %s", r.line, r, by_ip[ip]) terom@233: fail = True terom@233: terom@233: return fail terom@233: terom@252: def process_zone_soa (soa, serial) : terom@252: return pvl.dns.zone.SOA( terom@252: soa.master, soa.contact, terom@252: serial, soa.refresh, soa.retry, soa.expire, soa.nxttl terom@252: ) terom@252: terom@252: def process_zone_serial (zone, serial) : terom@252: for rr in zone : terom@252: if rr.type == 'SOA' : terom@252: # XXX: as SOA record.. terom@252: yield process_zone_soa(pvl.dns.zone.SOA.parse(rr.line), serial) terom@252: else : terom@252: yield rr terom@252: terom@233: def process_zone_forwards (zone, txt=False, mx=False) : terom@233: """ terom@233: Process zone data -> forward zone data. terom@233: """ terom@233: terom@233: for r in zone : terom@233: yield r terom@233: terom@233: if r.type == 'A' : terom@233: if txt : terom@233: # comment? terom@233: comment = r.line.comment terom@233: terom@233: if comment : terom@247: yield ZoneRecord.TXT(None, comment, ttl=r.ttl) terom@233: terom@233: terom@233: # XXX: RP, do we need it? terom@233: terom@233: if mx : terom@247: # XXX: is this even a good idea? terom@247: yield ZoneRecord.MX(None, 10, mx, ttl=r.ttl) terom@233: terom@233: def process_zone_meta (zone, ignore=None) : terom@233: """ terom@233: Process zone metadata -> output. terom@233: """ terom@233: terom@247: TIMESTAMP_FORMAT = '%Y/%m/%d' terom@233: terom@233: for r in zone : terom@233: if ignore and r.name in ignore : terom@233: # skip terom@233: log.debug("Ignore record: %s", r) terom@233: continue terom@233: terom@233: # for hosts.. terom@233: if r.type == 'A' : terom@233: # timestamp? terom@233: timestamp = r.line.timestamp terom@233: terom@233: if timestamp : terom@247: yield ZoneRecord.TXT(r.name, timestamp.strftime(TIMESTAMP_FORMAT), ttl=r.ttl) terom@233: terom@233: def process_zone_reverse (zone, origin, domain) : terom@233: """ terom@233: Process zone data -> reverse zone data. terom@233: """ terom@233: terom@233: name = None terom@233: terom@233: for r in zone : terom@233: # keep name from previous.. terom@233: if r.name : terom@233: name = r.name terom@233: terom@233: if r.type == 'A' : terom@233: ip, = r.data terom@233: ptr = reverse_ipv4(ip) terom@233: terom@233: elif r.type == 'AAAA' : terom@233: ip, = r.data terom@233: ptr = reverse_ipv6(ip) terom@233: terom@233: else : terom@233: continue terom@233: terom@233: # verify terom@233: if zone and ptr.endswith(origin) : terom@233: ptr = ptr[:-(len(origin) + 1)] terom@233: terom@233: else : terom@233: log.warning("Reverse does not match zone origin, skipping: (%s) -> %s <-> %s", ip, ptr, origin) terom@233: continue terom@233: terom@233: # domain to use terom@233: host_domain = r.origin or domain terom@233: host_fqdn = fqdn(name, host_domain) terom@233: terom@247: yield ZoneRecord.PTR(ptr, host_fqdn) terom@233: terom@233: def write_zone_records (file, zone) : terom@233: for r in zone : terom@247: file.write(unicode(r)) terom@247: file.write('\n') terom@233: terom@233: def open_file (path, mode, charset) : terom@233: """ terom@233: Open unicode-enabled file from path, with - using stdio. terom@233: """ terom@233: terom@233: if path == '-' : terom@233: # use stdin/out based on mode terom@233: stream, func = { terom@233: 'r': (sys.stdin, codecs.getreader), terom@233: 'w': (sys.stdout, codecs.getwriter), terom@233: }[mode[0]] terom@233: terom@233: # wrap terom@233: return func(charset)(stream) terom@233: terom@233: else : terom@233: # open terom@233: return codecs.open(path, mode, charset) terom@233: terom@233: def main (argv) : terom@233: options, args = parse_options(argv) terom@233: terom@233: if args : terom@233: # open files terom@233: input_files = [open_file(path, 'r', options.input_charset) for path in args] terom@233: terom@233: else : terom@233: # default to stdout terom@233: input_files = [open_file('-', 'r', options.input_charset)] terom@233: terom@233: # process zone data terom@233: zone = [] terom@233: terom@233: for file in input_files : terom@233: log.info("Reading zone: %s", file) terom@233: terom@250: zone += list(pvl.dns.zone.ZoneRecord.load(file, terom@233: line_timestamp_prefix = options.input_line_date, terom@233: )) terom@233: terom@233: # check? terom@233: if options.check_hosts : terom@233: whitelist = set(options.check_exempt) terom@233: terom@233: log.debug("checking hosts; whitelist=%r", whitelist) terom@233: terom@233: if check_zone_hosts(zone, whitelist=whitelist) : terom@233: log.warn("Hosts check failed") terom@233: return 2 terom@233: terom@233: else : terom@233: log.info("Hosts check OK") terom@233: terom@252: if options.serial : terom@252: log.info("Set zone serial: %s", options.serial) terom@252: terom@252: zone = list(process_zone_serial(zone, serial=options.serial)) terom@252: terom@233: # output file terom@233: output = open_file(options.output, 'w', options.output_charset) terom@233: terom@233: if options.forward_zone : terom@233: log.info("Write forward zone: %s", output) terom@233: terom@233: zone = list(process_zone_forwards(zone, txt=options.forward_txt, mx=options.forward_mx)) terom@233: terom@233: elif options.meta_zone : terom@233: log.info("Write metadata zone: %s", output) terom@233: terom@233: if not options.input_line_date : terom@233: log.error("--meta-zone requires --input-line-date") terom@233: return 1 terom@233: terom@233: zone = list(process_zone_meta(zone, ignore=set(options.meta_ignore))) terom@233: terom@233: elif options.reverse_zone : terom@233: if ':' in options.reverse_zone : terom@233: # IPv6 terom@233: origin = reverse_ipv6(options.reverse_zone) terom@233: terom@233: else : terom@233: # IPv4 terom@233: origin = reverse_ipv4(options.reverse_zone) terom@233: terom@233: domain = options.reverse_domain terom@233: terom@233: if not domain : terom@233: log.error("--reverse-zone requires --reverse-domain") terom@233: return 1 terom@233: terom@233: zone = list(process_zone_reverse(zone, origin=origin, domain=domain)) terom@233: terom@233: elif options.check_hosts : terom@233: # we only did that, done terom@233: return 0 terom@233: terom@233: else : terom@249: # pass-through terom@251: log.info("Passing through zonefile") terom@233: terom@233: write_zone_records(output, zone) terom@233: terom@233: return 0 terom@233: terom@233: if __name__ == '__main__': terom@233: import sys terom@233: sys.exit(main(sys.argv))