mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-15 13:16:29 +00:00
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:
parent
4af92b9d25
commit
7798e7cde2
1
API.md
1
API.md
@ -303,6 +303,7 @@ Output is the raw Docker log.
|
||||
|
||||
- POST `/homeassistant/restart`
|
||||
- POST `/homeassistant/options`
|
||||
- POST `/homeassistant/check`
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -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."""
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user