Create repair issues when automatic backup fails (#133513)

* Create repair issues when automatic backup fails

* Improve test coverage

* Adjust issues
pull/133561/head
Erik Montnemery 2024-12-19 10:40:07 +01:00 committed by GitHub
parent cd384cadbe
commit a76f82080b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 261 additions and 1 deletions

View File

@ -23,7 +23,11 @@ from homeassistant.backup_restore import RESTORE_BACKUP_FILE, password_to_key
from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import instance_id, integration_platform
from homeassistant.helpers import (
instance_id,
integration_platform,
issue_registry as ir,
)
from homeassistant.helpers.json import json_bytes
from homeassistant.util import dt as dt_util
@ -691,6 +695,8 @@ class BackupManager:
CreateBackupEvent(stage=None, state=CreateBackupState.FAILED)
)
self.async_on_backup_event(IdleEvent())
if with_automatic_settings:
self._update_issue_backup_failed()
raise
async def _async_create_backup(
@ -750,6 +756,8 @@ class BackupManager:
self.async_on_backup_event(
CreateBackupEvent(stage=None, state=CreateBackupState.FAILED)
)
if with_automatic_settings:
self._update_issue_backup_failed()
else:
LOGGER.debug(
"Generated new backup with backup_id %s, uploading to agents %s",
@ -772,6 +780,7 @@ class BackupManager:
# create backup was successful, update last_completed_automatic_backup
self.config.data.last_completed_automatic_backup = dt_util.now()
self.store.save()
self._update_issue_after_agent_upload(agent_errors)
self.known_backups.add(written_backup.backup, agent_errors)
# delete old backups more numerous than copies
@ -878,6 +887,38 @@ class BackupManager:
self._backup_event_subscriptions.append(on_event)
return remove_subscription
def _update_issue_backup_failed(self) -> None:
"""Update issue registry when a backup fails."""
ir.async_create_issue(
self.hass,
DOMAIN,
"automatic_backup_failed",
is_fixable=False,
is_persistent=True,
learn_more_url="homeassistant://config/backup",
severity=ir.IssueSeverity.WARNING,
translation_key="automatic_backup_failed_create",
)
def _update_issue_after_agent_upload(
self, agent_errors: dict[str, Exception]
) -> None:
"""Update issue registry after a backup is uploaded to agents."""
if not agent_errors:
ir.async_delete_issue(self.hass, DOMAIN, "automatic_backup_failed")
return
ir.async_create_issue(
self.hass,
DOMAIN,
"automatic_backup_failed",
is_fixable=False,
is_persistent=True,
learn_more_url="homeassistant://config/backup",
severity=ir.IssueSeverity.WARNING,
translation_key="automatic_backup_failed_upload_agents",
translation_placeholders={"failed_agents": ", ".join(agent_errors)},
)
class KnownBackups:
"""Track known backups."""

View File

@ -1,4 +1,14 @@
{
"issues": {
"automatic_backup_failed_create": {
"title": "Automatic backup could not be created",
"description": "The automatic backup could not be created. Please check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
},
"automatic_backup_failed_upload_agents": {
"title": "Automatic backup could not be uploaded to agents",
"description": "The automatic backup could not be uploaded to agents {failed_agents}. Please check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
}
},
"services": {
"create": {
"name": "Create backup",

View File

@ -34,6 +34,7 @@ from homeassistant.components.backup.manager import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
from .common import (
@ -534,6 +535,214 @@ async def test_async_initiate_backup_with_agent_error(
]
@pytest.mark.usefixtures("mock_backup_generation")
@pytest.mark.parametrize(
("create_backup_command", "issues_after_create_backup"),
[
(
{"type": "backup/generate", "agent_ids": [LOCAL_AGENT_ID]},
{(DOMAIN, "automatic_backup_failed")},
),
(
{"type": "backup/generate_with_automatic_settings"},
set(),
),
],
)
async def test_create_backup_success_clears_issue(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
create_backup_command: dict[str, Any],
issues_after_create_backup: set[tuple[str, str]],
) -> None:
"""Test backup issue is cleared after backup is created."""
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
# Create a backup issue
ir.async_create_issue(
hass,
DOMAIN,
"automatic_backup_failed",
is_fixable=False,
is_persistent=True,
severity=ir.IssueSeverity.WARNING,
translation_key="automatic_backup_failed_create",
)
ws_client = await hass_ws_client(hass)
await ws_client.send_json_auto_id(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": [LOCAL_AGENT_ID]},
}
)
result = await ws_client.receive_json()
assert result["success"] is True
await ws_client.send_json_auto_id(create_backup_command)
result = await ws_client.receive_json()
assert result["success"] is True
await hass.async_block_till_done()
issue_registry = ir.async_get(hass)
assert set(issue_registry.issues) == issues_after_create_backup
async def delayed_boom(*args, **kwargs) -> None:
"""Raise an exception after a delay."""
async def delayed_boom() -> None:
await asyncio.sleep(0)
raise Exception("Boom!") # noqa: TRY002
return (NewBackup(backup_job_id="abc123"), delayed_boom())
@pytest.mark.parametrize(
(
"create_backup_command",
"create_backup_side_effect",
"agent_upload_side_effect",
"create_backup_result",
"issues_after_create_backup",
),
[
# No error
(
{"type": "backup/generate", "agent_ids": ["test.remote"]},
None,
None,
True,
{},
),
(
{"type": "backup/generate_with_automatic_settings"},
None,
None,
True,
{},
),
# Error raised in async_initiate_backup
(
{"type": "backup/generate", "agent_ids": ["test.remote"]},
Exception("Boom!"),
None,
False,
{},
),
(
{"type": "backup/generate_with_automatic_settings"},
Exception("Boom!"),
None,
False,
{
(DOMAIN, "automatic_backup_failed"): {
"translation_key": "automatic_backup_failed_create",
"translation_placeholders": None,
}
},
),
# Error raised when awaiting the backup task
(
{"type": "backup/generate", "agent_ids": ["test.remote"]},
delayed_boom,
None,
True,
{},
),
(
{"type": "backup/generate_with_automatic_settings"},
delayed_boom,
None,
True,
{
(DOMAIN, "automatic_backup_failed"): {
"translation_key": "automatic_backup_failed_create",
"translation_placeholders": None,
}
},
),
# Error raised in async_upload_backup
(
{"type": "backup/generate", "agent_ids": ["test.remote"]},
None,
Exception("Boom!"),
True,
{},
),
(
{"type": "backup/generate_with_automatic_settings"},
None,
Exception("Boom!"),
True,
{
(DOMAIN, "automatic_backup_failed"): {
"translation_key": "automatic_backup_failed_upload_agents",
"translation_placeholders": {"failed_agents": "test.remote"},
}
},
),
],
)
async def test_create_backup_failure_raises_issue(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
create_backup: AsyncMock,
create_backup_command: dict[str, Any],
create_backup_side_effect: Exception | None,
agent_upload_side_effect: Exception | None,
create_backup_result: bool,
issues_after_create_backup: dict[tuple[str, str], dict[str, Any]],
) -> None:
"""Test backup issue is cleared after backup is created."""
remote_agent = BackupAgentTest("remote", backups=[])
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
await _setup_backup_platform(
hass,
domain="test",
platform=Mock(
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
spec_set=BackupAgentPlatformProtocol,
),
)
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
ws_client = await hass_ws_client(hass)
create_backup.side_effect = create_backup_side_effect
await ws_client.send_json_auto_id(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.remote"]},
}
)
result = await ws_client.receive_json()
assert result["success"] is True
with patch.object(
remote_agent, "async_upload_backup", side_effect=agent_upload_side_effect
):
await ws_client.send_json_auto_id(create_backup_command)
result = await ws_client.receive_json()
assert result["success"] == create_backup_result
await hass.async_block_till_done()
issue_registry = ir.async_get(hass)
assert set(issue_registry.issues) == set(issues_after_create_backup)
for issue_id, issue_data in issues_after_create_backup.items():
issue = issue_registry.issues[issue_id]
assert issue.translation_key == issue_data["translation_key"]
assert issue.translation_placeholders == issue_data["translation_placeholders"]
async def test_loading_platforms(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,