Add support for stats & code cleanup (#297)

* Add support for stats & code cleanup

* Add more stats

* Move code into own object

* Add to API

* Update API

* Add error handling

* fix lint

* fix block io
This commit is contained in:
Pascal Vizeli 2018-01-07 15:53:54 +01:00 committed by GitHub
parent e992b70f92
commit eebe90bd14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 254 additions and 11 deletions

39
API.md
View File

@ -86,6 +86,19 @@ Reload addons/version.
Output is the raw docker log. Output is the raw docker log.
- GET `/supervisor/stats`
```json
{
"cpu_percent": 0.0,
"memory_usage": 283123,
"memory_limit": 329392,
"network_tx": 0,
"network_rx": 0,
"blk_read": 0,
"blk_write": 0
}
```
### Security ### Security
- GET `/security/info` - GET `/security/info`
@ -334,6 +347,19 @@ Proxy to real home-assistant instance.
Proxy to real websocket instance. Proxy to real websocket instance.
- GET `/homeassistant/stats`
```json
{
"cpu_percent": 0.0,
"memory_usage": 283123,
"memory_limit": 329392,
"network_tx": 0,
"network_rx": 0,
"blk_read": 0,
"blk_write": 0
}
```
### RESTful for API addons ### RESTful for API addons
- GET `/addons` - GET `/addons`
@ -452,6 +478,19 @@ Only supported for local build addons
Write data to add-on stdin Write data to add-on stdin
- GET `/addons/{addon}/stats`
```json
{
"cpu_percent": 0.0,
"memory_usage": 283123,
"memory_limit": 329392,
"network_tx": 0,
"network_rx": 0,
"blk_read": 0,
"blk_write": 0
}
```
## Host Control ## Host Control
Communicate over UNIX socket with a host daemon. Communicate over UNIX socket with a host daemon.

View File

@ -596,6 +596,14 @@ class Addon(CoreSysAttributes):
""" """
return self.instance.logs() return self.instance.logs()
@check_installed
def stats(self):
"""Return stats of container.
Return a coroutine.
"""
return self.instance.stats()
@check_installed @check_installed
async def rebuild(self): async def rebuild(self):
"""Performe a rebuild of local build addon.""" """Performe a rebuild of local build addon."""

View File

@ -69,6 +69,7 @@ class RestAPI(CoreSysAttributes):
self.webapp.router.add_get('/supervisor/ping', api_supervisor.ping) self.webapp.router.add_get('/supervisor/ping', api_supervisor.ping)
self.webapp.router.add_get('/supervisor/info', api_supervisor.info) self.webapp.router.add_get('/supervisor/info', api_supervisor.info)
self.webapp.router.add_get('/supervisor/stats', api_supervisor.stats)
self.webapp.router.add_post( self.webapp.router.add_post(
'/supervisor/update', api_supervisor.update) '/supervisor/update', api_supervisor.update)
self.webapp.router.add_post( self.webapp.router.add_post(
@ -84,6 +85,7 @@ class RestAPI(CoreSysAttributes):
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_get('/homeassistant/logs', api_hass.logs)
self.webapp.router.add_get('/homeassistant/stats', api_hass.stats)
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)
@ -114,7 +116,6 @@ class RestAPI(CoreSysAttributes):
self.webapp.router.add_get('/addons', api_addons.list) self.webapp.router.add_get('/addons', api_addons.list)
self.webapp.router.add_post('/addons/reload', api_addons.reload) self.webapp.router.add_post('/addons/reload', api_addons.reload)
self.webapp.router.add_get('/addons/{addon}/info', api_addons.info) self.webapp.router.add_get('/addons/{addon}/info', api_addons.info)
self.webapp.router.add_post( self.webapp.router.add_post(
'/addons/{addon}/install', api_addons.install) '/addons/{addon}/install', api_addons.install)
@ -135,6 +136,7 @@ class RestAPI(CoreSysAttributes):
self.webapp.router.add_get( self.webapp.router.add_get(
'/addons/{addon}/changelog', api_addons.changelog) '/addons/{addon}/changelog', api_addons.changelog)
self.webapp.router.add_post('/addons/{addon}/stdin', api_addons.stdin) self.webapp.router.add_post('/addons/{addon}/stdin', api_addons.stdin)
self.webapp.router.add_get('/addons/{addon}/stats', api_addons.stats)
def _register_security(self): def _register_security(self):
"""Register security function.""" """Register security function."""

View File

@ -15,6 +15,8 @@ from ..const import (
ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API,
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, BOOT_AUTO, BOOT_MANUAL, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, BOOT_AUTO, BOOT_MANUAL,
ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_LONG_DESCRIPTION, ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_LONG_DESCRIPTION,
ATTR_CPU_PERCENT, ATTR_MEMORY_LIMIT, ATTR_MEMORY_USAGE, ATTR_NETWORK_TX,
ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE,
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT) CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..validate import DOCKER_PORTS from ..validate import DOCKER_PORTS
@ -158,6 +160,25 @@ class APIAddons(CoreSysAttributes):
return True return True
@api_process
async def stats(self, request):
"""Return resource information."""
addon = self._extract_addon(request)
stats = await addon.stats()
if not stats:
raise RuntimeError("No stats available")
return {
ATTR_CPU_PERCENT: stats.cpu_percent,
ATTR_MEMORY_USAGE: stats.memory_usage,
ATTR_MEMORY_LIMIT: stats.memory_limit,
ATTR_NETWORK_RX: stats.network_rx,
ATTR_NETWORK_TX: stats.network_tx,
ATTR_BLK_READ: stats.blk_read,
ATTR_BLK_WRITE: stats.blk_write,
}
@api_process @api_process
def install(self, request): def install(self, request):
"""Install addon.""" """Install addon."""

View File

@ -7,7 +7,9 @@ import voluptuous as vol
from .utils import api_process, api_process_raw, api_validate from .utils import api_process, api_process_raw, api_validate
from ..const import ( from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_IMAGE, ATTR_CUSTOM, ATTR_BOOT, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_IMAGE, ATTR_CUSTOM, ATTR_BOOT,
ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, CONTENT_TYPE_BINARY) ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, ATTR_CPU_PERCENT,
ATTR_MEMORY_USAGE, ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX,
ATTR_BLK_READ, ATTR_BLK_WRITE, CONTENT_TYPE_BINARY)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..validate import NETWORK_PORT from ..validate import NETWORK_PORT
@ -76,6 +78,23 @@ class APIHomeAssistant(CoreSysAttributes):
self._homeassistant.save() self._homeassistant.save()
return True return True
@api_process
async def stats(self, request):
"""Return resource information."""
stats = await self._homeassistant.stats()
if not stats:
raise RuntimeError("No stats available")
return {
ATTR_CPU_PERCENT: stats.cpu_percent,
ATTR_MEMORY_USAGE: stats.memory_usage,
ATTR_MEMORY_LIMIT: stats.memory_limit,
ATTR_NETWORK_RX: stats.network_rx,
ATTR_NETWORK_TX: stats.network_tx,
ATTR_BLK_READ: stats.blk_read,
ATTR_BLK_WRITE: stats.blk_write,
}
@api_process @api_process
async def update(self, request): async def update(self, request):
"""Update homeassistant.""" """Update homeassistant."""

View File

@ -9,7 +9,9 @@ from ..const import (
ATTR_ADDONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_BETA_CHANNEL, ATTR_ARCH, ATTR_ADDONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_BETA_CHANNEL, ATTR_ARCH,
HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_LOGO, ATTR_REPOSITORY, HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_LOGO, ATTR_REPOSITORY,
ATTR_DESCRIPTON, ATTR_NAME, ATTR_SLUG, ATTR_INSTALLED, ATTR_TIMEZONE, ATTR_DESCRIPTON, ATTR_NAME, ATTR_SLUG, ATTR_INSTALLED, ATTR_TIMEZONE,
ATTR_STATE, ATTR_WAIT_BOOT, CONTENT_TYPE_BINARY) ATTR_STATE, ATTR_WAIT_BOOT, ATTR_CPU_PERCENT, ATTR_MEMORY_USAGE,
ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_BLK_READ,
ATTR_BLK_WRITE, CONTENT_TYPE_BINARY)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..validate import validate_timezone, WAIT_BOOT from ..validate import validate_timezone, WAIT_BOOT
@ -86,6 +88,23 @@ class APISupervisor(CoreSysAttributes):
self._config.save() self._config.save()
return True return True
@api_process
async def stats(self, request):
"""Return resource information."""
stats = await self._supervisor.stats()
if not stats:
raise RuntimeError("No stats available")
return {
ATTR_CPU_PERCENT: stats.cpu_percent,
ATTR_MEMORY_USAGE: stats.memory_usage,
ATTR_MEMORY_LIMIT: stats.memory_limit,
ATTR_NETWORK_RX: stats.network_rx,
ATTR_NETWORK_TX: stats.network_tx,
ATTR_BLK_READ: stats.blk_read,
ATTR_BLK_WRITE: stats.blk_write,
}
@api_process @api_process
async def update(self, request): async def update(self, request):
"""Update supervisor OS.""" """Update supervisor OS."""

View File

@ -128,6 +128,13 @@ ATTR_SQUASH = 'squash'
ATTR_GPIO = 'gpio' ATTR_GPIO = 'gpio'
ATTR_LEGACY = 'legacy' ATTR_LEGACY = 'legacy'
ATTR_ADDONS_CUSTOM_LIST = 'addons_custom_list' ATTR_ADDONS_CUSTOM_LIST = 'addons_custom_list'
ATTR_CPU_PERCENT = 'cpu_percent'
ATTR_NETWORK_RX = 'network_rx'
ATTR_NETWORK_TX = 'network_tx'
ATTR_MEMORY_LIMIT = 'memory_limit'
ATTR_MEMORY_USAGE = 'memory_usage'
ATTR_BLK_READ = 'blk_read'
ATTR_BLK_WRITE = 'blk_write'
STARTUP_INITIALIZE = 'initialize' STARTUP_INITIALIZE = 'initialize'
STARTUP_SYSTEM = 'system' STARTUP_SYSTEM = 'system'

View File

@ -4,10 +4,10 @@ import aiohttp
from .config import CoreConfig from .config import CoreConfig
from .docker import DockerAPI from .docker import DockerAPI
from .dns import DNSForward from .misc.dns import DNSForward
from .hardware import Hardware from .misc.hardware import Hardware
from .host_control import HostControl from .misc.host_control import HostControl
from .scheduler import Scheduler from .misc.scheduler import Scheduler
class CoreSys(object): class CoreSys(object):

View File

@ -6,6 +6,7 @@ import logging
import docker import docker
from .utils import docker_process from .utils import docker_process
from .stats import DockerStats
from ..const import LABEL_VERSION, LABEL_ARCH from ..const import LABEL_VERSION, LABEL_ARCH
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
@ -325,3 +326,24 @@ class DockerInterface(CoreSysAttributes):
Need run inside executor. Need run inside executor.
""" """
raise NotImplementedError() raise NotImplementedError()
def stats(self):
"""Read and return stats from container."""
return self._loop.run_in_executor(None, self._stats)
def _stats(self):
"""Create a temporary container and run command.
Need run inside executor.
"""
try:
container = self._docker.containers.get(self.name)
except docker.errors.DockerException:
return None
try:
stats = container.stats(stream=False)
return DockerStats(stats)
except docker.errors.DockerException as err:
_LOGGER.error("Can't read stats from %s: %s", self.name, err)
return None

90
hassio/docker/stats.py Normal file
View File

@ -0,0 +1,90 @@
"""Calc & represent docker stats data."""
from contextlib import suppress
class DockerStats(object):
"""Hold stats data from container inside."""
def __init__(self, stats):
"""Initialize docker stats."""
self._cpu = 0.0
self._network_rx = 0
self._network_tx = 0
self._blk_read = 0
self._blk_write = 0
try:
self._memory_usage = stats['memory_stats']['usage']
self._memory_limit = stats['memory_stats']['limit']
except KeyError:
self._memory_usage = 0
self._memory_limit = 0
with suppress(KeyError):
self._calc_cpu_percent(stats)
with suppress(KeyError):
self._calc_network(stats['networks'])
with suppress(KeyError):
self._calc_block_io(stats['blkio_stats'])
def _calc_cpu_percent(self, stats):
"""Calculate CPU percent."""
cpu_delta = stats['cpu_stats']['cpu_usage']['total_usage'] - \
stats['precpu_stats']['cpu_usage']['total_usage']
system_delta = stats['cpu_stats']['system_cpu_usage'] - \
stats['precpu_stats']['system_cpu_usage']
if system_delta > 0.0 and cpu_delta > 0.0:
self._cpu = (cpu_delta / system_delta) * \
len(stats['cpu_stats']['cpu_usage']['percpu_usage']) * 100.0
def _calc_network(self, networks):
"""Calculate Network IO stats."""
for _, stats in networks.items():
self._network_rx += stats['rx_bytes']
self._network_tx += stats['tx_bytes']
def _calc_block_io(self, blkio):
"""Calculate block IO stats."""
for stats in blkio['io_service_bytes_recursive']:
if stats['op'] == 'Read':
self._blk_read += stats['value']
elif stats['op'] == 'Write':
self._blk_write += stats['value']
@property
def cpu_percent(self):
"""Return CPU percent."""
return self._cpu
@property
def memory_usage(self):
"""Return memory usage."""
return self._memory_usage
@property
def memory_limit(self):
"""Return memory limit."""
return self._memory_limit
@property
def network_rx(self):
"""Return network rx stats."""
return self._network_rx
@property
def network_tx(self):
"""Return network rx stats."""
return self._network_tx
@property
def blk_read(self):
"""Return block IO read stats."""
return self._blk_read
@property
def blk_write(self):
"""Return block IO write stats."""
return self._blk_write

View File

@ -219,6 +219,13 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
""" """
return self.instance.logs() return self.instance.logs()
def stats(self):
"""Return stats of HomeAssistant.
Return a coroutine.
"""
return self.instance.stats()
def is_running(self): def is_running(self):
"""Return True if docker container is running. """Return True if docker container is running.

1
hassio/misc/__init__.py Normal file
View File

@ -0,0 +1 @@
"""Special object and tools for Hass.io."""

View File

@ -6,7 +6,7 @@ import re
import pyudev import pyudev
from .const import ATTR_NAME, ATTR_TYPE, ATTR_DEVICES from ..const import ATTR_NAME, ATTR_TYPE, ATTR_DEVICES
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -5,7 +5,7 @@ import logging
import async_timeout import async_timeout
from .const import ( from ..const import (
SOCKET_HC, ATTR_LAST_VERSION, ATTR_VERSION, ATTR_TYPE, ATTR_FEATURES, SOCKET_HC, ATTR_LAST_VERSION, ATTR_VERSION, ATTR_TYPE, ATTR_FEATURES,
ATTR_HOSTNAME, ATTR_OS) ATTR_HOSTNAME, ATTR_OS)

View File

@ -68,3 +68,10 @@ class Supervisor(CoreSysAttributes):
Return a coroutine. Return a coroutine.
""" """
return self.instance.logs() return self.instance.logs()
def stats(self):
"""Return stats of Supervisor.
Return a coroutine.
"""
return self.instance.stats()

View File

@ -31,10 +31,11 @@ setup(
platforms='any', platforms='any',
packages=[ packages=[
'hassio', 'hassio',
'hassio.utils',
'hassio.docker', 'hassio.docker',
'hassio.api',
'hassio.addons', 'hassio.addons',
'hassio.api',
'hassio.misc',
'hassio.utils',
'hassio.snapshots' 'hassio.snapshots'
], ],
include_package_data=True, include_package_data=True,