mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Check for errors when restoring backups using supervisor (#137217)
* Check for errors when restoring backups using supervisor * Break long line in test * Improve comments
This commit is contained in:
parent
0b2b222fca
commit
9cfe109210
@ -517,17 +517,22 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
|||||||
raise HomeAssistantError(message) from err
|
raise HomeAssistantError(message) from err
|
||||||
|
|
||||||
restore_complete = asyncio.Event()
|
restore_complete = asyncio.Event()
|
||||||
|
restore_errors: list[dict[str, str]] = []
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def on_job_progress(data: Mapping[str, Any]) -> None:
|
def on_job_progress(data: Mapping[str, Any]) -> None:
|
||||||
"""Handle backup restore progress."""
|
"""Handle backup restore progress."""
|
||||||
if data.get("done") is True:
|
if data.get("done") is True:
|
||||||
restore_complete.set()
|
restore_complete.set()
|
||||||
|
restore_errors.extend(data.get("errors", []))
|
||||||
|
|
||||||
unsub = self._async_listen_job_events(job.job_id, on_job_progress)
|
unsub = self._async_listen_job_events(job.job_id, on_job_progress)
|
||||||
try:
|
try:
|
||||||
await self._get_job_state(job.job_id, on_job_progress)
|
await self._get_job_state(job.job_id, on_job_progress)
|
||||||
await restore_complete.wait()
|
await restore_complete.wait()
|
||||||
|
if restore_errors:
|
||||||
|
# We should add more specific error handling here in the future
|
||||||
|
raise BackupReaderWriterError(f"Restore failed: {restore_errors}")
|
||||||
finally:
|
finally:
|
||||||
unsub()
|
unsub()
|
||||||
|
|
||||||
@ -554,11 +559,23 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
on_progress(
|
restore_errors = data.get("errors", [])
|
||||||
RestoreBackupEvent(
|
if restore_errors:
|
||||||
reason="", stage=None, state=RestoreBackupState.COMPLETED
|
_LOGGER.warning("Restore backup failed: %s", restore_errors)
|
||||||
|
# We should add more specific error handling here in the future
|
||||||
|
on_progress(
|
||||||
|
RestoreBackupEvent(
|
||||||
|
reason="unknown_error",
|
||||||
|
stage=None,
|
||||||
|
state=RestoreBackupState.FAILED,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
on_progress(
|
||||||
|
RestoreBackupEvent(
|
||||||
|
reason="", stage=None, state=RestoreBackupState.COMPLETED
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
on_progress(IdleEvent())
|
on_progress(IdleEvent())
|
||||||
unsub()
|
unsub()
|
||||||
|
|
||||||
|
@ -324,6 +324,24 @@ TEST_JOB_DONE = supervisor_jobs.Job(
|
|||||||
errors=[],
|
errors=[],
|
||||||
child_jobs=[],
|
child_jobs=[],
|
||||||
)
|
)
|
||||||
|
TEST_RESTORE_JOB_DONE_WITH_ERROR = supervisor_jobs.Job(
|
||||||
|
name="backup_manager_partial_restore",
|
||||||
|
reference="1ef41507",
|
||||||
|
uuid=UUID(TEST_JOB_ID),
|
||||||
|
progress=0.0,
|
||||||
|
stage="copy_additional_locations",
|
||||||
|
done=True,
|
||||||
|
errors=[
|
||||||
|
supervisor_jobs.JobError(
|
||||||
|
type="BackupInvalidError",
|
||||||
|
message=(
|
||||||
|
"Backup was made on supervisor version 2025.02.2.dev3105, "
|
||||||
|
"can't restore on 2025.01.2.dev3105"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
child_jobs=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
@ -1946,6 +1964,97 @@ async def test_reader_writer_restore_error(
|
|||||||
assert response["error"]["code"] == expected_error_code
|
assert response["error"]["code"] == expected_error_code
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("hassio_client", "setup_integration")
|
||||||
|
async def test_reader_writer_restore_late_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
supervisor_client: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test restoring a backup with error."""
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
supervisor_client.backups.partial_restore.return_value.job_id = TEST_JOB_ID
|
||||||
|
supervisor_client.backups.list.return_value = [TEST_BACKUP]
|
||||||
|
supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS
|
||||||
|
supervisor_client.jobs.get_job.return_value = TEST_JOB_NOT_DONE
|
||||||
|
|
||||||
|
await client.send_json_auto_id({"type": "backup/subscribe_events"})
|
||||||
|
response = await client.receive_json()
|
||||||
|
assert response["event"] == {"manager_state": "idle"}
|
||||||
|
response = await client.receive_json()
|
||||||
|
assert response["success"]
|
||||||
|
|
||||||
|
await client.send_json_auto_id(
|
||||||
|
{"type": "backup/restore", "agent_id": "hassio.local", "backup_id": "abc123"}
|
||||||
|
)
|
||||||
|
response = await client.receive_json()
|
||||||
|
assert response["event"] == {
|
||||||
|
"manager_state": "restore_backup",
|
||||||
|
"reason": None,
|
||||||
|
"stage": None,
|
||||||
|
"state": "in_progress",
|
||||||
|
}
|
||||||
|
|
||||||
|
supervisor_client.backups.partial_restore.assert_called_once_with(
|
||||||
|
"abc123",
|
||||||
|
supervisor_backups.PartialRestoreOptions(
|
||||||
|
addons=None,
|
||||||
|
background=True,
|
||||||
|
folders=None,
|
||||||
|
homeassistant=True,
|
||||||
|
location=None,
|
||||||
|
password=None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
event = {
|
||||||
|
"event": "job",
|
||||||
|
"data": {
|
||||||
|
"name": "backup_manager_partial_restore",
|
||||||
|
"reference": "7c54aeed",
|
||||||
|
"uuid": TEST_JOB_ID,
|
||||||
|
"progress": 0,
|
||||||
|
"stage": None,
|
||||||
|
"done": True,
|
||||||
|
"parent_id": None,
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"type": "BackupInvalidError",
|
||||||
|
"message": (
|
||||||
|
"Backup was made on supervisor version 2025.02.2.dev3105, can't"
|
||||||
|
" restore on 2025.01.2.dev3105. Must update supervisor first."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"created": "2025-02-03T08:27:49.297997+00:00",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await client.send_json_auto_id({"type": "supervisor/event", "data": event})
|
||||||
|
response = await client.receive_json()
|
||||||
|
assert response["success"]
|
||||||
|
|
||||||
|
response = await client.receive_json()
|
||||||
|
assert response["event"] == {
|
||||||
|
"manager_state": "restore_backup",
|
||||||
|
"reason": "backup_reader_writer_error",
|
||||||
|
"stage": None,
|
||||||
|
"state": "failed",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await client.receive_json()
|
||||||
|
assert response["event"] == {"manager_state": "idle"}
|
||||||
|
|
||||||
|
response = await client.receive_json()
|
||||||
|
assert not response["success"]
|
||||||
|
assert response["error"] == {
|
||||||
|
"code": "home_assistant_error",
|
||||||
|
"message": (
|
||||||
|
"Restore failed: [{'type': 'BackupInvalidError', 'message': \"Backup "
|
||||||
|
"was made on supervisor version 2025.02.2.dev3105, can't restore on "
|
||||||
|
'2025.01.2.dev3105. Must update supervisor first."}]'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("backup", "backup_details", "parameters", "expected_error"),
|
("backup", "backup_details", "parameters", "expected_error"),
|
||||||
[
|
[
|
||||||
@ -1999,15 +2108,40 @@ async def test_reader_writer_restore_wrong_parameters(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("get_job_result", "last_non_idle_event"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
TEST_JOB_DONE,
|
||||||
|
{
|
||||||
|
"manager_state": "restore_backup",
|
||||||
|
"reason": "",
|
||||||
|
"stage": None,
|
||||||
|
"state": "completed",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TEST_RESTORE_JOB_DONE_WITH_ERROR,
|
||||||
|
{
|
||||||
|
"manager_state": "restore_backup",
|
||||||
|
"reason": "unknown_error",
|
||||||
|
"stage": None,
|
||||||
|
"state": "failed",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
@pytest.mark.usefixtures("hassio_client")
|
@pytest.mark.usefixtures("hassio_client")
|
||||||
async def test_restore_progress_after_restart(
|
async def test_restore_progress_after_restart(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
supervisor_client: AsyncMock,
|
supervisor_client: AsyncMock,
|
||||||
|
get_job_result: supervisor_jobs.Job,
|
||||||
|
last_non_idle_event: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test restore backup progress after restart."""
|
"""Test restore backup progress after restart."""
|
||||||
|
|
||||||
supervisor_client.jobs.get_job.return_value = TEST_JOB_DONE
|
supervisor_client.jobs.get_job.return_value = get_job_result
|
||||||
|
|
||||||
with patch.dict(os.environ, MOCK_ENVIRON | {RESTORE_JOB_ID_ENV: TEST_JOB_ID}):
|
with patch.dict(os.environ, MOCK_ENVIRON | {RESTORE_JOB_ID_ENV: TEST_JOB_ID}):
|
||||||
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
|
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
|
||||||
@ -2018,12 +2152,7 @@ async def test_restore_progress_after_restart(
|
|||||||
response = await client.receive_json()
|
response = await client.receive_json()
|
||||||
|
|
||||||
assert response["success"]
|
assert response["success"]
|
||||||
assert response["result"]["last_non_idle_event"] == {
|
assert response["result"]["last_non_idle_event"] == last_non_idle_event
|
||||||
"manager_state": "restore_backup",
|
|
||||||
"reason": "",
|
|
||||||
"stage": None,
|
|
||||||
"state": "completed",
|
|
||||||
}
|
|
||||||
assert response["result"]["state"] == "idle"
|
assert response["result"]["state"] == "idle"
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user