From a028f11a4a514916b84803d2ca0a3ec6556ffe02 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 25 Apr 2017 18:36:40 +0200 Subject: [PATCH] Change handling of addon config (#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional extended description… --- API.md | 6 +- hassio/addons/__init__.py | 18 +++--- hassio/addons/data.py | 108 ++++++++++++++++++++--------------- hassio/api/addons.py | 21 +++++-- hassio/const.py | 3 +- hassio/dock/__init__.py | 4 -- hassio/dock/addon.py | 2 +- hassio/dock/homeassistant.py | 13 +++++ 8 files changed, 109 insertions(+), 66 deletions(-) diff --git a/API.md b/API.md index ad93c8f30..870e26d50 100644 --- a/API.md +++ b/API.md @@ -37,6 +37,7 @@ On success "slug": "xy", "version": "CURRENT_VERSION", "installed": "none|INSTALL_VERSION", + "dedicated": "bool", "description": "description" } ] @@ -146,7 +147,10 @@ Output the raw docker log - `/addons/{addon}/options` ```json -{ } +{ + "boot": "auto|manual", + "options": {}, +} ``` - `/addons/{addon}/start` diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index 7b10c99a1..fc2a6ca69 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -45,13 +45,8 @@ class AddonManager(AddonsData): self.read_addons_repo() # remove stalled addons - tasks = [] for addon in self.list_removed: - _LOGGER.info("Old addon %s found") - tasks.append(self.loop.create_task(self.uninstall(addon))) - - if tasks: - await asyncio.wait(tasks, loop=self.loop) + _LOGGER.warning("Dedicated addon '%s' found!", addon) async def auto_boot(self, start_type): """Boot addons with mode auto.""" @@ -88,7 +83,7 @@ class AddonManager(AddonsData): return False self.dockers[addon] = addon_docker - self.set_install_addon(addon, version) + self.set_addon_install(addon, version) return True async def uninstall(self, addon): @@ -110,7 +105,7 @@ class AddonManager(AddonsData): shutil.rmtree(self.path_data(addon)) self.dockers.pop(addon) - self.set_uninstall_addon(addon) + self.set_addon_uninstall(addon) return True async def state(self, addon): @@ -150,8 +145,13 @@ class AddonManager(AddonsData): return False version = version or self.get_version(addon) + is_running = self.dockers[addon].is_running() + + # update if await self.dockers[addon].update(version): - self.set_version(addon, version) + self.set_addon_update(addon, version) + if is_running: + await self.start(addon) return True return False diff --git a/hassio/addons/data.py b/hassio/addons/data.py index d7de29d72..96ca4a097 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -10,13 +10,15 @@ from ..const import ( FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_STARTUP, ATTR_BOOT, ATTR_MAP_SSL, ATTR_MAP_CONFIG, ATTR_OPTIONS, ATTR_PORTS, BOOT_AUTO, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA, - ATTR_IMAGE) + ATTR_IMAGE, ATTR_DEDICATED) from ..config import Config from ..tools import read_json_file, write_json_file _LOGGER = logging.getLogger(__name__) ADDONS_REPO_PATTERN = "{}/*/config.json" +SYSTEM = "system" +USER = "user" class AddonsData(Config): @@ -26,12 +28,22 @@ class AddonsData(Config): """Initialize data holder.""" super().__init__(FILE_HASSIO_ADDONS) self.config = config - self._addons_data = {} + self._addons_data = self._data.get(SYSTEM, {}) + self._user_data = self._data.get(USER, {}) + self._current_data = {} self.arch = None + def save(self): + """Store data to config file.""" + self._data = { + USER: self._user_data, + SYSTEM: self._addons_data, + } + super().save() + def read_addons_repo(self): """Read data from addons repository.""" - self._addons_data = {} + self._current_data = {} self._read_addons_folder(self.config.path_addons_repo) self._read_addons_folder(self.config.path_addons_custom) @@ -45,7 +57,7 @@ class AddonsData(Config): addon_config = read_json_file(addon) addon_config = SCHEMA_ADDON_CONFIG(addon_config) - self._addons_data[addon_config[ATTR_SLUG]] = addon_config + self._current_data[addon_config[ATTR_SLUG]] = addon_config except (OSError, KeyError): _LOGGER.warning("Can't read %s", addon) @@ -57,24 +69,25 @@ class AddonsData(Config): @property def list_installed(self): """Return a list of installed addons.""" - return set(self._data.keys()) - - @property - def list_all(self): - """Return a list of available addons.""" return set(self._addons_data.keys()) @property def list(self): """Return a list of available addons.""" data = [] - for addon, values in self._addons_data.items(): + all_addons = {**self._addons_data, **self._current_data} + dedicated = self.list_removed + + for addon, values in all_addons.items(): + i_version = self._addons_data.get(addon, {}).get(ATTR_VERSION) + data.append({ ATTR_NAME: values[ATTR_NAME], ATTR_SLUG: values[ATTR_SLUG], ATTR_DESCRIPTON: values[ATTR_DESCRIPTON], ATTR_VERSION: values[ATTR_VERSION], - ATTR_INSTALLED: self._data.get(addon, {}).get(ATTR_VERSION), + ATTR_INSTALLED: i_version, + ATTR_DEDICATED: addon in dedicated, }) return data @@ -82,7 +95,7 @@ class AddonsData(Config): def list_startup(self, start_type): """Get list of installed addon with need start by type.""" addon_list = set() - for addon in self._data.keys(): + for addon in self._addons_data.keys(): if self.get_boot(addon) != BOOT_AUTO: continue @@ -99,58 +112,64 @@ class AddonsData(Config): def list_removed(self): """Return local addons they not support from repo.""" addon_list = set() - for addon in self._data.keys(): - if addon not in self._addons_data: + for addon in self._addons_data.keys(): + if addon not in self._current_data: addon_list.add(addon) return addon_list def exists_addon(self, addon): """Return True if a addon exists.""" - return addon in self._addons_data + return addon in self._current_data or addon in self._addons_data def is_installed(self, addon): """Return True if a addon is installed.""" - return addon in self._data + return addon in self._addons_data def version_installed(self, addon): """Return installed version.""" - return self._data[addon][ATTR_VERSION] + return self._addons_data[addon][ATTR_VERSION] - def set_install_addon(self, addon, version): + def set_addon_install(self, addon, version): """Set addon as installed.""" - self._data[addon] = { - ATTR_VERSION: version, - ATTR_OPTIONS: {} + self._addons_data[addon] = self._current_data[addon] + self._user_data[addon] = { + ATTR_OPTIONS: {}, } self.save() - def set_uninstall_addon(self, addon): + def set_addon_uninstall(self, addon): """Set addon as uninstalled.""" - self._data.pop(addon, None) + self._addons_data.pop(addon, None) + self._user_data.pop(addon, None) + self.save() + + def set_addon_update(self, addon, version): + """Update version of addon.""" + self._addons_data[addon] = self._current_data[addon] self.save() def set_options(self, addon, options): """Store user addon options.""" - self._data[addon][ATTR_OPTIONS] = options + self._user_data[addon][ATTR_OPTIONS] = options self.save() - def set_version(self, addon, version): - """Update version of addon.""" - self._data[addon][ATTR_VERSION] = version + 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.""" - opt = self._addons_data[addon][ATTR_OPTIONS] - if addon in self._data: - opt.update(self._data[addon][ATTR_OPTIONS]) - return opt + return { + **self._addons_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._data[addon]: - return self._data[addon][ATTR_BOOT] + if ATTR_BOOT in self._user_data[addon]: + return self._user_data[addon][ATTR_BOOT] return self._addons_data[addon][ATTR_BOOT] @@ -164,11 +183,9 @@ class AddonsData(Config): def get_version(self, addon): """Return version of addon.""" - return self._addons_data[addon][ATTR_VERSION] - - def get_slug(self, addon): - """Return slug of addon.""" - return self._addons_data[addon][ATTR_SLUG] + if addon not in self._current_data: + return self.version_installed(addon) + return self._current_data[addon][ATTR_VERSION] def get_ports(self, addon): """Return ports of addon.""" @@ -176,11 +193,12 @@ class AddonsData(Config): def get_image(self, addon): """Return image name of addon.""" - if ATTR_IMAGE not in self._addons_data[addon]: - return "{}/{}-addon-{}".format( - DOCKER_REPO, self.arch, self.get_slug(addon)) + addon_data = self._addons_data.get(addon, self._current_data[addon]) - return self._addons_data[addon][ATTR_IMAGE] + if ATTR_IMAGE not in addon_data: + return "{}/{}-addon-{}".format(DOCKER_REPO, self.arch, addon) + + return addon_data[ATTR_IMAGE] def need_config(self, addon): """Return True if config map is needed.""" @@ -192,13 +210,11 @@ class AddonsData(Config): def path_data(self, addon): """Return addon data path inside supervisor.""" - return "{}/{}".format( - self.config.path_addons_data, self._addons_data[addon][ATTR_SLUG]) + return "{}/{}".format(self.config.path_addons_data, addon) def path_data_docker(self, addon): """Return addon data path external for docker.""" - return "{}/{}".format(self.config.path_addons_data_docker, - self._addons_data[addon][ATTR_SLUG]) + return "{}/{}".format(self.config.path_addons_data_docker, addon) def path_addon_options(self, addon): """Return path to addons options.""" diff --git a/hassio/api/addons.py b/hassio/api/addons.py index 8b4f3bf0e..71cfb6452 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -8,7 +8,7 @@ from voluptuous.humanize import humanize_error from .util import api_process, api_process_raw, api_validate from ..const import ( ATTR_VERSION, ATTR_CURRENT, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS, - STATE_STOPPED, STATE_STARTED) + STATE_STOPPED, STATE_STARTED, BOOT_AUTO, BOOT_MANUAL) _LOGGER = logging.getLogger(__name__) @@ -16,6 +16,10 @@ SCHEMA_VERSION = vol.Schema({ vol.Optional(ATTR_VERSION): vol.Coerce(str), }) +SCHEMA_OPTIONS = vol.Schema({ + vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]) +}) + class APIAddons(object): """Handle rest api for addons functions.""" @@ -56,10 +60,19 @@ class APIAddons(object): async def options(self, request): """Store user options for addon.""" addon = self._extract_addon(request) - schema = self.addons.get_schema(addon) + options_schema = self.addons.get_schema(addon) + + addon_schema = SCHEMA_OPTIONS.extend({ + vol.Optional(ATTR_OPTIONS): options_schema, + }) + + addon_config = await api_validate(addon_schema, request) + + if ATTR_OPTIONS in addon_config: + self.addons.set_options(addon, addon_config[ATTR_OPTIONS]) + if ATTR_BOOT in addon_config: + self.addons.set_options(addon, addon_config[ATTR_BOOT]) - options = await api_validate(schema, request) - self.addons.set_options(addon, options) return True @api_process diff --git a/hassio/const.py b/hassio/const.py index a200e7405..3d7fbf562 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -1,5 +1,5 @@ """Const file for HassIO.""" -HASSIO_VERSION = '0.12' +HASSIO_VERSION = '0.13' URL_HASSIO_VERSION = \ 'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json' @@ -45,6 +45,7 @@ ATTR_MAP_CONFIG = 'map_config' ATTR_MAP_SSL = 'map_ssl' ATTR_OPTIONS = 'options' ATTR_INSTALLED = 'installed' +ATTR_DEDICATED = 'dedicated' ATTR_STATE = 'state' ATTR_SCHEMA = 'schema' ATTR_IMAGE = 'image' diff --git a/hassio/dock/__init__.py b/hassio/dock/__init__.py index 94184e038..2339d3917 100644 --- a/hassio/dock/__init__.py +++ b/hassio/dock/__init__.py @@ -223,7 +223,6 @@ class DockerBase(object): Need run inside executor. """ - old_run = self._is_running() old_image = "{}:{}".format(self.image, self.version) _LOGGER.info("Update docker %s with %s:%s", @@ -238,9 +237,6 @@ class DockerBase(object): except docker.errors.DockerException as err: _LOGGER.warning( "Can't remove old image %s -> %s", old_image, err) - # restore - if old_run: - self._run() return True return False diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 64b9d41d2..0163faef4 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -24,7 +24,7 @@ class DockerAddon(DockerBase): @property def docker_name(self): """Return name of docker container.""" - return "addon_{}".format(self.addons_data.get_slug(self.addon)) + return "addon_{}".format(self.addon) def _run(self): """Run docker image. diff --git a/hassio/dock/homeassistant.py b/hassio/dock/homeassistant.py index 0a0d1ecd7..635e43af4 100644 --- a/hassio/dock/homeassistant.py +++ b/hassio/dock/homeassistant.py @@ -62,3 +62,16 @@ class DockerHomeAssistant(DockerBase): return False return True + + async def update(self, tag): + """Update homeassistant docker image.""" + if self._lock.locked(): + _LOGGER.error("Can't excute update while a task is in progress") + return False + + async with self._lock: + if await self.loop.run_in_executor(None, self._update, tag): + await self.loop.run_in_executor(None, self._run) + return True + + return False