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