mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-15 05:06:30 +00:00
De-duplicate Backup/Restore logic (#3311)
* De-duplicate Backup/Restore logic Create internal _do_backup()/_do_restore() method which de-duplicates some of the backup/restore logic previously part of full/partial backup/restore. * Add Backup/Restore test coverage
This commit is contained in:
parent
cde45e2e7a
commit
eadc629cd9
@ -48,7 +48,7 @@ from ..exceptions import AddonsError
|
|||||||
from ..utils.json import write_json_file
|
from ..utils.json import write_json_file
|
||||||
from ..utils.tar import SecureTarFile, atomic_contents_add, secure_path
|
from ..utils.tar import SecureTarFile, atomic_contents_add, secure_path
|
||||||
from .utils import key_to_iv, password_for_validating, password_to_key, remove_folder
|
from .utils import key_to_iv, password_for_validating, password_to_key, remove_folder
|
||||||
from .validate import ALL_FOLDERS, SCHEMA_BACKUP
|
from .validate import SCHEMA_BACKUP
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -311,9 +311,8 @@ class Backup(CoreSysAttributes):
|
|||||||
finally:
|
finally:
|
||||||
self._tmp.cleanup()
|
self._tmp.cleanup()
|
||||||
|
|
||||||
async def store_addons(self, addon_list: Optional[list[Addon]] = None):
|
async def store_addons(self, addon_list: list[str]):
|
||||||
"""Add a list of add-ons into backup."""
|
"""Add a list of add-ons into backup."""
|
||||||
addon_list: list[Addon] = addon_list or self.sys_addons.installed
|
|
||||||
|
|
||||||
async def _addon_save(addon: Addon):
|
async def _addon_save(addon: Addon):
|
||||||
"""Task to store an add-on into backup."""
|
"""Task to store an add-on into backup."""
|
||||||
@ -346,9 +345,8 @@ class Backup(CoreSysAttributes):
|
|||||||
except Exception as err: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
_LOGGER.warning("Can't save Add-on %s: %s", addon.slug, err)
|
_LOGGER.warning("Can't save Add-on %s: %s", addon.slug, err)
|
||||||
|
|
||||||
async def restore_addons(self, addon_list: Optional[list[str]] = None):
|
async def restore_addons(self, addon_list: list[str]):
|
||||||
"""Restore a list add-on from backup."""
|
"""Restore a list add-on from backup."""
|
||||||
addon_list: list[str] = addon_list or self.addon_list
|
|
||||||
|
|
||||||
async def _addon_restore(addon_slug: str):
|
async def _addon_restore(addon_slug: str):
|
||||||
"""Task to restore an add-on into backup."""
|
"""Task to restore an add-on into backup."""
|
||||||
@ -375,9 +373,8 @@ class Backup(CoreSysAttributes):
|
|||||||
except Exception as err: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
_LOGGER.warning("Can't restore Add-on %s: %s", slug, err)
|
_LOGGER.warning("Can't restore Add-on %s: %s", slug, err)
|
||||||
|
|
||||||
async def store_folders(self, folder_list: Optional[list[str]] = None):
|
async def store_folders(self, folder_list: list[str]):
|
||||||
"""Backup Supervisor data into backup."""
|
"""Backup Supervisor data into backup."""
|
||||||
folder_list: set[str] = set(folder_list or ALL_FOLDERS)
|
|
||||||
|
|
||||||
def _folder_save(name: str):
|
def _folder_save(name: str):
|
||||||
"""Take backup of a folder."""
|
"""Take backup of a folder."""
|
||||||
@ -414,9 +411,8 @@ class Backup(CoreSysAttributes):
|
|||||||
except Exception as err: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
_LOGGER.warning("Can't save folder %s: %s", folder, err)
|
_LOGGER.warning("Can't save folder %s: %s", folder, err)
|
||||||
|
|
||||||
async def restore_folders(self, folder_list: Optional[list[str]] = None):
|
async def restore_folders(self, folder_list: list[str]):
|
||||||
"""Backup Supervisor data into backup."""
|
"""Backup Supervisor data into backup."""
|
||||||
folder_list: set[str] = set(folder_list or self.folders)
|
|
||||||
|
|
||||||
def _folder_restore(name: str):
|
def _folder_restore(name: str):
|
||||||
"""Intenal function to restore a folder."""
|
"""Intenal function to restore a folder."""
|
||||||
|
@ -7,6 +7,9 @@ from typing import Awaitable
|
|||||||
from awesomeversion.awesomeversion import AwesomeVersion
|
from awesomeversion.awesomeversion import AwesomeVersion
|
||||||
from awesomeversion.exceptions import AwesomeVersionCompareException
|
from awesomeversion.exceptions import AwesomeVersionCompareException
|
||||||
|
|
||||||
|
from supervisor.addons.addon import Addon
|
||||||
|
from supervisor.backups.validate import ALL_FOLDERS
|
||||||
|
|
||||||
from ..const import FOLDER_HOMEASSISTANT, CoreState
|
from ..const import FOLDER_HOMEASSISTANT, CoreState
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import AddonsError
|
from ..exceptions import AddonsError
|
||||||
@ -126,6 +129,38 @@ class BackupManager(CoreSysAttributes):
|
|||||||
self._backups[backup.slug] = backup
|
self._backups[backup.slug] = backup
|
||||||
return backup
|
return backup
|
||||||
|
|
||||||
|
async def _do_backup(
|
||||||
|
self,
|
||||||
|
backup: Backup,
|
||||||
|
addon_list: list[Addon],
|
||||||
|
folder_list: list[str],
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
self.sys_core.state = CoreState.FREEZE
|
||||||
|
|
||||||
|
async with backup:
|
||||||
|
# Backup add-ons
|
||||||
|
if addon_list:
|
||||||
|
_LOGGER.info("Backing up %s store Add-ons", backup.slug)
|
||||||
|
await backup.store_addons(addon_list)
|
||||||
|
|
||||||
|
# Backup folders
|
||||||
|
if folder_list:
|
||||||
|
_LOGGER.info("Backing up %s store folders", backup.slug)
|
||||||
|
await backup.store_folders(folder_list)
|
||||||
|
|
||||||
|
except Exception as err: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Backup %s error", backup.slug)
|
||||||
|
self.sys_capture_exception(err)
|
||||||
|
return None
|
||||||
|
|
||||||
|
else:
|
||||||
|
self._backups[backup.slug] = backup
|
||||||
|
return backup
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.sys_core.state = CoreState.RUNNING
|
||||||
|
|
||||||
@Job(conditions=[JobCondition.FREE_SPACE, JobCondition.RUNNING])
|
@Job(conditions=[JobCondition.FREE_SPACE, JobCondition.RUNNING])
|
||||||
async def do_backup_full(self, name="", password=None):
|
async def do_backup_full(self, name="", password=None):
|
||||||
"""Create a full backup."""
|
"""Create a full backup."""
|
||||||
@ -134,34 +169,16 @@ class BackupManager(CoreSysAttributes):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
backup = self._create_backup(name, BackupType.FULL, password)
|
backup = self._create_backup(name, BackupType.FULL, password)
|
||||||
|
|
||||||
_LOGGER.info("Creating new full backup with slug %s", backup.slug)
|
_LOGGER.info("Creating new full backup with slug %s", backup.slug)
|
||||||
try:
|
async with self.lock:
|
||||||
self.sys_core.state = CoreState.FREEZE
|
backup = await self._do_backup(
|
||||||
await self.lock.acquire()
|
backup, self.sys_addons.installed, ALL_FOLDERS
|
||||||
|
)
|
||||||
async with backup:
|
if backup:
|
||||||
# Backup add-ons
|
_LOGGER.info("Creating full backup with slug %s completed", backup.slug)
|
||||||
_LOGGER.info("Backing up %s store Add-ons", backup.slug)
|
|
||||||
await backup.store_addons()
|
|
||||||
|
|
||||||
# Backup folders
|
|
||||||
_LOGGER.info("Backing up %s store folders", backup.slug)
|
|
||||||
await backup.store_folders()
|
|
||||||
|
|
||||||
except Exception as err: # pylint: disable=broad-except
|
|
||||||
_LOGGER.exception("Backup %s error", backup.slug)
|
|
||||||
self.sys_capture_exception(err)
|
|
||||||
return None
|
|
||||||
|
|
||||||
else:
|
|
||||||
_LOGGER.info("Creating full backup with slug %s completed", backup.slug)
|
|
||||||
self._backups[backup.slug] = backup
|
|
||||||
return backup
|
return backup
|
||||||
|
|
||||||
finally:
|
|
||||||
self.sys_core.state = CoreState.RUNNING
|
|
||||||
self.lock.release()
|
|
||||||
|
|
||||||
@Job(conditions=[JobCondition.FREE_SPACE, JobCondition.RUNNING])
|
@Job(conditions=[JobCondition.FREE_SPACE, JobCondition.RUNNING])
|
||||||
async def do_backup_partial(
|
async def do_backup_partial(
|
||||||
self, name="", addons=None, folders=None, password=None, homeassistant=True
|
self, name="", addons=None, folders=None, password=None, homeassistant=True
|
||||||
@ -176,47 +193,99 @@ class BackupManager(CoreSysAttributes):
|
|||||||
|
|
||||||
if len(addons) == 0 and len(folders) == 0 and not homeassistant:
|
if len(addons) == 0 and len(folders) == 0 and not homeassistant:
|
||||||
_LOGGER.error("Nothing to create backup for")
|
_LOGGER.error("Nothing to create backup for")
|
||||||
return
|
|
||||||
|
|
||||||
backup = self._create_backup(name, BackupType.PARTIAL, password, homeassistant)
|
backup = self._create_backup(name, BackupType.PARTIAL, password, homeassistant)
|
||||||
|
|
||||||
_LOGGER.info("Creating new partial backup with slug %s", backup.slug)
|
_LOGGER.info("Creating new partial backup with slug %s", backup.slug)
|
||||||
try:
|
async with self.lock:
|
||||||
self.sys_core.state = CoreState.FREEZE
|
addon_list = []
|
||||||
await self.lock.acquire()
|
for addon_slug in addons:
|
||||||
|
addon = self.sys_addons.get(addon_slug)
|
||||||
|
if addon and addon.is_installed:
|
||||||
|
addon_list.append(addon)
|
||||||
|
continue
|
||||||
|
_LOGGER.warning("Add-on %s not found/installed", addon_slug)
|
||||||
|
|
||||||
async with backup:
|
backup = await self._do_backup(backup, addon_list, folders)
|
||||||
# Backup add-ons
|
if backup:
|
||||||
addon_list = []
|
_LOGGER.info(
|
||||||
for addon_slug in addons:
|
"Creating partial backup with slug %s completed", backup.slug
|
||||||
addon = self.sys_addons.get(addon_slug)
|
)
|
||||||
if addon and addon.is_installed:
|
|
||||||
addon_list.append(addon)
|
|
||||||
continue
|
|
||||||
_LOGGER.warning("Add-on %s not found/installed", addon_slug)
|
|
||||||
|
|
||||||
if addon_list:
|
|
||||||
_LOGGER.info("Backing up %s store Add-ons", backup.slug)
|
|
||||||
await backup.store_addons(addon_list)
|
|
||||||
|
|
||||||
# Backup folders
|
|
||||||
if folders:
|
|
||||||
_LOGGER.info("Backing up %s store folders", backup.slug)
|
|
||||||
await backup.store_folders(folders)
|
|
||||||
|
|
||||||
except Exception as err: # pylint: disable=broad-except
|
|
||||||
_LOGGER.exception("Backup %s error", backup.slug)
|
|
||||||
self.sys_capture_exception(err)
|
|
||||||
return None
|
|
||||||
|
|
||||||
else:
|
|
||||||
_LOGGER.info("Creating partial backup with slug %s completed", backup.slug)
|
|
||||||
self._backups[backup.slug] = backup
|
|
||||||
return backup
|
return backup
|
||||||
|
|
||||||
|
async def _do_restore(
|
||||||
|
self,
|
||||||
|
backup: Backup,
|
||||||
|
addon_list: list[Addon],
|
||||||
|
folder_list: list[str],
|
||||||
|
homeassistant: bool,
|
||||||
|
remove_other_addons: bool,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
# Stop Home Assistant Core if we restore the version or config directory
|
||||||
|
if FOLDER_HOMEASSISTANT in folder_list or homeassistant:
|
||||||
|
await self.sys_homeassistant.core.stop()
|
||||||
|
|
||||||
|
async with backup:
|
||||||
|
# Restore docker config
|
||||||
|
_LOGGER.info("Restoring %s Docker config", backup.slug)
|
||||||
|
backup.restore_dockerconfig()
|
||||||
|
|
||||||
|
if FOLDER_HOMEASSISTANT in folder_list:
|
||||||
|
backup.restore_homeassistant()
|
||||||
|
|
||||||
|
# Process folders
|
||||||
|
if folder_list:
|
||||||
|
_LOGGER.info("Restoring %s folders", backup.slug)
|
||||||
|
await backup.restore_folders(folder_list)
|
||||||
|
|
||||||
|
# Process Home-Assistant
|
||||||
|
task_hass = None
|
||||||
|
if homeassistant:
|
||||||
|
_LOGGER.info("Restoring %s Home Assistant Core", backup.slug)
|
||||||
|
task_hass = self._update_core_task(backup.homeassistant_version)
|
||||||
|
|
||||||
|
if addon_list:
|
||||||
|
_LOGGER.info("Restoring %s Repositories", backup.slug)
|
||||||
|
await backup.restore_repositories()
|
||||||
|
|
||||||
|
_LOGGER.info("Restoring %s Add-ons", backup.slug)
|
||||||
|
await backup.restore_addons(addon_list)
|
||||||
|
|
||||||
|
# Delete delta add-ons
|
||||||
|
if remove_other_addons:
|
||||||
|
_LOGGER.info("Removing Add-ons not in the backup %s", backup.slug)
|
||||||
|
for addon in self.sys_addons.installed:
|
||||||
|
if addon.slug in backup.addon_list:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Remove Add-on because it's not a part of the new env
|
||||||
|
# Do it sequential avoid issue on slow IO
|
||||||
|
try:
|
||||||
|
await addon.uninstall()
|
||||||
|
except AddonsError:
|
||||||
|
_LOGGER.warning("Can't uninstall Add-on %s", addon.slug)
|
||||||
|
|
||||||
|
# Wait for Home Assistant Core update/downgrade
|
||||||
|
if task_hass:
|
||||||
|
_LOGGER.info("Restore %s wait for Home-Assistant", backup.slug)
|
||||||
|
await task_hass
|
||||||
|
|
||||||
|
except Exception as err: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Restore %s error", backup.slug)
|
||||||
|
self.sys_capture_exception(err)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
finally:
|
finally:
|
||||||
self.sys_core.state = CoreState.RUNNING
|
# Do we need start Home Assistant Core?
|
||||||
self.lock.release()
|
if not await self.sys_homeassistant.core.is_running():
|
||||||
|
await self.sys_homeassistant.core.start()
|
||||||
|
|
||||||
|
# Check If we can access to API / otherwise restart
|
||||||
|
if not await self.sys_homeassistant.api.check_api_state():
|
||||||
|
_LOGGER.warning("Need restart HomeAssistant for API")
|
||||||
|
await self.sys_homeassistant.core.restart()
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
conditions=[
|
conditions=[
|
||||||
@ -227,7 +296,7 @@ class BackupManager(CoreSysAttributes):
|
|||||||
JobCondition.RUNNING,
|
JobCondition.RUNNING,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
async def do_restore_full(self, backup, password=None):
|
async def do_restore_full(self, backup: Backup, password=None):
|
||||||
"""Restore a backup."""
|
"""Restore a backup."""
|
||||||
if self.lock.locked():
|
if self.lock.locked():
|
||||||
_LOGGER.error("A backup/restore process is already running")
|
_LOGGER.error("A backup/restore process is already running")
|
||||||
@ -242,65 +311,20 @@ class BackupManager(CoreSysAttributes):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
_LOGGER.info("Full-Restore %s start", backup.slug)
|
_LOGGER.info("Full-Restore %s start", backup.slug)
|
||||||
try:
|
async with self.lock:
|
||||||
self.sys_core.state = CoreState.FREEZE
|
self.sys_core.state = CoreState.FREEZE
|
||||||
await self.lock.acquire()
|
|
||||||
|
|
||||||
async with backup:
|
# Stop Home-Assistant / Add-ons
|
||||||
# Stop Home-Assistant / Add-ons
|
await self.sys_core.shutdown()
|
||||||
await self.sys_core.shutdown()
|
|
||||||
|
|
||||||
# Restore folders
|
success = await self._do_restore(
|
||||||
_LOGGER.info("Restoring %s folders", backup.slug)
|
backup, backup.addon_list, backup.folders, True, True
|
||||||
await backup.restore_folders()
|
)
|
||||||
|
|
||||||
# Restore docker config
|
|
||||||
_LOGGER.info("Restoring %s Docker Config", backup.slug)
|
|
||||||
backup.restore_dockerconfig()
|
|
||||||
|
|
||||||
# Start homeassistant restore
|
|
||||||
_LOGGER.info("Restoring %s Home-Assistant", backup.slug)
|
|
||||||
backup.restore_homeassistant()
|
|
||||||
task_hass = self._update_core_task(backup.homeassistant_version)
|
|
||||||
|
|
||||||
# Restore repositories
|
|
||||||
_LOGGER.info("Restoring %s Repositories", backup.slug)
|
|
||||||
await backup.restore_repositories()
|
|
||||||
|
|
||||||
# Delete delta add-ons
|
|
||||||
_LOGGER.info("Removing add-ons not in the backup %s", backup.slug)
|
|
||||||
for addon in self.sys_addons.installed:
|
|
||||||
if addon.slug in backup.addon_list:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Remove Add-on because it's not a part of the new env
|
|
||||||
# Do it sequential avoid issue on slow IO
|
|
||||||
try:
|
|
||||||
await addon.uninstall()
|
|
||||||
except AddonsError:
|
|
||||||
_LOGGER.warning("Can't uninstall Add-on %s", addon.slug)
|
|
||||||
|
|
||||||
# Restore add-ons
|
|
||||||
_LOGGER.info("Restore %s old add-ons", backup.slug)
|
|
||||||
await backup.restore_addons()
|
|
||||||
|
|
||||||
# finish homeassistant task
|
|
||||||
_LOGGER.info("Restore %s wait until homeassistant ready", backup.slug)
|
|
||||||
await task_hass
|
|
||||||
await self.sys_homeassistant.core.start()
|
|
||||||
|
|
||||||
except Exception as err: # pylint: disable=broad-except
|
|
||||||
_LOGGER.exception("Restore %s error", backup.slug)
|
|
||||||
self.sys_capture_exception(err)
|
|
||||||
return False
|
|
||||||
|
|
||||||
else:
|
|
||||||
_LOGGER.info("Full-Restore %s done", backup.slug)
|
|
||||||
return True
|
|
||||||
|
|
||||||
finally:
|
|
||||||
self.sys_core.state = CoreState.RUNNING
|
self.sys_core.state = CoreState.RUNNING
|
||||||
self.lock.release()
|
|
||||||
|
if success:
|
||||||
|
_LOGGER.info("Full-Restore %s done", backup.slug)
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
conditions=[
|
conditions=[
|
||||||
@ -323,68 +347,21 @@ class BackupManager(CoreSysAttributes):
|
|||||||
_LOGGER.error("Invalid password for backup %s", backup.slug)
|
_LOGGER.error("Invalid password for backup %s", backup.slug)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
addons = addons or []
|
addon_list = addons or []
|
||||||
folders = folders or []
|
folder_list = folders or []
|
||||||
|
|
||||||
_LOGGER.info("Partial-Restore %s start", backup.slug)
|
_LOGGER.info("Partial-Restore %s start", backup.slug)
|
||||||
try:
|
async with self.lock:
|
||||||
self.sys_core.state = CoreState.FREEZE
|
self.sys_core.state = CoreState.FREEZE
|
||||||
await self.lock.acquire()
|
|
||||||
|
|
||||||
async with backup:
|
success = await self._do_restore(
|
||||||
# Restore docker config
|
backup, addon_list, folder_list, homeassistant, False
|
||||||
_LOGGER.info("Restoring %s Docker Config", backup.slug)
|
)
|
||||||
backup.restore_dockerconfig()
|
|
||||||
|
|
||||||
# Stop Home-Assistant for config restore
|
|
||||||
if FOLDER_HOMEASSISTANT in folders:
|
|
||||||
await self.sys_homeassistant.core.stop()
|
|
||||||
backup.restore_homeassistant()
|
|
||||||
|
|
||||||
# Process folders
|
|
||||||
if folders:
|
|
||||||
_LOGGER.info("Restoring %s folders", backup.slug)
|
|
||||||
await backup.restore_folders(folders)
|
|
||||||
|
|
||||||
# Process Home-Assistant
|
|
||||||
task_hass = None
|
|
||||||
if homeassistant:
|
|
||||||
_LOGGER.info("Restoring %s Home-Assistant", backup.slug)
|
|
||||||
task_hass = self._update_core_task(backup.homeassistant_version)
|
|
||||||
|
|
||||||
if addons:
|
|
||||||
_LOGGER.info("Restoring %s Repositories", backup.slug)
|
|
||||||
await backup.restore_repositories()
|
|
||||||
|
|
||||||
_LOGGER.info("Restoring %s old add-ons", backup.slug)
|
|
||||||
await backup.restore_addons(addons)
|
|
||||||
|
|
||||||
# Make sure homeassistant run agen
|
|
||||||
if task_hass:
|
|
||||||
_LOGGER.info("Restore %s wait for Home-Assistant", backup.slug)
|
|
||||||
await task_hass
|
|
||||||
|
|
||||||
# Do we need start HomeAssistant?
|
|
||||||
if not await self.sys_homeassistant.core.is_running():
|
|
||||||
await self.sys_homeassistant.core.start()
|
|
||||||
|
|
||||||
# Check If we can access to API / otherwise restart
|
|
||||||
if not await self.sys_homeassistant.api.check_api_state():
|
|
||||||
_LOGGER.warning("Need restart HomeAssistant for API")
|
|
||||||
await self.sys_homeassistant.core.restart()
|
|
||||||
|
|
||||||
except Exception as err: # pylint: disable=broad-except
|
|
||||||
_LOGGER.exception("Restore %s error", backup.slug)
|
|
||||||
self.sys_capture_exception(err)
|
|
||||||
return False
|
|
||||||
|
|
||||||
else:
|
|
||||||
_LOGGER.info("Partial-Restore %s done", backup.slug)
|
|
||||||
return True
|
|
||||||
|
|
||||||
finally:
|
|
||||||
self.sys_core.state = CoreState.RUNNING
|
self.sys_core.state = CoreState.RUNNING
|
||||||
self.lock.release()
|
|
||||||
|
if success:
|
||||||
|
_LOGGER.info("Partial-Restore %s done", backup.slug)
|
||||||
|
|
||||||
def _update_core_task(self, version: AwesomeVersion) -> Awaitable[None]:
|
def _update_core_task(self, version: AwesomeVersion) -> Awaitable[None]:
|
||||||
"""Process core update if needed and make awaitable object."""
|
"""Process core update if needed and make awaitable object."""
|
||||||
|
1
tests/backups/__init__.py
Normal file
1
tests/backups/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Backup tests."""
|
46
tests/backups/conftest.py
Normal file
46
tests/backups/conftest.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""Mock test."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from supervisor.backups.const import BackupType
|
||||||
|
from supervisor.backups.validate import ALL_FOLDERS
|
||||||
|
|
||||||
|
from tests.const import TEST_ADDON_SLUG
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="backup_mock")
|
||||||
|
def fixture_backup_mock():
|
||||||
|
"""Backup class mock."""
|
||||||
|
with patch("supervisor.backups.manager.Backup") as backup_mock:
|
||||||
|
backup_instance = MagicMock()
|
||||||
|
backup_mock.return_value = backup_instance
|
||||||
|
|
||||||
|
backup_instance.store_addons = AsyncMock(return_value=None)
|
||||||
|
backup_instance.store_folders = AsyncMock(return_value=None)
|
||||||
|
backup_instance.restore_addons = AsyncMock(return_value=None)
|
||||||
|
backup_instance.restore_folders = AsyncMock(return_value=None)
|
||||||
|
backup_instance.restore_repositories = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
yield backup_mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def partial_backup_mock(backup_mock):
|
||||||
|
"""Partial backup mock."""
|
||||||
|
backup_instance = backup_mock.return_value
|
||||||
|
backup_instance.sys_type = BackupType.PARTIAL
|
||||||
|
backup_instance.folders = []
|
||||||
|
backup_instance.addon_list = [TEST_ADDON_SLUG]
|
||||||
|
yield backup_mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def full_backup_mock(backup_mock):
|
||||||
|
"""Full backup mock."""
|
||||||
|
backup_instance = backup_mock.return_value
|
||||||
|
backup_instance.sys_type = BackupType.FULL
|
||||||
|
backup_instance.folders = ALL_FOLDERS
|
||||||
|
backup_instance.addon_list = [TEST_ADDON_SLUG]
|
||||||
|
yield backup_mock
|
205
tests/backups/test_manager.py
Normal file
205
tests/backups/test_manager.py
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
"""Test BackupManager class."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
|
from supervisor.backups.const import BackupType
|
||||||
|
from supervisor.backups.manager import BackupManager
|
||||||
|
from supervisor.const import FOLDER_HOMEASSISTANT, CoreState
|
||||||
|
from supervisor.coresys import CoreSys
|
||||||
|
|
||||||
|
from tests.const import TEST_ADDON_SLUG
|
||||||
|
|
||||||
|
|
||||||
|
async def test_do_backup_full(coresys: CoreSys, backup_mock, install_addon_ssh):
|
||||||
|
"""Test creating Backup."""
|
||||||
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
|
||||||
|
manager = BackupManager(coresys)
|
||||||
|
|
||||||
|
# backup_mock fixture causes Backup() to be a MagicMock
|
||||||
|
backup_instance: MagicMock = await manager.do_backup_full()
|
||||||
|
|
||||||
|
# Check Backup has been created without password
|
||||||
|
assert backup_instance.new.call_args[0][3] == BackupType.FULL
|
||||||
|
assert backup_instance.new.call_args[0][4] is None
|
||||||
|
|
||||||
|
backup_instance.store_homeassistant.assert_called_once()
|
||||||
|
backup_instance.store_repositories.assert_called_once()
|
||||||
|
backup_instance.store_dockerconfig.assert_called_once()
|
||||||
|
|
||||||
|
backup_instance.store_addons.assert_called_once()
|
||||||
|
assert install_addon_ssh in backup_instance.store_addons.call_args[0][0]
|
||||||
|
|
||||||
|
backup_instance.store_folders.assert_called_once()
|
||||||
|
assert len(backup_instance.store_folders.call_args[0][0]) == 5
|
||||||
|
|
||||||
|
assert coresys.core.state == CoreState.RUNNING
|
||||||
|
|
||||||
|
|
||||||
|
async def test_do_backup_partial_minimal(
|
||||||
|
coresys: CoreSys, backup_mock, install_addon_ssh
|
||||||
|
):
|
||||||
|
"""Test creating minimal partial Backup."""
|
||||||
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
|
||||||
|
manager = BackupManager(coresys)
|
||||||
|
|
||||||
|
# backup_mock fixture causes Backup() to be a MagicMock
|
||||||
|
backup_instance: MagicMock = await manager.do_backup_partial(homeassistant=False)
|
||||||
|
|
||||||
|
# Check Backup has been created without password
|
||||||
|
assert backup_instance.new.call_args[0][3] == BackupType.PARTIAL
|
||||||
|
assert backup_instance.new.call_args[0][4] is None
|
||||||
|
|
||||||
|
backup_instance.store_homeassistant.assert_not_called()
|
||||||
|
backup_instance.store_repositories.assert_called_once()
|
||||||
|
backup_instance.store_dockerconfig.assert_called_once()
|
||||||
|
|
||||||
|
backup_instance.store_addons.assert_not_called()
|
||||||
|
|
||||||
|
backup_instance.store_folders.assert_not_called()
|
||||||
|
|
||||||
|
assert coresys.core.state == CoreState.RUNNING
|
||||||
|
|
||||||
|
|
||||||
|
async def test_do_backup_partial_maximal(
|
||||||
|
coresys: CoreSys, backup_mock, install_addon_ssh
|
||||||
|
):
|
||||||
|
"""Test creating maximal partial Backup."""
|
||||||
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
|
||||||
|
manager = BackupManager(coresys)
|
||||||
|
|
||||||
|
# backup_mock fixture causes Backup() to be a MagicMock
|
||||||
|
backup_instance: MagicMock = await manager.do_backup_partial(
|
||||||
|
addons=[TEST_ADDON_SLUG], folders=["/test"], homeassistant=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check Backup has been created without password
|
||||||
|
assert backup_instance.new.call_args[0][3] == BackupType.PARTIAL
|
||||||
|
assert backup_instance.new.call_args[0][4] is None
|
||||||
|
|
||||||
|
backup_instance.store_homeassistant.assert_called_once()
|
||||||
|
backup_instance.store_repositories.assert_called_once()
|
||||||
|
backup_instance.store_dockerconfig.assert_called_once()
|
||||||
|
|
||||||
|
backup_instance.store_addons.assert_called_once()
|
||||||
|
assert install_addon_ssh in backup_instance.store_addons.call_args[0][0]
|
||||||
|
|
||||||
|
backup_instance.store_folders.assert_called_once()
|
||||||
|
assert len(backup_instance.store_folders.call_args[0][0]) == 1
|
||||||
|
|
||||||
|
assert coresys.core.state == CoreState.RUNNING
|
||||||
|
|
||||||
|
|
||||||
|
async def test_do_restore_full(coresys: CoreSys, full_backup_mock, install_addon_ssh):
|
||||||
|
"""Test restoring full Backup."""
|
||||||
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
coresys.homeassistant.core.start = AsyncMock(return_value=None)
|
||||||
|
coresys.homeassistant.core.stop = AsyncMock(return_value=None)
|
||||||
|
coresys.homeassistant.core.update = AsyncMock(return_value=None)
|
||||||
|
install_addon_ssh.uninstall = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
manager = BackupManager(coresys)
|
||||||
|
|
||||||
|
backup_instance = full_backup_mock.return_value
|
||||||
|
await manager.do_restore_full(backup_instance)
|
||||||
|
|
||||||
|
backup_instance.restore_homeassistant.assert_called_once()
|
||||||
|
backup_instance.restore_repositories.assert_called_once()
|
||||||
|
backup_instance.restore_dockerconfig.assert_called_once()
|
||||||
|
|
||||||
|
backup_instance.restore_addons.assert_called_once()
|
||||||
|
install_addon_ssh.uninstall.assert_not_called()
|
||||||
|
|
||||||
|
backup_instance.restore_folders.assert_called_once()
|
||||||
|
|
||||||
|
assert coresys.core.state == CoreState.RUNNING
|
||||||
|
|
||||||
|
|
||||||
|
async def test_do_restore_full_different_addon(
|
||||||
|
coresys: CoreSys, full_backup_mock, install_addon_ssh
|
||||||
|
):
|
||||||
|
"""Test restoring full Backup with different addons than installed."""
|
||||||
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
coresys.homeassistant.core.start = AsyncMock(return_value=None)
|
||||||
|
coresys.homeassistant.core.stop = AsyncMock(return_value=None)
|
||||||
|
coresys.homeassistant.core.update = AsyncMock(return_value=None)
|
||||||
|
install_addon_ssh.uninstall = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
manager = BackupManager(coresys)
|
||||||
|
|
||||||
|
backup_instance = full_backup_mock.return_value
|
||||||
|
backup_instance.addon_list = ["differentslug"]
|
||||||
|
await manager.do_restore_full(backup_instance)
|
||||||
|
|
||||||
|
backup_instance.restore_homeassistant.assert_called_once()
|
||||||
|
backup_instance.restore_repositories.assert_called_once()
|
||||||
|
backup_instance.restore_dockerconfig.assert_called_once()
|
||||||
|
|
||||||
|
backup_instance.restore_addons.assert_called_once()
|
||||||
|
install_addon_ssh.uninstall.assert_called_once()
|
||||||
|
|
||||||
|
backup_instance.restore_folders.assert_called_once()
|
||||||
|
|
||||||
|
assert coresys.core.state == CoreState.RUNNING
|
||||||
|
|
||||||
|
|
||||||
|
async def test_do_restore_partial_minimal(
|
||||||
|
coresys: CoreSys, partial_backup_mock, install_addon_ssh
|
||||||
|
):
|
||||||
|
"""Test restoring partial Backup minimal."""
|
||||||
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
coresys.homeassistant.core.start = AsyncMock(return_value=None)
|
||||||
|
coresys.homeassistant.core.stop = AsyncMock(return_value=None)
|
||||||
|
coresys.homeassistant.core.update = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
manager = BackupManager(coresys)
|
||||||
|
|
||||||
|
backup_instance = partial_backup_mock.return_value
|
||||||
|
await manager.do_restore_partial(backup_instance, homeassistant=False)
|
||||||
|
|
||||||
|
backup_instance.restore_homeassistant.assert_not_called()
|
||||||
|
backup_instance.restore_repositories.assert_not_called()
|
||||||
|
backup_instance.restore_dockerconfig.assert_called_once()
|
||||||
|
|
||||||
|
backup_instance.restore_addons.assert_not_called()
|
||||||
|
|
||||||
|
backup_instance.restore_folders.assert_not_called()
|
||||||
|
|
||||||
|
assert coresys.core.state == CoreState.RUNNING
|
||||||
|
|
||||||
|
|
||||||
|
async def test_do_restore_partial_maximal(coresys: CoreSys, partial_backup_mock):
|
||||||
|
"""Test restoring partial Backup minimal."""
|
||||||
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
coresys.homeassistant.core.start = AsyncMock(return_value=None)
|
||||||
|
coresys.homeassistant.core.stop = AsyncMock(return_value=None)
|
||||||
|
coresys.homeassistant.core.update = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
manager = BackupManager(coresys)
|
||||||
|
|
||||||
|
backup_instance = partial_backup_mock.return_value
|
||||||
|
await manager.do_restore_partial(
|
||||||
|
backup_instance,
|
||||||
|
addons=[TEST_ADDON_SLUG],
|
||||||
|
folders=[FOLDER_HOMEASSISTANT],
|
||||||
|
homeassistant=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
backup_instance.restore_homeassistant.assert_called_once()
|
||||||
|
backup_instance.restore_repositories.assert_called_once()
|
||||||
|
backup_instance.restore_dockerconfig.assert_called_once()
|
||||||
|
|
||||||
|
backup_instance.restore_addons.assert_called_once()
|
||||||
|
|
||||||
|
backup_instance.restore_folders.assert_called_once()
|
||||||
|
|
||||||
|
assert coresys.core.state == CoreState.RUNNING
|
@ -288,3 +288,4 @@ def install_addon_ssh(coresys: CoreSys, repository):
|
|||||||
coresys.addons.data.install(store)
|
coresys.addons.data.install(store)
|
||||||
addon = Addon(coresys, store.slug)
|
addon = Addon(coresys, store.slug)
|
||||||
coresys.addons.local[addon.slug] = addon
|
coresys.addons.local[addon.slug] = addon
|
||||||
|
yield addon
|
||||||
|
Loading…
x
Reference in New Issue
Block a user