chiark / gitweb /
peerdb/tripe-newpeers.in: Abolish `ConfigSection.has_option'.
[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
e3ec3a3a
MW
159class MissingSectionException (Exception):
160 def __init__(me, sec):
161 me.key = key
162 def __str__(me):
163 return "Section `%s' not found" % (me.sec)
164
bd3db76c
MW
165class 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
e3ec3a3a
MW
172class 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
e3ec3a3a
MW
202 def _get(me, key, map = None, path = None):
203 """
204 Low-level option-fetching method.
205
206 Fetch the value for the named KEY in this section, or maybe (recursively)
207 a section which it inherits from.
208
209 Returns a pair VALUE, PATH. The value is not expanded; nor do we check
210 for the special `name' key. The caller is expected to do these things.
211 Returns None if no value could be found.
212 """
213
214 ## If we weren't given a memoization map or path, then we'd better make
215 ## one.
216 if map is None: map = {}
217 if path is None: path = []
218
219 ## Extend the path to cover us, but remember to remove us again when
220 ## we've finished. If we need to pass the current path back upwards,
221 ## then remember to take a copy.
222 path.append(me.name)
223 try:
224
225 ## If we've been this way before on another pass through then return
226 ## the value we found then. If we're still thinking about it then
227 ## we've found a cycle.
228 try: threadp, value = map[me.name]
229 except KeyError: pass
230 else:
231 if threadp: raise InheritanceCycleError(key, path[:])
232
233 ## See whether the answer is ready waiting for us.
234 try: v = me._itemmap[key]
235 except KeyError: pass
236 else: return v, path[:]
237
238 ## No, apparently, not. Find out our list of parents.
239 try:
240 parents = [me._cp.section(p) for p in
241 me._itemmap['@inherit'].replace(',', ' ').split()]
242 except KeyError:
243 parents = []
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 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 Return a list of (NAME, VALUE) items in this section.
291
292 This extends the default method by handling the inheritance chain.
293 """
294
295 ## Initialize for a depth-first walk of the inheritance graph.
296 d = {}
297 visited = {}
298 stack = [me.name]
299
300 ## Visit nodes, collecting their keys. Don't believe the values:
301 ## resolving inheritance is too hard to do like this.
302 while stack:
303 sec = me._cp.section(stack.pop())
304 if sec.name in visited: continue
305 visited[sec.name] = True
306
307 for key, value in sec._itemmap.iteritems():
308 if key == '@inherit': stack += value.replace(',', ' ').split()
309 else: d[key] = None
310
311 ## Now collect the values for the known keys, one by one.
312 items = []
313 for key in d: items.append((key, me.get(key, resolvep)))
314
315 ## And we're done.
316 return items
317
b7e5aa06 318class MyConfigParser (object):
6005ef9b
MW
319 """
320 A more advanced configuration parser.
321
b7e5aa06 322 This has four major enhancements over the standard ConfigParser which are
6005ef9b
MW
323 relevant to us.
324
325 * It recognizes `@inherits' keys and follows them when expanding a
326 value.
327
328 * It recognizes `$(VAR)' references to configuration variables during
329 expansion and processes them correctly.
330
331 * It recognizes `$[HOST]' name-resolver requests and handles them
332 correctly.
333
b7e5aa06
MW
334 * Its parsing behaviour is well-defined.
335
6005ef9b
MW
336 Use:
337
b7e5aa06 338 1. Call parse(FILENAME) to slurp in the configuration data.
6005ef9b
MW
339
340 2. Call resolve() to collect the hostnames which need to be resolved and
341 actually do the name resolution.
342
e3ec3a3a
MW
343 3. Call sections() to get a list of the configuration sections, or
344 section(NAME) to find a named section.
345
346 4. Call get(ITEM) on a section to collect the results, or items() to
6005ef9b
MW
347 iterate over them.
348 """
349
350 def __init__(me):
351 """
352 Initialize a new, empty configuration parser.
353 """
b7e5aa06 354 me._sectmap = dict()
6005ef9b
MW
355 me._resolver = BulkResolver()
356
b7e5aa06
MW
357 def parse(me, f):
358 """
359 Parse configuration from a file F.
360 """
361
362 ## Initial parser state.
363 sect = None
364 key = None
365 val = None
366 lno = 0
367
368 ## An unpleasant hack. Python makes it hard to capture a value in a
369 ## variable and examine it in a single action, and this is the best that
370 ## I came up with.
371 m = [None]
372 def match(rx): m[0] = rx.match(line); return m[0]
373
374 ## Commit a key's value when we've determined that there are no further
375 ## continuation lines.
376 def flush():
e3ec3a3a 377 if key is not None: sect._itemmap[key] = val.getvalue()
b7e5aa06
MW
378
379 ## Work through all of the input lines.
380 for line in f:
381 lno += 1
382
383 if match(RX_COMMENT):
384 ## A comment or a blank line. Nothing doing. (This means that we
385 ## leave out blank lines which look like they might be continuation
386 ## lines.)
387
388 pass
389
390 elif match(RX_GRPHDR):
391 ## A section header. Flush out any previous value and set up the new
392 ## group.
393
394 flush()
395 name = m[0].group(1)
396 try: sect = me._sectmap[name]
e3ec3a3a 397 except KeyError: sect = me._sectmap[name] = ConfigSection(name, me)
b7e5aa06
MW
398 key = None
399
400 elif match(RX_ASSGN):
401 ## A new assignment. Flush out the old one, and set up to store this
402 ## one.
403
404 if sect is None:
405 raise ConfigSyntaxError(f.name, lno, 'no active section to update')
406 flush()
407 key = m[0].group(1)
408 val = StringIO(); val.write(m[0].group(2))
409
410 elif match(RX_CONT):
411 ## A continuation line. Accumulate the value.
412
413 if key is None:
414 raise ConfigSyntaxError(f.name, lno, 'no config value to continue')
415 val.write('\n'); val.write(m[0].group(1))
416
417 else:
418 ## Something else.
419
420 raise ConfigSyntaxError(f.name, lno, 'incomprehensible line')
421
422 ## Don't forget to commit any final value material.
423 flush()
424
e3ec3a3a
MW
425 def section(me, name):
426 """Return a ConfigSection with the given NAME."""
427 try: return me._sectmap[name]
428 except KeyError: raise MissingSectionException(name)
429
b7e5aa06 430 def sections(me):
e3ec3a3a
MW
431 """Yield the known sections."""
432 return me._sectmap.itervalues()
b7e5aa06 433
6005ef9b
MW
434 def resolve(me):
435 """
436 Works out all of the hostnames which need resolving and resolves them.
437
438 Until you call this, attempts to fetch configuration items which need to
439 resolve hostnames will fail!
440 """
e3ec3a3a
MW
441 for sec in me.sections():
442 for key, value in sec.items(resolvep = False):
2d51bc9f 443 for match in RX_RESOLVE.finditer(value):
6005ef9b
MW
444 me._resolver.prepare(match.group(1))
445 me._resolver.run()
446
6005ef9b
MW
447###--------------------------------------------------------------------------
448### Command-line handling.
449
450def inputiter(things):
451 """
452 Iterate over command-line arguments, returning corresponding open files.
453
454 If none were given, or one is `-', assume standard input; if one is a
455 directory, scan it for files other than backups; otherwise return the
456 opened files.
457 """
458
459 if not things:
460 if OS.isatty(stdin.fileno()):
461 M.die('no input given, and stdin is a terminal')
462 yield stdin
463 else:
464 for thing in things:
465 if thing == '-':
466 yield stdin
467 elif OS.path.isdir(thing):
468 for item in OS.listdir(thing):
469 if item.endswith('~') or item.endswith('#'):
470 continue
471 name = OS.path.join(thing, item)
472 if not OS.path.isfile(name):
473 continue
474 yield file(name)
475 else:
476 yield file(thing)
477
478def parse_options(argv = argv):
479 """
480 Parse command-line options, returning a pair (OPTS, ARGS).
481 """
482 M.ego(argv[0])
483 op = OptionParser(usage = '%prog [-c CDB] INPUT...',
484 version = '%%prog (tripe, version %s)' % VERSION)
485 op.add_option('-c', '--cdb', metavar = 'CDB',
486 dest = 'cdbfile', default = None,
487 help = 'Compile output into a CDB file.')
488 opts, args = op.parse_args(argv)
489 return opts, args
490
491###--------------------------------------------------------------------------
492### Main code.
493
494def getconf(args):
495 """
496 Read the configuration files and return the accumulated result.
497
498 We make sure that all hostnames have been properly resolved.
499 """
500 conf = MyConfigParser()
501 for f in inputiter(args):
b7e5aa06 502 conf.parse(f)
6005ef9b
MW
503 conf.resolve()
504 return conf
505
506def output(conf, cdb):
507 """
508 Output the configuration information CONF to the database CDB.
509
510 This is where the special `user' and `auto' database entries get set.
511 """
512 auto = []
e3ec3a3a
MW
513 for sec in sorted(conf.sections(), key = lambda sec: sec.name):
514 if sec.name.startswith('@'):
6005ef9b 515 continue
e3ec3a3a
MW
516 elif sec.name.startswith('$'):
517 label = sec.name
6005ef9b 518 else:
e3ec3a3a 519 label = 'P%s' % sec.name
fd1ba90c
MW
520 try: a = sec.get('auto')
521 except MissingKeyException: pass
522 else:
523 if a in ('y', 'yes', 't', 'true', '1', 'on'): auto.append(sec.name)
524 try: u = sec.get('user')
525 except MissingKeyException: pass
526 else: cdb.add('U%s' % u)
6005ef9b 527 url = M.URLEncode(laxp = True, semip = True)
e3ec3a3a 528 for key, value in sorted(sec.items(), key = lambda (k, v): k):
6005ef9b
MW
529 if not key.startswith('@'):
530 url.encode(key, ' '.join(M.split(value)[0]))
531 cdb.add(label, url.result)
532 cdb.add('%AUTO', ' '.join(auto))
533 cdb.finish()
534
535def main():
536 """Main program."""
537 opts, args = parse_options()
538 if opts.cdbfile:
539 cdb = CDB.cdbmake(opts.cdbfile, opts.cdbfile + '.new')
540 else:
541 cdb = CDBFake()
542 conf = getconf(args[1:])
543 output(conf, cdb)
544
545if __name__ == '__main__':
546 main()
547
548###----- That's all, folks --------------------------------------------------