Skip to content
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
43 changes: 36 additions & 7 deletions homeassistant/components/hassio/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from aiohasupervisor import SupervisorClient, SupervisorError
from aiohasupervisor.models import (
Folder,
FullBackupOptions,
FullRestoreOptions,
PartialBackupOptions,
Expand Down Expand Up @@ -70,6 +71,31 @@

VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$"))

# Legacy alias used by the Supervisor API for the homeassistant flag, kept
# for backwards compatibility with existing automations.
LEGACY_FOLDER_HOMEASSISTANT = "homeassistant"


def _normalize_partial_options_data(data: dict[str, Any]) -> dict[str, Any]:
"""Map legacy aliases used by both partial backup and partial restore handlers."""
if ATTR_APPS in data:
data[ATTR_ADDONS] = data.pop(ATTR_APPS)
if ATTR_FOLDERS in data:
folders: set[Any] = set(data[ATTR_FOLDERS])
if LEGACY_FOLDER_HOMEASSISTANT in folders:
folders.discard(LEGACY_FOLDER_HOMEASSISTANT)
if data.get(ATTR_HOMEASSISTANT) is False:
raise ServiceValidationError(
f"{ATTR_HOMEASSISTANT}=False conflicts with the legacy "
f"{LEGACY_FOLDER_HOMEASSISTANT!r} entry in {ATTR_FOLDERS}"
)
data[ATTR_HOMEASSISTANT] = True
Comment thread
agners marked this conversation as resolved.
if folders:
data[ATTR_FOLDERS] = folders
else:
data.pop(ATTR_FOLDERS)
return data


def valid_addon(value: Any) -> str:
"""Validate value is a valid addon slug."""
Expand Down Expand Up @@ -113,7 +139,10 @@ def valid_addon(value: Any) -> str:
{
vol.Optional(ATTR_HOMEASSISTANT): cv.boolean,
vol.Optional(ATTR_FOLDERS): vol.All(
cv.ensure_list, [cv.string], vol.Unique(), vol.Coerce(set)
cv.ensure_list,
[vol.Any(LEGACY_FOLDER_HOMEASSISTANT, vol.Coerce(Folder))],
vol.Unique(),
vol.Coerce(set),
),
vol.Exclusive(ATTR_APPS, "apps_or_addons"): vol.All(
cv.ensure_list, [VALID_ADDON_SLUG], vol.Unique(), vol.Coerce(set)
Expand All @@ -136,7 +165,10 @@ def valid_addon(value: Any) -> str:
{
vol.Optional(ATTR_HOMEASSISTANT): cv.boolean,
vol.Optional(ATTR_FOLDERS): vol.All(
cv.ensure_list, [cv.string], vol.Unique(), vol.Coerce(set)
cv.ensure_list,
[vol.Any(LEGACY_FOLDER_HOMEASSISTANT, vol.Coerce(Folder))],
vol.Unique(),
vol.Coerce(set),
),
vol.Exclusive(ATTR_APPS, "apps_or_addons"): vol.All(
cv.ensure_list, [VALID_ADDON_SLUG], vol.Unique(), vol.Coerce(set)
Expand Down Expand Up @@ -343,9 +375,7 @@ async def async_partial_backup_service_handler(
service: ServiceCall,
) -> ServiceResponse:
"""Handler for create partial backup service. Returns the new backup's ID."""
data = service.data.copy()
if ATTR_APPS in data:
data[ATTR_ADDONS] = data.pop(ATTR_APPS)
data = _normalize_partial_options_data(service.data.copy())
options = PartialBackupOptions(**data)

try:
Expand Down Expand Up @@ -392,8 +422,7 @@ async def async_partial_restore_service_handler(service: ServiceCall) -> None:
"""Handler for partial restore service."""
data = service.data.copy()
backup_slug = data.pop(ATTR_SLUG)
if ATTR_APPS in data:
data[ATTR_ADDONS] = data.pop(ATTR_APPS)
data = _normalize_partial_options_data(data)
options = PartialRestoreOptions(**data)

try:
Expand Down
82 changes: 80 additions & 2 deletions tests/components/hassio/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
AddonStage,
AddonState,
CIFSMountResponse,
Folder,
FullBackupOptions,
HomeAssistantOptions,
InstalledAddon,
Expand Down Expand Up @@ -528,7 +529,7 @@ async def test_service_calls(
name="2021-11-13 03:48:00",
homeassistant=True,
addons={"test"},
folders={"ssl"},
folders={Folder.SSL},
password="123456",
)
)
Expand All @@ -552,7 +553,10 @@ async def test_service_calls(
supervisor_client.backups.partial_restore.assert_called_once_with(
"test",
PartialRestoreOptions(
homeassistant=False, addons={"test"}, folders={"ssl"}, password="123456"
homeassistant=False,
addons={"test"},
folders={Folder.SSL},
password="123456",
),
)

Expand Down Expand Up @@ -770,6 +774,80 @@ async def test_invalid_service_calls_folder_duplicates(hass: HomeAssistant) -> N
)


@pytest.mark.usefixtures("hassio_env")
async def test_partial_backup_legacy_homeassistant_folder(
hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
"""Test that the legacy "homeassistant" folder is translated to homeassistant=True."""
Comment thread
agners marked this conversation as resolved.
assert await async_setup_component(hass, "hassio", {})
supervisor_client.backups.partial_backup.return_value = NewBackup(
job_id=uuid4(), slug="partial"
)

await hass.services.async_call(
"hassio",
"backup_partial",
{"folders": ["homeassistant", "ssl"], "name": "test"},
blocking=True,
)
supervisor_client.backups.partial_backup.assert_called_once_with(
PartialBackupOptions(
name="test",
homeassistant=True,
folders={Folder.SSL},
)
)


@pytest.mark.usefixtures("hassio_env")
async def test_partial_restore_legacy_homeassistant_folder(
hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
"""Test that the legacy "homeassistant" folder is translated for restore too."""
assert await async_setup_component(hass, "hassio", {})

await hass.services.async_call(
"hassio",
"restore_partial",
{"slug": "test", "folders": ["homeassistant", "ssl"]},
blocking=True,
)
supervisor_client.backups.partial_restore.assert_called_once_with(
"test",
PartialRestoreOptions(
homeassistant=True,
folders={Folder.SSL},
),
)


@pytest.mark.usefixtures("hassio_env", "supervisor_client")
async def test_partial_backup_invalid_folder(hass: HomeAssistant) -> None:
"""Test that an unknown folder name is rejected."""
assert await async_setup_component(hass, "hassio", {})

with pytest.raises(Invalid, match="not a valid value"):
await hass.services.async_call(
"hassio", "backup_partial", {"folders": ["bogus"]}
)


@pytest.mark.usefixtures("hassio_env", "supervisor_client")
async def test_partial_backup_legacy_homeassistant_folder_conflict(
hass: HomeAssistant,
) -> None:
"""Reject combining homeassistant=False with the legacy "homeassistant" folder."""
assert await async_setup_component(hass, "hassio", {})

with pytest.raises(ServiceValidationError, match="conflicts"):
await hass.services.async_call(
"hassio",
"backup_partial",
{"homeassistant": False, "folders": ["homeassistant"]},
blocking=True,
)


@pytest.mark.usefixtures("addon_installed")
async def test_entry_load_and_unload(hass: HomeAssistant) -> None:
"""Test loading and unloading config entry."""
Expand Down