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.
32 from optparse import OptionParser
34 from sys import stdin, stdout, exit, argv
37 from cStringIO import StringIO
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 ResolverFailure (Exception):
55 def __init__(me, host, msg):
59 return "failed to resolve `%s': %s" % (me.host, me.msg)
61 class BulkResolver (object):
63 Resolve a number of DNS names in parallel.
65 The BulkResovler resolves a number of hostnames in parallel. Using it
66 works in three phases:
68 1. You call prepare(HOSTNAME) a number of times, to feed in the hostnames
71 2. You call run() to actually drive the resolver.
73 3. You call lookup(HOSTNAME) to get the address you wanted. This will
74 fail with KeyError if the resolver couldn't resolve the HOSTNAME.
78 """Initialize the resolver."""
82 def prepare(me, host):
83 """Prime the resolver to resolve the name HOST."""
84 if host not in me._resolvers:
85 me._resolvers[host] = M.SelResolveByName \
87 lambda name, alias, addr:
88 me._resolved(host, addr[0]),
89 lambda: me._resolved(host, None))
92 """Run the background DNS resolver until it's finished."""
98 Fetch the address corresponding to HOST.
100 addr = me._namemap[host]
102 raise ResolverFailure(host, '(unknown failure)')
105 def _resolved(me, host, addr):
106 """Callback function: remember that ADDR is the address for HOST."""
107 me._namemap[host] = addr
108 del me._resolvers[host]
110 ###--------------------------------------------------------------------------
111 ### The configuration parser.
113 ## Match a comment or empty line.
114 RX_COMMENT = RX.compile(r'(?x) ^ \s* (?: $ | [;#])')
116 ## Match a section group header.
117 RX_GRPHDR = RX.compile(r'(?x) ^ \s* \[ (.*) \] \s* $')
119 ## Match an assignment line.
120 RX_ASSGN = RX.compile(r'''(?x) ^
121 ([^\s:=] (?: [^:=]* [^\s:=])?)
126 ## Match a continuation line.
127 RX_CONT = RX.compile(r'''(?x) ^ \s+
131 ## Match a $(VAR) configuration variable reference; group 1 is the VAR.
132 RX_REF = RX.compile(r'(?x) \$ \( ([^)]+) \)')
134 ## Match a $[HOST] name resolution reference; group 1 is the HOST.
135 RX_RESOLVE = RX.compile(r'(?x) \$ \[ ([^]]+) \]')
137 class ConfigSyntaxError (Exception):
138 def __init__(me, fname, lno, msg):
143 return '%s:%d: %s' % (me.fname, me.lno, me.msg)
146 return ' -> '.join(["`%s'" % hop for hop in path])
148 class AmbiguousOptionError (Exception):
149 def __init__(me, key, patha, vala, pathb, valb):
151 me.patha, me.vala = patha, vala
152 me.pathb, me.valb = pathb, valb
154 return "Ambiguous answer resolving key `%s': " \
155 "path %s yields `%s' but %s yields `%s'" % \
156 (me.key, _fmt_path(me.patha), me.vala, _fmt_path(me.pathb), me.valb)
158 class InheritanceCycleError (Exception):
159 def __init__(me, key, path):
163 return "Found a cycle %s looking up key `%s'" % \
164 (_fmt_path(me.path), me.key)
166 class MissingSectionException (Exception):
167 def __init__(me, sec):
170 return "Section `%s' not found" % (me.sec)
172 class MissingKeyException (Exception):
173 def __init__(me, sec, key):
177 return "Key `%s' not found in section `%s'" % (me.key, me.sec)
179 class ConfigSection (object):
181 A section in a configuration parser.
183 This is where a lot of the nitty-gritty stuff actually happens. The
184 `MyConfigParser' knows a lot about the internals of this class, which saves
185 on building a complicated interface.
188 def __init__(me, name, cp):
189 """Initialize a new, empty section with a given NAME and parent CP."""
191 ## The cache maps item keys to entries, which consist of a pair of
192 ## objects. There are four possible states for a cache entry:
194 ## * missing -- there is no entry at all with this key, so we must
197 ## * None, None -- we are actively trying to resolve this key, so if we
198 ## encounter this state, we have found a cycle in the inheritance
201 ## * None, [] -- we know that this key isn't reachable through any of
204 ## * VALUE, PATH -- we know that the key resolves to VALUE, along the
205 ## PATH from us (exclusive) to the defining parent (inclusive).
211 def _expand(me, string, resolvep):
213 Expands $(...) and (optionally) $[...] placeholders in STRING.
215 RESOLVEP is a boolean switch: do we bother to tax the resolver or not?
216 This is turned off by MyConfigParser's resolve() method while it's
217 collecting hostnames to be resolved.
219 string = RX_REF.sub \
220 (lambda m: me.get(m.group(1), resolvep), string)
222 string = RX_RESOLVE.sub(lambda m: me._cp._resolver.lookup(m.group(1)),
227 """Yield this section's parents."""
228 try: names = me._itemmap['@inherit']
229 except KeyError: return
230 for name in names.replace(',', ' ').split():
231 yield me._cp.section(name)
233 def _get(me, key, path = None):
235 Low-level option-fetching method.
237 Fetch the value for the named KEY in this section, or maybe (recursively)
238 a section which it inherits from.
240 Returns a pair VALUE, PATH. The value is not expanded; nor do we check
241 for the special `name' key. The caller is expected to do these things.
242 Returns None if no value could be found.
245 ## If we weren't given a path, then we'd better make one.
246 if path is None: path = []
248 ## Extend the path to cover us, but remember to remove us again when
249 ## we've finished. If we need to pass the current path back upwards,
250 ## then remember to take a copy.
254 ## If we've been this way before on another pass through then return the
255 ## value we found then. If we're still thinking about it then we've
257 try: v, p = me._cache[key]
258 except KeyError: pass
260 if p is None: raise InheritanceCycleError(key, path[:])
261 else: return v, path + p
263 ## See whether the answer is ready waiting for us.
264 try: v = me._itemmap[key]
265 except KeyError: pass
268 me._cache[key] = v, []
271 ## Initially we have no idea.
275 ## Go through our parents and ask them what they think.
276 me._cache[key] = None, None
277 for p in me._parents():
279 ## See whether we get an answer. If not, keep on going.
280 v, pp = p._get(key, path)
281 if v is None: continue
283 ## If we got an answer, check that it matches any previous ones.
288 raise AmbiguousOptionError(key, winner, value, pp, v)
290 ## That's the best we could manage.
291 me._cache[key] = value, winner[len(path):]
295 ## Remove us from the path again.
298 def get(me, key, resolvep = True):
300 Retrieve the value of KEY from this section.
303 ## Special handling for the `name' key.
305 value = me._itemmap.get('name', me.name)
306 elif key == '@inherits':
307 try: return me._itemmap['@inherits']
308 except KeyError: raise MissingKeyException(me.name, key)
310 value, _ = me._get(key)
312 raise MissingKeyException(me.name, key)
314 ## Expand the value and return it.
315 return me._expand(value, resolvep)
317 def items(me, resolvep = True):
319 Yield a list of item names in the section.
322 ## Initialize for a depth-first walk of the inheritance graph.
323 seen = { 'name': True }
324 visiting = { me.name: True }
327 ## Visit nodes, collecting their keys. Don't believe the values:
328 ## resolving inheritance is too hard to do like this.
331 for p in sec._parents():
332 if p.name not in visiting:
333 stack.append(p); visiting[p.name] = True
335 for key in sec._itemmap.iterkeys(): seen[key] = None
338 return seen.iterkeys()
340 class MyConfigParser (object):
342 A more advanced configuration parser.
344 This has four major enhancements over the standard ConfigParser which are
347 * It recognizes `@inherits' keys and follows them when expanding a
350 * It recognizes `$(VAR)' references to configuration variables during
351 expansion and processes them correctly.
353 * It recognizes `$[HOST]' name-resolver requests and handles them
356 * Its parsing behaviour is well-defined.
360 1. Call parse(FILENAME) to slurp in the configuration data.
362 2. Call resolve() to collect the hostnames which need to be resolved and
363 actually do the name resolution.
365 3. Call sections() to get a list of the configuration sections, or
366 section(NAME) to find a named section.
368 4. Call get(ITEM) on a section to collect the results, or items() to
374 Initialize a new, empty configuration parser.
377 me._resolver = BulkResolver()
381 Parse configuration from a file F.
384 ## Initial parser state.
390 ## An unpleasant hack. Python makes it hard to capture a value in a
391 ## variable and examine it in a single action, and this is the best that
394 def match(rx): m[0] = rx.match(line); return m[0]
396 ## Commit a key's value when we've determined that there are no further
397 ## continuation lines.
399 if key is not None: sect._itemmap[key] = val.getvalue()
401 ## Work through all of the input lines.
405 if match(RX_COMMENT):
406 ## A comment or a blank line. Nothing doing. (This means that we
407 ## leave out blank lines which look like they might be continuation
412 elif match(RX_GRPHDR):
413 ## A section header. Flush out any previous value and set up the new
418 try: sect = me._sectmap[name]
419 except KeyError: sect = me._sectmap[name] = ConfigSection(name, me)
422 elif match(RX_ASSGN):
423 ## A new assignment. Flush out the old one, and set up to store this
427 raise ConfigSyntaxError(f.name, lno, 'no active section to update')
430 val = StringIO(); val.write(m[0].group(2))
433 ## A continuation line. Accumulate the value.
436 raise ConfigSyntaxError(f.name, lno, 'no config value to continue')
437 val.write('\n'); val.write(m[0].group(1))
442 raise ConfigSyntaxError(f.name, lno, 'incomprehensible line')
444 ## Don't forget to commit any final value material.
447 def section(me, name):
448 """Return a ConfigSection with the given NAME."""
449 try: return me._sectmap[name]
450 except KeyError: raise MissingSectionException(name)
453 """Yield the known sections."""
454 return me._sectmap.itervalues()
458 Works out all of the hostnames which need resolving and resolves them.
460 Until you call this, attempts to fetch configuration items which need to
461 resolve hostnames will fail!
463 for sec in me.sections():
464 for key in sec.items():
465 value = sec.get(key, resolvep = False)
466 for match in RX_RESOLVE.finditer(value):
467 me._resolver.prepare(match.group(1))
470 ###--------------------------------------------------------------------------
471 ### Command-line handling.
473 def inputiter(things):
475 Iterate over command-line arguments, returning corresponding open files.
477 If none were given, or one is `-', assume standard input; if one is a
478 directory, scan it for files other than backups; otherwise return the
483 if OS.isatty(stdin.fileno()):
484 M.die('no input given, and stdin is a terminal')
490 elif OS.path.isdir(thing):
491 for item in OS.listdir(thing):
492 if item.endswith('~') or item.endswith('#'):
494 name = OS.path.join(thing, item)
495 if not OS.path.isfile(name):
501 def parse_options(argv = argv):
503 Parse command-line options, returning a pair (OPTS, ARGS).
506 op = OptionParser(usage = '%prog [-c CDB] INPUT...',
507 version = '%%prog (tripe, version %s)' % VERSION)
508 op.add_option('-c', '--cdb', metavar = 'CDB',
509 dest = 'cdbfile', default = None,
510 help = 'Compile output into a CDB file.')
511 opts, args = op.parse_args(argv)
514 ###--------------------------------------------------------------------------
519 Read the configuration files and return the accumulated result.
521 We make sure that all hostnames have been properly resolved.
523 conf = MyConfigParser()
524 for f in inputiter(args):
529 def output(conf, cdb):
531 Output the configuration information CONF to the database CDB.
533 This is where the special `user' and `auto' database entries get set.
536 for sec in sorted(conf.sections(), key = lambda sec: sec.name):
537 if sec.name.startswith('@'):
539 elif sec.name.startswith('$'):
542 label = 'P%s' % sec.name
543 try: a = sec.get('auto')
544 except MissingKeyException: pass
546 if a in ('y', 'yes', 't', 'true', '1', 'on'): auto.append(sec.name)
547 try: u = sec.get('user')
548 except MissingKeyException: pass
549 else: cdb.add('U%s' % u)
550 url = M.URLEncode(semip = True)
551 for key in sorted(sec.items()):
552 if not key.startswith('@'):
553 url.encode(key, sec.get(key))
554 cdb.add(label, url.result)
555 cdb.add('%AUTO', ' '.join(auto))
560 opts, args = parse_options()
562 cdb = CDB.cdbmake(opts.cdbfile, opts.cdbfile + '.new')
565 conf = getconf(args[1:])
568 if __name__ == '__main__':
571 ###----- That's all, folks --------------------------------------------------