Compare commits

..

20 Commits
0.52 ... 0.55

Author SHA1 Message Date
Pascal Vizeli
a3209c4bde Merge remote-tracking branch 'origin/dev' 2017-08-16 02:17:57 +02:00
Pascal Vizeli
f3e60f6c28 Update hass.io to 0.55 2017-08-16 02:13:54 +02:00
Pascal Vizeli
7798e7cde2 Code cleanup & add config check to API (#155)
* Code cleanup & add config check to API

* Fix comments

* fix lint p1

* Fix lint p2

* fix coro

* fix parameter

* add log output

* fix command layout

* fix Pannel

* fix lint p4

* fix regex

* convert to ascii

* fix lint p5

* add temp logging

* fix output on non-zero exit

* remove temporary log
2017-08-16 02:10:38 +02:00
Pascal Vizeli
4af92b9d25 Don't return false for addon startup on update (#153) 2017-08-15 21:30:05 +02:00
Pascal Vizeli
eab958860c Pump hassio to 0.55 2017-08-15 09:15:21 +02:00
pvizeli
09bba96940 Merge remote-tracking branch 'origin/dev' 2017-08-15 09:07:47 +02:00
Pascal Vizeli
a34806d4e2 Update hass.io to 0.54 2017-08-15 08:56:40 +02:00
Pascal Vizeli
f00b21dc28 Bugfix git clone (#152)
Add an optional extended description…
2017-08-15 08:55:55 +02:00
Pascal Vizeli
021946e181 Pump version to 0.54 2017-08-15 00:35:58 +02:00
Pascal Vizeli
6cab017042 Fix version conflict 2017-08-15 00:27:47 +02:00
Pascal Vizeli
5999b48be4 Update Hass.io 0.53 2017-08-15 00:05:55 +02:00
Pascal Vizeli
57f3178408 Change update flow to a higher level (#150) 2017-08-14 23:56:52 +02:00
Franck Nijhof
14013ac923 Recursively git clone addon repositories, allowing submodules (#148) 2017-08-14 16:15:08 +02:00
Pascal Vizeli
d08343d040 Update HomeAssistant 0.51.2 2017-08-14 13:15:45 +02:00
Pascal Vizeli
2f9f9c6165 Update HomeAssistant 0.51.2 2017-08-14 13:15:15 +02:00
Pascal Vizeli
8ab0ed5047 Update HomeAssistant 0.51.1 2017-08-13 14:59:50 +02:00
Pascal Vizeli
0119b52e11 Update HomeAssistant 0.51.1 2017-08-13 14:59:33 +02:00
Pascal Vizeli
1382a7b36e Update to HomeAssistant 0.51 2017-08-13 09:24:05 +02:00
Pascal Vizeli
2eeb8bf388 Update to HomeAssistant 0.51 2017-08-13 09:08:41 +02:00
Pascal Vizeli
5af3040223 Pump version to 0.53 2017-08-10 11:25:07 +02:00
18 changed files with 206 additions and 174 deletions

1
API.md
View File

@@ -303,6 +303,7 @@ Output is the raw Docker log.
- POST `/homeassistant/restart`
- POST `/homeassistant/options`
- POST `/homeassistant/check`
```json
{

View File

@@ -41,12 +41,12 @@ class Addon(object):
self.data = data
self._id = slug
self.addon_docker = DockerAddon(config, loop, dock, self)
self.docker = DockerAddon(config, loop, dock, self)
async def load(self):
"""Async initialize of object."""
if self.is_installed:
await self.addon_docker.attach()
await self.docker.attach()
@property
def slug(self):
@@ -434,7 +434,7 @@ class Addon(object):
self.path_data.mkdir()
version = version or self.last_version
if not await self.addon_docker.install(version):
if not await self.docker.install(version):
return False
self._set_install(version)
@@ -443,7 +443,7 @@ class Addon(object):
@check_installed
async def uninstall(self):
"""Remove a addon."""
if not await self.addon_docker.remove():
if not await self.docker.remove():
return False
if self.path_data.is_dir():
@@ -459,7 +459,7 @@ class Addon(object):
if not self.is_installed:
return STATE_NONE
if await self.addon_docker.is_running():
if await self.docker.is_running():
return STATE_STARTED
return STATE_STOPPED
@@ -469,7 +469,7 @@ class Addon(object):
Return a coroutine.
"""
return self.addon_docker.run()
return self.docker.run()
@check_installed
def stop(self):
@@ -477,22 +477,26 @@ class Addon(object):
Return a coroutine.
"""
return self.addon_docker.stop()
return self.docker.stop()
@check_installed
async def update(self, version=None):
"""Update addon."""
version = version or self.last_version
last_state = await self.state()
if version == self.version_installed:
_LOGGER.warning(
"Addon %s is already installed in %s", self._id, version)
return False
if not await self.addon_docker.update(version):
if not await self.docker.update(version):
return False
self._set_update(version)
# restore state
if last_state == STATE_STARTED:
await self.docker.run()
return True
@check_installed
@@ -501,7 +505,7 @@ class Addon(object):
Return a coroutine.
"""
return self.addon_docker.restart()
return self.docker.restart()
@check_installed
def logs(self):
@@ -509,7 +513,7 @@ class Addon(object):
Return a coroutine.
"""
return self.addon_docker.logs()
return self.docker.logs()
@check_installed
async def snapshot(self, tar_file):
@@ -517,7 +521,7 @@ class Addon(object):
with TemporaryDirectory(dir=str(self.config.path_tmp)) as temp:
# store local image
if self.need_build and not await \
self.addon_docker.export_image(Path(temp, "image.tar")):
self.docker.export_image(Path(temp, "image.tar")):
return False
data = {
@@ -582,15 +586,15 @@ class Addon(object):
# check version / restore image
version = data[ATTR_VERSION]
if version != self.addon_docker.version:
if version != self.docker.version:
image_file = Path(temp, "image.tar")
if image_file.is_file():
await self.addon_docker.import_image(image_file, version)
await self.docker.import_image(image_file, version)
else:
if await self.addon_docker.install(version):
await self.addon_docker.cleanup()
if await self.docker.install(version):
await self.docker.cleanup()
else:
await self.addon_docker.stop()
await self.docker.stop()
# restore data
def _restore_data():

View File

@@ -1,6 +1,7 @@
"""Init file for HassIO addons git."""
import asyncio
import logging
import functools as ft
from pathlib import Path
import shutil
@@ -48,7 +49,9 @@ class GitRepo(object):
try:
_LOGGER.info("Clone addon %s repository", self.url)
self.repo = await self.loop.run_in_executor(
None, git.Repo.clone_from, self.url, str(self.path))
None, ft.partial(
git.Repo.clone_from, self.url, str(self.path),
recursive=True))
except (git.InvalidGitRepositoryError, git.NoSuchPathError,
git.GitCommandError) as err:

View File

@@ -68,10 +68,11 @@ class RestAPI(object):
api_hass = APIHomeAssistant(self.config, self.loop, dock_homeassistant)
self.webapp.router.add_get('/homeassistant/info', api_hass.info)
self.webapp.router.add_get('/homeassistant/logs', api_hass.logs)
self.webapp.router.add_post('/homeassistant/options', api_hass.options)
self.webapp.router.add_post('/homeassistant/update', api_hass.update)
self.webapp.router.add_post('/homeassistant/restart', api_hass.restart)
self.webapp.router.add_get('/homeassistant/logs', api_hass.logs)
self.webapp.router.add_post('/homeassistant/check', api_hass.check)
def register_addons(self, addons):
"""Register homeassistant function."""

View File

@@ -170,19 +170,13 @@ class APIAddons(object):
@api_process
def uninstall(self, request):
"""Uninstall addon.
Return a coroutine.
"""
"""Uninstall addon."""
addon = self._extract_addon(request)
return asyncio.shield(addon.uninstall(), loop=self.loop)
@api_process
def start(self, request):
"""Start addon.
Return a coroutine.
"""
"""Start addon."""
addon = self._extract_addon(request)
# check options
@@ -196,10 +190,7 @@ class APIAddons(object):
@api_process
def stop(self, request):
"""Stop addon.
Return a coroutine.
"""
"""Stop addon."""
addon = self._extract_addon(request)
return asyncio.shield(addon.stop(), loop=self.loop)
@@ -218,19 +209,13 @@ class APIAddons(object):
@api_process
def restart(self, request):
"""Restart addon.
Return a coroutine.
"""
"""Restart addon."""
addon = self._extract_addon(request)
return asyncio.shield(addon.restart(), loop=self.loop)
@api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request):
"""Return logs from addon.
Return a coroutine.
"""
"""Return logs from addon."""
addon = self._extract_addon(request)
return addon.logs()

View File

@@ -73,16 +73,19 @@ class APIHomeAssistant(object):
@api_process
def restart(self, request):
"""Restart homeassistant.
Return a coroutine.
"""
"""Restart homeassistant."""
return asyncio.shield(self.homeassistant.restart(), loop=self.loop)
@api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request):
"""Return homeassistant docker logs.
Return a coroutine.
"""
"""Return homeassistant docker logs."""
return self.homeassistant.logs()
@api_process
async def check(self, request):
"""Check config of homeassistant."""
code, message = await self.homeassistant.check_config()
if not code:
raise RuntimeError(message)
return True

View File

@@ -59,18 +59,12 @@ class APIHost(object):
@api_process_hostcontrol
def reboot(self, request):
"""Reboot host.
Return a coroutine.
"""
"""Reboot host."""
return self.host_control.reboot()
@api_process_hostcontrol
def shutdown(self, request):
"""Poweroff host.
Return a coroutine.
"""
"""Poweroff host."""
return self.host_control.shutdown()
@api_process_hostcontrol

View File

@@ -112,10 +112,7 @@ class APISnapshots(object):
@api_process
def restore_full(self, request):
"""Full-Restore a snapshot.
Return a coroutine.
"""
"""Full-Restore a snapshot."""
snapshot = self._extract_snapshot(request)
return asyncio.shield(
self.snapshots.do_restore_full(snapshot), loop=self.loop)

View File

@@ -121,8 +121,5 @@ class APISupervisor(object):
@api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request):
"""Return supervisor docker logs.
Return a coroutine.
"""
"""Return supervisor docker logs."""
return self.supervisor.logs()

View File

@@ -1,7 +1,7 @@
"""Const file for HassIO."""
from pathlib import Path
HASSIO_VERSION = '0.52'
HASSIO_VERSION = '0.55'
URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/'
'hassio/{}/version.json')

View File

@@ -5,6 +5,7 @@ import logging
import docker
from .util import docker_process
from ..const import LABEL_VERSION, LABEL_ARCH
_LOGGER = logging.getLogger(__name__)
@@ -52,14 +53,10 @@ class DockerBase(object):
if need_arch and LABEL_ARCH in metadata['Config']['Labels']:
self.arch = metadata['Config']['Labels'][LABEL_ARCH]
async def install(self, tag):
@docker_process
def install(self, tag):
"""Pull docker image."""
if self._lock.locked():
_LOGGER.error("Can't excute install while a task is in progress")
return False
async with self._lock:
return await self.loop.run_in_executor(None, self._install, tag)
return self.loop.run_in_executor(None, self._install, tag)
def _install(self, tag):
"""Pull docker image.
@@ -80,10 +77,7 @@ class DockerBase(object):
return True
def exists(self):
"""Return True if docker image exists in local repo.
Return a Future.
"""
"""Return True if docker image exists in local repo."""
return self.loop.run_in_executor(None, self._exists)
def _exists(self):
@@ -126,14 +120,10 @@ class DockerBase(object):
return True
async def attach(self):
@docker_process
def attach(self):
"""Attach to running docker container."""
if self._lock.locked():
_LOGGER.error("Can't excute attach while a task is in progress")
return False
async with self._lock:
return await self.loop.run_in_executor(None, self._attach)
return self.loop.run_in_executor(None, self._attach)
def _attach(self):
"""Attach to running docker container.
@@ -154,14 +144,10 @@ class DockerBase(object):
return True
async def run(self):
@docker_process
def run(self):
"""Run docker image."""
if self._lock.locked():
_LOGGER.error("Can't excute run while a task is in progress")
return False
async with self._lock:
return await self.loop.run_in_executor(None, self._run)
return self.loop.run_in_executor(None, self._run)
def _run(self):
"""Run docker image.
@@ -170,15 +156,10 @@ class DockerBase(object):
"""
raise NotImplementedError()
async def stop(self):
@docker_process
def stop(self):
"""Stop/remove docker container."""
if self._lock.locked():
_LOGGER.error("Can't excute stop while a task is in progress")
return False
async with self._lock:
await self.loop.run_in_executor(None, self._stop)
return True
return self.loop.run_in_executor(None, self._stop)
def _stop(self):
"""Stop/remove and remove docker container.
@@ -188,7 +169,7 @@ class DockerBase(object):
try:
container = self.dock.containers.get(self.name)
except docker.errors.DockerException:
return
return False
if container.status == 'running':
_LOGGER.info("Stop %s docker application", self.image)
@@ -199,14 +180,12 @@ class DockerBase(object):
_LOGGER.info("Clean %s docker application", self.image)
container.remove(force=True)
async def remove(self):
"""Remove docker images."""
if self._lock.locked():
_LOGGER.error("Can't excute remove while a task is in progress")
return False
return True
async with self._lock:
return await self.loop.run_in_executor(None, self._remove)
@docker_process
def remove(self):
"""Remove docker images."""
return self.loop.run_in_executor(None, self._remove)
def _remove(self):
"""remove docker images.
@@ -235,24 +214,19 @@ class DockerBase(object):
# clean metadata
self.version = None
self.arch = None
return True
async def update(self, tag):
@docker_process
def update(self, tag):
"""Update a 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)
return self.loop.run_in_executor(None, self._update, tag)
def _update(self, tag):
"""Update a docker image.
Need run inside executor.
"""
was_running = self._is_running()
_LOGGER.info(
"Update docker %s with %s:%s", self.version, self.image, tag)
@@ -260,25 +234,16 @@ class DockerBase(object):
if not self._install(tag):
return False
# run or cleanup container
if was_running:
self._run()
else:
self._stop()
# cleanup images
# stop container & cleanup
self._stop()
self._cleanup()
return True
async def logs(self):
@docker_process
def logs(self):
"""Return docker logs of container."""
if self._lock.locked():
_LOGGER.error("Can't excute logs while a task is in progress")
return b""
async with self._lock:
return await self.loop.run_in_executor(None, self._logs)
return self.loop.run_in_executor(None, self._logs)
def _logs(self):
"""Return docker logs of container.
@@ -295,14 +260,10 @@ class DockerBase(object):
except docker.errors.DockerException as err:
_LOGGER.warning("Can't grap logs from %s -> %s", self.image, err)
async def restart(self):
@docker_process
def restart(self):
"""Restart docker container."""
if self._lock.locked():
_LOGGER.error("Can't excute restart while a task is in progress")
return False
async with self._lock:
return await self.loop.run_in_executor(None, self._restart)
return self.loop.run_in_executor(None, self._restart)
def _restart(self):
"""Restart docker container.
@@ -324,14 +285,10 @@ class DockerBase(object):
return True
async def cleanup(self):
@docker_process
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)
return self.loop.run_in_executor(None, self._cleanup)
def _cleanup(self):
"""Check if old version exists and cleanup.
@@ -342,7 +299,7 @@ class DockerBase(object):
latest = self.dock.images.get(self.image)
except docker.errors.DockerException:
_LOGGER.warning("Can't find %s for cleanup", self.image)
return
return False
for image in self.dock.images.list(name=self.image):
if latest.id == image.id:
@@ -351,3 +308,17 @@ class DockerBase(object):
with suppress(docker.errors.DockerException):
_LOGGER.info("Cleanup docker images: %s", image.tags)
self.dock.images.remove(image.id, force=True)
return True
@docker_process
def execute_command(self, command):
"""Create a temporary container and run command."""
return self.loop.run_in_executor(None, self._execute_command, command)
def _execute_command(self, command):
"""Create a temporary container and run command.
Need run inside executor.
"""
raise NotImplementedError()

View File

@@ -7,7 +7,7 @@ import docker
import requests
from . import DockerBase
from .util import dockerfile_template
from .util import dockerfile_template, docker_process
from ..const import (
META_ADDON, MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE)
@@ -218,15 +218,10 @@ class DockerAddon(DockerBase):
finally:
shutil.rmtree(str(build_dir), ignore_errors=True)
async def export_image(self, path):
@docker_process
def export_image(self, path):
"""Export current images into a tar file."""
if self._lock.locked():
_LOGGER.error("Can't excute export while a task is in progress")
return False
async with self._lock:
return await self.loop.run_in_executor(
None, self._export_image, path)
return self.loop.run_in_executor(None, self._export_image, path)
def _export_image(self, tar_file):
"""Export current images into a tar file.
@@ -250,15 +245,10 @@ class DockerAddon(DockerBase):
_LOGGER.info("Export image %s to %s", self.image, tar_file)
return True
async def import_image(self, path, tag):
@docker_process
def import_image(self, path, tag):
"""Import a tar file as image."""
if self._lock.locked():
_LOGGER.error("Can't excute import while a task is in progress")
return False
async with self._lock:
return await self.loop.run_in_executor(
None, self._import_image, path, tag)
return self.loop.run_in_executor(None, self._import_image, path, tag)
def _import_image(self, tar_file, tag):
"""Import a tar file as image.

View File

@@ -1,4 +1,5 @@
"""Init file for HassIO docker object."""
from contextlib import suppress
import logging
import docker
@@ -66,7 +67,8 @@ class DockerHomeAssistant(DockerBase):
{'bind': '/ssl', 'mode': 'ro'},
str(self.config.path_extern_share):
{'bind': '/share', 'mode': 'rw'},
})
}
)
except docker.errors.DockerException as err:
_LOGGER.error("Can't run %s -> %s", self.image, err)
@@ -75,3 +77,41 @@ class DockerHomeAssistant(DockerBase):
_LOGGER.info(
"Start homeassistant %s with version %s", self.image, self.version)
return True
def _execute_command(self, command):
"""Create a temporary container and run command.
Need run inside executor.
"""
_LOGGER.info("Run command '%s' on %s", command, self.image)
try:
container = self.dock.containers.run(
self.image,
command=command,
detach=True,
stdout=True,
stderr=True,
environment={
'TZ': self.config.timezone,
},
volumes={
str(self.config.path_extern_config):
{'bind': '/config', 'mode': 'ro'},
str(self.config.path_extern_ssl):
{'bind': '/ssl', 'mode': 'ro'},
}
)
# wait until command is done
container.wait()
output = container.logs()
except docker.errors.DockerException as err:
_LOGGER.error("Can't execute command -> %s", err)
return b""
# cleanup container
with suppress(docker.errors.DockerException):
container.remove(force=True)
return output

View File

@@ -3,6 +3,7 @@ import logging
import os
from . import DockerBase
from .util import docker_process
from ..const import RESTART_EXIT_CODE
_LOGGER = logging.getLogger(__name__)
@@ -21,20 +22,16 @@ class DockerSupervisor(DockerBase):
"""Return name of docker container."""
return os.environ['SUPERVISOR_NAME']
@docker_process
async def update(self, tag):
"""Update a supervisor docker image."""
if self._lock.locked():
_LOGGER.error("Can't excute update while a task is in progress")
return False
_LOGGER.info("Update supervisor docker to %s:%s", self.image, tag)
async with self._lock:
if await self.loop.run_in_executor(None, self._install, tag):
self.loop.create_task(self.stop_callback(RESTART_EXIT_CODE))
return True
if await self.loop.run_in_executor(None, self._install, tag):
self.loop.create_task(self.stop_callback(RESTART_EXIT_CODE))
return True
return False
return False
async def run(self):
"""Run docker image."""

View File

@@ -1,8 +1,10 @@
"""HassIO docker utilitys."""
import logging
import re
from ..const import ARCH_AARCH64, ARCH_ARMHF, ARCH_I386, ARCH_AMD64
_LOGGER = logging.getLogger(__name__)
HASSIO_BASE_IMAGE = {
ARCH_ARMHF: "homeassistant/armhf-base:latest",
@@ -40,3 +42,19 @@ def create_metadata(version, arch, meta_type):
return ('LABEL io.hass.version="{}" '
'io.hass.arch="{}" '
'io.hass.type="{}"').format(version, arch, meta_type)
# pylint: disable=protected-access
def docker_process(method):
"""Wrap function with only run once."""
async def wrap_api(api, *args, **kwargs):
"""Return api wrapper."""
if api._lock.locked():
_LOGGER.error(
"Can't excute %s while a task is in progress", method.__name__)
return False
async with api._lock:
return await method(api, *args, **kwargs)
return wrap_api

View File

@@ -2,16 +2,19 @@
import asyncio
import logging
import os
import re
from .const import (
FILE_HASSIO_HOMEASSISTANT, ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION,
ATTR_VERSION)
from .dock.homeassistant import DockerHomeAssistant
from .tools import JsonConfig
from .tools import JsonConfig, convert_to_ascii
from .validate import SCHEMA_HASS_CONFIG
_LOGGER = logging.getLogger(__name__)
RE_CONFIG_CHECK = re.compile(r"error", re.IGNORECASE)
class HomeAssistant(JsonConfig):
"""Hass core object for handle it."""
@@ -121,7 +124,10 @@ class HomeAssistant(JsonConfig):
_LOGGER.warning("Version %s is already installed", version)
return False
return await self.docker.update(version)
try:
return await self.docker.update(version)
finally:
await self.docker.run()
def run(self):
"""Run HomeAssistant docker.
@@ -162,3 +168,20 @@ class HomeAssistant(JsonConfig):
def in_progress(self):
"""Return True if a task is in progress."""
return self.docker.in_progress
async def check_config(self):
"""Run homeassistant config check."""
log = await self.docker.execute_command(
"python3 -m homeassistant -c /config --script check_config"
)
# if not valid
if not log:
return (False, "")
# parse output
log = convert_to_ascii(log)
if RE_CONFIG_CHECK.search(log):
return (False, log)
return (True, log)

View File

@@ -5,6 +5,7 @@ from datetime import datetime
import json
import logging
import socket
import re
import aiohttp
import async_timeout
@@ -15,6 +16,8 @@ _LOGGER = logging.getLogger(__name__)
FREEGEOIP_URL = "https://freegeoip.io/json/"
RE_STRING = re.compile(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))")
def get_local_ip(loop):
"""Retrieve local IP address.
@@ -68,6 +71,11 @@ async def fetch_timezone(websession):
return data.get('time_zone', 'UTC')
def convert_to_ascii(raw):
"""Convert binary to ascii and remove colors."""
return RE_STRING.sub("", raw.decode())
class JsonConfig(object):
"""Hass core object for handle it."""

View File

@@ -1,6 +1,6 @@
{
"hassio": "0.52",
"homeassistant": "0.50.2",
"hassio": "0.55",
"homeassistant": "0.51.2",
"resinos": "1.0",
"resinhup": "0.3",
"generic": "0.3",