mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-19 07:06:30 +00:00
commit
a3f67809a6
10
API.md
10
API.md
@ -69,7 +69,15 @@ On success
|
|||||||
"version": "INSTALL_VERSION",
|
"version": "INSTALL_VERSION",
|
||||||
"current": "CURRENT_VERSION",
|
"current": "CURRENT_VERSION",
|
||||||
"beta": "true|false",
|
"beta": "true|false",
|
||||||
"addons": {}
|
"addons": [
|
||||||
|
{
|
||||||
|
"name": "xy bla",
|
||||||
|
"slug": "xy",
|
||||||
|
"version": "CURRENT_VERSION",
|
||||||
|
"installed": "none|INSTALL_VERSION",
|
||||||
|
"description": "description"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
159
hassio/addons/__init__.py
Normal file
159
hassio/addons/__init__.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
"""Init file for HassIO addons."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from .data import AddonsData
|
||||||
|
from .git import AddonsRepo
|
||||||
|
from ..const import STATE_STOPPED, STATE_STARTED
|
||||||
|
from ..dock.addon import DockerAddon
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AddonManager(AddonsData):
|
||||||
|
"""Manage addons inside HassIO."""
|
||||||
|
|
||||||
|
def __init__(self, config, loop, dock):
|
||||||
|
"""Initialize docker base wrapper."""
|
||||||
|
super().__init__(config)
|
||||||
|
|
||||||
|
self.loop = loop
|
||||||
|
self.dock = dock
|
||||||
|
self.repo = AddonsRepo(config, loop)
|
||||||
|
self.dockers = {}
|
||||||
|
|
||||||
|
async def prepare(self, arch):
|
||||||
|
"""Startup addon management."""
|
||||||
|
self.arch = arch
|
||||||
|
|
||||||
|
# load addon repository
|
||||||
|
if await self.repo.load():
|
||||||
|
self.read_addons_repo()
|
||||||
|
|
||||||
|
# load installed addons
|
||||||
|
for addon in self.list_installed:
|
||||||
|
self.dockers[addon] = DockerAddon(
|
||||||
|
self.config, self.loop, self.dock, self, addon)
|
||||||
|
|
||||||
|
async def relaod(self):
|
||||||
|
"""Update addons from repo and reload list."""
|
||||||
|
if not await self.repo.pull():
|
||||||
|
return
|
||||||
|
self.read_addons_repo()
|
||||||
|
|
||||||
|
# remove stalled addons
|
||||||
|
tasks = []
|
||||||
|
for addon in self.list_removed:
|
||||||
|
_LOGGER.info("Old addon %s found")
|
||||||
|
tasks.append(self.loop.create_task(self.dockers[addon].remove()))
|
||||||
|
|
||||||
|
if tasks:
|
||||||
|
await asyncio.wait(tasks, loop=self.loop)
|
||||||
|
|
||||||
|
async def auto_boot(self, start_type):
|
||||||
|
"""Boot addons with mode auto."""
|
||||||
|
boot_list = self.list_startup(start_type)
|
||||||
|
tasks = []
|
||||||
|
|
||||||
|
for addon in boot_list:
|
||||||
|
tasks.append(self.loop.create_task(self.start(addon)))
|
||||||
|
|
||||||
|
_LOGGER.info("Startup %s run %d addons", start_type, len(tasks))
|
||||||
|
if tasks:
|
||||||
|
await asyncio.wait(tasks, loop=self.loop)
|
||||||
|
|
||||||
|
async def install(self, addon, version=None):
|
||||||
|
"""Install a addon."""
|
||||||
|
if not self.exists_addon(addon):
|
||||||
|
_LOGGER.error("Addon %s not exists for install", addon)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.is_installed(addon):
|
||||||
|
_LOGGER.error("Addon %s is already installed", addon)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not os.path.isdir(self.path_data(addon)):
|
||||||
|
_LOGGER.info("Create Home-Assistant addon data folder %s",
|
||||||
|
self.path_data(addon))
|
||||||
|
os.mkdir(self.path_data(addon))
|
||||||
|
|
||||||
|
addon_docker = DockerAddon(
|
||||||
|
self.config, self.loop, self.dock, self, addon)
|
||||||
|
|
||||||
|
version = version or self.get_version(addon)
|
||||||
|
if not await addon_docker.install(version):
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.dockers[addon] = addon_docker
|
||||||
|
self.set_install_addon(addon, version)
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def uninstall(self, addon):
|
||||||
|
"""Remove a addon."""
|
||||||
|
if not self.is_installed(addon):
|
||||||
|
_LOGGER.error("Addon %s is already uninstalled", addon)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if addon not in self.dockers:
|
||||||
|
_LOGGER.error("No docker found for addon %s", addon)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not await self.dockers[addon].remove():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if os.path.isdir(self.path_data(addon)):
|
||||||
|
_LOGGER.info("Remove Home-Assistant addon data folder %s",
|
||||||
|
self.path_data(addon))
|
||||||
|
shutil.rmtree(self.path_data(addon))
|
||||||
|
|
||||||
|
self.dockers.pop(addon)
|
||||||
|
self.set_uninstall_addon(addon)
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def state(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_STOPPED
|
||||||
|
|
||||||
|
async def start(self, addon):
|
||||||
|
"""Set options and start addon."""
|
||||||
|
if addon not in self.dockers:
|
||||||
|
_LOGGER.error("No docker found for addon %s", addon)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self.write_addon_options(addon):
|
||||||
|
_LOGGER.error("Can't write options for addon %s", addon)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return await self.dockers[addon].run()
|
||||||
|
|
||||||
|
async def stop(self, addon):
|
||||||
|
"""Stop addon."""
|
||||||
|
if addon not in self.dockers:
|
||||||
|
_LOGGER.error("No docker found for addon %s", addon)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return await self.dockers[addon].stop()
|
||||||
|
|
||||||
|
async def update(self, addon, version=None):
|
||||||
|
"""Update addon."""
|
||||||
|
if not self.is_installed(addon):
|
||||||
|
_LOGGER.error("Addon %s is not installed", addon)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if addon not in self.dockers:
|
||||||
|
_LOGGER.error("No docker found for addon %s", addon)
|
||||||
|
return False
|
||||||
|
|
||||||
|
version = version or self.get_version(addon)
|
||||||
|
if await self.dockers[addon].update(version):
|
||||||
|
self.set_version(addon, version)
|
||||||
|
return True
|
||||||
|
return False
|
263
hassio/addons/data.py
Normal file
263
hassio/addons/data.py
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
"""Init file for HassIO addons."""
|
||||||
|
import logging
|
||||||
|
import glob
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
|
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_OPTIONS,
|
||||||
|
ATTR_PORTS, STARTUP_ONCE, STARTUP_AFTER, STARTUP_BEFORE, BOOT_AUTO,
|
||||||
|
BOOT_MANUAL, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA, ATTR_IMAGE)
|
||||||
|
from ..config import Config
|
||||||
|
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),
|
||||||
|
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):
|
||||||
|
"""Hold data for addons inside HassIO."""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
"""Initialize data holder."""
|
||||||
|
super().__init__(FILE_HASSIO_ADDONS)
|
||||||
|
self.config = config
|
||||||
|
self._addons_data = {}
|
||||||
|
self.arch = None
|
||||||
|
|
||||||
|
def read_addons_repo(self):
|
||||||
|
"""Read data from addons repository."""
|
||||||
|
self._read_addons_folder(self.config.path_addons_repo)
|
||||||
|
self._read_addons_folder(self.config.path_addons_custom)
|
||||||
|
|
||||||
|
def _read_addons_folder(self, folder):
|
||||||
|
"""Read data from addons folder."""
|
||||||
|
pattern = ADDONS_REPO_PATTERN.format(folder)
|
||||||
|
|
||||||
|
for addon in glob.iglob(pattern):
|
||||||
|
try:
|
||||||
|
addon_config = read_json_file(addon)
|
||||||
|
|
||||||
|
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
|
||||||
|
self._addons_data[addon_config[ATTR_SLUG]] = addon_config
|
||||||
|
|
||||||
|
except (OSError, KeyError):
|
||||||
|
_LOGGER.warning("Can't read %s", addon)
|
||||||
|
|
||||||
|
except vol.Invalid as ex:
|
||||||
|
_LOGGER.warning("Can't read %s -> %s", addon,
|
||||||
|
humanize_error(addon_config, ex))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def list_installed(self):
|
||||||
|
"""Return a list of installed addons."""
|
||||||
|
return set(self._data.keys())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def list_all(self):
|
||||||
|
"""Return a list of available addons."""
|
||||||
|
return set(self._addons_data.keys())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def list(self):
|
||||||
|
"""Return a list of available addons."""
|
||||||
|
data = []
|
||||||
|
for addon, values in self._addons_data.items():
|
||||||
|
data.append({
|
||||||
|
ATTR_NAME: values[ATTR_NAME],
|
||||||
|
ATTR_SLUG: values[ATTR_SLUG],
|
||||||
|
ATTR_DESCRIPTON: values[ATTR_DESCRIPTON],
|
||||||
|
ATTR_VERSION: values[ATTR_VERSION],
|
||||||
|
ATTR_INSTALLED: self._data.get(addon, {}).get(ATTR_VERSION),
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def list_startup(self, start_type):
|
||||||
|
"""Get list of installed addon with need start by type."""
|
||||||
|
addon_list = set()
|
||||||
|
for addon in self._data.keys():
|
||||||
|
if self.get_boot(addon) != BOOT_AUTO:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self._addons_data[addon][ATTR_STARTUP] == start_type:
|
||||||
|
addon_list.add(addon)
|
||||||
|
except KeyError:
|
||||||
|
_LOGGER.warning("Orphaned addon detect %s", addon)
|
||||||
|
continue
|
||||||
|
|
||||||
|
return addon_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def list_removed(self):
|
||||||
|
"""Return local addons they not support from repo."""
|
||||||
|
addon_list = set()
|
||||||
|
for addon in self._data.keys():
|
||||||
|
if addon not in self._addons_data:
|
||||||
|
addon_list.add(addon)
|
||||||
|
|
||||||
|
return addon_list
|
||||||
|
|
||||||
|
def exists_addon(self, addon):
|
||||||
|
"""Return True if a addon exists."""
|
||||||
|
return addon in self._addons_data
|
||||||
|
|
||||||
|
def is_installed(self, addon):
|
||||||
|
"""Return True if a addon is installed."""
|
||||||
|
return addon in self._data
|
||||||
|
|
||||||
|
def version_installed(self, addon):
|
||||||
|
"""Return installed version."""
|
||||||
|
return self._data[addon][ATTR_VERSION]
|
||||||
|
|
||||||
|
def set_install_addon(self, addon, version):
|
||||||
|
"""Set addon as installed."""
|
||||||
|
self._data[addon] = {
|
||||||
|
ATTR_VERSION: version,
|
||||||
|
ATTR_OPTIONS: {}
|
||||||
|
}
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def set_uninstall_addon(self, addon):
|
||||||
|
"""Set addon as uninstalled."""
|
||||||
|
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 set_version(self, addon, version):
|
||||||
|
"""Update version of addon."""
|
||||||
|
self._data[addon][ATTR_VERSION] = version
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def get_options(self, addon):
|
||||||
|
"""Return options with local changes."""
|
||||||
|
opt = self._addons_data[addon][ATTR_OPTIONS]
|
||||||
|
if addon in self._data:
|
||||||
|
opt.update(self._data[addon][ATTR_OPTIONS])
|
||||||
|
return opt
|
||||||
|
|
||||||
|
def get_boot(self, addon):
|
||||||
|
"""Return boot config with prio local settings."""
|
||||||
|
if ATTR_BOOT in self._data[addon]:
|
||||||
|
return self._data[addon][ATTR_BOOT]
|
||||||
|
|
||||||
|
return self._addons_data[addon][ATTR_BOOT]
|
||||||
|
|
||||||
|
def get_name(self, addon):
|
||||||
|
"""Return name of addon."""
|
||||||
|
return self._addons_data[addon][ATTR_NAME]
|
||||||
|
|
||||||
|
def get_description(self, addon):
|
||||||
|
"""Return description of addon."""
|
||||||
|
return self._addons_data[addon][ATTR_DESCRIPTON]
|
||||||
|
|
||||||
|
def get_version(self, addon):
|
||||||
|
"""Return version of addon."""
|
||||||
|
return self._addons_data[addon][ATTR_VERSION]
|
||||||
|
|
||||||
|
def get_slug(self, addon):
|
||||||
|
"""Return slug of addon."""
|
||||||
|
return self._addons_data[addon][ATTR_SLUG]
|
||||||
|
|
||||||
|
def get_ports(self, addon):
|
||||||
|
"""Return ports of addon."""
|
||||||
|
return self._addons_data[addon].get(ATTR_PORTS)
|
||||||
|
|
||||||
|
def get_image(self, addon):
|
||||||
|
"""Return image name of addon."""
|
||||||
|
if ATTR_IMAGE not in self._addons_data[addon]:
|
||||||
|
return "{}/{}-addon-{}".format(
|
||||||
|
DOCKER_REPO, self.arch, self.get_slug(addon))
|
||||||
|
|
||||||
|
return self._addons_data[addon][ATTR_IMAGE]
|
||||||
|
|
||||||
|
def need_config(self, addon):
|
||||||
|
"""Return True if config map is needed."""
|
||||||
|
return self._addons_data[addon][ATTR_MAP_CONFIG]
|
||||||
|
|
||||||
|
def need_ssl(self, addon):
|
||||||
|
"""Return True if ssl map is needed."""
|
||||||
|
return self._addons_data[addon][ATTR_MAP_SSL]
|
||||||
|
|
||||||
|
def path_data(self, addon):
|
||||||
|
"""Return addon data path inside supervisor."""
|
||||||
|
return "{}/{}".format(
|
||||||
|
self.config.path_addons_data, self._addons_data[addon][ATTR_SLUG])
|
||||||
|
|
||||||
|
def path_data_docker(self, addon):
|
||||||
|
"""Return addon data path external for docker."""
|
||||||
|
return "{}/{}".format(self.config.path_addons_data_docker,
|
||||||
|
self._addons_data[addon][ATTR_SLUG])
|
||||||
|
|
||||||
|
def path_addon_options(self, addon):
|
||||||
|
"""Return path to addons options."""
|
||||||
|
return "{}/options.json".format(self.path_data(addon))
|
||||||
|
|
||||||
|
def write_addon_options(self, addon):
|
||||||
|
"""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."""
|
||||||
|
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
|
71
hassio/addons/git.py
Normal file
71
hassio/addons/git.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"""Init file for HassIO addons git."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import git
|
||||||
|
|
||||||
|
from ..const import URL_HASSIO_ADDONS
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AddonsRepo(object):
|
||||||
|
"""Manage addons git repo."""
|
||||||
|
|
||||||
|
def __init__(self, config, loop):
|
||||||
|
"""Initialize docker base wrapper."""
|
||||||
|
self.config = config
|
||||||
|
self.loop = loop
|
||||||
|
self.repo = None
|
||||||
|
self._lock = asyncio.Lock(loop=loop)
|
||||||
|
|
||||||
|
async def load(self):
|
||||||
|
"""Init git addon repo."""
|
||||||
|
if not os.path.isdir(self.config.path_addons_repo):
|
||||||
|
return await self.clone()
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
try:
|
||||||
|
_LOGGER.info("Load addons repository")
|
||||||
|
self.repo = await self.loop.run_in_executor(
|
||||||
|
None, git.Repo, self.config.path_addons_repo)
|
||||||
|
|
||||||
|
except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err:
|
||||||
|
_LOGGER.error("Can't load addons repo: %s.", err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def clone(self):
|
||||||
|
"""Clone git addon repo."""
|
||||||
|
async with self._lock:
|
||||||
|
try:
|
||||||
|
_LOGGER.info("Clone addons repository")
|
||||||
|
self.repo = await self.loop.run_in_executor(
|
||||||
|
None, git.Repo.clone_from, URL_HASSIO_ADDONS,
|
||||||
|
self.config.path_addons_repo)
|
||||||
|
|
||||||
|
except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err:
|
||||||
|
_LOGGER.error("Can't clone addons repo: %s.", err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def pull(self):
|
||||||
|
"""Pull git addon repo."""
|
||||||
|
if self._lock.locked():
|
||||||
|
_LOGGER.warning("It is already a task in progress.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
try:
|
||||||
|
_LOGGER.info("Pull addons repository")
|
||||||
|
await self.loop.run_in_executor(
|
||||||
|
None, self.repo.remotes.origin.pull)
|
||||||
|
|
||||||
|
except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err:
|
||||||
|
_LOGGER.error("Can't pull addons repo: %s.", err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
@ -3,10 +3,11 @@ import logging
|
|||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
|
from .addons import APIAddons
|
||||||
|
from .homeassistant import APIHomeAssistant
|
||||||
from .host import APIHost
|
from .host import APIHost
|
||||||
from .network import APINetwork
|
from .network import APINetwork
|
||||||
from .supervisor import APISupervisor
|
from .supervisor import APISupervisor
|
||||||
from .homeassistant import APIHomeAssistant
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -40,9 +41,10 @@ class RestAPI(object):
|
|||||||
self.webapp.router.add_get('/network/info', api_net.info)
|
self.webapp.router.add_get('/network/info', api_net.info)
|
||||||
self.webapp.router.add_get('/network/options', api_net.options)
|
self.webapp.router.add_get('/network/options', api_net.options)
|
||||||
|
|
||||||
def register_supervisor(self, host_controll):
|
def register_supervisor(self, host_controll, addons):
|
||||||
"""Register supervisor function."""
|
"""Register supervisor function."""
|
||||||
api_supervisor = APISupervisor(self.config, self.loop, host_controll)
|
api_supervisor = APISupervisor(
|
||||||
|
self.config, self.loop, host_controll, addons)
|
||||||
|
|
||||||
self.webapp.router.add_get('/supervisor/ping', api_supervisor.ping)
|
self.webapp.router.add_get('/supervisor/ping', api_supervisor.ping)
|
||||||
self.webapp.router.add_get('/supervisor/info', api_supervisor.info)
|
self.webapp.router.add_get('/supervisor/info', api_supervisor.info)
|
||||||
@ -57,6 +59,21 @@ class RestAPI(object):
|
|||||||
self.webapp.router.add_get('/homeassistant/info', api_hass.info)
|
self.webapp.router.add_get('/homeassistant/info', api_hass.info)
|
||||||
self.webapp.router.add_get('/homeassistant/update', api_hass.update)
|
self.webapp.router.add_get('/homeassistant/update', api_hass.update)
|
||||||
|
|
||||||
|
def register_addons(self, addons):
|
||||||
|
"""Register homeassistant function."""
|
||||||
|
api_addons = APIAddons(self.config, self.loop, addons)
|
||||||
|
|
||||||
|
self.webapp.router.add_get('/addons/{addon}/info', api_addons.info)
|
||||||
|
self.webapp.router.add_get(
|
||||||
|
'/addons/{addon}/install', api_addons.install)
|
||||||
|
self.webapp.router.add_get(
|
||||||
|
'/addons/{addon}/uninstall', api_addons.uninstall)
|
||||||
|
self.webapp.router.add_get('/addons/{addon}/start', api_addons.start)
|
||||||
|
self.webapp.router.add_get('/addons/{addon}/stop', api_addons.stop)
|
||||||
|
self.webapp.router.add_get('/addons/{addon}/update', api_addons.update)
|
||||||
|
self.webapp.router.add_get(
|
||||||
|
'/addons/{addon}/options', api_addons.options)
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""Run rest api webserver."""
|
"""Run rest api webserver."""
|
||||||
self._handler = self.webapp.make_handler(loop=self.loop)
|
self._handler = self.webapp.make_handler(loop=self.loop)
|
||||||
|
117
hassio/api/addons.py
Normal file
117
hassio/api/addons.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
"""Init file for HassIO homeassistant rest api."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from .util import api_process, api_validate
|
||||||
|
from ..const import (
|
||||||
|
ATTR_VERSION, ATTR_CURRENT, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
|
||||||
|
STATE_STOPPED, STATE_STARTED)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SCHEMA_VERSION = vol.Schema({
|
||||||
|
vol.Optional(ATTR_VERSION): vol.Coerce(str),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class APIAddons(object):
|
||||||
|
"""Handle rest api for addons functions."""
|
||||||
|
|
||||||
|
def __init__(self, config, loop, addons):
|
||||||
|
"""Initialize homeassistant rest api part."""
|
||||||
|
self.config = config
|
||||||
|
self.loop = loop
|
||||||
|
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 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),
|
||||||
|
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.install(addon, version), loop=self.loop)
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def uninstall(self, request):
|
||||||
|
"""Uninstall addon."""
|
||||||
|
addon = self._extract_addon(request)
|
||||||
|
|
||||||
|
return await asyncio.shield(
|
||||||
|
self.addons.uninstall(addon), loop=self.loop)
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def start(self, request):
|
||||||
|
"""Start addon."""
|
||||||
|
addon = self._extract_addon(request)
|
||||||
|
|
||||||
|
if await self.addons.state(addon) == STATE_STARTED:
|
||||||
|
raise RuntimeError("Addon is already running")
|
||||||
|
|
||||||
|
return await asyncio.shield(
|
||||||
|
self.addons.start(addon), loop=self.loop)
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def stop(self, request):
|
||||||
|
"""Stop addon."""
|
||||||
|
addon = self._extract_addon(request)
|
||||||
|
|
||||||
|
if await self.addons.state(addon) == STATE_STOPPED:
|
||||||
|
raise RuntimeError("Addon is already stoped")
|
||||||
|
|
||||||
|
return await asyncio.shield(
|
||||||
|
self.addons.stop(addon), loop=self.loop)
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def update(self, request):
|
||||||
|
"""Update addon."""
|
||||||
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
|
addon = self._extract_addon(request)
|
||||||
|
version = body.get(
|
||||||
|
ATTR_VERSION, self.addons.get_version(addon))
|
||||||
|
|
||||||
|
if version == self.addons.version_installed(addon):
|
||||||
|
raise RuntimeError("Version is already in use")
|
||||||
|
|
||||||
|
return await asyncio.shield(
|
||||||
|
self.addons.update(addon, version), loop=self.loop)
|
@ -40,9 +40,10 @@ class APIHomeAssistant(object):
|
|||||||
version = body.get(ATTR_VERSION, self.config.current_homeassistant)
|
version = body.get(ATTR_VERSION, self.config.current_homeassistant)
|
||||||
|
|
||||||
if self.dock_hass.in_progress:
|
if self.dock_hass.in_progress:
|
||||||
raise RuntimeError("Other task is in progress.")
|
raise RuntimeError("Other task is in progress")
|
||||||
|
|
||||||
if version == self.dock_hass.version:
|
if version == self.dock_hass.version:
|
||||||
raise RuntimeError("%s is already in use.", version)
|
raise RuntimeError("Version is already in use")
|
||||||
|
|
||||||
return await asyncio.shield(self.dock_hass.update(version))
|
return await asyncio.shield(
|
||||||
|
self.dock_hass.update(version), loop=self.loop)
|
||||||
|
@ -56,6 +56,6 @@ class APIHost(object):
|
|||||||
version = body.get(ATTR_VERSION)
|
version = body.get(ATTR_VERSION)
|
||||||
|
|
||||||
if version == self.host_controll.version:
|
if version == self.host_controll.version:
|
||||||
raise RuntimeError("%s is already in use.", version)
|
raise RuntimeError("Version is already in use")
|
||||||
|
|
||||||
return await self.host_controll.host_update(version=version)
|
return await self.host_controll.host_update(version=version)
|
||||||
|
@ -4,7 +4,8 @@ import logging
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from .util import api_process, api_process_hostcontroll, api_validate
|
from .util import api_process, api_process_hostcontroll, api_validate
|
||||||
from ..const import ATTR_VERSION, ATTR_CURRENT, ATTR_BETA, HASSIO_VERSION
|
from ..const import (
|
||||||
|
ATTR_ADDONS, ATTR_VERSION, ATTR_CURRENT, ATTR_BETA, HASSIO_VERSION)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -21,11 +22,12 @@ SCHEMA_VERSION = vol.Schema({
|
|||||||
class APISupervisor(object):
|
class APISupervisor(object):
|
||||||
"""Handle rest api for supervisor functions."""
|
"""Handle rest api for supervisor functions."""
|
||||||
|
|
||||||
def __init__(self, config, loop, host_controll):
|
def __init__(self, config, loop, host_controll, addons):
|
||||||
"""Initialize supervisor rest api part."""
|
"""Initialize supervisor rest api part."""
|
||||||
self.config = config
|
self.config = config
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.host_controll = host_controll
|
self.host_controll = host_controll
|
||||||
|
self.addons = addons
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def ping(self, request):
|
async def ping(self, request):
|
||||||
@ -39,8 +41,8 @@ class APISupervisor(object):
|
|||||||
ATTR_VERSION: HASSIO_VERSION,
|
ATTR_VERSION: HASSIO_VERSION,
|
||||||
ATTR_CURRENT: self.config.current_hassio,
|
ATTR_CURRENT: self.config.current_hassio,
|
||||||
ATTR_BETA: self.config.upstream_beta,
|
ATTR_BETA: self.config.upstream_beta,
|
||||||
|
ATTR_ADDONS: self.addons.list,
|
||||||
}
|
}
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
@ -60,6 +62,6 @@ class APISupervisor(object):
|
|||||||
version = body.get(ATTR_VERSION, self.config.current_hassio)
|
version = body.get(ATTR_VERSION, self.config.current_hassio)
|
||||||
|
|
||||||
if version == HASSIO_VERSION:
|
if version == HASSIO_VERSION:
|
||||||
raise RuntimeError("%s is already in use.", version)
|
raise RuntimeError("Version is already in use")
|
||||||
|
|
||||||
return await self.host_controll.supervisor_update(version=version)
|
return await self.host_controll.supervisor_update(version=version)
|
||||||
|
@ -82,7 +82,7 @@ async def api_validate(schema, request):
|
|||||||
"""Validate request data with schema."""
|
"""Validate request data with schema."""
|
||||||
data = await request.json(loads=json_loads)
|
data = await request.json(loads=json_loads)
|
||||||
try:
|
try:
|
||||||
schema(data)
|
data = schema(data)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
raise RuntimeError(humanize_error(data, ex)) from None
|
raise RuntimeError(humanize_error(data, ex)) from None
|
||||||
|
|
||||||
|
@ -26,6 +26,17 @@ def initialize_system_data(websession):
|
|||||||
_LOGGER.info("Create Home-Assistant ssl folder %s", config.path_ssl)
|
_LOGGER.info("Create Home-Assistant ssl folder %s", config.path_ssl)
|
||||||
os.mkdir(config.path_ssl)
|
os.mkdir(config.path_ssl)
|
||||||
|
|
||||||
|
# homeassistant addon data folder
|
||||||
|
if not os.path.isdir(config.path_addons_data):
|
||||||
|
_LOGGER.info("Create Home-Assistant addon data folder %s",
|
||||||
|
config.path_addons_data)
|
||||||
|
os.mkdir(config.path_addons_data)
|
||||||
|
|
||||||
|
if not os.path.isdir(config.path_addons_custom):
|
||||||
|
_LOGGER.info("Create Home-Assistant addon custom folder %s",
|
||||||
|
config.path_addons_custom)
|
||||||
|
os.mkdir(config.path_addons_custom)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
"""Bootstrap HassIO."""
|
"""Bootstrap HassIO."""
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from .const import FILE_HASSIO_CONFIG, HASSIO_SHARE
|
from .const import FILE_HASSIO_CONFIG, HASSIO_SHARE
|
||||||
from .tools import fetch_current_versions
|
from .tools import (
|
||||||
|
fetch_current_versions, write_json_file, read_json_file)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -14,26 +14,46 @@ HOMEASSISTANT_CURRENT = 'homeassistant_current'
|
|||||||
|
|
||||||
HASSIO_SSL = "{}/ssl"
|
HASSIO_SSL = "{}/ssl"
|
||||||
HASSIO_CURRENT = 'hassio_current'
|
HASSIO_CURRENT = 'hassio_current'
|
||||||
|
|
||||||
|
ADDONS_REPO = "{}/addons"
|
||||||
|
ADDONS_DATA = "{}/addons_data"
|
||||||
|
ADDONS_CUSTOM = "{}/addons_custom"
|
||||||
|
|
||||||
UPSTREAM_BETA = 'upstream_beta'
|
UPSTREAM_BETA = 'upstream_beta'
|
||||||
|
|
||||||
|
|
||||||
class CoreConfig(object):
|
class Config(object):
|
||||||
"""Hold all config data."""
|
"""Hold all config data."""
|
||||||
|
|
||||||
def __init__(self, websession, config_file=FILE_HASSIO_CONFIG):
|
def __init__(self, config_file):
|
||||||
"""Initialize config object."""
|
"""Initialize config object."""
|
||||||
self.websession = websession
|
|
||||||
self._filename = config_file
|
self._filename = config_file
|
||||||
self._data = {}
|
self._data = {}
|
||||||
|
|
||||||
# init or load data
|
# init or load data
|
||||||
if os.path.isfile(self._filename):
|
if os.path.isfile(self._filename):
|
||||||
try:
|
try:
|
||||||
with open(self._filename, 'r') as cfile:
|
self._data = read_json_file(self._filename)
|
||||||
self._data = json.loads(cfile.read())
|
|
||||||
except OSError:
|
except OSError:
|
||||||
_LOGGER.warning("Can't read %s", self._filename)
|
_LOGGER.warning("Can't read %s", self._filename)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""Store data to config file."""
|
||||||
|
if not write_json_file(self._filename, self._data):
|
||||||
|
_LOGGER.exception("Can't store config in %s", self._filename)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(Config):
|
||||||
|
"""Hold all core config data."""
|
||||||
|
|
||||||
|
def __init__(self, websession):
|
||||||
|
"""Initialize config object."""
|
||||||
|
self.websession = websession
|
||||||
|
|
||||||
|
super().__init__(FILE_HASSIO_CONFIG)
|
||||||
|
|
||||||
# init data
|
# init data
|
||||||
if not self._data:
|
if not self._data:
|
||||||
self._data.update({
|
self._data.update({
|
||||||
@ -42,17 +62,6 @@ class CoreConfig(object):
|
|||||||
})
|
})
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def save(self):
|
|
||||||
"""Store data to config file."""
|
|
||||||
try:
|
|
||||||
with open(self._filename, 'w') as conf_file:
|
|
||||||
conf_file.write(json.dumps(self._data))
|
|
||||||
except OSError:
|
|
||||||
_LOGGER.exception("Can't store config in %s", self._filename)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def fetch_update_infos(self):
|
async def fetch_update_infos(self):
|
||||||
"""Read current versions from web."""
|
"""Read current versions from web."""
|
||||||
current = await fetch_current_versions(
|
current = await fetch_current_versions(
|
||||||
@ -112,3 +121,23 @@ class CoreConfig(object):
|
|||||||
def path_ssl(self):
|
def path_ssl(self):
|
||||||
"""Return SSL path inside supervisor."""
|
"""Return SSL path inside supervisor."""
|
||||||
return HASSIO_SSL.format(HASSIO_SHARE)
|
return HASSIO_SSL.format(HASSIO_SHARE)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_addons_repo(self):
|
||||||
|
"""Return git repo path for addons."""
|
||||||
|
return ADDONS_REPO.format(HASSIO_SHARE)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_addons_custom(self):
|
||||||
|
"""Return path for customs addons."""
|
||||||
|
return ADDONS_CUSTOM.format(HASSIO_SHARE)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_addons_data(self):
|
||||||
|
"""Return root addon data folder."""
|
||||||
|
return ADDONS_DATA.format(HASSIO_SHARE)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_addons_data_docker(self):
|
||||||
|
"""Return root addon data folder extern for docker."""
|
||||||
|
return ADDONS_DATA.format(os.environ['SUPERVISOR_SHARE'])
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
"""Const file for HassIO."""
|
"""Const file for HassIO."""
|
||||||
HASSIO_VERSION = '0.5'
|
HASSIO_VERSION = '0.6'
|
||||||
|
|
||||||
URL_HASSIO_VERSION = \
|
URL_HASSIO_VERSION = \
|
||||||
'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json'
|
'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json'
|
||||||
URL_HASSIO_VERSION_BETA = \
|
URL_HASSIO_VERSION_BETA = \
|
||||||
'https://raw.githubusercontent.com/pvizeli/hassio/master/version_beta.json'
|
'https://raw.githubusercontent.com/pvizeli/hassio/master/version_beta.json'
|
||||||
|
|
||||||
URL_ADDONS_REPO = 'https://github.com/pvizeli/hassio-addons'
|
URL_HASSIO_ADDONS = 'https://github.com/pvizeli/hassio-addons'
|
||||||
|
|
||||||
|
DOCKER_REPO = "pvizeli"
|
||||||
|
|
||||||
HASSIO_SHARE = "/data"
|
HASSIO_SHARE = "/data"
|
||||||
|
|
||||||
RUN_UPDATE_INFO_TASKS = 28800
|
RUN_UPDATE_INFO_TASKS = 28800
|
||||||
|
RUN_RELOAD_ADDONS_TASKS = 28800
|
||||||
|
|
||||||
FILE_HASSIO_ADDONS = "{}/addons.json".format(HASSIO_SHARE)
|
FILE_HASSIO_ADDONS = "{}/addons.json".format(HASSIO_SHARE)
|
||||||
FILE_HASSIO_CONFIG = "{}/config.json".format(HASSIO_SHARE)
|
FILE_HASSIO_CONFIG = "{}/config.json".format(HASSIO_SHARE)
|
||||||
@ -25,6 +28,28 @@ JSON_MESSAGE = 'message'
|
|||||||
RESULT_ERROR = 'error'
|
RESULT_ERROR = 'error'
|
||||||
RESULT_OK = 'ok'
|
RESULT_OK = 'ok'
|
||||||
|
|
||||||
|
ATTR_ADDONS = 'addons'
|
||||||
ATTR_VERSION = 'version'
|
ATTR_VERSION = 'version'
|
||||||
ATTR_CURRENT = 'current'
|
ATTR_CURRENT = 'current'
|
||||||
ATTR_BETA = 'beta'
|
ATTR_BETA = 'beta'
|
||||||
|
ATTR_NAME = 'name'
|
||||||
|
ATTR_SLUG = 'slug'
|
||||||
|
ATTR_DESCRIPTON = 'description'
|
||||||
|
ATTR_STARTUP = 'startup'
|
||||||
|
ATTR_BOOT = 'boot'
|
||||||
|
ATTR_PORTS = 'ports'
|
||||||
|
ATTR_MAP_CONFIG = 'map_config'
|
||||||
|
ATTR_MAP_SSL = 'map_ssl'
|
||||||
|
ATTR_OPTIONS = 'options'
|
||||||
|
ATTR_INSTALLED = 'installed'
|
||||||
|
ATTR_STATE = 'state'
|
||||||
|
ATTR_SCHEMA = 'schema'
|
||||||
|
ATTR_IMAGE = 'image'
|
||||||
|
|
||||||
|
STARTUP_BEFORE = 'before'
|
||||||
|
STARTUP_AFTER = 'after'
|
||||||
|
STARTUP_ONCE = 'once'
|
||||||
|
BOOT_AUTO = 'auto'
|
||||||
|
BOOT_MANUAL = 'manual'
|
||||||
|
STATE_STARTED = 'started'
|
||||||
|
STATE_STOPPED = 'stopped'
|
||||||
|
@ -6,12 +6,16 @@ import aiohttp
|
|||||||
import docker
|
import docker
|
||||||
|
|
||||||
from . import bootstrap
|
from . import bootstrap
|
||||||
|
from .addons import AddonManager
|
||||||
from .api import RestAPI
|
from .api import RestAPI
|
||||||
from .host_controll import HostControll
|
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,
|
||||||
|
STARTUP_AFTER, STARTUP_BEFORE)
|
||||||
from .scheduler import Scheduler
|
from .scheduler import Scheduler
|
||||||
from .dock.homeassistant import DockerHomeAssistant
|
from .dock.homeassistant import DockerHomeAssistant
|
||||||
from .dock.supervisor import DockerSupervisor
|
from .dock.supervisor import DockerSupervisor
|
||||||
|
from .tools import get_arch_from_image
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -38,6 +42,9 @@ class HassIO(object):
|
|||||||
# init HostControll
|
# init HostControll
|
||||||
self.host_controll = HostControll(self.loop)
|
self.host_controll = HostControll(self.loop)
|
||||||
|
|
||||||
|
# init addon system
|
||||||
|
self.addons = AddonManager(self.config, self.loop, self.dock)
|
||||||
|
|
||||||
async def setup(self):
|
async def setup(self):
|
||||||
"""Setup HassIO orchestration."""
|
"""Setup HassIO orchestration."""
|
||||||
# supervisor
|
# supervisor
|
||||||
@ -56,8 +63,9 @@ class HassIO(object):
|
|||||||
# rest api views
|
# rest api views
|
||||||
self.api.register_host(self.host_controll)
|
self.api.register_host(self.host_controll)
|
||||||
self.api.register_network(self.host_controll)
|
self.api.register_network(self.host_controll)
|
||||||
self.api.register_supervisor(self.host_controll)
|
self.api.register_supervisor(self.host_controll, self.addons)
|
||||||
self.api.register_homeassistant(self.homeassistant)
|
self.api.register_homeassistant(self.homeassistant)
|
||||||
|
self.api.register_addons(self.addons)
|
||||||
|
|
||||||
# schedule update info tasks
|
# schedule update info tasks
|
||||||
self.scheduler.register_task(
|
self.scheduler.register_task(
|
||||||
@ -69,14 +77,33 @@ class HassIO(object):
|
|||||||
_LOGGER.info("No HomeAssistant docker found.")
|
_LOGGER.info("No HomeAssistant docker found.")
|
||||||
await self._setup_homeassistant()
|
await self._setup_homeassistant()
|
||||||
|
|
||||||
|
# Load addons
|
||||||
|
arch = get_arch_from_image(self.supervisor.image)
|
||||||
|
await self.addons.prepare(arch)
|
||||||
|
|
||||||
|
# schedule addon update task
|
||||||
|
self.scheduler.register_task(
|
||||||
|
self.addons.relaod, RUN_RELOAD_ADDONS_TASKS, first_run=True)
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""Start HassIO orchestration."""
|
"""Start HassIO orchestration."""
|
||||||
# start api
|
# start api
|
||||||
await self.api.start()
|
await self.api.start()
|
||||||
|
|
||||||
|
# HomeAssistant is already running / supervisor have only reboot
|
||||||
|
if await self.homeassistant.is_running():
|
||||||
|
_LOGGER.info("HassIO reboot detected")
|
||||||
|
return
|
||||||
|
|
||||||
|
# start addon mark as before
|
||||||
|
await self.addons.auto_boot(STARTUP_BEFORE)
|
||||||
|
|
||||||
# run HomeAssistant
|
# run HomeAssistant
|
||||||
await self.homeassistant.run()
|
await self.homeassistant.run()
|
||||||
|
|
||||||
|
# start addon mark as after
|
||||||
|
await self.addons.auto_boot(STARTUP_AFTER)
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
"""Stop a running orchestration."""
|
"""Stop a running orchestration."""
|
||||||
tasks = [self.websession.close(), self.api.stop()]
|
tasks = [self.websession.close(), self.api.stop()]
|
||||||
|
@ -53,7 +53,7 @@ class DockerBase(object):
|
|||||||
|
|
||||||
image.tag(self.image, tag='latest')
|
image.tag(self.image, tag='latest')
|
||||||
self.version = get_version_from_env(image.attrs['Config']['Env'])
|
self.version = get_version_from_env(image.attrs['Config']['Env'])
|
||||||
_LOGGER.info("Tag image %s with version %s as latest.",
|
_LOGGER.info("Tag image %s with version %s as latest",
|
||||||
self.image, self.version)
|
self.image, self.version)
|
||||||
except docker.errors.APIError as err:
|
except docker.errors.APIError as err:
|
||||||
_LOGGER.error("Can't install %s:%s -> %s.", self.image, tag, err)
|
_LOGGER.error("Can't install %s:%s -> %s.", self.image, tag, err)
|
||||||
@ -122,7 +122,7 @@ class DockerBase(object):
|
|||||||
self.image = self.container.attrs['Config']['Image']
|
self.image = self.container.attrs['Config']['Image']
|
||||||
self.version = get_version_from_env(
|
self.version = get_version_from_env(
|
||||||
self.container.attrs['Config']['Env'])
|
self.container.attrs['Config']['Env'])
|
||||||
_LOGGER.info("Attach to image %s with version %s.",
|
_LOGGER.info("Attach to image %s with version %s",
|
||||||
self.image, self.version)
|
self.image, self.version)
|
||||||
except (docker.errors.DockerException, KeyError):
|
except (docker.errors.DockerException, KeyError):
|
||||||
_LOGGER.fatal(
|
_LOGGER.fatal(
|
||||||
@ -138,7 +138,7 @@ class DockerBase(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
_LOGGER.info("Run docker image %s with version %s.",
|
_LOGGER.info("Run docker image %s with version %s",
|
||||||
self.image, self.version)
|
self.image, self.version)
|
||||||
return await self.loop.run_in_executor(None, self._run)
|
return await self.loop.run_in_executor(None, self._run)
|
||||||
|
|
||||||
@ -177,6 +177,37 @@ class DockerBase(object):
|
|||||||
|
|
||||||
self.container = None
|
self.container = None
|
||||||
|
|
||||||
|
async def remove(self):
|
||||||
|
"""Remove docker container."""
|
||||||
|
if self._lock.locked():
|
||||||
|
_LOGGER.error("Can't excute remove while a task is in progress")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
return await self.loop.run_in_executor(None, self._remove)
|
||||||
|
|
||||||
|
def _remove(self):
|
||||||
|
"""remove docker container.
|
||||||
|
|
||||||
|
Need run inside executor.
|
||||||
|
"""
|
||||||
|
if self._is_running():
|
||||||
|
self._stop()
|
||||||
|
|
||||||
|
_LOGGER.info("Remove docker %s with latest and %s",
|
||||||
|
self.image, self.version)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.dock.images.remove(
|
||||||
|
image="{}:latest".format(self.image), force=True)
|
||||||
|
self.dock.images.remove(
|
||||||
|
image="{}:{}".format(self.image, self.version), force=True)
|
||||||
|
except docker.errors.DockerException as err:
|
||||||
|
_LOGGER.warning("Can't remove image %s -> %s", self.image, err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
async def update(self, tag):
|
async def update(self, tag):
|
||||||
"""Update a docker image.
|
"""Update a docker image.
|
||||||
|
|
||||||
@ -194,10 +225,10 @@ class DockerBase(object):
|
|||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
old_image = "{}:{}".format(self.image, self.version)
|
|
||||||
old_run = self._is_running()
|
old_run = self._is_running()
|
||||||
|
old_image = "{}:{}".format(self.image, self.version)
|
||||||
|
|
||||||
_LOGGER.info("Update docker %s with %s:%s.",
|
_LOGGER.info("Update docker %s with %s:%s",
|
||||||
old_image, self.image, tag)
|
old_image, self.image, tag)
|
||||||
|
|
||||||
# update docker image
|
# update docker image
|
||||||
@ -208,7 +239,7 @@ class DockerBase(object):
|
|||||||
self.dock.images.remove(image=old_image, force=True)
|
self.dock.images.remove(image=old_image, force=True)
|
||||||
except docker.errors.DockerException as err:
|
except docker.errors.DockerException as err:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Can't remove old image %s -> %s.", old_image, err)
|
"Can't remove old image %s -> %s", old_image, err)
|
||||||
# restore
|
# restore
|
||||||
if old_run:
|
if old_run:
|
||||||
self._run()
|
self._run()
|
||||||
|
76
hassio/dock/addon.py
Normal file
76
hassio/dock/addon.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
"""Init file for HassIO addon docker object."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import docker
|
||||||
|
|
||||||
|
from . import DockerBase
|
||||||
|
from ..tools import get_version_from_env
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
HASS_DOCKER_NAME = 'homeassistant'
|
||||||
|
|
||||||
|
|
||||||
|
class DockerAddon(DockerBase):
|
||||||
|
"""Docker hassio wrapper for HomeAssistant."""
|
||||||
|
|
||||||
|
def __init__(self, config, loop, dock, addons_data, addon):
|
||||||
|
"""Initialize docker homeassistant wrapper."""
|
||||||
|
super().__init__(
|
||||||
|
config, loop, dock, image=addons_data.get_image(addon))
|
||||||
|
self.addon = addon
|
||||||
|
self.addons_data = addons_data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def docker_name(self):
|
||||||
|
"""Return name of docker container."""
|
||||||
|
return "addon_{}".format(self.addons_data.get_slug(self.addon))
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
"""Run docker image.
|
||||||
|
|
||||||
|
Need run inside executor.
|
||||||
|
"""
|
||||||
|
if self._is_running():
|
||||||
|
return
|
||||||
|
|
||||||
|
# cleanup old container
|
||||||
|
self._stop()
|
||||||
|
|
||||||
|
# volumes
|
||||||
|
volumes = {
|
||||||
|
self.addons_data.path_data_docker(self.addon): {
|
||||||
|
'bind': '/data', 'mode': 'rw'
|
||||||
|
}}
|
||||||
|
if self.addons_data.need_config(self.addon):
|
||||||
|
volumes.update({
|
||||||
|
self.config.path_config_docker: {
|
||||||
|
'bind': '/config', 'mode': 'rw'
|
||||||
|
}})
|
||||||
|
if self.addons_data.need_ssl(self.addon):
|
||||||
|
volumes.update({
|
||||||
|
self.config.path_ssl_docker: {
|
||||||
|
'bind': '/ssl', 'mode': 'rw'
|
||||||
|
}})
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.container = self.dock.containers.run(
|
||||||
|
self.image,
|
||||||
|
name=self.docker_name,
|
||||||
|
detach=True,
|
||||||
|
network_mode='bridge',
|
||||||
|
ports=self.addons_data.get_ports(self.addon),
|
||||||
|
restart_policy={
|
||||||
|
"Name": "on-failure",
|
||||||
|
"MaximumRetryCount": 10,
|
||||||
|
},
|
||||||
|
volumes=volumes,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.version = get_version_from_env(
|
||||||
|
self.container.attrs['Config']['Env'])
|
||||||
|
except docker.errors.DockerException as err:
|
||||||
|
_LOGGER.error("Can't run %s -> %s", self.image, err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
@ -60,7 +60,7 @@ class DockerHomeAssistant(DockerBase):
|
|||||||
self.version = get_version_from_env(
|
self.version = get_version_from_env(
|
||||||
self.container.attrs['Config']['Env'])
|
self.container.attrs['Config']['Env'])
|
||||||
except docker.errors.DockerException as err:
|
except docker.errors.DockerException as err:
|
||||||
_LOGGER.error("Can't run %s -> %s.", self.image, err)
|
_LOGGER.error("Can't run %s -> %s", self.image, err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -27,3 +27,7 @@ class DockerSupervisor(DockerBase):
|
|||||||
async def update(self, tag):
|
async def update(self, tag):
|
||||||
"""Update docker image."""
|
"""Update docker image."""
|
||||||
raise RuntimeError("Not support on supervisor docker container!")
|
raise RuntimeError("Not support on supervisor docker container!")
|
||||||
|
|
||||||
|
async def remove(self):
|
||||||
|
"""Remove docker image."""
|
||||||
|
raise RuntimeError("Not support on supervisor docker container!")
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Tools file for HassIO."""
|
"""Tools file for HassIO."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
@ -12,6 +13,7 @@ from .const import URL_HASSIO_VERSION, URL_HASSIO_VERSION_BETA
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
_RE_VERSION = re.compile(r"VERSION=(.*)")
|
_RE_VERSION = re.compile(r"VERSION=(.*)")
|
||||||
|
_IMAGE_ARCH = re.compile(r".*/([a-z0-9]*)-hassio-supervisor")
|
||||||
|
|
||||||
|
|
||||||
async def fetch_current_versions(websession, beta=False):
|
async def fetch_current_versions(websession, beta=False):
|
||||||
@ -25,9 +27,19 @@ async def fetch_current_versions(websession, beta=False):
|
|||||||
async with websession.get(url) as request:
|
async with websession.get(url) as request:
|
||||||
return await request.json(content_type=None)
|
return await request.json(content_type=None)
|
||||||
|
|
||||||
except (ValueError, aiohttp.ClientError, asyncio.TimeoutError) as err:
|
except (aiohttp.ClientError, asyncio.TimeoutError, KeyError) as err:
|
||||||
_LOGGER.warning("Can't fetch versions from %s! %s", url, err)
|
_LOGGER.warning("Can't fetch versions from %s! %s", url, err)
|
||||||
|
|
||||||
|
except json.JSONDecodeError as err:
|
||||||
|
_LOGGER.warning("Can't parse versions from %s! %s", url, err)
|
||||||
|
|
||||||
|
|
||||||
|
def get_arch_from_image(image):
|
||||||
|
"""Return arch from hassio image name."""
|
||||||
|
found = _IMAGE_ARCH.match(image)
|
||||||
|
if found:
|
||||||
|
return found.group(1)
|
||||||
|
|
||||||
|
|
||||||
def get_version_from_env(env_list):
|
def get_version_from_env(env_list):
|
||||||
"""Extract Version from ENV list."""
|
"""Extract Version from ENV list."""
|
||||||
@ -56,3 +68,20 @@ def get_local_ip(loop):
|
|||||||
return socket.gethostbyname(socket.gethostname())
|
return socket.gethostbyname(socket.gethostname())
|
||||||
finally:
|
finally:
|
||||||
sock.close()
|
sock.close()
|
||||||
|
|
||||||
|
|
||||||
|
def write_json_file(jsonfile, data):
|
||||||
|
"""Write a json file."""
|
||||||
|
try:
|
||||||
|
with open(jsonfile, 'w') as conf_file:
|
||||||
|
conf_file.write(json.dumps(data))
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def read_json_file(jsonfile):
|
||||||
|
"""Read a json file and return a dict."""
|
||||||
|
with open(jsonfile, 'r') as cfile:
|
||||||
|
return json.loads(cfile.read())
|
||||||
|
3
setup.py
3
setup.py
@ -29,7 +29,7 @@ setup(
|
|||||||
keywords=['docker', 'home-assistant', 'api'],
|
keywords=['docker', 'home-assistant', 'api'],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
platforms='any',
|
platforms='any',
|
||||||
packages=['hassio', 'hassio.dock', 'hassio.api'],
|
packages=['hassio', 'hassio.dock', 'hassio.api', 'hassio.addons'],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'async_timeout',
|
'async_timeout',
|
||||||
@ -37,5 +37,6 @@ setup(
|
|||||||
'docker',
|
'docker',
|
||||||
'colorlog',
|
'colorlog',
|
||||||
'voluptuous',
|
'voluptuous',
|
||||||
|
'gitpython',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"hassio_tag": "0.5",
|
"hassio_tag": "0.6",
|
||||||
"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