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

- -{% elif firewall_status = 'not_running' %} +{% if firewall_status = 'not_running' %}

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

- -{% else %} -
{% csrf_token %} @@ -52,8 +43,6 @@
-{% endif %} - {% endblock %} {% block page_js %} diff --git a/plinth/modules/tor/templates/tor.html b/plinth/modules/tor/templates/tor.html index 73d571232..7d43c2457 100644 --- a/plinth/modules/tor/templates/tor.html +++ b/plinth/modules/tor/templates/tor.html @@ -24,8 +24,6 @@

Tor

-{% if is_installed %} -

Status

@@ -80,15 +78,6 @@ port-forwarded, if necessary:

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.

- -{% endif %} - {% endblock %} {% block sidebar %} diff --git a/plinth/modules/tor/tor.py b/plinth/modules/tor/tor.py index 9c2fa4308..7e300d169 100644 --- a/plinth/modules/tor/tor.py +++ b/plinth/modules/tor/tor.py @@ -27,6 +27,7 @@ from gettext import gettext as _ from plinth import actions from plinth import cfg +from plinth import package class TorForm(forms.Form): # pylint: disable=W0232 @@ -43,6 +44,7 @@ def init(): @login_required +@package.required('tor') def index(request): """Service the index page""" status = get_status() @@ -61,7 +63,6 @@ def index(request): return TemplateResponse(request, 'tor.html', {'title': _('Tor Control Panel'), - 'is_installed': status['is_installed'], 'is_running': status['is_running'], 'tor_ports': status['ports'], 'tor_hs_enabled': status['hs_enabled'], @@ -72,9 +73,6 @@ def index(request): def get_status(): """Return the current status""" - is_installed = actions.superuser_run( - 'tor', - ['get-installed']).strip() == 'installed' is_running = actions.superuser_run('tor', ['is-running']).strip() == 'yes' output = actions.superuser_run('tor-get-ports') @@ -103,8 +101,7 @@ def get_status(): hs_hostname = hs_info[0] hs_ports = hs_info[1] - return {'is_installed': is_installed, - 'is_running': is_running, + return {'is_running': is_running, 'ports': ports, 'hs_enabled': hs_enabled, 'hs_hostname': hs_hostname, diff --git a/plinth/modules/xmpp/templates/xmpp.html b/plinth/modules/xmpp/templates/xmpp.html index 1a8659e47..7a81d9b8a 100644 --- a/plinth/modules/xmpp/templates/xmpp.html +++ b/plinth/modules/xmpp/templates/xmpp.html @@ -22,8 +22,6 @@ {% block content %} -{% if is_installed %} -

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 @@

Launch web client

-{% else %} - -

XMPP Server

- -

The XMPP server ejabberd is not installed.

- -

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?

+ + + + + + + {% for package in packages %} + + + + + {% endfor %} + +
PackageSummary
{{ package.get_name }}{{ package.get_summary }}
+ +
+ {% csrf_token %} + +
+ + {% else %} + + {% for key, transaction in transactions.items %} +
Installing {{ transaction.package_names|join:", " }}: + {{ transaction.status_string }} +
+
+
+ {{ transaction.percentage }}% complete +
+
+ {% endfor %} + + {% endif %} + +{% endblock %} diff --git a/plinth/views.py b/plinth/views.py index 481911979..e63501af5 100644 --- a/plinth/views.py +++ b/plinth/views.py @@ -21,11 +21,42 @@ Main Plinth views from django.core.urlresolvers import reverse from django.http.response import HttpResponseRedirect +from django.views.generic import TemplateView + +from plinth import package as package_module def index(request): - """Serve the main index page""" + """Serve the main index page.""" if request.user.is_authenticated(): return HttpResponseRedirect(reverse('apps:index')) return HttpResponseRedirect(reverse('help:about')) + + +class PackageInstallView(TemplateView): + """View to prompt and install packages.""" + template_name = 'package_install.html' + + def get_context_data(self, **kwargs): + """Return the context data rendering the template.""" + context = super(PackageInstallView, self).get_context_data(**kwargs) + + if 'packages_names' not in context: + context['package_names'] = self.kwargs.get('package_names', []) + context['packages'] = [package_module.packages_resolved[package_name] + for package_name in context['package_names']] + context['is_installing'] = \ + package_module.is_installing(context['package_names']) + context['transactions'] = package_module.transactions + + return context + + def post(self, *args, **kwargs): + """Handle installing packages + + Start the package installation, and refresh the page every x seconds to + keep displaying PackageInstallView.get() with the installation status. + """ + package_module.start_install(self.kwargs['package_names']) + return self.render_to_response(self.get_context_data()) diff --git a/setup.py b/setup.py index 970a0f8de..1e0d85247 100755 --- a/setup.py +++ b/setup.py @@ -114,7 +114,8 @@ setuptools.setup( install_requires=[ 'cherrypy >= 3.0', 'django >= 1.7.0', - 'django-bootstrap-form' + 'django-bootstrap-form', + 'pygobject' ], tests_require=['coverage >= 3.7'], include_package_data=True,