Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report progress while restoring supervisor backup #137313

Merged
merged 1 commit into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions homeassistant/components/backup/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
ManagerBackup,
NewBackup,
RestoreBackupEvent,
RestoreBackupStage,
RestoreBackupState,
WrittenBackup,
)
Expand Down Expand Up @@ -60,6 +61,7 @@
"ManagerBackup",
"NewBackup",
"RestoreBackupEvent",
"RestoreBackupStage",
"RestoreBackupState",
"WrittenBackup",
"async_get_manager",
Expand Down
32 changes: 26 additions & 6 deletions homeassistant/components/hassio/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
ManagerBackup,
NewBackup,
RestoreBackupEvent,
RestoreBackupStage,
RestoreBackupState,
WrittenBackup,
async_get_manager as async_get_backup_manager,
Expand Down Expand Up @@ -540,6 +541,14 @@ async def async_restore_backup(
@callback
def on_job_progress(data: Mapping[str, Any]) -> None:
"""Handle backup restore progress."""
if not (stage := try_parse_enum(RestoreBackupStage, data.get("stage"))):
_LOGGER.debug("Unknown restore stage: %s", data.get("stage"))
else:
on_progress(
RestoreBackupEvent(
reason=None, stage=stage, state=RestoreBackupState.IN_PROGRESS
)
)
if data.get("done") is True:
restore_complete.set()
restore_errors.extend(data.get("errors", []))
Expand All @@ -566,15 +575,26 @@ async def async_resume_restore_progress_after_restart(

_LOGGER.debug("Found restore job ID %s in environment", restore_job_id)

sent_event = False

@callback
def on_job_progress(data: Mapping[str, Any]) -> None:
"""Handle backup restore progress."""
nonlocal sent_event

if not (stage := try_parse_enum(RestoreBackupStage, data.get("stage"))):
_LOGGER.debug("Unknown restore stage: %s", data.get("stage"))

if data.get("done") is not True:
on_progress(
RestoreBackupEvent(
reason="", stage=None, state=RestoreBackupState.IN_PROGRESS
if stage or not sent_event:
sent_event = True
on_progress(
RestoreBackupEvent(
reason=None,
stage=stage,
state=RestoreBackupState.IN_PROGRESS,
)
)
)
return

restore_errors = data.get("errors", [])
Expand All @@ -584,14 +604,14 @@ def on_job_progress(data: Mapping[str, Any]) -> None:
on_progress(
RestoreBackupEvent(
reason="unknown_error",
stage=None,
stage=stage,
state=RestoreBackupState.FAILED,
)
)
else:
on_progress(
RestoreBackupEvent(
reason="", stage=None, state=RestoreBackupState.COMPLETED
reason=None, stage=stage, state=RestoreBackupState.COMPLETED
)
)
on_progress(IdleEvent())
Expand Down
187 changes: 186 additions & 1 deletion tests/components/hassio/test_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2022,6 +2022,109 @@ async def test_reader_writer_restore(
assert response["result"] is None


@pytest.mark.usefixtures("hassio_client", "setup_integration")
async def test_reader_writer_restore_report_progress(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
supervisor_client: AsyncMock,
) -> None:
"""Test restoring a backup."""
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,
),
)

supervisor_event_base = {"uuid": TEST_JOB_ID, "reference": "test_slug"}
supervisor_events = [
supervisor_event_base | {"done": False, "stage": "addon_repositories"},
supervisor_event_base | {"done": False, "stage": None}, # Will be skipped
supervisor_event_base | {"done": False, "stage": "unknown"}, # Will be skipped
supervisor_event_base | {"done": False, "stage": "home_assistant"},
supervisor_event_base | {"done": True, "stage": "addons"},
]
expected_manager_events = [
"addon_repositories",
"home_assistant",
"addons",
]

for supervisor_event in supervisor_events:
await client.send_json_auto_id(
{
"type": "supervisor/event",
"data": {"event": "job", "data": supervisor_event},
}
)

acks = 0
events = []
for _ in range(len(supervisor_events) + len(expected_manager_events)):
response = await client.receive_json()
if "event" in response:
events.append(response)
continue
assert response["success"]
acks += 1

assert acks == len(supervisor_events)
assert len(events) == len(expected_manager_events)

for i, event in enumerate(events):
assert event["event"] == {
"manager_state": "restore_backup",
"reason": None,
"stage": expected_manager_events[i],
"state": "in_progress",
}

response = await client.receive_json()
assert response["event"] == {
"manager_state": "restore_backup",
"reason": None,
"stage": None,
"state": "completed",
}

response = await client.receive_json()
assert response["event"] == {"manager_state": "idle"}

response = await client.receive_json()
assert response["success"]
assert response["result"] is None


@pytest.mark.parametrize(
("supervisor_error_string", "expected_error_code", "expected_reason"),
[
Expand Down Expand Up @@ -2245,7 +2348,7 @@ async def test_reader_writer_restore_wrong_parameters(
TEST_JOB_DONE,
{
"manager_state": "restore_backup",
"reason": "",
"reason": None,
"stage": None,
"state": "completed",
},
Expand Down Expand Up @@ -2286,6 +2389,88 @@ async def test_restore_progress_after_restart(
assert response["result"]["state"] == "idle"


@pytest.mark.usefixtures("hassio_client")
async def test_restore_progress_after_restart_report_progress(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
supervisor_client: AsyncMock,
) -> None:
"""Test restore backup progress after restart."""

supervisor_client.jobs.get_job.return_value = TEST_JOB_NOT_DONE

with patch.dict(os.environ, MOCK_ENVIRON | {RESTORE_JOB_ID_ENV: TEST_JOB_ID}):
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})

client = await hass_ws_client(hass)

await client.send_json_auto_id({"type": "backup/subscribe_events"})
response = await client.receive_json()
assert response["event"] == {
"manager_state": "restore_backup",
"reason": None,
"stage": None,
"state": "in_progress",
}
response = await client.receive_json()
assert response["success"]

supervisor_event_base = {"uuid": TEST_JOB_ID, "reference": "test_slug"}
supervisor_events = [
supervisor_event_base | {"done": False, "stage": "addon_repositories"},
supervisor_event_base | {"done": False, "stage": None}, # Will be skipped
supervisor_event_base | {"done": False, "stage": "unknown"}, # Will be skipped
supervisor_event_base | {"done": False, "stage": "home_assistant"},
supervisor_event_base | {"done": True, "stage": "addons"},
]
expected_manager_events = ["addon_repositories", "home_assistant", "addons"]
expected_manager_states = ["in_progress", "in_progress", "completed"]

for supervisor_event in supervisor_events:
await client.send_json_auto_id(
{
"type": "supervisor/event",
"data": {"event": "job", "data": supervisor_event},
}
)

acks = 0
events = []
for _ in range(len(supervisor_events) + len(expected_manager_events)):
response = await client.receive_json()
if "event" in response:
events.append(response)
continue
assert response["success"]
acks += 1

assert acks == len(supervisor_events)
assert len(events) == len(expected_manager_events)

for i, event in enumerate(events):
assert event["event"] == {
"manager_state": "restore_backup",
"reason": None,
"stage": expected_manager_events[i],
"state": expected_manager_states[i],
}

response = await client.receive_json()
assert response["event"] == {"manager_state": "idle"}

await client.send_json_auto_id({"type": "backup/info"})
response = await client.receive_json()

assert response["success"]
assert response["result"]["last_non_idle_event"] == {
"manager_state": "restore_backup",
"reason": None,
"stage": "addons",
"state": "completed",
}
assert response["result"]["state"] == "idle"


@pytest.mark.usefixtures("hassio_client")
async def test_restore_progress_after_restart_unknown_job(
hass: HomeAssistant,
Expand Down
Loading