From f1e764f2e5728113f191456236d02fdae6e7680a Mon Sep 17 00:00:00 2001
From: bnewbold
Date: Thu, 12 Jul 2012 17:21:54 -0400
Subject: [PATCH 01/24] integrate exmachina configuration management layer
- add exmachina code and test code
- modify plinth.py to listen for shared secret on stdin at start
(if appropriate flag is set) and try to connect to exmachina daemon
- use exmachina to read and set /etc/hostname as a demo
- update plinth init.d script to start exmachina and share keys
- update docs with new deps and run instructions
---
INSTALL | 23 +-
NOTES | 12 +
README | 3 +-
exmachina/__init__.py | 0
exmachina/exmachina.py | 353 +++++++++++++++++++++++++++++
exmachina/init_test.sh | 13 ++
exmachina/test_exmachina.py | 71 ++++++
fabfile.py | 2 +-
modules/installed/system/config.py | 28 ++-
plinth.py | 18 ++
share/init.d/plinth | 45 +++-
11 files changed, 544 insertions(+), 24 deletions(-)
create mode 100644 exmachina/__init__.py
create mode 100755 exmachina/exmachina.py
create mode 100755 exmachina/init_test.sh
create mode 100755 exmachina/test_exmachina.py
diff --git a/INSTALL b/INSTALL
index 9b2705496..9108689e2 100644
--- a/INSTALL
+++ b/INSTALL
@@ -2,10 +2,10 @@
## Installing Plinth
-Install the python-cheetah package and pandoc:
-
-apt-get install python-cheetah pandoc
+Install the python-cheetah package, pandoc, python-augeas, and
+bjsonrpc:
+apt-get install python-cheetah pandoc python-augeas python-bjsonrpc
Install the python-simplejson
@@ -13,9 +13,16 @@ apt-get install python-simplejson
Unzip and untar the source into a directory. Change to the directory
-containing the program. Do `make` and then run `./plinth.py` and
-point your web browser at `localhost:8000`. The default username is
-"admin" and the default password is "secret".
+containing the program. Run:
+
+ $ make
+ $ ./plinth.py
+
+and point your web browser at `localhost:8000`. The default username is "admin"
+and the default password is "secret". To actually edit the configuration of
+your local/dev machine, also run:
+
+ $ sudo ./exmachina/exmachina.py -v &
## Dependencies
@@ -27,6 +34,10 @@ point your web browser at `localhost:8000`. The default username is
* *GNU Make* is used to build the templates and such.
+* bjsonrpc - used for configuration management layer
+
+* python-augeas and augeas - used for configuration management
+
The documentation has some dependencies too.
* *Markdown* is used to format and style docs.
diff --git a/NOTES b/NOTES
index ff46c2c7d..dbdebd53c 100644
--- a/NOTES
+++ b/NOTES
@@ -2,6 +2,18 @@
%
% February 2012
+# Edits by bnewbold
+
+## 2012-07-12 "exmachina" configuration management layer
+
+- this new code is very ugly and in the "just make it work" style
+- add exmachina code and test code
+- modify plinth.py to listen for shared secret on stdin at start
+ (if appropriate flag is set) and try to connect to exmachina daemon
+- use exmachina to read and set /etc/hostname as a demo
+- update plinth init.d script to start exmachina and share keys
+- update docs with new deps and run instructions
+
# Edits by seandiggity
## 2012-02-27 new theme based upon bootstrap
diff --git a/README b/README
index 6d56c72fd..d2cdd3c1b 100644
--- a/README
+++ b/README
@@ -50,4 +50,5 @@ interface will overwrite those changes at first opportunity. This
interface is not a tool for super admins facing complex scenarios. It
is for home users to do a wide variety of basic tasks.
-
+See comments in exmachina/exmachina.py for more details about the configuration
+management process seperation scheme.
diff --git a/exmachina/__init__.py b/exmachina/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/exmachina/exmachina.py b/exmachina/exmachina.py
new file mode 100755
index 000000000..fc04beb1d
--- /dev/null
+++ b/exmachina/exmachina.py
@@ -0,0 +1,353 @@
+#!/usr/bin/env python
+
+"""
+Author: bnewbold
+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
+import optparse
+import logging
+import socket
+import subprocess
+import stat
+import time
+import base64
+
+import bjsonrpc
+import bjsonrpc.handlers
+import bjsonrpc.server
+import augeas
+
+
+log = logging.getLogger(__name__)
+
+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):
+ return "ERROR: so such service"
+
+ 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() == None:
+ time.sleep(poll_seconds)
+
+ if proc.poll() == 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
+
+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):
+ self.augeas = augeas.Augeas()
+
+ def authenticate(self, secret_key):
+ if not self.secret_key:
+ log.warn("Unecessary authentication attempt")
+ return
+ if not secret_key.strip() == self.secret_key.strip():
+ # fail hard
+ log.error("Authentication failed!")
+ sys.exit()
+ self.secret_key = None
+
+ # ------------- Augeas API Passthrough -----------------
+ def augeas_save(self):
+ if not self.secret_key:
+ log.info("augeas: saving config")
+ return self.augeas.save()
+
+ def augeas_set(self, path, value):
+ if not self.secret_key:
+ log.info("augeas: set %s=%s" % (path, value))
+ return self.augeas.set(path.encode('utf-8'),
+ value.encode('utf-8'))
+
+ def augeas_setm(self, base, sub, value):
+ if not self.secret_key:
+ 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'))
+
+ def augeas_get(self, path):
+ if not self.secret_key:
+ # reduce verbosity
+ log.debug("augeas: get %s" % path)
+ return self.augeas.get(path.encode('utf-8'))
+
+ def augeas_match(self, path):
+ if not self.secret_key:
+ # reduce verbosity
+ log.debug("augeas: match %s" % path)
+ return self.augeas.match("%s" % path.encode('utf-8'))
+
+ def augeas_insert(self, path, label, before=True):
+ if not self.secret_key:
+ log.info("augeas: insert %s=%s" % (path, value))
+ return self.augeas.insert(path.encode('utf-8'),
+ label.encode('utf-8'),
+ before=before)
+
+ def augeas_move(self, src, dst):
+ if not self.secret_key:
+ log.info("augeas: move %s -> %s" % (src, dst))
+ return self.augeas.move(src.encode('utf-8'), dst.encode('utf-8'))
+
+ def augeas_remove(self, path):
+ if not self.secret_key:
+ log.info("augeas: remove %s" % path)
+ return self.augeas.remove(path.encode('utf-8'))
+
+ # ------------- init.d Service Control -----------------
+ def initd_status(self, servicename):
+ if not self.secret_key:
+ return execute_service(servicename, "status")
+
+ def initd_start(self, servicename):
+ if not self.secret_key:
+ return execute_service(servicename, "start")
+
+ def initd_stop(self, servicename):
+ if not self.secret_key:
+ return execute_service(servicename, "stop")
+
+ def initd_restart(self, servicename):
+ if not self.secret_key:
+ 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",
+ secret_key=None):
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.sock.connect(socket_path)
+ self.conn = bjsonrpc.connection.Connection(self.sock)
+
+ if secret_key:
+ self.conn.call.authenticate(secret_key)
+
+ self.augeas = EmptyClass()
+ self.initd = 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
+
+ def close(self):
+ self.sock.close()
+
+def run_server(socket_path, secret_key=None):
+
+ if not 0 == os.geteuid():
+ log.warn("Expected to be running as root!")
+
+ if os.path.exists(socket_path):
+ os.unlink(socket_path)
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ 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
+
+ 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 = optparse.OptionParser(usage=
+ "usage: %prog [options]\n"
+ "%prog --help for more info."
+ )
+ parser.add_option("-v", "--verbose",
+ default=False,
+ help="Show more debugging statements",
+ action="store_true")
+ parser.add_option("-q", "--quiet",
+ default=False,
+ help="Show fewer informational statements",
+ action="store_true")
+ parser.add_option("-k", "--key",
+ default=False,
+ help="Wait for Secret Access Key on stdin before starting",
+ action="store_true")
+ parser.add_option("--random-key",
+ 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("--pidfile",
+ default=None,
+ help="Daemonize and write pid to this file",
+ metavar="FILE")
+
+ (options, args) = parser.parse_args()
+
+ if len(args) != 0:
+ parser.error("Incorrect number of arguments")
+
+ if options.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 options.verbose:
+ log.setLevel(logging.DEBUG)
+ elif options.quiet:
+ log.setLevel(logging.ERROR)
+ else:
+ log.setLevel(logging.INFO)
+
+ secret_key = None
+ if options.key:
+ log.debug("Waiting for secret key on stdin...")
+ secret_key = sys.stdin.readline().strip()
+ log.debug("Got it!")
+
+ if options.pidfile:
+ with open(options.pidfile, 'w') as pfile:
+ # ensure file is available/writable
+ pass
+ os.unlink(options.pidfile)
+ daemonize()
+ pid = os.getpid()
+ with open(options.pidfile, 'w') as pfile:
+ pfile.write("%s" % pid)
+ log.info("Daemonized, pid is %s" % pid)
+
+ run_server(secret_key=secret_key, socket_path=options.socket_path)
+
+if __name__ == '__main__':
+ main()
diff --git a/exmachina/init_test.sh b/exmachina/init_test.sh
new file mode 100755
index 000000000..c53d76bd2
--- /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
+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..86c71bf9b
--- /dev/null
+++ b/exmachina/test_exmachina.py
@@ -0,0 +1,71 @@
+#!/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 "" | ./exmachina.py -vk
+ $ echo "" | ./test.py -k
+
+To use without, do the following in seperate terminals:
+
+ $ echo "" | ./exmachina.py -vk
+ $ echo "" | ./test.py -k
+
+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 optparse
+import logging
+import socket
+
+import bjsonrpc
+import bjsonrpc.connection
+import augeas
+
+from exmachina import ExMachinaClient
+
+# =============================================================================
+# Command line handling
+def main():
+
+ socket_path="/tmp/exmachina.sock"
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ sock.connect(socket_path)
+
+ secret_key = None
+ if sys.argv[-1] == "-k":
+ print "waiting for key on stdin..."
+ secret_key = sys.stdin.readline()
+ print "sent!"
+
+ 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")
+ print client.initd.status("greentooth")
+ print "(expect Error on the above line)"
+ print client.initd.status("bluetooth")
+ client.close()
+
+if __name__ == '__main__':
+ main()
diff --git a/fabfile.py b/fabfile.py
index 92a52e196..4b4d25410 100644
--- a/fabfile.py
+++ b/fabfile.py
@@ -138,7 +138,7 @@ def apache():
@task
def deps():
"Basic plinth dependencies"
- sudo('apt-get install --no-install-recommends -y python make python-cheetah pandoc python-simplejson python-pyme')
+ sudo('apt-get install --no-install-recommends -y python make python-cheetah pandoc python-simplejson python-pyme python-augeas python-bjsonrpc')
@task
def update():
diff --git a/modules/installed/system/config.py b/modules/installed/system/config.py
index c8b5190a8..b31dc6009 100644
--- a/modules/installed/system/config.py
+++ b/modules/installed/system/config.py
@@ -41,20 +41,18 @@ def valid_hostname(name):
def set_hostname(hostname):
"Sets machine hostname to hostname"
- cfg.log.info("Writing '%s' to /etc/hostname" % hostname)
- unslurp("/etc/hostname", hostname+"\n")
+ cfg.log.info("Writing '%s' to /etc/hostname with exmachina" % hostname)
+
try:
- retcode = subprocess.call("/etc/init.d/hostname.sh start", shell=True)
- if retcode < 0:
- cfg.log.error("Hostname restart terminated by signal: return code is %s" % retcode)
- else:
- cfg.log.debug("Hostname restart returned %s" % retcode)
+ cfg.exmachina.augeas.set("/files/etc/hostname/*", hostname)
+ cfg.exmachina.augeas.save()
+ # don't persist/cache change unless it was saved successfuly
+ sys_store = filedict_con(cfg.store_file, 'sys')
+ sys_store['hostname'] = hostname
+ cfg.exmachina.initd.restart("hostname.sh") # is hostname.sh debian-only?
except OSError, e:
raise cherrypy.HTTPError(500, "Hostname restart failed: %s" % e)
- sys_store = filedict_con(cfg.store_file, 'sys')
- sys_store['hostname'] = hostname
-
class general(FormPlugin, PagePlugin):
url = ["/sys/config"]
order = 30
@@ -72,8 +70,11 @@ class general(FormPlugin, PagePlugin):
def main(self, message='', **kwargs):
sys_store = filedict_con(cfg.store_file, 'sys')
+ hostname = cfg.exmachina.augeas.get("/files/etc/hostname/*")
+ # this layer of persisting configuration in sys_store could/should be
+ # removed -BLN
defaults = {'time_zone': "slurp('/etc/timezone').rstrip()",
- 'hostname': "gethostname()",
+ 'hostname': "hostname",
}
for k,c in defaults.items():
if not k in kwargs:
@@ -81,6 +82,8 @@ class general(FormPlugin, PagePlugin):
kwargs[k] = sys_store[k]
except KeyError:
exec("if not '%(k)s' in kwargs: sys_store['%(k)s'] = kwargs['%(k)s'] = %(c)s" % {'k':k, 'c':c})
+ # over-ride the sys_store cached value
+ kwargs['hostname'] = hostname
## Get the list of supported timezones and the index in that list of the current one
module_file = __file__
@@ -120,7 +123,8 @@ class general(FormPlugin, PagePlugin):
old_val = sys_store['hostname']
try:
set_hostname(hostname)
- except:
+ except Exception, e:
+ cfg.log.error(e)
cfg.log.info("Trying to restore old hostname value.")
set_hostname(old_val)
raise
diff --git a/plinth.py b/plinth.py
index 2eff29f40..adab11012 100755
--- a/plinth.py
+++ b/plinth.py
@@ -17,6 +17,9 @@ from util import *
from logger import Logger
#from modules.auth import AuthController, require, member_of, name_is
+from exmachina.exmachina import ExMachinaClient
+import socket
+
__version__ = "0.2.14"
__author__ = "James Vasile"
__copyright__ = "Copyright 2011, James Vasile"
@@ -71,9 +74,17 @@ def parse_arguments():
parser = argparse.ArgumentParser(description='Plinth web interface for the FreedomBox.')
parser.add_argument('--pidfile', default="",
help='specify a file in which the server may write its pid')
+ parser.add_argument('--listen-exmachina-key', default=False, action='store_true',
+ help='listen for JSON-RPC shared secret key on stdin at startup')
args=parser.parse_args()
if args.pidfile:
cfg.pidfile = args.pidfile
+ if args.listen_exmachina_key:
+ # this is where we optionally try to read in a shared secret key to
+ # authenticate connections to exmachina
+ cfg.exmachina_secret_key = sys.stdin.readline().strip()
+ else:
+ cfg.exmachina_secret_key = None
def setup():
parse_arguments()
@@ -85,6 +96,13 @@ def setup():
except AttributeError:
pass
+ try:
+ cfg.exmachina = ExMachinaClient(
+ secret_key=cfg.exmachina_secret_key or None)
+ except socket.error:
+ cfg.exmachina = None
+ print "couldn't connect to exmachina daemon, but continuing anyways..."
+
os.chdir(cfg.file_root)
cherrypy.config.update({'error_page.404': error_page_404})
cherrypy.config.update({'error_page.500': error_page_500})
diff --git a/share/init.d/plinth b/share/init.d/plinth
index e364fc123..8cf88a93c 100755
--- a/share/init.d/plinth
+++ b/share/init.d/plinth
@@ -12,23 +12,53 @@
# This file is /etc/init.d/plinth
DAEMON=/usr/local/bin/plinth.py
+EXMACHINA_DAEMON=/usr/local/bin/exmachina.py
PID_FILE=/var/run/plinth.pid
-
+EXMACHINA_PID_FILE=/var/run/exmachina.pid
+
+PLINTH_USER=www-data
+PLINTH_GROUP=www-data
+
+test -x $DAEMON || exit 0
+test -x $EXMACHINA_DAEMON || exit 0
+
+set -e
+
+. /lib/lsb/init-functions
+
start_plinth (){
if [ -f $PID_FILE ]; then
- echo Already running with a pid of `cat $PID_FILE`.
+ echo Already running with a pid of `cat $PID_FILE`.
else
- $DAEMON --pidfile=$PID_FILE
+ if [ -f $EXMACHINA_PID_FILE ]; then
+ echo exmachina was already running with a pid of `cat $EXMACHINA_PID_FILE`.
+ kill -15 `cat $EXMACHINA_PID_FILE`
+ rm -rf $EXMACHINA_PID_FILE
+ fi
+ SHAREDKEY=`$EXMACHINA_DAEMON --random-key`
+ touch $PID_FILE
+ chown $PLINTH_USER:$PLINTH_GROUP $PID_FILE
+ echo $SHAREDKEY | $EXMACHINA_DAEMON --pidfile=$EXMACHINA_PID_FILE || rm $PID_FILE
+ sleep 0.5
+ echo $SHAREDKEY | sudo -u $PLINTH_USER -g $PLINTH_GROUP $DAEMON --pidfile=$PID_FILE
fi
}
stop_plinth () {
if [ -f $PID_FILE ]; then
- kill -15 `cat $PID_FILE`
+ kill -15 `cat $PID_FILE` || true
rm -rf $PID_FILE
+ echo "killed plinth"
else
echo "No pid file at $PID_FILE suggests plinth is not running."
fi
+ if [ -f $EXMACHINA_PID_FILE ]; then
+ kill -15 `cat $EXMACHINA_PID_FILE` || true
+ rm -rf $EXMACHINA_PID_FILE
+ echo "killed exmachina"
+ else
+ echo "No pid file at $EXMACHINA_PID_FILE suggests exmachina is not running."
+ fi
}
test -x $DAEMON || exit 0
@@ -45,5 +75,12 @@ case "$1" in
$0 stop
$0 start
;;
+ status)
+ status_of_proc -p $PID_FILE "$DAEMON" plinth && exit 0 || exit $?
+ ;;
+ *)
+ echo "Usage: $NAME {start|stop|restart|status}" >&2
+ exit 1
+ ;;
esac
From 0430892518760d139258dcf8f8de0045bee4b2e3 Mon Sep 17 00:00:00 2001
From: bnewbold
Date: Wed, 25 Jul 2012 23:14:42 -0700
Subject: [PATCH 02/24] updates/improvements to exmachina
---
exmachina/exmachina.py | 174 ++++++++++++++++++++++++++----------
exmachina/init_test.sh | 2 +-
exmachina/test_exmachina.py | 21 +++--
3 files changed, 146 insertions(+), 51 deletions(-)
diff --git a/exmachina/exmachina.py b/exmachina/exmachina.py
index fc04beb1d..21de49322 100755
--- a/exmachina/exmachina.py
+++ b/exmachina/exmachina.py
@@ -30,11 +30,12 @@ client in the same way. The init_test.sh script demonstrates this mechanism.
import os
import sys
-import optparse
+import grp
+import shutil
+import argparse
import logging
import socket
import subprocess
-import stat
import time
import base64
@@ -43,9 +44,9 @@ import bjsonrpc.handlers
import bjsonrpc.server
import augeas
-
log = logging.getLogger(__name__)
+
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
@@ -54,7 +55,7 @@ def execute_service(servicename, action, timeout=10):
script = "/etc/init.d/" + os.path.split(servicename)[1]
if not os.path.exists(script):
- return "ERROR: so such service"
+ raise ValueError("so such service: %s" % servicename)
command_list = [script, action]
log.info("executing: %s" % command_list)
@@ -64,18 +65,51 @@ def execute_service(servicename, action, timeout=10):
stderr=subprocess.PIPE)
poll_seconds = .250
deadline = time.time() + timeout
- while time.time() < deadline and proc.poll() == None:
+ while time.time() < deadline and proc.poll() is None:
time.sleep(poll_seconds)
- if proc.poll() == None:
+ if proc.poll() is None:
if float(sys.version[:3]) >= 2.6:
proc.terminate()
- raise Exception("execution timed out (>%d seconds): %s" %
+ raise Exception("execution timed out (>%d seconds): %s" %
+ (timeout, command_list))
+
+ stdout, stderr = proc.communicate()
+ # TODO: 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
+
class ExMachinaHandler(bjsonrpc.handlers.BaseHandler):
# authentication state variable. If not None, still need to authenticate;
@@ -145,6 +179,24 @@ class ExMachinaHandler(bjsonrpc.handlers.BaseHandler):
log.info("augeas: remove %s" % path)
return self.augeas.remove(path.encode('utf-8'))
+ # ------------- Misc. non-Augeas Helpers -----------------
+ def set_timezone(self, tzname):
+ if not self.secret_key:
+ 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 -----------------
def initd_status(self, servicename):
if not self.secret_key:
@@ -161,11 +213,26 @@ class ExMachinaHandler(bjsonrpc.handlers.BaseHandler):
def initd_restart(self, servicename):
if not self.secret_key:
return execute_service(servicename, "restart")
-
+
+ # ------------- apt-get Package Control -----------------
+ def apt_install(self, packagename):
+ if not self.secret_key:
+ return execute_apt(packagename, "install")
+
+ def apt_update(self):
+ if not self.secret_key:
+ return execute_apt("", "update")
+
+ def apt_remove(self, packagename):
+ if not self.secret_key:
+ 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.
@@ -193,6 +260,8 @@ class ExMachinaClient():
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
@@ -206,11 +275,16 @@ class ExMachinaClient():
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):
+
+def run_server(socket_path, secret_key=None, socket_group=None):
if not 0 == os.geteuid():
log.warn("Expected to be running as root!")
@@ -221,15 +295,20 @@ def run_server(socket_path, secret_key=None):
sock.bind(socket_path)
sock.listen(1)
- # TODO: www-data group permissions only?
- os.chmod(socket_path, 0666)
+ if socket_group is not None:
+ 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)
+ else:
+ os.chmod(socket_path, 0666)
if secret_key:
ExMachinaHandler.secret_key = secret_key
serv = bjsonrpc.server.Server(sock, handler_factory=ExMachinaHandler)
serv.serve()
-def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
+def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
"""
From: http://www.noah.org/wiki/Daemonize_Python
@@ -241,30 +320,30 @@ def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
output may not appear in the order that you expect. """
# Do first fork.
- try:
- pid = os.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) )
+ 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()
+ os.chdir("/")
+ os.umask(0)
+ os.setsid()
# Do second fork.
- try:
- pid = os.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) )
+ 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+')
@@ -279,41 +358,44 @@ def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
def main():
global log
- parser = optparse.OptionParser(usage=
+ parser = argparse.ArgumentParser(usage=
"usage: %prog [options]\n"
"%prog --help for more info."
)
- parser.add_option("-v", "--verbose",
+ parser.add_argument("-v", "--verbose",
default=False,
- help="Show more debugging statements",
+ help="Show more debugging statements",
action="store_true")
- parser.add_option("-q", "--quiet",
+ parser.add_argument("-q", "--quiet",
default=False,
- help="Show fewer informational statements",
+ help="Show fewer informational statements",
action="store_true")
- parser.add_option("-k", "--key",
+ parser.add_argument("-k", "--key",
default=False,
help="Wait for Secret Access Key on stdin before starting",
action="store_true")
- parser.add_option("--random-key",
+ parser.add_argument("--random-key",
default=False,
help="Just dump a random base64 key and exit",
action="store_true")
- parser.add_option("-s", "--socket-path",
+ parser.add_argument("-s", "--socket-path",
default="/tmp/exmachina.sock",
help="UNIX Domain socket file path to listen on",
metavar="FILE")
- parser.add_option("--pidfile",
+ 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")
- (options, args) = parser.parse_args()
+ args = parser.parse_args()
- if len(args) != 0:
- parser.error("Incorrect number of arguments")
+ #if len(args) != 0:
+ #parser.error("Incorrect number of arguments")
- if options.random_key:
+ if args.random_key:
sys.stdout.write(base64.urlsafe_b64encode(os.urandom(128)))
sys.exit(0)
@@ -323,31 +405,33 @@ def main():
hdlr.setFormatter(formatter)
log.addHandler(hdlr)
- if options.verbose:
+ if args.verbose:
log.setLevel(logging.DEBUG)
- elif options.quiet:
+ elif args.quiet:
log.setLevel(logging.ERROR)
else:
log.setLevel(logging.INFO)
secret_key = None
- if options.key:
+ if args.key:
log.debug("Waiting for secret key on stdin...")
secret_key = sys.stdin.readline().strip()
log.debug("Got it!")
- if options.pidfile:
- with open(options.pidfile, 'w') as pfile:
+ if args.pidfile:
+ with open(args.pidfile, 'w') as pfile:
# ensure file is available/writable
pass
- os.unlink(options.pidfile)
+ os.unlink(args.pidfile)
daemonize()
pid = os.getpid()
- with open(options.pidfile, 'w') as pfile:
+ 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=options.socket_path)
+ run_server(secret_key=secret_key,
+ socket_path=args.socket_path,
+ socket_group=args.group)
if __name__ == '__main__':
main()
diff --git a/exmachina/init_test.sh b/exmachina/init_test.sh
index c53d76bd2..941285d36 100755
--- a/exmachina/init_test.sh
+++ b/exmachina/init_test.sh
@@ -4,7 +4,7 @@
export key=`./exmachina.py --random-key`
-echo $key | ./exmachina.py -vk --pidfile /tmp/exmachina_test.pid
+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
diff --git a/exmachina/test_exmachina.py b/exmachina/test_exmachina.py
index 86c71bf9b..e8d239d32 100755
--- a/exmachina/test_exmachina.py
+++ b/exmachina/test_exmachina.py
@@ -20,13 +20,11 @@ at the same time:
"""
import sys
-import optparse
-import logging
import socket
import bjsonrpc
import bjsonrpc.connection
-import augeas
+from bjsonrpc.exceptions import ServerError
from exmachina import ExMachinaClient
@@ -34,7 +32,7 @@ from exmachina import ExMachinaClient
# Command line handling
def main():
- socket_path="/tmp/exmachina.sock"
+ socket_path = "/tmp/exmachina.sock"
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(socket_path)
@@ -62,9 +60,22 @@ def main():
client = ExMachinaClient(secret_key=secret_key)
print client.augeas.match("/files/etc/*")
#print client.initd.restart("bluetooth")
- print client.initd.status("greentooth")
+ 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__':
From 53d1d8f2801d8142b28877a639c1d6920d5fb950 Mon Sep 17 00:00:00 2001
From: bnewbold
Date: Thu, 26 Jul 2012 12:30:29 -0700
Subject: [PATCH 03/24] add exmachina HOWTO
---
exmachina/HOWTO | 59 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 59 insertions(+)
create mode 100644 exmachina/HOWTO
diff --git a/exmachina/HOWTO b/exmachina/HOWTO
new file mode 100644
index 000000000..f76b3448f
--- /dev/null
+++ b/exmachina/HOWTO
@@ -0,0 +1,59 @@
+
+HOWTO: Use exmachina in Plinth to Change Configuration and Restart Services
+-----------------------------------------------------------------------------
+
+exmachina is the name of a small and simple privilege separation layer that
+lets Plinth (running as a web application with user/group www-data) edit
+FreedomBox system configuration (which requires root privileges). The Augeas
+library is used to provide a uniform interface to *most* configuration files in
+/etc/, and a wrapper around init.d scripts allows services to be restarted
+(which is often the way to have configuration changes go live).
+
+The exmachina daemon and client library is initialized when Plinth starts, and
+is accessible in Plinth code through the "cfg" global variable:
+
+ import cfg
+ cfg.exmachina.initd.restart("networking")
+
+Existing on-disk configuration settings can be read back through the "get"
+action, and files/settings can be found with the "match" method. For example,
+the following would print out a list of all the files in /etc/ with
+augeas-editing support, and then print the system's hostname according to
+/etc/hostname (a minimal one-line configuration file):
+
+ for fname in c.call.augeas_match("/files/etc/*"):
+ print fname
+ cfg.exmachina.augeas.get("/files/etc/hostname/*")
+
+The mutating Augeas actions ("set", "insert", "remove", etc) operate on an
+in-memory cache of the configuration files; to commit all changes (for all
+modified files) to disk, use the "save" action. The following would set the
+local hostname to fluffy.example.net, and then commit that change to disk.
+
+ cfg.exmachina.augeas.set("/files/etc/hostname/*", "fluffy.example.net")
+ cfg.exmachina.augeas.save()
+
+After the above, the /etc/hostname file will be updated, but running network
+services will not have picked up on the change. To have changes that have been
+written to disk actually take effect, the "initd" module allows access to
+restarting system services. In the case of hostnames, in debian there exists a
+helper script called "/etc/init.d/hostname.sh", so the following would be used
+to update all network daemons to a new hostname:
+
+ cfg.exmachina.initd.restart("hostname.sh")
+
+Augeas makes some effort to prevent partial writes or configuration file
+corruption, and in some cases provides basic type checking. However, it does
+not do any logical error detection or correction. For example, two separate
+services could be configured to try and listen on the same port, or an invalid
+hostname could be specified (eg, "&.^$#"). Application code in Plinth is
+responsible for ensuring the logical correctness of configuration changes.
+
+If there is a serious problem executing an exmachina method call (eg, tried to
+restart a non-existent service), a bjsonrpc.exceptions.ServerError exception
+will be raised, with the actual exception message (but not the traceback)
+passed through.
+
+Reading:
+- Augeas API notes: http://augeas.net/docs/api.html
+- exmachina repository: https://github.com/bnewbold/exmachina
From 09fbb23e90b19bf831888644f94ed3ca7e984bb7 Mon Sep 17 00:00:00 2001
From: bnewbold
Date: Thu, 26 Jul 2012 12:41:26 -0700
Subject: [PATCH 04/24] use exmachina to configure timezone
---
modules/installed/system/config.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/modules/installed/system/config.py b/modules/installed/system/config.py
index b31dc6009..01c4ec693 100644
--- a/modules/installed/system/config.py
+++ b/modules/installed/system/config.py
@@ -1,4 +1,4 @@
-import os, shutil, subprocess
+import os, subprocess
from socket import gethostname
import cherrypy
import simplejson as json
@@ -132,8 +132,8 @@ class general(FormPlugin, PagePlugin):
message += msg
if time_zone != sys_store['time_zone']:
src = os.path.join("/usr/share/zoneinfo", time_zone)
- cfg.log.info("Copying %s to /etc/localtime" % src)
- shutil.copy(src, "/etc/localtime")
+ cfg.log.info("Setting timezone to %s" % time_zone)
+ cfg.exmachina.misc.set_timezone(time_zone)
sys_store['time_zone'] = time_zone
return message or "Settings updated."
From 9519ac9ebfc8954ccbfa4d0822e478460a25d2e1 Mon Sep 17 00:00:00 2001
From: bnewbold
Date: Thu, 26 Jul 2012 12:42:06 -0700
Subject: [PATCH 05/24] use permissions for exmachina socket
---
share/init.d/plinth | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/share/init.d/plinth b/share/init.d/plinth
index 8cf88a93c..90bfcec98 100755
--- a/share/init.d/plinth
+++ b/share/init.d/plinth
@@ -38,7 +38,7 @@ start_plinth (){
SHAREDKEY=`$EXMACHINA_DAEMON --random-key`
touch $PID_FILE
chown $PLINTH_USER:$PLINTH_GROUP $PID_FILE
- echo $SHAREDKEY | $EXMACHINA_DAEMON --pidfile=$EXMACHINA_PID_FILE || rm $PID_FILE
+ echo $SHAREDKEY | $EXMACHINA_DAEMON --pidfile=$EXMACHINA_PID_FILE --group=$PLINTH_GROUP || rm $PID_FILE
sleep 0.5
echo $SHAREDKEY | sudo -u $PLINTH_USER -g $PLINTH_GROUP $DAEMON --pidfile=$PID_FILE
fi
From 7c9cb6ccb9f735e78ea82230d5488c9efbe2e592 Mon Sep 17 00:00:00 2001
From: bnewbold
Date: Thu, 26 Jul 2012 12:53:42 -0700
Subject: [PATCH 06/24] update .gitignore
---
.gitignore | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/.gitignore b/.gitignore
index 2088e658a..570bf0b09 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,7 @@ templates/*.py
TODO
\#*
.#*
+cfg.py
+cherrypy.config
+data/users.sqlite3
+
From 82df0a800d3894bb35eae3d53a57b508b4b4803f Mon Sep 17 00:00:00 2001
From: bnewbold
Date: Tue, 7 Aug 2012 16:42:29 -0700
Subject: [PATCH 07/24] fix test comments (thx nick daly)
---
exmachina/test_exmachina.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/exmachina/test_exmachina.py b/exmachina/test_exmachina.py
index e8d239d32..6c1db9418 100755
--- a/exmachina/test_exmachina.py
+++ b/exmachina/test_exmachina.py
@@ -5,13 +5,13 @@ This file tests the "client side" of the exmachina layer.
To use with secret keys, do the following in seperate terminals:
- $ echo "" | ./exmachina.py -vk
- $ echo "" | ./test.py -k
+ $ echo "" | sudo ./exmachina.py -vk
+ $ echo "" | ./test_exmachina.py -k
To use without, do the following in seperate terminals:
- $ echo "" | ./exmachina.py -vk
- $ echo "" | ./test.py -k
+ $ 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:
From 3ef8e8bbc25854a699b386d57497dd5c93f20c20 Mon Sep 17 00:00:00 2001
From: Nick Daly
Date: Sun, 23 Sep 2012 18:32:16 -0500
Subject: [PATCH 08/24] Customize host in cfg.py.
We start with a default of 127.0.0.1, because anything else is stupid.
Change to 0.0.0.0 to serve to everybody.
---
cfg.sample.py | 1 +
plinth.py | 8 ++++----
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/cfg.sample.py b/cfg.sample.py
index 799984a86..ee57bb6e0 100644
--- a/cfg.sample.py
+++ b/cfg.sample.py
@@ -12,6 +12,7 @@ users_dir = os.path.join(data_dir, "users")
product_name = "Plinth"
box_name = "FreedomBox"
+host = 127.0.0.1
port = 8000
## Do not edit below this line ##
diff --git a/plinth.py b/plinth.py
index adab11012..5cdce88e2 100755
--- a/plinth.py
+++ b/plinth.py
@@ -54,7 +54,7 @@ class Root(plugin_mount.PagePlugin):
raise cherrypy.InternalRedirect('/router')
else:
raise cherrypy.InternalRedirect('/help/about')
-
+
def load_modules():
"""Import all the symlinked .py files in the modules directory and
all the .py files in directories linked in the modules directory
@@ -121,7 +121,7 @@ def setup():
server.subscribe()
# Configure default server
- cherrypy.config.update({'server.socket_host': '127.0.0.1',
+ cherrypy.config.update({'server.socket_host': cfg.host,
'server.socket_port': cfg.port,
'server.thread_pool':10,
'tools.staticdir.root': cfg.file_root,
@@ -130,7 +130,7 @@ def setup():
'tools.sessions.storage_type':"file",
'tools.sessions.timeout':90,
'tools.sessions.storage_path':"%s/data/cherrypy_sessions" % cfg.file_root,
-
+
})
config = {'/': {'tools.staticdir.root': '%s/static' % cfg.file_root,
@@ -142,7 +142,7 @@ def setup():
}
cherrypy.tree.mount(cfg.html_root, '/', config=config)
cherrypy.engine.signal_handler.subscribe()
-
+
def main():
setup()
From 480aacfac55772128e18366aa3870d69bc636b0e Mon Sep 17 00:00:00 2001
From: James Valleroy
Date: Thu, 4 Oct 2012 21:39:12 -0400
Subject: [PATCH 09/24] Fix host cfg and print on startup
---
cfg.sample.py | 2 +-
plinth.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/cfg.sample.py b/cfg.sample.py
index ee57bb6e0..160272446 100644
--- a/cfg.sample.py
+++ b/cfg.sample.py
@@ -12,7 +12,7 @@ users_dir = os.path.join(data_dir, "users")
product_name = "Plinth"
box_name = "FreedomBox"
-host = 127.0.0.1
+host = "127.0.0.1"
port = 8000
## Do not edit below this line ##
diff --git a/plinth.py b/plinth.py
index 5cdce88e2..28f7207b1 100755
--- a/plinth.py
+++ b/plinth.py
@@ -146,7 +146,7 @@ def setup():
def main():
setup()
- print "localhost %d" % cfg.port
+ print "%s %d" % (cfg.host, cfg.port)
cherrypy.engine.start()
cherrypy.engine.block()
From f03e184c78c2a78abd03e45a335262b945e24e12 Mon Sep 17 00:00:00 2001
From: Nick Daly
Date: Sun, 7 Oct 2012 20:28:42 -0500
Subject: [PATCH 10/24] Added default pidfile.
---
plinth.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/plinth.py b/plinth.py
index 5cdce88e2..d2a3e3ed4 100755
--- a/plinth.py
+++ b/plinth.py
@@ -79,6 +79,8 @@ def parse_arguments():
args=parser.parse_args()
if args.pidfile:
cfg.pidfile = args.pidfile
+ else:
+ cfg.pidfile = "plinth.pid"
if args.listen_exmachina_key:
# this is where we optionally try to read in a shared secret key to
# authenticate connections to exmachina
From ded54c9c48bb8ff829ec628961c0517baaf89883 Mon Sep 17 00:00:00 2001
From: Nick Daly
Date: Sun, 7 Oct 2012 20:36:50 -0500
Subject: [PATCH 11/24] Created link issue.
Plinth really should link clients to the right pages.
---
issues/links.org | 42 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
create mode 100644 issues/links.org
diff --git a/issues/links.org b/issues/links.org
new file mode 100644
index 000000000..c95b563c5
--- /dev/null
+++ b/issues/links.org
@@ -0,0 +1,42 @@
+# -*- mode: org; mode: auto-fill; fill-column: 80 -*-
+
+#+TITLE: Make Links Portable
+#+OPTIONS: d:t
+#+LINK_UP: ./
+#+LINK_HOME: ../
+
+* Issue
+
+ Currently, all the links in Plinth point to 127.0.0.1/(something), and that
+ sucks for serving Plinth on a local network, like most use cases imply.
+
+* Fixes [0/1]
+
+** TODO Links work when accessed from remote clients.
+
+ Investigate the following:
+
+ : grep -nHr basehref ../*
+
+ : grep -nHr 127.0 ../*
+
+ #+begin_ascii
+ fabfile.py:40: if env.host == "localhost" or env.host=="127.0.0.1":
+ fabfile.py:46: if env.host == "localhost" or env.host=="127.0.0.1":
+ fabfile.py:102: hidden_service_config = "HiddenServiceDir %s\nHiddenServicePort 80 127.0.0.1:%d" % (tor_dir, santiago_port)
+ modules/installed/santiago/santiago.py:48: hidden_service_config = "HiddenServiceDir %s\nHiddenServicePort 80 127.0.0.1:%d" % (self.tor_dir, santiago_port)
+ plinth.py:119: server.socket_host = '127.0.0.1'
+ #+end_ascii
+
+ Also, why is base_href blank in [[file:~/programs/freedombox/plinth/cfg.sample.py][cfg.sample.py]]?
+
+* Discussion
+
+* Metadata
+ :PROPERTIES:
+ :Status: Incomplete
+ :Priority: 0
+ :Owner: Nick Daly
+ :Description:
+ :Tags:
+ :END:
From 88225aa3ae7b5178b24961f6cb005f40c55bdd7d Mon Sep 17 00:00:00 2001
From: Nick Daly
Date: Sun, 11 Nov 2012 12:25:11 -0600
Subject: [PATCH 12/24] Created start.sh to handle ExMachina outside of Plinth.
---
INSTALL | 20 +++++++++-----------
start.sh | 8 ++++++++
2 files changed, 17 insertions(+), 11 deletions(-)
create mode 100755 start.sh
diff --git a/INSTALL b/INSTALL
index 9108689e2..e5761a149 100644
--- a/INSTALL
+++ b/INSTALL
@@ -11,18 +11,18 @@ Install the python-simplejson
apt-get install python-simplejson
-
+
Unzip and untar the source into a directory. Change to the directory
-containing the program. Run:
+containing the program. Run:
+
+ $ make
+
+To start Plinth, run:
+
+ $ ./start.sh
- $ make
- $ ./plinth.py
-
and point your web browser at `localhost:8000`. The default username is "admin"
-and the default password is "secret". To actually edit the configuration of
-your local/dev machine, also run:
-
- $ sudo ./exmachina/exmachina.py -v &
+and the default password is "secret".
## Dependencies
@@ -53,5 +53,3 @@ The documentation has some dependencies too.
Documentation has been collected into a pdf that can be built using
`make doc`. It also gets built into smaller files and other formats,
including one suitable for install as a man page.
-
-
diff --git a/start.sh b/start.sh
new file mode 100755
index 000000000..01bc36e57
--- /dev/null
+++ b/start.sh
@@ -0,0 +1,8 @@
+#! /bin/sh
+
+PYTHONPATH=../exmachina:$PYTHONPATH
+
+sudo killall exmachina.py
+sudo ../exmachina/exmachina.py -v &
+python plinth.py
+sudo killall exmachina.py
From fed5bd13f1bdb5c1359c96512fd91e1dd445a9be Mon Sep 17 00:00:00 2001
From: Nick Daly
Date: Sun, 11 Nov 2012 13:13:22 -0600
Subject: [PATCH 13/24] Externalized ExMachina again, partially reverting
change:
f1e764f2e5728113f191456236d02fdae6e7680a
Partially revert the EM integration change, it's not solid yet. This
allows EM to grow on its own. However, I'm not reverting the whole
change because I want to make it easy to use EM from an external
repository, and most of the Plinth-specific changes are good. To use
EM in Plinth again, make sure EM and Plinth are in the same directory
before running start.sh. The directory structure should look like:
./exmachina/
./plinth/
start.sh updates Python's path correctly, so this change should be
transparent and Plinth should still run the same.
---
exmachina/HOWTO | 59 -----
exmachina/__init__.py | 0
exmachina/exmachina.py | 437 ------------------------------------
exmachina/init_test.sh | 13 --
exmachina/test_exmachina.py | 82 -------
5 files changed, 591 deletions(-)
delete mode 100644 exmachina/HOWTO
delete mode 100644 exmachina/__init__.py
delete mode 100755 exmachina/exmachina.py
delete mode 100755 exmachina/init_test.sh
delete mode 100755 exmachina/test_exmachina.py
diff --git a/exmachina/HOWTO b/exmachina/HOWTO
deleted file mode 100644
index f76b3448f..000000000
--- a/exmachina/HOWTO
+++ /dev/null
@@ -1,59 +0,0 @@
-
-HOWTO: Use exmachina in Plinth to Change Configuration and Restart Services
------------------------------------------------------------------------------
-
-exmachina is the name of a small and simple privilege separation layer that
-lets Plinth (running as a web application with user/group www-data) edit
-FreedomBox system configuration (which requires root privileges). The Augeas
-library is used to provide a uniform interface to *most* configuration files in
-/etc/, and a wrapper around init.d scripts allows services to be restarted
-(which is often the way to have configuration changes go live).
-
-The exmachina daemon and client library is initialized when Plinth starts, and
-is accessible in Plinth code through the "cfg" global variable:
-
- import cfg
- cfg.exmachina.initd.restart("networking")
-
-Existing on-disk configuration settings can be read back through the "get"
-action, and files/settings can be found with the "match" method. For example,
-the following would print out a list of all the files in /etc/ with
-augeas-editing support, and then print the system's hostname according to
-/etc/hostname (a minimal one-line configuration file):
-
- for fname in c.call.augeas_match("/files/etc/*"):
- print fname
- cfg.exmachina.augeas.get("/files/etc/hostname/*")
-
-The mutating Augeas actions ("set", "insert", "remove", etc) operate on an
-in-memory cache of the configuration files; to commit all changes (for all
-modified files) to disk, use the "save" action. The following would set the
-local hostname to fluffy.example.net, and then commit that change to disk.
-
- cfg.exmachina.augeas.set("/files/etc/hostname/*", "fluffy.example.net")
- cfg.exmachina.augeas.save()
-
-After the above, the /etc/hostname file will be updated, but running network
-services will not have picked up on the change. To have changes that have been
-written to disk actually take effect, the "initd" module allows access to
-restarting system services. In the case of hostnames, in debian there exists a
-helper script called "/etc/init.d/hostname.sh", so the following would be used
-to update all network daemons to a new hostname:
-
- cfg.exmachina.initd.restart("hostname.sh")
-
-Augeas makes some effort to prevent partial writes or configuration file
-corruption, and in some cases provides basic type checking. However, it does
-not do any logical error detection or correction. For example, two separate
-services could be configured to try and listen on the same port, or an invalid
-hostname could be specified (eg, "&.^$#"). Application code in Plinth is
-responsible for ensuring the logical correctness of configuration changes.
-
-If there is a serious problem executing an exmachina method call (eg, tried to
-restart a non-existent service), a bjsonrpc.exceptions.ServerError exception
-will be raised, with the actual exception message (but not the traceback)
-passed through.
-
-Reading:
-- Augeas API notes: http://augeas.net/docs/api.html
-- exmachina repository: https://github.com/bnewbold/exmachina
diff --git a/exmachina/__init__.py b/exmachina/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/exmachina/exmachina.py b/exmachina/exmachina.py
deleted file mode 100755
index 21de49322..000000000
--- a/exmachina/exmachina.py
+++ /dev/null
@@ -1,437 +0,0 @@
-#!/usr/bin/env python
-
-"""
-Author: bnewbold
-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
-import grp
-import shutil
-import argparse
-import logging
-import socket
-import subprocess
-import time
-import base64
-
-import bjsonrpc
-import bjsonrpc.handlers
-import bjsonrpc.server
-import augeas
-
-log = logging.getLogger(__name__)
-
-
-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()
- # TODO: 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
-
-
-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):
- self.augeas = augeas.Augeas()
-
- def authenticate(self, secret_key):
- if not self.secret_key:
- log.warn("Unecessary authentication attempt")
- return
- if not secret_key.strip() == self.secret_key.strip():
- # fail hard
- log.error("Authentication failed!")
- sys.exit()
- self.secret_key = None
-
- # ------------- Augeas API Passthrough -----------------
- def augeas_save(self):
- if not self.secret_key:
- log.info("augeas: saving config")
- return self.augeas.save()
-
- def augeas_set(self, path, value):
- if not self.secret_key:
- log.info("augeas: set %s=%s" % (path, value))
- return self.augeas.set(path.encode('utf-8'),
- value.encode('utf-8'))
-
- def augeas_setm(self, base, sub, value):
- if not self.secret_key:
- 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'))
-
- def augeas_get(self, path):
- if not self.secret_key:
- # reduce verbosity
- log.debug("augeas: get %s" % path)
- return self.augeas.get(path.encode('utf-8'))
-
- def augeas_match(self, path):
- if not self.secret_key:
- # reduce verbosity
- log.debug("augeas: match %s" % path)
- return self.augeas.match("%s" % path.encode('utf-8'))
-
- def augeas_insert(self, path, label, before=True):
- if not self.secret_key:
- log.info("augeas: insert %s=%s" % (path, value))
- return self.augeas.insert(path.encode('utf-8'),
- label.encode('utf-8'),
- before=before)
-
- def augeas_move(self, src, dst):
- if not self.secret_key:
- log.info("augeas: move %s -> %s" % (src, dst))
- return self.augeas.move(src.encode('utf-8'), dst.encode('utf-8'))
-
- def augeas_remove(self, path):
- if not self.secret_key:
- log.info("augeas: remove %s" % path)
- return self.augeas.remove(path.encode('utf-8'))
-
- # ------------- Misc. non-Augeas Helpers -----------------
- def set_timezone(self, tzname):
- if not self.secret_key:
- 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 -----------------
- def initd_status(self, servicename):
- if not self.secret_key:
- return execute_service(servicename, "status")
-
- def initd_start(self, servicename):
- if not self.secret_key:
- return execute_service(servicename, "start")
-
- def initd_stop(self, servicename):
- if not self.secret_key:
- return execute_service(servicename, "stop")
-
- def initd_restart(self, servicename):
- if not self.secret_key:
- return execute_service(servicename, "restart")
-
- # ------------- apt-get Package Control -----------------
- def apt_install(self, packagename):
- if not self.secret_key:
- return execute_apt(packagename, "install")
-
- def apt_update(self):
- if not self.secret_key:
- return execute_apt("", "update")
-
- def apt_remove(self, packagename):
- if not self.secret_key:
- 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):
- self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- self.sock.connect(socket_path)
- self.conn = bjsonrpc.connection.Connection(self.sock)
-
- if secret_key:
- self.conn.call.authenticate(secret_key)
-
- 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):
-
- if not 0 == os.geteuid():
- log.warn("Expected to be running as root!")
-
- if os.path.exists(socket_path):
- os.unlink(socket_path)
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- sock.bind(socket_path)
- sock.listen(1)
-
- if socket_group is not None:
- 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)
- else:
- os.chmod(socket_path, 0666)
- if secret_key:
- ExMachinaHandler.secret_key = secret_key
-
- 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: %prog [options]\n"
- "%prog --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")
-
- args = parser.parse_args()
-
- #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)
-
-if __name__ == '__main__':
- main()
diff --git a/exmachina/init_test.sh b/exmachina/init_test.sh
deleted file mode 100755
index 941285d36..000000000
--- a/exmachina/init_test.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/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
deleted file mode 100755
index 6c1db9418..000000000
--- a/exmachina/test_exmachina.py
+++ /dev/null
@@ -1,82 +0,0 @@
-#!/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 import ExMachinaClient
-
-# =============================================================================
-# Command line handling
-def main():
-
- socket_path = "/tmp/exmachina.sock"
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- sock.connect(socket_path)
-
- secret_key = None
- if sys.argv[-1] == "-k":
- print "waiting for key on stdin..."
- secret_key = sys.stdin.readline()
- print "sent!"
-
- 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()
From b2c157ec1dafe4d5d5f35545b18161ee707d3767 Mon Sep 17 00:00:00 2001
From: Tom Galloway
Date: Thu, 3 Jan 2013 15:08:06 +0000
Subject: [PATCH 14/24] Added predepend packages and updated python path for
build folders.
---
.gitignore | 3 ++-
Makefile | 18 +++++++++++++++++-
plinth.py | 2 +-
start.sh | 7 +++++--
4 files changed, 25 insertions(+), 5 deletions(-)
diff --git a/.gitignore b/.gitignore
index 570bf0b09..3b943e1bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,4 +23,5 @@ TODO
cfg.py
cherrypy.config
data/users.sqlite3
-
+predepend
+build/
diff --git a/Makefile b/Makefile
index 3afdbcfb0..cf10e973c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,6 @@
#SHELL := /bin/bash
MAKE=make
+BUILD_DIR = build
#TODO: add install target
@@ -9,9 +10,22 @@ COMPRESSED_CSS := $(patsubst %.css,%.tiny.css,$(CSS))
PWD=`pwd`
## Catch-all tagets
-default: cfg cherrypy.config dirs template css docs dbs
+default: predepend cfg cherrypy.config dirs template css docs dbs $(BUILD_DIR)/exmachina $(BUILD_DIR)/bjsonrpc
all: default
+build:
+ mkdir -p $(BUILD_DIR)
+
+predepend:
+ sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson"
+ touch predepend
+
+$(BUILD_DIR)/exmachina: build
+ git clone git://github.com/bnewbold/exmachina $(BUILD_DIR)/exmachina
+
+$(BUILD_DIR)/bjsonrpc: build
+ git clone git://github.com/deavid/bjsonrpc.git $(BUILD_DIR)/bjsonrpc
+
dbs: data/users.sqlite3
data/users.sqlite3: data/users.sqlite3.distrib
@@ -69,3 +83,5 @@ clean:
@find . -name "*.bak" -exec rm {} \;
@$(MAKE) -s -C doc clean
@$(MAKE) -s -C templates clean
+ rm -rf build
+ rm -f predepend
diff --git a/plinth.py b/plinth.py
index 6c197463c..eb39a4251 100755
--- a/plinth.py
+++ b/plinth.py
@@ -17,7 +17,7 @@ from util import *
from logger import Logger
#from modules.auth import AuthController, require, member_of, name_is
-from exmachina.exmachina import ExMachinaClient
+from exmachina import ExMachinaClient
import socket
__version__ = "0.2.14"
diff --git a/start.sh b/start.sh
index 01bc36e57..1a16d1a5f 100755
--- a/start.sh
+++ b/start.sh
@@ -1,8 +1,11 @@
#! /bin/sh
-PYTHONPATH=../exmachina:$PYTHONPATH
+PYTHONPATH=build/exmachina:$PYTHONPATH
+PYTHONPATH=build/bjsonrpc:$PYTHONPATH
+
+export PYTHONPATH
sudo killall exmachina.py
-sudo ../exmachina/exmachina.py -v &
+sudo build/exmachina/exmachina.py -v &
python plinth.py
sudo killall exmachina.py
From 076bf829e72676575a3d44f53722cbfcb6e03fde Mon Sep 17 00:00:00 2001
From: Tom Galloway
Date: Thu, 3 Jan 2013 15:12:15 +0000
Subject: [PATCH 15/24] Extra predepend
---
Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index cf10e973c..b28db0a17 100644
--- a/Makefile
+++ b/Makefile
@@ -17,7 +17,7 @@ build:
mkdir -p $(BUILD_DIR)
predepend:
- sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson"
+ sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson pandoc"
touch predepend
$(BUILD_DIR)/exmachina: build
From 847d000f998d05afc7eef589319159778ff3d5de Mon Sep 17 00:00:00 2001
From: Tom Galloway
Date: Thu, 3 Jan 2013 15:20:37 +0000
Subject: [PATCH 16/24] Plinth needs to be run as root currently.
---
start.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/start.sh b/start.sh
index 1a16d1a5f..da7a4d34e 100755
--- a/start.sh
+++ b/start.sh
@@ -7,5 +7,5 @@ export PYTHONPATH
sudo killall exmachina.py
sudo build/exmachina/exmachina.py -v &
-python plinth.py
+sudo python plinth.py
sudo killall exmachina.py
From 95118b3b96d18c5255f1d273b07c387058b32e09 Mon Sep 17 00:00:00 2001
From: Tom Galloway
Date: Thu, 3 Jan 2013 15:22:16 +0000
Subject: [PATCH 17/24] Clean now uses BUILD_DIR variable.
---
Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index b28db0a17..eb1fad1ce 100644
--- a/Makefile
+++ b/Makefile
@@ -83,5 +83,5 @@ clean:
@find . -name "*.bak" -exec rm {} \;
@$(MAKE) -s -C doc clean
@$(MAKE) -s -C templates clean
- rm -rf build
+ rm -rf $(BUILD_DIR)
rm -f predepend
From b45d5cfe85bc7686a6296aa0af4735710a6f7122 Mon Sep 17 00:00:00 2001
From: Tom Galloway
Date: Fri, 4 Jan 2013 12:50:51 +0000
Subject: [PATCH 18/24] Extra predepend's, plinth doesn't need to run as root,
pidfile location set in cfg file.
---
Makefile | 2 +-
cfg.sample.py | 1 +
plinth.py | 3 ++-
start.sh | 2 +-
4 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/Makefile b/Makefile
index eb1fad1ce..6c2f8ac8c 100644
--- a/Makefile
+++ b/Makefile
@@ -17,7 +17,7 @@ build:
mkdir -p $(BUILD_DIR)
predepend:
- sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson pandoc"
+ sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson pandoc python-cheetah"
touch predepend
$(BUILD_DIR)/exmachina: build
diff --git a/cfg.sample.py b/cfg.sample.py
index 160272446..8521ca653 100644
--- a/cfg.sample.py
+++ b/cfg.sample.py
@@ -8,6 +8,7 @@ user_db = os.path.join(data_dir, "users")
status_log_file = os.path.join(data_dir, "status.log")
access_log_file = os.path.join(data_dir, "access.log")
users_dir = os.path.join(data_dir, "users")
+pidfile = os.path.join(data_dir, "pidfile.pid")
product_name = "Plinth"
box_name = "FreedomBox"
diff --git a/plinth.py b/plinth.py
index eb39a4251..3b8a1001a 100755
--- a/plinth.py
+++ b/plinth.py
@@ -80,7 +80,8 @@ def parse_arguments():
if args.pidfile:
cfg.pidfile = args.pidfile
else:
- cfg.pidfile = "plinth.pid"
+ if not cfg.pidfile:
+ cfg.pidfile = "plinth.pid"
if args.listen_exmachina_key:
# this is where we optionally try to read in a shared secret key to
# authenticate connections to exmachina
diff --git a/start.sh b/start.sh
index da7a4d34e..1a16d1a5f 100755
--- a/start.sh
+++ b/start.sh
@@ -7,5 +7,5 @@ export PYTHONPATH
sudo killall exmachina.py
sudo build/exmachina/exmachina.py -v &
-sudo python plinth.py
+python plinth.py
sudo killall exmachina.py
From 8e16374136b5d230edc1cb049f30dc4ca6e88f78 Mon Sep 17 00:00:00 2001
From: Nick Daly
Date: Sun, 6 Jan 2013 16:05:58 -0600
Subject: [PATCH 19/24] Commented out the extra bjsonrpc call.
---
Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index 6c2f8ac8c..40460de7a 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,7 @@ COMPRESSED_CSS := $(patsubst %.css,%.tiny.css,$(CSS))
PWD=`pwd`
## Catch-all tagets
-default: predepend cfg cherrypy.config dirs template css docs dbs $(BUILD_DIR)/exmachina $(BUILD_DIR)/bjsonrpc
+default: predepend cfg cherrypy.config dirs template css docs dbs $(BUILD_DIR)/exmachina #$(BUILD_DIR)/bjsonrpc
all: default
build:
From b54a4906e67e6e35a571c754865c1a517722674f Mon Sep 17 00:00:00 2001
From: Tom Galloway
Date: Thu, 10 Jan 2013 09:15:31 +0000
Subject: [PATCH 20/24] Fix to allow hostname to be updated on Ubuntu. This
needed a change to exmachina which I've created a pull request for. I've
updated Make to point to my fork of exmachina until this has been merged,
when it can be reverted.
---
Makefile | 2 +-
modules/installed/system/config.py | 6 +++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/Makefile b/Makefile
index 40460de7a..72d916965 100644
--- a/Makefile
+++ b/Makefile
@@ -21,7 +21,7 @@ predepend:
touch predepend
$(BUILD_DIR)/exmachina: build
- git clone git://github.com/bnewbold/exmachina $(BUILD_DIR)/exmachina
+ git clone git://github.com/tomgalloway/exmachina $(BUILD_DIR)/exmachina
$(BUILD_DIR)/bjsonrpc: build
git clone git://github.com/deavid/bjsonrpc.git $(BUILD_DIR)/bjsonrpc
diff --git a/modules/installed/system/config.py b/modules/installed/system/config.py
index 01c4ec693..b671fcc12 100644
--- a/modules/installed/system/config.py
+++ b/modules/installed/system/config.py
@@ -10,6 +10,7 @@ import cfg
from forms import Form
from model import User
from util import *
+import platform
class Config(PagePlugin):
def __init__(self, *args, **kwargs):
@@ -49,7 +50,10 @@ def set_hostname(hostname):
# don't persist/cache change unless it was saved successfuly
sys_store = filedict_con(cfg.store_file, 'sys')
sys_store['hostname'] = hostname
- cfg.exmachina.initd.restart("hostname.sh") # is hostname.sh debian-only?
+ if platform.linux_distribution()[0]=="Ubuntu" :
+ cfg.exmachina.service.start("hostname")
+ else:
+ cfg.exmachina.initd.restart("hostname.sh") # is hostname.sh debian-only?
except OSError, e:
raise cherrypy.HTTPError(500, "Hostname restart failed: %s" % e)
From 646b5518bbd33c6979cac548adca2416d401833a Mon Sep 17 00:00:00 2001
From: Tom Galloway
Date: Wed, 16 Jan 2013 13:08:48 +0000
Subject: [PATCH 21/24] withsqlite is now retrieved from github. Manage User &
Groups pages now display correctly but don't do anything yet.
---
.gitignore | 1 +
Makefile | 9 ++++++++-
modules/installed/system/users.py | 19 ++++++++++---------
start.sh | 1 -
4 files changed, 19 insertions(+), 11 deletions(-)
diff --git a/.gitignore b/.gitignore
index 3b943e1bb..d5be6856d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,4 @@ cherrypy.config
data/users.sqlite3
predepend
build/
+*.pid
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 72d916965..f5bf865b9 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,7 @@
#SHELL := /bin/bash
MAKE=make
BUILD_DIR = build
+VENDOR_DIR = vendor
#TODO: add install target
@@ -10,12 +11,15 @@ COMPRESSED_CSS := $(patsubst %.css,%.tiny.css,$(CSS))
PWD=`pwd`
## Catch-all tagets
-default: predepend cfg cherrypy.config dirs template css docs dbs $(BUILD_DIR)/exmachina #$(BUILD_DIR)/bjsonrpc
+default: predepend cfg cherrypy.config dirs template css docs dbs $(BUILD_DIR)/exmachina $(VENDOR_DIR)/withsqlite #$(BUILD_DIR)/bjsonrpc
all: default
build:
mkdir -p $(BUILD_DIR)
+vendor:
+ mkdir -p $(VENDOR_DIR)
+
predepend:
sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson pandoc python-cheetah"
touch predepend
@@ -23,6 +27,9 @@ predepend:
$(BUILD_DIR)/exmachina: build
git clone git://github.com/tomgalloway/exmachina $(BUILD_DIR)/exmachina
+$(VENDOR_DIR)/withsqlite: vendor
+ git clone git://github.com/jvasile/withsqlite.git $(VENDOR_DIR)/withsqlite
+
$(BUILD_DIR)/bjsonrpc: build
git clone git://github.com/deavid/bjsonrpc.git $(BUILD_DIR)/bjsonrpc
diff --git a/modules/installed/system/users.py b/modules/installed/system/users.py
index c4ac97771..81e747b2f 100644
--- a/modules/installed/system/users.py
+++ b/modules/installed/system/users.py
@@ -5,22 +5,23 @@ from plugin_mount import PagePlugin, FormPlugin
import cfg
from forms import Form
from util import *
+from pprint import pprint
class users(PagePlugin):
order = 20 # order of running init in PagePlugins
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("sys.users")
+ self.register_page("sys.users.add")
+ self.register_page("sys.users.edit")
@cherrypy.expose
@require()
def index(self):
- parts = self.forms('/sys/config')
- parts['title']=_("Manage Users and Groups")
- return self.fill_template(**parts)
+ return self.fill_template(title="Manage Users and Groups", sidebar_right="""Add User
Edit Users""")
class add(FormPlugin, PagePlugin):
- url = ["/sys/users"]
+ url = ["/sys/users/add"]
order = 30
sidebar_left = ''
@@ -63,10 +64,10 @@ class add(FormPlugin, PagePlugin):
msg = add_message(msg, "%s saved." % username)
main = self.make_form(username, name, email, message=msg)
- return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right)
+ return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right)
class edit(FormPlugin, PagePlugin):
- url = ["/sys/users"]
+ url = ["/sys/users/edit"]
order = 35
sidebar_left = ''
@@ -77,7 +78,7 @@ class edit(FormPlugin, PagePlugin):
system.
Deleting users is permanent!
""" % (cfg.product_name, cfg.box_name))
def main(self, msg=''):
- users = cfg.users.keys()
+ users = cfg.users
add_form = Form(title=_("Edit or Delete User"), action="/sys/users/edit", message=msg)
add_form.html('Delete
')
for uname in sorted(users.keys()):
@@ -114,7 +115,7 @@ class edit(FormPlugin, PagePlugin):
else:
msg.add = _("Must specify at least one valid, existing user.")
main = self.make_form(msg=msg.text)
- return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right)
+ return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right)
sidebar_right = ''
u = cfg.users[kwargs['username']]
@@ -125,4 +126,4 @@ class edit(FormPlugin, PagePlugin):
main = _("""Edit User '%s'""" % u['username'])
sidebar_right = ''
- return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=sidebar_right)
+ return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=sidebar_right)
diff --git a/start.sh b/start.sh
index 1a16d1a5f..4be20c1b6 100755
--- a/start.sh
+++ b/start.sh
@@ -1,7 +1,6 @@
#! /bin/sh
PYTHONPATH=build/exmachina:$PYTHONPATH
-PYTHONPATH=build/bjsonrpc:$PYTHONPATH
export PYTHONPATH
From a312b6d288dc2f978237c1d5e309972d3c873327 Mon Sep 17 00:00:00 2001
From: Tom Galloway
Date: Wed, 16 Jan 2013 15:05:04 +0000
Subject: [PATCH 22/24] Removed changes to get withsqlite from github as it's a
different version?
---
Makefile | 9 +--------
1 file changed, 1 insertion(+), 8 deletions(-)
diff --git a/Makefile b/Makefile
index f5bf865b9..72d916965 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,6 @@
#SHELL := /bin/bash
MAKE=make
BUILD_DIR = build
-VENDOR_DIR = vendor
#TODO: add install target
@@ -11,15 +10,12 @@ COMPRESSED_CSS := $(patsubst %.css,%.tiny.css,$(CSS))
PWD=`pwd`
## Catch-all tagets
-default: predepend cfg cherrypy.config dirs template css docs dbs $(BUILD_DIR)/exmachina $(VENDOR_DIR)/withsqlite #$(BUILD_DIR)/bjsonrpc
+default: predepend cfg cherrypy.config dirs template css docs dbs $(BUILD_DIR)/exmachina #$(BUILD_DIR)/bjsonrpc
all: default
build:
mkdir -p $(BUILD_DIR)
-vendor:
- mkdir -p $(VENDOR_DIR)
-
predepend:
sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson pandoc python-cheetah"
touch predepend
@@ -27,9 +23,6 @@ predepend:
$(BUILD_DIR)/exmachina: build
git clone git://github.com/tomgalloway/exmachina $(BUILD_DIR)/exmachina
-$(VENDOR_DIR)/withsqlite: vendor
- git clone git://github.com/jvasile/withsqlite.git $(VENDOR_DIR)/withsqlite
-
$(BUILD_DIR)/bjsonrpc: build
git clone git://github.com/deavid/bjsonrpc.git $(BUILD_DIR)/bjsonrpc
From c4cddbfc0e42afb8e69dce08b561ac00b0f07b35 Mon Sep 17 00:00:00 2001
From: Tom Galloway
Date: Mon, 21 Jan 2013 10:30:52 +0000
Subject: [PATCH 23/24] Changes to get user management screens started. Updated
UserStore to add all expected functions. Added tests for these functions.
---
model.py | 9 +--
modules/installed/lib/user_store.py | 38 +++++++++++-
modules/installed/system/users.py | 24 ++++----
test.sh | 9 +++
tests/test_user_store.py | 86 ++++++++++++++++++++++++++++
tests/testdata/users.sqlite3 | Bin 0 -> 3072 bytes
6 files changed, 148 insertions(+), 18 deletions(-)
create mode 100755 test.sh
create mode 100644 tests/test_user_store.py
create mode 100644 tests/testdata/users.sqlite3
diff --git a/model.py b/model.py
index d3807d0b1..e0b3cd20d 100644
--- a/model.py
+++ b/model.py
@@ -1,14 +1,15 @@
class User(dict):
- """ Every user must have keys for a username, name, password (this
+ """ Every user must have keys for a username, name, passphrase (this
is a md5 hash of the password), groups, and an email address. They can be
blank or None, but the keys must exist. """
def __init__(self, dict=None):
- for key in ['username', 'name', 'password', 'email']:
+ for key in ['username', 'name', 'passphrase', 'email']:
self[key] = ''
for key in ['groups']:
self[key] = []
- for key in dict:
- self[key] = dict[key]
+ if dict:
+ for key in dict:
+ self[key] = dict[key]
def __getattr__(self, attr):
return None
diff --git a/modules/installed/lib/user_store.py b/modules/installed/lib/user_store.py
index a4042c102..236b73a8f 100644
--- a/modules/installed/lib/user_store.py
+++ b/modules/installed/lib/user_store.py
@@ -12,10 +12,42 @@ class UserStore(UserStoreModule, sqlite_db):
self.db_file = cfg.user_db
sqlite_db.__init__(self, self.db_file, autocommit=True)
self.__enter__()
+
def close(self):
- self.__exit__()
- def expert(self):
- return False
+ self.__exit__(None,None,None)
+
+ def expert(self, username=None):
+ groups = self.attr(username,"groups")
+ if not groups:
+ return False
+ return 'expert' in groups
+
+ def attr(self, username=None, field=None):
+ return self.get(username)[field]
+
+ def get(self,username=None):
+ return User(sqlite_db.get(self,username))
+
+ def exists(self, username=None):
+ try:
+ user = self.get(username)
+ if not user:
+ return False
+ elif user["username"]=='':
+ return False
+ return True
+ except TypeError:
+ return False
+
+ def remove(self,username=None):
+ self.__delitem__(username)
+ self.commit()
+
+ def get_all(self):
+ return self.items()
+
+ def set(self,username=None,user=None):
+ sqlite_db.__setitem__(self,username, user)
class UserStoreOld():
#class UserStore(UserStoreModule):
diff --git a/modules/installed/system/users.py b/modules/installed/system/users.py
index 81e747b2f..63d9c7631 100644
--- a/modules/installed/system/users.py
+++ b/modules/installed/system/users.py
@@ -47,23 +47,25 @@ class add(FormPlugin, PagePlugin):
return form.render()
def process_form(self, username=None, name=None, email=None, md5_password=None, **kwargs):
- msg = ''
+ msg = Message()
- if not username: msg = add_message(msg, _("Must specify a username!"))
- if not md5_password: msg = add_message(msg, _("Must specify a password!"))
+ if not username: msg.add = _("Must specify a username!")
+ if not md5_password: msg.add = _("Must specify a password!")
- if username in cfg.users:
- msg = add_message(msg, _("User already exists!"))
+ if username in cfg.users.keys():
+ msg.add = _("User already exists!")
else:
try:
- cfg.users[username]= User(dict={'username':username, 'name':name, 'email':email, 'password':md5_password})
+ di = {'username':username, 'name':name, 'email':email, 'passphrase':md5_password}
+ new_user = User(dict=di)
+ cfg.users.set(username,new_user)
except:
- msg = add_message(msg, _("Error storing user!"))
+ msg.add = _("Error storing user!")
if not msg:
- msg = add_message(msg, "%s saved." % username)
-
- main = self.make_form(username, name, email, message=msg)
+ msg.add = _("%s saved." % username)
+ cfg.log(msg.text)
+ #main = self.make_form(username, name, email, msg=msg.text)
return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right)
class edit(FormPlugin, PagePlugin):
@@ -114,7 +116,7 @@ class edit(FormPlugin, PagePlugin):
msg.add(_("User %s does not exist." % username))
else:
msg.add = _("Must specify at least one valid, existing user.")
- main = self.make_form(msg=msg.text)
+ #main = self.make_form(msg=msg.text)
return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right)
sidebar_right = ''
diff --git a/test.sh b/test.sh
new file mode 100755
index 000000000..4b6f701d4
--- /dev/null
+++ b/test.sh
@@ -0,0 +1,9 @@
+#! /bin/sh
+
+PYTHONPATH=build/exmachina:$PYTHONPATH
+PYTHONPATH=modules/installed/lib:$PYTHONPATH
+PYTHONPATH=vendor:$PYTHONPATH
+
+export PYTHONPATH
+
+python tests/test_user_store.py
diff --git a/tests/test_user_store.py b/tests/test_user_store.py
new file mode 100644
index 000000000..e6a3e8433
--- /dev/null
+++ b/tests/test_user_store.py
@@ -0,0 +1,86 @@
+#! /usr/bin/env python
+# -*- mode: python; mode: auto-fill; fill-column: 80 -*-
+
+import user_store
+from logger import Logger
+import cfg
+import unittest
+import cherrypy
+import plugin_mount
+import os
+from model import User
+cfg.log = Logger()
+
+cherrypy.log.access_file = None
+
+class UserStore(unittest.TestCase):
+ """Test each function of user_store to confirm they work as expected"""
+
+ def setUp(self):
+ cfg.user_db = os.path.join(cfg.file_root, "tests/testdata/users");
+ self.userstore = plugin_mount.UserStoreModule.get_plugins()[0]
+
+ def tearDown(self):
+ for user in self.userstore.get_all():
+ self.userstore.remove(user[0])
+ self.userstore.close()
+
+ def test_user_does_not_exist(self):
+ self.assertEqual(self.userstore.exists("notausername"),False)
+
+ def test_user_does_exist(self):
+ self.add_user("isausername", False)
+ self.assertEqual(self.userstore.exists("isausername"),True)
+
+ def test_add_user(self):
+ self.assertEqual(len(self.userstore.items()),0)
+ self.add_user("test_user", False)
+ self.assertEqual(len(self.userstore.items()),1)
+
+ def test_user_is_in_expert_group(self):
+ self.add_user("test_user", True)
+ self.assertEqual(self.userstore.expert("test_user"),True)
+
+ def test_user_is_not_in_expert_group(self):
+ self.add_user("test_user", False)
+ self.assertEqual(self.userstore.expert("test_user"),False)
+
+ def test_user_removal(self):
+ self.assertEqual(len(self.userstore.items()),0)
+ self.add_user("test_user", False)
+ self.assertEqual(len(self.userstore.items()),1)
+ self.userstore.remove("test_user")
+ self.assertEqual(len(self.userstore.items()),0)
+
+ def test_get_user_email_attribute(self):
+ self.add_user("test_user", False,"test@home")
+ self.assertEqual(self.userstore.attr("test_user","email"),"test@home")
+
+ def test_get_user(self):
+ test_user = self.add_user("test_user", False)
+ self.assertEqual(self.userstore.get("test_user"),test_user)
+
+ def test_get_all_users(self):
+ self.add_user("test_user1", False)
+ self.add_user("test_user2", False)
+ self.assertEqual(len(self.userstore.get_all()),2)
+
+ def add_user(self, test_username, add_to_expert_group, email=''):
+ test_user = self.create_user(test_username, email)
+ if add_to_expert_group:
+ test_user = self.add_user_to_expert_group(test_user)
+ self.userstore.set(test_username,test_user)
+ return test_user
+
+ def create_user(self, username, email=''):
+ test_user = User()
+ test_user["username"] = username
+ test_user["email"] = email
+ return test_user
+
+ def add_user_to_expert_group(self, user):
+ user["groups"] = ["expert"]
+ return user
+
+if __name__ == "__main__":
+ unittest.main()
\ No newline at end of file
diff --git a/tests/testdata/users.sqlite3 b/tests/testdata/users.sqlite3
new file mode 100644
index 0000000000000000000000000000000000000000..03782a7d7f4282049ab931c01f0df26b5534545f
GIT binary patch
literal 3072
zcmWFz^vNtqRY=P(%1ta$FlJz3U}R))P*7lCU`%FUU|@
zBSl{3KMYLF-3-j#%oouVjB-aqU^E0EA>hKuCN3_{m|2pTl#^Oql3xTNn4E)L9Yb6d
zLL8lZTou6L3L4p|l?o-P6(tI#d6|W!sX7W}i8){aO-&{?aa(alhRnQ_)QaN59ANH_
zPb@9T2lL~>+T#tu6ew@A@Ph1S0cP|*W(kNhM#<3-7!3hn2)v+qD70a*p$Ll@4TZQs
eW`pwoC+0^qv}V-k(GVC7fq@DEIW&v;5PSg0W;EIW
literal 0
HcmV?d00001
From 76d67d67f17513e07dab0b4c4579d4e12b599d5d Mon Sep 17 00:00:00 2001
From: Tom Galloway
Date: Tue, 22 Jan 2013 20:22:19 +0000
Subject: [PATCH 24/24] Add User & Delete User now works correctly.
---
modules/installed/system/users.py | 27 ++++++++++++++-------------
1 file changed, 14 insertions(+), 13 deletions(-)
diff --git a/modules/installed/system/users.py b/modules/installed/system/users.py
index 63d9c7631..4277f1050 100644
--- a/modules/installed/system/users.py
+++ b/modules/installed/system/users.py
@@ -5,7 +5,7 @@ from plugin_mount import PagePlugin, FormPlugin
import cfg
from forms import Form
from util import *
-from pprint import pprint
+from model import User
class users(PagePlugin):
order = 20 # order of running init in PagePlugins
@@ -41,7 +41,7 @@ class add(FormPlugin, PagePlugin):
form.text_input(_("Username"), name="username", value=username)
form.text_input(_("Full name"), name="name", value=name)
form.text_input(_("Email"), name="email", value=email)
- form.text_input(_("Password"), name="password")
+ form.text_input(_("Password"), name="password", type="password")
form.text_input(name="md5_password", type="hidden")
form.submit(label=_("Create User"), name="create")
return form.render()
@@ -52,12 +52,12 @@ class add(FormPlugin, PagePlugin):
if not username: msg.add = _("Must specify a username!")
if not md5_password: msg.add = _("Must specify a password!")
- if username in cfg.users.keys():
+ if username in cfg.users.get_all():
msg.add = _("User already exists!")
else:
try:
di = {'username':username, 'name':name, 'email':email, 'passphrase':md5_password}
- new_user = User(dict=di)
+ new_user = User(di)
cfg.users.set(username,new_user)
except:
msg.add = _("Error storing user!")
@@ -65,7 +65,7 @@ class add(FormPlugin, PagePlugin):
if not msg:
msg.add = _("%s saved." % username)
cfg.log(msg.text)
- #main = self.make_form(username, name, email, msg=msg.text)
+ main = self.main(username, name, email, msg=msg.text)
return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right)
class edit(FormPlugin, PagePlugin):
@@ -80,14 +80,15 @@ class edit(FormPlugin, PagePlugin):
system.Deleting users is permanent!
""" % (cfg.product_name, cfg.box_name))
def main(self, msg=''):
- users = cfg.users
+ users = cfg.users.get_all()
add_form = Form(title=_("Edit or Delete User"), action="/sys/users/edit", message=msg)
add_form.html('Delete
')
- for uname in sorted(users.keys()):
+ for uname in users:
+ user = User(uname[1])
add_form.html(' %s ' %
- add_form.get_checkbox(name=uname) +
+ add_form.get_checkbox(name=user['username']) +
'%s (%s)
' %
- (uname, users[uname]['name'], uname))
+ (user['username'], user['name'], user['username']))
add_form.submit(label=_("Delete User"), name="delete")
return add_form.render()
@@ -98,12 +99,12 @@ class edit(FormPlugin, PagePlugin):
cfg.log.info("%s asked to delete %s" % (cherrypy.session.get(cfg.session_key), usernames))
if usernames:
for username in usernames:
- if username in cfg.users:
+ if cfg.users.exists(username):
try:
- del cfg.users[username]
+ cfg.users.remove(username)
msg.add(_("Deleted user %s." % username))
except IOError, e:
- if 'username' in cfg.users:
+ if cfg.users.exists(username):
m = _("Error on deletion, user %s not fully deleted: %s" % (username, e))
cfg.log.error(m)
msg.add(m)
@@ -116,7 +117,7 @@ class edit(FormPlugin, PagePlugin):
msg.add(_("User %s does not exist." % username))
else:
msg.add = _("Must specify at least one valid, existing user.")
- #main = self.make_form(msg=msg.text)
+ main = self.main(msg=msg.text)
return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right)
sidebar_right = ''