Make local backup a backup agent
This commit is contained in:
parent
e8179f7a73
commit
400f792bff
9 changed files with 372 additions and 216 deletions
|
@ -32,6 +32,7 @@ SERVICE_CREATE_SCHEMA = vol.Schema({vol.Optional(CONF_PASSWORD): str})
|
|||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Backup integration."""
|
||||
hass.data[DOMAIN] = backup_manager = BackupManager(hass)
|
||||
await backup_manager.async_setup()
|
||||
|
||||
with_hassio = is_hassio(hass)
|
||||
|
||||
|
@ -49,6 +50,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
"""Service handler for creating backups."""
|
||||
await backup_manager.async_create_backup(
|
||||
addons_included=None,
|
||||
# pylint: disable=fixme
|
||||
# TODO: Don't forget to remove this when the implementation is complete
|
||||
agent_ids=[], # TODO: Should we default to local?
|
||||
database_included=True,
|
||||
folders_included=None,
|
||||
name=None,
|
||||
|
|
146
homeassistant/components/backup/backup.py
Normal file
146
homeassistant/components/backup/backup.py
Normal file
|
@ -0,0 +1,146 @@
|
|||
"""Local backup support for Core and Container installations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tarfile
|
||||
from tarfile import TarError
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util.json import json_loads_object
|
||||
|
||||
from .agent import BackupAgent, UploadedBackup
|
||||
from .const import BUF_SIZE, LOGGER
|
||||
from .models import BackupUploadMetadata
|
||||
|
||||
|
||||
async def async_get_backup_agents(
|
||||
hass: HomeAssistant,
|
||||
**kwargs: Any,
|
||||
) -> list[BackupAgent]:
|
||||
"""Register the backup agent."""
|
||||
return [LocalBackupAgent(hass)]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class LocalBackup(UploadedBackup):
|
||||
"""Local backup class."""
|
||||
|
||||
path: Path
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
"""Return a dict representation of this backup."""
|
||||
return {**asdict(self), "path": self.path.as_posix()}
|
||||
|
||||
|
||||
class LocalBackupAgent(BackupAgent):
|
||||
"""Define the format that backup agents can have."""
|
||||
|
||||
name = "local"
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the backup agent."""
|
||||
super().__init__()
|
||||
self._hass = hass
|
||||
self.backup_dir = Path(hass.config.path("backups"))
|
||||
self.backups: dict[str, LocalBackup] = {}
|
||||
self.loaded_backups = False
|
||||
|
||||
async def load_backups(self) -> None:
|
||||
"""Load data of stored backup files."""
|
||||
backups = await self._hass.async_add_executor_job(self._read_backups)
|
||||
LOGGER.debug("Loaded %s local backups", len(backups))
|
||||
self.backups = backups
|
||||
self.loaded_backups = True
|
||||
|
||||
def _read_backups(self) -> dict[str, LocalBackup]:
|
||||
"""Read backups from disk."""
|
||||
backups: dict[str, LocalBackup] = {}
|
||||
for backup_path in self.backup_dir.glob("*.tar"):
|
||||
try:
|
||||
with tarfile.open(backup_path, "r:", bufsize=BUF_SIZE) as backup_file:
|
||||
if data_file := backup_file.extractfile("./backup.json"):
|
||||
data = json_loads_object(data_file.read())
|
||||
backup = LocalBackup(
|
||||
id=cast(str, data["slug"]), # Do we need another ID?
|
||||
slug=cast(str, data["slug"]),
|
||||
name=cast(str, data["name"]),
|
||||
date=cast(str, data["date"]),
|
||||
path=backup_path,
|
||||
size=round(backup_path.stat().st_size / 1_048_576, 2),
|
||||
protected=cast(bool, data.get("protected", False)),
|
||||
)
|
||||
backups[backup.slug] = backup
|
||||
except (OSError, TarError, json.JSONDecodeError, KeyError) as err:
|
||||
LOGGER.warning("Unable to read backup %s: %s", backup_path, err)
|
||||
return backups
|
||||
|
||||
async def async_download_backup(
|
||||
self,
|
||||
*,
|
||||
id: str,
|
||||
path: Path,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Download a backup file."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_upload_backup(
|
||||
self,
|
||||
*,
|
||||
path: Path,
|
||||
metadata: BackupUploadMetadata,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Upload a backup."""
|
||||
self.backups[metadata.slug] = LocalBackup(
|
||||
id=metadata.slug, # Do we need another ID?
|
||||
slug=metadata.slug,
|
||||
name=metadata.name,
|
||||
date=metadata.date,
|
||||
path=path,
|
||||
size=round(path.stat().st_size / 1_048_576, 2),
|
||||
protected=metadata.protected,
|
||||
)
|
||||
|
||||
async def async_list_backups(self, **kwargs: Any) -> list[UploadedBackup]:
|
||||
"""List backups."""
|
||||
if not self.loaded_backups:
|
||||
await self.load_backups()
|
||||
return list(self.backups.values())
|
||||
|
||||
async def async_get_backup(
|
||||
self, *, slug: str, **kwargs: Any
|
||||
) -> UploadedBackup | None:
|
||||
"""Return a backup."""
|
||||
if not self.loaded_backups:
|
||||
await self.load_backups()
|
||||
|
||||
if not (backup := self.backups.get(slug)):
|
||||
return None
|
||||
|
||||
if not backup.path.exists():
|
||||
LOGGER.debug(
|
||||
(
|
||||
"Removing tracked backup (%s) that does not exists on the expected"
|
||||
" path %s"
|
||||
),
|
||||
backup.slug,
|
||||
backup.path,
|
||||
)
|
||||
self.backups.pop(slug)
|
||||
return None
|
||||
|
||||
return backup
|
||||
|
||||
async def async_remove_backup(self, *, slug: str, **kwargs: Any) -> None:
|
||||
"""Remove a backup."""
|
||||
if (backup := await self.async_get_backup(slug=slug)) is None:
|
||||
return
|
||||
|
||||
await self._hass.async_add_executor_job(backup.path.unlink, True) # type: ignore[attr-defined]
|
||||
LOGGER.debug("Removed backup located at %s", backup.path) # type: ignore[attr-defined]
|
||||
self.backups.pop(slug)
|
|
@ -11,6 +11,7 @@ if TYPE_CHECKING:
|
|||
from .manager import BaseBackupManager
|
||||
from .models import BaseBackup
|
||||
|
||||
BUF_SIZE = 2**20 * 4 # 4MB
|
||||
DOMAIN = "backup"
|
||||
DATA_MANAGER: HassKey[BaseBackupManager[BaseBackup]] = HassKey(DOMAIN)
|
||||
LOGGER = getLogger(__package__)
|
||||
|
|
|
@ -13,10 +13,9 @@ from pathlib import Path
|
|||
from queue import SimpleQueue
|
||||
import shutil
|
||||
import tarfile
|
||||
from tarfile import TarError
|
||||
from tempfile import TemporaryDirectory
|
||||
import time
|
||||
from typing import Any, Generic, Protocol, cast
|
||||
from typing import Any, Generic, Protocol
|
||||
|
||||
import aiohttp
|
||||
from securetar import SecureTarFile, atomic_contents_add
|
||||
|
@ -29,13 +28,22 @@ from homeassistant.exceptions import HomeAssistantError
|
|||
from homeassistant.helpers import integration_platform
|
||||
from homeassistant.helpers.json import json_bytes
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.json import json_loads_object
|
||||
|
||||
from .agent import BackupAgent, BackupAgentPlatformProtocol
|
||||
from .const import DOMAIN, EXCLUDE_DATABASE_FROM_BACKUP, EXCLUDE_FROM_BACKUP, LOGGER
|
||||
from .const import (
|
||||
BUF_SIZE,
|
||||
DOMAIN,
|
||||
EXCLUDE_DATABASE_FROM_BACKUP,
|
||||
EXCLUDE_FROM_BACKUP,
|
||||
LOGGER,
|
||||
)
|
||||
from .models import BackupUploadMetadata, BaseBackup
|
||||
|
||||
BUF_SIZE = 2**20 * 4 # 4MB
|
||||
# pylint: disable=fixme
|
||||
# TODO: Don't forget to remove this when the implementation is complete
|
||||
|
||||
|
||||
LOCAL_AGENT_ID = f"{DOMAIN}.local"
|
||||
|
||||
_BackupT = TypeVar("_BackupT", bound=BaseBackup, default=BaseBackup)
|
||||
|
||||
|
@ -51,6 +59,7 @@ class NewBackup:
|
|||
class Backup(BaseBackup):
|
||||
"""Backup class."""
|
||||
|
||||
agent_ids: list[str]
|
||||
path: Path
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
|
@ -84,20 +93,21 @@ class BaseBackupManager(abc.ABC, Generic[_BackupT]):
|
|||
"""Initialize the backup manager."""
|
||||
self.hass = hass
|
||||
self.backup_task: asyncio.Task | None = None
|
||||
self.backups: dict[str, _BackupT] = {}
|
||||
self.loaded_platforms = False
|
||||
self.platforms: dict[str, BackupPlatformProtocol] = {}
|
||||
self.backup_agents: dict[str, BackupAgent] = {}
|
||||
self.syncing = False
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up the backup manager."""
|
||||
await self.load_platforms()
|
||||
|
||||
@callback
|
||||
def _add_platform_pre_post_handlers(
|
||||
def _add_platform_pre_post_handler(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
integration_domain: str,
|
||||
platform: BackupPlatformProtocol,
|
||||
) -> None:
|
||||
"""Add a platform to the backup manager."""
|
||||
"""Add a backup platform."""
|
||||
if not hasattr(platform, "async_pre_backup") or not hasattr(
|
||||
platform, "async_post_backup"
|
||||
):
|
||||
|
@ -107,7 +117,6 @@ class BaseBackupManager(abc.ABC, Generic[_BackupT]):
|
|||
|
||||
async def _async_add_platform_agents(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
integration_domain: str,
|
||||
platform: BackupAgentPlatformProtocol,
|
||||
) -> None:
|
||||
|
@ -115,16 +124,23 @@ class BaseBackupManager(abc.ABC, Generic[_BackupT]):
|
|||
if not hasattr(platform, "async_get_backup_agents"):
|
||||
return
|
||||
|
||||
agents = await platform.async_get_backup_agents(hass=hass)
|
||||
agents = await platform.async_get_backup_agents(self.hass)
|
||||
self.backup_agents.update(
|
||||
{f"{integration_domain}.{agent.name}": agent for agent in agents}
|
||||
)
|
||||
|
||||
async def _add_platform(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
integration_domain: str,
|
||||
platform: Any,
|
||||
) -> None:
|
||||
"""Add a backup platform manager."""
|
||||
self._add_platform_pre_post_handler(integration_domain, platform)
|
||||
await self._async_add_platform_agents(integration_domain, platform)
|
||||
|
||||
async def async_pre_backup_actions(self, **kwargs: Any) -> None:
|
||||
"""Perform pre backup actions."""
|
||||
if not self.loaded_platforms:
|
||||
await self.load_platforms()
|
||||
|
||||
pre_backup_results = await asyncio.gather(
|
||||
*(
|
||||
platform.async_pre_backup(self.hass)
|
||||
|
@ -138,9 +154,6 @@ class BaseBackupManager(abc.ABC, Generic[_BackupT]):
|
|||
|
||||
async def async_post_backup_actions(self, **kwargs: Any) -> None:
|
||||
"""Perform post backup actions."""
|
||||
if not self.loaded_platforms:
|
||||
await self.load_platforms()
|
||||
|
||||
post_backup_results = await asyncio.gather(
|
||||
*(
|
||||
platform.async_post_backup(self.hass)
|
||||
|
@ -154,23 +167,14 @@ class BaseBackupManager(abc.ABC, Generic[_BackupT]):
|
|||
|
||||
async def load_platforms(self) -> None:
|
||||
"""Load backup platforms."""
|
||||
if self.loaded_platforms:
|
||||
return
|
||||
await integration_platform.async_process_integration_platforms(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
self._add_platform_pre_post_handlers,
|
||||
wait_for_platforms=True,
|
||||
)
|
||||
await integration_platform.async_process_integration_platforms(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
self._async_add_platform_agents,
|
||||
self._add_platform,
|
||||
wait_for_platforms=True,
|
||||
)
|
||||
LOGGER.debug("Loaded %s platforms", len(self.platforms))
|
||||
LOGGER.debug("Loaded %s agents", len(self.backup_agents))
|
||||
self.loaded_platforms = True
|
||||
|
||||
@abc.abstractmethod
|
||||
async def async_restore_backup(
|
||||
|
@ -187,6 +191,7 @@ class BaseBackupManager(abc.ABC, Generic[_BackupT]):
|
|||
self,
|
||||
*,
|
||||
addons_included: list[str] | None,
|
||||
agent_ids: list[str],
|
||||
database_included: bool,
|
||||
folders_included: list[str] | None,
|
||||
name: str | None,
|
||||
|
@ -236,22 +241,34 @@ class BackupManager(BaseBackupManager[Backup]):
|
|||
"""Initialize the backup manager."""
|
||||
super().__init__(hass=hass)
|
||||
self.backup_dir = Path(hass.config.path("backups"))
|
||||
self.loaded_backups = False
|
||||
self.temp_backup_dir = Path(hass.config.path("tmp_backups"))
|
||||
|
||||
async def async_upload_backup(self, *, slug: str, **kwargs: Any) -> None:
|
||||
"""Upload a backup."""
|
||||
await self.load_platforms()
|
||||
|
||||
"""Upload a backup to all agents."""
|
||||
if not self.backup_agents:
|
||||
return
|
||||
|
||||
if not (backup := await self.async_get_backup(slug=slug)):
|
||||
return
|
||||
|
||||
await self._async_upload_backup(
|
||||
slug=slug,
|
||||
backup=backup,
|
||||
agent_ids=list(self.backup_agents.keys()),
|
||||
)
|
||||
|
||||
async def _async_upload_backup(
|
||||
self,
|
||||
*,
|
||||
slug: str,
|
||||
backup: Backup,
|
||||
agent_ids: list[str],
|
||||
) -> None:
|
||||
"""Upload a backup to selected agents."""
|
||||
self.syncing = True
|
||||
sync_backup_results = await asyncio.gather(
|
||||
*(
|
||||
agent.async_upload_backup(
|
||||
self.backup_agents[agent_id].async_upload_backup(
|
||||
path=backup.path,
|
||||
metadata=BackupUploadMetadata(
|
||||
homeassistant=HAVERSION,
|
||||
|
@ -262,80 +279,48 @@ class BackupManager(BaseBackupManager[Backup]):
|
|||
protected=backup.protected,
|
||||
),
|
||||
)
|
||||
for agent in self.backup_agents.values()
|
||||
for agent_id in agent_ids
|
||||
),
|
||||
return_exceptions=True,
|
||||
)
|
||||
for result in sync_backup_results:
|
||||
if isinstance(result, Exception):
|
||||
LOGGER.error("Error during backup upload - %s", result)
|
||||
# TODO: Reset self.syncing in a finally block
|
||||
self.syncing = False
|
||||
|
||||
async def load_backups(self) -> None:
|
||||
"""Load data of stored backup files."""
|
||||
backups = await self.hass.async_add_executor_job(self._read_backups)
|
||||
LOGGER.debug("Loaded %s local backups", len(backups))
|
||||
self.backups = backups
|
||||
self.loaded_backups = True
|
||||
|
||||
def _read_backups(self) -> dict[str, Backup]:
|
||||
"""Read backups from disk."""
|
||||
backups: dict[str, Backup] = {}
|
||||
for backup_path in self.backup_dir.glob("*.tar"):
|
||||
try:
|
||||
with tarfile.open(backup_path, "r:", bufsize=BUF_SIZE) as backup_file:
|
||||
if data_file := backup_file.extractfile("./backup.json"):
|
||||
data = json_loads_object(data_file.read())
|
||||
backup = Backup(
|
||||
slug=cast(str, data["slug"]),
|
||||
name=cast(str, data["name"]),
|
||||
date=cast(str, data["date"]),
|
||||
path=backup_path,
|
||||
size=round(backup_path.stat().st_size / 1_048_576, 2),
|
||||
protected=cast(bool, data.get("protected", False)),
|
||||
)
|
||||
backups[backup.slug] = backup
|
||||
except (OSError, TarError, json.JSONDecodeError, KeyError) as err:
|
||||
LOGGER.warning("Unable to read backup %s: %s", backup_path, err)
|
||||
return backups
|
||||
|
||||
async def async_get_backups(self, **kwargs: Any) -> dict[str, Backup]:
|
||||
"""Return backups."""
|
||||
if not self.loaded_backups:
|
||||
await self.load_backups()
|
||||
backups: dict[str, Backup] = {}
|
||||
for agent_id, agent in self.backup_agents.items():
|
||||
agent_backups = await agent.async_list_backups()
|
||||
for agent_backup in agent_backups:
|
||||
if agent_backup.slug not in backups:
|
||||
backups[agent_backup.slug] = Backup(
|
||||
slug=agent_backup.slug,
|
||||
name=agent_backup.name,
|
||||
date=agent_backup.date,
|
||||
agent_ids=[],
|
||||
# TODO: Do we need to expose the path?
|
||||
path=agent_backup.path, # type: ignore[attr-defined]
|
||||
size=agent_backup.size,
|
||||
protected=agent_backup.protected,
|
||||
)
|
||||
backups[agent_backup.slug].agent_ids.append(agent_id)
|
||||
|
||||
return self.backups
|
||||
return backups
|
||||
|
||||
async def async_get_backup(self, *, slug: str, **kwargs: Any) -> Backup | None:
|
||||
"""Return a backup."""
|
||||
if not self.loaded_backups:
|
||||
await self.load_backups()
|
||||
|
||||
if not (backup := self.backups.get(slug)):
|
||||
return None
|
||||
|
||||
if not backup.path.exists():
|
||||
LOGGER.debug(
|
||||
(
|
||||
"Removing tracked backup (%s) that does not exists on the expected"
|
||||
" path %s"
|
||||
),
|
||||
backup.slug,
|
||||
backup.path,
|
||||
)
|
||||
self.backups.pop(slug)
|
||||
return None
|
||||
|
||||
return backup
|
||||
# TODO: This is not efficient, but it's fine for draft
|
||||
backups = await self.async_get_backups()
|
||||
return backups.get(slug)
|
||||
|
||||
async def async_remove_backup(self, *, slug: str, **kwargs: Any) -> None:
|
||||
"""Remove a backup."""
|
||||
if (backup := await self.async_get_backup(slug=slug)) is None:
|
||||
return
|
||||
|
||||
await self.hass.async_add_executor_job(backup.path.unlink, True)
|
||||
LOGGER.debug("Removed backup located at %s", backup.path)
|
||||
self.backups.pop(slug)
|
||||
# TODO: We should only remove from the agents that have the backup
|
||||
for agent in self.backup_agents.values():
|
||||
await agent.async_remove_backup(slug=slug) # type: ignore[attr-defined]
|
||||
|
||||
async def async_receive_backup(
|
||||
self,
|
||||
|
@ -392,12 +377,13 @@ class BackupManager(BaseBackupManager[Backup]):
|
|||
temp_dir_handler.cleanup()
|
||||
|
||||
await self.hass.async_add_executor_job(_move_and_cleanup)
|
||||
await self.load_backups()
|
||||
# TODO: What do we need to do instead?
|
||||
|
||||
async def async_create_backup(
|
||||
self,
|
||||
*,
|
||||
addons_included: list[str] | None,
|
||||
agent_ids: list[str],
|
||||
database_included: bool,
|
||||
folders_included: list[str] | None,
|
||||
name: str | None,
|
||||
|
@ -408,12 +394,17 @@ class BackupManager(BaseBackupManager[Backup]):
|
|||
"""Initiate generating a backup."""
|
||||
if self.backup_task:
|
||||
raise HomeAssistantError("Backup already in progress")
|
||||
if not agent_ids:
|
||||
raise HomeAssistantError("At least one agent must be selected")
|
||||
if any(agent_id not in self.backup_agents for agent_id in agent_ids):
|
||||
raise HomeAssistantError("Invalid agent selected")
|
||||
backup_name = name or f"Core {HAVERSION}"
|
||||
date_str = dt_util.now().isoformat()
|
||||
slug = _generate_slug(date_str, backup_name)
|
||||
self.backup_task = self.hass.async_create_task(
|
||||
self._async_create_backup(
|
||||
addons_included=addons_included,
|
||||
agent_ids=agent_ids,
|
||||
backup_name=backup_name,
|
||||
database_included=database_included,
|
||||
date_str=date_str,
|
||||
|
@ -431,6 +422,7 @@ class BackupManager(BaseBackupManager[Backup]):
|
|||
self,
|
||||
*,
|
||||
addons_included: list[str] | None,
|
||||
agent_ids: list[str],
|
||||
database_included: bool,
|
||||
backup_name: str,
|
||||
date_str: str,
|
||||
|
@ -441,6 +433,11 @@ class BackupManager(BaseBackupManager[Backup]):
|
|||
) -> Backup:
|
||||
"""Generate a backup."""
|
||||
success = False
|
||||
if LOCAL_AGENT_ID in agent_ids:
|
||||
backup_dir = self.backup_dir
|
||||
else:
|
||||
backup_dir = self.temp_backup_dir
|
||||
|
||||
try:
|
||||
await self.async_pre_backup_actions()
|
||||
|
||||
|
@ -458,9 +455,10 @@ class BackupManager(BaseBackupManager[Backup]):
|
|||
"protected": password is not None,
|
||||
}
|
||||
|
||||
tar_file_path = Path(self.backup_dir, f"{backup_data['slug']}.tar")
|
||||
tar_file_path = Path(backup_dir, f"{backup_data['slug']}.tar")
|
||||
size_in_bytes = await self.hass.async_add_executor_job(
|
||||
self._mkdir_and_generate_backup_contents,
|
||||
backup_dir,
|
||||
tar_file_path,
|
||||
backup_data,
|
||||
database_included,
|
||||
|
@ -473,10 +471,19 @@ class BackupManager(BaseBackupManager[Backup]):
|
|||
path=tar_file_path,
|
||||
size=round(size_in_bytes / 1_048_576, 2),
|
||||
protected=password is not None,
|
||||
agent_ids=agent_ids, # TODO: This should maybe be set after upload
|
||||
)
|
||||
if self.loaded_backups:
|
||||
self.backups[slug] = backup
|
||||
LOGGER.debug("Generated new backup with slug %s", slug)
|
||||
# TODO: We should add a cache of the backup metadata
|
||||
LOGGER.debug(
|
||||
"Generated new backup with slug %s, uploading to agents %s",
|
||||
slug,
|
||||
agent_ids,
|
||||
)
|
||||
await self._async_upload_backup(
|
||||
slug=slug, backup=backup, agent_ids=agent_ids
|
||||
)
|
||||
# TODO: Upload to other agents
|
||||
# TODO: Remove from local store if not uploaded to local agent
|
||||
success = True
|
||||
return backup
|
||||
finally:
|
||||
|
@ -487,15 +494,16 @@ class BackupManager(BaseBackupManager[Backup]):
|
|||
|
||||
def _mkdir_and_generate_backup_contents(
|
||||
self,
|
||||
backup_dir: Path,
|
||||
tar_file_path: Path,
|
||||
backup_data: dict[str, Any],
|
||||
database_included: bool,
|
||||
password: str | None = None,
|
||||
) -> int:
|
||||
"""Generate backup contents and return the size."""
|
||||
if not self.backup_dir.exists():
|
||||
LOGGER.debug("Creating backup directory")
|
||||
self.backup_dir.mkdir()
|
||||
if not backup_dir.exists():
|
||||
LOGGER.debug("Creating backup directory %s", backup_dir)
|
||||
backup_dir.mkdir()
|
||||
|
||||
excludes = EXCLUDE_FROM_BACKUP
|
||||
if not database_included:
|
||||
|
|
|
@ -120,6 +120,7 @@ async def handle_restore(
|
|||
{
|
||||
vol.Required("type"): "backup/generate",
|
||||
vol.Optional("addons_included"): [str],
|
||||
vol.Required("agent_ids"): [str],
|
||||
vol.Optional("database_included", default=True): bool,
|
||||
vol.Optional("folders_included"): [str],
|
||||
vol.Optional("name"): str,
|
||||
|
@ -139,6 +140,7 @@ async def handle_create(
|
|||
|
||||
backup = await hass.data[DATA_MANAGER].async_create_backup(
|
||||
addons_included=msg.get("addons_included"),
|
||||
agent_ids=msg["agent_ids"],
|
||||
database_included=msg["database_included"],
|
||||
folders_included=msg.get("folders_included"),
|
||||
name=msg.get("name"),
|
||||
|
@ -283,7 +285,7 @@ async def backup_agents_download(
|
|||
try:
|
||||
await agent.async_download_backup(
|
||||
id=msg["backup_id"],
|
||||
path=Path(hass.config.path("backup"), f"{msg['slug']}.tar"),
|
||||
path=Path(manager.backup_dir, f"{msg['slug']}.tar"), # type: ignore[attr-defined]
|
||||
)
|
||||
except Exception as err: # noqa: BLE001
|
||||
connection.send_error(msg["id"], "backup_agents_download", str(err))
|
||||
|
|
|
@ -12,12 +12,14 @@ from homeassistant.components.backup import (
|
|||
BackupUploadMetadata,
|
||||
UploadedBackup,
|
||||
)
|
||||
from homeassistant.components.backup.manager import Backup
|
||||
from homeassistant.components.backup.const import DATA_MANAGER
|
||||
from homeassistant.components.backup.manager import LOCAL_AGENT_ID, Backup
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
TEST_BACKUP = Backup(
|
||||
agent_ids=["backup.local"],
|
||||
slug="abc123",
|
||||
name="Test",
|
||||
date="1970-01-01T00:00:00.000Z",
|
||||
|
@ -70,7 +72,16 @@ async def setup_backup_integration(
|
|||
hass: HomeAssistant,
|
||||
with_hassio: bool = False,
|
||||
configuration: ConfigType | None = None,
|
||||
backups: list[Backup] | None = None,
|
||||
) -> bool:
|
||||
"""Set up the Backup integration."""
|
||||
with patch("homeassistant.components.backup.is_hassio", return_value=with_hassio):
|
||||
return await async_setup_component(hass, DOMAIN, configuration or {})
|
||||
result = await async_setup_component(hass, DOMAIN, configuration or {})
|
||||
if with_hassio or not backups:
|
||||
return result
|
||||
|
||||
local_agent = hass.data[DATA_MANAGER].backup_agents[LOCAL_AGENT_ID]
|
||||
local_agent.backups = {backups.slug: backups for backups in backups}
|
||||
local_agent.loaded_backups = True
|
||||
|
||||
return result
|
||||
|
|
|
@ -45,6 +45,9 @@
|
|||
dict({
|
||||
'agent_id': 'domain.test',
|
||||
}),
|
||||
dict({
|
||||
'agent_id': 'backup.local',
|
||||
}),
|
||||
]),
|
||||
'syncing': False,
|
||||
}),
|
||||
|
@ -60,6 +63,9 @@
|
|||
dict({
|
||||
'agent_id': 'domain.test',
|
||||
}),
|
||||
dict({
|
||||
'agent_id': 'backup.local',
|
||||
}),
|
||||
]),
|
||||
'syncing': False,
|
||||
}),
|
||||
|
@ -352,6 +358,9 @@
|
|||
'id': 1,
|
||||
'result': dict({
|
||||
'backup': dict({
|
||||
'agent_ids': list([
|
||||
'backup.local',
|
||||
]),
|
||||
'date': '1970-01-01T00:00:00.000Z',
|
||||
'name': 'Test',
|
||||
'path': 'abc123.tar',
|
||||
|
@ -508,6 +517,9 @@
|
|||
'backing_up': False,
|
||||
'backups': list([
|
||||
dict({
|
||||
'agent_ids': list([
|
||||
'backup.local',
|
||||
]),
|
||||
'date': '1970-01-01T00:00:00.000Z',
|
||||
'name': 'Test',
|
||||
'path': 'abc123.tar',
|
||||
|
|
|
@ -11,12 +11,14 @@ from multidict import CIMultiDict, CIMultiDictProxy
|
|||
import pytest
|
||||
|
||||
from homeassistant.components.backup import (
|
||||
DOMAIN,
|
||||
BackupAgentPlatformProtocol,
|
||||
BackupManager,
|
||||
BackupPlatformProtocol,
|
||||
BackupUploadMetadata,
|
||||
backup as local_backup_platform,
|
||||
)
|
||||
from homeassistant.components.backup.manager import BackupProgress
|
||||
from homeassistant.components.backup.manager import LOCAL_AGENT_ID, BackupProgress
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
@ -52,6 +54,7 @@ async def _mock_backup_generation(
|
|||
assert manager.backup_task is None
|
||||
await manager.async_create_backup(
|
||||
addons_included=[],
|
||||
agent_ids=[LOCAL_AGENT_ID],
|
||||
database_included=database_included,
|
||||
folders_included=[],
|
||||
name=name,
|
||||
|
@ -92,29 +95,36 @@ async def _mock_backup_generation(
|
|||
return backup
|
||||
|
||||
|
||||
async def _setup_mock_domain(
|
||||
async def _setup_backup_platform(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
domain: str = "some_domain",
|
||||
platform: BackupPlatformProtocol | BackupAgentPlatformProtocol | None = None,
|
||||
) -> None:
|
||||
"""Set up a mock domain."""
|
||||
mock_platform(hass, "some_domain.backup", platform or MockPlatform())
|
||||
assert await async_setup_component(hass, "some_domain", {})
|
||||
mock_platform(hass, f"{domain}.backup", platform or MockPlatform())
|
||||
assert await async_setup_component(hass, domain, {})
|
||||
|
||||
|
||||
async def test_constructor(hass: HomeAssistant) -> None:
|
||||
"""Test BackupManager constructor."""
|
||||
manager = BackupManager(hass)
|
||||
assert manager.backup_dir.as_posix() == hass.config.path("backups")
|
||||
assert manager.temp_backup_dir.as_posix() == hass.config.path("tmp_backups")
|
||||
|
||||
|
||||
async def test_load_backups(hass: HomeAssistant) -> None:
|
||||
"""Test loading backups."""
|
||||
manager = BackupManager(hass)
|
||||
|
||||
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
|
||||
await manager.load_platforms()
|
||||
|
||||
with (
|
||||
patch("pathlib.Path.glob", return_value=[TEST_BACKUP.path]),
|
||||
patch("tarfile.open", return_value=MagicMock()),
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.json_loads_object",
|
||||
"homeassistant.components.backup.backup.json_loads_object",
|
||||
return_value={
|
||||
"slug": TEST_BACKUP.slug,
|
||||
"name": TEST_BACKUP.name,
|
||||
|
@ -126,7 +136,7 @@ async def test_load_backups(hass: HomeAssistant) -> None:
|
|||
return_value=MagicMock(st_size=TEST_BACKUP.size),
|
||||
),
|
||||
):
|
||||
await manager.load_backups()
|
||||
await manager.backup_agents[LOCAL_AGENT_ID].load_backups()
|
||||
backups = await manager.async_get_backups()
|
||||
assert backups == {TEST_BACKUP.slug: TEST_BACKUP}
|
||||
|
||||
|
@ -137,11 +147,15 @@ async def test_load_backups_with_exception(
|
|||
) -> None:
|
||||
"""Test loading backups with exception."""
|
||||
manager = BackupManager(hass)
|
||||
|
||||
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
|
||||
await manager.load_platforms()
|
||||
|
||||
with (
|
||||
patch("pathlib.Path.glob", return_value=[TEST_BACKUP.path]),
|
||||
patch("tarfile.open", side_effect=OSError("Test exception")),
|
||||
):
|
||||
await manager.load_backups()
|
||||
await manager.backup_agents[LOCAL_AGENT_ID].load_backups()
|
||||
backups = await manager.async_get_backups()
|
||||
assert f"Unable to read backup {TEST_BACKUP.path}: Test exception" in caplog.text
|
||||
assert backups == {}
|
||||
|
@ -153,8 +167,13 @@ async def test_removing_backup(
|
|||
) -> None:
|
||||
"""Test removing backup."""
|
||||
manager = BackupManager(hass)
|
||||
manager.backups = {TEST_BACKUP.slug: TEST_BACKUP}
|
||||
manager.loaded_backups = True
|
||||
|
||||
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
|
||||
await manager.load_platforms()
|
||||
|
||||
local_agent = manager.backup_agents[LOCAL_AGENT_ID]
|
||||
local_agent.backups = {TEST_BACKUP.slug: TEST_BACKUP}
|
||||
local_agent.loaded_backups = True
|
||||
|
||||
with patch("pathlib.Path.exists", return_value=True):
|
||||
await manager.async_remove_backup(slug=TEST_BACKUP.slug)
|
||||
|
@ -168,18 +187,27 @@ async def test_removing_non_existing_backup(
|
|||
"""Test removing not existing backup."""
|
||||
manager = BackupManager(hass)
|
||||
|
||||
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
|
||||
await manager.load_platforms()
|
||||
|
||||
await manager.async_remove_backup(slug="non_existing")
|
||||
assert "Removed backup located at" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="Cleanup not implemented in the draft")
|
||||
async def test_getting_backup_that_does_not_exist(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test getting backup that does not exist."""
|
||||
manager = BackupManager(hass)
|
||||
manager.backups = {TEST_BACKUP.slug: TEST_BACKUP}
|
||||
manager.loaded_backups = True
|
||||
|
||||
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
|
||||
await manager.load_platforms()
|
||||
|
||||
local_agent = manager.backup_agents[LOCAL_AGENT_ID]
|
||||
local_agent.backups = {TEST_BACKUP.slug: TEST_BACKUP}
|
||||
local_agent.loaded_backups = True
|
||||
|
||||
with patch("pathlib.Path.exists", return_value=False):
|
||||
backup = await manager.async_get_backup(slug=TEST_BACKUP.slug)
|
||||
|
@ -199,6 +227,7 @@ async def test_async_create_backup_when_backing_up(hass: HomeAssistant) -> None:
|
|||
with pytest.raises(HomeAssistantError, match="Backup already in progress"):
|
||||
await manager.async_create_backup(
|
||||
addons_included=[],
|
||||
agent_ids=[LOCAL_AGENT_ID],
|
||||
database_included=True,
|
||||
folders_included=[],
|
||||
name=None,
|
||||
|
@ -227,7 +256,12 @@ async def test_async_create_backup(
|
|||
) -> None:
|
||||
"""Test generate backup."""
|
||||
manager = BackupManager(hass)
|
||||
manager.loaded_backups = True
|
||||
|
||||
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
|
||||
await manager.load_platforms()
|
||||
|
||||
local_agent = manager.backup_agents[LOCAL_AGENT_ID]
|
||||
local_agent.loaded_backups = True
|
||||
|
||||
await _mock_backup_generation(
|
||||
hass, manager, mocked_json_bytes, mocked_tarfile, **params
|
||||
|
@ -236,10 +270,10 @@ async def test_async_create_backup(
|
|||
assert "Generated new backup with slug " in caplog.text
|
||||
assert "Creating backup directory" in caplog.text
|
||||
assert "Loaded 0 platforms" in caplog.text
|
||||
assert "Loaded 0 agents" in caplog.text
|
||||
assert "Loaded 1 agents" in caplog.text
|
||||
|
||||
assert len(manager.backups) == 1
|
||||
backup = list(manager.backups.values())[0]
|
||||
assert len(local_agent.backups) == 1
|
||||
backup = list(local_agent.backups.values())[0]
|
||||
assert backup.protected is bool(params.get("password"))
|
||||
|
||||
|
||||
|
@ -250,12 +284,11 @@ async def test_loading_platforms(
|
|||
"""Test loading backup platforms."""
|
||||
manager = BackupManager(hass)
|
||||
|
||||
assert not manager.loaded_platforms
|
||||
assert not manager.platforms
|
||||
|
||||
await _setup_mock_domain(
|
||||
await _setup_backup_platform(
|
||||
hass,
|
||||
Mock(
|
||||
platform=Mock(
|
||||
async_pre_backup=AsyncMock(),
|
||||
async_post_backup=AsyncMock(),
|
||||
async_get_backup_agents=AsyncMock(),
|
||||
|
@ -264,7 +297,6 @@ async def test_loading_platforms(
|
|||
await manager.load_platforms()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert manager.loaded_platforms
|
||||
assert len(manager.platforms) == 1
|
||||
|
||||
assert "Loaded 1 platforms" in caplog.text
|
||||
|
@ -277,19 +309,17 @@ async def test_loading_agents(
|
|||
"""Test loading backup agents."""
|
||||
manager = BackupManager(hass)
|
||||
|
||||
assert not manager.loaded_platforms
|
||||
assert not manager.platforms
|
||||
|
||||
await _setup_mock_domain(
|
||||
await _setup_backup_platform(
|
||||
hass,
|
||||
Mock(
|
||||
platform=Mock(
|
||||
async_get_backup_agents=AsyncMock(return_value=[BackupAgentTest("test")]),
|
||||
),
|
||||
)
|
||||
await manager.load_platforms()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert manager.loaded_platforms
|
||||
assert len(manager.backup_agents) == 1
|
||||
|
||||
assert "Loaded 1 agents" in caplog.text
|
||||
|
@ -303,14 +333,12 @@ async def test_not_loading_bad_platforms(
|
|||
"""Test loading backup platforms."""
|
||||
manager = BackupManager(hass)
|
||||
|
||||
assert not manager.loaded_platforms
|
||||
assert not manager.platforms
|
||||
|
||||
await _setup_mock_domain(hass)
|
||||
await _setup_backup_platform(hass)
|
||||
await manager.load_platforms()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert manager.loaded_platforms
|
||||
assert len(manager.platforms) == 0
|
||||
|
||||
assert "Loaded 0 platforms" in caplog.text
|
||||
|
@ -326,9 +354,10 @@ async def test_syncing_backup(
|
|||
"""Test syncing a backup."""
|
||||
manager = BackupManager(hass)
|
||||
|
||||
await _setup_mock_domain(
|
||||
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
|
||||
await _setup_backup_platform(
|
||||
hass,
|
||||
Mock(
|
||||
platform=Mock(
|
||||
async_pre_backup=AsyncMock(),
|
||||
async_post_backup=AsyncMock(),
|
||||
async_get_backup_agents=AsyncMock(
|
||||
|
@ -387,9 +416,10 @@ async def test_syncing_backup_with_exception(
|
|||
async def async_upload_backup(self, **kwargs: Any) -> None:
|
||||
raise HomeAssistantError("Test exception")
|
||||
|
||||
await _setup_mock_domain(
|
||||
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
|
||||
await _setup_backup_platform(
|
||||
hass,
|
||||
Mock(
|
||||
platform=Mock(
|
||||
async_pre_backup=AsyncMock(),
|
||||
async_post_backup=AsyncMock(),
|
||||
async_get_backup_agents=AsyncMock(
|
||||
|
@ -448,9 +478,10 @@ async def test_syncing_backup_no_agents(
|
|||
"""Test syncing a backup with no agents."""
|
||||
manager = BackupManager(hass)
|
||||
|
||||
await _setup_mock_domain(
|
||||
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
|
||||
await _setup_backup_platform(
|
||||
hass,
|
||||
Mock(
|
||||
platform=Mock(
|
||||
async_pre_backup=AsyncMock(),
|
||||
async_post_backup=AsyncMock(),
|
||||
async_get_backup_agents=AsyncMock(return_value=[]),
|
||||
|
@ -479,9 +510,9 @@ async def test_exception_plaform_pre(
|
|||
async def _mock_step(hass: HomeAssistant) -> None:
|
||||
raise HomeAssistantError("Test exception")
|
||||
|
||||
await _setup_mock_domain(
|
||||
await _setup_backup_platform(
|
||||
hass,
|
||||
Mock(
|
||||
platform=Mock(
|
||||
async_pre_backup=_mock_step,
|
||||
async_post_backup=AsyncMock(),
|
||||
async_get_backup_agents=AsyncMock(),
|
||||
|
@ -502,9 +533,9 @@ async def test_exception_plaform_post(
|
|||
async def _mock_step(hass: HomeAssistant) -> None:
|
||||
raise HomeAssistantError("Test exception")
|
||||
|
||||
await _setup_mock_domain(
|
||||
await _setup_backup_platform(
|
||||
hass,
|
||||
Mock(
|
||||
platform=Mock(
|
||||
async_pre_backup=AsyncMock(),
|
||||
async_post_backup=_mock_step,
|
||||
async_get_backup_agents=AsyncMock(),
|
||||
|
@ -515,58 +546,6 @@ async def test_exception_plaform_post(
|
|||
await _mock_backup_generation(hass, manager, mocked_json_bytes, mocked_tarfile)
|
||||
|
||||
|
||||
async def test_loading_platforms_when_running_async_pre_backup_actions(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test loading backup platforms when running post backup actions."""
|
||||
manager = BackupManager(hass)
|
||||
|
||||
assert not manager.loaded_platforms
|
||||
assert not manager.platforms
|
||||
|
||||
await _setup_mock_domain(
|
||||
hass,
|
||||
Mock(
|
||||
async_pre_backup=AsyncMock(),
|
||||
async_post_backup=AsyncMock(),
|
||||
async_get_backup_agents=AsyncMock(),
|
||||
),
|
||||
)
|
||||
await manager.async_pre_backup_actions()
|
||||
|
||||
assert manager.loaded_platforms
|
||||
assert len(manager.platforms) == 1
|
||||
|
||||
assert "Loaded 1 platforms" in caplog.text
|
||||
|
||||
|
||||
async def test_loading_platforms_when_running_async_post_backup_actions(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test loading backup platforms when running post backup actions."""
|
||||
manager = BackupManager(hass)
|
||||
|
||||
assert not manager.loaded_platforms
|
||||
assert not manager.platforms
|
||||
|
||||
await _setup_mock_domain(
|
||||
hass,
|
||||
Mock(
|
||||
async_pre_backup=AsyncMock(),
|
||||
async_post_backup=AsyncMock(),
|
||||
async_get_backup_agents=AsyncMock(),
|
||||
),
|
||||
)
|
||||
await manager.async_post_backup_actions()
|
||||
|
||||
assert manager.loaded_platforms
|
||||
assert len(manager.platforms) == 1
|
||||
|
||||
assert "Loaded 1 platforms" in caplog.text
|
||||
|
||||
|
||||
async def test_async_receive_backup(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
|
@ -601,6 +580,7 @@ async def test_async_receive_backup(
|
|||
assert mover_mock.mock_calls[0].args[1].name == "abc123.tar"
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="Restore not implemented in the draft")
|
||||
async def test_async_trigger_restore(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
|
@ -623,6 +603,7 @@ async def test_async_trigger_restore(
|
|||
assert mocked_service_call.called
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="Restore not implemented in the draft")
|
||||
async def test_async_trigger_restore_with_password(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
|
@ -645,6 +626,7 @@ async def test_async_trigger_restore_with_password(
|
|||
assert mocked_service_call.called
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="Restore not implemented in the draft")
|
||||
async def test_async_trigger_restore_missing_backup(hass: HomeAssistant) -> None:
|
||||
"""Test trigger restore."""
|
||||
manager = BackupManager(hass)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import ANY, AsyncMock, patch
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
@ -45,31 +45,19 @@ async def test_info(
|
|||
with_hassio: bool,
|
||||
) -> None:
|
||||
"""Test getting backup info."""
|
||||
await setup_backup_integration(hass, with_hassio=with_hassio)
|
||||
|
||||
hass.data[DATA_MANAGER].backups = {TEST_BACKUP.slug: TEST_BACKUP}
|
||||
await setup_backup_integration(hass, with_hassio=with_hassio, backups=[TEST_BACKUP])
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.load_backups",
|
||||
AsyncMock(),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_get_backups",
|
||||
return_value={TEST_BACKUP.slug: TEST_BACKUP},
|
||||
),
|
||||
):
|
||||
await client.send_json_auto_id({"type": "backup/info"})
|
||||
assert await client.receive_json() == snapshot
|
||||
await client.send_json_auto_id({"type": "backup/info"})
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"backup_content",
|
||||
[
|
||||
pytest.param(TEST_BACKUP, id="with_backup_content"),
|
||||
pytest.param([TEST_BACKUP], id="with_backup_content"),
|
||||
pytest.param(None, id="without_backup_content"),
|
||||
],
|
||||
)
|
||||
|
@ -88,17 +76,15 @@ async def test_details(
|
|||
backup_content: BaseBackup | None,
|
||||
) -> None:
|
||||
"""Test getting backup info."""
|
||||
await setup_backup_integration(hass, with_hassio=with_hassio)
|
||||
await setup_backup_integration(
|
||||
hass, with_hassio=with_hassio, backups=backup_content
|
||||
)
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_get_backup",
|
||||
return_value=backup_content,
|
||||
):
|
||||
await client.send_json_auto_id({"type": "backup/details", "slug": "abc123"})
|
||||
assert await client.receive_json() == snapshot
|
||||
await client.send_json_auto_id({"type": "backup/details", "slug": "abc123"})
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -159,7 +145,9 @@ async def test_generate(
|
|||
freezer.move_to("2024-11-13 12:01:00+01:00")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json_auto_id({"type": "backup/generate", **(data or {})})
|
||||
await client.send_json_auto_id(
|
||||
{"type": "backup/generate", **{"agent_ids": ["backup.local"]} | (data or {})}
|
||||
)
|
||||
for _ in range(number_of_messages):
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
|
@ -168,16 +156,18 @@ async def test_generate(
|
|||
@pytest.mark.parametrize(
|
||||
("params", "expected_extra_call_params"),
|
||||
[
|
||||
({}, {}),
|
||||
({"agent_ids": ["backup.local"]}, {"agent_ids": ["backup.local"]}),
|
||||
(
|
||||
{
|
||||
"addons_included": ["ssl"],
|
||||
"agent_ids": ["backup.local"],
|
||||
"database_included": False,
|
||||
"folders_included": ["media"],
|
||||
"name": "abc123",
|
||||
},
|
||||
{
|
||||
"addons_included": ["ssl"],
|
||||
"agent_ids": ["backup.local"],
|
||||
"database_included": False,
|
||||
"folders_included": ["media"],
|
||||
"name": "abc123",
|
||||
|
@ -525,7 +515,7 @@ async def test_agents_download(
|
|||
assert await client.receive_json() == snapshot
|
||||
assert download_mock.call_args[1] == {
|
||||
"id": "abc123",
|
||||
"path": Path(hass.config.path("backup"), "abc123.tar"),
|
||||
"path": Path(hass.config.path("backups"), "abc123.tar"),
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue