From 5ad180fcc922e3f0d0ac3cc439b7fad56696b75f Mon Sep 17 00:00:00 2001 From: Joseph Nuthalpati Date: Tue, 28 Mar 2017 16:10:55 +0530 Subject: [PATCH] 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. --- .gitignore | 1 + LICENSES | 3 +- actions/tahoe-lafs | 260 ++++++++++++++++++ .../apache2/conf-available/tahoe-plinth.conf | 14 + data/etc/plinth/modules-enabled/tahoe | 1 + .../lib/firewalld/services/tahoe-plinth.xml | 7 + plinth/actions.py | 15 +- plinth/forms.py | 17 ++ plinth/modules/diaspora/__init__.py | 16 +- plinth/modules/diaspora/views.py | 3 +- plinth/modules/matrixsynapse/__init__.py | 16 -- plinth/modules/matrixsynapse/forms.py | 4 +- plinth/modules/matrixsynapse/views.py | 3 +- plinth/modules/tahoe/__init__.py | 218 +++++++++++++++ plinth/modules/tahoe/errors.py | 27 ++ .../tahoe/templates/tahoe-post-setup.html | 110 ++++++++ .../tahoe/templates/tahoe-pre-setup.html | 62 +++++ plinth/modules/tahoe/urls.py | 37 +++ plinth/modules/tahoe/views.py | 83 ++++++ plinth/tests/test_utils.py | 46 +++- plinth/utils.py | 49 ++++ static/themes/default/icons/tahoe-lafs.png | Bin 0 -> 37926 bytes 22 files changed, 953 insertions(+), 39 deletions(-) create mode 100755 actions/tahoe-lafs create mode 100644 data/etc/apache2/conf-available/tahoe-plinth.conf create mode 100644 data/etc/plinth/modules-enabled/tahoe create mode 100644 data/usr/lib/firewalld/services/tahoe-plinth.xml create mode 100644 plinth/modules/tahoe/__init__.py create mode 100644 plinth/modules/tahoe/errors.py create mode 100644 plinth/modules/tahoe/templates/tahoe-post-setup.html create mode 100644 plinth/modules/tahoe/templates/tahoe-pre-setup.html create mode 100644 plinth/modules/tahoe/urls.py create mode 100644 plinth/modules/tahoe/views.py create mode 100644 static/themes/default/icons/tahoe-lafs.png 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" %}

+ +
+ {% csrf_token %} + + {{ form|bootstrap }} + + +
+
+ +

{% trans "Local introducer" %}

+ + + + + + + + + + + +
{% trans "Pet Name" %} furl
{{ local_introducer.0 }} {{ local_introducer.1 }}
+ +
+ {% csrf_token %} +

{% trans "Add new introducer" %}

+ +
+ + +
+
+ + +
+ +
+ +
+ +

{% trans "Connected introducers" %}

+ + + + + + + + {% for introducer, furl in introducers %} + + + + + {% csrf_token %} + + + + {% endfor %} +
{% trans "Pet Name" %} furl
{{ introducer }} {{ furl }}
+ +{% 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 %} +

+ + {% block status %} + {% endblock %} + + {% block diagnostics %} + {% endblock %} + + {% block configuration %} + {% if domain_names|length > 0 %} +

{% trans Configuration %}

+
+ {% csrf_token %} + + {{ form|bootstrap }} + + +
+ {% 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 0000000000000000000000000000000000000000..cb25e69dffd4c8b499c53019e73d7a9ed8a4e7d4 GIT binary patch literal 37926 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 literal 0 HcmV?d00001