svc/conntrack.in (kickpeers): Rename `map' variable.
[tripe] / svc / conntrack.in
index 83b2993..99491b1 100644 (file)
@@ -36,6 +36,7 @@ import socket as S
 import mLib as M
 import tripe as T
 import dbus as D
+import re as RX
 for i in ['mainloop', 'mainloop.glib']:
   __import__('dbus.%s' % i)
 try: from gi.repository import GLib as G
@@ -53,47 +54,6 @@ class struct (object):
   def __init__(me, **kw):
     me.__dict__.update(kw)
 
-def toposort(cmp, things):
-  """
-  Generate the THINGS in an order consistent with a given partial order.
-
-  The function CMP(X, Y) should return true if X must precede Y, and false if
-  it doesn't care.  If X and Y are equal then it should return false.
-
-  The THINGS may be any finite iterable; it is converted to a list
-  internally.
-  """
-
-  ## Make sure we can index the THINGS, and prepare an ordering table.
-  ## What's going on?  The THINGS might not have a helpful equality
-  ## predicate, so it's easier to work with indices.  The ordering table will
-  ## remember which THINGS (by index) are considered greater than other
-  ## things.
-  things = list(things)
-  n = len(things)
-  order = [{} for i in xrange(n)]
-  rorder = [{} for i in xrange(n)]
-  for i in xrange(n):
-    for j in xrange(n):
-      if i != j and cmp(things[i], things[j]):
-        order[j][i] = True
-        rorder[i][j] = True
-
-  ## Now we can do the sort.
-  out = []
-  while True:
-    done = True
-    for i in xrange(n):
-      if order[i] is not None:
-        done = False
-        if len(order[i]) == 0:
-          for j in rorder[i]:
-            del order[j][i]
-          yield things[i]
-          order[i] = None
-    if done:
-      break
-
 ###--------------------------------------------------------------------------
 ### Address manipulation.
 
@@ -158,6 +118,19 @@ def straddr(a): return a is None and '#<none>' or str(a)
 
 TESTADDR = InetAddress('1.2.3.4')
 
+CONFSYNTAX = [
+  ('COMMENT', RX.compile(r'^\s*($|[;#])')),
+  ('GRPHDR', RX.compile(r'^\s*\[(.*)\]\s*$')),
+  ('ASSGN', RX.compile(r'\s*([\w.-]+)\s*[:=]\s*(|\S|\S.*\S)\s*$'))]
+
+class ConfigError (Exception):
+  def __init__(me, file, lno, msg):
+    me.file = file
+    me.lno = lno
+    me.msg = msg
+  def __str__(me):
+    return '%s:%d: %s' % (me.file, me.lno, me.msg)
+
 class Config (object):
   """
   Represents a configuration file.
@@ -191,52 +164,99 @@ class Config (object):
     Internal function to update the configuration from the underlying file.
     """
 
-    ## Read the configuration.  We have no need of the fancy substitutions,
-    ## so turn them all off.
-    cp = RawConfigParser()
-    cp.read(me._file)
     if T._debug: print '# reread config'
 
-    ## Save the test address.  Make sure it's vaguely sensible.  The default
-    ## is probably good for most cases, in fact, since that address isn't
-    ## actually in use.  Note that we never send packets to the test address;
-    ## we just use it to discover routing information.
-    if cp.has_option('DEFAULT', 'test-addr'):
-      testaddr = InetAddress(cp.get('DEFAULT', 'test-addr'))
-    else:
-      testaddr = TESTADDR
-
-    ## Scan the configuration file and build the groups structure.
-    groups = []
-    for sec in cp.sections():
-      pats = []
-      for tag in cp.options(sec):
-        spec = cp.get(sec, tag).split()
-
-        ## Parse the entry into peer and network.
-        if len(spec) == 1:
-          peer = None
-          net = spec[0]
+    ## Initial state.
+    testaddr = None
+    groups = {}
+    grpname = None
+    grplist = []
+
+    ## Open the file and start reading.
+    with open(me._file) as f:
+      lno = 0
+      for line in f:
+        lno += 1
+        for tag, rx in CONFSYNTAX:
+          m = rx.match(line)
+          if m: break
         else:
-          peer = InetAddress(spec[0])
-          net = spec[1]
-
-        ## Syntax of a net is ADDRESS/MASK, where ADDRESS is a dotted-quad,
-        ## and MASK is either a dotted-quad or a single integer N indicating
-        ## a mask with N leading ones followed by trailing zeroes.
-        net = parse_net(net)
-        pats.append((tag, peer, net))
-
-      ## Annoyingly, RawConfigParser doesn't preserve the order of options.
-      ## In order to make things vaguely sane, we topologically sort the
-      ## patterns so that more specific patterns are checked first.
-      pats = list(toposort(lambda (t, p, n), (tt, pp, nn): \
-                             (p and not pp) or \
-                             (p == pp and n.withinp(nn)),
-                           pats))
-      groups.append((sec, pats))
+          raise ConfigError(me._file, lno, 'failed to parse line: %r' % line)
+
+        if tag == 'COMMENT':
+          ## A comment.  Ignore it and hope it goes away.
+
+          continue
+
+        elif tag == 'GRPHDR':
+          ## A group header.  Flush the old group and start a new one.
+          newname = m.group(1)
+
+          if grpname is not None: groups[grpname] = grplist
+          if newname in groups:
+            raise ConfigError(me._file, lno,
+                              "duplicate group name `%s'" % newname)
+          grpname = newname
+          grplist = []
+
+        elif tag == 'ASSGN':
+           ## An assignment.  Deal with it.
+          name, value = m.group(1), m.group(2)
+
+          if grpname is None:
+            ## We're outside of any group, so this is a global configuration
+            ## tweak.
+
+            if name == 'test-addr':
+              for astr in value.split():
+                try:
+                  a = parse_address(astr)
+                except Exception, e:
+                  raise ConfigError(me._file, lno,
+                                    "invalid IP address `%s': %s" %
+                                    (astr, e))
+                if testaddr is not None:
+                  raise ConfigError(me._file, lno, 'duplicate test-address')
+                testaddr = a
+            else:
+              raise ConfigError(me._file, lno,
+                                "unknown global option `%s'" % name)
+
+          else:
+            ## Parse a pattern and add it to the group.
+            spec = value.split()
+            i = 0
+
+            ## Check for an explicit target address.
+            if i >= len(spec) or spec[i].find('/') >= 0:
+              peer = None
+            else:
+              try:
+                peer = parse_address(spec[i])
+              except Exception, e:
+                raise ConfigError(me._file, lno,
+                                  "invalid IP address `%s': %s" %
+                                  (spec[i], e))
+              i += 1
+
+            ## Parse the local network.
+            if len(spec) != i + 1:
+              raise ConfigError(me._file, lno, 'no network defined')
+            try:
+              net = parse_net(spec[i])
+            except Exception, e:
+              raise ConfigError(me._file, lno,
+                                "invalid IP network `%s': %s" %
+                                (spec[i], e))
+
+            ## Add this entry to the list.
+            grplist.append((name, peer, net))
+
+    ## Fill in the default test address if necessary.
+    if testaddr is None: testaddr = TESTADDR
 
     ## Done.
+    if grpname is not None: groups[grpname] = grplist
     me.testaddr = testaddr
     me.groups = groups
 
@@ -246,15 +266,11 @@ CF = None
 def cmd_showconfig():
   T.svcinfo('test-addr=%s' % CF.testaddr)
 def cmd_showgroups():
-  for sec, pats in CF.groups:
-    T.svcinfo(sec)
+  for g in sorted(CF.groups.iterkeys()):
+    T.svcinfo(g)
 def cmd_showgroup(g):
-  for s, p in CF.groups:
-    if s == g:
-      pats = p
-      break
-  else:
-    raise T.TripeJobError('unknown-group', g)
+  try: pats = CF.groups[g]
+  except KeyError: raise T.TripeJobError('unknown-group', g)
   for t, p, n in pats:
     T.svcinfo('peer', t,
               'target', p and str(p) or '(default)',
@@ -340,12 +356,12 @@ def kickpeers():
 
     ## Now decide what to do.
     changes = []
-    for g, pp in CF.groups:
+    for g, pp in CF.groups.iteritems():
       if T._debug: print '#   check group %s' % g
 
       ## Find out which peer in the group ought to be active.
       ip = None
-      map = {}
+      statemap = {}
       want = None
       for t, p, n in pp:
         if p is None or not upness:
@@ -358,7 +374,7 @@ def kickpeers():
         if upness and ip is None and \
               ipq is not None and ipq.withinp(n):
           if T._debug: print '#     %s: SELECTED' % info
-          map[t] = 'up'
+          statemap[t] = 'up'
           select.append('%s=%s' % (g, t))
           if t == 'down' or t.startswith('down/'):
             want = None
@@ -366,14 +382,14 @@ def kickpeers():
             want = t
           ip = ipq
         else:
-          map[t] = 'down'
+          statemap[t] = 'down'
           if T._debug: print '#     %s: skipped' % info
 
       ## Shut down the wrong ones.
       found = False
-      if T._debug: print '#   peer-map = %r' % map
+      if T._debug: print '#   peer-map = %r' % statemap
       for p in peers:
-        what = map.get(p, 'leave')
+        what = statemap.get(p, 'leave')
         if what == 'up':
           found = True
           if T._debug: print '#   peer %s: already up' % p