From 02e409a3a1584d59e7f9f03276398b49a721752d Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 13 Feb 2024 16:27:39 -0800 Subject: [PATCH] daemon: Add new component for daemons shared across apps - This is useful for managing redis service needed by the upcoming Nextcloud app. - Disable the daemon only if all the apps using it are disabled. Enable it when even one of the them is enabled. - The component is not a 'leader' component as it does not decide the enabled/disabled status of the app. Tests: - Unit tests pass. - Install zoph and wordpress with full patch series. If one of the apps is disabled, mysql service is still enabled and running. If both apps are disabled, then mysql service is disabled and not running. Enabled/disabled status of apps are accurate after they are enabled/disabled. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- doc/dev/reference/components/daemon.rst | 3 ++ plinth/daemon.py | 35 ++++++++++++++++ plinth/tests/test_daemon.py | 55 ++++++++++++++++++++++++- 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/doc/dev/reference/components/daemon.rst b/doc/dev/reference/components/daemon.rst index ff6dd4664..69ef72786 100644 --- a/doc/dev/reference/components/daemon.rst +++ b/doc/dev/reference/components/daemon.rst @@ -8,3 +8,6 @@ Daemon .. autoclass:: plinth.daemon.RelatedDaemon :members: + +.. autoclass:: plinth.daemon.SharedDaemon + :members: diff --git a/plinth/daemon.py b/plinth/daemon.py index 88750ece0..24844f1be 100644 --- a/plinth/daemon.py +++ b/plinth/daemon.py @@ -135,6 +135,41 @@ class RelatedDaemon(app.FollowerComponent): self.unit = unit +class SharedDaemon(Daemon): + """Component to manage a daemon that is used by multiple apps. + + Daemons such as a database server are a hard requirement for an app. + However, there may be multiple apps using that server. This component + ensures that server is enabled and running when app is enabled. It runs + diagnostics on the daemon when app is diagnosed. The primary difference + from the Daemon component is that when the app is disabled the daemon must + only be disabled if there is no other app using this daemon. + """ + + # A shared daemon may be running even when an app is disabled because + # another app might be using the daemon. Hence, the enabled/disabled state + # of this component can't be used to determine the enabled/disabled state + # of the app. + is_leader = False + + def set_enabled(self, enabled): + """Do nothing. Enabled state is still determined by unit status.""" + + def disable(self): + """Disable the daemon iff this is the last app using the daemon.""" + other_apps_enabled = False + for other_app in app.App.list(): + if other_app.app_id == self.app_id: + continue + + for component in other_app.get_components_of_type(SharedDaemon): + if component.unit == self.unit and other_app.is_enabled(): + other_apps_enabled = True + + if not other_apps_enabled: + super().disable() + + def app_is_running(app_): """Return whether all the daemons in the app are running.""" for component in app_.components.values(): diff --git a/plinth/tests/test_daemon.py b/plinth/tests/test_daemon.py index 7051d6fe3..9dabdf092 100644 --- a/plinth/tests/test_daemon.py +++ b/plinth/tests/test_daemon.py @@ -10,7 +10,7 @@ from unittest.mock import Mock, call, patch import pytest from plinth.app import App, FollowerComponent, Info -from plinth.daemon import (Daemon, RelatedDaemon, app_is_running, +from plinth.daemon import (Daemon, RelatedDaemon, SharedDaemon, app_is_running, diagnose_netcat, diagnose_port_listening) from plinth.modules.diagnostics.check import DiagnosticCheck, Result @@ -332,3 +332,56 @@ def test_related_daemon_initialization(): with pytest.raises(ValueError): RelatedDaemon(None, 'test-daemon') + + +def test_shared_daemon_leader(): + """Test that shared daemon is not a leader component.""" + component1 = SharedDaemon('test-component1', 'test-daemon') + assert not component1.is_leader + + +@patch('plinth.action_utils.service_is_enabled') +def test_shared_daemon_set_enabled(service_is_enabled): + """Test that enabled status is determined by unit status.""" + component = SharedDaemon('test-component', 'test-daemon') + + service_is_enabled.return_value = False + component.set_enabled(False) + assert not component.is_enabled() + component.set_enabled(True) + assert not component.is_enabled() + + service_is_enabled.return_value = True + component.set_enabled(False) + assert component.is_enabled() + component.set_enabled(True) + assert component.is_enabled() + + +@patch('plinth.privileged.service.disable') +def test_shared_daemon_disable(disable_method): + """Test that shared daemon disables service correctly.""" + + class AppTest2(App): + """Test application class.""" + app_id = 'test-app-2' + + component1 = SharedDaemon('test-component1', 'test-daemon') + app1 = AppTest() + app1.add(component1) + app1.is_enabled = Mock() + + component2 = SharedDaemon('test-component2', 'test-daemon') + app2 = AppTest2() + app2.add(component2) + + # When another app is enabled, service should not be disabled + app1.is_enabled.return_value = True + app2.disable() + assert disable_method.mock_calls == [] + + # When all other apps are disabled, service should be disabled + disable_method.reset_mock() + app1.is_enabled.return_value = False + app2.disable() + assert disable_method.mock_calls == [call('test-daemon')]