4 ### Build a CDB file from configuration file
6 ### (c) 2007 Straylight/Edgeware
9 ###----- Licensing notice ---------------------------------------------------
11 ### This file is part of Trivial IP Encryption (TrIPE).
13 ### TrIPE is free software: you can redistribute it and/or modify it under
14 ### the terms of the GNU General Public License as published by the Free
15 ### Software Foundation; either version 3 of the License, or (at your
16 ### option) any later version.
18 ### TrIPE is distributed in the hope that it will be useful, but WITHOUT
19 ### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
20 ### FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
23 ### You should have received a copy of the GNU General Public License
24 ### along with TrIPE. If not, see <https://www.gnu.org/licenses/>.
28 ###--------------------------------------------------------------------------
29 ### External dependencies.
31 import ConfigParser as CP
33 from optparse import OptionParser
35 from sys import stdin, stdout, exit, argv
39 ###--------------------------------------------------------------------------
42 class CDBFake (object):
43 """Like cdbmake, but just outputs data suitable for cdb-map."""
44 def __init__(me, file = stdout):
46 def add(me, key, value):
47 me.file.write('%s:%s\n' % (key, value))
51 ###--------------------------------------------------------------------------
52 ### A bulk DNS resolver.
54 class BulkResolver (object):
56 Resolve a number of DNS names in parallel.
58 The BulkResovler resolves a number of hostnames in parallel. Using it
59 works in three phases:
61 1. You call prepare(HOSTNAME) a number of times, to feed in the hostnames
64 2. You call run() to actually drive the resolver.
66 3. You call lookup(HOSTNAME) to get the address you wanted. This will
67 fail with KeyError if the resolver couldn't resolve the HOSTNAME.
71 """Initialize the resolver."""
75 def prepare(me, host):
76 """Prime the resolver to resolve the name HOST."""
77 if host not in me._resolvers:
78 me._resolvers[host] = M.SelResolveByName \
80 lambda name, alias, addr:
81 me._resolved(host, addr[0]),
82 lambda: me._resolved(host, None))
85 """Run the background DNS resolver until it's finished."""
91 Fetch the address corresponding to HOST.
93 addr = me._namemap[host]
98 def _resolved(me, host, addr):
99 """Callback function: remember that ADDR is the address for HOST."""
100 me._namemap[host] = addr
101 del me._resolvers[host]
103 ###--------------------------------------------------------------------------
104 ### The configuration parser.
106 ## Match a $(VAR) configuration variable reference; group 1 is the VAR.
107 r_ref = RX.compile(r'\$\(([^)]+)\)')
109 ## Match a $[HOST] name resolution reference; group 1 is the HOST.
110 r_resolve = RX.compile(r'\$\[([^]]+)\]')
113 return ' -> '.join(["`%s'" % hop for hop in path])
115 class AmbiguousOptionError (Exception):
116 def __init__(me, key, patha, vala, pathb, valb):
118 me.patha, me.vala = patha, vala
119 me.pathb, me.valb = pathb, valb
121 return "Ambiguous answer resolving key `%s': " \
122 "path %s yields `%s' but %s yields `%s'" % \
123 (me.key, _fmt_path(me.patha), me.vala, _fmt_path(me.pathb), me.valb)
125 class InheritanceCycleError (Exception):
126 def __init__(me, key, path):
130 return "Found a cycle %s looking up key `%s'" % \
131 (_fmt_path(me.path), me.key)
133 class MissingKeyException (Exception):
134 def __init__(me, sec, key):
138 return "Key `%s' not found in section `%s'" % (me.key, me.sec)
140 class MyConfigParser (CP.RawConfigParser):
142 A more advanced configuration parser.
144 This has two major enhancements over the standard ConfigParser which are
147 * It recognizes `@inherits' keys and follows them when expanding a
150 * It recognizes `$(VAR)' references to configuration variables during
151 expansion and processes them correctly.
153 * It recognizes `$[HOST]' name-resolver requests and handles them
158 1. Call read(FILENAME) and/or read(FP, [FILENAME]) to slurp in the
161 2. Call resolve() to collect the hostnames which need to be resolved and
162 actually do the name resolution.
164 3. Call get(SECTION, ITEM) to collect the results, or items(SECTION) to
170 Initialize a new, empty configuration parser.
172 CP.RawConfigParser.__init__(me)
173 me._resolver = BulkResolver()
177 Works out all of the hostnames which need resolving and resolves them.
179 Until you call this, attempts to fetch configuration items which need to
180 resolve hostnames will fail!
182 for sec in me.sections():
183 for key, value in me.items(sec, resolvep = False):
184 for match in r_resolve.finditer(value):
185 me._resolver.prepare(match.group(1))
188 def _expand(me, sec, string, resolvep):
190 Expands $(...) and (optionally) $[...] placeholders in STRING.
192 The SEC is the configuration section from which to satisfy $(...)
193 requests. RESOLVEP is a boolean switch: do we bother to tax the resolver
194 or not? This is turned off by the resolve() method while it's collecting
195 hostnames to be resolved.
198 (lambda m: me.get(sec, m.group(1), resolvep), string)
200 string = r_resolve.sub(lambda m: me._resolver.lookup(m.group(1)),
204 def has_option(me, sec, key):
206 Decide whether section SEC has a configuration key KEY.
208 This version of the method properly handles the @inherit key.
210 return key == 'name' or me._get(sec, key)[0] is not None
212 def _get(me, sec, key, map = None, path = None):
214 Low-level option-fetching method.
216 Fetch the value for the named KEY from section SEC, or maybe
217 (recursively) a section which SEC inherits from.
219 Returns a pair VALUE, PATH. The value is not expanded; nor do we check
220 for the special `name' key. The caller is expected to do these things.
221 Returns None if no value could be found.
224 ## If we weren't given a memoization map or path, then we'd better make
226 if map is None: map = {}
227 if path is None: path = []
229 ## If we've been this way before on another pass through then return the
230 ## value we found then. If we're still thinking about it then we've
234 threadp, value = map[sec]
239 raise InheritanceCycleError, (key, path)
241 ## See whether the answer is ready waiting for us.
243 v = CP.RawConfigParser.get(me, sec, key)
244 except CP.NoOptionError:
251 ## No, apparently, not. Find out our list of parents.
253 parents = CP.RawConfigParser.get(me, sec, '@inherit').\
254 replace(',', ' ').split()
255 except CP.NoOptionError:
258 ## Initially we have no idea.
262 ## Go through our parents and ask them what they think.
263 map[sec] = True, None
266 ## See whether we get an answer. If not, keep on going.
267 v, pp = me._get(p, key, map, path)
268 if v is None: continue
270 ## If we got an answer, check that it matches any previous ones.
275 raise AmbiguousOptionError, (key, winner, value, pp, v)
277 ## That's the best we could manage.
279 map[sec] = False, value
282 def get(me, sec, key, resolvep = True):
284 Retrieve the value of KEY from section SEC.
287 ## Special handling for the `name' key.
289 try: value = CP.RawConfigParser.get(me, sec, key)
290 except CP.NoOptionError: value = sec
292 value, _ = me._get(sec, key)
294 raise MissingKeyException, (sec, key)
296 ## Expand the value and return it.
297 return me._expand(sec, value, resolvep)
299 def items(me, sec, resolvep = True):
301 Return a list of (NAME, VALUE) items in section SEC.
303 This extends the default method by handling the inheritance chain.
306 ## Initialize for a depth-first walk of the inheritance graph.
312 ## Visit nodes, collecting their keys. Don't believe the values:
313 ## resolving inheritance is too hard to do like this.
316 if sec in visited: continue
319 for key, value in CP.RawConfigParser.items(me, sec):
320 if key == '@inherit': stack += value.replace(',', ' ').split()
323 ## Now collect the values for the known keys, one by one.
325 for key in d: items.append((key, me.get(basesec, key, resolvep)))
330 ###--------------------------------------------------------------------------
331 ### Command-line handling.
333 def inputiter(things):
335 Iterate over command-line arguments, returning corresponding open files.
337 If none were given, or one is `-', assume standard input; if one is a
338 directory, scan it for files other than backups; otherwise return the
343 if OS.isatty(stdin.fileno()):
344 M.die('no input given, and stdin is a terminal')
350 elif OS.path.isdir(thing):
351 for item in OS.listdir(thing):
352 if item.endswith('~') or item.endswith('#'):
354 name = OS.path.join(thing, item)
355 if not OS.path.isfile(name):
361 def parse_options(argv = argv):
363 Parse command-line options, returning a pair (OPTS, ARGS).
366 op = OptionParser(usage = '%prog [-c CDB] INPUT...',
367 version = '%%prog (tripe, version %s)' % VERSION)
368 op.add_option('-c', '--cdb', metavar = 'CDB',
369 dest = 'cdbfile', default = None,
370 help = 'Compile output into a CDB file.')
371 opts, args = op.parse_args(argv)
374 ###--------------------------------------------------------------------------
379 Read the configuration files and return the accumulated result.
381 We make sure that all hostnames have been properly resolved.
383 conf = MyConfigParser()
384 for f in inputiter(args):
389 def output(conf, cdb):
391 Output the configuration information CONF to the database CDB.
393 This is where the special `user' and `auto' database entries get set.
396 for sec in sorted(conf.sections()):
397 if sec.startswith('@'):
399 elif sec.startswith('$'):
403 if conf.has_option(sec, 'auto') and \
404 conf.get(sec, 'auto') in ('y', 'yes', 't', 'true', '1', 'on'):
406 if conf.has_option(sec, 'user'):
407 cdb.add('U%s' % conf.get(sec, 'user'), sec)
408 url = M.URLEncode(laxp = True, semip = True)
409 for key, value in sorted(conf.items(sec), key = lambda (k, v): k):
410 if not key.startswith('@'):
411 url.encode(key, ' '.join(M.split(value)[0]))
412 cdb.add(label, url.result)
413 cdb.add('%AUTO', ' '.join(auto))
418 opts, args = parse_options()
420 cdb = CDB.cdbmake(opts.cdbfile, opts.cdbfile + '.new')
423 conf = getconf(args[1:])
426 if __name__ == '__main__':
429 ###----- That's all, folks --------------------------------------------------