Merge remote-tracking branch 'upstream/master' into dynamicdns

This commit is contained in:
Daniel Steglich 2015-01-04 23:52:14 +01:00
commit 6c214c915c
26 changed files with 546 additions and 223 deletions

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# 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

View File

@ -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'])

View File

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

View File

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

View File

@ -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'])

View File

@ -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 = []

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
#
# 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

View File

@ -19,4 +19,4 @@
Plinth package init file
"""
__version__ = '0.4.1'
__version__ = '0.4.2'

View File

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

View File

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

View File

@ -29,13 +29,7 @@ threat from the Internet.</p>
<p>The following is the current status:</p>
{% if firewall_status = 'not_installed' %}
<p>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 <code>aptitude install firewalld</code></p>
{% elif firewall_status = 'not_running' %}
{% if firewall_status = 'not_running' %}
<p>Firewall daemon is not running. Please run it. Firewall comes
enabled by default on {{ cfg.box_name }}. On any Debian based system

View File

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

View File

@ -27,14 +27,10 @@
<h2>ownCloud</h2>
<p>When enabled, the owncloud installation will be available
<p>When enabled, the ownCloud installation will be available
from <a href="/owncloud">/owncloud</a> path on the web server.
Visit this URL to set up the initial administration account for
owncloud.</p>
<p><strong>Note: Setting up owncloud for the first time might take
5 minutes or more, depending on download bandwidth from the
Debian APT sources.</strong></p>
ownCloud.</p>
{{ form|bootstrap }}
<input type="submit" class="btn btn-primary btn-md" value="Update setup"/>

View File

@ -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</a>'))
@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')

View File

@ -22,15 +22,6 @@
{% block content %}
{% if not status %}
<p>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
<code>aptitude install pagekite<code></p>
{% else %}
<form class="form" method="post">
{% csrf_token %}
@ -52,8 +43,6 @@
</form>
{% endif %}
{% endblock %}
{% block page_js %}

View File

@ -24,8 +24,6 @@
<h2>Tor</h2>
{% if is_installed %}
<h3>Status</h3>
<p>
@ -80,15 +78,6 @@ port-forwarded, if necessary:</p>
<p>A Tor SOCKS port is available on your {{ cfg.box_name }} on TCP port
9050.</p>
{% else %}
<p>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
<code>aptitude install tor</code>.</p>
{% endif %}
{% endblock %}
{% block sidebar %}

View File

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

View File

@ -22,8 +22,6 @@
{% block content %}
{% if is_installed %}
<p>XMPP is an open and standardized communication protocol. Here you
can run and configure your XMPP server, called ejabberd. To actually
communicate, you can use the <a href='/jwchat'>web client</a> or any
@ -33,16 +31,4 @@
<p><a href='/jwchat' target='_blank'class='btn btn-primary'> Launch web
client</a></p>
{% else %}
<h2>XMPP Server</h2>
<p>The XMPP server <i>ejabberd</i> is not installed.</p>
<p>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
<code>aptitude install ejabberd</code>.</p>
{% endif %}
{% endblock %}

View File

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

184
plinth/package.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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()

View File

@ -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'])

View File

@ -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 <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load bootstrap %}
{% block page_head %}
{% if is_installing %}
<meta http-equiv="refresh" content="3" />
{% endif %}
{% endblock %}
{% block content %}
<h2>Installation</h2>
{% if not is_installing %}
<p>This feature requires addtional packages to be installed. Do you
wish to install them?</p>
<table class="table">
<thead>
<tr><th>Package</th><th>Summary</th></tr>
</thead>
<tbody>
{% for package in packages %}
<tr>
<td>{{ package.get_name }}</td>
<td>{{ package.get_summary }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<form action="" method="post">
{% csrf_token %}
<input type="submit" class="btn btn-md btn-primary" value="Install" />
</form>
{% else %}
{% for key, transaction in transactions.items %}
<div>Installing {{ transaction.package_names|join:", " }}:
{{ transaction.status_string }}
</div>
<div class="progress">
<div class="progress-bar progress-bar-striped active"
role="progressbar" aria-valuemin="0" aria-valuemax="100"
aria-valuenow="{{ transaction.percentage }}"
style="width: {{ transaction.percentage }}%">
<span class="sr-only">{{ transaction.percentage }}% complete</span>
</div>
</div>
{% endfor %}
{% endif %}
{% endblock %}

View File

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

View File

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