mirror of
https://github.com/home-assistant/core.git
synced 2026-09-09 23:21:00 +01:00
Fix hassio.backup_partial AttributeError when folders are specified (#170312)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
dc9eba372a
commit
6d8b010245
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
from aiohasupervisor import SupervisorClient, SupervisorError
|
||||
from aiohasupervisor.models import (
|
||||
Folder,
|
||||
FullBackupOptions,
|
||||
FullRestoreOptions,
|
||||
PartialBackupOptions,
|
||||
@@ -70,6 +71,31 @@ SERVICE_MOUNT_RELOAD = "mount_reload"
|
||||
|
||||
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
|
||||
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."""
|
||||
@@ -113,7 +139,10 @@ SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend(
|
||||
{
|
||||
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)
|
||||
@@ -136,7 +165,10 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend(
|
||||
{
|
||||
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)
|
||||
@@ -343,9 +375,7 @@ def async_register_backup_restore_services(
|
||||
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:
|
||||
@@ -392,8 +422,7 @@ def async_register_backup_restore_services(
|
||||
"""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:
|
||||
|
||||
@@ -14,6 +14,7 @@ from aiohasupervisor.models import (
|
||||
AddonStage,
|
||||
AddonState,
|
||||
CIFSMountResponse,
|
||||
Folder,
|
||||
FullBackupOptions,
|
||||
HomeAssistantOptions,
|
||||
InstalledAddon,
|
||||
@@ -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",
|
||||
)
|
||||
)
|
||||
@@ -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",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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."""
|
||||
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."""
|
||||
|
||||
Reference in New Issue
Block a user