From 20318b2f25631457f8e1a27e5b543e9fa73237d1 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Tue, 2 Dec 2014 22:28:43 -0500 Subject: [PATCH 01/30] Use invoke-rc.d to avoid service mask error. Fixes #9. --- actions/hostname-change | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/hostname-change b/actions/hostname-change index 3556529ce..37a6b1e1e 100755 --- a/actions/hostname-change +++ b/actions/hostname-change @@ -20,7 +20,7 @@ hostname="$1" echo "$hostname" > /etc/hostname if [ -x /etc/init.d/hostname.sh ] ; then - service hostname.sh start + invoke-rc.d hostname.sh start else service hostname start fi From ff4fd4728ceb4a85a8fe3d7a83aef025f6af4852 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Tue, 2 Dec 2014 22:54:25 -0500 Subject: [PATCH 02/30] Don't use dpkg-reconfigure to modify configuration files. It will fail if the files have been modified another way. --- actions/xmpp-hostname-change | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/actions/xmpp-hostname-change b/actions/xmpp-hostname-change index 653fdfcf5..8bde7c9ce 100755 --- a/actions/xmpp-hostname-change +++ b/actions/xmpp-hostname-change @@ -25,13 +25,7 @@ old_hostname=`debconf-show ejabberd | awk '/hostname/ { print $3 }'` 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/var SITENAME = \"$old_hostname\";/var SITENAME = \"$hostname\";/" /etc/jwchat/config.js sed -i "s/$old_hostname/$hostname/g" /etc/ejabberd/ejabberd.yml sed -i "s/$old_hostname/$hostname/g" $BACKUP From 6fc721a72784bce3535ac398951ef6b9a2016931 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Wed, 3 Dec 2014 20:00:03 -0500 Subject: [PATCH 03/30] Convert xmpp-hostname-change to python, merge into xmpp action file. --- actions/xmpp | 52 +++++++++++++++++++++++++++++++++ actions/xmpp-hostname-change | 37 ----------------------- plinth/modules/config/config.py | 6 +++- 3 files changed, 57 insertions(+), 38 deletions(-) delete mode 100755 actions/xmpp-hostname-change diff --git a/actions/xmpp b/actions/xmpp index 1a5a5783a..9de08ad1f 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -23,6 +23,13 @@ Configuration helper for the ejabberd service import argparse import subprocess +import re +import time +import os + +JWCHAT_CONFIG = '/etc/jwchat/config.js' +EJABBERD_CONFIG = '/etc/ejabberd/ejabberd.yml' +EJABBERD_BACKUP = '/tmp/ejabberd.dump' def parse_arguments(): @@ -34,6 +41,15 @@ def parse_arguments(): subparsers.add_parser('get-installed', help='Get whether ejabberd is installed') + # 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') + # Register a new user account register = subparsers.add_parser('register', help='Register a new user account') @@ -50,6 +66,42 @@ def subcommand_get_installed(_): print('installed' if get_installed() else 'not installed') +def subcommand_change_hostname(arguments): + """Update ejabberd and jwchat with new hostname""" + if not get_installed(): + print('Failed to update XMPP hostname: ejabberd is not installed.') + + old_hostname = arguments.old_hostname + new_hostname = arguments.new_hostname + + with open(JWCHAT_CONFIG, 'r') as conffile: + lines = conffile.readlines() + with open(JWCHAT_CONFIG, 'w') as conffile: + for line in lines: + conffile.write(re.sub('var SITENAME = "' + old_hostname + '";', + 'var SITENAME = "' + new_hostname + '";', + line)) + + with open(EJABBERD_CONFIG, 'r') as conffile: + lines = conffile.readlines() + with open(EJABBERD_CONFIG, 'w') as conffile: + for line in lines: + conffile.write(re.sub(old_hostname, new_hostname, line)) + + with open(EJABBERD_BACKUP, 'r') as dumpfile: + lines = dumpfile.readlines() + with open(EJABBERD_BACKUP, 'w') as dumpfile: + for line in lines: + dumpfile.write(re.sub(old_hostname, new_hostname, line)) + + subprocess.call(['service', 'ejabberd', 'restart']) + + # load backup database + time.sleep(10) + subprocess.call(['ejabberdctl', 'load', EJABBERD_BACKUP]) + os.remove(EJABBERD_BACKUP) + + def subcommand_register(arguments): """Register a new user account""" if not get_installed(): diff --git a/actions/xmpp-hostname-change b/actions/xmpp-hostname-change deleted file mode 100755 index 8bde7c9ce..000000000 --- a/actions/xmpp-hostname-change +++ /dev/null @@ -1,37 +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 - -sed -i "s/var SITENAME = \"$old_hostname\";/var SITENAME = \"$hostname\";/" /etc/jwchat/config.js -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/modules/config/config.py b/plinth/modules/config/config.py index ec55909b1..4d6398389 100644 --- a/plinth/modules/config/config.py +++ b/plinth/modules/config/config.py @@ -154,6 +154,8 @@ 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) @@ -161,4 +163,6 @@ def set_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) + actions.superuser_run('xmpp', 'change-hostname', + '--old-hostname', old_hostname, + '--new-hostname', hostname, async=True) From 10b234921bbaf01fa7cd261cba4271248a9fa411 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Wed, 3 Dec 2014 21:10:01 -0500 Subject: [PATCH 04/30] Track which section of ejabberd config we are in, and only edit the hosts section. Check for jwchat config and ejabberd dumpfile before trying to modify them. --- actions/xmpp | 50 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/actions/xmpp b/actions/xmpp index 9de08ad1f..b04d32fdc 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -69,37 +69,53 @@ def subcommand_get_installed(_): def subcommand_change_hostname(arguments): """Update ejabberd and jwchat with new hostname""" if not get_installed(): - print('Failed to update XMPP hostname: ejabberd is not installed.') + return old_hostname = arguments.old_hostname new_hostname = arguments.new_hostname - with open(JWCHAT_CONFIG, 'r') as conffile: - lines = conffile.readlines() - with open(JWCHAT_CONFIG, 'w') as conffile: - for line in lines: - conffile.write(re.sub('var SITENAME = "' + old_hostname + '";', - 'var SITENAME = "' + new_hostname + '";', - line)) + # 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: + conffile.write(re.sub('var SITENAME = "' + old_hostname + '";', + 'var SITENAME = "' + new_hostname + '";', + line)) + # update ejabberd hosts with open(EJABBERD_CONFIG, 'r') as conffile: lines = conffile.readlines() with open(EJABBERD_CONFIG, 'w') as conffile: + in_hosts_section = False for line in lines: - conffile.write(re.sub(old_hostname, new_hostname, line)) + if in_hosts_section: + if line.startswith(' - "'): + conffile.write(re.sub(old_hostname, new_hostname, line)) + else: + in_hosts_section = False + conffile.write(line) + else: + if line.startswith('hosts:'): + in_hosts_section = True + conffile.write(line) - with open(EJABBERD_BACKUP, 'r') as dumpfile: - lines = dumpfile.readlines() - with open(EJABBERD_BACKUP, 'w') as dumpfile: - for line in lines: - dumpfile.write(re.sub(old_hostname, new_hostname, line)) + # update ejabberd backup database + if os.path.exists(EJABBERD_BACKUP): + with open(EJABBERD_BACKUP, 'r') as dumpfile: + lines = dumpfile.readlines() + with open(EJABBERD_BACKUP, 'w') as dumpfile: + for line in lines: + dumpfile.write(re.sub(old_hostname, new_hostname, line)) subprocess.call(['service', 'ejabberd', 'restart']) # load backup database - time.sleep(10) - subprocess.call(['ejabberdctl', 'load', EJABBERD_BACKUP]) - os.remove(EJABBERD_BACKUP) + if os.path.exists(EJABBERD_BACKUP): + time.sleep(10) + subprocess.call(['ejabberdctl', 'load', EJABBERD_BACKUP]) + os.remove(EJABBERD_BACKUP) def subcommand_register(arguments): From 298938de736d83126b94226c67ac449aedcb5a4e Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Wed, 3 Dec 2014 22:10:24 -0500 Subject: [PATCH 05/30] Signal xmpp module before and after hostname is changed. --- plinth/modules/config/config.py | 13 +++++++++---- plinth/modules/xmpp/xmpp.py | 28 ++++++++++++++++++++++++++++ plinth/signals.py | 2 ++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/plinth/modules/config/config.py b/plinth/modules/config/config.py index 4d6398389..d91215d45 100644 --- a/plinth/modules/config/config.py +++ b/plinth/modules/config/config.py @@ -31,6 +31,7 @@ import socket from plinth import actions from plinth import cfg +from plinth.signals import pre_hostname_change, post_hostname_change LOGGER = logging.getLogger(__name__) @@ -160,9 +161,13 @@ def set_hostname(hostname): # 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', 'change-hostname', - '--old-hostname', old_hostname, - '--new-hostname', hostname, async=True) + + post_hostname_change.send_robust(sender='config', + old_hostname=old_hostname, + new_hostname=hostname) diff --git a/plinth/modules/xmpp/xmpp.py b/plinth/modules/xmpp/xmpp.py index 3a67c14a8..fe4a21ef0 100644 --- a/plinth/modules/xmpp/xmpp.py +++ b/plinth/modules/xmpp/xmpp.py @@ -26,6 +26,7 @@ import logging from plinth import actions from plinth import cfg from plinth import service +from plinth.signals import pre_hostname_change, post_hostname_change LOGGER = logging.getLogger(__name__) @@ -53,6 +54,9 @@ 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) + @login_required def index(request): @@ -178,3 +182,27 @@ 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 old_hostname # Unused + del new_hostname # Unused + del kwargs # Unused + + actions.superuser_run('xmpp-pre-hostname-change') + + +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', hostname, async=True) diff --git a/plinth/signals.py b/plinth/signals.py index f02002289..e5e5d718c 100644 --- a/plinth/signals.py +++ b/plinth/signals.py @@ -25,3 +25,5 @@ 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']) From 48b6ff372867b4ff9c30a9c860821a8ee4e4a04c Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Thu, 4 Dec 2014 20:13:14 -0500 Subject: [PATCH 06/30] Store ejabberd backup in non-world-readable location. --- actions/xmpp | 2 +- actions/xmpp-pre-hostname-change | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/xmpp b/actions/xmpp index b04d32fdc..44843553a 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -29,7 +29,7 @@ import os JWCHAT_CONFIG = '/etc/jwchat/config.js' EJABBERD_CONFIG = '/etc/ejabberd/ejabberd.yml' -EJABBERD_BACKUP = '/tmp/ejabberd.dump' +EJABBERD_BACKUP = '/var/log/ejabberd/ejabberd.dump' def parse_arguments(): diff --git a/actions/xmpp-pre-hostname-change b/actions/xmpp-pre-hostname-change index 6b3cbb6e8..22a4e70cc 100755 --- a/actions/xmpp-pre-hostname-change +++ b/actions/xmpp-pre-hostname-change @@ -18,7 +18,7 @@ # Action to backup ejabberd database before changing hostname. -BACKUP=/tmp/ejabberd.dump +BACKUP=/var/log/ejabberd/ejabberd.dump ejabberdctl dump $BACKUP ejabberdctl stop From 7fd611a5c9388359301c2cfa02f5a8f311d7f73d Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Thu, 4 Dec 2014 21:15:18 -0500 Subject: [PATCH 07/30] Only modify specified lines in ejabberd backup database when changing hostname. --- actions/xmpp | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/actions/xmpp b/actions/xmpp index 44843553a..df4e0edc2 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -69,6 +69,7 @@ def subcommand_get_installed(_): def subcommand_change_hostname(arguments): """Update ejabberd and jwchat with new hostname""" if not get_installed(): + print('Failed to update XMPP hostname: ejabberd is not installed.') return old_hostname = arguments.old_hostname @@ -83,6 +84,9 @@ def subcommand_change_hostname(arguments): conffile.write(re.sub('var SITENAME = "' + old_hostname + '";', 'var SITENAME = "' + new_hostname + '";', line)) + else: + print('Skipping configuring jwchat hostname: %s not found' + % JWCHAT_CONFIG) # update ejabberd hosts with open(EJABBERD_CONFIG, 'r') as conffile: @@ -92,7 +96,7 @@ def subcommand_change_hostname(arguments): for line in lines: if in_hosts_section: if line.startswith(' - "'): - conffile.write(re.sub(old_hostname, new_hostname, line)) + conffile.write(line.replace(old_hostname, new_hostname)) else: in_hosts_section = False conffile.write(line) @@ -106,8 +110,36 @@ def subcommand_change_hostname(arguments): with open(EJABBERD_BACKUP, 'r') as dumpfile: lines = dumpfile.readlines() with open(EJABBERD_BACKUP, 'w') as dumpfile: + in_pubsub_node = False for line in lines: - dumpfile.write(re.sub(old_hostname, new_hostname, line)) + if in_pubsub_node: + if line.startswith(' '): + dumpfile.write(re.sub( + '>>,<>,', + '>>,<>,', + line)) + continue + else: + in_pubsub_node = False # check other cases below + + if line.startswith('{passwd,'): + dumpfile.write(re.sub( + '">>,<<"' + old_hostname + '">>},<<"', + '">>,<<"' + new_hostname + '">>},<<"', + line)) + elif line.startswith('{pubsub_state,'): + dumpfile.write(re.sub( + '>>,<<"pubsub.' + old_hostname + '">>,<<', + '>>,<<"pubsub.' + new_hostname + '">>,<<', + line)) + elif line.startswith('{pubsub_node,'): + dumpfile.write(re.sub( + ',{<<"pubsub.' + old_hostname + '">>,<<', + ',{<<"pubsub.' + new_hostname + '">>,<<', + line)) + in_pubsub_node = True + else: + dumpfile.write(line) subprocess.call(['service', 'ejabberd', 'restart']) @@ -116,6 +148,9 @@ def subcommand_change_hostname(arguments): time.sleep(10) subprocess.call(['ejabberdctl', 'load', EJABBERD_BACKUP]) os.remove(EJABBERD_BACKUP) + else: + print('Could not load ejabberd backup database: %s not found' + % EJABBERD_BACKUP) def subcommand_register(arguments): From 588eb7250ee8968ce00ef0f5727afbcb92e015c0 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Thu, 4 Dec 2014 21:55:55 -0500 Subject: [PATCH 08/30] Fix xmpp change-hostname options. --- plinth/modules/xmpp/xmpp.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plinth/modules/xmpp/xmpp.py b/plinth/modules/xmpp/xmpp.py index fe4a21ef0..6d0df3689 100644 --- a/plinth/modules/xmpp/xmpp.py +++ b/plinth/modules/xmpp/xmpp.py @@ -203,6 +203,8 @@ def on_post_hostname_change(sender, old_hostname, new_hostname, **kwargs): del sender # Unused del kwargs # Unused - actions.superuser_run('xmpp', 'change-hostname', - '--old-hostname', old_hostname, - '--new-hostname', hostname, async=True) + actions.superuser_run('xmpp', + ['change-hostname', + '--old-hostname', old_hostname, + '--new-hostname', new_hostname], + async=True) From c12fbec30f860cdaa259c34e2dcfd059dd159169 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Fri, 5 Dec 2014 20:10:09 -0500 Subject: [PATCH 09/30] Change hostname for some more fields that may appear in ejabberd backup database. --- actions/xmpp | 54 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/actions/xmpp b/actions/xmpp index df4e0edc2..24ac73b94 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -23,7 +23,6 @@ Configuration helper for the ejabberd service import argparse import subprocess -import re import time import os @@ -81,9 +80,9 @@ def subcommand_change_hostname(arguments): lines = conffile.readlines() with open(JWCHAT_CONFIG, 'w') as conffile: for line in lines: - conffile.write(re.sub('var SITENAME = "' + old_hostname + '";', - 'var SITENAME = "' + new_hostname + '";', - line)) + conffile.write(line.replace( + 'var SITENAME = "' + old_hostname + '";', + 'var SITENAME = "' + new_hostname + '";')) else: print('Skipping configuring jwchat hostname: %s not found' % JWCHAT_CONFIG) @@ -110,33 +109,54 @@ def subcommand_change_hostname(arguments): with open(EJABBERD_BACKUP, 'r') as dumpfile: lines = dumpfile.readlines() with open(EJABBERD_BACKUP, 'w') as dumpfile: + in_roster = False in_pubsub_node = False for line in lines: + if in_roster: + if line.startswith(' '): + dumpfile.write(line.replace( + '>>,<<"' + old_hostname + '">>', + '>>,<<"' + new_hostname + '">>')) + continue + else: + in_roster = False # check other cases below + if in_pubsub_node: if line.startswith(' '): - dumpfile.write(re.sub( - '>>,<>,', - '>>,<>,', - line)) + dumpfile.write(line.replace( + '>>,<<"pubsub.' + old_hostname + '">>,<<', + '>>,<<"pubsub.' + new_hostname + '">>,<<')) continue else: in_pubsub_node = False # check other cases below if line.startswith('{passwd,'): - dumpfile.write(re.sub( + dumpfile.write(line.replace( '">>,<<"' + old_hostname + '">>},<<"', - '">>,<<"' + new_hostname + '">>},<<"', - line)) + '">>,<<"' + new_hostname + '">>},<<"')) + elif line.startswith('{private_storage,'): + dumpfile.write(line.replace( + '>>,<<"' + old_hostname + '">>,<<', + '>>,<<"' + new_hostname + '">>,<<')) + elif line.startswith('{last_activity,'): + dumpfile.write(line.replace( + '>>,<<"' + old_hostname + '">>},', + '>>,<<"' + new_hostname + '">>},')) + elif line.startswith('{roster,'): + dumpfile.write(line.replace( + '>>,<<"' + old_hostname + '">>,', + '>>,<<"' + new_hostname + '">>,')) + in_roster = True elif line.startswith('{pubsub_state,'): - dumpfile.write(re.sub( + dumpfile.write(line.replace( '>>,<<"pubsub.' + old_hostname + '">>,<<', - '>>,<<"pubsub.' + new_hostname + '">>,<<', - line)) + '>>,<<"pubsub.' + new_hostname + '">>,<<')) elif line.startswith('{pubsub_node,'): - dumpfile.write(re.sub( + dumpfile.write(line.replace( ',{<<"pubsub.' + old_hostname + '">>,<<', - ',{<<"pubsub.' + new_hostname + '">>,<<', - line)) + ',{<<"pubsub.' + new_hostname + '">>,<<').replace( + '>>,<<"/home/' + old_hostname + '">>},', + '>>,<<"/home/' + new_hostname + '"..},')) in_pubsub_node = True else: dumpfile.write(line) From 6d3c39d16654bd04b02eb38a9616353e76df1993 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Sun, 14 Dec 2014 21:34:12 -0500 Subject: [PATCH 10/30] Make sure new hostname will show up immediately. --- actions/hostname-change | 1 + 1 file changed, 1 insertion(+) diff --git a/actions/hostname-change b/actions/hostname-change index 37a6b1e1e..5bf091638 100755 --- a/actions/hostname-change +++ b/actions/hostname-change @@ -24,5 +24,6 @@ if [ -x /etc/init.d/hostname.sh ] ; then else service hostname start fi +hostname "$hostname" service avahi-daemon restart From 079d7744760844683098c9d92762f698bf18294e Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Mon, 15 Dec 2014 21:02:40 -0500 Subject: [PATCH 11/30] Use regexes to relax matches when updating xmpp hostname. --- actions/xmpp | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/actions/xmpp b/actions/xmpp index 24ac73b94..2e2e7e6bb 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -25,6 +25,7 @@ import argparse import subprocess import time import os +import re JWCHAT_CONFIG = '/etc/jwchat/config.js' EJABBERD_CONFIG = '/etc/ejabberd/ejabberd.yml' @@ -80,9 +81,10 @@ def subcommand_change_hostname(arguments): lines = conffile.readlines() with open(JWCHAT_CONFIG, 'w') as conffile: for line in lines: - conffile.write(line.replace( - 'var SITENAME = "' + old_hostname + '";', - 'var SITENAME = "' + new_hostname + '";')) + if re.match(r'\s*var\s+SITENAME', line): + conffile.write('var SITENAME = "' + new_hostname + '";') + else: + conffile.write(line) else: print('Skipping configuring jwchat hostname: %s not found' % JWCHAT_CONFIG) @@ -94,13 +96,13 @@ def subcommand_change_hostname(arguments): in_hosts_section = False for line in lines: if in_hosts_section: - if line.startswith(' - "'): + if re.match(r'\s*-\s+"', line): conffile.write(line.replace(old_hostname, new_hostname)) else: in_hosts_section = False conffile.write(line) else: - if line.startswith('hosts:'): + if re.match(r'\s*hosts:', line): in_hosts_section = True conffile.write(line) @@ -113,7 +115,7 @@ def subcommand_change_hostname(arguments): in_pubsub_node = False for line in lines: if in_roster: - if line.startswith(' '): + if re.match(r'\s+', line): dumpfile.write(line.replace( '>>,<<"' + old_hostname + '">>', '>>,<<"' + new_hostname + '">>')) @@ -122,7 +124,7 @@ def subcommand_change_hostname(arguments): in_roster = False # check other cases below if in_pubsub_node: - if line.startswith(' '): + if re.match(r'\s+', line): dumpfile.write(line.replace( '>>,<<"pubsub.' + old_hostname + '">>,<<', '>>,<<"pubsub.' + new_hostname + '">>,<<')) @@ -130,33 +132,33 @@ def subcommand_change_hostname(arguments): else: in_pubsub_node = False # check other cases below - if line.startswith('{passwd,'): + if re.match(r'\s*{passwd,', line): dumpfile.write(line.replace( '">>,<<"' + old_hostname + '">>},<<"', '">>,<<"' + new_hostname + '">>},<<"')) - elif line.startswith('{private_storage,'): + elif re.match(r'\s*{private_storage,', line): dumpfile.write(line.replace( '>>,<<"' + old_hostname + '">>,<<', '>>,<<"' + new_hostname + '">>,<<')) - elif line.startswith('{last_activity,'): + elif re.match(r'\s*{last_activity,', line): dumpfile.write(line.replace( '>>,<<"' + old_hostname + '">>},', '>>,<<"' + new_hostname + '">>},')) - elif line.startswith('{roster,'): + elif re.match(r'\s*{roster,', line): dumpfile.write(line.replace( '>>,<<"' + old_hostname + '">>,', '>>,<<"' + new_hostname + '">>,')) in_roster = True - elif line.startswith('{pubsub_state,'): + elif re.match(r'\s*{pubsub_state,', line): dumpfile.write(line.replace( '>>,<<"pubsub.' + old_hostname + '">>,<<', '>>,<<"pubsub.' + new_hostname + '">>,<<')) - elif line.startswith('{pubsub_node,'): + elif re.match(r'\s*{pubsub_node,', line): dumpfile.write(line.replace( ',{<<"pubsub.' + old_hostname + '">>,<<', ',{<<"pubsub.' + new_hostname + '">>,<<').replace( '>>,<<"/home/' + old_hostname + '">>},', - '>>,<<"/home/' + new_hostname + '"..},')) + '>>,<<"/home/' + new_hostname + '">>},')) in_pubsub_node = True else: dumpfile.write(line) From 5bee775bc8d81469e0763c3125f75a2f24f46cbb Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Mon, 15 Dec 2014 23:22:36 -0500 Subject: [PATCH 12/30] Fix some issues with replacing hostname in xmpp config. --- actions/xmpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/actions/xmpp b/actions/xmpp index 2e2e7e6bb..c02203ded 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -82,7 +82,7 @@ def subcommand_change_hostname(arguments): with open(JWCHAT_CONFIG, 'w') as conffile: for line in lines: if re.match(r'\s*var\s+SITENAME', line): - conffile.write('var SITENAME = "' + new_hostname + '";') + conffile.write('var SITENAME = "' + new_hostname + '";\n') else: conffile.write(line) else: @@ -97,7 +97,8 @@ def subcommand_change_hostname(arguments): for line in lines: if in_hosts_section: if re.match(r'\s*-\s+"', line): - conffile.write(line.replace(old_hostname, new_hostname)) + conffile.write(line.replace('"' + old_hostname + '"', + '"' + new_hostname + '"')) else: in_hosts_section = False conffile.write(line) From b98a9ad84da574ec8c169b7a8eed222ba34fd258 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Mon, 15 Dec 2014 23:23:10 -0500 Subject: [PATCH 13/30] Don't delete the ejabberd backup database if it fails to load. --- actions/xmpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/actions/xmpp b/actions/xmpp index c02203ded..6c2dae8f1 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -168,9 +168,13 @@ def subcommand_change_hostname(arguments): # load backup database if os.path.exists(EJABBERD_BACKUP): - time.sleep(10) - subprocess.call(['ejabberdctl', 'load', EJABBERD_BACKUP]) - os.remove(EJABBERD_BACKUP) + try: + subprocess.check_output(['ejabberdctl', + 'load', + EJABBERD_BACKUP]) + os.remove(EJABBERD_BACKUP) + except subprocess.CalledProcessError as err: + print('Failed to load ejabberd backup database: %s', err) else: print('Could not load ejabberd backup database: %s not found' % EJABBERD_BACKUP) From 219b7ff862e56ec60922a2662daba5d696fa4185 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Thu, 18 Dec 2014 21:09:27 -0500 Subject: [PATCH 14/30] Apply patch from Sunil to correctly set hostname in systemd environment. --- actions/hostname-change | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/actions/hostname-change b/actions/hostname-change index 5bf091638..0ae714366 100755 --- a/actions/hostname-change +++ b/actions/hostname-change @@ -18,12 +18,15 @@ hostname="$1" -echo "$hostname" > /etc/hostname -if [ -x /etc/init.d/hostname.sh ] ; then - invoke-rc.d 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 -hostname "$hostname" service avahi-daemon restart From 90c9e3b9a84e31391eb6414be2a7600beef58fc7 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Thu, 18 Dec 2014 21:56:53 -0500 Subject: [PATCH 15/30] Convert xmpp-pre-hostname-change to python, merge into xmpp action file. --- actions/xmpp | 27 +++++++++++++++++++++++++++ actions/xmpp-pre-hostname-change | 27 --------------------------- plinth/modules/xmpp/xmpp.py | 7 ++++--- 3 files changed, 31 insertions(+), 30 deletions(-) delete mode 100755 actions/xmpp-pre-hostname-change diff --git a/actions/xmpp b/actions/xmpp index 6c2dae8f1..66e36bb28 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -41,6 +41,15 @@ def parse_arguments(): 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', @@ -66,6 +75,24 @@ def subcommand_get_installed(_): print('installed' if get_installed() else 'not installed') +def subcommand_pre_change_hostname(arguments): + """Prepare ejabberd for hostname change""" + if not get_installed(): + print('Failed to update XMPP hostname: ejabberd is not installed.') + return + + old_hostname = arguments.old_hostname + new_hostname = arguments.new_hostname + + subprocess.call(['ejabberdctl', 'dump', EJABBERD_BACKUP]) + subprocess.call(['ejabberdctl', 'stop']) + + # 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) + + def subcommand_change_hostname(arguments): """Update ejabberd and jwchat with new hostname""" if not get_installed(): diff --git a/actions/xmpp-pre-hostname-change b/actions/xmpp-pre-hostname-change deleted file mode 100755 index 22a4e70cc..000000000 --- a/actions/xmpp-pre-hostname-change +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/sh -# -# 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 backup ejabberd database before changing hostname. - -BACKUP=/var/log/ejabberd/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/ diff --git a/plinth/modules/xmpp/xmpp.py b/plinth/modules/xmpp/xmpp.py index 6d0df3689..503ee8b7c 100644 --- a/plinth/modules/xmpp/xmpp.py +++ b/plinth/modules/xmpp/xmpp.py @@ -189,11 +189,12 @@ def on_pre_hostname_change(sender, old_hostname, new_hostname, **kwargs): Backup ejabberd database before hostname is changed. """ del sender # Unused - del old_hostname # Unused - del new_hostname # Unused del kwargs # Unused - actions.superuser_run('xmpp-pre-hostname-change') + 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): From 126d466dc75432f86e0fb2dfb7d33d0cad139b1c Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Fri, 19 Dec 2014 20:54:46 -0500 Subject: [PATCH 16/30] Use mnesia-change-nodename to update ejabberd backup database. As Sunil suggested, rearrange commands so we can do this with only 1 ejabberd restart. --- actions/xmpp | 94 ++++++++++++++-------------------------------------- 1 file changed, 24 insertions(+), 70 deletions(-) diff --git a/actions/xmpp b/actions/xmpp index 66e36bb28..52a323df9 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -30,6 +30,7 @@ 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(): @@ -84,13 +85,14 @@ def subcommand_pre_change_hostname(arguments): old_hostname = arguments.old_hostname new_hostname = arguments.new_hostname - subprocess.call(['ejabberdctl', 'dump', EJABBERD_BACKUP]) - subprocess.call(['ejabberdctl', 'stop']) - - # 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(['ejabberdctl', 'backup', EJABBERD_BACKUP]) + try: + subprocess.check_output(['ejabberdctl', 'mnesia-change-nodename', + old_hostname, 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): @@ -134,77 +136,29 @@ def subcommand_change_hostname(arguments): in_hosts_section = True conffile.write(line) - # update ejabberd backup database - if os.path.exists(EJABBERD_BACKUP): - with open(EJABBERD_BACKUP, 'r') as dumpfile: - lines = dumpfile.readlines() - with open(EJABBERD_BACKUP, 'w') as dumpfile: - in_roster = False - in_pubsub_node = False - for line in lines: - if in_roster: - if re.match(r'\s+', line): - dumpfile.write(line.replace( - '>>,<<"' + old_hostname + '">>', - '>>,<<"' + new_hostname + '">>')) - continue - else: - in_roster = False # check other cases below + subprocess.call(['ejabberdctl', 'stop']) + subprocess.call(['service', 'ejabberd', 'stop']) + subprocess.call(['pkill', '-u', 'ejabberd']) - if in_pubsub_node: - if re.match(r'\s+', line): - dumpfile.write(line.replace( - '>>,<<"pubsub.' + old_hostname + '">>,<<', - '>>,<<"pubsub.' + new_hostname + '">>,<<')) - continue - else: - in_pubsub_node = False # check other cases below + # 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) - if re.match(r'\s*{passwd,', line): - dumpfile.write(line.replace( - '">>,<<"' + old_hostname + '">>},<<"', - '">>,<<"' + new_hostname + '">>},<<"')) - elif re.match(r'\s*{private_storage,', line): - dumpfile.write(line.replace( - '>>,<<"' + old_hostname + '">>,<<', - '>>,<<"' + new_hostname + '">>,<<')) - elif re.match(r'\s*{last_activity,', line): - dumpfile.write(line.replace( - '>>,<<"' + old_hostname + '">>},', - '>>,<<"' + new_hostname + '">>},')) - elif re.match(r'\s*{roster,', line): - dumpfile.write(line.replace( - '>>,<<"' + old_hostname + '">>,', - '>>,<<"' + new_hostname + '">>,')) - in_roster = True - elif re.match(r'\s*{pubsub_state,', line): - dumpfile.write(line.replace( - '>>,<<"pubsub.' + old_hostname + '">>,<<', - '>>,<<"pubsub.' + new_hostname + '">>,<<')) - elif re.match(r'\s*{pubsub_node,', line): - dumpfile.write(line.replace( - ',{<<"pubsub.' + old_hostname + '">>,<<', - ',{<<"pubsub.' + new_hostname + '">>,<<').replace( - '>>,<<"/home/' + old_hostname + '">>},', - '>>,<<"/home/' + new_hostname + '">>},')) - in_pubsub_node = True - else: - dumpfile.write(line) + subprocess.call(['service', 'ejabberd', 'start']) - subprocess.call(['service', 'ejabberd', 'restart']) - - # load backup database - if os.path.exists(EJABBERD_BACKUP): + # restore backup database + if os.path.exists(EJABBERD_BACKUP_NEW): try: subprocess.check_output(['ejabberdctl', - 'load', - EJABBERD_BACKUP]) - os.remove(EJABBERD_BACKUP) + 'restore', + EJABBERD_BACKUP_NEW]) + os.remove(EJABBERD_BACKUP_NEW) except subprocess.CalledProcessError as err: - print('Failed to load ejabberd backup database: %s', err) + print('Failed to restore ejabberd backup database: %s', err) else: print('Could not load ejabberd backup database: %s not found' - % EJABBERD_BACKUP) + % EJABBERD_BACKUP_NEW) def subcommand_register(arguments): From 8c96519e342fe92ca93c2982a46143c3ea0b0452 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Fri, 19 Dec 2014 21:54:59 -0500 Subject: [PATCH 17/30] Use correct node names for ejabberd. --- actions/xmpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/actions/xmpp b/actions/xmpp index 52a323df9..f4537608a 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -88,7 +88,8 @@ def subcommand_pre_change_hostname(arguments): subprocess.call(['ejabberdctl', 'backup', EJABBERD_BACKUP]) try: subprocess.check_output(['ejabberdctl', 'mnesia-change-nodename', - old_hostname, new_hostname, + 'ejabberd@' + old_hostname, + 'ejabberd@' + new_hostname, EJABBERD_BACKUP, EJABBERD_BACKUP_NEW]) os.remove(EJABBERD_BACKUP) except subprocess.CalledProcessError as err: From b8a70075529a4ee283c3d2e8157e69ab1cd150ec Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Fri, 19 Dec 2014 22:06:34 -0500 Subject: [PATCH 18/30] Remove unnecessary ejabberdctl call. --- actions/xmpp | 1 - 1 file changed, 1 deletion(-) diff --git a/actions/xmpp b/actions/xmpp index f4537608a..94ccfeb8d 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -137,7 +137,6 @@ def subcommand_change_hostname(arguments): in_hosts_section = True conffile.write(line) - subprocess.call(['ejabberdctl', 'stop']) subprocess.call(['service', 'ejabberd', 'stop']) subprocess.call(['pkill', '-u', 'ejabberd']) From 01bea7808cc797a45d6a787cdb24c03a7721ec02 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Sat, 20 Dec 2014 11:22:50 -0500 Subject: [PATCH 19/30] Don't change ejabberd hosts or jwchat sitename when changing hostname. Update these only when the domain name is changed. --- actions/xmpp | 75 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/actions/xmpp b/actions/xmpp index 94ccfeb8d..989b95f63 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -60,6 +60,12 @@ def parse_arguments(): 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', help='Register a new user account') @@ -105,38 +111,6 @@ def subcommand_change_hostname(arguments): old_hostname = arguments.old_hostname new_hostname = arguments.new_hostname - # 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 = "' + new_hostname + '";\n') - else: - conffile.write(line) - else: - print('Skipping configuring jwchat hostname: %s not found' - % JWCHAT_CONFIG) - - # update ejabberd hosts - with open(EJABBERD_CONFIG, 'r') as conffile: - lines = conffile.readlines() - with open(EJABBERD_CONFIG, 'w') as conffile: - in_hosts_section = False - for line in lines: - if in_hosts_section: - if re.match(r'\s*-\s+"', line): - conffile.write(line.replace('"' + old_hostname + '"', - '"' + new_hostname + '"')) - else: - in_hosts_section = False - conffile.write(line) - else: - if re.match(r'\s*hosts:', line): - in_hosts_section = True - conffile.write(line) - subprocess.call(['service', 'ejabberd', 'stop']) subprocess.call(['pkill', '-u', 'ejabberd']) @@ -161,6 +135,43 @@ def subcommand_change_hostname(arguments): % EJABBERD_BACKUP_NEW) +def subcommand_change_domainname(arguments): + """Update ejabberd and jwchat with new domainname""" + if not get_installed(): + print('Failed to update XMPP domainname: ejabberd is not installed.') + return + + domainname = arguments.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 = "' + domainname + '";\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 new domainname 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(' - "' + domainname + '"\n') + + subprocess.call(['service', 'ejabberd', 'start']) + + def subcommand_register(arguments): """Register a new user account""" if not get_installed(): From 87b860bff4a80bea3b0986df412004448d415d17 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Sat, 20 Dec 2014 12:15:27 -0500 Subject: [PATCH 20/30] Add a configuration field to change FQDN. --- actions/fqdn-change | 27 +++++++++++++++++++++ plinth/modules/config/config.py | 42 +++++++++++++++++++++++++++++++++ plinth/signals.py | 1 + 3 files changed, 70 insertions(+) create mode 100755 actions/fqdn-change diff --git a/actions/fqdn-change b/actions/fqdn-change new file mode 100755 index 000000000..e12d038c3 --- /dev/null +++ b/actions/fqdn-change @@ -0,0 +1,27 @@ +#!/bin/sh +# +# 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 . +# + +fqdn="$1" +hostname=$(hostname) + +if grep -q 127.0.1.1 /etc/hosts ; then + sed -i "s/127.0.1.1.*/127.0.1.1 $fqdn $hostname/" /etc/hosts +else + sed -i "/127.0.0.1.*/a \ +127.0.1.1 $fqdn $hostname" /etc/hosts +fi diff --git a/plinth/modules/config/config.py b/plinth/modules/config/config.py index d91215d45..9c5efd641 100644 --- a/plinth/modules/config/config.py +++ b/plinth/modules/config/config.py @@ -32,6 +32,7 @@ import socket from plinth import actions from plinth import cfg from plinth.signals import pre_hostname_change, post_hostname_change +from plinth.signals import fqdn_change LOGGER = logging.getLogger(__name__) @@ -42,6 +43,11 @@ def get_hostname(): return socket.gethostname() +def get_fqdn(): + """Return the fully qualified domain name""" + return socket.getfqdn() + + class TrimmedCharField(forms.CharField): """Trim the contents of a CharField""" def clean(self, value): @@ -69,6 +75,15 @@ and must not be greater than 63 characters in length.'), validators.RegexValidator(r'^[a-zA-Z][a-zA-Z0-9]{,62}$', _('Invalid hostname'))]) + fqdn = TrimmedCharField( + label=_('FQDN'), + help_text=_('Your FQDN 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 FQDN'))]) + def __init__(self, *args, **kwargs): # pylint: disable-msg=E1101, W0233 forms.Form.__init__(self, *args, **kwargs) @@ -125,6 +140,7 @@ def index(request): def get_status(): """Return the current status""" return {'hostname': get_hostname(), + 'fqdn': get_fqdn(), 'time_zone': open('/etc/timezone').read().rstrip()} @@ -141,6 +157,17 @@ def _apply_changes(request, old_status, new_status): else: messages.info(request, _('Hostname is unchanged')) + if old_status['fqdn'] != new_status['fqdn']: + try: + set_fqdn(new_status['fqdn']) + except Exception as exception: + messages.error(request, _('Error setting FQDN: %s') % + exception) + else: + messages.success(request, _('FQDN set')) + else: + messages.info(request, _('FQDN is unchanged')) + if old_status['time_zone'] != new_status['time_zone']: try: actions.superuser_run('timezone-change', [new_status['time_zone']]) @@ -171,3 +198,18 @@ def set_hostname(hostname): post_hostname_change.send_robust(sender='config', old_hostname=old_hostname, new_hostname=hostname) + + +def set_fqdn(fqdn): + """Sets machine FQDN to fqdn""" + old_fqdn = get_fqdn() + + # FQDN should be ASCII. If it's unicode, convert to ASCII. + fqdn = str(fqdn) + + LOGGER.info('Changing FQDN to - %s', fqdn) + actions.superuser_run('fqdn-change', fqdn) + + fqdn_change.send_robust(sender='config', + old_fqdn=old_fqdn, + new_fqdn=fqdn) diff --git a/plinth/signals.py b/plinth/signals.py index e5e5d718c..e07d9380b 100644 --- a/plinth/signals.py +++ b/plinth/signals.py @@ -27,3 +27,4 @@ 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']) +fqdn_change = Signal(providing_args=['old_fqdn', 'new_fqdn']) From 3d7de0778b3ca4855100b55256e561a6408560ae Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Sat, 20 Dec 2014 14:01:59 -0500 Subject: [PATCH 21/30] Allow user to change domain name, then determine the FQDN from it. --- actions/{fqdn-change => domainname-change} | 6 +-- actions/xmpp | 12 +++-- plinth/modules/config/config.py | 51 +++++++++++----------- plinth/modules/xmpp/xmpp.py | 16 +++++++ plinth/signals.py | 2 +- 5 files changed, 51 insertions(+), 36 deletions(-) rename actions/{fqdn-change => domainname-change} (83%) diff --git a/actions/fqdn-change b/actions/domainname-change similarity index 83% rename from actions/fqdn-change rename to actions/domainname-change index e12d038c3..d1d501591 100755 --- a/actions/fqdn-change +++ b/actions/domainname-change @@ -16,12 +16,12 @@ # along with this program. If not, see . # -fqdn="$1" +domainname="$1" hostname=$(hostname) if grep -q 127.0.1.1 /etc/hosts ; then - sed -i "s/127.0.1.1.*/127.0.1.1 $fqdn $hostname/" /etc/hosts + 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 $fqdn $hostname" /etc/hosts +127.0.1.1 $hostname.$domainname $hostname" /etc/hosts fi diff --git a/actions/xmpp b/actions/xmpp index 989b95f63..1d8ef0b99 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -23,8 +23,8 @@ Configuration helper for the ejabberd service import argparse import subprocess -import time import os +import socket import re JWCHAT_CONFIG = '/etc/jwchat/config.js' @@ -108,9 +108,6 @@ def subcommand_change_hostname(arguments): print('Failed to update XMPP hostname: ejabberd is not installed.') return - old_hostname = arguments.old_hostname - new_hostname = arguments.new_hostname - subprocess.call(['service', 'ejabberd', 'stop']) subprocess.call(['pkill', '-u', 'ejabberd']) @@ -142,6 +139,7 @@ def subcommand_change_domainname(arguments): return domainname = arguments.domainname + fqdn = socket.gethostname() + '.' + domainname # update jwchat's sitename, if it's installed if os.path.exists(JWCHAT_CONFIG): @@ -150,7 +148,7 @@ def subcommand_change_domainname(arguments): with open(JWCHAT_CONFIG, 'w') as conffile: for line in lines: if re.match(r'\s*var\s+SITENAME', line): - conffile.write('var SITENAME = "' + domainname + '";\n') + conffile.write('var SITENAME = "' + fqdn + '";\n') else: conffile.write(line) else: @@ -160,14 +158,14 @@ def subcommand_change_domainname(arguments): subprocess.call(['service', 'ejabberd', 'stop']) subprocess.call(['pkill', '-u', 'ejabberd']) - # add new domainname to top of ejabberd hosts list + # 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(' - "' + domainname + '"\n') + conffile.write(' - "' + fqdn + '"\n') subprocess.call(['service', 'ejabberd', 'start']) diff --git a/plinth/modules/config/config.py b/plinth/modules/config/config.py index 9c5efd641..046e1aa67 100644 --- a/plinth/modules/config/config.py +++ b/plinth/modules/config/config.py @@ -32,7 +32,7 @@ import socket from plinth import actions from plinth import cfg from plinth.signals import pre_hostname_change, post_hostname_change -from plinth.signals import fqdn_change +from plinth.signals import domainname_change LOGGER = logging.getLogger(__name__) @@ -43,9 +43,10 @@ def get_hostname(): return socket.gethostname() -def get_fqdn(): - """Return the fully qualified domain name""" - return socket.getfqdn() +def get_domainname(): + """Return the domainname""" + fqdn = socket.getfqdn() + return '.'.join(fqdn.split('.')[1:]) class TrimmedCharField(forms.CharField): @@ -75,14 +76,14 @@ and must not be greater than 63 characters in length.'), validators.RegexValidator(r'^[a-zA-Z][a-zA-Z0-9]{,62}$', _('Invalid hostname'))]) - fqdn = TrimmedCharField( - label=_('FQDN'), - help_text=_('Your FQDN is the global name by which other machines \ -on the Internet can reach you. It must consist of alphanumeric words \ + 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 FQDN'))]) + _('Invalid domain name'))]) def __init__(self, *args, **kwargs): # pylint: disable-msg=E1101, W0233 @@ -140,7 +141,7 @@ def index(request): def get_status(): """Return the current status""" return {'hostname': get_hostname(), - 'fqdn': get_fqdn(), + 'domainname': get_domainname(), 'time_zone': open('/etc/timezone').read().rstrip()} @@ -157,16 +158,16 @@ def _apply_changes(request, old_status, new_status): else: messages.info(request, _('Hostname is unchanged')) - if old_status['fqdn'] != new_status['fqdn']: + if old_status['domainname'] != new_status['domainname']: try: - set_fqdn(new_status['fqdn']) + set_domainname(new_status['domainname']) except Exception as exception: - messages.error(request, _('Error setting FQDN: %s') % + messages.error(request, _('Error setting domain name: %s') % exception) else: - messages.success(request, _('FQDN set')) + messages.success(request, _('Domain name set')) else: - messages.info(request, _('FQDN is unchanged')) + messages.info(request, _('Domain name is unchanged')) if old_status['time_zone'] != new_status['time_zone']: try: @@ -200,16 +201,16 @@ def set_hostname(hostname): new_hostname=hostname) -def set_fqdn(fqdn): - """Sets machine FQDN to fqdn""" - old_fqdn = get_fqdn() +def set_domainname(domainname): + """Sets machine domain name to domainname""" + old_domainname = get_domainname() - # FQDN should be ASCII. If it's unicode, convert to ASCII. - fqdn = str(fqdn) + # Domain name should be ASCII. If it's unicode, convert to ASCII. + domainname = str(domainname) - LOGGER.info('Changing FQDN to - %s', fqdn) - actions.superuser_run('fqdn-change', fqdn) + LOGGER.info('Changing domain name to - %s', domainname) + actions.superuser_run('domainname-change', domainname) - fqdn_change.send_robust(sender='config', - old_fqdn=old_fqdn, - new_fqdn=fqdn) + domainname_change.send_robust(sender='config', + old_domainname=old_domainname, + new_domainname=domainname) diff --git a/plinth/modules/xmpp/xmpp.py b/plinth/modules/xmpp/xmpp.py index 503ee8b7c..8a38ec2bb 100644 --- a/plinth/modules/xmpp/xmpp.py +++ b/plinth/modules/xmpp/xmpp.py @@ -27,6 +27,7 @@ from plinth import actions from plinth import cfg from plinth import service from plinth.signals import pre_hostname_change, post_hostname_change +from plinth.signals import domainname_change LOGGER = logging.getLogger(__name__) @@ -56,6 +57,7 @@ def init(): pre_hostname_change.connect(on_pre_hostname_change) post_hostname_change.connect(on_post_hostname_change) + domainname_change.connect(on_domainname_change) @login_required @@ -209,3 +211,17 @@ def on_post_hostname_change(sender, old_hostname, new_hostname, **kwargs): '--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/signals.py b/plinth/signals.py index e07d9380b..568c437fe 100644 --- a/plinth/signals.py +++ b/plinth/signals.py @@ -27,4 +27,4 @@ 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']) -fqdn_change = Signal(providing_args=['old_fqdn', 'new_fqdn']) +domainname_change = Signal(providing_args=['old_domainname', 'new_domainname']) From 7680d398a6d7b38163fff3e7cfdaeab7e38dc4c7 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Sat, 20 Dec 2014 14:05:03 -0500 Subject: [PATCH 22/30] Use FQDN instead of hostname when registering an account. --- actions/xmpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/xmpp b/actions/xmpp index 1d8ef0b99..ea2d0c29e 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -178,11 +178,11 @@ def subcommand_register(arguments): 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()) From a4be4605380a1fbb4acd2db707cfcf8aef9940e0 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sun, 21 Dec 2014 17:15:56 +0530 Subject: [PATCH 23/30] Introduce framework for checking/installing packages - Uses PackageKit dameon, Glib library wrapping packagekit DBUS API and Python bindings for the Glib library. - Implement a decorator to wrap views requiring packages. - Framework allows for parallel operations. However, doing parallel operations hangs because of what appears to be PackageKit backend limitations. --- INSTALL | 2 +- plinth/forms.py | 33 +++++ plinth/package.py | 184 ++++++++++++++++++++++++++ plinth/templates/package_install.html | 82 ++++++++++++ plinth/views.py | 50 ++++++- setup.py | 3 +- 6 files changed, 351 insertions(+), 3 deletions(-) create mode 100644 plinth/forms.py create mode 100644 plinth/package.py create mode 100644 plinth/templates/package_install.html 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/plinth/forms.py b/plinth/forms.py new file mode 100644 index 000000000..aa3ee74b4 --- /dev/null +++ b/plinth/forms.py @@ -0,0 +1,33 @@ +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +Plinth framework forms +""" + +from django import forms + + +class PackageInstallForm(forms.Form): + """Prompt for installation of a package. + + XXX: Don't store the package list in a hidden input as it can be + modified on the client side. Use session store to store and retrieve + the package list. It has to be form specific so that multiple + instances of forms don't clash with each other. + """ + package_names = forms.CharField(widget=forms.HiddenInput) 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/templates/package_install.html b/plinth/templates/package_install.html new file mode 100644 index 000000000..0624a8898 --- /dev/null +++ b/plinth/templates/package_install.html @@ -0,0 +1,82 @@ +{% 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 %} + + {{ form|bootstrap }} + + +
+ + {% 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..81a58c022 100644 --- a/plinth/views.py +++ b/plinth/views.py @@ -19,13 +19,61 @@ Main Plinth views """ +from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse from django.http.response import HttpResponseRedirect +from django.views.generic.edit import FormView + +from plinth import package as package_module +from plinth.forms import PackageInstallForm 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(FormView): + """View to prompt and install packages.""" + template_name = 'package_install.html' + form_class = PackageInstallForm + + 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', []) + + # Package details must have been resolved before building the form + 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 get_initial(self): + """Return the initial data to be filled in the form.""" + initial = super(PackageInstallView, self).get_initial() + try: + initial['package_names'] = ','.join(self.kwargs['package_names']) + except KeyError: + raise ImproperlyConfigured('Argument package_names must be ' + 'provided to PackageInstallView') + + return initial + + def form_valid(self, form): + """Handle successful validation of the form. + + Start the package installation and show this view again. + """ + package_names = form.cleaned_data['package_names'].split(',') + package_module.start_install(package_names) + + return self.render_to_response( + self.get_context_data(package_names=package_names)) 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, From 9b9d112927d6cc895bc7847c127ff97630e2a5a5 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sun, 21 Dec 2014 17:24:11 +0530 Subject: [PATCH 24/30] Use package framework for installing ownCloud --- actions/owncloud-setup | 8 -------- plinth/modules/owncloud/owncloud.py | 2 ++ plinth/modules/owncloud/templates/owncloud.html | 8 ++------ 3 files changed, 4 insertions(+), 14 deletions(-) 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/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 }} From d79f346591222468552e38e71ad1c6422c46df00 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 22 Dec 2014 23:27:16 +0530 Subject: [PATCH 25/30] Use package framework for installing firewalld --- actions/firewall | 12 ------------ plinth/modules/firewall/firewall.py | 15 +++------------ plinth/modules/firewall/templates/firewall.html | 8 +------- 3 files changed, 4 insertions(+), 31 deletions(-) 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/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 From 7b45ad1813aeea97f8180304618282654274d6ee Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 22 Dec 2014 23:29:15 +0530 Subject: [PATCH 26/30] Use package framework for installing pagekite --- actions/pagekite-configure | 12 ------------ plinth/modules/pagekite/pagekite.py | 7 ++----- .../pagekite/templates/pagekite_configure.html | 11 ----------- 3 files changed, 2 insertions(+), 28 deletions(-) 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/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 %} From c7f27e493eedef7a35e5ea9aa7fa45ab02f608ed Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 22 Dec 2014 23:31:50 +0530 Subject: [PATCH 27/30] Use package framework for installing tor --- actions/tor | 27 ++------------------------- plinth/modules/tor/templates/tor.html | 11 ----------- plinth/modules/tor/tor.py | 9 +++------ 3 files changed, 5 insertions(+), 42 deletions(-) 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/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, From b3e8e53c73a313358a2a20bb4bebe619a7d66a57 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 22 Dec 2014 23:34:16 +0530 Subject: [PATCH 28/30] Use package framework for installing ejabberd and jwchat --- actions/xmpp | 33 ------------------------- plinth/modules/xmpp/templates/xmpp.html | 14 ----------- plinth/modules/xmpp/xmpp.py | 14 +++-------- 3 files changed, 3 insertions(+), 58 deletions(-) diff --git a/actions/xmpp b/actions/xmpp index ea2d0c29e..115fe375c 100755 --- a/actions/xmpp +++ b/actions/xmpp @@ -38,10 +38,6 @@ 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', @@ -77,17 +73,8 @@ 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""" - if not get_installed(): - print('Failed to update XMPP hostname: ejabberd is not installed.') - return - old_hostname = arguments.old_hostname new_hostname = arguments.new_hostname @@ -104,10 +91,6 @@ def subcommand_pre_change_hostname(arguments): def subcommand_change_hostname(arguments): """Update ejabberd and jwchat with new hostname""" - if not get_installed(): - print('Failed to update XMPP hostname: ejabberd is not installed.') - return - subprocess.call(['service', 'ejabberd', 'stop']) subprocess.call(['pkill', '-u', 'ejabberd']) @@ -134,10 +117,6 @@ def subcommand_change_hostname(arguments): def subcommand_change_domainname(arguments): """Update ejabberd and jwchat with new domainname""" - if not get_installed(): - print('Failed to update XMPP domainname: ejabberd is not installed.') - return - domainname = arguments.domainname fqdn = socket.gethostname() + '.' + domainname @@ -172,10 +151,6 @@ def subcommand_change_domainname(arguments): 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 fqdn = socket.getfqdn() @@ -188,14 +163,6 @@ def subcommand_register(arguments): 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/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 8a38ec2bb..718c981e5 100644 --- a/plinth/modules/xmpp/xmpp.py +++ b/plinth/modules/xmpp/xmpp.py @@ -25,6 +25,7 @@ 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 @@ -61,21 +62,12 @@ def init(): @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 From e905d1a8f24f616d835e5bfc5f87329e3609ec23 Mon Sep 17 00:00:00 2001 From: fonfon Date: Sat, 27 Dec 2014 13:59:09 +0100 Subject: [PATCH 29/30] packagekit: use TemplateView instead of FormView For the installation procedure a TemplateView is sufficient, and the user won't be able to edit any form-data on the client-side. --- plinth/forms.py | 33 ------------------------- plinth/templates/package_install.html | 3 --- plinth/views.py | 35 +++++++-------------------- 3 files changed, 9 insertions(+), 62 deletions(-) delete mode 100644 plinth/forms.py diff --git a/plinth/forms.py b/plinth/forms.py deleted file mode 100644 index aa3ee74b4..000000000 --- a/plinth/forms.py +++ /dev/null @@ -1,33 +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 . -# - -""" -Plinth framework forms -""" - -from django import forms - - -class PackageInstallForm(forms.Form): - """Prompt for installation of a package. - - XXX: Don't store the package list in a hidden input as it can be - modified on the client side. Use session store to store and retrieve - the package list. It has to be form specific so that multiple - instances of forms don't clash with each other. - """ - package_names = forms.CharField(widget=forms.HiddenInput) diff --git a/plinth/templates/package_install.html b/plinth/templates/package_install.html index 0624a8898..f853a3e2c 100644 --- a/plinth/templates/package_install.html +++ b/plinth/templates/package_install.html @@ -55,9 +55,6 @@
{% csrf_token %} - - {{ form|bootstrap }} -
diff --git a/plinth/views.py b/plinth/views.py index 81a58c022..e63501af5 100644 --- a/plinth/views.py +++ b/plinth/views.py @@ -19,13 +19,11 @@ Main Plinth views """ -from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse from django.http.response import HttpResponseRedirect -from django.views.generic.edit import FormView +from django.views.generic import TemplateView from plinth import package as package_module -from plinth.forms import PackageInstallForm def index(request): @@ -36,18 +34,16 @@ def index(request): return HttpResponseRedirect(reverse('help:about')) -class PackageInstallView(FormView): +class PackageInstallView(TemplateView): """View to prompt and install packages.""" template_name = 'package_install.html' - form_class = PackageInstallForm 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', []) - - # Package details must have been resolved before building the form context['packages'] = [package_module.packages_resolved[package_name] for package_name in context['package_names']] context['is_installing'] = \ @@ -56,24 +52,11 @@ class PackageInstallView(FormView): return context - def get_initial(self): - """Return the initial data to be filled in the form.""" - initial = super(PackageInstallView, self).get_initial() - try: - initial['package_names'] = ','.join(self.kwargs['package_names']) - except KeyError: - raise ImproperlyConfigured('Argument package_names must be ' - 'provided to PackageInstallView') + def post(self, *args, **kwargs): + """Handle installing packages - return initial - - def form_valid(self, form): - """Handle successful validation of the form. - - Start the package installation and show this view again. + Start the package installation, and refresh the page every x seconds to + keep displaying PackageInstallView.get() with the installation status. """ - package_names = form.cleaned_data['package_names'].split(',') - package_module.start_install(package_names) - - return self.render_to_response( - self.get_context_data(package_names=package_names)) + package_module.start_install(self.kwargs['package_names']) + return self.render_to_response(self.get_context_data()) From 2015b52798610fd8830a93b789398d92dcf95874 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 5 Jan 2015 02:38:48 +0530 Subject: [PATCH 30/30] Release 0.4.2 --- plinth/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'