FreedomBox/actions/ejabberd
Sunil Mohan Adapa ca8ea9427c
action_utils: Introduce utility for setting debconf answers
Tests:

mldonkey
 - Installs
 - /etc/default/mldonkey-server has LAUNCH_AT_STARTUP=true
 - systemctl status mldonkey-server shows daemon running

ejabberd
 - Installs
 - /etc/ejabberd/ejabberd.yml has domainname properly configured

privoxy
 - Installs
 - /etc/privoxy/config has listen-address set to '[::]:8118'

roundcube
 - Installs and works
 - /etc/roundcube/debian-db.php contains dbtype sqlite3

ttrss
 - Installs and works
 - /etc/ttrss/database.php has dbtype 'pgsql'.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
2019-08-31 08:24:59 -04:00

316 lines
11 KiB
Python
Executable File

#!/usr/bin/python3
#
# This file is part of FreedomBox.
#
# 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/>.
#
"""
Configuration helper for the ejabberd service
"""
import argparse
import os
import pathlib
import shutil
import socket
import subprocess
from distutils.version import LooseVersion as LV
import ruamel.yaml
from plinth import action_utils
from plinth.modules import config
from plinth.modules.letsencrypt import LIVE_DIRECTORY as LE_LIVE_DIRECTORY
EJABBERD_CONFIG = '/etc/ejabberd/ejabberd.yml'
EJABBERD_BACKUP = '/var/log/ejabberd/ejabberd.dump'
EJABBERD_BACKUP_NEW = '/var/log/ejabberd/ejabberd_new.dump'
EJABBERD_ORIG_CERT = '/etc/ejabberd/ejabberd.pem'
IQDISC_DEPRECATED_VERSION = LV('18.03')
MOD_IRC_DEPRECATED_VERSION = LV('18.06')
def parse_arguments():
"""Return parsed command line arguments as dictionary"""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
# Preseed debconf values before packages are installed.
pre_install = subparsers.add_parser(
'pre-install',
help='Preseed debconf values before packages are installed.')
pre_install.add_argument(
'--domainname',
help='The domain name that will be used by the XMPP service.')
# Setup ejabberd configuration
setup = subparsers.add_parser('setup', help='Setup ejabberd configuration')
setup.add_argument(
'--domainname',
help='The domain name that will be used by the XMPP service.')
# Prepare ejabberd for hostname change
pre_hostname_change = subparsers.add_parser(
'pre-change-hostname', help='Prepare ejabberd for nodename change')
pre_hostname_change.add_argument('--old-hostname',
help='Previous hostname')
pre_hostname_change.add_argument('--new-hostname', help='New hostname')
# Update ejabberd nodename
hostname_change = subparsers.add_parser('change-hostname',
help='Update ejabberd nodename')
hostname_change.add_argument('--old-hostname', help='Previous hostname')
hostname_change.add_argument('--new-hostname', help='New hostname')
# Add a domain name to ejabberd
add_domain = subparsers.add_parser('add-domain',
help='Add a domain name to ejabberd')
add_domain.add_argument('--domainname', help='New domain name')
# Switch/check Message Archive Management (MAM) in ejabberd config
help_MAM = 'Switch or check Message Archive Management (MAM).'
mam = subparsers.add_parser('mam', help=help_MAM)
mam.add_argument('command', choices=('enable', 'disable', 'status'),
help=help_MAM)
help_LE = "Add/drop Let's Encrypt certificate if configured domain matches"
letsencrypt = subparsers.add_parser('letsencrypt', help=help_LE)
letsencrypt.add_argument('command', choices=('add', 'drop'), help=help_LE)
letsencrypt.add_argument('--domain', help='Domain name to drop.')
subparsers.required = True
return parser.parse_args()
def subcommand_pre_install(arguments):
"""Preseed debconf values before packages are installed."""
domainname = arguments.domainname
if not domainname:
# If new domainname is blank, use hostname instead.
domainname = socket.gethostname()
action_utils.debconf_set_selections(
['ejabberd ejabberd/hostname string ' + domainname])
def subcommand_setup(arguments):
"""Enabled LDAP authentication"""
with open(EJABBERD_CONFIG, 'r') as file_handle:
conf = ruamel.yaml.round_trip_load(file_handle, preserve_quotes=True)
for listen_port in conf['listen']:
if 'tls' in listen_port:
listen_port['tls'] = False
conf['auth_method'] = 'ldap'
conf['ldap_servers'] = [
ruamel.yaml.scalarstring.DoubleQuotedScalarString('localhost')
]
conf['ldap_base'] = ruamel.yaml.scalarstring.DoubleQuotedScalarString(
'ou=users,dc=thisbox')
with open(EJABBERD_CONFIG, 'w') as file_handle:
ruamel.yaml.round_trip_dump(conf, file_handle)
upgrade_config(arguments.domainname)
try:
subprocess.check_output(['ejabberdctl', 'restart'])
except subprocess.CalledProcessError as err:
print('Failed to restart ejabberd with new configuration: %s', err)
def upgrade_config(domain):
"""Fix the config file by removing deprecated settings"""
current_version = _get_version()
if not current_version:
print('Warning: Unable to get ejabberd version.')
with open(EJABBERD_CONFIG, 'r') as file_handle:
conf = ruamel.yaml.round_trip_load(file_handle, preserve_quotes=True)
# Check if `iqdisc` is present and remove it
if 'mod_mam' in conf['modules'] and \
(not current_version or current_version > IQDISC_DEPRECATED_VERSION):
conf['modules']['mod_mam'].pop('iqdisc', None)
# check if mod_irc is present in modules and remove it
if 'mod_irc' in conf['modules'] and \
(not current_version or current_version > MOD_IRC_DEPRECATED_VERSION):
conf['modules'].pop('mod_irc')
# BOSH port was changed from 5280 to 5443.
for listen_port in conf['listen']:
if listen_port['port'] == 5280:
listen_port['port'] = 5443
cert_dir = pathlib.Path('/etc/ejabberd/letsencrypt') / domain
cert_file = str(cert_dir / 'ejabberd.pem')
cert_file = ruamel.yaml.scalarstring.DoubleQuotedScalarString(cert_file)
conf['s2s_certfile'] = cert_file
for listen_port in conf['listen']:
if 'certfile' in listen_port:
listen_port['certfile'] = cert_file
# Write changes back to the file
with open(EJABBERD_CONFIG, 'w') as file_handle:
ruamel.yaml.round_trip_dump(conf, file_handle)
def subcommand_pre_change_hostname(arguments):
"""Prepare ejabberd for hostname change"""
if not shutil.which('ejabberdctl'):
print('ejabberdctl not found. Is ejabberd installed?')
return
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 with new hostname"""
if not shutil.which('ejabberdctl'):
print('ejabberdctl not found. Is ejabberd installed?')
return
action_utils.service_stop('ejabberd')
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)
action_utils.service_start('ejabberd')
# 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_add_domain(arguments):
"""Update ejabberd with new domainname"""
if not shutil.which('ejabberdctl'):
print('ejabberdctl not found. Is ejabberd installed?')
return
domainname = arguments.domainname
# Add updated domainname to ejabberd hosts list.
with open(EJABBERD_CONFIG, 'r') as file_handle:
conf = ruamel.yaml.round_trip_load(file_handle, preserve_quotes=True)
conf['hosts'].append(
ruamel.yaml.scalarstring.DoubleQuotedScalarString(domainname))
conf['hosts'] = list(set(conf['hosts']))
with open(EJABBERD_CONFIG, 'w') as file_handle:
ruamel.yaml.round_trip_dump(conf, file_handle)
def subcommand_mam(argument):
"""Enable, disable, or get status of Message Archive Management (MAM)."""
with open(EJABBERD_CONFIG, 'r') as file_handle:
conf = ruamel.yaml.round_trip_load(file_handle, preserve_quotes=True)
if 'modules' not in conf:
print('Found no "modules" entry in ejabberd configuration file.')
return
if argument.command == 'status':
if 'mod_mam' in conf['modules']:
print('enabled')
return
else:
print('disabled')
return
if argument.command == 'enable':
# Explicitly set the recommended / default settings for mod_mam,
# see https://docs.ejabberd.im/admin/configuration/#mod-mam.
settings_mod_mam = {
'mod_mam': {
'db_type':
'mnesia', # default is 'mnesia' (w/o set default_db)
'default': 'never', # policy, default 'never'
'request_activates_archiving': False, # default False
'assume_mam_usage': False, # for non-ack'd msgs, default False
'cache_size': 1000, # default is 1000 items
'cache_life_time': 3600 # default is 3600 seconds = 1h
}
}
conf['modules'].update(settings_mod_mam)
elif argument.command == 'disable':
# disable modules by erasing from config file
if 'mod_mam' in conf['modules']:
conf['modules'].pop('mod_mam')
else:
print("Unknown command: %s" % argument.command)
return
with open(EJABBERD_CONFIG, 'w') as file_handle:
ruamel.yaml.round_trip_dump(conf, file_handle)
if action_utils.service_is_running('ejabberd'):
subprocess.call(['ejabberdctl', 'reload_config'])
def _get_version():
""" Get the current ejabberd version """
try:
output = subprocess.check_output(['ejabberdctl',
'status']).decode('utf-8')
except subprocess.CalledProcessError:
return None
version_info = output.strip().split('\n')[-1].split()
if version_info:
version = str(version_info[1])
return LV(version)
return None
def main():
"""Parse arguments and perform all duties"""
arguments = parse_arguments()
subcommand = arguments.subcommand.replace('-', '_')
subcommand_method = globals()['subcommand_' + subcommand]
subcommand_method(arguments)
if __name__ == '__main__':
main()