diff --git a/sonic_package_manager/manager.py b/sonic_package_manager/manager.py index b6a3be50c36..c6dfe47e05f 100644 --- a/sonic_package_manager/manager.py +++ b/sonic_package_manager/manager.py @@ -456,6 +456,10 @@ def install_from_source(self, self.database.update_package(package.entry) self.database.commit() + # Ensure all files (plugins, services, database) are synced to disk + # This prevents data loss or corruption after power cycle + os.sync() + @under_lock def update(self, name: str, @@ -546,6 +550,10 @@ def uninstall(self, name: str, package.entry.version = None self.database.update_package(package.entry) self.database.commit() + + # Ensure database update is synced to disk + os.sync() + manifest_path = os.path.join(MANIFESTS_LOCATION, name) edit_path = os.path.join(MANIFESTS_LOCATION, name + ".edit") if os.path.exists(manifest_path): @@ -692,6 +700,10 @@ def upgrade_from_source(self, new_package_entry.version = new_version self.database.update_package(new_package_entry) self.database.commit() + + # Ensure all files are synced to disk after upgrade + os.sync() + if update_only: manifest_path = os.path.join(MANIFESTS_LOCATION, name) edit_path = os.path.join(MANIFESTS_LOCATION, name + ".edit") diff --git a/tests/sonic_package_manager/test_manager.py b/tests/sonic_package_manager/test_manager.py index d45f16fe79f..a75ddd1b566 100644 --- a/tests/sonic_package_manager/test_manager.py +++ b/tests/sonic_package_manager/test_manager.py @@ -17,10 +17,13 @@ def mock_run_command(): def test_installation_not_installed(package_manager): - package_manager.install('test-package') - package = package_manager.get_installed_package('test-package') - assert package.installed - assert package.entry.default_reference == '1.6.0' + with patch('sonic_package_manager.manager.os.sync') as mock_sync: + package_manager.install('test-package') + package = package_manager.get_installed_package('test-package') + assert package.installed + assert package.entry.default_reference == '1.6.0' + # Verify os.sync() is called after installation to ensure data persistence + mock_sync.assert_called() def test_installation_already_installed(package_manager): @@ -179,7 +182,8 @@ def test_installation_multiple_cli_plugin(package_manager, fake_metadata_resolve manifest['cli']= {'show': ['/cli/plugin.py', '/cli/plugin2.py']} with patch('sonic_package_manager.manager.get_cli_plugin_directory') as get_dir_mock, \ patch('os.remove') as remove_mock, \ - patch('os.path.exists') as path_exists_mock: + patch('os.path.exists'), \ + patch('sonic_package_manager.manager.os.sync') as mock_sync: get_dir_mock.return_value = '/' package_manager.install('test-package') package_manager.docker.extract.assert_has_calls( @@ -189,6 +193,9 @@ def test_installation_multiple_cli_plugin(package_manager, fake_metadata_resolve ], any_order=True, ) + # Verify os.sync() is called after installation + install_sync_count = mock_sync.call_count + assert install_sync_count >= 1, "os.sync() should be called after installation" package_manager._set_feature_state = Mock() package_manager.uninstall('test-package', force=True) @@ -199,6 +206,9 @@ def test_installation_multiple_cli_plugin(package_manager, fake_metadata_resolve ], any_order=True, ) + # Verify os.sync() is called after uninstallation + uninstall_sync_count = mock_sync.call_count + assert uninstall_sync_count > install_sync_count, "os.sync() should be called after uninstallation" def test_installation_cli_plugin_skipped(package_manager, fake_metadata_resolver, anything): @@ -328,10 +338,13 @@ def test_manager_upgrade(package_manager, sonic_fs, mock_run_command): package_manager.install('test-package-6=1.5.0') package = package_manager.get_installed_package('test-package-6') - package_manager.install('test-package-6=2.0.0') - upgraded_package = package_manager.get_installed_package('test-package-6') - assert upgraded_package.entry.version == Version.parse('2.0.0') - assert upgraded_package.entry.default_reference == package.entry.default_reference + with patch('sonic_package_manager.manager.os.sync') as mock_sync: + package_manager.install('test-package-6=2.0.0') + upgraded_package = package_manager.get_installed_package('test-package-6') + assert upgraded_package.entry.version == Version.parse('2.0.0') + assert upgraded_package.entry.default_reference == package.entry.default_reference + # Verify os.sync() is called after upgrade to ensure data persistence + mock_sync.assert_called() mock_run_command.assert_has_calls( [