Merge branch 'better-setup'

This commit is contained in:
James Valleroy 2016-02-19 22:11:02 -05:00
commit 77134cd55b
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
104 changed files with 1535 additions and 1371 deletions

View File

@ -33,6 +33,7 @@ from cherrypy.process.plugins import Daemonizer
from plinth import cfg from plinth import cfg
from plinth import module_loader from plinth import module_loader
from plinth import service from plinth import service
from plinth import setup
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -57,6 +58,9 @@ def parse_arguments():
parser.add_argument( parser.add_argument(
'--no-daemon', action='store_true', default=cfg.no_daemon, '--no-daemon', action='store_true', default=cfg.no_daemon,
help='do not start as a daemon') help='do not start as a daemon')
parser.add_argument(
'--setup', action='store_true', default=False,
help='run setup tasks on all essential modules and exit')
parser.add_argument( parser.add_argument(
'--diagnose', action='store_true', default=False, '--diagnose', action='store_true', default=False,
help='run diagnostic tests and exit') help='run diagnostic tests and exit')
@ -132,9 +136,7 @@ def setup_server():
cherrypy.tree.mount(None, manual_url, config) cherrypy.tree.mount(None, manual_url, config)
logger.debug('Serving manual images %s on %s', manual_dir, manual_url) logger.debug('Serving manual images %s on %s', manual_dir, manual_url)
for module_import_path in module_loader.loaded_modules: for module_name, module in module_loader.loaded_modules.items():
module = importlib.import_module(module_import_path)
module_name = module_import_path.split('.')[-1]
module_path = os.path.dirname(module.__file__) module_path = os.path.dirname(module.__file__)
static_dir = os.path.join(module_path, 'static') static_dir = os.path.join(module_path, 'static')
if not os.path.isdir(static_dir): if not os.path.isdir(static_dir):
@ -258,6 +260,7 @@ def configure_django():
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'stronghold.middleware.LoginRequiredMiddleware', 'stronghold.middleware.LoginRequiredMiddleware',
'plinth.modules.first_boot.middleware.FirstBootMiddleware', 'plinth.modules.first_boot.middleware.FirstBootMiddleware',
'plinth.middleware.SetupMiddleware',
), ),
ROOT_URLCONF='plinth.urls', ROOT_URLCONF='plinth.urls',
SECURE_PROXY_SSL_HEADER=secure_proxy_ssl_header, SECURE_PROXY_SSL_HEADER=secure_proxy_ssl_header,
@ -278,6 +281,18 @@ def configure_django():
os.chmod(cfg.store_file, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) os.chmod(cfg.store_file, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
def run_setup_and_exit():
"""Run setup on all essential modules and exit."""
error_code = 0
try:
setup.setup_all_modules(essential=True)
except Exception as exception:
logger.error('Error running setup - %s', exception)
error_code = 1
sys.exit(error_code)
def run_diagnostics_and_exit(): def run_diagnostics_and_exit():
"""Run diagostics on all modules and exit.""" """Run diagostics on all modules and exit."""
module = importlib.import_module('plinth.modules.diagnostics.diagnostics') module = importlib.import_module('plinth.modules.diagnostics.diagnostics')
@ -315,6 +330,9 @@ def main():
module_loader.load_modules() module_loader.load_modules()
if arguments.setup:
run_setup_and_exit()
if arguments.diagnose: if arguments.diagnose:
run_diagnostics_and_exit() run_diagnostics_and_exit()

75
plinth/middleware.py Normal file
View File

@ -0,0 +1,75 @@
#
# 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/>.
#
"""
Django middleware to show pre-setup message and setup progress.
"""
from django.contrib import messages
from django.core.urlresolvers import resolve
from django.utils.translation import ugettext_lazy as _
import logging
import plinth
from plinth.package import PackageException
from . import views
logger = logging.getLogger(__name__)
class SetupMiddleware(object):
"""Show setup page or progress if setup is neccessary or running."""
@staticmethod
def process_request(request):
"""Handle a request as Django middleware request handler."""
# Perform a URL resolution. This is slightly inefficient as
# Django will do this resolution again.
resolver_match = resolve(request.path_info)
if not resolver_match.namespaces or not len(resolver_match.namespaces):
# Requested URL does not belong to any application
return
module_name = resolver_match.namespaces[0]
module = plinth.module_loader.loaded_modules[module_name]
# Collect errors from any previous operations and show them
if module.setup_helper.is_finished:
exception = module.setup_helper.collect_result()
if not exception:
messages.success(request, _('Application installed.'))
else:
if isinstance(exception, PackageException):
error_string = getattr(exception, 'error_string',
str(exception))
error_details = getattr(exception, 'error_details', '')
message = _('Error installing application: {string} '
'{details}').format(
string=error_string, details=error_details)
else:
message = _('Error installing application: {error}') \
.format(error=exception)
messages.error(request, message)
# Check if application is up-to-date
if module.setup_helper.get_state() == 'up-to-date':
return
view = views.SetupView.as_view()
return view(request, setup_helper=module.setup_helper)

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-02-10 12:30
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('plinth', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Module',
fields=[
('name', models.TextField(primary_key=True, serialize=False)),
('setup_version', models.IntegerField()),
],
),
]

View File

@ -37,3 +37,9 @@ class KVStore(models.Model):
def value(self, val): def value(self, val):
"""Store the value of the key/value pair by JSON encoding it""" """Store the value of the key/value pair by JSON encoding it"""
self.value_json = json.dumps(val) self.value_json = json.dumps(val)
class Module(models.Model):
"""Model to store current setup versions of a module."""
name = models.TextField(primary_key=True)
setup_version = models.IntegerField()

View File

@ -19,6 +19,7 @@
Discover, load and manage Plinth modules Discover, load and manage Plinth modules
""" """
import collections
import django import django
import importlib import importlib
import logging import logging
@ -27,11 +28,12 @@ import re
from plinth import cfg from plinth import cfg
from plinth import urls from plinth import urls
from plinth import setup
from plinth.signals import pre_module_loading, post_module_loading from plinth.signals import pre_module_loading, post_module_loading
LOGGER = logging.getLogger(__name__) logger = logging.getLogger(__name__)
loaded_modules = [] loaded_modules = collections.OrderedDict()
_modules_to_load = None _modules_to_load = None
@ -42,16 +44,18 @@ def load_modules():
""" """
pre_module_loading.send_robust(sender="module_loader") pre_module_loading.send_robust(sender="module_loader")
modules = {} modules = {}
for module_name in get_modules_to_load(): for module_import_path in get_modules_to_load():
LOGGER.info('Importing %s', module_name) logger.info('Importing %s', module_import_path)
module_name = module_import_path.split('.')[-1]
try: try:
modules[module_name] = importlib.import_module(module_name) modules[module_name] = importlib.import_module(module_import_path)
except Exception as exception: except Exception as exception:
LOGGER.exception('Could not import %s: %s', module_name, exception) logger.exception('Could not import %s: %s', module_import_path,
exception)
if cfg.debug: if cfg.debug:
raise raise
_include_module_urls(module_name) _include_module_urls(module_import_path, module_name)
ordered_modules = [] ordered_modules = []
remaining_modules = dict(modules) # Make a copy remaining_modules = dict(modules) # Make a copy
@ -64,14 +68,14 @@ def load_modules():
_insert_modules(module_name, module, remaining_modules, _insert_modules(module_name, module, remaining_modules,
ordered_modules) ordered_modules)
except KeyError: except KeyError:
LOGGER.error('Unsatified dependency for module - %s', logger.error('Unsatified dependency for module - %s',
module_name) module_name)
LOGGER.debug('Module load order - %s', ordered_modules) logger.debug('Module load order - %s', ordered_modules)
for module_name in ordered_modules: for module_name in ordered_modules:
_initialize_module(modules[module_name]) _initialize_module(module_name, modules[module_name])
loaded_modules.append(module_name) loaded_modules[module_name] = modules[module_name]
post_module_loading.send_robust(sender="module_loader") post_module_loading.send_robust(sender="module_loader")
@ -94,7 +98,7 @@ def _insert_modules(module_name, module, remaining_modules, ordered_modules):
try: try:
module = remaining_modules.pop(dependency) module = remaining_modules.pop(dependency)
except KeyError: except KeyError:
LOGGER.error('Not found or circular dependency - %s, %s', logger.error('Not found or circular dependency - %s, %s',
module_name, dependency) module_name, dependency)
raise raise
@ -103,32 +107,34 @@ def _insert_modules(module_name, module, remaining_modules, ordered_modules):
ordered_modules.append(module_name) ordered_modules.append(module_name)
def _include_module_urls(module_name): def _include_module_urls(module_import_path, module_name):
"""Include the module's URLs in global project URLs list""" """Include the module's URLs in global project URLs list"""
namespace = module_name.split('.')[-1] url_module = module_import_path + '.urls'
url_module = module_name + '.urls'
try: try:
urls.urlpatterns += [ urls.urlpatterns += [
django.conf.urls.url( django.conf.urls.url(
r'', django.conf.urls.include(url_module, namespace))] r'', django.conf.urls.include(url_module, module_name))]
except ImportError: except ImportError:
LOGGER.debug('No URLs for %s', module_name) logger.debug('No URLs for %s', module_name)
if cfg.debug: if cfg.debug:
raise raise
def _initialize_module(module): def _initialize_module(module_name, module):
"""Call initialization method in the module if it exists""" """Call initialization method in the module if it exists"""
# Perform setup related initialization on the module
setup.init(module_name, module)
try: try:
init = module.init init = module.init
except AttributeError: except AttributeError:
LOGGER.debug('No init() for module - %s', module.__name__) logger.debug('No init() for module - %s', module.__name__)
return return
try: try:
init() init()
except Exception as exception: except Exception as exception:
LOGGER.exception('Exception while running init for %s: %s', logger.exception('Exception while running init for %s: %s',
module, exception) module, exception)
if cfg.debug: if cfg.debug:
raise raise

View File

@ -23,3 +23,7 @@ from . import apps
from .apps import init from .apps import init
__all__ = ['apps', 'init'] __all__ = ['apps', 'init']
version = 1
is_essential = 1

View File

@ -20,16 +20,32 @@ Plinth module for service discovery.
""" """
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import subprocess
from plinth import actions
from plinth import action_utils from plinth import action_utils
from plinth import cfg from plinth import cfg
from plinth import service as service_module from plinth import service as service_module
from plinth.utils import format_lazy
# pylint: disable=C0103 # pylint: disable=C0103
depends = ['plinth.modules.system'] version = 1
is_essential = True
depends = ['system']
title = _('Service Discovery')
description = [
format_lazy(
_('Service discovery allows other devices on the network to '
'discover your {{ box_name }} and services running on it. It '
'also allows {{ box_name }} to discover other devices and '
'services running on your local network. Service discovery is '
'not essential and works only on internal networks. It may be '
'disabled to improve security especially when connecting to a '
'hostile local network.'), box_name=_(cfg.box_name))
]
service = None service = None
@ -37,13 +53,16 @@ service = None
def init(): def init():
"""Intialize the service discovery module.""" """Intialize the service discovery module."""
menu = cfg.main_menu.get('system:index') menu = cfg.main_menu.get('system:index')
menu.add_urlname(_('Service Discovery'), 'glyphicon-lamp', menu.add_urlname(title, 'glyphicon-lamp', 'avahi:index', 950)
'avahi:index', 950)
global service # pylint: disable=W0603 global service # pylint: disable=W0603
service = service_module.Service( service = service_module.Service(
'avahi', _('Service Discovery'), ['mdns'], 'avahi', title, ['mdns'], is_external=False, enabled=is_enabled())
is_external=False, enabled=is_enabled())
def setup(helper, old_version=False):
"""Install and configure the module."""
helper.install(['avahi-daemon'])
def is_enabled(): def is_enabled():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,21 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{% trans "Service Discovery" %}</h2>
<p>
{% blocktrans trimmed %}
Service discovery allows other devices on the network to
discover your {{ box_name }} and services running on it. It
also allows {{ box_name }} to discover other devices and
services running on your local network. Service discovery is
not essential and works only on internal networks. It may be
disabled to improve security especially when connecting to a
hostile local network.
{% endblocktrans %}
</p>
<h3>{% trans "Status" %}</h3> <h3>{% trans "Status" %}</h3>

View File

@ -26,14 +26,12 @@ import logging
from .forms import ServiceDiscoveryForm from .forms import ServiceDiscoveryForm
from plinth import actions from plinth import actions
from plinth import package
from plinth.modules import avahi from plinth.modules import avahi
logger = logging.getLogger(__name__) # pylint: disable=C0103 logger = logging.getLogger(__name__) # pylint: disable=C0103
@package.required(['avahi-daemon'])
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
status = get_status() status = get_status()
@ -50,7 +48,8 @@ def index(request):
form = ServiceDiscoveryForm(initial=status, prefix='avahi') form = ServiceDiscoveryForm(initial=status, prefix='avahi')
return TemplateResponse(request, 'avahi.html', return TemplateResponse(request, 'avahi.html',
{'title': _('Service Discovery'), {'title': avahi.title,
'description': avahi.description,
'status': status, 'status': status,
'form': form}) 'form': form})

View File

@ -24,6 +24,8 @@ from .config import init
__all__ = ['config', 'init'] __all__ = ['config', 'init']
depends = ['plinth.modules.system', version = 1
'plinth.modules.firewall',
'plinth.modules.names'] is_essential = True
depends = ['system', 'firewall', 'names']

View File

@ -33,7 +33,7 @@ import socket
from plinth import actions from plinth import actions
from plinth import cfg from plinth import cfg
from plinth.modules.firewall import firewall from plinth.modules import firewall
from plinth.modules.names import SERVICES from plinth.modules.names import SERVICES
from plinth.signals import pre_hostname_change, post_hostname_change from plinth.signals import pre_hostname_change, post_hostname_change
from plinth.signals import domainname_change from plinth.signals import domainname_change

View File

@ -19,7 +19,7 @@
URLs for the Configuration module URLs for the Configuration module
""" """
from django.conf.urls import patterns, url from django.conf.urls import url
from . import config as views from . import config as views

View File

@ -22,13 +22,23 @@ Plinth module to configure system date and time
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import subprocess import subprocess
from plinth import actions
from plinth import action_utils from plinth import action_utils
from plinth import cfg from plinth import cfg
from plinth import service as service_module from plinth import service as service_module
depends = ['plinth.modules.system'] version = 1
is_essential = True
depends = ['system']
title = _('Date & Time')
description = [
_('Network time server is a program that maintians the system time '
'in synchronization with servers on the Internet.')
]
service = None service = None
@ -36,13 +46,17 @@ service = None
def init(): def init():
"""Intialize the date/time module.""" """Intialize the date/time module."""
menu = cfg.main_menu.get('system:index') menu = cfg.main_menu.get('system:index')
menu.add_urlname(_('Date & Time'), 'glyphicon-time', menu.add_urlname(title, 'glyphicon-time', 'datetime:index', 900)
'datetime:index', 900)
global service global service
service = service_module.Service( service = service_module.Service(
'ntp', _('Network Time Server'), 'ntp', title, is_external=False, enabled=is_enabled())
is_external=False, enabled=is_enabled())
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['ntp'])
helper.call('post', service.notify_enabled, None, True)
def is_enabled(): def is_enabled():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,16 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{% trans "Date & Time" %}</h2>
<p>
{% blocktrans trimmed %}
Network time server is a program that maintians the system time
in synchronization with servers on the Internet.
{% endblocktrans %}
</p>
<h3>{% trans "Status" %}</h3> <h3>{% trans "Status" %}</h3>

View File

@ -26,18 +26,11 @@ import logging
from .forms import DateTimeForm from .forms import DateTimeForm
from plinth import actions from plinth import actions
from plinth import package
from plinth.modules import datetime from plinth.modules import datetime
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def on_install():
"""Notify that the service is now enabled."""
datetime.service.notify_enabled(None, True)
@package.required(['ntp'], on_install=on_install)
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
status = get_status() status = get_status()
@ -55,7 +48,8 @@ def index(request):
form = DateTimeForm(initial=status, prefix='datetime') form = DateTimeForm(initial=status, prefix='datetime')
return TemplateResponse(request, 'datetime.html', return TemplateResponse(request, 'datetime.html',
{'title': _('Date & Time'), {'title': datetime.title,
'description': datetime.description,
'status': status, 'status': status,
'form': form}) 'form': form})

View File

@ -27,7 +27,20 @@ from plinth import cfg
from plinth import service as service_module from plinth import service as service_module
depends = ['plinth.modules.apps'] version = 1
depends = ['apps']
title = _('BitTorrent Web Client (Deluge)')
description = [
_('Deluge is a BitTorrent client that features a Web UI.'),
_('When enabled, the Deluge web client will be available from '
'<a href="/deluge">/deluge</a> path on the web server. The '
'default password is \'deluge\', but you should log in and change '
'it immediately after enabling this service.')
]
service = None service = None
@ -35,13 +48,19 @@ service = None
def init(): def init():
"""Initialize the Deluge module.""" """Initialize the Deluge module."""
menu = cfg.main_menu.get('apps:index') menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('BitTorrent (Deluge)'), 'glyphicon-magnet', menu.add_urlname(title, 'glyphicon-magnet', 'deluge:index', 200)
'deluge:index', 200)
global service global service
service = service_module.Service( service = service_module.Service(
'deluge', _('Deluge BitTorrent'), ['http', 'https'], 'deluge', title, ['http', 'https'], is_external=True,
is_external=True, enabled=is_enabled()) enabled=is_enabled())
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['deluged', 'deluge-web'])
helper.call('post', actions.superuser_run, 'deluge', ['enable'])
helper.call('post', service.notify_enabled, None, True)
def is_enabled(): def is_enabled():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,20 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{% trans "BitTorrent Web Client (Deluge)" %}</h2>
<p>{% trans "Deluge is a BitTorrent client that features a Web UI." %}</p>
<p>
{% blocktrans trimmed %}
When enabled, the Deluge web client will be available from
<a href="/deluge">/deluge</a> path on the web server. The
default password is 'deluge', but you should log in and change
it immediately after enabling this service.
{% endblocktrans %}
</p>
<h3>{% trans "Status" %}</h3> <h3>{% trans "Status" %}</h3>

View File

@ -25,17 +25,9 @@ from django.utils.translation import ugettext as _
from .forms import DelugeForm from .forms import DelugeForm
from plinth import actions from plinth import actions
from plinth import package
from plinth.modules import deluge from plinth.modules import deluge
def on_install():
"""Tasks to run after package install."""
actions.superuser_run('deluge', ['enable'])
deluge.service.notify_enabled(None, True)
@package.required(['deluged', 'deluge-web'], on_install=on_install)
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
status = get_status() status = get_status()
@ -53,7 +45,8 @@ def index(request):
form = DelugeForm(initial=status, prefix='deluge') form = DelugeForm(initial=status, prefix='deluge')
return TemplateResponse(request, 'deluge.html', return TemplateResponse(request, 'deluge.html',
{'title': _('BitTorrent (Deluge)'), {'title': deluge.title,
'description': deluge.description,
'status': status, 'status': status,
'form': form}) 'form': form})

View File

@ -19,13 +19,30 @@
Plinth module for system diagnostics Plinth module for system diagnostics
""" """
from . import diagnostics from django.utils.translation import ugettext_lazy as _
from .diagnostics import init
from plinth import action_utils from plinth import action_utils
from plinth import cfg
__all__ = ['diagnostics', 'init'] version = 1
depends = ['plinth.modules.system'] is_essential = True
title = _('Diagnostics')
description = [
_('The system diagnostic test will run a number of checks on your '
'system to confirm that applications and services are working as '
'expected.')
]
depends = ['system']
def init():
"""Initialize the module"""
menu = cfg.main_menu.get('system:index')
menu.add_urlname(title, 'glyphicon-screenshot', 'diagnostics:index', 30)
def diagnose(): def diagnose():

View File

@ -24,12 +24,11 @@ from django.http import Http404
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import importlib
import logging import logging
import threading import threading
from plinth import cfg
from plinth import module_loader from plinth import module_loader
from plinth.modules import diagnostics
logger = logging.Logger(__name__) logger = logging.Logger(__name__)
@ -39,20 +38,14 @@ current_results = {}
_running_task = None _running_task = None
def init():
"""Initialize the module"""
menu = cfg.main_menu.get('system:index')
menu.add_urlname(_('Diagnostics'), 'glyphicon-screenshot',
'diagnostics:index', 30)
def index(request): def index(request):
"""Serve the index page""" """Serve the index page"""
if request.method == 'POST' and not _running_task: if request.method == 'POST' and not _running_task:
_start_task() _start_task()
return TemplateResponse(request, 'diagnostics.html', return TemplateResponse(request, 'diagnostics.html',
{'title': _('System Diagnostics'), {'title': diagnostics.title,
'description': diagnostics.description,
'is_running': _running_task is not None, 'is_running': _running_task is not None,
'results': current_results}) 'results': current_results})
@ -60,19 +53,14 @@ def index(request):
@require_POST @require_POST
def module(request, module_name): def module(request, module_name):
"""Return diagnostics for a particular module.""" """Return diagnostics for a particular module."""
found = False try:
for module_import_path in module_loader.loaded_modules: module = module_loader.loaded_modules[module_name]
if module_name == module_import_path.split('.')[-1]: except KeyError:
found = True
break
if not found:
raise Http404('Module does not exist or not loaded') raise Http404('Module does not exist or not loaded')
loaded_module = importlib.import_module(module_import_path)
results = [] results = []
if hasattr(loaded_module, 'diagnose'): if hasattr(module, 'diagnose'):
results = loaded_module.diagnose() results = module.diagnose()
return TemplateResponse(request, 'diagnostics_module.html', return TemplateResponse(request, 'diagnostics_module.html',
{'title': _('Diagnostic Test'), {'title': _('Diagnostic Test'),
@ -110,17 +98,15 @@ def run_on_all_modules():
'progress_percentage': 0} 'progress_percentage': 0}
modules = [] modules = []
for module_import_path in module_loader.loaded_modules: for module_name, module in module_loader.loaded_modules.items():
loaded_module = importlib.import_module(module_import_path) if not hasattr(module, 'diagnose'):
if not hasattr(loaded_module, 'diagnose'):
continue continue
module_name = module_import_path.split('.')[-1] modules.append((module_name, module))
modules.append((module_name, loaded_module))
current_results['results'][module_name] = None current_results['results'][module_name] = None
current_results['modules'] = modules current_results['modules'] = modules
for current_index, (module_name, loaded_module) in enumerate(modules): for current_index, (module_name, module) in enumerate(modules):
current_results['results'][module_name] = loaded_module.diagnose() current_results['results'][module_name] = module.diagnose()
current_results['progress_percentage'] = \ current_results['progress_percentage'] = \
int((current_index + 1) * 100 / len(modules)) int((current_index + 1) * 100 / len(modules))

View File

@ -1,4 +1,4 @@
{% extends 'base.html' %} {% extends 'app.html' %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -29,17 +29,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block configuration %}
<h2>{{ title }}</h2>
<p>
{% blocktrans trimmed %}
The system diagnostic test will run a number of checks on your
system to confirm that applications and services are working as
expected.
{% endblocktrans %}
</p>
{% if not is_running %} {% if not is_running %}
<form class="form form-diagnostics-button" method="post" <form class="form form-diagnostics-button" method="post"

View File

@ -19,9 +19,42 @@
Plinth module to configure ez-ipupdate client Plinth module to configure ez-ipupdate client
""" """
from . import dynamicdns from django.utils.translation import ugettext_lazy as _
from .dynamicdns import init
__all__ = ['dynamicdns', 'init'] from plinth import cfg
from plinth.utils import format_lazy
depends = ['plinth.modules.apps'] version = 1
depends = ['apps']
title = _('Dynamic DNS Client')
description = [
format_lazy(
_('If your internet provider changes your IP address periodic '
'(i.e. every 24h) it may be hard for others to find you in the '
'WEB. And for this reason nobody may find the services which are '
'provided by {box_name}, such as ownCloud.'),
box_name=_(cfg.box_name)),
_('The solution is to assign a DNS name to your IP address and '
'update the DNS name every time your IP is changed by your '
'Internet provider. Dynamic DNS allows you to push your current '
'public IP address to an '
'<a href=\'http://gnudip2.sourceforge.net/\' target=\'_blank\'> '
'gnudip </a> server. Afterwards the Server will assign your DNS name '
'with the new IP and if someone from the Internet asks for your DNS '
'name he will get your current IP answered.')
]
def init():
"""Initialize the module."""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(title, 'glyphicon-refresh', 'dynamicdns:index', 500)
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['ez-ipupdate'])

View File

@ -25,7 +25,7 @@ import logging
from plinth import actions from plinth import actions
from plinth import cfg from plinth import cfg
from plinth import package from plinth.modules import dynamicdns
from plinth.utils import format_lazy from plinth.utils import format_lazy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -39,19 +39,11 @@ subsubmenu = [{'url': reverse_lazy('dynamicdns:index'),
'text': ugettext_lazy('Status')}] 'text': ugettext_lazy('Status')}]
def init():
"""Initialize the dynamicdns module"""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(ugettext_lazy('Dynamic DNS'), 'glyphicon-refresh',
'dynamicdns:index', 500)
@package.required(['ez-ipupdate'])
def index(request): def index(request):
"""Serve Dynamic DNS page.""" """Serve Dynamic DNS page."""
return TemplateResponse(request, 'dynamicdns.html', return TemplateResponse(request, 'dynamicdns.html',
{'title': _('Dynamic DNS'), {'title': dynamicdns.title,
'description': dynamicdns.description,
'subsubmenu': subsubmenu}) 'subsubmenu': subsubmenu})
@ -198,7 +190,6 @@ class ConfigureForm(forms.Form):
raise forms.ValidationError(_('Please provide a password')) raise forms.ValidationError(_('Please provide a password'))
@package.required(['ez-ipupdate'])
def configure(request): def configure(request):
"""Serve the configuration form.""" """Serve the configuration form."""
status = get_status() status = get_status()
@ -219,7 +210,6 @@ def configure(request):
'subsubmenu': subsubmenu}) 'subsubmenu': subsubmenu})
@package.required(['ez-ipupdate'])
def statuspage(request): def statuspage(request):
"""Serve the status page.""" """Serve the status page."""
check_nat = actions.run('dynamicdns', ['get-nat']) check_nat = actions.run('dynamicdns', ['get-nat'])

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -20,32 +20,7 @@
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{% trans "Dynamic DNS Client" %}</h2>
<p>
{% blocktrans trimmed %}
If your internet provider changes your IP address periodic
(i.e. every 24h) it may be hard for others to find you in the
WEB. And for this reason nobody may find the services which are
provided by {{ box_name }}, such as ownCloud.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
The solution is to assign a DNS name to your IP address and
update the DNS name every time your IP is changed by your
Internet provider. Dynamic DNS allows you to push your current
public IP address to an
<a href='http://gnudip2.sourceforge.net/' target='_blank'> gnudip </a>
server. Afterwards the Server will assign your DNS name with the
new IP and if someone from the Internet asks for your DNS name
he will get your current IP answered.
{% endblocktrans %}
</p>
<p> <p>
{% blocktrans trimmed %} {% blocktrans trimmed %}
If you are looking for a free dynamic DNS account, you may find If you are looking for a free dynamic DNS account, you may find

View File

@ -19,9 +19,119 @@
Plinth module to configure a firewall Plinth module to configure a firewall
""" """
from . import firewall from django.utils.translation import ugettext_lazy as _
from .firewall import init import logging
__all__ = ['firewall', 'init'] from plinth import actions
from plinth import cfg
from plinth.signals import service_enabled
import plinth.service as service_module
from plinth.utils import format_lazy
depends = ['plinth.modules.system'] version = 1
is_essential = True
depends = ['system']
title = _('Firewall')
description = [
format_lazy(
_('Firewall is a security system that controls the incoming and '
'outgoing network traffic on your {box_name}. Keeping a '
'firewall enabled and properly configured reduces risk of '
'security threat from the Internet.'), box_name=cfg.box_name)
]
LOGGER = logging.getLogger(__name__)
def init():
"""Initailze firewall module"""
menu = cfg.main_menu.get('system:index')
menu.add_urlname(title, 'glyphicon-fire', 'firewall:index', 50)
service_enabled.connect(on_service_enabled)
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['firewalld'])
def get_enabled_status():
"""Return whether firewall is enabled"""
output = _run(['get-status'], superuser=True)
return output.split()[0] == 'running'
def get_enabled_services(zone):
"""Return the status of various services currently enabled"""
output = _run(['get-enabled-services', '--zone', zone], superuser=True)
return output.split()
def add_service(port, zone):
"""Enable a service in firewall"""
_run(['add-service', port, '--zone', zone], superuser=True)
def remove_service(port, zone):
"""Remove a service in firewall"""
_run(['remove-service', port, '--zone', zone], superuser=True)
def on_service_enabled(sender, service_id, enabled, **kwargs):
"""
Enable/disable firewall ports when a service is
enabled/disabled.
"""
del sender # Unused
del kwargs # Unused
internal_enabled_services = get_enabled_services(zone='internal')
external_enabled_services = get_enabled_services(zone='external')
LOGGER.info('Service enabled - %s, %s', service_id, enabled)
service = service_module.services[service_id]
for port in service.ports:
if enabled:
if port not in internal_enabled_services:
add_service(port, zone='internal')
if (service.is_external and
port not in external_enabled_services):
add_service(port, zone='external')
else:
# service already configured.
pass
else:
if port in internal_enabled_services:
enabled_services_on_port = [
service_.is_enabled()
for service_ in service_module.services.values()
if port in service_.ports and
service_id != service_.service_id]
if not any(enabled_services_on_port):
remove_service(port, zone='internal')
if port in external_enabled_services:
enabled_services_on_port = [
service_.is_enabled()
for service_ in service_module.services.values()
if port in service_.ports and
service_id != service_.service_id and
service_.is_external]
if not any(enabled_services_on_port):
remove_service(port, zone='external')
def _run(arguments, superuser=False):
"""Run an given command and raise exception if there was an error"""
command = 'firewall'
if superuser:
return actions.superuser_run(command, arguments)
else:
return actions.run(command, arguments)

View File

@ -1,137 +0,0 @@
#
# 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 a firewall
"""
from django.template.response import TemplateResponse
from django.utils.translation import ugettext_lazy as _
import logging
from plinth import actions
from plinth import cfg
from plinth import package
from plinth.signals import service_enabled
import plinth.service as service_module
LOGGER = logging.getLogger(__name__)
def init():
"""Initailze firewall module"""
menu = cfg.main_menu.get('system:index')
menu.add_urlname(_('Firewall'), 'glyphicon-fire', 'firewall:index', 50)
service_enabled.connect(on_service_enabled)
@package.required(['firewalld'])
def index(request):
"""Serve introcution page"""
if not get_enabled_status():
return TemplateResponse(request, 'firewall.html',
{'title': _('Firewall'),
'firewall_status': 'not_running'})
internal_enabled_services = get_enabled_services(zone='internal')
external_enabled_services = get_enabled_services(zone='external')
return TemplateResponse(
request, 'firewall.html',
{'title': _('Firewall'),
'services': list(service_module.services.values()),
'internal_enabled_services': internal_enabled_services,
'external_enabled_services': external_enabled_services})
def get_enabled_status():
"""Return whether firewall is enabled"""
output = _run(['get-status'], superuser=True)
return output.split()[0] == 'running'
def get_enabled_services(zone):
"""Return the status of various services currently enabled"""
output = _run(['get-enabled-services', '--zone', zone], superuser=True)
return output.split()
def add_service(port, zone):
"""Enable a service in firewall"""
_run(['add-service', port, '--zone', zone], superuser=True)
def remove_service(port, zone):
"""Remove a service in firewall"""
_run(['remove-service', port, '--zone', zone], superuser=True)
def on_service_enabled(sender, service_id, enabled, **kwargs):
"""
Enable/disable firewall ports when a service is
enabled/disabled.
"""
del sender # Unused
del kwargs # Unused
internal_enabled_services = get_enabled_services(zone='internal')
external_enabled_services = get_enabled_services(zone='external')
LOGGER.info('Service enabled - %s, %s', service_id, enabled)
service = service_module.services[service_id]
for port in service.ports:
if enabled:
if port not in internal_enabled_services:
add_service(port, zone='internal')
if (service.is_external and
port not in external_enabled_services):
add_service(port, zone='external')
else:
# service already configured.
pass
else:
if port in internal_enabled_services:
enabled_services_on_port = [
service_.is_enabled()
for service_ in service_module.services.values()
if port in service_.ports and
service_id != service_.service_id]
if not any(enabled_services_on_port):
remove_service(port, zone='internal')
if port in external_enabled_services:
enabled_services_on_port = [
service_.is_enabled()
for service_ in service_module.services.values()
if port in service_.ports and
service_id != service_.service_id and
service_.is_external]
if not any(enabled_services_on_port):
remove_service(port, zone='external')
def _run(arguments, superuser=False):
"""Run an given command and raise exception if there was an error"""
command = 'firewall'
if superuser:
return actions.superuser_run(command, arguments)
else:
return actions.run(command, arguments)

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -20,18 +20,7 @@
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{{ title }}</h2>
<p>
{% blocktrans trimmed %}
Firewall is a security system that controls the incoming and
outgoing network traffic on your {{ box_name }}. Keeping a
firewall enabled and properly configured reduces risk of
security threat from the Internet.
{% endblocktrans %}
</p>
<p>{% trans "Current status:" %}</p> <p>{% trans "Current status:" %}</p>

View File

@ -21,7 +21,7 @@ URLs for the Firewall module
from django.conf.urls import url from django.conf.urls import url
from . import firewall as views from . import views
urlpatterns = [ urlpatterns = [

View File

@ -0,0 +1,45 @@
#
# 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 a firewall
"""
from django.template.response import TemplateResponse
from plinth.modules import firewall
import plinth.service as service_module
def index(request):
"""Serve introcution page"""
if not firewall.get_enabled_status():
return TemplateResponse(request, 'firewall.html',
{'title': firewall.title,
'description': firewall.description,
'firewall_status': 'not_running'})
internal_enabled_services = firewall.get_enabled_services(zone='internal')
external_enabled_services = firewall.get_enabled_services(zone='external')
return TemplateResponse(
request, 'firewall.html',
{'title': firewall.title,
'description': firewall.description,
'services': list(service_module.services.values()),
'internal_enabled_services': internal_enabled_services,
'external_enabled_services': external_enabled_services})

View File

@ -18,3 +18,7 @@
""" """
Plinth module for first boot wizard Plinth module for first boot wizard
""" """
version = 1
is_essential = True

View File

@ -27,7 +27,16 @@ from plinth import cfg
from plinth import service as service_module from plinth import service as service_module
depends = ['plinth.modules.apps'] version = 1
depends = ['apps']
title = _('Wiki and Blog (ikiwiki)')
description = [
_('When enabled, the blogs and wikis will be available '
'from <a href="/ikiwiki">/ikiwiki</a>.')
]
service = None service = None
@ -35,13 +44,25 @@ service = None
def init(): def init():
"""Initialize the ikiwiki module.""" """Initialize the ikiwiki module."""
menu = cfg.main_menu.get('apps:index') menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Wiki and Blog (ikiwiki)'), 'glyphicon-edit', menu.add_urlname(title, 'glyphicon-edit', 'ikiwiki:index', 1100)
'ikiwiki:index', 1100)
global service global service
service = service_module.Service( service = service_module.Service(
'ikiwiki', _('ikiwiki wikis and blogs'), ['http', 'https'], 'ikiwiki', title, ['http', 'https'], is_external=True,
is_external=True, enabled=is_enabled()) enabled=is_enabled())
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['ikiwiki',
'gcc',
'libc6-dev',
'libtimedate-perl',
'libcgi-formbuilder-perl',
'libcgi-session-perl',
'libxml-writer-perl'])
helper.call('post', actions.superuser_run, 'ikiwiki', ['setup'])
helper.call('post', service.notify_enabled, None, True)
def is_enabled(): def is_enabled():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,14 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<p>
{% blocktrans trimmed %}
When enabled, the blogs and wikis will be available
from <a href="/ikiwiki">/ikiwiki</a>.
{% endblocktrans %}
</p>
{% include "diagnostics_button.html" with module="ikiwiki" %} {% include "diagnostics_button.html" with module="ikiwiki" %}

View File

@ -27,8 +27,6 @@ from django.utils.translation import ugettext as _, ugettext_lazy
from .forms import IkiwikiForm, IkiwikiCreateForm from .forms import IkiwikiForm, IkiwikiCreateForm
from plinth import actions from plinth import actions
from plinth import action_utils
from plinth import package
from plinth.modules import ikiwiki from plinth.modules import ikiwiki
@ -40,20 +38,6 @@ subsubmenu = [{'url': reverse_lazy('ikiwiki:index'),
'text': ugettext_lazy('Create')}] 'text': ugettext_lazy('Create')}]
def on_install():
"""Enable ikiwiki on install."""
actions.superuser_run('ikiwiki', ['setup'])
ikiwiki.service.notify_enabled(None, True)
@package.required(['ikiwiki',
'gcc',
'libc6-dev',
'libtimedate-perl',
'libcgi-formbuilder-perl',
'libcgi-session-perl',
'libxml-writer-perl'],
on_install=on_install)
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
status = get_status() status = get_status()
@ -70,7 +54,8 @@ def index(request):
form = IkiwikiForm(initial=status, prefix='ikiwiki') form = IkiwikiForm(initial=status, prefix='ikiwiki')
return TemplateResponse(request, 'ikiwiki.html', return TemplateResponse(request, 'ikiwiki.html',
{'title': _('Wiki and Blog'), {'title': ikiwiki.title,
'description': ikiwiki.description,
'status': status, 'status': status,
'form': form, 'form': form,
'subsubmenu': subsubmenu}) 'subsubmenu': subsubmenu})

View File

@ -20,20 +20,38 @@ Plinth module for using Let's Encrypt.
""" """
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import json
from plinth import actions
from plinth import action_utils from plinth import action_utils
from plinth import cfg from plinth import cfg
from plinth import service as service_module
from plinth.modules import names from plinth.modules import names
from plinth.utils import format_lazy
depends = [ version = 1
'plinth.modules.apps',
'plinth.modules.names' is_essential = True
depends = ['apps', 'names']
title = _('Certificates (Let\'s Encrypt)')
description = [
format_lazy(
_('A digital certficate allows users of a web service to verify the '
'identity of the service and to securely communicate with it. '
'{box_name} can automatically obtain and setup digital '
'certificates for each available domain. It does so by proving '
'itself to be the owner of a domain to Let\'s Encrypt, a '
'certficate authority (CA).'), box_name=_(cfg.box_name)),
_('Let\'s Encrypt is a free, automated, and open certificate '
'authority, run for the publics benefit by the Internet Security '
'Research Group (ISRG). Please read and agree with the '
'<a href="https://letsencrypt.org/repository/">Let\'s Encrypt '
'Subscriber Agreement</a> before using this service.')
] ]
service = None service = None
@ -44,6 +62,11 @@ def init():
'glyphicon-lock', 'letsencrypt:index', 20) 'glyphicon-lock', 'letsencrypt:index', 20)
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['letsencrypt'])
def diagnose(): def diagnose():
"""Run diagnostics and return the results.""" """Run diagnostics and return the results."""
results = [] results = []

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -33,30 +33,7 @@
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block configuration %}
<h2>{% trans "Certificates (Let's Encrypt)" %}</h2>
<p>
{% blocktrans trimmed %}
A digital certficate allows users of a web service to verify the
identity of the service and to securely communicate with it.
{{ box_name }} can automatically obtain and setup digital
certificates for each available domain. It does so by proving
itself to be the owner of a domain to Let's Encrypt, a
certficate authority (CA).
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
Let's Encrypt is a free, automated, and open certificate
authority, run for the publics benefit by the Internet Security
Research Group (ISRG). Please read and agree with the
<a href="https://letsencrypt.org/repository/">Let's Encrypt
Subscriber Agreement</a> before using this service.
{% endblocktrans %}
</p>
<div class="row"> <div class="row">
<div class="col-lg-8"> <div class="col-lg-8">

View File

@ -29,20 +29,20 @@ import json
import logging import logging
from plinth import actions from plinth import actions
from plinth import package
from plinth.errors import ActionError from plinth.errors import ActionError
from plinth.modules import letsencrypt
from plinth.modules import names from plinth.modules import names
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@package.required(['letsencrypt'])
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
status = get_status() status = get_status()
return TemplateResponse(request, 'letsencrypt.html', return TemplateResponse(request, 'letsencrypt.html',
{'title': _('Certificates (Let\'s Encrypt)'), {'title': letsencrypt.title,
'description': letsencrypt.description,
'status': status}) 'status': status})

View File

@ -23,7 +23,22 @@ from django.utils.translation import ugettext_lazy as _
from plinth import cfg from plinth import cfg
depends = ['plinth.modules.system'] version = 1
depends = ['system']
title = _('Monkeysphere')
description = [
_('With Monkeysphere, a PGP key can be generated for each configured '
'domain serving SSH. The PGP public key can then be uploaded to the PGP '
'keyservers. Users connecting to this machine through SSH can verify '
'that they are connecting to the correct host. For users to trust the '
'key, at least one person (usually the machine owner) must sign the key '
'using the regular PGP key signing process. See the '
'<a href="http://web.monkeysphere.info/getting-started-ssh/"> '
'Monkeysphere SSH documentation</a> for more details.')
]
def init(): def init():
@ -31,3 +46,8 @@ def init():
menu = cfg.main_menu.get('system:index') menu = cfg.main_menu.get('system:index')
menu.add_urlname(_('Monkeysphere'), 'glyphicon-certificate', menu.add_urlname(_('Monkeysphere'), 'glyphicon-certificate',
'monkeysphere:index', 970) 'monkeysphere:index', 970)
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['monkeysphere'])

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -30,22 +30,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block configuration %}
<h2>{% trans "Monkeysphere" %}</h2>
<p>
{% blocktrans trimmed %}
With Monkeysphere, a PGP key can be generated for each configured domain
serving SSH. The PGP public key can then be uploaded to the PGP
keyservers. Users connecting to this machine through SSH can verify that
they are connecting to the correct host. For users to trust the key, at
least one person (usually the machine owner) must sign the key using the
regular PGP key signing process. See the
<a href="http://web.monkeysphere.info/getting-started-ssh/">
Monkeysphere SSH documentation</a> for more details.
{% endblocktrans %}
</p>
{% if running %} {% if running %}
<p class="running-status-parent"> <p class="running-status-parent">

View File

@ -28,20 +28,20 @@ from django.views.decorators.http import require_POST
import json import json
from plinth import actions from plinth import actions
from plinth import package from plinth.modules import monkeysphere
from plinth.modules import names from plinth.modules import names
publish_process = None publish_process = None
@package.required(['monkeysphere'])
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
_collect_publish_result(request) _collect_publish_result(request)
status = get_status() status = get_status()
return TemplateResponse( return TemplateResponse(
request, 'monkeysphere.html', request, 'monkeysphere.html',
{'title': _('Monkeysphere'), {'title': monkeysphere.title,
'description': monkeysphere.description,
'status': status, 'status': status,
'running': bool(publish_process)}) 'running': bool(publish_process)})

View File

@ -21,13 +21,25 @@ Plinth module to configure Mumble server
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from plinth import actions
from plinth import action_utils from plinth import action_utils
from plinth import cfg from plinth import cfg
from plinth import service as service_module from plinth import service as service_module
depends = ['plinth.modules.apps'] version = 1
depends = ['apps']
title = _('Voice Chat (Mumble)')
description = [
_('Mumble is an open source, low-latency, encrypted, high quality '
'voice chat software.'),
_('You can connect to your Mumble server on the regular Mumble port '
'64738. <a href="http://mumble.info">Clients</a> to connect to Mumble '
'from your desktop and Android devices are available.')
]
service = None service = None
@ -35,13 +47,17 @@ service = None
def init(): def init():
"""Intialize the Mumble module.""" """Intialize the Mumble module."""
menu = cfg.main_menu.get('apps:index') menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Voice Chat (Mumble)'), 'glyphicon-headphones', menu.add_urlname(title, 'glyphicon-headphones', 'mumble:index', 900)
'mumble:index', 900)
global service global service
service = service_module.Service( service = service_module.Service(
'mumble-plinth', _('Mumble Voice Chat Server'), 'mumble-plinth', title, is_external=True, enabled=is_enabled())
is_external=True, enabled=is_enabled())
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['mumble-server'])
helper.call('post', service.notify_enabled, None, True)
def is_enabled(): def is_enabled():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,25 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{% trans "Voice Chat (Mumble)" %}</h2>
<p>
{% blocktrans trimmed %}
Mumble is an open source, low-latency, encrypted, high quality
voice chat software.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
You can connect to your Mumble server on the regular Mumble port 64738.
<a href="http://mumble.info">Clients</a> to connect to Mumble
from your desktop and Android devices are available.
{% endblocktrans %}
</p>
<h3>{% trans "Status" %}</h3> <h3>{% trans "Status" %}</h3>

View File

@ -26,18 +26,11 @@ import logging
from .forms import MumbleForm from .forms import MumbleForm
from plinth import actions from plinth import actions
from plinth import package
from plinth.modules import mumble from plinth.modules import mumble
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def on_install():
"""Notify that the service is now enabled."""
mumble.service.notify_enabled(None, True)
@package.required(['mumble-server'], on_install=on_install)
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
status = get_status() status = get_status()
@ -55,7 +48,8 @@ def index(request):
form = MumbleForm(initial=status, prefix='mumble') form = MumbleForm(initial=status, prefix='mumble')
return TemplateResponse(request, 'mumble.html', return TemplateResponse(request, 'mumble.html',
{'title': _('Voice Chat (Mumble)'), {'title': mumble.title,
'description': mumble.description,
'status': status, 'status': status,
'form': form}) 'form': form})

View File

@ -31,7 +31,13 @@ SERVICES = (
('ssh', _('SSH'), 22), ('ssh', _('SSH'), 22),
) )
depends = ['plinth.modules.system'] version = 1
is_essential = True
depends = ['system']
title = _('Name Services')
domain_types = {} domain_types = {}
domains = {} domains = {}
@ -42,8 +48,7 @@ logger = logging.getLogger(__name__)
def init(): def init():
"""Initialize the names module.""" """Initialize the names module."""
menu = cfg.main_menu.get('system:index') menu = cfg.main_menu.get('system:index')
menu.add_urlname(_('Name Services'), 'glyphicon-tag', menu.add_urlname(title, 'glyphicon-tag', 'names:index', 19)
'names:index', 19)
domain_added.connect(on_domain_added) domain_added.connect(on_domain_added)
domain_removed.connect(on_domain_removed) domain_removed.connect(on_domain_removed)
@ -54,6 +59,7 @@ def on_domain_added(sender, domain_type, name='', description='',
"""Add domain to global list.""" """Add domain to global list."""
if not domain_type: if not domain_type:
return return
domain_types[domain_type] = description domain_types[domain_type] = description
if not name: if not name:

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,9 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{{ title }}</h2>
<div class="row"> <div class="row">
<div class="col-sm-5"> <div class="col-sm-5">

View File

@ -24,6 +24,7 @@ from django.utils.translation import ugettext as _
from . import SERVICES, get_domain_types, get_description from . import SERVICES, get_domain_types, get_description
from . import get_domain, get_services_status from . import get_domain, get_services_status
from plinth.modules import names
def index(request): def index(request):
@ -31,7 +32,7 @@ def index(request):
status = get_status() status = get_status()
return TemplateResponse(request, 'names.html', return TemplateResponse(request, 'names.html',
{'title': _('Name Services'), {'title': names.title,
'status': status}) 'status': status})

View File

@ -19,23 +19,37 @@
Plinth module to interface with network-manager Plinth module to interface with network-manager
""" """
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _
from logging import Logger from logging import Logger
import subprocess import subprocess
from . import networks
from .networks import init
from plinth import action_utils from plinth import action_utils
from plinth import cfg
from plinth import network from plinth import network
__all__ = ['networks', 'init'] version = 1
depends = ['plinth.modules.system'] is_essential = True
depends = ['system']
title = _('Networks')
logger = Logger(__name__) logger = Logger(__name__)
def init():
"""Initialize the Networks module."""
menu = cfg.main_menu.get('system:index')
menu.add_urlname(title, 'glyphicon-signal', 'networks:index', 18)
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['network-manager'])
def diagnose(): def diagnose():
"""Run diagnostics and return the results.""" """Run diagnostics and return the results."""
results = [] results = []
@ -44,8 +58,10 @@ def diagnose():
addresses = _get_interface_addresses(interfaces) addresses = _get_interface_addresses(interfaces)
for address in addresses: for address in addresses:
results.append(action_utils.diagnose_port_listening(53, 'tcp', address)) results.append(
results.append(action_utils.diagnose_port_listening(53, 'udp', address)) action_utils.diagnose_port_listening(53, 'tcp', address))
results.append(
action_utils.diagnose_port_listening(53, 'udp', address))
results.append(_diagnose_dnssec('4')) results.append(_diagnose_dnssec('4'))
results.append(_diagnose_dnssec('6')) results.append(_diagnose_dnssec('6'))

View File

@ -25,9 +25,7 @@ from logging import Logger
from .forms import (ConnectionTypeSelectForm, EthernetForm, PPPoEForm, from .forms import (ConnectionTypeSelectForm, EthernetForm, PPPoEForm,
WifiForm) WifiForm)
from plinth import cfg
from plinth import network from plinth import network
from plinth import package
logger = Logger(__name__) logger = Logger(__name__)
@ -40,14 +38,6 @@ subsubmenu = [{'url': reverse_lazy('networks:index'),
'text': ugettext_lazy('Add Connection')}] 'text': ugettext_lazy('Add Connection')}]
def init():
"""Initialize the Networks module."""
menu = cfg.main_menu.get('system:index')
menu.add_urlname(ugettext_lazy('Networks'), 'glyphicon-signal',
'networks:index', 18)
@package.required(['network-manager'])
def index(request): def index(request):
"""Show connection list.""" """Show connection list."""
connections = network.get_connection_list() connections = network.get_connection_list()

View File

@ -25,9 +25,25 @@ from plinth import actions
from plinth import action_utils from plinth import action_utils
from plinth import cfg from plinth import cfg
from plinth import service as service_module from plinth import service as service_module
from plinth.utils import format_lazy
depends = ['plinth.modules.apps'] version = 1
depends = ['apps']
title = _('Virtual Private Network (OpenVPN)')
description = [
format_lazy(
_('Virtual Private Network (VPN) is a technique for securely '
'connecting two devices in order to access resources of a '
'private network. While you are away from home, you can connect '
'to your {box_name} in order to join your home network and '
'access private/internal services provided by {box_name}. '
'You can also access the rest of the Internet via {box_name} '
'for added security and anonymity.'), box_name=_(cfg.box_name))
]
service = None service = None
@ -35,13 +51,16 @@ service = None
def init(): def init():
"""Intialize the OpenVPN module.""" """Intialize the OpenVPN module."""
menu = cfg.main_menu.get('apps:index') menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Virtual Private Network (OpenVPN)'), 'glyphicon-lock', menu.add_urlname(title, 'glyphicon-lock', 'openvpn:index', 850)
'openvpn:index', 850)
global service global service
service = service_module.Service( service = service_module.Service(
'openvpn', _('OpenVPN'), ['openvpn'], 'openvpn', title, ['openvpn'], is_external=True, enabled=is_enabled())
is_external=True, enabled=is_enabled())
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['openvpn', 'easy-rsa'])
def is_enabled(): def is_enabled():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -30,21 +30,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block configuration %}
<h2>{% trans "Virtual Private Network (OpenVPN)" %}</h2>
<p>
{% blocktrans trimmed %}
Virtual Private Network (VPN) is a technique for securely
connecting two devices in order to access resources of a
private network. While you are away from home, you can connect
to your {{ box_name }} in order to join your home network and
access private/internal services provided by {{ box_name }}.
You can also access the rest of the Internet via {{ box_name }}
for added security and anonymity.
{% endblocktrans %}
</p>
{% if status.is_setup %} {% if status.is_setup %}

View File

@ -29,7 +29,6 @@ import logging
from .forms import OpenVpnForm from .forms import OpenVpnForm
from plinth import actions from plinth import actions
from plinth import package
from plinth.modules import openvpn from plinth.modules import openvpn
from plinth.modules.config import config from plinth.modules.config import config
@ -38,7 +37,6 @@ logger = logging.getLogger(__name__)
setup_process = None setup_process = None
@package.required(['openvpn', 'easy-rsa'])
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
status = get_status() status = get_status()
@ -59,7 +57,8 @@ def index(request):
form = OpenVpnForm(initial=status, prefix='openvpn') form = OpenVpnForm(initial=status, prefix='openvpn')
return TemplateResponse(request, 'openvpn.html', return TemplateResponse(request, 'openvpn.html',
{'title': _('Virtual Private Network (OpenVPN)'), {'title': openvpn.title,
'description': openvpn.description,
'status': status, 'status': status,
'form': form}) 'form': form})

View File

@ -19,14 +19,60 @@
Plinth module to configure ownCloud Plinth module to configure ownCloud
""" """
from . import owncloud from django.utils.translation import ugettext_lazy as _
from .owncloud import init
from plinth import actions
from plinth import action_utils from plinth import action_utils
from plinth import cfg
from plinth import service as service_module
__all__ = ['owncloud', 'init'] version = 1
depends = ['plinth.modules.apps'] depends = ['apps']
title = _('File Hosting (ownCloud)')
description = [
_('ownCloud gives you universal access to your files through a web '
'interface or WebDAV. It also provides a platform to easily view '
'& sync your contacts, calendars and bookmarks across all your '
'devices and enables basic editing right on the web. Installation '
'has minimal server requirements, doesn\'t need special '
'permissions and is quick. ownCloud is extendable via a simple '
'but powerful API for applications and plugins.'),
_('When enabled, the ownCloud installation will be available '
'from <a href="/owncloud">/owncloud</a> path on the web server. '
'Visit this URL to set up the initial administration account for '
'ownCloud.')
]
service = None
def init():
"""Initialize the ownCloud module"""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(title, 'glyphicon-picture', 'owncloud:index', 700)
global service
service = service_module.Service(
'owncloud', title, ['http', 'https'], is_external=True,
enabled=is_enabled())
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['postgresql', 'php5-pgsql', 'owncloud', 'php-dropbox',
'php-google-api-php-client'])
helper.call('post', actions.superuser_run, 'owncloud-setup', ['enable'])
helper.call('post', service.notify_enabled, None, True)
def is_enabled():
"""Return whether the module is enabled."""
output = actions.run('owncloud-setup', ['status'])
return 'enable' in output.split()
def diagnose(): def diagnose():

View File

@ -0,0 +1,30 @@
#
# 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/>.
#
"""
Forms for configuring ownCloud.
"""
from django import forms
from django.utils.translation import ugettext_lazy as _
class OwnCloudForm(forms.Form): # pylint: disable-msg=W0232
"""ownCloud configuration form"""
enabled = forms.BooleanField(
label=_('Enable ownCloud'),
required=False)

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,30 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{% trans "File Hosting (ownCloud)" %}</h2>
<p>
{% blocktrans trimmed %}
ownCloud gives you universal access to your files through a web
interface or WebDAV. It also provides a platform to easily view
& sync your contacts, calendars and bookmarks across all your
devices and enables basic editing right on the web. Installation
has minimal server requirements, doesn't need special
permissions and is quick. ownCloud is extendable via a simple
but powerful API for applications and plugins.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
When enabled, the ownCloud installation will be available
from <a href="/owncloud">/owncloud</a> path on the web server.
Visit this URL to set up the initial administration account for
ownCloud.
{% endblocktrans %}
</p>
{% include "diagnostics_button.html" with module="owncloud" %} {% include "diagnostics_button.html" with module="owncloud" %}

View File

@ -21,7 +21,7 @@ URLs for the ownCloud module
from django.conf.urls import url from django.conf.urls import url
from . import owncloud as views from . import views
urlpatterns = [ urlpatterns = [

View File

@ -19,47 +19,15 @@
Plinth module for configuring ownCloud. Plinth module for configuring ownCloud.
""" """
from django import forms
from django.contrib import messages from django.contrib import messages
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from .forms import OwnCloudForm
from plinth import actions from plinth import actions
from plinth import cfg from plinth.modules import owncloud
from plinth import package
from plinth import service as service_module
service = None
class OwnCloudForm(forms.Form): # pylint: disable-msg=W0232
"""ownCloud configuration form"""
enabled = forms.BooleanField(label=_('Enable ownCloud'), required=False)
def init():
"""Initialize the ownCloud module"""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('File Hosting (ownCloud)'), 'glyphicon-picture',
'owncloud:index', 700)
status = get_status()
global service # pylint: disable-msg=W0603
service = service_module.Service(
'owncloud', _('ownCloud'), ['http', 'https'], is_external=True,
enabled=status['enabled'])
def on_install():
"""Tasks to run after package install."""
actions.superuser_run('owncloud-setup', ['enable'])
service.notify_enabled(None, True)
@package.required(['postgresql', 'php5-pgsql', 'owncloud', 'php-dropbox',
'php-google-api-php-client'], on_install=on_install)
def index(request): def index(request):
"""Serve the ownCloud configuration page""" """Serve the ownCloud configuration page"""
status = get_status() status = get_status()
@ -77,14 +45,14 @@ def index(request):
form = OwnCloudForm(initial=status, prefix='owncloud') form = OwnCloudForm(initial=status, prefix='owncloud')
return TemplateResponse(request, 'owncloud.html', return TemplateResponse(request, 'owncloud.html',
{'title': _('ownCloud'), {'title': owncloud.title,
'description': owncloud.description,
'form': form}) 'form': form})
def get_status(): def get_status():
"""Return the current status""" """Return the current status"""
output = actions.run('owncloud-setup', ['status']) return {'enabled': owncloud.is_enabled()}
return {'enabled': 'enable' in output.split()}
def _apply_changes(request, old_status, new_status): def _apply_changes(request, old_status, new_status):
@ -104,4 +72,4 @@ def _apply_changes(request, old_status, new_status):
# Send a signal to other modules that the service is # Send a signal to other modules that the service is
# enabled/disabled # enabled/disabled
service.notify_enabled(None, new_status['enabled']) owncloud.service.notify_enabled(None, new_status['enabled'])

View File

@ -21,19 +21,59 @@ Plinth module to configure PageKite
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from plinth import cfg from plinth import cfg
from plinth.utils import format_lazy
from . import utils from . import utils
__all__ = ['init'] version = 1
depends = ['plinth.modules.apps', 'plinth.modules.names'] depends = ['apps', 'names']
title = _('Public Visibility (PageKite)')
description = [
format_lazy(
_('PageKite is a system for exposing {box_name} services when '
'you don\'t have a direct connection to the Internet. You only '
'need this if your {box_name} services are unreachable from '
'the rest of the Internet. This includes the following '
'situations:'), box_name=_(cfg.box_name)),
format_lazy(
_('{box_name} is behind a restricted firewall.'),
box_name=_(cfg.box_name)),
format_lazy(
_('{box_name} is connected to a (wireless) router which you '
'don\'t control.'), box_name=_(cfg.box_name)),
_('Your ISP does not provide you an external IP address and '
'instead provides Internet connection through NAT.'),
_('Your ISP does not provide you a static IP address and your IP '
'address changes evertime you connect to Internet.'),
_('Your ISP limits incoming connections.'),
format_lazy(
_('PageKite works around NAT, firewalls and IP-address limitations '
'by using a combination of tunnels and reverse proxies. You can '
'use any pagekite service provider, for example '
'<a href="https://pagekite.net">pagekite.net</a>. In future it '
'might be possible to use your buddy\'s {box_name} for this.'),
box_name=_(cfg.box_name))
]
def init(): def init():
"""Intialize the PageKite module""" """Intialize the PageKite module"""
menu = cfg.main_menu.get('apps:index') menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Public Visibility (PageKite)'), menu.add_urlname(title, 'glyphicon-flag', 'pagekite:index', 800)
'glyphicon-flag', 'pagekite:index', 800)
# Register kite name with Name Services module. # Register kite name with Name Services module.
utils.update_names_module(initial_registration=True) utils.update_names_module(initial_registration=True)
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['pagekite'])

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -20,58 +20,7 @@
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<p>
{% blocktrans trimmed %}
PageKite is a system for exposing {{ box_name }} services when
you don't have a direct connection to the Internet. You only
need this if your {{ box_name }} services are unreachable from
the rest of the Internet. This includes the following
situations:
{% endblocktrans %}
</p>
<ul>
<li>
{% blocktrans trimmed %}
{{ box_name }} is behind a restricted firewall.
{% endblocktrans %}
</li>
<li>
{% blocktrans trimmed %}
{{ box_name }} is connected to a (wireless) router which you
don't control.
{% endblocktrans %}
</li>
<li>
{% blocktrans trimmed %}
Your ISP does not provide you an external IP address and
instead provides Internet connection through NAT.
{% endblocktrans %}
</li>
<li>
{% blocktrans trimmed %}
Your ISP does not provide you a static IP address and your IP
address changes evertime you connect to Internet.
{% endblocktrans %}
</li>
<li>{% trans "Your ISP limits incoming connections." %}</li>
</ul>
<p>
{% blocktrans trimmed %}
PageKite works around NAT, firewalls and IP-address limitations
by using a combination of tunnels and reverse proxies. You can
use any pagekite service provider, for example
<a href="https://pagekite.net">pagekite.net</a>. In future it
might be possible to use your buddy's {{ box_name }} for this.
{% endblocktrans %}
</p>
<p> <p>
<a class="btn btn-primary btn-md" href="{% url 'pagekite:configure' %}"> <a class="btn btn-primary btn-md" href="{% url 'pagekite:configure' %}">

View File

@ -18,18 +18,16 @@
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
from django.http.response import HttpResponseRedirect from django.http.response import HttpResponseRedirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import View, TemplateView from django.views.generic import View, TemplateView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from plinth import package
from . import utils from . import utils
from .forms import ConfigurationForm, StandardServiceForm, \ from .forms import ConfigurationForm, StandardServiceForm, \
AddCustomServiceForm, DeleteCustomServiceForm AddCustomServiceForm, DeleteCustomServiceForm
from plinth.modules import pagekite
required_packages = ('pagekite',)
subsubmenu = [{'url': reverse_lazy('pagekite:index'), subsubmenu = [{'url': reverse_lazy('pagekite:index'),
'text': _('About PageKite')}, 'text': _('About PageKite')},
{'url': reverse_lazy('pagekite:configure'), {'url': reverse_lazy('pagekite:configure'),
@ -43,7 +41,8 @@ subsubmenu = [{'url': reverse_lazy('pagekite:index'),
def index(request): def index(request):
"""Serve introduction page""" """Serve introduction page"""
return TemplateResponse(request, 'pagekite_introduction.html', return TemplateResponse(request, 'pagekite_introduction.html',
{'title': _('Public Visibility (PageKite)'), {'title': pagekite.title,
'description': pagekite.description,
'subsubmenu': subsubmenu}) 'subsubmenu': subsubmenu})
@ -59,7 +58,6 @@ class ContextMixin(object):
context['subsubmenu'] = subsubmenu context['subsubmenu'] = subsubmenu
return context return context
@method_decorator(package.required(required_packages))
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(ContextMixin, self).dispatch(*args, **kwargs) return super(ContextMixin, self).dispatch(*args, **kwargs)
@ -81,8 +79,9 @@ class CustomServiceView(ContextMixin, TemplateView):
unused, custom_services = utils.get_pagekite_services() unused, custom_services = utils.get_pagekite_services()
for service in custom_services: for service in custom_services:
service['form'] = AddCustomServiceForm(initial=service) service['form'] = AddCustomServiceForm(initial=service)
context['custom_services'] = [utils.prepare_service_for_display(service) context['custom_services'] = [
for service in custom_services] utils.prepare_service_for_display(service)
for service in custom_services]
context.update(utils.get_kite_details()) context.update(utils.get_kite_details())
return context return context

View File

@ -23,11 +23,20 @@ from django.utils.translation import ugettext_lazy as _
from plinth import cfg from plinth import cfg
depends = ['plinth.modules.system'] version = 1
is_essential = True
depends = ['system']
title = _('Power')
description = [
_('Restart or shut down the system.')
]
def init(): def init():
"""Initialize the power module.""" """Initialize the power module."""
menu = cfg.main_menu.get('system:index') menu = cfg.main_menu.get('system:index')
menu.add_urlname(_('Power'), 'glyphicon-off', menu.add_urlname(title, 'glyphicon-off', 'power:index', 1000)
'power:index', 1000)

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,13 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{{ title }}</h2>
<p>
{% blocktrans trimmed %}Restart or shut down the system.{% endblocktrans %}
</p>
<p> <p>
<a class="btn btn-default btn-md" href="{% url 'power:restart' %}"> <a class="btn btn-default btn-md" href="{% url 'power:restart' %}">

View File

@ -26,11 +26,14 @@ from django.template.response import TemplateResponse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from plinth import actions from plinth import actions
from plinth.modules import power
def index(request): def index(request):
"""Serve power controls page.""" """Serve power controls page."""
return TemplateResponse(request, 'power.html', {'title': _('Power')}) return TemplateResponse(request, 'power.html',
{'title': power.title,
'description': power.description})
def restart(request): def restart(request):

View File

@ -20,15 +20,36 @@ Plinth module to configure Privoxy.
""" """
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import json
from plinth import actions from plinth import actions
from plinth import action_utils from plinth import action_utils
from plinth import cfg from plinth import cfg
from plinth import service as service_module from plinth import service as service_module
from plinth.utils import format_lazy
depends = ['plinth.modules.apps'] version = 1
is_essential = False
depends = ['apps']
title = _('Web Proxy (Privoxy)')
description = [
_('Privoxy is a non-caching web proxy with advanced filtering '
'capabilities for enhancing privacy, modifying web page data and '
'HTTP headers, controlling access, and removing ads and other '
'obnoxious Internet junk. '),
format_lazy(
_('You can use Privoxy by modifying your browser proxy settings to '
'your {box_name} hostname (or IP address) with port 8118. '
'While using Privoxy, you can see its configuration details and '
'documentation at '
'<a href="http://config.privoxy.org">http://config.privoxy.org/</a> '
'or <a href="http://p.p">http://p.p</a>.'), box_name=_(cfg.box_name))
]
service = None service = None
@ -36,13 +57,18 @@ service = None
def init(): def init():
"""Intialize the module.""" """Intialize the module."""
menu = cfg.main_menu.get('apps:index') menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Web Proxy (Privoxy)'), 'glyphicon-cloud-upload', menu.add_urlname(title, 'glyphicon-cloud-upload', 'privoxy:index', 1000)
'privoxy:index', 1000)
global service global service
service = service_module.Service( service = service_module.Service(
'privoxy', _('Privoxy Web Proxy'), 'privoxy', title, is_external=False, enabled=is_enabled())
is_external=False, enabled=is_enabled())
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['privoxy'])
helper.call('post', actions.superuser_run, 'privoxy', ['setup'])
helper.call('post', service.notify_enabled, None, True)
def is_enabled(): def is_enabled():
@ -84,9 +110,7 @@ def diagnose_url_with_proxy():
result = action_utils.diagnose_url(url, kind=address['kind'], env=env) result = action_utils.diagnose_url(url, kind=address['kind'], env=env)
result[0] = _('Access {url} with proxy {proxy} on tcp{kind}') \ result[0] = _('Access {url} with proxy {proxy} on tcp{kind}') \
.format(url=url, proxy=proxy, kind=address['kind']) .format(url=url, proxy=proxy, kind=address['kind'])
results.append(result) results.append(result)
return results return results

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,29 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{% trans "Web Proxy (Privoxy)" %}</h2>
<p>
{% blocktrans trimmed %}
Privoxy is a non-caching web proxy with advanced filtering
capabilities for enhancing privacy, modifying web page data and
HTTP headers, controlling access, and removing ads and other
obnoxious Internet junk.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
You can use Privoxy by modifying your browser proxy settings to
your {{ box_name }} hostname (or IP address) with port 8118.
While using Privoxy, you can see its configuration details and
documentation at
<a href="http://config.privoxy.org">http://config.privoxy.org/</a>
or <a href="http://p.p">http://p.p</a>.
{% endblocktrans %}
</p>
<h3>{% trans "Status" %}</h3> <h3>{% trans "Status" %}</h3>

View File

@ -26,19 +26,11 @@ import logging
from .forms import PrivoxyForm from .forms import PrivoxyForm
from plinth import actions from plinth import actions
from plinth import package
from plinth.modules import privoxy from plinth.modules import privoxy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def on_install():
"""Notify that the service is now enabled."""
actions.superuser_run('privoxy', ['setup'])
privoxy.service.notify_enabled(None, True)
@package.required(['privoxy'], on_install=on_install)
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
status = get_status() status = get_status()
@ -56,7 +48,8 @@ def index(request):
form = PrivoxyForm(initial=status, prefix='privoxy') form = PrivoxyForm(initial=status, prefix='privoxy')
return TemplateResponse(request, 'privoxy.html', return TemplateResponse(request, 'privoxy.html',
{'title': _('Web Proxy (Privoxy)'), {'title': privoxy.title,
'description': privoxy.description,
'status': status, 'status': status,
'form': form}) 'form': form})

View File

@ -16,7 +16,7 @@
# #
""" """
Plinth module for quassel. Plinth module for Quassel.
""" """
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -24,8 +24,30 @@ from django.utils.translation import ugettext_lazy as _
from plinth import action_utils from plinth import action_utils
from plinth import cfg from plinth import cfg
from plinth import service as service_module from plinth import service as service_module
from plinth.utils import format_lazy
depends = ['plinth.modules.apps'] version = 1
depends = ['apps']
title = _('IRC Client (Quassel)')
description = [
format_lazy(
_('Quassel is an IRC application that is split into two parts, a '
'"core" and a "client". This allows the core to remain connected '
'to IRC servers, and to continue receiving messages, even when '
'the client is disconnected. {box_name} can run the Quassel '
'core service keeping you always online and one or more Quassel '
'clients from a desktop or a mobile can be used to connect and '
'disconnect from it.'), box_name=_(cfg.box_name)),
_('You can connect to your Quassel core on the default Quassel port '
'4242. Clients to connect to Quassel from your '
'<a href="http://quassel-irc.org/downloads">desktop</a> and '
'<a href="http://quasseldroid.iskrembilen.com/">mobile</a> devices '
'are available.')
]
service = None service = None
@ -33,13 +55,17 @@ service = None
def init(): def init():
"""Initialize the quassel module.""" """Initialize the quassel module."""
menu = cfg.main_menu.get('apps:index') menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('IRC Client (Quassel)'), 'glyphicon-retweet', menu.add_urlname(title, 'glyphicon-retweet', 'quassel:index', 730)
'quassel:index', 730)
global service global service
service = service_module.Service( service = service_module.Service(
'quassel-plinth', _('Quassel IRC Client'), 'quassel-plinth', title, is_external=True, enabled=is_enabled())
is_external=True, enabled=is_enabled())
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['quassel-core'])
helper.call('post', service.notify_enabled, None, True)
def is_enabled(): def is_enabled():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,31 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{% trans "IRC Client (Quassel)" %}</h2>
<p>
{% blocktrans trimmed %}
Quassel is an IRC application that is split into two parts, a
"core" and a "client". This allows the core to remain connected
to IRC servers, and to continue receiving messages, even when
the client is disconnected. {{ box_name }} can run the Quassel
core service keeping you always online and one or more Quassel
clients from a desktop or a mobile can be used to connect and
disconnect from it.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
You can connect to your Quassel core on the default Quassel port
4242. Clients to connect to Quassel from your
<a href="http://quassel-irc.org/downloads">desktop</a> and
<a href="http://quasseldroid.iskrembilen.com/">mobile</a> devices
are available.
{% endblocktrans %}
</p>
<h3>{% trans "Status" %}</h3> <h3>{% trans "Status" %}</h3>

View File

@ -25,16 +25,9 @@ from django.utils.translation import ugettext as _
from .forms import QuasselForm from .forms import QuasselForm
from plinth import actions from plinth import actions
from plinth import package
from plinth.modules import quassel from plinth.modules import quassel
def on_install():
"""Notify that the service is now enabled."""
quassel.service.notify_enabled(None, True)
@package.required(['quassel-core'], on_install=on_install)
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
status = get_status() status = get_status()
@ -51,7 +44,8 @@ def index(request):
form = QuasselForm(initial=status, prefix='quassel') form = QuasselForm(initial=status, prefix='quassel')
return TemplateResponse(request, 'quassel.html', return TemplateResponse(request, 'quassel.html',
{'title': _('IRC Client (Quassel)'), {'title': quassel.title,
'description': quassel.description,
'status': status, 'status': status,
'form': form}) 'form': form})

View File

@ -21,11 +21,37 @@ Plinth module for repro.
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from plinth import actions
from plinth import action_utils from plinth import action_utils
from plinth import cfg from plinth import cfg
from plinth import service as service_module from plinth import service as service_module
depends = ['plinth.modules.apps'] version = 1
depends = ['apps']
title = _('SIP Server (repro)')
description = [
_('repro provides various SIP services that a SIP softphone can utilize '
'to provide audio and video calls as well as presence and instant '
'messaging. repro provides a server and SIP user accounts that clients '
'can use to let their presence known. It also acts as a proxy to '
'federate SIP communications to other servers on the Internet similar '
'to email.'),
_('To make SIP calls, a client application is needed. Available clients '
'include <a href="https://jitsi.org/">Jitsi</a> (for computers) and '
'<a href="https://f-droid.org/repository/browse/?fdid=com.csipsimple"> '
'CSipSimple</a> (for Android phones).'),
_('<strong>Note:</strong> Before using repro, domains and users will '
'need to be configured using the <a href="/repro/domains.html">'
'web-based configuration panel</a>. Users in the <em>admin</em> group '
'will be able to log in to the repro configuration panel. After setting '
'the domain, it is required to restart the repro service. Disable the '
'service and re-enable it.'),
]
service = None service = None
@ -33,13 +59,19 @@ service = None
def init(): def init():
"""Initialize the repro module.""" """Initialize the repro module."""
menu = cfg.main_menu.get('apps:index') menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('SIP Server (repro)'), 'glyphicon-phone-alt', menu.add_urlname(title, 'glyphicon-phone-alt', 'repro:index', 825)
'repro:index', 825)
global service global service
service = service_module.Service( service = service_module.Service(
'repro', _('repro SIP Server'), ['sip-plinth', 'sip-tls-plinth'], 'repro', title, ['sip-plinth', 'sip-tls-plinth'], is_external=True,
is_external=True, enabled=is_enabled()) enabled=is_enabled())
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['repro'])
helper.call('post', actions.superuser_run, 'repro', ['setup'])
helper.call('post', service.notify_enabled, None, True)
def is_enabled(): def is_enabled():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,39 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{% trans "SIP Server (repro)" %}</h2>
<p>
{% blocktrans trimmed %}
repro provides various SIP services that a SIP softphone can utilize to
provide audio and video calls as well as presence and instant messaging.
repro provides a server and SIP user accounts that clients can use to let
their presence known. It also acts as a proxy to federate SIP
communications to other servers on the Internet similar to email.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
To make SIP calls, a client application is needed. Available clients
include <a href="https://jitsi.org/">Jitsi</a> (for computers) and
<a href="https://f-droid.org/repository/browse/?fdid=com.csipsimple">
CSipSimple</a> (for Android phones).
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
<strong>Note:</strong> Before using repro, domains and users will need
to be configured using the <a href="/repro/domains.html">web-based
configuration panel</a>. Users in the <em>admin</em> group will be able
to log in to the repro configuration panel. After setting the domain, it
is required to restart the repro service. Disable the service and
re-enable it.
{% endblocktrans %}
</p>
<h3>{% trans "Status" %}</h3> <h3>{% trans "Status" %}</h3>

View File

@ -25,17 +25,9 @@ from django.utils.translation import ugettext as _
from .forms import ReproForm from .forms import ReproForm
from plinth import actions from plinth import actions
from plinth import package
from plinth.modules import repro from plinth.modules import repro
def on_install():
"""Notify that the service is now enabled."""
actions.superuser_run('repro', ['setup'])
repro.service.notify_enabled(None, True)
@package.required(['repro'], on_install=on_install)
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
status = get_status() status = get_status()
@ -52,7 +44,8 @@ def index(request):
form = ReproForm(initial=status, prefix='repro') form = ReproForm(initial=status, prefix='repro')
return TemplateResponse(request, 'repro.html', return TemplateResponse(request, 'repro.html',
{'title': _('SIP Server (repro)'), {'title': repro.title,
'description': repro.description,
'status': status, 'status': status,
'form': form}) 'form': form})

View File

@ -22,24 +22,44 @@ Plinth module to configure reStore.
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from plinth import action_utils, cfg from plinth import action_utils, cfg
from plinth import service as service_module from plinth import service as service_module
from plinth.utils import format_lazy
service = None service = None
__all__ = ['init'] version = 1
depends = ['plinth.modules.apps'] depends = ['apps']
title = _('Unhosted Storage (reStore)')
description = [
format_lazy(
_('reStore is a server for <a href=\'https://unhosted.org/\'>'
'unhosted</a> web applications. The idea is to uncouple web '
'applications from data. No matter where a web application is '
'served from, the data can be stored on an unhosted storage '
'server of user\'s choice. With reStore, your {box_name} becomes '
'your unhosted storage server.'), box_name=_(cfg.box_name)),
_('You can create and edit accounts in the '
'<a href=\'/restore/\'>reStore web-interface</a>.')
]
def init(): def init():
"""Initialize the reStore module.""" """Initialize the reStore module."""
menu = cfg.main_menu.get('apps:index') menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Unhosted Storage (reStore)'), 'glyphicon-hdd', menu.add_urlname(title, 'glyphicon-hdd', 'restore:index', 750)
'restore:index', 750)
global service global service
service = service_module.Service( service = service_module.Service(
'node-restore', _('reStore'), ['http', 'https'], 'node-restore', title, ['http', 'https'], is_external=False,
is_external=False, enabled=is_enabled()) enabled=is_enabled())
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['node-restore'])
def is_enabled(): def is_enabled():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,27 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{% trans "Unhosted Storage (reStore)" %}</h2>
<p>
{% blocktrans trimmed %}
reStore is a server for <a href='https://unhosted.org/'>unhosted</a>
web applications. The idea is to uncouple web applications from
data. No matter where a web application is served from, the
data can be stored on an unhosted storage server of user's
choice. With reStore, your {{ box_name }} becomes your
unhosted storage server.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
You can create and edit accounts in the
<a href='/restore/'>reStore web-interface</a>.
{% endblocktrans %}
</p>
<h3>Configuration</h3> <h3>Configuration</h3>

View File

@ -24,11 +24,10 @@ from django.template.response import TemplateResponse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from .forms import ReStoreForm from .forms import ReStoreForm
from plinth import actions, package from plinth import actions
from plinth.modules import restore from plinth.modules import restore
@package.required(['node-restore'])
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
status = get_status() status = get_status()
@ -43,7 +42,8 @@ def index(request):
form = ReStoreForm(initial=status, prefix='restore') form = ReStoreForm(initial=status, prefix='restore')
return TemplateResponse(request, 'restore_index.html', return TemplateResponse(request, 'restore_index.html',
{'title': _('Unhosted Storage (reStore)'), {'title': restore.title,
'description': restore.description,
'status': status, 'status': status,
'form': form}) 'form': form})

View File

@ -24,17 +24,49 @@ from django.utils.translation import ugettext_lazy as _
from plinth import actions from plinth import actions
from plinth import action_utils from plinth import action_utils
from plinth import cfg from plinth import cfg
from plinth import service as service_module
depends = ['plinth.modules.apps'] version = 1
depends = ['apps']
title = _('Email Client (Roundcube)')
description = [
_('Roundcube webmail is a browser-based multilingual IMAP '
'client with an application-like user interface. It provides '
'full functionality you expect from an email client, including '
'MIME support, address book, folder manipulation, message '
'searching and spell checking.'),
_('You can access Roundcube from <a href="/roundcube">'
'/roundcube</a>. Provide the username and password of the email '
'account you wish to access followed by the domain name of the '
'IMAP server for your email provider, like <code>imap.example.com'
'</code>. For IMAP over SSL (recommended), fill the server field '
'like <code>imaps://imap.example.com</code>.'),
_('For Gmail, username will be your Gmail address, password will be '
'your Google account password and server will be '
'<code>imaps://imap.gmail.com</code>. Note that you will also need '
'to enable "Less secure apps" in your Google account settings '
'(<a href="https://www.google.com/settings/security/lesssecureapps"'
'>https://www.google.com/settings/security/lesssecureapps</a>).'),
]
def init(): def init():
"""Intialize the module.""" """Intialize the module."""
menu = cfg.main_menu.get('apps:index') menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Email Client (Roundcube)'), 'glyphicon-envelope', menu.add_urlname(title, 'glyphicon-envelope', 'roundcube:index', 600)
'roundcube:index', 600)
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.call('pre', actions.superuser_run, 'roundcube', ['pre-install'])
helper.install(['sqlite3', 'roundcube', 'roundcube-sqlite3'])
helper.call('pre', actions.superuser_run, 'roundcube', ['setup'])
def is_enabled(): def is_enabled():
"""Return whether the module is enabled.""" """Return whether the module is enabled."""

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,41 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{% trans "Email Client (Roundcube)" %}</h2>
<p>
{% blocktrans trimmed %}
Roundcube webmail is a browser-based multilingual IMAP client
with an application-like user interface. It provides full
functionality you expect from an email client, including MIME
support, address book, folder manipulation, message searching
and spell checking.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
You can access Roundcube from <a href="/roundcube">/roundcube</a>.
Provide the username and password of the email account you wish
to access followed by the domain name of the IMAP server for
your email provider, like <code>imap.example.com</code>. For
IMAP over SSL (recommended), fill the server field like
<code>imaps://imap.example.com</code>.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
For Gmail, username will be your Gmail address, password will be
your Google account password and server will be
<code>imaps://imap.gmail.com</code>. Note that you will also need
to enable "Less secure apps" in your Google account settings
(<a href="https://www.google.com/settings/security/lesssecureapps"
>https://www.google.com/settings/security/lesssecureapps</a>).
{% endblocktrans %}
</p>
{% include "diagnostics_button.html" with module="roundcube" %} {% include "diagnostics_button.html" with module="roundcube" %}

View File

@ -26,24 +26,11 @@ import logging
from .forms import RoundcubeForm from .forms import RoundcubeForm
from plinth import actions from plinth import actions
from plinth import package
from plinth.modules import roundcube from plinth.modules import roundcube
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def before_install():
"""Preseed debconf values before the packages are installed."""
actions.superuser_run('roundcube', ['pre-install'])
def on_install():
"""Setup Roundcube Apache configuration."""
actions.superuser_run('roundcube', ['setup'])
@package.required(['sqlite3', 'roundcube', 'roundcube-sqlite3'],
before_install=before_install, on_install=on_install)
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
status = get_status() status = get_status()
@ -61,7 +48,8 @@ def index(request):
form = RoundcubeForm(initial=status, prefix='roundcube') form = RoundcubeForm(initial=status, prefix='roundcube')
return TemplateResponse(request, 'roundcube.html', return TemplateResponse(request, 'roundcube.html',
{'title': _('Email Client (Roundcube)'), {'title': roundcube.title,
'description': roundcube.description,
'status': status, 'status': status,
'form': form}) 'form': form})

View File

@ -26,7 +26,20 @@ from plinth import cfg
from plinth import service as service_module from plinth import service as service_module
depends = ['plinth.modules.apps'] version = 1
depends = ['apps']
title = _('Bookmarks (Shaarli)')
description = [
_('Shaarli allows you to save and share bookmarks.'),
_('When enabled, Shaarli will be available from <a href="/shaarli">'
'/shaarli</a> path on the web server. Note that Shaarli only supports a '
'single user account, which you will need to setup on the initial '
'visit.'),
]
service = None service = None
@ -34,13 +47,18 @@ service = None
def init(): def init():
"""Initialize the module.""" """Initialize the module."""
menu = cfg.main_menu.get('apps:index') menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Bookmarks (Shaarli)'), 'glyphicon-bookmark', menu.add_urlname(title, 'glyphicon-bookmark', 'shaarli:index', 350)
'shaarli:index', 350)
global service global service
service = service_module.Service( service = service_module.Service(
'shaarli', _('Shaarli'), ['http', 'https'], 'shaarli', title, ['http', 'https'], is_external=True,
is_external=True, enabled=is_enabled()) enabled=is_enabled())
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['shaarli'])
helper.call('post', service.notify_enabled, None, True)
def is_enabled(): def is_enabled():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,21 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{% trans "Bookmarks (Shaarli)" %}</h2>
<p>{% trans "Shaarli allows you to save and share bookmarks." %}</p>
<p>
{% blocktrans trimmed %}
When enabled, Shaarli will be available from <a href="/shaarli">/shaarli</a>
path on the web server. Note that Shaarli only supports a single
user account, which you will need to setup on the initial visit.
{% endblocktrans %}
</p>
<h3>{% trans "Configuration" %}</h3> <h3>{% trans "Configuration" %}</h3>

View File

@ -25,14 +25,9 @@ from django.utils.translation import ugettext as _
from .forms import ShaarliForm from .forms import ShaarliForm
from plinth import actions from plinth import actions
from plinth import package
from plinth.modules import shaarli from plinth.modules import shaarli
def on_install():
"""Notify that the service is now enabled."""
shaarli.service.notify_enabled(None, True)
@package.required(['shaarli'], on_install=on_install)
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
status = get_status() status = get_status()
@ -49,7 +44,8 @@ def index(request):
form = ShaarliForm(initial=status, prefix='shaarli') form = ShaarliForm(initial=status, prefix='shaarli')
return TemplateResponse(request, 'shaarli.html', return TemplateResponse(request, 'shaarli.html',
{'title': _('Bookmarks (Shaarli)'), {'title': shaarli.title,
'description': shaarli.description,
'status': status, 'status': status,
'form': form}) 'form': form})

View File

@ -19,8 +19,29 @@
Plinth module for system section page Plinth module for system section page
""" """
from . import system from django.utils.translation import ugettext_lazy as _
from .system import init
from plinth import cfg
from plinth.utils import format_lazy
__all__ = ['system', 'init'] version = 1
is_essential = 1
title = _('System Configuration')
description = [
format_lazy(
_('Here you can administrate the underlying system of your '
'{box_name}.'), box_name=_(cfg.box_name)),
format_lazy(
_('The options affect the {box_name} at its most general level, '
'so be careful!'), box_name=_(cfg.box_name))
]
def init():
"""Initialize the system module"""
cfg.main_menu.add_urlname(title, 'glyphicon-cog', 'system:index', 100)

View File

@ -1,4 +1,4 @@
{% extends 'base.html' %} {% extends 'app.html' %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -17,25 +17,3 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
{% endcomment %} {% endcomment %}
{% load i18n %}
{% block content %}
<h2>{% trans "System Configuration" %}</h2>
<p>
{% blocktrans trimmed %}
Here you can administrate the underlying system of your
{{ box_name }}.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
The options affect the {{ box_name }} at its most general level,
so be careful!
{% endblocktrans %}
</p>
{% endblock %}

View File

@ -21,7 +21,7 @@ URLs for the System module
from django.conf.urls import url from django.conf.urls import url
from . import system as views from . import views
urlpatterns = [ urlpatterns = [

View File

@ -16,18 +16,12 @@
# #
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.translation import ugettext_lazy as _
from plinth import cfg from plinth.modules import system
def init():
"""Initialize the system module"""
cfg.main_menu.add_urlname(_('System'), 'glyphicon-cog', 'system:index',
100)
def index(request): def index(request):
"""Serve the index page""" """Serve the index page"""
return TemplateResponse(request, 'system.html', return TemplateResponse(request, 'system.html',
{'title': _('System Configuration')}) {'title': system.title,
'description': system.description})

View File

@ -20,7 +20,7 @@ Plinth module to configure Tor.
""" """
import augeas import augeas
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _
import glob import glob
import itertools import itertools
@ -32,7 +32,20 @@ from plinth.modules.names import SERVICES
from plinth.signals import domain_added from plinth.signals import domain_added
depends = ['plinth.modules.apps', 'plinth.modules.names'] version = 1
depends = ['apps', 'names']
title = _('Anonymity Network (Tor)')
description = [
_('Tor is an anonymous communication system. You can learn more '
'about it from the <a href="https://www.torproject.org/">Tor '
'Project</a> website. For best protection when web surfing, the '
'Tor Project recommends that you use the '
'<a href="https://www.torproject.org/download/download-easy.html.en">'
'Tor Browser</a>.')
]
socks_service = None socks_service = None
bridge_service = None bridge_service = None
@ -45,8 +58,7 @@ APT_TOR_PREFIX = 'tor+'
def init(): def init():
"""Initialize the module.""" """Initialize the module."""
menu = cfg.main_menu.get('apps:index') menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Anonymity Network (Tor)'), 'glyphicon-eye-close', menu.add_urlname(title, 'glyphicon-eye-close', 'tor:index', 100)
'tor:index', 100)
global socks_service global socks_service
socks_service = service_module.Service( socks_service = service_module.Service(
@ -77,6 +89,17 @@ def init():
services=hs_services) services=hs_services)
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['tor', 'tor-geoipdb', 'torsocks', 'obfs4proxy',
'apt-transport-tor'])
helper.call('post', actions.superuser_run, 'tor', ['setup'])
helper.call('post', actions.superuser_run, 'tor',
['configure', '--apt-transport-tor', 'enable'])
helper.call('post', socks_service.notify_enabled, None, True)
helper.call('post', bridge_service.notify_enabled, None, True)
def is_enabled(): def is_enabled():
"""Return whether the module is enabled.""" """Return whether the module is enabled."""
return action_utils.service_is_enabled('tor') return action_utils.service_is_enabled('tor')

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -30,20 +30,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block configuration %}
<h2>{% trans "Anonymity Network (Tor)" %}</h2>
<p>
{% blocktrans trimmed %}
Tor is an anonymous communication system. You can learn more
about it from the <a href="https://www.torproject.org/">Tor
Project</a> website. For best protection when web surfing, the
Tor Project recommends that you use the
<a href="https://www.torproject.org/download/download-easy.html.en">
Tor Browser</a>.
{% endblocktrans %}
</p>
<h3>{% trans "Status" %}</h3> <h3>{% trans "Status" %}</h3>

View File

@ -25,7 +25,6 @@ from django.utils.translation import ugettext_lazy as _
from .forms import TorForm from .forms import TorForm
from plinth import actions from plinth import actions
from plinth import package
from plinth.errors import ActionError from plinth.errors import ActionError
from plinth.modules import tor from plinth.modules import tor
from plinth.modules.names import SERVICES from plinth.modules.names import SERVICES
@ -34,18 +33,6 @@ from plinth.signals import domain_added, domain_removed
config_process = None config_process = None
def on_install():
"""Setup Tor configuration as soon as it is installed."""
actions.superuser_run('tor', ['setup'])
actions.superuser_run('tor',
['configure', '--apt-transport-tor', 'enable'])
tor.socks_service.notify_enabled(None, True)
tor.bridge_service.notify_enabled(None, True)
@package.required(['tor', 'tor-geoipdb', 'torsocks', 'obfs4proxy',
'apt-transport-tor'],
on_install=on_install)
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
if config_process: if config_process:
@ -65,7 +52,8 @@ def index(request):
form = TorForm(initial=status, prefix='tor') form = TorForm(initial=status, prefix='tor')
return TemplateResponse(request, 'tor.html', return TemplateResponse(request, 'tor.html',
{'title': _('Tor Control Panel'), {'title': tor.title,
'description': tor.description,
'status': status, 'status': status,
'config_running': bool(config_process), 'config_running': bool(config_process),
'form': form}) 'form': form})

View File

@ -20,6 +20,7 @@ Plinth module to configure Transmission server
""" """
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import json
from plinth import actions from plinth import actions
from plinth import action_utils from plinth import action_utils
@ -27,7 +28,17 @@ from plinth import cfg
from plinth import service as service_module from plinth import service as service_module
depends = ['plinth.modules.apps'] version = 1
depends = ['apps']
title = _('BitTorrent (Transmission)')
description = [
_('BitTorrent is a peer-to-peer file sharing protocol. '
'Transmission daemon handles Bitorrent file sharing. Note that '
'BitTorrent is not anonymous.')
]
service = None service = None
@ -35,13 +46,25 @@ service = None
def init(): def init():
"""Intialize the Transmission module.""" """Intialize the Transmission module."""
menu = cfg.main_menu.get('apps:index') menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('BitTorrent (Transmission)'), 'glyphicon-save', menu.add_urlname(title, 'glyphicon-save', 'transmission:index', 300)
'transmission:index', 300)
global service global service
service = service_module.Service( service = service_module.Service(
'transmission', _('Transmission BitTorrent'), ['http', 'https'], 'transmission', title, ['http', 'https'], is_external=True,
is_external=True, enabled=is_enabled()) enabled=is_enabled())
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['transmission-daemon'])
new_configuration = {'rpc-whitelist-enabled': False}
helper.call('post', actions.superuser_run, 'transmission',
['merge-configuration'],
input=json.dumps(new_configuration).encode())
helper.call('post', actions.superuser_run, 'transmission', ['enable'])
helper.call('post', service.notify_enabled, None, True)
def is_enabled(): def is_enabled():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,17 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{% trans "BitTorrent (Transmission)" %}</h2>
<p>
{% blocktrans trimmed %}
BitTorrent is a peer-to-peer file sharing protocol.
Transmission daemon handles Bitorrent file sharing. Note that
BitTorrent is not anonymous.
{% endblocktrans %}
</p>
<p> <p>
{% blocktrans trimmed %} {% blocktrans trimmed %}

View File

@ -28,7 +28,6 @@ import socket
from .forms import TransmissionForm from .forms import TransmissionForm
from plinth import actions from plinth import actions
from plinth import package
from plinth.modules import transmission from plinth.modules import transmission
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -36,17 +35,6 @@ logger = logging.getLogger(__name__)
TRANSMISSION_CONFIG = '/etc/transmission-daemon/settings.json' TRANSMISSION_CONFIG = '/etc/transmission-daemon/settings.json'
def on_install():
"""Enable transmission as soon as it is installed."""
new_configuration = {'rpc-whitelist-enabled': False}
actions.superuser_run('transmission', ['merge-configuration'],
input=json.dumps(new_configuration).encode())
actions.superuser_run('transmission', ['enable'])
transmission.service.notify_enabled(None, True)
@package.required(['transmission-daemon'], on_install=on_install)
def index(request): def index(request):
"""Serve configuration page.""" """Serve configuration page."""
status = get_status() status = get_status()
@ -64,7 +52,8 @@ def index(request):
form = TransmissionForm(initial=status, prefix='transmission') form = TransmissionForm(initial=status, prefix='transmission')
return TemplateResponse(request, 'transmission.html', return TemplateResponse(request, 'transmission.html',
{'title': _('BitTorrent (Transmission)'), {'title': transmission.title,
'description': transmission.description,
'status': status, 'status': status,
'form': form}) 'form': form})

View File

@ -21,14 +21,32 @@ Plinth module for upgrades
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from plinth import actions
from plinth import cfg from plinth import cfg
depends = ['plinth.modules.system'] version = 1
is_essential = 1
depends = ['system']
title = _('Software Upgrades')
description = [
_('Upgrades install the latest software and security updates. When '
'automatic upgrades are enabled, upgrades are automatically run every '
'night. You don\'t normally need to start the upgrade process.')
]
def init(): def init():
"""Initialize the module.""" """Initialize the module."""
menu = cfg.main_menu.get('system:index') menu = cfg.main_menu.get('system:index')
menu.add_urlname(_('Software Upgrades'), 'glyphicon-refresh', menu.add_urlname(title, 'glyphicon-refresh', 'upgrades:index', 21)
'upgrades:index', 21)
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['unattended-upgrades'])
helper.call('post', actions.superuser_run, 'upgrades', ['enable-auto'])

View File

@ -1,4 +1,4 @@
{% extends 'base.html' %} {% extends 'app.html' %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -29,17 +29,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block configuration %}
<h2>{{ title }}</h2>
<p>
{% blocktrans trimmed %}
Upgrades install the latest software and security updates. When automatic
upgrades are enabled, upgrades are automatically run every night. You
don't normally need to start the upgrade process.
{% endblocktrans %}
</p>
<p> <p>
{% blocktrans trimmed %} {% blocktrans trimmed %}

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,9 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{{ title }}</h2>
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}

View File

@ -21,16 +21,14 @@ Plinth module for upgrades
from django.contrib import messages from django.contrib import messages
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django.shortcuts import redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.translation import ugettext as _, ugettext_lazy from django.utils.translation import ugettext as _, ugettext_lazy
from django.views.decorators.http import require_POST
import subprocess import subprocess
from .forms import ConfigureForm from .forms import ConfigureForm
from plinth import actions from plinth import actions
from plinth import package
from plinth.errors import ActionError from plinth.errors import ActionError
from plinth.modules import upgrades
subsubmenu = [{'url': reverse_lazy('upgrades:index'), subsubmenu = [{'url': reverse_lazy('upgrades:index'),
'text': ugettext_lazy('Automatic Upgrades')}, 'text': ugettext_lazy('Automatic Upgrades')},
@ -41,12 +39,6 @@ LOG_FILE = '/var/log/unattended-upgrades/unattended-upgrades.log'
LOCK_FILE = '/var/log/dpkg/lock' LOCK_FILE = '/var/log/dpkg/lock'
def on_install():
"""Enable automatic upgrades after install."""
actions.superuser_run('upgrades', ['enable-auto'])
@package.required(['unattended-upgrades'], on_install=on_install)
def index(request): def index(request):
"""Serve the configuration form.""" """Serve the configuration form."""
status = get_status() status = get_status()
@ -63,10 +55,12 @@ def index(request):
form = ConfigureForm(initial=status, prefix='upgrades') form = ConfigureForm(initial=status, prefix='upgrades')
return TemplateResponse(request, 'upgrades_configure.html', return TemplateResponse(request, 'upgrades_configure.html',
{'title': _('Automatic Upgrades'), {'title': upgrades.title,
'description': upgrades.description,
'form': form, 'form': form,
'subsubmenu': subsubmenu}) 'subsubmenu': subsubmenu})
def is_package_manager_busy(): def is_package_manager_busy():
"""Return whether a package manager is running.""" """Return whether a package manager is running."""
try: try:
@ -85,7 +79,6 @@ def get_log():
return None return None
@package.required(['unattended-upgrades'], on_install=on_install)
def upgrade(request): def upgrade(request):
"""Serve the upgrade page.""" """Serve the upgrade page."""
is_busy = is_package_manager_busy() is_busy = is_package_manager_busy()
@ -99,7 +92,8 @@ def upgrade(request):
messages.error(request, _('Starting upgrade failed.')) messages.error(request, _('Starting upgrade failed.'))
return TemplateResponse(request, 'upgrades.html', return TemplateResponse(request, 'upgrades.html',
{'title': _('Package Upgrades'), {'title': upgrades.title,
'description': upgrades.description,
'subsubmenu': subsubmenu, 'subsubmenu': subsubmenu,
'is_busy': is_busy, 'is_busy': is_busy,
'log': get_log()}) 'log': get_log()})

View File

@ -20,21 +20,24 @@ Plinth module to manage users
""" """
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import json
import subprocess import subprocess
from plinth import cfg from plinth import cfg
from plinth import actions
from plinth import action_utils from plinth import action_utils
depends = ['plinth.modules.system'] version = 1
is_essential = True
depends = ['system']
title = _('Users and Groups')
def init(): def init():
"""Intialize the user module.""" """Intialize the user module."""
menu = cfg.main_menu.get('system:index') menu = cfg.main_menu.get('system:index')
menu.add_urlname(_('Users and Groups'), 'glyphicon-user', 'users:index', menu.add_urlname(title, 'glyphicon-user', 'users:index', 15)
15)
def diagnose(): def diagnose():

View File

@ -20,7 +20,8 @@ Plinth module to configure XMPP server
""" """
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import json import logging
import socket
from plinth import actions from plinth import actions
from plinth import action_utils from plinth import action_utils
@ -30,21 +31,35 @@ from plinth.signals import pre_hostname_change, post_hostname_change
from plinth.signals import domainname_change from plinth.signals import domainname_change
depends = ['plinth.modules.apps'] version = 1
depends = ['apps']
title = _('Chat Server (XMPP)')
description = [
_('XMPP is an open and standardized communication protocol. Here '
'you can run and configure your XMPP server, called ejabberd.'),
_('To actually communicate, you can use the <a href=\'/jwchat\'>web '
'client</a> or any other '
'<a href=\'http://xmpp.org/xmpp-software/clients/\' target=\'_blank\''
'>XMPP client</a>.')
]
service = None service = None
logger = logging.getLogger(__name__)
def init(): def init():
"""Initialize the XMPP module""" """Initialize the XMPP module"""
menu = cfg.main_menu.get('apps:index') menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Chat Server (XMPP)'), 'glyphicon-comment', menu.add_urlname(title, 'glyphicon-comment', 'xmpp:index', 400)
'xmpp:index', 400)
global service global service
service = service_module.Service( service = service_module.Service(
'xmpp', _('Chat Server (XMPP)'), 'xmpp', title, ['xmpp-client', 'xmpp-server', 'xmpp-bosh'],
['xmpp-client', 'xmpp-server', 'xmpp-bosh'],
is_external=True, enabled=is_enabled()) is_external=True, enabled=is_enabled())
pre_hostname_change.connect(on_pre_hostname_change) pre_hostname_change.connect(on_pre_hostname_change)
@ -52,6 +67,18 @@ def init():
domainname_change.connect(on_domainname_change) domainname_change.connect(on_domainname_change)
def setup(helper, old_version=None):
"""Install and configure the module."""
domainname = get_domainname()
logger.info('XMPP service domainname - %s', domainname)
helper.call('pre', actions.superuser_run, 'xmpp',
['pre-install', '--domainname', domainname])
helper.install(['jwchat', 'ejabberd'])
helper.call('post', actions.superuser_run, 'xmpp', ['setup'])
helper.call('post', service.notify_enabled, None, True)
def is_enabled(): def is_enabled():
"""Return whether the module is enabled.""" """Return whether the module is enabled."""
return (action_utils.service_is_enabled('ejabberd') and return (action_utils.service_is_enabled('ejabberd') and
@ -63,6 +90,12 @@ def is_running():
return action_utils.service_is_running('ejabberd') return action_utils.service_is_running('ejabberd')
def get_domainname():
"""Return the domainname"""
fqdn = socket.getfqdn()
return '.'.join(fqdn.split('.')[1:])
def on_pre_hostname_change(sender, old_hostname, new_hostname, **kwargs): def on_pre_hostname_change(sender, old_hostname, new_hostname, **kwargs):
""" """
Backup ejabberd database before hostname is changed. Backup ejabberd database before hostname is changed.

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "app.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -21,24 +21,7 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block content %} {% block configuration %}
<h2>{% trans "Chat Server (XMPP)" %}</h2>
<p>
{% blocktrans trimmed %}
XMPP is an open and standardized communication protocol. Here
you can run and configure your XMPP server, called ejabberd.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
To actually communicate, you can use the <a href='/jwchat'>web
client</a> or any other <a href='http://xmpp.org/xmpp-software/clients/'
target='_blank'>XMPP client</a>.
{% endblocktrans %}
</p>
<p> <p>
{% url 'config:index' as index_url %} {% url 'config:index' as index_url %}

View File

@ -23,39 +23,15 @@ from django.contrib import messages
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import logging import logging
import socket
from .forms import XmppForm from .forms import XmppForm
from plinth import actions from plinth import actions
from plinth import package
from plinth.modules import xmpp from plinth.modules import xmpp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_domainname():
"""Return the domainname"""
fqdn = socket.getfqdn()
return '.'.join(fqdn.split('.')[1:])
def before_install():
"""Preseed debconf values before the packages are installed."""
domainname = get_domainname()
logger.info('XMPP service domainname - %s', domainname)
actions.superuser_run('xmpp', ['pre-install', '--domainname', domainname])
def on_install():
"""Setup jwchat apache conf"""
actions.superuser_run('xmpp', ['setup'])
xmpp.service.notify_enabled(None, True)
@package.required(['jwchat', 'ejabberd'],
before_install=before_install,
on_install=on_install)
def index(request): def index(request):
"""Serve configuration page""" """Serve configuration page"""
status = get_status() status = get_status()
@ -72,7 +48,8 @@ def index(request):
form = XmppForm(initial=status, prefix='xmpp') form = XmppForm(initial=status, prefix='xmpp')
return TemplateResponse(request, 'xmpp.html', return TemplateResponse(request, 'xmpp.html',
{'title': _('Chat Server (XMPP)'), {'title': xmpp.title,
'description': xmpp.description,
'status': status, 'status': status,
'form': form}) 'form': form})
@ -81,7 +58,7 @@ def get_status():
"""Get the current settings.""" """Get the current settings."""
status = {'enabled': xmpp.is_enabled(), status = {'enabled': xmpp.is_enabled(),
'is_running': xmpp.is_running(), 'is_running': xmpp.is_running(),
'domainname': get_domainname()} 'domainname': xmpp.get_domainname()}
return status return status

View File

@ -19,13 +19,9 @@
Framework for installing and updating distribution packages Framework for installing and updating distribution packages
""" """
from django.contrib import messages
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import functools
import logging import logging
import threading
import plinth
from plinth.utils import import_from_gi from plinth.utils import import_from_gi
glib = import_from_gi('GLib', '2.0') glib = import_from_gi('GLib', '2.0')
packagekit = import_from_gi('PackageKitGlib', '1.0') packagekit = import_from_gi('PackageKitGlib', '1.0')
@ -46,19 +42,21 @@ class PackageException(Exception):
self.error_string = error_string self.error_string = error_string
self.error_details = error_details self.error_details = error_details
def __str__(self):
"""Return the strin representation of the exception."""
return 'PackageException(error_string="{0}", error_details="{1}")' \
.format(self.error_string, self.error_details)
class Transaction(object): class Transaction(object):
"""Information about an ongoing transaction.""" """Information about an ongoing transaction."""
def __init__(self, package_names, before_install=None, on_install=None): def __init__(self, package_names):
"""Initialize transaction object. """Initialize transaction object.
Set most values to None until they are sent as progress update. Set most values to None until they are sent as progress update.
""" """
self.package_names = package_names self.package_names = package_names
# XXX: This is hack, remove after implementing proper setup mechanism.
self.before_install = before_install
self.on_install = on_install
# Progress # Progress
self.allow_cancel = None self.allow_cancel = None
@ -74,10 +72,6 @@ class Transaction(object):
self.download_size_remaining = None self.download_size_remaining = None
self.speed = None self.speed = None
# Completion
self.is_finished = False
self.exception = None
def get_id(self): def get_id(self):
"""Return a identifier to use as a key in a map of transactions.""" """Return a identifier to use as a key in a map of transactions."""
return frozenset(self.package_names) return frozenset(self.package_names)
@ -89,43 +83,12 @@ class Transaction(object):
self.package_names, self.allow_cancel, self.status_string, self.package_names, self.allow_cancel, self.status_string,
self.percentage, self.package, self.item_progress) self.percentage, self.package, self.item_progress)
def start_install(self): def install(self):
"""Start a PackageKit transaction to install given list of packages.
This operation is non-blocking at it spawns a new thread.
"""
thread = threading.Thread(target=self._install)
thread.start()
def _install(self):
"""Run a PackageKit transaction to install given packages.""" """Run a PackageKit transaction to install given packages."""
try:
if self.before_install:
self.before_install()
except Exception as exception:
logger.exception('Error during setup before install - %s',
exception)
self.finish(exception)
return
try: try:
self._do_install() self._do_install()
except PackageException as exception:
self.finish(exception)
return
except glib.Error as exception: except glib.Error as exception:
self.finish(PackageException(exception.message)) raise PackageException(exception.message) from exception
return
try:
if self.on_install:
self.on_install()
except Exception as exception:
logger.exception('Error during setup - %s', exception)
self.finish(exception)
return
self.finish()
def _do_install(self): def _do_install(self):
"""Run a PackageKit transaction to install given packages. """Run a PackageKit transaction to install given packages.
@ -203,115 +166,3 @@ class Transaction(object):
else: else:
logger.info('Unhandle packagekit progress callback - %s, %s', logger.info('Unhandle packagekit progress callback - %s, %s',
progress, progress_type) progress, progress_type)
def finish(self, exception=None):
"""Mark transaction as complected and store exception if any."""
self.is_finished = True
self.exception = exception
def collect_result(self):
"""Retrieve the result of this transaction.
Also remove self from global transactions list.
"""
assert self.is_finished
del transactions[self.get_id()]
return self.exception
def required(package_names, before_install=None, on_install=None):
"""Decorate a view to check and install required packages."""
def wrapper2(func):
"""Return a function to check and install packages."""
@functools.wraps(func)
def wrapper(request, *args, **kwargs):
"""Check and install packages required by a view."""
if not _should_show_install_view(request, package_names):
return func(request, *args, **kwargs)
view = plinth.views.PackageInstallView.as_view()
return view(request, package_names=package_names,
before_install=before_install, on_install=on_install,
*args, **kwargs)
return wrapper
return wrapper2
def _should_show_install_view(request, package_names):
"""Return whether the installation view should be shown."""
transaction_id = frozenset(package_names)
# No transaction in progress
if transaction_id not in transactions:
is_installed = check_installed(package_names)
return not is_installed
# Installing
transaction = transactions[transaction_id]
if not transaction.is_finished:
return True
# Transaction finished, waiting to show the result
exception = transaction.collect_result()
if not exception:
messages.success(request,
_('Installed and configured packages successfully.'))
return False
else:
error_string = getattr(exception, 'error_string', str(exception))
error_details = getattr(exception, 'error_details', '')
messages.error(request, _('Error installing packages: {string} {details}')
.format(string=error_string, details=error_details))
return True
def check_installed(package_names):
"""Return a boolean installed status of package.
This operation is blocking and waits until the check is finished.
"""
def _callback(progress, progress_type, user_data):
"""Process progress updates on package resolve operation."""
pass
client = packagekit.Client()
response = client.resolve(packagekit.FilterEnum.INSTALLED,
tuple(package_names) + (None, ), None,
_callback, None)
installed_package_names = []
for package in response.get_package_array():
if package.get_info() == packagekit.InfoEnum.INSTALLED:
installed_package_names.append(package.get_name())
packages_resolved[package.get_name()] = package
# When package names could not be resolved
for package_name in package_names:
if package_name not in packages_resolved:
packages_resolved[package_name] = None
return set(installed_package_names) == set(package_names)
def is_installing(package_names):
"""Return whether a set of packages are currently being installed."""
return frozenset(package_names) in transactions
def start_install(package_names, before_install=None, on_install=None):
"""Start a PackageKit transaction to install given list of packages.
This operation is non-blocking at it spawns a new thread.
"""
transaction = Transaction(package_names,
before_install=before_install,
on_install=on_install)
transactions[frozenset(package_names)] = transaction
transaction.start_install()

160
plinth/setup.py Normal file
View File

@ -0,0 +1,160 @@
#
# 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 with utilites for performing application setup operations.
"""
import logging
import threading
from . import package
import plinth
logger = logging.getLogger(__name__)
class Helper(object):
"""Helper routines for modules to show progress."""
def __init__(self, module_name, module):
"""Initialize the object."""
self.module_name = module_name
self.module = module
self.current_operation = None
self.is_finished = None
self.exception = None
def run_in_thread(self):
"""Execute the setup process in a thread."""
thread = threading.Thread(target=self._run)
thread.start()
def _run(self):
"""Collect exceptions when running in a thread."""
try:
self.run()
except Exception as exception:
self.exception = exception
def collect_result(self):
"""Return the exception if any."""
exception = self.exception
self.exception = None
self.is_finished = None
return exception
def run(self):
"""Execute the setup process."""
# Setup for the module is already running
if self.current_operation:
return
current_version = self.get_setup_version()
if current_version >= self.module.version:
return
self.exception = None
self.current_operation = None
self.is_finished = False
try:
if hasattr(self.module, 'setup'):
logger.info('Running module setup - %s', self.module_name)
self.module.setup(self, old_version=current_version)
else:
logger.info('Module does not require setup - %s',
self.module_name)
except Exception as exception:
logger.exception('Error running setup - %s', exception)
raise exception
else:
self.set_setup_version(self.module.version)
finally:
self.is_finished = True
self.current_operation = None
def install(self, package_names):
"""Install a set of packages marking progress."""
logger.info('Running install for module - %s, packages - %s',
self.module_name, package_names)
transaction = package.Transaction(package_names)
self.current_operation = {
'step': 'install',
'transaction': transaction,
}
transaction.install()
def call(self, step, method, *args, **kwargs):
"""Call an arbitrary method during setup and note down its stage."""
logger.info('Running step for module - %s, step - %s',
self.module_name, step)
self.current_operation = {'step': step}
return method(*args, **kwargs)
def get_state(self):
"""Return whether the module is not setup or needs upgrade."""
current_version = self.get_setup_version()
if current_version and self.module.version <= current_version:
return 'up-to-date'
# If a module need installing/updating but no setup method is
# available, then automatically set version.
#
# Minor violation of 'get' only discipline for convenience.
if not hasattr(self.module, 'setup'):
self.set_setup_version(self.module.version)
return 'up-to-date'
if not current_version:
return 'needs-setup'
else:
return 'needs-update'
def get_setup_version(self):
"""Return the setup version of a module."""
# XXX: Optimize version gets
from . import models
try:
module_entry = models.Module.objects.get(pk=self.module_name)
return module_entry.setup_version
except models.Module.DoesNotExist:
return 0
def set_setup_version(self, version):
"""Set a module's setup version."""
from . import models
models.Module.objects.update_or_create(
pk=self.module_name, defaults={'setup_version': version})
def init(module_name, module):
"""Create a setup helper for a module for later use."""
if not hasattr(module, 'setup_helper'):
module.setup_helper = Helper(module_name, module)
def setup_all_modules(essential=False):
"""Run setup on all essential modules and exit."""
logger.info('Running setup for all modules, essential - %s', essential)
for module_name, module in plinth.module_loader.loaded_modules.items():
if essential and not getattr(module, 'is_essential', False):
continue
module.setup_helper.run()

33
plinth/templates/app.html Normal file
View File

@ -0,0 +1,33 @@
{% 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 i18n %}
{% block content %}
<h2>{{ title }}</h2>
{% for paragraph in description %}
<p>{{ paragraph|safe }}</p>
{% endfor %}
{% block configuration %}
{% endblock %}
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More