Extend API to view logs from docker (#11)

* Extend API to view logs from docker

* Pump version

* Fix lint

* Change to raw api output

* Fix aiohttp response

* Fix aiohttp response p2

* Fix body convert

* Add attach to docker addon
This commit is contained in:
Pascal Vizeli 2017-04-24 12:00:44 +02:00 committed by GitHub
parent 98f0ff2a01
commit f99c6335c0
11 changed files with 108 additions and 16 deletions

12
API.md
View File

@ -62,6 +62,10 @@ Optional:
Reload addons/version.
- `/supervisor/logs`
Output the raw docker log
### Host
- `/host/shutdown`
@ -123,6 +127,10 @@ Optional:
}
```
- `/homeassistant/logs`
Output the raw docker log
### REST API addons
- `/addons/{addon}/info`
@ -163,6 +171,10 @@ Optional:
}
```
- `/addons/{addon}/logs`
Output the raw docker log
## Host Controll
Communicate over unix socket with a host daemon.

View File

@ -7,7 +7,6 @@ It is a docker image (supervisor) they manage HomeAssistant docker and give a in
## Feature in progress
- Backup/Restore
- Read docker logs and extend to api
- MQTT addon
- DHCP-Server addon

View File

@ -36,6 +36,7 @@ class AddonManager(AddonsData):
for addon in self.list_installed:
self.dockers[addon] = DockerAddon(
self.config, self.loop, self.dock, self, addon)
await self.dockers[addon].attach()
async def reload(self):
"""Update addons from repo and reload list."""
@ -144,10 +145,6 @@ class AddonManager(AddonsData):
async def update(self, addon, version=None):
"""Update addon."""
if not self.is_installed(addon):
_LOGGER.error("Addon %s is not installed", addon)
return False
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
@ -157,3 +154,11 @@ class AddonManager(AddonsData):
self.set_version(addon, version)
return True
return False
async def logs(self, addon):
"""Return addons log output."""
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
return await self.dockers[addon].logs()

View File

@ -52,6 +52,7 @@ class RestAPI(object):
self.webapp.router.add_get('/supervisor/reload', api_supervisor.reload)
self.webapp.router.add_get(
'/supervisor/options', api_supervisor.options)
self.webapp.router.add_get('/supervisor/logs', api_supervisor.logs)
def register_homeassistant(self, dock_homeassistant):
"""Register homeassistant function."""
@ -59,6 +60,7 @@ class RestAPI(object):
self.webapp.router.add_get('/homeassistant/info', api_hass.info)
self.webapp.router.add_get('/homeassistant/update', api_hass.update)
self.webapp.router.add_get('/homeassistant/logs', api_hass.logs)
def register_addons(self, addons):
"""Register homeassistant function."""
@ -74,6 +76,7 @@ class RestAPI(object):
self.webapp.router.add_get('/addons/{addon}/update', api_addons.update)
self.webapp.router.add_get(
'/addons/{addon}/options', api_addons.options)
self.webapp.router.add_get('/addons/{addon}/logs', api_addons.logs)
async def start(self):
"""Run rest api webserver."""

View File

@ -5,7 +5,7 @@ import logging
import voluptuous as vol
from voluptuous.humanize import humanize_error
from .util import api_process, api_validate
from .util import api_process, api_process_raw, api_validate
from ..const import (
ATTR_VERSION, ATTR_CURRENT, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
STATE_STOPPED, STATE_STARTED)
@ -124,3 +124,9 @@ class APIAddons(object):
return await asyncio.shield(
self.addons.update(addon, version), loop=self.loop)
@api_process_raw
def logs(self, request):
"""Return logs from addon."""
addon = self._extract_addon(request)
return self.addons.logs(addon)

View File

@ -4,7 +4,7 @@ import logging
import voluptuous as vol
from .util import api_process, api_validate
from .util import api_process, api_process_raw, api_validate
from ..const import ATTR_VERSION, ATTR_CURRENT
_LOGGER = logging.getLogger(__name__)
@ -17,17 +17,17 @@ SCHEMA_VERSION = vol.Schema({
class APIHomeAssistant(object):
"""Handle rest api for homeassistant functions."""
def __init__(self, config, loop, dock_hass):
def __init__(self, config, loop, homeassistant):
"""Initialize homeassistant rest api part."""
self.config = config
self.loop = loop
self.dock_hass = dock_hass
self.homeassistant = homeassistant
@api_process
async def info(self, request):
"""Return host information."""
info = {
ATTR_VERSION: self.dock_hass.version,
ATTR_VERSION: self.homeassistant.version,
ATTR_CURRENT: self.config.current_homeassistant,
}
@ -39,11 +39,19 @@ class APIHomeAssistant(object):
body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.config.current_homeassistant)
if self.dock_hass.in_progress:
if self.homeassistant.in_progress:
raise RuntimeError("Other task is in progress")
if version == self.dock_hass.version:
if version == self.homeassistant.version:
raise RuntimeError("Version is already in use")
return await asyncio.shield(
self.dock_hass.update(version), loop=self.loop)
self.homeassistant.update(version), loop=self.loop)
@api_process_raw
def logs(self, request):
"""Return homeassistant docker logs.
Return a coroutine.
"""
return self.homeassistant.logs()

View File

@ -4,7 +4,7 @@ import logging
import voluptuous as vol
from .util import api_process, api_validate
from .util import api_process, api_process_raw, api_validate
from ..const import (
ATTR_ADDONS, ATTR_VERSION, ATTR_CURRENT, ATTR_BETA, HASSIO_VERSION)
@ -80,3 +80,11 @@ class APISupervisor(object):
raise RuntimeError("Some reload task fails!")
return True
@api_process_raw
def logs(self, request):
"""Return supervisor docker logs.
Return a coroutine.
"""
return self.supervisor.logs()

View File

@ -62,6 +62,20 @@ def api_process_hostcontroll(method):
return wrap_hostcontroll
def api_process_raw(method):
"""Wrap function with raw output to rest api."""
async def wrap_api(api, *args, **kwargs):
"""Return api information."""
try:
message = await method(api, *args, **kwargs)
except RuntimeError as err:
message = str(err).encode()
return web.Response(body=message)
return wrap_api
def api_return_error(message=None):
"""Return a API error message."""
return web.json_response({

View File

@ -1,5 +1,5 @@
"""Const file for HassIO."""
HASSIO_VERSION = '0.11'
HASSIO_VERSION = '0.12'
URL_HASSIO_VERSION = \
'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json'

View File

@ -99,8 +99,9 @@ class DockerBase(object):
self.container.attrs['Config']['Env'])
except docker.errors.DockerException:
return False
else:
self.container.reload()
self.container.reload()
return self.container.status == 'running'
async def attach(self):
@ -243,3 +244,25 @@ class DockerBase(object):
return True
return False
async 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 False
async with self._lock:
return await self.loop.run_in_executor(None, self._logs)
def _logs(self):
"""Return docker logs of container.
Need run inside executor.
"""
if not self.container:
return
try:
return self.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)

View File

@ -74,3 +74,17 @@ class DockerAddon(DockerBase):
return False
return True
def _attach(self):
"""Attach to running docker container.
Need run inside executor.
"""
try:
self.container = self.dock.containers.get(self.docker_name)
self.version = get_version_from_env(
self.container.attrs['Config']['Env'])
_LOGGER.info("Attach to image %s with version %s",
self.image, self.version)
except (docker.errors.DockerException, KeyError):
pass