mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-12-24 20:35:55 +00:00
Add snapshot feature (#88)
* Add API layout for snapshot
* Update api
* Add support for export/import docker images
* Move restore into addon
* Add restore to addon
* Fix lint
* fix lint
* cleanup
* init object
* fix executor
* cleanup
* Change flow of init
* Revert "Change flow of init"
This reverts commit 6b3215e44c.
* allow restore from none
* forward working
* add size
* add context for snapshot
* add init function to set meta data
* update local addon on load
* add more validate and optimaze code
* Optimaze code for restore data
* add validate layer
* Add more function to snapshot / cleanup others
* finish snapshot function
* Cleanup config / optimaze code
* Finish snapshot on core
* Some improvments first object for api
* finish
* fix lint p1
* fix lint p2
* fix lint p3
* fix async with
* fix lint p4
* fix lint p5
* fix p6
* make staticmethod
* fix schema
* fix parse system data
* fix bugs
* fix get function
* extend snapshot/restore
* add type
* fix lint
* move to gz / xz is to slow
* move to gz / xz is to slow p2
* Fix config folder
* small compresslevel for more speed
* fix lint
* fix load
* fix tar stream
* fix tar stream p2
* fix parse
* fix partial
* fix start hass
* fix rep
* fix set
* fix real
* fix generator
* Cleanup old image
* add log
* fix lint
* fix lint p2
* fix load from tar
This commit is contained in:
@@ -1,22 +1,29 @@
|
||||
"""Init file for HassIO addons."""
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
import json
|
||||
from pathlib import Path, PurePath
|
||||
import re
|
||||
import shutil
|
||||
import tarfile
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from .validate import validate_options, MAP_VOLUME
|
||||
from .validate import (
|
||||
validate_options, SCHEMA_ADDON_USER, SCHEMA_ADDON_SYSTEM,
|
||||
SCHEMA_ADDON_SNAPSHOT, MAP_VOLUME)
|
||||
from ..const import (
|
||||
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP,
|
||||
ATTR_OPTIONS, ATTR_PORTS, ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY,
|
||||
ATTR_URL, ATTR_ARCH, ATTR_LOCATON, ATTR_DEVICES, ATTR_ENVIRONMENT,
|
||||
ATTR_HOST_NETWORK, ATTR_TMPFS, ATTR_PRIVILEGED, ATTR_STARTUP,
|
||||
STATE_STARTED, STATE_STOPPED, STATE_NONE)
|
||||
STATE_STARTED, STATE_STOPPED, STATE_NONE, ATTR_USER, ATTR_SYSTEM,
|
||||
ATTR_STATE)
|
||||
from .util import check_installed
|
||||
from ..dock.addon import DockerAddon
|
||||
from ..tools import write_json_file
|
||||
from ..tools import write_json_file, read_json_file
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,22 +33,33 @@ RE_VOLUME = re.compile(MAP_VOLUME)
|
||||
class Addon(object):
|
||||
"""Hold data for addon inside HassIO."""
|
||||
|
||||
def __init__(self, config, loop, dock, data, addon_slug):
|
||||
def __init__(self, config, loop, dock, data, slug):
|
||||
"""Initialize data holder."""
|
||||
self.loop = loop
|
||||
self.config = config
|
||||
self.data = data
|
||||
self._id = addon_slug
|
||||
|
||||
if self._mesh is None:
|
||||
raise RuntimeError("{} not a valid addon!".format(self._id))
|
||||
self._id = slug
|
||||
|
||||
self.addon_docker = DockerAddon(config, loop, dock, self)
|
||||
|
||||
async def load(self):
|
||||
"""Async initialize of object."""
|
||||
if self.is_installed:
|
||||
self._validate_system_user()
|
||||
await self.addon_docker.attach()
|
||||
|
||||
def _validate_system_user(self):
|
||||
"""Validate internal data they read from file."""
|
||||
for data, schema in ((self.data.system, SCHEMA_ADDON_SYSTEM),
|
||||
(self.data.user, SCHEMA_ADDON_USER)):
|
||||
try:
|
||||
data[self._id] = schema(data[self._id])
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.warning("Can't validate addon load %s -> %s", self._id,
|
||||
humanize_error(data[self._id], err))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
"""Return slug/id of addon."""
|
||||
@@ -88,6 +106,12 @@ class Addon(object):
|
||||
self.data.user[self._id][ATTR_VERSION] = version
|
||||
self.data.save()
|
||||
|
||||
def _restore_data(self, user, system):
|
||||
"""Restore data to addon."""
|
||||
self.data.user[self._id] = deepcopy(user)
|
||||
self.data.system[self._id] = deepcopy(system)
|
||||
self.data.save()
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
"""Return options with local changes."""
|
||||
@@ -281,12 +305,9 @@ class Addon(object):
|
||||
self._set_install(version)
|
||||
return True
|
||||
|
||||
@check_installed
|
||||
async def uninstall(self):
|
||||
"""Remove a addon."""
|
||||
if not self.is_installed:
|
||||
_LOGGER.error("Addon %s is not installed", self._id)
|
||||
return False
|
||||
|
||||
if not await self.addon_docker.remove():
|
||||
return False
|
||||
|
||||
@@ -307,29 +328,21 @@ class Addon(object):
|
||||
return STATE_STARTED
|
||||
return STATE_STOPPED
|
||||
|
||||
@check_installed
|
||||
async def start(self):
|
||||
"""Set options and start addon."""
|
||||
if not self.is_installed:
|
||||
_LOGGER.error("Addon %s is not installed", self._id)
|
||||
return False
|
||||
|
||||
return await self.addon_docker.run()
|
||||
|
||||
@check_installed
|
||||
async def stop(self):
|
||||
"""Stop addon."""
|
||||
if not self.is_installed:
|
||||
_LOGGER.error("Addon %s is not installed", self._id)
|
||||
return False
|
||||
|
||||
return await self.addon_docker.stop()
|
||||
|
||||
@check_installed
|
||||
async def update(self, version=None):
|
||||
"""Update addon."""
|
||||
if not self.is_installed:
|
||||
_LOGGER.error("Addon %s is not installed", self._id)
|
||||
return False
|
||||
|
||||
version = version or self.last_version
|
||||
|
||||
if version == self.version_installed:
|
||||
_LOGGER.warning(
|
||||
"Addon %s is already installed in %s", self._id, version)
|
||||
@@ -341,18 +354,113 @@ class Addon(object):
|
||||
self._set_update(version)
|
||||
return True
|
||||
|
||||
@check_installed
|
||||
async def restart(self):
|
||||
"""Restart addon."""
|
||||
if not self.is_installed:
|
||||
_LOGGER.error("Addon %s is not installed", self._id)
|
||||
return False
|
||||
|
||||
return await self.addon_docker.restart()
|
||||
|
||||
@check_installed
|
||||
async def logs(self):
|
||||
"""Return addons log output."""
|
||||
if not self.is_installed:
|
||||
_LOGGER.error("Addon %s is not installed", self._id)
|
||||
return False
|
||||
|
||||
return await self.addon_docker.logs()
|
||||
|
||||
@check_installed
|
||||
async def snapshot(self, tar_file):
|
||||
"""Snapshot a state of a addon."""
|
||||
with TemporaryDirectory(dir=str(self.config.path_tmp)) as temp:
|
||||
# store local image
|
||||
if self.need_build and not await \
|
||||
self.addon_docker.export_image(Path(temp, "image.tar")):
|
||||
return False
|
||||
|
||||
data = {
|
||||
ATTR_USER: self.data.user.get(self._id, {}),
|
||||
ATTR_SYSTEM: self.data.system.get(self._id, {}),
|
||||
ATTR_VERSION: self.version_installed,
|
||||
ATTR_STATE: await self.state(),
|
||||
}
|
||||
|
||||
# store local configs/state
|
||||
if not write_json_file(Path(temp, "addon.json"), data):
|
||||
_LOGGER.error("Can't write addon.json for %s", self._id)
|
||||
return False
|
||||
|
||||
# write into tarfile
|
||||
def _create_tar():
|
||||
"""Write tar inside loop."""
|
||||
with tarfile.open(tar_file, "w:gz",
|
||||
compresslevel=1) as snapshot:
|
||||
snapshot.add(temp, arcname=".")
|
||||
snapshot.add(self.path_data, arcname="data")
|
||||
|
||||
try:
|
||||
await self.loop.run_in_executor(None, _create_tar)
|
||||
except tarfile.TarError as err:
|
||||
_LOGGER.error("Can't write tarfile %s -> %s", tar_file, err)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def restore(self, tar_file):
|
||||
"""Restore a state of a addon."""
|
||||
with TemporaryDirectory(dir=str(self.config.path_tmp)) as temp:
|
||||
# extract snapshot
|
||||
def _extract_tar():
|
||||
"""Extract tar snapshot."""
|
||||
with tarfile.open(tar_file, "r:gz") as snapshot:
|
||||
snapshot.extractall(path=Path(temp))
|
||||
|
||||
try:
|
||||
await self.loop.run_in_executor(None, _extract_tar)
|
||||
except tarfile.TarError as err:
|
||||
_LOGGER.error("Can't read tarfile %s -> %s", tar_file, err)
|
||||
return False
|
||||
|
||||
# read snapshot data
|
||||
try:
|
||||
data = read_json_file(Path(temp, "addon.json"))
|
||||
except (OSError, json.JSONDecodeError) as err:
|
||||
_LOGGER.error("Can't read addon.json -> %s", err)
|
||||
|
||||
# validate
|
||||
try:
|
||||
data = SCHEMA_ADDON_SNAPSHOT(data)
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error("Can't validate %s, snapshot data -> %s",
|
||||
self._id, humanize_error(data, err))
|
||||
return False
|
||||
|
||||
# restore data / reload addon
|
||||
self._restore_data(data[ATTR_USER], data[ATTR_SYSTEM])
|
||||
|
||||
# check version / restore image
|
||||
if data[ATTR_VERSION] != self.addon_docker.version:
|
||||
image_file = Path(temp, "image.tar")
|
||||
if image_file.is_file():
|
||||
if not await self.addon_docker.import_image(image_file):
|
||||
return False
|
||||
else:
|
||||
if not await self.addon_docker.install(data[ATTR_VERSION]):
|
||||
return False
|
||||
await self.addon_docker.cleanup()
|
||||
else:
|
||||
await self.addon_docker.stop()
|
||||
|
||||
# restore data
|
||||
def _restore_data():
|
||||
"""Restore data."""
|
||||
if self.path_data.is_dir():
|
||||
shutil.rmtree(str(self.path_data), ignore_errors=True)
|
||||
shutil.copytree(str(Path(temp, "data")), str(self.path_data))
|
||||
|
||||
try:
|
||||
await self.loop.run_in_executor(None, _restore_data)
|
||||
except shutil.Error as err:
|
||||
_LOGGER.error("Can't restore origin data -> %s", err)
|
||||
return False
|
||||
|
||||
# run addon
|
||||
if data[ATTR_STATE] == STATE_STARTED:
|
||||
return await self.start()
|
||||
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user