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