Update hassio to use the backup integration to make backups before update (#136235)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Erik Montnemery 2025-01-27 08:25:22 +01:00 committed by GitHub
parent 69938545df
commit 245ee2498e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 979 additions and 64 deletions

View File

@ -1,6 +1,7 @@
"""The Backup integration.""" """The Backup integration."""
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -19,6 +20,7 @@ from .const import DATA_MANAGER, DOMAIN
from .http import async_register_http_views from .http import async_register_http_views
from .manager import ( from .manager import (
BackupManager, BackupManager,
BackupManagerError,
BackupPlatformProtocol, BackupPlatformProtocol,
BackupReaderWriter, BackupReaderWriter,
BackupReaderWriterError, BackupReaderWriterError,
@ -39,6 +41,7 @@ __all__ = [
"BackupAgent", "BackupAgent",
"BackupAgentError", "BackupAgentError",
"BackupAgentPlatformProtocol", "BackupAgentPlatformProtocol",
"BackupManagerError",
"BackupPlatformProtocol", "BackupPlatformProtocol",
"BackupReaderWriter", "BackupReaderWriter",
"BackupReaderWriterError", "BackupReaderWriterError",
@ -90,18 +93,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_handle_create_automatic_service(call: ServiceCall) -> None: async def async_handle_create_automatic_service(call: ServiceCall) -> None:
"""Service handler for creating automatic backups.""" """Service handler for creating automatic backups."""
config_data = backup_manager.config.data await backup_manager.async_create_automatic_backup()
await backup_manager.async_create_backup(
agent_ids=config_data.create_backup.agent_ids,
include_addons=config_data.create_backup.include_addons,
include_all_addons=config_data.create_backup.include_all_addons,
include_database=config_data.create_backup.include_database,
include_folders=config_data.create_backup.include_folders,
include_homeassistant=True, # always include HA
name=config_data.create_backup.name,
password=config_data.create_backup.password,
with_automatic_settings=True,
)
if not with_hassio: if not with_hassio:
hass.services.async_register(DOMAIN, "create", async_handle_create_service) hass.services.async_register(DOMAIN, "create", async_handle_create_service)
@ -112,3 +104,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async_register_http_views(hass) async_register_http_views(hass)
return True return True
@callback
def async_get_manager(hass: HomeAssistant) -> BackupManager:
"""Get the backup manager instance.
Raises HomeAssistantError if the backup integration is not available.
"""
if DATA_MANAGER not in hass.data:
raise HomeAssistantError("Backup integration is not available")
return hass.data[DATA_MANAGER]

View File

@ -390,22 +390,11 @@ class BackupSchedule:
async def _create_backup(now: datetime) -> None: async def _create_backup(now: datetime) -> None:
"""Create backup.""" """Create backup."""
manager.remove_next_backup_event = None manager.remove_next_backup_event = None
config_data = manager.config.data
self._schedule_next(cron_pattern, manager) self._schedule_next(cron_pattern, manager)
# create the backup # create the backup
try: try:
await manager.async_create_backup( await manager.async_create_automatic_backup()
agent_ids=config_data.create_backup.agent_ids,
include_addons=config_data.create_backup.include_addons,
include_all_addons=config_data.create_backup.include_all_addons,
include_database=config_data.create_backup.include_database,
include_folders=config_data.create_backup.include_folders,
include_homeassistant=True, # always include HA
name=config_data.create_backup.name,
password=config_data.create_backup.password,
with_automatic_settings=True,
)
except BackupManagerError as err: except BackupManagerError as err:
LOGGER.error("Error creating backup: %s", err) LOGGER.error("Error creating backup: %s", err)
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001

View File

@ -698,6 +698,21 @@ class BackupManager:
await self._backup_finish_task await self._backup_finish_task
return new_backup return new_backup
async def async_create_automatic_backup(self) -> NewBackup:
"""Create a backup with automatic backup settings."""
config_data = self.config.data
return await self.async_create_backup(
agent_ids=config_data.create_backup.agent_ids,
include_addons=config_data.create_backup.include_addons,
include_all_addons=config_data.create_backup.include_all_addons,
include_database=config_data.create_backup.include_database,
include_folders=config_data.create_backup.include_folders,
include_homeassistant=True, # always include HA
name=config_data.create_backup.name,
password=config_data.create_backup.password,
with_automatic_settings=True,
)
async def async_initiate_backup( async def async_initiate_backup(
self, self,
*, *,

View File

@ -8,6 +8,7 @@ import logging
from pathlib import Path from pathlib import Path
from typing import Any, cast from typing import Any, cast
from aiohasupervisor import SupervisorClient
from aiohasupervisor.exceptions import ( from aiohasupervisor.exceptions import (
SupervisorBadRequestError, SupervisorBadRequestError,
SupervisorError, SupervisorError,
@ -23,6 +24,7 @@ from homeassistant.components.backup import (
AddonInfo, AddonInfo,
AgentBackup, AgentBackup,
BackupAgent, BackupAgent,
BackupManagerError,
BackupReaderWriter, BackupReaderWriter,
BackupReaderWriterError, BackupReaderWriterError,
CreateBackupEvent, CreateBackupEvent,
@ -31,7 +33,9 @@ from homeassistant.components.backup import (
NewBackup, NewBackup,
RestoreBackupEvent, RestoreBackupEvent,
WrittenBackup, WrittenBackup,
async_get_manager as async_get_backup_manager,
) )
from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -477,3 +481,66 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
self._hass, EVENT_SUPERVISOR_EVENT, handle_signal self._hass, EVENT_SUPERVISOR_EVENT, handle_signal
) )
return unsub return unsub
async def _default_agent(client: SupervisorClient) -> str:
"""Return the default agent for creating a backup."""
mounts = await client.mounts.info()
default_mount = mounts.default_backup_mount
return f"hassio.{default_mount if default_mount is not None else 'local'}"
async def backup_addon_before_update(
hass: HomeAssistant,
addon: str,
addon_name: str | None,
installed_version: str | None,
) -> None:
"""Prepare for updating an add-on."""
backup_manager = hass.data[DATA_MANAGER]
client = get_supervisor_client(hass)
# Use the password from automatic settings if available
if backup_manager.config.data.create_backup.agent_ids:
password = backup_manager.config.data.create_backup.password
else:
password = None
try:
await backup_manager.async_create_backup(
agent_ids=[await _default_agent(client)],
include_addons=[addon],
include_all_addons=False,
include_database=False,
include_folders=None,
include_homeassistant=False,
name=f"{addon_name or addon} {installed_version or '<unknown>'}",
password=password,
)
except BackupManagerError as err:
raise HomeAssistantError(f"Error creating backup: {err}") from err
async def backup_core_before_update(hass: HomeAssistant) -> None:
"""Prepare for updating core."""
backup_manager = async_get_backup_manager(hass)
client = get_supervisor_client(hass)
try:
if backup_manager.config.data.create_backup.agent_ids:
# Create a backup with automatic settings
await backup_manager.async_create_automatic_backup()
else:
# Create a manual backup
await backup_manager.async_create_backup(
agent_ids=[await _default_agent(client)],
include_addons=None,
include_all_addons=False,
include_database=True,
include_folders=None,
include_homeassistant=True,
name=f"Home Assistant Core {HAVERSION}",
password=None,
)
except BackupManagerError as err:
raise HomeAssistantError(f"Error creating backup: {err}") from err

View File

@ -4,12 +4,8 @@ from __future__ import annotations
from typing import Any from typing import Any
from aiohasupervisor import SupervisorError from aiohasupervisor import SupervisorClient, SupervisorError
from aiohasupervisor.models import ( from aiohasupervisor.models import OSUpdate
HomeAssistantUpdateOptions,
OSUpdate,
StoreAddonUpdate,
)
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
from homeassistant.components.update import ( from homeassistant.components.update import (
@ -40,6 +36,7 @@ from .entity import (
HassioOSEntity, HassioOSEntity,
HassioSupervisorEntity, HassioSupervisorEntity,
) )
from .update_helper import update_addon, update_core
ENTITY_DESCRIPTION = UpdateEntityDescription( ENTITY_DESCRIPTION = UpdateEntityDescription(
name="Update", name="Update",
@ -163,13 +160,9 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Install an update.""" """Install an update."""
try: await update_addon(
await self.coordinator.supervisor_client.store.update_addon( self.hass, self._addon_slug, backup, self.title, self.installed_version
self._addon_slug, StoreAddonUpdate(backup=backup)
) )
except SupervisorError as err:
raise HomeAssistantError(f"Error updating {self.title}: {err}") from err
await self.coordinator.force_info_update_supervisor() await self.coordinator.force_info_update_supervisor()
@ -303,11 +296,11 @@ class SupervisorCoreUpdateEntity(HassioCoreEntity, UpdateEntity):
self, version: str | None, backup: bool, **kwargs: Any self, version: str | None, backup: bool, **kwargs: Any
) -> None: ) -> None:
"""Install an update.""" """Install an update."""
try: await update_core(self.hass, version, backup)
await self.coordinator.supervisor_client.homeassistant.update(
HomeAssistantUpdateOptions(version=version, backup=backup)
) async def _default_agent(client: SupervisorClient) -> str:
except SupervisorError as err: """Return the default agent for creating a backup."""
raise HomeAssistantError( mounts = await client.mounts.info()
f"Error updating Home Assistant Core: {err}" default_mount = mounts.default_backup_mount
) from err return f"hassio.{default_mount if default_mount is not None else 'local'}"

View File

@ -0,0 +1,59 @@
"""Update helpers for Supervisor."""
from __future__ import annotations
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import HomeAssistantUpdateOptions, StoreAddonUpdate
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .handler import get_supervisor_client
async def update_addon(
hass: HomeAssistant,
addon: str,
backup: bool,
addon_name: str | None,
installed_version: str | None,
) -> None:
"""Update an addon.
Optionally make a backup before updating.
"""
client = get_supervisor_client(hass)
if backup:
# pylint: disable-next=import-outside-toplevel
from .backup import backup_addon_before_update
await backup_addon_before_update(hass, addon, addon_name, installed_version)
try:
await client.store.update_addon(addon, StoreAddonUpdate(backup=False))
except SupervisorError as err:
raise HomeAssistantError(
f"Error updating {addon_name or addon}: {err}"
) from err
async def update_core(hass: HomeAssistant, version: str | None, backup: bool) -> None:
"""Update core.
Optionally make a 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.homeassistant.update(
HomeAssistantUpdateOptions(version=version, backup=False)
)
except SupervisorError as err:
raise HomeAssistantError(f"Error updating Home Assistant Core: {err}") from err

View File

@ -9,6 +9,7 @@ import voluptuous as vol
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.components.websocket_api import ActiveConnection from homeassistant.components.websocket_api import ActiveConnection
from homeassistant.const import ATTR_NAME
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import Unauthorized from homeassistant.exceptions import Unauthorized
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -23,7 +24,9 @@ from .const import (
ATTR_ENDPOINT, ATTR_ENDPOINT,
ATTR_METHOD, ATTR_METHOD,
ATTR_SESSION_DATA_USER_ID, ATTR_SESSION_DATA_USER_ID,
ATTR_SLUG,
ATTR_TIMEOUT, ATTR_TIMEOUT,
ATTR_VERSION,
ATTR_WS_EVENT, ATTR_WS_EVENT,
DATA_COMPONENT, DATA_COMPONENT,
EVENT_SUPERVISOR_EVENT, EVENT_SUPERVISOR_EVENT,
@ -33,6 +36,8 @@ from .const import (
WS_TYPE_EVENT, WS_TYPE_EVENT,
WS_TYPE_SUBSCRIBE, WS_TYPE_SUBSCRIBE,
) )
from .coordinator import get_supervisor_info
from .update_helper import update_addon, update_core
SCHEMA_WEBSOCKET_EVENT = vol.Schema( SCHEMA_WEBSOCKET_EVENT = vol.Schema(
{vol.Required(ATTR_WS_EVENT): cv.string}, {vol.Required(ATTR_WS_EVENT): cv.string},
@ -58,6 +63,8 @@ def async_load_websocket_api(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, websocket_supervisor_event) websocket_api.async_register_command(hass, websocket_supervisor_event)
websocket_api.async_register_command(hass, websocket_supervisor_api) websocket_api.async_register_command(hass, websocket_supervisor_api)
websocket_api.async_register_command(hass, websocket_subscribe) websocket_api.async_register_command(hass, websocket_subscribe)
websocket_api.async_register_command(hass, websocket_update_addon)
websocket_api.async_register_command(hass, websocket_update_core)
@callback @callback
@ -137,3 +144,44 @@ async def websocket_supervisor_api(
) )
else: else:
connection.send_result(msg[WS_ID], result.get(ATTR_DATA, {})) connection.send_result(msg[WS_ID], result.get(ATTR_DATA, {}))
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required(WS_TYPE): "hassio/update/addon",
vol.Required("addon"): str,
vol.Required("backup"): bool,
}
)
@websocket_api.async_response
async def websocket_update_addon(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Websocket handler to update an addon."""
addon_name: str | None = None
addon_version: str | None = None
addons: list = (get_supervisor_info(hass) or {}).get("addons", [])
for addon in addons:
if addon[ATTR_SLUG] == msg["addon"]:
addon_name = addon[ATTR_NAME]
addon_version = addon[ATTR_VERSION]
break
await update_addon(hass, msg["addon"], msg["backup"], addon_name, addon_version)
connection.send_result(msg[WS_ID])
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required(WS_TYPE): "hassio/update/core",
vol.Required("backup"): bool,
}
)
@websocket_api.async_response
async def websocket_update_core(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Websocket handler to update an addon."""
await update_core(hass, None, msg["backup"])
connection.send_result(msg[WS_ID])

View File

@ -528,7 +528,7 @@ def resolution_suggestions_for_issue_fixture(supervisor_client: AsyncMock) -> As
@pytest.fixture(name="supervisor_client") @pytest.fixture(name="supervisor_client")
def supervisor_client() -> Generator[AsyncMock]: def supervisor_client() -> Generator[AsyncMock]:
"""Mock the supervisor client.""" """Mock the supervisor client."""
mounts_info_mock = AsyncMock(spec_set=["mounts"]) mounts_info_mock = AsyncMock(spec_set=["default_backup_mount", "mounts"])
mounts_info_mock.mounts = [] mounts_info_mock.mounts = []
supervisor_client = AsyncMock() supervisor_client = AsyncMock()
supervisor_client.addons = AsyncMock() supervisor_client.addons = AsyncMock()
@ -572,6 +572,10 @@ def supervisor_client() -> Generator[AsyncMock]:
"homeassistant.components.hassio.repairs.get_supervisor_client", "homeassistant.components.hassio.repairs.get_supervisor_client",
return_value=supervisor_client, return_value=supervisor_client,
), ),
patch(
"homeassistant.components.hassio.update_helper.get_supervisor_client",
return_value=supervisor_client,
),
): ):
yield supervisor_client yield supervisor_client

View File

@ -2,14 +2,17 @@
from datetime import timedelta from datetime import timedelta
import os import os
from typing import Any
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from aiohasupervisor import SupervisorBadRequestError, SupervisorError from aiohasupervisor import SupervisorBadRequestError, SupervisorError
from aiohasupervisor.models import StoreAddonUpdate from aiohasupervisor.models import HomeAssistantUpdateOptions, StoreAddonUpdate
import pytest import pytest
from homeassistant.components.backup import BackupManagerError
from homeassistant.components.hassio import DOMAIN from homeassistant.components.hassio import DOMAIN
from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY
from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -216,12 +219,119 @@ async def test_update_addon(hass: HomeAssistant, update_addon: AsyncMock) -> Non
assert result assert result
await hass.async_block_till_done() await hass.async_block_till_done()
with patch(
"homeassistant.components.backup.manager.BackupManager.async_create_backup",
) as mock_create_backup:
await hass.services.async_call( await hass.services.async_call(
"update", "update",
"install", "install",
{"entity_id": "update.test_update"}, {"entity_id": "update.test_update"},
blocking=True, blocking=True,
) )
mock_create_backup.assert_not_called()
update_addon.assert_called_once_with("test", StoreAddonUpdate(backup=False))
@pytest.mark.parametrize(
("commands", "default_mount", "expected_kwargs"),
[
(
[],
None,
{
"agent_ids": ["hassio.local"],
"include_addons": ["test"],
"include_all_addons": False,
"include_database": False,
"include_folders": None,
"include_homeassistant": False,
"name": "test 2.0.0",
"password": None,
},
),
(
[],
"my_nas",
{
"agent_ids": ["hassio.my_nas"],
"include_addons": ["test"],
"include_all_addons": False,
"include_database": False,
"include_folders": None,
"include_homeassistant": False,
"name": "test 2.0.0",
"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": ["hassio.local"],
"include_addons": ["test"],
"include_all_addons": False,
"include_database": False,
"include_folders": None,
"include_homeassistant": False,
"name": "test 2.0.0",
"password": "hunter2",
},
),
],
)
async def test_update_addon_with_backup(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
supervisor_client: AsyncMock,
update_addon: AsyncMock,
commands: list[dict[str, Any]],
default_mount: str | None,
expected_kwargs: dict[str, Any],
) -> None:
"""Test updating addon 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
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
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.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.test_update", "backup": True},
blocking=True,
)
mock_create_backup.assert_called_once_with(**expected_kwargs)
update_addon.assert_called_once_with("test", StoreAddonUpdate(backup=False)) update_addon.assert_called_once_with("test", StoreAddonUpdate(backup=False))
@ -264,13 +374,125 @@ async def test_update_core(hass: HomeAssistant, supervisor_client: AsyncMock) ->
await hass.async_block_till_done() await hass.async_block_till_done()
supervisor_client.homeassistant.update.return_value = None supervisor_client.homeassistant.update.return_value = None
with patch(
"homeassistant.components.backup.manager.BackupManager.async_create_backup",
) as mock_create_backup:
await hass.services.async_call( await hass.services.async_call(
"update", "update",
"install", "install",
{"entity_id": "update.home_assistant_core_update"}, {"entity_id": "update.home_assistant_core_update"},
blocking=True, blocking=True,
) )
supervisor_client.homeassistant.update.assert_called_once() mock_create_backup.assert_not_called()
supervisor_client.homeassistant.update.assert_called_once_with(
HomeAssistantUpdateOptions(version=None, backup=False)
)
@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_core_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 core 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
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
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.homeassistant.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_core_update", "backup": True},
blocking=True,
)
mock_create_backup.assert_called_once_with(**expected_kwargs)
supervisor_client.homeassistant.update.assert_called_once_with(
HomeAssistantUpdateOptions(version=None, backup=False)
)
async def test_update_supervisor( async def test_update_supervisor(
@ -325,6 +547,41 @@ async def test_update_addon_with_error(
) )
async def test_update_addon_with_backup_and_error(
hass: HomeAssistant,
supervisor_client: AsyncMock,
) -> None:
"""Test updating addon 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
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
supervisor_client.homeassistant.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:"),
):
assert not await hass.services.async_call(
"update",
"install",
{"entity_id": "update.test_update", "backup": True},
blocking=True,
)
async def test_update_os_with_error( async def test_update_os_with_error(
hass: HomeAssistant, supervisor_client: AsyncMock hass: HomeAssistant, supervisor_client: AsyncMock
) -> None: ) -> None:
@ -406,6 +663,41 @@ async def test_update_core_with_error(
) )
async def test_update_core_with_backup_and_error(
hass: HomeAssistant,
supervisor_client: AsyncMock,
) -> None:
"""Test updating core 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
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
supervisor_client.homeassistant.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:"),
):
assert not await hass.services.async_call(
"update",
"install",
{"entity_id": "update.home_assistant_core_update", "backup": True},
blocking=True,
)
async def test_release_notes_between_versions( async def test_release_notes_between_versions(
hass: HomeAssistant, hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker, aioclient_mock: AiohttpClientMocker,

View File

@ -1,9 +1,15 @@
"""Test websocket API.""" """Test websocket API."""
from unittest.mock import AsyncMock import os
from typing import Any
from unittest.mock import AsyncMock, patch
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import HomeAssistantUpdateOptions, StoreAddonUpdate
import pytest import pytest
from homeassistant.components.backup import BackupManagerError
from homeassistant.components.hassio import DOMAIN
from homeassistant.components.hassio.const import ( from homeassistant.components.hassio.const import (
ATTR_DATA, ATTR_DATA,
ATTR_ENDPOINT, ATTR_ENDPOINT,
@ -15,14 +21,17 @@ from homeassistant.components.hassio.const import (
WS_TYPE_API, WS_TYPE_API,
WS_TYPE_SUBSCRIBE, WS_TYPE_SUBSCRIBE,
) )
from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import MockUser, async_mock_signal from tests.common import MockConfigEntry, MockUser, async_mock_signal
from tests.test_util.aiohttp import AiohttpClientMocker from tests.test_util.aiohttp import AiohttpClientMocker
from tests.typing import WebSocketGenerator from tests.typing import WebSocketGenerator
MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"}
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_all( def mock_all(
@ -56,7 +65,7 @@ def mock_all(
) )
aioclient_mock.get( aioclient_mock.get(
"http://127.0.0.1/core/info", "http://127.0.0.1/core/info",
json={"result": "ok", "data": {"version_latest": "1.0.0"}}, json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}},
) )
aioclient_mock.get( aioclient_mock.get(
"http://127.0.0.1/os/info", "http://127.0.0.1/os/info",
@ -64,11 +73,42 @@ def mock_all(
) )
aioclient_mock.get( aioclient_mock.get(
"http://127.0.0.1/supervisor/info", "http://127.0.0.1/supervisor/info",
json={"result": "ok", "data": {"version_latest": "1.0.0"}}, json={
"result": "ok",
"data": {
"version": "1.0.0",
"version_latest": "1.0.0",
"auto_update": True,
"addons": [
{
"name": "test",
"state": "started",
"slug": "test",
"installed": True,
"update_available": True,
"icon": False,
"version": "2.0.0",
"version_latest": "2.0.1",
"repository": "core",
"url": "https://github.com/home-assistant/addons/test",
},
],
},
},
) )
aioclient_mock.get( aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
) )
aioclient_mock.get(
"http://127.0.0.1/network/info",
json={
"result": "ok",
"data": {
"host_internet": True,
"supervisor_internet": True,
},
},
)
@pytest.mark.usefixtures("hassio_env") @pytest.mark.usefixtures("hassio_env")
@ -279,3 +319,407 @@ async def test_websocket_non_admin_user(
msg = await websocket_client.receive_json() msg = await websocket_client.receive_json()
assert msg["error"]["message"] == "Unauthorized" assert msg["error"]["message"] == "Unauthorized"
async def test_update_addon(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
update_addon: AsyncMock,
) -> None:
"""Test updating addon."""
client = await hass_ws_client(hass)
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 hass.async_block_till_done()
with patch(
"homeassistant.components.backup.manager.BackupManager.async_create_backup",
) as mock_create_backup:
await client.send_json_auto_id(
{"type": "hassio/update/addon", "addon": "test", "backup": False}
)
result = await client.receive_json()
assert result["success"]
mock_create_backup.assert_not_called()
update_addon.assert_called_once_with("test", StoreAddonUpdate(backup=False))
@pytest.mark.parametrize(
("commands", "default_mount", "expected_kwargs"),
[
(
[],
None,
{
"agent_ids": ["hassio.local"],
"include_addons": ["test"],
"include_all_addons": False,
"include_database": False,
"include_folders": None,
"include_homeassistant": False,
"name": "test 2.0.0",
"password": None,
},
),
(
[],
"my_nas",
{
"agent_ids": ["hassio.my_nas"],
"include_addons": ["test"],
"include_all_addons": False,
"include_database": False,
"include_folders": None,
"include_homeassistant": False,
"name": "test 2.0.0",
"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": ["hassio.local"],
"include_addons": ["test"],
"include_all_addons": False,
"include_database": False,
"include_folders": None,
"include_homeassistant": False,
"name": "test 2.0.0",
"password": "hunter2",
},
),
],
)
async def test_update_addon_with_backup(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
supervisor_client: AsyncMock,
update_addon: AsyncMock,
commands: list[dict[str, Any]],
default_mount: str | None,
expected_kwargs: dict[str, Any],
) -> None:
"""Test updating addon with backup."""
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
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
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.mounts.info.return_value.default_backup_mount = default_mount
with patch(
"homeassistant.components.backup.manager.BackupManager.async_create_backup",
) as mock_create_backup:
await client.send_json_auto_id(
{"type": "hassio/update/addon", "addon": "test", "backup": True}
)
result = await client.receive_json()
assert result["success"]
mock_create_backup.assert_called_once_with(**expected_kwargs)
update_addon.assert_called_once_with("test", StoreAddonUpdate(backup=False))
async def test_update_core(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
supervisor_client: AsyncMock,
) -> None:
"""Test updating core."""
client = await hass_ws_client(hass)
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 hass.async_block_till_done()
supervisor_client.homeassistant.update.return_value = None
with patch(
"homeassistant.components.backup.manager.BackupManager.async_create_backup",
) as mock_create_backup:
await client.send_json_auto_id({"type": "hassio/update/core", "backup": False})
result = await client.receive_json()
assert result["success"]
mock_create_backup.assert_not_called()
supervisor_client.homeassistant.update.assert_called_once_with(
HomeAssistantUpdateOptions(version=None, backup=False)
)
@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_core_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 core with backup."""
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
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
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.homeassistant.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 client.send_json_auto_id({"type": "hassio/update/core", "backup": True})
result = await client.receive_json()
assert result["success"]
mock_create_backup.assert_called_once_with(**expected_kwargs)
supervisor_client.homeassistant.update.assert_called_once_with(
HomeAssistantUpdateOptions(version=None, backup=False)
)
async def test_update_addon_with_error(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
update_addon: AsyncMock,
) -> None:
"""Test updating addon with error."""
client = await hass_ws_client(hass)
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
await hass.async_block_till_done()
update_addon.side_effect = SupervisorError
await client.send_json_auto_id(
{"type": "hassio/update/addon", "addon": "test", "backup": False}
)
result = await client.receive_json()
assert not result["success"]
assert result["error"] == {
"code": "home_assistant_error",
"message": "Error updating test: ",
}
async def test_update_addon_with_backup_and_error(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
supervisor_client: AsyncMock,
) -> None:
"""Test updating addon with backup and error."""
client = await hass_ws_client(hass)
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
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
supervisor_client.homeassistant.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,
),
):
await client.send_json_auto_id(
{"type": "hassio/update/addon", "addon": "test", "backup": True}
)
result = await client.receive_json()
assert not result["success"]
assert result["error"] == {
"code": "home_assistant_error",
"message": "Error creating backup: ",
}
async def test_update_core_with_error(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
supervisor_client: AsyncMock,
) -> None:
"""Test updating core with error."""
client = await hass_ws_client(hass)
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
await hass.async_block_till_done()
supervisor_client.homeassistant.update.side_effect = SupervisorError
await client.send_json_auto_id({"type": "hassio/update/core", "backup": False})
result = await client.receive_json()
assert not result["success"]
assert result["error"] == {
"code": "home_assistant_error",
"message": "Error updating Home Assistant Core: ",
}
async def test_update_core_with_backup_and_error(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
supervisor_client: AsyncMock,
) -> None:
"""Test updating core with backup and error."""
client = await hass_ws_client(hass)
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
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
supervisor_client.homeassistant.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,
),
):
await client.send_json_auto_id(
{"type": "hassio/update/addon", "addon": "test", "backup": True}
)
result = await client.receive_json()
assert not result["success"]
assert result["error"] == {
"code": "home_assistant_error",
"message": "Error creating backup: ",
}