Include backup agent error in response to WS command backup/info (#130884)

This commit is contained in:
Erik Montnemery
2024-11-18 18:48:21 +01:00
committed by GitHub
parent a47a70df52
commit c5757b57c3
6 changed files with 107 additions and 11 deletions

View File

@@ -8,10 +8,21 @@ from pathlib import Path
from typing import Any, Protocol
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .models import BackupUploadMetadata, BaseBackup
class BackupAgentError(HomeAssistantError):
"""Base class for backup agent errors."""
class BackupAgentUnreachableError(BackupAgentError):
"""Raised when the agent can't reach it's API."""
_message = "The backup agent is unreachable."
@dataclass(slots=True)
class UploadedBackup(BaseBackup):
"""Uploaded backup class."""

View File

@@ -29,7 +29,12 @@ from homeassistant.helpers import integration_platform
from homeassistant.helpers.json import json_bytes
from homeassistant.util import dt as dt_util
from .agent import BackupAgent, BackupAgentPlatformProtocol, LocalBackupAgent
from .agent import (
BackupAgent,
BackupAgentError,
BackupAgentPlatformProtocol,
LocalBackupAgent,
)
from .const import (
BUF_SIZE,
DOMAIN,
@@ -207,7 +212,9 @@ class BaseBackupManager(abc.ABC, Generic[_BackupT]):
"""
@abc.abstractmethod
async def async_get_backups(self, **kwargs: Any) -> dict[str, _BackupT]:
async def async_get_backups(
self, **kwargs: Any
) -> tuple[dict[str, Backup], dict[str, Exception]]:
"""Get backups.
Return a dictionary of Backup instances keyed by their slug.
@@ -275,12 +282,25 @@ class BackupManager(BaseBackupManager[Backup]):
finally:
self.syncing = False
async def async_get_backups(self, **kwargs: Any) -> dict[str, Backup]:
async def async_get_backups(
self, **kwargs: Any
) -> tuple[dict[str, Backup], dict[str, Exception]]:
"""Return backups."""
backups: dict[str, Backup] = {}
for agent_id, agent in self.backup_agents.items():
agent_backups = await agent.async_list_backups()
for agent_backup in agent_backups:
agent_errors: dict[str, Exception] = {}
agent_ids = list(self.backup_agents.keys())
list_backups_results = await asyncio.gather(
*(agent.async_list_backups() for agent in self.backup_agents.values()),
return_exceptions=True,
)
for idx, result in enumerate(list_backups_results):
if isinstance(result, BackupAgentError):
agent_errors[agent_ids[idx]] = result
continue
if isinstance(result, BaseException):
raise result
for agent_backup in result:
if agent_backup.slug not in backups:
backups[agent_backup.slug] = Backup(
slug=agent_backup.slug,
@@ -290,9 +310,9 @@ class BackupManager(BaseBackupManager[Backup]):
size=agent_backup.size,
protected=agent_backup.protected,
)
backups[agent_backup.slug].agent_ids.append(agent_id)
backups[agent_backup.slug].agent_ids.append(agent_ids[idx])
return backups
return (backups, agent_errors)
async def async_get_backup(self, *, slug: str, **kwargs: Any) -> Backup | None:
"""Return a backup."""

View File

@@ -40,10 +40,13 @@ async def handle_info(
) -> None:
"""List all stored backups."""
manager = hass.data[DATA_MANAGER]
backups = await manager.async_get_backups()
backups, agent_errors = await manager.async_get_backups()
connection.send_result(
msg["id"],
{
"agent_errors": {
agent_id: str(err) for agent_id, err in agent_errors.items()
},
"backups": [b.as_dict() for b in backups.values()],
"backing_up": manager.backup_task is not None,
},

View File

@@ -439,6 +439,8 @@
dict({
'id': 1,
'result': dict({
'agent_errors': dict({
}),
'backing_up': False,
'backups': list([
dict({
@@ -457,6 +459,42 @@
'type': 'result',
})
# ---
# name: test_info_with_errors[BackupAgentUnreachableError]
dict({
'id': 1,
'result': dict({
'agent_errors': dict({
'domain.test': 'The backup agent is unreachable.',
}),
'backing_up': False,
'backups': list([
dict({
'agent_ids': list([
'backup.local',
]),
'date': '1970-01-01T00:00:00.000Z',
'name': 'Test',
'protected': False,
'size': 0.0,
'slug': 'abc123',
}),
]),
}),
'success': True,
'type': 'result',
})
# ---
# name: test_info_with_errors[side_effect0]
dict({
'error': dict({
'code': 'home_assistant_error',
'message': 'Boom!',
}),
'id': 1,
'success': False,
'type': 'result',
})
# ---
# name: test_remove[with_hassio]
dict({
'error': dict({

View File

@@ -143,8 +143,9 @@ async def test_load_backups(hass: HomeAssistant) -> None:
),
):
await manager.backup_agents[LOCAL_AGENT_ID].load_backups()
backups = await manager.async_get_backups()
backups, agent_errors = await manager.async_get_backups()
assert backups == {TEST_BACKUP.slug: TEST_BACKUP}
assert agent_errors == {}
async def test_load_backups_with_exception(
@@ -162,9 +163,10 @@ async def test_load_backups_with_exception(
patch("tarfile.open", side_effect=OSError("Test exception")),
):
await manager.backup_agents[LOCAL_AGENT_ID].load_backups()
backups = await manager.async_get_backups()
backups, agent_errors = await manager.async_get_backups()
assert f"Unable to read backup {TEST_BACKUP_PATH}: Test exception" in caplog.text
assert backups == {}
assert agent_errors == {}
async def test_removing_backup(

View File

@@ -9,6 +9,7 @@ import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.backup import BaseBackup
from homeassistant.components.backup.agent import BackupAgentUnreachableError
from homeassistant.components.backup.const import DATA_MANAGER
from homeassistant.components.backup.manager import NewBackup
from homeassistant.core import HomeAssistant
@@ -56,6 +57,27 @@ async def test_info(
assert await client.receive_json() == snapshot
@pytest.mark.parametrize(
"side_effect", [HomeAssistantError("Boom!"), BackupAgentUnreachableError]
)
async def test_info_with_errors(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
side_effect: Exception,
snapshot: SnapshotAssertion,
) -> None:
"""Test getting backup info with one unavailable agent."""
await setup_backup_integration(hass, with_hassio=False, backups=[TEST_LOCAL_BACKUP])
hass.data[DATA_MANAGER].backup_agents["domain.test"] = BackupAgentTest("test")
client = await hass_ws_client(hass)
await hass.async_block_till_done()
with patch.object(BackupAgentTest, "async_list_backups", side_effect=side_effect):
await client.send_json_auto_id({"type": "backup/info"})
assert await client.receive_json() == snapshot
@pytest.mark.parametrize(
"backup_content",
[