From f9c7371140bbdd547fb439a312ebc91f9a9dd80c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 19 Apr 2017 17:07:24 +0200 Subject: [PATCH] Extend addons options to allow lists (#5) Extend addons options to allow lists --- hassio/addons/__init__.py | 2 +- hassio/addons/data.py | 75 +++++++------------------ hassio/addons/validate.py | 113 ++++++++++++++++++++++++++++++++++++++ hassio/api/addons.py | 9 +++ hassio/config.py | 11 +++- hassio/const.py | 3 +- hassio/dock/__init__.py | 2 + hassio/dock/addon.py | 5 ++ hassio/dock/supervisor.py | 3 + version_beta.json | 2 +- 10 files changed, 165 insertions(+), 60 deletions(-) create mode 100644 hassio/addons/validate.py diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index d250de166..9fcf9370f 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -47,7 +47,7 @@ class AddonManager(AddonsData): tasks = [] for addon in self.list_removed: _LOGGER.info("Old addon %s found") - tasks.append(self.loop.create_task(self.dockers[addon].remove())) + tasks.append(self.loop.create_task(self.uninstall(addon))) if tasks: await asyncio.wait(tasks, loop=self.loop) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 846d693ec..acb00930a 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -5,11 +5,12 @@ import glob import voluptuous as vol from voluptuous.humanize import humanize_error +from .validate import validate_options, SCHEMA_ADDON_CONFIG 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, STARTUP_ONCE, STARTUP_AFTER, STARTUP_BEFORE, BOOT_AUTO, - BOOT_MANUAL, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA, ATTR_IMAGE) + ATTR_PORTS, BOOT_AUTO, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA, + ATTR_IMAGE, ATTR_MAP_HASSIO) from ..config import Config from ..tools import read_json_file, write_json_file @@ -17,32 +18,6 @@ _LOGGER = logging.getLogger(__name__) ADDONS_REPO_PATTERN = "{}/*/config.json" -V_STR = 'str' -V_INT = 'int' -V_FLOAT = 'float' -V_BOOL = 'bool' - - -# pylint: disable=no-value-for-parameter -SCHEMA_ADDON_CONFIG = vol.Schema({ - vol.Required(ATTR_NAME): vol.Coerce(str), - vol.Required(ATTR_VERSION): vol.Coerce(str), - vol.Required(ATTR_SLUG): vol.Coerce(str), - vol.Required(ATTR_DESCRIPTON): vol.Coerce(str), - vol.Required(ATTR_STARTUP): - vol.In([STARTUP_BEFORE, STARTUP_AFTER, STARTUP_ONCE]), - vol.Required(ATTR_BOOT): - vol.In([BOOT_AUTO, BOOT_MANUAL]), - vol.Optional(ATTR_PORTS): dict, - vol.Required(ATTR_MAP_CONFIG): vol.Boolean(), - vol.Required(ATTR_MAP_SSL): vol.Boolean(), - vol.Required(ATTR_OPTIONS): dict, - vol.Required(ATTR_SCHEMA): { - vol.Coerce(str): vol.In([V_STR, V_INT, V_FLOAT, V_BOOL]) - }, - vol.Optional(ATTR_IMAGE): vol.Match(r"\w*/\w*"), -}) - class AddonsData(Config): """Hold data for addons inside HassIO.""" @@ -56,6 +31,8 @@ class AddonsData(Config): def read_addons_repo(self): """Read data from addons repository.""" + self._addons_data = {} + self._read_addons_folder(self.config.path_addons_repo) self._read_addons_folder(self.config.path_addons_custom) @@ -213,6 +190,10 @@ class AddonsData(Config): """Return True if ssl map is needed.""" return self._addons_data[addon][ATTR_MAP_SSL] + def need_hassio(self, addon): + """Return True if hassio map is needed.""" + return self._addons_data[addon][ATTR_MAP_HASSIO] + def path_data(self, addon): """Return addon data path inside supervisor.""" return "{}/{}".format( @@ -229,35 +210,21 @@ class AddonsData(Config): def write_addon_options(self, addon): """Return True if addon options is written to data.""" - return write_json_file( - self.path_addon_options(addon), self.get_options(addon)) + 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._addons_data[addon][ATTR_SCHEMA] - def validate(struct): - """Validate schema.""" - options = {} - for key, value in struct.items(): - if key not in raw_schema: - raise vol.Invalid("Unknown options {}.".format(key)) - - typ = raw_schema[key] - try: - if typ == V_STR: - options[key] = str(value) - elif typ == V_INT: - options[key] = int(value) - elif typ == V_FLOAT: - options[key] = float(value) - elif typ == V_BOOL: - options[key] = vol.Boolean()(value) - except TypeError: - raise vol.Invalid( - "Type error for {}.".format(key)) from None - - return options - - schema = vol.Schema(vol.All(dict(), validate)) + schema = vol.Schema(vol.All(dict, validate_options(raw_schema))) return schema diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py new file mode 100644 index 000000000..2bd46f8de --- /dev/null +++ b/hassio/addons/validate.py @@ -0,0 +1,113 @@ +"""Validate addons options schema.""" +import voluptuous as vol + +from ..const import ( + ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_STARTUP, + ATTR_BOOT, ATTR_MAP_SSL, ATTR_MAP_CONFIG, ATTR_OPTIONS, + ATTR_PORTS, STARTUP_ONCE, STARTUP_AFTER, STARTUP_BEFORE, BOOT_AUTO, + BOOT_MANUAL, ATTR_SCHEMA, ATTR_IMAGE, ATTR_MAP_HASSIO) + +V_STR = 'str' +V_INT = 'int' +V_FLOAT = 'float' +V_BOOL = 'bool' +V_EMAIL = 'email' +V_URL = 'url' + +ADDON_ELEMENT = vol.In([V_STR, V_INT, V_FLOAT, V_BOOL, V_EMAIL, V_URL]) + +# pylint: disable=no-value-for-parameter +SCHEMA_ADDON_CONFIG = vol.Schema({ + vol.Required(ATTR_NAME): vol.Coerce(str), + vol.Required(ATTR_VERSION): vol.Coerce(str), + vol.Required(ATTR_SLUG): vol.Coerce(str), + vol.Required(ATTR_DESCRIPTON): vol.Coerce(str), + vol.Required(ATTR_STARTUP): + vol.In([STARTUP_BEFORE, STARTUP_AFTER, STARTUP_ONCE]), + vol.Required(ATTR_BOOT): + vol.In([BOOT_AUTO, BOOT_MANUAL]), + vol.Optional(ATTR_PORTS): dict, + vol.Optional(ATTR_MAP_CONFIG, default=False): vol.Boolean(), + vol.Optional(ATTR_MAP_SSL, default=False): vol.Boolean(), + vol.Optional(ATTR_MAP_HASSIO, default=False): vol.Boolean(), + vol.Required(ATTR_OPTIONS): dict, + vol.Required(ATTR_SCHEMA): { + vol.Coerce(str): vol.Any(ADDON_ELEMENT, [ + vol.Any(ADDON_ELEMENT, {vol.Coerce(str): ADDON_ELEMENT}) + ]) + }, + vol.Optional(ATTR_IMAGE): vol.Match(r"\w*/\w*"), +}) + + +def validate_options(raw_schema): + """Validate schema.""" + def validate(struct): + """Create schema validator for addons options.""" + options = {} + + # read options + for key, value in struct.items(): + if key not in raw_schema: + raise vol.Invalid("Unknown options {}.".format(key)) + + typ = raw_schema[key] + try: + if isinstance(typ, list): + # nested value + options[key] = _nested_validate(typ[0], value) + else: + # normal value + options[key] = _single_validate(typ, value) + except (IndexError, KeyError): + raise vol.Invalid( + "Type error for {}.".format(key)) from None + + return options + + return validate + + +# pylint: disable=no-value-for-parameter +def _single_validate(typ, value): + """Validate a single element.""" + try: + if typ == V_STR: + return str(value) + elif typ == V_INT: + return int(value) + elif typ == V_FLOAT: + return float(value) + elif typ == V_BOOL: + return vol.Boolean()(value) + elif typ == V_EMAIL: + return vol.Email()(value) + elif typ == V_URL: + return vol.Url()(value) + + raise vol.Invalid("Fatal error for {}.".format(value)) + except TypeError: + raise vol.Invalid( + "Type {} error for {}.".format(typ, value)) from None + + +def _nested_validate(typ, data_list): + """Validate nested items.""" + options = [] + + for element in data_list: + # dict list + if isinstance(typ, dict): + c_options = {} + for c_key, c_value in element.items(): + if c_key not in typ: + raise vol.Invalid( + "Unknown nested options {}.".format(c_key)) + + c_options[c_key] = _single_validate(typ[c_key], c_value) + options.append(c_options) + # normal list + else: + options.append(_single_validate(typ, element)) + + return options diff --git a/hassio/api/addons.py b/hassio/api/addons.py index 74a5bee59..690455082 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -3,6 +3,7 @@ import asyncio import logging import voluptuous as vol +from voluptuous.humanize import humanize_error from .util import api_process, api_validate from ..const import ( @@ -88,6 +89,14 @@ class APIAddons(object): 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) diff --git a/hassio/config.py b/hassio/config.py index f7fa29b3d..904d5af81 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -117,10 +117,15 @@ class CoreConfig(Config): """Actual version of hassio.""" return self._data.get(HASSIO_CURRENT) + @property + def path_hassio_docker(self): + """Return hassio data path extern for docker.""" + return os.environ['SUPERVISOR_SHARE'] + @property def path_config_docker(self): """Return config path extern for docker.""" - return HOMEASSISTANT_CONFIG.format(os.environ['SUPERVISOR_SHARE']) + return HOMEASSISTANT_CONFIG.format(self.path_hassio_docker) @property def path_config(self): @@ -130,7 +135,7 @@ class CoreConfig(Config): @property def path_ssl_docker(self): """Return SSL path extern for docker.""" - return HASSIO_SSL.format(os.environ['SUPERVISOR_SHARE']) + return HASSIO_SSL.format(self.path_hassio_docker) @property def path_ssl(self): @@ -155,4 +160,4 @@ class CoreConfig(Config): @property def path_addons_data_docker(self): """Return root addon data folder extern for docker.""" - return ADDONS_DATA.format(os.environ['SUPERVISOR_SHARE']) + return ADDONS_DATA.format(self.path_hassio_docker) diff --git a/hassio/const.py b/hassio/const.py index 2dc2897c7..ba86e09e1 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -1,5 +1,5 @@ """Const file for HassIO.""" -HASSIO_VERSION = '0.7' +HASSIO_VERSION = '0.8' URL_HASSIO_VERSION = \ 'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json' @@ -42,6 +42,7 @@ ATTR_BOOT = 'boot' ATTR_PORTS = 'ports' ATTR_MAP_CONFIG = 'map_config' ATTR_MAP_SSL = 'map_ssl' +ATTR_MAP_HASSIO = 'map_hassio' ATTR_OPTIONS = 'options' ATTR_INSTALLED = 'installed' ATTR_STATE = 'state' diff --git a/hassio/dock/__init__.py b/hassio/dock/__init__.py index 07b25108a..91bf68cc7 100644 --- a/hassio/dock/__init__.py +++ b/hassio/dock/__init__.py @@ -167,6 +167,8 @@ class DockerBase(object): if not self.container: return + _LOGGER.info("Stop %s docker application.", self.image) + self.container.reload() if self.container.status == 'running': with suppress(docker.errors.DockerException): diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 3afeb2889..2fb44e7db 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -52,6 +52,11 @@ class DockerAddon(DockerBase): self.config.path_ssl_docker: { 'bind': '/ssl', 'mode': 'rw' }}) + if self.addons_data.need_hassio(self.addon): + volumes.update({ + self.config.path_hassio_docker: { + 'bind': '/hassio', 'mode': 'rw' + }}) try: self.container = self.dock.containers.run( diff --git a/hassio/dock/supervisor.py b/hassio/dock/supervisor.py index 41c463338..7d9179b3c 100644 --- a/hassio/dock/supervisor.py +++ b/hassio/dock/supervisor.py @@ -37,6 +37,9 @@ class DockerSupervisor(DockerBase): if await self.loop.run_in_executor(None, self._install, tag): self.config.hassio_cleanup = old_version self.loop.create_task(self.hassio.stop(RESTART_EXIT_CODE)) + return True + + return False async def cleanup(self): """Check if old supervisor version exists and cleanup.""" diff --git a/version_beta.json b/version_beta.json index e768d292e..31449e8fe 100644 --- a/version_beta.json +++ b/version_beta.json @@ -1,5 +1,5 @@ { - "hassio_tag": "0.7", + "hassio_tag": "0.8", "homeassistant_tag": "0.42.3", "resinos_version": "0.3", "resinhup_version": "0.1"