diff --git a/.gitignore b/.gitignore
index 2088e658a..d5be6856d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,9 @@ templates/*.py
TODO
\#*
.#*
+cfg.py
+cherrypy.config
+data/users.sqlite3
+predepend
+build/
+*.pid
\ No newline at end of file
diff --git a/INSTALL b/INSTALL
index 9b2705496..e5761a149 100644
--- a/INSTALL
+++ b/INSTALL
@@ -2,20 +2,27 @@
## 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
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
+
+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
@@ -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.
@@ -42,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/Makefile b/Makefile
index 3afdbcfb0..72d916965 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 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
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_DIR)
+ rm -f predepend
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/cfg.sample.py b/cfg.sample.py
index 799984a86..8521ca653 100644
--- a/cfg.sample.py
+++ b/cfg.sample.py
@@ -8,10 +8,12 @@ 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 ##
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/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:
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/config.py b/modules/installed/system/config.py
index c8b5190a8..b671fcc12 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
@@ -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):
@@ -41,20 +42,21 @@ 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)
+ 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
+ if platform.linux_distribution()[0]=="Ubuntu" :
+ cfg.exmachina.service.start("hostname")
else:
- cfg.log.debug("Hostname restart returned %s" % retcode)
+ 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 +74,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 +86,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 +127,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
@@ -128,8 +136,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."
diff --git a/modules/installed/system/users.py b/modules/installed/system/users.py
index c4ac97771..4277f1050 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 model import User
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 = ''
@@ -40,33 +41,35 @@ 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()
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.get_all():
+ 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(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)
- return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right)
+ msg.add = _("%s saved." % username)
+ cfg.log(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):
- url = ["/sys/users"]
+ url = ["/sys/users/edit"]
order = 35
sidebar_left = ''
@@ -77,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.keys() + users = cfg.users.get_all() add_form = Form(title=_("Edit or Delete User"), action="/sys/users/edit", message=msg) add_form.html('Delete