chiark / gitweb /
uslip/uslip.c: Be consistent about `VERB_NOUN' function naming.
[tripe] / peerdb / tripe-newpeers.in
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
27 VERSION = '@VERSION@'
28
29 ###--------------------------------------------------------------------------
30 ### External dependencies.
31
32 import ConfigParser as CP
33 import mLib as M
34 from optparse import OptionParser
35 import cdb as CDB
36 from sys import stdin, stdout, exit, argv
37 import re as RX
38 import os as OS
39
40 ###--------------------------------------------------------------------------
41 ### Utilities.
42
43 class 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
55 class 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."""
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))
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.
108 r_ref = RX.compile(r'\$\(([^)]+)\)')
109
110 ## Match a $[HOST] name resolution reference; group 1 is the HOST.
111 r_resolve = RX.compile(r'\$\[([^]]+)\]')
112
113 def _fmt_path(path):
114   return ' -> '.join(["`%s'" % hop for hop in path])
115
116 class 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
126 class 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
134 class 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
141 class 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     """
211     return key == 'name' or me._get(sec, key)[0] is not None
212
213   def _get(me, sec, key, map = None, path = None):
214     """
215     Low-level option-fetching method.
216
217     Fetch the value for the named KEY from section SEC, or maybe
218     (recursively) a section which SEC inherits from.
219
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     """
224
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 = []
229
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)
234     try:
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)
245     except CP.NoOptionError:
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
282
283   def get(me, sec, key, resolvep = True):
284     """
285     Retrieve the value of KEY from section SEC.
286     """
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)
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     """
306
307     ## Initialize for a depth-first walk of the inheritance graph.
308     d = {}
309     visited = {}
310     basesec = sec
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
320       for key, value in CP.RawConfigParser.items(me, sec):
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
330
331 ###--------------------------------------------------------------------------
332 ### Command-line handling.
333
334 def 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
362 def 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
378 def 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
390 def 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 = []
397   for sec in sorted(conf.sections()):
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)
410     for key, value in sorted(conf.items(sec), key = lambda (k, v): k):
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
417 def 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
427 if __name__ == '__main__':
428   main()
429
430 ###----- That's all, folks --------------------------------------------------