mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-12-24 12:29:08 +00:00
Add Ingress support (#991)
* Add Ingress support to supervisor * Update security * cleanup add-on extraction * update description * fix header part * fix * Fix header check * fix tox * Migrate docker interface typing * Update home assistant to new docker * Migrate supervisor * Fix host add-on problem * Update hassos * Update API * Expose data to API * Check on API ingress support * Add ingress URL * Some cleanups * debug * disable uvloop * Fix issue * test * Fix bug * Fix flow * Fix interface * Fix network * Fix metadata * cleanups * Fix exception * Migrate to token system * Fix webui * Fix update * Fix relaod * Update log messages * Attach ingress url only if enabled * Cleanup ingress url handling * Ingress update * Support check version * Fix raise error * Migrate default port * Fix junks * search error * Fix content filter * Add debug * Update log * Update flags * Update documentation * Cleanup debugs * Fix lint * change default port to 8099 * Fix lint * fix lint
This commit is contained in:
@@ -1,41 +1,105 @@
|
||||
"""Init file for Hass.io add-ons."""
|
||||
from contextlib import suppress
|
||||
from copy import deepcopy
|
||||
from distutils.version import StrictVersion
|
||||
from ipaddress import IPv4Address, ip_address
|
||||
import logging
|
||||
from pathlib import Path, PurePath
|
||||
import re
|
||||
import shutil
|
||||
import tarfile
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Dict, Any
|
||||
from typing import Any, Awaitable, Dict, Optional
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from ..const import (
|
||||
ATTR_ACCESS_TOKEN, ATTR_APPARMOR, ATTR_ARCH, ATTR_AUDIO, ATTR_AUDIO_INPUT,
|
||||
ATTR_AUDIO_OUTPUT, ATTR_AUTH_API, ATTR_AUTO_UART, ATTR_AUTO_UPDATE,
|
||||
ATTR_BOOT, ATTR_DESCRIPTON, ATTR_DEVICES, ATTR_DEVICETREE, ATTR_DISCOVERY,
|
||||
ATTR_DOCKER_API, ATTR_ENVIRONMENT, ATTR_FULL_ACCESS, ATTR_GPIO,
|
||||
ATTR_HASSIO_API, ATTR_HASSIO_ROLE, ATTR_HOMEASSISTANT_API, ATTR_HOST_DBUS,
|
||||
ATTR_HOST_IPC, ATTR_HOST_NETWORK, ATTR_HOST_PID, ATTR_IMAGE,
|
||||
ATTR_KERNEL_MODULES, ATTR_LEGACY, ATTR_LOCATON, ATTR_MACHINE, ATTR_MAP,
|
||||
ATTR_NAME, ATTR_NETWORK, ATTR_OPTIONS, ATTR_PORTS, ATTR_PRIVILEGED,
|
||||
ATTR_PROTECTED, ATTR_REPOSITORY, ATTR_SCHEMA, ATTR_SERVICES, ATTR_SLUG,
|
||||
ATTR_STARTUP, ATTR_STATE, ATTR_STDIN, ATTR_SYSTEM, ATTR_TIMEOUT,
|
||||
ATTR_TMPFS, ATTR_URL, ATTR_USER, ATTR_UUID, ATTR_VERSION, ATTR_WEBUI,
|
||||
SECURITY_DEFAULT, SECURITY_DISABLE, SECURITY_PROFILE, STATE_NONE,
|
||||
STATE_STARTED, STATE_STOPPED)
|
||||
from ..coresys import CoreSysAttributes
|
||||
ATTR_ACCESS_TOKEN,
|
||||
ATTR_APPARMOR,
|
||||
ATTR_ARCH,
|
||||
ATTR_AUDIO,
|
||||
ATTR_AUDIO_INPUT,
|
||||
ATTR_AUDIO_OUTPUT,
|
||||
ATTR_AUTH_API,
|
||||
ATTR_AUTO_UART,
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_BOOT,
|
||||
ATTR_DESCRIPTON,
|
||||
ATTR_DEVICES,
|
||||
ATTR_DEVICETREE,
|
||||
ATTR_DISCOVERY,
|
||||
ATTR_DOCKER_API,
|
||||
ATTR_ENVIRONMENT,
|
||||
ATTR_FULL_ACCESS,
|
||||
ATTR_GPIO,
|
||||
ATTR_HASSIO_API,
|
||||
ATTR_HASSIO_ROLE,
|
||||
ATTR_HOMEASSISTANT,
|
||||
ATTR_HOMEASSISTANT_API,
|
||||
ATTR_HOST_DBUS,
|
||||
ATTR_HOST_IPC,
|
||||
ATTR_HOST_NETWORK,
|
||||
ATTR_HOST_PID,
|
||||
ATTR_IMAGE,
|
||||
ATTR_INGRESS,
|
||||
ATTR_INGRESS_ENTRY,
|
||||
ATTR_INGRESS_PORT,
|
||||
ATTR_INGRESS_TOKEN,
|
||||
ATTR_KERNEL_MODULES,
|
||||
ATTR_LEGACY,
|
||||
ATTR_LOCATON,
|
||||
ATTR_MACHINE,
|
||||
ATTR_MAP,
|
||||
ATTR_NAME,
|
||||
ATTR_NETWORK,
|
||||
ATTR_OPTIONS,
|
||||
ATTR_PORTS,
|
||||
ATTR_PRIVILEGED,
|
||||
ATTR_PROTECTED,
|
||||
ATTR_REPOSITORY,
|
||||
ATTR_SCHEMA,
|
||||
ATTR_SERVICES,
|
||||
ATTR_SLUG,
|
||||
ATTR_STARTUP,
|
||||
ATTR_STATE,
|
||||
ATTR_STDIN,
|
||||
ATTR_SYSTEM,
|
||||
ATTR_TIMEOUT,
|
||||
ATTR_TMPFS,
|
||||
ATTR_URL,
|
||||
ATTR_USER,
|
||||
ATTR_UUID,
|
||||
ATTR_VERSION,
|
||||
ATTR_WEBUI,
|
||||
SECURITY_DEFAULT,
|
||||
SECURITY_DISABLE,
|
||||
SECURITY_PROFILE,
|
||||
STATE_NONE,
|
||||
STATE_STARTED,
|
||||
STATE_STOPPED,
|
||||
)
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..docker.addon import DockerAddon
|
||||
from ..exceptions import HostAppArmorError, JsonFileError
|
||||
from ..docker.stats import DockerStats
|
||||
from ..exceptions import (
|
||||
AddonsError,
|
||||
AddonsNotSupportedError,
|
||||
DockerAPIError,
|
||||
HostAppArmorError,
|
||||
JsonFileError,
|
||||
)
|
||||
from ..utils import create_token
|
||||
from ..utils.apparmor import adjust_profile
|
||||
from ..utils.json import read_json_file, write_json_file
|
||||
from .utils import check_installed, remove_data
|
||||
from .validate import (
|
||||
MACHINE_ALL, RE_SERVICE, RE_VOLUME, SCHEMA_ADDON_SNAPSHOT,
|
||||
validate_options)
|
||||
MACHINE_ALL,
|
||||
RE_SERVICE,
|
||||
RE_VOLUME,
|
||||
SCHEMA_ADDON_SNAPSHOT,
|
||||
validate_options,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -47,21 +111,28 @@ RE_WEBUI = re.compile(
|
||||
class Addon(CoreSysAttributes):
|
||||
"""Hold data for add-on inside Hass.io."""
|
||||
|
||||
def __init__(self, coresys, slug):
|
||||
def __init__(self, coresys: CoreSys, slug: str):
|
||||
"""Initialize data holder."""
|
||||
self.coresys = coresys
|
||||
self.instance = DockerAddon(coresys, slug)
|
||||
self.coresys: CoreSys = coresys
|
||||
self.instance: DockerAddon = DockerAddon(coresys, slug)
|
||||
self._id: str = slug
|
||||
|
||||
self._id = slug
|
||||
|
||||
async def load(self):
|
||||
async def load(self) -> None:
|
||||
"""Async initialize of object."""
|
||||
if not self.is_installed:
|
||||
return
|
||||
await self.instance.attach()
|
||||
with suppress(DockerAPIError):
|
||||
await self.instance.attach()
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
def ip_address(self) -> IPv4Address:
|
||||
"""Return IP of Add-on instance."""
|
||||
if not self.is_installed:
|
||||
return ip_address("0.0.0.0")
|
||||
return self.instance.ip_address
|
||||
|
||||
@property
|
||||
def slug(self) -> str:
|
||||
"""Return slug/id of add-on."""
|
||||
return self._id
|
||||
|
||||
@@ -76,30 +147,41 @@ class Addon(CoreSysAttributes):
|
||||
return self.sys_addons.data
|
||||
|
||||
@property
|
||||
def is_installed(self):
|
||||
def is_installed(self) -> bool:
|
||||
"""Return True if an add-on is installed."""
|
||||
return self._id in self._data.system
|
||||
|
||||
@property
|
||||
def is_detached(self):
|
||||
def is_detached(self) -> bool:
|
||||
"""Return True if add-on is detached."""
|
||||
return self._id not in self._data.cache
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
def available(self) -> bool:
|
||||
"""Return True if this add-on is available on this platform."""
|
||||
if self.is_detached:
|
||||
addon_data = self._data.system.get(self._id)
|
||||
else:
|
||||
addon_data = self._data.cache.get(self._id)
|
||||
|
||||
# Architecture
|
||||
if not self.sys_arch.is_supported(self.supported_arch):
|
||||
if not self.sys_arch.is_supported(addon_data[ATTR_ARCH]):
|
||||
return False
|
||||
|
||||
# Machine / Hardware
|
||||
if self.sys_machine not in self.supported_machine:
|
||||
machine = addon_data.get(ATTR_MACHINE) or MACHINE_ALL
|
||||
if self.sys_machine not in machine:
|
||||
return False
|
||||
|
||||
# Home Assistant
|
||||
version = addon_data.get(ATTR_HOMEASSISTANT) or self.sys_homeassistant.version
|
||||
if StrictVersion(self.sys_homeassistant.version) < StrictVersion(version):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def version_installed(self):
|
||||
def version_installed(self) -> Optional[str]:
|
||||
"""Return installed version."""
|
||||
return self._data.user.get(self._id, {}).get(ATTR_VERSION)
|
||||
|
||||
@@ -202,6 +284,20 @@ class Addon(CoreSysAttributes):
|
||||
return self._data.user[self._id].get(ATTR_ACCESS_TOKEN)
|
||||
return None
|
||||
|
||||
@property
|
||||
def ingress_token(self):
|
||||
"""Return access token for Hass.io API."""
|
||||
if self.is_installed:
|
||||
return self._data.user[self._id].get(ATTR_INGRESS_TOKEN)
|
||||
return None
|
||||
|
||||
@property
|
||||
def ingress_entry(self):
|
||||
"""Return ingress external URL."""
|
||||
if self.is_installed and self.with_ingress:
|
||||
return f"/api/hassio_ingress/{self.ingress_token}"
|
||||
return None
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""Return description of add-on."""
|
||||
@@ -292,6 +388,18 @@ class Addon(CoreSysAttributes):
|
||||
|
||||
self._data.user[self._id][ATTR_NETWORK] = new_ports
|
||||
|
||||
@property
|
||||
def ingress_url(self):
|
||||
"""Return URL to ingress url."""
|
||||
# Use ingress
|
||||
if not self.with_ingress:
|
||||
return None
|
||||
|
||||
webui = f"/api/hassio_ingress/{self.ingress_token}/"
|
||||
if ATTR_INGRESS_ENTRY in self._mesh:
|
||||
return f"{webui}{self._mesh[ATTR_INGRESS_ENTRY]}"
|
||||
return webui
|
||||
|
||||
@property
|
||||
def webui(self):
|
||||
"""Return URL to webui or None."""
|
||||
@@ -323,6 +431,11 @@ class Addon(CoreSysAttributes):
|
||||
|
||||
return f"{proto}://[HOST]:{port}{s_suffix}"
|
||||
|
||||
@property
|
||||
def ingress_internal(self):
|
||||
"""Return Ingress host URL."""
|
||||
return f"http://{self.ip_address}:{self._mesh[ATTR_INGRESS_PORT]}"
|
||||
|
||||
@property
|
||||
def host_network(self):
|
||||
"""Return True if add-on run on host network."""
|
||||
@@ -407,6 +520,11 @@ class Addon(CoreSysAttributes):
|
||||
"""Return True if the add-on access use stdin input."""
|
||||
return self._mesh[ATTR_STDIN]
|
||||
|
||||
@property
|
||||
def with_ingress(self):
|
||||
"""Return True if the add-on access support ingress."""
|
||||
return self._mesh[ATTR_INGRESS]
|
||||
|
||||
@property
|
||||
def with_gpio(self):
|
||||
"""Return True if the add-on access to GPIO interface."""
|
||||
@@ -437,6 +555,11 @@ class Addon(CoreSysAttributes):
|
||||
"""Return True if the add-on access to audio."""
|
||||
return self._mesh[ATTR_AUDIO]
|
||||
|
||||
@property
|
||||
def homeassistant_version(self) -> Optional[str]:
|
||||
"""Return min Home Assistant version they needed by Add-on."""
|
||||
return self._mesh.get(ATTR_HOMEASSISTANT)
|
||||
|
||||
@property
|
||||
def audio_output(self):
|
||||
"""Return ALSA config for output or None."""
|
||||
@@ -642,7 +765,7 @@ class Addon(CoreSysAttributes):
|
||||
|
||||
return True
|
||||
|
||||
async def _install_apparmor(self):
|
||||
async def _install_apparmor(self) -> None:
|
||||
"""Install or Update AppArmor profile for Add-on."""
|
||||
exists_local = self.sys_host.apparmor.exists(self.slug)
|
||||
exists_addon = self.path_apparmor.exists()
|
||||
@@ -664,7 +787,7 @@ class Addon(CoreSysAttributes):
|
||||
await self.sys_host.apparmor.load_profile(self.slug, profile_file)
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
def schema(self) -> vol.Schema:
|
||||
"""Create a schema for add-on options."""
|
||||
raw_schema = self._mesh[ATTR_SCHEMA]
|
||||
|
||||
@@ -672,7 +795,7 @@ class Addon(CoreSysAttributes):
|
||||
return vol.Schema(dict)
|
||||
return vol.Schema(vol.All(dict, validate_options(raw_schema)))
|
||||
|
||||
def test_update_schema(self):
|
||||
def test_update_schema(self) -> bool:
|
||||
"""Check if the existing configuration is valid after update."""
|
||||
if not self.is_installed or self.is_detached:
|
||||
return True
|
||||
@@ -702,17 +825,17 @@ class Addon(CoreSysAttributes):
|
||||
return False
|
||||
return True
|
||||
|
||||
async def install(self):
|
||||
async def install(self) -> None:
|
||||
"""Install an add-on."""
|
||||
if not self.available:
|
||||
_LOGGER.error(
|
||||
"Add-on %s not supported on %s with %s architecture",
|
||||
self._id, self.sys_machine, self.sys_arch.supported)
|
||||
return False
|
||||
raise AddonsNotSupportedError()
|
||||
|
||||
if self.is_installed:
|
||||
_LOGGER.error("Add-on %s is already installed", self._id)
|
||||
return False
|
||||
_LOGGER.warning("Add-on %s is already installed", self._id)
|
||||
return
|
||||
|
||||
if not self.path_data.is_dir():
|
||||
_LOGGER.info(
|
||||
@@ -722,18 +845,20 @@ class Addon(CoreSysAttributes):
|
||||
# Setup/Fix AppArmor profile
|
||||
await self._install_apparmor()
|
||||
|
||||
if not await self.instance.install(
|
||||
self.last_version, self.image_next):
|
||||
return False
|
||||
|
||||
self._set_install(self.image_next, self.last_version)
|
||||
return True
|
||||
try:
|
||||
await self.instance.install(self.last_version, self.image_next)
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
else:
|
||||
self._set_install(self.image_next, self.last_version)
|
||||
|
||||
@check_installed
|
||||
async def uninstall(self):
|
||||
async def uninstall(self) -> None:
|
||||
"""Remove an add-on."""
|
||||
if not await self.instance.remove():
|
||||
return False
|
||||
try:
|
||||
await self.instance.remove()
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
|
||||
if self.path_data.is_dir():
|
||||
_LOGGER.info(
|
||||
@@ -750,13 +875,11 @@ class Addon(CoreSysAttributes):
|
||||
with suppress(HostAppArmorError):
|
||||
await self.sys_host.apparmor.remove_profile(self.slug)
|
||||
|
||||
# Remove discovery messages
|
||||
# Cleanup internal data
|
||||
self.remove_discovery()
|
||||
|
||||
self._set_uninstall()
|
||||
return True
|
||||
|
||||
async def state(self):
|
||||
async def state(self) -> str:
|
||||
"""Return running state of add-on."""
|
||||
if not self.is_installed:
|
||||
return STATE_NONE
|
||||
@@ -766,7 +889,7 @@ class Addon(CoreSysAttributes):
|
||||
return STATE_STOPPED
|
||||
|
||||
@check_installed
|
||||
async def start(self):
|
||||
async def start(self) -> None:
|
||||
"""Set options and start add-on."""
|
||||
if await self.instance.is_running():
|
||||
_LOGGER.warning("%s already running!", self.slug)
|
||||
@@ -778,34 +901,45 @@ class Addon(CoreSysAttributes):
|
||||
|
||||
# Options
|
||||
if not self.write_options():
|
||||
return False
|
||||
raise AddonsError()
|
||||
|
||||
# Sound
|
||||
if self.with_audio and not self.write_asound():
|
||||
return False
|
||||
raise AddonsError()
|
||||
|
||||
return await self.instance.run()
|
||||
try:
|
||||
await self.instance.run()
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
|
||||
@check_installed
|
||||
def stop(self):
|
||||
"""Stop add-on.
|
||||
|
||||
Return a coroutine.
|
||||
"""
|
||||
return self.instance.stop()
|
||||
async def stop(self) -> None:
|
||||
"""Stop add-on."""
|
||||
try:
|
||||
return await self.instance.stop()
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
|
||||
@check_installed
|
||||
async def update(self):
|
||||
async def update(self) -> None:
|
||||
"""Update add-on."""
|
||||
last_state = await self.state()
|
||||
|
||||
if self.last_version == self.version_installed:
|
||||
_LOGGER.warning("No update available for add-on %s", self._id)
|
||||
return False
|
||||
return
|
||||
|
||||
if not await self.instance.update(
|
||||
self.last_version, self.image_next):
|
||||
return False
|
||||
# Check if available, Maybe something have changed
|
||||
if not self.available:
|
||||
_LOGGER.error(
|
||||
"Add-on %s not supported on %s with %s architecture",
|
||||
self._id, self.sys_machine, self.sys_arch.supported)
|
||||
raise AddonsNotSupportedError()
|
||||
|
||||
# Update instance
|
||||
last_state = await self.state()
|
||||
try:
|
||||
await self.instance.update(self.last_version, self.image_next)
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
self._set_update(self.image_next, self.last_version)
|
||||
|
||||
# Setup/Fix AppArmor profile
|
||||
@@ -814,16 +948,16 @@ class Addon(CoreSysAttributes):
|
||||
# restore state
|
||||
if last_state == STATE_STARTED:
|
||||
await self.start()
|
||||
return True
|
||||
|
||||
@check_installed
|
||||
async def restart(self):
|
||||
async def restart(self) -> None:
|
||||
"""Restart add-on."""
|
||||
await self.stop()
|
||||
return await self.start()
|
||||
with suppress(AddonsError):
|
||||
await self.stop()
|
||||
await self.start()
|
||||
|
||||
@check_installed
|
||||
def logs(self):
|
||||
def logs(self) -> Awaitable[bytes]:
|
||||
"""Return add-ons log output.
|
||||
|
||||
Return a coroutine.
|
||||
@@ -831,33 +965,32 @@ class Addon(CoreSysAttributes):
|
||||
return self.instance.logs()
|
||||
|
||||
@check_installed
|
||||
def stats(self):
|
||||
"""Return stats of container.
|
||||
|
||||
Return a coroutine.
|
||||
"""
|
||||
return self.instance.stats()
|
||||
async def stats(self) -> DockerStats:
|
||||
"""Return stats of container."""
|
||||
try:
|
||||
return await self.instance.stats()
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
|
||||
@check_installed
|
||||
async def rebuild(self):
|
||||
async def rebuild(self) -> None:
|
||||
"""Perform a rebuild of local build add-on."""
|
||||
last_state = await self.state()
|
||||
|
||||
if not self.need_build:
|
||||
_LOGGER.error("Can't rebuild a none local build add-on!")
|
||||
return False
|
||||
raise AddonsNotSupportedError()
|
||||
|
||||
# remove docker container but not addon config
|
||||
if not await self.instance.remove():
|
||||
return False
|
||||
|
||||
if not await self.instance.install(self.version_installed):
|
||||
return False
|
||||
try:
|
||||
await self.instance.remove()
|
||||
await self.instance.install(self.version_installed)
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
|
||||
# restore state
|
||||
if last_state == STATE_STARTED:
|
||||
await self.start()
|
||||
return True
|
||||
|
||||
@check_installed
|
||||
async def write_stdin(self, data):
|
||||
@@ -867,18 +1000,23 @@ class Addon(CoreSysAttributes):
|
||||
"""
|
||||
if not self.with_stdin:
|
||||
_LOGGER.error("Add-on don't support write to stdin!")
|
||||
return False
|
||||
raise AddonsNotSupportedError()
|
||||
|
||||
return await self.instance.write_stdin(data)
|
||||
try:
|
||||
return await self.instance.write_stdin(data)
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
|
||||
@check_installed
|
||||
async def snapshot(self, tar_file):
|
||||
async def snapshot(self, tar_file: tarfile.TarFile) -> None:
|
||||
"""Snapshot state of an add-on."""
|
||||
with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp:
|
||||
# store local image
|
||||
if self.need_build and not await \
|
||||
self.instance.export_image(Path(temp, 'image.tar')):
|
||||
return False
|
||||
if self.need_build:
|
||||
try:
|
||||
await self.instance.export_image(Path(temp, 'image.tar'))
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
|
||||
data = {
|
||||
ATTR_USER: self._data.user.get(self._id, {}),
|
||||
@@ -892,7 +1030,7 @@ class Addon(CoreSysAttributes):
|
||||
write_json_file(Path(temp, 'addon.json'), data)
|
||||
except JsonFileError:
|
||||
_LOGGER.error("Can't save meta for %s", self._id)
|
||||
return False
|
||||
raise AddonsError() from None
|
||||
|
||||
# Store AppArmor Profile
|
||||
if self.sys_host.apparmor.exists(self.slug):
|
||||
@@ -901,7 +1039,7 @@ class Addon(CoreSysAttributes):
|
||||
self.sys_host.apparmor.backup_profile(self.slug, profile)
|
||||
except HostAppArmorError:
|
||||
_LOGGER.error("Can't backup AppArmor profile")
|
||||
return False
|
||||
raise AddonsError() from None
|
||||
|
||||
# write into tarfile
|
||||
def _write_tarfile():
|
||||
@@ -915,12 +1053,11 @@ class Addon(CoreSysAttributes):
|
||||
await self.sys_run_in_executor(_write_tarfile)
|
||||
except (tarfile.TarError, OSError) as err:
|
||||
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
|
||||
return False
|
||||
raise AddonsError() from None
|
||||
|
||||
_LOGGER.info("Finish snapshot for addon %s", self._id)
|
||||
return True
|
||||
|
||||
async def restore(self, tar_file):
|
||||
async def restore(self, tar_file: tarfile.TarFile) -> None:
|
||||
"""Restore state of an add-on."""
|
||||
with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp:
|
||||
# extract snapshot
|
||||
@@ -933,13 +1070,13 @@ class Addon(CoreSysAttributes):
|
||||
await self.sys_run_in_executor(_extract_tarfile)
|
||||
except tarfile.TarError as err:
|
||||
_LOGGER.error("Can't read tarfile %s: %s", tar_file, err)
|
||||
return False
|
||||
raise AddonsError() from None
|
||||
|
||||
# Read snapshot data
|
||||
try:
|
||||
data = read_json_file(Path(temp, 'addon.json'))
|
||||
except JsonFileError:
|
||||
return False
|
||||
raise AddonsError() from None
|
||||
|
||||
# Validate
|
||||
try:
|
||||
@@ -947,7 +1084,7 @@ class Addon(CoreSysAttributes):
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error("Can't validate %s, snapshot data: %s",
|
||||
self._id, humanize_error(data, err))
|
||||
return False
|
||||
raise AddonsError() from None
|
||||
|
||||
# Restore local add-on informations
|
||||
_LOGGER.info("Restore config for addon %s", self._id)
|
||||
@@ -961,15 +1098,19 @@ class Addon(CoreSysAttributes):
|
||||
|
||||
image_file = Path(temp, 'image.tar')
|
||||
if image_file.is_file():
|
||||
await self.instance.import_image(image_file, version)
|
||||
with suppress(DockerAPIError):
|
||||
await self.instance.import_image(image_file, version)
|
||||
else:
|
||||
if await self.instance.install(version, restore_image):
|
||||
with suppress(DockerAPIError):
|
||||
await self.instance.install(version, restore_image)
|
||||
await self.instance.cleanup()
|
||||
elif self.instance.version != version or self.legacy:
|
||||
_LOGGER.info("Restore/Update image for addon %s", self._id)
|
||||
await self.instance.update(version, restore_image)
|
||||
with suppress(DockerAPIError):
|
||||
await self.instance.update(version, restore_image)
|
||||
else:
|
||||
await self.instance.stop()
|
||||
with suppress(DockerAPIError):
|
||||
await self.instance.stop()
|
||||
|
||||
# Restore data
|
||||
def _restore_data():
|
||||
@@ -983,7 +1124,7 @@ class Addon(CoreSysAttributes):
|
||||
await self.sys_run_in_executor(_restore_data)
|
||||
except shutil.Error as err:
|
||||
_LOGGER.error("Can't restore origin data: %s", err)
|
||||
return False
|
||||
raise AddonsError() from None
|
||||
|
||||
# Restore AppArmor
|
||||
profile_file = Path(temp, 'apparmor.txt')
|
||||
@@ -993,11 +1134,10 @@ class Addon(CoreSysAttributes):
|
||||
self.slug, profile_file)
|
||||
except HostAppArmorError:
|
||||
_LOGGER.error("Can't restore AppArmor profile")
|
||||
return False
|
||||
raise AddonsError() from None
|
||||
|
||||
# Run add-on
|
||||
if data[ATTR_STATE] == STATE_STARTED:
|
||||
return await self.start()
|
||||
|
||||
_LOGGER.info("Finish restore for add-on %s", self._id)
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user