diff --git a/.gitignore b/.gitignore
index fe23a6fe4..11c70b289 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,4 @@ plinth/tests/coverage/report/
.idea/
.DS_Store
*.box
+.eggs
diff --git a/LICENSES b/LICENSES
index 443f8b7d9..5d00a3b57 100644
--- a/LICENSES
+++ b/LICENSES
@@ -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]]
diff --git a/actions/tahoe-lafs b/actions/tahoe-lafs
new file mode 100755
index 000000000..c4770af56
--- /dev/null
+++ b/actions/tahoe-lafs
@@ -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 .
+#
+"""
+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()
diff --git a/data/etc/apache2/conf-available/tahoe-plinth.conf b/data/etc/apache2/conf-available/tahoe-plinth.conf
new file mode 100644
index 000000000..80e5c5fdd
--- /dev/null
+++ b/data/etc/apache2/conf-available/tahoe-plinth.conf
@@ -0,0 +1,14 @@
+# Tahoe-LAFS Storage Node web interface
+
+Listen 5678
+
+# XXX: SSL is not configured?
+# TODO: Use subdomain?
+
+
+ Include includes/freedombox-auth-ldap.conf
+ Require ldap-group cn=admin,ou=groups,dc=thisbox
+
+ ProxyPass http://localhost:1234/
+
+
diff --git a/data/etc/plinth/modules-enabled/tahoe b/data/etc/plinth/modules-enabled/tahoe
new file mode 100644
index 000000000..89041b3fc
--- /dev/null
+++ b/data/etc/plinth/modules-enabled/tahoe
@@ -0,0 +1 @@
+plinth.modules.tahoe
diff --git a/data/usr/lib/firewalld/services/tahoe-plinth.xml b/data/usr/lib/firewalld/services/tahoe-plinth.xml
new file mode 100644
index 000000000..e745d9636
--- /dev/null
+++ b/data/usr/lib/firewalld/services/tahoe-plinth.xml
@@ -0,0 +1,7 @@
+
+
+ Tahoe-LAFS
+ Tahoe-LAFS is a distributed file storage system
+
+
+
diff --git a/plinth/actions.py b/plinth/actions.py
index 7043c69cb..01de248f9 100644
--- a/plinth/actions.py
+++ b/plinth/actions.py
@@ -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)
diff --git a/plinth/forms.py b/plinth/forms.py
index b9aca6c7b..cbf7fe63e 100644
--- a/plinth/forms.py
+++ b/plinth/forms.py
@@ -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=[]
+ )
diff --git a/plinth/modules/diaspora/__init__.py b/plinth/modules/diaspora/__init__.py
index 2658ea80e..cdd6626df 100644
--- a/plinth/modules/diaspora/__init__.py
+++ b/plinth/modules/diaspora/__init__.py
@@ -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 = [
'diaspora.{host} 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():
diff --git a/plinth/modules/diaspora/views.py b/plinth/modules/diaspora/views.py
index edd61f7d7..157521af7 100644
--- a/plinth/modules/diaspora/views.py
+++ b/plinth/modules/diaspora/views.py
@@ -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
diff --git a/plinth/modules/matrixsynapse/__init__.py b/plinth/modules/matrixsynapse/__init__.py
index af515b01b..0b4b50d0b 100644
--- a/plinth/modules/matrixsynapse/__init__.py
+++ b/plinth/modules/matrixsynapse/__init__.py
@@ -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():
diff --git a/plinth/modules/matrixsynapse/forms.py b/plinth/modules/matrixsynapse/forms.py
index 18872c524..48918065d 100644
--- a/plinth/modules/matrixsynapse/forms.py
+++ b/plinth/modules/matrixsynapse/forms.py
@@ -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()
diff --git a/plinth/modules/matrixsynapse/views.py b/plinth/modules/matrixsynapse/views.py
index 9ad5363f9..f670c50d3 100644
--- a/plinth/modules/matrixsynapse/views.py
+++ b/plinth/modules/matrixsynapse/views.py
@@ -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
diff --git a/plinth/modules/tahoe/__init__.py b/plinth/modules/tahoe/__init__.py
new file mode 100644
index 000000000..c3f16bf5b
--- /dev/null
+++ b/plinth/modules/tahoe/__init__.py
@@ -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 .
+#
+"""
+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)
diff --git a/plinth/modules/tahoe/errors.py b/plinth/modules/tahoe/errors.py
new file mode 100644
index 000000000..aaa5896e7
--- /dev/null
+++ b/plinth/modules/tahoe/errors.py
@@ -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 .
+#
+
+"""
+Errors for Tahoe-LAFS module
+"""
+
+from plinth.errors import PlinthError
+
+
+class TahoeConfigurationError(PlinthError):
+ """Tahoe-LAFS has not been configured for domain name."""
+ pass
diff --git a/plinth/modules/tahoe/templates/tahoe-post-setup.html b/plinth/modules/tahoe/templates/tahoe-post-setup.html
new file mode 100644
index 000000000..ebac240cd
--- /dev/null
+++ b/plinth/modules/tahoe/templates/tahoe-post-setup.html
@@ -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
+.
+#
+{% endcomment %}
+
+{% load i18n %}
+{% load bootstrap %}
+
+{% block description %}
+
+{% for paragraph in description %}
+
{{ paragraph|safe }}
+{% endfor %}
+
+
+ {% url 'config:index' as index_url %}
+ {% blocktrans trimmed with domain_name=domain_name %}
+ The Tahoe-LAFS server domain is set to {{ domain_name }}.
+ Changing the FreedomBox domain name needs a reinstall of
+ Tahoe-LAFS and you WILL LOSE DATA. You can access Tahoe-LAFS at
+ https://{{domain_name}}:5678.
+ {% endblocktrans %}
+
+{% endblock %}
+
+{% block configuration %}
+
{% trans "Configuration" %}
+
+
+
+
+
{% trans "Local introducer" %}
+
+
+
+
{% trans "Pet Name" %}
+
furl
+
+
+
+
{{ local_introducer.0 }}
+
{{ local_introducer.1 }}
+
+
+
+
+
+
+
+
{% trans "Connected introducers" %}
+
+
+
+
{% trans "Pet Name" %}
+
furl
+
+
+ {% for introducer, furl in introducers %}
+
+
{{ introducer }}
+
{{ furl }}
+
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/plinth/modules/tahoe/templates/tahoe-pre-setup.html b/plinth/modules/tahoe/templates/tahoe-pre-setup.html
new file mode 100644
index 000000000..3bbcc61eb
--- /dev/null
+++ b/plinth/modules/tahoe/templates/tahoe-pre-setup.html
@@ -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 .
+#
+{% endcomment %}
+
+{% load bootstrap %}
+{% load i18n %}
+
+{% block content %}
+ {% block pagetitle %}
+
{{ title }}
+ {% endblock %}
+
+ {% block description %}
+ {% for paragraph in description %}
+
{{ paragraph|safe }}
+ {% endfor %}
+ {% endblock %}
+
+
+ {% 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
+ {% trans "Configure" %} page.
+ {% endif %}
+
+
+ {% endif %}
+ {% endblock %}
+{% endblock %}
diff --git a/plinth/modules/tahoe/urls.py b/plinth/modules/tahoe/urls.py
new file mode 100644
index 000000000..754e9e8f6
--- /dev/null
+++ b/plinth/modules/tahoe/urls.py
@@ -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 .
+#
+"""
+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[0-9a-zA-Z_]+)$',
+ views.remove_introducer, name="remove-introducer"),
+ url(r'^apps/tahoe-lafs/$', TahoeServiceView.as_view(),
+ name='index')
+]
diff --git a/plinth/modules/tahoe/views.py b/plinth/modules/tahoe/views.py
new file mode 100644
index 000000000..b0aba6ff5
--- /dev/null
+++ b/plinth/modules/tahoe/views.py
@@ -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 .
+#
+"""
+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')
diff --git a/plinth/tests/test_utils.py b/plinth/tests/test_utils.py
index 514126805..cf980935b 100644
--- a/plinth/tests/test_utils.py
+++ b/plinth/tests/test_utils.py
@@ -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}
diff --git a/plinth/utils.py b/plinth/utils.py
index e360983b6..8c14755c1 100644
--- a/plinth/utils.py
+++ b/plinth/utils.py
@@ -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
diff --git a/static/themes/default/icons/tahoe-lafs.png b/static/themes/default/icons/tahoe-lafs.png
new file mode 100644
index 000000000..cb25e69df
Binary files /dev/null and b/static/themes/default/icons/tahoe-lafs.png differ