ffdns/genconfig.py
2014-05-06 11:26:47 +02:00

162 lines
5.8 KiB
Python
Executable file

#!/usr/bin/env python
import os
from textwrap import dedent
from optparse import OptionParser
from socket import AF_INET, AF_INET6, inet_pton
class Formatter(object):
"""
Abstract class to define the interface for formatters.
"""
def __init__(self):
"""
Initialize formatter, maybe generate a preamble for the config
"""
self.config = []
self.add_comment(dedent(
"""
This file is automatically generated by the ffdns generator.
Don't edit it manually! Instead, send pull requests to
https://github.com/freifunkhamburg/ffdns
and re-generate it using the genconfig.py there!
"""
))
def add_comment(self, comment):
"""
Add a comment to the config. Default is prefixed with #
"""
self.config.append("# " + "\n# ".join(comment.split("\n")))
def add_data(self, domains, servers):
"""
Add config directives so that every domain in domains is forwarded to
every server in servers.
"""
raise NotImplementedError()
def finalize(self):
"""
Finalize configuration and return it
"""
return "\n".join(self.config)
class DnsmasqFormatter(Formatter):
"Formatter for dnsmasq"
def add_data(self, domains, servers):
for domain in domains:
for server in servers:
self.config.append("server=/%s/%s" % (domain, server))
class BindFormatter(Formatter):
"Formatter for bind9 (>=9.8) using type static-stub"
def add_data(self, domains, servers):
for domain in domains:
self.config.append(dedent("""
zone "%s" {
type static-stub;
server-addresses { %s; };
};
""" % (domain, "; ".join(servers))).lstrip())
class BindForwardFormatter(Formatter):
"Formatter for bind9 using type forward"
def add_data(self, domains, servers):
for domain in domains:
self.config.append(dedent("""
zone "%s" {
type forward;
forwarders { %s; };
forward only;
};
""" % (domain, "; ".join(servers))).lstrip())
def create_config(srcdir, fmtclass, exclude=None, filters=[]):
"""
Generates a configuration using all files in srcdir (non-recursively)
except those in the iterable exclude.
The files are read in lexicographic order to produce deterministic results.
Every option=value pair in the files is passed through all callables in the
iterable filters. Only if none return False, it is assumed valid.
"""
COMMENT_CHAR = "#"
OPTION_CHAR = "="
formatter = fmtclass()
for fname in sorted(list(set(os.listdir(srcdir)) - set(exclude))):
fpath = os.path.join(srcdir, fname)
if os.path.isfile(fpath):
domains = []
servers = []
with open(fpath) as f:
formatter.add_comment("\n%s\n" % fname)
for line in f:
if COMMENT_CHAR in line:
line, comment = line.split(COMMENT_CHAR, 1)
comment = comment.strip()
if comment:
formatter.add_comment(comment)
if OPTION_CHAR in line:
option, value = line.split(OPTION_CHAR, 1)
option = option.strip()
value = value.strip()
if not all(filt(option, value) for filt in filters):
continue
if option == "server":
servers.append(value)
elif option == "domain":
domains.append(value)
else:
raise RuntimeError("Unknown option '%s' in file '%s'" % (option, fpath))
elif line.strip():
raise RuntimeError("Unrecognized line '%s' in file '%s'" % (line, fpath))
if not domains:
formatter.add_comment("No valid domains found")
elif not servers:
formatter.add_comment("No valid servers found")
else:
formatter.add_data(domains, servers)
print(formatter.finalize())
if __name__ == "__main__":
def try_inet_pton(af, ip):
try:
inet_pton(af, ip)
return True
except:
return False
formatters = {
"dnsmasq": DnsmasqFormatter,
"bind": BindFormatter,
"bind-forward": BindForwardFormatter,
}
filters = {
"v4": lambda option, value: option != "server" or try_inet_pton(AF_INET, value),
"v6": lambda option, value: option != "server" or try_inet_pton(AF_INET6, value),
}
parser = OptionParser()
parser.add_option("-f", "--format", dest="fmt",
help="Create config in format FMT. Possible values: %s. Default: dnsmasq" % ", ".join(formatters.keys()), metavar="FMT",
choices=list(formatters.keys()),
default="dnsmasq")
parser.add_option("-s", "--sourcedir", dest="src",
help="Use files in DIR as input files. Default: data/", metavar="DIR",
default="data")
parser.add_option("-x", "--exclude", dest="exclude",
help="Exclude the comma-separated list of FILES in the sourcedir from the generation", metavar="FILES",
default="")
parser.add_option("--filter", dest="filter",
help="Only include certain servers. Possible choices: %s" % ", ".join(filters.keys()),
choices=list(filters.keys()))
(options, args) = parser.parse_args()
create_config(options.src, formatters[options.fmt], options.exclude.split(","), [filters[options.filter]] if options.filter else [])