designate/contrib/zoneextractor.py
Artom Lifshitz 179a9aa265 Zoneextractor tool
zoneextractor.py helps on the client-side of the new zone
import API. It takes a BIND9 named.conf and writes out or prints a
zonefile for every configured master zone. Said zonefile can then be
curled or otherwise submitted to the zonefile import API.

Change-Id: Ibc15639771da52e678be067ec4474bcbcc2f0865
2013-10-24 09:40:53 -04:00

207 lines
7.0 KiB
Python

# Copyright (C) 2013 eNovance SAS <licensing@enovance.com>
#
# Author: Artom Lifshitz <artom.lifshitz@enovance.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import sys
import re
import os
import dns.zone
import argparse
import logging
logging.basicConfig()
LOG = logging.getLogger(__name__)
class Zone:
"""
Encapsulates a dnspython zone to provide easier printing and writing to
files
"""
def __init__(self, dnszone):
self._dnszone = dnszone
def to_stdout(self):
self.to_file(sys.stdout)
def to_file(self, f):
if type(f) is file:
fd = f
elif type(f) is str:
if os.path.isdir(f):
fd = open(os.path.join(f, self._dnszone.origin.to_text()), 'w')
else:
fd = open(f, 'w')
else:
raise ValueError('f must be a file name or file object')
fd.write('$ORIGIN %s\n' % self._dnszone.origin.to_text())
self._dnszone.to_file(fd, relativize=False)
fd.write('\n')
if fd is not sys.stdout:
fd.close()
class Extractor:
"""
Extracts all the zones configured in a named.conf, including included
files
"""
# The regexes we use to extract information from the config file
_include_regex = re.compile(
r"""
include \s* # The include keyword, possibly followed by
# whitespace
" # Open quote
(?P<file> [^"]+ ) # The included file (without quotes), as group 'file'
" # Close quote
\s* ; # Semicolon, possibly preceeded by whitespace
""", re.MULTILINE | re.VERBOSE)
_zone_regex = re.compile(
r"""
zone \s* # The zone keyword, possibly followed by
# whitespace
" # Open quote
(?P<name> [^"]+ ) # The zone name (without quotes), as group 'name'
" # Close quote
\s* # Possible whitespace
{ # Open bracket
(?P<content> [^{}]+ ) # The contents of the zone block (without
# brackets) as group 'content'
} # Close bracket
\s* ; # Semicolon, possibly preceeded by whitespace
""", re.MULTILINE | re.VERBOSE)
_type_master_regex = re.compile(
r"""
type \s+ # The type keyword, followed by some whitespace
master # The master keyword
\s* ; # Semicolon, possibly preceeded by whitespace
""", re.MULTILINE | re.VERBOSE)
_zonefile_regex = re.compile(r"""
file \s* # The file keyword, possible followed by whitespace
" # Open quote
(?P<file> [^"]+ ) # The zonefile (without quotes), as group 'file'
" # Close quote
\s* ; # Semicolor, possible preceeded by whitespace
""", re.MULTILINE | re.VERBOSE)
def __init__(self, conf_file):
self._conf_file = conf_file
self._conf = self._filter_comments(conf_file)
def _skip_until(self, f, stop):
skip = ''
while True:
skip += f.read(1)
if skip.endswith(stop):
break
def _filter_comments(self, conf_file):
"""
Reads the named.conf, skipping comments and returning the filtered
configuration
"""
f = open(conf_file)
conf = ''
while True:
c = f.read(1)
if c == '':
break
conf += c
# If we just appended a commenter:
if conf.endswith('#'):
self._skip_until(f, '\n')
# Strip the '#' we appended earlier
conf = conf[:-1]
elif conf.endswith('//'):
self._skip_until(f, '\n')
# Strip the '//' we appended earlier
conf = conf[:-2]
elif conf.endswith('/*'):
self._skip_until(f, '*/')
# Strip the '/*' we appended earlier
conf = conf[:-2]
f.close()
return conf
def extract(self):
zones = []
zones.extend(self._process_includes())
zones.extend(self._extract_zones())
return zones
def _process_includes(self):
zones = []
for include in self._include_regex.finditer(self._conf):
x = Extractor(include.group('file'))
zones.extend(x.extract())
return zones
def _extract_zones(self):
zones = []
for zone in self._zone_regex.finditer(self._conf):
content = zone.group('content')
name = zone.group('name')
# Make sure it's a master zone:
if self._type_master_regex.search(content):
zonefile = self._zonefile_regex.search(content).group('file')
try:
zone_object = dns.zone.from_file(zonefile,
allow_include=True)
except dns.zone.UnknownOrigin:
LOG.info('%s is missing $ORIGIN, inserting %s' %
(zonefile, name))
zone_object = dns.zone.from_file(zonefile,
allow_include=True,
origin=name)
except dns.zone.NoSOA:
LOG.error('%s has no SOA' % zonefile)
zones.append(Zone(zone_object))
return zones
def main():
parser = argparse.ArgumentParser(
description='Extract zonefiles from named.conf.')
parser.add_argument('named_conf', metavar='FILE', type=str, nargs=1,
help='the named.conf to parse')
parser.add_argument('-w', '--write', metavar='DIR', type=str,
help='Wwrite each extracted zonefile as its own file'
' in DIR')
parser.add_argument('-v', '--verbose', action='store_true',
help='verbose output')
args = parser.parse_args()
if args.verbose:
LOG.setLevel(logging.INFO)
else:
LOG.setLevel(logging.WARNING)
try:
x = Extractor(args.named_conf[0])
for zone in x.extract():
if args.write is not None:
zone.to_file(args.write)
else:
zone.to_stdout()
except IOError as e:
LOG.error(e)
if __name__ == '__main__':
main()