#!/usr/bin/env python
"""
Manipulate host definitions for dns/dhcp.
"""
import pvl.args, optparse
import pvl.dns.zone
import pvl.dhcp.config
import collections
import re
import logging; log = logging.getLogger('main')
__version__ = '0.1'
def parse_options (argv) :
"""
Parse command-line arguments.
"""
parser = optparse.OptionParser(
prog = argv[0],
usage = '%prog: [options]',
version = __version__,
# module docstring
description = __doc__,
)
# logging
parser.add_option_group(pvl.args.parser(parser))
parser.add_option('-c', '--input-charset', metavar='CHARSET', default='utf-8',
help="Encoding used for input files")
parser.add_option('--output-charset', metavar='CHARSET', default='utf-8',
help="Encoding used for output files")
# input
parser.add_option('--import-zone-hosts', metavar='FILE',
help="Load hosts from DNS zone")
parser.add_option('--import-dhcp-hosts', metavar='FILE',
help="Load hosts from DHCP config")
# defaults
parser.add_option('--hosts-domain', metavar='DOMAIN',
help="Default domain for hosts")
parser.add_option('--zone-unused', metavar='HOST',
help="DNS name for unallocated hosts")
# output
parser.add_option('--output-hosts', metavar='FILE',
help="Output hosts file")
# defaults
parser.set_defaults(
)
# parse
options, args = parser.parse_args(argv[1:])
# apply
pvl.args.apply(options, argv[0])
return options, args
ZONE_COMMENTS = (
re.compile(r'(?P<owner>[^/]+)\s*-\s+(?P<host>.+)'),
re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)\s+[/-]\s+(?P<host>.+)'),
re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)\s+[(]\s*(?P<host>.+)[)]'),
re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)'),
re.compile(r'(?P<owner>.+)'),
)
ZONE_OWNER_MAIL = re.compile(r'(?P<owner>.*?)\s*<(?P<mail>.+?)>')
def process_zone_comment (options, hostname, comment) :
"""
Attempt to parse a host comment field... :D
"""
yield 'comment', comment
for regex in ZONE_COMMENTS :
match = regex.match(comment)
if match :
break
else :
log.warn("%s: unparsed comment: %s", hostname, comment)
return
matches = match.groupdict()
owner = matches.pop('owner', None)
if owner :
mail_match = ZONE_OWNER_MAIL.match(owner)
if mail_match :
mail_matches = mail_match.groupdict()
owner = mail_matches['owner']
yield 'comment-mail', mail_matches['mail']
else :
mail_matches = { }
else :
mail_matches = { }
yield 'comment-owner', owner
for group, value in matches.iteritems() :
if value :
yield 'comment-{group}'.format(group=group), value.strip()
print u"{hostname:20} {comment:80} = {group:15} / {owner:20} <{mail:20}> / {host}".format(
hostname = hostname,
comment = comment,
group = matches.get('group', ''),
owner = owner,
mail = mail_matches.get('mail', ''),
host = matches.get('host', ''),
).encode('utf-8')
def process_zone_hosts (options, file) :
"""
Yield host info from zonefile records.
"""
for rr in pvl.dns.zone.ZoneRecord.load(file) :
if options.zone_unused and rr.name == options.zone_unused :
log.debug("%s: skip %s", rr.name, rr)
continue
elif rr.type == 'A' :
ip, = rr.data
yield rr.name, 'ip', ip
if rr.comment :
for field, value in process_zone_comment(options, rr.name, rr.comment) :
yield rr.name, field, value
elif rr.type == 'CNAME' :
host, = rr.data
yield host, 'alias', rr.name
else :
log.warn("%s: unknown rr: %s", rr.name, rr)
def process_dhcp_host (options, host, items) :
"""
Yield host infos from a dhcp host ... { ... }
"""
hostname = None
ethernet = []
fixed_address = None
for item in items :
item, args = item[0], item[1:]
if item == 'hardware' :
_ethernet, ethernet = args
assert _ethernet == 'ethernet'
elif item == 'fixed-address' :
fixed_address, = args
elif item == 'option' :
option = args.pop(0)
if option == 'host-name' :
hostname, = args
else :
log.warn("host %s: ignore unknown option: %s", host, option)
else :
log.warn("host %s: ignore unknown item: %s", host, item)
# determine hostname
if hostname :
pass
elif fixed_address and not re.match(r'\d+\.\d+\.\d+.\d+', fixed_address) :
hostname, domain = fixed_address.split('.', 1)
elif '-' in host :
hostname, suffix = host.rsplit('-', 1)
else :
log.warn("%s: guess hostname: %s", host, host)
hostname = host
if hostname :
yield hostname, 'ethernet', ethernet
def process_dhcp_hosts (options, blocks) :
"""
Process hosts from a parsed block
"""
for block, items, blocks in blocks :
log.info("%s", block)
block, args = block[0], block[1:]
if block == 'group' :
for info in process_dhcp_hosts(options, blocks) :
yield info
elif block == 'host' :
host, = args
try :
for info in process_dhcp_host(options, host, items) :
yield info
except ValueError as error :
log.warn("%s: invalid host: %s", host, error)
else:
log.warn("ignore unknown block: %s", block)
def process_dhcp_conf (options, file) :
items, blocks = pvl.dhcp.config.DHCPConfigParser().load(file)
for item in items :
item, args = item[0], item[1:]
if item == 'include' :
include, = args
for info in process_dhcp_conf(options, pvl.args.apply_file(include)) :
yield info
else :
log.warn("ignore unknown item: %s", item)
for info in process_dhcp_hosts(options, blocks) :
yield info
def apply_hosts_import (options) :
"""
Import host infos from given files.
"""
if options.import_zone_hosts:
for info in process_zone_hosts(options,
pvl.args.apply_file(options.import_zone_hosts)) :
yield info
if options.import_dhcp_hosts:
for info in process_dhcp_conf(options,
pvl.args.apply_file(options.import_dhcp_hosts)) :
yield info
def process_hosts_import (options, import_hosts) :
"""
Import host definitions from given infos
"""
hosts = collections.defaultdict(lambda: collections.defaultdict(list))
for host, field, value in import_hosts :
hosts[host][field].append(value)
return hosts.iteritems()
def main (argv) :
options, args = parse_options(argv)
if args :
# direct from file
hosts = pvl.args.apply_files(args, 'r', options.input_charset)
else :
# import
import_hosts = apply_hosts_import(options)
hosts = process_hosts_import(options, import_hosts)
# output
if options.output_hosts :
for host, fields in hosts :
print host
for field, values in fields.iteritems() :
for value in values :
print "\t", field, "\t", value.encode(options.output_charset)
return 0
if __name__ == '__main__':
pvl.args.main(main)