8b4d800c41f92acb01605845c53a1bb3a23a7dfb
[tripe] / peerdb / tripe-newpeers.in
1 #! @PYTHON@
2 ### -*-python-*-
3 ###
4 ### Build a CDB file from configuration file
5 ###
6 ### (c) 2007 Straylight/Edgeware
7 ###
8
9 ###----- Licensing notice ---------------------------------------------------
10 ###
11 ### This file is part of Trivial IP Encryption (TrIPE).
12 ###
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.
17 ###
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
21 ### for more details.
22 ###
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/>.
25
26 VERSION = '@VERSION@'
27
28 ###--------------------------------------------------------------------------
29 ### External dependencies.
30
31 import mLib as M
32 from optparse import OptionParser
33 import cdb as CDB
34 from sys import stdin, stdout, exit, argv
35 import re as RX
36 import os as OS
37 from cStringIO import StringIO
38
39 ###--------------------------------------------------------------------------
40 ### Utilities.
41
42 class CDBFake (object):
43 """Like cdbmake, but just outputs data suitable for cdb-map."""
44 def __init__(me, file = stdout):
45 me.file = file
46 def add(me, key, value):
47 me.file.write('%s:%s\n' % (key, value))
48 def finish(me):
49 pass
50
51 ###--------------------------------------------------------------------------
52 ### A bulk DNS resolver.
53
54 class BulkResolver (object):
55 """
56 Resolve a number of DNS names in parallel.
57
58 The BulkResovler resolves a number of hostnames in parallel. Using it
59 works in three phases:
60
61 1. You call prepare(HOSTNAME) a number of times, to feed in the hostnames
62 you're interested in.
63
64 2. You call run() to actually drive the resolver.
65
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.
68 """
69
70 def __init__(me):
71 """Initialize the resolver."""
72 me._resolvers = {}
73 me._namemap = {}
74
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 \
79 (host,
80 lambda name, alias, addr:
81 me._resolved(host, addr[0]),
82 lambda: me._resolved(host, None))
83
84 def run(me):
85 """Run the background DNS resolver until it's finished."""
86 while me._resolvers:
87 M.select()
88
89 def lookup(me, host):
90 """
91 Fetch the address corresponding to HOST.
92 """
93 addr = me._namemap[host]
94 if addr is None:
95 raise KeyError(host)
96 return addr
97
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]
102
103 ###--------------------------------------------------------------------------
104 ### The configuration parser.
105
106 ## Match a comment or empty line.
107 RX_COMMENT = RX.compile(r'(?x) ^ \s* (?: $ | [;#])')
108
109 ## Match a section group header.
110 RX_GRPHDR = RX.compile(r'(?x) ^ \s* \[ (.*) \] \s* $')
111
112 ## Match an assignment line.
113 RX_ASSGN = RX.compile(r'''(?x) ^
114 ([^\s:=] (?: [^:=]* [^\s:=])?)
115 \s* [:=] \s*
116 (| \S | \S.*\S)
117 \s* $''')
118
119 ## Match a continuation line.
120 RX_CONT = RX.compile(r'''(?x) ^ \s+
121 (| \S | \S.*\S)
122 \s* $''')
123
124 ## Match a $(VAR) configuration variable reference; group 1 is the VAR.
125 RX_REF = RX.compile(r'(?x) \$ \( ([^)]+) \)')
126
127 ## Match a $[HOST] name resolution reference; group 1 is the HOST.
128 RX_RESOLVE = RX.compile(r'(?x) \$ \[ ([^]]+) \]')
129
130 class ConfigSyntaxError (Exception):
131 def __init__(me, fname, lno, msg):
132 me.fname = fname
133 me.lno = lno
134 me.msg = msg
135 def __str__(me):
136 return '%s:%d: %s' % (me.fname, me.lno, me.msg)
137
138 def _fmt_path(path):
139 return ' -> '.join(["`%s'" % hop for hop in path])
140
141 class AmbiguousOptionError (Exception):
142 def __init__(me, key, patha, vala, pathb, valb):
143 me.key = key
144 me.patha, me.vala = patha, vala
145 me.pathb, me.valb = pathb, valb
146 def __str__(me):
147 return "Ambiguous answer resolving key `%s': " \
148 "path %s yields `%s' but %s yields `%s'" % \
149 (me.key, _fmt_path(me.patha), me.vala, _fmt_path(me.pathb), me.valb)
150
151 class InheritanceCycleError (Exception):
152 def __init__(me, key, path):
153 me.key = key
154 me.path = path
155 def __str__(me):
156 return "Found a cycle %s looking up key `%s'" % \
157 (_fmt_path(me.path), me.key)
158
159 class MissingSectionException (Exception):
160 def __init__(me, sec):
161 me.key = key
162 def __str__(me):
163 return "Section `%s' not found" % (me.sec)
164
165 class MissingKeyException (Exception):
166 def __init__(me, sec, key):
167 me.sec = sec
168 me.key = key
169 def __str__(me):
170 return "Key `%s' not found in section `%s'" % (me.key, me.sec)
171
172 class ConfigSection (object):
173 """
174 A section in a configuration parser.
175
176 This is where a lot of the nitty-gritty stuff actually happens. The
177 `MyConfigParser' knows a lot about the internals of this class, which saves
178 on building a complicated interface.
179 """
180
181 def __init__(me, name, cp):
182 """Initialize a new, empty section with a given NAME and parent CP."""
183 me.name = name
184 me._itemmap = dict()
185 me._cp = cp
186
187 def _expand(me, string, resolvep):
188 """
189 Expands $(...) and (optionally) $[...] placeholders in STRING.
190
191 RESOLVEP is a boolean switch: do we bother to tax the resolver or not?
192 This is turned off by MyConfigParser's resolve() method while it's
193 collecting hostnames to be resolved.
194 """
195 string = RX_REF.sub \
196 (lambda m: me.get(m.group(1), resolvep), string)
197 if resolvep:
198 string = RX_RESOLVE.sub(lambda m: me._cp._resolver.lookup(m.group(1)),
199 string)
200 return string
201
202 def _parents(me):
203 """Yield this section's parents."""
204 try: names = me._itemmap['@inherit']
205 except KeyError: return
206 for name in names.replace(',', ' ').split():
207 yield me._cp.section(name)
208
209 def _get(me, key, map = None, path = None):
210 """
211 Low-level option-fetching method.
212
213 Fetch the value for the named KEY in this section, or maybe (recursively)
214 a section which it inherits from.
215
216 Returns a pair VALUE, PATH. The value is not expanded; nor do we check
217 for the special `name' key. The caller is expected to do these things.
218 Returns None if no value could be found.
219 """
220
221 ## If we weren't given a memoization map or path, then we'd better make
222 ## one.
223 if map is None: map = {}
224 if path is None: path = []
225
226 ## Extend the path to cover us, but remember to remove us again when
227 ## we've finished. If we need to pass the current path back upwards,
228 ## then remember to take a copy.
229 path.append(me.name)
230 try:
231
232 ## If we've been this way before on another pass through then return
233 ## the value we found then. If we're still thinking about it then
234 ## we've found a cycle.
235 try: threadp, value = map[me.name]
236 except KeyError: pass
237 else:
238 if threadp: raise InheritanceCycleError(key, path[:])
239
240 ## See whether the answer is ready waiting for us.
241 try: v = me._itemmap[key]
242 except KeyError: pass
243 else: return v, path[:]
244
245 ## Initially we have no idea.
246 value = None
247 winner = None
248
249 ## Go through our parents and ask them what they think.
250 map[me.name] = True, None
251 for p in me._parents():
252
253 ## See whether we get an answer. If not, keep on going.
254 v, pp = p._get(key, map, path)
255 if v is None: continue
256
257 ## If we got an answer, check that it matches any previous ones.
258 if value is None:
259 value = v
260 winner = pp
261 elif value != v:
262 raise AmbiguousOptionError(key, winner, value, pp, v)
263
264 ## That's the best we could manage.
265 map[me.name] = False, value
266 return value, winner
267
268 finally:
269 ## Remove us from the path again.
270 path.pop()
271
272 def get(me, key, resolvep = True):
273 """
274 Retrieve the value of KEY from this section.
275 """
276
277 ## Special handling for the `name' key.
278 if key == 'name':
279 value = me._itemmap.get('name', me.name)
280 else:
281 value, _ = me._get(key)
282 if value is None:
283 raise MissingKeyException(me.name, key)
284
285 ## Expand the value and return it.
286 return me._expand(value, resolvep)
287
288 def items(me, resolvep = True):
289 """
290 Yield a list of item names in the section.
291 """
292
293 ## Initialize for a depth-first walk of the inheritance graph.
294 d = {}
295 visited = {}
296 stack = [me]
297
298 ## Visit nodes, collecting their keys. Don't believe the values:
299 ## resolving inheritance is too hard to do like this.
300 while stack:
301 sec = stack.pop()
302 if sec.name in visited: continue
303 visited[sec.name] = True
304 stack += sec._parents()
305
306 for key in sec._itemmap.iterkeys():
307 if key != '@inherit': d[key] = None
308
309 ## And we're done.
310 return d.iterkeys()
311
312 class MyConfigParser (object):
313 """
314 A more advanced configuration parser.
315
316 This has four major enhancements over the standard ConfigParser which are
317 relevant to us.
318
319 * It recognizes `@inherits' keys and follows them when expanding a
320 value.
321
322 * It recognizes `$(VAR)' references to configuration variables during
323 expansion and processes them correctly.
324
325 * It recognizes `$[HOST]' name-resolver requests and handles them
326 correctly.
327
328 * Its parsing behaviour is well-defined.
329
330 Use:
331
332 1. Call parse(FILENAME) to slurp in the configuration data.
333
334 2. Call resolve() to collect the hostnames which need to be resolved and
335 actually do the name resolution.
336
337 3. Call sections() to get a list of the configuration sections, or
338 section(NAME) to find a named section.
339
340 4. Call get(ITEM) on a section to collect the results, or items() to
341 iterate over them.
342 """
343
344 def __init__(me):
345 """
346 Initialize a new, empty configuration parser.
347 """
348 me._sectmap = dict()
349 me._resolver = BulkResolver()
350
351 def parse(me, f):
352 """
353 Parse configuration from a file F.
354 """
355
356 ## Initial parser state.
357 sect = None
358 key = None
359 val = None
360 lno = 0
361
362 ## An unpleasant hack. Python makes it hard to capture a value in a
363 ## variable and examine it in a single action, and this is the best that
364 ## I came up with.
365 m = [None]
366 def match(rx): m[0] = rx.match(line); return m[0]
367
368 ## Commit a key's value when we've determined that there are no further
369 ## continuation lines.
370 def flush():
371 if key is not None: sect._itemmap[key] = val.getvalue()
372
373 ## Work through all of the input lines.
374 for line in f:
375 lno += 1
376
377 if match(RX_COMMENT):
378 ## A comment or a blank line. Nothing doing. (This means that we
379 ## leave out blank lines which look like they might be continuation
380 ## lines.)
381
382 pass
383
384 elif match(RX_GRPHDR):
385 ## A section header. Flush out any previous value and set up the new
386 ## group.
387
388 flush()
389 name = m[0].group(1)
390 try: sect = me._sectmap[name]
391 except KeyError: sect = me._sectmap[name] = ConfigSection(name, me)
392 key = None
393
394 elif match(RX_ASSGN):
395 ## A new assignment. Flush out the old one, and set up to store this
396 ## one.
397
398 if sect is None:
399 raise ConfigSyntaxError(f.name, lno, 'no active section to update')
400 flush()
401 key = m[0].group(1)
402 val = StringIO(); val.write(m[0].group(2))
403
404 elif match(RX_CONT):
405 ## A continuation line. Accumulate the value.
406
407 if key is None:
408 raise ConfigSyntaxError(f.name, lno, 'no config value to continue')
409 val.write('\n'); val.write(m[0].group(1))
410
411 else:
412 ## Something else.
413
414 raise ConfigSyntaxError(f.name, lno, 'incomprehensible line')
415
416 ## Don't forget to commit any final value material.
417 flush()
418
419 def section(me, name):
420 """Return a ConfigSection with the given NAME."""
421 try: return me._sectmap[name]
422 except KeyError: raise MissingSectionException(name)
423
424 def sections(me):
425 """Yield the known sections."""
426 return me._sectmap.itervalues()
427
428 def resolve(me):
429 """
430 Works out all of the hostnames which need resolving and resolves them.
431
432 Until you call this, attempts to fetch configuration items which need to
433 resolve hostnames will fail!
434 """
435 for sec in me.sections():
436 for key in sec.items():
437 value = sec.get(key, resolvep = False)
438 for match in RX_RESOLVE.finditer(value):
439 me._resolver.prepare(match.group(1))
440 me._resolver.run()
441
442 ###--------------------------------------------------------------------------
443 ### Command-line handling.
444
445 def inputiter(things):
446 """
447 Iterate over command-line arguments, returning corresponding open files.
448
449 If none were given, or one is `-', assume standard input; if one is a
450 directory, scan it for files other than backups; otherwise return the
451 opened files.
452 """
453
454 if not things:
455 if OS.isatty(stdin.fileno()):
456 M.die('no input given, and stdin is a terminal')
457 yield stdin
458 else:
459 for thing in things:
460 if thing == '-':
461 yield stdin
462 elif OS.path.isdir(thing):
463 for item in OS.listdir(thing):
464 if item.endswith('~') or item.endswith('#'):
465 continue
466 name = OS.path.join(thing, item)
467 if not OS.path.isfile(name):
468 continue
469 yield file(name)
470 else:
471 yield file(thing)
472
473 def parse_options(argv = argv):
474 """
475 Parse command-line options, returning a pair (OPTS, ARGS).
476 """
477 M.ego(argv[0])
478 op = OptionParser(usage = '%prog [-c CDB] INPUT...',
479 version = '%%prog (tripe, version %s)' % VERSION)
480 op.add_option('-c', '--cdb', metavar = 'CDB',
481 dest = 'cdbfile', default = None,
482 help = 'Compile output into a CDB file.')
483 opts, args = op.parse_args(argv)
484 return opts, args
485
486 ###--------------------------------------------------------------------------
487 ### Main code.
488
489 def getconf(args):
490 """
491 Read the configuration files and return the accumulated result.
492
493 We make sure that all hostnames have been properly resolved.
494 """
495 conf = MyConfigParser()
496 for f in inputiter(args):
497 conf.parse(f)
498 conf.resolve()
499 return conf
500
501 def output(conf, cdb):
502 """
503 Output the configuration information CONF to the database CDB.
504
505 This is where the special `user' and `auto' database entries get set.
506 """
507 auto = []
508 for sec in sorted(conf.sections(), key = lambda sec: sec.name):
509 if sec.name.startswith('@'):
510 continue
511 elif sec.name.startswith('$'):
512 label = sec.name
513 else:
514 label = 'P%s' % sec.name
515 try: a = sec.get('auto')
516 except MissingKeyException: pass
517 else:
518 if a in ('y', 'yes', 't', 'true', '1', 'on'): auto.append(sec.name)
519 try: u = sec.get('user')
520 except MissingKeyException: pass
521 else: cdb.add('U%s' % u)
522 url = M.URLEncode(semip = True)
523 for key in sorted(sec.items()):
524 if not key.startswith('@'):
525 url.encode(key, sec.get(key))
526 cdb.add(label, url.result)
527 cdb.add('%AUTO', ' '.join(auto))
528 cdb.finish()
529
530 def main():
531 """Main program."""
532 opts, args = parse_options()
533 if opts.cdbfile:
534 cdb = CDB.cdbmake(opts.cdbfile, opts.cdbfile + '.new')
535 else:
536 cdb = CDBFake()
537 conf = getconf(args[1:])
538 output(conf, cdb)
539
540 if __name__ == '__main__':
541 main()
542
543 ###----- That's all, folks --------------------------------------------------