mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-06-25 19:36:29 +00:00
Extend addons options to allow lists (#5)
Extend addons options to allow lists
This commit is contained in:
parent
71590f90ae
commit
f9c7371140
@ -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)
|
||||||
|
@ -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
113
hassio/addons/validate.py
Normal 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
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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'
|
||||||
|
@ -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):
|
||||||
|
@ -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(
|
||||||
|
@ -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."""
|
||||||
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user