From d64ce4ae05ad8250e08296af18d7d6ddcc5bbc9a Mon Sep 17 00:00:00 2001 Message-Id: From: Mark Wooding Date: Sat, 13 Jul 2013 16:34:40 +0100 Subject: [PATCH] Merge the `connect' and `watch' services. Organization: Straylight/Edgeware From: Mark Wooding The result is called `connect', because that's the one which provided scripted external interface. But it's best looked at as a merge of pieces of `connect' into `watch', followed by a rename. --- debian/tripe-peer-services.install | 2 - debian/tripe-peer-services.postinst | 2 +- peerdb/peers.in.5.in | 20 +- svc/Makefile.am | 14 - svc/connect.8.in | 469 ++++++++++++++--- svc/connect.in | 713 +++++++++++++++++++++++-- svc/watch.8.in | 495 ------------------ svc/watch.in | 773 ---------------------------- 8 files changed, 1092 insertions(+), 1396 deletions(-) delete mode 100644 svc/watch.8.in delete mode 100644 svc/watch.in diff --git a/debian/tripe-peer-services.install b/debian/tripe-peer-services.install index 3227e155..9bd42cbe 100644 --- a/debian/tripe-peer-services.install +++ b/debian/tripe-peer-services.install @@ -2,8 +2,6 @@ debian/tmp/usr/lib/tripe/services/connect debian/tmp/usr/share/man/man8/connect.8 debian/tmp/usr/lib/tripe/services/conntrack debian/tmp/usr/share/man/man8/conntrack.8 -debian/tmp/usr/lib/tripe/services/watch -debian/tmp/usr/share/man/man8/watch.8 debian/tmp/usr/sbin/tripe-newpeers debian/tmp/usr/share/man/man8/tripe-newpeers.8 debian/tmp/usr/share/man/man5/peers.in.5 diff --git a/debian/tripe-peer-services.postinst b/debian/tripe-peer-services.postinst index 870ba1ae..db937deb 100644 --- a/debian/tripe-peer-services.postinst +++ b/debian/tripe-peer-services.postinst @@ -95,6 +95,6 @@ restart_services () { echo "." } -restart_services conntrack connect watch +restart_services conntrack connect #DEBHELPER# diff --git a/peerdb/peers.in.5.in b/peerdb/peers.in.5.in index e31d0e37..240296e9 100644 --- a/peerdb/peers.in.5.in +++ b/peerdb/peers.in.5.in @@ -44,6 +44,7 @@ file is a plain text configuration file. It is read by in order to produce the .BR tripe.cdb (8) database used by services and other tools. +. .SS "General structure" The configuration file is line-oriented. Blank lines are ignored; lines beginning with a hash @@ -115,6 +116,7 @@ Apart from its effect on lookups, as just described, the .B @inherits key is entirely ignored. In particular, it is never written to the database. +. .SS "Standard keys and their meanings" The following keys have meanings to programs in the TrIPE suite. Other keys may be used by separately distributed extensions or for local use. @@ -132,7 +134,7 @@ described below. .TP .B connect Shell command for initiating connection to this peer. Used by -.BR watch (8). +.BR connect (8). .TP .B cork Don't initiate immediate key exchange. Used by @@ -140,15 +142,15 @@ Don't initiate immediate key exchange. Used by .TP .B disconnect Shell command for closing down connection to this peer. Used by -.BR watch (8). +.BR connect (8). .TP .B every Interval for checking that the peer is still alive and well. Used by -.BR watch (8). +.BR connect (8). .TP .B ifdown Script to bring down tunnel interface connected to the peer. Used by -.BR watch (8). +.BR connect (8). .TP .B ifname Interface name to set for the tunnel interface to the peer. Used by @@ -156,7 +158,7 @@ Interface name to set for the tunnel interface to the peer. Used by .TP .B ifup Script to bring up tunnel interface connected to the peer. Used by -.BR watch (8). +.BR connect (8). .TP .B ifupextra Script containing additional interface setup. Used by @@ -194,6 +196,8 @@ Used by .TP .B priv Tag of the private key to use when communicating with the peer. +Used by +.BR connect (8). .TP .B raddr Remote address for the tunnel interface to the peer. Used by @@ -201,11 +205,11 @@ Remote address for the tunnel interface to the peer. Used by .TP .B retries Number of failed ping attempts before attempting reconnection. Used by -.BR watch (8). +.BR connect (8). .TP .B timeout Timeout for ping probes. Used by -.BR watch (8). +.BR connect (8). .TP .B tunnel Tunnel driver to use when adding the peer. Used by @@ -219,6 +223,7 @@ Used by and .BR tripe-newpeers (8); described below. +. .SS "Conversion" This section describes how the textual .B peers.in @@ -286,7 +291,6 @@ is created whose contents is the section name. .BR tripe-newpeers (8), .BR peers.cdb (5), .BR connect (8), -.BR watch (8), .BR tripe-ifup (8). . .\"-------------------------------------------------------------------------- diff --git a/svc/Makefile.am b/svc/Makefile.am index 121df6df..a695e14a 100644 --- a/svc/Makefile.am +++ b/svc/Makefile.am @@ -51,19 +51,6 @@ connect: connect.in Makefile $(SUBST) $(srcdir)/connect.in >$@.new $(SUBSTITUTIONS) && \ chmod +x $@.new && mv $@.new $@ -## Watch for peers arriving and disconnecting. -services_SCRIPTS += watch -CLEANFILES += watch -EXTRA_DIST += watch.in - -man_MANS += watch.8 -CLEANFILES += watch.8 -EXTRA_DIST += watch.8.in - -watch: watch.in Makefile - $(SUBST) $(srcdir)/watch.in >$@.new $(SUBSTITUTIONS) && \ - chmod +x $@.new && mv $@.new $@ - ## Watch D-Bus to keep track of external connectivity. services_SCRIPTS += conntrack CLEANFILES += conntrack @@ -91,4 +78,3 @@ tripe-ifup: tripe-ifup.in Makefile chmod +x $@.new && mv $@.new $@ ###----- That's all, folks -------------------------------------------------- - diff --git a/svc/connect.8.in b/svc/connect.8.in index 2cc3c5ce..7021c6b6 100644 --- a/svc/connect.8.in +++ b/svc/connect.8.in @@ -27,12 +27,12 @@ .so ../defs.man.in \"@@@PRE@@@ . .\"-------------------------------------------------------------------------- -.TH connect 8 "8 January 2007" "Straylight/Edgeware" "TrIPE: Trivial IP Encryption" +.TH connect 8 "11 December 2007" "Straylight/Edgeware" "TrIPE: Trivial IP Encryption" . .\"-------------------------------------------------------------------------- .SH "NAME" . -connect \- tripe service to make connections to peers +connect \- tripe service to handle addition and removal of peers . .\"-------------------------------------------------------------------------- .SH "SYNOPSIS" @@ -55,9 +55,24 @@ connect \- tripe service to make connections to peers . The .B connect -service registers new peers with the +service tracks associations with peers and performs various actions at +appropriate stages in the assocations' lifecycles. It also registers +new peers with the .BR tripe (8) -server. +server on demand. +.PP +For example: +.hP \*o +When a peer is added, it arranges to configure the corresponding network +interface correctly, and (if necessary) to initiate a dynamic +connection. +.hP \*o +When a peer is removed, it arranges to bring down the network interface. +.hP \*o +While the peer is known, it +.BR PING s +it at regular intervals. If the peer fails to respond, it can be +removed or reconnected. .PP A peer may participate .I actively @@ -98,11 +113,189 @@ environment variable is used; if that's not set either, then the default default of .B peers.cdb in the current working directory is used instead. -.SS "Dynamic connection protocol" -Dynamic connections are used when the peer's address or port are -unknown, e.g., when it is hidden behind a NAT firewall. +. +.\"-------------------------------------------------------------------------- +.SH "BEHAVIOUR" +. +.SS "Adoption" +The +.B connect +service maintains a list of peers which it has adopted. A peer is +.I eligible for adoption +if it has a record in the peer database +.BR peers.cdb (5) +in which the +.B watch +key is assigned the value +.BR t , +.BR true , +.BR y , +.BR yes , +or +.BR on . +.PP +The service pings adopted peers periodically in order to ensure that +they are alive, and takes appropriate action if no replies are received. .PP -The protocol for passive connection works as follows. +A peer is said to be +.I adopted +when it is added to this list, and +.I disowned +when it removed. +. +.SS "Configuring interfaces" +The +.B connect +service configures network interfaces by invoking an +.B ifup +script. The script is invoked as +.IP +.I script +.IR args ... +.I peer +.I ifname +.IR addr ... +.PP +where the elements are as described below. +.TP +.IR script " and " args +The peer's database record is retrieved; the value assigned to the +.B ifup +key is split into words (quoting is allowed; see +.BR tripe-admin (5) +for details). The first word is the +.IR script ; +subsequent words are gathered to form the +.IR args . +.TP +.I peer +The name of the peer. +.TP +.I ifname +The name of the network interface associated with the peer, as returned +by the +.B IFNAME +administration command (see +.BR tripe-admin (5)). +.TP +.I addr +The network address of the peer's TrIPE server, in the form output by +the +.B ADDR +administration command (see +.BR tripe-admin (5)). +The first word of +.I addr +is therefore a network address family, e.g., +.BR INET . +.PP +The +.B connect +service deconfigures interfaces by invoking an +.B ifdown +script, in a similar manner. The script is invoked as +.IP +.I script +.IR args ... +.I peer +.PP +where the elements are as above, except that +.I script +and +.I args +are formed by splitting the value associated with the peer record's +.B ifdown +key. +.PP +In both of the above cases, if the relevant key (either +.B ifup +or +.BR ifdown ) +is absent, no action is taken. +.PP +The key/value pairs in the peer's database record and the server's +response to the +.B ALGS +administration command (see +.BR tripe-admin (5)) +are passed to the +.B ifup +and +.B ifdown +scripts as environment variables. The environment variable name +corresponding to a key is determined as follows: +.hP \*o +Convert all letters to upper-case. +.hP \*o Convert all sequences of one or more non-alphanumeric characters +to an underscore +.RB ` _ '. +.hP \*o Prefix the resulting name by +.RB ` P_ ' +or +.RB ` A_ ' +depending on whether it came from the peer's database record or the +.B ALGS +output respectively. +.PP +For example, +.B ifname +becomes +.BR P_IFNAME ; +and +.B cipher-blksz +becomes +.BR A_CIPHER_BLKSZ . +. +.SS "Dynamic connection" +If a peer's database record assigns a value to the +.B connect +key, then the +.B connect +service will attempt to establish a connection dynamically with the +peer. The value of the +.B connect +key is invoked as a Bourne shell command, i.e., +.IP +.B /bin/sh \-c +.I connect +.PP +is executed. The command is expected to contact the remote server and +report, on standard output, a challenge string, typically by issuing +a +.B passive +command to the instance of the +.B connect +service running on the peer. The +.B connect +service reads this challenge, and submits the command +.IP +.B GREET +.I peer +.I challenge +.PP +Typically, the +.B connect +command will issue a command such as +.IP +.B SVCSUBMIT connect passive +.I our-name +.PP +where +.I our-name +is the remote peer's name for this host. +.PP +Similarly, if the database record has a +.B disconnect +entry, then +.B connect +will use this to give the peer explicit notification that its services +are no longer needed. The value of the +.B disconnect +key is invoked as a Bourne shell command. This ought to result in a +.B KILL +command being issued to the peer's server. +.PP +In detail, the protocol for passive connection works as follows. .hP 1. The active peer .BR ADD s @@ -111,7 +304,7 @@ its partner, typically using the option to suppress the key-exchange message which the server usually sends immediately, since otherwise the passive peer will warn about it. .hP 2. -The active peer somehow issues the command +The active peer issues the command .RS .IP .B SVCSUBMIT connect passive @@ -119,10 +312,10 @@ The active peer somehow issues the command .PP to the passive peer's server. (Here, .I user -is a name identifying the active peer; see below.) This may be handled -by the -.BR watch (8) -service. +is a name identifying the active peer; see below.) Doing this is the +responsibility of the +.B connect +command. .RE .hP 3. The @@ -147,14 +340,61 @@ from the initial connection request, and .BR ADD s the appropriate peer, with the address from the .BR GREET ing. +. +.SS "Operation" +On startup, +.B connect +requests a list of current peers from the +.BR tripe (8) +server, and adopts any eligible peers. If the +.B \-\-startup +flag was passed on the command line, the newly adopted peers have their +interfaces configured and connection attempts are made. .PP -The -.BR watch (8) -service is capable of performing the active-peer part of this protocol, -sending the correct -.B GREET -command once the challenge has been obtained. The remaining difficulty -is in collecting the challenge from the passive peer. +Adopted peers are pinged at regular intervals (using the +.B PING +administrative command; see +.BR tripe-admin (5)). +This process can be configured by assigning values to keys in the peer's +database record. Some of these parameters are time intervals, +expressed as a nonnegative integer followed optionally by +.BR d , +.BR h , +.BR m , +or +.B s +for days, hours, minutes, or seconds, respectively; if no suffix is +given, seconds are assumed. +.PP +The parameters are as follows. +.TP +.B every +A time interval: how often to ping the peer to ensure that it's still +alive. The default is 2 minutes. +.TP +.B timeout +A time interval: how long to wait for a reply before retrying or giving +up. The default is 10 seconds. +.TP +.B retries +An integer: how many failed attempts to make before deciding that the +peer is unreachable and taking action. The default is 5 attempts. +.PP +The algorithm is as follows. Send up to +.I retries +pings; if a reply is received before the +.I timeout +then the peer is alive; wait +.I every +and check again. If no reply is received within the +.IR timeout , +then try again up to +.I retries +times. If no attempt succeeds, the peer is declared unreachable. If +the peer has a +.B connect +command (i.e., it connects dynamically) then another connection attempt +is made. Otherwise the peer is killed. . .\"-------------------------------------------------------------------------- .SH "SERVICE COMMAND REFERENCE" @@ -174,6 +414,8 @@ The service will submit the command .IR time ] .RB [ \-key .IR tag ] +.RB [ \-priv +.IR tag ] .RB [ \-mobile ] .RB [ \-tunnel .IR driver ] @@ -212,6 +454,15 @@ to the key. .hP \*o The option +.B \-priv +.I tag +is provided if the database record assigns a value +.I tag +to the +.B priv +key. +.hP \*o +The option .B \-mobile is provided if the peer's database record assigns the .B mobile @@ -239,6 +490,16 @@ is the value assigned to the key in the database record. .RE .SP +.B adopted +For each peer being tracked by the +.B connect +service, write a line +.B INFO +.IR name . +(Compatibility note: it's possible that further information will be +provided about each peer, in the form of subsequent tokens. Clients +should be prepared to ignore such tokens.) +.SP .BI "info " peer Lists the database record for the named .IR peer . @@ -250,8 +511,18 @@ For each key/value pair, a line .PP is output. The key/value pairs are output in an arbitrary order. .RE -.TP -.B "list" +.SP +.BI "kick " peer +If +.I peer +is currently added, and its record in the peer database contains a +.B connect +key (see +.BR peers.in ) +then force a reconnection attempt. See +.BR "Dynamic connection" . +.SP +.B "list-active" Output a list of peers in the database. For each peer name .IR peer , a line @@ -311,49 +582,35 @@ line identifying the peer corresponding to the name. . .\"-------------------------------------------------------------------------- -.SH "ERROR MESSAGES" +.SH "NOTIFICATIONS" . -.\"* 20 Error messages (FAIL codes) -The following error codes may be reported. -.SP -.B "connect-timeout" -(For -.BR passive .) -No -.BR GREET ing -was received within the timeout period (default 30 seconds). +.\"* 30 Notification broadcasts (NOTE codes) +All notifications issued by +.B connect +begin with the tokens +.BR "USER connect" . .SP -.BI "malformed-peer " peer " missing-key " key -The database record for -.I peer -has no value for the -.I key -but one was expected. +.B "USER connect peerdb-update" +The peer database has changed. Other interested clients should reopen +the database. .SP -.BI "passive-peer " peer -(For -.BR active .) -An active connection to +.BI "USER connect ping-failed " peer " " error\fR... +An attempt to +.B PING +the named .I peer -was requested, but the database record indicates that it is passive, -i.e., its -.B peer -key has the value -.BR PASSIVE . +failed; the server replied +.B FAIL +.IR error ... .SP -.BI "unknown-peer " peer +.BI "USER connect " process\fR... " stdout " line The -.I peer -has no record in the database. -.SP -.BI "unknown-user " user -(For -.B passive -and -.BR userinfo .) -There is no record of -.I user -in the database. +.I process +spawned by the +.B connect +service unexpectedly wrote +.I line +to its standard output. . .\"-------------------------------------------------------------------------- .SH "WARNINGS" @@ -372,6 +629,99 @@ automatically failed: the command reported .B FAIL .IR error ... +.SP +.BI "USER connect ping-ok " peer +A reply was received to a +.B PING +sent to the +.IR peer , +though earlier attempts had failed. +.SP +.BI "USER connect ping-timeout " peer " attempt " i " of " n +No reply was received to a +.B PING +sent to the +.IR peer . +So far, +.I i +.BR PING s +have been sent; if a total of +.I n +consecutive attempts time out, the +.B connect +service will take further action. +.SP +.B "USER connect reconnecting " peer +The dynamically connected +.I peer +seems to be unresponsive. The +.B connect +service will attempt to reconnect. +.SP +.BI "USER connect " process\fR... " stderr " line +The +.I process +spawned by the +.B connect +service wrote +.I line +to its standard error. +.SP +.BI "USER connect " process\fR... " exit-nonzero " code +The +.I process +spawned by the +.B connect +service exited with the nonzero status +.IR code . +.SP +.BI "USER connect " process\fR... " exit-signal S" code +The +.I process +spawned by the +.B connect +service was killed by signal +.IR code . +Here, +.I code +is the numeric value of the fatal signal. +.SP +.BI "USER connect " process\fR... " exit-unknown " status +The +.I process +spawned by the +.B connect +service exited with an unknown +.IR status . +Here, +.I status +is the raw exit status, as returned by +.BR waitpid (2), +in hexadecimal. +. +.\"-------------------------------------------------------------------------- +.SH "CHILD PROCESS IDENTIFIERS" +. +.\"* 50 Child process identifiers +Some of the warnings and notifications refer to processes spawned by +.B connect +under various circumstances. The process identifiers are as follows. +.SP +.BI "connect " peer +A child spawned in order to establish a dynamic connection with +.IR peer . +.SP +.BI "disconnect " peer +A child spawned in order to shut down a dynamic connection with +.IR peer . +.SP +.BI "ifdown " peer +A child spawned to deconfigure the network interface for +.IR peer . +.SP +.BI "ifup " peer +A child spawned to configure the network interface for +.IR peer . . .\"-------------------------------------------------------------------------- .SH "SUMMARY" @@ -383,7 +733,6 @@ command reported . .BR tripe-service (7), .BR peers.in (5), -.BR watch (8), .BR tripe (8). . .\"-------------------------------------------------------------------------- diff --git a/svc/connect.in b/svc/connect.in index 0031d36f..000ca50e 100644 --- a/svc/connect.in +++ b/svc/connect.in @@ -1,9 +1,9 @@ #! @PYTHON@ ### -*-python-*- ### -### Service for establishing dynamic connections +### Connect to remote peers, and keep track of them ### -### (c) 2006 Straylight/Edgeware +### (c) 2007 Straylight/Edgeware ### ###----- Licensing notice --------------------------------------------------- @@ -32,14 +32,243 @@ VERSION = '@VERSION@' from optparse import OptionParser import tripe as T import os as OS +import signal as SIG +import errno as E import cdb as CDB import mLib as M +import re as RX from time import time +import subprocess as PROC S = T.svcmgr ###-------------------------------------------------------------------------- -### Main service machinery. +### Running auxiliary commands. + +class SelLineQueue (M.SelLineBuffer): + """Glues the select-line-buffer into the coroutine queue system.""" + + def __new__(cls, file, queue, tag, kind): + """See __init__ for documentation.""" + return M.SelLineBuffer.__new__(cls, file.fileno()) + + def __init__(me, file, queue, tag, kind): + """ + Initialize a new line-reading adaptor. + + The adaptor reads lines from FILE. Each line is inserted as a message of + the stated KIND, bearing the TAG, into the QUEUE. End-of-file is + represented as None. + """ + me._q = queue + me._file = file + me._tag = tag + me._kind = kind + me.enable() + + @T._callback + def line(me, line): + me._q.put((me._tag, me._kind, line)) + + @T._callback + def eof(me): + me.disable() + me._q.put((me._tag, me._kind, None)) + +class ErrorWatch (T.Coroutine): + """ + An object which watches stderr streams for errors and converts them into + warnings of the form + + WARN connect INFO stderr LINE + + The INFO is a list of tokens associated with the file when it was + registered. + + Usually there is a single ErrorWatch object, called errorwatch. + """ + + def __init__(me): + """Initialization: there are no arguments.""" + T.Coroutine.__init__(me) + me._q = T.Queue() + me._map = {} + me._seq = 1 + + def watch(me, file, info): + """ + Adds FILE to the collection of files to watch. + + INFO will be written in the warning messages from this FILE. Returns a + sequence number which can be used to unregister the file again. + """ + seq = me._seq + me._seq += 1 + me._map[seq] = info, SelLineQueue(file, me._q, seq, 'stderr') + return seq + + def unwatch(me, seq): + """Stop watching the file with sequence number SEQ.""" + del me._map[seq] + return me + + def run(me): + """ + Coroutine function: read items from the queue and report them. + + Unregisters files automatically when they reach EOF. + """ + while True: + seq, _, line = me._q.get() + if line is None: + me.unwatch(seq) + else: + S.warn(*['connect'] + me._map[seq][0] + ['stderr', line]) + +def dbwatch(): + """ + Coroutine function: wake up every second and notice changes to the + database. When a change happens, tell the Pinger (q.v.) to rescan its + peers. + """ + cr = T.Coroutine.getcurrent() + main = cr.parent + fw = M.FWatch(opts.cdb) + while True: + timer = M.SelTimer(time() + 1, lambda: cr.switch()) + main.switch() + if fw.update(): + pinger.rescan(False) + S.notify('connect', 'peerdb-update') + +class ChildWatch (M.SelSignal): + """ + An object which watches for specified processes exiting and reports + terminations by writing items of the form (TAG, 'exit', RESULT) to a queue. + + There is usually only one ChildWatch object, called childwatch. + """ + + def __new__(cls): + """Initialize the child-watcher.""" + return M.SelSignal.__new__(cls, SIG.SIGCHLD) + + def __init__(me): + """Initialize the child-watcher.""" + me._pid = {} + me.enable() + + def watch(me, pid, queue, tag): + """ + Register PID as a child to watch. If it exits, write (TAG, 'exit', CODE) + to the QUEUE, where CODE is one of + + * None (successful termination) + * ['exit-nonzero', CODE] (CODE is a string!) + * ['exit-signal', 'S' + CODE] (CODE is the signal number as a string) + * ['exit-unknown', STATUS] (STATUS is the entire exit status, in hex) + """ + me._pid[pid] = queue, tag + return me + + def unwatch(me, pid): + """Unregister PID as a child to watch.""" + del me._pid[pid] + return me + + @T._callback + def signalled(me): + """ + Called when child processes exit: collect exit statuses and report + failures. + """ + while True: + try: + pid, status = OS.waitpid(-1, OS.WNOHANG) + except OSError, exc: + if exc.errno == E.ECHILD: + break + if pid == 0: + break + if pid not in me._pid: + continue + queue, tag = me._pid[pid] + if OS.WIFEXITED(status): + exit = OS.WEXITSTATUS(status) + if exit == 0: + code = None + else: + code = ['exit-nonzero', str(exit)] + elif OS.WIFSIGNALED(status): + code = ['exit-signal', 'S' + str(OS.WTERMSIG(status))] + else: + code = ['exit-unknown', hex(status)] + queue.put((tag, 'exit', code)) + +class Command (object): + """ + Represents a running command. + + This class is the main interface to the machery provided by the ChildWatch + and ErrorWatch objects. See also potwatch. + """ + + def __init__(me, info, queue, tag, args, env): + """ + Start a new child process. + + The ARGS are a list of arguments to be given to the child process. The + ENV is either None or a dictionary of environment variable assignments to + override the extant environment. INFO is a list of tokens to be included + in warnings about the child's stderr output. If the child writes a line + to standard output, put (TAG, 'stdout', LINE) to the QUEUE. When the + child exits, write (TAG, 'exit', CODE) to the QUEUE. + """ + me._info = info + me._q = queue + me._tag = tag + myenv = OS.environ.copy() + if env: myenv.update(env) + me._proc = PROC.Popen(args = args, env = myenv, bufsize = 1, + stdout = PROC.PIPE, stderr = PROC.PIPE) + me._lq = SelLineQueue(me._proc.stdout, queue, tag, 'stdout') + errorwatch.watch(me._proc.stderr, info) + childwatch.watch(me._proc.pid, queue, tag) + + def __del__(me): + """ + If I've been forgotten then stop watching for termination. + """ + childwatch.unwatch(me._proc.pid) + +def potwatch(what, name, q): + """ + Watch the queue Q for activity as reported by a Command object. + + Information from the process's stdout is reported as + + NOTE WHAT NAME stdout LINE + + abnormal termination is reported as + + WARN WHAT NAME CODE + + where CODE is what the ChildWatch wrote. + """ + eofp = deadp = False + while not deadp or not eofp: + _, kind, more = q.get() + if kind == 'stdout': + if more is None: + eofp = True + else: + S.notify('connect', what, name, 'stdout', more) + elif kind == 'exit': + if more: S.warn('connect', what, name, *more) + deadp = True + +###-------------------------------------------------------------------------- +### Peer database utilities. _magic = ['_magic'] # An object distinct from all others @@ -54,30 +283,382 @@ class Peer (object): one given on the command-line. """ me.name = peer - try: - record = (cdb or CDB.init(opts.cdb))['P' + peer] - except KeyError: - raise T.TripeJobError('unknown-peer', peer) + record = (cdb or CDB.init(opts.cdb))['P' + peer] me.__dict__.update(M.URLDecode(record, semip = True)) - def get(me, key, default = _magic): + def get(me, key, default = _magic, filter = None): """ Get the information stashed under KEY from the peer's database record. If DEFAULT is given, then use it if the database doesn't contain the - necessary information. If no DEFAULT is given, then report an error. + necessary information. If no DEFAULT is given, then report an error. If + a FILTER function is given then apply it to the information from the + database before returning it. """ attr = me.__dict__.get(key, default) if attr is _magic: raise T.TripeJobError('malformed-peer', me.name, 'missing-key', key) + elif filter is not None: + attr = filter(attr) return attr + def has(me, key): + """ + Return whether the peer's database record has the KEY. + """ + return key in me.__dict__ + def list(me): """ Iterate over the available keys in the peer's database record. """ return me.__dict__.iterkeys() +def boolean(value): + """Parse VALUE as a boolean.""" + return value in ['t', 'true', 'y', 'yes', 'on'] + +###-------------------------------------------------------------------------- +### Waking up and watching peers. + +def run_connect(peer, cmd): + """ + Start the job of connecting to the passive PEER. + + The CMD string is a shell command which will connect to the peer (via some + back-channel, say ssh and userv), issue a command + + SVCSUBMIT connect passive [OPTIONS] USER + + and write the resulting challenge to standard error. + """ + q = T.Queue() + cmd = Command(['connect', peer.name], q, 'connect', + ['/bin/sh', '-c', cmd], None) + _, kind, more = q.peek() + if kind == 'stdout': + if more is None: + S.warn('connect', 'connect', peer.name, 'unexpected-eof') + else: + chal = more + S.greet(peer.name, chal) + q.get() + potwatch('connect', peer.name, q) + +def run_disconnect(peer, cmd): + """ + Start the job of disconnecting from a passive PEER. + + The CMD string is a shell command which will disconnect from the peer. + """ + q = T.Queue() + cmd = Command(['disconnect', peer.name], q, 'disconnect', + ['/bin/sh', '-c', cmd], None) + potwatch('disconnect', peer.name, q) + +_pingseq = 0 +class PingPeer (object): + """ + Object representing a peer which we are pinging to ensure that it is still + present. + + PingPeer objects are held by the Pinger (q.v.). The Pinger maintains an + event queue -- which saves us from having an enormous swarm of coroutines + -- but most of the actual work is done here. + + In order to avoid confusion between different PingPeer instances for the + same actual peer, each PingPeer has a sequence number (its `seq' + attribute). Events for the PingPeer are identified by a (PEER, SEQ) pair. + (Using the PingPeer instance itself will prevent garbage collection of + otherwise defunct instances.) + """ + + def __init__(me, pinger, queue, peer, pingnow): + """ + Create a new PingPeer. + + The PINGER is the Pinger object we should send the results to. This is + used when we remove ourselves, if the peer has been explicitly removed. + + The QUEUE is the event queue on which timer and ping-command events + should be written. + + The PEER is a `Peer' object describing the peer. + + If PINGNOW is true, then immediately start pinging the peer. Otherwise + wait until the usual retry interval. + """ + global _pingseq + me._pinger = pinger + me._q = queue + me._peer = peer.name + me.update(peer) + me.seq = _pingseq + _pingseq += 1 + me._failures = 0 + if pingnow: + me._timer = None + me._ping() + else: + me._timer = M.SelTimer(time() + me._every, me._time) + + def update(me, peer): + """ + Refreshes the timer parameters for this peer. We don't, however, + immediately reschedule anything: that will happen next time anything + interesting happens. + """ + if peer is None: peer = Peer(me._peer) + assert peer.name == me._peer + me._every = peer.get('every', filter = T.timespec, default = 120) + me._timeout = peer.get('timeout', filter = T.timespec, default = 10) + me._retries = peer.get('retries', filter = int, default = 5) + me._connectp = peer.has('connect') + return me + + def _ping(me): + """ + Send a ping to the peer; the result is sent to the Pinger's event queue. + """ + S.rawcommand(T.TripeAsynchronousCommand( + me._q, (me._peer, me.seq), + ['EPING', + '-background', S.bgtag(), + '-timeout', str(me._timeout), + '--', + me._peer])) + + def _reconnect(me): + peer = Peer(me._peer) + if me._connectp: + S.warn('connect', 'reconnecting', me._peer) + S.forcekx(me._peer) + T.spawn(run_connect, peer, peer.get('connect')) + me._timer = M.SelTimer(time() + me._every, me._time) + else: + S.kill(me._peer) + + def event(me, code, stuff): + """ + Respond to an event which happened to this peer. + + Timer events indicate that we should start a new ping. (The server has + its own timeout which detects lost packets.) + + We trap unknown-peer responses and detach from the Pinger. + + If the ping fails and we run out of retries, we attempt to restart the + connection. + """ + if code == 'TIMER': + me._failures = 0 + me._ping() + elif code == 'FAIL': + S.notify('connect', 'ping-failed', me._peer, *stuff) + if not stuff: + pass + elif stuff[0] == 'unknown-peer': + me._pinger.kill(me._peer) + elif stuff[0] == 'ping-send-failed': + me._reconnect() + elif code == 'INFO': + if stuff[0] == 'ping-ok': + if me._failures > 0: + S.warn('connect', 'ping-ok', me._peer) + me._timer = M.SelTimer(time() + me._every, me._time) + elif stuff[0] == 'ping-timeout': + me._failures += 1 + S.warn('connect', 'ping-timeout', me._peer, + 'attempt', str(me._failures), 'of', str(me._retries)) + if me._failures < me._retries: + me._ping() + else: + me._reconnect() + elif stuff[0] == 'ping-peer-died': + me._pinger.kill(me._peer) + + @T._callback + def _time(me): + """ + Handle timer callbacks by posting a timeout event on the queue. + """ + me._timer = None + me._q.put(((me._peer, me.seq), 'TIMER', None)) + + def __str__(me): + return 'PingPeer(%s, %d, f = %d)' % (me._peer, me.seq, me._failures) + def __repr__(me): + return str(me) + +class Pinger (T.Coroutine): + """ + The Pinger keeps track of the peers which we expect to be connected and + takes action if they seem to stop responding. + + There is usually only one Pinger, called pinger. + + The Pinger maintains a collection of PingPeer objects, and an event queue. + The PingPeers direct the results of their pings, and timer events, to the + event queue. The Pinger's coroutine picks items off the queue and + dispatches them back to the PingPeers as appropriate. + """ + + def __init__(me): + """Initialize the Pinger.""" + T.Coroutine.__init__(me) + me._peers = {} + me._q = T.Queue() + + def run(me): + """ + Coroutine function: reads the pinger queue and sends events to the + PingPeer objects they correspond to. + """ + while True: + (peer, seq), code, stuff = me._q.get() + if peer in me._peers and seq == me._peers[peer].seq: + me._peers[peer].event(code, stuff) + + def add(me, peer, pingnow): + """ + Add PEER to the collection of peers under the Pinger's watchful eye. + The arguments are as for PingPeer: see above. + """ + me._peers[peer.name] = PingPeer(me, me._q, peer, pingnow) + return me + + def kill(me, peername): + """Remove PEER from the peers being watched by the Pinger.""" + del me._peers[peername] + return me + + def rescan(me, startup): + """ + General resynchronization method. + + We scan the list of peers (with connect scripts) known at the server. + Any which are known to the Pinger but aren't known to the server are + removed from our list; newly arrived peers are added. (Note that a peer + can change state here either due to the server sneakily changing its list + without issuing notifications or, more likely, the database changing its + idea of whether a peer is interesting.) Finally, PingPeers which are + still present are prodded to update their timing parameters. + + This method is called once at startup to pick up the peers already + installed, and again by the dbwatcher coroutine when it detects a change + to the database. + """ + if T._debug: print '# rescan peers' + correct = {} + start = {} + for name in S.list(): + try: peer = Peer(name) + except KeyError: continue + if peer.get('watch', filter = boolean, default = False): + if T._debug: print '# interesting peer %s' % peer + correct[peer.name] = start[peer.name] = peer + elif startup: + if T._debug: print '# peer %s ready for adoption' % peer + start[peer.name] = peer + for name, obj in me._peers.items(): + try: + peer = correct[name] + except KeyError: + if T._debug: print '# peer %s vanished' % name + del me._peers[name] + else: + obj.update(peer) + for name, peer in start.iteritems(): + if name in me._peers: continue + if startup: + if T._debug: print '# setting up peer %s' % name + ifname = S.ifname(name) + addr = S.addr(name) + T.defer(adoptpeer, peer, ifname, *addr) + else: + if T._debug: print '# adopting new peer %s' % name + me.add(peer, True) + return me + + def adopted(me): + """ + Returns the list of peers being watched by the Pinger. + """ + return me._peers.keys() + +###-------------------------------------------------------------------------- +### New connections. + +def encode_envvars(env, prefix, vars): + """ + Encode the variables in VARS suitably for including in a program + environment. Lowercase letters in variable names are forced to uppercase; + runs of non-alphanumeric characters are replaced by single underscores; and + the PREFIX is prepended. The resulting variables are written to ENV. + """ + for k, v in vars.iteritems(): + env[prefix + r_bad.sub('_', k.upper())] = v + +r_bad = RX.compile(r'[\W_]+') +def envvars(peer): + """ + Translate the database information for a PEER into a dictionary of + environment variables with plausible upper-case names and a P_ prefix. + Also collect the crypto information into A_ variables. + """ + env = {} + encode_envvars(env, 'P_', dict([(k, peer.get(k)) for k in peer.list()])) + encode_envvars(env, 'A_', S.algs(peer.name)) + return env + +def run_ifupdown(what, peer, *args): + """ + Run the interface up/down script for a peer. + + WHAT is 'ifup' or 'ifdown'. PEER names the peer in question. ARGS is a + list of arguments to pass to the script, in addition to the peer name. + + The command is run and watched in the background by potwatch. + """ + q = T.Queue() + c = Command([what, peer.name], q, what, + M.split(peer.get(what), quotep = True)[0] + + [peer.name] + list(args), + envvars(peer)) + potwatch(what, peer.name, q) + +def adoptpeer(peer, ifname, *addr): + """ + Add a new peer to our collection. + + PEER is the `Peer' object; IFNAME is the interface name for its tunnel; and + ADDR is the list of tokens representing its address. + + We try to bring up the interface and provoke a connection to the peer if + it's passive. + """ + if peer.has('ifup'): + T.Coroutine(run_ifupdown, name = 'ifup %s' % peer.name) \ + .switch('ifup', peer, ifname, *addr) + cmd = peer.get('connect', default = None) + if cmd is not None: + T.Coroutine(run_connect, name = 'connect %s' % peer.name) \ + .switch(peer, cmd) + if peer.get('watch', filter = boolean, default = False): + pinger.add(peer, False) + +def disownpeer(peer): + """Drop the PEER from the Pinger and put its interface to bed.""" + try: pinger.kill(peer) + except KeyError: pass + cmd = peer.get('disconnect', default = None) + if cmd is not None: + T.Coroutine(run_disconnect, name = 'disconnect %s' % peer.name) \ + .switch(peer, cmd) + if peer.has('ifdown'): + T.Coroutine(run_ifupdown, name = 'ifdown %s' % peer.name) \ + .switch('ifdown', peer) + def addpeer(peer, addr): """ Process a connect request from a new peer PEER on address ADDR. @@ -99,19 +680,65 @@ def addpeer(peer, addr): except T.TripeError, exc: raise T.TripeJobError(*exc.args) +## Dictionary mapping challenges to waiting passive-connection coroutines. +chalmap = {} + +def notify(_, code, *rest): + """ + Watch for notifications. + + We trap ADD and KILL notifications, and send them straight to adoptpeer and + disownpeer respectively; and dispatch GREET notifications to the + corresponding waiting coroutine. + """ + if code == 'ADD': + try: p = Peer(rest[0]) + except KeyError: return + adoptpeer(p, *rest[1:]) + elif code == 'KILL': + try: p = Peer(rest[0]) + except KeyError: return + disownpeer(p, *rest[1:]) + elif code == 'GREET': + chal = rest[0] + try: cr = chalmap[chal] + except KeyError: pass + else: cr.switch(rest[1:]) + +###-------------------------------------------------------------------------- +### Command implementation. + +def cmd_kick(name): + """ + kick NAME: Force a new connection attempt for the NAMEd peer. + """ + if name not in pinger.adopted(): + raise T.TripeJobError('peer-not-adopted', name) + try: peer = Peer(name) + except KeyError: raise T.TripeJobError('unknown-peer', name) + T.spawn(connect, peer) + +def cmd_adopted(): + """ + adopted: Report a list of adopted peers. + """ + for name in pinger.adopted(): + T.svcinfo(name) + def cmd_active(name): """ active NAME: Handle an active connection request for the peer called NAME. The appropriate address is read from the database automatically. """ - peer = Peer(name) + try: peer = Peer(name) + except KeyError: raise T.TripeJobError('unknown-peer', name) addr = peer.get('peer') if addr == 'PASSIVE': raise T.TripeJobError('passive-peer', name) addpeer(peer, M.split(addr, quotep = True)[0]) -def cmd_list(): +def cmd_listactive(): """ list: Report a list of the available active peers. """ @@ -124,7 +751,8 @@ def cmd_info(name): """ info NAME: Report the database entries for the named peer. """ - peer = Peer(name) + try: peer = Peer(name) + except KeyError: raise T.TripeJobError('unknown-peer', name) items = list(peer.list()) items.sort() for i in items: @@ -134,14 +762,9 @@ def cmd_userpeer(user): """ userpeer USER: Report the peer name for the named user. """ - try: - peer = CDB.init(opts.cdb)['U' + user] - except KeyError: - raise T.TripeJobError('unknown-user', user) - T.svcinfo(peer) - -## Dictionary mapping challenges to waiting passive-connection coroutines. -chalmap = {} + try: name = CDB.init(opts.cdb)['U' + user] + except KeyError: raise T.TripeJobError('unknown-user', user) + T.svcinfo(name) def cmd_passive(*args): """ @@ -156,10 +779,10 @@ def cmd_passive(*args): if opt == '-timeout': timeout = T.timespec(op.arg()) user, = op.rest(1, 1) - try: - peer = CDB.init(opts.cdb)['U' + user] - except KeyError: - raise T.TripeJobError('unknown-user', user) + try: name = CDB.init(opts.cdb)['U' + user] + except KeyError: raise T.TripeJobError('unknown-user', user) + try: peer = Peer(name) + except KeyError: raise T.TripeJobError('unknown-peer', name) chal = S.getchal() cr = T.Coroutine.getcurrent() timer = M.SelTimer(time() + timeout, lambda: cr.switch(None)) @@ -169,24 +792,10 @@ def cmd_passive(*args): addr = cr.parent.switch() if addr is None: raise T.TripeJobError('connect-timeout') - addpeer(Peer(peer), addr) + addpeer(peer, addr) finally: del chalmap[chal] -def notify(_, code, *rest): - """ - Watch for notifications. - - In particular, if a GREETing appears quoting a challenge in the chalmap - then wake up the corresponding coroutine. - """ - if code != 'GREET': - return - chal = rest[0] - addr = rest[1:] - if chal in chalmap: - chalmap[chal].switch(addr) - ###-------------------------------------------------------------------------- ### Start up. @@ -194,10 +803,14 @@ def setup(): """ Service setup. - Register the notification-watcher, and add the automatic active peers. + Register the notification watcher, rescan the peers, and add automatic + active peers. """ S.handler['NOTE'] = notify S.watch('+n') + + pinger.rescan(opts.startup) + if opts.startup: cdb = CDB.init(opts.cdb) try: @@ -211,6 +824,18 @@ def setup(): except T.TripeJobError, err: S.warn('connect', 'auto-add-failed', name, *err.args) +def init(): + """ + Initialization to be done before service startup. + """ + global errorwatch, childwatch, pinger + errorwatch = ErrorWatch() + childwatch = ChildWatch() + pinger = Pinger() + T.Coroutine(dbwatch, name = 'dbwatch').switch() + errorwatch.switch() + pinger.switch() + def parse_options(): """ Parse the command-line options. @@ -247,18 +872,20 @@ def parse_options(): return opts ## Service table, for running manually. -service_info = [('connect', VERSION, { +service_info = [('connect', T.VERSION, { + 'adopted': (0, 0, '', cmd_adopted), + 'kick': (1, 1, 'PEER', cmd_kick), 'passive': (1, None, '[OPTIONS] USER', cmd_passive), 'active': (1, 1, 'PEER', cmd_active), 'info': (1, 1, 'PEER', cmd_info), - 'list': (0, 0, '', cmd_list), + 'list-active': (0, 0, '', cmd_listactive), 'userpeer': (1, 1, 'USER', cmd_userpeer) })] if __name__ == '__main__': opts = parse_options() T.runservices(opts.tripesock, service_info, - setup = setup, + init = init, setup = setup, daemon = opts.daemon) ###----- That's all, folks -------------------------------------------------- diff --git a/svc/watch.8.in b/svc/watch.8.in deleted file mode 100644 index 79ddbef9..00000000 --- a/svc/watch.8.in +++ /dev/null @@ -1,495 +0,0 @@ -.\" -*-nroff-*- -.\". -.\" Manual for the watch service -.\" -.\" (c) 2008 Straylight/Edgeware -.\" -. -.\"----- Licensing notice --------------------------------------------------- -.\" -.\" This file is part of Trivial IP Encryption (TrIPE). -.\" -.\" TrIPE is free software; you can redistribute it and/or modify -.\" it under the terms of the GNU General Public License as published by -.\" the Free Software Foundation; either version 2 of the License, or -.\" (at your option) any later version. -.\" -.\" TrIPE is distributed in the hope that it will be useful, -.\" but WITHOUT ANY WARRANTY; without even the implied warranty of -.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -.\" GNU General Public License for more details. -.\" -.\" You should have received a copy of the GNU General Public License -.\" along with TrIPE; if not, write to the Free Software Foundation, -.\" Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -. -.\"-------------------------------------------------------------------------- -.so ../defs.man.in \"@@@PRE@@@ -. -.\"-------------------------------------------------------------------------- -.TH watch 8 "11 December 2007" "Straylight/Edgeware" "TrIPE: Trivial IP Encryption" -. -.\"-------------------------------------------------------------------------- -.SH "NAME" -. -watch \- tripe service handle addition and removal of peers -. -.\"-------------------------------------------------------------------------- -.SH "SYNOPSIS" -. -.B watch -.RB [ \-a -.IR socket ] -.RB [ \-d -.IR dir ] -.RB [ \-p -.IR file ] -.br -\& \c -.RB [ \-\-daemon ] -.RB [ \-\-debug ] -.RB [ \-\-startup ] -. -.\"-------------------------------------------------------------------------- -.SH "DESCRIPTION" -. -The -.B watch -service tracks associations with peers and performs various actions at -appropriate stages in the assocations' lifecycles. -.PP -For example: -.hP \*o -When a peer is added, it arranges to configure the corresponding network -interface correctly, and (if necessary) to initiate a dynamic -connection. -.hP \*o -When a peer is removed, it arranges to bring down the network interface. -.hP \*o -While the peer is known, it -.BR PING s -it at regular intervals. If the peer fails to respond, it can be -removed or reconnected. -.SS "Command line" -In addition to the standard options described in -.BR tripe-service (7), -the following command-line options are recognized. -.TP -.BI "\-p, \-\-peerdb=" file -Use -.I file -as the (CDB format) peer database. In the absence of this option, the -file named by the -.B TRIPEPEERDB -environment variable is used; if that's not set either, then the default -default of -.B peers.cdb -in the current working directory is used instead. -. -.\"-------------------------------------------------------------------------- -.SH "BEHAVIOUR" -. -.SS "Adoption" -The -.B watch -service maintains a list of peers which it has adopted. A peer is -.I eligible for adoption -if it has a record in the peer database -.BR peers.cdb (5) -in which the -.B watch -key is assigned the value -.BR t , -.BR true , -.BR y , -.BR yes , -or -.BR on . -.PP -The service pings adopted peers periodically in order to ensure that -they are alive, and takes appropriate action if no replies are received. -.PP -A peer is said to be -.I adopted -when it is added to this list, and -.I disowned -when it removed. -.SS "Configuring interfaces" -The -.B watch -service configures network interfaces by invoking an -.B ifup -script. The script is invoked as -.IP -.I script -.IR args ... -.I peer -.I ifname -.IR addr ... -.PP -where the elements are as described below. -.TP -.IR script " and " args -The peer's database record is retrieved; the value assigned to the -.B ifup -key is split into words (quoting is allowed; see -.BR tripe-admin (5) -for details). The first word is the -.IR script ; -subsequent words are gathered to form the -.IR args . -.TP -.I peer -The name of the peer. -.TP -.I ifname -The name of the network interface associated with the peer, as returned -by the -.B IFNAME -administration command (see -.BR tripe-admin (5)). -.TP -.I addr -The network address of the peer's TrIPE server, in the form output by -the -.B ADDR -administration command (see -.BR tripe-admin (5)). -The first word of -.I addr -is therefore a network address family, e.g., -.BR INET . -.PP -The -.B watch -service deconfigures interfaces by invoking an -.B ifdown -script, in a similar manner. The script is invoked as -.IP -.I script -.IR args ... -.I peer -.PP -where the elements are as above, except that -.I script -and -.I args -are formed by splitting the value associated with the peer record's -.B ifdown -key. -.PP -In both of the above cases, if the relevant key (either -.B ifup -or -.BR ifdown ) -is absent, no action is taken. -.PP -The key/value pairs in the peer's database record and the server's -response to the -.B ALGS -administration command (see -.BR tripe-admin (5)) -are passed to the -.B ifup -and -.B ifdown -scripts as environment variables. The environment variable name -corresponding to a key is determined as follows: -.hP \*o -Convert all letters to upper-case. -.hP \*o Convert all sequences of one or more non-alphanumeric characters -to an underscore -.RB ` _ '. -.hP \*o Prefix the resulting name by -.RB ` P_ ' -or -.RB ` A_ ' -depending on whether it came from the peer's database record or the -.B ALGS -output respectively. -.PP -For example, -.B ifname -becomes -.BR P_IFNAME ; -and -.B cipher-blksz -becomes -.BR A_CIPHER_BLKSZ . -.SS "Dynamic connection" -If a peer's database record assigns a value to the -.B connect -key, then the -.B watch -service will attempt to establish a connection dynamically with the -peer. The value of the -.B connect -key is invoked as a Bourne shell command, i.e., -.IP -.B /bin/sh \-c -.I connect -.PP -is executed. The command is expected to contact the remote server and -report, on standard output, a challenge string. The -.B watch -service reads this challenge, and submits the command -.IP -.B GREET -.I peer -.I challenge -.PP -Typically, the -.B connect -command will issue a command such as -.IP -.B SVCSUBMIT connect passive -.I our-name -.PP -where -.I our-name -is the remote peer's name for this host. -.PP -Similarly, if the database record has a -.B disconnect -entry, then -.B watch -will use this to give the peer explicit notification that its services -are no longer needed. The value f the -.B disconnect -key is invoked as a Bourne shell command. This ought to result in a -.B KILL -command being issued to the peer's server. -.SS "Operation" -On startup, -.B watch -requests a list of current peers from the -.BR tripe (8) -server, and adopts any eligible peers. If the -.B \-\-startup -flag was passed on the command line, -the newly adopted peers have their interfaces configured and connection -attempts are made. -.PP -Adopted peers are pinged at regular intervals (using the -.B PING -administrative command; see -.BR tripe-admin (5)). -This process can be configured by assigning values to keys in the peer's -database record. Some of these parameters are time intervals, -expressed as a nonnegative integer followed optionally by -.BR d , -.BR h , -.BR m , -or -.B s -for days, hours, minutes, or seconds, respectively; if no suffix is -given, seconds are assumed. -.PP -The parameters are as follows. -.TP -.B every -A time interval: how often to ping the peer to ensure that it's still -alive. The default is 2 minutes. -.TP -.B timeout -A time interval: how long to wait for a reply before retrying or giving -up. The default is 10 seconds. -.TP -.B retries -An integer: how many failed attempts to make before deciding that the -peer is unreachable and taking action. The default is 5 attempts. -.PP -The algorithm is as follows. Send up to -.I retries -pings; if a reply is received before the -.I timeout -then the peer is alive; wait -.I every -and check again. If no reply is received within the -.IR timeout , -then try again up to -.I retries -times. If no attempt succeeds, the peer is declared unreachable. If -the peer has a -.B connect -command (i.e., it connects dynamically) then another connection attempt -is made. Otherwise the peer is killed. -. -.\"-------------------------------------------------------------------------- -.SH "SERVICE COMMAND REFERENCE" -. -.\"* 10 Service commands -The commands provided by the service are as follows. -.SP -.B adopted -For each peer being tracked by the -.B watch -service, write a line -.B INFO -.IR name . -(Compatibility note: it's possible that further information will be -provided about each peer, in the form of subsequent tokens. Clients -should be prepared to ignore such tokens.) -.SP -.BI "kick " peer -If -.I peer -is currently added, and its record in the peer database contains a -.B connect -key (see -.BR peers.in ) -then force a reconnection attempt. See -.BR "Dynamic connection" . -. -.\"-------------------------------------------------------------------------- -.SH "NOTIFICATIONS" -. -.\"* 30 Notification broadcasts (NOTE codes) -All notifications issued by -.B watch -begin with the tokens -.BR "USER watch" . -.SP -.B "USER watch peerdb-update" -The peer database has changed. Other interested clients should reopen -the database. -.SP -.BI "USER watch ping-failed " peer " " error\fR... -An attempt to -.B PING -the named -.I peer -failed; the server replied -.B FAIL -.IR error ... -.SP -.BI "USER watch " process\fR... " stdout " line -The -.I process -spawned by the -.B watch -service unexpectedly wrote -.I line -to its standard output. -. -.\"-------------------------------------------------------------------------- -.SH "WARNINGS" -. -.\"* 40 Warning broadcasts (WARN codes) -All warnings issued by -.B watch -begin with the tokens -.BR "USER watch" . -.SP -.BI "USER watch ping-ok " peer -A reply was received to a -.B PING -sent to the -.IR peer , -though earlier attempts had failed. -.SP -.BI "USER watch ping-timeout " peer " attempt " i " of " n -No reply was received to a -.B PING -sent to the -.IR peer . -So far, -.I i -.BR PING s -have been sent; if a total of -.I n -consecutive attempts time out, the -.B watch -service will take further action. -.SP -.B "USER watch reconnecting " peer -The dynamically connected -.I peer -seems to be unresponsive. The -.B watch -service will attempt to reconnect. -.SP -.BI "USER watch " process\fR... " stderr " line -The -.I process -spawned by the -.B watch -service wrote -.I line -to its standard error. -.SP -.BI "USER watch " process\fR... " exit-nonzero " code -The -.I process -spawned by the -.B watch -service exited with the nonzero status -.IR code . -.SP -.BI "USER watch " process\fR... " exit-signal S" code -The -.I process -spawned by the -.B watch -service was killed by signal -.IR code . -Here, -.I code -is the numeric value of the fatal signal. -.SP -.BI "USER watch " process\fR... " exit-unknown " status -The -.I process -spawned by the -.B watch -service exited with an unknown -.IR status . -Here, -.I status -is the raw exit status, as returned by -.BR waitpid (2), -in hexadecimal. -. -.\"-------------------------------------------------------------------------- -.SH "CHILD PROCESS IDENTIFIERS" -. -.\"* 50 Child process identifiers -Some of the warnings and notifications refer to processes spawned by -.B watch -under various circumstances. The process identifiers are as follows. -.SP -.BI "connect " peer -A child spawned in order to establish a dynamic connection with -.IR peer . -.SP -.BI "disconnect " peer -A child spawned in order to shut down a dynamic connection with -.IR peer . -.SP -.BI "ifdown " peer -A child spawned to deconfigure the network interface for -.IR peer . -.SP -.BI "ifup " peer -A child spawned to configure the network interface for -.IR peer . -. -.\"-------------------------------------------------------------------------- -.SH "SUMMARY" -. -.\"= summary -. -.\"-------------------------------------------------------------------------- -.SH "SEE ALSO" -. -.BR tripe-service (7), -.BR peers.in (5), -.BR connect (8), -.BR tripe (8). -. -.\"-------------------------------------------------------------------------- -.SH "AUTHOR" -. -Mark Wooding, -. -.\"----- That's all, folks -------------------------------------------------- diff --git a/svc/watch.in b/svc/watch.in deleted file mode 100644 index 7bdda365..00000000 --- a/svc/watch.in +++ /dev/null @@ -1,773 +0,0 @@ -#! @PYTHON@ -### -*-python-*- -### -### Watch arrival and departure of peers -### -### (c) 2007 Straylight/Edgeware -### - -###----- Licensing notice --------------------------------------------------- -### -### This file is part of Trivial IP Encryption (TrIPE). -### -### TrIPE is free software; you can redistribute it and/or modify -### it under the terms of the GNU General Public License as published by -### the Free Software Foundation; either version 2 of the License, or -### (at your option) any later version. -### -### TrIPE is distributed in the hope that it will be useful, -### but WITHOUT ANY WARRANTY; without even the implied warranty of -### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -### GNU General Public License for more details. -### -### You should have received a copy of the GNU General Public License -### along with TrIPE; if not, write to the Free Software Foundation, -### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - -VERSION = '@VERSION@' - -###-------------------------------------------------------------------------- -### External dependencies. - -from optparse import OptionParser -import tripe as T -import os as OS -import signal as SIG -import errno as E -import cdb as CDB -import mLib as M -import re as RX -from time import time -import subprocess as PROC - -S = T.svcmgr - -###-------------------------------------------------------------------------- -### Running auxiliary commands. - -class SelLineQueue (M.SelLineBuffer): - """Glues the select-line-buffer into the coroutine queue system.""" - - def __new__(cls, file, queue, tag, kind): - """See __init__ for documentation.""" - return M.SelLineBuffer.__new__(cls, file.fileno()) - - def __init__(me, file, queue, tag, kind): - """ - Initialize a new line-reading adaptor. - - The adaptor reads lines from FILE. Each line is inserted as a message of - the stated KIND, bearing the TAG, into the QUEUE. End-of-file is - represented as None. - """ - me._q = queue - me._file = file - me._tag = tag - me._kind = kind - me.enable() - - @T._callback - def line(me, line): - me._q.put((me._tag, me._kind, line)) - - @T._callback - def eof(me): - me.disable() - me._q.put((me._tag, me._kind, None)) - -class ErrorWatch (T.Coroutine): - """ - An object which watches stderr streams for errors and converts them into - warnings of the form - - WARN watch INFO stderr LINE - - The INFO is a list of tokens associated with the file when it was - registered. - - Usually there is a single ErrorWatch object, called errorwatch. - """ - - def __init__(me): - """Initialization: there are no arguments.""" - T.Coroutine.__init__(me) - me._q = T.Queue() - me._map = {} - me._seq = 1 - - def watch(me, file, info): - """ - Adds FILE to the collection of files to watch. - - INFO will be written in the warning messages from this FILE. Returns a - sequence number which can be used to unregister the file again. - """ - seq = me._seq - me._seq += 1 - me._map[seq] = info, SelLineQueue(file, me._q, seq, 'stderr') - return seq - - def unwatch(me, seq): - """Stop watching the file with sequence number SEQ.""" - del me._map[seq] - return me - - def run(me): - """ - Coroutine function: read items from the queue and report them. - - Unregisters files automatically when they reach EOF. - """ - while True: - seq, _, line = me._q.get() - if line is None: - me.unwatch(seq) - else: - S.warn(*['watch'] + me._map[seq][0] + ['stderr', line]) - -def dbwatch(): - """ - Coroutine function: wake up every second and notice changes to the - database. When a change happens, tell the Pinger (q.v.) to rescan its - peers. - """ - cr = T.Coroutine.getcurrent() - main = cr.parent - fw = M.FWatch(opts.cdb) - while True: - timer = M.SelTimer(time() + 1, lambda: cr.switch()) - main.switch() - if fw.update(): - pinger.rescan(False) - S.notify('watch', 'peerdb-update') - -class ChildWatch (M.SelSignal): - """ - An object which watches for specified processes exiting and reports - terminations by writing items of the form (TAG, 'exit', RESULT) to a queue. - - There is usually only one ChildWatch object, called childwatch. - """ - - def __new__(cls): - """Initialize the child-watcher.""" - return M.SelSignal.__new__(cls, SIG.SIGCHLD) - - def __init__(me): - """Initialize the child-watcher.""" - me._pid = {} - me.enable() - - def watch(me, pid, queue, tag): - """ - Register PID as a child to watch. If it exits, write (TAG, 'exit', CODE) - to the QUEUE, where CODE is one of - - * None (successful termination) - * ['exit-nonzero', CODE] (CODE is a string!) - * ['exit-signal', 'S' + CODE] (CODE is the signal number as a string) - * ['exit-unknown', STATUS] (STATUS is the entire exit status, in hex) - """ - me._pid[pid] = queue, tag - return me - - def unwatch(me, pid): - """Unregister PID as a child to watch.""" - del me._pid[pid] - return me - - @T._callback - def signalled(me): - """ - Called when child processes exit: collect exit statuses and report - failures. - """ - while True: - try: - pid, status = OS.waitpid(-1, OS.WNOHANG) - except OSError, exc: - if exc.errno == E.ECHILD: - break - if pid == 0: - break - if pid not in me._pid: - continue - queue, tag = me._pid[pid] - if OS.WIFEXITED(status): - exit = OS.WEXITSTATUS(status) - if exit == 0: - code = None - else: - code = ['exit-nonzero', str(exit)] - elif OS.WIFSIGNALED(status): - code = ['exit-signal', 'S' + str(OS.WTERMSIG(status))] - else: - code = ['exit-unknown', hex(status)] - queue.put((tag, 'exit', code)) - -class Command (object): - """ - Represents a running command. - - This class is the main interface to the machery provided by the ChildWatch - and ErrorWatch objects. See also potwatch. - """ - - def __init__(me, info, queue, tag, args, env): - """ - Start a new child process. - - The ARGS are a list of arguments to be given to the child process. The - ENV is either None or a dictionary of environment variable assignments to - override the extant environment. INFO is a list of tokens to be included - in warnings about the child's stderr output. If the child writes a line - to standard output, put (TAG, 'stdout', LINE) to the QUEUE. When the - child exits, write (TAG, 'exit', CODE) to the QUEUE. - """ - me._info = info - me._q = queue - me._tag = tag - myenv = OS.environ.copy() - if env: myenv.update(env) - me._proc = PROC.Popen(args = args, env = myenv, bufsize = 1, - stdout = PROC.PIPE, stderr = PROC.PIPE) - me._lq = SelLineQueue(me._proc.stdout, queue, tag, 'stdout') - errorwatch.watch(me._proc.stderr, info) - childwatch.watch(me._proc.pid, queue, tag) - - def __del__(me): - """ - If I've been forgotten then stop watching for termination. - """ - childwatch.unwatch(me._proc.pid) - -def potwatch(what, name, q): - """ - Watch the queue Q for activity as reported by a Command object. - - Information from the process's stdout is reported as - - NOTE WHAT NAME stdout LINE - - abnormal termination is reported as - - WARN WHAT NAME CODE - - where CODE is what the ChildWatch wrote. - """ - eofp = deadp = False - while not deadp or not eofp: - _, kind, more = q.get() - if kind == 'stdout': - if more is None: - eofp = True - else: - S.notify('watch', what, name, 'stdout', more) - elif kind == 'exit': - if more: S.warn('watch', what, name, *more) - deadp = True - -###-------------------------------------------------------------------------- -### Peer database utilities. - -_magic = ['_magic'] # An object distinct from all others - -class Peer (object): - """Representation of a peer in the database.""" - - def __init__(me, peer, cdb = None): - """ - Create a new peer, named PEER. - - Information about the peer is read from the database CDB, or the default - one given on the command-line. - """ - me.name = peer - record = (cdb or CDB.init(opts.cdb))['P' + peer] - me.__dict__.update(M.URLDecode(record, semip = True)) - - def get(me, key, default = _magic, filter = None): - """ - Get the information stashed under KEY from the peer's database record. - - If DEFAULT is given, then use it if the database doesn't contain the - necessary information. If no DEFAULT is given, then report an error. If - a FILTER function is given then apply it to the information from the - database before returning it. - """ - attr = me.__dict__.get(key, default) - if attr is _magic: - raise T.TripeJobError('malformed-peer', me.name, 'missing-key', key) - elif filter is not None: - attr = filter(attr) - return attr - - def has(me, key): - """ - Return whether the peer's database record has the KEY. - """ - return key in me.__dict__ - - def list(me): - """ - Iterate over the available keys in the peer's database record. - """ - return me.__dict__.iterkeys() - -def boolean(value): - """Parse VALUE as a boolean.""" - return value in ['t', 'true', 'y', 'yes', 'on'] - -###-------------------------------------------------------------------------- -### Waking up and watching peers. - -def run_connect(peer, cmd): - """ - Start the job of connecting to the passive PEER. - - The CMD string is a shell command which will connect to the peer (via some - back-channel, say ssh and userv), issue a command - - SVCSUBMIT connect passive [OPTIONS] USER - - and write the resulting challenge to standard error. - """ - q = T.Queue() - cmd = Command(['connect', peer.name], q, 'connect', - ['/bin/sh', '-c', cmd], None) - _, kind, more = q.peek() - if kind == 'stdout': - if more is None: - S.warn('watch', 'connect', peer.name, 'unexpected-eof') - else: - chal = more - S.greet(peer.name, chal) - q.get() - potwatch('connect', peer.name, q) - -def run_disconnect(peer, cmd): - """ - Start the job of disconnecting from a passive PEER. - - The CMD string is a shell command which will disconnect from the peer. - """ - q = T.Queue() - cmd = Command(['disconnect', peer.name], q, 'disconnect', - ['/bin/sh', '-c', cmd], None) - potwatch('disconnect', peer.name, q) - -_pingseq = 0 -class PingPeer (object): - """ - Object representing a peer which we are pinging to ensure that it is still - present. - - PingPeer objects are held by the Pinger (q.v.). The Pinger maintains an - event queue -- which saves us from having an enormous swarm of coroutines - -- but most of the actual work is done here. - - In order to avoid confusion between different PingPeer instances for the - same actual peer, each PingPeer has a sequence number (its `seq' - attribute). Events for the PingPeer are identified by a (PEER, SEQ) pair. - (Using the PingPeer instance itself will prevent garbage collection of - otherwise defunct instances.) - """ - - def __init__(me, pinger, queue, peer, pingnow): - """ - Create a new PingPeer. - - The PINGER is the Pinger object we should send the results to. This is - used when we remove ourselves, if the peer has been explicitly removed. - - The QUEUE is the event queue on which timer and ping-command events - should be written. - - The PEER is a `Peer' object describing the peer. - - If PINGNOW is true, then immediately start pinging the peer. Otherwise - wait until the usual retry interval. - """ - global _pingseq - me._pinger = pinger - me._q = queue - me._peer = peer.name - me.update(peer) - me.seq = _pingseq - _pingseq += 1 - me._failures = 0 - if pingnow: - me._timer = None - me._ping() - else: - me._timer = M.SelTimer(time() + me._every, me._time) - - def update(me, peer): - """ - Refreshes the timer parameters for this peer. We don't, however, - immediately reschedule anything: that will happen next time anything - interesting happens. - """ - if peer is None: peer = Peer(me._peer) - assert peer.name == me._peer - me._every = peer.get('every', filter = T.timespec, default = 120) - me._timeout = peer.get('timeout', filter = T.timespec, default = 10) - me._retries = peer.get('retries', filter = int, default = 5) - me._connectp = peer.has('connect') - return me - - def _ping(me): - """ - Send a ping to the peer; the result is sent to the Pinger's event queue. - """ - S.rawcommand(T.TripeAsynchronousCommand( - me._q, (me._peer, me.seq), - ['EPING', - '-background', S.bgtag(), - '-timeout', str(me._timeout), - '--', - me._peer])) - - def _reconnect(me): - peer = Peer(me._peer) - if peer.has('connect'): - S.warn('watch', 'reconnecting', me._peer) - S.forcekx(me._peer) - T.spawn(run_connect, peer, peer.get('connect')) - me._timer = M.SelTimer(time() + me._every, me._time) - else: - S.kill(me._peer) - - def event(me, code, stuff): - """ - Respond to an event which happened to this peer. - - Timer events indicate that we should start a new ping. (The server has - its own timeout which detects lost packets.) - - We trap unknown-peer responses and detach from the Pinger. - - If the ping fails and we run out of retries, we attempt to restart the - connection. - """ - if code == 'TIMER': - me._failures = 0 - me._ping() - elif code == 'FAIL': - S.notify('watch', 'ping-failed', me._peer, *stuff) - if not stuff: - pass - elif stuff[0] == 'unknown-peer': - me._pinger.kill(me._peer) - elif stuff[0] == 'ping-send-failed': - me._reconnect() - elif code == 'INFO': - if stuff[0] == 'ping-ok': - if me._failures > 0: - S.warn('watch', 'ping-ok', me._peer) - me._timer = M.SelTimer(time() + me._every, me._time) - elif stuff[0] == 'ping-timeout': - me._failures += 1 - S.warn('watch', 'ping-timeout', me._peer, - 'attempt', str(me._failures), 'of', str(me._retries)) - if me._failures < me._retries: - me._ping() - else: - me._reconnect() - elif stuff[0] == 'ping-peer-died': - me._pinger.kill(me._peer) - - @T._callback - def _time(me): - """ - Handle timer callbacks by posting a timeout event on the queue. - """ - me._timer = None - me._q.put(((me._peer, me.seq), 'TIMER', None)) - - def __str__(me): - return 'PingPeer(%s, %d, f = %d)' % (me._peer, me.seq, me._failures) - def __repr__(me): - return str(me) - -class Pinger (T.Coroutine): - """ - The Pinger keeps track of the peers which we expect to be connected and - takes action if they seem to stop responding. - - There is usually only one Pinger, called pinger. - - The Pinger maintains a collection of PingPeer objects, and an event queue. - The PingPeers direct the results of their pings, and timer events, to the - event queue. The Pinger's coroutine picks items off the queue and - dispatches them back to the PingPeers as appropriate. - """ - - def __init__(me): - """Initialize the Pinger.""" - T.Coroutine.__init__(me) - me._peers = {} - me._q = T.Queue() - - def run(me): - """ - Coroutine function: reads the pinger queue and sends events to the - PingPeer objects they correspond to. - """ - while True: - (peer, seq), code, stuff = me._q.get() - if peer in me._peers and seq == me._peers[peer].seq: - me._peers[peer].event(code, stuff) - - def add(me, peer, pingnow): - """ - Add PEER to the collection of peers under the Pinger's watchful eye. - The arguments are as for PingPeer: see above. - """ - me._peers[peer.name] = PingPeer(me, me._q, peer, pingnow) - return me - - def kill(me, peername): - """Remove PEER from the peers being watched by the Pinger.""" - del me._peers[peername] - return me - - def rescan(me, startup): - """ - General resynchronization method. - - We scan the list of peers (with connect scripts) known at the server. - Any which are known to the Pinger but aren't known to the server are - removed from our list; newly arrived peers are added. (Note that a peer - can change state here either due to the server sneakily changing its list - without issuing notifications or, more likely, the database changing its - idea of whether a peer is interesting.) Finally, PingPeers which are - still present are prodded to update their timing parameters. - - This method is called once at startup to pick up the peers already - installed, and again by the dbwatcher coroutine when it detects a change - to the database. - """ - if T._debug: print '# rescan peers' - correct = {} - start = {} - for name in S.list(): - try: peer = Peer(name) - except KeyError: continue - if peer.get('watch', filter = boolean, default = False): - if T._debug: print '# interesting peer %s' % peer - correct[peer.name] = start[peer.name] = peer - elif startup: - if T._debug: print '# peer %s ready for adoption' % peer - start[peer.name] = peer - for name, obj in me._peers.items(): - try: - peer = correct[name] - except KeyError: - if T._debug: print '# peer %s vanished' % name - del me._peers[name] - else: - obj.update(peer) - for name, peer in start.iteritems(): - if name in me._peers: continue - if startup: - if T._debug: print '# setting up peer %s' % name - ifname = S.ifname(name) - addr = S.addr(name) - T.defer(adoptpeer, peer, ifname, *addr) - else: - if T._debug: print '# adopting new peer %s' % name - me.add(peer, True) - return me - - def adopted(me): - """ - Returns the list of peers being watched by the Pinger. - """ - return me._peers.keys() - -###-------------------------------------------------------------------------- -### New connections. - -def encode_envvars(env, prefix, vars): - """ - Encode the variables in VARS suitably for including in a program - environment. Lowercase letters in variable names are forced to uppercase; - runs of non-alphanumeric characters are replaced by single underscores; and - the PREFIX is prepended. The resulting variables are written to ENV. - """ - for k, v in vars.iteritems(): - env[prefix + r_bad.sub('_', k.upper())] = v - -r_bad = RX.compile(r'[\W_]+') -def envvars(peer): - """ - Translate the database information for a PEER into a dictionary of - environment variables with plausible upper-case names and a P_ prefix. - Also collect the crypto information into A_ variables. - """ - env = {} - encode_envvars(env, 'P_', dict([(k, peer.get(k)) for k in peer.list()])) - encode_envvars(env, 'A_', S.algs(peer.name)) - return env - -def run_ifupdown(what, peer, *args): - """ - Run the interface up/down script for a peer. - - WHAT is 'ifup' or 'ifdown'. PEER names the peer in question. ARGS is a - list of arguments to pass to the script, in addition to the peer name. - - The command is run and watched in the background by potwatch. - """ - q = T.Queue() - c = Command([what, peer.name], q, what, - M.split(peer.get(what), quotep = True)[0] + - [peer.name] + list(args), - envvars(peer)) - potwatch(what, peer.name, q) - -def adoptpeer(peer, ifname, *addr): - """ - Add a new peer to our collection. - - PEER is the `Peer' object; IFNAME is the interface name for its tunnel; and - ADDR is the list of tokens representing its address. - - We try to bring up the interface and provoke a connection to the peer if - it's passive. - """ - if peer.has('ifup'): - T.Coroutine(run_ifupdown, name = 'ifup %s' % peer.name) \ - .switch('ifup', peer, ifname, *addr) - cmd = peer.get('connect', default = None) - if cmd is not None: - T.Coroutine(run_connect, name = 'connect %s' % peer.name) \ - .switch(peer, cmd) - if peer.get('watch', filter = boolean, default = False): - pinger.add(peer, False) - -def disownpeer(peer): - """Drop the PEER from the Pinger and put its interface to bed.""" - try: pinger.kill(peer) - except KeyError: pass - cmd = peer.get('disconnect', default = None) - if cmd is not None: - T.Coroutine(run_disconnect, name = 'disconnect %s' % peer.name) \ - .switch(peer, cmd) - if peer.has('ifdown'): - T.Coroutine(run_ifupdown, name = 'ifdown %s' % peer.name) \ - .switch('ifdown', peer) - -def notify(_, code, *rest): - """ - Watch for notifications. - - We trap ADD and KILL notifications, and send them straight to addpeer and - delpeer respectively. - """ - if code == 'ADD': - try: p = Peer(rest[0]) - except KeyError: return - adoptpeer(p, *rest[1:]) - elif code == 'KILL': - try: p = Peer(rest[0]) - except KeyError: return - disownpeer(p, *rest[1:]) - -###-------------------------------------------------------------------------- -### Command stubs. - -def cmd_stub(*args): - raise T.TripeJobError('not-implemented') - -def cmd_kick(name): - """ - kick NAME: Force a new connection attempt for the NAMEd peer. - """ - if name not in pinger.adopted(): - raise T.TripeJobError('peer-not-adopted', name) - try: peer = Peer(name) - except KeyError: raise T.TripeJobError('unknown-peer', name) - T.spawn(connect, peer) - -def cmd_adopted(): - """ - adopted: Report a list of adopted peers. - """ - for name in pinger.adopted(): - T.svcinfo(name) - -###-------------------------------------------------------------------------- -### Start up. - -def setup(): - """ - Service setup. - - Register the notification watcher, and rescan the peers. - """ - S.handler['NOTE'] = notify - S.watch('+n') - pinger.rescan(opts.startup) - -def init(): - """ - Initialization to be done before service startup. - """ - global errorwatch, childwatch, pinger - errorwatch = ErrorWatch() - childwatch = ChildWatch() - pinger = Pinger() - T.Coroutine(dbwatch, name = 'dbwatch').switch() - errorwatch.switch() - pinger.switch() - -def parse_options(): - """ - Parse the command-line options. - - Automatically changes directory to the requested configdir, and turns on - debugging. Returns the options object. - """ - op = OptionParser(usage = '%prog [-a FILE] [-d DIR]', - version = '%%prog %s' % VERSION) - - op.add_option('-a', '--admin-socket', - metavar = 'FILE', dest = 'tripesock', default = T.tripesock, - help = 'Select socket to connect to [default %default]') - op.add_option('-d', '--directory', - metavar = 'DIR', dest = 'dir', default = T.configdir, - help = 'Select current diretory [default %default]') - op.add_option('-p', '--peerdb', - metavar = 'FILE', dest = 'cdb', default = T.peerdb, - help = 'Select peers database [default %default]') - op.add_option('--daemon', dest = 'daemon', - default = False, action = 'store_true', - help = 'Become a daemon after successful initialization') - op.add_option('--debug', dest = 'debug', - default = False, action = 'store_true', - help = 'Emit debugging trace information') - op.add_option('--startup', dest = 'startup', - default = False, action = 'store_true', - help = 'Being called as part of the server startup') - - opts, args = op.parse_args() - if args: op.error('no arguments permitted') - OS.chdir(opts.dir) - T._debug = opts.debug - return opts - -## Service table, for running manually. -service_info = [('watch', T.VERSION, { - 'adopted': (0, 0, '', cmd_adopted), - 'kick': (1, 1, 'PEER', cmd_kick) -})] - -if __name__ == '__main__': - opts = parse_options() - T.runservices(opts.tripesock, service_info, - init = init, setup = setup, - daemon = opts.daemon) - -###----- That's all, folks -------------------------------------------------- -- [mdw]