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