diff --git a/INSTALL b/INSTALL
index 80fe31ab0..14b9dd327 100644
--- a/INSTALL
+++ b/INSTALL
@@ -7,7 +7,7 @@
$ sudo apt-get install libjs-jquery libjs-modernizr \
libjs-bootstrap make pandoc python3 python3-cherrypy3 \
python3-coverage python3-django python3-bootstrapform \
- python3-setuptools
+ python3-gi python3-setuptools gir1.2-packagekitglib-1.0
2. Install Plinth:
diff --git a/actions/xmpp-pre-hostname-change b/actions/domainname-change
similarity index 58%
rename from actions/xmpp-pre-hostname-change
rename to actions/domainname-change
index 6b3cbb6e8..d1d501591 100755
--- a/actions/xmpp-pre-hostname-change
+++ b/actions/domainname-change
@@ -9,19 +9,19 @@
#
# 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
+# 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 .
+# along with this program. If not, see .
#
-# Action to backup ejabberd database before changing hostname.
+domainname="$1"
+hostname=$(hostname)
-BACKUP=/tmp/ejabberd.dump
-ejabberdctl dump $BACKUP
-ejabberdctl stop
-
-# Make sure there aren't files in the Mnesia spool dir
-mkdir -p /var/lib/ejabberd/oldfiles
-mv /var/lib/ejabberd/*.* /var/lib/ejabberd/oldfiles/
+if grep -q 127.0.1.1 /etc/hosts ; then
+ sed -i "s/127.0.1.1.*/127.0.1.1 $hostname.$domainname $hostname/" /etc/hosts
+else
+ sed -i "/127.0.0.1.*/a \
+127.0.1.1 $hostname.$domainname $hostname" /etc/hosts
+fi
diff --git a/actions/firewall b/actions/firewall
index 98d40fe8d..cae05e86c 100755
--- a/actions/firewall
+++ b/actions/firewall
@@ -30,10 +30,6 @@ def parse_arguments():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
- # Get installed status
- subparsers.add_parser('get-installed',
- help='Get whether firewalld is installed')
-
# Get status
subparsers.add_parser('get-status',
help='Get whether firewalld is running')
@@ -64,14 +60,6 @@ def parse_arguments():
return parser.parse_args()
-def subcommand_get_installed(_):
- """Print whether firewalld is installed"""
- with open('/dev/null', 'w') as file_handle:
- status = subprocess.call(['which', 'firewalld'], stdout=file_handle)
-
- print('installed' if not status else 'not installed')
-
-
def subcommand_get_status(_):
"""Print status of the firewalld service"""
subprocess.call(['firewall-cmd', '--state'])
diff --git a/actions/hostname-change b/actions/hostname-change
index 3556529ce..0ae714366 100755
--- a/actions/hostname-change
+++ b/actions/hostname-change
@@ -18,11 +18,15 @@
hostname="$1"
-echo "$hostname" > /etc/hostname
-if [ -x /etc/init.d/hostname.sh ] ; then
- service hostname.sh start
+if [ -d /run/systemd/system ] ; then
+ hostnamectl set-hostname --transient --static "$hostname"
else
- service hostname start
+ echo "$hostname" > /etc/hostname
+ if [ -x /etc/init.d/hostname.sh ] ; then
+ invoke-rc.d hostname.sh start
+ else
+ service hostname start
+ fi
fi
service avahi-daemon restart
diff --git a/actions/owncloud-setup b/actions/owncloud-setup
index 3b82d0c9e..c221acd2e 100755
--- a/actions/owncloud-setup
+++ b/actions/owncloud-setup
@@ -58,14 +58,6 @@ done
if [ "$owncloud_enable" != "$owncloud_enable_cur" ] ; then
if $owncloud_enable ; then
- # Select postgresql as the backend database for OwnCloud, and
- # make sure its php support is enabled when owncloud is
- # installed.
- DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends \
- install -y postgresql php5-pgsql 2>&1 | logger -t owncloud-setup
- DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends \
- install -y owncloud 2>&1 | logger -t owncloud-setup
-
# Keep existing configuration if it exist
if [ ! -e /etc/owncloud/config.php ] ; then
# Set up postgresql database and user
diff --git a/actions/pagekite-configure b/actions/pagekite-configure
index 8a22142a5..a5af96fb0 100755
--- a/actions/pagekite-configure
+++ b/actions/pagekite-configure
@@ -48,10 +48,6 @@ def parse_arguments():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
- # Get installed status
- subparsers.add_parser('get-installed',
- help='Get whether PakeKite is installed')
-
# Start PageKite
subparsers.add_parser('start', help='Start PageKite service')
@@ -91,14 +87,6 @@ def parse_arguments():
return parser.parse_args()
-def subcommand_get_installed(_):
- """Print whether PageKite is installed"""
- with open('/dev/null', 'w') as file_handle:
- status = subprocess.call(['which', 'pagekite'], stdout=file_handle)
-
- print('installed' if not status else 'not installed')
-
-
def subcommand_start(_):
"""Start PageKite service"""
status = subprocess.call(['service', 'pagekite', 'start'])
diff --git a/actions/tor b/actions/tor
index 0c2cdbfb0..f72c7fd5e 100755
--- a/actions/tor
+++ b/actions/tor
@@ -34,10 +34,6 @@ def parse_arguments():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
- # Get whether Tor is installed
- subparsers.add_parser('get-installed',
- help='Get whether Tor is installed')
-
# Get whether Tor is running
subparsers.add_parser('is-running', help='Get whether Tor is running')
@@ -59,11 +55,6 @@ def parse_arguments():
return parser.parse_args()
-def subcommand_get_installed(_):
- """Get whether Tor is installed"""
- print('installed' if get_installed() else 'not installed')
-
-
def subcommand_is_running(_):
"""Get whether Tor is running"""
try:
@@ -101,7 +92,7 @@ def subcommand_get_hs(_):
def subcommand_enable_hs(_):
"""Enable Tor hidden service"""
- if not get_installed() or get_hidden_service():
+ if get_hidden_service():
return
with open(TOR_CONFIG, 'r') as conffile:
@@ -121,7 +112,7 @@ def subcommand_enable_hs(_):
def subcommand_disable_hs(_):
"""Disable Tor hidden service"""
- if not get_installed() or not get_hidden_service():
+ if not get_hidden_service():
return
with open(TOR_CONFIG, 'r') as conffile:
@@ -154,19 +145,8 @@ def subcommand_disable_hs(_):
subprocess.call(['service', 'tor', 'restart'])
-def get_installed():
- """Get whether Tor is installed"""
- with open('/dev/null', 'w') as file_handle:
- status = subprocess.call(['which', 'tor'], stdout=file_handle)
-
- return not status
-
-
def set_tor_service(enable):
"""Enable/disable Tor service; enable: boolean"""
- if not get_installed():
- return
-
newline = 'RUN_DAEMON="yes"\n' if enable else 'RUN_DAEMON="no"\n'
with open(SERVICE_CONFIG, 'r') as file:
@@ -182,9 +162,6 @@ def set_tor_service(enable):
def get_hidden_service():
"""Return a string with configured Tor hidden service information"""
- if not get_installed():
- return ''
-
hs_dir = None
hs_ports = []
diff --git a/actions/xmpp b/actions/xmpp
index 1a5a5783a..115fe375c 100755
--- a/actions/xmpp
+++ b/actions/xmpp
@@ -23,6 +23,14 @@ Configuration helper for the ejabberd service
import argparse
import subprocess
+import os
+import socket
+import re
+
+JWCHAT_CONFIG = '/etc/jwchat/config.js'
+EJABBERD_CONFIG = '/etc/ejabberd/ejabberd.yml'
+EJABBERD_BACKUP = '/var/log/ejabberd/ejabberd.dump'
+EJABBERD_BACKUP_NEW = '/var/log/ejabberd/ejabberd_new.dump'
def parse_arguments():
@@ -30,9 +38,29 @@ def parse_arguments():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
- # Get whether ejabberd is installed
- subparsers.add_parser('get-installed',
- help='Get whether ejabberd is installed')
+ # Prepare ejabberd for hostname change
+ pre_hostname_change = subparsers.add_parser(
+ 'pre-change-hostname',
+ help='Prepare ejabberd for hostname change')
+ pre_hostname_change.add_argument('--old-hostname',
+ help='Previous hostname')
+ pre_hostname_change.add_argument('--new-hostname',
+ help='New hostname')
+
+ # Update ejabberd and jwchat with new hostname
+ hostname_change = subparsers.add_parser(
+ 'change-hostname',
+ help='Update ejabberd and jwchat with new hostname')
+ hostname_change.add_argument('--old-hostname',
+ help='Previous hostname')
+ hostname_change.add_argument('--new-hostname',
+ help='New hostname')
+
+ # Update ejabberd and jwchat with new domainname
+ domainname_change = subparsers.add_parser(
+ 'change-domainname',
+ help='Update ejabberd and jwchat with new domainname')
+ domainname_change.add_argument('--domainname', help='New domainname')
# Register a new user account
register = subparsers.add_parser('register',
@@ -45,37 +73,96 @@ def parse_arguments():
return parser.parse_args()
-def subcommand_get_installed(_):
- """Get whether ejabberd is installed"""
- print('installed' if get_installed() else 'not installed')
+def subcommand_pre_change_hostname(arguments):
+ """Prepare ejabberd for hostname change"""
+ old_hostname = arguments.old_hostname
+ new_hostname = arguments.new_hostname
+
+ subprocess.call(['ejabberdctl', 'backup', EJABBERD_BACKUP])
+ try:
+ subprocess.check_output(['ejabberdctl', 'mnesia-change-nodename',
+ 'ejabberd@' + old_hostname,
+ 'ejabberd@' + new_hostname,
+ EJABBERD_BACKUP, EJABBERD_BACKUP_NEW])
+ os.remove(EJABBERD_BACKUP)
+ except subprocess.CalledProcessError as err:
+ print('Failed to change hostname in ejabberd backup database: %s', err)
+
+
+def subcommand_change_hostname(arguments):
+ """Update ejabberd and jwchat with new hostname"""
+ subprocess.call(['service', 'ejabberd', 'stop'])
+ subprocess.call(['pkill', '-u', 'ejabberd'])
+
+ # Make sure there aren't files in the Mnesia spool dir
+ os.makedirs('/var/lib/ejabberd/oldfiles', exist_ok=True)
+ subprocess.call('mv /var/lib/ejabberd/*.* /var/lib/ejabberd/oldfiles/',
+ shell=True)
+
+ subprocess.call(['service', 'ejabberd', 'start'])
+
+ # restore backup database
+ if os.path.exists(EJABBERD_BACKUP_NEW):
+ try:
+ subprocess.check_output(['ejabberdctl',
+ 'restore',
+ EJABBERD_BACKUP_NEW])
+ os.remove(EJABBERD_BACKUP_NEW)
+ except subprocess.CalledProcessError as err:
+ print('Failed to restore ejabberd backup database: %s', err)
+ else:
+ print('Could not load ejabberd backup database: %s not found'
+ % EJABBERD_BACKUP_NEW)
+
+
+def subcommand_change_domainname(arguments):
+ """Update ejabberd and jwchat with new domainname"""
+ domainname = arguments.domainname
+ fqdn = socket.gethostname() + '.' + domainname
+
+ # update jwchat's sitename, if it's installed
+ if os.path.exists(JWCHAT_CONFIG):
+ with open(JWCHAT_CONFIG, 'r') as conffile:
+ lines = conffile.readlines()
+ with open(JWCHAT_CONFIG, 'w') as conffile:
+ for line in lines:
+ if re.match(r'\s*var\s+SITENAME', line):
+ conffile.write('var SITENAME = "' + fqdn + '";\n')
+ else:
+ conffile.write(line)
+ else:
+ print('Skipping configuring jwchat sitename: %s not found',
+ JWCHAT_CONFIG)
+
+ subprocess.call(['service', 'ejabberd', 'stop'])
+ subprocess.call(['pkill', '-u', 'ejabberd'])
+
+ # add updated FQDN to top of ejabberd hosts list
+ with open(EJABBERD_CONFIG, 'r') as conffile:
+ lines = conffile.readlines()
+ with open(EJABBERD_CONFIG, 'w') as conffile:
+ for line in lines:
+ conffile.write(line)
+ if re.match(r'\s*hosts:', line):
+ conffile.write(' - "' + fqdn + '"\n')
+
+ subprocess.call(['service', 'ejabberd', 'start'])
def subcommand_register(arguments):
"""Register a new user account"""
- if not get_installed():
- print('Failed to register XMPP account: ejabberd is not installed.')
- return
-
username = arguments.username
password = arguments.password
- hostname = subprocess.check_output(['hostname'])
+ fqdn = socket.getfqdn()
try:
output = subprocess.check_output(['ejabberdctl', 'register',
- username, hostname, password])
+ username, fqdn, password])
print(output.decode())
except subprocess.CalledProcessError as e:
print('Failed to register XMPP account:', e.output.decode())
-def get_installed():
- """Check if ejabberd is installed"""
- with open('/dev/null', 'w') as file_handle:
- status = subprocess.call(['which', 'ejabberdctl'], stdout=file_handle)
-
- return not status
-
-
def main():
"""Parse arguments and perform all duties"""
arguments = parse_arguments()
diff --git a/actions/xmpp-hostname-change b/actions/xmpp-hostname-change
deleted file mode 100755
index 653fdfcf5..000000000
--- a/actions/xmpp-hostname-change
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/bin/bash
-#
-# 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 .
-#
-
-# Action to set up new hostname for ejabberd and jwchat.
-
-hostname="$1"
-old_hostname=`debconf-show ejabberd | awk '/hostname/ { print $3 }'`
-
-# Based on http://www.process-one.net/docs/ejabberd/guide_en.html#htoc77
-
-BACKUP=/tmp/ejabberd.dump
-
-# Note: dpkg-reconfigure will fail if there have been manual changes made to the
-# configuration file for a package. Since this is the case for ejabberd,
-# manually update the hostname in the configuration file.
-echo "ejabberd ejabberd/hostname string $hostname" | debconf-set-selections
-echo "jwchat jwchat/ApacheServerName string $hostname" | debconf-set-selections
-DEBIAN_FRONTEND=noninteractive dpkg-reconfigure jwchat
-
-sed -i "s/$old_hostname/$hostname/g" /etc/ejabberd/ejabberd.yml
-sed -i "s/$old_hostname/$hostname/g" $BACKUP
-
-service ejabberd restart
-
-# Load backup database
-sleep 10
-ejabberdctl load $BACKUP
-rm $BACKUP
diff --git a/plinth/__init__.py b/plinth/__init__.py
index 4a9b47e83..387fc2ce7 100644
--- a/plinth/__init__.py
+++ b/plinth/__init__.py
@@ -19,4 +19,4 @@
Plinth package init file
"""
-__version__ = '0.4.1'
+__version__ = '0.4.2'
diff --git a/plinth/modules/config/config.py b/plinth/modules/config/config.py
index ec55909b1..046e1aa67 100644
--- a/plinth/modules/config/config.py
+++ b/plinth/modules/config/config.py
@@ -31,6 +31,8 @@ import socket
from plinth import actions
from plinth import cfg
+from plinth.signals import pre_hostname_change, post_hostname_change
+from plinth.signals import domainname_change
LOGGER = logging.getLogger(__name__)
@@ -41,6 +43,12 @@ def get_hostname():
return socket.gethostname()
+def get_domainname():
+ """Return the domainname"""
+ fqdn = socket.getfqdn()
+ return '.'.join(fqdn.split('.')[1:])
+
+
class TrimmedCharField(forms.CharField):
"""Trim the contents of a CharField"""
def clean(self, value):
@@ -68,6 +76,15 @@ and must not be greater than 63 characters in length.'),
validators.RegexValidator(r'^[a-zA-Z][a-zA-Z0-9]{,62}$',
_('Invalid hostname'))])
+ domainname = TrimmedCharField(
+ label=_('Domain Name'),
+ help_text=_('Your domain name is the global name by which other \
+machines on the Internet can reach you. It must consist of alphanumeric words \
+separated by dots.'),
+ validators=[
+ validators.RegexValidator(r'^[a-zA-Z][a-zA-Z0-9.]*$',
+ _('Invalid domain name'))])
+
def __init__(self, *args, **kwargs):
# pylint: disable-msg=E1101, W0233
forms.Form.__init__(self, *args, **kwargs)
@@ -124,6 +141,7 @@ def index(request):
def get_status():
"""Return the current status"""
return {'hostname': get_hostname(),
+ 'domainname': get_domainname(),
'time_zone': open('/etc/timezone').read().rstrip()}
@@ -140,6 +158,17 @@ def _apply_changes(request, old_status, new_status):
else:
messages.info(request, _('Hostname is unchanged'))
+ if old_status['domainname'] != new_status['domainname']:
+ try:
+ set_domainname(new_status['domainname'])
+ except Exception as exception:
+ messages.error(request, _('Error setting domain name: %s') %
+ exception)
+ else:
+ messages.success(request, _('Domain name set'))
+ else:
+ messages.info(request, _('Domain name is unchanged'))
+
if old_status['time_zone'] != new_status['time_zone']:
try:
actions.superuser_run('timezone-change', [new_status['time_zone']])
@@ -154,11 +183,34 @@ def _apply_changes(request, old_status, new_status):
def set_hostname(hostname):
"""Sets machine hostname to hostname"""
+ old_hostname = get_hostname()
+
# Hostname should be ASCII. If it's unicode but passed our
# valid_hostname check, convert to ASCII.
hostname = str(hostname)
+ pre_hostname_change.send_robust(sender='config',
+ old_hostname=old_hostname,
+ new_hostname=hostname)
+
LOGGER.info('Changing hostname to - %s', hostname)
- actions.superuser_run('xmpp-pre-hostname-change')
actions.superuser_run('hostname-change', hostname)
- actions.superuser_run('xmpp-hostname-change', hostname, async=True)
+
+ post_hostname_change.send_robust(sender='config',
+ old_hostname=old_hostname,
+ new_hostname=hostname)
+
+
+def set_domainname(domainname):
+ """Sets machine domain name to domainname"""
+ old_domainname = get_domainname()
+
+ # Domain name should be ASCII. If it's unicode, convert to ASCII.
+ domainname = str(domainname)
+
+ LOGGER.info('Changing domain name to - %s', domainname)
+ actions.superuser_run('domainname-change', domainname)
+
+ domainname_change.send_robust(sender='config',
+ old_domainname=old_domainname,
+ new_domainname=domainname)
diff --git a/plinth/modules/firewall/firewall.py b/plinth/modules/firewall/firewall.py
index 906573286..189762255 100644
--- a/plinth/modules/firewall/firewall.py
+++ b/plinth/modules/firewall/firewall.py
@@ -26,6 +26,7 @@ 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
@@ -42,13 +43,9 @@ def init():
@login_required
+@package.required('firewalld')
def index(request):
"""Serve introcution page"""
- if not get_installed_status():
- return TemplateResponse(request, 'firewall.html',
- {'title': _('Firewall'),
- 'firewall_status': 'not_installed'})
-
if not get_enabled_status():
return TemplateResponse(request, 'firewall.html',
{'title': _('Firewall'),
@@ -65,14 +62,8 @@ def index(request):
'external_enabled_services': external_enabled_services})
-def get_installed_status():
- """Return whether firewall is installed"""
- output = _run(['get-installed'], superuser=True)
- return output.split()[0] == 'installed'
-
-
def get_enabled_status():
- """Return whether firewall is installed"""
+ """Return whether firewall is enabled"""
output = _run(['get-status'], superuser=True)
return output.split()[0] == 'running'
diff --git a/plinth/modules/firewall/templates/firewall.html b/plinth/modules/firewall/templates/firewall.html
index aa4ea3eb6..5eac0b6db 100644
--- a/plinth/modules/firewall/templates/firewall.html
+++ b/plinth/modules/firewall/templates/firewall.html
@@ -29,13 +29,7 @@ threat from the Internet.
The following is the current status:
-{% if firewall_status = 'not_installed' %}
-
Firewall is not installed. Please install it. Firewall comes
-pre-installed with {{ cfg.box_name }}. On any Debian based system (such
-as {{ cfg.box_name }}) you may install it using the
-command aptitude install firewalld
Firewall daemon is not running. Please run it. Firewall comes
enabled by default on {{ cfg.box_name }}. On any Debian based system
diff --git a/plinth/modules/owncloud/owncloud.py b/plinth/modules/owncloud/owncloud.py
index 0fc143744..cd3402d8d 100644
--- a/plinth/modules/owncloud/owncloud.py
+++ b/plinth/modules/owncloud/owncloud.py
@@ -23,6 +23,7 @@ from gettext import gettext as _
from plinth import actions
from plinth import cfg
+from plinth import package
from plinth import service
@@ -47,6 +48,7 @@ def init():
@login_required
+@package.required('postgresql', 'php5-pgsql', 'owncloud')
def index(request):
"""Serve the ownCloud configuration page"""
status = get_status()
diff --git a/plinth/modules/owncloud/templates/owncloud.html b/plinth/modules/owncloud/templates/owncloud.html
index 663e1893f..f18dc211b 100644
--- a/plinth/modules/owncloud/templates/owncloud.html
+++ b/plinth/modules/owncloud/templates/owncloud.html
@@ -27,14 +27,10 @@
ownCloud
-
When enabled, the owncloud installation will be available
+
When enabled, the ownCloud installation will be available
from /owncloud path on the web server.
Visit this URL to set up the initial administration account for
- owncloud.
-
-
Note: Setting up owncloud for the first time might take
- 5 minutes or more, depending on download bandwidth from the
- Debian APT sources.
+ ownCloud.
{{ form|bootstrap }}
diff --git a/plinth/modules/pagekite/pagekite.py b/plinth/modules/pagekite/pagekite.py
index 9f435b77e..519a7b0fd 100644
--- a/plinth/modules/pagekite/pagekite.py
+++ b/plinth/modules/pagekite/pagekite.py
@@ -30,6 +30,7 @@ import logging
from plinth import actions
from plinth import cfg
+from plinth import package
LOGGER = logging.getLogger(__name__)
@@ -101,6 +102,7 @@ https://pagekite.net/wiki/Howto/SshOverPageKite/">instructions'))
@login_required
+@package.required('pagekite')
def configure(request):
"""Serve the configuration form"""
status = get_status()
@@ -131,11 +133,6 @@ def get_status():
"""
status = {}
- # Check if PageKite is installed
- output = _run(['get-installed'])
- if output.split()[0] != 'installed':
- return None
-
# PageKite service enabled/disabled
output = _run(['get-status'])
status['enabled'] = (output.split()[0] == 'enabled')
diff --git a/plinth/modules/pagekite/templates/pagekite_configure.html b/plinth/modules/pagekite/templates/pagekite_configure.html
index e41436a56..0d737ba9b 100644
--- a/plinth/modules/pagekite/templates/pagekite_configure.html
+++ b/plinth/modules/pagekite/templates/pagekite_configure.html
@@ -22,15 +22,6 @@
{% block content %}
-{% if not status %}
-
-
PageKite is not installed, please install it. PageKite comes
- pre-installed with {{ cfg.box_name }}. On any Debian based system
- (such as {{ cfg.box_name }}) you may install it using the command
- aptitude install pagekite
A Tor SOCKS port is available on your {{ cfg.box_name }} on TCP port
9050.
-{% else %}
-
-
Tor is not installed, please install it. Tor comes pre-installed
- with {{ cfg.box_name }}. On any Debian-based system (such as
- {{ cfg.box_name }}) you may install it using the command
- aptitude install tor.
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 web client or any
@@ -33,16 +31,4 @@
ejabberd comes pre-installed with {{ cfg.box_name }}. On any Debian-based
- system (such as {{ cfg.box_name }}) you may install it using the command
- aptitude install ejabberd.
-
-{% endif %}
-
{% endblock %}
diff --git a/plinth/modules/xmpp/xmpp.py b/plinth/modules/xmpp/xmpp.py
index 3a67c14a8..718c981e5 100644
--- a/plinth/modules/xmpp/xmpp.py
+++ b/plinth/modules/xmpp/xmpp.py
@@ -25,7 +25,10 @@ import logging
from plinth import actions
from plinth import cfg
+from plinth import package
from plinth import service
+from plinth.signals import pre_hostname_change, post_hostname_change
+from plinth.signals import domainname_change
LOGGER = logging.getLogger(__name__)
@@ -53,23 +56,18 @@ def init():
'xmpp-bosh', _('Chat Server - web interface'), is_external=True,
enabled=True)
+ pre_hostname_change.connect(on_pre_hostname_change)
+ post_hostname_change.connect(on_post_hostname_change)
+ domainname_change.connect(on_domainname_change)
+
@login_required
+@package.required('jwchat', 'ejabberd')
def index(request):
"""Serve XMPP page"""
- is_installed = actions.superuser_run(
- 'xmpp',
- ['get-installed']).strip() == 'installed'
-
- if is_installed:
- index_subsubmenu = subsubmenu
- else:
- index_subsubmenu = None
-
return TemplateResponse(request, 'xmpp.html',
{'title': _('XMPP Server'),
- 'is_installed': is_installed,
- 'subsubmenu': index_subsubmenu})
+ 'subsubmenu': subsubmenu})
class ConfigureForm(forms.Form): # pylint: disable-msg=W0232
@@ -178,3 +176,44 @@ def _register_user(request, data):
messages.error(request,
_('Failed to register account for %s: %s') %
(data['username'], output))
+
+
+def on_pre_hostname_change(sender, old_hostname, new_hostname, **kwargs):
+ """
+ Backup ejabberd database before hostname is changed.
+ """
+ del sender # Unused
+ del kwargs # Unused
+
+ actions.superuser_run('xmpp',
+ ['pre-change-hostname',
+ '--old-hostname', old_hostname,
+ '--new-hostname', new_hostname])
+
+
+def on_post_hostname_change(sender, old_hostname, new_hostname, **kwargs):
+ """
+ Update ejabberd and jwchat config after hostname is changed.
+ """
+ del sender # Unused
+ del kwargs # Unused
+
+ actions.superuser_run('xmpp',
+ ['change-hostname',
+ '--old-hostname', old_hostname,
+ '--new-hostname', new_hostname],
+ async=True)
+
+
+def on_domainname_change(sender, old_domainname, new_domainname, **kwargs):
+ """
+ Update ejabberd and jwchat config after domain name is changed.
+ """
+ del sender # Unused
+ del old_domainname # Unused
+ del kwargs # Unused
+
+ actions.superuser_run('xmpp',
+ ['change-domainname',
+ '--domainname', new_domainname],
+ async=True)
diff --git a/plinth/package.py b/plinth/package.py
new file mode 100644
index 000000000..e7fd3a344
--- /dev/null
+++ b/plinth/package.py
@@ -0,0 +1,184 @@
+#
+# 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 .
+#
+
+"""
+Framework for installing and updating distribution packages
+"""
+
+import functools
+from gi.repository import PackageKitGlib as packagekit
+import logging
+import threading
+
+import plinth
+
+
+logger = logging.getLogger(__name__)
+transactions = {}
+packages_resolved = {}
+
+
+class Transaction(object):
+ """Information about an ongoing transaction."""
+
+ 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
+
+ # Progress
+ self.allow_cancel = None
+ self.percentage = None
+ self.status = None
+ self.status_string = None
+ self.flags = None
+ self.package = None
+ self.package_id = None
+ self.item_progress = None
+ self.role = None
+ self.caller_active = None
+ self.download_size_remaining = None
+
+ def get_id(self):
+ """Return a identifier to use as a key in a map of transactions."""
+ return frozenset(self.package_names)
+
+ def __str__(self):
+ """Return the string representation of the object"""
+ return ('Transaction(packages={0}, allow_cancel={1}, status={2}, '
+ ' percentage={3}, package={4}, item_progress={5})').format(
+ 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):
+ """Run a PackageKit transaction to install given packages."""
+ package_ids = [packages_resolved[package_name].get_id()
+ for package_name in self.package_names]
+ client = packagekit.Client()
+ client.set_interactive(False)
+ client.install_packages(packagekit.TransactionFlagEnum.ONLY_TRUSTED,
+ package_ids + [None], None,
+ self.progress_callback, self)
+
+ def progress_callback(self, progress, progress_type, user_data):
+ """Process progress updates on package resolve operation"""
+ if progress_type == packagekit.ProgressType.PERCENTAGE:
+ self.percentage = progress.props.percentage
+ elif progress_type == packagekit.ProgressType.PACKAGE:
+ self.package = progress.props.package
+ elif progress_type == packagekit.ProgressType.ALLOW_CANCEL:
+ self.allow_cancel = progress.props.allow_cancel
+ elif progress_type == packagekit.ProgressType.PACKAGE_ID:
+ self.package_id = progress.props.package_id
+ elif progress_type == packagekit.ProgressType.ITEM_PROGRESS:
+ self.item_progress = progress.props.item_progress
+ elif progress_type == packagekit.ProgressType.STATUS:
+ self.status = progress.props.status
+ self.status_string = \
+ packagekit.StatusEnum.to_string(progress.props.status)
+ if self.status == packagekit.StatusEnum.FINISHED:
+ self.finish()
+ elif progress_type == packagekit.ProgressType.TRANSACTION_FLAGS:
+ self.flags = progress.props.transaction_flags
+ elif progress_type == packagekit.ProgressType.ROLE:
+ self.role = progress.props.role
+ elif progress_type == packagekit.ProgressType.CALLER_ACTIVE:
+ self.caller_active = progress.props.caller_active
+ elif progress_type == packagekit.ProgressType.DOWNLOAD_SIZE_REMAINING:
+ self.download_size_remaining = \
+ progress.props.download_size_remaining
+ else:
+ logger.info('Unhandle packagekit progress callback - %s, %s',
+ progress, progress_type)
+
+ def finish(self):
+ """Perform clean up operations on the transaction.
+
+ Remove self from global transactions list.
+ """
+ del transactions[self.get_id()]
+
+
+def required(*package_names):
+ """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 is_installing(package_names) and \
+ check_installed(package_names):
+ return func(request, *args, **kwargs)
+
+ view = plinth.views.PackageInstallView.as_view()
+ return view(request, package_names=package_names, *args, **kwargs)
+
+ return wrapper
+
+ return wrapper2
+
+
+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,
+ 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
+
+ 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):
+ """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)
+ transactions[frozenset(package_names)] = transaction
+
+ transaction.start_install()
diff --git a/plinth/signals.py b/plinth/signals.py
index f02002289..568c437fe 100644
--- a/plinth/signals.py
+++ b/plinth/signals.py
@@ -25,3 +25,6 @@ from django.dispatch import Signal
service_enabled = Signal(providing_args=['service_id', 'enabled'])
pre_module_loading = Signal()
post_module_loading = Signal()
+pre_hostname_change = Signal(providing_args=['old_hostname', 'new_hostname'])
+post_hostname_change = Signal(providing_args=['old_hostname', 'new_hostname'])
+domainname_change = Signal(providing_args=['old_domainname', 'new_domainname'])
diff --git a/plinth/templates/package_install.html b/plinth/templates/package_install.html
new file mode 100644
index 000000000..f853a3e2c
--- /dev/null
+++ b/plinth/templates/package_install.html
@@ -0,0 +1,79 @@
+{% extends "base.html" %}
+{% comment %}
+#
+# This file is part of Plinth.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+{% endcomment %}
+
+{% load bootstrap %}
+
+
+{% block page_head %}
+
+ {% if is_installing %}
+
+ {% endif %}
+
+{% endblock %}
+
+
+{% block content %}
+
+
Installation
+
+ {% if not is_installing %}
+
+
This feature requires addtional packages to be installed. Do you
+ wish to install them?
+
+
+
+
Package
Summary
+
+
+ {% for package in packages %}
+
+
{{ package.get_name }}
+
{{ package.get_summary }}
+
+ {% endfor %}
+
+
+
+
+
+ {% else %}
+
+ {% for key, transaction in transactions.items %}
+