diff --git a/Makefile b/Makefile index 4273b87d2..65622287b 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,8 @@ install: default $(DESTDIR)/usr/share/doc/plinth $(DESTDIR)/usr/share/man/man1 cp -a static themes $(DESTDIR)$(DATADIR)/ cp -a *.py modules templates $(DESTDIR)$(PYDIR)/ + mkdir -p $(DESTDIR)$(PYDIR)/exmachina + cp -a exmachina/exmachina.py $(DESTDIR)$(PYDIR)/exmachina/. cp share/init.d/plinth $(DESTDIR)/etc/init.d install plinth $(DESTDIR)/usr/bin/ mkdir -p $(DESTDIR)/var/lib/plinth/cherrypy_sessions $(DESTDIR)/var/log/plinth $(DESTDIR)/var/run diff --git a/exmachina/README b/exmachina/README new file mode 100644 index 000000000..6216029ad --- /dev/null +++ b/exmachina/README @@ -0,0 +1,105 @@ + _ _ + _____ ___ __ ___ __ _ ___| |__ (_)_ __ __ _ + / _ \ \/ / '_ ` _ \ / _` |/ __| '_ \| | '_ \ / _` | + | __/> <| | | | | | (_| | (__| | | | | | | | (_| | + \___/_/\_\_| |_| |_|\__,_|\___|_| |_|_|_| |_|\__,_| + + + +### DISCLAIMER + + ----- ACHTUNG! WARNING! DANGER! ---- + +This code is hackish and not "production quality. It represents a potential +approach to a specific problem (privilege separation for system configuration). +It has not been extensively reviewed or tested and does not represent a known +best practice. + +### What is this? + +exmachina is a small system configuration system which runs as separate but +coupled client/server UNIX processes for the purpose of privilege separation: +the "server" process runs with root privileges and a python program using the +"client" library runs as any unprivileged user. The commands and parameters +that the client can send to the server are limited, though in this particular +case can of course be used to deny service (reboot or shutdown the machine) or +probably escalate privileges one way or another (install arbitrary packages, +reconfigure networks, enable callback scripts, edit system configuration +files). + +The server and client processes should be one-to-one: only one client should +ever connect to the server. The init_test.sh script shows how this could be +achieved in a SysV-style /etc/init.d script. + +The intended use case is writing a user-friendly web control panel for a Debian +server or router: the web designer creating the user interface should not be +overly concerned with writing secure code, and the web application itself +(possibly including lots of third party framework code, javascript libraries, +etc) should not run with strong system permissions, but core components of the +system (such as hostname, wireless access point configuration, network +settings, package installation, locale, timezone, etc) need to be modified. + +See the comments in exmachina.py for more information. + +### Alternatives + +The most simple alternative to exmachina that has been recommended to me is to +create simple setuid/setgid programs or scripts to execute privileged system +changes, and to only allow execute permissions to those programs for the +user/group of the less-trusted user interface program. This seems to be the +current best practice. For the more complicated case of generalized system +configuration, the setuid/setgid program becomes complicated, or you need to +write and install many of them, but this is no worse that the situation with +exmachina. + +Another approach is the Assuan protocol used by GPG, which has been generalized +as libassuan: + + "Assuan permits the servers, which do the actual work, e.g. encryption and + decryption of data using a secret key, to be developed independently of the + user interfaces, e.g. mail clients and other encryption front ends." + + http://www.gnupg.org/related_software/libassuan/index.en.html + +### Status + +Basic server and client functionality implemented. Crude, and far more simple +than it may appear or the length of code would imply. + +This was code was written in a weekend "sprint" for the FreedomBox project and +their Plinth web user interface in 2012. + +I may or may not maintain this code. I have hesitation even publishing it +because i'm almost certain there are implementation bugs and that the entire +concept is problematic. + +Features: +* shared secret key process/privilege separation +* call augeas API: match, set, setm, get, save, move, insert, remove +* call init.d service scripts: status, start, stop, restart + +In late 2012 Nick Daly (of the FreedomBox project) wrote up a brief audit of +this code and concept on his blog (https://www.betweennowhere.net/). Link is +frequantly broken. + +### Dependencies (server) + +* augeas configuration editing library +* python-augeas wrapper for augeas +* bjsonrpc python library + +On debian (wheezy) try: + + $ sudo apt-get install augeas-tools python-bjsonrpc python-augeas + +### Dependencies (client) + +* bjsonrpc + +On debian (wheezy) try: + + $ sudo apt-get install bjsonrpc + +### License + +exmachina.py is GPLv3 or later diff --git a/exmachina/exmachina.py b/exmachina/exmachina.py new file mode 100755 index 000000000..4fe6e75aa --- /dev/null +++ b/exmachina/exmachina.py @@ -0,0 +1,557 @@ +#!/usr/bin/env python + +""" +Author: bnewbold +Date: July 2012 +License: GPLv3 or later (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. + +Note that the authentication mechanism only tells the server that the client +seems to be legitimate, it doesn't prevent a rapid "man in the middle" style +attack on the client, which could feed back malicious information. + +Alternatively, an optional user or group can be specified and the socket file +will have it's ownership and permissions changed appropriately. + +Note that the socket path would need to be changed on a per-application basis +so that competing daemons don't block/clobber each other. +""" + +import os +import sys +import grp +import shutil +import argparse +import logging +import socket +import subprocess +import time +import base64 +import functools +import hashlib +import atexit +import stat +import pwd + +import bjsonrpc +import bjsonrpc.handlers +import bjsonrpc.server +import augeas + +log = logging.getLogger(__name__) + +# hackish way to enforce single client connection +allow_connect = True + +def execute_service(servicename, action, timeout=10): + """This function mostly ripped from StackOverflow: + http://stackoverflow.com/questions/1556348/python-run-a-process-with-timeout-and-capture-stdout-stderr-and-exit-status + """ + # ensure service name isn't tricky trick + script = "/etc/init.d/" + os.path.split(servicename)[1] + + if not os.path.exists(script): + raise ValueError("so such service: %s" % servicename) + + command_list = [script, action] + log.info("executing: %s" % command_list) + proc = subprocess.Popen(command_list, + bufsize=0, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + poll_seconds = .250 + deadline = time.time() + timeout + while time.time() < deadline and proc.poll() is None: + time.sleep(poll_seconds) + + if proc.poll() is None: + if float(sys.version[:3]) >= 2.6: + proc.terminate() + raise Exception("execution timed out (>%d seconds): %s" % + (timeout, command_list)) + + stdout, stderr = proc.communicate() + # TBD: should raise exception here if proc.returncode != 0? + return stdout, stderr, proc.returncode + +def execute_apt(packagename, action, timeout=120, aptargs=['-q', '-y']): + # ensure package name isn't tricky trick + if action != "update" \ + and (packagename != packagename.strip().split()[0] \ + or packagename.startswith('-')): + raise ValueError("Not a good apt package name: %s" % packagename) + + if action == "update": + command_list = ['apt-get', action] + else: + command_list = ['apt-get', action, packagename] + command_list.extend(aptargs) + log.info("executing: %s" % command_list) + proc = subprocess.Popen(command_list, + bufsize=0, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + poll_seconds = .250 + deadline = time.time() + timeout + while time.time() < deadline and proc.poll() is None: + time.sleep(poll_seconds) + + if proc.poll() is None: + if float(sys.version[:3]) >= 2.6: + proc.terminate() + raise Exception("execution timed out (>%d seconds): %s" % + (timeout, command_list)) + + stdout, stderr = proc.communicate() + return stdout, stderr, proc.returncode + +def authreq(fn): + """ + Decorator to force authentication before allowing calls to a method + """ + @functools.wraps(fn) + def wrappedfunc(self, *args, **kwargs): + if not self.secret_key: + return fn(self, *args, **kwargs) + else: + log.error("Unauthorized function call attempt; bailing") + sys.exit(-1) + return wrappedfunc + +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): + global allow_connect + if not allow_connect: + log.error("second client tried to connect, exiting") + sys.exit(-1) + allow_connect = False + self.augeas = augeas.Augeas() + + def _shutdown(self): + # Server shuts down after a single client connection closes + log.info("connection closing, server exiting") + sys.exit(-1) + + def authenticate(self, secret_key): + if not self.secret_key: + log.warn("Unecessary authentication attempt") + return + if not hashlib.sha256(secret_key.strip()).hexdigest() == \ + hashlib.sha256(self.secret_key.strip()).hexdigest(): + # key doesn't match, fail hard + log.error("Authentication failed!") + sys.exit() + self.secret_key = None + + def need_to_auth(self): + """ + Helper for clients to learn whether they still need to authenticate + """ + return self.secret_key != None + + # ------------- Augeas API Passthrough ----------------- + @authreq + def augeas_save(self): + log.info("augeas: saving config") + return self.augeas.save() + + @authreq + def augeas_set(self, path, value): + log.info("augeas: set %s=%s" % (path, value)) + return self.augeas.set(path.encode('utf-8'), + value.encode('utf-8')) + + @authreq + def augeas_setm(self, base, sub, value): + log.info("augeas: setm %s %s = %s" % (base, sub, value)) + return self.augeas.setm(base.encode('utf-8'), + sub.encode('utf-8'), + value.encode('utf-8')) + + @authreq + def augeas_get(self, path): + # reduce verbosity + log.debug("augeas: get %s" % path) + return self.augeas.get(path.encode('utf-8')) + + @authreq + def augeas_match(self, path): + # reduce verbosity + log.debug("augeas: match %s" % path) + return self.augeas.match("%s" % path.encode('utf-8')) + + @authreq + def augeas_insert(self, path, label, before=True): + log.info("augeas: insert %s=%s" % (path, value)) + return self.augeas.insert(path.encode('utf-8'), + label.encode('utf-8'), + before=before) + + @authreq + def augeas_move(self, src, dst): + log.info("augeas: move %s -> %s" % (src, dst)) + return self.augeas.move(src.encode('utf-8'), dst.encode('utf-8')) + + @authreq + def augeas_remove(self, path): + log.info("augeas: remove %s" % path) + return self.augeas.remove(path.encode('utf-8')) + + # ------------- Misc. non-Augeas Helpers ----------------- + @authreq + def set_timezone(self, tzname): + log.info("reset timezone to %s" % tzname) + tzname = tzname.strip() + tzpath = os.path.join("/usr/share/zoneinfo", tzname) + try: + os.stat(tzpath) + except OSError: + # file not found + raise ValueError("timezone not valid: %s" % tzname) + shutil.copy( + os.path.join("/usr/share/zoneinfo", tzname), + "/etc/localtime") + with open("/etc/timezone", "w") as tzfile: + tzfile.write(tzname + "\n") + return "timezone changed to %s" % tzname + + # ------------- init.d Service Control ----------------- + @authreq + def initd_status(self, servicename): + return execute_service(servicename, "status") + + @authreq + def initd_start(self, servicename): + return execute_service(servicename, "start") + + @authreq + def initd_stop(self, servicename): + return execute_service(servicename, "stop") + + @authreq + def initd_restart(self, servicename): + return execute_service(servicename, "restart") + + # ------------- apt-get Package Control ----------------- + @authreq + def apt_install(self, packagename): + return execute_apt(packagename, "install") + + @authreq + def apt_update(self): + return execute_apt("", "update") + + @authreq + def apt_remove(self, packagename): + return execute_apt(packagename, "remove") + + +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", + secret_key=None): + + if secret_key: + secret_key = hashlib.sha256(secret_key.strip() + "|exmachina")\ + .hexdigest() + + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.sock.connect(socket_path) + self.conn = bjsonrpc.connection.Connection(self.sock) + + if self.conn.call.need_to_auth(): + if secret_key: + self.conn.call.authenticate(secret_key) + else: + self.conn.close() + raise Exception( + "authentication required but no secret_key passed") + elif secret_key: + print "secret_key passed but no authentication required; ignoring" + + self.augeas = EmptyClass() + self.initd = EmptyClass() + self.apt = EmptyClass() + self.misc = EmptyClass() + + self.augeas.save = self.conn.call.augeas_save + self.augeas.set = self.conn.call.augeas_set + self.augeas.setm = self.conn.call.augeas_setm + self.augeas.get = self.conn.call.augeas_get + self.augeas.match = self.conn.call.augeas_match + self.augeas.insert = self.conn.call.augeas_insert + self.augeas.move = self.conn.call.augeas_move + self.augeas.remove = self.conn.call.augeas_remove + self.initd.status = self.conn.call.initd_status + self.initd.start = self.conn.call.initd_start + self.initd.stop = self.conn.call.initd_stop + self.initd.restart = self.conn.call.initd_restart + self.apt.install = self.conn.call.apt_install + self.apt.update = self.conn.call.apt_update + self.apt.remove = self.conn.call.apt_remove + self.misc.set_timezone = self.conn.call.set_timezone + + def close(self): + self.sock.close() + + +def run_server(socket_path, secret_key=None, socket_group=None, + socket_user=None): + + if secret_key: + secret_key = hashlib.sha256(secret_key.strip() + "|exmachina")\ + .hexdigest() + + if not 0 == os.geteuid(): + log.warn("Expected to be running as root!") + if socket_group or socket_user: + log.error("Can't change socket permissions if non-root, exiting") + sys.exit(-1) + + # check if the socket was left open after a previous run, overwrite it + if os.path.exists(socket_path): + if not stat.S_ISSOCK(os.stat(socket_path).st_mode): + log.error("socket_path exists and isn't a stale socket: %s" % + socket_path) + sys.exit(-1) + # socket file exists, need to check if it's stale from a previous + # session + test_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + test_sock.connect(socket_path) + log.error("socket_path already exists and seems to be active: %s" % + socket_path) + test_sock.close() + sys.exit(-1) + except Exception, e: + print e + # if we got this far it's probably a stale socket and should be + # destroyed + log.warn("Clobbering pre-existing socket: %s" % socket_path) + os.unlink(socket_path) + + # open and bind to unix socket + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.bind(socket_path) + + # we just created the socket file, so now let's register an atexit callback + # to clean up after ourselves if we get Ctrl-C'd (or exit for any other + # reason, including normal cleanup) + def delete_socket(): + os.unlink(socket_path) + atexit.register(delete_socket) + + if socket_group is not None: + # optionally set group-only permissions on socket file before we start + # accepting connections + socket_uid = os.stat(socket_path).st_uid + socket_gid = grp.getgrnam(socket_group).gr_gid + os.chmod(socket_path, 0660) + os.chown(socket_path, socket_uid, socket_gid) + elif socket_user is not None: + # optionally set user-only permissions on socket file before we start + # accepting connections + pwn = pwd.getpwnam(socket_user) + socket_uid = pwn.pw_uid + socket_gid = pwn.pw_gid + os.chmod(socket_path, 0660) + os.chown(socket_path, socket_uid, socket_gid) + else: + os.chmod(socket_path, 0666) + + # only going to allow a single client, so don't allow queued connections + sock.listen(0) + + if secret_key: + # key already got hashed above + ExMachinaHandler.secret_key = secret_key + + # get bjsonrpc server started. it would make more sense to just listen for + # a single client connection and pass that off to the bjsonrpc handler, + # then close the socket when that's done, but I don't see an easy way to do + # that with the bjsonrpc API, so instead we let it wait indefinately for + # connections, but actual only allow one and bail when that one closes. + 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. + These arguments are optional and default to /dev/null. Note that stderr is + opened unbuffered, so if it shares a file with stdout then interleaved + output may not appear in the order that you expect. """ + + # Do first fork. + try: + pid = os.fork() + if pid > 0: + sys.exit(0) # Exit first parent. + except OSError, e: + sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror)) + sys.exit(1) + + # Decouple from parent environment. + os.chdir("/") + os.umask(0) + os.setsid() + + # Do second fork. + try: + pid = os.fork() + if pid > 0: + sys.exit(0) # Exit second parent. + except OSError, e: + sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror)) + sys.exit(1) + + # Now I am a daemon! + + # Redirect standard file descriptors. + si = open(stdin, 'r') + so = open(stdout, 'a+') + se = open(stderr, 'a+', 0) + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + return pid + +# ============================================================================= +# Command line handling +def main(): + + global log + parser = argparse.ArgumentParser(usage= + "usage: exmachina.py [options]\n" + "exmachina.py --help for more info." + ) + parser.add_argument("-v", "--verbose", + default=False, + help="Show more debugging statements", + action="store_true") + parser.add_argument("-q", "--quiet", + default=False, + help="Show fewer informational statements", + action="store_true") + parser.add_argument("-k", "--key", + default=False, + help="Wait for Secret Access Key on stdin before starting", + action="store_true") + parser.add_argument("--random-key", + default=False, + help="Just dump a random base64 key and exit", + action="store_true") + parser.add_argument("-s", "--socket-path", + default="/tmp/exmachina.sock", + help="UNIX Domain socket file path to listen on", + metavar="FILE") + parser.add_argument("--pidfile", + default=None, + help="Daemonize and write pid to this file", + metavar="FILE") + parser.add_argument("-g", "--group", + default=None, + help="chgrp socket file to this group and set 0660 permissions") + parser.add_argument("-u", "--user", + default=None, + help="chown socket file to this user/group and set 0600 permissions") + + args = parser.parse_args() + + if args.user and args.group: + parser.error("set user or group option, but not both") + + #if len(args) != 0: + #parser.error("Incorrect number of arguments") + + if args.random_key: + sys.stdout.write(base64.urlsafe_b64encode(os.urandom(128))) + sys.exit(0) + + log = logging.getLogger() + hdlr = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + hdlr.setFormatter(formatter) + log.addHandler(hdlr) + + if args.verbose: + log.setLevel(logging.DEBUG) + elif args.quiet: + log.setLevel(logging.ERROR) + else: + log.setLevel(logging.INFO) + + secret_key = None + if args.key: + log.debug("Waiting for secret key on stdin...") + secret_key = sys.stdin.readline().strip() + log.debug("Got it!") + + if args.pidfile: + with open(args.pidfile, 'w') as pfile: + # ensure file is available/writable + pass + os.unlink(args.pidfile) + daemonize() + pid = os.getpid() + with open(args.pidfile, 'w') as pfile: + pfile.write("%s" % pid) + log.info("Daemonized, pid is %s" % pid) + + run_server(secret_key=secret_key, + socket_path=args.socket_path, + socket_group=args.group, + socket_user=args.user) + +if __name__ == '__main__': + main() diff --git a/exmachina/init_test.sh b/exmachina/init_test.sh new file mode 100755 index 000000000..941285d36 --- /dev/null +++ b/exmachina/init_test.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# Test init.d-style initialization; run this script as root (or sudo it) + +export key=`./exmachina.py --random-key` + +echo $key | ./exmachina.py -vk --pidfile /tmp/exmachina_test.pid -g www-data +sleep 1 +echo $key | sudo -u www-data -g www-data ./test_exmachina.py -k + +kill `cat /tmp/exmachina_test.pid` && rm /tmp/exmachina_test.pid +sleep 1 +jobs diff --git a/exmachina/test_exmachina.py b/exmachina/test_exmachina.py new file mode 100755 index 000000000..510d30d3a --- /dev/null +++ b/exmachina/test_exmachina.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python + +""" +This file tests the "client side" of the exmachina layer. + +To use with secret keys, do the following in seperate terminals: + + $ echo "" | sudo ./exmachina.py -vk + $ echo "" | ./test_exmachina.py -k + +To use without, do the following in seperate terminals: + + $ sudo ./exmachina.py -v + $ ./test_exmachina.py + +Use the init_test.sh script to test shared key passing and privilage seperation +at the same time: + + $ sudo ./init_test.sh +""" + +import sys +import socket + +import bjsonrpc +import bjsonrpc.connection +from bjsonrpc.exceptions import ServerError + +from exmachina.exmachina import ExMachinaClient + +# ============================================================================= +# Command line handling +def main(): + + secret_key = None + if sys.argv[-1] == "-k": + print "waiting for key on stdin..." + secret_key = sys.stdin.readline() + print "got it!" + + """ + # both tests together won't work now that server exits after single client + socket_path = "/tmp/exmachina.sock" + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(socket_path) + + print "========= Testing JSON-RPC connection" + c = bjsonrpc.connection.Connection(sock) + if secret_key: + c.call.authenticate(secret_key) + print "/*: %s" % c.call.augeas_match("/*") + print "/augeas/*: %s" % c.call.augeas_match("/augeas/*") + print "/etc/* files:" + for name in c.call.augeas_match("/files/etc/*"): + print "\t%s" % name + print c.call.initd_status("bluetooth") + print "hostname: %s" % c.call.augeas_get("/files/etc/hostname/*") + print "localhost: %s" % c.call.augeas_get("/files/etc/hosts/1/canonical") + sock.close() + """ + + print "========= Testing user client library" + client = ExMachinaClient(secret_key=secret_key) + print client.augeas.match("/files/etc/*") + #print client.initd.restart("bluetooth") + try: + print client.initd.status("greentooth") + print "ERROR: should have failed above!" + except ServerError: + print "(got expected error, good!)" + print "(expect Error on the above line)" + print client.initd.status("bluetooth") + print client.apt.install("pkg_which_does_not_exist") + print client.apt.remove("pkg_which_does_not_exist") + #print client.apt.update() # can be slow... + #print client.misc.set_timezone("UTC") # don't clobber system... + try: + print client.misc.set_timezone("whoopie") # should be an error + print "ERROR: should have failed above!" + except ServerError: + print "(got expected error, good!)" + client.close() + +if __name__ == '__main__': + main() diff --git a/plinth.py b/plinth.py index b3ada9d71..6243fb7d5 100755 --- a/plinth.py +++ b/plinth.py @@ -114,7 +114,7 @@ def setup(): pass try: - from exmachina import ExMachinaClient + from exmachina.exmachina import ExMachinaClient except ImportError: cfg.exmachina = None else: diff --git a/start.sh b/start.sh index 8e141fcf8..79baa1268 100755 --- a/start.sh +++ b/start.sh @@ -1,10 +1,10 @@ #! /bin/sh -#PYTHONPATH=vendor/exmachina:$PYTHONPATH +#PYTHONPATH=exmachina:$PYTHONPATH export PYTHONPATH sudo killall exmachina.py -sudo /usr/share/pyshared/exmachina/exmachina.py -v & +sudo exmachina/exmachina.py -v & python plinth.py sudo killall exmachina.py diff --git a/test.sh b/test.sh index 4b6f701d4..54ec30292 100755 --- a/test.sh +++ b/test.sh @@ -1,6 +1,6 @@ #! /bin/sh -PYTHONPATH=build/exmachina:$PYTHONPATH +PYTHONPATH=exmachina:$PYTHONPATH PYTHONPATH=modules/installed/lib:$PYTHONPATH PYTHONPATH=vendor:$PYTHONPATH