Create repair issues when automatic backup fails (#133513)
* Create repair issues when automatic backup fails * Improve test coverage * Adjust issuespull/133561/head
parent
cd384cadbe
commit
a76f82080b
|
@ -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."""
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue