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
This commit is contained in:
Pascal Vizeli 2017-08-16 02:10:38 +02:00 committed by GitHub
parent 4af92b9d25
commit 7798e7cde2
14 changed files with 173 additions and 146 deletions

1
API.md
View File

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

View File

@ -68,10 +68,11 @@ class RestAPI(object):
api_hass = APIHomeAssistant(self.config, self.loop, dock_homeassistant) 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/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/options', api_hass.options)
self.webapp.router.add_post('/homeassistant/update', api_hass.update) self.webapp.router.add_post('/homeassistant/update', api_hass.update)
self.webapp.router.add_post('/homeassistant/restart', api_hass.restart) 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): def register_addons(self, addons):
"""Register homeassistant function.""" """Register homeassistant function."""

View File

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

View File

@ -73,16 +73,19 @@ class APIHomeAssistant(object):
@api_process @api_process
def restart(self, request): def restart(self, request):
"""Restart homeassistant. """Restart homeassistant."""
Return a coroutine.
"""
return asyncio.shield(self.homeassistant.restart(), loop=self.loop) return asyncio.shield(self.homeassistant.restart(), loop=self.loop)
@api_process_raw(CONTENT_TYPE_BINARY) @api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request): def logs(self, request):
"""Return homeassistant docker logs. """Return homeassistant docker logs."""
Return a coroutine.
"""
return self.homeassistant.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 @api_process_hostcontrol
def reboot(self, request): def reboot(self, request):
"""Reboot host. """Reboot host."""
Return a coroutine.
"""
return self.host_control.reboot() return self.host_control.reboot()
@api_process_hostcontrol @api_process_hostcontrol
def shutdown(self, request): def shutdown(self, request):
"""Poweroff host. """Poweroff host."""
Return a coroutine.
"""
return self.host_control.shutdown() return self.host_control.shutdown()
@api_process_hostcontrol @api_process_hostcontrol

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import logging
import docker import docker
from .util import docker_process
from ..const import LABEL_VERSION, LABEL_ARCH from ..const import LABEL_VERSION, LABEL_ARCH
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -52,14 +53,10 @@ class DockerBase(object):
if need_arch and LABEL_ARCH in metadata['Config']['Labels']: if need_arch and LABEL_ARCH in metadata['Config']['Labels']:
self.arch = metadata['Config']['Labels'][LABEL_ARCH] self.arch = metadata['Config']['Labels'][LABEL_ARCH]
async def install(self, tag): @docker_process
def install(self, tag):
"""Pull docker image.""" """Pull docker image."""
if self._lock.locked(): return self.loop.run_in_executor(None, self._install, tag)
_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)
def _install(self, tag): def _install(self, tag):
"""Pull docker image. """Pull docker image.
@ -80,10 +77,7 @@ class DockerBase(object):
return True return True
def exists(self): def exists(self):
"""Return True if docker image exists in local repo. """Return True if docker image exists in local repo."""
Return a Future.
"""
return self.loop.run_in_executor(None, self._exists) return self.loop.run_in_executor(None, self._exists)
def _exists(self): def _exists(self):
@ -126,14 +120,10 @@ class DockerBase(object):
return True return True
async def attach(self): @docker_process
def attach(self):
"""Attach to running docker container.""" """Attach to running docker container."""
if self._lock.locked(): return self.loop.run_in_executor(None, self._attach)
_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)
def _attach(self): def _attach(self):
"""Attach to running docker container. """Attach to running docker container.
@ -154,14 +144,10 @@ class DockerBase(object):
return True return True
async def run(self): @docker_process
def run(self):
"""Run docker image.""" """Run docker image."""
if self._lock.locked(): return self.loop.run_in_executor(None, self._run)
_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)
def _run(self): def _run(self):
"""Run docker image. """Run docker image.
@ -170,15 +156,10 @@ class DockerBase(object):
""" """
raise NotImplementedError() raise NotImplementedError()
async def stop(self): @docker_process
def stop(self):
"""Stop/remove docker container.""" """Stop/remove docker container."""
if self._lock.locked(): return self.loop.run_in_executor(None, self._stop)
_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
def _stop(self): def _stop(self):
"""Stop/remove and remove docker container. """Stop/remove and remove docker container.
@ -188,7 +169,7 @@ class DockerBase(object):
try: try:
container = self.dock.containers.get(self.name) container = self.dock.containers.get(self.name)
except docker.errors.DockerException: except docker.errors.DockerException:
return return False
if container.status == 'running': if container.status == 'running':
_LOGGER.info("Stop %s docker application", self.image) _LOGGER.info("Stop %s docker application", self.image)
@ -199,14 +180,12 @@ class DockerBase(object):
_LOGGER.info("Clean %s docker application", self.image) _LOGGER.info("Clean %s docker application", self.image)
container.remove(force=True) container.remove(force=True)
async def remove(self): return True
"""Remove docker images."""
if self._lock.locked():
_LOGGER.error("Can't excute remove while a task is in progress")
return False
async with self._lock: @docker_process
return await self.loop.run_in_executor(None, self._remove) def remove(self):
"""Remove docker images."""
return self.loop.run_in_executor(None, self._remove)
def _remove(self): def _remove(self):
"""remove docker images. """remove docker images.
@ -235,16 +214,13 @@ class DockerBase(object):
# clean metadata # clean metadata
self.version = None self.version = None
self.arch = None self.arch = None
return True return True
async def update(self, tag): @docker_process
def update(self, tag):
"""Update a docker image.""" """Update a docker image."""
if self._lock.locked(): return self.loop.run_in_executor(None, self._update, tag)
_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)
def _update(self, tag): def _update(self, tag):
"""Update a docker image. """Update a docker image.
@ -258,22 +234,16 @@ class DockerBase(object):
if not self._install(tag): if not self._install(tag):
return False return False
# container # stop container & cleanup
self._stop() self._stop()
# cleanup images
self._cleanup() self._cleanup()
return True return True
async def logs(self): @docker_process
def logs(self):
"""Return docker logs of container.""" """Return docker logs of container."""
if self._lock.locked(): return self.loop.run_in_executor(None, self._logs)
_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)
def _logs(self): def _logs(self):
"""Return docker logs of container. """Return docker logs of container.
@ -290,14 +260,10 @@ class DockerBase(object):
except docker.errors.DockerException as err: except docker.errors.DockerException as err:
_LOGGER.warning("Can't grap logs from %s -> %s", self.image, 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.""" """Restart docker container."""
if self._lock.locked(): return self.loop.run_in_executor(None, self._restart)
_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)
def _restart(self): def _restart(self):
"""Restart docker container. """Restart docker container.
@ -319,14 +285,10 @@ class DockerBase(object):
return True return True
async def cleanup(self): @docker_process
def cleanup(self):
"""Check if old version exists and cleanup.""" """Check if old version exists and cleanup."""
if self._lock.locked(): return self.loop.run_in_executor(None, self._cleanup)
_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): def _cleanup(self):
"""Check if old version exists and cleanup. """Check if old version exists and cleanup.
@ -337,7 +299,7 @@ class DockerBase(object):
latest = self.dock.images.get(self.image) latest = self.dock.images.get(self.image)
except docker.errors.DockerException: except docker.errors.DockerException:
_LOGGER.warning("Can't find %s for cleanup", self.image) _LOGGER.warning("Can't find %s for cleanup", self.image)
return return False
for image in self.dock.images.list(name=self.image): for image in self.dock.images.list(name=self.image):
if latest.id == image.id: if latest.id == image.id:
@ -346,3 +308,17 @@ class DockerBase(object):
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException):
_LOGGER.info("Cleanup docker images: %s", image.tags) _LOGGER.info("Cleanup docker images: %s", image.tags)
self.dock.images.remove(image.id, force=True) 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 import requests
from . import DockerBase from . import DockerBase
from .util import dockerfile_template from .util import dockerfile_template, docker_process
from ..const import ( from ..const import (
META_ADDON, MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE) META_ADDON, MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE)
@ -218,15 +218,10 @@ class DockerAddon(DockerBase):
finally: finally:
shutil.rmtree(str(build_dir), ignore_errors=True) 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.""" """Export current images into a tar file."""
if self._lock.locked(): return self.loop.run_in_executor(None, self._export_image, path)
_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)
def _export_image(self, tar_file): def _export_image(self, tar_file):
"""Export current images into a 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) _LOGGER.info("Export image %s to %s", self.image, tar_file)
return True return True
async def import_image(self, path, tag): @docker_process
def import_image(self, path, tag):
"""Import a tar file as image.""" """Import a tar file as image."""
if self._lock.locked(): return self.loop.run_in_executor(None, self._import_image, path, tag)
_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)
def _import_image(self, tar_file, tag): def _import_image(self, tar_file, tag):
"""Import a tar file as image. """Import a tar file as image.

View File

@ -1,4 +1,5 @@
"""Init file for HassIO docker object.""" """Init file for HassIO docker object."""
from contextlib import suppress
import logging import logging
import docker import docker
@ -66,7 +67,8 @@ class DockerHomeAssistant(DockerBase):
{'bind': '/ssl', 'mode': 'ro'}, {'bind': '/ssl', 'mode': 'ro'},
str(self.config.path_extern_share): str(self.config.path_extern_share):
{'bind': '/share', 'mode': 'rw'}, {'bind': '/share', 'mode': 'rw'},
}) }
)
except docker.errors.DockerException as err: 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)
@ -75,3 +77,41 @@ class DockerHomeAssistant(DockerBase):
_LOGGER.info( _LOGGER.info(
"Start homeassistant %s with version %s", self.image, self.version) "Start homeassistant %s with version %s", self.image, self.version)
return True 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 import os
from . import DockerBase from . import DockerBase
from .util import docker_process
from ..const import RESTART_EXIT_CODE from ..const import RESTART_EXIT_CODE
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -21,20 +22,16 @@ class DockerSupervisor(DockerBase):
"""Return name of docker container.""" """Return name of docker container."""
return os.environ['SUPERVISOR_NAME'] return os.environ['SUPERVISOR_NAME']
@docker_process
async def update(self, tag): async def update(self, tag):
"""Update a supervisor docker image.""" """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) _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):
if await self.loop.run_in_executor(None, self._install, tag): self.loop.create_task(self.stop_callback(RESTART_EXIT_CODE))
self.loop.create_task(self.stop_callback(RESTART_EXIT_CODE)) return True
return True
return False return False
async def run(self): async def run(self):
"""Run docker image.""" """Run docker image."""

View File

@ -1,8 +1,10 @@
"""HassIO docker utilitys.""" """HassIO docker utilitys."""
import logging
import re import re
from ..const import ARCH_AARCH64, ARCH_ARMHF, ARCH_I386, ARCH_AMD64 from ..const import ARCH_AARCH64, ARCH_ARMHF, ARCH_I386, ARCH_AMD64
_LOGGER = logging.getLogger(__name__)
HASSIO_BASE_IMAGE = { HASSIO_BASE_IMAGE = {
ARCH_ARMHF: "homeassistant/armhf-base:latest", ARCH_ARMHF: "homeassistant/armhf-base:latest",
@ -40,3 +42,19 @@ def create_metadata(version, arch, meta_type):
return ('LABEL io.hass.version="{}" ' return ('LABEL io.hass.version="{}" '
'io.hass.arch="{}" ' 'io.hass.arch="{}" '
'io.hass.type="{}"').format(version, arch, meta_type) '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 asyncio
import logging import logging
import os import os
import re
from .const import ( from .const import (
FILE_HASSIO_HOMEASSISTANT, ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION, FILE_HASSIO_HOMEASSISTANT, ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION,
ATTR_VERSION) ATTR_VERSION)
from .dock.homeassistant import DockerHomeAssistant from .dock.homeassistant import DockerHomeAssistant
from .tools import JsonConfig from .tools import JsonConfig, convert_to_ascii
from .validate import SCHEMA_HASS_CONFIG from .validate import SCHEMA_HASS_CONFIG
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
RE_CONFIG_CHECK = re.compile(r"error", re.IGNORECASE)
class HomeAssistant(JsonConfig): class HomeAssistant(JsonConfig):
"""Hass core object for handle it.""" """Hass core object for handle it."""
@ -165,3 +168,20 @@ class HomeAssistant(JsonConfig):
def in_progress(self): def in_progress(self):
"""Return True if a task is in progress.""" """Return True if a task is in progress."""
return self.docker.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 json
import logging import logging
import socket import socket
import re
import aiohttp import aiohttp
import async_timeout import async_timeout
@ -15,6 +16,8 @@ _LOGGER = logging.getLogger(__name__)
FREEGEOIP_URL = "https://freegeoip.io/json/" FREEGEOIP_URL = "https://freegeoip.io/json/"
RE_STRING = re.compile(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))")
def get_local_ip(loop): def get_local_ip(loop):
"""Retrieve local IP address. """Retrieve local IP address.
@ -68,6 +71,11 @@ async def fetch_timezone(websession):
return data.get('time_zone', 'UTC') 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): class JsonConfig(object):
"""Hass core object for handle it.""" """Hass core object for handle it."""