Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve error handling when supervisor backups are deleted #137331

Merged
merged 2 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion homeassistant/components/backup/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
RestoreBackupState,
WrittenBackup,
)
from .models import AddonInfo, AgentBackup, Folder
from .models import AddonInfo, AgentBackup, BackupNotFound, Folder
from .util import suggested_filename, suggested_filename_from_name_date
from .websocket import async_register_websocket_handlers

Expand All @@ -48,6 +48,7 @@
"BackupAgentError",
"BackupAgentPlatformProtocol",
"BackupManagerError",
"BackupNotFound",
"BackupPlatformProtocol",
"BackupReaderWriter",
"BackupReaderWriterError",
Expand Down
14 changes: 1 addition & 13 deletions homeassistant/components/backup/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,7 @@

from homeassistant.core import HomeAssistant, callback

from .models import AgentBackup, BackupError


class BackupAgentError(BackupError):
"""Base class for backup agent errors."""

error_code = "backup_agent_error"
from .models import AgentBackup, BackupAgentError


class BackupAgentUnreachableError(BackupAgentError):
Expand All @@ -27,12 +21,6 @@ class BackupAgentUnreachableError(BackupAgentError):
_message = "The backup agent is unreachable."


class BackupNotFound(BackupAgentError):
"""Raised when a backup is not found."""

error_code = "backup_not_found"


class BackupAgent(abc.ABC):
"""Backup agent interface."""

Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/backup/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.hassio import is_hassio

from .agent import BackupAgent, BackupNotFound, LocalBackupAgent
from .agent import BackupAgent, LocalBackupAgent
from .const import DOMAIN, LOGGER
from .models import AgentBackup
from .models import AgentBackup, BackupNotFound
from .util import read_backup, suggested_filename


Expand Down
16 changes: 10 additions & 6 deletions homeassistant/components/backup/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .agent import BackupAgent
from .const import DATA_MANAGER
from .manager import BackupManager
from .models import BackupNotFound


@callback
Expand Down Expand Up @@ -69,13 +70,16 @@ async def get(
CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar"
}

if not password or not backup.protected:
return await self._send_backup_no_password(
request, headers, backup_id, agent_id, agent, manager
try:
if not password or not backup.protected:
return await self._send_backup_no_password(
request, headers, backup_id, agent_id, agent, manager
)
return await self._send_backup_with_password(
hass, request, headers, backup_id, agent_id, password, agent, manager
)
return await self._send_backup_with_password(
hass, request, headers, backup_id, agent_id, password, agent, manager
)
except BackupNotFound:
return Response(status=HTTPStatus.NOT_FOUND)

async def _send_backup_no_password(
self,
Expand Down
15 changes: 8 additions & 7 deletions homeassistant/components/backup/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@
EXCLUDE_FROM_BACKUP,
LOGGER,
)
from .models import AgentBackup, BackupError, BackupManagerError, BaseBackup, Folder
from .models import (
AgentBackup,
BackupError,
BackupManagerError,
BackupReaderWriterError,
BaseBackup,
Folder,
)
from .store import BackupStore
from .util import (
AsyncIteratorReader,
Expand Down Expand Up @@ -274,12 +281,6 @@ async def async_resume_restore_progress_after_restart(
"""Get restore events after core restart."""


class BackupReaderWriterError(BackupError):
"""Backup reader/writer error."""

error_code = "backup_reader_writer_error"


class IncorrectPasswordError(BackupReaderWriterError):
"""Raised when the password is incorrect."""

Expand Down
18 changes: 18 additions & 0 deletions homeassistant/components/backup/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,25 @@ class BackupError(HomeAssistantError):
error_code = "unknown"


class BackupAgentError(BackupError):
"""Base class for backup agent errors."""

error_code = "backup_agent_error"


class BackupManagerError(BackupError):
"""Backup manager error."""

error_code = "backup_manager_error"


class BackupReaderWriterError(BackupError):
"""Backup reader/writer error."""

error_code = "backup_reader_writer_error"


class BackupNotFound(BackupAgentError, BackupManagerError):
"""Raised when a backup is not found."""

error_code = "backup_not_found"
6 changes: 5 additions & 1 deletion homeassistant/components/backup/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
IncorrectPasswordError,
ManagerStateEvent,
)
from .models import Folder
from .models import BackupNotFound, Folder


@callback
Expand Down Expand Up @@ -151,6 +151,8 @@ async def handle_restore(
restore_folders=msg.get("restore_folders"),
restore_homeassistant=msg["restore_homeassistant"],
)
except BackupNotFound:
connection.send_error(msg["id"], "backup_not_found", "Backup not found")
except IncorrectPasswordError:
connection.send_error(msg["id"], "password_incorrect", "Incorrect password")
else:
Expand Down Expand Up @@ -179,6 +181,8 @@ async def handle_can_decrypt_on_download(
agent_id=msg["agent_id"],
password=msg.get("password"),
)
except BackupNotFound:
connection.send_error(msg["id"], "backup_not_found", "Backup not found")
except IncorrectPasswordError:
connection.send_error(msg["id"], "password_incorrect", "Incorrect password")
except DecryptOnDowloadNotSupported:
Expand Down
16 changes: 12 additions & 4 deletions homeassistant/components/hassio/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
AgentBackup,
BackupAgent,
BackupManagerError,
BackupNotFound,
BackupReaderWriter,
BackupReaderWriterError,
CreateBackupEvent,
Expand Down Expand Up @@ -162,10 +163,15 @@ async def async_download_backup(
**kwargs: Any,
) -> AsyncIterator[bytes]:
"""Download a backup file."""
return await self._client.backups.download_backup(
backup_id,
options=supervisor_backups.DownloadBackupOptions(location=self.location),
)
try:
return await self._client.backups.download_backup(
backup_id,
options=supervisor_backups.DownloadBackupOptions(
location=self.location
),
)
except SupervisorNotFoundError as err:
raise BackupNotFound from err

async def async_upload_backup(
self,
Expand Down Expand Up @@ -528,6 +534,8 @@ async def async_restore_backup(
location=restore_location,
),
)
except SupervisorNotFoundError as err:
raise BackupNotFound from err
except SupervisorBadRequestError as err:
# Supervisor currently does not transmit machine parsable error types
message = err.args[0]
Expand Down
22 changes: 22 additions & 0 deletions tests/components/backup/snapshots/test_websocket.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,28 @@
'type': 'result',
})
# ---
# name: test_can_decrypt_on_download_with_agent_error[BackupAgentError]
dict({
'error': dict({
'code': 'home_assistant_error',
'message': 'Unknown error',
}),
'id': 1,
'success': False,
'type': 'result',
})
# ---
# name: test_can_decrypt_on_download_with_agent_error[BackupNotFound]
dict({
'error': dict({
'code': 'backup_not_found',
'message': 'Backup not found',
}),
'id': 1,
'success': False,
'type': 'result',
})
# ---
# name: test_config_info[storage_data0]
dict({
'id': 1,
Expand Down
52 changes: 51 additions & 1 deletion tests/components/backup/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@
from aiohttp import web
import pytest

from homeassistant.components.backup import AddonInfo, AgentBackup, Folder
from homeassistant.components.backup import (
AddonInfo,
AgentBackup,
BackupAgentError,
BackupNotFound,
Folder,
)
from homeassistant.components.backup.const import DATA_MANAGER, DOMAIN
from homeassistant.core import HomeAssistant

Expand Down Expand Up @@ -141,6 +147,50 @@ async def download_backup(backup_id: str, **kwargs: Any) -> AsyncIterator[bytes]
await _test_downloading_encrypted_backup(hass_client, "domain.test")


@pytest.mark.parametrize(
("error", "status"),
[
(BackupAgentError, 500),
(BackupNotFound, 404),
],
)
@patch.object(BackupAgentTest, "async_download_backup")
async def test_downloading_remote_encrypted_backup_with_error(
download_mock,
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
error: Exception,
status: int,
) -> None:
"""Test downloading a local backup file."""
await setup_backup_integration(hass)
hass.data[DATA_MANAGER].backup_agents["domain.test"] = BackupAgentTest(
"test",
[
AgentBackup(
addons=[AddonInfo(name="Test", slug="test", version="1.0.0")],
backup_id="abc123",
database_included=True,
date="1970-01-01T00:00:00Z",
extra_metadata={},
folders=[Folder.MEDIA, Folder.SHARE],
homeassistant_included=True,
homeassistant_version="2024.12.0",
name="Test",
protected=True,
size=13,
)
],
)

download_mock.side_effect = error
client = await hass_client()
resp = await client.get(
"/api/backup/download/abc123?agent_id=domain.test&password=blah"
)
assert resp.status == status


async def _test_downloading_encrypted_backup(
hass_client: ClientSessionGenerator,
agent_id: str,
Expand Down
37 changes: 37 additions & 0 deletions tests/components/backup/test_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
AgentBackup,
BackupAgentError,
BackupAgentPlatformProtocol,
BackupNotFound,
BackupReaderWriterError,
Folder,
store,
Expand Down Expand Up @@ -2967,3 +2968,39 @@ async def test_can_decrypt_on_download(
}
)
assert await client.receive_json() == snapshot


@pytest.mark.parametrize(
"error",
[
BackupAgentError,
BackupNotFound,
],
)
@pytest.mark.usefixtures("mock_backups")
async def test_can_decrypt_on_download_with_agent_error(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
error: Exception,
) -> None:
"""Test can decrypt on download."""

await setup_backup_integration(
hass,
with_hassio=False,
backups={"test.remote": [TEST_BACKUP_ABC123]},
remote_agents=["remote"],
)
client = await hass_ws_client(hass)

with patch.object(BackupAgentTest, "async_download_backup", side_effect=error):
await client.send_json_auto_id(
{
"type": "backup/can_decrypt_on_download",
"backup_id": TEST_BACKUP_ABC123.backup_id,
"agent_id": "test.remote",
"password": "hunter2",
}
)
assert await client.receive_json() == snapshot
Loading
Loading