svc/conntrack.in: Split out a base class from `InetAddress'.
[tripe] / svc / conntrack.in
CommitLineData
2ec90437
MW
1#! @PYTHON@
2### -*-python-*-
3###
4### Service for automatically tracking network connection status
5###
6### (c) 2010 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.
2ec90437 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.
2ec90437
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/>.
2ec90437
MW
25
26VERSION = '@VERSION@'
27
28###--------------------------------------------------------------------------
29### External dependencies.
30
31from ConfigParser import RawConfigParser
32from optparse import OptionParser
33import os as OS
34import sys as SYS
35import socket as S
36import mLib as M
37import tripe as T
38import dbus as D
f8204e50 39import re as RX
2ec90437
MW
40for i in ['mainloop', 'mainloop.glib']:
41 __import__('dbus.%s' % i)
a69f4417
MW
42try: from gi.repository import GLib as G
43except ImportError: import gobject as G
2ec90437 44from struct import pack, unpack
c21d936a 45from cStringIO import StringIO
2ec90437
MW
46
47SM = T.svcmgr
48##__import__('rmcr').__debug = True
49
50###--------------------------------------------------------------------------
51### Utilities.
52
53class struct (object):
54 """A simple container object."""
55 def __init__(me, **kw):
56 me.__dict__.update(kw)
57
c21d936a
MW
58def loadb(s):
59 n = 0
60 for ch in s: n = 256*n + ord(ch)
61 return n
62
63def storeb(n, wd = None):
64 if wd is None: wd = n.bit_length()
65 s = StringIO()
66 for i in xrange((wd - 1)&-8, -8, -8): s.write(chr((n >> i)&0xff))
67 return s.getvalue()
68
6e8bbeeb
MW
69###--------------------------------------------------------------------------
70### Address manipulation.
c21d936a
MW
71###
72### I think this is the most demanding application, in terms of address
73### hacking, in the entire TrIPE suite. At least we don't have to do it in
74### C.
6e8bbeeb 75
c21d936a 76class BaseAddress (object):
6aa21132 77 def __init__(me, addrstr, maskstr = None):
c21d936a 78 me._setaddr(addrstr)
6aa21132
MW
79 if maskstr is None:
80 me.mask = -1
81 elif maskstr.isdigit():
c21d936a 82 me.mask = (1 << me.NBITS) - (1 << me.NBITS - int(maskstr))
6aa21132 83 else:
c21d936a 84 me._setmask(maskstr)
6aa21132
MW
85 if me.addr&~me.mask:
86 raise ValueError('network contains bits set beyond mask')
87 def _addrstr_to_int(me, addrstr):
c21d936a
MW
88 try: return loadb(S.inet_pton(me.AF, addrstr))
89 except S.error: raise ValueError('bad address syntax')
6aa21132 90 def _int_to_addrstr(me, n):
c21d936a
MW
91 return S.inet_ntop(me.AF, storeb(me.addr, me.NBITS))
92 def _setmask(me, maskstr):
93 raise ValueError('only prefix masked supported')
94 def _maskstr(me):
95 raise ValueError('only prefix masked supported')
6aa21132
MW
96 def sockaddr(me, port = 0):
97 if me.mask != -1: raise ValueError('not a simple address')
c21d936a 98 return me._sockaddr(port)
6aa21132 99 def __str__(me):
c21d936a 100 addrstr = me._addrstr()
6aa21132
MW
101 if me.mask == -1:
102 return addrstr
103 else:
c21d936a 104 inv = me.mask ^ ((1 << me.NBITS) - 1)
6aa21132 105 if (inv&(inv + 1)) == 0:
c21d936a 106 return '%s/%d' % (addrstr, me.NBITS - inv.bit_length())
6aa21132 107 else:
c21d936a 108 return '%s/%s' % (addrstr, me._maskstr())
6aa21132 109 def withinp(me, net):
c21d936a 110 if type(net) != type(me): return False
6aa21132
MW
111 if (me.mask&net.mask) != net.mask: return False
112 if (me.addr ^ net.addr)&net.mask: return False
c21d936a 113 return me._withinp(net)
6aa21132 114 def eq(me, other):
c21d936a 115 if type(me) != type(other): return False
6aa21132
MW
116 if me.mask != other.mask: return False
117 if me.addr != other.addr: return False
c21d936a
MW
118 return me._eq(other)
119 def _withinp(me, net):
6aa21132 120 return True
c21d936a
MW
121 def _eq(me, other):
122 return True
123
124class InetAddress (BaseAddress):
125 AF = S.AF_INET
126 AFNAME = 'IPv4'
127 NBITS = 32
128 def _addrstr_to_int(me, addrstr):
129 try: return loadb(S.inet_aton(addrstr))
130 except S.error: raise ValueError('bad address syntax')
131 def _setaddr(me, addrstr):
132 me.addr = me._addrstr_to_int(addrstr)
133 def _setmask(me, maskstr):
134 me.mask = me._addrstr_to_int(maskstr)
135 def _addrstr(me):
136 return me._int_to_addrstr(me.addr)
137 def _maskstr(me):
138 return me._int_to_addrstr(me.mask)
139 def _sockaddr(me, port = 0):
140 return (me._addrstr(), port)
6aa21132
MW
141 @classmethod
142 def from_sockaddr(cls, sa):
143 addr, port = (lambda a, p: (a, p))(*sa)
144 return cls(addr), port
145
146def parse_address(addrstr, maskstr = None):
147 return InetAddress(addrstr, maskstr)
11ab0da6 148
69bdcb64
MW
149def parse_net(netstr):
150 try: sl = netstr.index('/')
151 except ValueError: raise ValueError('missing mask')
6aa21132 152 return parse_address(netstr[:sl], netstr[sl + 1:])
69bdcb64 153
6aa21132 154def straddr(a): return a is None and '#<none>' or str(a)
6e8bbeeb 155
2ec90437
MW
156###--------------------------------------------------------------------------
157### Parse the configuration file.
158
159## Hmm. Should I try to integrate this with the peers database? It's not a
160## good fit; it'd need special hacks in tripe-newpeers. And the use case for
161## this service are largely going to be satellite notes, I don't think
162## scalability's going to be a problem.
163
e152ccf2 164TESTADDRS = [InetAddress('1.2.3.4')]
6aa21132 165
f8204e50
MW
166CONFSYNTAX = [
167 ('COMMENT', RX.compile(r'^\s*($|[;#])')),
168 ('GRPHDR', RX.compile(r'^\s*\[(.*)\]\s*$')),
169 ('ASSGN', RX.compile(r'\s*([\w.-]+)\s*[:=]\s*(|\S|\S.*\S)\s*$'))]
170
171class ConfigError (Exception):
172 def __init__(me, file, lno, msg):
173 me.file = file
174 me.lno = lno
175 me.msg = msg
176 def __str__(me):
177 return '%s:%d: %s' % (me.file, me.lno, me.msg)
178
2ec90437
MW
179class Config (object):
180 """
181 Represents a configuration file.
182
183 The most interesting thing is probably the `groups' slot, which stores a
184 list of pairs (NAME, PATTERNS); the NAME is a string, and the PATTERNS a
a59afe07 185 list of (TAG, PEER, NETS) triples. The implication is that there should be
6aa21132 186 precisely one peer from the set, and that it should be named TAG, where
a59afe07 187 (TAG, PEER, NETS) is the first triple such that the host's primary IP
6aa21132 188 address (if PEER is None -- or the IP address it would use for
a59afe07 189 communicating with PEER) is within one of the networks defined by NETS.
2ec90437
MW
190 """
191
192 def __init__(me, file):
193 """
194 Construct a new Config object, reading the given FILE.
195 """
196 me._file = file
197 me._fwatch = M.FWatch(file)
198 me._update()
199
200 def check(me):
201 """
202 See whether the configuration file has been updated.
203 """
204 if me._fwatch.update():
205 me._update()
206
207 def _update(me):
208 """
209 Internal function to update the configuration from the underlying file.
210 """
211
2d4998c4 212 if T._debug: print '# reread config'
2ec90437 213
f8204e50 214 ## Initial state.
e152ccf2 215 testaddrs = {}
f8d6fc7b 216 groups = {}
f8204e50
MW
217 grpname = None
218 grplist = []
219
220 ## Open the file and start reading.
221 with open(me._file) as f:
222 lno = 0
223 for line in f:
224 lno += 1
225 for tag, rx in CONFSYNTAX:
226 m = rx.match(line)
227 if m: break
2ec90437 228 else:
f8204e50
MW
229 raise ConfigError(me._file, lno, 'failed to parse line: %r' % line)
230
231 if tag == 'COMMENT':
232 ## A comment. Ignore it and hope it goes away.
233
234 continue
235
236 elif tag == 'GRPHDR':
237 ## A group header. Flush the old group and start a new one.
238 newname = m.group(1)
239
240 if grpname is not None: groups[grpname] = grplist
241 if newname in groups:
242 raise ConfigError(me._file, lno,
243 "duplicate group name `%s'" % newname)
244 grpname = newname
245 grplist = []
246
247 elif tag == 'ASSGN':
248 ## An assignment. Deal with it.
249 name, value = m.group(1), m.group(2)
250
251 if grpname is None:
252 ## We're outside of any group, so this is a global configuration
253 ## tweak.
254
255 if name == 'test-addr':
256 for astr in value.split():
257 try:
258 a = parse_address(astr)
259 except Exception, e:
260 raise ConfigError(me._file, lno,
261 "invalid IP address `%s': %s" %
262 (astr, e))
e152ccf2
MW
263 if a.AF in testaddrs:
264 raise ConfigError(me._file, lno,
265 'duplicate %s test-address' % a.AFNAME)
266 testaddrs[a.AF] = a
f8204e50
MW
267 else:
268 raise ConfigError(me._file, lno,
269 "unknown global option `%s'" % name)
270
271 else:
272 ## Parse a pattern and add it to the group.
273 spec = value.split()
274 i = 0
275
276 ## Check for an explicit target address.
277 if i >= len(spec) or spec[i].find('/') >= 0:
278 peer = None
e152ccf2 279 af = None
f8204e50
MW
280 else:
281 try:
282 peer = parse_address(spec[i])
283 except Exception, e:
284 raise ConfigError(me._file, lno,
285 "invalid IP address `%s': %s" %
286 (spec[i], e))
e152ccf2 287 af = peer.AF
f8204e50
MW
288 i += 1
289
a59afe07
MW
290 ## Parse the list of local networks.
291 nets = []
292 while i < len(spec):
293 try:
294 net = parse_net(spec[i])
295 except Exception, e:
296 raise ConfigError(me._file, lno,
297 "invalid IP network `%s': %s" %
298 (spec[i], e))
299 else:
300 nets.append(net)
301 i += 1
302 if not nets:
303 raise ConfigError(me._file, lno, 'no networks defined')
f8204e50 304
e152ccf2
MW
305 ## Make sure that the addresses are consistent.
306 for net in nets:
307 if af is None:
308 af = net.AF
309 elif net.AF != af:
310 raise ConfigError(me._file, lno,
311 "net %s doesn't match" % net)
312
f8204e50 313 ## Add this entry to the list.
a59afe07 314 grplist.append((name, peer, nets))
f8204e50 315
e152ccf2
MW
316 ## Fill in the default test addresses if necessary.
317 for a in TESTADDRS: testaddrs.setdefault(a.AF, a)
2ec90437
MW
318
319 ## Done.
f8204e50 320 if grpname is not None: groups[grpname] = grplist
e152ccf2 321 me.testaddrs = testaddrs
2ec90437
MW
322 me.groups = groups
323
324### This will be a configuration file.
325CF = None
326
2d4998c4 327def cmd_showconfig():
e152ccf2
MW
328 T.svcinfo('test-addr=%s' %
329 ' '.join(str(a)
330 for a in sorted(CF.testaddrs.itervalues(),
331 key = lambda a: a.AFNAME)))
2d4998c4 332def cmd_showgroups():
f8d6fc7b
MW
333 for g in sorted(CF.groups.iterkeys()):
334 T.svcinfo(g)
2d4998c4 335def cmd_showgroup(g):
f8d6fc7b
MW
336 try: pats = CF.groups[g]
337 except KeyError: raise T.TripeJobError('unknown-group', g)
a59afe07 338 for t, p, nn in pats:
2d4998c4 339 T.svcinfo('peer', t,
6aa21132 340 'target', p and str(p) or '(default)',
a59afe07 341 'net', ' '.join(map(str, nn)))
2d4998c4 342
2ec90437
MW
343###--------------------------------------------------------------------------
344### Responding to a network up/down event.
345
346def localaddr(peer):
347 """
348 Return the local IP address used for talking to PEER.
349 """
e152ccf2 350 sk = S.socket(peer.AF, S.SOCK_DGRAM)
2ec90437
MW
351 try:
352 try:
6aa21132
MW
353 sk.connect(peer.sockaddr(1))
354 addr = sk.getsockname()
e152ccf2 355 return type(peer).from_sockaddr(addr)[0]
2ec90437
MW
356 except S.error:
357 return None
358 finally:
359 sk.close()
360
361_kick = T.Queue()
f5393555
MW
362_delay = None
363
364def cancel_delay():
365 global _delay
366 if _delay is not None:
367 if T._debug: print '# cancel delayed kick'
368 G.source_remove(_delay)
369 _delay = None
4f6b41b9
MW
370
371def netupdown(upness, reason):
372 """
373 Add or kill peers according to whether the network is up or down.
374
375 UPNESS is true if the network is up, or false if it's down.
376 """
377
378 _kick.put((upness, reason))
379
f5393555
MW
380def delay_netupdown(upness, reason):
381 global _delay
382 cancel_delay()
383 def _func():
384 global _delay
385 if T._debug: print '# delayed %s: %s' % (upness, reason)
386 _delay = None
387 netupdown(upness, reason)
388 return False
389 if T._debug: print '# delaying %s: %s' % (upness, reason)
390 _delay = G.timeout_add(2000, _func)
391
2ec90437
MW
392def kickpeers():
393 while True:
394 upness, reason = _kick.get()
2d4998c4
MW
395 if T._debug: print '# kickpeers %s: %s' % (upness, reason)
396 select = []
f5393555 397 cancel_delay()
2ec90437
MW
398
399 ## Make sure the configuration file is up-to-date. Don't worry if we
400 ## can't do anything useful.
401 try:
402 CF.check()
403 except Exception, exc:
404 SM.warn('conntrack', 'config-file-error',
405 exc.__class__.__name__, str(exc))
406
407 ## Find the current list of peers.
408 peers = SM.list()
409
e152ccf2
MW
410 ## Work out the primary IP addresses.
411 locals = {}
2ec90437 412 if upness:
e152ccf2
MW
413 for af, remote in CF.testaddrs.iteritems():
414 local = localaddr(remote)
415 if local is not None: locals[af] = local
416 if not locals: upness = False
2d4998c4 417 if not T._debug: pass
e152ccf2
MW
418 elif not locals: print '# offline'
419 else:
420 for local in locals.itervalues():
421 print '# local %s address = %s' % (local.AFNAME, local)
2ec90437
MW
422
423 ## Now decide what to do.
424 changes = []
f8d6fc7b 425 for g, pp in CF.groups.iteritems():
2d4998c4 426 if T._debug: print '# check group %s' % g
2ec90437
MW
427
428 ## Find out which peer in the group ought to be active.
31d7aa8d 429 statemap = {}
b10a8c3d 430 want = None
fa59a04b 431 matchp = False
a59afe07 432 for t, p, nn in pp:
e152ccf2
MW
433 af = nn[0].AF
434 if p is None or not upness: ip = locals.get(af)
fa59a04b 435 else: ip = localaddr(p)
2d4998c4 436 if T._debug:
a59afe07
MW
437 info = 'peer = %s; target = %s; nets = %s; local = %s' % (
438 t, p or '(default)', ', '.join(map(str, nn)), straddr(ip))
fa59a04b 439 if upness and not matchp and \
a59afe07 440 ip is not None and any(ip.withinp(n) for n in nn):
2d4998c4 441 if T._debug: print '# %s: SELECTED' % info
31d7aa8d 442 statemap[t] = 'up'
2d4998c4 443 select.append('%s=%s' % (g, t))
fa59a04b
MW
444 if t == 'down' or t.startswith('down/'): want = None
445 else: want = t
446 matchp = True
b10a8c3d 447 else:
31d7aa8d 448 statemap[t] = 'down'
2d4998c4 449 if T._debug: print '# %s: skipped' % info
2ec90437
MW
450
451 ## Shut down the wrong ones.
452 found = False
31d7aa8d 453 if T._debug: print '# peer-map = %r' % statemap
2ec90437 454 for p in peers:
31d7aa8d 455 what = statemap.get(p, 'leave')
b10a8c3d 456 if what == 'up':
2ec90437 457 found = True
2d4998c4 458 if T._debug: print '# peer %s: already up' % p
b10a8c3d 459 elif what == 'down':
cf2e4ea6
MW
460 def _(p = p):
461 try:
462 SM.kill(p)
463 except T.TripeError, exc:
464 if exc.args[0] == 'unknown-peer':
465 ## Inherently racy; don't worry about this.
466 pass
467 else:
468 raise
2d4998c4 469 if T._debug: print '# peer %s: bring down' % p
cf2e4ea6 470 changes.append(_)
2ec90437
MW
471
472 ## Start the right one if necessary.
7b7e3c74 473 if want is not None and not found:
cf2e4ea6
MW
474 def _(want = want):
475 try:
8d1d183e 476 list(SM.svcsubmit('connect', 'active', want))
cf2e4ea6
MW
477 except T.TripeError, exc:
478 SM.warn('conntrack', 'connect-failed', want, *exc.args)
2d4998c4 479 if T._debug: print '# peer %s: bring up' % want
cf2e4ea6 480 changes.append(_)
2ec90437
MW
481
482 ## Commit the changes.
483 if changes:
2d4998c4 484 SM.notify('conntrack', upness and 'up' or 'down', *select + reason)
2ec90437
MW
485 for c in changes: c()
486
2ec90437
MW
487###--------------------------------------------------------------------------
488### NetworkManager monitor.
489
498d9f42
MW
490DBPROPS_IFACE = 'org.freedesktop.DBus.Properties'
491
2ec90437
MW
492NM_NAME = 'org.freedesktop.NetworkManager'
493NM_PATH = '/org/freedesktop/NetworkManager'
494NM_IFACE = NM_NAME
495NMCA_IFACE = NM_NAME + '.Connection.Active'
496
2079efa1
MW
497NM_STATE_CONNECTED = 3 #obsolete
498NM_STATE_CONNECTED_LOCAL = 50
499NM_STATE_CONNECTED_SITE = 60
500NM_STATE_CONNECTED_GLOBAL = 70
501NM_CONNSTATES = set([NM_STATE_CONNECTED,
502 NM_STATE_CONNECTED_LOCAL,
503 NM_STATE_CONNECTED_SITE,
504 NM_STATE_CONNECTED_GLOBAL])
2ec90437
MW
505
506class NetworkManagerMonitor (object):
507 """
508 Watch NetworkManager signals for changes in network state.
509 """
510
511 ## Strategy. There are two kinds of interesting state transitions for us.
512 ## The first one is the global are-we-connected state, which we'll use to
513 ## toggle network upness on a global level. The second is which connection
514 ## has the default route, which we'll use to tweak which peer in the peer
515 ## group is active. The former is most easily tracked using the signal
516 ## org.freedesktop.NetworkManager.StateChanged; for the latter, we track
517 ## org.freedesktop.NetworkManager.Connection.Active.PropertiesChanged and
518 ## look for when a new connection gains the default route.
519
520 def attach(me, bus):
521 try:
522 nm = bus.get_object(NM_NAME, NM_PATH)
498d9f42 523 state = nm.Get(NM_IFACE, 'State', dbus_interface = DBPROPS_IFACE)
2079efa1 524 if state in NM_CONNSTATES:
2ec90437
MW
525 netupdown(True, ['nm', 'initially-connected'])
526 else:
527 netupdown(False, ['nm', 'initially-disconnected'])
bd9bd714
MW
528 except D.DBusException, e:
529 if T._debug: print '# exception attaching to network-manager: %s' % e
2079efa1
MW
530 bus.add_signal_receiver(me._nm_state, 'StateChanged',
531 NM_IFACE, NM_NAME, NM_PATH)
532 bus.add_signal_receiver(me._nm_connchange, 'PropertiesChanged',
533 NMCA_IFACE, NM_NAME, None)
2ec90437
MW
534
535 def _nm_state(me, state):
2079efa1 536 if state in NM_CONNSTATES:
f5393555 537 delay_netupdown(True, ['nm', 'connected'])
2ec90437 538 else:
f5393555 539 delay_netupdown(False, ['nm', 'disconnected'])
2ec90437
MW
540
541 def _nm_connchange(me, props):
f5393555
MW
542 if props.get('Default', False) or props.get('Default6', False):
543 delay_netupdown(True, ['nm', 'default-connection-change'])
2ec90437 544
a95eb44a
MW
545##--------------------------------------------------------------------------
546### Connman monitor.
547
548CM_NAME = 'net.connman'
549CM_PATH = '/'
550CM_IFACE = 'net.connman.Manager'
551
552class ConnManMonitor (object):
553 """
554 Watch ConnMan signls for changes in network state.
555 """
556
557 ## Strategy. Everything seems to be usefully encoded in the `State'
558 ## property. If it's `offline', `idle' or `ready' then we don't expect a
559 ## network connection. During handover from one network to another, the
560 ## property passes through `ready' to `online'.
561
562 def attach(me, bus):
563 try:
564 cm = bus.get_object(CM_NAME, CM_PATH)
565 props = cm.GetProperties(dbus_interface = CM_IFACE)
566 state = props['State']
567 netupdown(state == 'online', ['connman', 'initially-%s' % state])
bd9bd714
MW
568 except D.DBusException, e:
569 if T._debug: print '# exception attaching to connman: %s' % e
a95eb44a
MW
570 bus.add_signal_receiver(me._cm_state, 'PropertyChanged',
571 CM_IFACE, CM_NAME, CM_PATH)
572
573 def _cm_state(me, prop, value):
574 if prop != 'State': return
f5393555 575 delay_netupdown(value == 'online', ['connman', value])
a95eb44a 576
2ec90437
MW
577###--------------------------------------------------------------------------
578### Maemo monitor.
579
580ICD_NAME = 'com.nokia.icd'
581ICD_PATH = '/com/nokia/icd'
582ICD_IFACE = ICD_NAME
583
584class MaemoICdMonitor (object):
585 """
586 Watch ICd signals for changes in network state.
587 """
588
589 ## Strategy. ICd only handles one connection at a time in steady state,
590 ## though when switching between connections, it tries to bring the new one
591 ## up before shutting down the old one. This makes life a bit easier than
592 ## it is with NetworkManager. On the other hand, the notifications are
593 ## relative to particular connections only, and the indicator that the old
594 ## connection is down (`IDLE') comes /after/ the new one comes up
595 ## (`CONNECTED'), so we have to remember which one is active.
596
597 def attach(me, bus):
598 try:
599 icd = bus.get_object(ICD_NAME, ICD_PATH)
600 try:
601 iap = icd.get_ipinfo(dbus_interface = ICD_IFACE)[0]
602 me._iap = iap
603 netupdown(True, ['icd', 'initially-connected', iap])
604 except D.DBusException:
605 me._iap = None
606 netupdown(False, ['icd', 'initially-disconnected'])
bd9bd714
MW
607 except D.DBusException, e:
608 if T._debug: print '# exception attaching to icd: %s' % e
2ec90437
MW
609 me._iap = None
610 bus.add_signal_receiver(me._icd_state, 'status_changed', ICD_IFACE,
611 ICD_NAME, ICD_PATH)
612
613 def _icd_state(me, iap, ty, state, hunoz):
614 if state == 'CONNECTED':
615 me._iap = iap
f5393555 616 delay_netupdown(True, ['icd', 'connected', iap])
2ec90437
MW
617 elif state == 'IDLE' and iap == me._iap:
618 me._iap = None
f5393555 619 delay_netupdown(False, ['icd', 'idle'])
2ec90437
MW
620
621###--------------------------------------------------------------------------
622### D-Bus connection tracking.
623
624class DBusMonitor (object):
625 """
626 Maintains a connection to the system D-Bus, and watches for signals.
627
628 If the connection is initially down, or drops for some reason, we retry
629 periodically (every five seconds at the moment). If the connection
630 resurfaces, we reattach the monitors.
631 """
632
633 def __init__(me):
634 """
635 Initialise the object and try to establish a connection to the bus.
636 """
637 me._mons = []
638 me._loop = D.mainloop.glib.DBusGMainLoop()
7bfa1e06 639 me._state = 'startup'
2ec90437
MW
640 me._reconnect()
641
642 def addmon(me, mon):
643 """
644 Add a monitor object to watch for signals.
645
646 MON.attach(BUS) is called, with BUS being the connection to the system
647 bus. MON should query its service's current status and watch for
648 relevant signals.
649 """
650 me._mons.append(mon)
651 if me._bus is not None:
652 mon.attach(me._bus)
653
16650038 654 def _reconnect(me, hunoz = None):
2ec90437
MW
655 """
656 Start connecting to the bus.
657
658 If we fail the first time, retry periodically.
659 """
7bfa1e06
MW
660 if me._state == 'startup':
661 T.aside(SM.notify, 'conntrack', 'dbus-connection', 'startup')
662 elif me._state == 'connected':
663 T.aside(SM.notify, 'conntrack', 'dbus-connection', 'lost')
664 else:
665 T.aside(SM.notify, 'conntrack', 'dbus-connection',
666 'state=%s' % me._state)
667 me._state == 'reconnecting'
2ec90437
MW
668 me._bus = None
669 if me._try_connect():
670 G.timeout_add_seconds(5, me._try_connect)
671
672 def _try_connect(me):
673 """
674 Actually make a connection attempt.
675
676 If we succeed, attach the monitors.
677 """
678 try:
7bfa1e06
MW
679 addr = OS.getenv('TRIPE_CONNTRACK_BUS')
680 if addr == 'SESSION':
681 bus = D.SessionBus(mainloop = me._loop, private = True)
682 elif addr is not None:
683 bus = D.bus.BusConnection(addr, mainloop = me._loop)
684 else:
685 bus = D.SystemBus(mainloop = me._loop, private = True)
686 for m in me._mons:
687 m.attach(bus)
688 except D.DBusException, e:
2ec90437
MW
689 return True
690 me._bus = bus
7bfa1e06 691 me._state = 'connected'
2ec90437 692 bus.call_on_disconnection(me._reconnect)
7bfa1e06 693 T.aside(SM.notify, 'conntrack', 'dbus-connection', 'connected')
2ec90437
MW
694 return False
695
696###--------------------------------------------------------------------------
697### TrIPE service.
698
699class GIOWatcher (object):
700 """
701 Monitor I/O events using glib.
702 """
703 def __init__(me, conn, mc = G.main_context_default()):
704 me._conn = conn
705 me._watch = None
706 me._mc = mc
707 def connected(me, sock):
708 me._watch = G.io_add_watch(sock, G.IO_IN,
709 lambda *hunoz: me._conn.receive())
710 def disconnected(me):
711 G.source_remove(me._watch)
712 me._watch = None
713 def iterate(me):
714 me._mc.iteration(True)
715
716SM.iowatch = GIOWatcher(SM)
717
718def init():
719 """
720 Service initialization.
721
722 Add the D-Bus monitor here, because we might send commands off immediately,
723 and we want to make sure the server connection is up.
724 """
29807d89 725 global DBM
22b47552 726 T.Coroutine(kickpeers, name = 'kickpeers').switch()
29807d89
MW
727 DBM = DBusMonitor()
728 DBM.addmon(NetworkManagerMonitor())
a95eb44a 729 DBM.addmon(ConnManMonitor())
29807d89 730 DBM.addmon(MaemoICdMonitor())
f5393555
MW
731 G.timeout_add_seconds(30, lambda: (_delay is not None or
732 netupdown(True, ['interval-timer']) or
733 True))
2ec90437
MW
734
735def parse_options():
736 """
737 Parse the command-line options.
738
739 Automatically changes directory to the requested configdir, and turns on
740 debugging. Returns the options object.
741 """
742 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
743 version = '%%prog %s' % VERSION)
744
745 op.add_option('-a', '--admin-socket',
746 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
747 help = 'Select socket to connect to [default %default]')
748 op.add_option('-d', '--directory',
749 metavar = 'DIR', dest = 'dir', default = T.configdir,
750 help = 'Select current diretory [default %default]')
751 op.add_option('-c', '--config',
752 metavar = 'FILE', dest = 'conf', default = 'conntrack.conf',
753 help = 'Select configuration [default %default]')
754 op.add_option('--daemon', dest = 'daemon',
755 default = False, action = 'store_true',
756 help = 'Become a daemon after successful initialization')
757 op.add_option('--debug', dest = 'debug',
758 default = False, action = 'store_true',
759 help = 'Emit debugging trace information')
760 op.add_option('--startup', dest = 'startup',
761 default = False, action = 'store_true',
762 help = 'Being called as part of the server startup')
763
764 opts, args = op.parse_args()
765 if args: op.error('no arguments permitted')
766 OS.chdir(opts.dir)
767 T._debug = opts.debug
768 return opts
769
770## Service table, for running manually.
771def cmd_updown(upness):
772 return lambda *args: T.defer(netupdown, upness, ['manual'] + list(args))
773service_info = [('conntrack', VERSION, {
774 'up': (0, None, '', cmd_updown(True)),
2d4998c4
MW
775 'down': (0, None, '', cmd_updown(False)),
776 'show-config': (0, 0, '', cmd_showconfig),
777 'show-groups': (0, 0, '', cmd_showgroups),
778 'show-group': (1, 1, 'GROUP', cmd_showgroup)
2ec90437
MW
779})]
780
781if __name__ == '__main__':
782 opts = parse_options()
783 CF = Config(opts.conf)
784 T.runservices(opts.tripesock, service_info,
785 init = init, daemon = opts.daemon)
786
787###----- That's all, folks --------------------------------------------------