diff options
| author | bnewbold <bnewbold@robocracy.org> | 2012-07-12 16:28:06 -0400 | 
|---|---|---|
| committer | bnewbold <bnewbold@robocracy.org> | 2012-07-12 16:28:06 -0400 | 
| commit | 47543de6fa707b8f82d9b7c9c8af0fda02971f0e (patch) | |
| tree | 01fd95c92633c79bb0bc1b222d0a595266e926f3 | |
| parent | 420106d5a9823b81fe686789831dd2354bfaa678 (diff) | |
| download | exmachina-47543de6fa707b8f82d9b7c9c8af0fda02971f0e.tar.gz exmachina-47543de6fa707b8f82d9b7c9c8af0fda02971f0e.zip | |
documentation, cleanup
| -rwxr-xr-x | exmachina.py | 94 | ||||
| -rwxr-xr-x | init_test.sh | 2 | ||||
| -rwxr-xr-x | test_exmachina.py (renamed from test.py) | 5 | 
3 files changed, 63 insertions, 38 deletions
| diff --git a/exmachina.py b/exmachina.py index 1a90c3b..35559da 100755 --- a/exmachina.py +++ b/exmachina.py @@ -1,8 +1,32 @@  #!/usr/bin/env python -# Author: bnewbold <bnewbold@robocracy.org> -# Date: July 2012 -# License: GPLv3 (see http://www.gnu.org/licenses/gpl-3.0.html) +""" +Author: bnewbold <bnewbold@robocracy.org> +Date: July 2012 +License: GPLv3 (see http://www.gnu.org/licenses/gpl-3.0.html) +         (two helper functions copied from web, as cited below) +Package Requirements: python-augeas, bjsonrpc + +This file implements both ends (privilaged daemon and unprivilaged python +client library) of a crude system configuration message bus, intended for use +(initially) with the Plinth web interface to the FreedomBox operating system. + +The goal is to provide partially-untrusted processes (such as the web interface +running as the www-data user) access to core system configuration files +(through the Augeas library) and daemon control (through the init.d scripts). + +The daemon process (started in the same startup script as Plinth) runs as root +and accepts JSON-RPC method calls through a unix domain socket +(/tmp/exmachina.sock by default). Because file access control may not be +sufficiently flexible for access control, a somewhat-elaborate secret key +mechanism can be used to control access to the RPC mechanism. + +The (optional) shared secret-key mechanism requires clients to first call the +"authenticate" RPC method before any other methods. The secret key is passed to +the server process through stdin at startup (command line arguments could be +snooped by unprivilaged processes), and would presumably be passed on to the +client in the same way. The init_test.sh script demonstrates this mechanism. +"""  import os  import sys @@ -33,7 +57,7 @@ def execute_service(servicename, action, timeout=10):          return "ERROR: so such service"      command_list = [script, action] -    log.info("running: %s" % command_list) +    log.info("executing: %s" % command_list)      proc = subprocess.Popen(command_list,                              bufsize=0,                              stdout=subprocess.PIPE, @@ -46,13 +70,18 @@ def execute_service(servicename, action, timeout=10):      if proc.poll() == None:          if float(sys.version[:3]) >= 2.6:              proc.terminate() -        raise Exception("Timeout: %s" % command_list) +        raise Exception("execution timed out (>%d seconds): %s" %  +                        (timeout, command_list))      stdout, stderr = proc.communicate()      return stdout, stderr, proc.returncode  class ExMachinaHandler(bjsonrpc.handlers.BaseHandler): +    # authentication state variable. If not None, still need to authenticate; +    # if None then authentication not require or was already successful for +    # this instantiation of the Handler.  This class variable gets optionally +    # overridden on a per-process basis      secret_key = None      def _setup(self): @@ -60,18 +89,11 @@ class ExMachinaHandler(bjsonrpc.handlers.BaseHandler):      def authenticate(self, secret_key):          if not secret_key.strip() == self.secret_key.strip(): +            # fail hard              log.error("Authentication failed!")              sys.exit()          self.secret_key = None -    def test_whattime(self): -        log.debug("whattime") -        return time.time() - -    def test_listfiles(self): -        log.debug("listfiles") -        return self.augeas.match("/files/etc/*") -      # ------------- Augeas API Passthrough -----------------      def augeas_save(self):          if not self.secret_key: @@ -120,7 +142,7 @@ class ExMachinaHandler(bjsonrpc.handlers.BaseHandler):              log.info("augeas: remove %s" % path)              return self.augeas.remove(path.encode('utf-8')) -    # ------------- Service Control ----------------- +    # ------------- init.d Service Control -----------------      def initd_status(self, servicename):          if not self.secret_key:              return execute_service(servicename, "status") @@ -138,9 +160,23 @@ class ExMachinaHandler(bjsonrpc.handlers.BaseHandler):              return execute_service(servicename, "restart")  class EmptyClass(): +    # Used by ExMachinaClient below      pass  class ExMachinaClient(): +    """Simple client wrapper library to expose augeas and init.d methods. + +    In brief, use augeas.get/set/insert to modify system configuration files +    under the /files/etc/* namespace. augeas.match with a wildcard can be used +    to find variables to edit. + +    After making any changes, use augeas.save to commit to disk, then +    initd.restart to restart the appropriate system daemons. In many cases, +    this would be the 'networking' meta-daemon. + +    See test_exmachina.py for some simple examples; see the augeas docs for +    more in depth guidance. +    """      def __init__(self,                   socket_path="/tmp/exmachina.sock", @@ -171,8 +207,7 @@ class ExMachinaClient():      def close(self):          self.sock.close() -def run_server(socket_path="/tmp/exmachina.sock", secret_key=None): -    # TODO: check for root permissions, warn if not root +def run_server(socket_path, secret_key=None):      if not 0 == os.geteuid():          log.warn("Expected to be running as root!") @@ -183,31 +218,18 @@ def run_server(socket_path="/tmp/exmachina.sock", secret_key=None):      sock.bind(socket_path)      sock.listen(1) +    # TODO: www-data group permissions only?      os.chmod(socket_path, 0666)      if secret_key:          ExMachinaHandler.secret_key = secret_key -        """ -        (conn, addr) = sock.accept() -        print addr -        msg = conn.recv(1024).strip() -        if not msg == secret_key: -            print "|%s| != |%s|" % (msg, secret_key) -            log.error("Didn't receive secret key at socket initialization!") -            conn.close() -            sys.exit() -        # now that connection is established, lock the pipe -        os.chmod(socket_path, 0600) -        log.info("Auth!") -        """ -    serv = bjsonrpc.server.Server(sock, handler_factory=ExMachinaHandler) -    # TODO: www-data group permissions only? -    #os.chmod(socket_path, 0666) +    serv = bjsonrpc.server.Server(sock, handler_factory=ExMachinaHandler)      serv.serve()  def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):      """      From: http://www.noah.org/wiki/Daemonize_Python +      This forks the current process into a daemon. The stdin, stdout, and      stderr arguments are file names that will be opened and be used to replace      the standard file descriptors in sys.stdin, sys.stdout, and sys.stderr. @@ -274,6 +296,10 @@ def main():          default=False,          help="Just dump a random base64 key and exit",          action="store_true") +    parser.add_option("-s", "--socket-path",  +        default="/tmp/exmachina.sock", +        help="UNIX Domain socket file path to listen on", +        metavar="FILE")      parser.add_option("--pid-file",           default=None,          help="Daemonize and write pid to this file", @@ -309,7 +335,7 @@ def main():      if options.pid_file:          with open(options.pid_file, 'w') as pfile: -            # ensure file is available +            # ensure file is available/writable              pass          os.unlink(options.pid_file)          daemonize() @@ -318,7 +344,7 @@ def main():              pfile.write("%s" % pid)          log.info("Daemonized, pid is %s" % pid) -    run_server(secret_key=secret_key) +    run_server(secret_key=secret_key, socket_path=options.socket_path)  if __name__ == '__main__':      main() diff --git a/init_test.sh b/init_test.sh index d1e6f7d..4a00734 100755 --- a/init_test.sh +++ b/init_test.sh @@ -6,7 +6,7 @@ export key=`./exmachina.py --random-key`  echo $key | ./exmachina.py -vk --pid-file /tmp/exmachina_test.pid  sleep 1 -echo $key | ./test.py -k +echo $key | sudo -u www-data ./test_exmachina.py -k  kill `cat /tmp/exmachina_test.pid` && rm /tmp/exmachina_test.pid  sleep 1 diff --git a/test.py b/test_exmachina.py index 240b47d..6c2a97d 100755 --- a/test.py +++ b/test_exmachina.py @@ -32,11 +32,10 @@ def main():          secret_key = sys.stdin.readline()          print "sent!" -    print "========= Testing low level connection" +    print "========= Testing JSON-RPC connection"      c = bjsonrpc.connection.Connection(sock)      if secret_key:          c.call.authenticate(secret_key) -    print "time: %s" % c.call.test_whattime()      print "/*: %s" % c.call.augeas_match("/*")      print "/augeas/*: %s" % c.call.augeas_match("/augeas/*")      print "/etc/* files:" @@ -50,7 +49,7 @@ def main():      print "========= Testing user client library"      client = ExMachinaClient(secret_key=secret_key)      print client.augeas.match("/files/etc/*") -    print client.initd.restart("bluetooth") +    #print client.initd.restart("bluetooth")      print client.initd.status("greentooth")      print "(expect Error on the above line)"      print client.initd.status("bluetooth") | 
