mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-16 05:36:29 +00:00
Finish api, add options validate
This commit is contained in:
parent
3ef76a4ada
commit
20856126c8
@ -3,71 +3,72 @@ import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from .config import AddonsConfig
|
||||
from .data import AddonsData
|
||||
from .git import AddonsRepo
|
||||
from ..const import STATE_STOPED, STATE_STARTED
|
||||
from ..docker.addon import DockerAddon
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AddonManager(object):
|
||||
class AddonManager(AddonsData):
|
||||
"""Manage addons inside HassIO."""
|
||||
|
||||
def __init__(self, config, loop, dock):
|
||||
"""Initialize docker base wrapper."""
|
||||
self.config = config
|
||||
super().__init__(config)
|
||||
|
||||
self.loop = loop
|
||||
self.dock = dock
|
||||
self.repo = AddonsRepo(config, loop)
|
||||
self.addons = AddonsConfig(config)
|
||||
self.dockers = {}
|
||||
|
||||
async def prepare(self):
|
||||
"""Startup addon management."""
|
||||
# load addon repository
|
||||
if await self.repo.load():
|
||||
self.addons.read_addons_repo()
|
||||
self.read_addons_repo()
|
||||
|
||||
# load installed addons
|
||||
for addon in self.addons.list_installed:
|
||||
for addon in self.list_installed:
|
||||
self.dockers[addon] = DockerAddon(
|
||||
self.config, self.loop, self.dock, self.addons, addon)
|
||||
self.config, self.loop, self.dock, self, addon)
|
||||
|
||||
async def relaod_addons(self):
|
||||
async def relaod(self):
|
||||
"""Update addons from repo and reload list."""
|
||||
if not await self.repo.pull():
|
||||
return
|
||||
self.addons.read_addons_repo()
|
||||
self.read_addons_repo()
|
||||
|
||||
async def install_addon(self, addon, version=None):
|
||||
"""Install a addon."""
|
||||
if not self.addons.exists_addon(addon):
|
||||
if not self.exists_addon(addon):
|
||||
_LOGGER.error("Addon %s not exists for install.", addon)
|
||||
return False
|
||||
|
||||
if self.addons.is_installed(addon):
|
||||
if self.is_installed(addon):
|
||||
_LOGGER.error("Addon %s is already installed.", addon)
|
||||
return False
|
||||
|
||||
if not os.path.isdir(self.addons.path_data(addon)):
|
||||
if not os.path.isdir(self.path_data(addon)):
|
||||
_LOGGER.info("Create Home-Assistant addon data folder %s",
|
||||
self.addon.path_data(addon))
|
||||
os.mkdir(self.addons.path_data(addon))
|
||||
self.path_data(addon))
|
||||
os.mkdir(self.path_data(addon))
|
||||
|
||||
addon_docker = DockerAddon(
|
||||
self.config, self.loop, self.dock, self.addons, addon)
|
||||
self.config, self.loop, self.dock, self, addon)
|
||||
|
||||
version = version or self.addons.get_version(addon)
|
||||
version = version or self.get_version(addon)
|
||||
if not await addon_docker.install(version):
|
||||
return False
|
||||
|
||||
self.dockers[addon] = addon_docker
|
||||
self.addons.set_install_addon(addon, version)
|
||||
self.set_install_addon(addon, version)
|
||||
return True
|
||||
|
||||
async def uninstall_addon(self, addon):
|
||||
"""Remove a addon."""
|
||||
if not self.addons.is_installed(addon):
|
||||
if not self.is_installed(addon):
|
||||
_LOGGER.error("Addon %s is already uninstalled.", addon)
|
||||
return False
|
||||
|
||||
@ -78,15 +79,25 @@ class AddonManager(object):
|
||||
if not await self.dockers[addon].remove(version):
|
||||
return False
|
||||
|
||||
if os.path.isdir(self.addons.path_data(addon)):
|
||||
if os.path.isdir(self.path_data(addon)):
|
||||
_LOGGER.info("Remove Home-Assistant addon data folder %s",
|
||||
self.addon.path_data(addon))
|
||||
shutil.rmtree(self.addons.path_data(addon))
|
||||
self.path_data(addon))
|
||||
shutil.rmtree(self.path_data(addon))
|
||||
|
||||
self.dockers.pop(addon)
|
||||
self.addons.set_uninstall_addon(addon)
|
||||
self.set_uninstall_addon(addon)
|
||||
return True
|
||||
|
||||
async def state_addon(self, addon):
|
||||
"""Return running state of addon."""
|
||||
if addon not in self.dockers:
|
||||
_LOGGER.error("No docker found for addon %s.", addon)
|
||||
return
|
||||
|
||||
if await self.dockers[addon].is_running():
|
||||
return STATE_STARTED
|
||||
return STATE_STOPED
|
||||
|
||||
async def start_addon(self, addon):
|
||||
"""Set options and start addon."""
|
||||
if addon not in self.dockers:
|
||||
@ -115,7 +126,7 @@ class AddonManager(object):
|
||||
|
||||
async def update_addon(self, addon, version=None):
|
||||
"""Update addon."""
|
||||
if self.addons.is_installed(addon):
|
||||
if self.is_installed(addon):
|
||||
_LOGGER.error("Addon %s is not installed.", addon)
|
||||
return False
|
||||
|
||||
@ -123,7 +134,7 @@ class AddonManager(object):
|
||||
_LOGGER.error("No docker found for addon %s.", addon)
|
||||
return False
|
||||
|
||||
version = version or self.addons.get_version(addon)
|
||||
version = version or self.get_version(addon)
|
||||
if not await self.dockers[addon].update(version):
|
||||
return False
|
||||
|
||||
|
@ -9,13 +9,19 @@ 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_MAP_DATA,
|
||||
ATTR_OPTIONS, ATTR_PORTS, STARTUP_ONCE, STARTUP_AFTER, STARTUP_BEFORE,
|
||||
BOOT_AUTO, BOOT_MANUAL, DOCKER_REPO, ATTR_INSTALLED)
|
||||
BOOT_AUTO, BOOT_MANUAL, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA)
|
||||
from ..tools import read_json_file, write_json_file
|
||||
|
||||
_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),
|
||||
@ -30,14 +36,17 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
|
||||
vol.Required(ATTR_MAP_CONFIG): vol.Boolean(),
|
||||
vol.Required(ATTR_MAP_SSL): vol.Boolean(),
|
||||
vol.Required(ATTR_OPTIONS): dict,
|
||||
vol.Required(ATTR_SCHEMA): {
|
||||
vol.Any: vol.In([V_STR, V_INT, V_FLOAT, V_BOOL])
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
class AddonsConfig(Config):
|
||||
"""Config for addons inside HassIO."""
|
||||
class AddonsData(Config):
|
||||
"""Hold data for addons inside HassIO."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize docker base wrapper."""
|
||||
"""Initialize data holder."""
|
||||
super().__init__(FILE_HASSIO_ADDONS)
|
||||
self.config
|
||||
self._addons_data = {}
|
||||
@ -110,6 +119,11 @@ class AddonsConfig(Config):
|
||||
self._data.pop(addon, None)
|
||||
self.save()
|
||||
|
||||
def set_options(self, addon, options):
|
||||
"""Store user addon options."""
|
||||
self._data[addon][ATTR_OPTIONS] = options
|
||||
self.save()
|
||||
|
||||
def get_options(self, addon):
|
||||
"""Return options with local changes."""
|
||||
opt = self._addons_data[addon][ATTR_OPTIONS]
|
||||
@ -180,3 +194,33 @@ class AddonsConfig(Config):
|
||||
"""Return True if addon options is written to data."""
|
||||
return write_json_file(
|
||||
self.path_addon_options(addon), self.get_options(addon))
|
||||
|
||||
def get_schema(self, addon):
|
||||
"""Create a schema for addon options."""
|
||||
raw_schema = self._addons_data[addon][ATTR_SCHEMA]
|
||||
|
||||
def validate(struct):
|
||||
"""Validate schema."""
|
||||
validated = {}
|
||||
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:
|
||||
validate[key] = str(value)
|
||||
elif typ == V_INT:
|
||||
validate[key] = int(value)
|
||||
elif typ == V_FLOAT:
|
||||
validate[key] = float(value)
|
||||
elif typ == V_BOOL:
|
||||
validate[key] = vol.Boolean()(value)
|
||||
except TypeError:
|
||||
raise vol.Invalid(
|
||||
"Type error for {}.".format(key)) from None
|
||||
|
||||
return validated
|
||||
|
||||
schema = vol.Schema(vol.All(dict(), validate))
|
||||
return schema
|
@ -41,10 +41,10 @@ class RestAPI(object):
|
||||
self.webapp.router.add_get('/network/info', api_net.info)
|
||||
self.webapp.router.add_get('/network/options', api_net.options)
|
||||
|
||||
def register_supervisor(self, host_controll, addon_manager):
|
||||
def register_supervisor(self, host_controll, addons):
|
||||
"""Register supervisor function."""
|
||||
api_supervisor = APISupervisor(
|
||||
self.config, self.loop, host_controll, addon_manager)
|
||||
self.config, self.loop, host_controll, addons)
|
||||
|
||||
self.webapp.router.add_get('/supervisor/ping', api_supervisor.ping)
|
||||
self.webapp.router.add_get('/supervisor/info', api_supervisor.info)
|
||||
@ -59,9 +59,9 @@ class RestAPI(object):
|
||||
self.webapp.router.add_get('/homeassistant/info', api_hass.info)
|
||||
self.webapp.router.add_get('/homeassistant/update', api_hass.update)
|
||||
|
||||
def register_addons(self, addon_manager):
|
||||
def register_addons(self, addons):
|
||||
"""Register homeassistant function."""
|
||||
api_addons = APIAddons(self.config, self.loop, addon_manager)
|
||||
api_addons = APIAddons(self.config, self.loop, addons)
|
||||
|
||||
self.webapp.router.add_get('/addons/{addon}/info', api_addons.info)
|
||||
self.webapp.router.add_get(
|
||||
|
@ -5,7 +5,8 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from .util import api_process, api_validate
|
||||
from ..const import ATTR_VERSION, ATTR_CURRENT
|
||||
from ..const import (
|
||||
ATTR_VERSION, ATTR_CURRENT, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -17,26 +18,99 @@ SCHEMA_VERSION = vol.Schema({
|
||||
class APIAddons(object):
|
||||
"""Handle rest api for addons functions."""
|
||||
|
||||
def __init__(self, config, loop, addon_manager):
|
||||
def __init__(self, config, loop, addons):
|
||||
"""Initialize homeassistant rest api part."""
|
||||
self.config = config
|
||||
self.loop = loop
|
||||
self.addon_manager = addon_manager
|
||||
self.addons = addons
|
||||
|
||||
def _extract_addon(self, request, check_installed=True):
|
||||
"""Return addon and if not exists trow a exception."""
|
||||
addon = request.match_info.get('addon')
|
||||
|
||||
# check data
|
||||
if not self.addons.exists_addon(addon):
|
||||
raise RuntimeError("Addon not exists.")
|
||||
if check_installed and not self.addons.is_installed(addon):
|
||||
raise RuntimeError("Addon is not installed.")
|
||||
|
||||
return addon
|
||||
|
||||
@api_process
|
||||
async def info(self, request):
|
||||
"""Return host information."""
|
||||
"""Return addon information."""
|
||||
addon = self._extract_addon(request)
|
||||
|
||||
info = {
|
||||
ATTR_VERSION: self.addons.version_installed(addon),
|
||||
ATTR_CURRENT: self.addons.get_version(addon),
|
||||
ATTR_STATE: await self.addons.state_addon(addon),
|
||||
ATTR_BOOT: self.addons.get_boot(addon),
|
||||
ATTR_OPTIONS: self.addons.get_options(addon),
|
||||
}
|
||||
return info
|
||||
|
||||
@api_process
|
||||
async def options(self, request):
|
||||
"""Store user options for addon."""
|
||||
addon = self._extract_addon(request)
|
||||
schema = self.addons.get_schema(addon)
|
||||
|
||||
options = await api_validate(schema, request)
|
||||
self.addons.set_options(addon, options)
|
||||
return True
|
||||
|
||||
@api_process
|
||||
async def install(self, request):
|
||||
"""Install addon."""
|
||||
body = await api_validate(SCHEMA_VERSION, request)
|
||||
addon = self._extract_addon(request, check_installed=False)
|
||||
version = body.get(
|
||||
ATTR_VERSION, self.addons.get_version(addon))
|
||||
|
||||
return await asyncio.shield(
|
||||
self.addons.addon_install(addon, version))
|
||||
|
||||
@api_process
|
||||
async def uninstall(self, request):
|
||||
"""Uninstall addon."""
|
||||
addon = self._extract_addon(request)
|
||||
|
||||
return await asyncio.shield(
|
||||
self.addons.addon_uninstall(addon))
|
||||
|
||||
@api_process
|
||||
async def start(self, request):
|
||||
"""Start addon."""
|
||||
addon = self._extract_addon(request)
|
||||
|
||||
if await self.addons.state_addon(addon) == STATE_STARTED:
|
||||
raise RuntimeError("Addon is already running.")
|
||||
|
||||
return await asyncio.shield(
|
||||
self.addons.addon_start(addon))
|
||||
|
||||
@api_process
|
||||
async def stop(self, request):
|
||||
"""Stop addon."""
|
||||
addon = self._extract_addon(request)
|
||||
|
||||
if await self.addons.state_addon(addon) == STATE_STOPED:
|
||||
raise RuntimeError("Addon is already stoped.")
|
||||
|
||||
return await asyncio.shield(
|
||||
self.addons.addon_stop(addon))
|
||||
|
||||
@api_process
|
||||
async def update(self, request):
|
||||
"""Update host OS."""
|
||||
"""Update addon."""
|
||||
body = await api_validate(SCHEMA_VERSION, request)
|
||||
version = body.get(ATTR_VERSION, self.config.current_homeassistant)
|
||||
addon = self._extract_addon(request)
|
||||
version = body.get(
|
||||
ATTR_VERSION, self.addons.get_version(addon))
|
||||
|
||||
if self.dock_hass.in_progress:
|
||||
raise RuntimeError("Other task is in progress.")
|
||||
if version == self.addons.version_installed(addon):
|
||||
raise RuntimeError("Version is already in use.")
|
||||
|
||||
if version == self.dock_hass.version:
|
||||
raise RuntimeError("%s is already in use.", version)
|
||||
|
||||
return await asyncio.shield(self.dock_hass.update(version))
|
||||
return await asyncio.shield(
|
||||
self.addons.addon_update(addon, version))
|
||||
|
@ -22,12 +22,12 @@ SCHEMA_VERSION = vol.Schema({
|
||||
class APISupervisor(object):
|
||||
"""Handle rest api for supervisor functions."""
|
||||
|
||||
def __init__(self, config, loop, host_controll, addon_manager):
|
||||
def __init__(self, config, loop, host_controll, addons):
|
||||
"""Initialize supervisor rest api part."""
|
||||
self.config = config
|
||||
self.loop = loop
|
||||
self.host_controll = host_controll
|
||||
self.addon_manager = addon_manager
|
||||
self.addons = addons
|
||||
|
||||
@api_process
|
||||
async def ping(self, request):
|
||||
@ -41,7 +41,7 @@ class APISupervisor(object):
|
||||
ATTR_VERSION: HASSIO_VERSION,
|
||||
ATTR_CURRENT: self.config.current_hassio,
|
||||
ATTR_BETA: self.config.upstream_beta,
|
||||
ATTR_ADDONS: self.addon_manager.addons.list,
|
||||
ATTR_ADDONS: self.addons.list,
|
||||
}
|
||||
return info
|
||||
|
||||
|
@ -13,6 +13,7 @@ DOCKER_REPO = "pvizeli"
|
||||
HASSIO_SHARE = "/data"
|
||||
|
||||
RUN_UPDATE_INFO_TASKS = 28800
|
||||
RUN_RELOAD_ADDONS_TASKS = 28800
|
||||
|
||||
FILE_HASSIO_ADDONS = "{}/addons.json".format(HASSIO_SHARE)
|
||||
FILE_HASSIO_CONFIG = "{}/config.json".format(HASSIO_SHARE)
|
||||
@ -48,3 +49,5 @@ STARTUP_AFTER = 'after'
|
||||
STARTUP_ONCE = 'once'
|
||||
BOOT_STOP = 'auto'
|
||||
BOOT_MANUAL = 'manual'
|
||||
STATE_STARTED = 'started'
|
||||
STATE_STOPED = 'stoped'
|
||||
|
@ -9,7 +9,8 @@ from . import bootstrap
|
||||
from .addons import AddonManager
|
||||
from .api import RestAPI
|
||||
from .host_controll import HostControll
|
||||
from .const import SOCKET_DOCKER, RUN_UPDATE_INFO_TASKS
|
||||
from .const import (
|
||||
SOCKET_DOCKER, RUN_UPDATE_INFO_TASKS, RUN_RELOAD_ADDONS_TASKS)
|
||||
from .scheduler import Scheduler
|
||||
from .dock.homeassistant import DockerHomeAssistant
|
||||
from .dock.supervisor import DockerSupervisor
|
||||
@ -40,7 +41,7 @@ class HassIO(object):
|
||||
self.host_controll = HostControll(self.loop)
|
||||
|
||||
# init addon system
|
||||
self.addon_manager = AddonManager(self.config, self.loop, self.dock)
|
||||
self.addons = AddonManager(self.config, self.loop, self.dock)
|
||||
|
||||
async def setup(self):
|
||||
"""Setup HassIO orchestration."""
|
||||
@ -60,9 +61,9 @@ class HassIO(object):
|
||||
# rest api views
|
||||
self.api.register_host(self.host_controll)
|
||||
self.api.register_network(self.host_controll)
|
||||
self.api.register_supervisor(self.host_controll, self.addon_manager)
|
||||
self.api.register_supervisor(self.host_controll, self.addons)
|
||||
self.api.register_homeassistant(self.homeassistant)
|
||||
self.api.register_addons(self.addon_manager)
|
||||
self.api.register_addons(self.addons)
|
||||
|
||||
# schedule update info tasks
|
||||
self.scheduler.register_task(
|
||||
@ -75,7 +76,11 @@ class HassIO(object):
|
||||
await self._setup_homeassistant()
|
||||
|
||||
# Load addons
|
||||
await self.addon_manager.prepare()
|
||||
await self.addons.prepare()
|
||||
|
||||
# schedule addon update task
|
||||
self.scheduler.register_task(
|
||||
self.addons.relaod, RUN_RELOAD_ADDONS_TASKS, first_run=True)
|
||||
|
||||
async def start(self):
|
||||
"""Start HassIO orchestration."""
|
||||
|
@ -14,17 +14,17 @@ HASS_DOCKER_NAME = 'homeassistant'
|
||||
class DockerAddon(DockerBase):
|
||||
"""Docker hassio wrapper for HomeAssistant."""
|
||||
|
||||
def __init__(self, config, loop, dock, addon_config, addon):
|
||||
def __init__(self, config, loop, dock, addons_data, addon):
|
||||
"""Initialize docker homeassistant wrapper."""
|
||||
super().__init__(
|
||||
config, loop, dock, image=addon_config.get_image(addon))
|
||||
config, loop, dock, image=addons_data.get_image(addon))
|
||||
self.addon = addon
|
||||
self.addon_config
|
||||
self.addons_data
|
||||
|
||||
@property
|
||||
def docker_name(self):
|
||||
"""Return name of docker container."""
|
||||
return "addon_{}".format(self.addon_config.get_slug(self.addon))
|
||||
return "addon_{}".format(self.addons_data.get_slug(self.addon))
|
||||
|
||||
def _run(self):
|
||||
"""Run docker image.
|
||||
@ -39,15 +39,15 @@ class DockerAddon(DockerBase):
|
||||
|
||||
# volumes
|
||||
volumes = {
|
||||
self.addon_config.path_data_docker(self.addon): {
|
||||
self.addons_data.path_data_docker(self.addon): {
|
||||
'bind': '/data', 'mode': 'rw'
|
||||
}}
|
||||
if self.addon_config.need_config(self.addon):
|
||||
if self.addons_data.need_config(self.addon):
|
||||
volumes.update({
|
||||
self.config.path_config_docker: {
|
||||
'bind': '/config', 'mode': 'rw'
|
||||
}})
|
||||
if self.addon_config.need_ssl(self.addon):
|
||||
if self.addons_data.need_ssl(self.addon):
|
||||
volumes.update({
|
||||
self.config.path_ssl_docker: {
|
||||
'bind': '/ssl', 'mode': 'rw'
|
||||
@ -59,7 +59,7 @@ class DockerAddon(DockerBase):
|
||||
name=self.docker_name,
|
||||
detach=True,
|
||||
network_mode='bridge',
|
||||
ports=self.addon_config.get_ports(self.addon),
|
||||
ports=self.addons_data.get_ports(self.addon),
|
||||
restart_policy={
|
||||
"Name": "on-failure",
|
||||
"MaximumRetryCount": 10,
|
||||
|
Loading…
x
Reference in New Issue
Block a user