chiark / gitweb /
svc/conntrack.in: Monitor class for ConnMan.
[tripe] / svc / conntrack.in
index eabdc2b114a0de2350da5e017bdec181e4e12426..6ada45d73950e68f6ca1d1c4419c08c6718de579 100644 (file)
@@ -39,7 +39,8 @@ import tripe as T
 import dbus as D
 for i in ['mainloop', 'mainloop.glib']:
   __import__('dbus.%s' % i)
-import gobject as G
+try: from gi.repository import GLib as G
+except ImportError: import gobject as G
 from struct import pack, unpack
 
 SM = T.svcmgr
@@ -140,6 +141,7 @@ class Config (object):
     ## 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
@@ -193,6 +195,29 @@ class Config (object):
 ### This will be a configuration file.
 CF = None
 
+def straddr(a): return a is None and '#<none>' or S.inet_ntoa(pack('>L', a))
+def strmask(m):
+  for i in xrange(33):
+    if m == 0xffffffff ^ ((1 << (32 - i)) - 1): return i
+  return straddr(m)
+
+def cmd_showconfig():
+  T.svcinfo('test-addr=%s' % CF.testaddr)
+def cmd_showgroups():
+  for sec, pats in CF.groups:
+    T.svcinfo(sec)
+def cmd_showgroup(g):
+  for s, p in CF.groups:
+    if s == g:
+      pats = p
+      break
+  else:
+    raise T.TripeJobError, 'unknown-group', g
+  for t, p, a, m in pats:
+    T.svcinfo('peer', t,
+              'target', p or '(default)',
+              'net', '%s/%s' % (straddr(a), strmask(m)))
+
 ###--------------------------------------------------------------------------
 ### Responding to a network up/down event.
 
@@ -216,6 +241,8 @@ _kick = T.Queue()
 def kickpeers():
   while True:
     upness, reason = _kick.get()
+    if T._debug: print '# kickpeers %s: %s' % (upness, reason)
+    select = []
 
     ## Make sure the configuration file is up-to-date.  Don't worry if we
     ## can't do anything useful.
@@ -233,39 +260,77 @@ def kickpeers():
       addr = localaddr(CF.testaddr)
       if addr is None:
         upness = False
+    else:
+      addr = None
+    if not T._debug: pass
+    elif addr: print '#   local address = %s' % straddr(addr)
+    else: print '#   offline'
 
     ## Now decide what to do.
     changes = []
     for g, pp in CF.groups:
+      if T._debug: print '#   check group %s' % g
 
       ## Find out which peer in the group ought to be active.
-      want = None                       # unequal to any string
-      if upness:
-        for t, p, a, m in pp:
-          if p is None:
-            aq = addr
+      ip = None
+      map = {}
+      want = None
+      for t, p, a, m in pp:
+        if p is None or not upness:
+          ipq = addr
+        else:
+          ipq = localaddr(p)
+        if T._debug:
+          info = 'peer=%s; target=%s; net=%s/%s; local=%s' % (
+            t, p or '(default)', straddr(a), strmask(m), straddr(ipq))
+        if upness and ip is None and \
+              ipq is not None and (ipq & m) == a:
+          if T._debug: print '#     %s: SELECTED' % info
+          map[t] = 'up'
+          select.append('%s=%s' % (g, t))
+          if t == 'down' or t.startswith('down/'):
+            want = None
           else:
-            aq = localaddr(p)
-          if aq is not None and (aq & m) == a:
             want = t
-            break
+          ip = ipq
+        else:
+          map[t] = 'down'
+          if T._debug: print '#     %s: skipped' % info
 
       ## Shut down the wrong ones.
       found = False
+      if T._debug: print '#   peer-map = %r' % map
       for p in peers:
-        if p == want:
+        what = map.get(p, 'leave')
+        if what == 'up':
           found = True
-        elif p.startswith(g) and p != want:
-          changes.append(lambda p=p: SM.kill(p))
+          if T._debug: print '#   peer %s: already up' % p
+        elif what == 'down':
+          def _(p = p):
+            try:
+              SM.kill(p)
+            except T.TripeError, exc:
+              if exc.args[0] == 'unknown-peer':
+                ## Inherently racy; don't worry about this.
+                pass
+              else:
+                raise
+          if T._debug: print '#   peer %s: bring down' % p
+          changes.append(_)
 
       ## Start the right one if necessary.
       if want is not None and not found:
-        changes.append(lambda: T._simple(SM.svcsubmit('connect', 'active',
-                                                      want)))
+        def _(want = want):
+          try:
+            SM.svcsubmit('connect', 'active', want)
+          except T.TripeError, exc:
+            SM.warn('conntrack', 'connect-failed', want, *exc.args)
+        if T._debug: print '#   peer %s: bring up' % want
+        changes.append(_)
 
     ## Commit the changes.
     if changes:
-      SM.notify('conntrack', upness and 'up' or 'down', *reason)
+      SM.notify('conntrack', upness and 'up' or 'down', *select + reason)
       for c in changes: c()
 
 def netupdown(upness, reason):
@@ -285,7 +350,14 @@ NM_PATH = '/org/freedesktop/NetworkManager'
 NM_IFACE = NM_NAME
 NMCA_IFACE = NM_NAME + '.Connection.Active'
 
-NM_STATE_CONNECTED = 3
+NM_STATE_CONNECTED = 3 #obsolete
+NM_STATE_CONNECTED_LOCAL = 50
+NM_STATE_CONNECTED_SITE = 60
+NM_STATE_CONNECTED_GLOBAL = 70
+NM_CONNSTATES = set([NM_STATE_CONNECTED,
+                     NM_STATE_CONNECTED_LOCAL,
+                     NM_STATE_CONNECTED_SITE,
+                     NM_STATE_CONNECTED_GLOBAL])
 
 class NetworkManagerMonitor (object):
   """
@@ -305,20 +377,19 @@ class NetworkManagerMonitor (object):
     try:
       nm = bus.get_object(NM_NAME, NM_PATH)
       state = nm.Get(NM_IFACE, 'State')
-      if state == NM_STATE_CONNECTED:
+      if state in NM_CONNSTATES:
         netupdown(True, ['nm', 'initially-connected'])
       else:
         netupdown(False, ['nm', 'initially-disconnected'])
     except D.DBusException:
       pass
-    bus.add_signal_receiver(me._nm_state, 'StateChanged', NM_IFACE,
-                            NM_NAME, NM_PATH)
-    bus.add_signal_receiver(me._nm_connchange,
-                            'PropertiesChanged', NMCA_IFACE,
-                            NM_NAME, None)
+    bus.add_signal_receiver(me._nm_state, 'StateChanged',
+                            NM_IFACE, NM_NAME, NM_PATH)
+    bus.add_signal_receiver(me._nm_connchange, 'PropertiesChanged',
+                            NMCA_IFACE, NM_NAME, None)
 
   def _nm_state(me, state):
-    if state == NM_STATE_CONNECTED:
+    if state in NM_CONNSTATES:
       netupdown(True, ['nm', 'connected'])
     else:
       netupdown(False, ['nm', 'disconnected'])
@@ -327,6 +398,38 @@ class NetworkManagerMonitor (object):
     if props.get('Default', False):
       netupdown(True, ['nm', 'default-connection-change'])
 
+##--------------------------------------------------------------------------
+### Connman monitor.
+
+CM_NAME = 'net.connman'
+CM_PATH = '/'
+CM_IFACE = 'net.connman.Manager'
+
+class ConnManMonitor (object):
+  """
+  Watch ConnMan signls for changes in network state.
+  """
+
+  ## Strategy.  Everything seems to be usefully encoded in the `State'
+  ## property.  If it's `offline', `idle' or `ready' then we don't expect a
+  ## network connection.  During handover from one network to another, the
+  ## property passes through `ready' to `online'.
+
+  def attach(me, bus):
+    try:
+      cm = bus.get_object(CM_NAME, CM_PATH)
+      props = cm.GetProperties(dbus_interface = CM_IFACE)
+      state = props['State']
+      netupdown(state == 'online', ['connman', 'initially-%s' % state])
+    except D.DBusException:
+      pass
+    bus.add_signal_receiver(me._cm_state, 'PropertyChanged',
+                            CM_IFACE, CM_NAME, CM_PATH)
+
+  def _cm_state(me, prop, value):
+    if prop != 'State': return
+    netupdown(value == 'online', ['connman', value])
+
 ###--------------------------------------------------------------------------
 ### Maemo monitor.
 
@@ -388,6 +491,7 @@ class DBusMonitor (object):
     """
     me._mons = []
     me._loop = D.mainloop.glib.DBusGMainLoop()
+    me._state = 'startup'
     me._reconnect()
 
   def addmon(me, mon):
@@ -402,12 +506,20 @@ class DBusMonitor (object):
     if me._bus is not None:
       mon.attach(me._bus)
 
-  def _reconnect(me):
+  def _reconnect(me, hunoz = None):
     """
     Start connecting to the bus.
 
     If we fail the first time, retry periodically.
     """
+    if me._state == 'startup':
+      T.aside(SM.notify, 'conntrack', 'dbus-connection', 'startup')
+    elif me._state == 'connected':
+      T.aside(SM.notify, 'conntrack', 'dbus-connection', 'lost')
+    else:
+      T.aside(SM.notify, 'conntrack', 'dbus-connection',
+              'state=%s' % me._state)
+    me._state == 'reconnecting'
     me._bus = None
     if me._try_connect():
       G.timeout_add_seconds(5, me._try_connect)
@@ -419,13 +531,21 @@ class DBusMonitor (object):
     If we succeed, attach the monitors.
     """
     try:
-      bus = D.SystemBus(mainloop = me._loop, private = True)
-    except D.DBusException:
+      addr = OS.getenv('TRIPE_CONNTRACK_BUS')
+      if addr == 'SESSION':
+        bus = D.SessionBus(mainloop = me._loop, private = True)
+      elif addr is not None:
+        bus = D.bus.BusConnection(addr, mainloop = me._loop)
+      else:
+        bus = D.SystemBus(mainloop = me._loop, private = True)
+      for m in me._mons:
+        m.attach(bus)
+    except D.DBusException, e:
       return True
     me._bus = bus
+    me._state = 'connected'
     bus.call_on_disconnection(me._reconnect)
-    for m in me._mons:
-      m.attach(bus)
+    T.aside(SM.notify, 'conntrack', 'dbus-connection', 'connected')
     return False
 
 ###--------------------------------------------------------------------------
@@ -457,12 +577,14 @@ def init():
   Add the D-Bus monitor here, because we might send commands off immediately,
   and we want to make sure the server connection is up.
   """
+  global DBM
   T.Coroutine(kickpeers, name = 'kickpeers').switch()
-  dbm = DBusMonitor()
-  dbm.addmon(NetworkManagerMonitor())
-  dbm.addmon(MaemoICdMonitor())
-  G.timeout_add_seconds(300, lambda: (netupdown(True, ['interval-timer'])
-                                      or True))
+  DBM = DBusMonitor()
+  DBM.addmon(NetworkManagerMonitor())
+  DBM.addmon(ConnManMonitor())
+  DBM.addmon(MaemoICdMonitor())
+  G.timeout_add_seconds(30, lambda: (netupdown(True, ['interval-timer'])
+                                     or True))
 
 def parse_options():
   """
@@ -504,7 +626,10 @@ def cmd_updown(upness):
   return lambda *args: T.defer(netupdown, upness, ['manual'] + list(args))
 service_info = [('conntrack', VERSION, {
   'up': (0, None, '', cmd_updown(True)),
-  'down': (0, None, '', cmd_updown(False))
+  'down': (0, None, '', cmd_updown(False)),
+  'show-config': (0, 0, '', cmd_showconfig),
+  'show-groups': (0, 0, '', cmd_showgroups),
+  'show-group': (1, 1, 'GROUP', cmd_showgroup)
 })]
 
 if __name__ == '__main__':