Support HOT/COLD snapshots for Add-ons (#2943)

* Support HOT/COLD snapshots for Add-ons

* Apply suggestions from code review

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>

* Add warning

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
This commit is contained in:
Pascal Vizeli 2021-06-21 09:07:54 +02:00 committed by GitHub
parent 16f2f63081
commit 0177b38ded
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 53 additions and 2 deletions

View File

@ -65,6 +65,7 @@ from ..utils import check_port
from ..utils.apparmor import adjust_profile from ..utils.apparmor import adjust_profile
from ..utils.json import read_json_file, write_json_file from ..utils.json import read_json_file, write_json_file
from ..utils.tar import atomic_contents_add, secure_path from ..utils.tar import atomic_contents_add, secure_path
from .const import SnapshotAddonMode
from .model import AddonModel, Data from .model import AddonModel, Data
from .options import AddonOptions from .options import AddonOptions
from .utils import remove_data from .utils import remove_data
@ -695,6 +696,8 @@ class Addon(AddonModel):
async def snapshot(self, tar_file: tarfile.TarFile) -> None: async def snapshot(self, tar_file: tarfile.TarFile) -> None:
"""Snapshot state of an add-on.""" """Snapshot state of an add-on."""
is_running = await self.is_running()
with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp: with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp:
temp_path = Path(temp) temp_path = Path(temp)
@ -744,8 +747,15 @@ class Addon(AddonModel):
arcname="data", arcname="data",
) )
if self.snapshot_pre is not None: if (
is_running
and self.snapshot_mode == SnapshotAddonMode.HOT
and self.snapshot_pre is not None
):
await self._snapshot_command(self.snapshot_pre) await self._snapshot_command(self.snapshot_pre)
elif is_running and self.snapshot_mode == SnapshotAddonMode.COLD:
_LOGGER.info("Shutdown add-on %s for cold snapshot", self.slug)
await self.instance.stop()
try: try:
_LOGGER.info("Building snapshot for add-on %s", self.slug) _LOGGER.info("Building snapshot for add-on %s", self.slug)
@ -754,8 +764,15 @@ class Addon(AddonModel):
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err) _LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
raise AddonsError() from err raise AddonsError() from err
finally: finally:
if self.snapshot_post is not None: if (
is_running
and self.snapshot_mode == SnapshotAddonMode.HOT
and self.snapshot_post is not None
):
await self._snapshot_command(self.snapshot_post) await self._snapshot_command(self.snapshot_post)
elif is_running and self.snapshot_mode is SnapshotAddonMode.COLD:
_LOGGER.info("Starting add-on %s again", self.slug)
await self.start()
_LOGGER.info("Finish snapshot for addon %s", self.slug) _LOGGER.info("Finish snapshot for addon %s", self.slug)

View File

@ -0,0 +1,12 @@
"""Add-on static data."""
from enum import Enum
class SnapshotAddonMode(str, Enum):
"""Snapshot mode of an Add-on."""
HOT = "hot"
COLD = "cold"
ATTR_SNAPSHOT = "snapshot"

View File

@ -5,6 +5,8 @@ from typing import Any, Awaitable, Dict, List, Optional
from awesomeversion import AwesomeVersion, AwesomeVersionException from awesomeversion import AwesomeVersion, AwesomeVersionException
from supervisor.addons.const import SnapshotAddonMode
from ..const import ( from ..const import (
ATTR_ADVANCED, ATTR_ADVANCED,
ATTR_APPARMOR, ATTR_APPARMOR,
@ -76,6 +78,7 @@ from ..const import (
) )
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..docker.const import Capabilities from ..docker.const import Capabilities
from .const import ATTR_SNAPSHOT
from .options import AddonOptions, UiOptions from .options import AddonOptions, UiOptions
from .validate import RE_SERVICE, RE_VOLUME from .validate import RE_SERVICE, RE_VOLUME
@ -370,6 +373,11 @@ class AddonModel(CoreSysAttributes, ABC):
"""Return post-snapshot command.""" """Return post-snapshot command."""
return self.data.get(ATTR_SNAPSHOT_POST) return self.data.get(ATTR_SNAPSHOT_POST)
@property
def snapshot_mode(self) -> SnapshotAddonMode:
"""Return if snapshot is hot/cold."""
return self.data[ATTR_SNAPSHOT]
@property @property
def default_init(self) -> bool: def default_init(self) -> bool:
"""Return True if the add-on have no own init.""" """Return True if the add-on have no own init."""

View File

@ -7,6 +7,8 @@ import uuid
import voluptuous as vol import voluptuous as vol
from supervisor.addons.const import SnapshotAddonMode
from ..const import ( from ..const import (
ARCH_ALL, ARCH_ALL,
ATTR_ACCESS_TOKEN, ATTR_ACCESS_TOKEN,
@ -107,6 +109,7 @@ from ..validate import (
uuid_match, uuid_match,
version_tag, version_tag,
) )
from .const import ATTR_SNAPSHOT
from .options import RE_SCHEMA_ELEMENT from .options import RE_SCHEMA_ELEMENT
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -161,6 +164,14 @@ def _warn_addon_config(config: Dict[str, Any]):
name, name,
) )
if config.get(ATTR_SNAPSHOT, SnapshotAddonMode.HOT) == SnapshotAddonMode.COLD and (
config.get(ATTR_SNAPSHOT_POST) or config.get(ATTR_SNAPSHOT_PRE)
):
_LOGGER.warning(
"Add-on which only support COLD backups trying to use post/pre commands. Please report this to the maintainer of %s",
name,
)
return config return config
@ -284,6 +295,9 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
vol.Optional(ATTR_SNAPSHOT_EXCLUDE): [str], vol.Optional(ATTR_SNAPSHOT_EXCLUDE): [str],
vol.Optional(ATTR_SNAPSHOT_PRE): str, vol.Optional(ATTR_SNAPSHOT_PRE): str,
vol.Optional(ATTR_SNAPSHOT_POST): str, vol.Optional(ATTR_SNAPSHOT_POST): str,
vol.Optional(ATTR_SNAPSHOT, default=SnapshotAddonMode.HOT): vol.Coerce(
SnapshotAddonMode
),
vol.Optional(ATTR_OPTIONS, default={}): dict, vol.Optional(ATTR_OPTIONS, default={}): dict,
vol.Optional(ATTR_SCHEMA, default={}): vol.Any( vol.Optional(ATTR_SCHEMA, default={}): vol.Any(
vol.Schema( vol.Schema(