Add more addons functions. (#91)

* Add more addons functions.

* fix lint

* fix lint p2

* Allow more customable network settings

* fix lint

* change point of validate

* fix lint

* fix handling

* fix lint & validate data before write
This commit is contained in:
Pascal Vizeli 2017-07-06 23:40:49 +02:00 committed by GitHub
parent f52d1c4509
commit 7186f5a8c0
14 changed files with 175 additions and 62 deletions

9
API.md
View File

@ -303,6 +303,7 @@ Output the raw docker log
{
"name": "xy bla",
"description": "description",
"auto_update": "bool",
"url": "null|url of addon",
"detached": "bool",
"repository": "12345678|null",
@ -312,6 +313,8 @@ Output the raw docker log
"boot": "auto|manual",
"build": "bool",
"options": "{}",
"network": "{}|null",
"host_network": "bool"
}
```
@ -319,10 +322,16 @@ Output the raw docker log
```json
{
"boot": "auto|manual",
"auto_update": "bool",
"network": {
"CONTAINER": "port|[ip, port]"
},
"options": {},
}
```
For reset custom network settings, set it `null`.
- POST `/addons/{addon}/start`
- POST `/addons/{addon}/stop`

View File

@ -12,15 +12,14 @@ import voluptuous as vol
from voluptuous.humanize import humanize_error
from .validate import (
validate_options, SCHEMA_ADDON_USER, SCHEMA_ADDON_SYSTEM,
SCHEMA_ADDON_SNAPSHOT, MAP_VOLUME)
validate_options, SCHEMA_ADDON_SNAPSHOT, 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, ATTR_USER, ATTR_SYSTEM,
ATTR_STATE)
ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK)
from .util import check_installed
from ..dock.addon import DockerAddon
from ..tools import write_json_file, read_json_file
@ -45,21 +44,8 @@ class Addon(object):
async def load(self):
"""Async initialize of object."""
if self.is_installed:
self._validate_system_user()
await self.addon_docker.attach()
def _validate_system_user(self):
"""Validate internal data they read from file."""
for data, schema in ((self.data.system, SCHEMA_ADDON_SYSTEM),
(self.data.user, SCHEMA_ADDON_USER)):
try:
data[self._id] = schema(data[self._id])
except vol.Invalid as err:
_LOGGER.warning("Can't validate addon load %s -> %s", self._id,
humanize_error(data[self._id], err))
except KeyError:
pass
@property
def slug(self):
"""Return slug/id of addon."""
@ -141,11 +127,27 @@ class Addon(object):
self.data.user[self._id][ATTR_BOOT] = value
self.data.save()
@property
def auto_update(self):
"""Return if auto update is enable."""
return self.data.user[self._id][ATTR_AUTO_UPDATE]
@auto_update.setter
def auto_update(self, value):
"""Set auto update."""
self.data.user[self._id][ATTR_AUTO_UPDATE] = value
self.data.save()
@property
def name(self):
"""Return name of addon."""
return self._mesh[ATTR_NAME]
@property
def timeout(self):
"""Return timeout of addon for docker stop."""
return self._mesh[ATTR_TIMEOUT]
@property
def description(self):
"""Return description of addon."""
@ -171,7 +173,28 @@ class Addon(object):
@property
def ports(self):
"""Return ports of addon."""
return self._mesh.get(ATTR_PORTS)
if self.network_mode != 'bridge' or ATTR_PORTS not in self._mesh:
return
if not self.is_installed or \
ATTR_NETWORK not in self.data.user[self._id]:
return self._mesh[ATTR_PORTS]
return self.data.user[self._id][ATTR_NETWORK]
@ports.setter
def ports(self, value):
"""Set custom ports of addon."""
if value is None:
self.data.user[self._id].pop(ATTR_NETWORK, None)
else:
new_ports = {}
for container_port, host_port in value.items():
if container_port in self._mesh.get(ATTR_PORTS, {}):
new_ports[container_port] = host_port
self.data.user[self._id][ATTR_NETWORK] = new_ports
self.data.save()
@property
def network_mode(self):

View File

@ -10,7 +10,8 @@ from voluptuous.humanize import humanize_error
from .util import extract_hash_from_path
from .validate import (
SCHEMA_ADDON, SCHEMA_REPOSITORY_CONFIG, MAP_VOLUME)
SCHEMA_ADDON_CONFIG, SCHEMA_ADDON_FILE, SCHEMA_REPOSITORY_CONFIG,
MAP_VOLUME)
from ..const import (
FILE_HASSIO_ADDONS, ATTR_VERSION, ATTR_SLUG, ATTR_REPOSITORY, ATTR_LOCATON,
REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_USER, ATTR_SYSTEM)
@ -40,13 +41,23 @@ class Data(object):
_LOGGER.warning("Can't read %s", self._file)
self._data = {}
# init data
if not self._data:
self._data[ATTR_USER] = {}
self._data[ATTR_SYSTEM] = {}
# validate
try:
self._data = SCHEMA_ADDON_FILE(self._data)
except vol.Invalid as ex:
_LOGGER.error("Can't parse addons.json -> %s",
humanize_error(self._data, ex))
def save(self):
"""Store data to config file."""
# validate
try:
self._data = SCHEMA_ADDON_FILE(self._data)
except vol.Invalid as ex:
_LOGGER.error("Can't parse addons data -> %s",
humanize_error(self._data, ex))
return False
if not write_json_file(self._file, self._data):
_LOGGER.error("Can't store config in %s", self._file)
return False
@ -127,7 +138,7 @@ class Data(object):
addon_config = read_json_file(addon)
# validate
addon_config = SCHEMA_ADDON(addon_config)
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
# Generate slug
addon_slug = "{}_{}".format(

View File

@ -8,7 +8,9 @@ from ..const import (
ATTR_IMAGE, ATTR_URL, ATTR_MAINTAINER, ATTR_ARCH, ATTR_DEVICES,
ATTR_ENVIRONMENT, ATTR_HOST_NETWORK, ARCH_ARMHF, ARCH_AARCH64, ARCH_AMD64,
ARCH_I386, ATTR_TMPFS, ATTR_PRIVILEGED, ATTR_USER, ATTR_STATE, ATTR_SYSTEM,
STATE_STARTED, STATE_STOPPED, ATTR_LOCATON, ATTR_REPOSITORY)
STATE_STARTED, STATE_STOPPED, ATTR_LOCATON, ATTR_REPOSITORY, ATTR_TIMEOUT,
ATTR_NETWORK, ATTR_AUTO_UPDATE)
from ..validate import NETWORK_PORT, DOCKER_PORTS
MAP_VOLUME = r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$"
@ -19,8 +21,9 @@ V_FLOAT = 'float'
V_BOOL = 'bool'
V_EMAIL = 'email'
V_URL = 'url'
V_PORT = 'port'
ADDON_ELEMENT = vol.In([V_STR, V_INT, V_FLOAT, V_BOOL, V_EMAIL, V_URL])
ADDON_ELEMENT = vol.In([V_STR, V_INT, V_FLOAT, V_BOOL, V_EMAIL, V_URL, V_PORT])
ARCH_ALL = [
ARCH_ARMHF, ARCH_AARCH64, ARCH_AMD64, ARCH_I386
@ -31,16 +34,6 @@ PRIVILEGE_ALL = [
]
def check_network(data):
"""Validate network settings."""
host_network = data[ATTR_HOST_NETWORK]
if ATTR_PORTS in data and host_network:
raise vol.Invalid("Hostnetwork & ports are not allow!")
return data
# pylint: disable=no-value-for-parameter
SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Required(ATTR_NAME): vol.Coerce(str),
@ -54,7 +47,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
STARTUP_INITIALIZE]),
vol.Required(ATTR_BOOT):
vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_PORTS): dict,
vol.Optional(ATTR_PORTS): DOCKER_PORTS,
vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(),
vol.Optional(ATTR_DEVICES): [vol.Match(r"^(.*):(.*):([rwm]{1,3})$")],
vol.Optional(ATTR_TMPFS):
@ -69,8 +62,10 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
])
}, False),
vol.Optional(ATTR_IMAGE): vol.Match(r"\w*/\w*"),
vol.Optional(ATTR_TIMEOUT, default=10):
vol.All(vol.Coerce(int), vol.Range(min=10, max=120))
}, extra=vol.ALLOW_EXTRA)
SCHEMA_ADDON = vol.Schema(vol.All(SCHEMA_ADDON_CONFIG, check_network))
# pylint: disable=no-value-for-parameter
SCHEMA_REPOSITORY_CONFIG = vol.Schema({
@ -80,11 +75,14 @@ SCHEMA_REPOSITORY_CONFIG = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
# pylint: disable=no-value-for-parameter
SCHEMA_ADDON_USER = vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Required(ATTR_OPTIONS): dict,
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
vol.Optional(ATTR_BOOT):
vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_NETWORK): DOCKER_PORTS,
})
@ -94,6 +92,16 @@ SCHEMA_ADDON_SYSTEM = SCHEMA_ADDON_CONFIG.extend({
})
SCHEMA_ADDON_FILE = vol.Schema({
vol.Optional(ATTR_USER, default={}): {
vol.Coerce(str): SCHEMA_ADDON_USER,
},
vol.Optional(ATTR_SYSTEM, default={}): {
vol.Coerce(str): SCHEMA_ADDON_SYSTEM,
}
})
SCHEMA_ADDON_SNAPSHOT = vol.Schema({
vol.Required(ATTR_USER): SCHEMA_ADDON_USER,
vol.Required(ATTR_SYSTEM): SCHEMA_ADDON_SYSTEM,
@ -150,6 +158,8 @@ def _single_validate(typ, value, key):
return vol.Email()(value)
elif typ == V_URL:
return vol.Url()(value)
elif typ == V_PORT:
return NETWORK_PORT(value)
raise vol.Invalid("Fatal error for {} type {}.".format(key, typ))
except ValueError:

View File

@ -9,7 +9,9 @@ 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, BOOT_AUTO, BOOT_MANUAL)
ATTR_BUILD, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_HOST_NETWORK,
BOOT_AUTO, BOOT_MANUAL)
from ..validate import DOCKER_PORTS
_LOGGER = logging.getLogger(__name__)
@ -17,8 +19,11 @@ SCHEMA_VERSION = vol.Schema({
vol.Optional(ATTR_VERSION): vol.Coerce(str),
})
# pylint: disable=no-value-for-parameter
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL])
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_NETWORK): vol.Any(None, DOCKER_PORTS),
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
})
@ -51,6 +56,7 @@ class APIAddons(object):
ATTR_NAME: addon.name,
ATTR_DESCRIPTON: addon.description,
ATTR_VERSION: addon.version_installed,
ATTR_AUTO_UPDATE: addon.auto_update,
ATTR_REPOSITORY: addon.repository,
ATTR_LAST_VERSION: addon.last_version,
ATTR_STATE: await addon.state(),
@ -59,6 +65,8 @@ class APIAddons(object):
ATTR_URL: addon.url,
ATTR_DETACHED: addon.is_detached,
ATTR_BUILD: addon.need_build,
ATTR_NETWORK: addon.ports,
ATTR_HOST_NETWORK: addon.network_mode == 'host',
}
@api_process
@ -76,6 +84,10 @@ class APIAddons(object):
addon.options = body[ATTR_OPTIONS]
if ATTR_BOOT in body:
addon.boot = body[ATTR_BOOT]
if ATTR_AUTO_UPDATE in body:
addon.auto_update = body[ATTR_AUTO_UPDATE]
if ATTR_NETWORK in body:
addon.ports = body[ATTR_NETWORK]
return True

View File

@ -6,12 +6,13 @@ import voluptuous as vol
from .util import api_process, api_process_raw, api_validate
from ..const import ATTR_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES
from ..validate import HASS_DEVICES
_LOGGER = logging.getLogger(__name__)
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_DEVICES): [vol.Match(r"^[^/]*$")],
vol.Optional(ATTR_DEVICES): HASS_DEVICES,
})
SCHEMA_VERSION = vol.Schema({

View File

@ -11,6 +11,7 @@ from voluptuous.humanize import humanize_error
from .const import FILE_HASSIO_CONFIG, HASSIO_DATA
from .tools import (
fetch_last_versions, write_json_file, read_json_file, validate_timezone)
from .validate import HASS_DEVICES
_LOGGER = logging.getLogger(__name__)
@ -49,7 +50,7 @@ SCHEMA_CONFIG = vol.Schema({
vol.Optional(API_ENDPOINT): vol.Coerce(str),
vol.Optional(TIMEZONE, default='UTC'): validate_timezone,
vol.Optional(HOMEASSISTANT_LAST): vol.Coerce(str),
vol.Optional(HOMEASSISTANT_DEVICES, default=[]): [vol.Coerce(str)],
vol.Optional(HOMEASSISTANT_DEVICES, default=[]): HASS_DEVICES,
vol.Optional(HASSIO_LAST): vol.Coerce(str),
vol.Optional(ADDONS_CUSTOM_LIST, default=[]): [vol.Url()],
vol.Optional(SECURITY_INITIALIZE, default=False): vol.Boolean(),

View File

@ -14,6 +14,7 @@ HASSIO_DATA = Path("/data")
RUN_UPDATE_INFO_TASKS = 28800
RUN_UPDATE_SUPERVISOR_TASKS = 29100
RUN_UPDATE_ADDONS_TASKS = 57600
RUN_RELOAD_ADDONS_TASKS = 28800
RUN_RELOAD_SNAPSHOTS_TASKS = 72000
RUN_WATCHDOG_HOMEASSISTANT = 15
@ -81,6 +82,7 @@ ATTR_BUILD = 'build'
ATTR_DEVICES = 'devices'
ATTR_ENVIRONMENT = 'environment'
ATTR_HOST_NETWORK = 'host_network'
ATTR_NETWORK = 'network'
ATTR_TMPFS = 'tmpfs'
ATTR_PRIVILEGED = 'privileged'
ATTR_USER = 'user'
@ -90,6 +92,8 @@ ATTR_HOMEASSISTANT = 'homeassistant'
ATTR_FOLDERS = 'folders'
ATTR_SIZE = 'size'
ATTR_TYPE = 'type'
ATTR_TIMEOUT = 'timeout'
ATTR_AUTO_UPDATE = 'auto_update'
STARTUP_INITIALIZE = 'initialize'
STARTUP_BEFORE = 'before'

View File

@ -12,14 +12,14 @@ from .const import (
SOCKET_DOCKER, RUN_UPDATE_INFO_TASKS, RUN_RELOAD_ADDONS_TASKS,
RUN_UPDATE_SUPERVISOR_TASKS, RUN_WATCHDOG_HOMEASSISTANT,
RUN_CLEANUP_API_SESSIONS, STARTUP_AFTER, STARTUP_BEFORE,
STARTUP_INITIALIZE, RUN_RELOAD_SNAPSHOTS_TASKS)
STARTUP_INITIALIZE, RUN_RELOAD_SNAPSHOTS_TASKS, RUN_UPDATE_ADDONS_TASKS)
from .scheduler import Scheduler
from .dock.homeassistant import DockerHomeAssistant
from .dock.supervisor import DockerSupervisor
from .snapshots import SnapshotsManager
from .tasks import (
hassio_update, homeassistant_watchdog, homeassistant_setup,
api_sessions_cleanup)
api_sessions_cleanup, addons_update)
from .tools import get_local_ip, fetch_timezone
_LOGGER = logging.getLogger(__name__)
@ -108,6 +108,8 @@ class HassIO(object):
# schedule addon update task
self.scheduler.register_task(
self.addons.reload, RUN_RELOAD_ADDONS_TASKS, now=True)
self.scheduler.register_task(
addons_update(self.loop, self.addons), RUN_UPDATE_ADDONS_TASKS)
# schedule self update task
self.scheduler.register_task(

View File

@ -13,12 +13,13 @@ _LOGGER = logging.getLogger(__name__)
class DockerBase(object):
"""Docker hassio wrapper."""
def __init__(self, config, loop, dock, image=None):
def __init__(self, config, loop, dock, image=None, timeout=30):
"""Initialize docker base wrapper."""
self.config = config
self.loop = loop
self.dock = dock
self.image = image
self.timeout = timeout
self.version = None
self.arch = None
self._lock = asyncio.Lock(loop=loop)
@ -192,7 +193,7 @@ class DockerBase(object):
if container.status == 'running':
_LOGGER.info("Stop %s docker application", self.image)
with suppress(docker.errors.DockerException):
container.stop(timeout=15)
container.stop(timeout=self.timeout)
with suppress(docker.errors.DockerException):
_LOGGER.info("Clean %s docker application", self.image)
@ -312,7 +313,7 @@ class DockerBase(object):
_LOGGER.info("Restart %s", self.image)
try:
container.restart(timeout=30)
container.restart(timeout=self.timeout)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't restart %s -> %s", self.image, err)
return False

View File

@ -1,5 +1,4 @@
"""Init file for HassIO addon docker object."""
from contextlib import suppress
import logging
from pathlib import Path
import shutil
@ -21,7 +20,7 @@ class DockerAddon(DockerBase):
def __init__(self, config, loop, dock, addon):
"""Initialize docker homeassistant wrapper."""
super().__init__(
config, loop, dock, image=addon.image)
config, loop, dock, image=addon.image, timeout=addon.timeout)
self.addon = addon
@property
@ -249,17 +248,5 @@ class DockerAddon(DockerBase):
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
# for restart it need to run!
if container.status != 'running':
return False
_LOGGER.info("Restart %s", self.image)
with suppress(docker.errors.DockerException):
container.stop(timeout=15)
self._stop()
return self._run()

View File

@ -7,6 +7,7 @@ from ..const import (
ATTR_VERSION, ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_TYPE, ATTR_DEVICES,
FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL,
SNAPSHOT_FULL, SNAPSHOT_PARTIAL)
from ..validate import HASS_DEVICES
ALL_FOLDERS = [FOLDER_HOMEASSISTANT, FOLDER_SHARE, FOLDER_ADDONS, FOLDER_SSL]
@ -18,7 +19,7 @@ SCHEMA_SNAPSHOT = vol.Schema({
vol.Required(ATTR_DATE): vol.Coerce(str),
vol.Required(ATTR_HOMEASSISTANT): vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Optional(ATTR_DEVICES, default=[]): [vol.Match(r"^[^/]*$")],
vol.Optional(ATTR_DEVICES, default=[]): HASS_DEVICES,
}),
vol.Optional(ATTR_FOLDERS, default=[]): [vol.In(ALL_FOLDERS)],
vol.Optional(ATTR_ADDONS, default=[]): [vol.Schema({

View File

@ -18,6 +18,25 @@ def api_sessions_cleanup(config):
return _api_sessions_cleanup
def addons_update(loop, addons):
"""Create scheduler task for auto update addons."""
async def _addons_update():
"""Check if a update is available of a addon and update it."""
tasks = []
for addon in addons.list_addons:
if not addon.is_installed:
continue
if addon.version_installed != addon.version:
tasks.append(addon.update())
if tasks:
_LOGGER.info("Addon auto update process %d tasks", len(tasks))
await asyncio.wait(tasks, loop=loop)
return _addons_update
def hassio_update(config, supervisor, websession):
"""Create scheduler task for update of supervisor hassio."""
async def _hassio_update():

32
hassio/validate.py Normal file
View File

@ -0,0 +1,32 @@
"""Validate functions."""
import voluptuous as vol
NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
HASS_DEVICES = [vol.Match(r"^[^/]*$")]
def convert_to_docker_ports(data):
"""Convert data into docker port list."""
# dynamic ports
if data is None:
return
# single port
if isinstance(data, int):
return NETWORK_PORT(data)
# port list
if isinstance(data, list) and len(data) > 2:
return vol.Schema([NETWORK_PORT])(data)
# ip port mapping
if isinstance(data, list) and len(data) == 2:
return (vol.Coerce(str)(data[0]), NETWORK_PORT(data[1]))
raise vol.Invalid("Can't validate docker host settings")
DOCKER_PORTS = vol.Schema({
vol.All(vol.Coerce(str), vol.Match(r"^\d+(?:/tcp|/udp)?$")):
convert_to_docker_ports,
})