FreedomBox/actions/ejabberd
James Valleroy 13646d58d5
ejabberd: Change BOSH port from 5280 to 5443
This is the default for new installs of ejabberd, starting with
version 18.09-1. For existing installs from previous versions, the
config must be upgraded to use the new port.

Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
Reviewed-by: Joseph Nuthalapati <njoseph@thoughtworks.com>
2018-11-24 17:48:13 +05:30

414 lines
15 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 shutil
import socket
import stat
import subprocess
import sys
import ruamel.yaml
from distutils.version import LooseVersion as LV
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
subparsers.add_parser('setup', help='Setup ejabberd configuration')
subparsers.add_parser('enable', help='Enable XMPP service')
subparsers.add_parser('disable', help='Disable 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')
# Update ejabberd with new domainname
domainname_change = subparsers.add_parser(
'change-domainname', help='Update ejabberd with new domainname')
domainname_change.add_argument('--domainname', help='New domainname')
# 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()
subprocess.check_output(
['debconf-set-selections'],
input=b'ejabberd ejabberd/hostname string ' + domainname.encode())
def subcommand_setup(_):
"""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()
try:
subprocess.check_output(['ejabberdctl', 'restart'])
except subprocess.CalledProcessError as err:
print('Failed to restart ejabberd with new configuration: %s', err)
with action_utils.WebserverChange() as webserver_change:
webserver_change.enable('jwchat-plinth')
def upgrade_config():
"""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
# Write changes back to the file
with open(EJABBERD_CONFIG, 'w') as file_handle:
ruamel.yaml.round_trip_dump(conf, file_handle)
def subcommand_enable(_):
"""Enable XMPP service"""
action_utils.service_enable('ejabberd')
action_utils.webserver_enable('jwchat-plinth')
def subcommand_disable(_):
"""Disable XMPP service"""
action_utils.webserver_disable('jwchat-plinth')
action_utils.service_disable('ejabberd')
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_change_domainname(arguments):
"""Update ejabberd with new domainname"""
if not shutil.which('ejabberdctl'):
print('ejabberdctl not found. Is ejabberd installed?')
return
domainname = arguments.domainname
if not domainname:
# If new domainname is blank, use hostname instead.
domainname = socket.gethostname()
action_utils.service_stop('ejabberd')
subprocess.call(['pkill', '-u', 'ejabberd'])
# 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))
with open(EJABBERD_CONFIG, 'w') as file_handle:
ruamel.yaml.round_trip_dump(conf, file_handle)
action_utils.service_start('ejabberd')
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 subcommand_letsencrypt(arguments):
"""
Add/drop usage of Let's Encrypt cert. The command 'add' applies only to
current domain, will be called by action 'letsencrypt run_renew_hooks',
when certbot renews the cert (if ejabberd is selected for cert use).
Drop of a cert must be possible for any domain to respond to domain change.
"""
current_domain = config.get_domainname()
with open(EJABBERD_CONFIG, 'r') as file_handle:
conf = ruamel.yaml.round_trip_load(file_handle, preserve_quotes=True)
if arguments.domain is not None and arguments.domain not in conf['hosts']:
print('Aborted: Current domain "%s" not configured for ejabberd.' %
arguments.domain)
sys.exit(1)
if arguments.command == 'add' and arguments.domain is not None \
and arguments.domain != current_domain:
print('Aborted: Only certificate of current domain "%s" can be added.'
% current_domain)
sys.exit(2)
if arguments.domain is None:
arguments.domain = current_domain
cert_folder = '/etc/ejabberd/letsencrypt/' + arguments.domain
cert_file = cert_folder + '/ejabberd.pem'
if arguments.command == 'add':
le_folder = os.path.join(LE_LIVE_DIRECTORY, current_domain)
le_privkey = os.path.join(le_folder, 'privkey.pem')
le_fullchain = os.path.join(le_folder, 'fullchain.pem')
if not os.path.exists(le_folder):
print('Aborted: No certificate directory at %s.' % le_folder)
sys.exit(3)
if not os.path.exists(cert_folder):
os.makedirs(cert_folder)
shutil.chown(cert_folder, 'ejabberd', 'ejabberd')
with open(cert_file, 'w') as outfile:
with open(le_privkey, 'r') as infile:
for line in infile:
if line.strip():
outfile.write(line)
with open(le_fullchain, 'r') as infile:
for line in infile:
if line.strip():
outfile.write(line)
shutil.chown(cert_file, 'ejabberd', 'ejabberd')
os.chmod(cert_file, stat.S_IRUSR | stat.S_IWUSR)
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
else: # arguments.command == 'drop' (ensured by parser)
orig_cert_file = ruamel.yaml.scalarstring.DoubleQuotedScalarString(
EJABBERD_ORIG_CERT)
for listen_port in conf['listen']:
if 'certfile' in listen_port \
and listen_port['certfile'] == cert_file:
listen_port['certfile'] = orig_cert_file
if conf['s2s_certfile'] == cert_file:
conf['s2s_certfile'] = orig_cert_file
if os.path.exists(cert_folder):
shutil.rmtree(cert_folder)
with open(EJABBERD_CONFIG, 'w') as file_handle:
ruamel.yaml.round_trip_dump(conf, file_handle)
if action_utils.service_is_running('ejabberd'):
action_utils.service_restart('ejabberd')
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()