Merge pull request #5 from NickDaly/master

Stuff James Will Like
This commit is contained in:
jvasile 2013-02-05 10:55:19 -08:00
commit d0e0285892
18 changed files with 365 additions and 69 deletions

6
.gitignore vendored
View File

@ -20,3 +20,9 @@ templates/*.py
TODO TODO
\#* \#*
.#* .#*
cfg.py
cherrypy.config
data/users.sqlite3
predepend
build/
*.pid

27
INSTALL
View File

@ -2,20 +2,27 @@
## Installing Plinth ## Installing Plinth
Install the python-cheetah package and pandoc: Install the python-cheetah package, pandoc, python-augeas, and
bjsonrpc:
apt-get install python-cheetah pandoc
apt-get install python-cheetah pandoc python-augeas python-bjsonrpc
Install the python-simplejson Install the python-simplejson
apt-get install python-simplejson apt-get install python-simplejson
Unzip and untar the source into a directory. Change to the directory Unzip and untar the source into a directory. Change to the directory
containing the program. Do `make` and then run `./plinth.py` and containing the program. Run:
point your web browser at `localhost:8000`. The default username is
"admin" and the default password is "secret". $ make
To start Plinth, run:
$ ./start.sh
and point your web browser at `localhost:8000`. The default username is "admin"
and the default password is "secret".
## Dependencies ## 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. * *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. The documentation has some dependencies too.
* *Markdown* is used to format and style docs. * *Markdown* is used to format and style docs.
@ -42,5 +53,3 @@ The documentation has some dependencies too.
Documentation has been collected into a pdf that can be built using Documentation has been collected into a pdf that can be built using
`make doc`. It also gets built into smaller files and other formats, `make doc`. It also gets built into smaller files and other formats,
including one suitable for install as a man page. including one suitable for install as a man page.
<a name="installing_systemwide" />

View File

@ -1,5 +1,6 @@
#SHELL := /bin/bash #SHELL := /bin/bash
MAKE=make MAKE=make
BUILD_DIR = build
#TODO: add install target #TODO: add install target
@ -9,9 +10,22 @@ COMPRESSED_CSS := $(patsubst %.css,%.tiny.css,$(CSS))
PWD=`pwd` PWD=`pwd`
## Catch-all tagets ## 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 all: default
build:
mkdir -p $(BUILD_DIR)
predepend:
sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson pandoc python-cheetah"
touch predepend
$(BUILD_DIR)/exmachina: build
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
dbs: data/users.sqlite3 dbs: data/users.sqlite3
data/users.sqlite3: data/users.sqlite3.distrib data/users.sqlite3: data/users.sqlite3.distrib
@ -69,3 +83,5 @@ clean:
@find . -name "*.bak" -exec rm {} \; @find . -name "*.bak" -exec rm {} \;
@$(MAKE) -s -C doc clean @$(MAKE) -s -C doc clean
@$(MAKE) -s -C templates clean @$(MAKE) -s -C templates clean
rm -rf $(BUILD_DIR)
rm -f predepend

12
NOTES
View File

@ -2,6 +2,18 @@
% %
% February 2012 % 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 # Edits by seandiggity
## 2012-02-27 new theme based upon bootstrap ## 2012-02-27 new theme based upon bootstrap

3
README
View File

@ -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 interface is not a tool for super admins facing complex scenarios. It
is for home users to do a wide variety of basic tasks. 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.

View File

@ -8,10 +8,12 @@ user_db = os.path.join(data_dir, "users")
status_log_file = os.path.join(data_dir, "status.log") status_log_file = os.path.join(data_dir, "status.log")
access_log_file = os.path.join(data_dir, "access.log") access_log_file = os.path.join(data_dir, "access.log")
users_dir = os.path.join(data_dir, "users") users_dir = os.path.join(data_dir, "users")
pidfile = os.path.join(data_dir, "pidfile.pid")
product_name = "Plinth" product_name = "Plinth"
box_name = "FreedomBox" box_name = "FreedomBox"
host = "127.0.0.1"
port = 8000 port = 8000
## Do not edit below this line ## ## Do not edit below this line ##

2
fabfile.py vendored
View File

@ -138,7 +138,7 @@ def apache():
@task @task
def deps(): def deps():
"Basic plinth dependencies" "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 @task
def update(): def update():

42
issues/links.org Normal file
View File

@ -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:

View File

@ -1,14 +1,15 @@
class User(dict): 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 is a md5 hash of the password), groups, and an email address. They can be
blank or None, but the keys must exist. """ blank or None, but the keys must exist. """
def __init__(self, dict=None): def __init__(self, dict=None):
for key in ['username', 'name', 'password', 'email']: for key in ['username', 'name', 'passphrase', 'email']:
self[key] = '' self[key] = ''
for key in ['groups']: for key in ['groups']:
self[key] = [] self[key] = []
for key in dict: if dict:
self[key] = dict[key] for key in dict:
self[key] = dict[key]
def __getattr__(self, attr): def __getattr__(self, attr):
return None return None

View File

@ -12,10 +12,42 @@ class UserStore(UserStoreModule, sqlite_db):
self.db_file = cfg.user_db self.db_file = cfg.user_db
sqlite_db.__init__(self, self.db_file, autocommit=True) sqlite_db.__init__(self, self.db_file, autocommit=True)
self.__enter__() self.__enter__()
def close(self): def close(self):
self.__exit__() self.__exit__(None,None,None)
def expert(self):
return False 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 UserStoreOld():
#class UserStore(UserStoreModule): #class UserStore(UserStoreModule):

View File

@ -1,4 +1,4 @@
import os, shutil, subprocess import os, subprocess
from socket import gethostname from socket import gethostname
import cherrypy import cherrypy
import simplejson as json import simplejson as json
@ -10,6 +10,7 @@ import cfg
from forms import Form from forms import Form
from model import User from model import User
from util import * from util import *
import platform
class Config(PagePlugin): class Config(PagePlugin):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -41,20 +42,21 @@ def valid_hostname(name):
def set_hostname(hostname): def set_hostname(hostname):
"Sets machine hostname to hostname" "Sets machine hostname to hostname"
cfg.log.info("Writing '%s' to /etc/hostname" % hostname) cfg.log.info("Writing '%s' to /etc/hostname with exmachina" % hostname)
unslurp("/etc/hostname", hostname+"\n")
try: try:
retcode = subprocess.call("/etc/init.d/hostname.sh start", shell=True) cfg.exmachina.augeas.set("/files/etc/hostname/*", hostname)
if retcode < 0: cfg.exmachina.augeas.save()
cfg.log.error("Hostname restart terminated by signal: return code is %s" % retcode) # don't persist/cache change unless it was saved successfuly
sys_store = filedict_con(cfg.store_file, 'sys')
sys_store['hostname'] = hostname
if platform.linux_distribution()[0]=="Ubuntu" :
cfg.exmachina.service.start("hostname")
else: else:
cfg.log.debug("Hostname restart returned %s" % retcode) cfg.exmachina.initd.restart("hostname.sh") # is hostname.sh debian-only?
except OSError, e: except OSError, e:
raise cherrypy.HTTPError(500, "Hostname restart failed: %s" % 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): class general(FormPlugin, PagePlugin):
url = ["/sys/config"] url = ["/sys/config"]
order = 30 order = 30
@ -72,8 +74,11 @@ class general(FormPlugin, PagePlugin):
def main(self, message='', **kwargs): def main(self, message='', **kwargs):
sys_store = filedict_con(cfg.store_file, 'sys') 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()", defaults = {'time_zone': "slurp('/etc/timezone').rstrip()",
'hostname': "gethostname()", 'hostname': "hostname",
} }
for k,c in defaults.items(): for k,c in defaults.items():
if not k in kwargs: if not k in kwargs:
@ -81,6 +86,8 @@ class general(FormPlugin, PagePlugin):
kwargs[k] = sys_store[k] kwargs[k] = sys_store[k]
except KeyError: except KeyError:
exec("if not '%(k)s' in kwargs: sys_store['%(k)s'] = kwargs['%(k)s'] = %(c)s" % {'k':k, 'c':c}) 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 ## Get the list of supported timezones and the index in that list of the current one
module_file = __file__ module_file = __file__
@ -120,7 +127,8 @@ class general(FormPlugin, PagePlugin):
old_val = sys_store['hostname'] old_val = sys_store['hostname']
try: try:
set_hostname(hostname) set_hostname(hostname)
except: except Exception, e:
cfg.log.error(e)
cfg.log.info("Trying to restore old hostname value.") cfg.log.info("Trying to restore old hostname value.")
set_hostname(old_val) set_hostname(old_val)
raise raise
@ -128,8 +136,8 @@ class general(FormPlugin, PagePlugin):
message += msg message += msg
if time_zone != sys_store['time_zone']: if time_zone != sys_store['time_zone']:
src = os.path.join("/usr/share/zoneinfo", time_zone) src = os.path.join("/usr/share/zoneinfo", time_zone)
cfg.log.info("Copying %s to /etc/localtime" % src) cfg.log.info("Setting timezone to %s" % time_zone)
shutil.copy(src, "/etc/localtime") cfg.exmachina.misc.set_timezone(time_zone)
sys_store['time_zone'] = time_zone sys_store['time_zone'] = time_zone
return message or "Settings updated." return message or "Settings updated."

View File

@ -5,22 +5,23 @@ from plugin_mount import PagePlugin, FormPlugin
import cfg import cfg
from forms import Form from forms import Form
from util import * from util import *
from model import User
class users(PagePlugin): class users(PagePlugin):
order = 20 # order of running init in PagePlugins order = 20 # order of running init in PagePlugins
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs) PagePlugin.__init__(self, *args, **kwargs)
self.register_page("sys.users") self.register_page("sys.users")
self.register_page("sys.users.add")
self.register_page("sys.users.edit")
@cherrypy.expose @cherrypy.expose
@require() @require()
def index(self): def index(self):
parts = self.forms('/sys/config') return self.fill_template(title="Manage Users and Groups", sidebar_right="""<strong><a href="/sys/users/add">Add User</a></strong><br/><strong><a href="/sys/users/edit">Edit Users</a></strong>""")
parts['title']=_("Manage Users and Groups")
return self.fill_template(**parts)
class add(FormPlugin, PagePlugin): class add(FormPlugin, PagePlugin):
url = ["/sys/users"] url = ["/sys/users/add"]
order = 30 order = 30
sidebar_left = '' sidebar_left = ''
@ -40,33 +41,35 @@ class add(FormPlugin, PagePlugin):
form.text_input(_("Username"), name="username", value=username) form.text_input(_("Username"), name="username", value=username)
form.text_input(_("Full name"), name="name", value=name) form.text_input(_("Full name"), name="name", value=name)
form.text_input(_("Email"), name="email", value=email) 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.text_input(name="md5_password", type="hidden")
form.submit(label=_("Create User"), name="create") form.submit(label=_("Create User"), name="create")
return form.render() return form.render()
def process_form(self, username=None, name=None, email=None, md5_password=None, **kwargs): 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 username: msg.add = _("Must specify a username!")
if not md5_password: msg = add_message(msg, _("Must specify a password!")) if not md5_password: msg.add = _("Must specify a password!")
if username in cfg.users: if username in cfg.users.get_all():
msg = add_message(msg, _("User already exists!")) msg.add = _("User already exists!")
else: else:
try: 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(di)
cfg.users.set(username,new_user)
except: except:
msg = add_message(msg, _("Error storing user!")) msg.add = _("Error storing user!")
if not msg: if not msg:
msg = add_message(msg, "%s saved." % username) msg.add = _("%s saved." % username)
cfg.log(msg.text)
main = self.make_form(username, name, email, message=msg) main = self.main(username, name, email, 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)
class edit(FormPlugin, PagePlugin): class edit(FormPlugin, PagePlugin):
url = ["/sys/users"] url = ["/sys/users/edit"]
order = 35 order = 35
sidebar_left = '' sidebar_left = ''
@ -77,14 +80,15 @@ class edit(FormPlugin, PagePlugin):
system.</p><p>Deleting users is permanent!</p>""" % (cfg.product_name, cfg.box_name)) system.</p><p>Deleting users is permanent!</p>""" % (cfg.product_name, cfg.box_name))
def main(self, msg=''): def main(self, msg=''):
users = cfg.users.keys() users = cfg.users.get_all()
add_form = Form(title=_("Edit or Delete User"), action="/sys/users/edit", message=msg) add_form = Form(title=_("Edit or Delete User"), action="/sys/users/edit", message=msg)
add_form.html('<span class="indent"><strong>Delete</strong><br /></span>') add_form.html('<span class="indent"><strong>Delete</strong><br /></span>')
for uname in sorted(users.keys()): for uname in users:
user = User(uname[1])
add_form.html('<span class="indent">&nbsp;&nbsp;%s&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;' % add_form.html('<span class="indent">&nbsp;&nbsp;%s&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;' %
add_form.get_checkbox(name=uname) + add_form.get_checkbox(name=user['username']) +
'<a href="/sys/users/edit?username=%s">%s (%s)</a><br /></span>' % '<a href="/sys/users/edit?username=%s">%s (%s)</a><br /></span>' %
(uname, users[uname]['name'], uname)) (user['username'], user['name'], user['username']))
add_form.submit(label=_("Delete User"), name="delete") add_form.submit(label=_("Delete User"), name="delete")
return add_form.render() return add_form.render()
@ -95,12 +99,12 @@ class edit(FormPlugin, PagePlugin):
cfg.log.info("%s asked to delete %s" % (cherrypy.session.get(cfg.session_key), usernames)) cfg.log.info("%s asked to delete %s" % (cherrypy.session.get(cfg.session_key), usernames))
if usernames: if usernames:
for username in usernames: for username in usernames:
if username in cfg.users: if cfg.users.exists(username):
try: try:
del cfg.users[username] cfg.users.remove(username)
msg.add(_("Deleted user %s." % username)) msg.add(_("Deleted user %s." % username))
except IOError, e: 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)) m = _("Error on deletion, user %s not fully deleted: %s" % (username, e))
cfg.log.error(m) cfg.log.error(m)
msg.add(m) msg.add(m)
@ -113,8 +117,8 @@ class edit(FormPlugin, PagePlugin):
msg.add(_("User %s does not exist." % username)) msg.add(_("User %s does not exist." % username))
else: else:
msg.add = _("Must specify at least one valid, existing user.") 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="", 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 = '' sidebar_right = ''
u = cfg.users[kwargs['username']] u = cfg.users[kwargs['username']]
@ -125,4 +129,4 @@ class edit(FormPlugin, PagePlugin):
main = _("""<strong>Edit User '%s'</strong>""" % u['username']) main = _("""<strong>Edit User '%s'</strong>""" % u['username'])
sidebar_right = '' 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)

View File

@ -17,6 +17,9 @@ from util import *
from logger import Logger from logger import Logger
#from modules.auth import AuthController, require, member_of, name_is #from modules.auth import AuthController, require, member_of, name_is
from exmachina import ExMachinaClient
import socket
__version__ = "0.2.14" __version__ = "0.2.14"
__author__ = "James Vasile" __author__ = "James Vasile"
__copyright__ = "Copyright 2011, James Vasile" __copyright__ = "Copyright 2011, James Vasile"
@ -51,7 +54,7 @@ class Root(plugin_mount.PagePlugin):
raise cherrypy.InternalRedirect('/router') raise cherrypy.InternalRedirect('/router')
else: else:
raise cherrypy.InternalRedirect('/help/about') raise cherrypy.InternalRedirect('/help/about')
def load_modules(): def load_modules():
"""Import all the symlinked .py files in the modules directory and """Import all the symlinked .py files in the modules directory and
all the .py files in directories linked in the modules directory all the .py files in directories linked in the modules directory
@ -71,9 +74,20 @@ def parse_arguments():
parser = argparse.ArgumentParser(description='Plinth web interface for the FreedomBox.') parser = argparse.ArgumentParser(description='Plinth web interface for the FreedomBox.')
parser.add_argument('--pidfile', default="", parser.add_argument('--pidfile', default="",
help='specify a file in which the server may write its pid') 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() args=parser.parse_args()
if args.pidfile: if args.pidfile:
cfg.pidfile = args.pidfile cfg.pidfile = args.pidfile
else:
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
cfg.exmachina_secret_key = sys.stdin.readline().strip()
else:
cfg.exmachina_secret_key = None
def setup(): def setup():
parse_arguments() parse_arguments()
@ -85,6 +99,13 @@ def setup():
except AttributeError: except AttributeError:
pass 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) os.chdir(cfg.file_root)
cherrypy.config.update({'error_page.404': error_page_404}) cherrypy.config.update({'error_page.404': error_page_404})
cherrypy.config.update({'error_page.500': error_page_500}) cherrypy.config.update({'error_page.500': error_page_500})
@ -103,7 +124,7 @@ def setup():
server.subscribe() server.subscribe()
# Configure default server # 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.socket_port': cfg.port,
'server.thread_pool':10, 'server.thread_pool':10,
'tools.staticdir.root': cfg.file_root, 'tools.staticdir.root': cfg.file_root,
@ -112,7 +133,7 @@ def setup():
'tools.sessions.storage_type':"file", 'tools.sessions.storage_type':"file",
'tools.sessions.timeout':90, 'tools.sessions.timeout':90,
'tools.sessions.storage_path':"%s/data/cherrypy_sessions" % cfg.file_root, 'tools.sessions.storage_path':"%s/data/cherrypy_sessions" % cfg.file_root,
}) })
config = {'/': {'tools.staticdir.root': '%s/static' % cfg.file_root, config = {'/': {'tools.staticdir.root': '%s/static' % cfg.file_root,
@ -124,11 +145,11 @@ def setup():
} }
cherrypy.tree.mount(cfg.html_root, '/', config=config) cherrypy.tree.mount(cfg.html_root, '/', config=config)
cherrypy.engine.signal_handler.subscribe() cherrypy.engine.signal_handler.subscribe()
def main(): def main():
setup() setup()
print "localhost %d" % cfg.port print "%s %d" % (cfg.host, cfg.port)
cherrypy.engine.start() cherrypy.engine.start()
cherrypy.engine.block() cherrypy.engine.block()

View File

@ -12,23 +12,53 @@
# This file is /etc/init.d/plinth # This file is /etc/init.d/plinth
DAEMON=/usr/local/bin/plinth.py DAEMON=/usr/local/bin/plinth.py
EXMACHINA_DAEMON=/usr/local/bin/exmachina.py
PID_FILE=/var/run/plinth.pid 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 (){ start_plinth (){
if [ -f $PID_FILE ]; then 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 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 --group=$PLINTH_GROUP || rm $PID_FILE
sleep 0.5
echo $SHAREDKEY | sudo -u $PLINTH_USER -g $PLINTH_GROUP $DAEMON --pidfile=$PID_FILE
fi fi
} }
stop_plinth () { stop_plinth () {
if [ -f $PID_FILE ]; then if [ -f $PID_FILE ]; then
kill -15 `cat $PID_FILE` kill -15 `cat $PID_FILE` || true
rm -rf $PID_FILE rm -rf $PID_FILE
echo "killed plinth"
else else
echo "No pid file at $PID_FILE suggests plinth is not running." echo "No pid file at $PID_FILE suggests plinth is not running."
fi 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 test -x $DAEMON || exit 0
@ -45,5 +75,12 @@ case "$1" in
$0 stop $0 stop
$0 start $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 esac

10
start.sh Executable file
View File

@ -0,0 +1,10 @@
#! /bin/sh
PYTHONPATH=build/exmachina:$PYTHONPATH
export PYTHONPATH
sudo killall exmachina.py
sudo build/exmachina/exmachina.py -v &
python plinth.py
sudo killall exmachina.py

9
test.sh Executable file
View File

@ -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

86
tests/test_user_store.py Normal file
View File

@ -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()

BIN
tests/testdata/users.sqlite3 vendored Normal file

Binary file not shown.