Repository: cobbler/cobbler

Issue:
cobbler sync error - supervisord
### Describe the bug

Using Cobbler on a distro with supervisord and with `manage_dhcp` enabled, `cobbler sync` errors:

```
Exception occurred: <class 'cobbler.cexceptions.CX'>
Exception value: 'Cobbler trigger failed: cobbler.modules.sync_post_restart_services'
Exception Info:
!!! TASK FAILED !!!
```
and log shows stack trace:
```
[2022-02-01_153306_sync] 2022-02-01T15:33:08 - INFO | Exception occurred: <class 'cobbler.cexceptions.CX'>
[2022-02-01_153306_sync] 2022-02-01T15:33:08 - INFO | Exception value: 'Cobbler trigger failed: cobbler.modules.sync_post_restart_services'
[2022-02-01_153306_sync] 2022-02-01T15:33:08 - INFO | Exception Info:
  File "/usr/lib/python3.6/site-packages/cobbler/remote.py", line 99, in run
    rc = self._run(self)

  File "/usr/lib/python3.6/site-packages/cobbler/remote.py", line 218, in runner
    self.remote.api.sync(self.options.get("verbose", False), what=what)

  File "/usr/lib/python3.6/site-packages/cobbler/api.py", line 1406, in sync
    sync_obj.run()

  File "/usr/lib/python3.6/site-packages/cobbler/actions/sync.py", line 179, in run
    utils.run_triggers(self.api, None, "/var/lib/cobbler/triggers/sync/post/*")

  File "/usr/lib/python3.6/site-packages/cobbler/utils.py", line 983, in run_triggers
    raise CX("Cobbler trigger failed: %s" % m.__name__)

[2022-02-01_153306_sync] 2022-02-01T15:33:08 - ERROR | ### TASK FAILED ###
```

This is because in #2841 @th-2021 set [no return code](https://github.com/cobbler/cobbler/blob/a783fd652254ff8613ec347759bb738c35d51127/cobbler/utils.py#L258) for the supervisord case.  [ret](https://github.com/cobbler/cobbler/blob/a783fd652254ff8613ec347759bb738c35d51127/cobbler/utils.py#L270) is `None`

### Cobbler version

v3.3.1 (latest master)


Discussion:

Resolving patch:
diff --git a/cobbler/utils.py b/cobbler/utils.py
--- a/cobbler/utils.py
+++ b/cobbler/utils.py
@@ -36,6 +36,7 @@
 import urllib.error
 import urllib.parse
 import urllib.request
+import xmlrpc.client
 from functools import reduce
 from pathlib import Path
 from typing import List, Optional, Pattern, Union
@@ -253,20 +254,24 @@ def service_restart(service_name: str):
     """
     if is_supervisord():
         with ServerProxy('http://localhost:9001/RPC2') as server:
-            server.supervisor.stopProcess(service_name)
-            server.supervisor.startProcess(service_name)
-        return
+            try:
+                server.supervisor.stopProcess(service_name)
+                if server.supervisor.startProcess(service_name):  # returns a boolean
+                    return 0
+            except xmlrpc.client.Fault as clientFault:
+                logger.error('Restarting service "%s" failed', service_name, exc_info=clientFault)
+                return 1
     elif is_systemd():
-        restart_command = "systemctl restart " + service_name
+        restart_command = "systemctl restart %s" % service_name
     elif is_service():
-        restart_command = "service " + service_name + " restart"
+        restart_command = "service %s restart" % service_name
     else:
         logger.warning('We could not restart service "%s" due to an unsupported process manager!', service_name)
-        return
+        return 1
 
     ret = subprocess_call(restart_command, True)
     if ret != 0:
-        logger.error("%s service failed", service_name)
+        logger.error('Restarting service "%s" failed', service_name)
     return ret
 
 


Test patch:
diff --git a/tests/utils_test.py b/tests/utils_test.py
--- a/tests/utils_test.py
+++ b/tests/utils_test.py
@@ -10,7 +10,6 @@
 from cobbler.api import CobblerAPI
 from cobbler.cexceptions import CX
 from cobbler.items.distro import Distro
-from cobbler.items.profile import Profile
 from cobbler.settings import Settings
 from tests.conftest import does_not_raise
 
@@ -918,3 +917,100 @@ def test_kopts_overwrite():
     # Assert
     assert "textmode" in kopts
     assert "info" in kopts
+
+
+def test_service_restart_no_manager(mocker):
+    # Arrange
+    mocker.patch(
+        "cobbler.utils.is_supervisord",
+        autospec=True,
+        return_value=False
+    )
+    mocker.patch(
+        "cobbler.utils.is_systemd",
+        autospec=True,
+        return_value=False
+    )
+    mocker.patch(
+        "cobbler.utils.is_service",
+        autospec=True,
+        return_value=False
+    )
+
+    # Act
+    result = utils.service_restart("testservice")
+
+    # Assert
+    assert result == 1
+
+
+def test_service_restart_supervisord(mocker):
+    mocker.patch(
+        "cobbler.utils.is_supervisord",
+        autospec=True,
+        return_value=True
+    )
+    # TODO Mock supervisor API and return value
+
+    # Act
+    result = utils.service_restart("dhcpd")
+
+    # Assert
+    # Currently we check for 1 because dhcpd is not running.
+    assert result == 1
+
+
+def test_service_restart_systemctl(mocker):
+    mocker.patch(
+        "cobbler.utils.is_supervisord",
+        autospec=True,
+        return_value=False
+    )
+    mocker.patch(
+        "cobbler.utils.is_systemd",
+        autospec=True,
+        return_value=True
+    )
+    mocker.patch(
+        "cobbler.utils.subprocess_call",
+        autospec=True,
+        return_value=0
+    )
+
+    # Act
+    result = utils.service_restart("testservice")
+
+    # Assert
+    assert result == 0
+    utils.subprocess_call.assert_called_with("systemctl restart testservice", True)
+
+
+def test_service_restart_service(mocker):
+    # Arrange
+    mocker.patch(
+        "cobbler.utils.is_supervisord",
+        autospec=True,
+        return_value=False
+    )
+    mocker.patch(
+        "cobbler.utils.is_systemd",
+        autospec=True,
+        return_value=False
+    )
+    mocker.patch(
+        "cobbler.utils.is_service",
+        autospec=True,
+        return_value=True
+    )
+    mocker.patch(
+        "cobbler.utils.subprocess_call",
+        autospec=True,
+        return_value=0
+    )
+
+    # Act
+    result = utils.service_restart("testservice")
+
+    # Assert
+    assert result == 0
+    utils.subprocess_call.assert_called_with("service testservice restart", True)