Skip to content

Commit a66f086

Browse files
committed
Add a backup and restore step on the netplans directory
1 parent cbdb66f commit a66f086

File tree

3 files changed

+313
-0
lines changed

3 files changed

+313
-0
lines changed

providers/base/bin/wifi_nmcli_test.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@
1818
import subprocess as sp
1919
import sys
2020
import shlex
21+
from pathlib import Path
22+
import shutil
23+
import glob
2124

2225
from packaging import version as version_parser
2326

2427
from checkbox_support.helpers.retry import retry
2528
from gateway_ping_test import ping
2629

30+
BACKUP_DIR = "/tmp/netplan_backup" # hardcoded so that no one can send "/"
31+
NETPLAN_DIR = "/lib/netplan"
2732

2833
print = functools.partial(print, flush=True)
2934

@@ -386,6 +391,100 @@ def parser_args():
386391
return args
387392

388393

394+
def backup_netplan_files(backup_dir: str, netplan_dir: str) -> bool:
395+
"""
396+
Backup netplan YAML files from /etc/netplan/ to a
397+
temporary directory, if there are.
398+
399+
Returns:
400+
bool: True if the operation succeded.
401+
"""
402+
netplan_pattern = os.path.join(netplan_dir, "*.yaml")
403+
404+
# Find all netplan YAML files
405+
yaml_files = glob.glob(netplan_pattern)
406+
407+
if not yaml_files:
408+
print("No netplan YAML files found")
409+
return True
410+
411+
# Create temporary directory
412+
Path(backup_dir).mkdir(parents=True, exist_ok=True)
413+
414+
# Copy each file to temp directory
415+
for yaml_file in yaml_files:
416+
filename = os.path.basename(yaml_file)
417+
temp_path = os.path.join(backup_dir, filename)
418+
shutil.copy2(yaml_file, temp_path)
419+
# Then copy ownership
420+
st = os.stat(yaml_file)
421+
os.chown(temp_path, st.st_uid, st.st_gid)
422+
print(f"Backed up: {yaml_file} -> {temp_path}")
423+
424+
print(f"Netplan files backed up to: {backup_dir}")
425+
return True
426+
427+
428+
def restore_netplan_files(backup_dir: str, netplan_dir: str) -> bool:
429+
"""
430+
Restore netplan YAML files from backup directory to /etc/netplan/.
431+
432+
Returns:
433+
bool: True if restoration successful, False otherwise
434+
"""
435+
if not backup_dir or not os.path.exists(backup_dir):
436+
print(f"Backup directory does not exist: {netplan_dir}")
437+
return False
438+
439+
# Clean up existing netplan files first
440+
existing_files = glob.glob(os.path.join(netplan_dir, "*.yaml"))
441+
for existing_file in existing_files:
442+
os.remove(existing_file)
443+
print(f"Removed: {existing_file}")
444+
445+
# Find all YAML files in backup directory
446+
backup_pattern = os.path.join(backup_dir, "*.yaml")
447+
backup_files = glob.glob(backup_pattern)
448+
449+
if not backup_files:
450+
print("No netplan files found in backup directory")
451+
return False
452+
453+
try:
454+
# Restore each file
455+
for backup_file in backup_files:
456+
filename = os.path.basename(backup_file)
457+
target_path = os.path.join(netplan_dir, filename)
458+
shutil.copy2(backup_file, target_path)
459+
# Then copy ownership
460+
st = os.stat(backup_file)
461+
os.chown(target_path, st.st_uid, st.st_gid)
462+
print(f"Restored: {backup_file} -> {target_path}")
463+
464+
print("Netplan files restored successfully")
465+
return True
466+
467+
except Exception as e:
468+
print(f"Error during restoration: {e}")
469+
return False
470+
471+
472+
def cleanup_netplan_backup(backup_dir: str) -> bool:
473+
"""
474+
Clean up temporary backup directory.
475+
"""
476+
if not os.path.exists(backup_dir):
477+
return True
478+
479+
try:
480+
shutil.rmtree(backup_dir)
481+
print(f"Cleaned up backup directory: {backup_dir}")
482+
return True
483+
except Exception as e:
484+
print(f"Error cleaning up backup: {e}")
485+
return False
486+
487+
389488
@retry(max_attempts=5, delay=60)
390489
def main():
391490
args = parser_args()
@@ -408,6 +507,11 @@ def main():
408507
)
409508

410509
if args.func:
510+
# backup the test plans, because nmcli corrupts them
511+
# and debsums will complain afterwards
512+
# This is ugly. Ideally, nmcli should be patched instead
513+
backup_netplan_files(BACKUP_DIR, NETPLAN_DIR)
514+
411515
delete_test_ap_ssid_connection()
412516
activated_uuid = get_nm_activate_connection()
413517
turn_down_nm_connections()
@@ -423,6 +527,8 @@ def main():
423527
finally:
424528
turn_up_connection(activated_uuid)
425529
delete_test_ap_ssid_connection()
530+
restore_netplan_files(BACKUP_DIR, NETPLAN_DIR)
531+
cleanup_netplan_backup(BACKUP_DIR)
426532

427533

428534
if __name__ == "__main__":

providers/base/tests/test_wifi_client_test_netplan.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io
2525
import sys
2626
from unittest.mock import call
27+
2728
from wifi_client_test_netplan import (
2829
netplan_renderer,
2930
check_and_get_renderer,

providers/base/tests/test_wifi_nmcli_test.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import unittest
2121
from subprocess import CalledProcessError
2222
from unittest.mock import patch, call, MagicMock
23+
from pathlib import Path
24+
import shutil
2325

2426
from checkbox_support.helpers.retry import mock_retry
2527

@@ -42,6 +44,9 @@
4244
perform_ping_test,
4345
parser_args,
4446
main,
47+
restore_netplan_files,
48+
backup_netplan_files,
49+
cleanup_netplan_backup,
4550
)
4651

4752

@@ -606,6 +611,9 @@ def test_main_open_no_aps_found(
606611
"TestSSID": {"Chan": "11", "Freq": "2462", "Signal": "80"},
607612
},
608613
)
614+
@patch("wifi_nmcli_test.backup_netplan_files")
615+
@patch("wifi_nmcli_test.restore_netplan_files")
616+
@patch("wifi_nmcli_test.cleanup_netplan_backup")
609617
@patch("wifi_nmcli_test.open_connection", return_value=0)
610618
@patch(
611619
"wifi_nmcli_test.sys.argv",
@@ -616,5 +624,203 @@ def test_main_open_aps_found(
616624
list_aps_mock,
617625
get_nm_activate_connection_mock,
618626
mock_open_connection,
627+
mock_cleanup_back,
628+
mock_rest_back,
629+
mock_cr_back,
619630
):
620631
main()
632+
633+
634+
class TestNetplanBackupFunctions(unittest.TestCase):
635+
def setUp(self):
636+
self.TEST_BACKUP_DIR = "/tmp/test_netplan_backup"
637+
self.TEST_NETPLAN_DIR = "/tmp/etc/netplan"
638+
"""Set up test fixtures before each test method."""
639+
Path(self.TEST_BACKUP_DIR).mkdir(parents=True, exist_ok=True)
640+
Path(self.TEST_NETPLAN_DIR).mkdir(parents=True, exist_ok=True)
641+
642+
def tearDown(self):
643+
"""Clean up after each test method."""
644+
# Clean up test directories
645+
shutil.rmtree(Path(self.TEST_BACKUP_DIR), ignore_errors=True)
646+
shutil.rmtree(Path(self.TEST_NETPLAN_DIR), ignore_errors=True)
647+
648+
@patch("glob.glob")
649+
@patch("builtins.print")
650+
def test_backup_netplan_files_no_files_found(self, mock_print, mock_glob):
651+
"""Test backup when no YAML files are found."""
652+
mock_glob.return_value = []
653+
654+
result = backup_netplan_files(
655+
self.TEST_BACKUP_DIR, self.TEST_NETPLAN_DIR
656+
)
657+
self.assertTrue(result)
658+
659+
@patch("os.chown")
660+
@patch("os.stat")
661+
@patch("glob.glob")
662+
@patch("shutil.copy2")
663+
@patch("pathlib.Path.mkdir")
664+
@patch("builtins.print")
665+
def test_backup_netplan_files_success(
666+
self,
667+
mock_print,
668+
mock_mkdir,
669+
mock_copy2,
670+
mock_glob,
671+
mock_stat,
672+
mock_chown,
673+
):
674+
"""Test successful backup of netplan files."""
675+
mock_glob.return_value = [
676+
self.TEST_NETPLAN_DIR + "/config1.yaml",
677+
self.TEST_NETPLAN_DIR + "/config2.yaml",
678+
]
679+
680+
result = backup_netplan_files(
681+
self.TEST_BACKUP_DIR, self.TEST_NETPLAN_DIR
682+
)
683+
684+
self.assertTrue(result)
685+
self.assertEqual(mock_copy2.call_count, 2)
686+
mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
687+
688+
@patch("os.chown")
689+
@patch("os.stat")
690+
@patch("os.path.exists")
691+
@patch("glob.glob")
692+
@patch("os.remove")
693+
@patch("os.makedirs")
694+
@patch("shutil.copy2")
695+
@patch("builtins.print")
696+
def test_restore_netplan_files_success(
697+
self,
698+
mock_print,
699+
mock_copy2,
700+
mock_makedirs,
701+
mock_remove,
702+
mock_glob,
703+
mock_exists,
704+
mock_stat,
705+
mock_chown,
706+
):
707+
"""Test successful restore of netplan files."""
708+
mock_exists.return_value = True
709+
mock_glob.side_effect = [
710+
[self.TEST_NETPLAN_DIR + "/old1.yaml"], # Existing files to remove
711+
[
712+
f"{self.TEST_BACKUP_DIR}/config1.yaml",
713+
f"{self.TEST_BACKUP_DIR}/config2.yaml",
714+
], # Backup files
715+
]
716+
717+
result = restore_netplan_files(
718+
self.TEST_BACKUP_DIR, self.TEST_NETPLAN_DIR
719+
)
720+
721+
self.assertTrue(result)
722+
mock_remove.assert_called_once_with(
723+
self.TEST_NETPLAN_DIR + "/old1.yaml"
724+
)
725+
self.assertEqual(mock_copy2.call_count, 2)
726+
727+
@patch("os.chown")
728+
@patch("os.stat")
729+
@patch("os.path.exists")
730+
@patch("glob.glob")
731+
@patch("os.remove")
732+
@patch("shutil.copy2")
733+
@patch("builtins.print")
734+
def test_restore_netplan_files_copy_error(
735+
self,
736+
mock_print,
737+
mock_copy2,
738+
mock_remove,
739+
mock_glob,
740+
mock_exists,
741+
mock_stat,
742+
mock_chown,
743+
):
744+
"""Test restore when copy operation fails."""
745+
mock_exists.return_value = True
746+
mock_glob.side_effect = [[], [f"{self.TEST_BACKUP_DIR}/config1.yaml"]]
747+
mock_copy2.side_effect = OSError("Permission denied")
748+
749+
result = restore_netplan_files(
750+
self.TEST_BACKUP_DIR, self.TEST_NETPLAN_DIR
751+
)
752+
753+
self.assertFalse(result)
754+
755+
@patch("os.path.exists")
756+
@patch("builtins.print")
757+
def test_cleanup_backup_not_exists(self, mock_print, mock_exists):
758+
"""Test cleanup when backup directory doesn't exist."""
759+
mock_exists.return_value = False
760+
761+
result = cleanup_netplan_backup(self.TEST_BACKUP_DIR)
762+
763+
self.assertTrue(result)
764+
765+
@patch("os.path.exists")
766+
@patch("shutil.rmtree")
767+
@patch("builtins.print")
768+
def test_cleanup_backup_success(
769+
self, mock_print, mock_rmtree, mock_exists
770+
):
771+
"""Test successful cleanup of backup directory."""
772+
mock_exists.return_value = True
773+
774+
result = cleanup_netplan_backup(self.TEST_BACKUP_DIR)
775+
776+
self.assertTrue(result)
777+
mock_rmtree.assert_called_once_with(self.TEST_BACKUP_DIR)
778+
779+
@patch("os.path.exists")
780+
@patch("shutil.rmtree")
781+
@patch("builtins.print")
782+
def test_cleanup_backup_error(self, mock_print, mock_rmtree, mock_exists):
783+
"""Test cleanup when rmtree operation fails."""
784+
mock_exists.return_value = True
785+
mock_rmtree.side_effect = OSError("Permission denied")
786+
787+
result = cleanup_netplan_backup(self.TEST_BACKUP_DIR)
788+
789+
self.assertFalse(result)
790+
791+
@patch("os.path.exists")
792+
@patch("glob.glob")
793+
@patch("os.remove")
794+
@patch("builtins.print")
795+
def test_restore_netplan_files_remove_error(
796+
self, mock_print, mock_remove, mock_glob, mock_exists
797+
):
798+
"""Test restore when removing existing files fails."""
799+
mock_exists.return_value = True
800+
mock_glob.side_effect = [
801+
[self.TEST_NETPLAN_DIR + "/old1.yaml"], # Existing files to remove
802+
[f"{self.TEST_BACKUP_DIR}/config1.yaml"], # Backup files
803+
]
804+
mock_remove.side_effect = OSError("Permission denied")
805+
with self.assertRaises(OSError):
806+
result = restore_netplan_files(
807+
self.TEST_BACKUP_DIR, self.TEST_NETPLAN_DIR
808+
)
809+
810+
@patch("os.path.exists")
811+
@patch("glob.glob")
812+
@patch("os.makedirs")
813+
@patch("builtins.print")
814+
def test_restore_netplan_files_makedirs_error(
815+
self, mock_print, mock_makedirs, mock_glob, mock_exists
816+
):
817+
"""Test restore when makedirs operation fails."""
818+
mock_exists.return_value = True
819+
mock_glob.side_effect = [[], [f"{self.TEST_BACKUP_DIR}/config1.yaml"]]
820+
mock_makedirs.side_effect = OSError("Permission denied")
821+
822+
result = restore_netplan_files(
823+
self.TEST_BACKUP_DIR, self.TEST_NETPLAN_DIR
824+
)
825+
826+
self.assertFalse(result)

0 commit comments

Comments
 (0)