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