chiark / gitweb /
uslip/uslip.c: Be consistent about `VERB_NOUN' function naming.
[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###
13### TrIPE is free software; you can redistribute it and/or modify
14### it under the terms of the GNU General Public License as published by
15### the Free Software Foundation; either version 2 of the License, or
16### (at your option) any later version.
17###
18### TrIPE is distributed in the hope that it will be useful,
19### but WITHOUT ANY WARRANTY; without even the implied warranty of
20### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21### GNU General Public License for more details.
22###
23### You should have received a copy of the GNU General Public License
24### along with TrIPE; if not, write to the Free Software Foundation,
25### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26
27VERSION = '@VERSION@'
28
29###--------------------------------------------------------------------------
30### External dependencies.
31
32import ConfigParser as CP
33import mLib as M
34from optparse import OptionParser
35import cdb as CDB
36from sys import stdin, stdout, exit, argv
37import re as RX
38import os as OS
39
40###--------------------------------------------------------------------------
41### Utilities.
42
43class CDBFake (object):
44 """Like cdbmake, but just outputs data suitable for cdb-map."""
45 def __init__(me, file = stdout):
46 me.file = file
47 def add(me, key, value):
48 me.file.write('%s:%s\n' % (key, value))
49 def finish(me):
50 pass
51
52###--------------------------------------------------------------------------
53### A bulk DNS resolver.
54
55class BulkResolver (object):
56 """
57 Resolve a number of DNS names in parallel.
58
59 The BulkResovler resolves a number of hostnames in parallel. Using it
60 works in three phases:
61
62 1. You call prepare(HOSTNAME) a number of times, to feed in the hostnames
63 you're interested in.
64
65 2. You call run() to actually drive the resolver.
66
67 3. You call lookup(HOSTNAME) to get the address you wanted. This will
68 fail with KeyError if the resolver couldn't resolve the HOSTNAME.
69 """
70
71 def __init__(me):
72 """Initialize the resolver."""
73 me._resolvers = {}
74 me._namemap = {}
75
76 def prepare(me, host):
77 """Prime the resolver to resolve the name HOST."""
d8310a3a
MW
78 if host not in me._resolvers:
79 me._resolvers[host] = M.SelResolveByName \
80 (host,
81 lambda name, alias, addr:
82 me._resolved(host, addr[0]),
83 lambda: me._resolved(host, None))
6005ef9b
MW
84
85 def run(me):
86 """Run the background DNS resolver until it's finished."""
87 while me._resolvers:
88 M.select()
89
90 def lookup(me, host):
91 """
92 Fetch the address corresponding to HOST.
93 """
94 addr = me._namemap[host]
95 if addr is None:
96 raise KeyError, host
97 return addr
98
99 def _resolved(me, host, addr):
100 """Callback function: remember that ADDR is the address for HOST."""
101 me._namemap[host] = addr
102 del me._resolvers[host]
103
104###--------------------------------------------------------------------------
105### The configuration parser.
106
107## Match a $(VAR) configuration variable reference; group 1 is the VAR.
108r_ref = RX.compile(r'\$\(([^)]+)\)')
109
110## Match a $[HOST] name resolution reference; group 1 is the HOST.
111r_resolve = RX.compile(r'\$\[([^]]+)\]')
112
bd3db76c
MW
113def _fmt_path(path):
114 return ' -> '.join(["`%s'" % hop for hop in path])
115
116class AmbiguousOptionError (Exception):
117 def __init__(me, key, patha, vala, pathb, valb):
118 me.key = key
119 me.patha, me.vala = patha, vala
120 me.pathb, me.valb = pathb, valb
121 def __str__(me):
122 return "Ambiguous answer resolving key `%s': " \
123 "path %s yields `%s' but %s yields `%s'" % \
124 (me.key, _fmt_path(me.patha), me.vala, _fmt_path(me.pathb), me.valb)
125
126class InheritanceCycleError (Exception):
127 def __init__(me, key, path):
128 me.key = key
129 me.path = path
130 def __str__(me):
131 return "Found a cycle %s looking up key `%s'" % \
132 (_fmt_path(me.path), me.key)
133
134class MissingKeyException (Exception):
135 def __init__(me, sec, key):
136 me.sec = sec
137 me.key = key
138 def __str__(me):
139 return "Key `%s' not found in section `%s'" % (me.key, me.sec)
140
6005ef9b
MW
141class MyConfigParser (CP.RawConfigParser):
142 """
143 A more advanced configuration parser.
144
145 This has two major enhancements over the standard ConfigParser which are
146 relevant to us.
147
148 * It recognizes `@inherits' keys and follows them when expanding a
149 value.
150
151 * It recognizes `$(VAR)' references to configuration variables during
152 expansion and processes them correctly.
153
154 * It recognizes `$[HOST]' name-resolver requests and handles them
155 correctly.
156
157 Use:
158
159 1. Call read(FILENAME) and/or read(FP, [FILENAME]) to slurp in the
160 configuration data.
161
162 2. Call resolve() to collect the hostnames which need to be resolved and
163 actually do the name resolution.
164
165 3. Call get(SECTION, ITEM) to collect the results, or items(SECTION) to
166 iterate over them.
167 """
168
169 def __init__(me):
170 """
171 Initialize a new, empty configuration parser.
172 """
173 CP.RawConfigParser.__init__(me)
174 me._resolver = BulkResolver()
175
176 def resolve(me):
177 """
178 Works out all of the hostnames which need resolving and resolves them.
179
180 Until you call this, attempts to fetch configuration items which need to
181 resolve hostnames will fail!
182 """
183 for sec in me.sections():
184 for key, value in me.items(sec, resolvep = False):
185 for match in r_resolve.finditer(value):
186 me._resolver.prepare(match.group(1))
187 me._resolver.run()
188
189 def _expand(me, sec, string, resolvep):
190 """
191 Expands $(...) and (optionally) $[...] placeholders in STRING.
192
193 The SEC is the configuration section from which to satisfy $(...)
194 requests. RESOLVEP is a boolean switch: do we bother to tax the resolver
195 or not? This is turned off by the resolve() method while it's collecting
196 hostnames to be resolved.
197 """
198 string = r_ref.sub \
199 (lambda m: me.get(sec, m.group(1), resolvep), string)
200 if resolvep:
201 string = r_resolve.sub(lambda m: me._resolver.lookup(m.group(1)),
202 string)
203 return string
204
205 def has_option(me, sec, key):
206 """
207 Decide whether section SEC has a configuration key KEY.
208
209 This version of the method properly handles the @inherit key.
210 """
bd3db76c 211 return key == 'name' or me._get(sec, key)[0] is not None
6005ef9b 212
bd3db76c 213 def _get(me, sec, key, map = None, path = None):
6005ef9b
MW
214 """
215 Low-level option-fetching method.
216
217 Fetch the value for the named KEY from section SEC, or maybe
bd3db76c 218 (recursively) a section which SEC inherits from.
6005ef9b 219
bd3db76c
MW
220 Returns a pair VALUE, PATH. The value is not expanded; nor do we check
221 for the special `name' key. The caller is expected to do these things.
222 Returns None if no value could be found.
223 """
6005ef9b 224
bd3db76c
MW
225 ## If we weren't given a memoization map or path, then we'd better make
226 ## one.
227 if map is None: map = {}
228 if path is None: path = []
6005ef9b 229
bd3db76c
MW
230 ## If we've been this way before on another pass through then return the
231 ## value we found then. If we're still thinking about it then we've
232 ## found a cycle.
233 path.append(sec)
6005ef9b 234 try:
bd3db76c
MW
235 threadp, value = map[sec]
236 except KeyError:
237 pass
238 else:
239 if threadp:
240 raise InheritanceCycleError, (key, path)
241
242 ## See whether the answer is ready waiting for us.
243 try:
244 v = CP.RawConfigParser.get(me, sec, key)
6005ef9b 245 except CP.NoOptionError:
bd3db76c
MW
246 pass
247 else:
248 p = path[:]
249 path.pop()
250 return v, p
251
252 ## No, apparently, not. Find out our list of parents.
253 try:
254 parents = CP.RawConfigParser.get(me, sec, '@inherit').\
255 replace(',', ' ').split()
256 except CP.NoOptionError:
257 parents = []
258
259 ## Initially we have no idea.
260 value = None
261 winner = None
262
263 ## Go through our parents and ask them what they think.
264 map[sec] = True, None
265 for p in parents:
266
267 ## See whether we get an answer. If not, keep on going.
268 v, pp = me._get(p, key, map, path)
269 if v is None: continue
270
271 ## If we got an answer, check that it matches any previous ones.
272 if value is None:
273 value = v
274 winner = pp
275 elif value != v:
276 raise AmbiguousOptionError, (key, winner, value, pp, v)
277
278 ## That's the best we could manage.
279 path.pop()
280 map[sec] = False, value
281 return value, winner
6005ef9b
MW
282
283 def get(me, sec, key, resolvep = True):
284 """
285 Retrieve the value of KEY from section SEC.
286 """
bd3db76c
MW
287
288 ## Special handling for the `name' key.
289 if key == 'name':
290 try: value = CP.RawConfigParser.get(me, sec, key)
291 except CP.NoOptionError: value = sec
292 else:
293 value, _ = me._get(sec, key)
294 if value is None:
295 raise MissingKeyException, (sec, key)
296
297 ## Expand the value and return it.
298 return me._expand(sec, value, resolvep)
6005ef9b
MW
299
300 def items(me, sec, resolvep = True):
301 """
302 Return a list of (NAME, VALUE) items in section SEC.
303
304 This extends the default method by handling the inheritance chain.
305 """
bd3db76c
MW
306
307 ## Initialize for a depth-first walk of the inheritance graph.
6005ef9b 308 d = {}
bd3db76c 309 visited = {}
6005ef9b 310 basesec = sec
bd3db76c
MW
311 stack = [sec]
312
313 ## Visit nodes, collecting their keys. Don't believe the values:
314 ## resolving inheritance is too hard to do like this.
315 while stack:
316 sec = stack.pop()
317 if sec in visited: continue
318 visited[sec] = True
319
6005ef9b 320 for key, value in CP.RawConfigParser.items(me, sec):
bd3db76c
MW
321 if key == '@inherit': stack += value.replace(',', ' ').split()
322 else: d[key] = None
323
324 ## Now collect the values for the known keys, one by one.
325 items = []
326 for key in d: items.append((key, me.get(basesec, key, resolvep)))
327
328 ## And we're done.
329 return items
6005ef9b
MW
330
331###--------------------------------------------------------------------------
332### Command-line handling.
333
334def inputiter(things):
335 """
336 Iterate over command-line arguments, returning corresponding open files.
337
338 If none were given, or one is `-', assume standard input; if one is a
339 directory, scan it for files other than backups; otherwise return the
340 opened files.
341 """
342
343 if not things:
344 if OS.isatty(stdin.fileno()):
345 M.die('no input given, and stdin is a terminal')
346 yield stdin
347 else:
348 for thing in things:
349 if thing == '-':
350 yield stdin
351 elif OS.path.isdir(thing):
352 for item in OS.listdir(thing):
353 if item.endswith('~') or item.endswith('#'):
354 continue
355 name = OS.path.join(thing, item)
356 if not OS.path.isfile(name):
357 continue
358 yield file(name)
359 else:
360 yield file(thing)
361
362def parse_options(argv = argv):
363 """
364 Parse command-line options, returning a pair (OPTS, ARGS).
365 """
366 M.ego(argv[0])
367 op = OptionParser(usage = '%prog [-c CDB] INPUT...',
368 version = '%%prog (tripe, version %s)' % VERSION)
369 op.add_option('-c', '--cdb', metavar = 'CDB',
370 dest = 'cdbfile', default = None,
371 help = 'Compile output into a CDB file.')
372 opts, args = op.parse_args(argv)
373 return opts, args
374
375###--------------------------------------------------------------------------
376### Main code.
377
378def getconf(args):
379 """
380 Read the configuration files and return the accumulated result.
381
382 We make sure that all hostnames have been properly resolved.
383 """
384 conf = MyConfigParser()
385 for f in inputiter(args):
386 conf.readfp(f)
387 conf.resolve()
388 return conf
389
390def output(conf, cdb):
391 """
392 Output the configuration information CONF to the database CDB.
393
394 This is where the special `user' and `auto' database entries get set.
395 """
396 auto = []
527cd49f 397 for sec in sorted(conf.sections()):
6005ef9b
MW
398 if sec.startswith('@'):
399 continue
400 elif sec.startswith('$'):
401 label = sec
402 else:
403 label = 'P%s' % sec
404 if conf.has_option(sec, 'auto') and \
405 conf.get(sec, 'auto') in ('y', 'yes', 't', 'true', '1', 'on'):
406 auto.append(sec)
407 if conf.has_option(sec, 'user'):
408 cdb.add('U%s' % conf.get(sec, 'user'), sec)
409 url = M.URLEncode(laxp = True, semip = True)
527cd49f 410 for key, value in sorted(conf.items(sec), key = lambda (k, v): k):
6005ef9b
MW
411 if not key.startswith('@'):
412 url.encode(key, ' '.join(M.split(value)[0]))
413 cdb.add(label, url.result)
414 cdb.add('%AUTO', ' '.join(auto))
415 cdb.finish()
416
417def main():
418 """Main program."""
419 opts, args = parse_options()
420 if opts.cdbfile:
421 cdb = CDB.cdbmake(opts.cdbfile, opts.cdbfile + '.new')
422 else:
423 cdb = CDBFake()
424 conf = getconf(args[1:])
425 output(conf, cdb)
426
427if __name__ == '__main__':
428 main()
429
430###----- That's all, folks --------------------------------------------------