From f127de8059ed442b0b54421e6dfcb31da12667c2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 8 Apr 2017 00:06:10 +0200 Subject: [PATCH 01/34] Add support for git repo --- hassio/addons/__init__.py | 13 +++++++++ hassio/addons/git.py | 59 +++++++++++++++++++++++++++++++++++++++ hassio/config.py | 19 +++++++++++++ hassio/const.py | 2 +- setup.py | 3 +- 5 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 hassio/addons/__init__.py create mode 100644 hassio/addons/git.py diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py new file mode 100644 index 000000000..62cf1c8ce --- /dev/null +++ b/hassio/addons/__init__.py @@ -0,0 +1,13 @@ +"""Init file for HassIO addons.""" +import logging + +_LOGGER = logging.getLogger(__name__) + + +class AddonManager(object): + """Manage addons inside HassIO.""" + + def __init__(self, config, loop): + """Initialize docker base wrapper.""" + self.config = config + self.loop = loop diff --git a/hassio/addons/git.py b/hassio/addons/git.py new file mode 100644 index 000000000..54980ca0d --- /dev/null +++ b/hassio/addons/git.py @@ -0,0 +1,59 @@ +"""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 init(self): + """Init git addon repo.""" + if not os.path.isdir(self.config.path_addons_repo): + return await self.clone() + + await with self._lock: + try: + 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) + + async def clone(self): + """Clone git addon repo.""" + await with self._lock: + try: + 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) + + async def pull(self): + """Pull git addon repo.""" + if self._lock.locked(): + _LOGGER.warning("It is already a task in progress.") + return + + await with self._lock: + try: + yield from 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) diff --git a/hassio/config.py b/hassio/config.py index d710581c5..f0ca3e338 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -14,6 +14,10 @@ HOMEASSISTANT_CURRENT = 'homeassistant_current' HASSIO_SSL = "{}/ssl" HASSIO_CURRENT = 'hassio_current' + +ADDONS_REPO = "{}/addons" +ADDONS_DATA = "{}/addons_data" + UPSTREAM_BETA = 'upstream_beta' @@ -112,3 +116,18 @@ class CoreConfig(object): def path_ssl(self): """Return SSL path inside supervisor.""" 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_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']) diff --git a/hassio/const.py b/hassio/const.py index e641ea88a..40c97d4f4 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -6,7 +6,7 @@ URL_HASSIO_VERSION = \ URL_HASSIO_VERSION_BETA = \ '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' HASSIO_SHARE = "/data" diff --git a/setup.py b/setup.py index 31a39092c..31f333ffe 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup( keywords=['docker', 'home-assistant', 'api'], zip_safe=False, platforms='any', - packages=['hassio', 'hassio.dock', 'hassio.api'], + packages=['hassio', 'hassio.dock', 'hassio.api', 'hassio.addons'], include_package_data=True, install_requires=[ 'async_timeout', @@ -37,5 +37,6 @@ setup( 'docker', 'colorlog', 'voluptuous', + 'gitpython', ] ) From 530f17d50242b0fbcd3a9011bc018364eedbe77a Mon Sep 17 00:00:00 2001 From: pvizeli Date: Mon, 10 Apr 2017 11:32:19 +0200 Subject: [PATCH 02/34] Add support for addon config. --- hassio/addons/__init__.py | 20 ++++++++++++- hassio/addons/config.py | 62 +++++++++++++++++++++++++++++++++++++++ hassio/addons/git.py | 13 ++++++-- hassio/api/util.py | 2 +- hassio/config.py | 31 +++++++++++++------- hassio/const.py | 18 ++++++++++++ 6 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 hassio/addons/config.py diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index 62cf1c8ce..26b58fdb5 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -1,13 +1,31 @@ """Init file for HassIO addons.""" import logging +from .config import AddonsConfig +from .git import AddonsRepo + _LOGGER = logging.getLogger(__name__) -class AddonManager(object): +class AddonsManager(object): """Manage addons inside HassIO.""" def __init__(self, config, loop): """Initialize docker base wrapper.""" self.config = config self.loop = loop + 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() + + async def relaod_addons(self): + """Update addons from repo and reload list.""" + if not await self.repo.pull(): + return + self.addons.read_addons_repo() diff --git a/hassio/addons/config.py b/hassio/addons/config.py new file mode 100644 index 000000000..c4cd1c74f --- /dev/null +++ b/hassio/addons/config.py @@ -0,0 +1,62 @@ +"""Init file for HassIO addons.""" +import logging +import glob +import json + +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_MAP_DATA, + ATTR_OPTIONS, STARTUP_ONCE, STARTUP_AFTER, STARTUP_BEFORE, BOOT_START, + BOOT_STOP, BOOT_MANUAL) + +_LOGGER = logging.getLogger(__name__) + +ADDONS_REPO_PATTERN = "{}/*/config.json" + +# 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_START, BOOT_STOP, BOOT_MANUAL]), + vol.Required(ATTR_MAP_CONFIG): vol.Boolean(), + vol.Required(ATTR_MAP_SSL): vol.Boolean(), + vol.Required(ATTR_MAP_DATA): vol.Boolean(), + vol.Required(ATTR_OPTIONS): dict, +}) + + +class AddonsConfig(Config): + """Config for addons inside HassIO.""" + + def __init__(self, config): + """Initialize docker base wrapper.""" + super().__init__(FILE_HASSIO_ADDONS) + self.config + self._addons_data = {} + + def read_addons_repo(self): + """Read data from addons repository.""" + pattern = ADDONS_REPO_PATTERN.format(self.config.path_addons_repo) + + for addon in glob.iglob(pattern): + try: + with open(addon, 'r') as cfile: + addon_config = json.loads(cfile.read()) + + 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.warnign("Can't read %s -> %s.", addon, + humanize_error(addon_config, ex)) diff --git a/hassio/addons/git.py b/hassio/addons/git.py index 54980ca0d..a81731fa6 100644 --- a/hassio/addons/git.py +++ b/hassio/addons/git.py @@ -20,7 +20,7 @@ class AddonsRepo(object): self.repo = None self._lock = asyncio.Lock(loop=loop) - async def init(self): + async def load(self): """Init git addon repo.""" if not os.path.isdir(self.config.path_addons_repo): return await self.clone() @@ -32,6 +32,9 @@ class AddonsRepo(object): 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.""" @@ -43,12 +46,15 @@ class AddonsRepo(object): 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 + return False await with self._lock: try: @@ -57,3 +63,6 @@ class AddonsRepo(object): except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err: _LOGGER.error("Can't pull addons repo: %s.", err) + return False + + return True diff --git a/hassio/api/util.py b/hassio/api/util.py index fd75d5ec2..e435da684 100644 --- a/hassio/api/util.py +++ b/hassio/api/util.py @@ -82,7 +82,7 @@ async def api_validate(schema, request): """Validate request data with schema.""" data = await request.json(loads=json_loads) try: - schema(data) + data = schema(data) except vol.Invalid as ex: raise RuntimeError(humanize_error(data, ex)) from None diff --git a/hassio/config.py b/hassio/config.py index f0ca3e338..d10f497dc 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -21,12 +21,11 @@ ADDONS_DATA = "{}/addons_data" UPSTREAM_BETA = 'upstream_beta' -class CoreConfig(object): +class Config(object): """Hold all config data.""" - def __init__(self, websession, config_file=FILE_HASSIO_CONFIG): + def __init__(self, config_file): """Initialize config object.""" - self.websession = websession self._filename = config_file self._data = {} @@ -38,14 +37,6 @@ class CoreConfig(object): except OSError: _LOGGER.warning("Can't read %s", self._filename) - # init data - if not self._data: - self._data.update({ - HOMEASSISTANT_IMAGE: os.environ['HOMEASSISTANT_REPOSITORY'], - UPSTREAM_BETA: False, - }) - self.save() - def save(self): """Store data to config file.""" try: @@ -57,6 +48,24 @@ class CoreConfig(object): return True + +class CoreConfig(Config): + """Hold all core config data.""" + + def __init__(self, websession): + """Initialize config object.""" + self.websession = websession + + super().__ini__(FILE_HASSIO_CONFIG) + + # init data + if not self._data: + self._data.update({ + HOMEASSISTANT_IMAGE: os.environ['HOMEASSISTANT_REPOSITORY'], + UPSTREAM_BETA: False, + }) + self.save() + async def fetch_update_infos(self): """Read current versions from web.""" current = await fetch_current_versions( diff --git a/hassio/const.py b/hassio/const.py index 40c97d4f4..99fc13fbb 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -28,3 +28,21 @@ RESULT_OK = 'ok' ATTR_VERSION = 'version' ATTR_CURRENT = 'current' 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_MAP_DATA = 'map_data' +ATTR_OPTIONS = 'options' +ATTR_INSTALLED = 'installed' + +STARTUP_BEFORE = 'before' +STARTUP_AFTER = 'after' +STARTUP_ONCE = 'once' +BOOT_START = 'start' +BOOT_STOP = 'stop' +BOOT_MANUAL = 'manual' From ae003e5b7663034cdab073e8b20da8c4b5b5e3af Mon Sep 17 00:00:00 2001 From: pvizeli Date: Mon, 10 Apr 2017 12:17:33 +0200 Subject: [PATCH 03/34] Add first version of docker for addons --- API.md | 10 ++++++- hassio/addons/__init__.py | 3 +- hassio/addons/config.py | 7 +++-- hassio/const.py | 4 +-- hassio/dock/__init__.py | 24 +++++++++++++++ hassio/dock/addon.py | 63 +++++++++++++++++++++++++++++++++++++++ hassio/dock/supervisor.py | 4 +++ 7 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 hassio/dock/addon.py diff --git a/API.md b/API.md index 65ffd3400..7ed976be2 100644 --- a/API.md +++ b/API.md @@ -69,7 +69,15 @@ On success "version": "INSTALL_VERSION", "current": "CURRENT_VERSION", "beta": "true|false", - "addons": {} + "addons": [ + { + "name": "xy bla", + "slug": "xy", + "version": "CURRENT_VERSION", + "installed": "none|INSTALL_VERSION", + "description": "description" + } + ] } ``` diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index 26b58fdb5..b42a39858 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -10,10 +10,11 @@ _LOGGER = logging.getLogger(__name__) class AddonsManager(object): """Manage addons inside HassIO.""" - def __init__(self, config, loop): + def __init__(self, config, loop, dock): """Initialize docker base wrapper.""" self.config = config self.loop = loop + self.dock = dock self.repo = AddonsRepo(config, loop) self.addons = AddonsConfig(config) self.dockers = {} diff --git a/hassio/addons/config.py b/hassio/addons/config.py index c4cd1c74f..b48c5864d 100644 --- a/hassio/addons/config.py +++ b/hassio/addons/config.py @@ -9,8 +9,8 @@ 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_MAP_DATA, - ATTR_OPTIONS, STARTUP_ONCE, STARTUP_AFTER, STARTUP_BEFORE, BOOT_START, - BOOT_STOP, BOOT_MANUAL) + ATTR_OPTIONS, ATTR_PORTS, STARTUP_ONCE, STARTUP_AFTER, STARTUP_BEFORE, + BOOT_AUTO, BOOT_MANUAL) _LOGGER = logging.getLogger(__name__) @@ -25,7 +25,8 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Required(ATTR_STARTUP): vol.In([STARTUP_BEFORE, STARTUP_AFTER, STARTUP_ONCE]), vol.Required(ATTR_BOOT): - vol.IN([BOOT_START, BOOT_STOP, BOOT_MANUAL]), + 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_MAP_DATA): vol.Boolean(), diff --git a/hassio/const.py b/hassio/const.py index 99fc13fbb..4d561d958 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -39,10 +39,10 @@ ATTR_MAP_SSL = 'map_ssl' ATTR_MAP_DATA = 'map_data' ATTR_OPTIONS = 'options' ATTR_INSTALLED = 'installed' +ATTR_STATE = 'state' STARTUP_BEFORE = 'before' STARTUP_AFTER = 'after' STARTUP_ONCE = 'once' -BOOT_START = 'start' -BOOT_STOP = 'stop' +BOOT_STOP = 'auto' BOOT_MANUAL = 'manual' diff --git a/hassio/dock/__init__.py b/hassio/dock/__init__.py index 633a17870..88e2f1c02 100644 --- a/hassio/dock/__init__.py +++ b/hassio/dock/__init__.py @@ -177,6 +177,30 @@ class DockerBase(object): 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: + await self.loop.run_in_executor(None, self._remove) + return True + + def _stop(self): + """Stop/remove and remove docker container. + + Need run inside executor. + """ + if self.container: + self._stop() + + image = "{}:latest".format(self.image) + try: + self.dock.images.remove(image=image, force=True) + except docker.errors.DockerException as err: + _LOGGER.warning("Can't remove image %s -> %s.", image, err) + async def update(self, tag): """Update a docker image. diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py new file mode 100644 index 000000000..97665573b --- /dev/null +++ b/hassio/dock/addon.py @@ -0,0 +1,63 @@ +"""Init file for HassIO addon docker object.""" +import logging + +import docker + +from . import DockerBase +from ..const import ATTR_SLUG, ATTR_PORTS +from ..tools import get_version_from_env + +_LOGGER = logging.getLogger(__name__) + +HASS_DOCKER_NAME = 'homeassistant' + + +class DockerHomeAssistant(DockerBase): + """Docker hassio wrapper for HomeAssistant.""" + + def __init__(self, config, loop, dock, addon_config, image): + """Initialize docker homeassistant wrapper.""" + super().__init__(config, loop, dock, image=image) + self.addon_config + + @property + def docker_name(self): + """Return name of docker container.""" + return "addon_{}".format(self.addon_config[ATTR_SLUG]) + + def _run(self): + """Run docker image. + + Need run inside executor. + """ + if self._is_running(): + return + + # cleanup old container + self._stop() + + try: + self.container = self.dock.containers.run( + self.image, + name=self.docker_name, + detach=True, + network_mode='bridge', + ports=self.addon_config[ATTR_PORTS], + restart_policy={ + "Name": "on-failure", + "MaximumRetryCount": 10, + }, + volumes={ + self.config.path_config_docker: + {'bind': '/config', 'mode': 'rw'}, + self.config.path_ssl_docker: + {'bind': '/ssl', 'mode': 'rw'}, + }) + + 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 diff --git a/hassio/dock/supervisor.py b/hassio/dock/supervisor.py index c932e9d04..1f61ecbde 100644 --- a/hassio/dock/supervisor.py +++ b/hassio/dock/supervisor.py @@ -27,3 +27,7 @@ class DockerSupervisor(DockerBase): async def update(self, tag): """Update docker image.""" raise RuntimeError("Not support on supervisor docker container!") + + async def remove(self, tag): + """Remove docker image.""" + raise RuntimeError("Not support on supervisor docker container!") From 082770256bb064a30e6a458d9b15d212c799bc5b Mon Sep 17 00:00:00 2001 From: pvizeli Date: Mon, 10 Apr 2017 17:22:26 +0200 Subject: [PATCH 04/34] save, for next working too --- hassio/addons/config.py | 28 +++++++++++++++++++++++++++- hassio/const.py | 1 - hassio/dock/addon.py | 32 ++++++++++++++++++++++---------- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/hassio/addons/config.py b/hassio/addons/config.py index b48c5864d..211cbb6ab 100644 --- a/hassio/addons/config.py +++ b/hassio/addons/config.py @@ -29,7 +29,6 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Optional(ATTR_PORTS): dict, vol.Required(ATTR_MAP_CONFIG): vol.Boolean(), vol.Required(ATTR_MAP_SSL): vol.Boolean(), - vol.Required(ATTR_MAP_DATA): vol.Boolean(), vol.Required(ATTR_OPTIONS): dict, }) @@ -61,3 +60,30 @@ class AddonsConfig(Config): except vol.Invalid as ex: _LOGGER.warnign("Can't read %s -> %s.", addon, humanize_error(addon_config, ex)) + + 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 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 need_data(self, addon): + """Return True if data map is needed.""" + return self._addons_data[addon][ATTR_MAP_DATA] + + def path_data(self, addon): + """Return addon data path inside supervisor.""" + + + def path_data_docker(self, addon): + """Return addon data path external for docker.""" diff --git a/hassio/const.py b/hassio/const.py index 4d561d958..680d270c9 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -36,7 +36,6 @@ ATTR_BOOT = 'boot' ATTR_PORTS = 'ports' ATTR_MAP_CONFIG = 'map_config' ATTR_MAP_SSL = 'map_ssl' -ATTR_MAP_DATA = 'map_data' ATTR_OPTIONS = 'options' ATTR_INSTALLED = 'installed' ATTR_STATE = 'state' diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 97665573b..9f8bdc2f4 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -4,7 +4,6 @@ import logging import docker from . import DockerBase -from ..const import ATTR_SLUG, ATTR_PORTS from ..tools import get_version_from_env _LOGGER = logging.getLogger(__name__) @@ -15,15 +14,16 @@ HASS_DOCKER_NAME = 'homeassistant' class DockerHomeAssistant(DockerBase): """Docker hassio wrapper for HomeAssistant.""" - def __init__(self, config, loop, dock, addon_config, image): + def __init__(self, config, loop, dock, addon_config, addon, image): """Initialize docker homeassistant wrapper.""" super().__init__(config, loop, dock, image=image) + self.addon = addon self.addon_config @property def docker_name(self): """Return name of docker container.""" - return "addon_{}".format(self.addon_config[ATTR_SLUG]) + return "addon_{}".format(self.addon_config.get_slug(self.addon)) def _run(self): """Run docker image. @@ -36,23 +36,35 @@ class DockerHomeAssistant(DockerBase): # cleanup old container self._stop() + # volumes + volumes = { + self.addon_config.path_data_docker(self.addon): { + 'bind': '/data', 'mode': 'rw' + }} + if self.addon_config.need_config(self.addon): + volumes.update({ + self.config.path_config_docker: { + 'bind': '/config', 'mode': 'rw' + }}) + if self.addon_config.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.addon_config[ATTR_PORTS], + ports=self.addon_config.get_ports(self.addon), restart_policy={ "Name": "on-failure", "MaximumRetryCount": 10, }, - volumes={ - self.config.path_config_docker: - {'bind': '/config', 'mode': 'rw'}, - self.config.path_ssl_docker: - {'bind': '/ssl', 'mode': 'rw'}, - }) + volumes=volumes, + ) self.version = get_version_from_env( self.container.attrs['Config']['Env']) From 5c70d68262096ac782352eb2b74bbdcbc36e32f6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 10 Apr 2017 22:08:57 +0200 Subject: [PATCH 05/34] Addons config support --- hassio/addons/__init__.py | 1 + hassio/addons/config.py | 13 +++++++++++-- hassio/config.py | 7 ++++++- hassio/const.py | 2 ++ hassio/dock/addon.py | 11 ++++++----- hassio/tools.py | 8 ++++++++ 6 files changed, 34 insertions(+), 8 deletions(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index b42a39858..cf4361260 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -3,6 +3,7 @@ import logging from .config import AddonsConfig from .git import AddonsRepo +from ..docker.addon import DockerAddon _LOGGER = logging.getLogger(__name__) diff --git a/hassio/addons/config.py b/hassio/addons/config.py index 211cbb6ab..d16102bfd 100644 --- a/hassio/addons/config.py +++ b/hassio/addons/config.py @@ -10,7 +10,7 @@ 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) + BOOT_AUTO, BOOT_MANUAL, DOCKER_REPO) _LOGGER = logging.getLogger(__name__) @@ -61,6 +61,12 @@ class AddonsConfig(Config): _LOGGER.warnign("Can't read %s -> %s.", addon, humanize_error(addon_config, ex)) + def get_image(self, addon): + """Return name of addon docker image.""" + return "{}/{}-addon-{}".format( + DOCKER_REPO, self.config.hassio_arch, + self._addons_data[addon][ATTR_SLUG]) + def get_slug(self, addon): """Return slug of addon.""" return self._addons_data[addon][ATTR_SLUG] @@ -83,7 +89,10 @@ class AddonsConfig(Config): 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]) diff --git a/hassio/config.py b/hassio/config.py index d10f497dc..65905682c 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -4,7 +4,7 @@ import logging import os from .const import FILE_HASSIO_CONFIG, HASSIO_SHARE -from .tools import fetch_current_versions +from .tools import fetch_current_versions, get_arch_from_image _LOGGER = logging.getLogger(__name__) @@ -91,6 +91,11 @@ class CoreConfig(Config): """Set beta upstream mode.""" self._data[UPSTREAM_BETA] = bool(value) + @property + def hassio_arch(self): + """Return arch they run.""" + return get_arch_from_image(self.homeassistant_image) + @property def homeassistant_image(self): """Return docker homeassistant repository.""" diff --git a/hassio/const.py b/hassio/const.py index 680d270c9..0cdb868d3 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -8,6 +8,8 @@ URL_HASSIO_VERSION_BETA = \ URL_HASSIO_ADDONS = 'https://github.com/pvizeli/hassio-addons' +DOCKER_REPO = "pvizeli" + HASSIO_SHARE = "/data" RUN_UPDATE_INFO_TASKS = 28800 diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 9f8bdc2f4..510ae01f3 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -11,12 +11,13 @@ _LOGGER = logging.getLogger(__name__) HASS_DOCKER_NAME = 'homeassistant' -class DockerHomeAssistant(DockerBase): +class DockerAddon(DockerBase): """Docker hassio wrapper for HomeAssistant.""" - def __init__(self, config, loop, dock, addon_config, addon, image): + def __init__(self, config, loop, dock, addon_config, addon): """Initialize docker homeassistant wrapper.""" - super().__init__(config, loop, dock, image=image) + super().__init__( + config, loop, dock, image=addon_config.get_image(addon)) self.addon = addon self.addon_config @@ -45,12 +46,12 @@ class DockerHomeAssistant(DockerBase): volumes.update({ self.config.path_config_docker: { 'bind': '/config', 'mode': 'rw' - }}) + }}) if self.addon_config.need_ssl(self.addon): volumes.update({ self.config.path_ssl_docker: { 'bind': '/ssl', 'mode': 'rw' - }}) + }}) try: self.container = self.dock.containers.run( diff --git a/hassio/tools.py b/hassio/tools.py index 3368b58a1..e0033170e 100644 --- a/hassio/tools.py +++ b/hassio/tools.py @@ -12,6 +12,7 @@ from .const import URL_HASSIO_VERSION, URL_HASSIO_VERSION_BETA _LOGGER = logging.getLogger(__name__) _RE_VERSION = re.compile(r"VERSION=(.*)") +_IMAGE_ARCH = re.compile(r"([a-z0-9]*)-hassio-supervisor") async def fetch_current_versions(websession, beta=False): @@ -29,6 +30,13 @@ async def fetch_current_versions(websession, beta=False): _LOGGER.warning("Can't fetch 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): """Extract Version from ENV list.""" for env in env_list: From 318ca828ccbeb77f9d7099f8a829d8fe73af6b0f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 11 Apr 2017 00:41:18 +0200 Subject: [PATCH 06/34] Add install/uninstall to manager --- hassio/addons/__init__.py | 57 +++++++++++++++++++++++++++++++++++++++ hassio/addons/config.py | 27 +++++++++++++++++++ hassio/bootstrap.py | 6 +++++ hassio/dock/__init__.py | 10 ++++--- 4 files changed, 96 insertions(+), 4 deletions(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index cf4361260..1d25d8dc9 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -1,5 +1,7 @@ """Init file for HassIO addons.""" import logging +import os +import shutil from .config import AddonsConfig from .git import AddonsRepo @@ -26,8 +28,63 @@ class AddonsManager(object): if await self.repo.load(): self.addons.read_addons_repo() + # load installed addons + for addon in self.addons.list_installed: + self.dockers[addon] = DockerAddon( + self.config, self.loop, self.dock, self.addons, addon) + async def relaod_addons(self): """Update addons from repo and reload list.""" if not await self.repo.pull(): return self.addons.read_addons_repo() + + async def install_addon(self, addon, version=None): + """Install a addon.""" + if not self.addons.exists_addon(addon): + _LOGGER.error("Addon %s not exists for install.", addon) + return False + + if self.addons.is_installed(addon): + _LOGGER.error("Addon %s is allready installed.", addon) + return False + + if not os.path.isdir(self.addons.path_data(addon)): + _LOGGER.info("Create Home-Assistant addon data folder %s", + self.addon.path_data(addon)) + os.mkdir(self.addons.path_data(addon)) + + addon_docker = DockerAddon( + self.config, self.loop, self.dock, self.addons, addon) + + version = version or self.addons.get_version(addon) + if not await addon_docker.install(version): + _LOGGER.error("Can't install addon %s version %s.", addon, version) + return False + + self.dockers[addon] = addon_docker + self.addons.set_install_addon(addon, version) + return True + + async def uninstall_addon(self, addon): + """Remove a addon.""" + if self.addons.is_installed(addon): + _LOGGER.error("Addon %s is allready installed.", 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(version): + _LOGGER.error("Can't install addon %s.", addon) + return False + + if os.path.isdir(self.addons.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.dockers.pop(addon) + self.addons.set_uninstall_addon(addon) + return True diff --git a/hassio/addons/config.py b/hassio/addons/config.py index d16102bfd..c90bc9f74 100644 --- a/hassio/addons/config.py +++ b/hassio/addons/config.py @@ -61,12 +61,39 @@ class AddonsConfig(Config): _LOGGER.warnign("Can't read %s -> %s.", addon, humanize_error(addon_config, ex)) + @property + def list_installed(self): + """Return a list of installed addons.""" + return self._data.keys() + + 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 set_install_addon(self, addon, version): + """Set addon as installed.""" + self._data[addon] = {ATTR_VERSION: version} + self.save() + + def set_uninstall_addon(self, addon, version): + """Set addon as uninstalled.""" + self._data.pop(addon, None) + self.save() + def get_image(self, addon): """Return name of addon docker image.""" return "{}/{}-addon-{}".format( DOCKER_REPO, self.config.hassio_arch, self._addons_data[addon][ATTR_SLUG]) + 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] diff --git a/hassio/bootstrap.py b/hassio/bootstrap.py index d4ce6844e..779b91b0d 100644 --- a/hassio/bootstrap.py +++ b/hassio/bootstrap.py @@ -26,6 +26,12 @@ def initialize_system_data(websession): _LOGGER.info("Create Home-Assistant ssl folder %s", 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) + return config diff --git a/hassio/dock/__init__.py b/hassio/dock/__init__.py index 88e2f1c02..bfb53df21 100644 --- a/hassio/dock/__init__.py +++ b/hassio/dock/__init__.py @@ -184,11 +184,10 @@ class DockerBase(object): return False async with self._lock: - await self.loop.run_in_executor(None, self._remove) - return True + return await self.loop.run_in_executor(None, self._remove) - def _stop(self): - """Stop/remove and remove docker container. + def _remove(self): + """remove docker container. Need run inside executor. """ @@ -200,6 +199,9 @@ class DockerBase(object): self.dock.images.remove(image=image, force=True) except docker.errors.DockerException as err: _LOGGER.warning("Can't remove image %s -> %s.", image, err) + return False + + return True async def update(self, tag): """Update a docker image. From 14500d3ac4890f75cc5fdc5a6484d33c74423e5c Mon Sep 17 00:00:00 2001 From: pvizeli Date: Tue, 11 Apr 2017 17:34:14 +0200 Subject: [PATCH 07/34] Cleanups --- hassio/addons/__init__.py | 36 ++++++++++++++++++++++++++++++------ hassio/addons/config.py | 26 ++++++++++++++++++++++---- hassio/config.py | 13 +++++-------- hassio/core.py | 7 +++++++ hassio/tools.py | 18 ++++++++++++++++++ 5 files changed, 82 insertions(+), 18 deletions(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index 1d25d8dc9..f7c40ab3f 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -10,7 +10,7 @@ from ..docker.addon import DockerAddon _LOGGER = logging.getLogger(__name__) -class AddonsManager(object): +class AddonManager(object): """Manage addons inside HassIO.""" def __init__(self, config, loop, dock): @@ -46,7 +46,7 @@ class AddonsManager(object): return False if self.addons.is_installed(addon): - _LOGGER.error("Addon %s is allready installed.", addon) + _LOGGER.error("Addon %s is already installed.", addon) return False if not os.path.isdir(self.addons.path_data(addon)): @@ -59,7 +59,6 @@ class AddonsManager(object): version = version or self.addons.get_version(addon) if not await addon_docker.install(version): - _LOGGER.error("Can't install addon %s version %s.", addon, version) return False self.dockers[addon] = addon_docker @@ -68,8 +67,8 @@ class AddonsManager(object): async def uninstall_addon(self, addon): """Remove a addon.""" - if self.addons.is_installed(addon): - _LOGGER.error("Addon %s is allready installed.", addon) + if not self.addons.is_installed(addon): + _LOGGER.error("Addon %s is already uninstalled.", addon) return False if addon not in self.dockers: @@ -77,7 +76,6 @@ class AddonsManager(object): return False if not await self.dockers[addon].remove(version): - _LOGGER.error("Can't install addon %s.", addon) return False if os.path.isdir(self.addons.path_data(addon)): @@ -88,3 +86,29 @@ class AddonsManager(object): self.dockers.pop(addon) self.addons.set_uninstall_addon(addon) return True + + async def start_addon(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 + + if not await self.dockers[addon].run(): + return False + + return True + + async def stop_addon(self, addon): + """Stop addon.""" + if addon not in self.dockers: + _LOGGER.error("No docker found for addon %s.", addon) + return False + + if not await self.dockers[addon].stop(): + return False + + return True diff --git a/hassio/addons/config.py b/hassio/addons/config.py index c90bc9f74..6d7a91983 100644 --- a/hassio/addons/config.py +++ b/hassio/addons/config.py @@ -1,7 +1,6 @@ """Init file for HassIO addons.""" import logging import glob -import json import voluptuous as vol from voluptuous.humanize import humanize_error @@ -11,6 +10,7 @@ from ..const import ( 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) +from ..tools import read_json_file, write_json_file _LOGGER = logging.getLogger(__name__) @@ -48,8 +48,7 @@ class AddonsConfig(Config): for addon in glob.iglob(pattern): try: - with open(addon, 'r') as cfile: - addon_config = json.loads(cfile.read()) + addon_config = read_json_file(addon) addon_config = SCHEMA_ADDON_CONFIG(addon_config) self._addons_data[addon_config[ATTR_SLUG]] = addon_config @@ -76,7 +75,10 @@ class AddonsConfig(Config): def set_install_addon(self, addon, version): """Set addon as installed.""" - self._data[addon] = {ATTR_VERSION: version} + self._data[addon] = { + ATTR_VERSION: version, + ATTR_OPTIONS: {} + } self.save() def set_uninstall_addon(self, addon, version): @@ -84,6 +86,13 @@ class AddonsConfig(Config): self._data.pop(addon, None) 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_image(self, addon): """Return name of addon docker image.""" return "{}/{}-addon-{}".format( @@ -123,3 +132,12 @@ class AddonsConfig(Config): """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)) diff --git a/hassio/config.py b/hassio/config.py index 65905682c..173e7dee1 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -4,7 +4,9 @@ import logging import os from .const import FILE_HASSIO_CONFIG, HASSIO_SHARE -from .tools import fetch_current_versions, get_arch_from_image +from .tools import ( + fetch_current_versions, get_arch_from_image, write_json_file, + read_json_file) _LOGGER = logging.getLogger(__name__) @@ -32,20 +34,15 @@ class Config(object): # init or load data if os.path.isfile(self._filename): try: - with open(self._filename, 'r') as cfile: - self._data = json.loads(cfile.read()) + self._data = read_json_file(self._filename) except OSError: _LOGGER.warning("Can't read %s", self._filename) 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: + if not write_json_file(self._filename, self._data) _LOGGER.exception("Can't store config in %s", self._filename) return False - return True diff --git a/hassio/core.py b/hassio/core.py index 0b19eff78..e6dd4e9c1 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -6,6 +6,7 @@ import aiohttp import docker 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 @@ -38,6 +39,9 @@ class HassIO(object): # init HostControll self.host_controll = HostControll(self.loop) + # init addon system + self.addon_manager = AddonManager(self.config, self.loop, self.dock) + async def setup(self): """Setup HassIO orchestration.""" # supervisor @@ -69,6 +73,9 @@ class HassIO(object): _LOGGER.info("No HomeAssistant docker found.") await self._setup_homeassistant() + # Load addons + await self.addon_manager.prepare() + async def start(self): """Start HassIO orchestration.""" # start api diff --git a/hassio/tools.py b/hassio/tools.py index e0033170e..aedbd6df1 100644 --- a/hassio/tools.py +++ b/hassio/tools.py @@ -1,5 +1,6 @@ """Tools file for HassIO.""" import asyncio +import json import logging import re import socket @@ -64,3 +65,20 @@ def get_local_ip(loop): return socket.gethostbyname(socket.gethostname()) finally: 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()) From 3ef76a4adac29e65056ba7f4d1df411e0728883e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 12 Apr 2017 00:06:17 +0200 Subject: [PATCH 08/34] Add first part of api --- hassio/addons/__init__.py | 16 +++++++++++++++ hassio/addons/config.py | 41 +++++++++++++++++++++++++++++++++++++- hassio/api/__init__.py | 23 ++++++++++++++++++--- hassio/api/addons.py | 42 +++++++++++++++++++++++++++++++++++++++ hassio/api/supervisor.py | 8 +++++--- hassio/const.py | 1 + hassio/core.py | 3 ++- 7 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 hassio/api/addons.py diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index f7c40ab3f..286cdab0c 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -112,3 +112,19 @@ class AddonManager(object): return False return True + + async def update_addon(self, addon, version=None): + """Update addon.""" + if self.addons.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.addons.get_version(addon) + if not await self.dockers[addon].update(version): + return False + + return True diff --git a/hassio/addons/config.py b/hassio/addons/config.py index 6d7a91983..31bd92f2f 100644 --- a/hassio/addons/config.py +++ b/hassio/addons/config.py @@ -9,7 +9,7 @@ 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) + BOOT_AUTO, BOOT_MANUAL, DOCKER_REPO, ATTR_INSTALLED) from ..tools import read_json_file, write_json_file _LOGGER = logging.getLogger(__name__) @@ -65,6 +65,26 @@ class AddonsConfig(Config): """Return a list of installed addons.""" return self._data.keys() + @property + def list_all(self): + """Return a list of available addons.""" + return self._addons_data.keys() + + @property + def list(self): + """Return a list of available addons.""" + data = [] + for addon, values in self._addons.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 exists_addon(self, addon): """Return True if a addon exists.""" return addon in self._addons_data @@ -73,6 +93,10 @@ class AddonsConfig(Config): """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] = { @@ -93,12 +117,27 @@ class AddonsConfig(Config): 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_image(self, addon): """Return name of addon docker image.""" return "{}/{}-addon-{}".format( DOCKER_REPO, self.config.hassio_arch, self._addons_data[addon][ATTR_SLUG]) + 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] diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index 1bd0d577a..6135530a4 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -3,10 +3,11 @@ import logging from aiohttp import web +from .addons import APIAddonManager +from .homeassistant import APIHomeAssistant from .host import APIHost from .network import APINetwork from .supervisor import APISupervisor -from .homeassistant import APIHomeAssistant _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/options', api_net.options) - def register_supervisor(self, host_controll): + def register_supervisor(self, host_controll, addon_manager): """Register supervisor function.""" - api_supervisor = APISupervisor(self.config, self.loop, host_controll) + api_supervisor = APISupervisor( + self.config, self.loop, host_controll, addon_manager) self.webapp.router.add_get('/supervisor/ping', api_supervisor.ping) 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/update', api_hass.update) + def register_addons(self, addon_manager): + """Register homeassistant function.""" + api_addons = APIAddons(self.config, self.loop, addon_manager) + + 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): """Run rest api webserver.""" self._handler = self.webapp.make_handler(loop=self.loop) diff --git a/hassio/api/addons.py b/hassio/api/addons.py new file mode 100644 index 000000000..e87f3bcbf --- /dev/null +++ b/hassio/api/addons.py @@ -0,0 +1,42 @@ +"""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 + +_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, addon_manager): + """Initialize homeassistant rest api part.""" + self.config = config + self.loop = loop + self.addon_manager = addon_manager + + @api_process + async def info(self, request): + """Return host information.""" + + @api_process + async def update(self, request): + """Update host OS.""" + body = await api_validate(SCHEMA_VERSION, request) + version = body.get(ATTR_VERSION, self.config.current_homeassistant) + + if self.dock_hass.in_progress: + raise RuntimeError("Other task is in progress.") + + if version == self.dock_hass.version: + raise RuntimeError("%s is already in use.", version) + + return await asyncio.shield(self.dock_hass.update(version)) diff --git a/hassio/api/supervisor.py b/hassio/api/supervisor.py index 73ef6ae5b..4cd69c31f 100644 --- a/hassio/api/supervisor.py +++ b/hassio/api/supervisor.py @@ -4,7 +4,8 @@ import logging import voluptuous as vol 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__) @@ -21,11 +22,12 @@ SCHEMA_VERSION = vol.Schema({ class APISupervisor(object): """Handle rest api for supervisor functions.""" - def __init__(self, config, loop, host_controll): + def __init__(self, config, loop, host_controll, addon_manager): """Initialize supervisor rest api part.""" self.config = config self.loop = loop self.host_controll = host_controll + self.addon_manager = addon_manager @api_process async def ping(self, request): @@ -39,8 +41,8 @@ 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, } - return info @api_process diff --git a/hassio/const.py b/hassio/const.py index 0cdb868d3..41621b806 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -27,6 +27,7 @@ JSON_MESSAGE = 'message' RESULT_ERROR = 'error' RESULT_OK = 'ok' +ATTR_ADDONS = 'addons' ATTR_VERSION = 'version' ATTR_CURRENT = 'current' ATTR_BETA = 'beta' diff --git a/hassio/core.py b/hassio/core.py index e6dd4e9c1..176772ddd 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -60,8 +60,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.api.register_supervisor(self.host_controll, self.addon_manager) self.api.register_homeassistant(self.homeassistant) + self.api.register_addons(self.addon_manager) # schedule update info tasks self.scheduler.register_task( From 20856126c86c2fc8ac8b2bfb7c8af35ea6333fa6 Mon Sep 17 00:00:00 2001 From: pvizeli Date: Wed, 12 Apr 2017 17:27:51 +0200 Subject: [PATCH 09/34] Finish api, add options validate --- hassio/addons/__init__.py | 59 ++++++++++------- hassio/addons/{config.py => data.py} | 52 +++++++++++++-- hassio/api/__init__.py | 8 +-- hassio/api/addons.py | 98 ++++++++++++++++++++++++---- hassio/api/supervisor.py | 6 +- hassio/const.py | 3 + hassio/core.py | 15 +++-- hassio/dock/addon.py | 16 ++--- 8 files changed, 197 insertions(+), 60 deletions(-) rename hassio/addons/{config.py => data.py} (79%) 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, From f340a19e400f8ffe373949edac164bc0aac98b9b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 12 Apr 2017 22:59:40 +0200 Subject: [PATCH 10/34] fix lint --- hassio/addons/__init__.py | 8 ++++---- hassio/addons/data.py | 33 +++++++++++++++------------------ hassio/addons/git.py | 8 ++++---- hassio/api/__init__.py | 2 +- hassio/api/addons.py | 15 ++++++++------- hassio/api/homeassistant.py | 3 ++- hassio/config.py | 5 ++--- hassio/const.py | 5 +++-- hassio/dock/addon.py | 2 +- hassio/dock/supervisor.py | 2 +- 10 files changed, 41 insertions(+), 42 deletions(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index a02a66148..d6f7146a7 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -5,8 +5,8 @@ import shutil from .data import AddonsData from .git import AddonsRepo -from ..const import STATE_STOPED, STATE_STARTED -from ..docker.addon import DockerAddon +from ..const import STATE_STOPPED, STATE_STARTED +from ..dock.addon import DockerAddon _LOGGER = logging.getLogger(__name__) @@ -76,7 +76,7 @@ class AddonManager(AddonsData): _LOGGER.error("No docker found for addon %s.", addon) return False - if not await self.dockers[addon].remove(version): + if not await self.dockers[addon].remove(): return False if os.path.isdir(self.path_data(addon)): @@ -96,7 +96,7 @@ class AddonManager(AddonsData): if await self.dockers[addon].is_running(): return STATE_STARTED - return STATE_STOPED + return STATE_STOPPED async def start_addon(self, addon): """Set options and start addon.""" diff --git a/hassio/addons/data.py b/hassio/addons/data.py index aa8571725..dec72b829 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -7,9 +7,10 @@ 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_MAP_DATA, - ATTR_OPTIONS, ATTR_PORTS, STARTUP_ONCE, STARTUP_AFTER, STARTUP_BEFORE, - BOOT_AUTO, BOOT_MANUAL, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA) + 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) +from ..config import Config from ..tools import read_json_file, write_json_file _LOGGER = logging.getLogger(__name__) @@ -31,7 +32,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Required(ATTR_STARTUP): vol.In([STARTUP_BEFORE, STARTUP_AFTER, STARTUP_ONCE]), vol.Required(ATTR_BOOT): - vol.IN([BOOT_AUTO, BOOT_MANUAL]), + 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(), @@ -48,7 +49,7 @@ class AddonsData(Config): def __init__(self, config): """Initialize data holder.""" super().__init__(FILE_HASSIO_ADDONS) - self.config + self.config = config self._addons_data = {} def read_addons_repo(self): @@ -66,7 +67,7 @@ class AddonsData(Config): _LOGGER.warning("Can't read %s", addon) except vol.Invalid as ex: - _LOGGER.warnign("Can't read %s -> %s.", addon, + _LOGGER.warning("Can't read %s -> %s.", addon, humanize_error(addon_config, ex)) @property @@ -83,7 +84,7 @@ class AddonsData(Config): def list(self): """Return a list of available addons.""" data = [] - for addon, values in self._addons.items(): + for addon, values in self._addons_data.items(): data.append({ ATTR_NAME: values[ATTR_NAME], ATTR_SLUG: values[ATTR_SLUG], @@ -114,7 +115,7 @@ class AddonsData(Config): } self.save() - def set_uninstall_addon(self, addon, version): + def set_uninstall_addon(self, addon): """Set addon as uninstalled.""" self._data.pop(addon, None) self.save() @@ -172,10 +173,6 @@ class AddonsData(Config): """Return True if ssl map is needed.""" return self._addons_data[addon][ATTR_MAP_SSL] - def need_data(self, addon): - """Return True if data map is needed.""" - return self._addons_data[addon][ATTR_MAP_DATA] - def path_data(self, addon): """Return addon data path inside supervisor.""" return "{}/{}".format( @@ -201,7 +198,7 @@ class AddonsData(Config): def validate(struct): """Validate schema.""" - validated = {} + options = {} for key, value in struct.items(): if key not in raw_schema: raise vol.Invalid("Unknown options {}.".format(key)) @@ -209,18 +206,18 @@ class AddonsData(Config): typ = raw_schema[key] try: if typ == V_STR: - validate[key] = str(value) + options[key] = str(value) elif typ == V_INT: - validate[key] = int(value) + options[key] = int(value) elif typ == V_FLOAT: - validate[key] = float(value) + options[key] = float(value) elif typ == V_BOOL: - validate[key] = vol.Boolean()(value) + options[key] = vol.Boolean()(value) except TypeError: raise vol.Invalid( "Type error for {}.".format(key)) from None - return validated + return options schema = vol.Schema(vol.All(dict(), validate)) return schema diff --git a/hassio/addons/git.py b/hassio/addons/git.py index a81731fa6..216fd7c69 100644 --- a/hassio/addons/git.py +++ b/hassio/addons/git.py @@ -25,7 +25,7 @@ class AddonsRepo(object): if not os.path.isdir(self.config.path_addons_repo): return await self.clone() - await with self._lock: + async with self._lock: try: self.repo = await self.loop.run_in_executor( None, git.Repo(self.config.path_addons_repo)) @@ -38,7 +38,7 @@ class AddonsRepo(object): async def clone(self): """Clone git addon repo.""" - await with self._lock: + async with self._lock: try: self.repo = await self.loop.run_in_executor( None, git.Repo.clone_from, URL_HASSIO_ADDONS, @@ -56,9 +56,9 @@ class AddonsRepo(object): _LOGGER.warning("It is already a task in progress.") return False - await with self._lock: + async with self._lock: try: - yield from self.loop.run_in_executor( + await self.loop.run_in_executor( None, self.repo.remotes.origin.pull) except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err: diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index 70cad7121..84ef3573b 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -3,7 +3,7 @@ import logging from aiohttp import web -from .addons import APIAddonManager +from .addons import APIAddons from .homeassistant import APIHomeAssistant from .host import APIHost from .network import APINetwork diff --git a/hassio/api/addons.py b/hassio/api/addons.py index fade3c8a2..28a9d9c5e 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -6,7 +6,8 @@ import voluptuous as vol from .util import api_process, api_validate 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) _LOGGER = logging.getLogger(__name__) @@ -69,7 +70,7 @@ class APIAddons(object): ATTR_VERSION, self.addons.get_version(addon)) return await asyncio.shield( - self.addons.addon_install(addon, version)) + self.addons.addon_install(addon, version), loop=self.loop) @api_process async def uninstall(self, request): @@ -77,7 +78,7 @@ class APIAddons(object): addon = self._extract_addon(request) return await asyncio.shield( - self.addons.addon_uninstall(addon)) + self.addons.addon_uninstall(addon), loop=self.loop) @api_process async def start(self, request): @@ -88,18 +89,18 @@ class APIAddons(object): raise RuntimeError("Addon is already running.") return await asyncio.shield( - self.addons.addon_start(addon)) + self.addons.addon_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(addon) == STATE_STOPED: + if await self.addons.state_addon(addon) == STATE_STOPPED: raise RuntimeError("Addon is already stoped.") return await asyncio.shield( - self.addons.addon_stop(addon)) + self.addons.addon_stop(addon), loop=self.loop) @api_process async def update(self, request): @@ -113,4 +114,4 @@ class APIAddons(object): raise RuntimeError("Version is already in use.") return await asyncio.shield( - self.addons.addon_update(addon, version)) + self.addons.addon_update(addon, version), loop=self.loop) diff --git a/hassio/api/homeassistant.py b/hassio/api/homeassistant.py index cd55a2453..a36ba5f0d 100644 --- a/hassio/api/homeassistant.py +++ b/hassio/api/homeassistant.py @@ -45,4 +45,5 @@ class APIHomeAssistant(object): 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.dock_hass.update(version), loop=self.loop) diff --git a/hassio/config.py b/hassio/config.py index 173e7dee1..2bdc61a7f 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -1,5 +1,4 @@ """Bootstrap HassIO.""" -import json import logging import os @@ -40,7 +39,7 @@ class Config(object): def save(self): """Store data to config file.""" - if not write_json_file(self._filename, self._data) + if not write_json_file(self._filename, self._data): _LOGGER.exception("Can't store config in %s", self._filename) return False return True @@ -53,7 +52,7 @@ class CoreConfig(Config): """Initialize config object.""" self.websession = websession - super().__ini__(FILE_HASSIO_CONFIG) + super().__init__(FILE_HASSIO_CONFIG) # init data if not self._data: diff --git a/hassio/const.py b/hassio/const.py index 2c6123277..2f33d1f1d 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -43,11 +43,12 @@ ATTR_MAP_SSL = 'map_ssl' ATTR_OPTIONS = 'options' ATTR_INSTALLED = 'installed' ATTR_STATE = 'state' +ATTR_SCHEMA = 'schema' STARTUP_BEFORE = 'before' STARTUP_AFTER = 'after' STARTUP_ONCE = 'once' -BOOT_STOP = 'auto' +BOOT_AUTO = 'auto' BOOT_MANUAL = 'manual' STATE_STARTED = 'started' -STATE_STOPED = 'stoped' +STATE_STOPPED = 'stopped' diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 667968787..07ffb3fbe 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -19,7 +19,7 @@ class DockerAddon(DockerBase): super().__init__( config, loop, dock, image=addons_data.get_image(addon)) self.addon = addon - self.addons_data + self.addons_data = addons_data @property def docker_name(self): diff --git a/hassio/dock/supervisor.py b/hassio/dock/supervisor.py index 1f61ecbde..ed3f1aeed 100644 --- a/hassio/dock/supervisor.py +++ b/hassio/dock/supervisor.py @@ -28,6 +28,6 @@ class DockerSupervisor(DockerBase): """Update docker image.""" raise RuntimeError("Not support on supervisor docker container!") - async def remove(self, tag): + async def remove(self): """Remove docker image.""" raise RuntimeError("Not support on supervisor docker container!") From dbc080c24d4ceae7f97c9692cbc768cec8377000 Mon Sep 17 00:00:00 2001 From: pvizeli Date: Thu, 13 Apr 2017 11:25:06 +0200 Subject: [PATCH 11/34] Add automode --- hassio/addons/__init__.py | 27 +++++++++++++++------------ hassio/addons/data.py | 19 +++++++++++++++++-- hassio/core.py | 9 ++++++++- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index d6f7146a7..a14a514b5 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -40,6 +40,18 @@ class AddonManager(AddonsData): return self.read_addons_repo() + 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(addon))) + + _LOGGER.info("Startup %s run %d addons.", start_type, len(tasks)) + if tasks: + await asyncio.wait(tasks, loop=self.loop) + async def install_addon(self, addon, version=None): """Install a addon.""" if not self.exists_addon(addon): @@ -108,10 +120,7 @@ class AddonManager(AddonsData): _LOGGER.error("Can't write options for addon %s.", addon) return False - if not await self.dockers[addon].run(): - return False - - return True + return await self.dockers[addon].run(): async def stop_addon(self, addon): """Stop addon.""" @@ -119,10 +128,7 @@ class AddonManager(AddonsData): _LOGGER.error("No docker found for addon %s.", addon) return False - if not await self.dockers[addon].stop(): - return False - - return True + return await self.dockers[addon].stop(): async def update_addon(self, addon, version=None): """Update addon.""" @@ -135,7 +141,4 @@ class AddonManager(AddonsData): return False version = version or self.get_version(addon) - if not await self.dockers[addon].update(version): - return False - - return True + return await self.dockers[addon].update(version): diff --git a/hassio/addons/data.py b/hassio/addons/data.py index dec72b829..e15c84a35 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -73,12 +73,12 @@ class AddonsData(Config): @property def list_installed(self): """Return a list of installed addons.""" - return self._data.keys() + return set(self._data.keys()) @property def list_all(self): """Return a list of available addons.""" - return self._addons_data.keys() + return set(self._addons_data.keys()) @property def list(self): @@ -95,6 +95,21 @@ class AddonsData(Config): return data + def list_startup(self, start_type): + """Get list of installed addon with need start by type.""" + addon_list = set() + for addon, value in self._data.items(): + if self.get_boot(addon) != BOOT_AUTO: + continue + + try: + if self._addons_data[addon][ATTR_STARTUP] == start_type: + addon_list.add(addon) + except KeyError: + continue + + return addon_list + def exists_addon(self, addon): """Return True if a addon exists.""" return addon in self._addons_data diff --git a/hassio/core.py b/hassio/core.py index 5faf29359..62fcffb02 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -10,7 +10,8 @@ from .addons import AddonManager from .api import RestAPI from .host_controll import HostControll from .const import ( - SOCKET_DOCKER, RUN_UPDATE_INFO_TASKS, RUN_RELOAD_ADDONS_TASKS) + SOCKET_DOCKER, RUN_UPDATE_INFO_TASKS, RUN_RELOAD_ADDONS_TASKS, + STARTUP_AFTER, STARTUP_BEFORE) from .scheduler import Scheduler from .dock.homeassistant import DockerHomeAssistant from .dock.supervisor import DockerSupervisor @@ -87,9 +88,15 @@ class HassIO(object): # start api await self.api.start() + # start addon mark as before + await self.addons.auto_boot(STARTUP_BEFORE) + # run HomeAssistant await self.homeassistant.run() + # start addon mark as after + await self.addons.auto_boot(STARTUP_AFTER) + async def stop(self): """Stop a running orchestration.""" tasks = [self.websession.close(), self.api.stop()] From 40e5e6eb9d62660c810a81b5b48181a95dc69301 Mon Sep 17 00:00:00 2001 From: pvizeli Date: Thu, 13 Apr 2017 16:15:50 +0200 Subject: [PATCH 12/34] some cleanups / auto remove old addons. --- hassio/addons/__init__.py | 31 ++++++++++++++++++++----------- hassio/addons/data.py | 13 ++++++++++++- hassio/api/addons.py | 10 +++++----- hassio/api/homeassistant.py | 4 ++-- hassio/api/host.py | 2 +- hassio/api/supervisor.py | 2 +- hassio/core.py | 5 +++++ hassio/dock/__init__.py | 16 ++++++++-------- hassio/dock/addon.py | 2 +- hassio/dock/homeassistant.py | 2 +- 10 files changed, 56 insertions(+), 31 deletions(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index a14a514b5..5fd0e332b 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -40,6 +40,15 @@ class AddonManager(AddonsData): 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) @@ -48,18 +57,18 @@ class AddonManager(AddonsData): for addon in boot_list: tasks.append(self.loop.create_task(self.start_addon(addon))) - _LOGGER.info("Startup %s run %d addons.", start_type, len(tasks)) + _LOGGER.info("Startup %s run %d addons", start_type, len(tasks)) if tasks: await asyncio.wait(tasks, loop=self.loop) async def install_addon(self, addon, version=None): """Install a addon.""" if not self.exists_addon(addon): - _LOGGER.error("Addon %s not exists for install.", 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) + _LOGGER.error("Addon %s is already installed", addon) return False if not os.path.isdir(self.path_data(addon)): @@ -81,11 +90,11 @@ class AddonManager(AddonsData): async def uninstall_addon(self, addon): """Remove a addon.""" if not self.is_installed(addon): - _LOGGER.error("Addon %s is already uninstalled.", 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) + _LOGGER.error("No docker found for addon %s", addon) return False if not await self.dockers[addon].remove(): @@ -103,7 +112,7 @@ class AddonManager(AddonsData): 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) + _LOGGER.error("No docker found for addon %s", addon) return if await self.dockers[addon].is_running(): @@ -113,11 +122,11 @@ class AddonManager(AddonsData): async def start_addon(self, addon): """Set options and start addon.""" if addon not in self.dockers: - _LOGGER.error("No docker found for addon %s.", addon) + _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) + _LOGGER.error("Can't write options for addon %s", addon) return False return await self.dockers[addon].run(): @@ -125,7 +134,7 @@ class AddonManager(AddonsData): async def stop_addon(self, addon): """Stop addon.""" if addon not in self.dockers: - _LOGGER.error("No docker found for addon %s.", addon) + _LOGGER.error("No docker found for addon %s", addon) return False return await self.dockers[addon].stop(): @@ -133,11 +142,11 @@ class AddonManager(AddonsData): async def update_addon(self, addon, version=None): """Update addon.""" if self.is_installed(addon): - _LOGGER.error("Addon %s is not 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) + _LOGGER.error("No docker found for addon %s", addon) return False version = version or self.get_version(addon) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index e15c84a35..aed6d02f1 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -67,7 +67,7 @@ class AddonsData(Config): _LOGGER.warning("Can't read %s", addon) except vol.Invalid as ex: - _LOGGER.warning("Can't read %s -> %s.", addon, + _LOGGER.warning("Can't read %s -> %s", addon, humanize_error(addon_config, ex)) @property @@ -106,10 +106,21 @@ class AddonsData(Config): 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 diff --git a/hassio/api/addons.py b/hassio/api/addons.py index 28a9d9c5e..748b1639f 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -31,9 +31,9 @@ class APIAddons(object): # check data if not self.addons.exists_addon(addon): - raise RuntimeError("Addon not exists.") + raise RuntimeError("Addon not exists") if check_installed and not self.addons.is_installed(addon): - raise RuntimeError("Addon is not installed.") + raise RuntimeError("Addon is not installed") return addon @@ -86,7 +86,7 @@ class APIAddons(object): addon = self._extract_addon(request) if await self.addons.state_addon(addon) == STATE_STARTED: - raise RuntimeError("Addon is already running.") + raise RuntimeError("Addon is already running") return await asyncio.shield( self.addons.addon_start(addon), loop=self.loop) @@ -97,7 +97,7 @@ class APIAddons(object): addon = self._extract_addon(request) if await self.addons.state_addon(addon) == STATE_STOPPED: - raise RuntimeError("Addon is already stoped.") + raise RuntimeError("Addon is already stoped") return await asyncio.shield( self.addons.addon_stop(addon), loop=self.loop) @@ -111,7 +111,7 @@ class APIAddons(object): ATTR_VERSION, self.addons.get_version(addon)) if version == self.addons.version_installed(addon): - raise RuntimeError("Version is already in use.") + raise RuntimeError("Version is already in use") return await asyncio.shield( self.addons.addon_update(addon, version), loop=self.loop) diff --git a/hassio/api/homeassistant.py b/hassio/api/homeassistant.py index a36ba5f0d..6a81e66f6 100644 --- a/hassio/api/homeassistant.py +++ b/hassio/api/homeassistant.py @@ -40,10 +40,10 @@ class APIHomeAssistant(object): version = body.get(ATTR_VERSION, self.config.current_homeassistant) 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: - raise RuntimeError("%s is already in use.", version) + raise RuntimeError("Version is already in use") return await asyncio.shield( self.dock_hass.update(version), loop=self.loop) diff --git a/hassio/api/host.py b/hassio/api/host.py index 23f0fb453..c799a42cf 100644 --- a/hassio/api/host.py +++ b/hassio/api/host.py @@ -56,6 +56,6 @@ class APIHost(object): version = body.get(ATTR_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) diff --git a/hassio/api/supervisor.py b/hassio/api/supervisor.py index 109ff76ec..9ce407d71 100644 --- a/hassio/api/supervisor.py +++ b/hassio/api/supervisor.py @@ -62,6 +62,6 @@ class APISupervisor(object): version = body.get(ATTR_VERSION, self.config.current_hassio) 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) diff --git a/hassio/core.py b/hassio/core.py index 62fcffb02..d1b0bf8c3 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -88,6 +88,11 @@ class HassIO(object): # start api 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) diff --git a/hassio/dock/__init__.py b/hassio/dock/__init__.py index bfb53df21..cb3630a7c 100644 --- a/hassio/dock/__init__.py +++ b/hassio/dock/__init__.py @@ -53,7 +53,7 @@ class DockerBase(object): image.tag(self.image, tag='latest') 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) except docker.errors.APIError as 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.version = get_version_from_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) except (docker.errors.DockerException, KeyError): _LOGGER.fatal( @@ -138,7 +138,7 @@ class DockerBase(object): return False 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) return await self.loop.run_in_executor(None, self._run) @@ -191,14 +191,14 @@ class DockerBase(object): Need run inside executor. """ - if self.container: + if self._is_running(): self._stop() image = "{}:latest".format(self.image) try: self.dock.images.remove(image=image, force=True) except docker.errors.DockerException as err: - _LOGGER.warning("Can't remove image %s -> %s.", image, err) + _LOGGER.warning("Can't remove image %s -> %s", image, err) return False return True @@ -220,10 +220,10 @@ class DockerBase(object): Need run inside executor. """ - old_image = "{}:{}".format(self.image, self.version) 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) # update docker image @@ -234,7 +234,7 @@ class DockerBase(object): self.dock.images.remove(image=old_image, force=True) except docker.errors.DockerException as err: _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() diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 07ffb3fbe..3afeb2889 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -70,7 +70,7 @@ class DockerAddon(DockerBase): 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) + _LOGGER.error("Can't run %s -> %s", self.image, err) return False return True diff --git a/hassio/dock/homeassistant.py b/hassio/dock/homeassistant.py index 5bcbcac84..b9f0c829f 100644 --- a/hassio/dock/homeassistant.py +++ b/hassio/dock/homeassistant.py @@ -60,7 +60,7 @@ class DockerHomeAssistant(DockerBase): 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) + _LOGGER.error("Can't run %s -> %s", self.image, err) return False return True From 62df079be74563e0840222997b5f8806f1ed0738 Mon Sep 17 00:00:00 2001 From: pvizeli Date: Thu, 13 Apr 2017 16:40:35 +0200 Subject: [PATCH 13/34] Fix lint --- hassio/addons/__init__.py | 6 +++--- hassio/addons/data.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index 5fd0e332b..d4d04a2da 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -129,7 +129,7 @@ class AddonManager(AddonsData): _LOGGER.error("Can't write options for addon %s", addon) return False - return await self.dockers[addon].run(): + return await self.dockers[addon].run() async def stop_addon(self, addon): """Stop addon.""" @@ -137,7 +137,7 @@ class AddonManager(AddonsData): _LOGGER.error("No docker found for addon %s", addon) return False - return await self.dockers[addon].stop(): + return await self.dockers[addon].stop() async def update_addon(self, addon, version=None): """Update addon.""" @@ -150,4 +150,4 @@ class AddonManager(AddonsData): return False version = version or self.get_version(addon) - return await self.dockers[addon].update(version): + return await self.dockers[addon].update(version) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index aed6d02f1..3d60bc5c8 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -98,7 +98,7 @@ class AddonsData(Config): def list_startup(self, start_type): """Get list of installed addon with need start by type.""" addon_list = set() - for addon, value in self._data.items(): + for addon in self._data.keys(): if self.get_boot(addon) != BOOT_AUTO: continue From eb0ee31b5a3b8408da6c3b5a7fa114c7d237f7d7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 13 Apr 2017 22:41:03 +0200 Subject: [PATCH 14/34] update version --- hassio/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/const.py b/hassio/const.py index 2f33d1f1d..131a64d30 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -1,5 +1,5 @@ """Const file for HassIO.""" -HASSIO_VERSION = '0.5' +HASSIO_VERSION = '0.6' URL_HASSIO_VERSION = \ 'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json' From 360f546ab038c28dacab34fe77450f737517e366 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 13 Apr 2017 23:45:48 +0200 Subject: [PATCH 15/34] add log output --- hassio/addons/git.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hassio/addons/git.py b/hassio/addons/git.py index 216fd7c69..6a2a364a1 100644 --- a/hassio/addons/git.py +++ b/hassio/addons/git.py @@ -27,6 +27,7 @@ class AddonsRepo(object): 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)) @@ -40,6 +41,7 @@ class AddonsRepo(object): """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) @@ -58,6 +60,7 @@ class AddonsRepo(object): async with self._lock: try: + _LOGGER.info("Pull addons repository") await self.loop.run_in_executor( None, self.repo.remotes.origin.pull) From fe72e768ec1c2439f202dcc634ec4cf7e949d45b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 14 Apr 2017 01:16:43 +0200 Subject: [PATCH 16/34] fix lint --- hassio/addons/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index d4d04a2da..0c026be10 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -1,4 +1,5 @@ """Init file for HassIO addons.""" +import asyncio import logging import os import shutil From b5ec1e0cfd7426914430d2c1774d72099707cca7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 16 Apr 2017 11:39:54 +0200 Subject: [PATCH 17/34] add support for custom image --- hassio/addons/data.py | 18 ++++++++++++++++-- hassio/bootstrap.py | 5 +++++ hassio/config.py | 6 ++++++ hassio/const.py | 1 + hassio/dock/addon.py | 2 +- 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 3d60bc5c8..77b6ce012 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -9,7 +9,7 @@ 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) + BOOT_MANUAL, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA, ATTR_IMAGE) from ..config import Config from ..tools import read_json_file, write_json_file @@ -40,6 +40,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Required(ATTR_SCHEMA): { vol.Any: vol.In([V_STR, V_INT, V_FLOAT, V_BOOL]) }, + vol.Optional(ATTR_IMAGE): vol.Match(r"\w*/\w*"), }) @@ -54,7 +55,12 @@ class AddonsData(Config): def read_addons_repo(self): """Read data from addons repository.""" - pattern = ADDONS_REPO_PATTERN.format(self.config.path_addons_repo) + 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: @@ -191,6 +197,14 @@ class AddonsData(Config): """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 is not in self._addons_data[addon]: + return "{}/addon_{}".format( + DOCKER_REPO, self.addons_data.get_slug(self.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] diff --git a/hassio/bootstrap.py b/hassio/bootstrap.py index 779b91b0d..f5be8edea 100644 --- a/hassio/bootstrap.py +++ b/hassio/bootstrap.py @@ -32,6 +32,11 @@ def initialize_system_data(websession): 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 diff --git a/hassio/config.py b/hassio/config.py index 2bdc61a7f..1f7fd6832 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -18,6 +18,7 @@ HASSIO_CURRENT = 'hassio_current' ADDONS_REPO = "{}/addons" ADDONS_DATA = "{}/addons_data" +ADDONS_CUSTOM = "{}/addons_custom" UPSTREAM_BETA = 'upstream_beta' @@ -132,6 +133,11 @@ class CoreConfig(Config): """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.""" diff --git a/hassio/const.py b/hassio/const.py index 131a64d30..1a35682a3 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -44,6 +44,7 @@ ATTR_OPTIONS = 'options' ATTR_INSTALLED = 'installed' ATTR_STATE = 'state' ATTR_SCHEMA = 'schema' +ATTR_IMAGE = 'image' STARTUP_BEFORE = 'before' STARTUP_AFTER = 'after' diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 3afeb2889..ef61fc7e6 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -24,7 +24,7 @@ class DockerAddon(DockerBase): @property def docker_name(self): """Return name of docker container.""" - return "addon_{}".format(self.addons_data.get_slug(self.addon)) + return self.addons_data.get_image(self.addon) def _run(self): """Run docker image. From c76e851029150a8ffb22febda97083b12d348f3f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 16 Apr 2017 22:42:57 +0200 Subject: [PATCH 18/34] fix lint --- hassio/addons/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 77b6ce012..26d3045d3 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -199,7 +199,7 @@ class AddonsData(Config): def get_image(self, addon): """Return image name of addon.""" - if ATTR_IMAGE is not in self._addons_data[addon]: + if ATTR_IMAGE not in self._addons_data[addon]: return "{}/addon_{}".format( DOCKER_REPO, self.addons_data.get_slug(self.addon)) From 8af1dfc882312f26e6ed92935a4c371265d604d7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 16 Apr 2017 22:47:33 +0200 Subject: [PATCH 19/34] fix lint --- hassio/addons/data.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 26d3045d3..5dbc8a073 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -171,12 +171,6 @@ class AddonsData(Config): return self._addons_data[addon][ATTR_BOOT] - def get_image(self, addon): - """Return name of addon docker image.""" - return "{}/{}-addon-{}".format( - DOCKER_REPO, self.config.hassio_arch, - self._addons_data[addon][ATTR_SLUG]) - def get_name(self, addon): """Return name of addon.""" return self._addons_data[addon][ATTR_NAME] From b05f2db02327ed1c7fc83f79f600e18f418c5341 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 16 Apr 2017 22:49:28 +0200 Subject: [PATCH 20/34] fix lint --- hassio/addons/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 5dbc8a073..001bd0bef 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -195,7 +195,7 @@ class AddonsData(Config): """Return image name of addon.""" if ATTR_IMAGE not in self._addons_data[addon]: return "{}/addon_{}".format( - DOCKER_REPO, self.addons_data.get_slug(self.addon)) + DOCKER_REPO, self._addons_data.get_slug(addon)) return self._addons_data[addon][ATTR_IMAGE] From 29ac861b87835953d69c4c509ad3a49db7a4d940 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 16 Apr 2017 23:00:12 +0200 Subject: [PATCH 21/34] fix bug --- hassio/addons/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 001bd0bef..9c88b59af 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -195,7 +195,7 @@ class AddonsData(Config): """Return image name of addon.""" if ATTR_IMAGE not in self._addons_data[addon]: return "{}/addon_{}".format( - DOCKER_REPO, self._addons_data.get_slug(addon)) + DOCKER_REPO, self.get_slug(addon)) return self._addons_data[addon][ATTR_IMAGE] From f9500f6d90fa86a885d264dce6f177bcadf5c102 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 16 Apr 2017 23:32:23 +0200 Subject: [PATCH 22/34] fix fetch time bug --- hassio/tools.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hassio/tools.py b/hassio/tools.py index aedbd6df1..662211d12 100644 --- a/hassio/tools.py +++ b/hassio/tools.py @@ -27,9 +27,12 @@ async def fetch_current_versions(websession, beta=False): async with websession.get(url) as request: 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) + except json.JSONDecoder 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.""" From 5831177fd827921de6533c94839ccd17af341d6c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 17 Apr 2017 00:03:57 +0200 Subject: [PATCH 23/34] fix git repo open --- hassio/addons/git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/addons/git.py b/hassio/addons/git.py index 6a2a364a1..5b665e876 100644 --- a/hassio/addons/git.py +++ b/hassio/addons/git.py @@ -29,7 +29,7 @@ class AddonsRepo(object): try: _LOGGER.info("Load addons repository") self.repo = await self.loop.run_in_executor( - None, git.Repo(self.config.path_addons_repo)) + None, git.Repo, self.config.path_addons_repo) except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err: _LOGGER.error("Can't load addons repo: %s.", err) From daab4a86b2cf13b68ddc2360ebebffaa16f38488 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 17 Apr 2017 00:31:34 +0200 Subject: [PATCH 24/34] fix validate schema --- hassio/addons/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 9c88b59af..fbb08b9b6 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -38,7 +38,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ 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]) + vol.Coerce(str): vol.In([V_STR, V_INT, V_FLOAT, V_BOOL]) }, vol.Optional(ATTR_IMAGE): vol.Match(r"\w*/\w*"), }) From 5b23347563e7df837da8ffa2d1959dca0b7f9af3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 17 Apr 2017 17:36:29 +0200 Subject: [PATCH 25/34] fix addon name --- hassio/addons/data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index fbb08b9b6..95a6f16f4 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -194,8 +194,8 @@ class AddonsData(Config): 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.get_slug(addon)) + return "{}/{}-addon-{}".format( + DOCKER_REPO, self.config.hassio_arch, self.get_slug(addon)) return self._addons_data[addon][ATTR_IMAGE] From c019d1f3c5dadd522408fdd265692653b0cd1967 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 17 Apr 2017 18:12:58 +0200 Subject: [PATCH 26/34] Fix api function names --- hassio/addons/__init__.py | 14 +++++++------- hassio/api/addons.py | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index 0c026be10..96255a648 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -56,13 +56,13 @@ class AddonManager(AddonsData): tasks = [] for addon in boot_list: - tasks.append(self.loop.create_task(self.start_addon(addon))) + 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_addon(self, addon, version=None): + 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) @@ -88,7 +88,7 @@ class AddonManager(AddonsData): self.set_install_addon(addon, version) return True - async def uninstall_addon(self, addon): + async def uninstall(self, addon): """Remove a addon.""" if not self.is_installed(addon): _LOGGER.error("Addon %s is already uninstalled", addon) @@ -110,7 +110,7 @@ class AddonManager(AddonsData): self.set_uninstall_addon(addon) return True - async def state_addon(self, addon): + 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) @@ -120,7 +120,7 @@ class AddonManager(AddonsData): return STATE_STARTED return STATE_STOPPED - async def start_addon(self, addon): + 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) @@ -132,7 +132,7 @@ class AddonManager(AddonsData): return await self.dockers[addon].run() - async def stop_addon(self, addon): + async def stop(self, addon): """Stop addon.""" if addon not in self.dockers: _LOGGER.error("No docker found for addon %s", addon) @@ -140,7 +140,7 @@ class AddonManager(AddonsData): return await self.dockers[addon].stop() - async def update_addon(self, addon, version=None): + async def update(self, addon, version=None): """Update addon.""" if self.is_installed(addon): _LOGGER.error("Addon %s is not installed", addon) diff --git a/hassio/api/addons.py b/hassio/api/addons.py index 748b1639f..74a5bee59 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -45,7 +45,7 @@ class APIAddons(object): info = { ATTR_VERSION: self.addons.version_installed(addon), ATTR_CURRENT: self.addons.get_version(addon), - ATTR_STATE: await self.addons.state_addon(addon), + ATTR_STATE: await self.addons.state(addon), ATTR_BOOT: self.addons.get_boot(addon), ATTR_OPTIONS: self.addons.get_options(addon), } @@ -70,7 +70,7 @@ class APIAddons(object): ATTR_VERSION, self.addons.get_version(addon)) return await asyncio.shield( - self.addons.addon_install(addon, version), loop=self.loop) + self.addons.install(addon, version), loop=self.loop) @api_process async def uninstall(self, request): @@ -78,29 +78,29 @@ class APIAddons(object): addon = self._extract_addon(request) return await asyncio.shield( - self.addons.addon_uninstall(addon), loop=self.loop) + 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(addon) == STATE_STARTED: + if await self.addons.state(addon) == STATE_STARTED: raise RuntimeError("Addon is already running") return await asyncio.shield( - self.addons.addon_start(addon), loop=self.loop) + 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(addon) == STATE_STOPPED: + if await self.addons.state(addon) == STATE_STOPPED: raise RuntimeError("Addon is already stoped") return await asyncio.shield( - self.addons.addon_stop(addon), loop=self.loop) + self.addons.stop(addon), loop=self.loop) @api_process async def update(self, request): @@ -114,4 +114,4 @@ class APIAddons(object): raise RuntimeError("Version is already in use") return await asyncio.shield( - self.addons.addon_update(addon, version), loop=self.loop) + self.addons.update(addon, version), loop=self.loop) From 2bd1636097336d07b696aa41bbba3de1563dde0e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 17 Apr 2017 18:50:05 +0200 Subject: [PATCH 27/34] change handling with arch detection --- hassio/addons/__init__.py | 4 +++- hassio/addons/data.py | 3 ++- hassio/config.py | 8 +------- hassio/core.py | 4 +++- hassio/tools.py | 2 +- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index 96255a648..84889fbf9 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -24,8 +24,10 @@ class AddonManager(AddonsData): self.repo = AddonsRepo(config, loop) self.dockers = {} - async def prepare(self): + async def prepare(self, arch): """Startup addon management.""" + self.arch = arch + # load addon repository if await self.repo.load(): self.read_addons_repo() diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 95a6f16f4..0ca361b58 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -52,6 +52,7 @@ class AddonsData(Config): super().__init__(FILE_HASSIO_ADDONS) self.config = config self._addons_data = {} + self.arch = None def read_addons_repo(self): """Read data from addons repository.""" @@ -195,7 +196,7 @@ class AddonsData(Config): """Return image name of addon.""" if ATTR_IMAGE not in self._addons_data[addon]: return "{}/{}-addon-{}".format( - DOCKER_REPO, self.config.hassio_arch, self.get_slug(addon)) + DOCKER_REPO, self.arch, self.get_slug(addon)) return self._addons_data[addon][ATTR_IMAGE] diff --git a/hassio/config.py b/hassio/config.py index 1f7fd6832..8f47f6079 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -4,8 +4,7 @@ import os from .const import FILE_HASSIO_CONFIG, HASSIO_SHARE from .tools import ( - fetch_current_versions, get_arch_from_image, write_json_file, - read_json_file) + fetch_current_versions, write_json_file, read_json_file) _LOGGER = logging.getLogger(__name__) @@ -88,11 +87,6 @@ class CoreConfig(Config): """Set beta upstream mode.""" self._data[UPSTREAM_BETA] = bool(value) - @property - def hassio_arch(self): - """Return arch they run.""" - return get_arch_from_image(self.homeassistant_image) - @property def homeassistant_image(self): """Return docker homeassistant repository.""" diff --git a/hassio/core.py b/hassio/core.py index d1b0bf8c3..ea9db2a05 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -15,6 +15,7 @@ from .const import ( from .scheduler import Scheduler from .dock.homeassistant import DockerHomeAssistant from .dock.supervisor import DockerSupervisor +from .tools import get_arch_from_image _LOGGER = logging.getLogger(__name__) @@ -77,7 +78,8 @@ class HassIO(object): await self._setup_homeassistant() # Load addons - await self.addons.prepare() + arch = get_arch_from_image(self.supervisor.image) + await self.addons.prepare(arch) # schedule addon update task self.scheduler.register_task( diff --git a/hassio/tools.py b/hassio/tools.py index 662211d12..2c8d44d6d 100644 --- a/hassio/tools.py +++ b/hassio/tools.py @@ -13,7 +13,7 @@ from .const import URL_HASSIO_VERSION, URL_HASSIO_VERSION_BETA _LOGGER = logging.getLogger(__name__) _RE_VERSION = re.compile(r"VERSION=(.*)") -_IMAGE_ARCH = re.compile(r"([a-z0-9]*)-hassio-supervisor") +_IMAGE_ARCH = re.compile(r".*/([a-z0-9]*)-hassio-supervisor") async def fetch_current_versions(websession, beta=False): From e25d30af52f3f8cde9c7cfcc4295a6bc89fefcd5 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 17 Apr 2017 18:54:09 +0200 Subject: [PATCH 28/34] fix lint --- hassio/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/tools.py b/hassio/tools.py index 2c8d44d6d..9b3b9c15f 100644 --- a/hassio/tools.py +++ b/hassio/tools.py @@ -30,7 +30,7 @@ async def fetch_current_versions(websession, beta=False): except (aiohttp.ClientError, asyncio.TimeoutError, KeyError) as err: _LOGGER.warning("Can't fetch versions from %s! %s", url, err) - except json.JSONDecoder as err: + except json.JSONDecodeError as err: _LOGGER.warning("Can't parse versions from %s! %s", url, err) From e2bf267713107a97d2b7f51a1511848ecabbd842 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 17 Apr 2017 22:04:24 +0200 Subject: [PATCH 29/34] fix addon name --- hassio/dock/addon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index ef61fc7e6..3afeb2889 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -24,7 +24,7 @@ class DockerAddon(DockerBase): @property def docker_name(self): """Return name of docker container.""" - return self.addons_data.get_image(self.addon) + return "addon_{}".format(self.addons_data.get_slug(self.addon)) def _run(self): """Run docker image. From fc1789315811c38e0acf2f34ce00c3eee222dd5e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 17 Apr 2017 22:34:35 +0200 Subject: [PATCH 30/34] Fix cleanup --- hassio/dock/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hassio/dock/__init__.py b/hassio/dock/__init__.py index cb3630a7c..d26746d5c 100644 --- a/hassio/dock/__init__.py +++ b/hassio/dock/__init__.py @@ -194,11 +194,13 @@ class DockerBase(object): if self._is_running(): self._stop() - image = "{}:latest".format(self.image) + _LOGGER.info("Remove docker %s with latest and %s", + self.image, self.version) + try: self.dock.images.remove(image=image, force=True) except docker.errors.DockerException as err: - _LOGGER.warning("Can't remove image %s -> %s", image, err) + _LOGGER.warning("Can't remove image %s -> %s", self.image, err) return False return True From d2db89a66595634c1b3ff0301dced3348effe9a5 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 17 Apr 2017 22:36:13 +0200 Subject: [PATCH 31/34] Fix cleanup --- hassio/dock/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hassio/dock/__init__.py b/hassio/dock/__init__.py index d26746d5c..e4401fb74 100644 --- a/hassio/dock/__init__.py +++ b/hassio/dock/__init__.py @@ -198,7 +198,10 @@ class DockerBase(object): self.image, self.version) try: - self.dock.images.remove(image=image, force=True) + 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 From 322480bba144adb82bad171cd06ae5694f657da3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 17 Apr 2017 23:31:49 +0200 Subject: [PATCH 32/34] Fix bug with check installed --- hassio/addons/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index 84889fbf9..3c5430447 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -144,7 +144,7 @@ class AddonManager(AddonsData): async def update(self, addon, version=None): """Update addon.""" - if self.is_installed(addon): + if not self.is_installed(addon): _LOGGER.error("Addon %s is not installed", addon) return False From f1ce5faf17eef4a65c9895be9345d8c3d964fcdb Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 17 Apr 2017 23:49:52 +0200 Subject: [PATCH 33/34] update version in config --- hassio/addons/__init__.py | 5 ++++- hassio/addons/data.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index 3c5430447..d250de166 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -153,4 +153,7 @@ class AddonManager(AddonsData): return False version = version or self.get_version(addon) - return await self.dockers[addon].update(version) + if await self.dockers[addon].update(version): + self.set_version(addon, version) + return True + return False diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 0ca361b58..846d693ec 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -158,6 +158,11 @@ class AddonsData(Config): 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] From e0be15cb45dc90f7cab6d2f47f01721759673a8f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 18 Apr 2017 00:26:50 +0200 Subject: [PATCH 34/34] update version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 8c44a5662..02eb23bdd 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "hassio_tag": "0.5", + "hassio_tag": "0.6", "homeassistant_tag": "0.42.3", "resinos_version": "0.3", "resinhup_version": "0.1"