Report progress while creating supervisor backup (#137301)

* Report progress while creating supervisor backup

* Use enum util
pull/137390/head
Erik Montnemery 2025-02-04 11:36:03 +01:00 committed by Franck Nijhof
parent 0766b47161
commit 13bfa82038
No known key found for this signature in database
GPG Key ID: D62583BA8AB11CA3
3 changed files with 124 additions and 0 deletions

View File

@ -26,6 +26,8 @@ from .manager import (
BackupReaderWriterError, BackupReaderWriterError,
CoreBackupReaderWriter, CoreBackupReaderWriter,
CreateBackupEvent, CreateBackupEvent,
CreateBackupStage,
CreateBackupState,
IdleEvent, IdleEvent,
IncorrectPasswordError, IncorrectPasswordError,
ManagerBackup, ManagerBackup,
@ -49,6 +51,8 @@ __all__ = [
"BackupReaderWriter", "BackupReaderWriter",
"BackupReaderWriterError", "BackupReaderWriterError",
"CreateBackupEvent", "CreateBackupEvent",
"CreateBackupStage",
"CreateBackupState",
"Folder", "Folder",
"IdleEvent", "IdleEvent",
"IncorrectPasswordError", "IncorrectPasswordError",

View File

@ -30,6 +30,8 @@ from homeassistant.components.backup import (
BackupReaderWriter, BackupReaderWriter,
BackupReaderWriterError, BackupReaderWriterError,
CreateBackupEvent, CreateBackupEvent,
CreateBackupStage,
CreateBackupState,
Folder, Folder,
IdleEvent, IdleEvent,
IncorrectPasswordError, IncorrectPasswordError,
@ -47,6 +49,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.enum import try_parse_enum
from .const import DOMAIN, EVENT_SUPERVISOR_EVENT from .const import DOMAIN, EVENT_SUPERVISOR_EVENT
from .handler import get_supervisor_client from .handler import get_supervisor_client
@ -336,6 +339,7 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
self._async_wait_for_backup( self._async_wait_for_backup(
backup, backup,
locations, locations,
on_progress=on_progress,
remove_after_upload=locations == [LOCATION_CLOUD_BACKUP], remove_after_upload=locations == [LOCATION_CLOUD_BACKUP],
), ),
name="backup_manager_create_backup", name="backup_manager_create_backup",
@ -349,6 +353,7 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
backup: supervisor_backups.NewBackup, backup: supervisor_backups.NewBackup,
locations: list[str | None], locations: list[str | None],
*, *,
on_progress: Callable[[CreateBackupEvent], None],
remove_after_upload: bool, remove_after_upload: bool,
) -> WrittenBackup: ) -> WrittenBackup:
"""Wait for a backup to complete.""" """Wait for a backup to complete."""
@ -360,6 +365,14 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
def on_job_progress(data: Mapping[str, Any]) -> None: def on_job_progress(data: Mapping[str, Any]) -> None:
"""Handle backup progress.""" """Handle backup progress."""
nonlocal backup_id nonlocal backup_id
if not (stage := try_parse_enum(CreateBackupStage, data.get("stage"))):
_LOGGER.debug("Unknown create stage: %s", data.get("stage"))
else:
on_progress(
CreateBackupEvent(
reason=None, stage=stage, state=CreateBackupState.IN_PROGRESS
)
)
if data.get("done") is True: if data.get("done") is True:
backup_id = data.get("reference") backup_id = data.get("reference")
create_errors.extend(data.get("errors", [])) create_errors.extend(data.get("errors", []))

View File

@ -1002,6 +1002,113 @@ async def test_reader_writer_create(
assert response["event"] == {"manager_state": "idle"} assert response["event"] == {"manager_state": "idle"}
@pytest.mark.usefixtures("hassio_client", "setup_integration")
async def test_reader_writer_create_report_progress(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
freezer: FrozenDateTimeFactory,
supervisor_client: AsyncMock,
) -> None:
"""Test generating a backup."""
client = await hass_ws_client(hass)
freezer.move_to("2025-01-30 13:42:12.345678")
supervisor_client.backups.partial_backup.return_value.job_id = TEST_JOB_ID
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/generate", "agent_ids": ["hassio.local"], "name": "Test"}
)
response = await client.receive_json()
assert response["event"] == {
"manager_state": "create_backup",
"reason": None,
"stage": None,
"state": "in_progress",
}
response = await client.receive_json()
assert response["success"]
assert response["result"] == {"backup_job_id": TEST_JOB_ID}
supervisor_client.backups.partial_backup.assert_called_once_with(
DEFAULT_BACKUP_OPTIONS
)
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": False, "stage": "addons"},
supervisor_event_base | {"done": True, "stage": "finishing_file"},
]
expected_manager_events = [
"addon_repositories",
"home_assistant",
"addons",
"finishing_file",
]
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": "create_backup",
"reason": None,
"stage": expected_manager_events[i],
"state": "in_progress",
}
response = await client.receive_json()
assert response["event"] == {
"manager_state": "create_backup",
"reason": None,
"stage": "upload_to_agents",
"state": "in_progress",
}
response = await client.receive_json()
assert response["event"] == {
"manager_state": "create_backup",
"reason": None,
"stage": None,
"state": "completed",
}
supervisor_client.backups.download_backup.assert_not_called()
supervisor_client.backups.remove_backup.assert_not_called()
response = await client.receive_json()
assert response["event"] == {"manager_state": "idle"}
@pytest.mark.usefixtures("hassio_client", "setup_integration") @pytest.mark.usefixtures("hassio_client", "setup_integration")
async def test_reader_writer_create_job_done( async def test_reader_writer_create_job_done(
hass: HomeAssistant, hass: HomeAssistant,