diff --git a/providers/base/bin/wifi_nmcli_test.py b/providers/base/bin/wifi_nmcli_test.py index 61b4fb72c1..84f14e5615 100755 --- a/providers/base/bin/wifi_nmcli_test.py +++ b/providers/base/bin/wifi_nmcli_test.py @@ -18,12 +18,17 @@ import subprocess as sp import sys import shlex +from pathlib import Path +import shutil +import glob +import tempfile from packaging import version as version_parser from checkbox_support.helpers.retry import retry from gateway_ping_test import ping +NETPLAN_DIR = "/lib/netplan" print = functools.partial(print, flush=True) @@ -386,8 +391,75 @@ def parser_args(): return args +def backup_netplan_files(backup_dir: str, netplan_dir: str): + """ + Backup netplan YAML files from /etc/netplan/ to a + temporary directory, if there are. + """ + + # Find all netplan YAML files + yaml_files = glob.glob(os.path.join(netplan_dir, "*.yaml")) + + if not yaml_files: + print("No netplan YAML files found") + return + + # Create temporary directory + Path(backup_dir).mkdir(parents=True, exist_ok=True) + + # Copy each file to temp directory + for yaml_file in yaml_files: + filename = os.path.basename(yaml_file) + temp_path = os.path.join(backup_dir, filename) + shutil.copy2(yaml_file, temp_path) + # Then copy ownership + st = os.stat(yaml_file) + os.chown(temp_path, st.st_uid, st.st_gid) + print("Backed up: {} -> {}".format(yaml_file, temp_path)) + + print("Netplan files backed up to: {}", backup_dir) + + +def restore_netplan_files(backup_dir: str, netplan_dir: str): + """ + Restore netplan YAML files from backup directory to /etc/netplan/. + + Returns: + bool: True if restoration successful, False otherwise + """ + if not backup_dir or not os.path.exists(backup_dir): + print("Backup directory does not exist: {}".format(netplan_dir)) + return + + # Clean up existing netplan files first + existing_files = glob.glob(os.path.join(netplan_dir, "*.yaml")) + for existing_file in existing_files: + os.remove(existing_file) + print("Removed: {}".format(existing_file)) + + # Find all YAML files in backup directory + backup_files = glob.glob(os.path.join(backup_dir, "*.yaml")) + + if not backup_files: + print("No netplan files found in backup directory") + return + + # Restore each file + for backup_file in backup_files: + filename = os.path.basename(backup_file) + target_path = os.path.join(netplan_dir, filename) + shutil.copy2(backup_file, target_path) + # Then copy ownership + st = os.stat(backup_file) + os.chown(target_path, st.st_uid, st.st_gid) + print("Restored: {} -> {}".format(backup_file, target_path)) + + print("Netplan files restored successfully") + return + + @retry(max_attempts=5, delay=60) -def main(): +def run(): args = parser_args() start_time = datetime.datetime.now() device_rescan() @@ -425,5 +497,19 @@ def main(): delete_test_ap_ssid_connection() +def main(): + + # backup the netplans, because nmcli corrupts them + # and debsums will complain afterwards + # This is ugly. Ideally, nmcli should be patched instead + temp_dir = tempfile.TemporaryDirectory() + backup_netplan_files(str(temp_dir.name), NETPLAN_DIR) + + try: + run() + finally: + restore_netplan_files(str(temp_dir.name), NETPLAN_DIR) + + if __name__ == "__main__": sys.exit(main()) diff --git a/providers/base/tests/test_wifi_client_test_netplan.py b/providers/base/tests/test_wifi_client_test_netplan.py index 7a07c8c5f4..1414cb72c6 100644 --- a/providers/base/tests/test_wifi_client_test_netplan.py +++ b/providers/base/tests/test_wifi_client_test_netplan.py @@ -24,6 +24,7 @@ import io import sys from unittest.mock import call + from wifi_client_test_netplan import ( netplan_renderer, check_and_get_renderer, diff --git a/providers/base/tests/test_wifi_nmcli_test.py b/providers/base/tests/test_wifi_nmcli_test.py index de860d5ce9..4a58f8fa25 100644 --- a/providers/base/tests/test_wifi_nmcli_test.py +++ b/providers/base/tests/test_wifi_nmcli_test.py @@ -20,6 +20,9 @@ import unittest from subprocess import CalledProcessError from unittest.mock import patch, call, MagicMock +from pathlib import Path +import shutil +import tempfile from checkbox_support.helpers.retry import mock_retry @@ -42,6 +45,8 @@ perform_ping_test, parser_args, main, + restore_netplan_files, + backup_netplan_files, ) @@ -606,6 +611,8 @@ def test_main_open_no_aps_found( "TestSSID": {"Chan": "11", "Freq": "2462", "Signal": "80"}, }, ) + @patch("wifi_nmcli_test.backup_netplan_files") + @patch("wifi_nmcli_test.restore_netplan_files") @patch("wifi_nmcli_test.open_connection", return_value=0) @patch( "wifi_nmcli_test.sys.argv", @@ -616,5 +623,142 @@ def test_main_open_aps_found( list_aps_mock, get_nm_activate_connection_mock, mock_open_connection, + mock_rest_back, + mock_cr_back, ): main() + + +class TestNetplanBackupFunctions(unittest.TestCase): + def setUp(self): + self.TEST_BACKUP_DIR = tempfile.TemporaryDirectory() + self.TEST_NETPLAN_DIR = tempfile.TemporaryDirectory() + + @patch("glob.glob") + @patch("builtins.print") + def test_backup_netplan_files_no_files_found(self, mock_print, mock_glob): + """Test backup when no YAML files are found.""" + mock_glob.return_value = [] + + backup_netplan_files( + str(self.TEST_BACKUP_DIR.name), str(self.TEST_NETPLAN_DIR.name) + ) + + @patch("os.chown") + @patch("os.stat") + @patch("glob.glob") + @patch("shutil.copy2") + @patch("pathlib.Path.mkdir") + @patch("builtins.print") + def test_backup_netplan_files_success( + self, + mock_print, + mock_mkdir, + mock_copy2, + mock_glob, + mock_stat, + mock_chown, + ): + """Test successful backup of netplan files.""" + mock_glob.return_value = [ + str(self.TEST_NETPLAN_DIR.name) + "/config1.yaml", + str(self.TEST_NETPLAN_DIR.name) + "/config2.yaml", + ] + + backup_netplan_files( + str(self.TEST_BACKUP_DIR.name), str(self.TEST_NETPLAN_DIR.name) + ) + + self.assertEqual(mock_copy2.call_count, 2) + mock_mkdir.assert_called_once_with(parents=True, exist_ok=True) + + @patch("os.chown") + @patch("os.stat") + @patch("os.path.exists") + @patch("glob.glob") + @patch("os.remove") + @patch("os.makedirs") + @patch("shutil.copy2") + @patch("builtins.print") + def test_restore_netplan_files_success( + self, + mock_print, + mock_copy2, + mock_makedirs, + mock_remove, + mock_glob, + mock_exists, + mock_stat, + mock_chown, + ): + """Test successful restore of netplan files.""" + mock_exists.return_value = True + + mock_glob.side_effect = [ + [], + [], + ] + + restore_netplan_files(None, str(self.TEST_NETPLAN_DIR.name)) + restore_netplan_files( + str(self.TEST_BACKUP_DIR.name), str(self.TEST_NETPLAN_DIR.name) + ) + + mock_glob.side_effect = [ + # Existing files to remove + [str(self.TEST_NETPLAN_DIR.name) + "/old1.yaml"], + [ + "{}/config1.yaml".format(str(self.TEST_BACKUP_DIR.name)), + "{}/config2.yaml".format(str(self.TEST_BACKUP_DIR.name)), + ], # Backup files + ] + + restore_netplan_files( + str(self.TEST_BACKUP_DIR.name), str(self.TEST_NETPLAN_DIR.name) + ) + + mock_remove.assert_called_once_with( + str(self.TEST_NETPLAN_DIR.name) + "/old1.yaml" + ) + self.assertEqual(mock_copy2.call_count, 2) + + @patch("os.path.exists") + @patch("glob.glob") + @patch("os.remove") + @patch("builtins.print") + def test_restore_netplan_files_remove_error( + self, mock_print, mock_remove, mock_glob, mock_exists + ): + """Test restore when removing existing files fails.""" + mock_exists.return_value = True + mock_glob.side_effect = [ + # Existing files to remove + [str(self.TEST_NETPLAN_DIR.name) + "/old1.yaml"], + # Backup files + ["{}/config1.yaml".format(str(self.TEST_BACKUP_DIR.name))], + ] + mock_remove.side_effect = OSError("Permission denied") + with self.assertRaises(OSError): + restore_netplan_files( + str(self.TEST_BACKUP_DIR.name), str(self.TEST_NETPLAN_DIR.name) + ) + + @patch("os.path.exists") + @patch("glob.glob") + @patch("os.makedirs") + @patch("builtins.print") + def test_restore_netplan_files_makedirs_error( + self, mock_print, mock_makedirs, mock_glob, mock_exists + ): + """Test restore when makedirs operation fails.""" + mock_exists.return_value = True + mock_glob.side_effect = [ + [], + ["{}/config1.yaml".format(str(self.TEST_BACKUP_DIR.name))], + ] + mock_makedirs.side_effect = OSError("Permission denied") + + with self.assertRaises(FileNotFoundError): + restore_netplan_files( + str(self.TEST_BACKUP_DIR.name), str(self.TEST_NETPLAN_DIR.name) + )