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 typing import Any, Protocol
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .models import BackupUploadMetadata, BaseBackup 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) @dataclass(slots=True)
class UploadedBackup(BaseBackup): class UploadedBackup(BaseBackup):
"""Uploaded backup class.""" """Uploaded backup class."""

View File

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

View File

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

View File

@@ -439,6 +439,8 @@
dict({ dict({
'id': 1, 'id': 1,
'result': dict({ 'result': dict({
'agent_errors': dict({
}),
'backing_up': False, 'backing_up': False,
'backups': list([ 'backups': list([
dict({ dict({
@@ -457,6 +459,42 @@
'type': 'result', '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] # name: test_remove[with_hassio]
dict({ dict({
'error': 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() 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 backups == {TEST_BACKUP.slug: TEST_BACKUP}
assert agent_errors == {}
async def test_load_backups_with_exception( 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")), patch("tarfile.open", side_effect=OSError("Test exception")),
): ):
await manager.backup_agents[LOCAL_AGENT_ID].load_backups() 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 f"Unable to read backup {TEST_BACKUP_PATH}: Test exception" in caplog.text
assert backups == {} assert backups == {}
assert agent_errors == {}
async def test_removing_backup( async def test_removing_backup(

View File

@@ -9,6 +9,7 @@ import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from homeassistant.components.backup import BaseBackup 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.const import DATA_MANAGER
from homeassistant.components.backup.manager import NewBackup from homeassistant.components.backup.manager import NewBackup
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -56,6 +57,27 @@ async def test_info(
assert await client.receive_json() == snapshot 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( @pytest.mark.parametrize(
"backup_content", "backup_content",
[ [