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 class ExpectedError (Exception): pass
53 ###--------------------------------------------------------------------------
54 ### A bulk DNS resolver.
56 class ResolverFailure (ExpectedError):
57 def __init__(me, host, msg):
61 return "failed to resolve `%s': %s" % (me.host, me.msg)
63 class ResolvingHost (object):
65 A host name which is being looked up by a bulk-resolver instance.
68 def __init__(me, name):
69 """Make a new resolving-host object for the host NAME."""
74 def setaddr(me, addr):
75 """Add the address ADDR."""
80 Report that resolution of this host failed, with a human-readable MSG.
85 """Return the resolved address."""
86 if me.failure is not None: raise ResolverFailure(me.name, me.failure)
89 class BulkResolver (object):
91 Resolve a number of DNS names in parallel.
93 The BulkResovler resolves a number of hostnames in parallel. Using it
94 works in three phases:
96 1. You call prepare(HOSTNAME) a number of times, to feed in the hostnames
99 2. You call run() to actually drive the resolver.
101 3. You call lookup(HOSTNAME) to get the address you wanted. This will
102 fail with KeyError if the resolver couldn't resolve the HOSTNAME.
106 """Initialize the resolver."""
110 def prepare(me, name):
111 """Prime the resolver to resolve the given host NAME."""
112 if name not in me._namemap:
113 me._namemap[name] = host = ResolvingHost(name)
114 host._resolv = M.SelResolveByName(
116 lambda cname, alias, addr: me._resolved(host, addr[0]),
117 lambda: me._resolved(host, None))
121 """Run the background DNS resolver until it's finished."""
122 while me._noutstand: M.select()
124 def lookup(me, name):
125 """Fetch the address corresponding to the host NAME."""
126 return me._namemap[name].get()
128 def _resolved(me, host, addr):
129 """Callback function: remember that ADDR is the address for HOST."""
131 host.failed('(unknown failure)')
137 ###--------------------------------------------------------------------------
138 ### The configuration parser.
140 ## Match a comment or empty line.
141 RX_COMMENT = RX.compile(r'(?x) ^ \s* (?: $ | [;#])')
143 ## Match a section group header.
144 RX_GRPHDR = RX.compile(r'(?x) ^ \s* \[ (.*) \] \s* $')
146 ## Match an assignment line.
147 RX_ASSGN = RX.compile(r'''(?x) ^
148 ([^\s:=] (?: [^:=]* [^\s:=])?)
153 ## Match a continuation line.
154 RX_CONT = RX.compile(r'''(?x) ^ \s+
158 ## Match a $(VAR) configuration variable reference; group 1 is the VAR.
159 RX_REF = RX.compile(r'(?x) \$ \( ([^)]+) \)')
161 ## Match a $[HOST] name resolution reference; group 1 is the HOST.
162 RX_RESOLVE = RX.compile(r'(?x) \$ \[ ([^]]+) \]')
164 class ConfigSyntaxError (ExpectedError):
165 def __init__(me, fname, lno, msg):
170 return '%s:%d: %s' % (me.fname, me.lno, me.msg)
173 return ' -> '.join(["`%s'" % hop for hop in path])
175 class AmbiguousOptionError (ExpectedError):
176 def __init__(me, key, patha, vala, pathb, valb):
178 me.patha, me.vala = patha, vala
179 me.pathb, me.valb = pathb, valb
181 return "Ambiguous answer resolving key `%s': " \
182 "path %s yields `%s' but %s yields `%s'" % \
183 (me.key, _fmt_path(me.patha), me.vala, _fmt_path(me.pathb), me.valb)
185 class InheritanceCycleError (ExpectedError):
186 def __init__(me, key, path):
190 return "Found a cycle %s looking up key `%s'" % \
191 (_fmt_path(me.path), me.key)
193 class MissingSectionException (ExpectedError):
194 def __init__(me, sec):
197 return "Section `%s' not found" % (me.sec)
199 class MissingKeyException (ExpectedError):
200 def __init__(me, sec, key):
204 return "Key `%s' not found in section `%s'" % (me.key, me.sec)
206 class ConfigSection (object):
208 A section in a configuration parser.
210 This is where a lot of the nitty-gritty stuff actually happens. The
211 `MyConfigParser' knows a lot about the internals of this class, which saves
212 on building a complicated interface.
215 def __init__(me, name, cp):
216 """Initialize a new, empty section with a given NAME and parent CP."""
218 ## The cache maps item keys to entries, which consist of a pair of
219 ## objects. There are four possible states for a cache entry:
221 ## * missing -- there is no entry at all with this key, so we must
224 ## * None, None -- we are actively trying to resolve this key, so if we
225 ## encounter this state, we have found a cycle in the inheritance
228 ## * None, [] -- we know that this key isn't reachable through any of
231 ## * VALUE, PATH -- we know that the key resolves to VALUE, along the
232 ## PATH from us (exclusive) to the defining parent (inclusive).
238 def _expand(me, string, resolvep):
240 Expands $(...) and (optionally) $[...] placeholders in STRING.
242 RESOLVEP is a boolean switch: do we bother to tax the resolver or not?
243 This is turned off by MyConfigParser's resolve() method while it's
244 collecting hostnames to be resolved.
246 string = RX_REF.sub \
247 (lambda m: me.get(m.group(1), resolvep), string)
249 string = RX_RESOLVE.sub(lambda m: me._cp._resolver.lookup(m.group(1)),
254 """Yield this section's parents."""
255 try: names = me._itemmap['@inherit']
256 except KeyError: return
257 for name in names.replace(',', ' ').split():
258 yield me._cp.section(name)
260 def _get(me, key, path = None):
262 Low-level option-fetching method.
264 Fetch the value for the named KEY in this section, or maybe (recursively)
265 a section which it inherits from.
267 Returns a pair VALUE, PATH. The value is not expanded; nor do we check
268 for the special `name' key. The caller is expected to do these things.
269 Returns None if no value could be found.
272 ## If we weren't given a path, then we'd better make one.
273 if path is None: path = []
275 ## Extend the path to cover us, but remember to remove us again when
276 ## we've finished. If we need to pass the current path back upwards,
277 ## then remember to take a copy.
281 ## If we've been this way before on another pass through then return the
282 ## value we found then. If we're still thinking about it then we've
284 try: v, p = me._cache[key]
285 except KeyError: pass
287 if p is None: raise InheritanceCycleError(key, path[:])
288 else: return v, path + p
290 ## See whether the answer is ready waiting for us.
291 try: v = me._itemmap[key]
292 except KeyError: pass
295 me._cache[key] = v, []
298 ## Initially we have no idea.
302 ## Go through our parents and ask them what they think.
303 me._cache[key] = None, None
304 for p in me._parents():
306 ## See whether we get an answer. If not, keep on going.
307 v, pp = p._get(key, path)
308 if v is None: continue
310 ## If we got an answer, check that it matches any previous ones.
315 raise AmbiguousOptionError(key, winner, value, pp, v)
317 ## That's the best we could manage.
318 me._cache[key] = value, winner[len(path):]
322 ## Remove us from the path again.
325 def get(me, key, resolvep = True):
327 Retrieve the value of KEY from this section.
330 ## Special handling for the `name' key.
332 value = me._itemmap.get('name', me.name)
333 elif key == '@inherits':
334 try: return me._itemmap['@inherits']
335 except KeyError: raise MissingKeyException(me.name, key)
337 value, _ = me._get(key)
339 raise MissingKeyException(me.name, key)
341 ## Expand the value and return it.
342 return me._expand(value, resolvep)
344 def items(me, resolvep = True):
346 Yield a list of item names in the section.
349 ## Initialize for a depth-first walk of the inheritance graph.
350 seen = { 'name': True }
351 visiting = { me.name: True }
354 ## Visit nodes, collecting their keys. Don't believe the values:
355 ## resolving inheritance is too hard to do like this.
358 for p in sec._parents():
359 if p.name not in visiting:
360 stack.append(p); visiting[p.name] = True
362 for key in sec._itemmap.iterkeys(): seen[key] = None
365 return seen.iterkeys()
367 class MyConfigParser (object):
369 A more advanced configuration parser.
371 This has four major enhancements over the standard ConfigParser which are
374 * It recognizes `@inherits' keys and follows them when expanding a
377 * It recognizes `$(VAR)' references to configuration variables during
378 expansion and processes them correctly.
380 * It recognizes `$[HOST]' name-resolver requests and handles them
383 * Its parsing behaviour is well-defined.
387 1. Call parse(FILENAME) to slurp in the configuration data.
389 2. Call resolve() to collect the hostnames which need to be resolved and
390 actually do the name resolution.
392 3. Call sections() to get a list of the configuration sections, or
393 section(NAME) to find a named section.
395 4. Call get(ITEM) on a section to collect the results, or items() to
401 Initialize a new, empty configuration parser.
404 me._resolver = BulkResolver()
408 Parse configuration from a file F.
411 ## Initial parser state.
417 ## An unpleasant hack. Python makes it hard to capture a value in a
418 ## variable and examine it in a single action, and this is the best that
421 def match(rx): m[0] = rx.match(line); return m[0]
423 ## Commit a key's value when we've determined that there are no further
424 ## continuation lines.
426 if key is not None: sect._itemmap[key] = val.getvalue()
428 ## Work through all of the input lines.
432 if match(RX_COMMENT):
433 ## A comment or a blank line. Nothing doing. (This means that we
434 ## leave out blank lines which look like they might be continuation
439 elif match(RX_GRPHDR):
440 ## A section header. Flush out any previous value and set up the new
445 try: sect = me._sectmap[name]
446 except KeyError: sect = me._sectmap[name] = ConfigSection(name, me)
449 elif match(RX_ASSGN):
450 ## A new assignment. Flush out the old one, and set up to store this
454 raise ConfigSyntaxError(f.name, lno, 'no active section to update')
457 val = StringIO(); val.write(m[0].group(2))
460 ## A continuation line. Accumulate the value.
463 raise ConfigSyntaxError(f.name, lno, 'no config value to continue')
464 val.write('\n'); val.write(m[0].group(1))
469 raise ConfigSyntaxError(f.name, lno, 'incomprehensible line')
471 ## Don't forget to commit any final value material.
474 def section(me, name):
475 """Return a ConfigSection with the given NAME."""
476 try: return me._sectmap[name]
477 except KeyError: raise MissingSectionException(name)
480 """Yield the known sections."""
481 return me._sectmap.itervalues()
485 Works out all of the hostnames which need resolving and resolves them.
487 Until you call this, attempts to fetch configuration items which need to
488 resolve hostnames will fail!
490 for sec in me.sections():
491 for key in sec.items():
492 value = sec.get(key, resolvep = False)
493 for match in RX_RESOLVE.finditer(value):
494 me._resolver.prepare(match.group(1))
497 ###--------------------------------------------------------------------------
498 ### Command-line handling.
500 def inputiter(things):
502 Iterate over command-line arguments, returning corresponding open files.
504 If none were given, or one is `-', assume standard input; if one is a
505 directory, scan it for files other than backups; otherwise return the
510 if OS.isatty(stdin.fileno()):
511 M.die('no input given, and stdin is a terminal')
517 elif OS.path.isdir(thing):
518 for item in OS.listdir(thing):
519 if item.endswith('~') or item.endswith('#'):
521 name = OS.path.join(thing, item)
522 if not OS.path.isfile(name):
528 def parse_options(argv = argv):
530 Parse command-line options, returning a pair (OPTS, ARGS).
533 op = OptionParser(usage = '%prog [-c CDB] INPUT...',
534 version = '%%prog (tripe, version %s)' % VERSION)
535 op.add_option('-c', '--cdb', metavar = 'CDB',
536 dest = 'cdbfile', default = None,
537 help = 'Compile output into a CDB file.')
538 opts, args = op.parse_args(argv)
541 ###--------------------------------------------------------------------------
546 Read the configuration files and return the accumulated result.
548 We make sure that all hostnames have been properly resolved.
550 conf = MyConfigParser()
551 for f in inputiter(args):
556 def output(conf, cdb):
558 Output the configuration information CONF to the database CDB.
560 This is where the special `user' and `auto' database entries get set.
563 for sec in sorted(conf.sections(), key = lambda sec: sec.name):
564 if sec.name.startswith('@'):
566 elif sec.name.startswith('$'):
569 label = 'P%s' % sec.name
570 try: a = sec.get('auto')
571 except MissingKeyException: pass
573 if a in ('y', 'yes', 't', 'true', '1', 'on'): auto.append(sec.name)
574 try: u = sec.get('user')
575 except MissingKeyException: pass
576 else: cdb.add('U%s' % u)
577 url = M.URLEncode(semip = True)
578 for key in sorted(sec.items()):
579 if not key.startswith('@'):
580 url.encode(key, sec.get(key))
581 cdb.add(label, url.result)
582 cdb.add('%AUTO', ' '.join(auto))
587 opts, args = parse_options()
589 cdb = CDB.cdbmake(opts.cdbfile, opts.cdbfile + '.new')
593 conf = getconf(args[1:])
595 except ExpectedError, e:
599 if __name__ == '__main__':
602 ###----- That's all, folks --------------------------------------------------