Add new data Layer for addons (#81)

* Add new data Layer for addons

* draft v2

* part 3

* Cleanups for new addon layout

* cleanup part 5

* fix lint p1

* fix lint p2

* fix api bug

* Fix lint p3

* fix lint p4

* fix lint p5

* fix lint p6

* fix update

* fix lint

* Update repo add code part 1

* new repository load code

* reorder code

* Code cleanup p1

* Don't allow change options for not installed addons

* Cleanup error handling

* Fix addon restart config bug

* minimize timeout for bad addons

* set core timeout for docker to 15

* fix lint

* fix start bug

* Add startuptype

* change names / fix update bug

* fix update bug p2

* Cleanup arch

* Ignore built-in repositories

* fix lint

* fix bug

* fix bug p4

* fix arch handling / better bugfix for boot option

* fix lint

* fix arch

* fix options

* fix close is no coro

* update api description & fix lint

* Fix error

* fix api validate config

* cleanup api

* Fix repo loader

* cleanup slugs

* fix lint
This commit is contained in:
Pascal Vizeli 2017-06-23 00:51:28 +02:00 committed by GitHub
parent b7f5cc868b
commit 1b887e38d6
17 changed files with 680 additions and 602 deletions

6
API.md
View File

@ -239,12 +239,12 @@ Output the raw docker log
"url": "null|url of addon",
"detached": "bool",
"repository": "12345678|null",
"version": "VERSION",
"version": "null|VERSION_INSTALLED",
"last_version": "LAST_VERSION",
"state": "started|stopped",
"state": "none|started|stopped",
"boot": "auto|manual",
"build": "bool",
"options": {},
"options": "null|{}",
}
```

View File

@ -1,220 +1,133 @@
"""Init file for HassIO addons."""
import asyncio
import logging
import shutil
from .data import AddonsData
from .git import AddonsRepoHassIO, AddonsRepoCustom
from ..const import STATE_STOPPED, STATE_STARTED
from ..dock.addon import DockerAddon
from .addon import Addon
from .repository import Repository
from .data import Data
from ..const import REPOSITORY_CORE, REPOSITORY_LOCAL, BOOT_AUTO
_LOGGER = logging.getLogger(__name__)
BUILTIN_REPOSITORIES = set((REPOSITORY_CORE, REPOSITORY_LOCAL))
class AddonManager(AddonsData):
class AddonManager(object):
"""Manage addons inside HassIO."""
def __init__(self, config, loop, dock):
"""Initialize docker base wrapper."""
super().__init__(config)
self.loop = loop
self.config = config
self.dock = dock
self.repositories = []
self.dockers = {}
self.data = Data(config)
self.addons = {}
self.repositories = {}
async def prepare(self, arch):
@property
def list_addons(self):
"""Return a list of all addons."""
return list(self.addons.values())
@property
def list_repositories(self):
"""Return list of addon repositories."""
return list(self.repositories.values())
def get(self, addon_slug):
"""Return a adddon from slug."""
return self.addons.get(addon_slug)
async def prepare(self):
"""Startup addon management."""
self.arch = arch
self.data.reload()
# init hassio repository
self.repositories.append(AddonsRepoHassIO(self.config, self.loop))
# init hassio built-in repositories
repositories = \
set(self.config.addons_repositories) | BUILTIN_REPOSITORIES
# init custom repositories
for url in self.config.addons_repositories:
self.repositories.append(
AddonsRepoCustom(self.config, self.loop, url))
# load addon repository
tasks = [addon.load() for addon in self.repositories]
if tasks:
await asyncio.wait(tasks, loop=self.loop)
# read data from repositories
self.read_data_from_repositories()
self.merge_update_config()
# load installed addons
for addon in self.list_installed:
self.dockers[addon] = DockerAddon(
self.config, self.loop, self.dock, self, addon)
await self.dockers[addon].attach()
async def add_git_repository(self, url):
"""Add a new custom repository."""
if url in self.config.addons_repositories:
_LOGGER.warning("Repository already exists %s", url)
return False
repo = AddonsRepoCustom(self.config, self.loop, url)
if not await repo.load():
_LOGGER.error("Can't load from repository %s", url)
return False
self.config.addons_repositories = url
self.repositories.append(repo)
return True
def drop_git_repository(self, url):
"""Remove a custom repository."""
for repo in self.repositories:
if repo.url == url:
self.repositories.remove(repo)
self.config.drop_addon_repository(url)
repo.remove()
return True
return False
# init custom repositories & load addons
await self.load_repositories(repositories)
async def reload(self):
"""Update addons from repo and reload list."""
tasks = [addon.pull() for addon in self.repositories]
if not tasks:
return
await asyncio.wait(tasks, loop=self.loop)
# read data from repositories
self.read_data_from_repositories()
self.merge_update_config()
# remove stalled addons
for addon in self.list_detached:
_LOGGER.warning("Dedicated addon '%s' found!", addon)
async def auto_boot(self, start_type):
"""Boot addons with mode auto."""
boot_list = self.list_startup(start_type)
tasks = [self.start(addon) for addon in boot_list]
_LOGGER.info("Startup %s run %d addons", start_type, len(tasks))
tasks = [repository.update() for repository in
self.repositories.values()]
if tasks:
await asyncio.wait(tasks, loop=self.loop)
async def install(self, addon, version=None):
"""Install a addon."""
if not self.exists_addon(addon):
_LOGGER.error("Addon %s not exists for install", addon)
return False
# read data from repositories
self.data.reload()
if self.arch not in self.get_arch(addon):
_LOGGER.error("Addon %s not supported on %s", addon, self.arch)
return False
# update addons
await self.load_addons()
if self.is_installed(addon):
_LOGGER.error("Addon %s is already installed", addon)
return False
async def load_repositories(self, list_repositories):
"""Add a new custom repository."""
new_rep = set(list_repositories)
old_rep = set(self.repositories)
if not self.path_data(addon).is_dir():
_LOGGER.info("Create Home-Assistant addon data folder %s",
self.path_data(addon))
self.path_data(addon).mkdir()
# add new repository
async def _add_repository(url):
"""Helper function to async add repository."""
repository = Repository(self.config, self.loop, self.data, url)
if not await repository.load():
_LOGGER.error("Can't load from repository %s", url)
return
self.repositories[url] = repository
addon_docker = DockerAddon(
self.config, self.loop, self.dock, self, addon)
# don't add built-in repository to config
if url not in BUILTIN_REPOSITORIES:
self.config.addons_repositories = url
version = version or self.get_last_version(addon)
if not await addon_docker.install(version):
return False
tasks = [_add_repository(url) for url in new_rep - old_rep]
if tasks:
await asyncio.wait(tasks, loop=self.loop)
self.dockers[addon] = addon_docker
self.set_addon_install(addon, version)
return True
# del new repository
for url in old_rep - new_rep - BUILTIN_REPOSITORIES:
self.repositories.pop(url).remove()
self.config.drop_addon_repository(url)
async def uninstall(self, addon):
"""Remove a addon."""
if not self.is_installed(addon):
_LOGGER.error("Addon %s is already uninstalled", addon)
return False
# update data
self.data.reload()
await self.load_addons()
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
async def load_addons(self):
"""Update/add internal addon store."""
all_addons = set(self.data.system) | set(self.data.cache)
if not await self.dockers[addon].remove():
return False
# calc diff
add_addons = all_addons - set(self.addons)
del_addons = set(self.addons) - all_addons
if self.path_data(addon).is_dir():
_LOGGER.info("Remove Home-Assistant addon data folder %s",
self.path_data(addon))
shutil.rmtree(str(self.path_data(addon)))
_LOGGER.info("Load addons: %d all - %d new - %d remove",
len(all_addons), len(add_addons), len(del_addons))
self.dockers.pop(addon)
self.set_addon_uninstall(addon)
return True
# new addons
tasks = []
for addon_slug in add_addons:
addon = Addon(
self.config, self.loop, self.dock, self.data, addon_slug)
async def state(self, addon):
"""Return running state of addon."""
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return
tasks.append(addon.load())
self.addons[addon_slug] = addon
if await self.dockers[addon].is_running():
return STATE_STARTED
return STATE_STOPPED
if tasks:
await asyncio.wait(tasks, loop=self.loop)
async def start(self, addon):
"""Set options and start addon."""
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
# remove
for addon_slug in del_addons:
self.addons.pop(addon_slug)
if not self.write_addon_options(addon):
_LOGGER.error("Can't write options for addon %s", addon)
return False
async def auto_boot(self, stage):
"""Boot addons with mode auto."""
tasks = []
for addon in self.addons.values():
if addon.is_installed and addon.boot == BOOT_AUTO and \
addon.startup == stage:
tasks.append(addon.start())
return await self.dockers[addon].run()
async def stop(self, addon):
"""Stop addon."""
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
return await self.dockers[addon].stop()
async def update(self, addon, version=None):
"""Update addon."""
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
version = version or self.get_last_version(addon)
# update
if not await self.dockers[addon].update(version):
return False
self.set_addon_update(addon, version)
return True
async def restart(self, addon):
"""Restart addon."""
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
if not self.write_addon_options(addon):
_LOGGER.error("Can't write options for addon %s", addon)
return False
return await self.dockers[addon].restart()
async def logs(self, addon):
"""Return addons log output."""
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
return await self.dockers[addon].logs()
_LOGGER.info("Startup %s run %d addons", stage, len(tasks))
if tasks:
await asyncio.wait(tasks, loop=self.loop)

356
hassio/addons/addon.py Normal file
View File

@ -0,0 +1,356 @@
"""Init file for HassIO addons."""
from copy import deepcopy
import logging
from pathlib import Path, PurePath
import re
import shutil
import voluptuous as vol
from voluptuous.humanize import humanize_error
from .validate import validate_options, MAP_VOLUME
from ..const import (
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP,
ATTR_OPTIONS, ATTR_PORTS, ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY,
ATTR_URL, ATTR_ARCH, ATTR_LOCATON, ATTR_DEVICES, ATTR_ENVIRONMENT,
ATTR_HOST_NETWORK, ATTR_TMPFS, ATTR_PRIVILEGED, ATTR_STARTUP,
STATE_STARTED, STATE_STOPPED, STATE_NONE)
from ..dock.addon import DockerAddon
from ..tools import write_json_file
_LOGGER = logging.getLogger(__name__)
RE_VOLUME = re.compile(MAP_VOLUME)
class Addon(object):
"""Hold data for addon inside HassIO."""
def __init__(self, config, loop, dock, data, addon_slug):
"""Initialize data holder."""
self.config = config
self.data = data
self._id = addon_slug
if self._mesh is None:
raise RuntimeError("{} not a valid addon!".format(self._id))
self.addon_docker = DockerAddon(config, loop, dock, self)
async def load(self):
"""Async initialize of object."""
if self.is_installed:
await self.addon_docker.attach()
@property
def slug(self):
"""Return slug/id of addon."""
return self._id
@property
def _mesh(self):
"""Return addon data from system or cache."""
return self.data.system.get(self._id, self.data.cache.get(self._id))
@property
def is_installed(self):
"""Return True if a addon is installed."""
return self._id in self.data.system
@property
def is_detached(self):
"""Return True if addon is detached."""
return self._id not in self.data.cache
@property
def version_installed(self):
"""Return installed version."""
return self.data.user.get(self._id, {}).get(ATTR_VERSION)
def _set_install(self, version):
"""Set addon as installed."""
self.data.system[self._id] = deepcopy(self.data.cache[self._id])
self.data.user[self._id] = {
ATTR_OPTIONS: {},
ATTR_VERSION: version,
}
self.data.save()
def _set_uninstall(self):
"""Set addon as uninstalled."""
self.data.system.pop(self._id, None)
self.data.user.pop(self._id, None)
self.data.save()
def _set_update(self, version):
"""Update version of addon."""
self.data.system[self._id] = deepcopy(self.data.cache[self._id])
self.data.user[self._id][ATTR_VERSION] = version
self.data.save()
@property
def options(self):
"""Return options with local changes."""
if self.is_installed:
return {
**self.data.system[self._id][ATTR_OPTIONS],
**self.data.user[self._id][ATTR_OPTIONS],
}
@options.setter
def options(self, value):
"""Store user addon options."""
self.data.user[self._id][ATTR_OPTIONS] = deepcopy(value)
self.data.save()
@property
def boot(self):
"""Return boot config with prio local settings."""
if ATTR_BOOT in self.data.user.get(self._id, {}):
return self.data.user[self._id][ATTR_BOOT]
return self._mesh[ATTR_BOOT]
@boot.setter
def boot(self, value):
"""Store user boot options."""
self.data.user[self._id][ATTR_BOOT] = value
self.data.save()
@property
def name(self):
"""Return name of addon."""
return self._mesh[ATTR_NAME]
@property
def description(self):
"""Return description of addon."""
return self._mesh[ATTR_DESCRIPTON]
@property
def repository(self):
"""Return repository of addon."""
return self._mesh[ATTR_REPOSITORY]
@property
def last_version(self):
"""Return version of addon."""
if self._id in self.data.cache:
return self.data.cache[self._id][ATTR_VERSION]
return self.version_installed
@property
def startup(self):
"""Return startup type of addon."""
return self._mesh.get(ATTR_STARTUP)
@property
def ports(self):
"""Return ports of addon."""
return self._mesh.get(ATTR_PORTS)
@property
def network_mode(self):
"""Return network mode of addon."""
if self._mesh[ATTR_HOST_NETWORK]:
return 'host'
return 'bridge'
@property
def devices(self):
"""Return devices of addon."""
return self._mesh.get(ATTR_DEVICES)
@property
def tmpfs(self):
"""Return tmpfs of addon."""
return self._mesh.get(ATTR_TMPFS)
@property
def environment(self):
"""Return environment of addon."""
return self._mesh.get(ATTR_ENVIRONMENT)
@property
def privileged(self):
"""Return list of privilege."""
return self._mesh.get(ATTR_PRIVILEGED)
@property
def url(self):
"""Return url of addon."""
return self._mesh.get(ATTR_URL)
@property
def supported_arch(self):
"""Return list of supported arch."""
return self._mesh[ATTR_ARCH]
@property
def image(self):
"""Return image name of addon."""
addon_data = self._mesh
# Repository with dockerhub images
if ATTR_IMAGE in addon_data:
return addon_data[ATTR_IMAGE].format(arch=self.config.arch)
# local build
return "{}/{}-addon-{}".format(
addon_data[ATTR_REPOSITORY], self.config.arch,
addon_data[ATTR_SLUG])
@property
def need_build(self):
"""Return True if this addon need a local build."""
return ATTR_IMAGE not in self._mesh
@property
def map_volumes(self):
"""Return a dict of {volume: policy} from addon."""
volumes = {}
for volume in self._mesh[ATTR_MAP]:
result = RE_VOLUME.match(volume)
volumes[result.group(1)] = result.group(2) or 'ro'
return volumes
@property
def path_data(self):
"""Return addon data path inside supervisor."""
return Path(self.config.path_addons_data, self._id)
@property
def path_extern_data(self):
"""Return addon data path external for docker."""
return PurePath(self.config.path_extern_addons_data, self._id)
@property
def path_addon_options(self):
"""Return path to addons options."""
return Path(self.path_data, "options.json")
@property
def path_addon_location(self):
"""Return path to this addon."""
return Path(self._mesh[ATTR_LOCATON])
def write_addon_options(self):
"""Return True if addon options is written to data."""
schema = self.schema
options = self.options
try:
schema(options)
return write_json_file(self.path_addon_options, options)
except vol.Invalid as ex:
_LOGGER.error("Addon %s have wrong options -> %s", self._id,
humanize_error(options, ex))
return False
@property
def schema(self):
"""Create a schema for addon options."""
raw_schema = self._mesh[ATTR_SCHEMA]
if isinstance(raw_schema, bool):
return vol.Schema(dict)
return vol.Schema(vol.All(dict, validate_options(raw_schema)))
async def install(self, version=None):
"""Install a addon."""
if self.config.arch not in self.supported_arch:
raise RuntimeError("Addon {} not supported on {}".format(
self._id, self.config.arch))
if self.is_installed:
raise RuntimeError(
"Addon {} is already installed".format(self._id))
if not self.path_data.is_dir():
_LOGGER.info(
"Create Home-Assistant addon data folder %s", self.path_data)
self.path_data.mkdir()
version = version or self.last_version
if not await self.addon_docker.install(version):
return False
self._set_install(version)
return True
async def uninstall(self):
"""Remove a addon."""
if not self.is_installed:
raise RuntimeError("Addon {} is not installed".format(self._id))
if not await self.addon_docker.remove():
return False
if self.path_data.is_dir():
_LOGGER.info(
"Remove Home-Assistant addon data folder %s", self.path_data)
shutil.rmtree(str(self.path_data))
self._set_uninstall()
return True
async def state(self):
"""Return running state of addon."""
if not self.is_installed:
return STATE_NONE
if await self.addon_docker.is_running():
return STATE_STARTED
return STATE_STOPPED
async def start(self):
"""Set options and start addon."""
if not self.is_installed:
raise RuntimeError("Addon {} is not installed".format(self._id))
if not self.write_addon_options():
raise RuntimeError(
"Can't write options for addon {}".format(self._id))
return await self.addon_docker.run()
async def stop(self):
"""Stop addon."""
if not self.is_installed:
raise RuntimeError("Addon {} is not installed".format(self._id))
return await self.addon_docker.stop()
async def update(self, version=None):
"""Update addon."""
if not self.is_installed:
raise RuntimeError("Addon {} is not installed".format(self._id))
version = version or self.last_version
if version == self.version_installed:
raise RuntimeError("Version is already in use")
if not await self.addon_docker.update(version):
return False
self._set_update(version)
return True
async def restart(self):
"""Restart addon."""
if not self.is_installed:
raise RuntimeError("Addon {} is not installed".format(self._id))
if not self.write_addon_options():
raise RuntimeError(
"Can't write options for addon {}".format(self._id))
return await self.addon_docker.restart()
async def logs(self):
"""Return addons log output."""
if not self.is_installed:
raise RuntimeError("Addon {} is not installed".format(self._id))
return await self.addon_docker.logs()

View File

@ -1,12 +1,10 @@
{
"local": {
"slug": "local",
"name": "Local Add-Ons",
"url": "https://home-assistant.io/hassio",
"maintainer": "By our self"
},
"core": {
"slug": "core",
"name": "Built-in Add-Ons",
"url": "https://home-assistant.io/addons",
"maintainer": "Home Assistant authors"

View File

@ -2,7 +2,7 @@
import copy
import logging
import json
from pathlib import Path, PurePath
from pathlib import Path
import re
import voluptuous as vol
@ -10,29 +10,22 @@ from voluptuous.humanize import humanize_error
from .util import extract_hash_from_path
from .validate import (
validate_options, SCHEMA_ADDON_CONFIG, SCHEMA_REPOSITORY_CONFIG,
MAP_VOLUME)
SCHEMA_ADDON_CONFIG, SCHEMA_REPOSITORY_CONFIG, MAP_VOLUME)
from ..const import (
FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON,
ATTR_STARTUP, ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, BOOT_AUTO,
ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY, ATTR_URL, ATTR_ARCH,
ATTR_LOCATON, ATTR_DEVICES, ATTR_ENVIRONMENT, ATTR_HOST_NETWORK,
ATTR_TMPFS, ATTR_PRIVILEGED)
FILE_HASSIO_ADDONS, ATTR_VERSION, ATTR_SLUG, ATTR_REPOSITORY, ATTR_LOCATON,
REPOSITORY_CORE, REPOSITORY_LOCAL)
from ..config import Config
from ..tools import read_json_file, write_json_file
from ..tools import read_json_file
_LOGGER = logging.getLogger(__name__)
SYSTEM = 'system'
USER = 'user'
REPOSITORY_CORE = 'core'
REPOSITORY_LOCAL = 'local'
RE_VOLUME = re.compile(MAP_VOLUME)
class AddonsData(Config):
class Data(Config):
"""Hold data for addons inside HassIO."""
def __init__(self, config):
@ -41,9 +34,8 @@ class AddonsData(Config):
self.config = config
self._system_data = self._data.get(SYSTEM, {})
self._user_data = self._data.get(USER, {})
self._addons_cache = {}
self._cache_data = {}
self._repositories_data = {}
self.arch = None
def save(self):
"""Store data to config file."""
@ -53,9 +45,29 @@ class AddonsData(Config):
}
super().save()
def read_data_from_repositories(self):
@property
def user(self):
"""Return local addon user data."""
return self._user_data
@property
def system(self):
"""Return local addon data."""
return self._system_data
@property
def cache(self):
"""Return addon data from cache/repositories."""
return self._cache_data
@property
def repositories(self):
"""Return addon data from repositories."""
return self._repositories_data
def reload(self):
"""Read data from addons repository."""
self._addons_cache = {}
self._cache_data = {}
self._repositories_data = {}
# read core repository
@ -74,17 +86,19 @@ class AddonsData(Config):
if repository_element.is_dir():
self._read_git_repository(repository_element)
# update local data
self._merge_config()
def _read_git_repository(self, path):
"""Process a custom repository folder."""
slug = extract_hash_from_path(path)
repository_info = {ATTR_SLUG: slug}
# exists repository json
repository_file = Path(path, "repository.json")
try:
repository_info.update(SCHEMA_REPOSITORY_CONFIG(
repository_info = SCHEMA_REPOSITORY_CONFIG(
read_json_file(repository_file)
))
)
except OSError:
_LOGGER.warning("Can't read repository information from %s",
@ -115,7 +129,7 @@ class AddonsData(Config):
# store
addon_config[ATTR_REPOSITORY] = repository
addon_config[ATTR_LOCATON] = str(addon.parent)
self._addons_cache[addon_slug] = addon_config
self._cache_data[addon_slug] = addon_config
except OSError:
_LOGGER.warning("Can't read %s", addon)
@ -133,33 +147,27 @@ class AddonsData(Config):
_LOGGER.warning("Can't read built-in.json -> %s", err)
return
# if core addons are available
for data in self._addons_cache.values():
if data[ATTR_REPOSITORY] == REPOSITORY_CORE:
self._repositories_data[REPOSITORY_CORE] = \
builtin_data[REPOSITORY_CORE]
break
# core repository
self._repositories_data[REPOSITORY_CORE] = \
builtin_data[REPOSITORY_CORE]
# if local addons are available
for data in self._addons_cache.values():
if data[ATTR_REPOSITORY] == REPOSITORY_LOCAL:
self._repositories_data[REPOSITORY_LOCAL] = \
builtin_data[REPOSITORY_LOCAL]
break
# local repository
self._repositories_data[REPOSITORY_LOCAL] = \
builtin_data[REPOSITORY_LOCAL]
def merge_update_config(self):
def _merge_config(self):
"""Update local config if they have update.
It need to be the same version as the local version is.
It need to be the same version as the local version is for merge.
"""
have_change = False
for addon in self.list_installed:
for addon in set(self._system_data):
# detached
if addon not in self._addons_cache:
if addon not in self._cache_data:
continue
cache = self._addons_cache[addon]
cache = self._cache_data[addon]
data = self._system_data[addon]
if data[ATTR_VERSION] == cache[ATTR_VERSION]:
if data != cache:
@ -168,232 +176,3 @@ class AddonsData(Config):
if have_change:
self.save()
@property
def list_installed(self):
"""Return a list of installed addons."""
return set(self._system_data)
@property
def list_all(self):
"""Return a dict of all addons."""
return set(self._system_data) | set(self._addons_cache)
def list_startup(self, start_type):
"""Get list of installed addon with need start by type."""
addon_list = set()
for addon in self._system_data.keys():
if self.get_boot(addon) != BOOT_AUTO:
continue
try:
if self._system_data[addon][ATTR_STARTUP] == start_type:
addon_list.add(addon)
except KeyError:
_LOGGER.warning("Orphaned addon detect %s", addon)
continue
return addon_list
@property
def list_detached(self):
"""Return local addons they not support from repo."""
addon_list = set()
for addon in self._system_data.keys():
if addon not in self._addons_cache:
addon_list.add(addon)
return addon_list
@property
def list_repositories(self):
"""Return list of addon repositories."""
return list(self._repositories_data.values())
def exists_addon(self, addon):
"""Return True if a addon exists."""
return addon in self._addons_cache or addon in self._system_data
def is_installed(self, addon):
"""Return True if a addon is installed."""
return addon in self._system_data
def version_installed(self, addon):
"""Return installed version."""
return self._user_data.get(addon, {}).get(ATTR_VERSION)
def set_addon_install(self, addon, version):
"""Set addon as installed."""
self._system_data[addon] = copy.deepcopy(self._addons_cache[addon])
self._user_data[addon] = {
ATTR_OPTIONS: {},
ATTR_VERSION: version,
}
self.save()
def set_addon_uninstall(self, addon):
"""Set addon as uninstalled."""
self._system_data.pop(addon, None)
self._user_data.pop(addon, None)
self.save()
def set_addon_update(self, addon, version):
"""Update version of addon."""
self._system_data[addon] = copy.deepcopy(self._addons_cache[addon])
self._user_data[addon][ATTR_VERSION] = version
self.save()
def set_options(self, addon, options):
"""Store user addon options."""
self._user_data[addon][ATTR_OPTIONS] = copy.deepcopy(options)
self.save()
def set_boot(self, addon, boot):
"""Store user boot options."""
self._user_data[addon][ATTR_BOOT] = boot
self.save()
def get_options(self, addon):
"""Return options with local changes."""
return {
**self._system_data[addon][ATTR_OPTIONS],
**self._user_data[addon][ATTR_OPTIONS],
}
def get_boot(self, addon):
"""Return boot config with prio local settings."""
if ATTR_BOOT in self._user_data[addon]:
return self._user_data[addon][ATTR_BOOT]
return self._system_data[addon][ATTR_BOOT]
def get_name(self, addon):
"""Return name of addon."""
if addon in self._addons_cache:
return self._addons_cache[addon][ATTR_NAME]
return self._system_data[addon][ATTR_NAME]
def get_description(self, addon):
"""Return description of addon."""
if addon in self._addons_cache:
return self._addons_cache[addon][ATTR_DESCRIPTON]
return self._system_data[addon][ATTR_DESCRIPTON]
def get_repository(self, addon):
"""Return repository of addon."""
if addon in self._addons_cache:
return self._addons_cache[addon][ATTR_REPOSITORY]
return self._system_data[addon][ATTR_REPOSITORY]
def get_last_version(self, addon):
"""Return version of addon."""
if addon in self._addons_cache:
return self._addons_cache[addon][ATTR_VERSION]
return self.version_installed(addon)
def get_ports(self, addon):
"""Return ports of addon."""
return self._system_data[addon].get(ATTR_PORTS)
def get_network_mode(self, addon):
"""Return network mode of addon."""
if self._system_data[addon][ATTR_HOST_NETWORK]:
return 'host'
return 'bridge'
def get_devices(self, addon):
"""Return devices of addon."""
return self._system_data[addon].get(ATTR_DEVICES)
def get_tmpfs(self, addon):
"""Return tmpfs of addon."""
return self._system_data[addon].get(ATTR_TMPFS)
def get_environment(self, addon):
"""Return environment of addon."""
return self._system_data[addon].get(ATTR_ENVIRONMENT)
def get_privileged(self, addon):
"""Return list of privilege."""
return self._system_data[addon].get(ATTR_PRIVILEGED)
def get_url(self, addon):
"""Return url of addon."""
if addon in self._addons_cache:
return self._addons_cache[addon].get(ATTR_URL)
return self._system_data[addon].get(ATTR_URL)
def get_arch(self, addon):
"""Return list of supported arch."""
if addon in self._addons_cache:
return self._addons_cache[addon][ATTR_ARCH]
return self._system_data[addon][ATTR_ARCH]
def get_image(self, addon):
"""Return image name of addon."""
addon_data = self._system_data.get(
addon, self._addons_cache.get(addon)
)
# Repository with dockerhub images
if ATTR_IMAGE in addon_data:
return addon_data[ATTR_IMAGE].format(arch=self.arch)
# local build
return "{}/{}-addon-{}".format(
addon_data[ATTR_REPOSITORY], self.arch, addon_data[ATTR_SLUG])
def need_build(self, addon):
"""Return True if this addon need a local build."""
addon_data = self._system_data.get(
addon, self._addons_cache.get(addon)
)
return ATTR_IMAGE not in addon_data
def map_volumes(self, addon):
"""Return a dict of {volume: policy} from addon."""
volumes = {}
for volume in self._system_data[addon][ATTR_MAP]:
result = RE_VOLUME.match(volume)
volumes[result.group(1)] = result.group(2) or 'ro'
return volumes
def path_data(self, addon):
"""Return addon data path inside supervisor."""
return Path(self.config.path_addons_data, addon)
def path_extern_data(self, addon):
"""Return addon data path external for docker."""
return PurePath(self.config.path_extern_addons_data, addon)
def path_addon_options(self, addon):
"""Return path to addons options."""
return Path(self.path_data(addon), "options.json")
def path_addon_location(self, addon):
"""Return path to this addon."""
return Path(self._addons_cache[addon][ATTR_LOCATON])
def write_addon_options(self, addon):
"""Return True if addon options is written to data."""
schema = self.get_schema(addon)
options = self.get_options(addon)
try:
schema(options)
return write_json_file(self.path_addon_options(addon), options)
except vol.Invalid as ex:
_LOGGER.error("Addon %s have wrong options -> %s", addon,
humanize_error(options, ex))
return False
def get_schema(self, addon):
"""Create a schema for addon options."""
raw_schema = self._system_data[addon][ATTR_SCHEMA]
if isinstance(raw_schema, bool):
return vol.Schema(dict)
return vol.Schema(vol.All(dict, validate_options(raw_schema)))

View File

@ -12,7 +12,7 @@ from ..const import URL_HASSIO_ADDONS
_LOGGER = logging.getLogger(__name__)
class AddonsRepo(object):
class GitRepo(object):
"""Manage addons git repo."""
def __init__(self, config, loop, path, url):
@ -77,7 +77,7 @@ class AddonsRepo(object):
return True
class AddonsRepoHassIO(AddonsRepo):
class GitRepoHassIO(GitRepo):
"""HassIO addons repository."""
def __init__(self, config, loop):
@ -86,7 +86,7 @@ class AddonsRepoHassIO(AddonsRepo):
config, loop, config.path_addons_core, URL_HASSIO_ADDONS)
class AddonsRepoCustom(AddonsRepo):
class GitRepoCustom(GitRepo):
"""Custom addons repository."""
def __init__(self, config, loop, url):

View File

@ -0,0 +1,69 @@
"""Represent a HassIO repository."""
from .git import GitRepoHassIO, GitRepoCustom
from .util import get_hash_from_repository
from ..const import (
REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_NAME, ATTR_URL, ATTR_MAINTAINER)
class Repository(object):
"""Repository in HassIO."""
def __init__(self, config, loop, data, repository):
"""Initialize repository object."""
self.data = data
self.source = None
self.git = None
if repository == REPOSITORY_LOCAL:
self._id = repository
elif repository == REPOSITORY_CORE:
self._id = repository
self.git = GitRepoHassIO(config, loop)
else:
self._id = get_hash_from_repository(repository)
self.git = GitRepoCustom(config, loop, repository)
self.source = repository
@property
def _mesh(self):
"""Return data struct repository."""
return self.data.repositories.get(self._id, {})
@property
def slug(self):
"""Return slug of repository."""
return self._id
@property
def name(self):
"""Return name of repository."""
return self._mesh.get(ATTR_NAME, self.source)
@property
def url(self):
"""Return url of repository."""
return self._mesh.get(ATTR_URL)
@property
def maintainer(self):
"""Return url of repository."""
return self._mesh.get(ATTR_MAINTAINER)
async def load(self):
"""Load addon repository."""
if self.git:
return await self.git.load()
return True
async def update(self):
"""Update addon repository."""
if self.git:
return await self.git.pull()
return True
def remove(self):
"""Remove addon repository."""
if self._id in (REPOSITORY_CORE, REPOSITORY_LOCAL):
raise RuntimeError("Can't remove built-in repositories!")
self.git.remove()

View File

@ -19,8 +19,3 @@ def extract_hash_from_path(path):
if not RE_SHA1.match(repo_dir):
return get_hash_from_repository(repo_dir)
return repo_dir
def create_hash_index_list(name_list):
"""Create a dict with hash from repositories list."""
return {get_hash_from_repository(repo): repo for repo in name_list}

View File

@ -3,13 +3,12 @@ import asyncio
import logging
import voluptuous as vol
from voluptuous.humanize import humanize_error
from .util import api_process, api_process_raw, api_validate
from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
ATTR_URL, ATTR_DESCRIPTON, ATTR_DETACHED, ATTR_NAME, ATTR_REPOSITORY,
ATTR_BUILD, STATE_STOPPED, STATE_STARTED, BOOT_AUTO, BOOT_MANUAL)
ATTR_BUILD, BOOT_AUTO, BOOT_MANUAL)
_LOGGER = logging.getLogger(__name__)
@ -31,15 +30,11 @@ class APIAddons(object):
self.loop = loop
self.addons = addons
def _extract_addon(self, request, check_installed=True):
def _extract_addon(self, request):
"""Return addon and if not exists trow a exception."""
addon = request.match_info.get('addon')
# check data
if not self.addons.exists_addon(addon):
addon = self.addons.get(request.match_info.get('addon'))
if not addon:
raise RuntimeError("Addon not exists")
if check_installed and not self.addons.is_installed(addon):
raise RuntimeError("Addon is not installed")
return addon
@ -49,35 +44,37 @@ class APIAddons(object):
addon = self._extract_addon(request)
return {
ATTR_NAME: self.addons.get_name(addon),
ATTR_DESCRIPTON: self.addons.get_description(addon),
ATTR_VERSION: self.addons.version_installed(addon),
ATTR_REPOSITORY: self.addons.get_repository(addon),
ATTR_LAST_VERSION: self.addons.get_last_version(addon),
ATTR_STATE: await self.addons.state(addon),
ATTR_BOOT: self.addons.get_boot(addon),
ATTR_OPTIONS: self.addons.get_options(addon),
ATTR_URL: self.addons.get_url(addon),
ATTR_DETACHED: addon in self.addons.list_detached,
ATTR_BUILD: self.addons.need_build(addon),
ATTR_NAME: addon.name,
ATTR_DESCRIPTON: addon.description,
ATTR_VERSION: addon.version_installed,
ATTR_REPOSITORY: addon.repository,
ATTR_LAST_VERSION: addon.last_version,
ATTR_STATE: await addon.state(),
ATTR_BOOT: addon.boot,
ATTR_OPTIONS: addon.options,
ATTR_URL: addon.url,
ATTR_DETACHED: addon.is_detached,
ATTR_BUILD: addon.need_build,
}
@api_process
async def options(self, request):
"""Store user options for addon."""
addon = self._extract_addon(request)
options_schema = self.addons.get_schema(addon)
if not addon.is_installed:
raise RuntimeError("Addon {} is not installed!".format(addon.slug))
addon_schema = SCHEMA_OPTIONS.extend({
vol.Optional(ATTR_OPTIONS): options_schema,
vol.Optional(ATTR_OPTIONS): addon.schema,
})
body = await api_validate(addon_schema, request)
if ATTR_OPTIONS in body:
self.addons.set_options(addon, body[ATTR_OPTIONS])
addon.options = body[ATTR_OPTIONS]
if ATTR_BOOT in body:
self.addons.set_boot(addon, body[ATTR_BOOT])
addon.boot = body[ATTR_BOOT]
return True
@ -85,78 +82,49 @@ class APIAddons(object):
async def install(self, request):
"""Install addon."""
body = await api_validate(SCHEMA_VERSION, request)
addon = self._extract_addon(request, check_installed=False)
version = body.get(
ATTR_VERSION, self.addons.get_last_version(addon))
# check if arch supported
if self.addons.arch not in self.addons.get_arch(addon):
raise RuntimeError(
"Addon is not supported on {}".format(self.addons.arch))
addon = self._extract_addon(request)
version = body.get(ATTR_VERSION)
return await asyncio.shield(
self.addons.install(addon, version), loop=self.loop)
addon.install(version=version), loop=self.loop)
@api_process
async def uninstall(self, request):
"""Uninstall addon."""
addon = self._extract_addon(request)
return await asyncio.shield(
self.addons.uninstall(addon), loop=self.loop)
return await asyncio.shield(addon.uninstall(), loop=self.loop)
@api_process
async def start(self, request):
"""Start addon."""
addon = self._extract_addon(request)
if await self.addons.state(addon) == STATE_STARTED:
raise RuntimeError("Addon is already running")
# validate options
try:
schema = self.addons.get_schema(addon)
options = self.addons.get_options(addon)
schema(options)
except vol.Invalid as ex:
raise RuntimeError(humanize_error(options, ex)) from None
return await asyncio.shield(
self.addons.start(addon), loop=self.loop)
return await asyncio.shield(addon.start(), loop=self.loop)
@api_process
async def stop(self, request):
"""Stop addon."""
addon = self._extract_addon(request)
if await self.addons.state(addon) == STATE_STOPPED:
raise RuntimeError("Addon is already stoped")
return await asyncio.shield(
self.addons.stop(addon), loop=self.loop)
return await asyncio.shield(addon.stop(), loop=self.loop)
@api_process
async def update(self, request):
"""Update addon."""
body = await api_validate(SCHEMA_VERSION, request)
addon = self._extract_addon(request)
version = body.get(
ATTR_VERSION, self.addons.get_last_version(addon))
if version == self.addons.version_installed(addon):
raise RuntimeError("Version is already in use")
version = body.get(ATTR_VERSION)
return await asyncio.shield(
self.addons.update(addon, version), loop=self.loop)
addon.update(version=version), loop=self.loop)
@api_process
async def restart(self, request):
"""Restart addon."""
addon = self._extract_addon(request)
return await asyncio.shield(self.addons.restart(addon), loop=self.loop)
return await asyncio.shield(addon.restart(), loop=self.loop)
@api_process_raw
def logs(self, request):
"""Return logs from addon."""
addon = self._extract_addon(request)
return self.addons.logs(addon)
return addon.logs()

View File

@ -5,7 +5,6 @@ import logging
import voluptuous as vol
from .util import api_process, api_process_raw, api_validate
from ..addons.util import create_hash_index_list
from ..const import (
ATTR_ADDONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_BETA_CHANNEL,
HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_REPOSITORIES,
@ -41,26 +40,22 @@ class APISupervisor(object):
def _addons_list(self, only_installed=False):
"""Return a list of addons."""
detached = self.addons.list_detached
if only_installed:
addons = self.addons.list_installed
else:
addons = self.addons.list_all
data = []
for addon in addons:
for addon in self.addons.list_addons:
if only_installed and not addon.is_installed:
continue
data.append({
ATTR_NAME: self.addons.get_name(addon),
ATTR_SLUG: addon,
ATTR_DESCRIPTON: self.addons.get_description(addon),
ATTR_VERSION: self.addons.get_last_version(addon),
ATTR_INSTALLED: self.addons.version_installed(addon),
ATTR_ARCH: self.addons.get_arch(addon),
ATTR_DETACHED: addon in detached,
ATTR_REPOSITORY: self.addons.get_repository(addon),
ATTR_BUILD: self.addons.need_build(addon),
ATTR_URL: self.addons.get_url(addon),
ATTR_NAME: addon.name,
ATTR_SLUG: addon.slug,
ATTR_DESCRIPTON: addon.description,
ATTR_VERSION: addon.last_version,
ATTR_INSTALLED: addon.version_installed,
ATTR_ARCH: addon.supported_arch,
ATTR_DETACHED: addon.is_detached,
ATTR_REPOSITORY: addon.repository,
ATTR_BUILD: addon.need_build,
ATTR_URL: addon.url,
})
return data
@ -68,15 +63,13 @@ class APISupervisor(object):
def _repositories_list(self):
"""Return a list of addons repositories."""
data = []
list_id = create_hash_index_list(self.config.addons_repositories)
for repository in self.addons.list_repositories:
data.append({
ATTR_SLUG: repository[ATTR_SLUG],
ATTR_NAME: repository[ATTR_NAME],
ATTR_SOURCE: list_id.get(repository[ATTR_SLUG]),
ATTR_URL: repository.get(ATTR_URL),
ATTR_MAINTAINER: repository.get(ATTR_MAINTAINER),
ATTR_SLUG: repository.slug,
ATTR_NAME: repository.name,
ATTR_SOURCE: repository.source,
ATTR_URL: repository.url,
ATTR_MAINTAINER: repository.maintainer,
})
return data
@ -93,7 +86,7 @@ class APISupervisor(object):
ATTR_VERSION: HASSIO_VERSION,
ATTR_LAST_VERSION: self.config.last_hassio,
ATTR_BETA_CHANNEL: self.config.upstream_beta,
ATTR_ARCH: self.addons.arch,
ATTR_ARCH: self.config.arch,
ATTR_TIMEZONE: self.config.timezone,
ATTR_ADDONS: self._addons_list(only_installed=True),
ATTR_ADDONS_REPOSITORIES: self.config.addons_repositories,
@ -120,21 +113,7 @@ class APISupervisor(object):
if ATTR_ADDONS_REPOSITORIES in body:
new = set(body[ATTR_ADDONS_REPOSITORIES])
old = set(self.config.addons_repositories)
# add new repositories
tasks = [self.addons.add_git_repository(url) for url in
set(new - old)]
if tasks:
await asyncio.shield(
asyncio.wait(tasks, loop=self.loop), loop=self.loop)
# remove old repositories
for url in set(old - new):
self.addons.drop_git_repository(url)
# read repository
self.addons.read_data_from_repositories()
await asyncio.shield(self.addons.load_repositories(new))
return True

View File

@ -81,6 +81,8 @@ def api_process_raw(method):
def api_return_error(message=None):
"""Return a API error message."""
_LOGGER.error(message)
return web.json_response({
JSON_RESULT: RESULT_ERROR,
JSON_MESSAGE: message,

View File

@ -91,6 +91,7 @@ class CoreConfig(Config):
def __init__(self, websession):
"""Initialize config object."""
self.websession = websession
self.arch = None
super().__init__(FILE_HASSIO_CONFIG)

View File

@ -92,6 +92,7 @@ BOOT_MANUAL = 'manual'
STATE_STARTED = 'started'
STATE_STOPPED = 'stopped'
STATE_NONE = 'none'
MAP_CONFIG = 'config'
MAP_SSL = 'ssl'
@ -103,3 +104,6 @@ ARCH_ARMHF = 'armhf'
ARCH_AARCH64 = 'aarch64'
ARCH_AMD64 = 'amd64'
ARCH_I386 = 'i386'
REPOSITORY_CORE = 'core'
REPOSITORY_LOCAL = 'local'

View File

@ -1,5 +1,4 @@
"""Main file for HassIO."""
import asyncio
import logging
import aiohttp
@ -20,7 +19,7 @@ from .dock.supervisor import DockerSupervisor
from .tasks import (
hassio_update, homeassistant_watchdog, homeassistant_setup,
api_sessions_cleanup)
from .tools import get_arch_from_image, get_local_ip, fetch_timezone
from .tools import get_local_ip, fetch_timezone
_LOGGER = logging.getLogger(__name__)
@ -58,6 +57,9 @@ class HassIO(object):
_LOGGER.fatal("Can't attach to supervisor docker container!")
await self.supervisor.cleanup()
# set running arch
self.config.arch = self.supervisor.arch
# set api endpoint
self.config.api_endpoint = await get_local_ip(self.loop)
@ -101,8 +103,7 @@ class HassIO(object):
await self.homeassistant.attach()
# Load addons
arch = get_arch_from_image(self.supervisor.image)
await self.addons.prepare(arch)
await self.addons.prepare()
# schedule addon update task
self.scheduler.register_task(
@ -148,9 +149,9 @@ class HassIO(object):
# don't process scheduler anymore
self.scheduler.stop()
# process stop task pararell
tasks = [self.websession.close(), self.api.stop()]
await asyncio.wait(tasks, loop=self.loop)
# process stop tasks
self.websession.close()
await self.api.stop()
self.exit_code = exit_code
self.loop.stop()

View File

@ -5,7 +5,7 @@ import logging
import docker
from ..const import LABEL_VERSION
from ..const import LABEL_VERSION, LABEL_ARCH
_LOGGER = logging.getLogger(__name__)
@ -20,6 +20,7 @@ class DockerBase(object):
self.dock = dock
self.image = image
self.version = None
self.arch = None
self._lock = asyncio.Lock(loop=loop)
@property
@ -38,13 +39,18 @@ class DockerBase(object):
if not self.image:
self.image = metadata['Config']['Image']
# read metadata
# read version
need_version = force or not self.version
if need_version and LABEL_VERSION in metadata['Config']['Labels']:
self.version = metadata['Config']['Labels'][LABEL_VERSION]
elif need_version:
_LOGGER.warning("Can't read version from %s", self.name)
# read arch
need_arch = force or not self.arch
if need_arch and LABEL_ARCH in metadata['Config']['Labels']:
self.arch = metadata['Config']['Labels'][LABEL_ARCH]
async def install(self, tag):
"""Pull docker image."""
if self._lock.locked():
@ -187,7 +193,7 @@ class DockerBase(object):
if container.status == 'running':
with suppress(docker.errors.DockerException):
container.stop()
container.stop(timeout=15)
with suppress(docker.errors.DockerException):
container.remove(force=True)

View File

@ -1,4 +1,5 @@
"""Init file for HassIO addon docker object."""
from contextlib import suppress
import logging
from pathlib import Path
import shutil
@ -16,22 +17,21 @@ _LOGGER = logging.getLogger(__name__)
class DockerAddon(DockerBase):
"""Docker hassio wrapper for HomeAssistant."""
def __init__(self, config, loop, dock, addons_data, addon):
def __init__(self, config, loop, dock, addon):
"""Initialize docker homeassistant wrapper."""
super().__init__(
config, loop, dock, image=addons_data.get_image(addon))
config, loop, dock, image=addon.image)
self.addon = addon
self.addons_data = addons_data
@property
def name(self):
"""Return name of docker container."""
return "addon_{}".format(self.addon)
return "addon_{}".format(self.addon.slug)
@property
def environment(self):
"""Return environment for docker add-on."""
addon_env = self.addons_data.get_environment(self.addon) or {}
addon_env = self.addon.environment or {}
return {
**addon_env,
@ -41,7 +41,7 @@ class DockerAddon(DockerBase):
@property
def tmpfs(self):
"""Return tmpfs for docker add-on."""
options = self.addons_data.get_tmpfs(self.addon)
options = self.addon.tmpfs
if options:
return {"/tmpfs": "{}".format(options)}
return None
@ -50,11 +50,11 @@ class DockerAddon(DockerBase):
def volumes(self):
"""Generate volumes for mappings."""
volumes = {
str(self.addons_data.path_extern_data(self.addon)): {
str(self.addon.path_extern_data): {
'bind': '/data', 'mode': 'rw'
}}
addon_mapping = self.addons_data.map_volumes(self.addon)
addon_mapping = self.addon.map_volumes
if MAP_CONFIG in addon_mapping:
volumes.update({
@ -104,10 +104,10 @@ class DockerAddon(DockerBase):
self.image,
name=self.name,
detach=True,
network_mode=self.addons_data.get_network_mode(self.addon),
ports=self.addons_data.get_ports(self.addon),
devices=self.addons_data.get_devices(self.addon),
cap_add=self.addons_data.get_privileged(self.addon),
network_mode=self.addon.network_mode,
ports=self.addon.ports,
devices=self.addon.devices,
cap_add=self.addon.privileged,
environment=self.environment,
volumes=self.volumes,
tmpfs=self.tmpfs
@ -126,7 +126,7 @@ class DockerAddon(DockerBase):
Need run inside executor.
"""
if self.addons_data.need_build(self.addon):
if self.addon.need_build:
return self._build(tag)
return super()._install(tag)
@ -145,11 +145,11 @@ class DockerAddon(DockerBase):
Need run inside executor.
"""
build_dir = Path(self.config.path_addons_build, self.addon)
build_dir = Path(self.config.path_addons_build, self.addon.slug)
try:
# prepare temporary addon build folder
try:
source = self.addons_data.path_addon_location(self.addon)
source = self.addon.path_addon_location
shutil.copytree(str(source), str(build_dir))
except shutil.Error as err:
_LOGGER.error("Can't copy %s to temporary build folder -> %s",
@ -159,7 +159,7 @@ class DockerAddon(DockerBase):
# prepare Dockerfile
try:
dockerfile_template(
Path(build_dir, 'Dockerfile'), self.addons_data.arch,
Path(build_dir, 'Dockerfile'), self.config.arch,
tag, META_ADDON)
except OSError as err:
_LOGGER.error("Can't prepare dockerfile -> %s", err)
@ -184,3 +184,21 @@ class DockerAddon(DockerBase):
finally:
shutil.rmtree(str(build_dir), ignore_errors=True)
def _restart(self):
"""Restart docker container.
Addons prepare some thing on start and that is normaly not repeatable.
Need run inside executor.
"""
try:
container = self.dock.containers.get(self.name)
except docker.errors.DockerException:
return False
_LOGGER.info("Restart %s", self.image)
with suppress(docker.errors.DockerException):
container.stop(timeout=15)
return self._run()

View File

@ -3,7 +3,6 @@ import asyncio
from contextlib import suppress
import json
import logging
import re
import socket
import aiohttp
@ -17,9 +16,6 @@ _LOGGER = logging.getLogger(__name__)
FREEGEOIP_URL = "https://freegeoip.io/json/"
_RE_VERSION = re.compile(r"VERSION=(.*)")
_IMAGE_ARCH = re.compile(r".*/([a-z0-9]*)-hassio-supervisor")
async def fetch_last_versions(websession, beta=False):
"""Fetch current versions from github.
@ -39,13 +35,6 @@ async def fetch_last_versions(websession, beta=False):
_LOGGER.warning("Can't parse versions from %s! %s", url, err)
def get_arch_from_image(image):
"""Return arch from hassio image name."""
found = _IMAGE_ARCH.match(image)
if found:
return found.group(1)
def get_local_ip(loop):
"""Retrieve local IP address.