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 module_loader
from plinth import service
from plinth import setup
logger = logging.getLogger(__name__)
@ -57,6 +58,9 @@ def parse_arguments():
parser.add_argument(
'--no-daemon', action='store_true', default=cfg.no_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(
'--diagnose', action='store_true', default=False,
help='run diagnostic tests and exit')
@ -132,9 +136,7 @@ def setup_server():
cherrypy.tree.mount(None, manual_url, config)
logger.debug('Serving manual images %s on %s', manual_dir, manual_url)
for module_import_path in module_loader.loaded_modules:
module = importlib.import_module(module_import_path)
module_name = module_import_path.split('.')[-1]
for module_name, module in module_loader.loaded_modules.items():
module_path = os.path.dirname(module.__file__)
static_dir = os.path.join(module_path, 'static')
if not os.path.isdir(static_dir):
@ -258,6 +260,7 @@ def configure_django():
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'stronghold.middleware.LoginRequiredMiddleware',
'plinth.modules.first_boot.middleware.FirstBootMiddleware',
'plinth.middleware.SetupMiddleware',
),
ROOT_URLCONF='plinth.urls',
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)
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():
"""Run diagostics on all modules and exit."""
module = importlib.import_module('plinth.modules.diagnostics.diagnostics')
@ -315,6 +330,9 @@ def main():
module_loader.load_modules()
if arguments.setup:
run_setup_and_exit()
if arguments.diagnose:
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):
"""Store the value of the key/value pair by JSON encoding it"""
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
"""
import collections
import django
import importlib
import logging
@ -27,11 +28,12 @@ import re
from plinth import cfg
from plinth import urls
from plinth import setup
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
@ -42,16 +44,18 @@ def load_modules():
"""
pre_module_loading.send_robust(sender="module_loader")
modules = {}
for module_name in get_modules_to_load():
LOGGER.info('Importing %s', module_name)
for module_import_path in get_modules_to_load():
logger.info('Importing %s', module_import_path)
module_name = module_import_path.split('.')[-1]
try:
modules[module_name] = importlib.import_module(module_name)
modules[module_name] = importlib.import_module(module_import_path)
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:
raise
_include_module_urls(module_name)
_include_module_urls(module_import_path, module_name)
ordered_modules = []
remaining_modules = dict(modules) # Make a copy
@ -64,14 +68,14 @@ def load_modules():
_insert_modules(module_name, module, remaining_modules,
ordered_modules)
except KeyError:
LOGGER.error('Unsatified dependency for module - %s',
logger.error('Unsatified dependency for module - %s',
module_name)
LOGGER.debug('Module load order - %s', ordered_modules)
logger.debug('Module load order - %s', ordered_modules)
for module_name in ordered_modules:
_initialize_module(modules[module_name])
loaded_modules.append(module_name)
_initialize_module(module_name, modules[module_name])
loaded_modules[module_name] = modules[module_name]
post_module_loading.send_robust(sender="module_loader")
@ -94,7 +98,7 @@ def _insert_modules(module_name, module, remaining_modules, ordered_modules):
try:
module = remaining_modules.pop(dependency)
except KeyError:
LOGGER.error('Not found or circular dependency - %s, %s',
logger.error('Not found or circular dependency - %s, %s',
module_name, dependency)
raise
@ -103,32 +107,34 @@ def _insert_modules(module_name, module, remaining_modules, ordered_modules):
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"""
namespace = module_name.split('.')[-1]
url_module = module_name + '.urls'
url_module = module_import_path + '.urls'
try:
urls.urlpatterns += [
django.conf.urls.url(
r'', django.conf.urls.include(url_module, namespace))]
r'', django.conf.urls.include(url_module, module_name))]
except ImportError:
LOGGER.debug('No URLs for %s', module_name)
logger.debug('No URLs for %s', module_name)
if cfg.debug:
raise
def _initialize_module(module):
def _initialize_module(module_name, module):
"""Call initialization method in the module if it exists"""
# Perform setup related initialization on the module
setup.init(module_name, module)
try:
init = module.init
except AttributeError:
LOGGER.debug('No init() for module - %s', module.__name__)
logger.debug('No init() for module - %s', module.__name__)
return
try:
init()
except Exception as exception:
LOGGER.exception('Exception while running init for %s: %s',
logger.exception('Exception while running init for %s: %s',
module, exception)
if cfg.debug:
raise

View File

@ -23,3 +23,7 @@ from . import apps
from .apps import 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 _
import subprocess
from plinth import actions
from plinth import action_utils
from plinth import cfg
from plinth import service as service_module
from plinth.utils import format_lazy
# 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
@ -37,13 +53,16 @@ service = None
def init():
"""Intialize the service discovery module."""
menu = cfg.main_menu.get('system:index')
menu.add_urlname(_('Service Discovery'), 'glyphicon-lamp',
'avahi:index', 950)
menu.add_urlname(title, 'glyphicon-lamp', 'avahi:index', 950)
global service # pylint: disable=W0603
service = service_module.Service(
'avahi', _('Service Discovery'), ['mdns'],
is_external=False, enabled=is_enabled())
'avahi', title, ['mdns'], is_external=False, enabled=is_enabled())
def setup(helper, old_version=False):
"""Install and configure the module."""
helper.install(['avahi-daemon'])
def is_enabled():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -21,21 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
<h3>{% trans "Status" %}</h3>

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@
URLs for the Configuration module
"""
from django.conf.urls import patterns, url
from django.conf.urls import url
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 _
import subprocess
from plinth import actions
from plinth import action_utils
from plinth import cfg
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
@ -36,13 +46,17 @@ service = None
def init():
"""Intialize the date/time module."""
menu = cfg.main_menu.get('system:index')
menu.add_urlname(_('Date & Time'), 'glyphicon-time',
'datetime:index', 900)
menu.add_urlname(title, 'glyphicon-time', 'datetime:index', 900)
global service
service = service_module.Service(
'ntp', _('Network Time Server'),
is_external=False, enabled=is_enabled())
'ntp', title, 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():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -21,16 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
<h3>{% trans "Status" %}</h3>

View File

@ -26,18 +26,11 @@ import logging
from .forms import DateTimeForm
from plinth import actions
from plinth import package
from plinth.modules import datetime
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):
"""Serve configuration page."""
status = get_status()
@ -55,7 +48,8 @@ def index(request):
form = DateTimeForm(initial=status, prefix='datetime')
return TemplateResponse(request, 'datetime.html',
{'title': _('Date & Time'),
{'title': datetime.title,
'description': datetime.description,
'status': status,
'form': form})

View File

@ -27,7 +27,20 @@ from plinth import cfg
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
@ -35,13 +48,19 @@ service = None
def init():
"""Initialize the Deluge module."""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('BitTorrent (Deluge)'), 'glyphicon-magnet',
'deluge:index', 200)
menu.add_urlname(title, 'glyphicon-magnet', 'deluge:index', 200)
global service
service = service_module.Service(
'deluge', _('Deluge BitTorrent'), ['http', 'https'],
is_external=True, enabled=is_enabled())
'deluge', title, ['http', 'https'], is_external=True,
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():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -21,20 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
<h3>{% trans "Status" %}</h3>

View File

@ -25,17 +25,9 @@ from django.utils.translation import ugettext as _
from .forms import DelugeForm
from plinth import actions
from plinth import package
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):
"""Serve configuration page."""
status = get_status()
@ -53,7 +45,8 @@ def index(request):
form = DelugeForm(initial=status, prefix='deluge')
return TemplateResponse(request, 'deluge.html',
{'title': _('BitTorrent (Deluge)'),
{'title': deluge.title,
'description': deluge.description,
'status': status,
'form': form})

View File

@ -19,13 +19,30 @@
Plinth module for system diagnostics
"""
from . import diagnostics
from .diagnostics import init
from django.utils.translation import ugettext_lazy as _
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():

View File

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

View File

@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends 'app.html' %}
{% comment %}
#
# This file is part of Plinth.
@ -29,17 +29,7 @@
{% endblock %}
{% block content %}
<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>
{% block configuration %}
{% if not is_running %}
<form class="form form-diagnostics-button" method="post"

View File

@ -19,9 +19,42 @@
Plinth module to configure ez-ipupdate client
"""
from . import dynamicdns
from .dynamicdns import init
from django.utils.translation import ugettext_lazy as _
__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 cfg
from plinth import package
from plinth.modules import dynamicdns
from plinth.utils import format_lazy
logger = logging.getLogger(__name__)
@ -39,19 +39,11 @@ subsubmenu = [{'url': reverse_lazy('dynamicdns:index'),
'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):
"""Serve Dynamic DNS page."""
return TemplateResponse(request, 'dynamicdns.html',
{'title': _('Dynamic DNS'),
{'title': dynamicdns.title,
'description': dynamicdns.description,
'subsubmenu': subsubmenu})
@ -198,7 +190,6 @@ class ConfigureForm(forms.Form):
raise forms.ValidationError(_('Please provide a password'))
@package.required(['ez-ipupdate'])
def configure(request):
"""Serve the configuration form."""
status = get_status()
@ -219,7 +210,6 @@ def configure(request):
'subsubmenu': subsubmenu})
@package.required(['ez-ipupdate'])
def statuspage(request):
"""Serve the status page."""
check_nat = actions.run('dynamicdns', ['get-nat'])

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -20,32 +20,7 @@
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
<p>
{% blocktrans trimmed %}
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
"""
from . import firewall
from .firewall import init
from django.utils.translation import ugettext_lazy as _
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 %}
#
# This file is part of Plinth.
@ -20,18 +20,7 @@
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
<p>{% trans "Current status:" %}</p>

View File

@ -21,7 +21,7 @@ URLs for the Firewall module
from django.conf.urls import url
from . import firewall as views
from . import views
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
"""
version = 1
is_essential = True

View File

@ -27,7 +27,16 @@ from plinth import cfg
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
@ -35,13 +44,25 @@ service = None
def init():
"""Initialize the ikiwiki module."""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Wiki and Blog (ikiwiki)'), 'glyphicon-edit',
'ikiwiki:index', 1100)
menu.add_urlname(title, 'glyphicon-edit', 'ikiwiki:index', 1100)
global service
service = service_module.Service(
'ikiwiki', _('ikiwiki wikis and blogs'), ['http', 'https'],
is_external=True, enabled=is_enabled())
'ikiwiki', title, ['http', 'https'], is_external=True,
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():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -21,14 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<p>
{% blocktrans trimmed %}
When enabled, the blogs and wikis will be available
from <a href="/ikiwiki">/ikiwiki</a>.
{% endblocktrans %}
</p>
{% block configuration %}
{% 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 plinth import actions
from plinth import action_utils
from plinth import package
from plinth.modules import ikiwiki
@ -40,20 +38,6 @@ subsubmenu = [{'url': reverse_lazy('ikiwiki:index'),
'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):
"""Serve configuration page."""
status = get_status()
@ -70,7 +54,8 @@ def index(request):
form = IkiwikiForm(initial=status, prefix='ikiwiki')
return TemplateResponse(request, 'ikiwiki.html',
{'title': _('Wiki and Blog'),
{'title': ikiwiki.title,
'description': ikiwiki.description,
'status': status,
'form': form,
'subsubmenu': subsubmenu})

View File

@ -20,20 +20,38 @@ Plinth module for using Let's Encrypt.
"""
from django.utils.translation import ugettext_lazy as _
import json
from plinth import actions
from plinth import action_utils
from plinth import cfg
from plinth import service as service_module
from plinth.modules import names
from plinth.utils import format_lazy
depends = [
'plinth.modules.apps',
'plinth.modules.names'
version = 1
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
@ -44,6 +62,11 @@ def init():
'glyphicon-lock', 'letsencrypt:index', 20)
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['letsencrypt'])
def diagnose():
"""Run diagnostics and return the results."""
results = []

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -33,30 +33,7 @@
</style>
{% endblock %}
{% block content %}
<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>
{% block configuration %}
<div class="row">
<div class="col-lg-8">

View File

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

View File

@ -23,7 +23,22 @@ from django.utils.translation import ugettext_lazy as _
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():
@ -31,3 +46,8 @@ def init():
menu = cfg.main_menu.get('system:index')
menu.add_urlname(_('Monkeysphere'), 'glyphicon-certificate',
'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 %}
#
# This file is part of Plinth.
@ -30,22 +30,7 @@
{% endblock %}
{% block content %}
<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>
{% block configuration %}
{% if running %}
<p class="running-status-parent">

View File

@ -28,20 +28,20 @@ from django.views.decorators.http import require_POST
import json
from plinth import actions
from plinth import package
from plinth.modules import monkeysphere
from plinth.modules import names
publish_process = None
@package.required(['monkeysphere'])
def index(request):
"""Serve configuration page."""
_collect_publish_result(request)
status = get_status()
return TemplateResponse(
request, 'monkeysphere.html',
{'title': _('Monkeysphere'),
{'title': monkeysphere.title,
'description': monkeysphere.description,
'status': status,
'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 plinth import actions
from plinth import action_utils
from plinth import cfg
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
@ -35,13 +47,17 @@ service = None
def init():
"""Intialize the Mumble module."""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Voice Chat (Mumble)'), 'glyphicon-headphones',
'mumble:index', 900)
menu.add_urlname(title, 'glyphicon-headphones', 'mumble:index', 900)
global service
service = service_module.Service(
'mumble-plinth', _('Mumble Voice Chat Server'),
is_external=True, enabled=is_enabled())
'mumble-plinth', title, 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():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -21,25 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
<h3>{% trans "Status" %}</h3>

View File

@ -26,18 +26,11 @@ import logging
from .forms import MumbleForm
from plinth import actions
from plinth import package
from plinth.modules import mumble
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):
"""Serve configuration page."""
status = get_status()
@ -55,7 +48,8 @@ def index(request):
form = MumbleForm(initial=status, prefix='mumble')
return TemplateResponse(request, 'mumble.html',
{'title': _('Voice Chat (Mumble)'),
{'title': mumble.title,
'description': mumble.description,
'status': status,
'form': form})

View File

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

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -21,9 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<h2>{{ title }}</h2>
{% block configuration %}
<div class="row">
<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 get_domain, get_services_status
from plinth.modules import names
def index(request):
@ -31,7 +32,7 @@ def index(request):
status = get_status()
return TemplateResponse(request, 'names.html',
{'title': _('Name Services'),
{'title': names.title,
'status': status})

View File

@ -19,23 +19,37 @@
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
import subprocess
from . import networks
from .networks import init
from plinth import action_utils
from plinth import cfg
from plinth import network
__all__ = ['networks', 'init']
version = 1
depends = ['plinth.modules.system']
is_essential = True
depends = ['system']
title = _('Networks')
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():
"""Run diagnostics and return the results."""
results = []
@ -44,8 +58,10 @@ def diagnose():
addresses = _get_interface_addresses(interfaces)
for address in addresses:
results.append(action_utils.diagnose_port_listening(53, 'tcp', address))
results.append(action_utils.diagnose_port_listening(53, 'udp', address))
results.append(
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('6'))

View File

@ -25,9 +25,7 @@ from logging import Logger
from .forms import (ConnectionTypeSelectForm, EthernetForm, PPPoEForm,
WifiForm)
from plinth import cfg
from plinth import network
from plinth import package
logger = Logger(__name__)
@ -40,14 +38,6 @@ subsubmenu = [{'url': reverse_lazy('networks:index'),
'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):
"""Show 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 cfg
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
@ -35,13 +51,16 @@ service = None
def init():
"""Intialize the OpenVPN module."""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Virtual Private Network (OpenVPN)'), 'glyphicon-lock',
'openvpn:index', 850)
menu.add_urlname(title, 'glyphicon-lock', 'openvpn:index', 850)
global service
service = service_module.Service(
'openvpn', _('OpenVPN'), ['openvpn'],
is_external=True, enabled=is_enabled())
'openvpn', title, ['openvpn'], 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():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -30,21 +30,7 @@
{% endblock %}
{% block content %}
<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>
{% block configuration %}
{% if status.is_setup %}

View File

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

View File

@ -19,14 +19,60 @@
Plinth module to configure ownCloud
"""
from . import owncloud
from .owncloud import init
from django.utils.translation import ugettext_lazy as _
from plinth import actions
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():

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 %}
#
# This file is part of Plinth.
@ -21,30 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
{% 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 . import owncloud as views
from . import views
urlpatterns = [

View File

@ -19,47 +19,15 @@
Plinth module for configuring ownCloud.
"""
from django import forms
from django.contrib import messages
from django.template.response import TemplateResponse
from django.utils.translation import ugettext_lazy as _
from .forms import OwnCloudForm
from plinth import actions
from plinth import cfg
from plinth import package
from plinth import service as service_module
from plinth.modules import owncloud
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):
"""Serve the ownCloud configuration page"""
status = get_status()
@ -77,14 +45,14 @@ def index(request):
form = OwnCloudForm(initial=status, prefix='owncloud')
return TemplateResponse(request, 'owncloud.html',
{'title': _('ownCloud'),
{'title': owncloud.title,
'description': owncloud.description,
'form': form})
def get_status():
"""Return the current status"""
output = actions.run('owncloud-setup', ['status'])
return {'enabled': 'enable' in output.split()}
return {'enabled': owncloud.is_enabled()}
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
# 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 plinth import cfg
from plinth.utils import format_lazy
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():
"""Intialize the PageKite module"""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Public Visibility (PageKite)'),
'glyphicon-flag', 'pagekite:index', 800)
menu.add_urlname(title, 'glyphicon-flag', 'pagekite:index', 800)
# Register kite name with Name Services module.
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 %}
#
# This file is part of Plinth.
@ -20,58 +20,7 @@
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
<p>
<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.http.response import HttpResponseRedirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext_lazy as _
from django.views.generic import View, TemplateView
from django.views.generic.edit import FormView
from plinth import package
from . import utils
from .forms import ConfigurationForm, StandardServiceForm, \
AddCustomServiceForm, DeleteCustomServiceForm
from plinth.modules import pagekite
required_packages = ('pagekite',)
subsubmenu = [{'url': reverse_lazy('pagekite:index'),
'text': _('About PageKite')},
{'url': reverse_lazy('pagekite:configure'),
@ -43,7 +41,8 @@ subsubmenu = [{'url': reverse_lazy('pagekite:index'),
def index(request):
"""Serve introduction page"""
return TemplateResponse(request, 'pagekite_introduction.html',
{'title': _('Public Visibility (PageKite)'),
{'title': pagekite.title,
'description': pagekite.description,
'subsubmenu': subsubmenu})
@ -59,7 +58,6 @@ class ContextMixin(object):
context['subsubmenu'] = subsubmenu
return context
@method_decorator(package.required(required_packages))
def dispatch(self, *args, **kwargs):
return super(ContextMixin, self).dispatch(*args, **kwargs)
@ -81,8 +79,9 @@ class CustomServiceView(ContextMixin, TemplateView):
unused, custom_services = utils.get_pagekite_services()
for service in custom_services:
service['form'] = AddCustomServiceForm(initial=service)
context['custom_services'] = [utils.prepare_service_for_display(service)
for service in custom_services]
context['custom_services'] = [
utils.prepare_service_for_display(service)
for service in custom_services]
context.update(utils.get_kite_details())
return context

View File

@ -23,11 +23,20 @@ from django.utils.translation import ugettext_lazy as _
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():
"""Initialize the power module."""
menu = cfg.main_menu.get('system:index')
menu.add_urlname(_('Power'), 'glyphicon-off',
'power:index', 1000)
menu.add_urlname(title, 'glyphicon-off', 'power:index', 1000)

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -21,13 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<h2>{{ title }}</h2>
<p>
{% blocktrans trimmed %}Restart or shut down the system.{% endblocktrans %}
</p>
{% block configuration %}
<p>
<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 plinth import actions
from plinth.modules import power
def index(request):
"""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):

View File

@ -20,15 +20,36 @@ Plinth module to configure Privoxy.
"""
from django.utils.translation import ugettext_lazy as _
import json
from plinth import actions
from plinth import action_utils
from plinth import cfg
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
@ -36,13 +57,18 @@ service = None
def init():
"""Intialize the module."""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Web Proxy (Privoxy)'), 'glyphicon-cloud-upload',
'privoxy:index', 1000)
menu.add_urlname(title, 'glyphicon-cloud-upload', 'privoxy:index', 1000)
global service
service = service_module.Service(
'privoxy', _('Privoxy Web Proxy'),
is_external=False, enabled=is_enabled())
'privoxy', title, 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():
@ -84,9 +110,7 @@ def diagnose_url_with_proxy():
result = action_utils.diagnose_url(url, kind=address['kind'], env=env)
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)
return results

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -21,29 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
<h3>{% trans "Status" %}</h3>

View File

@ -26,19 +26,11 @@ import logging
from .forms import PrivoxyForm
from plinth import actions
from plinth import package
from plinth.modules import privoxy
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):
"""Serve configuration page."""
status = get_status()
@ -56,7 +48,8 @@ def index(request):
form = PrivoxyForm(initial=status, prefix='privoxy')
return TemplateResponse(request, 'privoxy.html',
{'title': _('Web Proxy (Privoxy)'),
{'title': privoxy.title,
'description': privoxy.description,
'status': status,
'form': form})

View File

@ -16,7 +16,7 @@
#
"""
Plinth module for quassel.
Plinth module for Quassel.
"""
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 cfg
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
@ -33,13 +55,17 @@ service = None
def init():
"""Initialize the quassel module."""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('IRC Client (Quassel)'), 'glyphicon-retweet',
'quassel:index', 730)
menu.add_urlname(title, 'glyphicon-retweet', 'quassel:index', 730)
global service
service = service_module.Service(
'quassel-plinth', _('Quassel IRC Client'),
is_external=True, enabled=is_enabled())
'quassel-plinth', title, 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():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -21,31 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
<h3>{% trans "Status" %}</h3>

View File

@ -25,16 +25,9 @@ from django.utils.translation import ugettext as _
from .forms import QuasselForm
from plinth import actions
from plinth import package
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):
"""Serve configuration page."""
status = get_status()
@ -51,7 +44,8 @@ def index(request):
form = QuasselForm(initial=status, prefix='quassel')
return TemplateResponse(request, 'quassel.html',
{'title': _('IRC Client (Quassel)'),
{'title': quassel.title,
'description': quassel.description,
'status': status,
'form': form})

View File

@ -21,11 +21,37 @@ Plinth module for repro.
from django.utils.translation import ugettext_lazy as _
from plinth import actions
from plinth import action_utils
from plinth import cfg
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
@ -33,13 +59,19 @@ service = None
def init():
"""Initialize the repro module."""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('SIP Server (repro)'), 'glyphicon-phone-alt',
'repro:index', 825)
menu.add_urlname(title, 'glyphicon-phone-alt', 'repro:index', 825)
global service
service = service_module.Service(
'repro', _('repro SIP Server'), ['sip-plinth', 'sip-tls-plinth'],
is_external=True, enabled=is_enabled())
'repro', title, ['sip-plinth', 'sip-tls-plinth'], is_external=True,
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():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -21,39 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
<h3>{% trans "Status" %}</h3>

View File

@ -25,17 +25,9 @@ from django.utils.translation import ugettext as _
from .forms import ReproForm
from plinth import actions
from plinth import package
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):
"""Serve configuration page."""
status = get_status()
@ -52,7 +44,8 @@ def index(request):
form = ReproForm(initial=status, prefix='repro')
return TemplateResponse(request, 'repro.html',
{'title': _('SIP Server (repro)'),
{'title': repro.title,
'description': repro.description,
'status': status,
'form': form})

View File

@ -22,24 +22,44 @@ Plinth module to configure reStore.
from django.utils.translation import ugettext_lazy as _
from plinth import action_utils, cfg
from plinth import service as service_module
from plinth.utils import format_lazy
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():
"""Initialize the reStore module."""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Unhosted Storage (reStore)'), 'glyphicon-hdd',
'restore:index', 750)
menu.add_urlname(title, 'glyphicon-hdd', 'restore:index', 750)
global service
service = service_module.Service(
'node-restore', _('reStore'), ['http', 'https'],
is_external=False, enabled=is_enabled())
'node-restore', title, ['http', 'https'], is_external=False,
enabled=is_enabled())
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(['node-restore'])
def is_enabled():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -21,27 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
<h3>Configuration</h3>

View File

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

View File

@ -24,17 +24,49 @@ from django.utils.translation import ugettext_lazy as _
from plinth import actions
from plinth import action_utils
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():
"""Intialize the module."""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Email Client (Roundcube)'), 'glyphicon-envelope',
'roundcube:index', 600)
menu.add_urlname(title, 'glyphicon-envelope', '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():
"""Return whether the module is enabled."""

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -21,41 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
{% include "diagnostics_button.html" with module="roundcube" %}

View File

@ -26,24 +26,11 @@ import logging
from .forms import RoundcubeForm
from plinth import actions
from plinth import package
from plinth.modules import roundcube
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):
"""Serve configuration page."""
status = get_status()
@ -61,7 +48,8 @@ def index(request):
form = RoundcubeForm(initial=status, prefix='roundcube')
return TemplateResponse(request, 'roundcube.html',
{'title': _('Email Client (Roundcube)'),
{'title': roundcube.title,
'description': roundcube.description,
'status': status,
'form': form})

View File

@ -26,7 +26,20 @@ from plinth import cfg
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
@ -34,13 +47,18 @@ service = None
def init():
"""Initialize the module."""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Bookmarks (Shaarli)'), 'glyphicon-bookmark',
'shaarli:index', 350)
menu.add_urlname(title, 'glyphicon-bookmark', 'shaarli:index', 350)
global service
service = service_module.Service(
'shaarli', _('Shaarli'), ['http', 'https'],
is_external=True, enabled=is_enabled())
'shaarli', title, ['http', 'https'], is_external=True,
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():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -21,21 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
<h3>{% trans "Configuration" %}</h3>

View File

@ -25,14 +25,9 @@ from django.utils.translation import ugettext as _
from .forms import ShaarliForm
from plinth import actions
from plinth import package
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):
"""Serve configuration page."""
status = get_status()
@ -49,7 +44,8 @@ def index(request):
form = ShaarliForm(initial=status, prefix='shaarli')
return TemplateResponse(request, 'shaarli.html',
{'title': _('Bookmarks (Shaarli)'),
{'title': shaarli.title,
'description': shaarli.description,
'status': status,
'form': form})

View File

@ -19,8 +19,29 @@
Plinth module for system section page
"""
from . import system
from .system import init
from django.utils.translation import ugettext_lazy as _
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 %}
#
# This file is part of Plinth.
@ -17,25 +17,3 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
{% 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 . import system as views
from . import views
urlpatterns = [

View File

@ -16,18 +16,12 @@
#
from django.template.response import TemplateResponse
from django.utils.translation import ugettext_lazy as _
from plinth import cfg
def init():
"""Initialize the system module"""
cfg.main_menu.add_urlname(_('System'), 'glyphicon-cog', 'system:index',
100)
from plinth.modules import system
def index(request):
"""Serve the index page"""
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
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy as _
import glob
import itertools
@ -32,7 +32,20 @@ from plinth.modules.names import SERVICES
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
bridge_service = None
@ -45,8 +58,7 @@ APT_TOR_PREFIX = 'tor+'
def init():
"""Initialize the module."""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Anonymity Network (Tor)'), 'glyphicon-eye-close',
'tor:index', 100)
menu.add_urlname(title, 'glyphicon-eye-close', 'tor:index', 100)
global socks_service
socks_service = service_module.Service(
@ -77,6 +89,17 @@ def init():
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():
"""Return whether the module is enabled."""
return action_utils.service_is_enabled('tor')

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -30,20 +30,7 @@
{% endblock %}
{% block content %}
<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>
{% block configuration %}
<h3>{% trans "Status" %}</h3>

View File

@ -25,7 +25,6 @@ from django.utils.translation import ugettext_lazy as _
from .forms import TorForm
from plinth import actions
from plinth import package
from plinth.errors import ActionError
from plinth.modules import tor
from plinth.modules.names import SERVICES
@ -34,18 +33,6 @@ from plinth.signals import domain_added, domain_removed
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):
"""Serve configuration page."""
if config_process:
@ -65,7 +52,8 @@ def index(request):
form = TorForm(initial=status, prefix='tor')
return TemplateResponse(request, 'tor.html',
{'title': _('Tor Control Panel'),
{'title': tor.title,
'description': tor.description,
'status': status,
'config_running': bool(config_process),
'form': form})

View File

@ -20,6 +20,7 @@ Plinth module to configure Transmission server
"""
from django.utils.translation import ugettext_lazy as _
import json
from plinth import actions
from plinth import action_utils
@ -27,7 +28,17 @@ from plinth import cfg
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
@ -35,13 +46,25 @@ service = None
def init():
"""Intialize the Transmission module."""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('BitTorrent (Transmission)'), 'glyphicon-save',
'transmission:index', 300)
menu.add_urlname(title, 'glyphicon-save', 'transmission:index', 300)
global service
service = service_module.Service(
'transmission', _('Transmission BitTorrent'), ['http', 'https'],
is_external=True, enabled=is_enabled())
'transmission', title, ['http', 'https'], is_external=True,
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():

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -21,17 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
<p>
{% blocktrans trimmed %}

View File

@ -28,7 +28,6 @@ import socket
from .forms import TransmissionForm
from plinth import actions
from plinth import package
from plinth.modules import transmission
logger = logging.getLogger(__name__)
@ -36,17 +35,6 @@ logger = logging.getLogger(__name__)
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):
"""Serve configuration page."""
status = get_status()
@ -64,7 +52,8 @@ def index(request):
form = TransmissionForm(initial=status, prefix='transmission')
return TemplateResponse(request, 'transmission.html',
{'title': _('BitTorrent (Transmission)'),
{'title': transmission.title,
'description': transmission.description,
'status': status,
'form': form})

View File

@ -21,14 +21,32 @@ Plinth module for upgrades
from django.utils.translation import ugettext_lazy as _
from plinth import actions
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():
"""Initialize the module."""
menu = cfg.main_menu.get('system:index')
menu.add_urlname(_('Software Upgrades'), 'glyphicon-refresh',
'upgrades:index', 21)
menu.add_urlname(title, 'glyphicon-refresh', '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 %}
#
# This file is part of Plinth.
@ -29,17 +29,7 @@
{% endblock %}
{% block content %}
<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>
{% block configuration %}
<p>
{% blocktrans trimmed %}

View File

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

View File

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

View File

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

View File

@ -20,7 +20,8 @@ Plinth module to configure XMPP server
"""
from django.utils.translation import ugettext_lazy as _
import json
import logging
import socket
from plinth import actions
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
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
logger = logging.getLogger(__name__)
def init():
"""Initialize the XMPP module"""
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Chat Server (XMPP)'), 'glyphicon-comment',
'xmpp:index', 400)
menu.add_urlname(title, 'glyphicon-comment', 'xmpp:index', 400)
global service
service = service_module.Service(
'xmpp', _('Chat Server (XMPP)'),
['xmpp-client', 'xmpp-server', 'xmpp-bosh'],
'xmpp', title, ['xmpp-client', 'xmpp-server', 'xmpp-bosh'],
is_external=True, enabled=is_enabled())
pre_hostname_change.connect(on_pre_hostname_change)
@ -52,6 +67,18 @@ def init():
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():
"""Return whether the module is enabled."""
return (action_utils.service_is_enabled('ejabberd') and
@ -63,6 +90,12 @@ def is_running():
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):
"""
Backup ejabberd database before hostname is changed.

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "app.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -21,24 +21,7 @@
{% load bootstrap %}
{% load i18n %}
{% block content %}
<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>
{% block configuration %}
<p>
{% 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.utils.translation import ugettext as _
import logging
import socket
from .forms import XmppForm
from plinth import actions
from plinth import package
from plinth.modules import xmpp
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):
"""Serve configuration page"""
status = get_status()
@ -72,7 +48,8 @@ def index(request):
form = XmppForm(initial=status, prefix='xmpp')
return TemplateResponse(request, 'xmpp.html',
{'title': _('Chat Server (XMPP)'),
{'title': xmpp.title,
'description': xmpp.description,
'status': status,
'form': form})
@ -81,7 +58,7 @@ def get_status():
"""Get the current settings."""
status = {'enabled': xmpp.is_enabled(),
'is_running': xmpp.is_running(),
'domainname': get_domainname()}
'domainname': xmpp.get_domainname()}
return status

View File

@ -19,13 +19,9 @@
Framework for installing and updating distribution packages
"""
from django.contrib import messages
from django.utils.translation import ugettext as _
import functools
import logging
import threading
import plinth
from plinth.utils import import_from_gi
glib = import_from_gi('GLib', '2.0')
packagekit = import_from_gi('PackageKitGlib', '1.0')
@ -46,19 +42,21 @@ class PackageException(Exception):
self.error_string = error_string
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):
"""Information about an ongoing transaction."""
def __init__(self, package_names, before_install=None, on_install=None):
def __init__(self, package_names):
"""Initialize transaction object.
Set most values to None until they are sent as progress update.
"""
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
self.allow_cancel = None
@ -74,10 +72,6 @@ class Transaction(object):
self.download_size_remaining = None
self.speed = None
# Completion
self.is_finished = False
self.exception = None
def get_id(self):
"""Return a identifier to use as a key in a map of transactions."""
return frozenset(self.package_names)
@ -89,43 +83,12 @@ class Transaction(object):
self.package_names, self.allow_cancel, self.status_string,
self.percentage, self.package, self.item_progress)
def start_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):
def install(self):
"""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:
self._do_install()
except PackageException as exception:
self.finish(exception)
return
except glib.Error as exception:
self.finish(PackageException(exception.message))
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()
raise PackageException(exception.message) from exception
def _do_install(self):
"""Run a PackageKit transaction to install given packages.
@ -203,115 +166,3 @@ class Transaction(object):
else:
logger.info('Unhandle packagekit progress callback - %s, %s',
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