From e33420f26ef3943b699ae1163c4da4e04fd826f4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 24 May 2017 00:09:47 +0200 Subject: [PATCH 1/5] Pump version to 0.34 --- hassio/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/const.py b/hassio/const.py index 6ac1e523972..1ae61b72f34 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -1,7 +1,7 @@ """Const file for HassIO.""" from pathlib import Path -HASSIO_VERSION = '0.33' +HASSIO_VERSION = '0.34' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' 'hassio/master/version.json') From c1cd9bba45b3f76267002de604abdf6b4d1c4688 Mon Sep 17 00:00:00 2001 From: bestlibre Date: Wed, 31 May 2017 09:39:22 +0200 Subject: [PATCH 2/5] Adding tmpfs to addon config (#72) * Adding tmpfs to addon config * Adding vol.Match and correcting syntax error * Missing import and linting * Update addon.py * Revert "Update addon.py" This reverts commit 82798c8f2dc4f1bd297ef437f96eb19c69ca5df0. * optimaze code --- hassio/addons/data.py | 7 ++++++- hassio/addons/validate.py | 4 +++- hassio/const.py | 1 + hassio/dock/addon.py | 11 ++++++++++- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index a4da39d10be..ad2d79d3a33 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -16,7 +16,8 @@ FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_STARTUP, ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, BOOT_AUTO, ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY, ATTR_URL, ATTR_ARCH, - ATTR_LOCATON, ATTR_DEVICES, ATTR_ENVIRONMENT, ATTR_HOST_NETWORK) + ATTR_LOCATON, ATTR_DEVICES, ATTR_ENVIRONMENT, ATTR_HOST_NETWORK, + ATTR_TMPFS) from ..config import Config from ..tools import read_json_file, write_json_file @@ -304,6 +305,10 @@ def get_devices(self, addon): """Return devices of addon.""" return self._system_data[addon].get(ATTR_DEVICES) + def get_tmpfs(self, addon): + """Return tmpfs of addon.""" + return self._system_data[addon].get(ATTR_TMPFS) + def get_environment(self, addon): """Return environment of addon.""" return self._system_data[addon].get(ATTR_ENVIRONMENT) diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 96842e30c47..f5eecb9d2a1 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -7,7 +7,7 @@ STARTUP_BEFORE, STARTUP_INITIALIZE, BOOT_AUTO, BOOT_MANUAL, ATTR_SCHEMA, ATTR_IMAGE, ATTR_URL, ATTR_MAINTAINER, ATTR_ARCH, ATTR_DEVICES, ATTR_ENVIRONMENT, ATTR_HOST_NETWORK, ARCH_ARMHF, ARCH_AARCH64, ARCH_AMD64, - ARCH_I386) + ARCH_I386, ATTR_TMPFS) MAP_VOLUME = r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$" @@ -52,6 +52,8 @@ def check_network(data): vol.Optional(ATTR_PORTS): dict, vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(), vol.Optional(ATTR_DEVICES): [vol.Match(r"^(.*):(.*):([rwm]{1,3})$")], + vol.Optional(ATTR_TMPFS): + vol.Match(r"^size=(\d)*[kmg](,uid=\d{1,4})?(,rw)?$"), vol.Optional(ATTR_MAP, default=[]): [vol.Match(MAP_VOLUME)], vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)}, vol.Required(ATTR_OPTIONS): dict, diff --git a/hassio/const.py b/hassio/const.py index 1ae61b72f34..af7b6732708 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -79,6 +79,7 @@ ATTR_DEVICES = 'devices' ATTR_ENVIRONMENT = 'environment' ATTR_HOST_NETWORK = 'host_network' +ATTR_TMPFS = 'tmpfs' STARTUP_INITIALIZE = 'initialize' STARTUP_BEFORE = 'before' diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 6c8a0ba8b68..3e7a63d033d 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -38,6 +38,14 @@ def environment(self): 'TZ': self.config.timezone, } + @property + def tmpfs(self): + """Return tmpfs for docker add-on.""" + options = self.addons_data.get_tmpfs(self.addon) + if options: + return {"/tmpfs": "{}".format(options)} + return None + @property def volumes(self): """Generate volumes for mappings.""" @@ -100,7 +108,8 @@ def _run(self): ports=self.addons_data.get_ports(self.addon), devices=self.addons_data.get_devices(self.addon), environment=self.environment, - volumes=self.volumes + volumes=self.volumes, + tmpfs=self.tmpfs ) self.process_metadata() From 6b16da93cd618a70690c451eca6011078e8ea14f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 31 May 2017 23:41:04 +0200 Subject: [PATCH 3/5] WIP: Refactory / Cleanup docker base (#73) * Refactory / Cleanup docker base * Check ID of running image * Small bugs / lint * Add log info * Fix lint * Add a real cleanup solution * fix unused import * Cleanup restart after updates * Use restart callback * rename callback * Add info log for cleanup & fix lint * Fix lint * fix wrong id * fix set addon as install --- hassio/addons/__init__.py | 12 ++- hassio/config.py | 16 ---- hassio/core.py | 7 +- hassio/dock/__init__.py | 148 +++++++++++++++++++++-------------- hassio/dock/addon.py | 42 ++-------- hassio/dock/homeassistant.py | 21 ++--- hassio/dock/supervisor.py | 38 +-------- hassio/tools.py | 11 --- 8 files changed, 118 insertions(+), 177 deletions(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index 93d9df0ceaa..9860e98012d 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -191,15 +191,13 @@ async def update(self, addon, version=None): return False version = version or self.get_last_version(addon) - is_running = await self.dockers[addon].is_running() # update - if await self.dockers[addon].update(version): - self.set_addon_update(addon, version) - if is_running: - await self.start(addon) - return True - return False + if not await self.dockers[addon].update(version): + return False + + self.set_addon_update(addon, version) + return True async def restart(self, addon): """Restart addon.""" diff --git a/hassio/config.py b/hassio/config.py index 7c0ff16f2dc..cfc29130e2b 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -21,7 +21,6 @@ HASSIO_SSL = PurePath("ssl") HASSIO_LAST = 'hassio_last' -HASSIO_CLEANUP = 'hassio_cleanup' ADDONS_CORE = PurePath("addons/core") ADDONS_LOCAL = PurePath("addons/local") @@ -51,7 +50,6 @@ vol.Optional(TIMEZONE, default='UTC'): validate_timezone, vol.Optional(HOMEASSISTANT_LAST): vol.Coerce(str), vol.Optional(HASSIO_LAST): vol.Coerce(str), - vol.Optional(HASSIO_CLEANUP): vol.Coerce(str), vol.Optional(ADDONS_CUSTOM_LIST, default=[]): [vol.Url()], vol.Optional(SECURITY_INITIALIZE, default=False): vol.Boolean(), vol.Optional(SECURITY_TOTP): vol.Coerce(str), @@ -148,20 +146,6 @@ def timezone(self, value): self._data[TIMEZONE] = value self.save() - @property - def hassio_cleanup(self): - """Return Version they need to cleanup.""" - return self._data.get(HASSIO_CLEANUP) - - @hassio_cleanup.setter - def hassio_cleanup(self, version): - """Set or remove cleanup flag.""" - if version is None: - self._data.pop(HASSIO_CLEANUP, None) - else: - self._data[HASSIO_CLEANUP] = version - self.save() - @property def homeassistant_image(self): """Return docker homeassistant repository.""" diff --git a/hassio/core.py b/hassio/core.py index 28cd4b7f642..32767418d50 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -41,7 +41,7 @@ def __init__(self, loop): # init basic docker container self.supervisor = DockerSupervisor( - self.config, self.loop, self.dock, self) + self.config, self.loop, self.dock, self.stop) self.homeassistant = DockerHomeAssistant( self.config, self.loop, self.dock) @@ -54,7 +54,8 @@ def __init__(self, loop): async def setup(self): """Setup HassIO orchestration.""" # supervisor - await self.supervisor.attach() + if not await self.supervisor.attach(): + _LOGGER.fatal("Can't attach to supervisor docker container!") await self.supervisor.cleanup() # set api endpoint @@ -96,6 +97,8 @@ async def setup(self): _LOGGER.info("No HomeAssistant docker found.") await homeassistant_setup( self.config, self.loop, self.homeassistant) + else: + await self.homeassistant.attach() # Load addons arch = get_arch_from_image(self.supervisor.image) diff --git a/hassio/dock/__init__.py b/hassio/dock/__init__.py index 1302d1d3fb1..8f149aa83a0 100644 --- a/hassio/dock/__init__.py +++ b/hassio/dock/__init__.py @@ -6,7 +6,6 @@ import docker from ..const import LABEL_VERSION -from ..tools import get_version_from_env _LOGGER = logging.getLogger(__name__) @@ -20,12 +19,11 @@ def __init__(self, config, loop, dock, image=None): self.loop = loop self.dock = dock self.image = image - self.container = None self.version = None self._lock = asyncio.Lock(loop=loop) @property - def docker_name(self): + def name(self): """Return name of docker container.""" return None @@ -34,18 +32,18 @@ def in_progress(self): """Return True if a task is in progress.""" return self._lock.locked() - def process_metadata(self, metadata=None, force=False): + def process_metadata(self, metadata, force=False): """Read metadata and set it to object.""" - if not force and self.version: - return + # read image + if not self.image: + self.image = metadata['Config']['Image'] # read metadata - metadata = metadata or self.container.attrs - if LABEL_VERSION in metadata['Config']['Labels']: + need_version = force or not self.version + if need_version and LABEL_VERSION in metadata['Config']['Labels']: self.version = metadata['Config']['Labels'][LABEL_VERSION] - else: - # dedicated - self.version = get_version_from_env(metadata['Config']['Env']) + elif need_version: + _LOGGER.warning("Can't read version from %s", self.name) async def install(self, tag): """Pull docker image.""" @@ -66,7 +64,7 @@ def _install(self, tag): image = self.dock.images.pull("{}:{}".format(self.image, tag)) image.tag(self.image, tag='latest') - self.process_metadata(metadata=image.attrs, force=True) + self.process_metadata(image.attrs, force=True) except docker.errors.APIError as err: _LOGGER.error("Can't install %s:%s -> %s.", self.image, tag, err) return False @@ -87,8 +85,7 @@ def _exists(self): Need run inside executor. """ try: - image = self.dock.images.get(self.image) - self.process_metadata(metadata=image.attrs) + self.dock.images.get(self.image) except docker.errors.DockerException: return False @@ -106,16 +103,21 @@ def _is_running(self): Need run inside executor. """ - if not self.container: - try: - self.container = self.dock.containers.get(self.docker_name) - self.process_metadata() - except docker.errors.DockerException: - return False - else: - self.container.reload() + try: + container = self.dock.containers.get(self.name) + image = self.dock.images.get(self.image) + except docker.errors.DockerException: + return False - return self.container.status == 'running' + # container is not running + if container.status != 'running': + return False + + # we run on a old image, stop and start it + if container.image.id != image.id: + return False + + return True async def attach(self): """Attach to running docker container.""" @@ -132,16 +134,17 @@ def _attach(self): Need run inside executor. """ try: - self.container = self.dock.containers.get(self.docker_name) - self.image = self.container.attrs['Config']['Image'] - self.process_metadata() - _LOGGER.info("Attach to image %s with version %s", - self.image, self.version) - except (docker.errors.DockerException, KeyError): - _LOGGER.fatal( - "Can't attach to %s docker container!", self.docker_name) + if self.image: + obj_data = self.dock.images.get(self.image).attrs + else: + obj_data = self.dock.containers.get(self.name).attrs + except docker.errors.DockerException: return False + self.process_metadata(obj_data) + _LOGGER.info( + "Attach to image %s with version %s", self.image, self.version) + return True async def run(self): @@ -175,20 +178,19 @@ def _stop(self): Need run inside executor. """ - if not self.container: + try: + container = self.dock.containers.get(self.name) + except docker.errors.DockerException: return _LOGGER.info("Stop %s docker application", self.image) - self.container.reload() - if self.container.status == 'running': + if container.status == 'running': with suppress(docker.errors.DockerException): - self.container.stop() + container.stop() with suppress(docker.errors.DockerException): - self.container.remove(force=True) - - self.container = None + container.remove(force=True) async def remove(self): """Remove docker container.""" @@ -207,8 +209,8 @@ def _remove(self): if self._is_running(): self._stop() - _LOGGER.info("Remove docker %s with latest and %s", - self.image, self.version) + _LOGGER.info( + "Remove docker %s with latest and %s", self.image, self.version) try: with suppress(docker.errors.ImageNotFound): @@ -239,23 +241,21 @@ def _update(self, tag): Need run inside executor. """ - old_image = "{}:{}".format(self.image, self.version) + was_running = self._is_running() - _LOGGER.info("Update docker %s with %s:%s", - old_image, self.image, tag) + _LOGGER.info( + "Update docker %s with %s:%s", self.version, self.image, tag) # update docker image - if self._install(tag): - _LOGGER.info("Cleanup old %s docker", old_image) - self._stop() - try: - 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) - return True + if not self._install(tag): + return False - return False + # cleanup old stuff + if was_running: + self._run() + self._cleanup() + + return True async def logs(self): """Return docker logs of container.""" @@ -271,11 +271,13 @@ def _logs(self): Need run inside executor. """ - if not self.container: - return + try: + container = self.dock.containers.get(self.name) + except docker.errors.DockerException: + return b"" try: - return self.container.logs(tail=100, stdout=True, stderr=True) + return container.logs(tail=100, stdout=True, stderr=True) except docker.errors.DockerException as err: _LOGGER.warning("Can't grap logs from %s -> %s", self.image, err) @@ -293,15 +295,45 @@ def _restart(self): Need run inside executor. """ - if not self.container: + try: + container = self.dock.containers.get(self.name) + except docker.errors.DockerException: return False _LOGGER.info("Restart %s", self.image) try: - self.container.restart(timeout=30) + container.restart(timeout=30) except docker.errors.DockerException as err: _LOGGER.warning("Can't restart %s -> %s", self.image, err) return False return True + + async def cleanup(self): + """Check if old version exists and cleanup.""" + if self._lock.locked(): + _LOGGER.error("Can't excute cleanup while a task is in progress") + return False + + async with self._lock: + await self.loop.run_in_executor(None, self._cleanup) + + def _cleanup(self): + """Check if old version exists and cleanup. + + Need run inside executor. + """ + try: + latest = self.dock.images.get(self.image) + except docker.errors.DockerException: + _LOGGER.warning("Can't find %s for cleanup", self.image) + return + + for image in self.dock.images.list(name=self.image): + if latest.id == image.id: + continue + + with suppress(docker.errors.DockerException): + _LOGGER.info("Cleanup docker images: %s", image.tags) + self.dock.images.remove(image.id, force=True) diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 3e7a63d033d..c998cb51fec 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -24,7 +24,7 @@ def __init__(self, config, loop, dock, addons_data, addon): self.addons_data = addons_data @property - def docker_name(self): + def name(self): """Return name of docker container.""" return "addon_{}".format(self.addon) @@ -96,13 +96,13 @@ def _run(self): if self._is_running(): return - # cleanup old container + # cleanup self._stop() try: - self.container = self.dock.containers.run( + self.dock.containers.run( self.image, - name=self.docker_name, + name=self.name, detach=True, network_mode=self.addons_data.get_network_mode(self.addon), ports=self.addons_data.get_ports(self.addon), @@ -112,42 +112,14 @@ def _run(self): tmpfs=self.tmpfs ) - self.process_metadata() - _LOGGER.info("Start docker addon %s with version %s", - self.image, self.version) - except docker.errors.DockerException as err: _LOGGER.error("Can't run %s -> %s", self.image, err) return False + _LOGGER.info( + "Start docker addon %s with version %s", self.image, self.version) return True - def _attach(self): - """Attach to running docker container. - - Need run inside executor. - """ - # read container - try: - self.container = self.dock.containers.get(self.docker_name) - self.process_metadata() - - _LOGGER.info("Attach to container %s with version %s", - self.image, self.version) - return - except (docker.errors.DockerException, KeyError): - pass - - # read image - try: - image = self.dock.images.get(self.image) - self.process_metadata(metadata=image.attrs) - - _LOGGER.info("Attach to image %s with version %s", - self.image, self.version) - except (docker.errors.DockerException, KeyError): - _LOGGER.error("No container/image found for %s", self.image) - def _install(self, tag): """Pull docker image or build it. @@ -200,7 +172,7 @@ def _build(self, tag): path=str(build_dir), tag=build_tag, pull=True) image.tag(self.image, tag='latest') - self.process_metadata(metadata=image.attrs, force=True) + self.process_metadata(image.attrs, force=True) except (docker.errors.DockerException, TypeError) as err: _LOGGER.error("Can't build %s -> %s", build_tag, err) diff --git a/hassio/dock/homeassistant.py b/hassio/dock/homeassistant.py index f4c298f8b91..59c0c7cba1f 100644 --- a/hassio/dock/homeassistant.py +++ b/hassio/dock/homeassistant.py @@ -18,7 +18,7 @@ def __init__(self, config, loop, dock): super().__init__(config, loop, dock, image=config.homeassistant_image) @property - def docker_name(self): + def name(self): """Return name of docker container.""" return HASS_DOCKER_NAME @@ -30,13 +30,13 @@ def _run(self): if self._is_running(): return - # cleanup old container + # cleanup self._stop() try: - self.container = self.dock.containers.run( + self.dock.containers.run( self.image, - name=self.docker_name, + name=self.name, detach=True, privileged=True, network_mode='host', @@ -53,15 +53,12 @@ def _run(self): {'bind': '/share', 'mode': 'rw'}, }) - self.process_metadata() - - _LOGGER.info("Start docker addon %s with version %s", - self.image, self.version) - except docker.errors.DockerException as err: _LOGGER.error("Can't run %s -> %s", self.image, err) return False + _LOGGER.info( + "Start homeassistant %s with version %s", self.image, self.version) return True async def update(self, tag): @@ -71,8 +68,4 @@ async def update(self, tag): return False async with self._lock: - if await self.loop.run_in_executor(None, self._update, tag): - await self.loop.run_in_executor(None, self._run) - return True - - return False + return await self.loop.run_in_executor(None, self._update, tag) diff --git a/hassio/dock/supervisor.py b/hassio/dock/supervisor.py index da9fbaa20fc..7e9dabc7b6b 100644 --- a/hassio/dock/supervisor.py +++ b/hassio/dock/supervisor.py @@ -2,8 +2,6 @@ import logging import os -import docker - from . import DockerBase from ..const import RESTART_EXIT_CODE @@ -13,14 +11,13 @@ class DockerSupervisor(DockerBase): """Docker hassio wrapper for HomeAssistant.""" - def __init__(self, config, loop, dock, hassio, image=None): + def __init__(self, config, loop, dock, stop_callback, image=None): """Initialize docker base wrapper.""" super().__init__(config, loop, dock, image=image) - - self.hassio = hassio + self.stop_callback = stop_callback @property - def docker_name(self): + def name(self): """Return name of docker container.""" return os.environ['SUPERVISOR_NAME'] @@ -31,41 +28,14 @@ async def update(self, tag): return False _LOGGER.info("Update supervisor docker to %s:%s", self.image, tag) - old_version = self.version async with self._lock: if await self.loop.run_in_executor(None, self._install, tag): - self.config.hassio_cleanup = old_version - self.loop.create_task(self.hassio.stop(RESTART_EXIT_CODE)) + self.loop.create_task(self.stop_callback(RESTART_EXIT_CODE)) return True return False - async def cleanup(self): - """Check if old supervisor version exists and cleanup.""" - if not self.config.hassio_cleanup: - return - - async with self._lock: - if await self.loop.run_in_executor(None, self._cleanup): - self.config.hassio_cleanup = None - - def _cleanup(self): - """Remove old image. - - Need run inside executor. - """ - old_image = "{}:{}".format(self.image, self.config.hassio_cleanup) - - _LOGGER.info("Old supervisor docker found %s", old_image) - try: - 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) - return False - - return True - async def run(self): """Run docker image.""" raise RuntimeError("Not support on supervisor docker container!") diff --git a/hassio/tools.py b/hassio/tools.py index 01f15fad84a..0879928fca9 100644 --- a/hassio/tools.py +++ b/hassio/tools.py @@ -46,17 +46,6 @@ def get_arch_from_image(image): return found.group(1) -def get_version_from_env(env_list): - """Extract Version from ENV list.""" - for env in env_list: - found = _RE_VERSION.match(env) - if found: - return found.group(1) - - _LOGGER.error("Can't find VERSION in env") - return None - - def get_local_ip(loop): """Retrieve local IP address. From 8287330c6731bd0d28a6da4098b26de6480cd531 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 31 May 2017 23:44:05 +0200 Subject: [PATCH 4/5] cleanup --- hassio/dock/homeassistant.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/hassio/dock/homeassistant.py b/hassio/dock/homeassistant.py index 59c0c7cba1f..6ce9f46027e 100644 --- a/hassio/dock/homeassistant.py +++ b/hassio/dock/homeassistant.py @@ -60,12 +60,3 @@ def _run(self): _LOGGER.info( "Start homeassistant %s with version %s", self.image, self.version) return True - - async def update(self, tag): - """Update homeassistant docker image.""" - if self._lock.locked(): - _LOGGER.error("Can't excute update while a task is in progress") - return False - - async with self._lock: - return await self.loop.run_in_executor(None, self._update, tag) From e35b0a54c10a92611d5df3971a5943f8ca482601 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 31 May 2017 23:53:22 +0200 Subject: [PATCH 5/5] Pump version to 0.34 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 20504a2a9b2..6a9da94a901 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "hassio": "0.33", + "hassio": "0.34", "homeassistant": "0.45.1", "resinos": "0.8", "resinhup": "0.1",