diff --git a/host_modules/reboot.py b/host_modules/reboot.py new file mode 100644 index 00000000..6f7593c2 --- /dev/null +++ b/host_modules/reboot.py @@ -0,0 +1,187 @@ +"""reboot module which performs reboot""" + +import json +import logging +import threading +import time +import docker +from host_modules import host_service +from utils.run_cmd import _run_command + +MOD_NAME = 'reboot' +# Reboot method in reboot request +# Both enum and string representations are supported +# Define an Enum for Reboot Methods which are defined as in https://github.com/openconfig/gnoi/blob/main/system/system.pb.go#L27 +REBOOT_METHOD_COLD_BOOT_VALUES = {1, "COLD"} +REBOOT_METHOD_HALT_BOOT_VALUES = {3, "HALT"} +REBOOT_METHOD_WARM_BOOT_VALUES = {4, "WARM"} + +# Timeout for SONiC Host Service to be killed during reboot +REBOOT_TIMEOUT = 260 +HALT_TIMEOUT = 60 + +EXECUTE_COLD_REBOOT_COMMAND = "sudo reboot" +EXECUTE_HALT_REBOOT_COMMAND = "sudo reboot -p" +EXECUTE_WARM_REBOOT_COMMAND = "sudo warm-reboot" + +logger = logging.getLogger(__name__) + + +class Reboot(host_service.HostModule): + """DBus endpoint that executes the reboot and returns the reboot status + """ + + def __init__(self, mod_name): + """Use threading.lock mechanism to read/write into response_data + since response_data can be read/write by multiple threads""" + self.lock = threading.Lock() + # reboot_status_flag is used to keep track of reboot status on host + self.reboot_status_flag = {} + # Populating with default value i.e., no active reboot + self.populate_reboot_status_flag() + super(Reboot, self).__init__(mod_name) + + def populate_reboot_status_flag(self, active = False, when = 0, reason = ""): + """Populates the reboot_status_flag with given input params""" + self.lock.acquire() + self.reboot_status_flag["active"] = active + self.reboot_status_flag["when"] = when + self.reboot_status_flag["reason"] = reason + self.lock.release() + return + + def validate_reboot_request(self, reboot_request): + # Check whether reboot method is present. + if "method" not in reboot_request: + return 1, "Reboot request must contain a reboot method" + + # Check whether reboot method is valid. + reboot_method = reboot_request["method"] + valid_reboot_method = REBOOT_METHOD_COLD_BOOT_VALUES | REBOOT_METHOD_HALT_BOOT_VALUES | REBOOT_METHOD_WARM_BOOT_VALUES + if reboot_method not in valid_reboot_method: + return 1, "Unsupported reboot method: " + str(reboot_method) + + # Check whether delay is non-zero. delay key will not exist in reboot_request if it is zero + if "delay" in reboot_request and reboot_request["delay"] != 0: + return 1, "Delayed reboot is not supported" + return 0, "" + + def is_container_running(self, container_name): + """Check if a given container is running using the Docker SDK.""" + try: + client = docker.from_env() + containers = client.containers.list(filters={"name": container_name}) + + for container in containers: + if container.name == container_name and container.status == "running": + return True + return False + except Exception as e: + logger.error("%s: Error checking container status for %s: [%s]", MOD_NAME, container_name, str(e)) + return False + + def execute_reboot(self, reboot_method): + """Executes reboot command based on the reboot_method initialised + and reset reboot_status_flag when reboot fails.""" + + if reboot_method in REBOOT_METHOD_COLD_BOOT_VALUES: + command = EXECUTE_COLD_REBOOT_COMMAND + logger.warning("%s: Issuing cold reboot", MOD_NAME) + elif reboot_method in REBOOT_METHOD_HALT_BOOT_VALUES: + command = EXECUTE_HALT_REBOOT_COMMAND + logger.warning("%s: Issuing halt reboot", MOD_NAME) + elif reboot_method in REBOOT_METHOD_WARM_BOOT_VALUES: + command = EXECUTE_WARM_REBOOT_COMMAND + logger.warning("%s: Issuing WARM reboot", MOD_NAME) + else: + logger.error("%s: Unsupported reboot method: %d", MOD_NAME, reboot_method) + return + + rc, stdout, stderr = _run_command(command) + if rc: + self.populate_reboot_status_flag() + logger.error("%s: Reboot failed execution with stdout: %s, " + "stderr: %s", MOD_NAME, stdout, stderr) + return + + """wait for the reboot to complete. Here, we expect that SONiC Host Service + will be killed during this waiting period if the reboot is successful. If this module + is still alive after the below waiting period, we can conclude that the reboot has failed. + Each container can take up to 20 seconds to get killed. In total, there are 10 containers, + and adding a buffer of 1 minute brings up the delay value. + For Halt reboot_method, wait for 60 secs timeout. we expect pmon, syncd containers are killed, + if Halt reboot is Successful.""" + if reboot_method in REBOOT_METHOD_HALT_BOOT_VALUES: + time.sleep(HALT_TIMEOUT) + is_pmon_running = self.is_container_running("pmon") + if is_pmon_running: + #Halt reboot has failed, as pmon is still running. + logger.error("%s: HALT reboot failed: pmon is still running", MOD_NAME) + self.populate_reboot_status_flag() + return + else: + logger.warning("%s: Pmon conatiner has stopped after Halt reboot execution", MOD_NAME) + + else: + time.sleep(REBOOT_TIMEOUT) + # Conclude that the reboot has failed if we reach this point + self.populate_reboot_status_flag() + return + + @host_service.method(host_service.bus_name(MOD_NAME), in_signature='as', out_signature='is') + + def issue_reboot(self, options): + """Initializes reboot thorugh RPC based on the reboot flag assigned. + Issues reboot after performing the following steps sequentially: + 1. Checks that reboot_status_flag is not set + 2. Validates the reboot request + 3. Sets the reboot_status_flag + 4. Issues the reboot in a separate thread + """ + logger.warning("%s: issue_reboot rpc called", MOD_NAME) + self.lock.acquire() + is_reboot_ongoing = self.reboot_status_flag["active"] + self.lock.release() + # Return without issuing the reboot if the previous reboot is ongoing + if is_reboot_ongoing: + return 1, "Previous reboot is ongoing" + + """Convert input json formatted reboot request into python dict. + reboot_request is a python dict with the following keys: + method - specifies the method of reboot + delay - delay to issue reboot, key exists only if it is non-zero + message - reason for reboot + force - either true/false, key exists only if it is true + """ + try: + reboot_request = json.loads(options[0]) + except ValueError: + return 1, "Failed to parse json formatted reboot request into python dict" + + # Validate reboot request + err, errstr = self.validate_reboot_request(reboot_request) + if err: + return err, errstr + + # Sets reboot_status_flag to be in active state + self.populate_reboot_status_flag(True, int(time.time()), reboot_request["message"]) + + # Issue reboot in a new thread and reset the reboot_status_flag if the reboot fails + try: + t = threading.Thread(target=self.execute_reboot, args=(reboot_request["method"],)) + t.start() + except RuntimeError as error: + return 1, "Failed to start thread to execute reboot with error: " + str(error) + return 0, "Successfully issued reboot" + + @host_service.method(host_service.bus_name(MOD_NAME), in_signature='', out_signature='is') + def get_reboot_status(self): + """Returns current reboot status on host in json format""" + self.lock.acquire() + response_data = json.dumps(self.reboot_status_flag) + self.lock.release() + return 0, response_data + +def register(): + """Return the class name""" + return Reboot, MOD_NAME diff --git a/host_modules/systemd_service.py b/host_modules/systemd_service.py index 974af1e0..86addd72 100644 --- a/host_modules/systemd_service.py +++ b/host_modules/systemd_service.py @@ -1,6 +1,5 @@ """Systemd service handler""" -from enum import Enum from host_modules import host_service import subprocess @@ -8,12 +7,6 @@ ALLOWED_SERVICES = ['snmp', 'swss', 'dhcp_relay', 'radv', 'restapi', 'lldp', 'sshd', 'pmon', 'rsyslog', 'telemetry'] EXIT_FAILURE = 1 -# Define an Enum for Reboot Methods which are defined as in -# https://github.com/openconfig/gnoi/blob/main/system/system.pb.go#L27 -class RebootMethod(Enum): - COLD = 1 - HALT = 3 - class SystemdService(host_service.HostModule): """ DBus endpoint that executes the service command @@ -32,15 +25,15 @@ def restart_service(self, service): msg = result.stderr.decode() if possible_expected_error not in msg: return result.returncode, msg # Throw error only if unexpected error - + msg = '' cmd = ['/usr/bin/systemctl', 'restart', service] result = subprocess.run(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode: msg = result.stderr.decode() - + return result.returncode, msg - + @host_service.method(host_service.bus_name(MOD_NAME), in_signature='s', out_signature='is') def stop_service(self, service): if not service: @@ -54,19 +47,3 @@ def stop_service(self, service): if result.returncode: msg = result.stderr.decode() return result.returncode, msg - - @host_service.method(host_service.bus_name(MOD_NAME), in_signature='i', out_signature='is') - def execute_reboot(self, rebootmethod): - if rebootmethod == RebootMethod.COLD: - cmd = ['/usr/local/bin/reboot'] - elif rebootmethod == RebootMethod.HALT: - cmd = ['/usr/local/bin/reboot','-p'] - else: - return EXIT_FAILURE, "{}: Invalid reboot method: {}".format(MOD_NAME, rebootmethod) - - result = subprocess.run(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - msg = '' - if result.returncode: - msg = result.stderr.decode() - - return result.returncode, msg diff --git a/scripts/sonic-host-server b/scripts/sonic-host-server index 2f203cc8..eec8208f 100755 --- a/scripts/sonic-host-server +++ b/scripts/sonic-host-server @@ -12,7 +12,7 @@ import dbus.service import dbus.mainloop.glib from gi.repository import GObject -from host_modules import config_engine, gcu, host_service, showtech, systemd_service, file_service, image_service, docker_service +from host_modules import config_engine, gcu, host_service, showtech, systemd_service, file_service, image_service, docker_service, reboot def register_dbus(): @@ -21,6 +21,7 @@ def register_dbus(): 'config': config_engine.Config('config'), 'gcu': gcu.GCU('gcu'), 'host_service': host_service.HostService('host_service'), + 'reboot': reboot.Reboot('reboot'), 'showtech': showtech.Showtech('showtech'), 'systemd': systemd_service.SystemdService('systemd'), 'image_service': image_service.ImageService('image_service'), diff --git a/setup.py b/setup.py index 9cb0ff39..f6d23680 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,8 @@ maintainer = 'Joe LeVeque', maintainer_email = 'jolevequ@microsoft.com', packages = [ - 'host_modules' + 'host_modules', + 'utils', ], scripts = [ 'scripts/caclmgrd', diff --git a/tests/host_modules/reboot_test.py b/tests/host_modules/reboot_test.py new file mode 100644 index 00000000..9983a77a --- /dev/null +++ b/tests/host_modules/reboot_test.py @@ -0,0 +1,273 @@ +"""Tests for reboot.""" + +import imp +import sys +import os +import pytest +import datetime + +if sys.version_info >= (3, 3): + from unittest import mock +else: + # Expect the 'mock' package for python 2 + # https://pypi.python.org/pypi/mock + import mock + +test_path = os.path.dirname(os.path.abspath(__file__)) +sonic_host_service_path = os.path.dirname(test_path) +host_modules_path = os.path.join(sonic_host_service_path, "../host_modules") +sys.path.insert(0, sonic_host_service_path) + +TIME = 1617811205 +TEST_ACTIVE_RESPONSE_DATA = "{\"active\": true, \"when\": 1617811205, \"reason\": \"testing reboot response\"}" +TEST_INACTIVE_RESPONSE_DATA = "{\"active\": false, \"when\": 0, \"reason\": \"\"}" + +REBOOT_METHOD_UNKNOWN_ENUM = 0 +REBOOT_METHOD_COLD_BOOT_ENUM = 1 +REBOOT_METHOD_HALT_BOOT_ENUM = 3 +REBOOT_METHOD_WARM_BOOT_ENUM = 4 + +TEST_TIMESTAMP = 1618942253.831912040 + +VALID_REBOOT_REQUEST_COLD = "{\"method\": 1, \"message\": \"test reboot request reason\"}" +VALID_REBOOT_REQUEST_HALT = "{\"method\": 3, \"message\": \"test reboot request reason\"}" +VALID_REBOOT_REQUEST_WARM = "{\"method\": \"WARM\", \"message\": \"test reboot request reason\"}" +INVALID_REBOOT_REQUEST = "\"method\": 1, \"message\": \"test reboot request reason\"" + +imp.load_source("host_service", host_modules_path + "/host_service.py") +imp.load_source("reboot", host_modules_path + "/reboot.py") +from reboot import * + + +class TestReboot(object): + @classmethod + def setup_class(cls): + with mock.patch("reboot.super") as mock_host_module: + cls.reboot_module = Reboot(MOD_NAME) + + def test_populate_reboot_status_flag(self): + with mock.patch("time.time", return_value=1617811205.25): + self.reboot_module.populate_reboot_status_flag() + assert self.reboot_module.reboot_status_flag["active"] == False + assert self.reboot_module.reboot_status_flag["when"] == 0 + assert self.reboot_module.reboot_status_flag["reason"] == "" + + def test_validate_reboot_request_success_cold_boot_enum_method(self): + reboot_request = {"method": REBOOT_METHOD_COLD_BOOT_ENUM, "reason": "test reboot request reason"} + result = self.reboot_module.validate_reboot_request(reboot_request) + assert result[0] == 0 + assert result[1] == "" + + def test_validate_reboot_request_success_cold_boot_string_method(self): + reboot_request = {"method": "COLD", "reason": "test reboot request reason"} + result = self.reboot_module.validate_reboot_request(reboot_request) + assert result[0] == 0 + assert result[1] == "" + + def test_validate_reboot_request_success_halt_boot_enum_method(self): + reboot_request = {"method": REBOOT_METHOD_HALT_BOOT_ENUM, "reason": "test reboot request reason"} + result = self.reboot_module.validate_reboot_request(reboot_request) + assert result[0] == 0 + assert result[1] == "" + + def test_validate_reboot_request_success_halt_boot_string_method(self): + reboot_request = {"method": "HALT", "reason": "test reboot request reason"} + result = self.reboot_module.validate_reboot_request(reboot_request) + assert result[0] == 0 + assert result[1] == "" + + def test_validate_reboot_request_success_warm_enum_method(self): + reboot_request = {"method": REBOOT_METHOD_WARM_BOOT_ENUM, "reason": "test reboot request reason"} + result = self.reboot_module.validate_reboot_request(reboot_request) + assert result[0] == 0 + assert result[1] == "" + + def test_validate_reboot_request_success_WARM_enum_method(self): + reboot_request = {"method": "WARM", "reason": "test reboot request reason"} + result = self.reboot_module.validate_reboot_request(reboot_request) + assert result[0] == 0 + assert result[1] == "" + + def test_validate_reboot_request_fail_unknown_method(self): + reboot_request = {"method": 0, "reason": "test reboot request reason"} + result = self.reboot_module.validate_reboot_request(reboot_request) + assert result[0] == 1 + assert result[1] == "Unsupported reboot method: 0" + + def test_validate_reboot_request_fail_no_method(self): + reboot_request = {"reason": "test reboot request reason"} + result = self.reboot_module.validate_reboot_request(reboot_request) + assert result[0] == 1 + assert result[1] == "Reboot request must contain a reboot method" + + def test_validate_reboot_request_fail_delayed_reboot(self): + reboot_request = {"method": REBOOT_METHOD_COLD_BOOT_ENUM, "delay": 10, "reason": "test reboot request reason"} + result = self.reboot_module.validate_reboot_request(reboot_request) + assert result[0] == 1 + assert result[1] == "Delayed reboot is not supported" + + def test_execute_reboot_success(self): + with ( + mock.patch("reboot._run_command") as mock_run_command, + mock.patch("time.sleep") as mock_sleep, + mock.patch("reboot.Reboot.populate_reboot_status_flag") as mock_populate_reboot_status_flag, + ): + mock_run_command.return_value = (0, ["stdout: execute WARM reboot"], ["stderror: execute WARM reboot"]) + self.reboot_module.execute_reboot("WARM") + mock_run_command.assert_called_once_with("sudo warm-reboot") + mock_sleep.assert_called_once_with(260) + mock_populate_reboot_status_flag.assert_called_once_with() + + def test_execute_reboot_fail_unknown_reboot(self, caplog): + with caplog.at_level(logging.ERROR): + self.reboot_module.execute_reboot(-1) + msg = "reboot: Unsupported reboot method: -1" + assert caplog.records[0].message == msg + + def test_execute_reboot_fail_issue_reboot_command_cold_boot(self, caplog): + with ( + mock.patch("reboot._run_command") as mock_run_command, + mock.patch("reboot.Reboot.populate_reboot_status_flag") as mock_populate_reboot_status_flag, + caplog.at_level(logging.ERROR), + ): + mock_run_command.return_value = (1, ["stdout: execute cold reboot"], ["stderror: execute cold reboot"]) + self.reboot_module.execute_reboot(REBOOT_METHOD_COLD_BOOT_ENUM) + msg = ("reboot: Reboot failed execution with " + "stdout: ['stdout: execute cold reboot'], stderr: " + "['stderror: execute cold reboot']") + assert caplog.records[0].message == msg + mock_populate_reboot_status_flag.assert_called_once_with() + + def test_execute_reboot_fail_issue_reboot_command_halt(self, caplog): + with ( + mock.patch("reboot._run_command") as mock_run_command, + mock.patch("reboot.Reboot.populate_reboot_status_flag") as mock_populate_reboot_status_flag, + caplog.at_level(logging.ERROR), + ): + mock_run_command.return_value = (1, ["stdout: execute halt reboot"], ["stderror: execute halt reboot"]) + self.reboot_module.execute_reboot(REBOOT_METHOD_HALT_BOOT_ENUM) + msg = ("reboot: Reboot failed execution with " + "stdout: ['stdout: execute halt reboot'], stderr: " + "['stderror: execute halt reboot']") + assert caplog.records[0].message == msg + mock_populate_reboot_status_flag.assert_called_once_with() + + def test_execute_reboot_fail_issue_reboot_command_warm(self, caplog): + with ( + mock.patch("reboot._run_command") as mock_run_command, + mock.patch("reboot.Reboot.populate_reboot_status_flag") as mock_populate_reboot_status_flag, + caplog.at_level(logging.ERROR), + ): + mock_run_command.return_value = (1, ["stdout: execute WARM reboot"], ["stderror: execute WARM reboot"]) + self.reboot_module.execute_reboot("WARM") + msg = ("reboot: Reboot failed execution with " + "stdout: ['stdout: execute WARM reboot'], stderr: " + "['stderror: execute WARM reboot']") + assert caplog.records[0].message == msg + mock_populate_reboot_status_flag.assert_called_once_with() + + def test_issue_reboot_success_cold_boot(self): + with ( + mock.patch("threading.Thread") as mock_thread, + mock.patch("reboot.Reboot.validate_reboot_request", return_value=(0, "")), + ): + self.reboot_module.populate_reboot_status_flag() + result = self.reboot_module.issue_reboot([VALID_REBOOT_REQUEST_COLD]) + assert result[0] == 0 + assert result[1] == "Successfully issued reboot" + mock_thread.assert_called_once_with( + target=self.reboot_module.execute_reboot, + args=(REBOOT_METHOD_COLD_BOOT_ENUM,), + ) + mock_thread.return_value.start.assert_called_once_with() + + def test_issue_reboot_success_halt(self): + with ( + mock.patch("threading.Thread") as mock_thread, + mock.patch("reboot.Reboot.validate_reboot_request", return_value=(0, "")), + ): + self.reboot_module.populate_reboot_status_flag() + result = self.reboot_module.issue_reboot([VALID_REBOOT_REQUEST_HALT]) + assert result[0] == 0 + assert result[1] == "Successfully issued reboot" + mock_thread.assert_called_once_with( + target=self.reboot_module.execute_reboot, + args=(REBOOT_METHOD_HALT_BOOT_ENUM,), + ) + mock_thread.return_value.start.assert_called_once_with() + + def test_issue_reboot_success_warm(self): + with ( + mock.patch("threading.Thread") as mock_thread, + mock.patch("reboot.Reboot.validate_reboot_request", return_value=(0, "")), + ): + self.reboot_module.populate_reboot_status_flag() + result = self.reboot_module.issue_reboot([VALID_REBOOT_REQUEST_WARM]) + assert result[0] == 0 + assert result[1] == "Successfully issued reboot" + mock_thread.assert_called_once_with( + target=self.reboot_module.execute_reboot, + args=("WARM",), + ) + mock_thread.return_value.start.assert_called_once_with() + + def test_issue_reboot_previous_reboot_ongoing(self): + self.reboot_module.populate_reboot_status_flag() + self.reboot_module.reboot_status_flag["active"] = True + result = self.reboot_module.issue_reboot([VALID_REBOOT_REQUEST_COLD]) + assert result[0] == 1 + assert result[1] == "Previous reboot is ongoing" + + def test_issue_reboot_bad_format_reboot_request(self): + self.reboot_module.populate_reboot_status_flag() + result = self.reboot_module.issue_reboot([INVALID_REBOOT_REQUEST]) + assert result[0] == 1 + assert result[1] == "Failed to parse json formatted reboot request into python dict" + + def test_issue_reboot_invalid_reboot_request(self): + with mock.patch("reboot.Reboot.validate_reboot_request", return_value=(1, "failed to validate reboot request")): + self.reboot_module.populate_reboot_status_flag() + result = self.reboot_module.issue_reboot([VALID_REBOOT_REQUEST_COLD]) + assert result[0] == 1 + assert result[1] == "failed to validate reboot request" + + def raise_runtime_exception_test(self): + raise RuntimeError('test raise RuntimeError exception') + + def test_issue_reboot_fail_issue_reboot_thread(self): + with mock.patch("threading.Thread") as mock_thread: + mock_thread.return_value.start = self.raise_runtime_exception_test + self.reboot_module.populate_reboot_status_flag() + result = self.reboot_module.issue_reboot([VALID_REBOOT_REQUEST_COLD]) + assert result[0] == 1 + assert result[1] == "Failed to start thread to execute reboot with error: test raise RuntimeError exception" + + def test_get_reboot_status_active(self): + MSG="testing reboot response" + self.reboot_module.populate_reboot_status_flag(True, TIME, MSG) + result = self.reboot_module.get_reboot_status() + assert result[0] == 0 + response_data = json.loads(result[1]) + assert response_data["active"] == True + assert response_data["when"] == TIME + assert response_data["reason"] == MSG + + def test_get_reboot_status_inactive(self): + self.reboot_module.populate_reboot_status_flag(False, 0, "") + result = self.reboot_module.get_reboot_status() + assert result[0] == 0 + response_data = json.loads(result[1]) + assert response_data["active"] == False + assert response_data["when"] == 0 + assert response_data["reason"] == "" + +# assert result[1] == TEST_INACTIVE_RESPONSE_DATA + + def test_register(self): + result = register() + assert result[0] == Reboot + assert result[1] == MOD_NAME + + @classmethod + def teardown_class(cls): + print("TEARDOWN") diff --git a/tests/host_modules/systemd_service_test.py b/tests/host_modules/systemd_service_test.py index 54961498..b713dbb6 100644 --- a/tests/host_modules/systemd_service_test.py +++ b/tests/host_modules/systemd_service_test.py @@ -24,7 +24,7 @@ def test_service_restart_valid(self, MockInit, MockBusName, MockSystemBus): assert "/usr/bin/systemctl" in call_args assert ret == test_ret, "Return value is wrong" assert msg == "", "Return message is wrong" - + @mock.patch("dbus.SystemBus") @mock.patch("dbus.service.BusName") @mock.patch("dbus.service.Object.__init__") @@ -34,7 +34,7 @@ def test_service_restart_invalid(self, MockInit, MockBusName, MockSystemBus): ret, msg = systemd_service_stub.restart_service(service) assert ret == 1 assert "Dbus does not support" in msg - + @mock.patch("dbus.SystemBus") @mock.patch("dbus.service.BusName") @mock.patch("dbus.service.Object.__init__") @@ -44,7 +44,7 @@ def test_service_restart_empty(self, MockInit, MockBusName, MockSystemBus): ret, msg = systemd_service_stub.restart_service(service) assert ret == 1 assert "restart_service called with no service specified" in msg - + @mock.patch("dbus.SystemBus") @mock.patch("dbus.service.BusName") @mock.patch("dbus.service.Object.__init__") @@ -84,57 +84,3 @@ def test_service_stop_empty(self, MockInit, MockBusName, MockSystemBus): ret, msg = systemd_service_stub.stop_service(service) assert ret == 1 assert "stop_service called with no service specified" in msg - - @mock.patch("dbus.SystemBus") - @mock.patch("dbus.service.BusName") - @mock.patch("dbus.service.Object.__init__") - def test_execute_reboot_cold(self, MockInit, MockBusName, MockSystemBus): - # Mock subprocess.run - with mock.patch("subprocess.run") as mock_run: - # Mock the result of subprocess.run - res_mock = mock.Mock() - test_ret = 0 - test_msg = b"Succeeded" - res_mock.configure_mock(returncode=test_ret, stderr=test_msg) - mock_run.return_value = res_mock - - method = systemd_service.RebootMethod.COLD - systemd_service_stub = systemd_service.SystemdService(systemd_service.MOD_NAME) - - # Execute the reboot method - ret, msg = systemd_service_stub.execute_reboot(method) - - # Assert the correct command was called - call_args = mock_run.call_args[0][0] - assert "/usr/local/bin/reboot" in call_args, f"Expected 'reboot' command, but got: {call_args}" - - # Assert the return values are correct - assert ret == test_ret, f"Expected return code {test_ret}, got {ret}" - assert msg == "", f"Expected return message '', got {msg}" - - @mock.patch("dbus.SystemBus") - @mock.patch("dbus.service.BusName") - @mock.patch("dbus.service.Object.__init__") - def test_execute_reboot_halt(self, MockInit, MockBusName, MockSystemBus): - # Mock subprocess.run - with mock.patch("subprocess.run") as mock_run: - # Mock the result of subprocess.run - res_mock = mock.Mock() - test_ret = 0 - test_msg = b"Succeeded" - res_mock.configure_mock(returncode=test_ret, stderr=test_msg) - mock_run.return_value = res_mock - - method = systemd_service.RebootMethod.HALT - systemd_service_stub = systemd_service.SystemdService(systemd_service.MOD_NAME) - - # Execute the reboot method - ret, msg = systemd_service_stub.execute_reboot(method) - - # Assert the correct command was called - call_args = mock_run.call_args[0][0] - assert "/usr/local/bin/reboot" in call_args, f"Expected 'reboot' command, but got: {call_args}" - - # Assert the return values are correct - assert ret == test_ret, f"Expected return code {test_ret}, got {ret}" - assert msg == "", f"Expected return message '', got {msg}" diff --git a/utils/run_cmd.py b/utils/run_cmd.py new file mode 100644 index 00000000..781c822a --- /dev/null +++ b/utils/run_cmd.py @@ -0,0 +1,34 @@ +import logging +import shlex +import subprocess + +logger = logging.getLogger(__name__) + + +def _run_command(cmd): + '''! + Execute a given command + + @param cmd (str) Command to execute. Since we execute the command directly, and not within the + context of the shell, the full path needs to be provided ($PATH is not used). + Command parameters are simply separated by a space. + Should be either string or a list. + ''' + try: + if not cmd: + return (0, None, None) + shcmd = shlex.split(cmd) + proc = subprocess.Popen(shcmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1, close_fds=True) + output_stdout, output_stderr = proc.communicate() + list_stdout = [] + for l in output_stdout.splitlines(): + list_stdout.append(str(l.decode())) + list_stderr = [] + for l in output_stderr.splitlines(): + list_stderr.append(str(l.decode())) + return (proc.returncode, list_stdout, list_stderr) + except (OSError, ValueError) as e: + logging.error( + "!Exception [%s] encountered while processing the command : %s", + str(e), str(cmd)) + return (1, None, None)