mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-04-02 08:12:47 +01:00
* Include network storage mount configurations in backups When creating backups, now stores mount configurations (CIFS/NFS shares) including server, share, credentials, and other settings. When restoring from a backup, mount configurations are automatically restored. This fixes the issue where network storage definitions for backups were lost after restoring from a backup, requiring users to manually reconfigure their network storage mounts. Changes: - Add ATTR_MOUNTS schema to backup validation - Add store_mounts() method to save mount configs during backup - Add restore_mounts() method to restore mount configs during restore - Add MOUNTS stage to backup/restore job stages - Update BackupManager to call mount backup/restore methods - Add tests for mount backup/restore functionality Fixes home-assistant/core#148663 * Address reviewer feedback for mount backup/restore Changes based on PR review: - Store mount configs in encrypted mounts.tar instead of unencrypted backup metadata (security fix for passwords) - Separate mount restore into config save + async activation tasks (mounts activate in background, failures don't block restore) - Add replace_default_backup_mount parameter to control whether to overwrite existing default mount setting - Remove unnecessary broad exception handler for default mount setter - Simplify schema: ATTR_MOUNTS is now just a boolean flag since actual data is in the encrypted tar file - Update tests to reflect new async API and return types * Fix code review issues in mount backup/restore - Add bind mount handling for MEDIA and SHARE usage types in _activate_restored_mount() to mirror MountManager.create_mount() - Fix double save_data() call by using needs_save flag - Import MountUsage const for usage type checks * Add pylint disable comments for protected member access * Tighten broad exception handlers in mount backup restore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Address second round of reviewer feedback - Catch OSError separately and check errno.EBADMSG for drive health - Validate mounts JSON against SCHEMA_MOUNTS_CONFIG before importing - Use mount_data[ATTR_NAME] instead of .get("name", "unknown") - Overwrite existing mounts on restore instead of skipping - Move restore_mount/activate logic to MountManager (no more protected-access in Backup) - Drop unused replace_default_backup_mount parameter - Fix test_backup_progress: add mounts stage to expected events - Fix test_store_mounts: avoid create_mount which requires dbus * Rename mounts.tar to supervisor.tar for generic supervisor config Rename the inner tar from mounts.tar to supervisor.tar so it can hold multiple config files (mounts.json now, docker credentials later). Rename store_mounts/restore_mounts to store_supervisor_config/ restore_supervisor_config and update stage names accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix pylint protected-access and test timeouts in backup tests - Add pylint disable comment for _mounts protected access in test_backup.py - Mock restore_supervisor_config in test_full_backup_to_mount and test_partial_backup_to_mount to avoid D-Bus mount activation during restore that causes timeouts in the test environment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Address agners review feedback - Change create_inner_tar() to create_tar() per #6575 - Remove "r" argument from SecureTarFile (now read-only by default) - Change warning to info for missing mounts tar (old backups won't have it) - Narrow exception handler to (MountError, vol.Invalid, KeyError, OSError) * Update supervisor/backups/backup.py * Address agners feedback: remove metadata flag, add mount feature check - Remove ATTR_SUPERVISOR boolean flag from backup metadata; instead check for physical presence of supervisor.tar (like folder backups) - Remove has_supervisor_config property - Always attempt supervisor config restore (tar existence check handles it) - Add HostFeature.MOUNT check in _activate_restored_mount before attempting to activate mounts on systems without mount support --------- Co-authored-by: Stefan Agner <stefan@agner.ch> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Mike Degatano <michael.degatano@gmail.com>
100 lines
3.6 KiB
Python
100 lines
3.6 KiB
Python
"""Mock test."""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from supervisor.backups.backup import BackupLocation
|
|
from supervisor.backups.const import LOCATION_CLOUD_BACKUP, LOCATION_TYPE, BackupType
|
|
from supervisor.backups.validate import ALL_FOLDERS
|
|
from supervisor.coresys import CoreSys
|
|
from supervisor.mounts.mount import Mount
|
|
|
|
from tests.const import TEST_ADDON_SLUG
|
|
|
|
|
|
@pytest.fixture(name="backup_mock")
|
|
def fixture_backup_mock():
|
|
"""Backup class mock."""
|
|
with patch("supervisor.backups.manager.Backup") as backup_mock:
|
|
backup_instance = MagicMock()
|
|
backup_mock.return_value = backup_instance
|
|
|
|
backup_instance.store_addons = AsyncMock(return_value=None)
|
|
backup_instance.store_folders = AsyncMock(return_value=None)
|
|
backup_instance.store_homeassistant = AsyncMock(return_value=None)
|
|
backup_instance.store_addons = AsyncMock(return_value=None)
|
|
backup_instance.store_supervisor_config = AsyncMock(return_value=None)
|
|
backup_instance.restore_folders = AsyncMock(return_value=True)
|
|
backup_instance.restore_homeassistant = AsyncMock(return_value=None)
|
|
backup_instance.restore_addons = AsyncMock(return_value=(True, []))
|
|
backup_instance.restore_repositories = AsyncMock(return_value=None)
|
|
backup_instance.restore_supervisor_config = AsyncMock(return_value=(True, []))
|
|
backup_instance.remove_delta_addons = AsyncMock(return_value=True)
|
|
|
|
yield backup_mock
|
|
|
|
|
|
@pytest.fixture
|
|
def partial_backup_mock(backup_mock):
|
|
"""Partial backup mock."""
|
|
backup_instance = backup_mock.return_value
|
|
backup_instance.sys_type = BackupType.PARTIAL
|
|
backup_instance.folders = []
|
|
backup_instance.addon_list = [TEST_ADDON_SLUG]
|
|
backup_instance.supervisor_version = "9999.09.9.dev9999"
|
|
backup_instance.location = None
|
|
backup_instance.all_locations = {
|
|
None: BackupLocation(path=Path("/"), protected=False, size_bytes=0)
|
|
}
|
|
backup_instance.validate_backup = AsyncMock()
|
|
yield backup_mock
|
|
|
|
|
|
@pytest.fixture
|
|
def full_backup_mock(backup_mock):
|
|
"""Full backup mock."""
|
|
backup_instance = backup_mock.return_value
|
|
backup_instance.sys_type = BackupType.FULL
|
|
backup_instance.folders = ALL_FOLDERS
|
|
backup_instance.addon_list = [TEST_ADDON_SLUG]
|
|
backup_instance.supervisor_version = "9999.09.9.dev9999"
|
|
backup_instance.location = None
|
|
backup_instance.all_locations = {
|
|
None: BackupLocation(path=Path("/"), protected=False, size_bytes=0)
|
|
}
|
|
backup_instance.validate_backup = AsyncMock()
|
|
yield backup_mock
|
|
|
|
|
|
@pytest.fixture(name="backup_locations")
|
|
async def fixture_backup_locations(
|
|
request: pytest.FixtureRequest, coresys: CoreSys, mount_propagation, mock_is_mount
|
|
) -> list[LOCATION_TYPE]:
|
|
"""Return a list of prcoessed backup locations."""
|
|
locations: list[LOCATION_TYPE] = []
|
|
loaded = False
|
|
for location in request.param:
|
|
if location in {None, LOCATION_CLOUD_BACKUP}:
|
|
locations.append(location)
|
|
else:
|
|
if not loaded:
|
|
await coresys.mounts.load()
|
|
|
|
await coresys.mounts.create_mount(
|
|
Mount.from_dict(
|
|
coresys,
|
|
{
|
|
"name": location,
|
|
"usage": "backup",
|
|
"type": "cifs",
|
|
"server": "test.local",
|
|
"share": "test",
|
|
},
|
|
)
|
|
)
|
|
locations.append(coresys.mounts.get(location))
|
|
|
|
return locations
|