mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-10 10:46:29 +00:00
WIP: Split add-on store logic (#1067)
* Split add-on store logic * finish data model * Cleanup models * Cleanup imports * split up store addons * More cleanup * Go to stable * Fix layout * Cleanup interface * Fix restore/snapshot * Fix algo * Fix reload task * Fix typing / remove indirect add-on references * Fix version * Fix repository data * Fix addon repo * Fix api check * Fix API return * Fix model * Temp fix available * Fix lint * Fix install * Fix partial restore * Fix store restore * Fix ingress port * Fix API * Fix style
This commit is contained in:
parent
4e94043bca
commit
9ce9e10dfd
@ -23,7 +23,6 @@ RUN export MAKEFLAGS="-j$(nproc)" \
|
|||||||
|
|
||||||
# Install HassIO
|
# Install HassIO
|
||||||
COPY . /usr/src/hassio
|
COPY . /usr/src/hassio
|
||||||
RUN pip3 install --no-cache-dir /usr/src/hassio \
|
RUN pip3 install --no-cache-dir -e /usr/src/hassio
|
||||||
&& rm -rf /usr/src/hassio
|
|
||||||
|
|
||||||
CMD [ "python3", "-m", "hassio" ]
|
CMD [ "python3", "-m", "hassio" ]
|
||||||
|
@ -1,158 +1,251 @@
|
|||||||
"""Init file for Hass.io add-ons."""
|
"""Init file for Hass.io add-ons."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from contextlib import suppress
|
||||||
import logging
|
import logging
|
||||||
|
import tarfile
|
||||||
|
from typing import Dict, List, Optional, Union
|
||||||
|
|
||||||
|
from ..const import BOOT_AUTO, STATE_STARTED
|
||||||
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
|
from ..exceptions import (
|
||||||
|
AddonsError,
|
||||||
|
AddonsNotSupportedError,
|
||||||
|
DockerAPIError,
|
||||||
|
HostAppArmorError,
|
||||||
|
)
|
||||||
|
from ..store.addon import AddonStore
|
||||||
from .addon import Addon
|
from .addon import Addon
|
||||||
from .repository import Repository
|
|
||||||
from .data import AddonsData
|
from .data import AddonsData
|
||||||
from ..const import REPOSITORY_CORE, REPOSITORY_LOCAL, BOOT_AUTO, STATE_STARTED
|
|
||||||
from ..coresys import CoreSysAttributes
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
BUILTIN_REPOSITORIES = set((REPOSITORY_CORE, REPOSITORY_LOCAL))
|
AnyAddon = Union[Addon, AddonStore]
|
||||||
|
|
||||||
|
|
||||||
class AddonManager(CoreSysAttributes):
|
class AddonManager(CoreSysAttributes):
|
||||||
"""Manage add-ons inside Hass.io."""
|
"""Manage add-ons inside Hass.io."""
|
||||||
|
|
||||||
def __init__(self, coresys):
|
def __init__(self, coresys: CoreSys):
|
||||||
"""Initialize Docker base wrapper."""
|
"""Initialize Docker base wrapper."""
|
||||||
self.coresys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self.data = AddonsData(coresys)
|
self.data: AddonsData = AddonsData(coresys)
|
||||||
self.addons_obj = {}
|
self.local: Dict[str, Addon] = {}
|
||||||
self.repositories_obj = {}
|
self.store: Dict[str, AddonStore] = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def list_addons(self):
|
def all(self) -> List[AnyAddon]:
|
||||||
"""Return a list of all add-ons."""
|
"""Return a list of all add-ons."""
|
||||||
return list(self.addons_obj.values())
|
addons = {**self.store, **self.local}
|
||||||
|
return list(addons.values())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def list_installed(self):
|
def installed(self) -> List[Addon]:
|
||||||
"""Return a list of installed add-ons."""
|
"""Return a list of all installed add-ons."""
|
||||||
return [addon for addon in self.addons_obj.values()
|
return list(self.local.values())
|
||||||
if addon.is_installed]
|
|
||||||
|
|
||||||
@property
|
def get(self, addon_slug: str) -> Optional[AnyAddon]:
|
||||||
def list_repositories(self):
|
"""Return an add-on from slug.
|
||||||
"""Return list of add-on repositories."""
|
|
||||||
return list(self.repositories_obj.values())
|
|
||||||
|
|
||||||
def get(self, addon_slug):
|
Prio:
|
||||||
"""Return an add-on from slug."""
|
1 - Local
|
||||||
return self.addons_obj.get(addon_slug)
|
2 - Store
|
||||||
|
"""
|
||||||
|
if addon_slug in self.local:
|
||||||
|
return self.local[addon_slug]
|
||||||
|
return self.store.get(addon_slug)
|
||||||
|
|
||||||
def from_token(self, token):
|
def from_token(self, token: str) -> Optional[Addon]:
|
||||||
"""Return an add-on from Hass.io token."""
|
"""Return an add-on from Hass.io token."""
|
||||||
for addon in self.list_addons:
|
for addon in self.installed:
|
||||||
if addon.is_installed and token == addon.hassio_token:
|
if token == addon.hassio_token:
|
||||||
return addon
|
return addon
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def load(self):
|
async def load(self) -> None:
|
||||||
"""Start up add-on management."""
|
"""Start up add-on management."""
|
||||||
self.data.reload()
|
|
||||||
|
|
||||||
# Init Hass.io built-in repositories
|
|
||||||
repositories = \
|
|
||||||
set(self.sys_config.addons_repositories) | BUILTIN_REPOSITORIES
|
|
||||||
|
|
||||||
# Init custom repositories and load add-ons
|
|
||||||
await self.load_repositories(repositories)
|
|
||||||
|
|
||||||
async def reload(self):
|
|
||||||
"""Update add-ons from repository and reload list."""
|
|
||||||
tasks = [repository.update() for repository in
|
|
||||||
self.repositories_obj.values()]
|
|
||||||
if tasks:
|
|
||||||
await asyncio.wait(tasks)
|
|
||||||
|
|
||||||
# read data from repositories
|
|
||||||
self.data.reload()
|
|
||||||
|
|
||||||
# update addons
|
|
||||||
await self.load_addons()
|
|
||||||
|
|
||||||
async def load_repositories(self, list_repositories):
|
|
||||||
"""Add a new custom repository."""
|
|
||||||
new_rep = set(list_repositories)
|
|
||||||
old_rep = set(self.repositories_obj)
|
|
||||||
|
|
||||||
# add new repository
|
|
||||||
async def _add_repository(url):
|
|
||||||
"""Helper function to async add repository."""
|
|
||||||
repository = Repository(self.coresys, url)
|
|
||||||
if not await repository.load():
|
|
||||||
_LOGGER.error("Can't load from repository %s", url)
|
|
||||||
return
|
|
||||||
self.repositories_obj[url] = repository
|
|
||||||
|
|
||||||
# don't add built-in repository to config
|
|
||||||
if url not in BUILTIN_REPOSITORIES:
|
|
||||||
self.sys_config.add_addon_repository(url)
|
|
||||||
|
|
||||||
tasks = [_add_repository(url) for url in new_rep - old_rep]
|
|
||||||
if tasks:
|
|
||||||
await asyncio.wait(tasks)
|
|
||||||
|
|
||||||
# del new repository
|
|
||||||
for url in old_rep - new_rep - BUILTIN_REPOSITORIES:
|
|
||||||
self.repositories_obj.pop(url).remove()
|
|
||||||
self.sys_config.drop_addon_repository(url)
|
|
||||||
|
|
||||||
# update data
|
|
||||||
self.data.reload()
|
|
||||||
await self.load_addons()
|
|
||||||
|
|
||||||
async def load_addons(self):
|
|
||||||
"""Update/add internal add-on store."""
|
|
||||||
all_addons = set(self.data.system) | set(self.data.cache)
|
|
||||||
|
|
||||||
# calc diff
|
|
||||||
add_addons = all_addons - set(self.addons_obj)
|
|
||||||
del_addons = set(self.addons_obj) - all_addons
|
|
||||||
|
|
||||||
_LOGGER.info("Load add-ons: %d all - %d new - %d remove",
|
|
||||||
len(all_addons), len(add_addons), len(del_addons))
|
|
||||||
|
|
||||||
# new addons
|
|
||||||
tasks = []
|
tasks = []
|
||||||
for addon_slug in add_addons:
|
for slug in self.data.system:
|
||||||
addon = Addon(self.coresys, addon_slug)
|
addon = self.local[slug] = Addon(self.coresys, slug)
|
||||||
|
|
||||||
tasks.append(addon.load())
|
tasks.append(addon.load())
|
||||||
self.addons_obj[addon_slug] = addon
|
|
||||||
|
|
||||||
|
# Run initial tasks
|
||||||
|
_LOGGER.info("Found %d installed add-ons", len(tasks))
|
||||||
if tasks:
|
if tasks:
|
||||||
await asyncio.wait(tasks)
|
await asyncio.wait(tasks)
|
||||||
|
|
||||||
# remove
|
async def boot(self, stage: str) -> None:
|
||||||
for addon_slug in del_addons:
|
|
||||||
self.addons_obj.pop(addon_slug)
|
|
||||||
|
|
||||||
async def boot(self, stage):
|
|
||||||
"""Boot add-ons with mode auto."""
|
"""Boot add-ons with mode auto."""
|
||||||
tasks = []
|
tasks = []
|
||||||
for addon in self.addons_obj.values():
|
for addon in self.installed:
|
||||||
if addon.is_installed and addon.boot == BOOT_AUTO and \
|
if addon.boot != BOOT_AUTO or addon.startup != stage:
|
||||||
addon.startup == stage:
|
continue
|
||||||
tasks.append(addon.start())
|
tasks.append(addon.start())
|
||||||
|
|
||||||
_LOGGER.info("Startup %s run %d add-ons", stage, len(tasks))
|
_LOGGER.info("Phase '%s' start %d add-ons", stage, len(tasks))
|
||||||
if tasks:
|
if tasks:
|
||||||
await asyncio.wait(tasks)
|
await asyncio.wait(tasks)
|
||||||
await asyncio.sleep(self.sys_config.wait_boot)
|
await asyncio.sleep(self.sys_config.wait_boot)
|
||||||
|
|
||||||
async def shutdown(self, stage):
|
async def shutdown(self, stage: str) -> None:
|
||||||
"""Shutdown addons."""
|
"""Shutdown addons."""
|
||||||
tasks = []
|
tasks = []
|
||||||
for addon in self.addons_obj.values():
|
for addon in self.installed:
|
||||||
if addon.is_installed and \
|
if await addon.state() != STATE_STARTED or addon.startup != stage:
|
||||||
await addon.state() == STATE_STARTED and \
|
continue
|
||||||
addon.startup == stage:
|
|
||||||
tasks.append(addon.stop())
|
tasks.append(addon.stop())
|
||||||
|
|
||||||
_LOGGER.info("Shutdown %s stop %d add-ons", stage, len(tasks))
|
_LOGGER.info("Phase '%s' stop %d add-ons", stage, len(tasks))
|
||||||
if tasks:
|
if tasks:
|
||||||
await asyncio.wait(tasks)
|
await asyncio.wait(tasks)
|
||||||
|
|
||||||
|
async def install(self, slug: str) -> None:
|
||||||
|
"""Install an add-on."""
|
||||||
|
if slug in self.local:
|
||||||
|
_LOGGER.warning("Add-on %s is already installed", slug)
|
||||||
|
return
|
||||||
|
store = self.store.get(slug)
|
||||||
|
|
||||||
|
if not store:
|
||||||
|
_LOGGER.error("Add-on %s not exists", slug)
|
||||||
|
raise AddonsError()
|
||||||
|
|
||||||
|
if not store.available:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Add-on %s not supported on that platform", slug)
|
||||||
|
raise AddonsNotSupportedError()
|
||||||
|
|
||||||
|
self.data.install(store)
|
||||||
|
addon = Addon(self.coresys, slug)
|
||||||
|
|
||||||
|
if not addon.path_data.is_dir():
|
||||||
|
_LOGGER.info(
|
||||||
|
"Create Home Assistant add-on data folder %s", addon.path_data)
|
||||||
|
addon.path_data.mkdir()
|
||||||
|
|
||||||
|
# Setup/Fix AppArmor profile
|
||||||
|
await addon.install_apparmor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await addon.instance.install(store.version, store.image)
|
||||||
|
except DockerAPIError:
|
||||||
|
self.data.uninstall(addon)
|
||||||
|
raise AddonsError() from None
|
||||||
|
else:
|
||||||
|
self.local[slug] = addon
|
||||||
|
|
||||||
|
async def uninstall(self, slug: str) -> None:
|
||||||
|
"""Remove an add-on."""
|
||||||
|
if slug not in self.local:
|
||||||
|
_LOGGER.warning("Add-on %s is not installed", slug)
|
||||||
|
return
|
||||||
|
addon = self.local.get(slug)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await addon.instance.remove()
|
||||||
|
except DockerAPIError:
|
||||||
|
raise AddonsError() from None
|
||||||
|
|
||||||
|
await addon.remove_data()
|
||||||
|
|
||||||
|
# Cleanup audio settings
|
||||||
|
if addon.path_asound.exists():
|
||||||
|
with suppress(OSError):
|
||||||
|
addon.path_asound.unlink()
|
||||||
|
|
||||||
|
# Cleanup AppArmor profile
|
||||||
|
with suppress(HostAppArmorError):
|
||||||
|
await addon.uninstall_apparmor()
|
||||||
|
|
||||||
|
# Cleanup internal data
|
||||||
|
addon.remove_discovery()
|
||||||
|
self.data.uninstall(addon)
|
||||||
|
self.local.pop(slug)
|
||||||
|
|
||||||
|
async def update(self, slug: str) -> None:
|
||||||
|
"""Update add-on."""
|
||||||
|
if slug not in self.local:
|
||||||
|
_LOGGER.error("Add-on %s is not installed", slug)
|
||||||
|
raise AddonsError()
|
||||||
|
addon = self.local.get(slug)
|
||||||
|
|
||||||
|
if addon.is_detached:
|
||||||
|
_LOGGER.error("Add-on %s is not available inside store", slug)
|
||||||
|
raise AddonsError()
|
||||||
|
store = self.store.get(slug)
|
||||||
|
|
||||||
|
if addon.version == store.version:
|
||||||
|
_LOGGER.warning("No update available for add-on %s", slug)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if available, Maybe something have changed
|
||||||
|
if not store.available:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Add-on %s not supported on that platform", slug)
|
||||||
|
raise AddonsNotSupportedError()
|
||||||
|
|
||||||
|
# Update instance
|
||||||
|
last_state = await addon.state()
|
||||||
|
try:
|
||||||
|
await addon.instance.update(store.version, store.image)
|
||||||
|
except DockerAPIError:
|
||||||
|
raise AddonsError() from None
|
||||||
|
self.data.update(store)
|
||||||
|
|
||||||
|
# Setup/Fix AppArmor profile
|
||||||
|
await addon.install_apparmor()
|
||||||
|
|
||||||
|
# restore state
|
||||||
|
if last_state == STATE_STARTED:
|
||||||
|
await addon.start()
|
||||||
|
|
||||||
|
async def rebuild(self, slug: str) -> None:
|
||||||
|
"""Perform a rebuild of local build add-on."""
|
||||||
|
if slug not in self.local:
|
||||||
|
_LOGGER.error("Add-on %s is not installed", slug)
|
||||||
|
raise AddonsError()
|
||||||
|
addon = self.local.get(slug)
|
||||||
|
|
||||||
|
if addon.is_detached:
|
||||||
|
_LOGGER.error("Add-on %s is not available inside store", slug)
|
||||||
|
raise AddonsError()
|
||||||
|
store = self.store.get(slug)
|
||||||
|
|
||||||
|
# Check if a rebuild is possible now
|
||||||
|
if addon.version != store.version:
|
||||||
|
_LOGGER.error("Version changed, use Update instead Rebuild")
|
||||||
|
raise AddonsError()
|
||||||
|
if not addon.need_build:
|
||||||
|
_LOGGER.error("Can't rebuild a image based add-on")
|
||||||
|
raise AddonsNotSupportedError()
|
||||||
|
|
||||||
|
# remove docker container but not addon config
|
||||||
|
last_state = await addon.state()
|
||||||
|
try:
|
||||||
|
await addon.instance.remove()
|
||||||
|
await addon.instance.install(addon.version)
|
||||||
|
except DockerAPIError:
|
||||||
|
raise AddonsError() from None
|
||||||
|
else:
|
||||||
|
self.data.update(store)
|
||||||
|
|
||||||
|
# restore state
|
||||||
|
if last_state == STATE_STARTED:
|
||||||
|
await addon.start()
|
||||||
|
|
||||||
|
async def restore(self, slug: str, tar_file: tarfile.TarFile) -> None:
|
||||||
|
"""Restore state of an add-on."""
|
||||||
|
if slug not in self.local:
|
||||||
|
_LOGGER.debug("Add-on %s is not local available for restore")
|
||||||
|
addon = Addon(self.coresys, slug)
|
||||||
|
else:
|
||||||
|
_LOGGER.debug("Add-on %s is local available for restore")
|
||||||
|
addon = self.local[slug]
|
||||||
|
|
||||||
|
await addon.restore(tar_file)
|
||||||
|
|
||||||
|
# Check if new
|
||||||
|
if slug in self.local:
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.info("Detect new Add-on after restore %s", slug)
|
||||||
|
self.local[slug] = addon
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -9,27 +9,23 @@ from ..utils.json import JsonConfig
|
|||||||
from .validate import SCHEMA_BUILD_CONFIG
|
from .validate import SCHEMA_BUILD_CONFIG
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .addon import Addon
|
from . import AnyAddon
|
||||||
|
|
||||||
|
|
||||||
class AddonBuild(JsonConfig, CoreSysAttributes):
|
class AddonBuild(JsonConfig, CoreSysAttributes):
|
||||||
"""Handle build options for add-ons."""
|
"""Handle build options for add-ons."""
|
||||||
|
|
||||||
def __init__(self, coresys: CoreSys, slug: str) -> None:
|
def __init__(self, coresys: CoreSys, addon: AnyAddon) -> None:
|
||||||
"""Initialize Hass.io add-on builder."""
|
"""Initialize Hass.io add-on builder."""
|
||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self._id: str = slug
|
self.addon = addon
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
Path(self.addon.path_location, 'build.json'), SCHEMA_BUILD_CONFIG)
|
Path(self.addon.path_location, 'build.json'), SCHEMA_BUILD_CONFIG)
|
||||||
|
|
||||||
def save_data(self):
|
def save_data(self):
|
||||||
"""Ignore save function."""
|
"""Ignore save function."""
|
||||||
|
raise RuntimeError()
|
||||||
@property
|
|
||||||
def addon(self) -> Addon:
|
|
||||||
"""Return add-on of build data."""
|
|
||||||
return self.sys_addons.get(self._id)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_image(self) -> str:
|
def base_image(self) -> str:
|
||||||
|
@ -1,38 +1,34 @@
|
|||||||
"""Init file for Hass.io add-on data."""
|
"""Init file for Hass.io add-on data."""
|
||||||
|
from copy import deepcopy
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from typing import Any, Dict
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
from voluptuous.humanize import humanize_error
|
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_LOCATON,
|
ATTR_IMAGE,
|
||||||
ATTR_REPOSITORY,
|
ATTR_OPTIONS,
|
||||||
ATTR_SLUG,
|
|
||||||
ATTR_SYSTEM,
|
ATTR_SYSTEM,
|
||||||
ATTR_USER,
|
ATTR_USER,
|
||||||
|
ATTR_VERSION,
|
||||||
FILE_HASSIO_ADDONS,
|
FILE_HASSIO_ADDONS,
|
||||||
REPOSITORY_CORE,
|
|
||||||
REPOSITORY_LOCAL,
|
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import JsonFileError
|
from ..utils.json import JsonConfig
|
||||||
from ..utils.json import JsonConfig, read_json_file
|
from ..store.addon import AddonStore
|
||||||
from .utils import extract_hash_from_path
|
from .addon import Addon
|
||||||
from .validate import SCHEMA_ADDON_CONFIG, SCHEMA_ADDONS_FILE, SCHEMA_REPOSITORY_CONFIG
|
from .validate import SCHEMA_ADDONS_FILE
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
Config = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
class AddonsData(JsonConfig, CoreSysAttributes):
|
class AddonsData(JsonConfig, CoreSysAttributes):
|
||||||
"""Hold data for Add-ons inside Hass.io."""
|
"""Hold data for installed Add-ons inside Hass.io."""
|
||||||
|
|
||||||
def __init__(self, coresys):
|
def __init__(self, coresys: CoreSys):
|
||||||
"""Initialize data holder."""
|
"""Initialize data holder."""
|
||||||
super().__init__(FILE_HASSIO_ADDONS, SCHEMA_ADDONS_FILE)
|
super().__init__(FILE_HASSIO_ADDONS, SCHEMA_ADDONS_FILE)
|
||||||
self.coresys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self._repositories = {}
|
|
||||||
self._cache = {}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user(self):
|
def user(self):
|
||||||
@ -44,93 +40,35 @@ class AddonsData(JsonConfig, CoreSysAttributes):
|
|||||||
"""Return local add-on data."""
|
"""Return local add-on data."""
|
||||||
return self._data[ATTR_SYSTEM]
|
return self._data[ATTR_SYSTEM]
|
||||||
|
|
||||||
@property
|
def install(self, addon: AddonStore) -> None:
|
||||||
def cache(self):
|
"""Set addon as installed."""
|
||||||
"""Return add-on data from cache/repositories."""
|
self.system[addon.slug] = deepcopy(addon.data)
|
||||||
return self._cache
|
self.user[addon.slug] = {
|
||||||
|
ATTR_OPTIONS: {},
|
||||||
|
ATTR_VERSION: addon.version,
|
||||||
|
ATTR_IMAGE: addon.image,
|
||||||
|
}
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
@property
|
def uninstall(self, addon: Addon) -> None:
|
||||||
def repositories(self):
|
"""Set add-on as uninstalled."""
|
||||||
"""Return add-on data from repositories."""
|
self.system.pop(addon.slug, None)
|
||||||
return self._repositories
|
self.user.pop(addon.slug, None)
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
def reload(self):
|
def update(self, addon: AddonStore) -> None:
|
||||||
"""Read data from add-on repository."""
|
"""Update version of add-on."""
|
||||||
self._cache = {}
|
self.system[addon.slug] = deepcopy(addon.data)
|
||||||
self._repositories = {}
|
self.user[addon.slug].update({
|
||||||
|
ATTR_VERSION: addon.version,
|
||||||
|
ATTR_IMAGE: addon.image,
|
||||||
|
})
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
# read core repository
|
def restore(self, slug: str, user: Config, system: Config, image: str) -> None:
|
||||||
self._read_addons_folder(self.sys_config.path_addons_core, REPOSITORY_CORE)
|
"""Restore data to add-on."""
|
||||||
|
self.user[slug] = deepcopy(user)
|
||||||
|
self.system[slug] = deepcopy(system)
|
||||||
|
|
||||||
# read local repository
|
self.user[slug][ATTR_IMAGE] = image
|
||||||
self._read_addons_folder(self.sys_config.path_addons_local, REPOSITORY_LOCAL)
|
self.save_data()
|
||||||
|
|
||||||
# add built-in repositories information
|
|
||||||
self._set_builtin_repositories()
|
|
||||||
|
|
||||||
# read custom git repositories
|
|
||||||
for repository_element in self.sys_config.path_addons_git.iterdir():
|
|
||||||
if repository_element.is_dir():
|
|
||||||
self._read_git_repository(repository_element)
|
|
||||||
|
|
||||||
def _read_git_repository(self, path):
|
|
||||||
"""Process a custom repository folder."""
|
|
||||||
slug = extract_hash_from_path(path)
|
|
||||||
|
|
||||||
# exists repository json
|
|
||||||
repository_file = Path(path, "repository.json")
|
|
||||||
try:
|
|
||||||
repository_info = SCHEMA_REPOSITORY_CONFIG(read_json_file(repository_file))
|
|
||||||
except JsonFileError:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Can't read repository information from %s", repository_file
|
|
||||||
)
|
|
||||||
return
|
|
||||||
except vol.Invalid:
|
|
||||||
_LOGGER.warning("Repository parse error %s", repository_file)
|
|
||||||
return
|
|
||||||
|
|
||||||
# process data
|
|
||||||
self._repositories[slug] = repository_info
|
|
||||||
self._read_addons_folder(path, slug)
|
|
||||||
|
|
||||||
def _read_addons_folder(self, path, repository):
|
|
||||||
"""Read data from add-ons folder."""
|
|
||||||
for addon in path.glob("**/config.json"):
|
|
||||||
try:
|
|
||||||
addon_config = read_json_file(addon)
|
|
||||||
except JsonFileError:
|
|
||||||
_LOGGER.warning("Can't read %s from repository %s", addon, repository)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# validate
|
|
||||||
try:
|
|
||||||
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
|
|
||||||
except vol.Invalid as ex:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Can't read %s: %s", addon, humanize_error(addon_config, ex)
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Generate slug
|
|
||||||
addon_slug = "{}_{}".format(repository, addon_config[ATTR_SLUG])
|
|
||||||
|
|
||||||
# store
|
|
||||||
addon_config[ATTR_REPOSITORY] = repository
|
|
||||||
addon_config[ATTR_LOCATON] = str(addon.parent)
|
|
||||||
self._cache[addon_slug] = addon_config
|
|
||||||
|
|
||||||
def _set_builtin_repositories(self):
|
|
||||||
"""Add local built-in repository into dataset."""
|
|
||||||
try:
|
|
||||||
builtin_file = Path(__file__).parent.joinpath("built-in.json")
|
|
||||||
builtin_data = read_json_file(builtin_file)
|
|
||||||
except JsonFileError:
|
|
||||||
_LOGGER.warning("Can't read built-in json")
|
|
||||||
return
|
|
||||||
|
|
||||||
# core repository
|
|
||||||
self._repositories[REPOSITORY_CORE] = builtin_data[REPOSITORY_CORE]
|
|
||||||
|
|
||||||
# local repository
|
|
||||||
self._repositories[REPOSITORY_LOCAL] = builtin_data[REPOSITORY_LOCAL]
|
|
||||||
|
498
hassio/addons/model.py
Normal file
498
hassio/addons/model.py
Normal file
@ -0,0 +1,498 @@
|
|||||||
|
"""Init file for Hass.io add-ons."""
|
||||||
|
from distutils.version import StrictVersion
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Awaitable, Dict, List, Optional
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from ..const import (
|
||||||
|
ATTR_APPARMOR,
|
||||||
|
ATTR_ARCH,
|
||||||
|
ATTR_AUDIO,
|
||||||
|
ATTR_AUTH_API,
|
||||||
|
ATTR_AUTO_UART,
|
||||||
|
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_KERNEL_MODULES,
|
||||||
|
ATTR_LEGACY,
|
||||||
|
ATTR_LOCATON,
|
||||||
|
ATTR_MACHINE,
|
||||||
|
ATTR_MAP,
|
||||||
|
ATTR_NAME,
|
||||||
|
ATTR_OPTIONS,
|
||||||
|
ATTR_PANEL_ADMIN,
|
||||||
|
ATTR_PANEL_ICON,
|
||||||
|
ATTR_PANEL_TITLE,
|
||||||
|
ATTR_PORTS,
|
||||||
|
ATTR_PORTS_DESCRIPTION,
|
||||||
|
ATTR_PRIVILEGED,
|
||||||
|
ATTR_REPOSITORY,
|
||||||
|
ATTR_SCHEMA,
|
||||||
|
ATTR_SERVICES,
|
||||||
|
ATTR_SLUG,
|
||||||
|
ATTR_STARTUP,
|
||||||
|
ATTR_STDIN,
|
||||||
|
ATTR_TIMEOUT,
|
||||||
|
ATTR_TMPFS,
|
||||||
|
ATTR_URL,
|
||||||
|
ATTR_VERSION,
|
||||||
|
ATTR_WEBUI,
|
||||||
|
SECURITY_DEFAULT,
|
||||||
|
SECURITY_DISABLE,
|
||||||
|
SECURITY_PROFILE,
|
||||||
|
)
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from .validate import MACHINE_ALL, RE_SERVICE, RE_VOLUME, validate_options
|
||||||
|
|
||||||
|
Data = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class AddonModel(CoreSysAttributes):
|
||||||
|
"""Add-on Data layout."""
|
||||||
|
|
||||||
|
slug: str = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self) -> Data:
|
||||||
|
"""Return Add-on config/data."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_installed(self) -> bool:
|
||||||
|
"""Return True if an add-on is installed."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_detached(self) -> bool:
|
||||||
|
"""Return True if add-on is detached."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if this add-on is available on this platform."""
|
||||||
|
return self._available(self.data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self) -> Dict[str, Any]:
|
||||||
|
"""Return options with local changes."""
|
||||||
|
return self.data[ATTR_OPTIONS]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def boot(self) -> bool:
|
||||||
|
"""Return boot config with prio local settings."""
|
||||||
|
return self.data[ATTR_BOOT]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auto_update(self) -> Optional[bool]:
|
||||||
|
"""Return if auto update is enable."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return name of add-on."""
|
||||||
|
return self.data[ATTR_NAME]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timeout(self) -> int:
|
||||||
|
"""Return timeout of addon for docker stop."""
|
||||||
|
return self.data[ATTR_TIMEOUT]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uuid(self) -> Optional[str]:
|
||||||
|
"""Return an API token for this add-on."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hassio_token(self) -> Optional[str]:
|
||||||
|
"""Return access token for Hass.io API."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ingress_token(self) -> Optional[str]:
|
||||||
|
"""Return access token for Hass.io API."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ingress_entry(self) -> Optional[str]:
|
||||||
|
"""Return ingress external URL."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
"""Return description of add-on."""
|
||||||
|
return self.data[ATTR_DESCRIPTON]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def long_description(self) -> Optional[str]:
|
||||||
|
"""Return README.md as long_description."""
|
||||||
|
readme = Path(self.path_location, 'README.md')
|
||||||
|
|
||||||
|
# If readme not exists
|
||||||
|
if not readme.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Return data
|
||||||
|
with readme.open('r') as readme_file:
|
||||||
|
return readme_file.read()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def repository(self) -> str:
|
||||||
|
"""Return repository of add-on."""
|
||||||
|
return self.data[ATTR_REPOSITORY]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_version(self) -> str:
|
||||||
|
"""Return latest version of add-on."""
|
||||||
|
return self.data[ATTR_VERSION]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self) -> str:
|
||||||
|
"""Return version of add-on."""
|
||||||
|
return self.data[ATTR_VERSION]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def protected(self) -> bool:
|
||||||
|
"""Return if add-on is in protected mode."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def startup(self) -> Optional[str]:
|
||||||
|
"""Return startup type of add-on."""
|
||||||
|
return self.data.get(ATTR_STARTUP)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def services_role(self) -> Dict[str, str]:
|
||||||
|
"""Return dict of services with rights."""
|
||||||
|
services_list = self.data.get(ATTR_SERVICES, [])
|
||||||
|
|
||||||
|
services = {}
|
||||||
|
for data in services_list:
|
||||||
|
service = RE_SERVICE.match(data)
|
||||||
|
services[service.group('service')] = service.group('rights')
|
||||||
|
|
||||||
|
return services
|
||||||
|
|
||||||
|
@property
|
||||||
|
def discovery(self) -> List[str]:
|
||||||
|
"""Return list of discoverable components/platforms."""
|
||||||
|
return self.data.get(ATTR_DISCOVERY, [])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ports_description(self) -> Optional[Dict[str, str]]:
|
||||||
|
"""Return descriptions of ports."""
|
||||||
|
return self.data.get(ATTR_PORTS_DESCRIPTION)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ports(self) -> Optional[Dict[str, Optional[int]]]:
|
||||||
|
"""Return ports of add-on."""
|
||||||
|
return self.data.get(ATTR_PORTS)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ingress_url(self) -> Optional[str]:
|
||||||
|
"""Return URL to ingress url."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def webui(self) -> Optional[str]:
|
||||||
|
"""Return URL to webui or None."""
|
||||||
|
return self.data.get(ATTR_WEBUI)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ingress_port(self) -> Optional[int]:
|
||||||
|
"""Return Ingress port."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def panel_icon(self) -> str:
|
||||||
|
"""Return panel icon for Ingress frame."""
|
||||||
|
return self.data[ATTR_PANEL_ICON]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def panel_title(self) -> str:
|
||||||
|
"""Return panel icon for Ingress frame."""
|
||||||
|
return self.data.get(ATTR_PANEL_TITLE, self.name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def panel_admin(self) -> str:
|
||||||
|
"""Return panel icon for Ingress frame."""
|
||||||
|
return self.data[ATTR_PANEL_ADMIN]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host_network(self) -> bool:
|
||||||
|
"""Return True if add-on run on host network."""
|
||||||
|
return self.data[ATTR_HOST_NETWORK]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host_pid(self) -> bool:
|
||||||
|
"""Return True if add-on run on host PID namespace."""
|
||||||
|
return self.data[ATTR_HOST_PID]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host_ipc(self) -> bool:
|
||||||
|
"""Return True if add-on run on host IPC namespace."""
|
||||||
|
return self.data[ATTR_HOST_IPC]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host_dbus(self) -> bool:
|
||||||
|
"""Return True if add-on run on host D-BUS."""
|
||||||
|
return self.data[ATTR_HOST_DBUS]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def devices(self) -> Optional[List[str]]:
|
||||||
|
"""Return devices of add-on."""
|
||||||
|
return self.data.get(ATTR_DEVICES)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auto_uart(self) -> bool:
|
||||||
|
"""Return True if we should map all UART device."""
|
||||||
|
return self.data[ATTR_AUTO_UART]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tmpfs(self) -> Optional[str]:
|
||||||
|
"""Return tmpfs of add-on."""
|
||||||
|
return self.data.get(ATTR_TMPFS)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def environment(self) -> Optional[Dict[str, str]]:
|
||||||
|
"""Return environment of add-on."""
|
||||||
|
return self.data.get(ATTR_ENVIRONMENT)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def privileged(self) -> List[str]:
|
||||||
|
"""Return list of privilege."""
|
||||||
|
return self.data.get(ATTR_PRIVILEGED, [])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def apparmor(self) -> str:
|
||||||
|
"""Return True if AppArmor is enabled."""
|
||||||
|
if not self.data.get(ATTR_APPARMOR):
|
||||||
|
return SECURITY_DISABLE
|
||||||
|
elif self.sys_host.apparmor.exists(self.slug):
|
||||||
|
return SECURITY_PROFILE
|
||||||
|
return SECURITY_DEFAULT
|
||||||
|
|
||||||
|
@property
|
||||||
|
def legacy(self) -> bool:
|
||||||
|
"""Return if the add-on don't support Home Assistant labels."""
|
||||||
|
return self.data[ATTR_LEGACY]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def access_docker_api(self) -> bool:
|
||||||
|
"""Return if the add-on need read-only Docker API access."""
|
||||||
|
return self.data[ATTR_DOCKER_API]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def access_hassio_api(self) -> bool:
|
||||||
|
"""Return True if the add-on access to Hass.io REASTful API."""
|
||||||
|
return self.data[ATTR_HASSIO_API]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def access_homeassistant_api(self) -> bool:
|
||||||
|
"""Return True if the add-on access to Home Assistant API proxy."""
|
||||||
|
return self.data[ATTR_HOMEASSISTANT_API]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hassio_role(self) -> str:
|
||||||
|
"""Return Hass.io role for API."""
|
||||||
|
return self.data[ATTR_HASSIO_ROLE]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_stdin(self) -> bool:
|
||||||
|
"""Return True if the add-on access use stdin input."""
|
||||||
|
return self.data[ATTR_STDIN]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_ingress(self) -> bool:
|
||||||
|
"""Return True if the add-on access support ingress."""
|
||||||
|
return self.data[ATTR_INGRESS]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ingress_panel(self) -> Optional[bool]:
|
||||||
|
"""Return True if the add-on access support ingress."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_gpio(self) -> bool:
|
||||||
|
"""Return True if the add-on access to GPIO interface."""
|
||||||
|
return self.data[ATTR_GPIO]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_kernel_modules(self) -> bool:
|
||||||
|
"""Return True if the add-on access to kernel modules."""
|
||||||
|
return self.data[ATTR_KERNEL_MODULES]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_full_access(self) -> bool:
|
||||||
|
"""Return True if the add-on want full access to hardware."""
|
||||||
|
return self.data[ATTR_FULL_ACCESS]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_devicetree(self) -> bool:
|
||||||
|
"""Return True if the add-on read access to devicetree."""
|
||||||
|
return self.data[ATTR_DEVICETREE]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def access_auth_api(self) -> bool:
|
||||||
|
"""Return True if the add-on access to login/auth backend."""
|
||||||
|
return self.data[ATTR_AUTH_API]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_audio(self) -> bool:
|
||||||
|
"""Return True if the add-on access to audio."""
|
||||||
|
return self.data[ATTR_AUDIO]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def homeassistant_version(self) -> Optional[str]:
|
||||||
|
"""Return min Home Assistant version they needed by Add-on."""
|
||||||
|
return self.data.get(ATTR_HOMEASSISTANT)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self) -> Optional[str]:
|
||||||
|
"""Return URL of add-on."""
|
||||||
|
return self.data.get(ATTR_URL)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_icon(self) -> bool:
|
||||||
|
"""Return True if an icon exists."""
|
||||||
|
return self.path_icon.exists()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_logo(self) -> bool:
|
||||||
|
"""Return True if a logo exists."""
|
||||||
|
return self.path_logo.exists()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_changelog(self) -> bool:
|
||||||
|
"""Return True if a changelog exists."""
|
||||||
|
return self.path_changelog.exists()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_arch(self) -> List[str]:
|
||||||
|
"""Return list of supported arch."""
|
||||||
|
return self.data[ATTR_ARCH]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_machine(self) -> List[str]:
|
||||||
|
"""Return list of supported machine."""
|
||||||
|
return self.data.get(ATTR_MACHINE, MACHINE_ALL)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def image(self) -> str:
|
||||||
|
"""Generate image name from data."""
|
||||||
|
return self._image(self.data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def need_build(self) -> bool:
|
||||||
|
"""Return True if this add-on need a local build."""
|
||||||
|
return ATTR_IMAGE not in self.data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def map_volumes(self) -> Dict[str, str]:
|
||||||
|
"""Return a dict of {volume: policy} from add-on."""
|
||||||
|
volumes = {}
|
||||||
|
for volume in self.data[ATTR_MAP]:
|
||||||
|
result = RE_VOLUME.match(volume)
|
||||||
|
volumes[result.group(1)] = result.group(2) or 'ro'
|
||||||
|
|
||||||
|
return volumes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_location(self) -> Path:
|
||||||
|
"""Return path to this add-on."""
|
||||||
|
return Path(self.data[ATTR_LOCATON])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_icon(self) -> Path:
|
||||||
|
"""Return path to add-on icon."""
|
||||||
|
return Path(self.path_location, 'icon.png')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_logo(self) -> Path:
|
||||||
|
"""Return path to add-on logo."""
|
||||||
|
return Path(self.path_location, 'logo.png')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_changelog(self) -> Path:
|
||||||
|
"""Return path to add-on changelog."""
|
||||||
|
return Path(self.path_location, 'CHANGELOG.md')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_apparmor(self) -> Path:
|
||||||
|
"""Return path to custom AppArmor profile."""
|
||||||
|
return Path(self.path_location, 'apparmor.txt')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def schema(self) -> vol.Schema:
|
||||||
|
"""Create a schema for add-on options."""
|
||||||
|
raw_schema = self.data[ATTR_SCHEMA]
|
||||||
|
|
||||||
|
if isinstance(raw_schema, bool):
|
||||||
|
return vol.Schema(dict)
|
||||||
|
return vol.Schema(vol.All(dict, validate_options(raw_schema)))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
"""Compaired add-on objects."""
|
||||||
|
if self.slug == other.slug:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _available(self, config) -> bool:
|
||||||
|
"""Return True if this add-on is available on this platform."""
|
||||||
|
# Architecture
|
||||||
|
if not self.sys_arch.is_supported(config[ATTR_ARCH]):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Machine / Hardware
|
||||||
|
machine = config.get(ATTR_MACHINE) or MACHINE_ALL
|
||||||
|
if self.sys_machine not in machine:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Home Assistant
|
||||||
|
version = config.get(ATTR_HOMEASSISTANT) or self.sys_homeassistant.version
|
||||||
|
if StrictVersion(self.sys_homeassistant.version) < StrictVersion(version):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _image(self, config) -> str:
|
||||||
|
"""Generate image name from data."""
|
||||||
|
# Repository with Dockerhub images
|
||||||
|
if ATTR_IMAGE in config:
|
||||||
|
arch = self.sys_arch.match(config[ATTR_ARCH])
|
||||||
|
return config[ATTR_IMAGE].format(arch=arch)
|
||||||
|
|
||||||
|
# local build
|
||||||
|
return f"{config[ATTR_REPOSITORY]}/{self.sys_arch.default}-addon-{config[ATTR_SLUG]}"
|
||||||
|
|
||||||
|
def install(self) -> Awaitable[None]:
|
||||||
|
"""Install this add-on."""
|
||||||
|
return self.sys_addons.install(self.slug)
|
||||||
|
|
||||||
|
def uninstall(self) -> Awaitable[None]:
|
||||||
|
"""Uninstall this add-on."""
|
||||||
|
return self.sys_addons.uninstall(self.slug)
|
||||||
|
|
||||||
|
def update(self) -> Awaitable[None]:
|
||||||
|
"""Update this add-on."""
|
||||||
|
return self.sys_addons.update(self.slug)
|
||||||
|
|
||||||
|
def rebuild(self) -> Awaitable[None]:
|
||||||
|
"""Rebuild this add-on."""
|
||||||
|
return self.sys_addons.rebuild(self.slug)
|
@ -2,10 +2,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
@ -20,16 +18,14 @@ from ..const import (
|
|||||||
SECURITY_DISABLE,
|
SECURITY_DISABLE,
|
||||||
SECURITY_PROFILE,
|
SECURITY_PROFILE,
|
||||||
)
|
)
|
||||||
from ..exceptions import AddonsNotSupportedError
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .addon import Addon
|
from .model import AddonModel
|
||||||
|
|
||||||
RE_SHA1 = re.compile(r"[a-f0-9]{8}")
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def rating_security(addon: Addon) -> int:
|
def rating_security(addon: AddonModel) -> int:
|
||||||
"""Return 1-6 for security rating.
|
"""Return 1-6 for security rating.
|
||||||
|
|
||||||
1 = not secure
|
1 = not secure
|
||||||
@ -86,34 +82,6 @@ def rating_security(addon: Addon) -> int:
|
|||||||
return max(min(6, rating), 1)
|
return max(min(6, rating), 1)
|
||||||
|
|
||||||
|
|
||||||
def get_hash_from_repository(name: str) -> str:
|
|
||||||
"""Generate a hash from repository."""
|
|
||||||
key = name.lower().encode()
|
|
||||||
return hashlib.sha1(key).hexdigest()[:8]
|
|
||||||
|
|
||||||
|
|
||||||
def extract_hash_from_path(path: Path) -> str:
|
|
||||||
"""Extract repo id from path."""
|
|
||||||
repository_dir = path.parts[-1]
|
|
||||||
|
|
||||||
if not RE_SHA1.match(repository_dir):
|
|
||||||
return get_hash_from_repository(repository_dir)
|
|
||||||
return repository_dir
|
|
||||||
|
|
||||||
|
|
||||||
def check_installed(method):
|
|
||||||
"""Wrap function with check if add-on is installed."""
|
|
||||||
|
|
||||||
async def wrap_check(addon, *args, **kwargs):
|
|
||||||
"""Return False if not installed or the function."""
|
|
||||||
if not addon.is_installed:
|
|
||||||
_LOGGER.error("Addon %s is not installed", addon.slug)
|
|
||||||
raise AddonsNotSupportedError()
|
|
||||||
return await method(addon, *args, **kwargs)
|
|
||||||
|
|
||||||
return wrap_check
|
|
||||||
|
|
||||||
|
|
||||||
async def remove_data(folder: Path) -> None:
|
async def remove_data(folder: Path) -> None:
|
||||||
"""Remove folder and reset privileged."""
|
"""Remove folder and reset privileged."""
|
||||||
try:
|
try:
|
||||||
|
@ -49,7 +49,6 @@ from ..const import (
|
|||||||
ATTR_LEGACY,
|
ATTR_LEGACY,
|
||||||
ATTR_LOCATON,
|
ATTR_LOCATON,
|
||||||
ATTR_MACHINE,
|
ATTR_MACHINE,
|
||||||
ATTR_MAINTAINER,
|
|
||||||
ATTR_MAP,
|
ATTR_MAP,
|
||||||
ATTR_NAME,
|
ATTR_NAME,
|
||||||
ATTR_NETWORK,
|
ATTR_NETWORK,
|
||||||
@ -211,14 +210,6 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
|
|||||||
}, extra=vol.REMOVE_EXTRA)
|
}, extra=vol.REMOVE_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-value-for-parameter
|
|
||||||
SCHEMA_REPOSITORY_CONFIG = vol.Schema({
|
|
||||||
vol.Required(ATTR_NAME): vol.Coerce(str),
|
|
||||||
vol.Optional(ATTR_URL): vol.Url(),
|
|
||||||
vol.Optional(ATTR_MAINTAINER): vol.Coerce(str),
|
|
||||||
}, extra=vol.REMOVE_EXTRA)
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_BUILD_CONFIG = vol.Schema({
|
SCHEMA_BUILD_CONFIG = vol.Schema({
|
||||||
vol.Optional(ATTR_BUILD_FROM, default=dict): vol.Schema({
|
vol.Optional(ATTR_BUILD_FROM, default=dict): vol.Schema({
|
||||||
|
@ -7,7 +7,7 @@ from aiohttp import web
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous.humanize import humanize_error
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
from ..addons.addon import Addon
|
from ..addons import AnyAddon
|
||||||
from ..addons.utils import rating_security
|
from ..addons.utils import rating_security
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_ADDONS,
|
ATTR_ADDONS,
|
||||||
@ -44,9 +44,9 @@ from ..const import (
|
|||||||
ATTR_ICON,
|
ATTR_ICON,
|
||||||
ATTR_INGRESS,
|
ATTR_INGRESS,
|
||||||
ATTR_INGRESS_ENTRY,
|
ATTR_INGRESS_ENTRY,
|
||||||
|
ATTR_INGRESS_PANEL,
|
||||||
ATTR_INGRESS_PORT,
|
ATTR_INGRESS_PORT,
|
||||||
ATTR_INGRESS_URL,
|
ATTR_INGRESS_URL,
|
||||||
ATTR_INGRESS_PANEL,
|
|
||||||
ATTR_INSTALLED,
|
ATTR_INSTALLED,
|
||||||
ATTR_IP_ADDRESS,
|
ATTR_IP_ADDRESS,
|
||||||
ATTR_KERNEL_MODULES,
|
ATTR_KERNEL_MODULES,
|
||||||
@ -82,6 +82,7 @@ from ..const import (
|
|||||||
CONTENT_TYPE_PNG,
|
CONTENT_TYPE_PNG,
|
||||||
CONTENT_TYPE_TEXT,
|
CONTENT_TYPE_TEXT,
|
||||||
REQUEST_FROM,
|
REQUEST_FROM,
|
||||||
|
STATE_NONE,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIError
|
from ..exceptions import APIError
|
||||||
@ -113,7 +114,7 @@ SCHEMA_SECURITY = vol.Schema({
|
|||||||
class APIAddons(CoreSysAttributes):
|
class APIAddons(CoreSysAttributes):
|
||||||
"""Handle RESTful API for add-on functions."""
|
"""Handle RESTful API for add-on functions."""
|
||||||
|
|
||||||
def _extract_addon(self, request: web.Request, check_installed: bool = True) -> Addon:
|
def _extract_addon(self, request: web.Request, check_installed: bool = True) -> AnyAddon:
|
||||||
"""Return addon, throw an exception it it doesn't exist."""
|
"""Return addon, throw an exception it it doesn't exist."""
|
||||||
addon_slug = request.match_info.get('addon')
|
addon_slug = request.match_info.get('addon')
|
||||||
|
|
||||||
@ -134,13 +135,13 @@ class APIAddons(CoreSysAttributes):
|
|||||||
async def list(self, request: web.Request) -> Dict[str, Any]:
|
async def list(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Return all add-ons or repositories."""
|
"""Return all add-ons or repositories."""
|
||||||
data_addons = []
|
data_addons = []
|
||||||
for addon in self.sys_addons.list_addons:
|
for addon in self.sys_addons.all:
|
||||||
data_addons.append({
|
data_addons.append({
|
||||||
ATTR_NAME: addon.name,
|
ATTR_NAME: addon.name,
|
||||||
ATTR_SLUG: addon.slug,
|
ATTR_SLUG: addon.slug,
|
||||||
ATTR_DESCRIPTON: addon.description,
|
ATTR_DESCRIPTON: addon.description,
|
||||||
ATTR_VERSION: addon.latest_version,
|
ATTR_VERSION: addon.latest_version,
|
||||||
ATTR_INSTALLED: addon.version_installed,
|
ATTR_INSTALLED: addon.version if addon.is_installed else None,
|
||||||
ATTR_AVAILABLE: addon.available,
|
ATTR_AVAILABLE: addon.available,
|
||||||
ATTR_DETACHED: addon.is_detached,
|
ATTR_DETACHED: addon.is_detached,
|
||||||
ATTR_REPOSITORY: addon.repository,
|
ATTR_REPOSITORY: addon.repository,
|
||||||
@ -151,7 +152,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
})
|
})
|
||||||
|
|
||||||
data_repositories = []
|
data_repositories = []
|
||||||
for repository in self.sys_addons.list_repositories:
|
for repository in self.sys_store.all:
|
||||||
data_repositories.append({
|
data_repositories.append({
|
||||||
ATTR_SLUG: repository.slug,
|
ATTR_SLUG: repository.slug,
|
||||||
ATTR_NAME: repository.name,
|
ATTR_NAME: repository.name,
|
||||||
@ -167,24 +168,23 @@ class APIAddons(CoreSysAttributes):
|
|||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def reload(self, request: web.Request) -> None:
|
async def reload(self, request: web.Request) -> None:
|
||||||
"""Reload all add-on data."""
|
"""Reload all add-on data from store."""
|
||||||
await asyncio.shield(self.sys_addons.reload())
|
await asyncio.shield(self.sys_store.reload())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Return add-on information."""
|
"""Return add-on information."""
|
||||||
addon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request, check_installed=False)
|
||||||
|
|
||||||
return {
|
data = {
|
||||||
ATTR_NAME: addon.name,
|
ATTR_NAME: addon.name,
|
||||||
ATTR_SLUG: addon.slug,
|
ATTR_SLUG: addon.slug,
|
||||||
ATTR_DESCRIPTON: addon.description,
|
ATTR_DESCRIPTON: addon.description,
|
||||||
ATTR_LONG_DESCRIPTION: addon.long_description,
|
ATTR_LONG_DESCRIPTION: addon.long_description,
|
||||||
ATTR_VERSION: addon.version_installed,
|
ATTR_AUTO_UPDATE: None,
|
||||||
ATTR_AUTO_UPDATE: addon.auto_update,
|
|
||||||
ATTR_REPOSITORY: addon.repository,
|
ATTR_REPOSITORY: addon.repository,
|
||||||
|
ATTR_VERSION: None,
|
||||||
ATTR_LAST_VERSION: addon.latest_version,
|
ATTR_LAST_VERSION: addon.latest_version,
|
||||||
ATTR_STATE: await addon.state(),
|
|
||||||
ATTR_PROTECTED: addon.protected,
|
ATTR_PROTECTED: addon.protected,
|
||||||
ATTR_RATING: rating_security(addon),
|
ATTR_RATING: rating_security(addon),
|
||||||
ATTR_BOOT: addon.boot,
|
ATTR_BOOT: addon.boot,
|
||||||
@ -193,6 +193,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_MACHINE: addon.supported_machine,
|
ATTR_MACHINE: addon.supported_machine,
|
||||||
ATTR_HOMEASSISTANT: addon.homeassistant_version,
|
ATTR_HOMEASSISTANT: addon.homeassistant_version,
|
||||||
ATTR_URL: addon.url,
|
ATTR_URL: addon.url,
|
||||||
|
ATTR_STATE: STATE_NONE,
|
||||||
ATTR_DETACHED: addon.is_detached,
|
ATTR_DETACHED: addon.is_detached,
|
||||||
ATTR_AVAILABLE: addon.available,
|
ATTR_AVAILABLE: addon.available,
|
||||||
ATTR_BUILD: addon.need_build,
|
ATTR_BUILD: addon.need_build,
|
||||||
@ -209,8 +210,8 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_ICON: addon.with_icon,
|
ATTR_ICON: addon.with_icon,
|
||||||
ATTR_LOGO: addon.with_logo,
|
ATTR_LOGO: addon.with_logo,
|
||||||
ATTR_CHANGELOG: addon.with_changelog,
|
ATTR_CHANGELOG: addon.with_changelog,
|
||||||
ATTR_WEBUI: addon.webui,
|
|
||||||
ATTR_STDIN: addon.with_stdin,
|
ATTR_STDIN: addon.with_stdin,
|
||||||
|
ATTR_WEBUI: None,
|
||||||
ATTR_HASSIO_API: addon.access_hassio_api,
|
ATTR_HASSIO_API: addon.access_hassio_api,
|
||||||
ATTR_HASSIO_ROLE: addon.hassio_role,
|
ATTR_HASSIO_ROLE: addon.hassio_role,
|
||||||
ATTR_AUTH_API: addon.access_auth_api,
|
ATTR_AUTH_API: addon.access_auth_api,
|
||||||
@ -220,17 +221,34 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_DEVICETREE: addon.with_devicetree,
|
ATTR_DEVICETREE: addon.with_devicetree,
|
||||||
ATTR_DOCKER_API: addon.access_docker_api,
|
ATTR_DOCKER_API: addon.access_docker_api,
|
||||||
ATTR_AUDIO: addon.with_audio,
|
ATTR_AUDIO: addon.with_audio,
|
||||||
ATTR_AUDIO_INPUT: addon.audio_input,
|
ATTR_AUDIO_INPUT: None,
|
||||||
ATTR_AUDIO_OUTPUT: addon.audio_output,
|
ATTR_AUDIO_OUTPUT: None,
|
||||||
ATTR_SERVICES: _pretty_services(addon),
|
ATTR_SERVICES: _pretty_services(addon),
|
||||||
ATTR_DISCOVERY: addon.discovery,
|
ATTR_DISCOVERY: addon.discovery,
|
||||||
ATTR_IP_ADDRESS: str(addon.ip_address),
|
ATTR_IP_ADDRESS: None,
|
||||||
ATTR_INGRESS: addon.with_ingress,
|
ATTR_INGRESS: addon.with_ingress,
|
||||||
|
ATTR_INGRESS_ENTRY: None,
|
||||||
|
ATTR_INGRESS_URL: None,
|
||||||
|
ATTR_INGRESS_PORT: None,
|
||||||
|
ATTR_INGRESS_PANEL: None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if addon.is_installed:
|
||||||
|
data.update({
|
||||||
|
ATTR_STATE: await addon.state(),
|
||||||
|
ATTR_WEBUI: addon.webui,
|
||||||
ATTR_INGRESS_ENTRY: addon.ingress_entry,
|
ATTR_INGRESS_ENTRY: addon.ingress_entry,
|
||||||
ATTR_INGRESS_URL: addon.ingress_url,
|
ATTR_INGRESS_URL: addon.ingress_url,
|
||||||
ATTR_INGRESS_PORT: addon.ingress_port,
|
ATTR_INGRESS_PORT: addon.ingress_port,
|
||||||
ATTR_INGRESS_PANEL: addon.ingress_panel,
|
ATTR_INGRESS_PANEL: addon.ingress_panel,
|
||||||
}
|
ATTR_AUDIO_INPUT: addon.audio_input,
|
||||||
|
ATTR_AUDIO_OUTPUT: addon.audio_output,
|
||||||
|
ATTR_AUTO_UPDATE: addon.auto_update,
|
||||||
|
ATTR_IP_ADDRESS: str(addon.ip_address),
|
||||||
|
ATTR_VERSION: addon.version,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def options(self, request: web.Request) -> None:
|
async def options(self, request: web.Request) -> None:
|
||||||
@ -258,7 +276,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
addon.ingress_panel = body[ATTR_INGRESS_PANEL]
|
addon.ingress_panel = body[ATTR_INGRESS_PANEL]
|
||||||
await self.sys_ingress.update_hass_panel(addon)
|
await self.sys_ingress.update_hass_panel(addon)
|
||||||
|
|
||||||
addon.save_data()
|
addon.save_persist()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def security(self, request: web.Request) -> None:
|
async def security(self, request: web.Request) -> None:
|
||||||
@ -325,7 +343,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
"""Update add-on."""
|
"""Update add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self._extract_addon(request)
|
||||||
|
|
||||||
if addon.latest_version == addon.version_installed:
|
if addon.latest_version == addon.version:
|
||||||
raise APIError("No update available!")
|
raise APIError("No update available!")
|
||||||
|
|
||||||
return asyncio.shield(addon.update())
|
return asyncio.shield(addon.update())
|
||||||
@ -392,7 +410,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
await asyncio.shield(addon.write_stdin(data))
|
await asyncio.shield(addon.write_stdin(data))
|
||||||
|
|
||||||
|
|
||||||
def _pretty_devices(addon: Addon) -> List[str]:
|
def _pretty_devices(addon: AnyAddon) -> List[str]:
|
||||||
"""Return a simplified device list."""
|
"""Return a simplified device list."""
|
||||||
dev_list = addon.devices
|
dev_list = addon.devices
|
||||||
if not dev_list:
|
if not dev_list:
|
||||||
@ -400,7 +418,7 @@ def _pretty_devices(addon: Addon) -> List[str]:
|
|||||||
return [row.split(':')[0] for row in dev_list]
|
return [row.split(':')[0] for row in dev_list]
|
||||||
|
|
||||||
|
|
||||||
def _pretty_services(addon: Addon) -> List[str]:
|
def _pretty_services(addon: AnyAddon) -> List[str]:
|
||||||
"""Return a simplified services role list."""
|
"""Return a simplified services role list."""
|
||||||
services = []
|
services = []
|
||||||
for name, access in addon.services_role.items():
|
for name, access in addon.services_role.items():
|
||||||
|
@ -73,8 +73,7 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Return host information."""
|
"""Return host information."""
|
||||||
list_addons = []
|
list_addons = []
|
||||||
for addon in self.sys_addons.list_addons:
|
for addon in self.sys_addons.installed:
|
||||||
if addon.is_installed:
|
|
||||||
list_addons.append(
|
list_addons.append(
|
||||||
{
|
{
|
||||||
ATTR_NAME: addon.name,
|
ATTR_NAME: addon.name,
|
||||||
@ -82,7 +81,7 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
ATTR_DESCRIPTON: addon.description,
|
ATTR_DESCRIPTON: addon.description,
|
||||||
ATTR_STATE: await addon.state(),
|
ATTR_STATE: await addon.state(),
|
||||||
ATTR_VERSION: addon.latest_version,
|
ATTR_VERSION: addon.latest_version,
|
||||||
ATTR_INSTALLED: addon.version_installed,
|
ATTR_INSTALLED: addon.version,
|
||||||
ATTR_REPOSITORY: addon.repository,
|
ATTR_REPOSITORY: addon.repository,
|
||||||
ATTR_ICON: addon.with_icon,
|
ATTR_ICON: addon.with_icon,
|
||||||
ATTR_LOGO: addon.with_logo,
|
ATTR_LOGO: addon.with_logo,
|
||||||
@ -127,7 +126,7 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
|
|
||||||
if ATTR_ADDONS_REPOSITORIES in body:
|
if ATTR_ADDONS_REPOSITORIES in body:
|
||||||
new = set(body[ATTR_ADDONS_REPOSITORIES])
|
new = set(body[ATTR_ADDONS_REPOSITORIES])
|
||||||
await asyncio.shield(self.sys_addons.load_repositories(new))
|
await asyncio.shield(self.sys_store.update_repositories(new))
|
||||||
|
|
||||||
self.sys_updater.save_data()
|
self.sys_updater.save_data()
|
||||||
self.sys_config.save_data()
|
self.sys_config.save_data()
|
||||||
|
@ -23,6 +23,7 @@ from .ingress import Ingress
|
|||||||
from .services import ServiceManager
|
from .services import ServiceManager
|
||||||
from .snapshots import SnapshotManager
|
from .snapshots import SnapshotManager
|
||||||
from .supervisor import Supervisor
|
from .supervisor import Supervisor
|
||||||
|
from .store import StoreManager
|
||||||
from .tasks import Tasks
|
from .tasks import Tasks
|
||||||
from .updater import Updater
|
from .updater import Updater
|
||||||
|
|
||||||
@ -53,6 +54,7 @@ async def initialize_coresys():
|
|||||||
coresys.ingress = Ingress(coresys)
|
coresys.ingress = Ingress(coresys)
|
||||||
coresys.tasks = Tasks(coresys)
|
coresys.tasks = Tasks(coresys)
|
||||||
coresys.services = ServiceManager(coresys)
|
coresys.services = ServiceManager(coresys)
|
||||||
|
coresys.store = StoreManager(coresys)
|
||||||
coresys.discovery = Discovery(coresys)
|
coresys.discovery = Discovery(coresys)
|
||||||
coresys.dbus = DBusManager(coresys)
|
coresys.dbus = DBusManager(coresys)
|
||||||
coresys.hassos = HassOS(coresys)
|
coresys.hassos = HassOS(coresys)
|
||||||
|
@ -44,6 +44,9 @@ class HassIO(CoreSysAttributes):
|
|||||||
# Load HassOS
|
# Load HassOS
|
||||||
await self.sys_hassos.load()
|
await self.sys_hassos.load()
|
||||||
|
|
||||||
|
# Load Stores
|
||||||
|
await self.sys_store.load()
|
||||||
|
|
||||||
# Load Add-ons
|
# Load Add-ons
|
||||||
await self.sys_addons.load()
|
await self.sys_addons.load()
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ if TYPE_CHECKING:
|
|||||||
from .services import ServiceManager
|
from .services import ServiceManager
|
||||||
from .snapshots import SnapshotManager
|
from .snapshots import SnapshotManager
|
||||||
from .supervisor import Supervisor
|
from .supervisor import Supervisor
|
||||||
|
from .store import StoreManager
|
||||||
from .tasks import Tasks
|
from .tasks import Tasks
|
||||||
from .updater import Updater
|
from .updater import Updater
|
||||||
|
|
||||||
@ -68,6 +69,7 @@ class CoreSys:
|
|||||||
self._dbus: DBusManager = None
|
self._dbus: DBusManager = None
|
||||||
self._hassos: HassOS = None
|
self._hassos: HassOS = None
|
||||||
self._services: ServiceManager = None
|
self._services: ServiceManager = None
|
||||||
|
self._store: StoreManager = None
|
||||||
self._discovery: Discovery = None
|
self._discovery: Discovery = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -223,6 +225,18 @@ class CoreSys:
|
|||||||
raise RuntimeError("AddonManager already set!")
|
raise RuntimeError("AddonManager already set!")
|
||||||
self._addons = value
|
self._addons = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def store(self) -> StoreManager:
|
||||||
|
"""Return StoreManager object."""
|
||||||
|
return self._store
|
||||||
|
|
||||||
|
@store.setter
|
||||||
|
def store(self, value: StoreManager):
|
||||||
|
"""Set a StoreManager object."""
|
||||||
|
if self._store:
|
||||||
|
raise RuntimeError("StoreManager already set!")
|
||||||
|
self._store = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def snapshots(self) -> SnapshotManager:
|
def snapshots(self) -> SnapshotManager:
|
||||||
"""Return SnapshotManager object."""
|
"""Return SnapshotManager object."""
|
||||||
@ -425,6 +439,11 @@ class CoreSysAttributes:
|
|||||||
"""Return AddonManager object."""
|
"""Return AddonManager object."""
|
||||||
return self.coresys.addons
|
return self.coresys.addons
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_store(self) -> StoreManager:
|
||||||
|
"""Return StoreManager object."""
|
||||||
|
return self.coresys.store
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sys_snapshots(self) -> SnapshotManager:
|
def sys_snapshots(self) -> SnapshotManager:
|
||||||
"""Return SnapshotManager object."""
|
"""Return SnapshotManager object."""
|
||||||
|
@ -31,6 +31,7 @@ from .interface import DockerInterface
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..addons.addon import Addon
|
from ..addons.addon import Addon
|
||||||
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
AUDIO_DEVICE = "/dev/snd:/dev/snd:rwm"
|
AUDIO_DEVICE = "/dev/snd:/dev/snd:rwm"
|
||||||
@ -39,15 +40,10 @@ AUDIO_DEVICE = "/dev/snd:/dev/snd:rwm"
|
|||||||
class DockerAddon(DockerInterface):
|
class DockerAddon(DockerInterface):
|
||||||
"""Docker Hass.io wrapper for Home Assistant."""
|
"""Docker Hass.io wrapper for Home Assistant."""
|
||||||
|
|
||||||
def __init__(self, coresys: CoreSys, slug: str):
|
def __init__(self, coresys: CoreSys, addon: Addon):
|
||||||
"""Initialize Docker Home Assistant wrapper."""
|
"""Initialize Docker Home Assistant wrapper."""
|
||||||
super().__init__(coresys)
|
super().__init__(coresys)
|
||||||
self._id: str = slug
|
self.addon = addon
|
||||||
|
|
||||||
@property
|
|
||||||
def addon(self) -> Addon:
|
|
||||||
"""Return add-on of Docker image."""
|
|
||||||
return self.sys_addons.get(self._id)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self) -> str:
|
def image(self) -> str:
|
||||||
@ -76,7 +72,7 @@ class DockerAddon(DockerInterface):
|
|||||||
def version(self) -> str:
|
def version(self) -> str:
|
||||||
"""Return version of Docker image."""
|
"""Return version of Docker image."""
|
||||||
if self.addon.legacy:
|
if self.addon.legacy:
|
||||||
return self.addon.version_installed
|
return self.addon.version
|
||||||
return super().version
|
return super().version
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -363,7 +359,7 @@ class DockerAddon(DockerInterface):
|
|||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
build_env = AddonBuild(self.coresys, self._id)
|
build_env = AddonBuild(self.coresys, self.addon)
|
||||||
|
|
||||||
_LOGGER.info("Start build %s:%s", self.image, tag)
|
_LOGGER.info("Start build %s:%s", self.image, tag)
|
||||||
try:
|
try:
|
||||||
|
@ -44,7 +44,7 @@ class Ingress(JsonConfig, CoreSysAttributes):
|
|||||||
def addons(self) -> List[Addon]:
|
def addons(self) -> List[Addon]:
|
||||||
"""Return list of ingress Add-ons."""
|
"""Return list of ingress Add-ons."""
|
||||||
addons = []
|
addons = []
|
||||||
for addon in self.sys_addons.list_installed:
|
for addon in self.sys_addons.installed:
|
||||||
if not addon.with_ingress:
|
if not addon.with_ingress:
|
||||||
continue
|
continue
|
||||||
addons.append(addon)
|
addons.append(addon)
|
||||||
|
@ -34,7 +34,7 @@ class ServiceInterface(CoreSysAttributes):
|
|||||||
def providers(self) -> List[str]:
|
def providers(self) -> List[str]:
|
||||||
"""Return name of service providers addon."""
|
"""Return name of service providers addon."""
|
||||||
addons = []
|
addons = []
|
||||||
for addon in self.sys_addons.list_installed:
|
for addon in self.sys_addons.installed:
|
||||||
if addon.services_role.get(self.slug) == PROVIDE_SERVICE:
|
if addon.services_role.get(self.slug) == PROVIDE_SERVICE:
|
||||||
addons.append(addon.slug)
|
addons.append(addon.slug)
|
||||||
return addons
|
return addons
|
||||||
|
@ -309,18 +309,9 @@ class SnapshotManager(CoreSysAttributes):
|
|||||||
self.sys_homeassistant.update(
|
self.sys_homeassistant.update(
|
||||||
snapshot.homeassistant_version))
|
snapshot.homeassistant_version))
|
||||||
|
|
||||||
# Process Add-ons
|
if addons:
|
||||||
addon_list = []
|
|
||||||
for slug in addons:
|
|
||||||
addon = self.sys_addons.get(slug)
|
|
||||||
if addon:
|
|
||||||
addon_list.append(addon)
|
|
||||||
continue
|
|
||||||
_LOGGER.warning("Can't restore addon %s", snapshot.slug)
|
|
||||||
|
|
||||||
if addon_list:
|
|
||||||
_LOGGER.info("Restore %s old add-ons", snapshot.slug)
|
_LOGGER.info("Restore %s old add-ons", snapshot.slug)
|
||||||
await snapshot.restore_addons(addon_list)
|
await snapshot.restore_addons(addons)
|
||||||
|
|
||||||
# Make sure homeassistant run agen
|
# Make sure homeassistant run agen
|
||||||
if task_hass:
|
if task_hass:
|
||||||
|
@ -281,7 +281,7 @@ class Snapshot(CoreSysAttributes):
|
|||||||
|
|
||||||
async def store_addons(self, addon_list=None):
|
async def store_addons(self, addon_list=None):
|
||||||
"""Add a list of add-ons into snapshot."""
|
"""Add a list of add-ons into snapshot."""
|
||||||
addon_list = addon_list or self.sys_addons.list_installed
|
addon_list = addon_list or self.sys_addons.installed
|
||||||
|
|
||||||
async def _addon_save(addon):
|
async def _addon_save(addon):
|
||||||
"""Task to store an add-on into snapshot."""
|
"""Task to store an add-on into snapshot."""
|
||||||
@ -300,7 +300,7 @@ class Snapshot(CoreSysAttributes):
|
|||||||
self._data[ATTR_ADDONS].append({
|
self._data[ATTR_ADDONS].append({
|
||||||
ATTR_SLUG: addon.slug,
|
ATTR_SLUG: addon.slug,
|
||||||
ATTR_NAME: addon.name,
|
ATTR_NAME: addon.name,
|
||||||
ATTR_VERSION: addon.version_installed,
|
ATTR_VERSION: addon.version,
|
||||||
ATTR_SIZE: addon_file.size,
|
ATTR_SIZE: addon_file.size,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -311,32 +311,26 @@ class Snapshot(CoreSysAttributes):
|
|||||||
|
|
||||||
async def restore_addons(self, addon_list=None):
|
async def restore_addons(self, addon_list=None):
|
||||||
"""Restore a list add-on from snapshot."""
|
"""Restore a list add-on from snapshot."""
|
||||||
if not addon_list:
|
addon_list = addon_list or self.addon_list
|
||||||
addon_list = []
|
|
||||||
for addon_slug in self.addon_list:
|
|
||||||
addon = self.sys_addons.get(addon_slug)
|
|
||||||
if addon:
|
|
||||||
addon_list.append(addon)
|
|
||||||
|
|
||||||
async def _addon_restore(addon):
|
async def _addon_restore(addon_slug):
|
||||||
"""Task to restore an add-on into snapshot."""
|
"""Task to restore an add-on into snapshot."""
|
||||||
addon_file = SecureTarFile(
|
addon_file = SecureTarFile(
|
||||||
Path(self._tmp.name, f"{addon.slug}.tar.gz"),
|
Path(self._tmp.name, f"{addon_slug}.tar.gz"), 'r', key=self._key)
|
||||||
'r', key=self._key)
|
|
||||||
|
|
||||||
# If exists inside snapshot
|
# If exists inside snapshot
|
||||||
if not addon_file.path.exists():
|
if not addon_file.path.exists():
|
||||||
_LOGGER.error("Can't find snapshot for %s", addon.slug)
|
_LOGGER.error("Can't find snapshot for %s", addon_slug)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Perform a restore
|
# Perform a restore
|
||||||
try:
|
try:
|
||||||
await addon.restore(addon_file)
|
await self.sys_addons.restore(addon_slug, addon_file)
|
||||||
except AddonsError:
|
except AddonsError:
|
||||||
_LOGGER.error("Can't restore snapshot for %s", addon.slug)
|
_LOGGER.error("Can't restore snapshot for %s", addon_slug)
|
||||||
|
|
||||||
# Run tasks
|
# Run tasks
|
||||||
tasks = [_addon_restore(addon) for addon in addon_list]
|
tasks = [_addon_restore(slug) for slug in addon_list]
|
||||||
if tasks:
|
if tasks:
|
||||||
await asyncio.wait(tasks)
|
await asyncio.wait(tasks)
|
||||||
|
|
||||||
@ -459,4 +453,4 @@ class Snapshot(CoreSysAttributes):
|
|||||||
|
|
||||||
Return a coroutine.
|
Return a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.sys_addons.load_repositories(self.repositories)
|
return self.sys_store.update_repositories(self.repositories)
|
||||||
|
101
hassio/store/__init__.py
Normal file
101
hassio/store/__init__.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
"""Add-on Store handler."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
|
from ..const import REPOSITORY_CORE, REPOSITORY_LOCAL
|
||||||
|
from .addon import AddonStore
|
||||||
|
from .data import StoreData
|
||||||
|
from .repository import Repository
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BUILTIN_REPOSITORIES = set((REPOSITORY_CORE, REPOSITORY_LOCAL))
|
||||||
|
|
||||||
|
|
||||||
|
class StoreManager(CoreSysAttributes):
|
||||||
|
"""Manage add-ons inside Hass.io."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys):
|
||||||
|
"""Initialize Docker base wrapper."""
|
||||||
|
self.coresys: CoreSys = coresys
|
||||||
|
self.data = StoreData(coresys)
|
||||||
|
self.repositories: Dict[str, Repository] = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all(self) -> List[Repository]:
|
||||||
|
"""Return list of add-on repositories."""
|
||||||
|
return list(self.repositories.values())
|
||||||
|
|
||||||
|
async def load(self) -> None:
|
||||||
|
"""Start up add-on management."""
|
||||||
|
self.data.update()
|
||||||
|
|
||||||
|
# Init Hass.io built-in repositories
|
||||||
|
repositories = \
|
||||||
|
set(self.sys_config.addons_repositories) | BUILTIN_REPOSITORIES
|
||||||
|
|
||||||
|
# Init custom repositories and load add-ons
|
||||||
|
await self.update_repositories(repositories)
|
||||||
|
|
||||||
|
async def reload(self) -> None:
|
||||||
|
"""Update add-ons from repository and reload list."""
|
||||||
|
tasks = [repository.update() for repository in
|
||||||
|
self.repositories.values()]
|
||||||
|
if tasks:
|
||||||
|
await asyncio.wait(tasks)
|
||||||
|
|
||||||
|
# read data from repositories
|
||||||
|
self.data.update()
|
||||||
|
self._read_addons()
|
||||||
|
|
||||||
|
async def update_repositories(self, list_repositories):
|
||||||
|
"""Add a new custom repository."""
|
||||||
|
new_rep = set(list_repositories)
|
||||||
|
old_rep = set(self.repositories)
|
||||||
|
|
||||||
|
# add new repository
|
||||||
|
async def _add_repository(url):
|
||||||
|
"""Helper function to async add repository."""
|
||||||
|
repository = Repository(self.coresys, url)
|
||||||
|
if not await repository.load():
|
||||||
|
_LOGGER.error("Can't load from repository %s", url)
|
||||||
|
return
|
||||||
|
self.repositories[url] = repository
|
||||||
|
|
||||||
|
# don't add built-in repository to config
|
||||||
|
if url not in BUILTIN_REPOSITORIES:
|
||||||
|
self.sys_config.add_addon_repository(url)
|
||||||
|
|
||||||
|
tasks = [_add_repository(url) for url in new_rep - old_rep]
|
||||||
|
if tasks:
|
||||||
|
await asyncio.wait(tasks)
|
||||||
|
|
||||||
|
# del new repository
|
||||||
|
for url in old_rep - new_rep - BUILTIN_REPOSITORIES:
|
||||||
|
self.repositories.pop(url).remove()
|
||||||
|
self.sys_config.drop_addon_repository(url)
|
||||||
|
|
||||||
|
# update data
|
||||||
|
self.data.update()
|
||||||
|
self._read_addons()
|
||||||
|
|
||||||
|
def _read_addons(self) -> None:
|
||||||
|
"""Reload add-ons inside store."""
|
||||||
|
all_addons = set(self.data.addons)
|
||||||
|
|
||||||
|
# calc diff
|
||||||
|
add_addons = all_addons - set(self.sys_addons.store)
|
||||||
|
del_addons = set(self.sys_addons.store) - all_addons
|
||||||
|
|
||||||
|
_LOGGER.info("Load add-ons from store: %d all - %d new - %d remove",
|
||||||
|
len(all_addons), len(add_addons), len(del_addons))
|
||||||
|
|
||||||
|
# new addons
|
||||||
|
for slug in add_addons:
|
||||||
|
self.sys_addons.store[slug] = AddonStore(self.coresys, slug)
|
||||||
|
|
||||||
|
# remove
|
||||||
|
for slug in del_addons:
|
||||||
|
self.sys_addons.store.pop(slug)
|
31
hassio/store/addon.py
Normal file
31
hassio/store/addon.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"""Init file for Hass.io add-ons."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ..coresys import CoreSys
|
||||||
|
from ..addons.model import AddonModel, Data
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AddonStore(AddonModel):
|
||||||
|
"""Hold data for add-on inside Hass.io."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys, slug: str):
|
||||||
|
"""Initialize data holder."""
|
||||||
|
self.coresys: CoreSys = coresys
|
||||||
|
self.slug: str = slug
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self) -> Data:
|
||||||
|
"""Return add-on data/config."""
|
||||||
|
return self.sys_store.data.addons[self.slug]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_installed(self) -> bool:
|
||||||
|
"""Return True if an add-on is installed."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_detached(self) -> bool:
|
||||||
|
"""Return True if add-on is detached."""
|
||||||
|
return False
|
114
hassio/store/data.py
Normal file
114
hassio/store/data.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
"""Init file for Hass.io add-on data."""
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
|
from ..addons.validate import SCHEMA_ADDON_CONFIG
|
||||||
|
from ..const import (
|
||||||
|
ATTR_LOCATON,
|
||||||
|
ATTR_REPOSITORY,
|
||||||
|
ATTR_SLUG,
|
||||||
|
REPOSITORY_CORE,
|
||||||
|
REPOSITORY_LOCAL,
|
||||||
|
)
|
||||||
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
|
from ..exceptions import JsonFileError
|
||||||
|
from ..utils.json import read_json_file
|
||||||
|
from .utils import extract_hash_from_path
|
||||||
|
from .validate import SCHEMA_REPOSITORY_CONFIG
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class StoreData(CoreSysAttributes):
|
||||||
|
"""Hold data for Add-ons inside Hass.io."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys):
|
||||||
|
"""Initialize data holder."""
|
||||||
|
self.coresys: CoreSys = coresys
|
||||||
|
self.repositories: Dict[str, Any] = {}
|
||||||
|
self.addons: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Read data from add-on repository."""
|
||||||
|
self.repositories.clear()
|
||||||
|
self.addons.clear()
|
||||||
|
|
||||||
|
# read core repository
|
||||||
|
self._read_addons_folder(self.sys_config.path_addons_core, REPOSITORY_CORE)
|
||||||
|
|
||||||
|
# read local repository
|
||||||
|
self._read_addons_folder(self.sys_config.path_addons_local, REPOSITORY_LOCAL)
|
||||||
|
|
||||||
|
# add built-in repositories information
|
||||||
|
self._set_builtin_repositories()
|
||||||
|
|
||||||
|
# read custom git repositories
|
||||||
|
for repository_element in self.sys_config.path_addons_git.iterdir():
|
||||||
|
if repository_element.is_dir():
|
||||||
|
self._read_git_repository(repository_element)
|
||||||
|
|
||||||
|
def _read_git_repository(self, path):
|
||||||
|
"""Process a custom repository folder."""
|
||||||
|
slug = extract_hash_from_path(path)
|
||||||
|
|
||||||
|
# exists repository json
|
||||||
|
repository_file = Path(path, "repository.json")
|
||||||
|
try:
|
||||||
|
repository_info = SCHEMA_REPOSITORY_CONFIG(read_json_file(repository_file))
|
||||||
|
except JsonFileError:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Can't read repository information from %s", repository_file
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except vol.Invalid:
|
||||||
|
_LOGGER.warning("Repository parse error %s", repository_file)
|
||||||
|
return
|
||||||
|
|
||||||
|
# process data
|
||||||
|
self.repositories[slug] = repository_info
|
||||||
|
self._read_addons_folder(path, slug)
|
||||||
|
|
||||||
|
def _read_addons_folder(self, path, repository):
|
||||||
|
"""Read data from add-ons folder."""
|
||||||
|
for addon in path.glob("**/config.json"):
|
||||||
|
try:
|
||||||
|
addon_config = read_json_file(addon)
|
||||||
|
except JsonFileError:
|
||||||
|
_LOGGER.warning("Can't read %s from repository %s", addon, repository)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# validate
|
||||||
|
try:
|
||||||
|
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
|
||||||
|
except vol.Invalid as ex:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Can't read %s: %s", addon, humanize_error(addon_config, ex)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Generate slug
|
||||||
|
addon_slug = "{}_{}".format(repository, addon_config[ATTR_SLUG])
|
||||||
|
|
||||||
|
# store
|
||||||
|
addon_config[ATTR_REPOSITORY] = repository
|
||||||
|
addon_config[ATTR_LOCATON] = str(addon.parent)
|
||||||
|
self.addons[addon_slug] = addon_config
|
||||||
|
|
||||||
|
def _set_builtin_repositories(self):
|
||||||
|
"""Add local built-in repository into dataset."""
|
||||||
|
try:
|
||||||
|
builtin_file = Path(__file__).parent.joinpath("built-in.json")
|
||||||
|
builtin_data = read_json_file(builtin_file)
|
||||||
|
except JsonFileError:
|
||||||
|
_LOGGER.warning("Can't read built-in json")
|
||||||
|
return
|
||||||
|
|
||||||
|
# core repository
|
||||||
|
self.repositories[REPOSITORY_CORE] = builtin_data[REPOSITORY_CORE]
|
||||||
|
|
||||||
|
# local repository
|
||||||
|
self.repositories[REPOSITORY_LOCAL] = builtin_data[REPOSITORY_LOCAL]
|
@ -25,17 +25,17 @@ class GitRepo(CoreSysAttributes):
|
|||||||
self.path = path
|
self.path = path
|
||||||
self.lock = asyncio.Lock(loop=coresys.loop)
|
self.lock = asyncio.Lock(loop=coresys.loop)
|
||||||
|
|
||||||
self._data = RE_REPOSITORY.match(url).groupdict()
|
self.data = RE_REPOSITORY.match(url).groupdict()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
"""Return repository URL."""
|
"""Return repository URL."""
|
||||||
return self._data[ATTR_URL]
|
return self.data[ATTR_URL]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def branch(self):
|
def branch(self):
|
||||||
"""Return repository branch."""
|
"""Return repository branch."""
|
||||||
return self._data[ATTR_BRANCH]
|
return self.data[ATTR_BRANCH]
|
||||||
|
|
||||||
async def load(self):
|
async def load(self):
|
||||||
"""Init Git add-on repository."""
|
"""Init Git add-on repository."""
|
@ -12,6 +12,8 @@ UNKNOWN = 'unknown'
|
|||||||
class Repository(CoreSysAttributes):
|
class Repository(CoreSysAttributes):
|
||||||
"""Repository in Hass.io."""
|
"""Repository in Hass.io."""
|
||||||
|
|
||||||
|
slug: str = None
|
||||||
|
|
||||||
def __init__(self, coresys, repository):
|
def __init__(self, coresys, repository):
|
||||||
"""Initialize repository object."""
|
"""Initialize repository object."""
|
||||||
self.coresys = coresys
|
self.coresys = coresys
|
||||||
@ -19,39 +21,34 @@ class Repository(CoreSysAttributes):
|
|||||||
self.git = None
|
self.git = None
|
||||||
|
|
||||||
if repository == REPOSITORY_LOCAL:
|
if repository == REPOSITORY_LOCAL:
|
||||||
self._id = repository
|
self.slug = repository
|
||||||
elif repository == REPOSITORY_CORE:
|
elif repository == REPOSITORY_CORE:
|
||||||
self._id = repository
|
self.slug = repository
|
||||||
self.git = GitRepoHassIO(coresys)
|
self.git = GitRepoHassIO(coresys)
|
||||||
else:
|
else:
|
||||||
self._id = get_hash_from_repository(repository)
|
self.slug = get_hash_from_repository(repository)
|
||||||
self.git = GitRepoCustom(coresys, repository)
|
self.git = GitRepoCustom(coresys, repository)
|
||||||
self.source = repository
|
self.source = repository
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _mesh(self):
|
def data(self):
|
||||||
"""Return data struct repository."""
|
"""Return data struct repository."""
|
||||||
return self.sys_addons.data.repositories.get(self._id, {})
|
return self.sys_store.data.repositories[self.slug]
|
||||||
|
|
||||||
@property
|
|
||||||
def slug(self):
|
|
||||||
"""Return slug of repository."""
|
|
||||||
return self._id
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return name of repository."""
|
"""Return name of repository."""
|
||||||
return self._mesh.get(ATTR_NAME, UNKNOWN)
|
return self.data.get(ATTR_NAME, UNKNOWN)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
"""Return URL of repository."""
|
"""Return URL of repository."""
|
||||||
return self._mesh.get(ATTR_URL, self.source)
|
return self.data.get(ATTR_URL, self.source)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def maintainer(self):
|
def maintainer(self):
|
||||||
"""Return url of repository."""
|
"""Return url of repository."""
|
||||||
return self._mesh.get(ATTR_MAINTAINER, UNKNOWN)
|
return self.data.get(ATTR_MAINTAINER, UNKNOWN)
|
||||||
|
|
||||||
async def load(self):
|
async def load(self):
|
||||||
"""Load addon repository."""
|
"""Load addon repository."""
|
||||||
@ -67,7 +64,7 @@ class Repository(CoreSysAttributes):
|
|||||||
|
|
||||||
def remove(self):
|
def remove(self):
|
||||||
"""Remove add-on repository."""
|
"""Remove add-on repository."""
|
||||||
if self._id in (REPOSITORY_CORE, REPOSITORY_LOCAL):
|
if self.slug in (REPOSITORY_CORE, REPOSITORY_LOCAL):
|
||||||
raise APIError("Can't remove built-in repositories!")
|
raise APIError("Can't remove built-in repositories!")
|
||||||
|
|
||||||
self.git.remove()
|
self.git.remove()
|
23
hassio/store/utils.py
Normal file
23
hassio/store/utils.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"""Util add-ons functions."""
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
RE_SHA1 = re.compile(r"[a-f0-9]{8}")
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_hash_from_repository(name: str) -> str:
|
||||||
|
"""Generate a hash from repository."""
|
||||||
|
key = name.lower().encode()
|
||||||
|
return hashlib.sha1(key).hexdigest()[:8]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_hash_from_path(path: Path) -> str:
|
||||||
|
"""Extract repo id from path."""
|
||||||
|
repository_dir = path.parts[-1]
|
||||||
|
|
||||||
|
if not RE_SHA1.match(repository_dir):
|
||||||
|
return get_hash_from_repository(repository_dir)
|
||||||
|
return repository_dir
|
13
hassio/store/validate.py
Normal file
13
hassio/store/validate.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"""Validate add-ons options schema."""
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from ..const import ATTR_NAME, ATTR_URL, ATTR_MAINTAINER
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=no-value-for-parameter
|
||||||
|
SCHEMA_REPOSITORY_CONFIG = vol.Schema({
|
||||||
|
vol.Required(ATTR_NAME): vol.Coerce(str),
|
||||||
|
vol.Optional(ATTR_URL): vol.Url(),
|
||||||
|
vol.Optional(ATTR_MAINTAINER): vol.Coerce(str),
|
||||||
|
}, extra=vol.REMOVE_EXTRA)
|
@ -51,7 +51,7 @@ class Tasks(CoreSysAttributes):
|
|||||||
|
|
||||||
# Reload
|
# Reload
|
||||||
self.jobs.add(
|
self.jobs.add(
|
||||||
self.sys_scheduler.register_task(self.sys_addons.reload, RUN_RELOAD_ADDONS)
|
self.sys_scheduler.register_task(self.sys_store.reload, RUN_RELOAD_ADDONS)
|
||||||
)
|
)
|
||||||
self.jobs.add(
|
self.jobs.add(
|
||||||
self.sys_scheduler.register_task(
|
self.sys_scheduler.register_task(
|
||||||
@ -93,7 +93,7 @@ class Tasks(CoreSysAttributes):
|
|||||||
if not addon.is_installed or not addon.auto_update:
|
if not addon.is_installed or not addon.auto_update:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if addon.version_installed == addon.latest_version:
|
if addon.version == addon.latest_version:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if addon.test_update_schema():
|
if addon.test_update_schema():
|
||||||
|
Loading…
x
Reference in New Issue
Block a user