mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-09-19 04:59:01 +00:00
ejabberd: Use privileged decorator for actions
Tests: - Functional tests work (backup test intermittent failure) - Initial setup works - Domain name is configured properly - FAIL: Changing hostname works (See #2276) - Adding a domain to the system works - Current list of domains shown properly in app page - Setting list of domains works - Showing TURN configuration works - Updating TURN configuration in coturn page works - Enabling/disabling MAM status works - Configure file is updated - App page shows correct status Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
79e48310a1
commit
a579c648fd
@ -1,15 +1,13 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""
|
"""FreedomBox app to configure ejabberd server."""
|
||||||
FreedomBox app to configure ejabberd server.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from plinth import actions
|
|
||||||
from plinth import app as app_module
|
from plinth import app as app_module
|
||||||
from plinth import cfg, frontpage, menu
|
from plinth import cfg, frontpage, menu
|
||||||
from plinth.daemon import Daemon
|
from plinth.daemon import Daemon
|
||||||
@ -25,7 +23,7 @@ from plinth.signals import (domain_added, post_hostname_change,
|
|||||||
pre_hostname_change)
|
pre_hostname_change)
|
||||||
from plinth.utils import format_lazy
|
from plinth.utils import format_lazy
|
||||||
|
|
||||||
from . import manifest
|
from . import manifest, privileged
|
||||||
|
|
||||||
_description = [
|
_description = [
|
||||||
_('XMPP is an open and standardized communication protocol. Here '
|
_('XMPP is an open and standardized communication protocol. Here '
|
||||||
@ -131,14 +129,12 @@ class EjabberdApp(app_module.App):
|
|||||||
domainname = config.get_domainname()
|
domainname = config.get_domainname()
|
||||||
logger.info('ejabberd service domainname - %s', domainname)
|
logger.info('ejabberd service domainname - %s', domainname)
|
||||||
|
|
||||||
actions.superuser_run('ejabberd',
|
privileged.pre_install(domainname)
|
||||||
['pre-install', '--domainname', domainname])
|
|
||||||
# XXX: Configure all other domain names
|
# XXX: Configure all other domain names
|
||||||
super().setup(old_version)
|
super().setup(old_version)
|
||||||
self.get_component('letsencrypt-ejabberd').setup_certificates(
|
self.get_component('letsencrypt-ejabberd').setup_certificates(
|
||||||
[domainname])
|
[domainname])
|
||||||
actions.superuser_run('ejabberd',
|
privileged.setup(domainname)
|
||||||
['setup', '--domainname', domainname])
|
|
||||||
self.enable()
|
self.enable()
|
||||||
|
|
||||||
# Configure STUN/TURN only if there's a valid TLS domain set for Coturn
|
# Configure STUN/TURN only if there's a valid TLS domain set for Coturn
|
||||||
@ -155,19 +151,14 @@ class EjabberdTurnConsumer(TurnConsumer):
|
|||||||
|
|
||||||
|
|
||||||
def on_pre_hostname_change(sender, old_hostname, new_hostname, **kwargs):
|
def on_pre_hostname_change(sender, old_hostname, new_hostname, **kwargs):
|
||||||
"""
|
"""Backup ejabberd database before hostname is changed."""
|
||||||
Backup ejabberd database before hostname is changed.
|
|
||||||
"""
|
|
||||||
del sender # Unused
|
del sender # Unused
|
||||||
del kwargs # Unused
|
del kwargs # Unused
|
||||||
app = app_module.App.get('ejabberd')
|
app = app_module.App.get('ejabberd')
|
||||||
if app.needs_setup():
|
if app.needs_setup():
|
||||||
return
|
return
|
||||||
|
|
||||||
actions.superuser_run('ejabberd', [
|
privileged.pre_change_hostname(old_hostname, new_hostname)
|
||||||
'pre-change-hostname', '--old-hostname', old_hostname,
|
|
||||||
'--new-hostname', new_hostname
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
def on_post_hostname_change(sender, old_hostname, new_hostname, **kwargs):
|
def on_post_hostname_change(sender, old_hostname, new_hostname, **kwargs):
|
||||||
@ -178,21 +169,16 @@ def on_post_hostname_change(sender, old_hostname, new_hostname, **kwargs):
|
|||||||
if app.needs_setup():
|
if app.needs_setup():
|
||||||
return
|
return
|
||||||
|
|
||||||
actions.superuser_run('ejabberd', [
|
privileged.change_hostname(_run_in_background=True)
|
||||||
'change-hostname', '--old-hostname', old_hostname, '--new-hostname',
|
|
||||||
new_hostname
|
|
||||||
], run_in_background=True)
|
|
||||||
|
|
||||||
|
|
||||||
def get_domains():
|
def get_domains():
|
||||||
"""Return the list of domains configured for ejabberd.
|
"""Return the list of domains configured for ejabberd."""
|
||||||
"""
|
|
||||||
app = app_module.App.get('ejabberd')
|
app = app_module.App.get('ejabberd')
|
||||||
if app.needs_setup():
|
if app.needs_setup():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
output = actions.superuser_run('ejabberd', ['get-domains'])
|
return privileged.get_domains()
|
||||||
return json.loads(output)
|
|
||||||
|
|
||||||
|
|
||||||
def on_domain_added(sender, domain_type, name='', description='',
|
def on_domain_added(sender, domain_type, name='', description='',
|
||||||
@ -204,7 +190,7 @@ def on_domain_added(sender, domain_type, name='', description='',
|
|||||||
|
|
||||||
domains = get_domains()
|
domains = get_domains()
|
||||||
if name not in domains:
|
if name not in domains:
|
||||||
actions.superuser_run('ejabberd', ['add-domain', '--domainname', name])
|
privileged.add_domain(name)
|
||||||
app.get_component('letsencrypt-ejabberd').setup_certificates()
|
app.get_component('letsencrypt-ejabberd').setup_certificates()
|
||||||
|
|
||||||
|
|
||||||
@ -214,9 +200,7 @@ def set_domains(domains):
|
|||||||
if not domains or app.needs_setup():
|
if not domains or app.needs_setup():
|
||||||
return
|
return
|
||||||
|
|
||||||
commands = ['set-domains', '--domains']
|
privileged.set_domains(domains)
|
||||||
commands.extend(domains)
|
|
||||||
actions.superuser_run('ejabberd', commands)
|
|
||||||
app.get_component('letsencrypt-ejabberd').setup_certificates()
|
app.get_component('letsencrypt-ejabberd').setup_certificates()
|
||||||
|
|
||||||
|
|
||||||
@ -227,14 +211,11 @@ def update_turn_configuration(config: TurnConfiguration, managed=True,
|
|||||||
if not force and app.needs_setup():
|
if not force and app.needs_setup():
|
||||||
return
|
return
|
||||||
|
|
||||||
params = ['configure-turn']
|
privileged.configure_turn(json.loads(config.to_json()), managed)
|
||||||
params += ['--managed'] if managed else []
|
|
||||||
actions.superuser_run('ejabberd', params, input=config.to_json().encode())
|
|
||||||
|
|
||||||
|
|
||||||
def get_turn_configuration() -> (TurnConfiguration, bool):
|
def get_turn_configuration() -> Tuple[TurnConfiguration, bool]:
|
||||||
"""Get the latest STUN/TURN configuration."""
|
"""Get the latest STUN/TURN configuration."""
|
||||||
json_config = actions.superuser_run('ejabberd', ['get-turn-config'])
|
tc, managed = privileged.get_turn_config()
|
||||||
tc, managed = json.loads(json_config)
|
return TurnConfiguration(tc['domain'], tc['uris'],
|
||||||
return (TurnConfiguration(tc['domain'], tc['uris'],
|
tc['shared_secret']), managed
|
||||||
tc['shared_secret']), managed)
|
|
||||||
|
|||||||
240
actions/ejabberd → plinth/modules/ejabberd/privileged.py
Executable file → Normal file
240
actions/ejabberd → plinth/modules/ejabberd/privileged.py
Executable file → Normal file
@ -1,24 +1,23 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""
|
"""Configuration helper for the ejabberd service."""
|
||||||
Configuration helper for the ejabberd service
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
import logging
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional, Tuple
|
||||||
|
|
||||||
from ruamel.yaml import YAML, scalarstring
|
from ruamel.yaml import YAML, scalarstring
|
||||||
|
|
||||||
from plinth import action_utils
|
from plinth import action_utils
|
||||||
|
from plinth.actions import privileged
|
||||||
from plinth.version import Version
|
from plinth.version import Version
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
EJABBERD_CONFIG = '/etc/ejabberd/ejabberd.yml'
|
EJABBERD_CONFIG = '/etc/ejabberd/ejabberd.yml'
|
||||||
EJABBERD_BACKUP = '/var/log/ejabberd/ejabberd.dump'
|
EJABBERD_BACKUP = '/var/log/ejabberd/ejabberd.dump'
|
||||||
EJABBERD_BACKUP_NEW = '/var/log/ejabberd/ejabberd_new.dump'
|
EJABBERD_BACKUP_NEW = '/var/log/ejabberd/ejabberd_new.dump'
|
||||||
@ -34,88 +33,9 @@ yaml.preserve_quotes = True
|
|||||||
TURN_URI_REGEX = r'(stun|turn):(.*):([0-9]{4})\?transport=(tcp|udp)'
|
TURN_URI_REGEX = r'(stun|turn):(.*):([0-9]{4})\?transport=(tcp|udp)'
|
||||||
|
|
||||||
|
|
||||||
def parse_arguments():
|
@privileged
|
||||||
"""Return parsed command line arguments as dictionary"""
|
def pre_install(domainname: str):
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
|
|
||||||
|
|
||||||
# Get configuration
|
|
||||||
subparsers.add_parser('get-configuration',
|
|
||||||
help='Return the current configuration')
|
|
||||||
|
|
||||||
# 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')
|
|
||||||
|
|
||||||
# Manage domain names
|
|
||||||
subparsers.add_parser('get-domains',
|
|
||||||
help='Get all configured domains in JSON format')
|
|
||||||
|
|
||||||
add_domain = subparsers.add_parser('add-domain',
|
|
||||||
help='Add a domain name to ejabberd')
|
|
||||||
add_domain.add_argument('--domainname', help='New domain name')
|
|
||||||
|
|
||||||
set_domains = subparsers.add_parser('set-domains',
|
|
||||||
help='Set list of ejabberd domains')
|
|
||||||
set_domains.add_argument('--domains', nargs='+',
|
|
||||||
help='One or more domain names')
|
|
||||||
|
|
||||||
# Configure STUN/TURN server for use with ejabberd
|
|
||||||
turn = subparsers.add_parser(
|
|
||||||
'configure-turn', help='Configure a TURN server for use with ejabberd')
|
|
||||||
turn.add_argument(
|
|
||||||
'--managed', required=False, default=False, action='store_true',
|
|
||||||
help='Whether configuration is provided by user or auto-managed by '
|
|
||||||
'FreedomBox')
|
|
||||||
|
|
||||||
subparsers.add_parser(
|
|
||||||
'get-turn-config',
|
|
||||||
help='Get the latest STUN/TURN configuration in JSON format')
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
subparsers.required = True
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def subcommand_get_configuration(_):
|
|
||||||
"""Return the current configuration, specifically domains configured."""
|
|
||||||
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
|
|
||||||
conf = yaml.load(file_handle)
|
|
||||||
|
|
||||||
print(json.dumps({'domains': conf['hosts']}))
|
|
||||||
|
|
||||||
|
|
||||||
def subcommand_pre_install(arguments):
|
|
||||||
"""Preseed debconf values before packages are installed."""
|
"""Preseed debconf values before packages are installed."""
|
||||||
domainname = arguments.domainname
|
|
||||||
if not domainname:
|
if not domainname:
|
||||||
# If new domainname is blank, use hostname instead.
|
# If new domainname is blank, use hostname instead.
|
||||||
domainname = socket.gethostname()
|
domainname = socket.gethostname()
|
||||||
@ -124,8 +44,9 @@ def subcommand_pre_install(arguments):
|
|||||||
['ejabberd ejabberd/hostname string ' + domainname])
|
['ejabberd ejabberd/hostname string ' + domainname])
|
||||||
|
|
||||||
|
|
||||||
def subcommand_setup(arguments):
|
@privileged
|
||||||
"""Enabled LDAP authentication"""
|
def setup(domainname: str):
|
||||||
|
"""Enable LDAP authentication."""
|
||||||
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
|
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
|
||||||
conf = yaml.load(file_handle)
|
conf = yaml.load(file_handle)
|
||||||
|
|
||||||
@ -143,19 +64,20 @@ def subcommand_setup(arguments):
|
|||||||
with open(EJABBERD_CONFIG, 'w', encoding='utf-8') as file_handle:
|
with open(EJABBERD_CONFIG, 'w', encoding='utf-8') as file_handle:
|
||||||
yaml.dump(conf, file_handle)
|
yaml.dump(conf, file_handle)
|
||||||
|
|
||||||
upgrade_config(arguments.domainname)
|
_upgrade_config(domainname)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.check_output(['ejabberdctl', 'restart'])
|
subprocess.check_output(['ejabberdctl', 'restart'])
|
||||||
except subprocess.CalledProcessError as err:
|
except subprocess.CalledProcessError as err:
|
||||||
print('Failed to restart ejabberd with new configuration: %s', err)
|
logger.warn('Failed to restart ejabberd with new configuration: %s',
|
||||||
|
err)
|
||||||
|
|
||||||
|
|
||||||
def upgrade_config(domain):
|
def _upgrade_config(domain):
|
||||||
"""Fix the config file by removing deprecated settings"""
|
"""Fix the config file by removing deprecated settings."""
|
||||||
current_version = _get_version()
|
current_version = _get_version()
|
||||||
if not current_version:
|
if not current_version:
|
||||||
print('Warning: Unable to get ejabberd version.')
|
logger.warn('Warning: Unable to get ejabberd version.')
|
||||||
|
|
||||||
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
|
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
|
||||||
conf = yaml.load(file_handle)
|
conf = yaml.load(file_handle)
|
||||||
@ -194,31 +116,25 @@ def upgrade_config(domain):
|
|||||||
yaml.dump(conf, file_handle)
|
yaml.dump(conf, file_handle)
|
||||||
|
|
||||||
|
|
||||||
def subcommand_pre_change_hostname(arguments):
|
@privileged
|
||||||
"""Prepare ejabberd for hostname change"""
|
def pre_change_hostname(old_hostname: str, new_hostname: str):
|
||||||
|
"""Prepare ejabberd for hostname change."""
|
||||||
if not shutil.which('ejabberdctl'):
|
if not shutil.which('ejabberdctl'):
|
||||||
print('ejabberdctl not found. Is ejabberd installed?')
|
logger.info('ejabberdctl not found')
|
||||||
return
|
return
|
||||||
|
|
||||||
old_hostname = arguments.old_hostname
|
|
||||||
new_hostname = arguments.new_hostname
|
|
||||||
|
|
||||||
subprocess.call(['ejabberdctl', 'backup', EJABBERD_BACKUP])
|
subprocess.call(['ejabberdctl', 'backup', EJABBERD_BACKUP])
|
||||||
try:
|
subprocess.check_output([
|
||||||
subprocess.check_output([
|
'ejabberdctl', 'mnesia-change-nodename', 'ejabberd@' + old_hostname,
|
||||||
'ejabberdctl', 'mnesia-change-nodename',
|
'ejabberd@' + new_hostname, EJABBERD_BACKUP, EJABBERD_BACKUP_NEW
|
||||||
'ejabberd@' + old_hostname, 'ejabberd@' + new_hostname,
|
])
|
||||||
EJABBERD_BACKUP, EJABBERD_BACKUP_NEW
|
os.remove(EJABBERD_BACKUP)
|
||||||
])
|
|
||||||
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):
|
@privileged
|
||||||
"""Update ejabberd with new hostname"""
|
def change_hostname():
|
||||||
|
"""Update ejabberd with new hostname."""
|
||||||
if not shutil.which('ejabberdctl'):
|
if not shutil.which('ejabberdctl'):
|
||||||
print('ejabberdctl not found. Is ejabberd installed?')
|
|
||||||
return
|
return
|
||||||
|
|
||||||
action_utils.service_stop('ejabberd')
|
action_utils.service_stop('ejabberd')
|
||||||
@ -238,35 +154,34 @@ def subcommand_change_hostname(arguments):
|
|||||||
['ejabberdctl', 'restore', EJABBERD_BACKUP_NEW])
|
['ejabberdctl', 'restore', EJABBERD_BACKUP_NEW])
|
||||||
os.remove(EJABBERD_BACKUP_NEW)
|
os.remove(EJABBERD_BACKUP_NEW)
|
||||||
except subprocess.CalledProcessError as err:
|
except subprocess.CalledProcessError as err:
|
||||||
print('Failed to restore ejabberd backup database: %s', err)
|
logger.error('Failed to restore ejabberd backup database: %s', err)
|
||||||
else:
|
else:
|
||||||
print('Could not load ejabberd backup database: %s not found' %
|
logger.error('Could not load ejabberd backup database: %s not found' %
|
||||||
EJABBERD_BACKUP_NEW)
|
EJABBERD_BACKUP_NEW)
|
||||||
|
|
||||||
|
|
||||||
def subcommand_get_domains(_):
|
@privileged
|
||||||
|
def get_domains() -> list[str]:
|
||||||
"""Get all configured domains."""
|
"""Get all configured domains."""
|
||||||
if not shutil.which('ejabberdctl'):
|
if not shutil.which('ejabberdctl'):
|
||||||
print('ejabberdctl not found. Is ejabberd installed?')
|
return []
|
||||||
return
|
|
||||||
|
|
||||||
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
|
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
|
||||||
conf = yaml.load(file_handle)
|
conf = yaml.load(file_handle)
|
||||||
|
|
||||||
print(json.dumps(conf['hosts']))
|
return conf['hosts']
|
||||||
|
|
||||||
|
|
||||||
def subcommand_add_domain(arguments):
|
@privileged
|
||||||
|
def add_domain(domainname: str):
|
||||||
"""Update ejabberd with new domainname.
|
"""Update ejabberd with new domainname.
|
||||||
|
|
||||||
Restarting ejabberd is handled by letsencrypt-ejabberd component.
|
Restarting ejabberd is handled by letsencrypt-ejabberd component.
|
||||||
"""
|
"""
|
||||||
if not shutil.which('ejabberdctl'):
|
if not shutil.which('ejabberdctl'):
|
||||||
print('ejabberdctl not found. Is ejabberd installed?')
|
logger.info('ejabberdctl not found')
|
||||||
return
|
return
|
||||||
|
|
||||||
domainname = arguments.domainname
|
|
||||||
|
|
||||||
# Add updated domainname to ejabberd hosts list.
|
# Add updated domainname to ejabberd hosts list.
|
||||||
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
|
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
|
||||||
conf = yaml.load(file_handle)
|
conf = yaml.load(file_handle)
|
||||||
@ -281,43 +196,43 @@ def subcommand_add_domain(arguments):
|
|||||||
# Restarting ejabberd is handled by letsencrypt-ejabberd component.
|
# Restarting ejabberd is handled by letsencrypt-ejabberd component.
|
||||||
|
|
||||||
|
|
||||||
def subcommand_set_domains(arguments):
|
@privileged
|
||||||
|
def set_domains(domains: list[str]):
|
||||||
"""Set list of ejabberd domains.
|
"""Set list of ejabberd domains.
|
||||||
|
|
||||||
Restarting ejabberd is handled by letsencrypt-ejabberd component.
|
Restarting ejabberd is handled by letsencrypt-ejabberd component.
|
||||||
"""
|
"""
|
||||||
|
if not len(domains):
|
||||||
|
raise ValueError('No domains provided')
|
||||||
|
|
||||||
if not shutil.which('ejabberdctl'):
|
if not shutil.which('ejabberdctl'):
|
||||||
print('ejabberdctl not found. Is ejabberd installed?')
|
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
|
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
|
||||||
conf = yaml.load(file_handle)
|
conf = yaml.load(file_handle)
|
||||||
|
|
||||||
conf['hosts'] = arguments.domains
|
conf['hosts'] = domains
|
||||||
|
|
||||||
with open(EJABBERD_CONFIG, 'w', encoding='utf-8') as file_handle:
|
with open(EJABBERD_CONFIG, 'w', encoding='utf-8') as file_handle:
|
||||||
yaml.dump(conf, file_handle)
|
yaml.dump(conf, file_handle)
|
||||||
|
|
||||||
|
|
||||||
def subcommand_mam(argument):
|
@privileged
|
||||||
|
def mam(command: str) -> Optional[bool]:
|
||||||
"""Enable, disable, or get status of Message Archive Management (MAM)."""
|
"""Enable, disable, or get status of Message Archive Management (MAM)."""
|
||||||
|
if command not in ('enable', 'disable', 'status'):
|
||||||
|
raise ValueError('Invalid command')
|
||||||
|
|
||||||
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
|
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
|
||||||
conf = yaml.load(file_handle)
|
conf = yaml.load(file_handle)
|
||||||
|
|
||||||
if 'modules' not in conf:
|
if 'modules' not in conf:
|
||||||
print('Found no "modules" entry in ejabberd configuration file.')
|
return None
|
||||||
return
|
|
||||||
|
|
||||||
if argument.command == 'status':
|
if command == 'status':
|
||||||
if 'mod_mam' in conf['modules']:
|
return 'mod_mam' in conf['modules']
|
||||||
print('enabled')
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
print('disabled')
|
|
||||||
return
|
|
||||||
|
|
||||||
if argument.command == 'enable':
|
if command == 'enable':
|
||||||
# Explicitly set the recommended / default settings for mod_mam,
|
# Explicitly set the recommended / default settings for mod_mam,
|
||||||
# see https://docs.ejabberd.im/admin/configuration/#mod-mam.
|
# see https://docs.ejabberd.im/admin/configuration/#mod-mam.
|
||||||
settings_mod_mam = {
|
settings_mod_mam = {
|
||||||
@ -332,13 +247,10 @@ def subcommand_mam(argument):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
conf['modules'].update(settings_mod_mam)
|
conf['modules'].update(settings_mod_mam)
|
||||||
elif argument.command == 'disable':
|
elif command == 'disable':
|
||||||
# disable modules by erasing from config file
|
# disable modules by erasing from config file
|
||||||
if 'mod_mam' in conf['modules']:
|
if 'mod_mam' in conf['modules']:
|
||||||
conf['modules'].pop('mod_mam')
|
conf['modules'].pop('mod_mam')
|
||||||
else:
|
|
||||||
print("Unknown command: %s" % argument.command)
|
|
||||||
return
|
|
||||||
|
|
||||||
with open(EJABBERD_CONFIG, 'w', encoding='utf-8') as file_handle:
|
with open(EJABBERD_CONFIG, 'w', encoding='utf-8') as file_handle:
|
||||||
yaml.dump(conf, file_handle)
|
yaml.dump(conf, file_handle)
|
||||||
@ -346,6 +258,8 @@ def subcommand_mam(argument):
|
|||||||
if action_utils.service_is_running('ejabberd'):
|
if action_utils.service_is_running('ejabberd'):
|
||||||
subprocess.call(['ejabberdctl', 'reload_config'])
|
subprocess.call(['ejabberdctl', 'reload_config'])
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _generate_service(uri: str) -> dict:
|
def _generate_service(uri: str) -> dict:
|
||||||
"""Generate ejabberd mod_stun_disco service config from Coturn URI."""
|
"""Generate ejabberd mod_stun_disco service config from Coturn URI."""
|
||||||
@ -368,7 +282,8 @@ def _generate_uris(services: list[dict]) -> list[str]:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def subcommand_get_turn_config(_):
|
@privileged
|
||||||
|
def get_turn_config() -> Tuple[dict[str, Any], bool]:
|
||||||
"""Get the latest STUN/TURN configuration in JSON format."""
|
"""Get the latest STUN/TURN configuration in JSON format."""
|
||||||
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
|
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
|
||||||
conf = yaml.load(file_handle)
|
conf = yaml.load(file_handle)
|
||||||
@ -377,24 +292,18 @@ def subcommand_get_turn_config(_):
|
|||||||
managed = os.path.exists(EJABBERD_MANAGED_COTURN)
|
managed = os.path.exists(EJABBERD_MANAGED_COTURN)
|
||||||
|
|
||||||
if bool(mod_stun_disco_config):
|
if bool(mod_stun_disco_config):
|
||||||
print(
|
return {
|
||||||
json.dumps([{
|
'domain': '',
|
||||||
'domain': '',
|
'uris': _generate_uris(mod_stun_disco_config['services']),
|
||||||
'uris': _generate_uris(mod_stun_disco_config['services']),
|
'shared_secret': mod_stun_disco_config['secret'],
|
||||||
'shared_secret': mod_stun_disco_config['secret'],
|
}, managed
|
||||||
}, managed]))
|
|
||||||
else:
|
else:
|
||||||
print(
|
return {'domain': None, 'uris': [], 'shared_secret': None}, managed
|
||||||
json.dumps([{
|
|
||||||
'domain': None,
|
|
||||||
'uris': [],
|
|
||||||
'shared_secret': None
|
|
||||||
}, managed]))
|
|
||||||
|
|
||||||
|
|
||||||
def subcommand_configure_turn(arguments):
|
@privileged
|
||||||
|
def configure_turn(turn_server_config: dict[str, Any], managed: bool):
|
||||||
"""Set parameters for the STUN/TURN server to use with ejabberd."""
|
"""Set parameters for the STUN/TURN server to use with ejabberd."""
|
||||||
turn_server_config = json.loads(''.join(sys.stdin))
|
|
||||||
uris = turn_server_config['uris']
|
uris = turn_server_config['uris']
|
||||||
mod_stun_disco_config = {}
|
mod_stun_disco_config = {}
|
||||||
|
|
||||||
@ -413,7 +322,7 @@ def subcommand_configure_turn(arguments):
|
|||||||
with open(EJABBERD_CONFIG, 'w', encoding='utf-8') as file_handle:
|
with open(EJABBERD_CONFIG, 'w', encoding='utf-8') as file_handle:
|
||||||
yaml.dump(conf, file_handle)
|
yaml.dump(conf, file_handle)
|
||||||
|
|
||||||
if arguments.managed:
|
if managed:
|
||||||
Path(EJABBERD_MANAGED_COTURN).touch()
|
Path(EJABBERD_MANAGED_COTURN).touch()
|
||||||
else:
|
else:
|
||||||
Path(EJABBERD_MANAGED_COTURN).unlink(missing_ok=True)
|
Path(EJABBERD_MANAGED_COTURN).unlink(missing_ok=True)
|
||||||
@ -423,7 +332,7 @@ def subcommand_configure_turn(arguments):
|
|||||||
|
|
||||||
|
|
||||||
def _get_version():
|
def _get_version():
|
||||||
""" Get the current ejabberd version """
|
"""Get the current ejabberd version."""
|
||||||
try:
|
try:
|
||||||
output = subprocess.check_output(['ejabberdctl',
|
output = subprocess.check_output(['ejabberdctl',
|
||||||
'status']).decode('utf-8')
|
'status']).decode('utf-8')
|
||||||
@ -435,16 +344,3 @@ def _get_version():
|
|||||||
version = str(version_info[1])
|
version = str(version_info[1])
|
||||||
return Version(version)
|
return Version(version)
|
||||||
return None
|
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()
|
|
||||||
@ -11,6 +11,7 @@ import pytest
|
|||||||
|
|
||||||
from plinth.modules import ejabberd
|
from plinth.modules import ejabberd
|
||||||
from plinth.modules.coturn.components import TurnConfiguration
|
from plinth.modules.coturn.components import TurnConfiguration
|
||||||
|
from plinth.modules.ejabberd import privileged
|
||||||
|
|
||||||
managed_configuration = TurnConfiguration(
|
managed_configuration = TurnConfiguration(
|
||||||
'freedombox.local', [],
|
'freedombox.local', [],
|
||||||
@ -20,9 +21,9 @@ overridden_configuration = TurnConfiguration(
|
|||||||
'public.coturn.site', [],
|
'public.coturn.site', [],
|
||||||
'BUeKvKzhAsTZ8MEwMd3yTwpr2uvbOxgWe51aiP02OAGyOlj6WGuCyqj7iaOsbkC7')
|
'BUeKvKzhAsTZ8MEwMd3yTwpr2uvbOxgWe51aiP02OAGyOlj6WGuCyqj7iaOsbkC7')
|
||||||
|
|
||||||
actions_name = 'ejabberd'
|
pytestmark = pytest.mark.usefixtures('mock_privileged')
|
||||||
|
|
||||||
current_directory = pathlib.Path(__file__).parent
|
current_directory = pathlib.Path(__file__).parent
|
||||||
|
privileged_modules_to_mock = ['plinth.modules.ejabberd.privileged']
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name='conf_file')
|
@pytest.fixture(name='conf_file')
|
||||||
@ -45,31 +46,27 @@ def fixture_managed_file(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def fixture_set_configuration_paths(actions_module, conf_file, managed_file):
|
def fixture_set_configuration_paths(conf_file, managed_file):
|
||||||
"""Setup configuration file paths in action module."""
|
"""Setup configuration file paths in action module."""
|
||||||
actions_module.EJABBERD_CONFIG = conf_file
|
privileged.EJABBERD_CONFIG = conf_file
|
||||||
actions_module.EJABBERD_MANAGED_COTURN = managed_file
|
privileged.EJABBERD_MANAGED_COTURN = managed_file
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name='test_configuration', autouse=True)
|
@pytest.fixture(name='test_configuration', autouse=True)
|
||||||
def fixture_test_configuration(call_action, conf_file):
|
def fixture_test_configuration(conf_file):
|
||||||
"""Use a separate ejabberd configuration for tests.
|
"""Use a separate ejabberd configuration for tests.
|
||||||
|
|
||||||
Patches actions.superuser_run with the fixture call_action.
|
|
||||||
The module state is patched to be 'up-to-date'.
|
The module state is patched to be 'up-to-date'.
|
||||||
"""
|
"""
|
||||||
with patch('plinth.actions.superuser_run',
|
with patch('plinth.app.App.get') as app_get:
|
||||||
call_action), patch('plinth.app.App.get') as app_get:
|
|
||||||
app = Mock()
|
app = Mock()
|
||||||
app_get.return_value = app
|
app_get.return_value = app
|
||||||
app.needs_setup.return_value = False
|
app.needs_setup.return_value = False
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
def _set_turn_configuration(monkeypatch, config=managed_configuration,
|
def _set_turn_configuration(config=managed_configuration, managed=True):
|
||||||
managed=True):
|
with patch('plinth.privileged.service.is_running', return_value=False):
|
||||||
monkeypatch.setattr('sys.stdin', config.to_json())
|
|
||||||
with patch('plinth.action_utils.service_is_running', return_value=False):
|
|
||||||
ejabberd.update_turn_configuration(config, managed=managed)
|
ejabberd.update_turn_configuration(config, managed=managed)
|
||||||
|
|
||||||
|
|
||||||
@ -81,17 +78,17 @@ def _assert_conf(expected_configuration, expected_managed):
|
|||||||
assert managed == expected_managed
|
assert managed == expected_managed
|
||||||
|
|
||||||
|
|
||||||
def test_managed_turn_server_configuration(monkeypatch):
|
def test_managed_turn_server_configuration():
|
||||||
_set_turn_configuration(monkeypatch)
|
_set_turn_configuration()
|
||||||
_assert_conf(managed_configuration, True)
|
_assert_conf(managed_configuration, True)
|
||||||
|
|
||||||
|
|
||||||
def test_overridden_turn_server_configuration(monkeypatch):
|
def test_overridden_turn_server_configuration():
|
||||||
_set_turn_configuration(monkeypatch, overridden_configuration, False)
|
_set_turn_configuration(overridden_configuration, False)
|
||||||
_assert_conf(overridden_configuration, False)
|
_assert_conf(overridden_configuration, False)
|
||||||
|
|
||||||
|
|
||||||
def test_remove_turn_configuration(monkeypatch):
|
def test_remove_turn_configuration():
|
||||||
_set_turn_configuration(monkeypatch)
|
_set_turn_configuration()
|
||||||
_set_turn_configuration(monkeypatch, TurnConfiguration(), False)
|
_set_turn_configuration(TurnConfiguration(), False)
|
||||||
_assert_conf(TurnConfiguration(), False)
|
_assert_conf(TurnConfiguration(), False)
|
||||||
|
|||||||
@ -1,31 +1,30 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""
|
"""Views for the Ejabberd app."""
|
||||||
Views for the Ejabberd module
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from plinth import actions
|
|
||||||
from plinth.modules import coturn, ejabberd
|
from plinth.modules import coturn, ejabberd
|
||||||
from plinth.modules.coturn.components import TurnConfiguration
|
from plinth.modules.coturn.components import TurnConfiguration
|
||||||
from plinth.views import AppView
|
from plinth.views import AppView
|
||||||
|
|
||||||
|
from . import privileged
|
||||||
from .forms import EjabberdForm
|
from .forms import EjabberdForm
|
||||||
|
|
||||||
|
|
||||||
class EjabberdAppView(AppView):
|
class EjabberdAppView(AppView):
|
||||||
"""Show ejabberd as a service."""
|
"""Show ejabberd as a service."""
|
||||||
|
|
||||||
app_id = 'ejabberd'
|
app_id = 'ejabberd'
|
||||||
template_name = 'ejabberd.html'
|
template_name = 'ejabberd.html'
|
||||||
form_class = EjabberdForm
|
form_class = EjabberdForm
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
"""Initial data to fill in the form."""
|
"""Return initial data to fill in the form."""
|
||||||
config, managed = ejabberd.get_turn_configuration()
|
config, managed = ejabberd.get_turn_configuration()
|
||||||
return super().get_initial() | {
|
return super().get_initial() | {
|
||||||
'domain_names': ejabberd.get_domains(),
|
'domain_names': ejabberd.get_domains(),
|
||||||
'MAM_enabled': self.is_MAM_enabled(),
|
'MAM_enabled': privileged.mam('status'),
|
||||||
'enable_managed_turn': managed,
|
'enable_managed_turn': managed,
|
||||||
'turn_uris': '\n'.join(config.uris),
|
'turn_uris': '\n'.join(config.uris),
|
||||||
'shared_secret': config.shared_secret
|
'shared_secret': config.shared_secret
|
||||||
@ -65,9 +64,9 @@ class EjabberdAppView(AppView):
|
|||||||
def _handle_MAM_configuration(old_config, new_config):
|
def _handle_MAM_configuration(old_config, new_config):
|
||||||
# note ejabberd action "enable" or "disable" restarts, if running
|
# note ejabberd action "enable" or "disable" restarts, if running
|
||||||
if new_config['MAM_enabled']:
|
if new_config['MAM_enabled']:
|
||||||
actions.superuser_run('ejabberd', ['mam', 'enable'])
|
privileged.mam('enable')
|
||||||
else:
|
else:
|
||||||
actions.superuser_run('ejabberd', ['mam', 'disable'])
|
privileged.mam('disable')
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""Enable/disable a service and set messages."""
|
"""Enable/disable a service and set messages."""
|
||||||
@ -96,8 +95,3 @@ class EjabberdAppView(AppView):
|
|||||||
messages.success(self.request, _('Configuration updated'))
|
messages.success(self.request, _('Configuration updated'))
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
def is_MAM_enabled(self):
|
|
||||||
"""Return whether Message Archive Management (MAM) is enabled."""
|
|
||||||
output = actions.superuser_run('ejabberd', ['mam', 'status'])
|
|
||||||
return output.strip() == 'enabled'
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user