-
Notifications
You must be signed in to change notification settings - Fork 135
[gNOI] Add support for gNOI Factory Reset #254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| """gNOI reset module which performs factory reset.""" | ||
|
|
||
| import json | ||
| import logging | ||
| import threading | ||
| import time | ||
| from host_modules import host_service | ||
| from host_modules.reboot import Reboot | ||
|
|
||
| MOD_NAME = "gnoi_reset" | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class GnoiReset(host_service.HostModule): | ||
| """DBus endpoint that executes the factory reset and returns the reset status and response.""" | ||
|
|
||
| def __init__(self, mod_name): | ||
| self.lock = threading.Lock() | ||
| self.is_reset_ongoing = False | ||
| self.reset_request = {} | ||
| self.reset_response = {} | ||
| super(GnoiReset, self).__init__(mod_name) | ||
|
|
||
| def populate_reset_response( | ||
| self, | ||
| reset_success=True, | ||
| factory_os_unsupported=False, | ||
| zero_fill_unsupported=False, | ||
| detail="", | ||
| ) -> tuple[int, str]: | ||
| """Populate the factory reset response. | ||
| """ | ||
| with self.lock: | ||
| self.reset_response = {} | ||
| response = {} | ||
| if reset_success: | ||
| self.reset_response["reset_success"] = {} | ||
| response["reset_success"] = {} | ||
| else: | ||
| self.reset_response["reset_error"] = {} | ||
| response["reset_error"] = {} | ||
| if factory_os_unsupported: | ||
| self.reset_response["reset_error"]["factory_os_unsupported"] = True | ||
| elif zero_fill_unsupported: | ||
| self.reset_response["reset_error"]["zero_fill_unsupported"] = True | ||
| else: | ||
| self.reset_response["reset_error"]["other"] = True | ||
| response["reset_error"]["detail"] = detail | ||
| response_data = json.dumps(response) | ||
| return 0, response_data | ||
|
|
||
| def _check_reboot_in_progress(self) -> int: | ||
| """Checks if reboot is already in progress.""" | ||
| if self.is_reset_ongoing: | ||
| return 1 | ||
| else: | ||
| return 0 | ||
|
|
||
| def _parse_arguments(self, options) -> tuple[int, str]: | ||
| """Parses and validates the given arguments into a reset request.""" | ||
| try: | ||
| raw = json.loads(options) | ||
| except ValueError as e: | ||
| logger.error("[%s]:Failed to parse factory reset request: %s", MOD_NAME, str(e)) | ||
| return self.populate_reset_response( | ||
| reset_success=False, | ||
| detail="Failed to parse json formatted factory reset request into python dict.", | ||
| ) | ||
|
|
||
| # Normalize: support both camelCase and snake_case | ||
| self.reset_request = { | ||
| "factoryOs": raw.get("factoryOs", raw.get("factory_os", False)), | ||
| "zeroFill": raw.get("zeroFill", raw.get("zero_fill", False)), | ||
| "retainCerts": raw.get("retainCerts", raw.get("retain_certs", False)), | ||
| } | ||
|
|
||
| # Reject the request if zero_fill is set. | ||
| if self.reset_request["factoryOs"] and self.reset_request["zeroFill"]: | ||
| return self.populate_reset_response( | ||
| reset_success=False, | ||
| zero_fill_unsupported=True, | ||
| detail="zero_fill operation is currently unsupported.", | ||
| ) | ||
| # Issue a warning if retain_certs is set. | ||
| if self.reset_request["factoryOs"] and self.reset_request["retainCerts"]: | ||
| logger.warning("%s: retain_certs is currently ignored.", MOD_NAME) | ||
| return self.populate_reset_response( | ||
| reset_success=False, | ||
| detail="Method FactoryReset.Start is currently unsupported." | ||
| ) | ||
| # Reject the request if factoryOs is set. As the method is currently unsupported | ||
| if self.reset_request["factoryOs"]: | ||
| return self.populate_reset_response( | ||
| reset_success=False, | ||
| detail="Method FactoryReset.Start is currently unsupported." | ||
| ) | ||
|
|
||
| # Default fallback if no valid options triggered any action | ||
| return self.populate_reset_response( | ||
| reset_success=False, | ||
| detail="Method FactoryReset.Start is currently unsupported." | ||
| ) | ||
|
|
||
| def _execute_reboot(self) -> int: | ||
| try: | ||
| r = Reboot("reboot") | ||
| t = threading.Thread(target=r.execute_reboot, args=("COLD",)) | ||
| t.start() | ||
| except RuntimeError: | ||
| self.is_reset_ongoing = False | ||
| return 1 | ||
|
|
||
| return 0 | ||
|
|
||
| @host_service.method( | ||
| host_service.bus_name(MOD_NAME), in_signature="as", out_signature="is" | ||
| ) | ||
|
|
||
| def issue_reset(self, options) -> tuple[int, str]: | ||
| """Issues the factory reset.""" | ||
| print("Issuing reset from back end") | ||
|
|
||
| rc, resp = self._parse_arguments(options) | ||
| if not rc: | ||
| return rc, resp | ||
|
|
||
| rc = self._check_reboot_in_progress() | ||
| if rc: | ||
| return self.populate_reset_response(reset_success=False, detail="Previous reset is ongoing.") | ||
|
|
||
| self.is_reset_ongoing = True | ||
|
|
||
| rc, resp = self._execute_reboot() | ||
| if rc: | ||
| return self.populate_reset_response(reset_success=False,detail="Failed to start thread to execute reboot.") | ||
|
|
||
| # Default fallback if no valid options triggered any action | ||
| return self.populate_reset_response( | ||
| reset_success=False, | ||
| detail="Method FactoryReset.Start is currently unsupported." | ||
| ) | ||
|
|
||
|
|
||
| def register(): | ||
| """Return the class name""" | ||
| return GnoiReset, MOD_NAME | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| """Tests for gnoi_reset.""" | ||
|
|
||
| import imp | ||
| import os | ||
| import sys | ||
| import logging | ||
|
|
||
| if sys.version_info >= (3, 3): | ||
| from unittest import mock | ||
| else: | ||
| 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) | ||
|
|
||
| # Input requests based on gNOI CLI examples (snake_case and camelCase mix) | ||
| VALID_RESET_REQUEST = '{"factory_os": true}' | ||
| ZERO_FILL_REQUEST = '{"factory_os": true, "zero_fill": true}' | ||
| RETAIN_CERTS_REQUEST = '{"factory_os": true, "retainCerts": true}' | ||
| INVALID_RESET_REQUEST = '"factory_os": true, "zero_fill": true' | ||
|
|
||
| imp.load_source("host_service", host_modules_path + "/host_service.py") | ||
| imp.load_source("gnoi_reset", host_modules_path + "/gnoi_reset.py") | ||
| from gnoi_reset import * | ||
|
|
||
|
|
||
| class TestGnoiReset: | ||
| @classmethod | ||
| def setup_class(cls): | ||
| with mock.patch("gnoi_reset.super"): | ||
| cls.gnoi_reset_module = GnoiReset(MOD_NAME) | ||
|
|
||
| def test_zero_fill_unsupported(self): | ||
| result = self.gnoi_reset_module.issue_reset(ZERO_FILL_REQUEST) | ||
| assert result[0] == 0 | ||
| assert result[1] == ( | ||
| '{"reset_error": {"detail": "zero_fill operation is currently unsupported."}}' | ||
| ) | ||
|
|
||
| def test_retain_certs_warning(self, caplog): | ||
| with caplog.at_level(logging.WARNING): | ||
| result = self.gnoi_reset_module.issue_reset(RETAIN_CERTS_REQUEST) | ||
| assert ( | ||
| caplog.records[0].message | ||
| == "gnoi_reset: retain_certs is currently ignored." | ||
| ) | ||
| assert result[0] == 0 | ||
| assert result[1] == ( | ||
| '{"reset_error": {"detail": "Method FactoryReset.Start is currently unsupported."}}' | ||
| ) | ||
|
|
||
| def test_invalid_json_format(self): | ||
| result = self.gnoi_reset_module.issue_reset(INVALID_RESET_REQUEST) | ||
| assert result[0] == 0 | ||
| assert result[1] == ( | ||
| '{"reset_error": {"detail": "Failed to parse json formatted factory reset request into python dict."}}' | ||
| ) | ||
|
|
||
| def test_valid_request_unimplemented(self): | ||
| result = self.gnoi_reset_module.issue_reset(VALID_RESET_REQUEST) | ||
| assert result[0] == 0 | ||
| assert result[1] == ( | ||
| '{"reset_error": {"detail": "Method FactoryReset.Start is currently unsupported."}}' | ||
| ) | ||
|
|
||
| def test_populate_reset_response_success(self): | ||
| _, response = self.gnoi_reset_module.populate_reset_response(reset_success=True) | ||
| assert response == '{"reset_success": {}}' | ||
|
|
||
| def test_populate_reset_response_other_error(self): | ||
| _, response = self.gnoi_reset_module.populate_reset_response( | ||
| reset_success=False, | ||
| detail="Generic failure." | ||
| ) | ||
| assert response == ( | ||
| '{"reset_error": {"detail": "Generic failure."}}' | ||
| ) | ||
|
|
||
| def test_check_reboot_in_progress_true(self): | ||
| self.gnoi_reset_module.is_reset_ongoing = True | ||
| rc = self.gnoi_reset_module._check_reboot_in_progress() | ||
| assert rc == 1 | ||
|
|
||
| def test_check_reboot_in_progress_false(self): | ||
| self.gnoi_reset_module.is_reset_ongoing = False | ||
| rc = self.gnoi_reset_module._check_reboot_in_progress() | ||
| assert rc == 0 | ||
|
|
||
| @mock.patch("gnoi_reset.Reboot") | ||
| @mock.patch("threading.Thread") | ||
| def test_execute_reboot_success(self, mock_thread_cls, mock_reboot_cls): | ||
| mock_reboot_instance = mock.Mock() | ||
| mock_reboot_cls.return_value = mock_reboot_instance | ||
|
|
||
| mock_thread_instance = mock.Mock() | ||
| mock_thread_cls.return_value = mock_thread_instance | ||
|
|
||
| rc = self.gnoi_reset_module._execute_reboot() | ||
|
|
||
| mock_reboot_cls.assert_called_once_with("reboot") | ||
| mock_thread_cls.assert_called_once_with( | ||
| target=mock_reboot_instance.execute_reboot, | ||
| args=("COLD",) | ||
| ) | ||
| mock_thread_instance.start.assert_called_once() | ||
| assert rc == 0 | ||
|
|
||
| @mock.patch("gnoi_reset.Reboot") | ||
| @mock.patch("threading.Thread", side_effect=RuntimeError) | ||
| def test_execute_reboot_runtime_error(self, mock_thread_cls, mock_reboot_cls): | ||
| self.gnoi_reset_module.is_reset_ongoing = True | ||
| rc = self.gnoi_reset_module._execute_reboot() | ||
| assert rc == 1 | ||
| assert self.gnoi_reset_module.is_reset_ongoing is False | ||
|
|
||
| def test_register(self): | ||
| result = register() | ||
| assert result[0] == GnoiReset | ||
| assert result[1] == MOD_NAME | ||
|
|
||
| @classmethod | ||
| def teardown_class(cls): | ||
| print("TEARDOWN") |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.