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