/root/bin/mk-named-dhcpd-conf.py (1)

From RaySoft
#!/usr/bin/python3
# ------------------------------------------------------------------------------
# mk-named-dhcpd-conf.py
# ======================
#
# Scope     Native
# Copyright (C) 2022 by RaySoft, Zurich, Switzerland
# License   GNU General Public License (GPL) 2.0
#           https://www.gnu.org/licenses/gpl2.txt
#
# ------------------------------------------------------------------------------

DNS_FILE = 'named-zones.conf'
DHCP_FILE = 'dhcpd-hosts.conf'

# ------------------------------------------------------------------------------

PROGRAM_NAME = 'mk-named-dhcpd-conf'
PROGRAM_VERSION = '0.1'

# ------------------------------------------------------------------------------

import argparse
from datetime import datetime
import json
import os
import os.path
import re
import sys

# ------------------------------------------------------------------------------


def main():
    parser = argparse.ArgumentParser(
        description='Generate the BIND and ISC DHCP configuration based on '
                    'a JSON file.', prog=PROGRAM_NAME,
    )
    parser.add_argument(
        '-V', '--version', action='version',
        version='%(prog)s {!s}'.format(PROGRAM_VERSION),
    )
    group1 = parser.add_mutually_exclusive_group()
    group1.add_argument(
        '-a', '--append', action='store_true',
        help='append if a needed file already exists',
    )
    group1.add_argument(
        '-o', '--overwrite', action='store_true',
        help='overwrite existing files',
    )
    parser.add_argument(
        '-p', '--path', metavar='PATH', dest='path', action='store',
        default=os.getcwd(), help="proceed in path (default is '.')",
    )
    parser.add_argument(
        'file', metavar='FILE', action='store', nargs=1,
        type=argparse.FileType(mode='r'), help='JSON file',
    )

    args = parser.parse_args()

    if not os.path.isdir(args.path):
        print("error finding directory '{!s}'!".format(args.path),
              file=sys.stderr)
        parser.print_usage()
        sys.exit(1)

    try:
        json_data = json.load(args.file[0])
    except (TypeError, ValueError, OverflowError) as error:
        print('error parsing json file: {!s}'.format(error), file=sys.stderr)

    try:
        self = json_data['self']
    except KeyError as error:
        print('error getting self: {!s}'.format(error), file=sys.stderr)

    try:
        soa = json_data['soa']
    except KeyError as error:
        print('error getting soa: {!s}'.format(error), file=sys.stderr)

    try:
        zones = json_data['zones']
    except KeyError as error:
        print('error getting zones: {!s}'.format(error), file=sys.stderr)

    try:
        servers = json_data['servers']
    except KeyError as error:
        print('error getting ns & mx: {!s}'.format(error), file=sys.stderr)

    try:
        hosts = json_data['hosts']
    except KeyError as error:
        print('error getting hosts', file=sys.stderr)

    for zone in ['forward', 'reverse']:
        if not zones[zone]:
            print('error getting {!s} zone'.format(zone), file=sys.stderr)

    soa['serial'] = datetime.now().strftime('%Y%m%d01')

    zones['forward']['origin'] = '{!s}.'.format(zones['forward']['name'])
    zones['forward']['file'] = '{!s}.zone'.format(zones['forward']['name'])

    zones['reverse']['origin'] = '.'.join(zones['reverse']['name']
                                          .split('.')[::-1]) \
                                 + '.in-addr.arpa.'
    zones['reverse']['file'] = '{!s}.zone'.format(zones['reverse']['name'])

    configs = {
        'dns': {},
        'dhcp': {},
    }

    configs['dns']['file'] = DNS_FILE
    configs['dhcp']['file'] = DHCP_FILE

    for obj in [zones['forward'], zones['reverse'], configs['dns'],
                configs['dhcp']]:
        if args.overwrite:
            obj['file-mode'] = 'w'
        elif args.append:
            obj['file-mode'] = 'a'
        else:
            if os.path.isfile(obj['file']):
                print('warning: file already exists: {!s}'.format(obj['file']))

                answer = input('[o]verwrite or [a]ppend: ')

                if answer == 'o':
                    obj['file-mode'] = 'w'
                elif answer == 'a':
                    obj['file-mode'] = 'a'
                else:
                    print('error parsing answer: {!s}'.format(answer))
                    sys.exit(1)
            else:
                obj['file-mode'] = 'w'

    with open(zones['forward']['file'], zones['forward']['file-mode']) \
            as zones['forward']['fh'], \
            open(zones['reverse']['file'], zones['reverse']['file-mode']) \
            as zones['reverse']['fh'], \
            open(configs['dns']['file'], configs['dns']['file-mode']) \
            as configs['dns']['fh'], \
            open(configs['dhcp']['file'], configs['dhcp']['file-mode']) \
            as configs['dhcp']['fh']:

        for config in configs.keys():
            if configs[config]['file-mode'] == 'w':
                write_header(configs[config], self, '#')

        for zone in sorted(zones.keys()):
            if zones[zone]['file-mode'] == 'w':
                write_header(zones[zone], self, ';')
                write_bind_var(zones[zone], 'origin')
                write_bind_var(zones[zone], 'ttl')
                write_bind_soa(zones[zone], soa)
                write_bind_ns(zones[zone], servers['dns'])

                write_bind_zone_config(configs['dns'], zones[zone])

        if 'mail' in servers and zones[zone]['file-mode'] == 'w':
            write_bind_mx(zones['forward'], servers['mail'])

        for host in sorted(hosts.keys()):
            hosts[host]['host'] = host

            if host:
                hosts[host]['fqdn'] = '{!s}.{!s}' \
                                      .format(host, zones['forward']['origin'])
            else:
                hosts[host]['fqdn'] = zones['forward']['origin']

            write_bind_forward_records(zones['forward'], hosts[host])

            write_bind_reverse_records(zones['reverse'], hosts[host],
                                       zones['forward']['origin'])

            if 'mac' in hosts[host]:
                write_dhcp_host_config(configs['dhcp'], hosts[host])


# ------------------------------------------------------------------------------

def write_header(obj, self, mark):
    obj['fh'].write(
        '{!s}{!s}\n'.format(mark, '-' * 79)
        + '{!s} {!s}\n'.format(mark, obj['file'])
        + '{!s} {!s}\n'.format(mark, '=' * len(obj['file']))
        + '{!s}\n'.format(mark)
        + '{!s} {:<9s} {!s}\n'.format(mark, 'Project', self['project'])
        + '{!s} {:<9s} {!s}\n'.format(mark, 'Scope', self['scope'])
        + '{!s} {:<9s} {!s}\n'.format(mark, 'Copyright', self['copyright'])
        + '{!s} {:<9s} {!s}\n'.format(mark, 'License', self['license'])
        + '{!s} {:<9s} {!s}\n'.format(mark, '', self['license_url'])
        + '{!s}\n'.format(mark)
        + '{!s}{!s}\n'.format(mark, '-' * 79)
        + '\n'
    )


# ------------------------------------------------------------------------------

def write_bind_var(zone, var):
    zone['fh'].write('${!s} {!s}\n'.format(var.upper(), zone[var]))


# ------------------------------------------------------------------------------

def write_bind_soa(zone, soa):
    zone['fh'].write(
        '\n'
        + '{:<11s} {:<11s} {!s} {!s} (\n'
          .format('@', 'IN  SOA', soa['master'], soa['admin'])
        + '{:<23s} {!s}\n'.format('', soa['serial'])
        + '{:<23s} {:d}\n'.format('', soa['refresh'])
        + '{:<23s} {:d}\n'.format('', soa['retry'])
        + '{:<23s} {:d}\n'.format('', soa['expire'])
        + '{:<23s} {:d} )\n'.format('', soa['ttl'])
        + '\n'
    )


# ------------------------------------------------------------------------------

def write_bind_ns(zone, ns):
    layout = '{:<11s} {:<11s} {!s}\n'

    for server in sorted(ns):
        zone['fh'].write(layout.format('', 'IN  NS', server))

    zone['fh'].write('\n')


# ------------------------------------------------------------------------------

def write_bind_mx(zone, mx):
    layout = '{:<11s} {!s} {:<4d} {!s}\n'

    for server in sorted(mx.keys()):
        zone['fh'].write(layout.format('', 'IN  MX', mx[server], server))

    zone['fh'].write('\n')


# ------------------------------------------------------------------------------

def write_bind_forward_records(zone, host):
    layout = '{:<11s} {:<11s} {!s}\n'

    zone['fh'].write(layout.format(host['host'], 'IN  A', host['ip']))

    if not host['host']:
        host['host'] = zone['origin']

    for name in sorted(host['alias']):
        if name in ['ns1', 'ns2', 'mx1', 'mx2', 'ntp']:
            zone['fh'].write(layout.format(name, 'IN  A', host['ip']))
        else:
            zone['fh'].write(layout.format(name, 'IN  CNAME', host['host']))

    zone['fh'].write('\n')


# ------------------------------------------------------------------------------

def write_bind_reverse_records(zone, host, origin):
    layout = '{:<11s} {:<11s} {!s}\n'

    ip_part = host['ip'].replace(zone['name'] + '.', '')
    regex = re.compile('^mx.$')

    zone['fh'].write(layout.format(ip_part, 'IN  PTR', host['fqdn']))

    for alias in host['alias']:
        if regex.match(alias):
            zone['fh'].write(layout.format(ip_part, 'IN  PTR',
                                           alias + '.' + origin))

    zone['fh'].write('\n')


# ------------------------------------------------------------------------------

def write_bind_zone_config(config, zone):
    config['fh'].write(
        'zone {!s} IN {{\n'.format(zone['origin'])
        + '  type master;\n'
        + '  file "pri/{!s}";\n'.format(zone['file'])
        + '};\n'
        + '\n'
    )


# ------------------------------------------------------------------------------

def write_dhcp_host_config(config, host):
    config['fh'].write(
        'host {!s} {{\n'.format(host['fqdn'])
        + '  hardware ethernet {!s};\n'.format(host['mac'])
        + '  fixed-address {!s};\n'.format(host['ip'])
        + '}\n'
        + '\n'
    )


# ------------------------------------------------------------------------------

if __name__ == '__main__':
    main()

sys.exit(0)

Usage