diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index 286cdab0c..a02a66148 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -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 diff --git a/hassio/addons/config.py b/hassio/addons/data.py similarity index 79% rename from hassio/addons/config.py rename to hassio/addons/data.py index 31bd92f2f..aa8571725 100644 --- a/hassio/addons/config.py +++ b/hassio/addons/data.py @@ -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 diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index 6135530a4..70cad7121 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -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( diff --git a/hassio/api/addons.py b/hassio/api/addons.py index e87f3bcbf..fade3c8a2 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -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)) diff --git a/hassio/api/supervisor.py b/hassio/api/supervisor.py index 4cd69c31f..109ff76ec 100644 --- a/hassio/api/supervisor.py +++ b/hassio/api/supervisor.py @@ -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 diff --git a/hassio/const.py b/hassio/const.py index 41621b806..2c6123277 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -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' diff --git a/hassio/core.py b/hassio/core.py index 176772ddd..5faf29359 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -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.""" diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 510ae01f3..667968787 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -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,