hw monitoring

This commit is contained in:
Devaev Maxim 2020-07-09 05:04:21 +03:00
parent 69f77ce48b
commit 53eb74670d
14 changed files with 230 additions and 4 deletions

View File

@ -221,6 +221,13 @@ def _get_config_scheme() -> Dict:
"extras": Option("/usr/share/kvmd/extras", type=valid_abs_dir),
},
"hw": {
"vcgencmd_cmd": Option(["/opt/vc/bin/vcgencmd"], type=valid_command),
"procfs_prefix": Option("", type=(lambda arg: str(arg).strip())),
"sysfs_prefix": Option("", type=(lambda arg: str(arg).strip())),
"state_poll": Option(10.0, type=valid_float_f01),
},
"wol": {
"ip": Option("255.255.255.255", type=(lambda arg: valid_ip(arg, v6=False))),
"port": Option(9, type=valid_port),

View File

@ -35,6 +35,7 @@ from .. import init
from .auth import AuthManager
from .info import InfoManager
from .hw import HwManager
from .logreader import LogReader
from .wol import WakeOnLan
from .streamer import Streamer
@ -77,6 +78,7 @@ def main(argv: Optional[List[str]]=None) -> None:
enabled=config.auth.enabled,
),
info_manager=InfoManager(global_config),
hw_manager=HwManager(**config.hw._unpack()),
log_reader=LogReader(),
wol=WakeOnLan(**config.wol._unpack()),

41
kvmd/apps/kvmd/api/hw.py Normal file
View File

@ -0,0 +1,41 @@
# ========================================================================== #
# #
# KVMD - The main Pi-KVM daemon. #
# #
# Copyright (C) 2018 Maxim Devaev <mdevaev@gmail.com> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
# #
# ========================================================================== #
from aiohttp.web import Request
from aiohttp.web import Response
from ..hw import HwManager
from ..http import exposed_http
from ..http import make_json_response
# =====
class HwApi:
def __init__(self, hw_manager: HwManager) -> None:
self.__hw_manager = hw_manager
# =====
@exposed_http("GET", "/hw")
async def __state_handler(self, _: Request) -> Response:
return make_json_response(await self.__hw_manager.get_state())

150
kvmd/apps/kvmd/hw.py Normal file
View File

@ -0,0 +1,150 @@
# ========================================================================== #
# #
# KVMD - The main Pi-KVM daemon. #
# #
# Copyright (C) 2018 Maxim Devaev <mdevaev@gmail.com> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
# #
# ========================================================================== #
import asyncio
from typing import List
from typing import Dict
from typing import Callable
from typing import AsyncGenerator
from typing import TypeVar
from typing import Optional
import aiofiles
from ...logging import get_logger
from ... import aioproc
# =====
_RetvalT = TypeVar("_RetvalT")
# =====
class HwManager:
def __init__(
self,
vcgencmd_cmd: List[str],
procfs_prefix: str,
sysfs_prefix: str,
state_poll: float,
) -> None:
self.__vcgencmd_cmd = vcgencmd_cmd
self.__sysfs_prefix = sysfs_prefix
self.__procfs_prefix = procfs_prefix
self.__state_poll = state_poll
async def get_state(self) -> Dict:
(model, cpu_temp, gpu_temp, throttling) = await asyncio.gather(
self.__get_dt_model(),
self.__get_cpu_temp(),
self.__get_gpu_temp(),
self.__get_throttling(),
)
return {
"platform": {
"type": "rpi",
"base": model,
},
"state": {
"temp": {
"cpu": cpu_temp,
"gpu": gpu_temp,
},
"throttling": throttling,
},
}
async def poll_state(self) -> AsyncGenerator[Dict, None]:
prev_state: Dict = {}
while True:
state = await self.get_state()
if state != prev_state:
yield state
prev_state = state
await asyncio.sleep(self.__state_poll)
# =====
async def __get_dt_model(self) -> Optional[str]:
model_path = f"{self.__procfs_prefix}/proc/device-tree/model"
try:
async with aiofiles.open(model_path) as model_file:
return (await model_file.read()).strip(" \t\r\n\0")
except Exception as err:
get_logger(0).error("Can't read DT model from %s: %s", model_path, err)
return None
async def __get_cpu_temp(self) -> Optional[float]:
temp_path = f"{self.__sysfs_prefix}/sys/class/thermal/thermal_zone0/temp"
try:
async with aiofiles.open(temp_path) as temp_file:
return int((await temp_file.read()).strip()) / 1000
except Exception as err:
get_logger(0).error("Can't read CPU temp from %s: %s", temp_path, err)
return None
async def __get_throttling(self) -> Optional[Dict]:
# https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=147781&start=50#p972790
if (flags := await self.__parse_vcgencmd(
arg="get_throttled",
parser=(lambda text: int(text.split("=")[-1].strip(), 16)),
)) is not None:
return {
"raw_flags": flags,
"parsed_flags": {
"undervoltage": {
"now": bool(flags & (1 << 0)),
"past": bool(flags & (1 << 16)),
},
"freq_capped": {
"now": bool(flags & (1 << 1)),
"past": bool(flags & (1 << 17)),
},
"throttled": {
"now": bool(flags & (1 << 2)),
"past": bool(flags & (1 << 18)),
},
},
}
return None
async def __get_gpu_temp(self) -> Optional[float]:
return (await self.__parse_vcgencmd(
arg="measure_temp",
parser=(lambda text: float(text.split("=")[1].split("'")[0])),
))
async def __parse_vcgencmd(self, arg: str, parser: Callable[[str], _RetvalT]) -> Optional[_RetvalT]:
cmd = [*self.__vcgencmd_cmd, arg]
try:
text = (await aioproc.read_process(cmd, err_to_null=True))[1]
except Exception:
get_logger(0).exception("Error while executing %s", cmd)
return None
try:
return parser(text)
except Exception as err:
get_logger(0).error("Can't parse %s output: %r: %s", cmd, text, err)
return None

View File

@ -62,6 +62,7 @@ from ... import aioproc
from .auth import AuthManager
from .info import InfoManager
from .hw import HwManager
from .logreader import LogReader
from .wol import WakeOnLan
from .streamer import Streamer
@ -81,6 +82,7 @@ from .api.auth import AuthApi
from .api.auth import check_request_auth
from .api.info import InfoApi
from .api.hw import HwApi
from .api.log import LogApi
from .api.wol import WolApi
from .api.hid import HidApi
@ -123,6 +125,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
self,
auth_manager: AuthManager,
info_manager: InfoManager,
hw_manager: HwManager,
log_reader: LogReader,
wol: WakeOnLan,
@ -148,6 +151,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
self.__components = [
_Component("Auth manager", "", auth_manager),
_Component("Info manager", "info_state", info_manager),
_Component("HW manager", "hw_state", hw_manager),
_Component("Wake-on-LAN", "wol_state", wol),
_Component("HID", "hid_state", hid),
_Component("ATX", "atx_state", atx),
@ -159,6 +163,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
self,
AuthApi(auth_manager),
InfoApi(info_manager),
HwApi(hw_manager),
LogApi(log_reader),
WolApi(wol),
HidApi(hid, keymap_path),

View File

@ -44,10 +44,14 @@ RUN pip install -r requirements.txt
RUN mkdir -p \
/etc/kvmd/nginx \
/var/lib/kvmd/msd/{images,meta} \
/opt/vc/bin \
/fake_sysfs/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0 \
&& cd /fake_sysfs/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0 \
&& touch file \
&& echo 1 > cdrom \
&& echo 1 > ro
/fake_sysfs/sys/class/thermal/thermal_zone0 \
/fake_procfs/proc/device-tree
COPY testenv/fakes/vcgencmd /opt/vc/bin/
COPY testenv/fakes/msd/* /fake_sysfs/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/
COPY testenv/fakes/cpu_temp /fake_sysfs/sys/class/thermal/thermal_zone0/temp
COPY testenv/fakes/dt_model /fake_procfs/proc/device-tree/model
CMD /bin/bash

1
testenv/fakes/cpu_temp Normal file
View File

@ -0,0 +1 @@
36511

1
testenv/fakes/dt_model Normal file
View File

@ -0,0 +1 @@
Virtual Raspberry Pi

1
testenv/fakes/msd/cdrom Normal file
View File

@ -0,0 +1 @@
1

0
testenv/fakes/msd/file Normal file
View File

1
testenv/fakes/msd/ro Normal file
View File

@ -0,0 +1 @@
1

5
testenv/fakes/vcgencmd Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
case $1 in
get_throttled) echo "throttled=0x0";;
measure_temp) echo "temp=35.0'C";;
esac

View File

@ -2,6 +2,10 @@ kvmd:
server:
unix_mode: 0666
hw:
procfs_prefix: /fake_procfs
sysfs_prefix: /fake_sysfs
hid:
device: /dev/ttyS10
noop: true

View File

@ -2,6 +2,10 @@ kvmd:
server:
unix_mode: 0666
hw:
procfs_prefix: /fake_procfs
sysfs_prefix: /fake_sysfs
hid:
keyboard:
device: /dev/null