From e0fd31c39056c2f81b02a6cb768c97c590d9a2ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 28 Dec 2021 11:13:54 +0100 Subject: [PATCH] Create backup of add-ons and core before update (#3369) * Create backup of addons and core before update * Move responsibility --- supervisor/addons/__init__.py | 9 ++++++++- supervisor/addons/model.py | 4 ++-- supervisor/api/homeassistant.py | 18 ++++++++++++++---- supervisor/api/store.py | 17 ++++++++++++++--- supervisor/const.py | 1 + supervisor/homeassistant/core.py | 15 ++++++++++++++- 6 files changed, 53 insertions(+), 11 deletions(-) diff --git a/supervisor/addons/__init__.py b/supervisor/addons/__init__.py index cacdccc78..9087ab5db 100644 --- a/supervisor/addons/__init__.py +++ b/supervisor/addons/__init__.py @@ -252,7 +252,7 @@ class AddonManager(CoreSysAttributes): ], on_condition=AddonsJobError, ) - async def update(self, slug: str) -> None: + async def update(self, slug: str, backup: Optional[bool] = False) -> None: """Update add-on.""" if slug not in self.local: raise AddonsError(f"Add-on {slug} is not installed", _LOGGER.error) @@ -273,6 +273,13 @@ class AddonManager(CoreSysAttributes): f"Add-on {slug} not supported on that platform", _LOGGER.error ) + if backup: + await self.sys_backups.do_backup_partial( + name=f"addon_{addon.slug}_{addon.version}", + homeassistant=False, + addons=[addon.slug], + ) + # Update instance last_state: AddonState = addon.state old_image = addon.image diff --git a/supervisor/addons/model.py b/supervisor/addons/model.py index 8f4c990f8..103aa8348 100644 --- a/supervisor/addons/model.py +++ b/supervisor/addons/model.py @@ -622,9 +622,9 @@ class AddonModel(CoreSysAttributes, ABC): """Uninstall this add-on.""" return self.sys_addons.uninstall(self.slug) - def update(self) -> Awaitable[None]: + def update(self, backup: Optional[bool] = False) -> Awaitable[None]: """Update this add-on.""" - return self.sys_addons.update(self.slug) + return self.sys_addons.update(self.slug, backup=backup) def rebuild(self) -> Awaitable[None]: """Rebuild this add-on.""" diff --git a/supervisor/api/homeassistant.py b/supervisor/api/homeassistant.py index 33d1a804b..19f1adde2 100644 --- a/supervisor/api/homeassistant.py +++ b/supervisor/api/homeassistant.py @@ -10,6 +10,7 @@ from ..const import ( ATTR_ARCH, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, + ATTR_BACKUP, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_BOOT, @@ -54,7 +55,12 @@ SCHEMA_OPTIONS = vol.Schema( } ) -SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag}) +SCHEMA_UPDATE = vol.Schema( + { + vol.Optional(ATTR_VERSION): version_tag, + vol.Optional(ATTR_BACKUP): bool, + } +) class APIHomeAssistant(CoreSysAttributes): @@ -137,10 +143,14 @@ class APIHomeAssistant(CoreSysAttributes): @api_process async def update(self, request: web.Request) -> None: """Update Home Assistant.""" - body = await api_validate(SCHEMA_VERSION, request) - version = body.get(ATTR_VERSION, self.sys_homeassistant.latest_version) + body = await api_validate(SCHEMA_UPDATE, request) - await asyncio.shield(self.sys_homeassistant.core.update(version)) + await asyncio.shield( + self.sys_homeassistant.core.update( + version=body.get(ATTR_VERSION, self.sys_homeassistant.latest_version), + backup=body.get(ATTR_BACKUP), + ) + ) @api_process def stop(self, request: web.Request) -> Awaitable[None]: diff --git a/supervisor/api/store.py b/supervisor/api/store.py index b820f3e35..48d429966 100644 --- a/supervisor/api/store.py +++ b/supervisor/api/store.py @@ -3,13 +3,15 @@ import asyncio from typing import Any, Awaitable from aiohttp import web +import voluptuous as vol from ..addons import AnyAddon -from ..api.utils import api_process +from ..api.utils import api_process, api_validate from ..const import ( ATTR_ADDONS, ATTR_ADVANCED, ATTR_AVAILABLE, + ATTR_BACKUP, ATTR_BUILD, ATTR_DESCRIPTON, ATTR_HOMEASSISTANT, @@ -34,6 +36,12 @@ from ..exceptions import APIError, APIForbidden from ..store.addon import AddonStore from ..store.repository import Repository +SCHEMA_UPDATE = vol.Schema( + { + vol.Optional(ATTR_BACKUP): bool, + } +) + class APIStore(CoreSysAttributes): """Handle RESTful API for store functions.""" @@ -134,12 +142,15 @@ class APIStore(CoreSysAttributes): return asyncio.shield(addon.install()) @api_process - def addons_addon_update(self, request: web.Request) -> Awaitable[None]: + async def addons_addon_update(self, request: web.Request) -> None: """Update add-on.""" addon = self._extract_addon(request, installed=True) if addon == request.get(REQUEST_FROM): raise APIForbidden(f"Add-on {addon.slug} can't update itself!") - return asyncio.shield(addon.update()) + + body = await api_validate(SCHEMA_UPDATE, request) + + return await asyncio.shield(addon.update(backup=body.get(ATTR_BACKUP))) @api_process async def addons_addon_info(self, request: web.Request) -> dict[str, Any]: diff --git a/supervisor/const.py b/supervisor/const.py index 43f0be7f5..5573a6ae0 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -114,6 +114,7 @@ ATTR_AUTH = "auth" ATTR_AUTH_API = "auth_api" ATTR_AUTO_UPDATE = "auto_update" ATTR_AVAILABLE = "available" +ATTR_BACKUP = "backup" ATTR_BACKUP_EXCLUDE = "backup_exclude" ATTR_BACKUP_POST = "backup_post" ATTR_BACKUP_PRE = "backup_pre" diff --git a/supervisor/homeassistant/core.py b/supervisor/homeassistant/core.py index 91cafc0d2..c6d3e62ae 100644 --- a/supervisor/homeassistant/core.py +++ b/supervisor/homeassistant/core.py @@ -12,6 +12,8 @@ from typing import Awaitable, Optional import attr from awesomeversion import AwesomeVersion, AwesomeVersionException +from supervisor.const import ATTR_HOMEASSISTANT + from ..coresys import CoreSys, CoreSysAttributes from ..docker.homeassistant import DockerHomeAssistant from ..docker.stats import DockerStats @@ -173,7 +175,11 @@ class HomeAssistantCore(CoreSysAttributes): ], on_condition=HomeAssistantJobError, ) - async def update(self, version: Optional[AwesomeVersion] = None) -> None: + async def update( + self, + version: Optional[AwesomeVersion] = None, + backup: Optional[bool] = False, + ) -> None: """Update HomeAssistant version.""" version = version or self.sys_homeassistant.latest_version old_image = self.sys_homeassistant.image @@ -186,6 +192,13 @@ class HomeAssistantCore(CoreSysAttributes): f"Version {version!s} is already installed", _LOGGER.warning ) + if backup: + await self.sys_backups.do_backup_partial( + name=f"core_{self.instance.version}", + homeassistant=True, + folders=[ATTR_HOMEASSISTANT], + ) + # process an update async def _update(to_version: AwesomeVersion) -> None: """Run Home Assistant update."""