chiark / gitweb /
service.py: Incompatible changes to CommandRemoteService.
[chopwood] / service.py
index 26406b5b5480bda9d9f31ec0a018efb131f9dd9c..aa773884061f04617826de7b48f2fe0e83431f63 100644 (file)
@@ -183,17 +183,17 @@ class BasicRemoteService (BasicService):
   configuration.
   """
 
-  def _run(me, cmd, input = None):
+  def _run(me, cmd, input = None, state = None):
     """
     This is the core of the remote service machinery.  It issues a command
     and parses the response.  It will generate strings of informational
     output from the command; error responses cause appropriate exceptions to
     be raised.
 
-    The command is determined by passing the CMD argument to the `_mkcmd'
-    method, which a subclass must implement; it should return a list of
-    command-line arguments suitable for `subprocess.Popen'.  The INPUT is a
-    string to make available on the command's stdin; if None, then no input
+    The command is determined by passing the CMD and STATE arguments to the
+    `_mkcmd' method, which a subclass must implement; it should return a list
+    of command-line arguments suitable for `subprocess.Popen'.  The INPUT is
+    string to make available on the command's stdin; if None, then no input
     is provided to the command.  The `_describe' method must provide a
     description of the remote service for use in timeout messages.
 
@@ -207,7 +207,7 @@ class BasicRemoteService (BasicService):
 
     ## Run the command and collect its output and status.
     with U.timeout(30, "waiting for remote service %s" % me._describe()):
-      proc = SUB.Popen(me._mkcmd(cmd),
+      proc = SUB.Popen(me._mkcmd(cmd, state),
                        stdin = input is not None and SUB.PIPE or None,
                        stdout = SUB.PIPE, stderr = SUB.PIPE)
       out, err = proc.communicate(input)
@@ -248,9 +248,9 @@ class BasicRemoteService (BasicService):
     if not win:
       raise U.ExpectedError, (500, 'No reply from remote service')
 
-  def _run_noout(me, cmd, input = None):
+  def _run_noout(me, cmd, input = None, state = None):
     """Like `_run', but expect no output."""
-    for _ in me._run(cmd, input):
+    for _ in me._run(cmd, input, state):
       raise U.ExpectedError, (500, 'Unexpected output from remote service')
 
 class SSHRemoteService (BasicRemoteService):
@@ -281,25 +281,24 @@ class SSHRemoteService (BasicRemoteService):
     (probably of the form `LOGIN@HOSTNAME') and referring to the service
     NAME.
     """
-    super(SSHRemoteService, me).__init__(*args, **kw)
+    super(SSHRemoteService, me).__init__(name = name, *args, **kw)
     me._remote = remote
-    me._name = name
 
   def _describe(me):
     """Description of the remote service."""
-    return "`%s' via SSH to `%s'" % (me._name, me._remote),
+    return "`%s' via SSH to `%s'" % (me.name, me._remote),
 
-  def _mkcmd(me, cmd):
+  def _mkcmd(me, cmd, state):
     """Format a command for SSH.  Mainly escaping arguments."""
     return ['ssh', me._remote, ' '.join(map(CGI.urlencode, cmd))]
 
   def setpasswd(me, user, passwd):
     """Service protocol: set the USER's password to PASSWD."""
-    me._run_noout(['set', me._name, user], passwd + '\n')
+    me._run_noout(['set', me.name, user], passwd + '\n')
 
   def clearpasswd(me, user):
     """Service protocol: clear the USER's password."""
-    me._run_noout(['clear', me._name, user])
+    me._run_noout(['clear', me.name, user])
 
 CONF.export('SSHRemoteService')
 
@@ -307,51 +306,79 @@ class CommandRemoteService (BasicRemoteService):
   """
   A remote service transported over a standard Unix command.
 
-  This is left rather generic.  We need to know some command lists SET and
-  CLEAR containing the relevant service names and arguments.  These are
-  simply executed, after simple placeholder substitution.
-
-  The SET command should read a password as its first line on stdin, and set
-  that as the user's new password.  The CLEAR command should simply prevent
-  the user from logging in with a password.  On success, the commands should
-  print a line `OK' to standard output, and on any kind of anticipated
-  failure, they should print `ERR' followed by an HTTP status code and a
-  message; in either case, the program should exit with status zero.  In
-  disastrous cases, it's acceptable to print an error message to stderr
-  and/or exit with a nonzero status.
-
-  The placeholders are as follows.
+  This is left rather generic.  Two strategies are available (and can be
+  combined using appropriate configuration).  A DEFAULT command list can be
+  specified, and will be invoked as `DEFAULT OP ARGS...', where OP ARGS form
+  a Chopwood remote command.  Additionally, an OPMAP dictionary can be
+  provided, mapping OP names (remote command names) to command lists
+  containing `%' placeholders, as follows:
 
   `%u'          the user's name
   `%%'          a single `%' character
+
+  On success, the commands should print a line `OK' to standard output, and
+  on any kind of anticipated failure, they should print `ERR' followed by an
+  HTTP status code and a message; in either case, the program should exit
+  with status zero.  In disastrous cases, it's acceptable to print an error
+  message to stderr and/or exit with a nonzero status.
+
+  Configuration hint: if you can only handle some subset of the available
+  commands, then your easy approach is to set commands for the operations you
+  can handle in the OPMAP, and set the DEFAULT to something like
+
+        ['echo', 'ERR 500', 'unsupported command:']
+
+  to reject other commands.
   """
 
   R_PAT = RX.compile('%(.)')
 
-  def __init__(me, set, clear, *args, **kw):
-    """
-    Initialize the command remote service.
-    """
+  def __init__(me, default = ['ERR', '500', 'unimplemented command:'],
+               opmap = {}, *args, **kw):
+    """Initialize the command remote service."""
     super(CommandRemoteService, me).__init__(*args, **kw)
-    me._set = set
-    me._clear = clear
-    me._map = dict(u = user)
+    me._default = default
+    me._opmap = opmap
 
-  def _subst(me, c):
+  def _describe(me):
+    """Description of the remote service."""
+    return "`%s' command service (%s)" % (me.name, ' '.join(me._default))
+
+  def _subst(me, c, map):
     """Return the substitution for the placeholder `%C'."""
-    return me._map.get(c, c)
+    return map.get(c, c)
 
-  def _mkcmd(me, cmd):
+  def _mkcmd(me, cmd, map):
     """Construct the command to be executed, by substituting placeholders."""
-    return [me.R_PAT.sub(lambda m: me._subst(m.group(1))) for arg in cmd]
+    if map is None: return cmd
+    return [me.R_PAT.sub(lambda m: me._subst(m.group(1), map), arg)
+            for arg in cmd]
+
+  def _dispatch(me, func, op, args, input = None):
+    """
+    Work out how to invoke a particular command.
+
+    Invoke FUNC, which works like `_run', with appropriate arguments.  The OP
+    is a remote command name; ARGS is a sequence of (C, ARG) pairs, where C
+    is a placeholder character and ARG is a string value; INPUT is the text
+    to provide to the command on standard input.
+    """
+    try:
+      cmd = me._opmap[op]
+    except KeyError:
+      cmd = me._default + [op] + [v for k, v in args]
+      map = None
+    else:
+      map = dict(args)
+    return func(cmd, input = input, state = map)
 
   def setpasswd(me, user, passwd):
     """Service protocol: set the USER's password to PASSWD."""
-    me._run_noout(me._set, passwd + '\n')
+    me._dispatch(me._run_noout, 'set', [('u', user)], passwd + '\n')
 
   def clearpasswd(me, user):
     """Service protocol: clear the USER's password."""
-    me._run_noout(me._clear)
+    me._dispatch(me._run_noout, 'clear', [('u', user)])
 
 CONF.export('CommandRemoteService')