Extend addons options to allow lists (#5)

Extend addons options to allow lists
This commit is contained in:
Pascal Vizeli 2017-04-19 17:07:24 +02:00 committed by GitHub
parent 71590f90ae
commit f9c7371140
10 changed files with 165 additions and 60 deletions

View File

@ -47,7 +47,7 @@ class AddonManager(AddonsData):
tasks = [] tasks = []
for addon in self.list_removed: for addon in self.list_removed:
_LOGGER.info("Old addon %s found") _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: if tasks:
await asyncio.wait(tasks, loop=self.loop) await asyncio.wait(tasks, loop=self.loop)

View File

@ -5,11 +5,12 @@ import glob
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
from .validate import validate_options, SCHEMA_ADDON_CONFIG
from ..const import ( from ..const import (
FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON,
ATTR_STARTUP, ATTR_BOOT, ATTR_MAP_SSL, ATTR_MAP_CONFIG, ATTR_OPTIONS, ATTR_STARTUP, ATTR_BOOT, ATTR_MAP_SSL, ATTR_MAP_CONFIG, ATTR_OPTIONS,
ATTR_PORTS, STARTUP_ONCE, STARTUP_AFTER, STARTUP_BEFORE, BOOT_AUTO, ATTR_PORTS, BOOT_AUTO, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA,
BOOT_MANUAL, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA, ATTR_IMAGE) ATTR_IMAGE, ATTR_MAP_HASSIO)
from ..config import Config from ..config import Config
from ..tools import read_json_file, write_json_file from ..tools import read_json_file, write_json_file
@ -17,32 +18,6 @@ _LOGGER = logging.getLogger(__name__)
ADDONS_REPO_PATTERN = "{}/*/config.json" 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): class AddonsData(Config):
"""Hold data for addons inside HassIO.""" """Hold data for addons inside HassIO."""
@ -56,6 +31,8 @@ class AddonsData(Config):
def read_addons_repo(self): def read_addons_repo(self):
"""Read data from addons repository.""" """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_repo)
self._read_addons_folder(self.config.path_addons_custom) self._read_addons_folder(self.config.path_addons_custom)
@ -213,6 +190,10 @@ class AddonsData(Config):
"""Return True if ssl map is needed.""" """Return True if ssl map is needed."""
return self._addons_data[addon][ATTR_MAP_SSL] 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): def path_data(self, addon):
"""Return addon data path inside supervisor.""" """Return addon data path inside supervisor."""
return "{}/{}".format( return "{}/{}".format(
@ -229,35 +210,21 @@ class AddonsData(Config):
def write_addon_options(self, addon): def write_addon_options(self, addon):
"""Return True if addon options is written to data.""" """Return True if addon options is written to data."""
return write_json_file( schema = self.get_schema(addon)
self.path_addon_options(addon), self.get_options(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): def get_schema(self, addon):
"""Create a schema for addon options.""" """Create a schema for addon options."""
raw_schema = self._addons_data[addon][ATTR_SCHEMA] raw_schema = self._addons_data[addon][ATTR_SCHEMA]
def validate(struct): schema = vol.Schema(vol.All(dict, validate_options(raw_schema)))
"""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))
return schema return schema

113
hassio/addons/validate.py Normal file
View File

@ -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

View File

@ -3,6 +3,7 @@ import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error
from .util import api_process, api_validate from .util import api_process, api_validate
from ..const import ( from ..const import (
@ -88,6 +89,14 @@ class APIAddons(object):
if await self.addons.state(addon) == STATE_STARTED: if await self.addons.state(addon) == STATE_STARTED:
raise RuntimeError("Addon is already running") 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( return await asyncio.shield(
self.addons.start(addon), loop=self.loop) self.addons.start(addon), loop=self.loop)

View File

@ -117,10 +117,15 @@ class CoreConfig(Config):
"""Actual version of hassio.""" """Actual version of hassio."""
return self._data.get(HASSIO_CURRENT) 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 @property
def path_config_docker(self): def path_config_docker(self):
"""Return config path extern for docker.""" """Return config path extern for docker."""
return HOMEASSISTANT_CONFIG.format(os.environ['SUPERVISOR_SHARE']) return HOMEASSISTANT_CONFIG.format(self.path_hassio_docker)
@property @property
def path_config(self): def path_config(self):
@ -130,7 +135,7 @@ class CoreConfig(Config):
@property @property
def path_ssl_docker(self): def path_ssl_docker(self):
"""Return SSL path extern for docker.""" """Return SSL path extern for docker."""
return HASSIO_SSL.format(os.environ['SUPERVISOR_SHARE']) return HASSIO_SSL.format(self.path_hassio_docker)
@property @property
def path_ssl(self): def path_ssl(self):
@ -155,4 +160,4 @@ class CoreConfig(Config):
@property @property
def path_addons_data_docker(self): def path_addons_data_docker(self):
"""Return root addon data folder extern for docker.""" """Return root addon data folder extern for docker."""
return ADDONS_DATA.format(os.environ['SUPERVISOR_SHARE']) return ADDONS_DATA.format(self.path_hassio_docker)

View File

@ -1,5 +1,5 @@
"""Const file for HassIO.""" """Const file for HassIO."""
HASSIO_VERSION = '0.7' HASSIO_VERSION = '0.8'
URL_HASSIO_VERSION = \ URL_HASSIO_VERSION = \
'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json' 'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json'
@ -42,6 +42,7 @@ ATTR_BOOT = 'boot'
ATTR_PORTS = 'ports' ATTR_PORTS = 'ports'
ATTR_MAP_CONFIG = 'map_config' ATTR_MAP_CONFIG = 'map_config'
ATTR_MAP_SSL = 'map_ssl' ATTR_MAP_SSL = 'map_ssl'
ATTR_MAP_HASSIO = 'map_hassio'
ATTR_OPTIONS = 'options' ATTR_OPTIONS = 'options'
ATTR_INSTALLED = 'installed' ATTR_INSTALLED = 'installed'
ATTR_STATE = 'state' ATTR_STATE = 'state'

View File

@ -167,6 +167,8 @@ class DockerBase(object):
if not self.container: if not self.container:
return return
_LOGGER.info("Stop %s docker application.", self.image)
self.container.reload() self.container.reload()
if self.container.status == 'running': if self.container.status == 'running':
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException):

View File

@ -52,6 +52,11 @@ class DockerAddon(DockerBase):
self.config.path_ssl_docker: { self.config.path_ssl_docker: {
'bind': '/ssl', 'mode': 'rw' 'bind': '/ssl', 'mode': 'rw'
}}) }})
if self.addons_data.need_hassio(self.addon):
volumes.update({
self.config.path_hassio_docker: {
'bind': '/hassio', 'mode': 'rw'
}})
try: try:
self.container = self.dock.containers.run( self.container = self.dock.containers.run(

View File

@ -37,6 +37,9 @@ class DockerSupervisor(DockerBase):
if await self.loop.run_in_executor(None, self._install, tag): if await self.loop.run_in_executor(None, self._install, tag):
self.config.hassio_cleanup = old_version self.config.hassio_cleanup = old_version
self.loop.create_task(self.hassio.stop(RESTART_EXIT_CODE)) self.loop.create_task(self.hassio.stop(RESTART_EXIT_CODE))
return True
return False
async def cleanup(self): async def cleanup(self):
"""Check if old supervisor version exists and cleanup.""" """Check if old supervisor version exists and cleanup."""

View File

@ -1,5 +1,5 @@
{ {
"hassio_tag": "0.7", "hassio_tag": "0.8",
"homeassistant_tag": "0.42.3", "homeassistant_tag": "0.42.3",
"resinos_version": "0.3", "resinos_version": "0.3",
"resinhup_version": "0.1" "resinhup_version": "0.1"