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/options`
- POST `/homeassistant/check`
```json
{

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

@ -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,16 +214,13 @@ 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.
@ -258,22 +234,16 @@ class DockerBase(object):
if not self._install(tag):
return False
# container
# stop container & cleanup
self._stop()
# cleanup images
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.
@ -290,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.
@ -319,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.
@ -337,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:
@ -346,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,15 +22,11 @@ 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

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."""
@ -165,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."""