Add Tahoe-LAFS module

- Created basic plinth app which starts an introducer and a storage
  node on the FreedomBox.
- Prompt user to set a domain name before creating Tahoe-LAFS nodes.
- Support adding and removing of introducers to the storage node.
- Serve Tahoe-LAFS from a different port.
- Start all nodes and introducers at system startup.
- Add utility class YAMLFile with test cases.
This commit is contained in:
Joseph Nuthalpati 2017-03-28 16:10:55 +05:30 committed by James Valleroy
parent 1b42d3bf53
commit 5ad180fcc9
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
22 changed files with 953 additions and 39 deletions

1
.gitignore vendored
View File

@ -24,3 +24,4 @@ plinth/tests/coverage/report/
.idea/
.DS_Store
*.box
.eggs

View File

@ -44,6 +44,7 @@ otherwise.
- static/themes/default/img/network-wireless.svg :: [[http://tango.freedesktop.org/][Public Domain]]
- static/themes/default/icons/deluge.png :: [[https://upload.wikimedia.org/wikipedia/commons/thumb/8/85//Deluge-Logo.svg/2000px-Deluge-Logo.svg.png][GPL]]
- static/themes/default/icons/diaspora.png :: [[https://upload.wikimedia.org/wikipedia/commons/thumb/8/85//Deluge-Logo.svg/2000px-Deluge-Logo.svg.png][Publc Domain]]
- static/themes/default/icons/ejabberd.png :: [[https://www.ejabberd.im/][GPL-2]]
- static/themes/default/icons/infinoted.png :: [[https://github.com/gobby/gobby/blob/master/COPYING][ISC]]
- static/themes/default/icons/ikiwiki.png :: [[https://ikiwiki.info/][GPL-2+]]
- static/themes/default/icons/jsxc.png :: -
@ -58,6 +59,6 @@ otherwise.
- static/themes/default/icons/roundcube.png :: [[https://roundcube.net/][GPL-3+]]
- static/themes/default/icons/shaarli.png :: [[https://github.com/shaarli/Shaarli][zlib/libpng]]
- static/themes/default/icons/syncthing.png :: [[https://github.com/syncthing/syncthing/][Mozilla Public License Version 2.0]]
- static/themes/default/icons/tahoe.png :: [[https://github.com/thekishanraval/Logos][GPLv3+]]
- static/themes/default/icons/transmission.png :: [[https://transmissionbt.com/][GPL]]
- static/themes/default/icons/ttrss.png :: [[https://tt-rss.org/gitlab/fox/tt-rss][GPL]]
- static/themes/default/icons/ejabberd.png :: [[https://www.ejabberd.im/][GPL-2]]

260
actions/tahoe-lafs Executable file
View File

@ -0,0 +1,260 @@
#!/usr/bin/python3
# -*- mode: python -*-
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# 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.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Configuration helper for Tahoe-LAFS.
"""
import argparse
import augeas
import grp
import json
import pwd
import shutil
import subprocess
import os
import ruamel.yaml
from plinth import action_utils
from plinth.modules.tahoe import (
introducer_name,
introducers_file,
storage_node_name,
tahoe_home,
introducer_furl_file)
from plinth.modules.tahoe.errors import TahoeConfigurationError
from plinth.utils import YAMLFile
domain_name_file = os.path.join(tahoe_home, 'domain_name')
DEFAULT_FILE = '/etc/default/tahoe-lafs'
def parse_arguments():
"""Return parsed command line arguments as dictionary."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
subparsers.add_parser('enable', help='Enable Tahoe-LAFS')
subparsers.add_parser('disable', help='Disable Tahoe-LAFS')
setup = subparsers.add_parser('setup',
help='Set domain name for Tahoe-LAFS')
setup.add_argument('--domain-name',
help='The domain name to be used by Tahoe-LAFS')
subparsers.add_parser('create-introducer',
help='Create and start the introducer node')
subparsers.add_parser('create-storage-node',
help='Create and start the storage node')
subparsers.add_parser('autostart',
help="Automatically start all introducers and "
"storage nodes on system startup")
intro_parser_add = subparsers.add_parser(
'add-introducer', help="Add an introducer to the storage node's list "
"of introducers.")
intro_parser_add.add_argument(
'--introducer', help="Add an introducer to the storage node's list "
"of introducers Param introducer must be a tuple "
"of (pet_name, furl)")
intro_parser_remove = subparsers.add_parser(
'remove-introducer', help="Rename the introducer entry in the "
"introducers.yaml file specified by the "
"param")
intro_parser_remove.add_argument(
'--pet-name', help='The domain name that will be used by '
'Tahoe-LAFS')
subparsers.add_parser('get-introducers',
help="Return a dictionary of all introducers and "
"their furls added to the storage node running "
"on this FreedomBox.")
subparsers.add_parser('get-local-introducer',
help="Return the name and furl of the introducer "
"created on this FreedomBox")
return parser.parse_args()
def subcommand_setup(arguments):
"""Actions to be performed after installing Tahoe-LAFS."""
# Create tahoe group if needed.
try:
grp.getgrnam('tahoe-lafs')
except KeyError:
subprocess.run(['addgroup', 'tahoe-lafs'], check=True)
# Create tahoe user if needed.
try:
pwd.getpwnam('tahoe-lafs')
except KeyError:
subprocess.run(
[
'adduser', '--system', '--ingroup', 'tahoe-lafs',
'--home', '/var/lib/tahoe-lafs',
'--gecos', 'Tahoe-LAFS distributed file system', 'tahoe-lafs'
],
check=True)
if not os.path.exists(tahoe_home):
os.makedirs(tahoe_home, mode=0o755)
shutil.chown(tahoe_home, user='tahoe-lafs', group='tahoe-lafs')
if not os.path.exists(domain_name_file):
with open(domain_name_file, 'w') as dnf:
dnf.write(arguments.domain_name)
def subcommand_autostart(_):
"""Automatically start all introducers and storage nodes on system startup.
"""
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Shellvars/lens', 'Shellvars.lns')
aug.set('/augeas/load/Shellvars/incl[last() + 1]', DEFAULT_FILE)
aug.load()
aug.set('/files' + DEFAULT_FILE + '/AUTOSTART', 'all')
aug.save()
def get_configured_domain_name():
"""Extract and return the domain name from the domain name file.
Throws TahoeConfigurationError if the domain name file is not found.
"""
if not os.path.exists(domain_name_file):
raise TahoeConfigurationError
else:
with open(domain_name_file) as dnf:
return dnf.read().rstrip()
def subcommand_create_introducer(_):
"""Create a Tahoe-LAFS introducer on this FreedomBox."""
os.chdir(tahoe_home)
if not os.path.exists(os.path.join(tahoe_home, introducer_name)):
subprocess.check_call([
'tahoe', 'create-introducer', '--port=3456',
'--location=tcp:{}:3456'.format(get_configured_domain_name()),
introducer_name
])
subprocess.call(['tahoe', 'start', introducer_name])
def subcommand_create_storage_node(_):
"""Create a Tahoe-LAFS storage node on this FreedomBox."""
os.chdir(tahoe_home)
if not os.path.exists(os.path.join(tahoe_home, storage_node_name)):
subprocess.check_call([
'tahoe', 'create-node', '--nickname=\"storage_node\"',
'--webport=1234',
'--hostname={}'.format(get_configured_domain_name()),
storage_node_name
])
with open(
os.path.join(tahoe_home, introducer_name, 'private',
introducer_name + '.furl'), 'r') as furl_file:
furl = furl_file.read().rstrip()
conf_dict = {'introducers': {introducer_name: {'furl': furl}}}
conf_yaml = ruamel.yaml.dump(
conf_dict, Dumper=ruamel.yaml.RoundTripDumper)
with open(
os.path.join(tahoe_home, storage_node_name, 'private',
'introducers.yaml'), 'w') as introducers_file:
introducers_file.write(conf_yaml)
subprocess.call(['tahoe', 'start', storage_node_name])
def subcommand_add_introducer(arguments):
"""Add an introducer to the storage node's list of introducers.
Param introducer must be a tuple of (pet_name, furl).
"""
with YAMLFile(introducers_file, restart_storage_node) as conf:
pet_name, furl = arguments.introducer.split(",")
conf['introducers'][pet_name] = {'furl': furl}
def subcommand_remove_introducer(arguments):
"""Rename the introducer entry in the introducers.yaml file specified
by the param pet_name
"""
with YAMLFile(introducers_file, restart_storage_node) as conf:
del conf['introducers'][arguments.pet_name]
def subcommand_get_introducers(_):
"""Return a dictionary of all introducers and their furls.
The ones added to the storage node running on this FreedomBox.
"""
with open(introducers_file, 'r') as intro_conf:
conf = ruamel.yaml.round_trip_load(intro_conf)
introducers = []
for pet_name in conf['introducers'].keys():
introducers.append((pet_name, conf['introducers'][pet_name]['furl']))
print(json.dumps(introducers))
def subcommand_get_local_introducer(_):
"""Return the name and furl of the introducer created on this FreedomBox
"""
with open(introducer_furl_file, 'r') as furl_file:
furl = furl_file.read().rstrip()
print(json.dumps((introducer_name, furl)))
def subcommand_enable(_):
"""Enable web configuration and reload."""
action_utils.service_enable('tahoe-lafs')
action_utils.webserver_enable('tahoe-plinth')
def subcommand_disable(_):
"""Disable web configuration and reload."""
action_utils.webserver_disable('tahoe-plinth')
action_utils.service_disable('tahoe-lafs')
def restart_storage_node():
"""Called after exiting context of editing introducers file."""
try:
subprocess.run(['tahoe', 'restart', 'storage_node'], check=True)
except subprocess.CalledProcessError as err:
print('Failed to restart storage_node with new configuration: %s', err)
def main():
"""Parse arguments and perform all duties."""
arguments = parse_arguments()
subcommand = arguments.subcommand.replace('-', '_')
subcommand_method = globals()['subcommand_' + subcommand]
subcommand_method(arguments)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,14 @@
# Tahoe-LAFS Storage Node web interface
Listen 5678
# XXX: SSL is not configured?
# TODO: Use subdomain?
<VirtualHost *:5678>
<Location "/">
Include includes/freedombox-auth-ldap.conf
Require ldap-group cn=admin,ou=groups,dc=thisbox
ProxyPass http://localhost:1234/
</Location>
</VirtualHost>

View File

@ -0,0 +1 @@
plinth.modules.tahoe

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>Tahoe-LAFS</short>
<description>Tahoe-LAFS is a distributed file storage system</description>
<port protocol="tcp" port="5678"/>
<port protocol="tcp" port="3456"/>
</service>

View File

@ -98,7 +98,6 @@ import subprocess
from plinth import cfg
from plinth.errors import ActionError
LOGGER = logging.getLogger(__name__)
@ -118,7 +117,17 @@ def superuser_run(action, options=None, input=None, async=False):
return _run(action, options, input, async, True)
def _run(action, options=None, input=None, async=False, run_as_root=False):
def run_as_user(action, options=None, input=None, async=False,
become_user=None):
"""Run a command as a different user.
If become_user is None, run as current user.
"""
return _run(action, options, input, async, False, become_user)
def _run(action, options=None, input=None, async=False, run_as_root=False,
become_user=None):
"""Safely run a specific action as a normal user or root.
Actions are pulled from the actions directory.
@ -159,6 +168,8 @@ def _run(action, options=None, input=None, async=False, run_as_root=False):
# Contract 1: commands can run via sudo.
if run_as_root:
cmd = ['sudo', '-n'] + cmd
elif become_user:
cmd = ['sudo', '-n', '-u', become_user] + cmd
LOGGER.info('Executing command - %s', cmd)

View File

@ -22,9 +22,26 @@ Common forms for use by modules.
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth import utils
class ServiceForm(forms.Form):
"""Generic configuration form for a service."""
is_enabled = forms.BooleanField(
label=_('Enable application'),
required=False)
class DomainSelectionForm(forms.Form):
"""Form for selecting a domain name to be used for
distributed federated applications
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['domain_name'].choices = utils.get_domain_names()
domain_name = forms.ChoiceField(
label=_('Select the domain name to be used for this application'),
choices=[]
)

View File

@ -18,7 +18,6 @@ import os
from django.utils.translation import ugettext_lazy as _
from plinth.modules import names
from plinth.utils import format_lazy
from plinth import actions, action_utils, frontpage, \
service as service_module
@ -66,7 +65,7 @@ description = [
'<a href="https://diaspora.{host}">diaspora.{host}</a> path on the '
'web server.'.format(host=get_configured_domain_name()) if is_setup()
else 'Please register a domain name for your FreedomBox to be able to'
' federate with other diaspora* pods.')
' federate with other diaspora* pods.')
]
@ -112,19 +111,6 @@ def setup(helper, old_version=None):
helper.call('post', add_shortcut)
def get_domain_names():
"""Return the domain name(s)"""
results = []
for domain_type, domains in names.domains.items():
if domain_type == 'hiddenservice':
continue
for domain in domains:
results.append((domain, domain))
return results
def add_shortcut():
"""Add shortcut to diaspora on the Plinth homepage"""
if is_setup():

View File

@ -25,6 +25,7 @@ from django.views.generic import FormView
from plinth import actions
from plinth.modules import diaspora
from plinth.modules.diaspora.forms import DiasporaForm
from plinth.utils import get_domain_names
from plinth.views import ServiceView
@ -48,7 +49,7 @@ class DiasporaSetupView(FormView):
context = super().get_context_data(**kwargs)
context['description'] = self.description
context['title'] = self.title
context['domain_names'] = diaspora.get_domain_names()
context['domain_names'] = get_domain_names()
return context

View File

@ -31,8 +31,6 @@ from plinth import actions
from plinth import frontpage
from plinth import service as service_module
from plinth.menu import main_menu
from plinth.modules import names
version = 1
@ -139,20 +137,6 @@ def diagnose():
return results
def get_domain_names():
"""Return the domain name(s)."""
results = []
for domain_type, domains in names.domains.items():
if domain_type == 'hiddenservice':
continue
for domain in domains:
results.append((domain, domain))
return results
def get_configured_domain_name():
"""Return the currently configured domain name."""
if not is_setup():

View File

@ -22,7 +22,7 @@ Forms for configuring matrix-synapse.
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth.modules import matrixsynapse
from plinth.utils import get_domain_names
class MatrixSynapseForm(forms.Form):
@ -35,4 +35,4 @@ class MatrixSynapseForm(forms.Form):
def __init__(self, *args, **kwargs):
"""Initialize the form object."""
super().__init__(*args, **kwargs)
self.fields['domain_name'].choices = matrixsynapse.get_domain_names()
self.fields['domain_name'].choices = get_domain_names()

View File

@ -27,6 +27,7 @@ from plinth import actions
from plinth import views
from plinth.modules import matrixsynapse
from plinth.modules.matrixsynapse.forms import MatrixSynapseForm
from plinth.utils import get_domain_names
class SetupView(FormView):
@ -49,7 +50,7 @@ class SetupView(FormView):
context['title'] = matrixsynapse.title
context['description'] = matrixsynapse.description
context['domain_names'] = matrixsynapse.get_domain_names()
context['domain_names'] = get_domain_names()
return context

View File

@ -0,0 +1,218 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# 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.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Plinth module to configure Tahoe-LAFS.
"""
import json
import os
from django.utils.translation import ugettext_lazy as _
from plinth import action_utils
from plinth import actions
from plinth import cfg
from plinth import frontpage
from plinth import service as service_module
from plinth.menu import main_menu
from plinth.utils import format_lazy
from .errors import TahoeConfigurationError
version = 1
managed_services = ['tahoe-lafs']
managed_packages = ['tahoe-lafs']
title = _('Distributed File Storage (Tahoe-LAFS)')
service = None
tahoe_home = '/var/lib/tahoe-lafs'
introducer_name = 'introducer'
storage_node_name = 'storage_node'
domain_name_file = os.path.join(tahoe_home, 'domain_name')
introducers_file = os.path.join(
tahoe_home, '{}/private/introducers.yaml'.format(storage_node_name))
introducer_furl_file = os.path.join(
tahoe_home, '{0}/private/{0}.furl'.format(introducer_name))
def is_setup():
"""Check whether Tahoe-LAFS is setup"""
return os.path.exists(domain_name_file)
def get_configured_domain_name():
"""Extract and return the domain name from the domain name file.
Throws TahoeConfigurationError if the domain name file is not found.
"""
if not os.path.exists(domain_name_file):
raise TahoeConfigurationError
else:
with open(domain_name_file) as dnf:
return dnf.read().rstrip()
description = [
_('Tahoe-LAFS is a decentralized secure file storage system. '
'It uses provider independent security to store files over a '
'distributed network of storage nodes. Even if some of the nodes fail, '
'your files can be retrieved from the remaining nodes.'),
format_lazy(
_('This {box_name} hosts a storage node and an introducer by default. '
'Additional introducers can be added, which will introduce this '
'node to the other storage nodes.'),
box_name=_(cfg.box_name)),
]
def init():
"""Intialize the module."""
menu = main_menu.get('apps')
menu.add_urlname(title, 'glyphicon-hdd', 'tahoe:index')
global service
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and is_setup():
service = service_module.Service(
managed_services[0],
title,
ports=['tahoe-plinth'],
is_external=True,
is_enabled=is_enabled,
enable=enable,
disable=disable,
is_running=is_running)
if is_enabled():
add_shortcut()
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
def post_setup(configured_domain_name):
"""Actions to be performed after installing tahoe-lafs package."""
actions.superuser_run('tahoe-lafs',
['setup', '--domain-name', configured_domain_name])
actions.superuser_run('tahoe-lafs', ['enable'])
actions.run_as_user('tahoe-lafs', ['create-introducer'],
become_user='tahoe-lafs')
actions.run_as_user('tahoe-lafs', ['create-storage-node'],
become_user='tahoe-lafs')
actions.superuser_run('tahoe-lafs', ['autostart'])
global service
if service is None:
service = service_module.Service(
managed_services[0],
title,
ports=['tahoe-plinth'],
is_external=True,
is_enabled=is_enabled,
enable=enable,
disable=disable,
is_running=is_running)
service.notify_enabled(None, True)
add_shortcut()
def add_shortcut():
"""Helper method to add a shortcut to the front page."""
# BUG: Current logo appears squashed on front page.
frontpage.add_shortcut(
'tahoe-lafs', title,
url='https://{}:5678'.format(get_configured_domain_name()),
login_required=True)
def is_running():
"""Return whether the service is running."""
return action_utils.service_is_running(managed_services[0])
def is_enabled():
"""Return whether the module is enabled."""
return (action_utils.service_is_enabled(managed_services[0]) and
action_utils.webserver_is_enabled('tahoe-plinth'))
def enable():
"""Enable the module."""
actions.superuser_run('tahoe-lafs', ['enable'])
add_shortcut()
def disable():
"""Enable the module."""
actions.superuser_run('tahoe-lafs', ['disable'])
frontpage.remove_shortcut('tahoe-lafs')
def diagnose():
"""Run diagnostics and return the results."""
return [action_utils.diagnose_url(
'http://localhost:5678', kind='4', check_certificate=False),
action_utils.diagnose_url(
'http://localhost:5678', kind='6', check_certificate=False),
action_utils.diagnose_url(
'http://{}:5678'.format(get_configured_domain_name()),
kind='4',
check_certificate=False)]
def add_introducer(introducer):
"""Add an introducer to the storage node's list of introducers.
Param introducer must be a tuple of (pet_name, furl)
"""
actions.run_as_user('tahoe-lafs',
['add-introducer',
"--introducer",
",".join(introducer)],
become_user='tahoe-lafs')
def remove_introducer(pet_name):
"""Rename the introducer entry in the introducers.yaml file specified by
the param pet_name.
"""
actions.run_as_user('tahoe-lafs',
['remove-introducer', '--pet-name', pet_name],
become_user='tahoe-lafs')
def get_introducers():
"""Return a dictionary of all introducers and their furls added to the
storage node running on this FreedomBox.
"""
introducers = actions.run_as_user('tahoe-lafs', ['get-introducers'],
become_user='tahoe-lafs')
return json.loads(introducers)
def get_local_introducer():
"""Return the name and furl of the introducer created on this FreedomBox.
"""
introducer = actions.run_as_user('tahoe-lafs', ['get-local-introducer'],
become_user='tahoe-lafs')
return json.loads(introducer)

View File

@ -0,0 +1,27 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# 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.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Errors for Tahoe-LAFS module
"""
from plinth.errors import PlinthError
class TahoeConfigurationError(PlinthError):
"""Tahoe-LAFS has not been configured for domain name."""
pass

View File

@ -0,0 +1,110 @@
{% extends "service.html" %}
{% comment %}
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# 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.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see
<http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load i18n %}
{% load bootstrap %}
{% block description %}
{% for paragraph in description %}
<p>{{ paragraph|safe }}</p>
{% endfor %}
<p>
{% url 'config:index' as index_url %}
{% blocktrans trimmed with domain_name=domain_name %}
The Tahoe-LAFS server domain is set to <b>{{ domain_name }}</b>.
Changing the FreedomBox domain name needs a reinstall of
Tahoe-LAFS and you WILL LOSE DATA. You can access Tahoe-LAFS at
<a href="https://{{domain_name}}:5678">https://{{domain_name}}:5678</a>.
{% endblocktrans %}
</p>
{% endblock %}
{% block configuration %}
<h3>{% trans "Configuration" %}</h3>
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn btn-primary"
value="{% trans "Update setup" %}"/>
</form>
<br/>
<h4>{% trans "Local introducer" %}</h4>
<table class="table table-bordered">
<thead>
<tr>
<th>{% trans "Pet Name" %}</th>
<th> furl </th>
</tr>
</thead>
<tr>
<td> {{ local_introducer.0 }} </td>
<td> {{ local_introducer.1 }} </td>
</tr>
</table>
<form class="form" method="post" action="/plinth/apps/tahoe-lafs/add_introducer">
{% csrf_token %}
<h4>{% trans "Add new introducer" %}</h4>
<div class="form-group">
<label>{% trans "Pet Name" %}:</label>
<input type="text" class="form-control" name="pet_name">
</div>
<div class="form-group">
<label>furl:</label>
<textarea class="form-control" rows="5" name="furl"></textarea>
</div>
<input type="submit" class="btn btn-primary"
value="{% trans "Add" %}"/>
</form>
<br/>
<h4>{% trans "Connected introducers" %}</h4>
<table class="table table-bordered">
<thead>
<tr>
<th>{% trans "Pet Name" %}</th>
<th> furl </th>
</tr>
</thead>
{% for introducer, furl in introducers %}
<tr>
<td> {{ introducer }} </td>
<td> {{ furl }} </td>
<form class="form" method="post"
action="/plinth/apps/tahoe-lafs/remove_introducer/{{introducer}}" id="introducer_list">
{% csrf_token %}
<td><button type="submit" class="btn btn-danger">
{% trans "Remove" %}
</button></td>
</form>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -0,0 +1,62 @@
{% extends "base.html" %}
{% comment %}
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# 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.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% block content %}
{% block pagetitle %}
<h2>{{ title }}</h2>
{% endblock %}
{% block description %}
{% for paragraph in description %}
<p>{{ paragraph|safe }}</p>
{% endfor %}
{% endblock %}
<p>
{% url 'config:index' as index_url %}
{% if domain_names|length == 0 %}
No domain(s) are set. You can setup your domain on the system at
<a href="{{ index_url }}">{% trans "Configure" %}</a> page.
{% endif %}
</p>
{% block status %}
{% endblock %}
{% block diagnostics %}
{% endblock %}
{% block configuration %}
{% if domain_names|length > 0 %}
<h3>{% trans Configuration %}</h3>
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn btn-primary"
value="{% trans "Update setup" %}"/>
</form>
{% endif %}
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,37 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# 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.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
URLs for the Tahoe-LAFS module.
"""
from . import views
from django.conf.urls import url
from .views import TahoeSetupView, TahoeServiceView
urlpatterns = [
url(r'^apps/tahoe-lafs/setup$', TahoeSetupView.as_view(),
name='setup'),
url(r'^apps/tahoe-lafs/add_introducer$', views.add_introducer,
name="add-introducer"),
url(r'^apps/tahoe-lafs/remove_introducer/(?P<introducer>[0-9a-zA-Z_]+)$',
views.remove_introducer, name="remove-introducer"),
url(r'^apps/tahoe-lafs/$', TahoeServiceView.as_view(),
name='index')
]

View File

@ -0,0 +1,83 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# 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.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Views for the Tahoe-LAFS module
"""
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.views.generic import FormView
from plinth.forms import DomainSelectionForm
from plinth.modules import tahoe
from plinth.utils import get_domain_names
from plinth.views import ServiceView
class TahoeSetupView(FormView):
"""Show tahoe-lafs setup page."""
template_name = 'tahoe-pre-setup.html'
form_class = DomainSelectionForm
description = tahoe.description
title = tahoe.title
success_url = reverse_lazy('tahoe:index')
def form_valid(self, form):
domain_name = form.cleaned_data['domain_name']
tahoe.post_setup(domain_name)
return super().form_valid(form)
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(**kwargs)
context['description'] = self.description
context['title'] = self.title
context['domain_names'] = get_domain_names()
return context
class TahoeServiceView(ServiceView):
"""Show tahoe-lafs service page."""
service_id = tahoe.managed_services[0]
template_name = 'tahoe-post-setup.html'
description = tahoe.description
diagnostics_module_name = 'tahoe'
def dispatch(self, request, *args, **kwargs):
if not tahoe.is_setup():
return redirect('tahoe:setup')
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(**kwargs)
context['domain_name'] = tahoe.get_configured_domain_name()
context['introducers'] = tahoe.get_introducers()
context['local_introducer'] = tahoe.get_local_introducer()
return context
def add_introducer(request):
if request.method == 'POST':
tahoe.add_introducer((request.POST['pet_name'], request.POST['furl']))
return redirect('tahoe:index')
def remove_introducer(request, introducer):
if request.method == 'POST':
tahoe.remove_introducer(introducer)
return redirect('tahoe:index')

View File

@ -19,11 +19,14 @@
Test module for Plinth's utilities.
"""
import tempfile
from unittest import TestCase
from unittest.mock import Mock, MagicMock
from django.test import TestCase
import ruamel.yaml
from django.test.client import RequestFactory
from plinth.utils import YAMLFile
from plinth.utils import is_user_admin
@ -81,3 +84,44 @@ class TestIsAdminUser(TestCase):
mock.assert_called_once_with()
session_mock.__getitem__.assert_called_once_with(
'cache_user_is_admin')
class TestYAMLFileUtil(TestCase):
"""Check updating YAML files"""
kv_pair = {'key': 'value'}
def test_update_empty_yaml_file(self):
"""
Update an empty YAML file with content.
"""
fp = tempfile.NamedTemporaryFile()
conf = {'property1': self.kv_pair}
with YAMLFile(fp.name) as file_conf:
for key in conf.keys():
file_conf[key] = conf[key]
with open(fp.name, 'r') as retrieved_conf:
assert retrieved_conf.read() == ruamel.yaml.round_trip_dump(conf)
def test_update_non_empty_yaml_file(self):
"""
Update a non-empty YAML file with modifications
"""
fp = tempfile.NamedTemporaryFile()
with open(fp.name, 'w') as conf_file:
conf_file.write(
ruamel.yaml.round_trip_dump({
'property1': self.kv_pair
}))
with YAMLFile(fp.name) as file_conf:
file_conf['property2'] = self.kv_pair
with open(fp.name, 'r') as retrieved_conf:
file_conf = ruamel.yaml.round_trip_load(retrieved_conf)
print(file_conf)
assert file_conf == {'property1': self.kv_pair,
'property2': self.kv_pair}

View File

@ -20,7 +20,10 @@ Miscellaneous utility methods.
"""
import importlib
import os
import ruamel.yaml
from django.utils.functional import lazy
from plinth.modules import names
def import_from_gi(library, version):
@ -63,3 +66,49 @@ def is_user_admin(request, cached=False):
user_is_admin = request.user.groups.filter(name='admin').exists()
request.session['cache_user_is_admin'] = user_is_admin
return user_is_admin
def get_domain_names():
"""Return the domain name(s)"""
domain_names = []
for domain_type, domains in names.domains.items():
if domain_type == 'hiddenservice':
continue
for domain in domains:
domain_names.append((domain, domain))
return domain_names
class YAMLFile(object):
"""A context management class for updating YAML files"""
def __init__(self, yaml_file, post_exit=None):
"""Return a context object for the YAML file.
Parameters:
yaml_file - the YAML file to update.
post_exit - a function that will be called after updating the YAML file.
"""
self.yaml_file = yaml_file
self.post_exit = post_exit
self.conf = None
def __enter__(self):
with open(self.yaml_file, 'r') as intro_conf:
if not self.is_file_empty():
self.conf = ruamel.yaml.round_trip_load(intro_conf)
else:
self.conf = {}
return self.conf
def __exit__(self, typ, value, traceback):
with open(self.yaml_file, 'w') as intro_conf:
ruamel.yaml.round_trip_dump(self.conf, intro_conf)
if self.post_exit:
self.post_exit()
def is_file_empty(self):
return os.stat(self.yaml_file).st_size == 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB