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 \ $ sudo apt-get install libjs-jquery libjs-modernizr \
libjs-bootstrap make pandoc python3 python3-cherrypy3 \ libjs-bootstrap make pandoc python3 python3-cherrypy3 \
python3-coverage python3-django python3-bootstrapform \ python3-coverage python3-django python3-bootstrapform \
python3-setuptools python3-gi python3-setuptools gir1.2-packagekitglib-1.0
2. Install Plinth: 2. Install Plinth:

View File

@ -9,19 +9,19 @@
# #
# This program is distributed in the hope that it will be useful, # This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # 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. # GNU Affero General Public License for more details.
# #
# You should have received a copy of the GNU Affero General Public License # 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 if grep -q 127.0.1.1 /etc/hosts ; then
ejabberdctl dump $BACKUP sed -i "s/127.0.1.1.*/127.0.1.1 $hostname.$domainname $hostname/" /etc/hosts
ejabberdctl stop else
sed -i "/127.0.0.1.*/a \
# Make sure there aren't files in the Mnesia spool dir 127.0.1.1 $hostname.$domainname $hostname" /etc/hosts
mkdir -p /var/lib/ejabberd/oldfiles fi
mv /var/lib/ejabberd/*.* /var/lib/ejabberd/oldfiles/

View File

@ -30,10 +30,6 @@ def parse_arguments():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') 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 # Get status
subparsers.add_parser('get-status', subparsers.add_parser('get-status',
help='Get whether firewalld is running') help='Get whether firewalld is running')
@ -64,14 +60,6 @@ def parse_arguments():
return parser.parse_args() 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(_): def subcommand_get_status(_):
"""Print status of the firewalld service""" """Print status of the firewalld service"""
subprocess.call(['firewall-cmd', '--state']) subprocess.call(['firewall-cmd', '--state'])

View File

@ -18,11 +18,15 @@
hostname="$1" hostname="$1"
echo "$hostname" > /etc/hostname if [ -d /run/systemd/system ] ; then
if [ -x /etc/init.d/hostname.sh ] ; then hostnamectl set-hostname --transient --static "$hostname"
service hostname.sh start
else 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 fi
service avahi-daemon restart service avahi-daemon restart

View File

@ -58,14 +58,6 @@ done
if [ "$owncloud_enable" != "$owncloud_enable_cur" ] ; then if [ "$owncloud_enable" != "$owncloud_enable_cur" ] ; then
if $owncloud_enable ; 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 # Keep existing configuration if it exist
if [ ! -e /etc/owncloud/config.php ] ; then if [ ! -e /etc/owncloud/config.php ] ; then
# Set up postgresql database and user # Set up postgresql database and user

View File

@ -48,10 +48,6 @@ def parse_arguments():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') 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 # Start PageKite
subparsers.add_parser('start', help='Start PageKite service') subparsers.add_parser('start', help='Start PageKite service')
@ -91,14 +87,6 @@ def parse_arguments():
return parser.parse_args() 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(_): def subcommand_start(_):
"""Start PageKite service""" """Start PageKite service"""
status = subprocess.call(['service', 'pagekite', 'start']) status = subprocess.call(['service', 'pagekite', 'start'])

View File

@ -34,10 +34,6 @@ def parse_arguments():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') 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 # Get whether Tor is running
subparsers.add_parser('is-running', help='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() 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(_): def subcommand_is_running(_):
"""Get whether Tor is running""" """Get whether Tor is running"""
try: try:
@ -101,7 +92,7 @@ def subcommand_get_hs(_):
def subcommand_enable_hs(_): def subcommand_enable_hs(_):
"""Enable Tor hidden service""" """Enable Tor hidden service"""
if not get_installed() or get_hidden_service(): if get_hidden_service():
return return
with open(TOR_CONFIG, 'r') as conffile: with open(TOR_CONFIG, 'r') as conffile:
@ -121,7 +112,7 @@ def subcommand_enable_hs(_):
def subcommand_disable_hs(_): def subcommand_disable_hs(_):
"""Disable Tor hidden service""" """Disable Tor hidden service"""
if not get_installed() or not get_hidden_service(): if not get_hidden_service():
return return
with open(TOR_CONFIG, 'r') as conffile: with open(TOR_CONFIG, 'r') as conffile:
@ -154,19 +145,8 @@ def subcommand_disable_hs(_):
subprocess.call(['service', 'tor', 'restart']) 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): def set_tor_service(enable):
"""Enable/disable Tor service; enable: boolean""" """Enable/disable Tor service; enable: boolean"""
if not get_installed():
return
newline = 'RUN_DAEMON="yes"\n' if enable else 'RUN_DAEMON="no"\n' newline = 'RUN_DAEMON="yes"\n' if enable else 'RUN_DAEMON="no"\n'
with open(SERVICE_CONFIG, 'r') as file: with open(SERVICE_CONFIG, 'r') as file:
@ -182,9 +162,6 @@ def set_tor_service(enable):
def get_hidden_service(): def get_hidden_service():
"""Return a string with configured Tor hidden service information""" """Return a string with configured Tor hidden service information"""
if not get_installed():
return ''
hs_dir = None hs_dir = None
hs_ports = [] hs_ports = []

View File

@ -23,6 +23,14 @@ Configuration helper for the ejabberd service
import argparse import argparse
import subprocess 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(): def parse_arguments():
@ -30,9 +38,29 @@ def parse_arguments():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
# Get whether ejabberd is installed # Prepare ejabberd for hostname change
subparsers.add_parser('get-installed', pre_hostname_change = subparsers.add_parser(
help='Get whether ejabberd is installed') '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 a new user account
register = subparsers.add_parser('register', register = subparsers.add_parser('register',
@ -45,37 +73,96 @@ def parse_arguments():
return parser.parse_args() return parser.parse_args()
def subcommand_get_installed(_): def subcommand_pre_change_hostname(arguments):
"""Get whether ejabberd is installed""" """Prepare ejabberd for hostname change"""
print('installed' if get_installed() else 'not installed') 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): def subcommand_register(arguments):
"""Register a new user account""" """Register a new user account"""
if not get_installed():
print('Failed to register XMPP account: ejabberd is not installed.')
return
username = arguments.username username = arguments.username
password = arguments.password password = arguments.password
hostname = subprocess.check_output(['hostname']) fqdn = socket.getfqdn()
try: try:
output = subprocess.check_output(['ejabberdctl', 'register', output = subprocess.check_output(['ejabberdctl', 'register',
username, hostname, password]) username, fqdn, password])
print(output.decode()) print(output.decode())
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print('Failed to register XMPP account:', e.output.decode()) 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(): def main():
"""Parse arguments and perform all duties""" """Parse arguments and perform all duties"""
arguments = parse_arguments() 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 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 actions
from plinth import cfg from plinth import cfg
from plinth.signals import pre_hostname_change, post_hostname_change
from plinth.signals import domainname_change
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -41,6 +43,12 @@ def get_hostname():
return socket.gethostname() return socket.gethostname()
def get_domainname():
"""Return the domainname"""
fqdn = socket.getfqdn()
return '.'.join(fqdn.split('.')[1:])
class TrimmedCharField(forms.CharField): class TrimmedCharField(forms.CharField):
"""Trim the contents of a CharField""" """Trim the contents of a CharField"""
def clean(self, value): 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}$', validators.RegexValidator(r'^[a-zA-Z][a-zA-Z0-9]{,62}$',
_('Invalid hostname'))]) _('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): def __init__(self, *args, **kwargs):
# pylint: disable-msg=E1101, W0233 # pylint: disable-msg=E1101, W0233
forms.Form.__init__(self, *args, **kwargs) forms.Form.__init__(self, *args, **kwargs)
@ -124,6 +141,7 @@ def index(request):
def get_status(): def get_status():
"""Return the current status""" """Return the current status"""
return {'hostname': get_hostname(), return {'hostname': get_hostname(),
'domainname': get_domainname(),
'time_zone': open('/etc/timezone').read().rstrip()} 'time_zone': open('/etc/timezone').read().rstrip()}
@ -140,6 +158,17 @@ def _apply_changes(request, old_status, new_status):
else: else:
messages.info(request, _('Hostname is unchanged')) 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']: if old_status['time_zone'] != new_status['time_zone']:
try: try:
actions.superuser_run('timezone-change', [new_status['time_zone']]) 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): def set_hostname(hostname):
"""Sets machine hostname to hostname""" """Sets machine hostname to hostname"""
old_hostname = get_hostname()
# Hostname should be ASCII. If it's unicode but passed our # Hostname should be ASCII. If it's unicode but passed our
# valid_hostname check, convert to ASCII. # valid_hostname check, convert to ASCII.
hostname = str(hostname) hostname = str(hostname)
pre_hostname_change.send_robust(sender='config',
old_hostname=old_hostname,
new_hostname=hostname)
LOGGER.info('Changing hostname to - %s', hostname) LOGGER.info('Changing hostname to - %s', hostname)
actions.superuser_run('xmpp-pre-hostname-change')
actions.superuser_run('hostname-change', hostname) 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 actions
from plinth import cfg from plinth import cfg
from plinth import package
from plinth.signals import service_enabled from plinth.signals import service_enabled
import plinth.service as service_module import plinth.service as service_module
@ -42,13 +43,9 @@ def init():
@login_required @login_required
@package.required('firewalld')
def index(request): def index(request):
"""Serve introcution page""" """Serve introcution page"""
if not get_installed_status():
return TemplateResponse(request, 'firewall.html',
{'title': _('Firewall'),
'firewall_status': 'not_installed'})
if not get_enabled_status(): if not get_enabled_status():
return TemplateResponse(request, 'firewall.html', return TemplateResponse(request, 'firewall.html',
{'title': _('Firewall'), {'title': _('Firewall'),
@ -65,14 +62,8 @@ def index(request):
'external_enabled_services': external_enabled_services}) '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(): def get_enabled_status():
"""Return whether firewall is installed""" """Return whether firewall is enabled"""
output = _run(['get-status'], superuser=True) output = _run(['get-status'], superuser=True)
return output.split()[0] == 'running' return output.split()[0] == 'running'

View File

@ -29,13 +29,7 @@ threat from the Internet.</p>
<p>The following is the current status:</p> <p>The following is the current status:</p>
{% if firewall_status = 'not_installed' %} {% if firewall_status = 'not_running' %}
<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' %}
<p>Firewall daemon is not running. Please run it. Firewall comes <p>Firewall daemon is not running. Please run it. Firewall comes
enabled by default on {{ cfg.box_name }}. On any Debian based system 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 actions
from plinth import cfg from plinth import cfg
from plinth import package
from plinth import service from plinth import service
@ -47,6 +48,7 @@ def init():
@login_required @login_required
@package.required('postgresql', 'php5-pgsql', 'owncloud')
def index(request): def index(request):
"""Serve the ownCloud configuration page""" """Serve the ownCloud configuration page"""
status = get_status() status = get_status()

View File

@ -27,14 +27,10 @@
<h2>ownCloud</h2> <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. from <a href="/owncloud">/owncloud</a> path on the web server.
Visit this URL to set up the initial administration account for Visit this URL to set up the initial administration account for
owncloud.</p> 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>
{{ form|bootstrap }} {{ form|bootstrap }}
<input type="submit" class="btn btn-primary btn-md" value="Update setup"/> <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 actions
from plinth import cfg from plinth import cfg
from plinth import package
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -101,6 +102,7 @@ https://pagekite.net/wiki/Howto/SshOverPageKite/">instructions</a>'))
@login_required @login_required
@package.required('pagekite')
def configure(request): def configure(request):
"""Serve the configuration form""" """Serve the configuration form"""
status = get_status() status = get_status()
@ -131,11 +133,6 @@ def get_status():
""" """
status = {} status = {}
# Check if PageKite is installed
output = _run(['get-installed'])
if output.split()[0] != 'installed':
return None
# PageKite service enabled/disabled # PageKite service enabled/disabled
output = _run(['get-status']) output = _run(['get-status'])
status['enabled'] = (output.split()[0] == 'enabled') status['enabled'] = (output.split()[0] == 'enabled')

View File

@ -22,15 +22,6 @@
{% block content %} {% 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"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
@ -52,8 +43,6 @@
</form> </form>
{% endif %}
{% endblock %} {% endblock %}
{% block page_js %} {% block page_js %}

View File

@ -24,8 +24,6 @@
<h2>Tor</h2> <h2>Tor</h2>
{% if is_installed %}
<h3>Status</h3> <h3>Status</h3>
<p> <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 <p>A Tor SOCKS port is available on your {{ cfg.box_name }} on TCP port
9050.</p> 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 %} {% endblock %}
{% block sidebar %} {% block sidebar %}

View File

@ -27,6 +27,7 @@ from gettext import gettext as _
from plinth import actions from plinth import actions
from plinth import cfg from plinth import cfg
from plinth import package
class TorForm(forms.Form): # pylint: disable=W0232 class TorForm(forms.Form): # pylint: disable=W0232
@ -43,6 +44,7 @@ def init():
@login_required @login_required
@package.required('tor')
def index(request): def index(request):
"""Service the index page""" """Service the index page"""
status = get_status() status = get_status()
@ -61,7 +63,6 @@ def index(request):
return TemplateResponse(request, 'tor.html', return TemplateResponse(request, 'tor.html',
{'title': _('Tor Control Panel'), {'title': _('Tor Control Panel'),
'is_installed': status['is_installed'],
'is_running': status['is_running'], 'is_running': status['is_running'],
'tor_ports': status['ports'], 'tor_ports': status['ports'],
'tor_hs_enabled': status['hs_enabled'], 'tor_hs_enabled': status['hs_enabled'],
@ -72,9 +73,6 @@ def index(request):
def get_status(): def get_status():
"""Return the current 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' is_running = actions.superuser_run('tor', ['is-running']).strip() == 'yes'
output = actions.superuser_run('tor-get-ports') output = actions.superuser_run('tor-get-ports')
@ -103,8 +101,7 @@ def get_status():
hs_hostname = hs_info[0] hs_hostname = hs_info[0]
hs_ports = hs_info[1] hs_ports = hs_info[1]
return {'is_installed': is_installed, return {'is_running': is_running,
'is_running': is_running,
'ports': ports, 'ports': ports,
'hs_enabled': hs_enabled, 'hs_enabled': hs_enabled,
'hs_hostname': hs_hostname, 'hs_hostname': hs_hostname,

View File

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

View File

@ -25,7 +25,10 @@ import logging
from plinth import actions from plinth import actions
from plinth import cfg from plinth import cfg
from plinth import package
from plinth import service from plinth import service
from plinth.signals import pre_hostname_change, post_hostname_change
from plinth.signals import domainname_change
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -53,23 +56,18 @@ def init():
'xmpp-bosh', _('Chat Server - web interface'), is_external=True, 'xmpp-bosh', _('Chat Server - web interface'), is_external=True,
enabled=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 @login_required
@package.required('jwchat', 'ejabberd')
def index(request): def index(request):
"""Serve XMPP page""" """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', return TemplateResponse(request, 'xmpp.html',
{'title': _('XMPP Server'), {'title': _('XMPP Server'),
'is_installed': is_installed, 'subsubmenu': subsubmenu})
'subsubmenu': index_subsubmenu})
class ConfigureForm(forms.Form): # pylint: disable-msg=W0232 class ConfigureForm(forms.Form): # pylint: disable-msg=W0232
@ -178,3 +176,44 @@ def _register_user(request, data):
messages.error(request, messages.error(request,
_('Failed to register account for %s: %s') % _('Failed to register account for %s: %s') %
(data['username'], output)) (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']) service_enabled = Signal(providing_args=['service_id', 'enabled'])
pre_module_loading = Signal() pre_module_loading = Signal()
post_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.core.urlresolvers import reverse
from django.http.response import HttpResponseRedirect from django.http.response import HttpResponseRedirect
from django.views.generic import TemplateView
from plinth import package as package_module
def index(request): def index(request):
"""Serve the main index page""" """Serve the main index page."""
if request.user.is_authenticated(): if request.user.is_authenticated():
return HttpResponseRedirect(reverse('apps:index')) return HttpResponseRedirect(reverse('apps:index'))
return HttpResponseRedirect(reverse('help:about')) 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=[ install_requires=[
'cherrypy >= 3.0', 'cherrypy >= 3.0',
'django >= 1.7.0', 'django >= 1.7.0',
'django-bootstrap-form' 'django-bootstrap-form',
'pygobject'
], ],
tests_require=['coverage >= 3.7'], tests_require=['coverage >= 3.7'],
include_package_data=True, include_package_data=True,