"""
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):
+ 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._default = default
+ me._opmap = opmap
def _describe(me):
"""Description of the remote service."""
def _mkcmd(me, cmd, map):
"""Construct the command to be executed, by substituting placeholders."""
+ 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', state = dict(u = user))
+ 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, state = dict(u = user))
+ me._dispatch(me._run_noout, 'clear', [('u', user)])
CONF.export('CommandRemoteService')