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

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
5 changed files with 192 additions and 83 deletions

View File

@@ -31,6 +31,7 @@ from supervisor.exceptions import (
DockerError,
)
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.const import JobCondition
@@ -335,9 +336,14 @@ async def test_fail_invalid_full_backup(
backup_instance.protected = False
backup_instance.supervisor_version = "2022.08.4"
with patch.object(
type(coresys.supervisor), "version", new=PropertyMock(return_value="2022.08.3")
), pytest.raises(BackupInvalidError):
with (
patch.object(
type(coresys.supervisor),
"version",
new=PropertyMock(return_value="2022.08.3"),
),
pytest.raises(BackupInvalidError),
):
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)
backup_instance.supervisor_version = "2022.08.4"
with patch.object(
type(coresys.supervisor), "version", new=PropertyMock(return_value="2022.08.3")
), pytest.raises(BackupInvalidError):
with (
patch.object(
type(coresys.supervisor),
"version",
new=PropertyMock(return_value="2022.08.3"),
),
pytest.raises(BackupInvalidError),
):
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(
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."""
# Add a default backup mount
@@ -926,9 +941,15 @@ async def test_backup_with_healthcheck(
nonlocal _container_events_task
_container_events_task = asyncio.create_task(container_events())
with patch.object(DockerAddon, "run", new=container_events_task), patch.object(
AddonModel, "backup_mode", new=PropertyMock(return_value=AddonBackupMode.COLD)
), patch.object(DockerAddon, "is_running", side_effect=[True, False, False]):
with (
patch.object(DockerAddon, "run", new=container_events_task),
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(
homeassistant=False, addons=["local_ssh"]
)
@@ -1000,10 +1021,11 @@ async def test_restore_with_healthcheck(
nonlocal _container_events_task
_container_events_task = asyncio.create_task(container_events())
with patch.object(DockerAddon, "run", new=container_events_task), patch.object(
DockerAddon, "is_running", return_value=False
), patch.object(AddonModel, "_validate_availability"), patch.object(
Addon, "with_ingress", new=PropertyMock(return_value=False)
with (
patch.object(DockerAddon, "run", new=container_events_task),
patch.object(DockerAddon, "is_running", 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"])
@@ -1054,16 +1076,22 @@ async def test_backup_progress(
coresys.core.state = CoreState.RUNNING
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
with patch.object(
AddonModel, "backup_mode", new=PropertyMock(return_value=AddonBackupMode.COLD)
), patch("supervisor.addons.addon.asyncio.Event.wait"):
with (
patch.object(
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()
await asyncio.sleep(0)
messages = [
call.args[0]
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"
]
assert messages == [
@@ -1075,10 +1103,10 @@ async def test_backup_progress(
_make_backup_message_for_assert(
reference=full_backup.slug, stage="docker_config"
),
_make_backup_message_for_assert(reference=full_backup.slug, stage="addons"),
_make_backup_message_for_assert(
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="finishing_file"
@@ -1100,7 +1128,8 @@ async def test_backup_progress(
messages = [
call.args[0]
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"
]
assert messages == [
@@ -1162,18 +1191,21 @@ async def test_restore_progress(
# Install another addon to be uninstalled
request.getfixturevalue("install_addon_example")
with patch("supervisor.addons.addon.asyncio.Event.wait"), patch.object(
HomeAssistant, "restore"
), patch.object(HomeAssistantCore, "update"), patch.object(
AddonModel, "_validate_availability"
), patch.object(AddonModel, "with_ingress", new=PropertyMock(return_value=False)):
with (
patch("supervisor.addons.addon.asyncio.Event.wait"),
patch.object(HomeAssistant, "restore"),
patch.object(HomeAssistantCore, "update"),
patch.object(AddonModel, "_validate_availability"),
patch.object(AddonModel, "with_ingress", new=PropertyMock(return_value=False)),
):
await coresys.backups.do_restore_full(full_backup)
await asyncio.sleep(0)
messages = [
call.args[0]
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"
]
assert messages == [
@@ -1242,7 +1274,8 @@ async def test_restore_progress(
messages = [
call.args[0]
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"
]
assert messages == [
@@ -1277,8 +1310,9 @@ async def test_restore_progress(
addon_backup: Backup = await coresys.backups.do_backup_partial(addons=["local_ssh"])
ha_ws_client.async_send_command.reset_mock()
with patch.object(AddonModel, "_validate_availability"), patch.object(
HomeAssistantCore, "start"
with (
patch.object(AddonModel, "_validate_availability"),
patch.object(HomeAssistantCore, "start"),
):
await coresys.backups.do_restore_partial(addon_backup, addons=["local_ssh"])
await asyncio.sleep(0)
@@ -1286,7 +1320,8 @@ async def test_restore_progress(
messages = [
call.args[0]
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"
]
assert messages == [
@@ -1338,10 +1373,13 @@ async def test_freeze_thaw(
container.exec_run.return_value = (0, None)
ha_ws_client.ha_version = AwesomeVersion("2022.1.0")
with patch.object(
AddonModel, "backup_pre", new=PropertyMock(return_value="pre_backup")
), patch.object(
AddonModel, "backup_post", new=PropertyMock(return_value="post_backup")
with (
patch.object(
AddonModel, "backup_pre", new=PropertyMock(return_value="pre_backup")
),
patch.object(
AddonModel, "backup_post", new=PropertyMock(return_value="post_backup")
),
):
# Run the freeze
await coresys.backups.freeze_all()
@@ -1465,11 +1503,12 @@ async def test_restore_only_reloads_ingress_on_change(
async def mock_is_running(*_) -> bool:
return True
with patch.object(
HomeAssistantCore, "is_running", new=mock_is_running
), patch.object(AddonModel, "_validate_availability"), patch.object(
DockerAddon, "attach"
), patch.object(HomeAssistantAPI, "make_request") as make_request:
with (
patch.object(HomeAssistantCore, "is_running", new=mock_is_running),
patch.object(AddonModel, "_validate_availability"),
patch.object(DockerAddon, "attach"),
patch.object(HomeAssistantAPI, "make_request") as make_request,
):
make_request.return_value.__aenter__.return_value.status = 200
# Has ingress before and after - not called
@@ -1518,8 +1557,9 @@ async def test_restore_new_addon(
await coresys.addons.uninstall("local_example")
assert "local_example" not in coresys.addons.local
with patch.object(AddonModel, "_validate_availability"), patch.object(
DockerAddon, "attach"
with (
patch.object(AddonModel, "_validate_availability"),
patch.object(DockerAddon, "attach"),
):
assert await coresys.backups.do_restore_partial(
backup, addons=["local_example"]
@@ -1554,8 +1594,9 @@ async def test_restore_preserves_data_config(
assert install_addon_example.path_config.exists()
assert test_config2.exists()
with patch.object(AddonModel, "_validate_availability"), patch.object(
DockerAddon, "attach"
with (
patch.object(AddonModel, "_validate_availability"),
patch.object(DockerAddon, "attach"),
):
assert await coresys.backups.do_restore_partial(
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_wal, {"hello": "world"})
with patch.object(HomeAssistantCore, "update"), patch.object(
HomeAssistantCore, "start"
with (
patch.object(HomeAssistantCore, "update"),
patch.object(HomeAssistantCore, "start"),
):
await coresys.backups.do_restore_partial(backup, homeassistant=True)
@@ -1735,8 +1777,9 @@ async def test_reload_error(
)
mock_is_mount.return_value = False
with patch("supervisor.backups.manager.Path.is_dir", new=mock_is_dir), patch(
"supervisor.backups.manager.Path.glob", return_value=[]
with (
patch("supervisor.backups.manager.Path.is_dir", new=mock_is_dir),
patch("supervisor.backups.manager.Path.glob", return_value=[]),
):
err.errno = errno.EBUSY
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])
assert coresys.core.state == CoreState.RUNNING
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
)