chiark / gitweb /
peerdb/tripe-newpeers.in: Add a new resolver based on adnshost(1).
[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
bf495093 35import subprocess as SUB
6005ef9b
MW
36import re as RX
37import os as OS
bf495093
MW
38import errno as E
39import fcntl as F
97567475 40import socket as S
b7e5aa06 41from cStringIO import StringIO
6005ef9b
MW
42
43###--------------------------------------------------------------------------
44### Utilities.
45
46class CDBFake (object):
47 """Like cdbmake, but just outputs data suitable for cdb-map."""
48 def __init__(me, file = stdout):
49 me.file = file
50 def add(me, key, value):
51 me.file.write('%s:%s\n' % (key, value))
52 def finish(me):
53 pass
54
1c4623dd
MW
55class ExpectedError (Exception): pass
56
6005ef9b
MW
57###--------------------------------------------------------------------------
58### A bulk DNS resolver.
59
1c4623dd 60class ResolverFailure (ExpectedError):
6f48da4a
MW
61 def __init__(me, host, msg):
62 me.host = host
63 me.msg = msg
64 def __str__(me):
65 return "failed to resolve `%s': %s" % (me.host, me.msg)
66
660564a1
MW
67class ResolvingHost (object):
68 """
69 A host name which is being looked up by a bulk-resolver instance.
ef7d7afb
MW
70
71 Most notably, this is where the flag-handling logic lives for the
72 $FLAGS[HOSTNAME] syntax.
660564a1
MW
73 """
74
75 def __init__(me, name):
76 """Make a new resolving-host object for the host NAME."""
77 me.name = name
97567475 78 me.addr = { 'INET': [], 'INET6': [] }
660564a1
MW
79 me.failure = None
80
cc72e4b3
MW
81 def addaddr(me, af, addr):
82 """
83 Add the address ADDR with address family AF.
84
97567475 85 The address family may be `INET' or `INET6'.
cc72e4b3
MW
86 """
87 me.addr[af].append(addr)
660564a1
MW
88
89 def failed(me, msg):
90 """
91 Report that resolution of this host failed, with a human-readable MSG.
92 """
93 me.failure = msg
94
ef7d7afb
MW
95 def get(me, flags):
96 """Return a list of addresses according to the FLAGS string."""
660564a1 97 if me.failure is not None: raise ResolverFailure(me.name, me.failure)
cc72e4b3
MW
98 aa = []
99 a4 = me.addr['INET']
97567475 100 a6 = me.addr['INET6']
cc72e4b3 101 all, any = False, False
ef7d7afb
MW
102 for ch in flags:
103 if ch == '*': all = True
cc72e4b3 104 elif ch == '4': aa += a4; any = True
97567475 105 elif ch == '6': aa += a6; any = True
ef7d7afb 106 else: raise ValueError("unknown address-resolution flag `%s'" % ch)
97567475 107 if not any: aa = a4 + a6
ef7d7afb
MW
108 if not aa: raise ResolverFailure(me.name, 'no matching addresses found')
109 if not all: aa = [aa[0]]
110 return aa
660564a1 111
ad367891 112class BaseBulkResolver (object):
6005ef9b
MW
113 """
114 Resolve a number of DNS names in parallel.
115
116 The BulkResovler resolves a number of hostnames in parallel. Using it
117 works in three phases:
118
119 1. You call prepare(HOSTNAME) a number of times, to feed in the hostnames
120 you're interested in.
121
122 2. You call run() to actually drive the resolver.
123
124 3. You call lookup(HOSTNAME) to get the address you wanted. This will
125 fail with KeyError if the resolver couldn't resolve the HOSTNAME.
126 """
127
128 def __init__(me):
129 """Initialize the resolver."""
6005ef9b 130 me._namemap = {}
81b1fdde 131
660564a1
MW
132 def prepare(me, name):
133 """Prime the resolver to resolve the given host NAME."""
134 if name not in me._namemap:
135 me._namemap[name] = host = ResolvingHost(name)
97567475
MW
136 try:
137 ailist = S.getaddrinfo(name, None, S.AF_UNSPEC, S.SOCK_DGRAM, 0,
138 S.AI_NUMERICHOST | S.AI_NUMERICSERV)
139 except S.gaierror:
140 me._prepare(host, name)
141 else:
142 for af, skty, proto, cname, sa in ailist:
143 if af == S.AF_INET: host.addaddr('INET', sa[0])
144 elif af == S.AF_INET6: host.addaddr('INET6', sa[0])
6005ef9b 145
ef7d7afb 146 def lookup(me, name, flags):
660564a1 147 """Fetch the address corresponding to the host NAME."""
ef7d7afb 148 return me._namemap[name].get(flags)
6005ef9b 149
ad367891
MW
150class BresBulkResolver (BaseBulkResolver):
151 """
152 A BulkResolver using mLib's `bres' background resolver.
153
154 This is always available (and might use ADNS), but only does IPv4.
155 """
156
157 def __init__(me):
158 super(BresBulkResolver, me).__init__()
159 """Initialize the resolver."""
160 me._noutstand = 0
161
162 def _prepare(me, host, name):
163 """Arrange to resolve a NAME, reporting the results to HOST."""
164 host._resolv = M.SelResolveByName(
165 name,
166 lambda cname, alias, addr: me._resolved(host, cname, addr),
167 lambda: me._resolved(host, None, []))
168 me._noutstand += 1
169
170 def run(me):
171 """Run the background DNS resolver until it's finished."""
172 while me._noutstand: M.select()
173
3c8803fa 174 def _resolved(me, host, cname, addr):
ef7d7afb
MW
175 """Callback function: remember that ADDRs are the addresses for HOST."""
176 if not addr:
660564a1
MW
177 host.failed('(unknown failure)')
178 else:
3c8803fa 179 if cname is not None: host.name = cname
cc72e4b3 180 for a in addr: host.addaddr('INET', a)
660564a1
MW
181 host._resolv = None
182 me._noutstand -= 1
6005ef9b 183
bf495093
MW
184class AdnsBulkResolver (BaseBulkResolver):
185 """
186 A BulkResolver using ADNS, via the `adnshost' command-line tool.
187
188 This can do simultaneous IPv4 and IPv6 lookups and is quite shiny.
189 """
190
191 def __init__(me):
192 """Initialize the resolver."""
193
194 super(AdnsBulkResolver, me).__init__()
195
196 ## Start the external resolver process.
197 me._kid = SUB.Popen(['adnshost', '-afs'],
198 stdin = SUB.PIPE, stdout = SUB.PIPE)
199
200 ## Set up the machinery for feeding input to the resolver.
201 me._in = me._kid.stdin
202 M.fdflags(me._in, fbic = OS.O_NONBLOCK, fxor = OS.O_NONBLOCK)
203 me._insel = M.SelFile(me._in.fileno(), M.SEL_WRITE, me._write)
204 me._inbuf, me._inoff, me._inlen = '', 0, 0
205 me._idmap = {}
206 me._nextid = 0
207
208 ## Set up the machinery for collecting the resolver's output.
209 me._out = me._kid.stdout
210 M.fdflags(me._out, fbic = OS.O_NONBLOCK, fxor = OS.O_NONBLOCK)
211 me._outline = M.SelLineBuffer(me._out,
212 lineproc = me._hostline, eofproc = me._eof)
213 me._outline.enable()
214
215 ## It's not finished yet.
216 me._done = False
217
218 def _prepare(me, host, name):
219 """Arrange for the resolver to resolve the name NAME."""
220
221 ## Work out the next job id, and associate that with the host record.
222 host.id = me._nextid; me._nextid += 1
223 me._namemap[name] = me._idmap[host.id] = host
224
225 ## Feed the name to the resolver process.
226 me._inbuf += name + '\n'
227 me._inlen += len(name) + 1
228 if not me._insel.activep: me._insel.enable()
229 while me._inoff < me._inlen: M.select()
230
231 def _write(me):
232 """Write material from `_inbuf' to the resolver when it's ready."""
233
234 ## Try to feed some more material to the resolver.
235 try: n = OS.write(me._in.fileno(), me._inbuf[me._inoff:])
236 except OSError, e:
237 if e.errno == E.EAGAIN or e.errno == E.EWOULDBLOCK: return
238 else: raise
239
240 ## If we're done, then clear the buffer.
241 me._inoff += n
242 if me._inoff >= me._inlen:
243 me._insel.disable()
244 me._inbuf, me._inoff, me._inlen = '', 0, 0
245
246 def _eof(me):
247 """Notice that the resolver has finished."""
248 me._outline.disable()
249 me._done = True
250 me._kid.wait()
251
252 def run(me):
253 """
254 Tell the resolver it has all of our input now, and wait for it to finish.
255 """
256 me._in.close()
257 while not me._done: M.select()
258 if me._idmap:
259 raise Exception('adnshost failed to process all the requests')
260
261 def _hostline(me, line):
262 """Handle a host line from the resolver."""
263
264 ## Parse the line into fields.
265 (id, nrrs, stty, stocde, stmsg, owner, cname, ststr), _ = \
266 M.split(line, quotep = True)
267 id, nrrs = int(id), int(nrrs)
268
269 ## Find the right record.
270 host = me._idmap[id]
271 if stty != 'ok': host.failed(ststr)
272
273 ## Stash away the canonical name of the host.
274 host.name = cname == '$' and owner or cname
275
276 ## If there are no record lines to come, then remove this record from the
277 ## list of outstanding jobs. Otherwise, switch to the handler for record
278 ## lines.
279 if not nrrs:
280 del me._idmap[id]
281 else:
282 me._outline.lineproc = me._rrline
283 me._nrrs = nrrs
284 me._outhost = host
285
286 def _rrline(me, line):
287 """Handle a record line from the resolver."""
288
289 ## Parse the line into fields.
290 ww, _ = M.split(line, quotep = True)
291 owner, type, af = ww[:3]
292
293 ## If this is an address record, and it looks like an interesting address
294 ## type, then stash the address.
295 if type == 'A' and (af == 'INET' or af == 'INET6'):
296 me._outhost.addaddr(af, ww[3])
297
298 ## Update the parser state. If there are no more records for this job
299 ## then mark the job as done and switch back to expecting a host line.
300 me._nrrs -= 1
301 if not me._nrrs:
302 me._outline.lineproc = me._hostline
303 del me._idmap[me._outhost.id]
304 me._outhost = None
305
306## Select a bulk resolver. If `adnshost' exists then we might as well use
307## it.
ad367891 308BulkResolver = BresBulkResolver
bf495093
MW
309try:
310 p = SUB.Popen(['adnshost', '--version'],
311 stdin = SUB.PIPE, stdout = SUB.PIPE, stderr = SUB.PIPE)
312 _out, _err = p.communicate()
313 st = p.wait()
314 if st == 0: BulkResolver = AdnsBulkResolver
315except OSError:
316 pass
ad367891 317
6005ef9b
MW
318###--------------------------------------------------------------------------
319### The configuration parser.
320
b7e5aa06
MW
321## Match a comment or empty line.
322RX_COMMENT = RX.compile(r'(?x) ^ \s* (?: $ | [;#])')
323
324## Match a section group header.
325RX_GRPHDR = RX.compile(r'(?x) ^ \s* \[ (.*) \] \s* $')
326
327## Match an assignment line.
328RX_ASSGN = RX.compile(r'''(?x) ^
329 ([^\s:=] (?: [^:=]* [^\s:=])?)
330 \s* [:=] \s*
331 (| \S | \S.*\S)
332 \s* $''')
333
334## Match a continuation line.
335RX_CONT = RX.compile(r'''(?x) ^ \s+
336 (| \S | \S.*\S)
337 \s* $''')
338
6005ef9b 339## Match a $(VAR) configuration variable reference; group 1 is the VAR.
2d51bc9f 340RX_REF = RX.compile(r'(?x) \$ \( ([^)]+) \)')
6005ef9b 341
ef7d7afb
MW
342## Match a $FLAGS[HOST] name resolution reference; group 1 are the flags;
343## group 2 is the HOST.
97567475 344RX_RESOLVE = RX.compile(r'(?x) \$ ([46*]*) \[ ([^]]+) \]')
6005ef9b 345
1c4623dd 346class ConfigSyntaxError (ExpectedError):
b7e5aa06
MW
347 def __init__(me, fname, lno, msg):
348 me.fname = fname
349 me.lno = lno
350 me.msg = msg
351 def __str__(me):
352 return '%s:%d: %s' % (me.fname, me.lno, me.msg)
353
bd3db76c
MW
354def _fmt_path(path):
355 return ' -> '.join(["`%s'" % hop for hop in path])
356
1c4623dd 357class AmbiguousOptionError (ExpectedError):
bd3db76c
MW
358 def __init__(me, key, patha, vala, pathb, valb):
359 me.key = key
360 me.patha, me.vala = patha, vala
361 me.pathb, me.valb = pathb, valb
362 def __str__(me):
363 return "Ambiguous answer resolving key `%s': " \
364 "path %s yields `%s' but %s yields `%s'" % \
365 (me.key, _fmt_path(me.patha), me.vala, _fmt_path(me.pathb), me.valb)
366
1c4623dd 367class InheritanceCycleError (ExpectedError):
bd3db76c
MW
368 def __init__(me, key, path):
369 me.key = key
370 me.path = path
371 def __str__(me):
372 return "Found a cycle %s looking up key `%s'" % \
373 (_fmt_path(me.path), me.key)
374
1c4623dd 375class MissingSectionException (ExpectedError):
e3ec3a3a 376 def __init__(me, sec):
260dce8e 377 me.sec = sec
e3ec3a3a
MW
378 def __str__(me):
379 return "Section `%s' not found" % (me.sec)
380
1c4623dd 381class MissingKeyException (ExpectedError):
bd3db76c
MW
382 def __init__(me, sec, key):
383 me.sec = sec
384 me.key = key
385 def __str__(me):
386 return "Key `%s' not found in section `%s'" % (me.key, me.sec)
387
e3ec3a3a
MW
388class ConfigSection (object):
389 """
390 A section in a configuration parser.
391
392 This is where a lot of the nitty-gritty stuff actually happens. The
393 `MyConfigParser' knows a lot about the internals of this class, which saves
394 on building a complicated interface.
395 """
396
397 def __init__(me, name, cp):
398 """Initialize a new, empty section with a given NAME and parent CP."""
886350e8
MW
399
400 ## The cache maps item keys to entries, which consist of a pair of
401 ## objects. There are four possible states for a cache entry:
402 ##
403 ## * missing -- there is no entry at all with this key, so we must
404 ## search for it;
405 ##
406 ## * None, None -- we are actively trying to resolve this key, so if we
407 ## encounter this state, we have found a cycle in the inheritance
408 ## graph;
409 ##
410 ## * None, [] -- we know that this key isn't reachable through any of
411 ## our parents;
412 ##
413 ## * VALUE, PATH -- we know that the key resolves to VALUE, along the
414 ## PATH from us (exclusive) to the defining parent (inclusive).
e3ec3a3a
MW
415 me.name = name
416 me._itemmap = dict()
886350e8 417 me._cache = dict()
e3ec3a3a
MW
418 me._cp = cp
419
420 def _expand(me, string, resolvep):
421 """
ef7d7afb 422 Expands $(...) and (optionally) $FLAGS[...] placeholders in STRING.
e3ec3a3a
MW
423
424 RESOLVEP is a boolean switch: do we bother to tax the resolver or not?
425 This is turned off by MyConfigParser's resolve() method while it's
426 collecting hostnames to be resolved.
427 """
ef7d7afb 428 string = RX_REF.sub(lambda m: me.get(m.group(1), resolvep), string)
e3ec3a3a 429 if resolvep:
ef7d7afb
MW
430 string = RX_RESOLVE.sub(
431 lambda m: ' '.join(me._cp._resolver.lookup(m.group(2), m.group(1))),
432 string)
e3ec3a3a
MW
433 return string
434
4251f8ad
MW
435 def _parents(me):
436 """Yield this section's parents."""
437 try: names = me._itemmap['@inherit']
438 except KeyError: return
439 for name in names.replace(',', ' ').split():
440 yield me._cp.section(name)
441
886350e8 442 def _get(me, key, path = None):
e3ec3a3a
MW
443 """
444 Low-level option-fetching method.
445
446 Fetch the value for the named KEY in this section, or maybe (recursively)
447 a section which it inherits from.
448
449 Returns a pair VALUE, PATH. The value is not expanded; nor do we check
450 for the special `name' key. The caller is expected to do these things.
451 Returns None if no value could be found.
452 """
453
886350e8 454 ## If we weren't given a path, then we'd better make one.
e3ec3a3a
MW
455 if path is None: path = []
456
457 ## Extend the path to cover us, but remember to remove us again when
458 ## we've finished. If we need to pass the current path back upwards,
459 ## then remember to take a copy.
460 path.append(me.name)
461 try:
462
886350e8
MW
463 ## If we've been this way before on another pass through then return the
464 ## value we found then. If we're still thinking about it then we've
465 ## found a cycle.
466 try: v, p = me._cache[key]
e3ec3a3a
MW
467 except KeyError: pass
468 else:
886350e8
MW
469 if p is None: raise InheritanceCycleError(key, path[:])
470 else: return v, path + p
e3ec3a3a
MW
471
472 ## See whether the answer is ready waiting for us.
473 try: v = me._itemmap[key]
474 except KeyError: pass
886350e8
MW
475 else:
476 p = path[:]
477 me._cache[key] = v, []
478 return v, p
e3ec3a3a 479
e3ec3a3a
MW
480 ## Initially we have no idea.
481 value = None
886350e8 482 winner = []
e3ec3a3a
MW
483
484 ## Go through our parents and ask them what they think.
886350e8 485 me._cache[key] = None, None
4251f8ad 486 for p in me._parents():
e3ec3a3a
MW
487
488 ## See whether we get an answer. If not, keep on going.
886350e8 489 v, pp = p._get(key, path)
e3ec3a3a
MW
490 if v is None: continue
491
492 ## If we got an answer, check that it matches any previous ones.
493 if value is None:
494 value = v
495 winner = pp
496 elif value != v:
497 raise AmbiguousOptionError(key, winner, value, pp, v)
498
499 ## That's the best we could manage.
886350e8 500 me._cache[key] = value, winner[len(path):]
e3ec3a3a
MW
501 return value, winner
502
503 finally:
504 ## Remove us from the path again.
505 path.pop()
506
507 def get(me, key, resolvep = True):
508 """
509 Retrieve the value of KEY from this section.
510 """
511
512 ## Special handling for the `name' key.
513 if key == 'name':
514 value = me._itemmap.get('name', me.name)
7dd9d51f
MW
515 elif key == '@inherits':
516 try: return me._itemmap['@inherits']
517 except KeyError: raise MissingKeyException(me.name, key)
e3ec3a3a
MW
518 else:
519 value, _ = me._get(key)
520 if value is None:
521 raise MissingKeyException(me.name, key)
522
523 ## Expand the value and return it.
524 return me._expand(value, resolvep)
525
526 def items(me, resolvep = True):
527 """
85341d9c 528 Yield a list of item names in the section.
e3ec3a3a
MW
529 """
530
531 ## Initialize for a depth-first walk of the inheritance graph.
4063c2b5 532 seen = { 'name': True }
f417591a 533 visiting = { me.name: True }
4251f8ad 534 stack = [me]
e3ec3a3a
MW
535
536 ## Visit nodes, collecting their keys. Don't believe the values:
537 ## resolving inheritance is too hard to do like this.
538 while stack:
4251f8ad 539 sec = stack.pop()
f417591a
MW
540 for p in sec._parents():
541 if p.name not in visiting:
542 stack.append(p); visiting[p.name] = True
e3ec3a3a 543
7dd9d51f 544 for key in sec._itemmap.iterkeys(): seen[key] = None
e3ec3a3a 545
e3ec3a3a 546 ## And we're done.
6e5794ef 547 return seen.iterkeys()
e3ec3a3a 548
b7e5aa06 549class MyConfigParser (object):
6005ef9b
MW
550 """
551 A more advanced configuration parser.
552
b7e5aa06 553 This has four major enhancements over the standard ConfigParser which are
6005ef9b
MW
554 relevant to us.
555
556 * It recognizes `@inherits' keys and follows them when expanding a
557 value.
558
559 * It recognizes `$(VAR)' references to configuration variables during
560 expansion and processes them correctly.
561
ef7d7afb 562 * It recognizes `$FLAGS[HOST]' name-resolver requests and handles them
97567475
MW
563 correctly. FLAGS consists of characters `4' (IPv4 addresses), `6'
564 (IPv6 addresses), and `*' (all, space-separated, rather than just the
565 first).
6005ef9b 566
b7e5aa06
MW
567 * Its parsing behaviour is well-defined.
568
6005ef9b
MW
569 Use:
570
b7e5aa06 571 1. Call parse(FILENAME) to slurp in the configuration data.
6005ef9b
MW
572
573 2. Call resolve() to collect the hostnames which need to be resolved and
574 actually do the name resolution.
575
e3ec3a3a
MW
576 3. Call sections() to get a list of the configuration sections, or
577 section(NAME) to find a named section.
578
579 4. Call get(ITEM) on a section to collect the results, or items() to
6005ef9b
MW
580 iterate over them.
581 """
582
583 def __init__(me):
584 """
585 Initialize a new, empty configuration parser.
586 """
b7e5aa06 587 me._sectmap = dict()
6005ef9b
MW
588 me._resolver = BulkResolver()
589
b7e5aa06
MW
590 def parse(me, f):
591 """
592 Parse configuration from a file F.
593 """
594
595 ## Initial parser state.
596 sect = None
597 key = None
598 val = None
599 lno = 0
600
601 ## An unpleasant hack. Python makes it hard to capture a value in a
602 ## variable and examine it in a single action, and this is the best that
603 ## I came up with.
604 m = [None]
605 def match(rx): m[0] = rx.match(line); return m[0]
606
607 ## Commit a key's value when we've determined that there are no further
608 ## continuation lines.
609 def flush():
e3ec3a3a 610 if key is not None: sect._itemmap[key] = val.getvalue()
b7e5aa06
MW
611
612 ## Work through all of the input lines.
613 for line in f:
614 lno += 1
615
616 if match(RX_COMMENT):
617 ## A comment or a blank line. Nothing doing. (This means that we
618 ## leave out blank lines which look like they might be continuation
619 ## lines.)
620
621 pass
622
623 elif match(RX_GRPHDR):
624 ## A section header. Flush out any previous value and set up the new
625 ## group.
626
627 flush()
628 name = m[0].group(1)
629 try: sect = me._sectmap[name]
e3ec3a3a 630 except KeyError: sect = me._sectmap[name] = ConfigSection(name, me)
b7e5aa06
MW
631 key = None
632
633 elif match(RX_ASSGN):
634 ## A new assignment. Flush out the old one, and set up to store this
635 ## one.
636
637 if sect is None:
638 raise ConfigSyntaxError(f.name, lno, 'no active section to update')
639 flush()
640 key = m[0].group(1)
641 val = StringIO(); val.write(m[0].group(2))
642
643 elif match(RX_CONT):
644 ## A continuation line. Accumulate the value.
645
646 if key is None:
647 raise ConfigSyntaxError(f.name, lno, 'no config value to continue')
648 val.write('\n'); val.write(m[0].group(1))
649
650 else:
651 ## Something else.
652
653 raise ConfigSyntaxError(f.name, lno, 'incomprehensible line')
654
655 ## Don't forget to commit any final value material.
656 flush()
657
e3ec3a3a
MW
658 def section(me, name):
659 """Return a ConfigSection with the given NAME."""
660 try: return me._sectmap[name]
661 except KeyError: raise MissingSectionException(name)
662
b7e5aa06 663 def sections(me):
e3ec3a3a
MW
664 """Yield the known sections."""
665 return me._sectmap.itervalues()
b7e5aa06 666
6005ef9b
MW
667 def resolve(me):
668 """
669 Works out all of the hostnames which need resolving and resolves them.
670
671 Until you call this, attempts to fetch configuration items which need to
672 resolve hostnames will fail!
673 """
e3ec3a3a 674 for sec in me.sections():
85341d9c
MW
675 for key in sec.items():
676 value = sec.get(key, resolvep = False)
2d51bc9f 677 for match in RX_RESOLVE.finditer(value):
ef7d7afb 678 me._resolver.prepare(match.group(2))
6005ef9b
MW
679 me._resolver.run()
680
6005ef9b
MW
681###--------------------------------------------------------------------------
682### Command-line handling.
683
684def inputiter(things):
685 """
686 Iterate over command-line arguments, returning corresponding open files.
687
688 If none were given, or one is `-', assume standard input; if one is a
689 directory, scan it for files other than backups; otherwise return the
690 opened files.
691 """
692
693 if not things:
694 if OS.isatty(stdin.fileno()):
695 M.die('no input given, and stdin is a terminal')
696 yield stdin
697 else:
698 for thing in things:
699 if thing == '-':
700 yield stdin
701 elif OS.path.isdir(thing):
702 for item in OS.listdir(thing):
703 if item.endswith('~') or item.endswith('#'):
704 continue
705 name = OS.path.join(thing, item)
706 if not OS.path.isfile(name):
707 continue
708 yield file(name)
709 else:
710 yield file(thing)
711
712def parse_options(argv = argv):
713 """
714 Parse command-line options, returning a pair (OPTS, ARGS).
715 """
716 M.ego(argv[0])
717 op = OptionParser(usage = '%prog [-c CDB] INPUT...',
718 version = '%%prog (tripe, version %s)' % VERSION)
719 op.add_option('-c', '--cdb', metavar = 'CDB',
720 dest = 'cdbfile', default = None,
721 help = 'Compile output into a CDB file.')
722 opts, args = op.parse_args(argv)
723 return opts, args
724
725###--------------------------------------------------------------------------
726### Main code.
727
728def getconf(args):
729 """
730 Read the configuration files and return the accumulated result.
731
732 We make sure that all hostnames have been properly resolved.
733 """
734 conf = MyConfigParser()
735 for f in inputiter(args):
b7e5aa06 736 conf.parse(f)
6005ef9b
MW
737 conf.resolve()
738 return conf
739
740def output(conf, cdb):
741 """
742 Output the configuration information CONF to the database CDB.
743
744 This is where the special `user' and `auto' database entries get set.
745 """
746 auto = []
e3ec3a3a
MW
747 for sec in sorted(conf.sections(), key = lambda sec: sec.name):
748 if sec.name.startswith('@'):
6005ef9b 749 continue
e3ec3a3a
MW
750 elif sec.name.startswith('$'):
751 label = sec.name
6005ef9b 752 else:
e3ec3a3a 753 label = 'P%s' % sec.name
fd1ba90c
MW
754 try: a = sec.get('auto')
755 except MissingKeyException: pass
756 else:
757 if a in ('y', 'yes', 't', 'true', '1', 'on'): auto.append(sec.name)
758 try: u = sec.get('user')
759 except MissingKeyException: pass
760 else: cdb.add('U%s' % u)
6090fc43 761 url = M.URLEncode(semip = True)
85341d9c 762 for key in sorted(sec.items()):
6005ef9b 763 if not key.startswith('@'):
6090fc43 764 url.encode(key, sec.get(key))
6005ef9b
MW
765 cdb.add(label, url.result)
766 cdb.add('%AUTO', ' '.join(auto))
767 cdb.finish()
768
769def main():
770 """Main program."""
771 opts, args = parse_options()
772 if opts.cdbfile:
773 cdb = CDB.cdbmake(opts.cdbfile, opts.cdbfile + '.new')
774 else:
775 cdb = CDBFake()
1c4623dd
MW
776 try:
777 conf = getconf(args[1:])
778 output(conf, cdb)
779 except ExpectedError, e:
780 M.moan(str(e))
781 exit(2)
6005ef9b
MW
782
783if __name__ == '__main__':
784 main()
785
786###----- That's all, folks --------------------------------------------------