mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-06-26 20:06:32 +00:00
Change handling of addon config (#12)
Add an optional extended description…
This commit is contained in:
parent
4edd02ca8e
commit
a028f11a4a
6
API.md
6
API.md
@ -37,6 +37,7 @@ On success
|
|||||||
"slug": "xy",
|
"slug": "xy",
|
||||||
"version": "CURRENT_VERSION",
|
"version": "CURRENT_VERSION",
|
||||||
"installed": "none|INSTALL_VERSION",
|
"installed": "none|INSTALL_VERSION",
|
||||||
|
"dedicated": "bool",
|
||||||
"description": "description"
|
"description": "description"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -146,7 +147,10 @@ Output the raw docker log
|
|||||||
|
|
||||||
- `/addons/{addon}/options`
|
- `/addons/{addon}/options`
|
||||||
```json
|
```json
|
||||||
{ }
|
{
|
||||||
|
"boot": "auto|manual",
|
||||||
|
"options": {},
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `/addons/{addon}/start`
|
- `/addons/{addon}/start`
|
||||||
|
@ -45,13 +45,8 @@ class AddonManager(AddonsData):
|
|||||||
self.read_addons_repo()
|
self.read_addons_repo()
|
||||||
|
|
||||||
# remove stalled addons
|
# remove stalled addons
|
||||||
tasks = []
|
|
||||||
for addon in self.list_removed:
|
for addon in self.list_removed:
|
||||||
_LOGGER.info("Old addon %s found")
|
_LOGGER.warning("Dedicated addon '%s' found!", addon)
|
||||||
tasks.append(self.loop.create_task(self.uninstall(addon)))
|
|
||||||
|
|
||||||
if tasks:
|
|
||||||
await asyncio.wait(tasks, loop=self.loop)
|
|
||||||
|
|
||||||
async def auto_boot(self, start_type):
|
async def auto_boot(self, start_type):
|
||||||
"""Boot addons with mode auto."""
|
"""Boot addons with mode auto."""
|
||||||
@ -88,7 +83,7 @@ class AddonManager(AddonsData):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
self.dockers[addon] = addon_docker
|
self.dockers[addon] = addon_docker
|
||||||
self.set_install_addon(addon, version)
|
self.set_addon_install(addon, version)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def uninstall(self, addon):
|
async def uninstall(self, addon):
|
||||||
@ -110,7 +105,7 @@ class AddonManager(AddonsData):
|
|||||||
shutil.rmtree(self.path_data(addon))
|
shutil.rmtree(self.path_data(addon))
|
||||||
|
|
||||||
self.dockers.pop(addon)
|
self.dockers.pop(addon)
|
||||||
self.set_uninstall_addon(addon)
|
self.set_addon_uninstall(addon)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def state(self, addon):
|
async def state(self, addon):
|
||||||
@ -150,8 +145,13 @@ class AddonManager(AddonsData):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
version = version or self.get_version(addon)
|
version = version or self.get_version(addon)
|
||||||
|
is_running = self.dockers[addon].is_running()
|
||||||
|
|
||||||
|
# update
|
||||||
if await self.dockers[addon].update(version):
|
if await self.dockers[addon].update(version):
|
||||||
self.set_version(addon, version)
|
self.set_addon_update(addon, version)
|
||||||
|
if is_running:
|
||||||
|
await self.start(addon)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -10,13 +10,15 @@ from ..const import (
|
|||||||
FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON,
|
FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON,
|
||||||
ATTR_STARTUP, ATTR_BOOT, ATTR_MAP_SSL, ATTR_MAP_CONFIG, ATTR_OPTIONS,
|
ATTR_STARTUP, ATTR_BOOT, ATTR_MAP_SSL, ATTR_MAP_CONFIG, ATTR_OPTIONS,
|
||||||
ATTR_PORTS, BOOT_AUTO, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA,
|
ATTR_PORTS, BOOT_AUTO, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA,
|
||||||
ATTR_IMAGE)
|
ATTR_IMAGE, ATTR_DEDICATED)
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
from ..tools import read_json_file, write_json_file
|
from ..tools import read_json_file, write_json_file
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ADDONS_REPO_PATTERN = "{}/*/config.json"
|
ADDONS_REPO_PATTERN = "{}/*/config.json"
|
||||||
|
SYSTEM = "system"
|
||||||
|
USER = "user"
|
||||||
|
|
||||||
|
|
||||||
class AddonsData(Config):
|
class AddonsData(Config):
|
||||||
@ -26,12 +28,22 @@ class AddonsData(Config):
|
|||||||
"""Initialize data holder."""
|
"""Initialize data holder."""
|
||||||
super().__init__(FILE_HASSIO_ADDONS)
|
super().__init__(FILE_HASSIO_ADDONS)
|
||||||
self.config = config
|
self.config = config
|
||||||
self._addons_data = {}
|
self._addons_data = self._data.get(SYSTEM, {})
|
||||||
|
self._user_data = self._data.get(USER, {})
|
||||||
|
self._current_data = {}
|
||||||
self.arch = None
|
self.arch = None
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""Store data to config file."""
|
||||||
|
self._data = {
|
||||||
|
USER: self._user_data,
|
||||||
|
SYSTEM: self._addons_data,
|
||||||
|
}
|
||||||
|
super().save()
|
||||||
|
|
||||||
def read_addons_repo(self):
|
def read_addons_repo(self):
|
||||||
"""Read data from addons repository."""
|
"""Read data from addons repository."""
|
||||||
self._addons_data = {}
|
self._current_data = {}
|
||||||
|
|
||||||
self._read_addons_folder(self.config.path_addons_repo)
|
self._read_addons_folder(self.config.path_addons_repo)
|
||||||
self._read_addons_folder(self.config.path_addons_custom)
|
self._read_addons_folder(self.config.path_addons_custom)
|
||||||
@ -45,7 +57,7 @@ class AddonsData(Config):
|
|||||||
addon_config = read_json_file(addon)
|
addon_config = read_json_file(addon)
|
||||||
|
|
||||||
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
|
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
|
||||||
self._addons_data[addon_config[ATTR_SLUG]] = addon_config
|
self._current_data[addon_config[ATTR_SLUG]] = addon_config
|
||||||
|
|
||||||
except (OSError, KeyError):
|
except (OSError, KeyError):
|
||||||
_LOGGER.warning("Can't read %s", addon)
|
_LOGGER.warning("Can't read %s", addon)
|
||||||
@ -57,24 +69,25 @@ class AddonsData(Config):
|
|||||||
@property
|
@property
|
||||||
def list_installed(self):
|
def list_installed(self):
|
||||||
"""Return a list of installed addons."""
|
"""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())
|
return set(self._addons_data.keys())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def list(self):
|
def list(self):
|
||||||
"""Return a list of available addons."""
|
"""Return a list of available addons."""
|
||||||
data = []
|
data = []
|
||||||
for addon, values in self._addons_data.items():
|
all_addons = {**self._addons_data, **self._current_data}
|
||||||
|
dedicated = self.list_removed
|
||||||
|
|
||||||
|
for addon, values in all_addons.items():
|
||||||
|
i_version = self._addons_data.get(addon, {}).get(ATTR_VERSION)
|
||||||
|
|
||||||
data.append({
|
data.append({
|
||||||
ATTR_NAME: values[ATTR_NAME],
|
ATTR_NAME: values[ATTR_NAME],
|
||||||
ATTR_SLUG: values[ATTR_SLUG],
|
ATTR_SLUG: values[ATTR_SLUG],
|
||||||
ATTR_DESCRIPTON: values[ATTR_DESCRIPTON],
|
ATTR_DESCRIPTON: values[ATTR_DESCRIPTON],
|
||||||
ATTR_VERSION: values[ATTR_VERSION],
|
ATTR_VERSION: values[ATTR_VERSION],
|
||||||
ATTR_INSTALLED: self._data.get(addon, {}).get(ATTR_VERSION),
|
ATTR_INSTALLED: i_version,
|
||||||
|
ATTR_DEDICATED: addon in dedicated,
|
||||||
})
|
})
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@ -82,7 +95,7 @@ class AddonsData(Config):
|
|||||||
def list_startup(self, start_type):
|
def list_startup(self, start_type):
|
||||||
"""Get list of installed addon with need start by type."""
|
"""Get list of installed addon with need start by type."""
|
||||||
addon_list = set()
|
addon_list = set()
|
||||||
for addon in self._data.keys():
|
for addon in self._addons_data.keys():
|
||||||
if self.get_boot(addon) != BOOT_AUTO:
|
if self.get_boot(addon) != BOOT_AUTO:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -99,58 +112,64 @@ class AddonsData(Config):
|
|||||||
def list_removed(self):
|
def list_removed(self):
|
||||||
"""Return local addons they not support from repo."""
|
"""Return local addons they not support from repo."""
|
||||||
addon_list = set()
|
addon_list = set()
|
||||||
for addon in self._data.keys():
|
for addon in self._addons_data.keys():
|
||||||
if addon not in self._addons_data:
|
if addon not in self._current_data:
|
||||||
addon_list.add(addon)
|
addon_list.add(addon)
|
||||||
|
|
||||||
return addon_list
|
return addon_list
|
||||||
|
|
||||||
def exists_addon(self, addon):
|
def exists_addon(self, addon):
|
||||||
"""Return True if a addon exists."""
|
"""Return True if a addon exists."""
|
||||||
return addon in self._addons_data
|
return addon in self._current_data or addon in self._addons_data
|
||||||
|
|
||||||
def is_installed(self, addon):
|
def is_installed(self, addon):
|
||||||
"""Return True if a addon is installed."""
|
"""Return True if a addon is installed."""
|
||||||
return addon in self._data
|
return addon in self._addons_data
|
||||||
|
|
||||||
def version_installed(self, addon):
|
def version_installed(self, addon):
|
||||||
"""Return installed version."""
|
"""Return installed version."""
|
||||||
return self._data[addon][ATTR_VERSION]
|
return self._addons_data[addon][ATTR_VERSION]
|
||||||
|
|
||||||
def set_install_addon(self, addon, version):
|
def set_addon_install(self, addon, version):
|
||||||
"""Set addon as installed."""
|
"""Set addon as installed."""
|
||||||
self._data[addon] = {
|
self._addons_data[addon] = self._current_data[addon]
|
||||||
ATTR_VERSION: version,
|
self._user_data[addon] = {
|
||||||
ATTR_OPTIONS: {}
|
ATTR_OPTIONS: {},
|
||||||
}
|
}
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def set_uninstall_addon(self, addon):
|
def set_addon_uninstall(self, addon):
|
||||||
"""Set addon as uninstalled."""
|
"""Set addon as uninstalled."""
|
||||||
self._data.pop(addon, None)
|
self._addons_data.pop(addon, None)
|
||||||
|
self._user_data.pop(addon, None)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def set_addon_update(self, addon, version):
|
||||||
|
"""Update version of addon."""
|
||||||
|
self._addons_data[addon] = self._current_data[addon]
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def set_options(self, addon, options):
|
def set_options(self, addon, options):
|
||||||
"""Store user addon options."""
|
"""Store user addon options."""
|
||||||
self._data[addon][ATTR_OPTIONS] = options
|
self._user_data[addon][ATTR_OPTIONS] = options
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def set_version(self, addon, version):
|
def set_boot(self, addon, boot):
|
||||||
"""Update version of addon."""
|
"""Store user boot options."""
|
||||||
self._data[addon][ATTR_VERSION] = version
|
self._user_data[addon][ATTR_BOOT] = boot
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def get_options(self, addon):
|
def get_options(self, addon):
|
||||||
"""Return options with local changes."""
|
"""Return options with local changes."""
|
||||||
opt = self._addons_data[addon][ATTR_OPTIONS]
|
return {
|
||||||
if addon in self._data:
|
**self._addons_data[addon][ATTR_OPTIONS],
|
||||||
opt.update(self._data[addon][ATTR_OPTIONS])
|
**self._user_data[addon][ATTR_OPTIONS],
|
||||||
return opt
|
}
|
||||||
|
|
||||||
def get_boot(self, addon):
|
def get_boot(self, addon):
|
||||||
"""Return boot config with prio local settings."""
|
"""Return boot config with prio local settings."""
|
||||||
if ATTR_BOOT in self._data[addon]:
|
if ATTR_BOOT in self._user_data[addon]:
|
||||||
return self._data[addon][ATTR_BOOT]
|
return self._user_data[addon][ATTR_BOOT]
|
||||||
|
|
||||||
return self._addons_data[addon][ATTR_BOOT]
|
return self._addons_data[addon][ATTR_BOOT]
|
||||||
|
|
||||||
@ -164,11 +183,9 @@ class AddonsData(Config):
|
|||||||
|
|
||||||
def get_version(self, addon):
|
def get_version(self, addon):
|
||||||
"""Return version of addon."""
|
"""Return version of addon."""
|
||||||
return self._addons_data[addon][ATTR_VERSION]
|
if addon not in self._current_data:
|
||||||
|
return self.version_installed(addon)
|
||||||
def get_slug(self, addon):
|
return self._current_data[addon][ATTR_VERSION]
|
||||||
"""Return slug of addon."""
|
|
||||||
return self._addons_data[addon][ATTR_SLUG]
|
|
||||||
|
|
||||||
def get_ports(self, addon):
|
def get_ports(self, addon):
|
||||||
"""Return ports of addon."""
|
"""Return ports of addon."""
|
||||||
@ -176,11 +193,12 @@ class AddonsData(Config):
|
|||||||
|
|
||||||
def get_image(self, addon):
|
def get_image(self, addon):
|
||||||
"""Return image name of addon."""
|
"""Return image name of addon."""
|
||||||
if ATTR_IMAGE not in self._addons_data[addon]:
|
addon_data = self._addons_data.get(addon, self._current_data[addon])
|
||||||
return "{}/{}-addon-{}".format(
|
|
||||||
DOCKER_REPO, self.arch, self.get_slug(addon))
|
|
||||||
|
|
||||||
return self._addons_data[addon][ATTR_IMAGE]
|
if ATTR_IMAGE not in addon_data:
|
||||||
|
return "{}/{}-addon-{}".format(DOCKER_REPO, self.arch, addon)
|
||||||
|
|
||||||
|
return addon_data[ATTR_IMAGE]
|
||||||
|
|
||||||
def need_config(self, addon):
|
def need_config(self, addon):
|
||||||
"""Return True if config map is needed."""
|
"""Return True if config map is needed."""
|
||||||
@ -192,13 +210,11 @@ class AddonsData(Config):
|
|||||||
|
|
||||||
def path_data(self, addon):
|
def path_data(self, addon):
|
||||||
"""Return addon data path inside supervisor."""
|
"""Return addon data path inside supervisor."""
|
||||||
return "{}/{}".format(
|
return "{}/{}".format(self.config.path_addons_data, addon)
|
||||||
self.config.path_addons_data, self._addons_data[addon][ATTR_SLUG])
|
|
||||||
|
|
||||||
def path_data_docker(self, addon):
|
def path_data_docker(self, addon):
|
||||||
"""Return addon data path external for docker."""
|
"""Return addon data path external for docker."""
|
||||||
return "{}/{}".format(self.config.path_addons_data_docker,
|
return "{}/{}".format(self.config.path_addons_data_docker, addon)
|
||||||
self._addons_data[addon][ATTR_SLUG])
|
|
||||||
|
|
||||||
def path_addon_options(self, addon):
|
def path_addon_options(self, addon):
|
||||||
"""Return path to addons options."""
|
"""Return path to addons options."""
|
||||||
|
@ -8,7 +8,7 @@ from voluptuous.humanize import humanize_error
|
|||||||
from .util import api_process, api_process_raw, api_validate
|
from .util import api_process, api_process_raw, api_validate
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_VERSION, ATTR_CURRENT, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
|
ATTR_VERSION, ATTR_CURRENT, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
|
||||||
STATE_STOPPED, STATE_STARTED)
|
STATE_STOPPED, STATE_STARTED, BOOT_AUTO, BOOT_MANUAL)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -16,6 +16,10 @@ SCHEMA_VERSION = vol.Schema({
|
|||||||
vol.Optional(ATTR_VERSION): vol.Coerce(str),
|
vol.Optional(ATTR_VERSION): vol.Coerce(str),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
SCHEMA_OPTIONS = vol.Schema({
|
||||||
|
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL])
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class APIAddons(object):
|
class APIAddons(object):
|
||||||
"""Handle rest api for addons functions."""
|
"""Handle rest api for addons functions."""
|
||||||
@ -56,10 +60,19 @@ class APIAddons(object):
|
|||||||
async def options(self, request):
|
async def options(self, request):
|
||||||
"""Store user options for addon."""
|
"""Store user options for addon."""
|
||||||
addon = self._extract_addon(request)
|
addon = self._extract_addon(request)
|
||||||
schema = self.addons.get_schema(addon)
|
options_schema = self.addons.get_schema(addon)
|
||||||
|
|
||||||
|
addon_schema = SCHEMA_OPTIONS.extend({
|
||||||
|
vol.Optional(ATTR_OPTIONS): options_schema,
|
||||||
|
})
|
||||||
|
|
||||||
|
addon_config = await api_validate(addon_schema, request)
|
||||||
|
|
||||||
|
if ATTR_OPTIONS in addon_config:
|
||||||
|
self.addons.set_options(addon, addon_config[ATTR_OPTIONS])
|
||||||
|
if ATTR_BOOT in addon_config:
|
||||||
|
self.addons.set_options(addon, addon_config[ATTR_BOOT])
|
||||||
|
|
||||||
options = await api_validate(schema, request)
|
|
||||||
self.addons.set_options(addon, options)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""Const file for HassIO."""
|
"""Const file for HassIO."""
|
||||||
HASSIO_VERSION = '0.12'
|
HASSIO_VERSION = '0.13'
|
||||||
|
|
||||||
URL_HASSIO_VERSION = \
|
URL_HASSIO_VERSION = \
|
||||||
'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json'
|
'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json'
|
||||||
@ -45,6 +45,7 @@ ATTR_MAP_CONFIG = 'map_config'
|
|||||||
ATTR_MAP_SSL = 'map_ssl'
|
ATTR_MAP_SSL = 'map_ssl'
|
||||||
ATTR_OPTIONS = 'options'
|
ATTR_OPTIONS = 'options'
|
||||||
ATTR_INSTALLED = 'installed'
|
ATTR_INSTALLED = 'installed'
|
||||||
|
ATTR_DEDICATED = 'dedicated'
|
||||||
ATTR_STATE = 'state'
|
ATTR_STATE = 'state'
|
||||||
ATTR_SCHEMA = 'schema'
|
ATTR_SCHEMA = 'schema'
|
||||||
ATTR_IMAGE = 'image'
|
ATTR_IMAGE = 'image'
|
||||||
|
@ -223,7 +223,6 @@ class DockerBase(object):
|
|||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
old_run = self._is_running()
|
|
||||||
old_image = "{}:{}".format(self.image, self.version)
|
old_image = "{}:{}".format(self.image, self.version)
|
||||||
|
|
||||||
_LOGGER.info("Update docker %s with %s:%s",
|
_LOGGER.info("Update docker %s with %s:%s",
|
||||||
@ -238,9 +237,6 @@ class DockerBase(object):
|
|||||||
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
|
|
||||||
if old_run:
|
|
||||||
self._run()
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -24,7 +24,7 @@ class DockerAddon(DockerBase):
|
|||||||
@property
|
@property
|
||||||
def docker_name(self):
|
def docker_name(self):
|
||||||
"""Return name of docker container."""
|
"""Return name of docker container."""
|
||||||
return "addon_{}".format(self.addons_data.get_slug(self.addon))
|
return "addon_{}".format(self.addon)
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
"""Run docker image.
|
"""Run docker image.
|
||||||
|
@ -62,3 +62,16 @@ class DockerHomeAssistant(DockerBase):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def update(self, tag):
|
||||||
|
"""Update homeassistant docker image."""
|
||||||
|
if self._lock.locked():
|
||||||
|
_LOGGER.error("Can't excute update while a task is in progress")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
if await self.loop.run_in_executor(None, self._update, tag):
|
||||||
|
await self.loop.run_in_executor(None, self._run)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
Loading…
x
Reference in New Issue
Block a user