Commit f55c7a48 authored by Nick Daly's avatar Nick Daly

Merged with James's upstream.

Hope I did it right.  If I screwed up, withsqlite is borked.
parent 157226f1
......@@ -20,9 +20,8 @@ templates/*.py
TODO
\#*
.#*
cfg.py
cherrypy.config
*~
data/users.sqlite3
predepend
build/
*.pid
\ No newline at end of file
*.pid
[submodule "vendor/withsqlite"]
path = vendor/withsqlite
url = git://github.com/jvasile/withsqlite.git
[submodule "vendor/exmachina"]
path = vendor/exmachina
url = git://github.com/jvasile/exmachina.git
# License to Copy the Plinth front end to the Freedom Plug
# License to Copy Plinth
Plinth is Copyright 2011 James Vasile (<james@hackervisions.org>). It
Plinth is Copyright 2011-2013 James Vasile (<james@hackervisions.org>). It
is distributed under the GNU Affero General Public License, Version 3
or later. A copy of GPLv3 is available [from the Free Software
Foundation](http://www.gnu.org/licenses/gpl.html).
or later. A copy of AGPLv3 is available [from the Free Software
Foundation](http://www.gnu.org/licenses/agpl.html).
In addition, the documentation to this software is distributed under a
Creative Commons Attribution-ShareAlike 3.0 Unported, Version 3
......
......@@ -5,12 +5,7 @@
Install the python-cheetah package, pandoc, python-augeas, and
bjsonrpc:
apt-get install python-cheetah pandoc python-augeas python-bjsonrpc
Install the python-simplejson
apt-get install python-simplejson
apt-get install python-cheetah pandoc python-augeas python-bjsonrpc python-cherrypy3 python-simplejson
Unzip and untar the source into a directory. Change to the directory
containing the program. Run:
......
#SHELL := /bin/bash
MAKE=make
BUILD_DIR = vendor
#TODO: add install target
CSS=$(wildcard *.css)
CSS=$(subst .tiny,,$(shell find themes -type f -name '*.css'))
......@@ -10,21 +6,43 @@ 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 config dirs template css docs dbs
all: default
build:
mkdir -p $(BUILD_DIR)
predepend:
su -c "apt-get -y install augeas-tools python-bjsonrpc python-augeas python-simplejson pandoc python-cheetah"
sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson pandoc python-cheetah python-cherrypy3"
git submodule init
git submodule update
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
install: default
mkdir -p $(DESTDIR)/etc/init.d $(DESTDIR)/etc/plinth
cp plinth.sample.fhs.config $(DESTDIR)/etc/plinth/plinth.config
mkdir -p $(DESTDIR)/usr/lib/python2.7/plinth $(DESTDIR)/usr/bin \
$(DESTDIR)/usr/share/doc/plinth $(DESTDIR)/usr/share/man/man1
rsync -L doc/* $(DESTDIR)/usr/share/doc/plinth/
gzip $(DESTDIR)/usr/share/doc/plinth/plinth.1
mv $(DESTDIR)/usr/share/doc/plinth/plinth.1.gz $(DESTDIR)/usr/share/man/man1
rsync -rl *.py modules templates vendor themes static \
--exclude static/doc --exclude ".git/*" --exclude "*.pyc" \
$(DESTDIR)/usr/lib/python2.7/plinth
mkdir -p $(DESTDIR)/usr/lib/python2.7/plinth/static/doc
cp doc/*.html $(DESTDIR)/usr/lib/python2.7/plinth/static/doc
rm -f $(DESTDIR)/usr/lib/python2.7/plinth/plinth.config
ln -s ../../../../etc/plinth/plinth.config $(DESTDIR)/usr/lib/python2.7/plinth/plinth.config
cp share/init.d/plinth $(DESTDIR)/etc/init.d
rm -f $(DESTDIR)/usr/bin/plinth
ln -s ../lib/python2.7/plinth/plinth.py $(DESTDIR)/usr/bin/plinth
mkdir -p $(DESTDIR)/var/lib/plinth/cherrypy_sessions $(DESTDIR)/var/log/plinth $(DESTDIR)/var/run
cp -r data/* $(DESTDIR)/var/lib/plinth
rm -f $(DESTDIR)/var/lib/plinth/users/sqlite3.distrib
uninstall:
rm -rf $(DESTDIR)/usr/lib/python2.7/plinth $(DESTDIR)/usr/share/plinth/ \
$(DESTDIR)/etc/plinth $(DESTDIR)/var/lib/plinth $(DESTDIR)/usr/share/doc/plinth/ \
$(DESTDIR)/var/log/plinth
rm -f $(DESTDIR)/usr/bin/plinth $(DESTDIR)/etc/init.d/plinth \
$(DESTDIR)/usr/share/man/man1/plinth.1.gz $(DESTDIR)/var/run/plinth.pid
dbs: data/users.sqlite3
......@@ -34,29 +52,8 @@ data/users.sqlite3: data/users.sqlite3.distrib
dirs:
@mkdir -p data/cherrypy_sessions
cfg: Makefile
test -f cfg.py || cp cfg.sample.py cfg.py
cherrypy.config: Makefile
@echo [global]\\n\
server.socket_host = \'0.0.0.0\'\\n\
server.socket_port = 8000\\n\
server.thread_pool = 10\\n\
tools.staticdir.root = \"$(PWD)\"\\n\
tools.sessions.on = True\\n\
tools.auth.on = True\\n\
tools.sessions.storage_type = \"file\"\\n\
tools.sessions.timeout = 90\\n\
tools.sessions.storage_path = \"$(PWD)/data/cherrypy_sessions\"\\n\
\\n\
[/static]\\n\
tools.staticdir.on = True\\n\
tools.staticdir.dir = \"static\"\\n\
\\n\
[/favicon.ico]\\n\
tools.staticfile.on = True\\n\
tools.staticfile.filename = \"$(PWD)/static/theme/favicon.ico\"\\n\
> cherrypy.config
config: Makefile
@test -f plinth.config || cp plinth.sample.config plinth.config
%.tiny.css: %.css
@cat $< | python -c 'import re,sys;print re.sub("\s*([{};,:])\s*", "\\1", re.sub("/\*.*?\*/", "", re.sub("\s+", " ", sys.stdin.read())))' > $@
......@@ -83,5 +80,5 @@ clean:
@find . -name "*.bak" -exec rm {} \;
@$(MAKE) -s -C doc clean
@$(MAKE) -s -C templates clean
rm -rf $(BUILD_DIR)
rm -rf $(BUILDDIR) $(DESTDIR)
rm -f predepend
......@@ -36,19 +36,12 @@ to get newer, better shinier functions. The modules will
automatically integrate into the existing menu system so you can
control all of the box's parts from one central location.
The interface has a 'basic' and an 'expert' mode. In basic mode, much
of Plinth's configuration and capability are hidden. Sane defaults
are chosen whenever possible. In expert mode, you can get down into
the details and configure things the average user never thinks about.
For example, experts can turn off ntp or switch ntp servers. Basic
users should never even know those options exist.
One caveat: expert mode is not as powerful as the commandline. We can
only do so much when translating free-form configuration files into
web forms. And if you manually change your configuration files, the
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.
The interface will eventually have a 'basic' and an 'expert' mode. In
basic mode, much of Plinth's configuration and capability are hidden.
Sane defaults are chosen whenever possible. In expert mode, you can
get down into the details and configure things the average user never
thinks about. For example, experts can turn off ntp or switch ntp
servers. Basic users should never even know those options exist.
See comments in exmachina/exmachina.py for more details about the
configuration management process seperation scheme.
......
from menu import Menu
import os
from ConfigParser import SafeConfigParser
parser = SafeConfigParser(
defaults={
'root':os.path.dirname(os.path.realpath(__file__)),
'product_name':"",
'box_name':"",
'file_root':"",
'data_dir':"",
'store_file':"",
'user_db':"",
'status_log_file':"",
'access_log_file':"",
'users_dir':"",
'host':"127.0.0.1",
'pidfile':"",
'port':"",
})
parser.read(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'plinth.config'))
product_name = parser.get('Name', 'product_name')
box_name = parser.get('Name', 'box_name')
root = parser.get('Path', 'root')
file_root = parser.get('Path', 'file_root')
data_dir = parser.get('Path', 'data_dir')
store_file = parser.get('Path', 'store_file')
user_db = parser.get('Path', 'user_db')
status_log_file = parser.get('Path', 'status_log_file')
access_log_file = parser.get('Path', 'access_log_file')
users_dir = parser.get('Path', 'users_dir')
pidfile = parser.get('Path', 'pidfile')
host = parser.get('Network', 'host')
port = int(parser.get('Network', 'port'))
html_root = None
main_menu = Menu()
base_href = ""
if store_file.endswith(".sqlite3"):
store_file = os.path.splitext(store_file)[0]
from menu import Menu
import os
file_root = os.path.dirname(os.path.realpath(__file__))
data_dir = os.path.join(file_root, "data")
store_file = os.path.join(data_dir, "store.sqlite3")
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"
host = "127.0.0.1"
port = 8000
## Do not edit below this line ##
html_root = None
main_menu = Menu()
base_href = ""
vendor/CherryPy-3.2.0/py2/cherrypy
\ No newline at end of file
......@@ -42,6 +42,9 @@ There are lots of bugs. We don't have a spec or tests, so a bug is
really just any unexpected behavior. I am not easily surprised, but
there are still lots of bugs.
There's an [issue tracker](https://github.com/jvasile/Plinth/issues).
Please add things and consult it for things to tackle.
<a name="hacking_code_practices" />
## Coding Practices
......@@ -91,7 +94,9 @@ directories. That makes removal easy.
## Todo
Plinth has a number of open todo items. Please help!
Plinth has a number of open todo items. And there are items in the
[issue tracker](https://github.com/jvasile/Plinth/issues) that need
tackling. Please help!
* Implement the functions in the submenus of router.py
* Unify our logging and cherrypy's.
installed/first_boot.py
\ No newline at end of file
from urlparse import urlparse
import os, cherrypy, re
from gettext import gettext as _
from plugin_mount import PagePlugin, PluginMount, FormPlugin
from modules.auth import require
from forms import Form
import util as u
from vendor.withsqlite.withsqlite import sqlite_db
import cfg
class FirstBoot(PagePlugin):
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("firstboot") # this is the url this page will hang off of (/firstboot)
@cherrypy.expose
def index(self, *args, **kwargs):
return self.state0(*args, **kwargs)
## TODO: flesh out these tests values
def valid_box_name_p(self, name):
name = name.strip()
if re.search("\W", name):
return False
return True
def valid_box_key_p(self, key):
return True
def generate_box_key(self):
return "fake key"
@cherrypy.expose
def state0(self, message="", box_name="", box_key="", submitted=False):
"""
In this state, we do time config over HTTP, name the box and
server key selection.
All the parameters are form inputs. They get passed in when
the form is submitted. This method checks the inputs and if
they validate, uses them to take action. If they do not
validate, it displays the form to give the user a chance to
input correct values. It might display an error message (in
the message parameter).
message is an optional string that we can display to the
user. It's a good place to put error messages.
"""
## Until LDAP is in place, we'll put the box name and key in the cfg.store_file
## Must resist the sick temptation to write an LDAP interface to the sqlite file
with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as db:
db['about'] = "This table is for information about this FreedomBox"
if box_name:
if self.valid_box_name_p(box_name):
db['box_name'] = box_name
else:
message += _("Invalid box name.")
elif 'box_name' in db and db['box_name']:
box_name = db['box_name']
#TODO: set /etc/hostname to box name via ex machina
if box_key:
if self.valid_box_key_p(box_key):
db['box_key'] = box_key
else:
message += _("Invalid key!")
elif 'box_key' in db and db['box_key']:
box_key = _("We already have a key for this box on file.") #TODO: Think this through and handle more gracefully
elif submitted and not box_key:
box_key = self.generate_box_key()
db['box_key'] = box_key
if box_name and box_key and self.valid_box_name_p(box_name) and self.valid_box_key_p(box_key):
## Update state to 1 and head there
with sqlite_db(cfg.store_file, table="firstboot", autocommit=True) as db:
db['state']=1
raise cherrypy.InternalRedirect('/firstboot/state1')
main = "<p>Welcome. It looks like this FreedomBox isn't set up yet. We'll need to ask you a just few questions to get started.</p>"
form = Form(title="Welcome to Your FreedomBox!",
action="/firstboot",
name="whats_my_name",
message=message)
if not box_name:
box_name = cfg.box_name
form.html("<p>For convenience, your FreedomBox needs a name. It should be something short that doesn't contain spaces or punctuation. 'Willard' would be a good name. 'Freestyle McFreedomBox!!!' would not.</p>")
form.text_input('Name your FreedomBox', id="box_name", value=box_name)
form.html("<p>%(box_name)s uses cryptographic keys so it can prove its identity when talking to you. %(box_name)s can make a key for itself, but if one already exists (from a prior FreedomBox, for example), you can paste it below. This key should not be the same as your key because you are not your FreedomBox!</p>" % {'box_name':cfg.box_name})
form.text_box("If you want, paste your box's key here.", id="box_key", value=box_key)
form.hidden(name="submitted", value="True")
form.submit("Box it up!")
main += form.render()
return self.fill_template(template="base", title=_("First Boot!"), main=main,
sidebar_right=_("""<strong>Getting Help</strong><p>We've done our best to make your FreedomBox easy to use. If you have questions during setup, there are a few places to turn for help. TODO: add links to such help.</p>"""))
@cherrypy.expose
def state1(self, message=None):
"""
State 1 is when we have a box name and key. In this state,
our task is to provide a certificate and maybe to guide the
user through installing it. We automatically move to State 2,
which is an HTTPS connection.
TODO: HTTPS failure in State 2 should returns to state 1.
"""
main = """<p>Here's a certificate.
TODO: explain all this cert stuff to the user.</p>
<p>TODO: add instrux for installing certificate.</p>
<p>After you have installed
"""
if False:
## Update state to 2 and head there
with sqlite_db(cfg.store_file, table="firstboot", autocommit=True) as db:
db['state']=1
#TODO: switch to HTTPS
raise cherrypy.InternalRedirect('/firstboot/state1')
return self.fill_template(template="base", title=_("Installing the Certificate"), main=main,
sidebar_right=_("""<strong>Getting Help</strong><p>We've done our best to make your FreedomBox easy to use. If you have questions during setup, there are a few places to turn for help. TODO: add links to such help.</p>"""))
......@@ -83,7 +83,6 @@ class View(PagePlugin):
if page not in ['design', 'plinth', 'hacking', 'faq']:
raise cherrypy.HTTPError(404, "The path '/help/view/%s' was not found." % page)
return self.fill_template(template="err", main="<p>Sorry, as much as I would like to show you that page, I don't seem to have a page named %s!</p>" % page)
IF = open(os.path.join("doc", "%s.part.html" % page), 'r')
main = IF.read()
IF.close()
return self.fill_template(template="two_col", title=_("%s Documentation" % cfg.product_name), main=main)
with open(os.path.join("doc", "%s.part.html" % page), 'r') as IF:
main = IF.read()
return self.fill_template(title=_("%s Documentation" % cfg.product_name), main=main)
"""
The Form class is a helper class that takes parameters and method
calls and can return html for a form with appropriate hooks for css
styling. It should allow you to display a form but have the
formatting and styling added by the class. You can worry less about
how it looks while still getting consistent, decent-looking forms.
Take a look at the FirstBoot class for an example of forms in action.
Copyright 2011-2013 James Vasile
This software is released to you (yes, you) under the terms of the GNU
Affero General Public License, version 3 or later, available at
<http://www.gnu.org/licenses/agpl.html>.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
"""
class Form():
def __init__(self, action=None, cls='form', title=None, onsubmit=None, name=None, message='', method="get"):
"""Note that there appears to be a bug in cherrypy whereby
forms submitted via post don't have their fields included in
kwargs for the default index method. So we use get by
default, though it's not as neat."""
default, though it's not as neat.
TODO: file bug on this w/ CherryPy project
"""
action = self.get_form_attrib_text('action', action)
onsubmit = self.get_form_attrib_text('onsubmit', onsubmit)
......@@ -65,13 +88,15 @@ class Form():
<span>%s</span>
<input type="%s" class="inputtext" name="%s" id="%s" value="%s" size="%s"/>
</label>""" % (label, type, name, id, value, size)
def text_box(self, label='', name=None, id=None):
def hidden(self, name=None, id=None, value=''):
self.text_input(type="hidden", name=name, id=id, value=value)
def text_box(self, label='', name=None, id=None, value=""):
name, id = self.name_or_id(name, id)
self.text += """
<label>
<span>%s</span>
<textarea class="textbox" name="%s" id="%s"></textarea>
</label>""" % (label, name, id)
<textarea class="textbox" name="%s" id="%s">%s</textarea>
</label>""" % (label, name, id, value)
def submit(self, label='', name=None, id=None):
name, id = self.name_or_id(name, id)
self.text += """
......
#!/usr/bin/env python
import os, sys, argparse
#import logging
from gettext import gettext as _
import cfg
if not os.path.join(cfg.file_root, "vendor") in sys.path:
......@@ -13,27 +12,32 @@ from cherrypy.process.plugins import Daemonizer
Daemonizer(cherrypy.engine).subscribe()
import plugin_mount
from util import *
import util as u
from logger import Logger
#from modules.auth import AuthController, require, member_of, name_is
from vendor.withsqlite.withsqlite import sqlite_db
from vendor.exmachina.exmachina import ExMachinaClient
import socket
__version__ = "0.2.14"
__author__ = "James Vasile"
__copyright__ = "Copyright 2011, James Vasile"
__copyright__ = "Copyright 2011-2013, James Vasile"
__license__ = "GPLv3 or later"
__maintainer__ = "James Vasile"
__email__ = "james@jamesvasile.com"
__status__ = "Development"
def error_page(status, dynamic_msg, stock_msg):
return page_template(template="err", title=status, main="<p>%s</p>%s" % (dynamic_msg, stock_msg))
return u.page_template(template="err", title=status, main="<p>%s</p>%s" % (dynamic_msg, stock_msg))
def error_page_404(status, message, traceback, version):
return error_page(status, message, """<p>If you believe this missing page should exist, please file a
bug with either the Plinth project or the people responsible for
the module you are trying to access.</p>
return error_page(status, message, """<p>If you believe this
missing page should exist, please file a bug with either the Plinth
project (<a href="https://github.com/jvasile/plinth/issues">it has
an issue tracker</a>) or the people responsible for the module you
are trying to access.</p>
<p>Sorry for the mistake.</p>
""")
......@@ -42,13 +46,20 @@ def error_page_500(status, message, traceback, version):
cfg.log.error("500 Internal Server Error. Trackback is above.")
more="""<p>This is an internal error and not something you caused
or can fix. Please report the error on the <a
href="https://github.com/seandiggity/Plinth/issues">bug tracker</a> so
href="https://github.com/jvasile/Plinth/issues">bug tracker</a> so
we can fix it.</p>"""
return error_page(status, message, "<p>%s</p><pre>%s</pre>" % (more, "\n".join(traceback.split("\n"))))
class Root(plugin_mount.PagePlugin):
@cherrypy.expose
def index(self):
## TODO: firstboot hijacking root should probably be in the firstboot module with a hook in plinth.py
with sqlite_db(cfg.store_file, table="firstboot") as db:
if not 'state' in db:
raise cherrypy.InternalRedirect('/firstboot')
elif db['state'] < 5:
cfg.log("First Boot state = %d" % db['state'])
raise cherrypy.InternalRedirect('/firstboot/state%d' % db['state'])
if cherrypy.session.get(cfg.session_key, None):
raise cherrypy.InternalRedirect('/router')
else:
......@@ -79,8 +90,12 @@ def parse_arguments():
if args.pidfile:
cfg.pidfile = args.pidfile
else:
if not cfg.pidfile:
try:
if not cfg.pidfile:
cfg.pidfile = "plinth.pid"
except AttributeError:
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
......@@ -129,25 +144,24 @@ def setup():
# Configure default server
cherrypy.config.update(
{ 'server.socket_host': cfg.host,
'server.socket_port': cfg.port,
'server.thread_pool':10,
'tools.staticdir.root': cfg.file_root,
'tools.sessions.on':True,
'tools.auth.on':True,
'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,
'tools.proxy.on':True,},
'/static': {'tools.staticdir.on': True,
'tools.staticdir.dir':"."},
'/favicon.ico':{'tools.staticfile.on':True,
'tools.staticfile.filename':
"%s/static/theme/favicon.ico" % cfg.file_root}}
{'server.socket_host': cfg.host,
'server.socket_port': cfg.port,
'server.thread_pool':10,
'tools.staticdir.root': cfg.file_root,
'tools.sessions.on':True,