complex info handle

This commit is contained in:
Devaev Maxim 2020-07-09 09:41:05 +03:00
parent 53eb74670d
commit 5f1733d002
16 changed files with 270 additions and 138 deletions

View File

@ -216,17 +216,16 @@ def _get_config_scheme() -> Dict:
}, },
}, },
"info": { # Accessed via global config, see kvmd/info.py for details "info": { # Accessed via global config, see kvmd/info for details
"meta": Option("/etc/kvmd/meta.yaml", type=valid_abs_file), "meta": Option("/etc/kvmd/meta.yaml", type=valid_abs_file),
"extras": Option("/usr/share/kvmd/extras", type=valid_abs_dir), "extras": Option("/usr/share/kvmd/extras", type=valid_abs_dir),
},
"hw": { "hw": {
"vcgencmd_cmd": Option(["/opt/vc/bin/vcgencmd"], type=valid_command), "vcgencmd_cmd": Option(["/opt/vc/bin/vcgencmd"], type=valid_command),
"procfs_prefix": Option("", type=(lambda arg: str(arg).strip())), "procfs_prefix": Option("", type=(lambda arg: str(arg).strip())),
"sysfs_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), "state_poll": Option(10.0, type=valid_float_f01),
}, },
},
"wol": { "wol": {
"ip": Option("255.255.255.255", type=(lambda arg: valid_ip(arg, v6=False))), "ip": Option("255.255.255.255", type=(lambda arg: valid_ip(arg, v6=False))),

View File

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

View File

@ -20,9 +20,16 @@
# ========================================================================== # # ========================================================================== #
import asyncio
from typing import List
from aiohttp.web import Request from aiohttp.web import Request
from aiohttp.web import Response from aiohttp.web import Response
from ....validators import check_string_in_list
from ....validators.basic import valid_string_list
from ..info import InfoManager from ..info import InfoManager
from ..http import exposed_http from ..http import exposed_http
@ -37,5 +44,18 @@ class InfoApi:
# ===== # =====
@exposed_http("GET", "/info") @exposed_http("GET", "/info")
async def __state_handler(self, _: Request) -> Response: async def __common_state_handler(self, request: Request) -> Response:
return make_json_response(await self.__info_manager.get_state()) fields = self.__valid_info_fields(request)
results = dict(zip(fields, await asyncio.gather(*[
self.__info_manager.get_submanager(field).get_state()
for field in fields
])))
return make_json_response(results)
def __valid_info_fields(self, request: Request) -> List[str]:
subs = self.__info_manager.get_subs()
return (sorted(set(valid_string_list(
arg=request.query.get("fields", ",".join(subs)),
subval=(lambda field: check_string_in_list(field, "info field", subs)),
name="info fields list",
))) or subs)

View File

@ -0,0 +1,48 @@
# ========================================================================== #
# #
# 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 typing import List
from ....yamlconf import Section
from .base import BaseInfoSubmanager
from .system import SystemInfoSubmanager
from .meta import MetaInfoSubmanager
from .extras import ExtrasInfoSubmanager
from .hw import HwInfoSubmanager
# =====
class InfoManager:
def __init__(self, config: Section) -> None:
self.__subs = {
"system": SystemInfoSubmanager(config.kvmd.streamer.cmd),
"meta": MetaInfoSubmanager(config.kvmd.info.meta),
"extras": ExtrasInfoSubmanager(config),
"hw": HwInfoSubmanager(**config.kvmd.info.hw._unpack()),
}
def get_subs(self) -> List[str]:
return list(self.__subs)
def get_submanager(self, name: str) -> BaseInfoSubmanager:
return self.__subs[name]

View File

@ -20,22 +20,11 @@
# ========================================================================== # # ========================================================================== #
from aiohttp.web import Request from typing import Dict
from aiohttp.web import Response from typing import Optional
from ..hw import HwManager
from ..http import exposed_http
from ..http import make_json_response
# ===== # =====
class HwApi: class BaseInfoSubmanager:
def __init__(self, hw_manager: HwManager) -> None: async def get_state(self) -> Optional[Dict]:
self.__hw_manager = hw_manager raise NotImplementedError
# =====
@exposed_http("GET", "/hw")
async def __state_handler(self, _: Request) -> Response:
return make_json_response(await self.__hw_manager.get_state())

View File

@ -21,8 +21,6 @@
import os import os
import asyncio
import platform
import contextlib import contextlib
from typing import Dict from typing import Dict
@ -31,81 +29,27 @@ from typing import Optional
import dbus # pylint: disable=import-error import dbus # pylint: disable=import-error
import dbus.exceptions import dbus.exceptions
from ...logging import get_logger from ....logging import get_logger
from ...yamlconf import Section from ....yamlconf import Section
from ...yamlconf.loader import load_yaml_file from ....yamlconf.loader import load_yaml_file
from ... import aiotools from .... import aiotools
from ... import aioproc
from ... import __version__ from .base import BaseInfoSubmanager
# ===== # =====
class InfoManager: class ExtrasInfoSubmanager(BaseInfoSubmanager):
def __init__(self, global_config: Section) -> None: def __init__(self, global_config: Section) -> None:
self.__global_config = global_config self.__global_config = global_config
async def get_state(self) -> Dict: async def get_state(self) -> Optional[Dict]:
(streamer_info, meta_info, extras_info) = await asyncio.gather( return (await aiotools.run_async(self.__inner_get_state))
self.__get_streamer_info(),
self.__get_meta_info(),
self.__get_extras_info(),
)
uname_info = platform.uname() # Uname using the internal cache
return {
"system": {
"kvmd": {"version": __version__},
"streamer": streamer_info,
"kernel": {
field: getattr(uname_info, field)
for field in ["system", "release", "version", "machine"]
},
},
"meta": meta_info,
"extras": extras_info,
}
# ===== # =====
async def __get_streamer_info(self) -> Dict: def __inner_get_state(self) -> Optional[Dict]:
version = ""
features: Dict[str, bool] = {}
try:
path = self.__global_config.kvmd.streamer.cmd[0]
((_, version), (_, features_text)) = await asyncio.gather(
aioproc.read_process([path, "--version"], err_to_null=True),
aioproc.read_process([path, "--features"], err_to_null=True),
)
except Exception:
get_logger(0).exception("Can't get streamer info")
else:
try:
for line in features_text.split("\n"):
(status, name) = map(str.strip, line.split(" "))
features[name] = (status == "+")
except Exception:
get_logger(0).exception("Can't parse streamer features")
return {
"app": os.path.basename(path),
"version": version,
"features": features,
}
async def __get_meta_info(self) -> Optional[Dict]:
try:
return ((await aiotools.run_async(load_yaml_file, self.__global_config.kvmd.info.meta)) or {})
except Exception:
get_logger(0).exception("Can't parse meta")
return None
async def __get_extras_info(self) -> Optional[Dict]:
return (await aiotools.run_async(self.__inner_get_extras_info))
# =====
def __inner_get_extras_info(self) -> Optional[Dict]:
try: try:
extras_path = self.__global_config.kvmd.info.extras extras_path = self.__global_config.kvmd.info.extras
extras: Dict[str, Dict] = {} extras: Dict[str, Dict] = {}

View File

@ -31,9 +31,11 @@ from typing import Optional
import aiofiles import aiofiles
from ...logging import get_logger from ....logging import get_logger
from ... import aioproc from .... import aioproc
from .base import BaseInfoSubmanager
# ===== # =====
@ -41,7 +43,7 @@ _RetvalT = TypeVar("_RetvalT")
# ===== # =====
class HwManager: class HwInfoSubmanager(BaseInfoSubmanager):
def __init__( def __init__(
self, self,
vcgencmd_cmd: List[str], vcgencmd_cmd: List[str],

View File

@ -0,0 +1,45 @@
# ========================================================================== #
# #
# 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 typing import Dict
from typing import Optional
from ....logging import get_logger
from ....yamlconf.loader import load_yaml_file
from .... import aiotools
from .base import BaseInfoSubmanager
# =====
class MetaInfoSubmanager(BaseInfoSubmanager):
def __init__(self, meta_path: str) -> None:
self.__meta_path = meta_path
async def get_state(self) -> Optional[Dict]:
try:
return ((await aiotools.run_async(load_yaml_file, self.__meta_path)) or {})
except Exception:
get_logger(0).exception("Can't parse meta")
return None

View File

@ -0,0 +1,80 @@
# ========================================================================== #
# #
# 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 os
import asyncio
import platform
from typing import List
from typing import Dict
from ....logging import get_logger
from .... import aioproc
from .... import __version__
from .base import BaseInfoSubmanager
# =====
class SystemInfoSubmanager(BaseInfoSubmanager):
def __init__(self, streamer_cmd: List[str]) -> None:
self.__streamer_cmd = streamer_cmd
async def get_state(self) -> Dict:
streamer_info = await self.__get_streamer_info()
uname_info = platform.uname() # Uname using the internal cache
return {
"kvmd": {"version": __version__},
"streamer": streamer_info,
"kernel": {
field: getattr(uname_info, field)
for field in ["system", "release", "version", "machine"]
},
}
# =====
async def __get_streamer_info(self) -> Dict:
version = ""
features: Dict[str, bool] = {}
try:
path = self.__streamer_cmd[0]
((_, version), (_, features_text)) = await asyncio.gather(
aioproc.read_process([path, "--version"], err_to_null=True),
aioproc.read_process([path, "--features"], err_to_null=True),
)
except Exception:
get_logger(0).exception("Can't get streamer info")
else:
try:
for line in features_text.split("\n"):
(status, name) = map(str.strip, line.split(" "))
features[name] = (status == "+")
except Exception:
get_logger(0).exception("Can't parse streamer features")
return {
"app": os.path.basename(path),
"version": version,
"features": features,
}

View File

@ -62,7 +62,6 @@ from ... import aioproc
from .auth import AuthManager from .auth import AuthManager
from .info import InfoManager from .info import InfoManager
from .hw import HwManager
from .logreader import LogReader from .logreader import LogReader
from .wol import WakeOnLan from .wol import WakeOnLan
from .streamer import Streamer from .streamer import Streamer
@ -82,7 +81,6 @@ from .api.auth import AuthApi
from .api.auth import check_request_auth from .api.auth import check_request_auth
from .api.info import InfoApi from .api.info import InfoApi
from .api.hw import HwApi
from .api.log import LogApi from .api.log import LogApi
from .api.wol import WolApi from .api.wol import WolApi
from .api.hid import HidApi from .api.hid import HidApi
@ -125,7 +123,6 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
self, self,
auth_manager: AuthManager, auth_manager: AuthManager,
info_manager: InfoManager, info_manager: InfoManager,
hw_manager: HwManager,
log_reader: LogReader, log_reader: LogReader,
wol: WakeOnLan, wol: WakeOnLan,
@ -149,21 +146,26 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
self.__heartbeat = heartbeat self.__heartbeat = heartbeat
self.__components = [ self.__components = [
*[
_Component("Auth manager", "", auth_manager), _Component("Auth manager", "", auth_manager),
_Component("Info manager", "info_state", info_manager), ],
_Component("HW manager", "hw_state", hw_manager), *[
_Component(f"Info manager ({sub})", f"info_{sub}_state", info_manager.get_submanager(sub))
for sub in info_manager.get_subs()
],
*[
_Component("Wake-on-LAN", "wol_state", wol), _Component("Wake-on-LAN", "wol_state", wol),
_Component("HID", "hid_state", hid), _Component("HID", "hid_state", hid),
_Component("ATX", "atx_state", atx), _Component("ATX", "atx_state", atx),
_Component("MSD", "msd_state", msd), _Component("MSD", "msd_state", msd),
_Component("Streamer", "streamer_state", streamer), _Component("Streamer", "streamer_state", streamer),
],
] ]
self.__apis: List[object] = [ self.__apis: List[object] = [
self, self,
AuthApi(auth_manager), AuthApi(auth_manager),
InfoApi(info_manager), InfoApi(info_manager),
HwApi(hw_manager),
LogApi(log_reader), LogApi(log_reader),
WolApi(wol), WolApi(wol),
HidApi(hid, keymap_path), HidApi(hid, keymap_path),
@ -325,7 +327,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
except Exception: except Exception:
logger.exception("Cleanup error on %s", component.name) logger.exception("Cleanup error on %s", component.name)
async def __broadcast_event(self, event_type: str, event: Dict) -> None: async def __broadcast_event(self, event_type: str, event: Optional[Dict]) -> None:
if self.__ws_clients: if self.__ws_clients:
await asyncio.gather(*[ await asyncio.gather(*[
client.ws.send_str(json.dumps({ client.ws.send_str(json.dumps({

View File

@ -149,9 +149,9 @@ class _Client(RfbClient): # pylint: disable=too-many-instance-attributes
self.__kvmd_ws = None self.__kvmd_ws = None
async def __process_ws_event(self, event: Dict) -> None: async def __process_ws_event(self, event: Dict) -> None:
if event["event_type"] == "info_state": if event["event_type"] == "info_meta_state":
try: try:
host = event["event"]["meta"]["server"]["host"] host = event["event"]["server"]["host"]
except Exception: except Exception:
host = None host = None
else: else:

View File

@ -90,6 +90,7 @@ def main() -> None:
"kvmd.clients", "kvmd.clients",
"kvmd.apps", "kvmd.apps",
"kvmd.apps.kvmd", "kvmd.apps.kvmd",
"kvmd.apps.kvmd.info",
"kvmd.apps.kvmd.api", "kvmd.apps.kvmd.api",
"kvmd.apps.otg", "kvmd.apps.otg",
"kvmd.apps.otg.hid", "kvmd.apps.otg.hid",

View File

@ -2,6 +2,7 @@ kvmd:
server: server:
unix_mode: 0666 unix_mode: 0666
info:
hw: hw:
procfs_prefix: /fake_procfs procfs_prefix: /fake_procfs
sysfs_prefix: /fake_sysfs sysfs_prefix: /fake_sysfs

View File

@ -2,6 +2,7 @@ kvmd:
server: server:
unix_mode: 0666 unix_mode: 0666
info:
hw: hw:
procfs_prefix: /fake_procfs procfs_prefix: /fake_procfs
sysfs_prefix: /fake_sysfs sysfs_prefix: /fake_sysfs

View File

@ -51,7 +51,7 @@ function __setAppText() {
} }
function __loadKvmdInfo() { function __loadKvmdInfo() {
let http = tools.makeRequest("GET", "/api/info", function() { let http = tools.makeRequest("GET", "/api/info?fields=meta,extras", function() {
if (http.readyState === 4) { if (http.readyState === 4) {
if (http.status === 200) { if (http.status === 200) {
let info = JSON.parse(http.responseText).result; let info = JSON.parse(http.responseText).result;

View File

@ -55,36 +55,37 @@ export function Session() {
/************************************************************************/ /************************************************************************/
var __setAboutInfo = function(state) { var __setAboutInfoSystem = function(state) {
if (state.meta != null) { $("about-version").innerHTML = `
let text = JSON.stringify(state.meta, undefined, 4).replace(/ /g, "&nbsp;").replace(/\n/g, "<br>"); KVMD: <span class="code-comment">${state.kvmd.version}</span><br>
<hr>
Streamer: <span class="code-comment">${state.streamer.version} (${state.streamer.app})</span>
${__formatStreamerFeatures(state.streamer.features)}
<hr>
${state.kernel.system} kernel:
${__formatUname(state.kernel)}
`;
};
var __setAboutInfoMeta = function(state) {
if (state != null) {
let text = JSON.stringify(state, undefined, 4).replace(/ /g, "&nbsp;").replace(/\n/g, "<br>");
$("about-meta").innerHTML = ` $("about-meta").innerHTML = `
<span class="code-comment">// The Pi-KVM metadata.<br> <span class="code-comment">// The Pi-KVM metadata.<br>
// You can get this json using handle <a target="_blank" href="/api/info">/api/info</a>.<br> // You can get this JSON using handle <a target="_blank" href="/api/info?fields=meta">/api/info?fields=meta</a>.<br>
// In the standard configuration this data<br> // In the standard configuration this data<br>
// is specified in the file /etc/kvmd/meta.yaml.</span><br> // is specified in the file /etc/kvmd/meta.yaml.</span><br>
<br> <br>
${text} ${text}
`; `;
if (state.meta.server && state.meta.server.host) { if (state.server && state.server.host) {
$("kvmd-meta-server-host").innerHTML = `Server: ${state.meta.server.host}`; $("kvmd-meta-server-host").innerHTML = `Server: ${state.server.host}`;
document.title = `Pi-KVM Session: ${state.meta.server.host}`; document.title = `Pi-KVM Session: ${state.server.host}`;
} else { } else {
$("kvmd-meta-server-host").innerHTML = ""; $("kvmd-meta-server-host").innerHTML = "";
document.title = "Pi-KVM Session"; document.title = "Pi-KVM Session";
} }
} }
let sys = state.system;
$("about-version").innerHTML = `
KVMD: <span class="code-comment">${sys.kvmd.version}</span><br>
<hr>
Streamer: <span class="code-comment">${sys.streamer.version} (${sys.streamer.app})</span>
${__formatStreamerFeatures(sys.streamer.features)}
<hr>
${sys.kernel.system} kernel:
${__formatUname(sys.kernel)}
`;
}; };
var __formatStreamerFeatures = function(features) { var __formatStreamerFeatures = function(features) {
@ -157,7 +158,8 @@ export function Session() {
let data = JSON.parse(event.data); let data = JSON.parse(event.data);
switch (data.event_type) { switch (data.event_type) {
case "pong": __missed_heartbeats = 0; break; case "pong": __missed_heartbeats = 0; break;
case "info_state": __setAboutInfo(data.event); break; case "info_system_state": __setAboutInfoSystem(data.event); break;
case "info_meta_state": __setAboutInfoMeta(data.event); break;
case "wol_state": __wol.setState(data.event); break; case "wol_state": __wol.setState(data.event); break;
case "hid_state": __hid.setState(data.event); break; case "hid_state": __hid.setState(data.event); break;
case "atx_state": __atx.setState(data.event); break; case "atx_state": __atx.setState(data.event); break;