Skip to content

Commit

Permalink
Change handling of addon config (#12)
Browse files Browse the repository at this point in the history
Add an optional extended description…
  • Loading branch information
pvizeli authored Apr 25, 2017
1 parent 4edd02c commit a028f11
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 66 deletions.
6 changes: 5 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ On success
"slug": "xy",
"version": "CURRENT_VERSION",
"installed": "none|INSTALL_VERSION",
"dedicated": "bool",
"description": "description"
}
]
Expand Down Expand Up @@ -146,7 +147,10 @@ Output the raw docker log

- `/addons/{addon}/options`
```json
{ }
{
"boot": "auto|manual",
"options": {},
}
```

- `/addons/{addon}/start`
Expand Down
18 changes: 9 additions & 9 deletions hassio/addons/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,8 @@ async def reload(self):
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.uninstall(addon)))

if tasks:
await asyncio.wait(tasks, loop=self.loop)
_LOGGER.warning("Dedicated addon '%s' found!", addon)

async def auto_boot(self, start_type):
"""Boot addons with mode auto."""
Expand Down Expand Up @@ -88,7 +83,7 @@ async def install(self, addon, version=None):
return False

self.dockers[addon] = addon_docker
self.set_install_addon(addon, version)
self.set_addon_install(addon, version)
return True

async def uninstall(self, addon):
Expand All @@ -110,7 +105,7 @@ async def uninstall(self, addon):
shutil.rmtree(self.path_data(addon))

self.dockers.pop(addon)
self.set_uninstall_addon(addon)
self.set_addon_uninstall(addon)
return True

async def state(self, addon):
Expand Down Expand Up @@ -150,8 +145,13 @@ async def update(self, addon, version=None):
return False

version = version or self.get_version(addon)
is_running = self.dockers[addon].is_running()

# update
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 False

Expand Down
108 changes: 62 additions & 46 deletions hassio/addons/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
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, BOOT_AUTO, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA,
ATTR_IMAGE)
ATTR_IMAGE, ATTR_DEDICATED)
from ..config import Config
from ..tools import read_json_file, write_json_file

_LOGGER = logging.getLogger(__name__)

ADDONS_REPO_PATTERN = "{}/*/config.json"
SYSTEM = "system"
USER = "user"


class AddonsData(Config):
Expand All @@ -26,12 +28,22 @@ def __init__(self, config):
"""Initialize data holder."""
super().__init__(FILE_HASSIO_ADDONS)
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

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):
"""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_custom)
Expand All @@ -45,7 +57,7 @@ def _read_addons_folder(self, folder):
addon_config = read_json_file(addon)

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):
_LOGGER.warning("Can't read %s", addon)
Expand All @@ -57,32 +69,33 @@ def _read_addons_folder(self, folder):
@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():
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({
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),
ATTR_INSTALLED: i_version,
ATTR_DEDICATED: addon in dedicated,
})

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():
for addon in self._addons_data.keys():
if self.get_boot(addon) != BOOT_AUTO:
continue

Expand All @@ -99,58 +112,64 @@ def list_startup(self, start_type):
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:
for addon in self._addons_data.keys():
if addon not in self._current_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
return addon in self._current_data or addon in self._addons_data

def is_installed(self, addon):
"""Return True if a addon is installed."""
return addon in self._data
return addon in self._addons_data

def version_installed(self, addon):
"""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."""
self._data[addon] = {
ATTR_VERSION: version,
ATTR_OPTIONS: {}
self._addons_data[addon] = self._current_data[addon]
self._user_data[addon] = {
ATTR_OPTIONS: {},
}
self.save()

def set_uninstall_addon(self, addon):
def set_addon_uninstall(self, addon):
"""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()

def set_options(self, addon, options):
"""Store user addon options."""
self._data[addon][ATTR_OPTIONS] = options
self._user_data[addon][ATTR_OPTIONS] = options
self.save()

def set_version(self, addon, version):
"""Update version of addon."""
self._data[addon][ATTR_VERSION] = version
def set_boot(self, addon, boot):
"""Store user boot options."""
self._user_data[addon][ATTR_BOOT] = boot
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
return {
**self._addons_data[addon][ATTR_OPTIONS],
**self._user_data[addon][ATTR_OPTIONS],
}

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]
if ATTR_BOOT in self._user_data[addon]:
return self._user_data[addon][ATTR_BOOT]

return self._addons_data[addon][ATTR_BOOT]

Expand All @@ -164,23 +183,22 @@ def get_description(self, addon):

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]
if addon not in self._current_data:
return self.version_installed(addon)
return self._current_data[addon][ATTR_VERSION]

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))
addon_data = self._addons_data.get(addon, self._current_data[addon])

if ATTR_IMAGE not in addon_data:
return "{}/{}-addon-{}".format(DOCKER_REPO, self.arch, addon)

return self._addons_data[addon][ATTR_IMAGE]
return addon_data[ATTR_IMAGE]

def need_config(self, addon):
"""Return True if config map is needed."""
Expand All @@ -192,13 +210,11 @@ def need_ssl(self, addon):

def path_data(self, addon):
"""Return addon data path inside supervisor."""
return "{}/{}".format(
self.config.path_addons_data, self._addons_data[addon][ATTR_SLUG])
return "{}/{}".format(self.config.path_addons_data, addon)

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])
return "{}/{}".format(self.config.path_addons_data_docker, addon)

def path_addon_options(self, addon):
"""Return path to addons options."""
Expand Down
21 changes: 17 additions & 4 deletions hassio/api/addons.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@
from .util import api_process, api_process_raw, api_validate
from ..const import (
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__)

SCHEMA_VERSION = vol.Schema({
vol.Optional(ATTR_VERSION): vol.Coerce(str),
})

SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL])
})


class APIAddons(object):
"""Handle rest api for addons functions."""
Expand Down Expand Up @@ -56,10 +60,19 @@ async def info(self, request):
async def options(self, request):
"""Store user options for addon."""
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

@api_process
Expand Down
3 changes: 2 additions & 1 deletion hassio/const.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Const file for HassIO."""
HASSIO_VERSION = '0.12'
HASSIO_VERSION = '0.13'

URL_HASSIO_VERSION = \
'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json'
Expand Down Expand Up @@ -45,6 +45,7 @@
ATTR_MAP_SSL = 'map_ssl'
ATTR_OPTIONS = 'options'
ATTR_INSTALLED = 'installed'
ATTR_DEDICATED = 'dedicated'
ATTR_STATE = 'state'
ATTR_SCHEMA = 'schema'
ATTR_IMAGE = 'image'
Expand Down
4 changes: 0 additions & 4 deletions hassio/dock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,6 @@ def _update(self, tag):
Need run inside executor.
"""
old_run = self._is_running()
old_image = "{}:{}".format(self.image, self.version)

_LOGGER.info("Update docker %s with %s:%s",
Expand All @@ -238,9 +237,6 @@ def _update(self, tag):
except docker.errors.DockerException as err:
_LOGGER.warning(
"Can't remove old image %s -> %s", old_image, err)
# restore
if old_run:
self._run()
return True

return False
Expand Down
2 changes: 1 addition & 1 deletion hassio/dock/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __init__(self, config, loop, dock, addons_data, addon):
@property
def docker_name(self):
"""Return name of docker container."""
return "addon_{}".format(self.addons_data.get_slug(self.addon))
return "addon_{}".format(self.addon)

def _run(self):
"""Run docker image.
Expand Down
13 changes: 13 additions & 0 deletions hassio/dock/homeassistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,16 @@ def _run(self):
return False

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

0 comments on commit a028f11

Please sign in to comment.