Stop backup if pre backup failed in Core (#5203)

* Stop backup if pre backup failed in Core

* Fix API tests

* Partial backup in ci since there's no Home assistant

* Add ssl folder to partial backup

* Allow backups when Home Assistant is not running

* Undo change to skip db test
This commit is contained in:
Mike Degatano 2024-07-25 11:08:43 -04:00 committed by GitHub
parent 5ee7d16687
commit 591b9a4d87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 192 additions and 83 deletions

View File

@ -1,4 +1,5 @@
"""Backup manager.""" """Backup manager."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
@ -259,11 +260,6 @@ class BackupManager(FileConfiguration, JobGroup):
self.sys_core.state = CoreState.FREEZE self.sys_core.state = CoreState.FREEZE
async with backup: async with backup:
# Backup add-ons
if addon_list:
self._change_stage(BackupJobStage.ADDONS, backup)
addon_start_tasks = await backup.store_addons(addon_list)
# HomeAssistant Folder is for v1 # HomeAssistant Folder is for v1
if homeassistant: if homeassistant:
self._change_stage(BackupJobStage.HOME_ASSISTANT, backup) self._change_stage(BackupJobStage.HOME_ASSISTANT, backup)
@ -273,6 +269,11 @@ class BackupManager(FileConfiguration, JobGroup):
else homeassistant_exclude_database else homeassistant_exclude_database
) )
# Backup add-ons
if addon_list:
self._change_stage(BackupJobStage.ADDONS, backup)
addon_start_tasks = await backup.store_addons(addon_list)
# Backup folders # Backup folders
if folder_list: if folder_list:
self._change_stage(BackupJobStage.FOLDERS, backup) self._change_stage(BackupJobStage.FOLDERS, backup)

View File

@ -7,7 +7,9 @@ from awesomeversion import AwesomeVersion
from ..const import CoreState from ..const import CoreState
ATTR_ERROR = "error"
ATTR_OVERRIDE_IMAGE = "override_image" ATTR_OVERRIDE_IMAGE = "override_image"
ATTR_SUCCESS = "success"
LANDINGPAGE: AwesomeVersion = AwesomeVersion("landingpage") LANDINGPAGE: AwesomeVersion = AwesomeVersion("landingpage")
WATCHDOG_RETRY_SECONDS = 10 WATCHDOG_RETRY_SECONDS = 10
WATCHDOG_MAX_ATTEMPTS = 5 WATCHDOG_MAX_ATTEMPTS = 5

View File

@ -1,4 +1,5 @@
"""Home Assistant control object.""" """Home Assistant control object."""
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
import errno import errno
@ -22,6 +23,7 @@ from ..const import (
ATTR_BACKUPS_EXCLUDE_DATABASE, ATTR_BACKUPS_EXCLUDE_DATABASE,
ATTR_BOOT, ATTR_BOOT,
ATTR_IMAGE, ATTR_IMAGE,
ATTR_MESSAGE,
ATTR_PORT, ATTR_PORT,
ATTR_REFRESH_TOKEN, ATTR_REFRESH_TOKEN,
ATTR_SSL, ATTR_SSL,
@ -48,7 +50,7 @@ from ..utils import remove_folder
from ..utils.common import FileConfiguration from ..utils.common import FileConfiguration
from ..utils.json import read_json_file, write_json_file from ..utils.json import read_json_file, write_json_file
from .api import HomeAssistantAPI from .api import HomeAssistantAPI
from .const import ATTR_OVERRIDE_IMAGE, LANDINGPAGE, WSType from .const import ATTR_ERROR, ATTR_OVERRIDE_IMAGE, ATTR_SUCCESS, LANDINGPAGE, WSType
from .core import HomeAssistantCore from .core import HomeAssistantCore
from .secrets import HomeAssistantSecrets from .secrets import HomeAssistantSecrets
from .validate import SCHEMA_HASS_CONFIG from .validate import SCHEMA_HASS_CONFIG
@ -345,20 +347,37 @@ class HomeAssistant(FileConfiguration, CoreSysAttributes):
async def begin_backup(self) -> None: async def begin_backup(self) -> None:
"""Inform Home Assistant a backup is beginning.""" """Inform Home Assistant a backup is beginning."""
try: try:
await self.websocket.async_send_command({ATTR_TYPE: WSType.BACKUP_START}) resp = await self.websocket.async_send_command(
except HomeAssistantWSError: {ATTR_TYPE: WSType.BACKUP_START}
_LOGGER.warning( )
"Preparing backup of Home Assistant Core failed. Check HA Core logs." except HomeAssistantWSError as err:
raise HomeAssistantBackupError(
"Preparing backup of Home Assistant Core failed. Check HA Core logs.",
_LOGGER.error,
) from err
if resp and not resp.get(ATTR_SUCCESS):
raise HomeAssistantBackupError(
f"Preparing backup of Home Assistant Core failed due to: {resp.get(ATTR_ERROR, {}).get(ATTR_MESSAGE, "")}. Check HA Core logs.",
_LOGGER.error,
) )
@Job(name="home_assistant_module_end_backup") @Job(name="home_assistant_module_end_backup")
async def end_backup(self) -> None: async def end_backup(self) -> None:
"""Inform Home Assistant the backup is ending.""" """Inform Home Assistant the backup is ending."""
try: try:
await self.websocket.async_send_command({ATTR_TYPE: WSType.BACKUP_END}) resp = await self.websocket.async_send_command(
{ATTR_TYPE: WSType.BACKUP_END}
)
except HomeAssistantWSError: except HomeAssistantWSError:
_LOGGER.warning( _LOGGER.warning(
"Error during Home Assistant Core backup. Check HA Core logs." "Error resuming normal operations after backup of Home Assistant Core. Check HA Core logs."
)
if resp and not resp.get(ATTR_SUCCESS):
_LOGGER.warning(
"Error resuming normal operations after backup of Home Assistant Core due to: %s. Check HA Core logs.",
resp.get(ATTR_ERROR, {}).get(ATTR_MESSAGE, ""),
) )
@Job(name="home_assistant_module_backup") @Job(name="home_assistant_module_backup")

View File

@ -348,15 +348,15 @@ async def test_api_backup_errors(
assert job["done"] is True assert job["done"] is True
assert job["reference"] == slug assert job["reference"] == slug
assert job["errors"] == [] assert job["errors"] == []
assert job["child_jobs"][0]["name"] == "backup_store_addons" assert job["child_jobs"][0]["name"] == "backup_store_homeassistant"
assert job["child_jobs"][0]["reference"] == slug assert job["child_jobs"][0]["reference"] == slug
assert job["child_jobs"][0]["child_jobs"][0]["name"] == "backup_addon_save" assert job["child_jobs"][1]["name"] == "backup_store_addons"
assert job["child_jobs"][0]["child_jobs"][0]["reference"] == "local_ssh" assert job["child_jobs"][1]["reference"] == slug
assert job["child_jobs"][0]["child_jobs"][0]["errors"] == [ assert job["child_jobs"][1]["child_jobs"][0]["name"] == "backup_addon_save"
assert job["child_jobs"][1]["child_jobs"][0]["reference"] == "local_ssh"
assert job["child_jobs"][1]["child_jobs"][0]["errors"] == [
{"type": "BackupError", "message": "Can't create backup for local_ssh"} {"type": "BackupError", "message": "Can't create backup for local_ssh"}
] ]
assert job["child_jobs"][1]["name"] == "backup_store_homeassistant"
assert job["child_jobs"][1]["reference"] == slug
assert job["child_jobs"][2]["name"] == "backup_store_folders" assert job["child_jobs"][2]["name"] == "backup_store_folders"
assert job["child_jobs"][2]["reference"] == slug assert job["child_jobs"][2]["reference"] == slug
assert {j["reference"] for j in job["child_jobs"][2]["child_jobs"]} == { assert {j["reference"] for j in job["child_jobs"][2]["child_jobs"]} == {
@ -366,9 +366,14 @@ async def test_api_backup_errors(
"ssl", "ssl",
} }
with patch.object( with (
HomeAssistant, "backup", side_effect=HomeAssistantBackupError("Backup error") patch.object(
), patch.object(Addon, "backup"): HomeAssistant,
"backup",
side_effect=HomeAssistantBackupError("Backup error"),
),
patch.object(Addon, "backup"),
):
resp = await api_client.post( resp = await api_client.post(
f"/backups/new/{backup_type}", f"/backups/new/{backup_type}",
json={"name": f"{backup_type} backup"} | options, json={"name": f"{backup_type} backup"} | options,
@ -384,10 +389,9 @@ async def test_api_backup_errors(
assert job["errors"] == ( assert job["errors"] == (
err := [{"type": "HomeAssistantBackupError", "message": "Backup error"}] err := [{"type": "HomeAssistantBackupError", "message": "Backup error"}]
) )
assert job["child_jobs"][0]["name"] == "backup_store_addons" assert job["child_jobs"][0]["name"] == "backup_store_homeassistant"
assert job["child_jobs"][1]["name"] == "backup_store_homeassistant" assert job["child_jobs"][0]["errors"] == err
assert job["child_jobs"][1]["errors"] == err assert len(job["child_jobs"]) == 1
assert len(job["child_jobs"]) == 2
async def test_backup_immediate_errors(api_client: TestClient, coresys: CoreSys): async def test_backup_immediate_errors(api_client: TestClient, coresys: CoreSys):
@ -426,14 +430,17 @@ async def test_restore_immediate_errors(
assert resp.status == 400 assert resp.status == 400
assert "only a partial backup" in (await resp.json())["message"] assert "only a partial backup" in (await resp.json())["message"]
with patch.object( with (
Backup, patch.object(
"supervisor_version", Backup,
new=PropertyMock(return_value=AwesomeVersion("2024.01.0")), "supervisor_version",
), patch.object( new=PropertyMock(return_value=AwesomeVersion("2024.01.0")),
Supervisor, ),
"version", patch.object(
new=PropertyMock(return_value=AwesomeVersion("2023.12.0")), Supervisor,
"version",
new=PropertyMock(return_value=AwesomeVersion("2023.12.0")),
),
): ):
resp = await api_client.post( resp = await api_client.post(
f"/backups/{mock_partial_backup.slug}/restore/partial", f"/backups/{mock_partial_backup.slug}/restore/partial",
@ -442,9 +449,10 @@ async def test_restore_immediate_errors(
assert resp.status == 400 assert resp.status == 400
assert "Must update supervisor" in (await resp.json())["message"] assert "Must update supervisor" in (await resp.json())["message"]
with patch.object( with (
Backup, "protected", new=PropertyMock(return_value=True) patch.object(Backup, "protected", new=PropertyMock(return_value=True)),
), patch.object(Backup, "set_password", return_value=False): patch.object(Backup, "set_password", return_value=False),
):
resp = await api_client.post( resp = await api_client.post(
f"/backups/{mock_partial_backup.slug}/restore/partial", f"/backups/{mock_partial_backup.slug}/restore/partial",
json={"background": True, "homeassistant": True}, json={"background": True, "homeassistant": True},

View File

@ -31,6 +31,7 @@ from supervisor.exceptions import (
DockerError, DockerError,
) )
from supervisor.homeassistant.api import HomeAssistantAPI from supervisor.homeassistant.api import HomeAssistantAPI
from supervisor.homeassistant.const import WSType
from supervisor.homeassistant.core import HomeAssistantCore from supervisor.homeassistant.core import HomeAssistantCore
from supervisor.homeassistant.module import HomeAssistant from supervisor.homeassistant.module import HomeAssistant
from supervisor.jobs.const import JobCondition from supervisor.jobs.const import JobCondition
@ -335,9 +336,14 @@ async def test_fail_invalid_full_backup(
backup_instance.protected = False backup_instance.protected = False
backup_instance.supervisor_version = "2022.08.4" backup_instance.supervisor_version = "2022.08.4"
with patch.object( with (
type(coresys.supervisor), "version", new=PropertyMock(return_value="2022.08.3") patch.object(
), pytest.raises(BackupInvalidError): type(coresys.supervisor),
"version",
new=PropertyMock(return_value="2022.08.3"),
),
pytest.raises(BackupInvalidError),
):
await manager.do_restore_full(backup_instance) await manager.do_restore_full(backup_instance)
@ -364,9 +370,14 @@ async def test_fail_invalid_partial_backup(
await manager.do_restore_partial(backup_instance, homeassistant=True) await manager.do_restore_partial(backup_instance, homeassistant=True)
backup_instance.supervisor_version = "2022.08.4" backup_instance.supervisor_version = "2022.08.4"
with patch.object( with (
type(coresys.supervisor), "version", new=PropertyMock(return_value="2022.08.3") patch.object(
), pytest.raises(BackupInvalidError): type(coresys.supervisor),
"version",
new=PropertyMock(return_value="2022.08.3"),
),
pytest.raises(BackupInvalidError),
):
await manager.do_restore_partial(backup_instance) await manager.do_restore_partial(backup_instance)
@ -766,7 +777,11 @@ async def test_backup_to_local_with_default(
async def test_backup_to_default( async def test_backup_to_default(
coresys: CoreSys, tmp_supervisor_data, path_extern, mount_propagation, mock_is_mount coresys: CoreSys,
tmp_supervisor_data,
path_extern,
mount_propagation,
mock_is_mount,
): ):
"""Test making backup to default mount.""" """Test making backup to default mount."""
# Add a default backup mount # Add a default backup mount
@ -926,9 +941,15 @@ async def test_backup_with_healthcheck(
nonlocal _container_events_task nonlocal _container_events_task
_container_events_task = asyncio.create_task(container_events()) _container_events_task = asyncio.create_task(container_events())
with patch.object(DockerAddon, "run", new=container_events_task), patch.object( with (
AddonModel, "backup_mode", new=PropertyMock(return_value=AddonBackupMode.COLD) patch.object(DockerAddon, "run", new=container_events_task),
), patch.object(DockerAddon, "is_running", side_effect=[True, False, False]): patch.object(
AddonModel,
"backup_mode",
new=PropertyMock(return_value=AddonBackupMode.COLD),
),
patch.object(DockerAddon, "is_running", side_effect=[True, False, False]),
):
backup = await coresys.backups.do_backup_partial( backup = await coresys.backups.do_backup_partial(
homeassistant=False, addons=["local_ssh"] homeassistant=False, addons=["local_ssh"]
) )
@ -1000,10 +1021,11 @@ async def test_restore_with_healthcheck(
nonlocal _container_events_task nonlocal _container_events_task
_container_events_task = asyncio.create_task(container_events()) _container_events_task = asyncio.create_task(container_events())
with patch.object(DockerAddon, "run", new=container_events_task), patch.object( with (
DockerAddon, "is_running", return_value=False patch.object(DockerAddon, "run", new=container_events_task),
), patch.object(AddonModel, "_validate_availability"), patch.object( patch.object(DockerAddon, "is_running", return_value=False),
Addon, "with_ingress", new=PropertyMock(return_value=False) patch.object(AddonModel, "_validate_availability"),
patch.object(Addon, "with_ingress", new=PropertyMock(return_value=False)),
): ):
await coresys.backups.do_restore_partial(backup, addons=["local_ssh"]) await coresys.backups.do_restore_partial(backup, addons=["local_ssh"])
@ -1054,16 +1076,22 @@ async def test_backup_progress(
coresys.core.state = CoreState.RUNNING coresys.core.state = CoreState.RUNNING
coresys.hardware.disk.get_disk_free_space = lambda x: 5000 coresys.hardware.disk.get_disk_free_space = lambda x: 5000
with patch.object( with (
AddonModel, "backup_mode", new=PropertyMock(return_value=AddonBackupMode.COLD) patch.object(
), patch("supervisor.addons.addon.asyncio.Event.wait"): AddonModel,
"backup_mode",
new=PropertyMock(return_value=AddonBackupMode.COLD),
),
patch("supervisor.addons.addon.asyncio.Event.wait"),
):
full_backup: Backup = await coresys.backups.do_backup_full() full_backup: Backup = await coresys.backups.do_backup_full()
await asyncio.sleep(0) await asyncio.sleep(0)
messages = [ messages = [
call.args[0] call.args[0]
for call in ha_ws_client.async_send_command.call_args_list for call in ha_ws_client.async_send_command.call_args_list
if call.args[0]["data"].get("data", {}).get("name") if call.args[0]["type"] == WSType.SUPERVISOR_EVENT
and call.args[0]["data"].get("data", {}).get("name")
== "backup_manager_full_backup" == "backup_manager_full_backup"
] ]
assert messages == [ assert messages == [
@ -1075,10 +1103,10 @@ async def test_backup_progress(
_make_backup_message_for_assert( _make_backup_message_for_assert(
reference=full_backup.slug, stage="docker_config" reference=full_backup.slug, stage="docker_config"
), ),
_make_backup_message_for_assert(reference=full_backup.slug, stage="addons"),
_make_backup_message_for_assert( _make_backup_message_for_assert(
reference=full_backup.slug, stage="home_assistant" reference=full_backup.slug, stage="home_assistant"
), ),
_make_backup_message_for_assert(reference=full_backup.slug, stage="addons"),
_make_backup_message_for_assert(reference=full_backup.slug, stage="folders"), _make_backup_message_for_assert(reference=full_backup.slug, stage="folders"),
_make_backup_message_for_assert( _make_backup_message_for_assert(
reference=full_backup.slug, stage="finishing_file" reference=full_backup.slug, stage="finishing_file"
@ -1100,7 +1128,8 @@ async def test_backup_progress(
messages = [ messages = [
call.args[0] call.args[0]
for call in ha_ws_client.async_send_command.call_args_list for call in ha_ws_client.async_send_command.call_args_list
if call.args[0]["data"].get("data", {}).get("name") if call.args[0]["type"] == WSType.SUPERVISOR_EVENT
and call.args[0]["data"].get("data", {}).get("name")
== "backup_manager_partial_backup" == "backup_manager_partial_backup"
] ]
assert messages == [ assert messages == [
@ -1162,18 +1191,21 @@ async def test_restore_progress(
# Install another addon to be uninstalled # Install another addon to be uninstalled
request.getfixturevalue("install_addon_example") request.getfixturevalue("install_addon_example")
with patch("supervisor.addons.addon.asyncio.Event.wait"), patch.object( with (
HomeAssistant, "restore" patch("supervisor.addons.addon.asyncio.Event.wait"),
), patch.object(HomeAssistantCore, "update"), patch.object( patch.object(HomeAssistant, "restore"),
AddonModel, "_validate_availability" patch.object(HomeAssistantCore, "update"),
), patch.object(AddonModel, "with_ingress", new=PropertyMock(return_value=False)): patch.object(AddonModel, "_validate_availability"),
patch.object(AddonModel, "with_ingress", new=PropertyMock(return_value=False)),
):
await coresys.backups.do_restore_full(full_backup) await coresys.backups.do_restore_full(full_backup)
await asyncio.sleep(0) await asyncio.sleep(0)
messages = [ messages = [
call.args[0] call.args[0]
for call in ha_ws_client.async_send_command.call_args_list for call in ha_ws_client.async_send_command.call_args_list
if call.args[0]["data"].get("data", {}).get("name") if call.args[0]["type"] == WSType.SUPERVISOR_EVENT
and call.args[0]["data"].get("data", {}).get("name")
== "backup_manager_full_restore" == "backup_manager_full_restore"
] ]
assert messages == [ assert messages == [
@ -1242,7 +1274,8 @@ async def test_restore_progress(
messages = [ messages = [
call.args[0] call.args[0]
for call in ha_ws_client.async_send_command.call_args_list for call in ha_ws_client.async_send_command.call_args_list
if call.args[0]["data"].get("data", {}).get("name") if call.args[0]["type"] == WSType.SUPERVISOR_EVENT
and call.args[0]["data"].get("data", {}).get("name")
== "backup_manager_partial_restore" == "backup_manager_partial_restore"
] ]
assert messages == [ assert messages == [
@ -1277,8 +1310,9 @@ async def test_restore_progress(
addon_backup: Backup = await coresys.backups.do_backup_partial(addons=["local_ssh"]) addon_backup: Backup = await coresys.backups.do_backup_partial(addons=["local_ssh"])
ha_ws_client.async_send_command.reset_mock() ha_ws_client.async_send_command.reset_mock()
with patch.object(AddonModel, "_validate_availability"), patch.object( with (
HomeAssistantCore, "start" patch.object(AddonModel, "_validate_availability"),
patch.object(HomeAssistantCore, "start"),
): ):
await coresys.backups.do_restore_partial(addon_backup, addons=["local_ssh"]) await coresys.backups.do_restore_partial(addon_backup, addons=["local_ssh"])
await asyncio.sleep(0) await asyncio.sleep(0)
@ -1286,7 +1320,8 @@ async def test_restore_progress(
messages = [ messages = [
call.args[0] call.args[0]
for call in ha_ws_client.async_send_command.call_args_list for call in ha_ws_client.async_send_command.call_args_list
if call.args[0]["data"].get("data", {}).get("name") if call.args[0]["type"] == WSType.SUPERVISOR_EVENT
and call.args[0]["data"].get("data", {}).get("name")
== "backup_manager_partial_restore" == "backup_manager_partial_restore"
] ]
assert messages == [ assert messages == [
@ -1338,10 +1373,13 @@ async def test_freeze_thaw(
container.exec_run.return_value = (0, None) container.exec_run.return_value = (0, None)
ha_ws_client.ha_version = AwesomeVersion("2022.1.0") ha_ws_client.ha_version = AwesomeVersion("2022.1.0")
with patch.object( with (
AddonModel, "backup_pre", new=PropertyMock(return_value="pre_backup") patch.object(
), patch.object( AddonModel, "backup_pre", new=PropertyMock(return_value="pre_backup")
AddonModel, "backup_post", new=PropertyMock(return_value="post_backup") ),
patch.object(
AddonModel, "backup_post", new=PropertyMock(return_value="post_backup")
),
): ):
# Run the freeze # Run the freeze
await coresys.backups.freeze_all() await coresys.backups.freeze_all()
@ -1465,11 +1503,12 @@ async def test_restore_only_reloads_ingress_on_change(
async def mock_is_running(*_) -> bool: async def mock_is_running(*_) -> bool:
return True return True
with patch.object( with (
HomeAssistantCore, "is_running", new=mock_is_running patch.object(HomeAssistantCore, "is_running", new=mock_is_running),
), patch.object(AddonModel, "_validate_availability"), patch.object( patch.object(AddonModel, "_validate_availability"),
DockerAddon, "attach" patch.object(DockerAddon, "attach"),
), patch.object(HomeAssistantAPI, "make_request") as make_request: patch.object(HomeAssistantAPI, "make_request") as make_request,
):
make_request.return_value.__aenter__.return_value.status = 200 make_request.return_value.__aenter__.return_value.status = 200
# Has ingress before and after - not called # Has ingress before and after - not called
@ -1518,8 +1557,9 @@ async def test_restore_new_addon(
await coresys.addons.uninstall("local_example") await coresys.addons.uninstall("local_example")
assert "local_example" not in coresys.addons.local assert "local_example" not in coresys.addons.local
with patch.object(AddonModel, "_validate_availability"), patch.object( with (
DockerAddon, "attach" patch.object(AddonModel, "_validate_availability"),
patch.object(DockerAddon, "attach"),
): ):
assert await coresys.backups.do_restore_partial( assert await coresys.backups.do_restore_partial(
backup, addons=["local_example"] backup, addons=["local_example"]
@ -1554,8 +1594,9 @@ async def test_restore_preserves_data_config(
assert install_addon_example.path_config.exists() assert install_addon_example.path_config.exists()
assert test_config2.exists() assert test_config2.exists()
with patch.object(AddonModel, "_validate_availability"), patch.object( with (
DockerAddon, "attach" patch.object(AddonModel, "_validate_availability"),
patch.object(DockerAddon, "attach"),
): ):
assert await coresys.backups.do_restore_partial( assert await coresys.backups.do_restore_partial(
backup, addons=["local_example"] backup, addons=["local_example"]
@ -1660,8 +1701,9 @@ async def test_skip_homeassistant_database(
write_json_file(test_db, {"hello": "world"}) write_json_file(test_db, {"hello": "world"})
write_json_file(test_db_wal, {"hello": "world"}) write_json_file(test_db_wal, {"hello": "world"})
with patch.object(HomeAssistantCore, "update"), patch.object( with (
HomeAssistantCore, "start" patch.object(HomeAssistantCore, "update"),
patch.object(HomeAssistantCore, "start"),
): ):
await coresys.backups.do_restore_partial(backup, homeassistant=True) await coresys.backups.do_restore_partial(backup, homeassistant=True)
@ -1735,8 +1777,9 @@ async def test_reload_error(
) )
mock_is_mount.return_value = False mock_is_mount.return_value = False
with patch("supervisor.backups.manager.Path.is_dir", new=mock_is_dir), patch( with (
"supervisor.backups.manager.Path.glob", return_value=[] patch("supervisor.backups.manager.Path.is_dir", new=mock_is_dir),
patch("supervisor.backups.manager.Path.glob", return_value=[]),
): ):
err.errno = errno.EBUSY err.errno = errno.EBUSY
await coresys.backups.reload() await coresys.backups.reload()
@ -1787,3 +1830,39 @@ async def test_monitoring_after_partial_restore(
backup_instance.restore_addons.assert_called_once_with([TEST_ADDON_SLUG]) backup_instance.restore_addons.assert_called_once_with([TEST_ADDON_SLUG])
assert coresys.core.state == CoreState.RUNNING assert coresys.core.state == CoreState.RUNNING
coresys.docker.unload.assert_not_called() coresys.docker.unload.assert_not_called()
@pytest.mark.parametrize(
"pre_backup_error",
[
{
"code": "pre_backup_actions_failed",
"message": "Database migration in progress",
},
{"code": "unknown_command", "message": "Unknown command."},
],
)
async def test_core_pre_backup_actions_failed(
coresys: CoreSys,
ha_ws_client: AsyncMock,
caplog: pytest.LogCaptureFixture,
pre_backup_error: dict[str, str],
tmp_supervisor_data,
path_extern,
):
"""Test pre-backup actions failed in HA core stops backup."""
coresys.core.state = CoreState.RUNNING
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
ha_ws_client.ha_version = AwesomeVersion("2024.7.0")
ha_ws_client.async_send_command.return_value = {
"error": pre_backup_error,
"id": 1,
"success": False,
"type": "result",
}
assert not await coresys.backups.do_backup_full()
assert (
f"Preparing backup of Home Assistant Core failed due to: {pre_backup_error['message']}"
in caplog.text
)