peerdb/tripe-newpeers.in (MyConfigParser): Abandon Python `ConfigParser'.
[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
51###--------------------------------------------------------------------------
52### A bulk DNS resolver.
53
54class 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."""
d8310a3a
MW
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))
6005ef9b
MW
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:
171206b5 95 raise KeyError(host)
6005ef9b
MW
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
b7e5aa06
MW
106## Match a comment or empty line.
107RX_COMMENT = RX.compile(r'(?x) ^ \s* (?: $ | [;#])')
108
109## Match a section group header.
110RX_GRPHDR = RX.compile(r'(?x) ^ \s* \[ (.*) \] \s* $')
111
112## Match an assignment line.
113RX_ASSGN = RX.compile(r'''(?x) ^
114 ([^\s:=] (?: [^:=]* [^\s:=])?)
115 \s* [:=] \s*
116 (| \S | \S.*\S)
117 \s* $''')
118
119## Match a continuation line.
120RX_CONT = RX.compile(r'''(?x) ^ \s+
121 (| \S | \S.*\S)
122 \s* $''')
123
6005ef9b 124## Match a $(VAR) configuration variable reference; group 1 is the VAR.
2d51bc9f 125RX_REF = RX.compile(r'(?x) \$ \( ([^)]+) \)')
6005ef9b
MW
126
127## Match a $[HOST] name resolution reference; group 1 is the HOST.
2d51bc9f 128RX_RESOLVE = RX.compile(r'(?x) \$ \[ ([^]]+) \]')
6005ef9b 129
b7e5aa06
MW
130class 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
bd3db76c
MW
138def _fmt_path(path):
139 return ' -> '.join(["`%s'" % hop for hop in path])
140
141class 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
151class 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
159class MissingKeyException (Exception):
160 def __init__(me, sec, key):
161 me.sec = sec
162 me.key = key
163 def __str__(me):
164 return "Key `%s' not found in section `%s'" % (me.key, me.sec)
165
b7e5aa06 166class MyConfigParser (object):
6005ef9b
MW
167 """
168 A more advanced configuration parser.
169
b7e5aa06 170 This has four major enhancements over the standard ConfigParser which are
6005ef9b
MW
171 relevant to us.
172
173 * It recognizes `@inherits' keys and follows them when expanding a
174 value.
175
176 * It recognizes `$(VAR)' references to configuration variables during
177 expansion and processes them correctly.
178
179 * It recognizes `$[HOST]' name-resolver requests and handles them
180 correctly.
181
b7e5aa06
MW
182 * Its parsing behaviour is well-defined.
183
6005ef9b
MW
184 Use:
185
b7e5aa06 186 1. Call parse(FILENAME) to slurp in the configuration data.
6005ef9b
MW
187
188 2. Call resolve() to collect the hostnames which need to be resolved and
189 actually do the name resolution.
190
191 3. Call get(SECTION, ITEM) to collect the results, or items(SECTION) to
192 iterate over them.
193 """
194
195 def __init__(me):
196 """
197 Initialize a new, empty configuration parser.
198 """
b7e5aa06 199 me._sectmap = dict()
6005ef9b
MW
200 me._resolver = BulkResolver()
201
b7e5aa06
MW
202 def parse(me, f):
203 """
204 Parse configuration from a file F.
205 """
206
207 ## Initial parser state.
208 sect = None
209 key = None
210 val = None
211 lno = 0
212
213 ## An unpleasant hack. Python makes it hard to capture a value in a
214 ## variable and examine it in a single action, and this is the best that
215 ## I came up with.
216 m = [None]
217 def match(rx): m[0] = rx.match(line); return m[0]
218
219 ## Commit a key's value when we've determined that there are no further
220 ## continuation lines.
221 def flush():
222 if key is not None: sect[key] = val.getvalue()
223
224 ## Work through all of the input lines.
225 for line in f:
226 lno += 1
227
228 if match(RX_COMMENT):
229 ## A comment or a blank line. Nothing doing. (This means that we
230 ## leave out blank lines which look like they might be continuation
231 ## lines.)
232
233 pass
234
235 elif match(RX_GRPHDR):
236 ## A section header. Flush out any previous value and set up the new
237 ## group.
238
239 flush()
240 name = m[0].group(1)
241 try: sect = me._sectmap[name]
242 except KeyError: sect = me._sectmap[name] = dict()
243 key = None
244
245 elif match(RX_ASSGN):
246 ## A new assignment. Flush out the old one, and set up to store this
247 ## one.
248
249 if sect is None:
250 raise ConfigSyntaxError(f.name, lno, 'no active section to update')
251 flush()
252 key = m[0].group(1)
253 val = StringIO(); val.write(m[0].group(2))
254
255 elif match(RX_CONT):
256 ## A continuation line. Accumulate the value.
257
258 if key is None:
259 raise ConfigSyntaxError(f.name, lno, 'no config value to continue')
260 val.write('\n'); val.write(m[0].group(1))
261
262 else:
263 ## Something else.
264
265 raise ConfigSyntaxError(f.name, lno, 'incomprehensible line')
266
267 ## Don't forget to commit any final value material.
268 flush()
269
270 def sections(me):
271 """Yield the known section names."""
272 return me._sectmap.iterkeys()
273
6005ef9b
MW
274 def resolve(me):
275 """
276 Works out all of the hostnames which need resolving and resolves them.
277
278 Until you call this, attempts to fetch configuration items which need to
279 resolve hostnames will fail!
280 """
b7e5aa06 281 for sec in me._sectmap.iterkeys():
6005ef9b 282 for key, value in me.items(sec, resolvep = False):
2d51bc9f 283 for match in RX_RESOLVE.finditer(value):
6005ef9b
MW
284 me._resolver.prepare(match.group(1))
285 me._resolver.run()
286
287 def _expand(me, sec, string, resolvep):
288 """
289 Expands $(...) and (optionally) $[...] placeholders in STRING.
290
291 The SEC is the configuration section from which to satisfy $(...)
292 requests. RESOLVEP is a boolean switch: do we bother to tax the resolver
293 or not? This is turned off by the resolve() method while it's collecting
294 hostnames to be resolved.
295 """
2d51bc9f 296 string = RX_REF.sub \
6005ef9b
MW
297 (lambda m: me.get(sec, m.group(1), resolvep), string)
298 if resolvep:
2d51bc9f
MW
299 string = RX_RESOLVE.sub(lambda m: me._resolver.lookup(m.group(1)),
300 string)
6005ef9b
MW
301 return string
302
303 def has_option(me, sec, key):
304 """
305 Decide whether section SEC has a configuration key KEY.
306
307 This version of the method properly handles the @inherit key.
308 """
bd3db76c 309 return key == 'name' or me._get(sec, key)[0] is not None
6005ef9b 310
bd3db76c 311 def _get(me, sec, key, map = None, path = None):
6005ef9b
MW
312 """
313 Low-level option-fetching method.
314
315 Fetch the value for the named KEY from section SEC, or maybe
bd3db76c 316 (recursively) a section which SEC inherits from.
6005ef9b 317
bd3db76c
MW
318 Returns a pair VALUE, PATH. The value is not expanded; nor do we check
319 for the special `name' key. The caller is expected to do these things.
320 Returns None if no value could be found.
321 """
6005ef9b 322
bd3db76c
MW
323 ## If we weren't given a memoization map or path, then we'd better make
324 ## one.
325 if map is None: map = {}
326 if path is None: path = []
6005ef9b 327
01932434
MW
328 ## Extend the path to cover the lookup section, but remember to remove us
329 ## again when we've finished. If we need to pass the current path back
330 ## upwards, then remember to take a copy.
bd3db76c 331 path.append(sec)
6005ef9b 332 try:
bd3db76c 333
01932434
MW
334 ## If we've been this way before on another pass through then return
335 ## the value we found then. If we're still thinking about it then
336 ## we've found a cycle.
334db748
MW
337 try: threadp, value = map[sec]
338 except KeyError: pass
01932434 339 else:
334db748 340 if threadp: raise InheritanceCycleError(key, path[:])
01932434
MW
341
342 ## See whether the answer is ready waiting for us.
b7e5aa06
MW
343 try: v = me._sectmap[sec][key]
344 except KeyError: pass
334db748 345 else: return v, path[:]
01932434
MW
346
347 ## No, apparently, not. Find out our list of parents.
348 try:
b7e5aa06
MW
349 parents = me._sectmap[sec]['@inherit'].replace(',', ' ').split()
350 except KeyError:
01932434
MW
351 parents = []
352
353 ## Initially we have no idea.
354 value = None
355 winner = None
356
357 ## Go through our parents and ask them what they think.
358 map[sec] = True, None
359 for p in parents:
360
361 ## See whether we get an answer. If not, keep on going.
362 v, pp = me._get(p, key, map, path)
363 if v is None: continue
364
365 ## If we got an answer, check that it matches any previous ones.
366 if value is None:
367 value = v
368 winner = pp
369 elif value != v:
370 raise AmbiguousOptionError(key, winner, value, pp, v)
371
372 ## That's the best we could manage.
373 map[sec] = False, value
374 return value, winner
375
376 finally:
377 ## Remove us from the path again.
378 path.pop()
6005ef9b
MW
379
380 def get(me, sec, key, resolvep = True):
381 """
382 Retrieve the value of KEY from section SEC.
383 """
bd3db76c
MW
384
385 ## Special handling for the `name' key.
386 if key == 'name':
b7e5aa06 387 value = me._sectmap[sec].get('name', sec)
bd3db76c
MW
388 else:
389 value, _ = me._get(sec, key)
390 if value is None:
171206b5 391 raise MissingKeyException(sec, key)
bd3db76c
MW
392
393 ## Expand the value and return it.
394 return me._expand(sec, value, resolvep)
6005ef9b
MW
395
396 def items(me, sec, resolvep = True):
397 """
398 Return a list of (NAME, VALUE) items in section SEC.
399
400 This extends the default method by handling the inheritance chain.
401 """
bd3db76c
MW
402
403 ## Initialize for a depth-first walk of the inheritance graph.
6005ef9b 404 d = {}
bd3db76c 405 visited = {}
6005ef9b 406 basesec = sec
bd3db76c
MW
407 stack = [sec]
408
409 ## Visit nodes, collecting their keys. Don't believe the values:
410 ## resolving inheritance is too hard to do like this.
411 while stack:
412 sec = stack.pop()
413 if sec in visited: continue
414 visited[sec] = True
415
b7e5aa06 416 for key, value in me._sectmap[sec].iteritems():
bd3db76c
MW
417 if key == '@inherit': stack += value.replace(',', ' ').split()
418 else: d[key] = None
419
420 ## Now collect the values for the known keys, one by one.
421 items = []
422 for key in d: items.append((key, me.get(basesec, key, resolvep)))
423
424 ## And we're done.
425 return items
6005ef9b
MW
426
427###--------------------------------------------------------------------------
428### Command-line handling.
429
430def inputiter(things):
431 """
432 Iterate over command-line arguments, returning corresponding open files.
433
434 If none were given, or one is `-', assume standard input; if one is a
435 directory, scan it for files other than backups; otherwise return the
436 opened files.
437 """
438
439 if not things:
440 if OS.isatty(stdin.fileno()):
441 M.die('no input given, and stdin is a terminal')
442 yield stdin
443 else:
444 for thing in things:
445 if thing == '-':
446 yield stdin
447 elif OS.path.isdir(thing):
448 for item in OS.listdir(thing):
449 if item.endswith('~') or item.endswith('#'):
450 continue
451 name = OS.path.join(thing, item)
452 if not OS.path.isfile(name):
453 continue
454 yield file(name)
455 else:
456 yield file(thing)
457
458def parse_options(argv = argv):
459 """
460 Parse command-line options, returning a pair (OPTS, ARGS).
461 """
462 M.ego(argv[0])
463 op = OptionParser(usage = '%prog [-c CDB] INPUT...',
464 version = '%%prog (tripe, version %s)' % VERSION)
465 op.add_option('-c', '--cdb', metavar = 'CDB',
466 dest = 'cdbfile', default = None,
467 help = 'Compile output into a CDB file.')
468 opts, args = op.parse_args(argv)
469 return opts, args
470
471###--------------------------------------------------------------------------
472### Main code.
473
474def getconf(args):
475 """
476 Read the configuration files and return the accumulated result.
477
478 We make sure that all hostnames have been properly resolved.
479 """
480 conf = MyConfigParser()
481 for f in inputiter(args):
b7e5aa06 482 conf.parse(f)
6005ef9b
MW
483 conf.resolve()
484 return conf
485
486def output(conf, cdb):
487 """
488 Output the configuration information CONF to the database CDB.
489
490 This is where the special `user' and `auto' database entries get set.
491 """
492 auto = []
527cd49f 493 for sec in sorted(conf.sections()):
6005ef9b
MW
494 if sec.startswith('@'):
495 continue
496 elif sec.startswith('$'):
497 label = sec
498 else:
499 label = 'P%s' % sec
500 if conf.has_option(sec, 'auto') and \
501 conf.get(sec, 'auto') in ('y', 'yes', 't', 'true', '1', 'on'):
502 auto.append(sec)
503 if conf.has_option(sec, 'user'):
504 cdb.add('U%s' % conf.get(sec, 'user'), sec)
505 url = M.URLEncode(laxp = True, semip = True)
527cd49f 506 for key, value in sorted(conf.items(sec), key = lambda (k, v): k):
6005ef9b
MW
507 if not key.startswith('@'):
508 url.encode(key, ' '.join(M.split(value)[0]))
509 cdb.add(label, url.result)
510 cdb.add('%AUTO', ' '.join(auto))
511 cdb.finish()
512
513def main():
514 """Main program."""
515 opts, args = parse_options()
516 if opts.cdbfile:
517 cdb = CDB.cdbmake(opts.cdbfile, opts.cdbfile + '.new')
518 else:
519 cdb = CDBFake()
520 conf = getconf(args[1:])
521 output(conf, cdb)
522
523if __name__ == '__main__':
524 main()
525
526###----- That's all, folks --------------------------------------------------