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
14 ### it under the terms of the GNU General Public License as published by
15 ### the Free Software Foundation; either version 2 of the License, or
16 ### (at your option) any later version.
18 ### TrIPE is distributed in the hope that it will be useful,
19 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ### GNU General Public License for more details.
23 ### You should have received a copy of the GNU General Public License
24 ### along with TrIPE; if not, write to the Free Software Foundation,
25 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29 ###--------------------------------------------------------------------------
30 ### External dependencies.
32 import ConfigParser as CP
34 from optparse import OptionParser
36 from sys import stdin, stdout, exit, argv
40 ###--------------------------------------------------------------------------
43 class CDBFake (object):
44 """Like cdbmake, but just outputs data suitable for cdb-map."""
45 def __init__(me, file = stdout):
47 def add(me, key, value):
48 me.file.write('%s:%s\n' % (key, value))
52 ###--------------------------------------------------------------------------
53 ### A bulk DNS resolver.
55 class BulkResolver (object):
57 Resolve a number of DNS names in parallel.
59 The BulkResovler resolves a number of hostnames in parallel. Using it
60 works in three phases:
62 1. You call prepare(HOSTNAME) a number of times, to feed in the hostnames
65 2. You call run() to actually drive the resolver.
67 3. You call lookup(HOSTNAME) to get the address you wanted. This will
68 fail with KeyError if the resolver couldn't resolve the HOSTNAME.
72 """Initialize the resolver."""
76 def prepare(me, host):
77 """Prime the resolver to resolve the name HOST."""
78 if host not in me._resolvers:
79 me._resolvers[host] = M.SelResolveByName \
81 lambda name, alias, addr:
82 me._resolved(host, addr[0]),
83 lambda: me._resolved(host, None))
86 """Run the background DNS resolver until it's finished."""
92 Fetch the address corresponding to HOST.
94 addr = me._namemap[host]
99 def _resolved(me, host, addr):
100 """Callback function: remember that ADDR is the address for HOST."""
101 me._namemap[host] = addr
102 del me._resolvers[host]
104 ###--------------------------------------------------------------------------
105 ### The configuration parser.
107 ## Match a $(VAR) configuration variable reference; group 1 is the VAR.
108 r_ref = RX.compile(r'\$\(([^)]+)\)')
110 ## Match a $[HOST] name resolution reference; group 1 is the HOST.
111 r_resolve = RX.compile(r'\$\[([^]]+)\]')
114 return ' -> '.join(["`%s'" % hop for hop in path])
116 class AmbiguousOptionError (Exception):
117 def __init__(me, key, patha, vala, pathb, valb):
119 me.patha, me.vala = patha, vala
120 me.pathb, me.valb = pathb, valb
122 return "Ambiguous answer resolving key `%s': " \
123 "path %s yields `%s' but %s yields `%s'" % \
124 (me.key, _fmt_path(me.patha), me.vala, _fmt_path(me.pathb), me.valb)
126 class InheritanceCycleError (Exception):
127 def __init__(me, key, path):
131 return "Found a cycle %s looking up key `%s'" % \
132 (_fmt_path(me.path), me.key)
134 class MissingKeyException (Exception):
135 def __init__(me, sec, key):
139 return "Key `%s' not found in section `%s'" % (me.key, me.sec)
141 class MyConfigParser (CP.RawConfigParser):
143 A more advanced configuration parser.
145 This has two major enhancements over the standard ConfigParser which are
148 * It recognizes `@inherits' keys and follows them when expanding a
151 * It recognizes `$(VAR)' references to configuration variables during
152 expansion and processes them correctly.
154 * It recognizes `$[HOST]' name-resolver requests and handles them
159 1. Call read(FILENAME) and/or read(FP, [FILENAME]) to slurp in the
162 2. Call resolve() to collect the hostnames which need to be resolved and
163 actually do the name resolution.
165 3. Call get(SECTION, ITEM) to collect the results, or items(SECTION) to
171 Initialize a new, empty configuration parser.
173 CP.RawConfigParser.__init__(me)
174 me._resolver = BulkResolver()
178 Works out all of the hostnames which need resolving and resolves them.
180 Until you call this, attempts to fetch configuration items which need to
181 resolve hostnames will fail!
183 for sec in me.sections():
184 for key, value in me.items(sec, resolvep = False):
185 for match in r_resolve.finditer(value):
186 me._resolver.prepare(match.group(1))
189 def _expand(me, sec, string, resolvep):
191 Expands $(...) and (optionally) $[...] placeholders in STRING.
193 The SEC is the configuration section from which to satisfy $(...)
194 requests. RESOLVEP is a boolean switch: do we bother to tax the resolver
195 or not? This is turned off by the resolve() method while it's collecting
196 hostnames to be resolved.
199 (lambda m: me.get(sec, m.group(1), resolvep), string)
201 string = r_resolve.sub(lambda m: me._resolver.lookup(m.group(1)),
205 def has_option(me, sec, key):
207 Decide whether section SEC has a configuration key KEY.
209 This version of the method properly handles the @inherit key.
211 return key == 'name' or me._get(sec, key)[0] is not None
213 def _get(me, sec, key, map = None, path = None):
215 Low-level option-fetching method.
217 Fetch the value for the named KEY from section SEC, or maybe
218 (recursively) a section which SEC inherits from.
220 Returns a pair VALUE, PATH. The value is not expanded; nor do we check
221 for the special `name' key. The caller is expected to do these things.
222 Returns None if no value could be found.
225 ## If we weren't given a memoization map or path, then we'd better make
227 if map is None: map = {}
228 if path is None: path = []
230 ## If we've been this way before on another pass through then return the
231 ## value we found then. If we're still thinking about it then we've
235 threadp, value = map[sec]
240 raise InheritanceCycleError, (key, path)
242 ## See whether the answer is ready waiting for us.
244 v = CP.RawConfigParser.get(me, sec, key)
245 except CP.NoOptionError:
252 ## No, apparently, not. Find out our list of parents.
254 parents = CP.RawConfigParser.get(me, sec, '@inherit').\
255 replace(',', ' ').split()
256 except CP.NoOptionError:
259 ## Initially we have no idea.
263 ## Go through our parents and ask them what they think.
264 map[sec] = True, None
267 ## See whether we get an answer. If not, keep on going.
268 v, pp = me._get(p, key, map, path)
269 if v is None: continue
271 ## If we got an answer, check that it matches any previous ones.
276 raise AmbiguousOptionError, (key, winner, value, pp, v)
278 ## That's the best we could manage.
280 map[sec] = False, value
283 def get(me, sec, key, resolvep = True):
285 Retrieve the value of KEY from section SEC.
288 ## Special handling for the `name' key.
290 try: value = CP.RawConfigParser.get(me, sec, key)
291 except CP.NoOptionError: value = sec
293 value, _ = me._get(sec, key)
295 raise MissingKeyException, (sec, key)
297 ## Expand the value and return it.
298 return me._expand(sec, value, resolvep)
300 def items(me, sec, resolvep = True):
302 Return a list of (NAME, VALUE) items in section SEC.
304 This extends the default method by handling the inheritance chain.
307 ## Initialize for a depth-first walk of the inheritance graph.
313 ## Visit nodes, collecting their keys. Don't believe the values:
314 ## resolving inheritance is too hard to do like this.
317 if sec in visited: continue
320 for key, value in CP.RawConfigParser.items(me, sec):
321 if key == '@inherit': stack += value.replace(',', ' ').split()
324 ## Now collect the values for the known keys, one by one.
326 for key in d: items.append((key, me.get(basesec, key, resolvep)))
331 ###--------------------------------------------------------------------------
332 ### Command-line handling.
334 def inputiter(things):
336 Iterate over command-line arguments, returning corresponding open files.
338 If none were given, or one is `-', assume standard input; if one is a
339 directory, scan it for files other than backups; otherwise return the
344 if OS.isatty(stdin.fileno()):
345 M.die('no input given, and stdin is a terminal')
351 elif OS.path.isdir(thing):
352 for item in OS.listdir(thing):
353 if item.endswith('~') or item.endswith('#'):
355 name = OS.path.join(thing, item)
356 if not OS.path.isfile(name):
362 def parse_options(argv = argv):
364 Parse command-line options, returning a pair (OPTS, ARGS).
367 op = OptionParser(usage = '%prog [-c CDB] INPUT...',
368 version = '%%prog (tripe, version %s)' % VERSION)
369 op.add_option('-c', '--cdb', metavar = 'CDB',
370 dest = 'cdbfile', default = None,
371 help = 'Compile output into a CDB file.')
372 opts, args = op.parse_args(argv)
375 ###--------------------------------------------------------------------------
380 Read the configuration files and return the accumulated result.
382 We make sure that all hostnames have been properly resolved.
384 conf = MyConfigParser()
385 for f in inputiter(args):
390 def output(conf, cdb):
392 Output the configuration information CONF to the database CDB.
394 This is where the special `user' and `auto' database entries get set.
397 for sec in sorted(conf.sections()):
398 if sec.startswith('@'):
400 elif sec.startswith('$'):
404 if conf.has_option(sec, 'auto') and \
405 conf.get(sec, 'auto') in ('y', 'yes', 't', 'true', '1', 'on'):
407 if conf.has_option(sec, 'user'):
408 cdb.add('U%s' % conf.get(sec, 'user'), sec)
409 url = M.URLEncode(laxp = True, semip = True)
410 for key, value in sorted(conf.items(sec), key = lambda (k, v): k):
411 if not key.startswith('@'):
412 url.encode(key, ' '.join(M.split(value)[0]))
413 cdb.add(label, url.result)
414 cdb.add('%AUTO', ' '.join(auto))
419 opts, args = parse_options()
421 cdb = CDB.cdbmake(opts.cdbfile, opts.cdbfile + '.new')
424 conf = getconf(args[1:])
427 if __name__ == '__main__':
430 ###----- That's all, folks --------------------------------------------------