diff --git a/CHANGELOG.md b/CHANGELOG.md index e9b6e4d13..345a4d394 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Add warning regarding move constraints to `pcs status` ([rhbz#2058247]) - Support for output formats `json` and `cmd` to `pcs resource config` and `pcs stonith config` commands ([rhbz#2058251], [rhbz#2058252]) +- Support systems with OpenRC init system ([ghissue#266]) ### Fixed - Booth ticket name validation ([rhbz#2053177]) @@ -24,6 +25,7 @@ - Preventing fence-loop caused when stonith-watchdog-timeout is set with wrong value ([rhbz#2058246]) +[ghissue#266]: https://github.com/ClusterLabs/pcs/issues/266 [rhbz#2024522]: https://bugzilla.redhat.com/show_bug.cgi?id=2024522 [rhbz#2053177]: https://bugzilla.redhat.com/show_bug.cgi?id=2053177 [rhbz#2054671]: https://bugzilla.redhat.com/show_bug.cgi?id=2054671 diff --git a/configure.ac b/configure.ac index 658b3e460..4544f894f 100644 --- a/configure.ac +++ b/configure.ac @@ -401,6 +401,8 @@ if test "x$SYSTEMCTL" = "x"; then AC_MSG_ERROR([Unable to find systemctl or service in $PATH]) fi fi +AC_PATH_PROG([RC_CONFIG], [rc-config]) +AC_PATH_PROG([RC_SERVICE], [rc-service]) if test "x$tests_only" != "xyes"; then AC_PATH_PROG([KILLALL], [killall]) diff --git a/pcs/Makefile.am b/pcs/Makefile.am index 35b4a1924..d4f412124 100644 --- a/pcs/Makefile.am +++ b/pcs/Makefile.am @@ -135,6 +135,7 @@ EXTRA_DIST = \ common/resource_agent/dto.py \ common/services/common.py \ common/services/drivers/__init__.py \ + common/services/drivers/openrc.py \ common/services/drivers/systemd.py \ common/services/drivers/sysvinit_rhel.py \ common/services_dto.py \ diff --git a/pcs/common/services/drivers/__init__.py b/pcs/common/services/drivers/__init__.py index a268a8920..f3bac7a91 100644 --- a/pcs/common/services/drivers/__init__.py +++ b/pcs/common/services/drivers/__init__.py @@ -1,2 +1,3 @@ +from .openrc import OpenRcDriver from .systemd import SystemdDriver from .sysvinit_rhel import SysVInitRhelDriver diff --git a/pcs/common/services/drivers/openrc.py b/pcs/common/services/drivers/openrc.py new file mode 100644 index 000000000..b2badbd79 --- /dev/null +++ b/pcs/common/services/drivers/openrc.py @@ -0,0 +1,97 @@ +import os.path +from typing import ( + List, + Optional, +) + +from .. import errors +from ..interfaces import ( + ExecutorInterface, + ServiceManagerInterface, +) + + +class OpenRcDriver(ServiceManagerInterface): + def __init__( + self, + executor: ExecutorInterface, + rc_service_bin: str, + rc_config_bin: str, + ): + """ + executor -- external commands used by this class are executed using + this object + rc_service_bin -- path to an executable used for starting and stopping + services and to check if a service is running + rc_config_bin -- path to an executable used for enabling, disabling and + listing available service and to check if service is enabled + """ + self._executor = executor + self._rc_service_bin = rc_service_bin + self._rc_config_bin = rc_config_bin + self._available_services: List[str] = [] + + def start(self, service: str, instance: Optional[str] = None) -> None: + result = self._executor.run([self._rc_service_bin, service, "start"]) + if result.retval != 0: + raise errors.StartServiceError(service, result.joined_output) + + def stop(self, service: str, instance: Optional[str] = None) -> None: + result = self._executor.run([self._rc_service_bin, service, "stop"]) + if result.retval != 0: + raise errors.StopServiceError(service, result.joined_output) + + def enable(self, service: str, instance: Optional[str] = None) -> None: + result = self._executor.run( + [self._rc_config_bin, "add", service, "default"] + ) + if result.retval != 0: + raise errors.EnableServiceError(service, result.joined_output) + + def disable(self, service: str, instance: Optional[str] = None) -> None: + if not self.is_installed(service): + return + result = self._executor.run( + [self._rc_config_bin, "delete", service, "default"] + ) + if result.retval != 0: + raise errors.DisableServiceError(service, result.joined_output) + + def is_enabled(self, service: str, instance: Optional[str] = None) -> bool: + result = self._executor.run([self._rc_config_bin, "list", "default"]) + for line in result.stdout.splitlines()[1:]: + if service == line.strip(): + return True + return False + + def is_running(self, service: str, instance: Optional[str] = None) -> bool: + return ( + self._executor.run([self._rc_service_bin, service, "status"]).retval + == 0 + ) + + def is_installed(self, service: str) -> bool: + return service in self.get_available_services() + + def get_available_services(self) -> List[str]: + if not self._available_services: + self._available_services = self._get_available_services() + return self._available_services + + def _get_available_services(self) -> List[str]: + result = self._executor.run([self._rc_config_bin, "list"]) + if result.retval != 0: + return [] + + service_list = [] + for service in result.stdout.splitlines()[1:]: + service = service.strip().split(" ")[0] + if service: + service_list.append(service) + return service_list + + def is_current_system_supported(self) -> bool: + return all( + os.path.isfile(binary) + for binary in (self._rc_service_bin, self._rc_config_bin) + ) diff --git a/pcs/lib/services.py b/pcs/lib/services.py index 5da73e64b..93d63185e 100644 --- a/pcs/lib/services.py +++ b/pcs/lib/services.py @@ -83,6 +83,9 @@ def get_service_manager( services.drivers.SystemdDriver( executor, settings.systemctl_binary, settings.systemd_unit_path ), + services.drivers.OpenRcDriver( + executor, settings.rc_service_binary, settings.rc_config_binary + ), services.drivers.SysVInitRhelDriver( executor, settings.service_binary, settings.chkconfig_binary ), diff --git a/pcs/settings.py.in b/pcs/settings.py.in index 6b80ea671..f979446df 100644 --- a/pcs/settings.py.in +++ b/pcs/settings.py.in @@ -4,6 +4,8 @@ systemctl_binary = "@SYSTEMCTL@" systemd_unit_path = "@SYSTEMD_UNIT_PATH@".split(":") chkconfig_binary = "/sbin/chkconfig" service_binary = "@SERVICE@" +rc_config_binary = "@RC_CONFIG@" +rc_service_binary = "@RC_SERVICE@" # Used only in utils.py in deprecated funcion pacemaker_binaries = "@PCMKEXECPREFIX@/sbin" corosync_binaries = "@COROEXECPREFIX@/sbin" @@ -118,6 +120,7 @@ pcs_data_dir = "@LIB_DIR@/pcs/data/" _ocf_1_0_schema_filename = "ocf-1.0.rng" _ocf_1_1_schema_filename = "ocf-1.1.rng" + class _PathManager: @property def ocf_1_0_schema(self): @@ -131,4 +134,5 @@ class _PathManager: def pcs_data_dir(self): return pcs_data_dir + path = _PathManager() diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am index 877f2736c..7ced99336 100644 --- a/pcs_test/Makefile.am +++ b/pcs_test/Makefile.am @@ -100,6 +100,7 @@ EXTRA_DIST = \ tier0/common/reports/test_item.py \ tier0/common/reports/test_messages.py \ tier0/common/services/drivers/__init__.py \ + tier0/common/services/drivers/test_openrc.py \ tier0/common/services/drivers/test_systemd.py \ tier0/common/services/drivers/test_sysvinit_rhel.py \ tier0/common/services/__init__.py \ diff --git a/pcs_test/tier0/common/services/drivers/test_openrc.py b/pcs_test/tier0/common/services/drivers/test_openrc.py new file mode 100644 index 000000000..8ef1090d0 --- /dev/null +++ b/pcs_test/tier0/common/services/drivers/test_openrc.py @@ -0,0 +1,277 @@ +from unittest import ( + TestCase, + mock, +) + +from pcs.common.services import errors +from pcs.common.services.drivers import OpenRcDriver +from pcs.common.services.interfaces import ExecutorInterface +from pcs.common.services.types import ExecutorResult + + +class Base(TestCase): + def setUp(self): + self.mock_executor = mock.MagicMock(spec_set=ExecutorInterface) + self.service = "service_name" + self.instance = "instance_name" + self.rc_service_bin = "rc_service_bin" + self.rc_config_bin = "rc_config_bin" + self.driver = OpenRcDriver( + self.mock_executor, self.rc_service_bin, self.rc_config_bin + ) + + +class BaseTestMixin: + cmd = [] + exception = None + executable = None + driver_callback = staticmethod(lambda: None) + + def test_success(self): + self.mock_executor.run.return_value = ExecutorResult(0, "", "") + self.driver_callback(self.service) + self.mock_executor.run.assert_called_once_with( + [self.executable] + self.cmd + ) + + def test_instance_success(self): + self.mock_executor.run.return_value = ExecutorResult(0, "", "") + self.driver_callback(self.service, self.instance) + self.mock_executor.run.assert_called_once_with( + [self.executable] + self.cmd + ) + + def test_failure(self): + result = ExecutorResult(1, "stdout", "stderr") + self.mock_executor.run.return_value = result + with self.assertRaises(self.exception) as cm: + self.driver_callback(self.service) + + self.assertEqual(cm.exception.service, self.service) + self.assertEqual(cm.exception.message, result.joined_output) + self.assertIsNone(cm.exception.instance) + self.mock_executor.run.assert_called_once_with( + [self.executable] + self.cmd + ) + + def test_instace_failure(self): + result = ExecutorResult(1, "stdout", "stderr") + self.mock_executor.run.return_value = result + with self.assertRaises(self.exception) as cm: + self.driver_callback(self.service, self.instance) + + self.assertEqual(cm.exception.service, self.service) + self.assertEqual(cm.exception.message, result.joined_output) + self.assertIsNone(cm.exception.instance) + self.mock_executor.run.assert_called_once_with( + [self.executable] + self.cmd + ) + + +class StartTest(Base, BaseTestMixin): + exception = errors.StartServiceError + + def setUp(self): + super().setUp() + self.driver_callback = self.driver.start + self.executable = self.rc_service_bin + self.cmd = [self.service, "start"] + + +class StopTest(Base, BaseTestMixin): + exception = errors.StopServiceError + + def setUp(self): + super().setUp() + self.driver_callback = self.driver.stop + self.executable = self.rc_service_bin + self.cmd = [self.service, "stop"] + + +class EnableTest(Base, BaseTestMixin): + exception = errors.EnableServiceError + + def setUp(self): + super().setUp() + self.driver_callback = self.driver.enable + self.executable = self.rc_config_bin + self.cmd = ["add", self.service, "default"] + + +class DisableTest(Base, BaseTestMixin): + exception = errors.DisableServiceError + + def setUp(self): + super().setUp() + # pylint: disable=protected-access + self.driver._available_services = [self.service] + self.driver_callback = self.driver.disable + self.executable = self.rc_config_bin + self.cmd = ["delete", self.service, "default"] + + def test_not_intalled(self): + # pylint: disable=protected-access + self.driver._available_services = [f"not_{self.service}"] + self.driver_callback(self.service) + self.mock_executor.run.assert_not_called() + + +class IsEnabledTest(Base): + def test_enabled(self): + output = ( + "\n".join( + [ + "This line is ignored", + " service1", + " abc", + " xyz", + f" {self.service}", + ] + ) + + "\n" + ) + self.mock_executor.run.return_value = ExecutorResult(0, output, "") + self.assertTrue(self.driver.is_enabled(self.service)) + self.mock_executor.run.assert_called_once_with( + [self.rc_config_bin, "list", "default"] + ) + + def test_instance_enabled(self): + output = ( + "\n".join( + [ + "This line is ignored", + " service1", + " abc", + " xyz", + f" {self.service}", + ] + ) + + "\n" + ) + self.mock_executor.run.return_value = ExecutorResult(0, output, "") + self.assertTrue(self.driver.is_enabled(self.service, self.instance)) + self.mock_executor.run.assert_called_once_with( + [self.rc_config_bin, "list", "default"] + ) + + def test_disabled(self): + output = ( + "\n".join( + [ + "This line is ignored", + " service1", + " abc", + " xyz", + ] + ) + + "\n" + ) + self.mock_executor.run.return_value = ExecutorResult(0, output, "") + self.assertFalse(self.driver.is_enabled(self.service)) + self.mock_executor.run.assert_called_once_with( + [self.rc_config_bin, "list", "default"] + ) + + def test_failure(self): + self.mock_executor.run.return_value = ExecutorResult(1, "", "") + self.assertFalse(self.driver.is_enabled(self.service)) + self.mock_executor.run.assert_called_once_with( + [self.rc_config_bin, "list", "default"] + ) + + +class IsRunningTest(Base): + def test_running(self): + self.mock_executor.run.return_value = ExecutorResult( + 0, "is running", "" + ) + self.assertTrue(self.driver.is_running(self.service)) + self.mock_executor.run.assert_called_once_with( + [self.rc_service_bin, self.service, "status"] + ) + + def test_instance_running(self): + self.mock_executor.run.return_value = ExecutorResult( + 0, "is running", "" + ) + self.assertTrue(self.driver.is_running(self.service, self.instance)) + self.mock_executor.run.assert_called_once_with( + [self.rc_service_bin, self.service, "status"] + ) + + def test_not_running(self): + self.mock_executor.run.return_value = ExecutorResult( + 3, "is stopped", "" + ) + self.assertFalse(self.driver.is_running(self.service)) + self.mock_executor.run.assert_called_once_with( + [self.rc_service_bin, self.service, "status"] + ) + + def test_failure(self): + self.mock_executor.run.return_value = ExecutorResult(1, "", "error") + self.assertFalse(self.driver.is_running(self.service)) + self.mock_executor.run.assert_called_once_with( + [self.rc_service_bin, self.service, "status"] + ) + + +class IsInstalledTest(Base): + def test_installed(self): + output = ( + "This line is ignored\n" + " service1 something otherthing\n" + " abc something otherthing\n" + " xyz something otherthing\n" + f" {self.service} something otherthing\n" + ) + self.mock_executor.run.return_value = ExecutorResult(0, output, "") + self.assertTrue(self.driver.is_installed(self.service)) + # Intetionally called twice to make sure that unit files listing is + # done only once + self.assertTrue(self.driver.is_installed(self.service)) + self.mock_executor.run.assert_called_once_with( + [self.rc_config_bin, "list"] + ) + + def test_not_installed(self): + output = ( + "This line is ignored\n" + " service1 something otherthing\n" + " abc something otherthing\n" + " xyz something otherthing\n" + ) + self.mock_executor.run.return_value = ExecutorResult(0, output, "") + self.assertFalse(self.driver.is_installed(self.service)) + # Intetionally called twice to make sure that unit files listing is + # done only once + self.assertFalse(self.driver.is_installed(self.service)) + self.mock_executor.run.assert_called_once_with( + [self.rc_config_bin, "list"] + ) + + +class GetAvailableServicesTest(Base): + def test_success(self): + output = ( + "This line is ignored\n" + " service1 something otherthing\n" + " abc something otherthing\n" + " xyz something otherthing\n" + ) + self.mock_executor.run.return_value = ExecutorResult(0, output, "") + self.assertEqual( + self.driver.get_available_services(), + ["service1", "abc", "xyz"], + ) + self.mock_executor.run.assert_called_once_with( + [self.rc_config_bin, "list"] + ) + + def test_failure(self): + self.mock_executor.run.return_value = ExecutorResult(1, "", "error") + self.assertEqual(self.driver.get_available_services(), []) + self.mock_executor.run.assert_called_once_with( + [self.rc_config_bin, "list"] + )