1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2025-12-24 12:29:08 +00:00

Add support for cloud backups in Core (#5438)

* Add support for cloud backups in Core

* Test cases and small fixes identified

* Add test for partial reload no file failure
This commit is contained in:
Mike Degatano
2024-11-21 18:14:20 -05:00
committed by GitHub
parent a45d507bee
commit 5519f6a53b
28 changed files with 675 additions and 89 deletions

View File

@@ -4,7 +4,7 @@ import asyncio
import errno
from functools import partial
from pathlib import Path
from shutil import rmtree
from shutil import copy, rmtree
from unittest.mock import ANY, AsyncMock, MagicMock, Mock, PropertyMock, patch
from awesomeversion import AwesomeVersion
@@ -34,10 +34,12 @@ from supervisor.homeassistant.api import HomeAssistantAPI
from supervisor.homeassistant.const import WSType
from supervisor.homeassistant.core import HomeAssistantCore
from supervisor.homeassistant.module import HomeAssistant
from supervisor.jobs import JobSchedulerOptions
from supervisor.jobs.const import JobCondition
from supervisor.mounts.mount import Mount
from supervisor.utils.json import read_json_file, write_json_file
from tests.common import get_fixture_path
from tests.const import TEST_ADDON_SLUG
from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.systemd import Systemd as SystemdService
@@ -626,7 +628,8 @@ async def test_full_backup_to_mount(
},
)
await coresys.mounts.create_mount(mount)
assert mount_dir in coresys.backups.backup_locations
assert "backup_test" in coresys.backups.backup_locations
assert coresys.backups.backup_locations["backup_test"] == mount_dir
# Make a backup and add it to mounts. Confirm it exists in the right place
coresys.core.state = CoreState.RUNNING
@@ -671,7 +674,8 @@ async def test_partial_backup_to_mount(
},
)
await coresys.mounts.create_mount(mount)
assert mount_dir in coresys.backups.backup_locations
assert "backup_test" in coresys.backups.backup_locations
assert coresys.backups.backup_locations["backup_test"] == mount_dir
# Make a backup and add it to mounts. Confirm it exists in the right place
coresys.core.state = CoreState.RUNNING
@@ -723,7 +727,8 @@ async def test_backup_to_down_mount_error(
},
)
await coresys.mounts.create_mount(mount)
assert mount_dir in coresys.backups.backup_locations
assert "backup_test" in coresys.backups.backup_locations
assert coresys.backups.backup_locations["backup_test"] == mount_dir
# Attempt to make a backup which fails because is_mount on directory is false
mock_is_mount.return_value = False
@@ -1866,3 +1871,161 @@ async def test_core_pre_backup_actions_failed(
f"Preparing backup of Home Assistant Core failed due to: {pre_backup_error['message']}"
in caplog.text
)
@pytest.mark.usefixtures("mount_propagation", "mock_is_mount", "path_extern")
async def test_reload_multiple_locations(coresys: CoreSys, tmp_supervisor_data: Path):
"""Test reload with a backup that exists in multiple locations."""
(mount_dir := coresys.config.path_mounts / "backup_test").mkdir()
await coresys.mounts.load()
mount = Mount.from_dict(
coresys,
{
"name": "backup_test",
"usage": "backup",
"type": "cifs",
"server": "test.local",
"share": "test",
},
)
await coresys.mounts.create_mount(mount)
assert not coresys.backups.list_backups
backup_file = get_fixture_path("backup_example.tar")
copy(backup_file, tmp_supervisor_data / "core/backup")
await coresys.backups.reload()
assert coresys.backups.list_backups
assert (backup := coresys.backups.get("7fed74c8"))
assert backup.location == ".cloud_backup"
assert backup.locations == [".cloud_backup"]
assert backup.all_locations == {".cloud_backup"}
copy(backup_file, tmp_supervisor_data / "backup")
await coresys.backups.reload()
assert coresys.backups.list_backups
assert (backup := coresys.backups.get("7fed74c8"))
assert backup.location is None
assert backup.locations == [None]
assert backup.all_locations == {".cloud_backup", None}
copy(backup_file, mount_dir)
await coresys.backups.reload()
assert coresys.backups.list_backups
assert (backup := coresys.backups.get("7fed74c8"))
assert backup.location in {None, "backup_test"}
assert backup.locations == [None, "backup_test"]
assert backup.all_locations == {".cloud_backup", None, "backup_test"}
@pytest.mark.usefixtures("mount_propagation", "mock_is_mount", "path_extern")
async def test_partial_reload_multiple_locations(
coresys: CoreSys, tmp_supervisor_data: Path
):
"""Test a partial reload with a backup that exists in multiple locations."""
(mount_dir := coresys.config.path_mounts / "backup_test").mkdir()
await coresys.mounts.load()
mount = Mount.from_dict(
coresys,
{
"name": "backup_test",
"usage": "backup",
"type": "cifs",
"server": "test.local",
"share": "test",
},
)
await coresys.mounts.create_mount(mount)
assert not coresys.backups.list_backups
backup_file = get_fixture_path("backup_example.tar")
copy(backup_file, tmp_supervisor_data / "core/backup")
await coresys.backups.reload()
assert coresys.backups.list_backups
assert (backup := coresys.backups.get("7fed74c8"))
assert backup.location == ".cloud_backup"
assert backup.locations == [".cloud_backup"]
assert backup.all_locations == {".cloud_backup"}
copy(backup_file, tmp_supervisor_data / "backup")
await coresys.backups.reload(location=None, filename="backup_example.tar")
assert coresys.backups.list_backups
assert (backup := coresys.backups.get("7fed74c8"))
assert backup.location is None
assert backup.locations == [None]
assert backup.all_locations == {".cloud_backup", None}
copy(backup_file, mount_dir)
await coresys.backups.reload(location=mount, filename="backup_example.tar")
assert coresys.backups.list_backups
assert (backup := coresys.backups.get("7fed74c8"))
assert backup.location is None
assert backup.locations == [None, "backup_test"]
assert backup.all_locations == {".cloud_backup", None, "backup_test"}
@pytest.mark.parametrize(
("location", "folder"), [(None, "backup"), (".cloud_backup", "cloud_backup")]
)
@pytest.mark.usefixtures("tmp_supervisor_data")
async def test_partial_backup_complete_ws_message(
coresys: CoreSys, ha_ws_client: AsyncMock, location: str | None, folder: str
):
"""Test WS message notifies core when a partial backup is complete."""
coresys.core.state = CoreState.RUNNING
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
ha_ws_client.ha_version = AwesomeVersion("2024.12.0")
# Test a partial backup
job, backup_task = coresys.jobs.schedule_job(
coresys.backups.do_backup_partial,
JobSchedulerOptions(),
"test",
folders=["media"],
location=location,
)
backup: Backup = await backup_task
assert ha_ws_client.async_send_command.call_args_list[-3].args[0] == {
"type": "backup/supervisor/backup_complete",
"data": {
"job_id": job.uuid,
"slug": backup.slug,
"path": f"/{folder}/{backup.slug}.tar",
},
}
@pytest.mark.parametrize(
("location", "folder"), [(None, "backup"), (".cloud_backup", "cloud_backup")]
)
@pytest.mark.usefixtures("tmp_supervisor_data")
async def test_full_backup_complete_ws_message(
coresys: CoreSys, ha_ws_client: AsyncMock, location: str | None, folder: str
):
"""Test WS message notifies core when a full backup is complete."""
coresys.core.state = CoreState.RUNNING
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
ha_ws_client.ha_version = AwesomeVersion("2024.12.0")
# Test a full backup
job, backup_task = coresys.jobs.schedule_job(
coresys.backups.do_backup_full, JobSchedulerOptions(), "test", location=location
)
backup: Backup = await backup_task
assert ha_ws_client.async_send_command.call_args_list[-3].args[0] == {
"type": "backup/supervisor/backup_complete",
"data": {
"job_id": job.uuid,
"slug": backup.slug,
"path": f"/{folder}/{backup.slug}.tar",
},
}