From 1a39212313e645fa004ce3e3ba0987c57ed0b6b5 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Wed, 8 Jun 2022 10:57:13 -0400 Subject: [PATCH] ejabberd: Add multi-select form for domains Choices includes all of the available domain names in the system, as well as any domains that are in ejabberd configuration. Tests: - Disable a domain. It is removed from ejabberd config. - Enable a domain. It is added to ejabberd config. - Enable all name services. Run ejabberd functional tests and observe that they pass. Reviewed-by: Sunil Mohan Adapa --- actions/ejabberd | 47 +++++++++++++++++- plinth/modules/ejabberd/__init__.py | 42 +++++++++------- plinth/modules/ejabberd/forms.py | 21 ++++++++ .../modules/ejabberd/tests/test_functional.py | 49 ++++++++++++++++++- plinth/modules/ejabberd/views.py | 11 +++++ 5 files changed, 147 insertions(+), 23 deletions(-) diff --git a/actions/ejabberd b/actions/ejabberd index 7867df160..5ac834066 100755 --- a/actions/ejabberd +++ b/actions/ejabberd @@ -70,11 +70,19 @@ def parse_arguments(): hostname_change.add_argument('--old-hostname', help='Previous hostname') hostname_change.add_argument('--new-hostname', help='New hostname') - # Add a domain name to ejabberd + # 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') @@ -236,8 +244,23 @@ def subcommand_change_hostname(arguments): EJABBERD_BACKUP_NEW) +def subcommand_get_domains(_): + """Get all configured domains.""" + if not shutil.which('ejabberdctl'): + print('ejabberdctl not found. Is ejabberd installed?') + return + + with open(EJABBERD_CONFIG, 'r') as file_handle: + conf = yaml.load(file_handle) + + print(json.dumps(conf['hosts'])) + + def subcommand_add_domain(arguments): - """Update ejabberd with new domainname""" + """Update ejabberd with new domainname. + + Restarting ejabberd is handled by letsencrypt-ejabberd component. + """ if not shutil.which('ejabberdctl'): print('ejabberdctl not found. Is ejabberd installed?') return @@ -255,6 +278,26 @@ def subcommand_add_domain(arguments): with open(EJABBERD_CONFIG, 'w') as file_handle: yaml.dump(conf, file_handle) + # Restarting ejabberd is handled by letsencrypt-ejabberd component. + + +def subcommand_set_domains(arguments): + """Set list of ejabberd domains. + + Restarting ejabberd is handled by letsencrypt-ejabberd component. + """ + if not shutil.which('ejabberdctl'): + print('ejabberdctl not found. Is ejabberd installed?') + return + + with open(EJABBERD_CONFIG, 'r') as file_handle: + conf = yaml.load(file_handle) + + conf['hosts'] = arguments.domains + + with open(EJABBERD_CONFIG, 'w') as file_handle: + yaml.dump(conf, file_handle) + def subcommand_mam(argument): """Enable, disable, or get status of Message Archive Management (MAM).""" diff --git a/plinth/modules/ejabberd/__init__.py b/plinth/modules/ejabberd/__init__.py index 96da4d69e..7cc86439a 100644 --- a/plinth/modules/ejabberd/__init__.py +++ b/plinth/modules/ejabberd/__init__.py @@ -158,22 +158,6 @@ def setup(helper, old_version=None): update_turn_configuration(configuration, force=True) -def get_domains(): - """Return the list of domains that ejabberd is interested in. - - XXX: Retrieve the list from ejabberd configuration. - - """ - if app.needs_setup(): - return [] - - domain_name = config.get_domainname() - if domain_name: - return [domain_name] - - return [] - - def on_pre_hostname_change(sender, old_hostname, new_hostname, **kwargs): """ Backup ejabberd database before hostname is changed. @@ -202,19 +186,39 @@ def on_post_hostname_change(sender, old_hostname, new_hostname, **kwargs): ], run_in_background=True) +def get_domains(): + """Return the list of domains configured for ejabberd. + """ + if app.needs_setup(): + return [] + + output = actions.superuser_run('ejabberd', ['get-domains']) + return json.loads(output) + + def on_domain_added(sender, domain_type, name='', description='', services=None, **kwargs): """Update ejabberd config after domain name change.""" if not name or app.needs_setup(): return - conf = actions.superuser_run('ejabberd', ['get-configuration']) - conf = json.loads(conf) - if name not in conf['domains']: + domains = get_domains() + if name not in domains: actions.superuser_run('ejabberd', ['add-domain', '--domainname', name]) app.get_component('letsencrypt-ejabberd').setup_certificates() +def set_domains(domains): + """Configure ejabberd to have this list of domains.""" + if not domains or app.needs_setup(): + return + + commands = ['set-domains', '--domains'] + commands.extend(domains) + actions.superuser_run('ejabberd', commands) + app.get_component('letsencrypt-ejabberd').setup_certificates() + + def update_turn_configuration(config: TurnConfiguration, managed=True, force=False): """Update ejabberd's STUN/TURN server configuration.""" diff --git a/plinth/modules/ejabberd/forms.py b/plinth/modules/ejabberd/forms.py index dc605a546..af2c3a47f 100644 --- a/plinth/modules/ejabberd/forms.py +++ b/plinth/modules/ejabberd/forms.py @@ -8,12 +8,21 @@ from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from plinth import cfg +from plinth.modules import ejabberd from plinth.modules.coturn.forms import turn_uris_validator from plinth.utils import format_lazy class EjabberdForm(forms.Form): """Ejabberd configuration form.""" + domain_names = forms.MultipleChoiceField( + label=_('Domain names'), widget=forms.CheckboxSelectMultiple, + help_text=_( + 'Domains to be used by ejabberd. "localhost" is always included, ' + 'so it is not shown here. Note that user accounts are unique for ' + 'each domain, and migrating users to a new domain name is not yet ' + 'implemented.'), choices=[]) + MAM_enabled = forms.BooleanField( label=_('Enable Message Archive Management'), required=False, help_text=format_lazy( @@ -43,6 +52,18 @@ class EjabberdForm(forms.Form): help_text=_('Shared secret used to compute passwords for the ' 'TURN server.')) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Start with any existing domains from ejabberd configuration. + domains = set(ejabberd.get_domains()) + + # Add other domains that can be configured. + from plinth.modules.names.components import DomainName + domains |= DomainName.list_names() + + self.fields['domain_names'].choices = zip(domains, domains) + def clean_turn_uris(self): """Normalize newlines in URIs.""" data = self.cleaned_data['turn_uris'] diff --git a/plinth/modules/ejabberd/tests/test_functional.py b/plinth/modules/ejabberd/tests/test_functional.py index 1f1663a31..539d670da 100644 --- a/plinth/modules/ejabberd/tests/test_functional.py +++ b/plinth/modules/ejabberd/tests/test_functional.py @@ -8,8 +8,7 @@ from plinth.tests import functional pytestmark = [pytest.mark.apps, pytest.mark.ejabberd] -# TODO Check service -# TODO Check domain name display +# TODO Check domain name displayed in description class TestEjabberdApp(functional.BaseAppTests): @@ -17,6 +16,29 @@ class TestEjabberdApp(functional.BaseAppTests): has_service = True has_web = False + @pytest.fixture(scope='module', autouse=True) + def fixture_background(self, session_browser): + """Login, install, and enable the app.""" + functional.login(session_browser) + functional.install(session_browser, self.app_name) + functional.app_enable(session_browser, self.app_name) + yield + functional.login(session_browser) + functional.app_disable(session_browser, self.app_name) + + def test_add_remove_domain(self, session_browser): + """Test adding and removing a domain.""" + functional.app_enable(session_browser, 'ejabberd') + _enable_domain(session_browser, 'freedombox.local') + + _disable_domain(session_browser, 'freedombox.local') + assert not _is_domain_enabled(session_browser, 'freedombox.local') + assert functional.service_is_running(session_browser, 'ejabberd') + + _enable_domain(session_browser, 'freedombox.local') + assert _is_domain_enabled(session_browser, 'freedombox.local') + assert functional.service_is_running(session_browser, 'ejabberd') + def test_message_archive_management(self, session_browser): """Test enabling message archive management.""" functional.app_enable(session_browser, 'ejabberd') @@ -39,6 +61,29 @@ class TestEjabberdApp(functional.BaseAppTests): _jsxc_assert_has_contact(session_browser) +def _enable_domain(browser, domain): + """Add domain name to Ejabberd configuration.""" + functional.nav_to_module(browser, 'ejabberd') + checkbox = browser.find_by_value(domain).first + checkbox.check() + functional.submit(browser, form_class='form-configuration') + + +def _disable_domain(browser, domain): + """Remove domain name from Ejabberd configuration.""" + functional.nav_to_module(browser, 'ejabberd') + checkbox = browser.find_by_value(domain).first + checkbox.uncheck() + functional.submit(browser, form_class='form-configuration') + + +def _is_domain_enabled(browser, domain): + """Return whether the domain name is enabled.""" + functional.nav_to_module(browser, 'ejabberd') + checkbox = browser.find_by_value(domain).first + return checkbox.checked + + def _enable_message_archive_management(browser): """Enable Message Archive Management in Ejabberd.""" functional.nav_to_module(browser, 'ejabberd') diff --git a/plinth/modules/ejabberd/views.py b/plinth/modules/ejabberd/views.py index 1f470be4f..cca1b102b 100644 --- a/plinth/modules/ejabberd/views.py +++ b/plinth/modules/ejabberd/views.py @@ -24,6 +24,7 @@ class EjabberdAppView(AppView): """Initial data to fill in the form.""" config, managed = ejabberd.get_turn_configuration() return super().get_initial() | { + 'domain_names': ejabberd.get_domains(), 'MAM_enabled': self.is_MAM_enabled(), 'enable_managed_turn': managed, 'turn_uris': '\n'.join(config.uris), @@ -37,6 +38,11 @@ class EjabberdAppView(AppView): context['domainname'] = domains[0] if domains else None return context + @staticmethod + def _handle_domain_names_configuration(new_config): + """Update list of domain names in configuration.""" + ejabberd.set_domains(new_config['domain_names']) + @staticmethod def _handle_turn_configuration(old_config, new_config): if not new_config['enable_managed_turn']: @@ -72,6 +78,11 @@ class EjabberdAppView(AppView): return old_config[prop] != new_config[prop] is_changed = False + + if changed('domain_names'): + self._handle_domain_names_configuration(new_config) + is_changed = True + if changed('MAM_enabled'): self._handle_MAM_configuration(old_config, new_config) is_changed = True