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 three 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 ## Extend the path to cover the lookup section, but remember to remove us
230 ## again when we've finished. If we need to pass the current path back
231 ## upwards, then remember to take a copy.
235 ## If we've been this way before on another pass through then return
236 ## the value we found then. If we're still thinking about it then
237 ## we've found a cycle.
239 threadp, value = map[sec]
244 raise InheritanceCycleError(key, path[:])
246 ## See whether the answer is ready waiting for us.
248 v = CP.RawConfigParser.get(me, sec, key)
249 except CP.NoOptionError:
254 ## No, apparently, not. Find out our list of parents.
256 parents = CP.RawConfigParser.get(me, sec, '@inherit').\
257 replace(',', ' ').split()
258 except CP.NoOptionError:
261 ## Initially we have no idea.
265 ## Go through our parents and ask them what they think.
266 map[sec] = True, None
269 ## See whether we get an answer. If not, keep on going.
270 v, pp = me._get(p, key, map, path)
271 if v is None: continue
273 ## If we got an answer, check that it matches any previous ones.
278 raise AmbiguousOptionError(key, winner, value, pp, v)
280 ## That's the best we could manage.
281 map[sec] = False, value
285 ## Remove us from the path again.
288 def get(me, sec, key, resolvep = True):
290 Retrieve the value of KEY from section SEC.
293 ## Special handling for the `name' key.
295 try: value = CP.RawConfigParser.get(me, sec, key)
296 except CP.NoOptionError: value = sec
298 value, _ = me._get(sec, key)
300 raise MissingKeyException(sec, key)
302 ## Expand the value and return it.
303 return me._expand(sec, value, resolvep)
305 def items(me, sec, resolvep = True):
307 Return a list of (NAME, VALUE) items in section SEC.
309 This extends the default method by handling the inheritance chain.
312 ## Initialize for a depth-first walk of the inheritance graph.
318 ## Visit nodes, collecting their keys. Don't believe the values:
319 ## resolving inheritance is too hard to do like this.
322 if sec in visited: continue
325 for key, value in CP.RawConfigParser.items(me, sec):
326 if key == '@inherit': stack += value.replace(',', ' ').split()
329 ## Now collect the values for the known keys, one by one.
331 for key in d: items.append((key, me.get(basesec, key, resolvep)))
336 ###--------------------------------------------------------------------------
337 ### Command-line handling.
339 def inputiter(things):
341 Iterate over command-line arguments, returning corresponding open files.
343 If none were given, or one is `-', assume standard input; if one is a
344 directory, scan it for files other than backups; otherwise return the
349 if OS.isatty(stdin.fileno()):
350 M.die('no input given, and stdin is a terminal')
356 elif OS.path.isdir(thing):
357 for item in OS.listdir(thing):
358 if item.endswith('~') or item.endswith('#'):
360 name = OS.path.join(thing, item)
361 if not OS.path.isfile(name):
367 def parse_options(argv = argv):
369 Parse command-line options, returning a pair (OPTS, ARGS).
372 op = OptionParser(usage = '%prog [-c CDB] INPUT...',
373 version = '%%prog (tripe, version %s)' % VERSION)
374 op.add_option('-c', '--cdb', metavar = 'CDB',
375 dest = 'cdbfile', default = None,
376 help = 'Compile output into a CDB file.')
377 opts, args = op.parse_args(argv)
380 ###--------------------------------------------------------------------------
385 Read the configuration files and return the accumulated result.
387 We make sure that all hostnames have been properly resolved.
389 conf = MyConfigParser()
390 for f in inputiter(args):
395 def output(conf, cdb):
397 Output the configuration information CONF to the database CDB.
399 This is where the special `user' and `auto' database entries get set.
402 for sec in sorted(conf.sections()):
403 if sec.startswith('@'):
405 elif sec.startswith('$'):
409 if conf.has_option(sec, 'auto') and \
410 conf.get(sec, 'auto') in ('y', 'yes', 't', 'true', '1', 'on'):
412 if conf.has_option(sec, 'user'):
413 cdb.add('U%s' % conf.get(sec, 'user'), sec)
414 url = M.URLEncode(laxp = True, semip = True)
415 for key, value in sorted(conf.items(sec), key = lambda (k, v): k):
416 if not key.startswith('@'):
417 url.encode(key, ' '.join(M.split(value)[0]))
418 cdb.add(label, url.result)
419 cdb.add('%AUTO', ' '.join(auto))
424 opts, args = parse_options()
426 cdb = CDB.cdbmake(opts.cdbfile, opts.cdbfile + '.new')
429 conf = getconf(args[1:])
432 if __name__ == '__main__':
435 ###----- That's all, folks --------------------------------------------------