Add backup support to the hassio OS update entity (#142580)

* Add backup support to the hassio OS update entity

* Remove meaningless assert
This commit is contained in:
Erik Montnemery 2025-04-10 20:56:02 +02:00 committed by GitHub
parent cf63175232
commit 5a09847596
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 190 additions and 20 deletions

View File

@ -5,7 +5,6 @@ from __future__ import annotations
from typing import Any
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import OSUpdate
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
from homeassistant.components.update import (
@ -36,7 +35,7 @@ from .entity import (
HassioOSEntity,
HassioSupervisorEntity,
)
from .update_helper import update_addon, update_core
from .update_helper import update_addon, update_core, update_os
ENTITY_DESCRIPTION = UpdateEntityDescription(
translation_key="update",
@ -170,7 +169,9 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
"""Update entity to handle updates for the Home Assistant Operating System."""
_attr_supported_features = (
UpdateEntityFeature.INSTALL | UpdateEntityFeature.SPECIFIC_VERSION
UpdateEntityFeature.INSTALL
| UpdateEntityFeature.SPECIFIC_VERSION
| UpdateEntityFeature.BACKUP
)
_attr_title = "Home Assistant Operating System"
@ -203,14 +204,7 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install an update."""
try:
await self.coordinator.supervisor_client.os.update(
OSUpdate(version=version)
)
except SupervisorError as err:
raise HomeAssistantError(
f"Error updating Home Assistant Operating System: {err}"
) from err
await update_os(self.hass, version, backup)
class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity):

View File

@ -3,7 +3,11 @@
from __future__ import annotations
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import HomeAssistantUpdateOptions, StoreAddonUpdate
from aiohasupervisor.models import (
HomeAssistantUpdateOptions,
OSUpdate,
StoreAddonUpdate,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@ -57,3 +61,24 @@ async def update_core(hass: HomeAssistant, version: str | None, backup: bool) ->
)
except SupervisorError as err:
raise HomeAssistantError(f"Error updating Home Assistant Core: {err}") from err
async def update_os(hass: HomeAssistant, version: str | None, backup: bool) -> None:
"""Update OS.
Optionally make a core backup before updating.
"""
client = get_supervisor_client(hass)
if backup:
# pylint: disable-next=import-outside-toplevel
from .backup import backup_core_before_update
await backup_core_before_update(hass)
try:
await client.os.update(OSUpdate(version=version))
except SupervisorError as err:
raise HomeAssistantError(
f"Error updating Home Assistant Operating System: {err}"
) from err

View File

@ -6,7 +6,11 @@ from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
from aiohasupervisor import SupervisorBadRequestError, SupervisorError
from aiohasupervisor.models import HomeAssistantUpdateOptions, StoreAddonUpdate
from aiohasupervisor.models import (
HomeAssistantUpdateOptions,
OSUpdate,
StoreAddonUpdate,
)
import pytest
from homeassistant.components.backup import BackupManagerError, ManagerBackup
@ -475,13 +479,123 @@ async def test_update_os(hass: HomeAssistant, supervisor_client: AsyncMock) -> N
await hass.async_block_till_done()
supervisor_client.os.update.return_value = None
await hass.services.async_call(
"update",
"install",
{"entity_id": "update.home_assistant_operating_system_update"},
blocking=True,
)
supervisor_client.os.update.assert_called_once()
with patch(
"homeassistant.components.backup.manager.BackupManager.async_create_backup",
) as mock_create_backup:
await hass.services.async_call(
"update",
"install",
{"entity_id": "update.home_assistant_operating_system_update"},
blocking=True,
)
mock_create_backup.assert_not_called()
supervisor_client.os.update.assert_called_once_with(OSUpdate(version=None))
@pytest.mark.parametrize(
("commands", "default_mount", "expected_kwargs"),
[
(
[],
None,
{
"agent_ids": ["hassio.local"],
"include_addons": None,
"include_all_addons": False,
"include_database": True,
"include_folders": None,
"include_homeassistant": True,
"name": f"Home Assistant Core {HAVERSION}",
"password": None,
},
),
(
[],
"my_nas",
{
"agent_ids": ["hassio.my_nas"],
"include_addons": None,
"include_all_addons": False,
"include_database": True,
"include_folders": None,
"include_homeassistant": True,
"name": f"Home Assistant Core {HAVERSION}",
"password": None,
},
),
(
[
{
"type": "backup/config/update",
"create_backup": {
"agent_ids": ["test-agent"],
"include_addons": ["my-addon"],
"include_all_addons": True,
"include_database": False,
"include_folders": ["share"],
"name": "cool_backup",
"password": "hunter2",
},
},
],
None,
{
"agent_ids": ["test-agent"],
"include_addons": ["my-addon"],
"include_all_addons": True,
"include_database": False,
"include_folders": ["share"],
"include_homeassistant": True,
"name": "cool_backup",
"password": "hunter2",
"with_automatic_settings": True,
},
),
],
)
async def test_update_os_with_backup(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
supervisor_client: AsyncMock,
commands: list[dict[str, Any]],
default_mount: str | None,
expected_kwargs: dict[str, Any],
) -> None:
"""Test updating OS update entity."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
result = await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
await setup_backup_integration(hass)
client = await hass_ws_client(hass)
for command in commands:
await client.send_json_auto_id(command)
result = await client.receive_json()
assert result["success"]
supervisor_client.os.update.return_value = None
supervisor_client.mounts.info.return_value.default_backup_mount = default_mount
with patch(
"homeassistant.components.backup.manager.BackupManager.async_create_backup",
) as mock_create_backup:
await hass.services.async_call(
"update",
"install",
{
"entity_id": "update.home_assistant_operating_system_update",
"backup": True,
},
blocking=True,
)
mock_create_backup.assert_called_once_with(**expected_kwargs)
supervisor_client.os.update.assert_called_once_with(OSUpdate(version=None))
async def test_update_core(hass: HomeAssistant, supervisor_client: AsyncMock) -> None:
@ -746,6 +860,43 @@ async def test_update_os_with_error(
)
async def test_update_os_with_backup_and_error(
hass: HomeAssistant,
supervisor_client: AsyncMock,
) -> None:
"""Test updating OS update entity with error."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
result = await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
await setup_backup_integration(hass)
supervisor_client.os.update.return_value = None
supervisor_client.mounts.info.return_value.default_backup_mount = None
with (
patch(
"homeassistant.components.backup.manager.BackupManager.async_create_backup",
side_effect=BackupManagerError,
),
pytest.raises(HomeAssistantError, match=r"^Error creating backup:"),
):
await hass.services.async_call(
"update",
"install",
{
"entity_id": "update.home_assistant_operating_system_update",
"backup": True,
},
blocking=True,
)
async def test_update_supervisor_with_error(
hass: HomeAssistant, supervisor_client: AsyncMock
) -> None: