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