summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDevaev Maxim <[email protected]>2018-10-28 06:51:51 +0300
committerDevaev Maxim <[email protected]>2018-10-28 07:03:14 +0300
commitab342111d0c88e13e0d0d4a79c1e14150a7d4ef3 (patch)
tree022ec24cc8b7fde70fba8833ef8a35fcad228e48
parent1f54776ce0a1429a7ddcc4ac8b09054c48d67953 (diff)
log interface
-rw-r--r--PKGBUILD1
-rw-r--r--configs/kvmd/v1-hdmi.yaml8
-rw-r--r--configs/kvmd/v1-vga.yaml8
-rw-r--r--configs/nginx/nginx.conf10
-rw-r--r--kvmd/__init__.py7
-rw-r--r--kvmd/logging.py46
-rw-r--r--kvmd/server.py56
-rw-r--r--requirements.txt1
-rw-r--r--testenv/Dockerfile1
-rw-r--r--testenv/kvmd.yaml6
-rw-r--r--testenv/requirements.txt1
-rw-r--r--web/index.html4
12 files changed, 133 insertions, 16 deletions
diff --git a/PKGBUILD b/PKGBUILD
index 7352030c..194ca972 100644
--- a/PKGBUILD
+++ b/PKGBUILD
@@ -18,6 +18,7 @@ depends=(
python-raspberry-gpio
python-pyserial
python-setproctitle
+ python-systemd
)
makedepends=(python-setuptools)
source=("$url/archive/v$pkgver.tar.gz")
diff --git a/configs/kvmd/v1-hdmi.yaml b/configs/kvmd/v1-hdmi.yaml
index 3ab7031d..2ca573e1 100644
--- a/configs/kvmd/v1-hdmi.yaml
+++ b/configs/kvmd/v1-hdmi.yaml
@@ -4,6 +4,11 @@ kvmd:
port: 8081
heartbeat: 3.0
+ log:
+ services:
+ - kvmd.service
+ - kvmd-tc358743.service
+
hid:
pinout:
reset: 4
@@ -66,8 +71,7 @@ logging:
console:
(): logging.Formatter
style: "{"
- datefmt: "%H:%M:%S"
- format: "[{asctime}] {name:20.20} {levelname:>7} --- {message}"
+ format: "{name:20.20} {levelname:>7} --- {message}"
handlers:
console:
diff --git a/configs/kvmd/v1-vga.yaml b/configs/kvmd/v1-vga.yaml
index 36e20fbf..48b8e511 100644
--- a/configs/kvmd/v1-vga.yaml
+++ b/configs/kvmd/v1-vga.yaml
@@ -4,6 +4,11 @@ kvmd:
port: 8081
heartbeat: 3.0
+ log:
+ services:
+ - kvmd.service
+ - kvmd-tc358743.service
+
hid:
pinout:
reset: 4
@@ -69,8 +74,7 @@ logging:
console:
(): logging.Formatter
style: "{"
- datefmt: "%H:%M:%S"
- format: "[{asctime}] {name:20.20} {levelname:>7} --- {message}"
+ format: "{name:20.20} {levelname:>7} --- {message}"
handlers:
console:
diff --git a/configs/nginx/nginx.conf b/configs/nginx/nginx.conf
index a06ced1b..e5fd03c4 100644
--- a/configs/nginx/nginx.conf
+++ b/configs/nginx/nginx.conf
@@ -109,6 +109,16 @@ http {
proxy_request_buffering off;
}
+ location /kvmd/log {
+ rewrite /kvmd/log /log break;
+ proxy_pass http://kvmd;
+ include /etc/nginx/proxy-params.conf;
+ proxy_read_timeout 7d;
+ postpone_output 0;
+ proxy_buffering off;
+ proxy_ignore_headers X-Accel-Buffering;
+ }
+
location /kvmd {
rewrite /kvmd/?(.*) /$1 break;
proxy_pass http://kvmd;
diff --git a/kvmd/__init__.py b/kvmd/__init__.py
index 77c49ad0..93f9e2de 100644
--- a/kvmd/__init__.py
+++ b/kvmd/__init__.py
@@ -2,6 +2,7 @@ import asyncio
from .application import init
from .logging import get_logger
+from .logging import Log
from .hid import Hid
from .atx import Atx
@@ -22,6 +23,11 @@ def main() -> None:
with gpio.bcm():
loop = asyncio.get_event_loop()
+ log = Log(
+ services=list(config["log"]["services"]),
+ loop=loop,
+ )
+
hid = Hid(
reset=int(config["hid"]["pinout"]["reset"]),
device_path=str(config["hid"]["device"]),
@@ -60,6 +66,7 @@ def main() -> None:
)
Server(
+ log=log,
hid=hid,
atx=atx,
msd=msd,
diff --git a/kvmd/logging.py b/kvmd/logging.py
index 838e13d1..0efb9cb4 100644
--- a/kvmd/logging.py
+++ b/kvmd/logging.py
@@ -1,5 +1,13 @@
import sys
+import asyncio
import logging
+import time
+
+from typing import List
+from typing import Dict
+from typing import AsyncGenerator
+
+import systemd.journal
# =====
@@ -13,3 +21,41 @@ def get_logger(depth: int=1) -> logging.Logger:
break
name = frames[depth].f_globals["__name__"]
return logging.getLogger(name)
+
+
+class Log:
+ def __init__(
+ self,
+ services: List[str],
+ loop: asyncio.AbstractEventLoop,
+ ) -> None:
+
+ self.__services = services
+ self.__loop = loop
+
+ async def log(self, seek: int, follow: bool) -> AsyncGenerator[Dict, None]:
+ reader = systemd.journal.Reader()
+ reader.this_boot()
+ reader.this_machine()
+ reader.log_level(systemd.journal.LOG_DEBUG)
+ for service in self.__services:
+ reader.add_match(_SYSTEMD_UNIT=service)
+ if seek > 0:
+ reader.seek_realtime(float(time.time() - seek))
+
+ for entry in reader:
+ yield self.__entry_to_record(entry)
+
+ while follow:
+ entry = reader.get_next()
+ if entry:
+ yield self.__entry_to_record(entry)
+ else:
+ await asyncio.sleep(1)
+
+ def __entry_to_record(self, entry: Dict) -> Dict[str, Dict]:
+ return {
+ "dt": entry["__REALTIME_TIMESTAMP"],
+ "service": entry["_SYSTEMD_UNIT"],
+ "msg": entry["MESSAGE"].rstrip(),
+ }
diff --git a/kvmd/server.py b/kvmd/server.py
index 4d038fdc..7738eaba 100644
--- a/kvmd/server.py
+++ b/kvmd/server.py
@@ -25,6 +25,7 @@ from .msd import MassStorageDevice
from .streamer import Streamer
from .logging import get_logger
+from .logging import Log
# =====
@@ -68,6 +69,28 @@ class BadRequest(Exception):
pass
+def _valid_bool(name: str, flag: Optional[str]) -> bool:
+ flag = str(flag).strip().lower()
+ if flag in ["1", "true", "yes"]:
+ return True
+ elif flag in ["0", "false", "no"]:
+ return False
+ raise BadRequest("Invalid param '%s'" % (name))
+
+
+def _valid_int(name: str, value: Optional[str], min_value: Optional[int]=None, max_value: Optional[int]=None) -> int:
+ try:
+ value_int = int(value) # type: ignore
+ if (
+ (min_value is not None and value_int < min_value)
+ or (max_value is not None and value_int > max_value)
+ ):
+ raise ValueError()
+ return value_int
+ except Exception:
+ raise BadRequest("Invalid param %r" % (name))
+
+
def _wrap_exceptions_for_web(msg: str) -> Callable:
def make_wrapper(method: Callable) -> Callable:
async def wrap(self: "Server", request: aiohttp.web.Request) -> aiohttp.web.Response:
@@ -82,8 +105,9 @@ def _wrap_exceptions_for_web(msg: str) -> Callable:
class Server: # pylint: disable=too-many-instance-attributes
- def __init__(
+ def __init__( # pylint: disable=too-many-arguments
self,
+ log: Log,
hid: Hid,
atx: Atx,
msd: MassStorageDevice,
@@ -97,6 +121,7 @@ class Server: # pylint: disable=too-many-instance-attributes
loop: asyncio.AbstractEventLoop,
) -> None:
+ self.__log = log
self.__hid = hid
self.__atx = atx
self.__msd = msd
@@ -125,6 +150,7 @@ class Server: # pylint: disable=too-many-instance-attributes
app = aiohttp.web.Application(loop=self.__loop)
app.router.add_get("/info", self.__info_handler)
+ app.router.add_get("/log", self.__log_handler)
app.router.add_get("/ws", self.__ws_handler)
@@ -154,7 +180,7 @@ class Server: # pylint: disable=too-many-instance-attributes
aiohttp.web.run_app(app, host=host, port=port, print=self.__run_app_print)
- # ===== INFO
+ # ===== SYSTEM
async def __info_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
return _json({
@@ -165,6 +191,20 @@ class Server: # pylint: disable=too-many-instance-attributes
"streamer": self.__streamer.get_app(),
})
+ @_wrap_exceptions_for_web("Log error")
+ async def __log_handler(self, request: aiohttp.web.Request) -> aiohttp.web.StreamResponse:
+ seek = _valid_int("seek", request.query.get("seek", "0"), 0)
+ follow = _valid_bool("follow", request.query.get("follow", "false"))
+ response = aiohttp.web.StreamResponse(status=200, reason="OK", headers={"Content-Type": "text/plain"})
+ await response.prepare(request)
+ async for record in self.__log.log(seek, follow):
+ await response.write(("[%s %s] --- %s" % (
+ record["dt"].strftime("%Y-%m-%d %H:%M:%S"),
+ record["service"],
+ record["msg"],
+ )).encode("utf-8") + b"\r\n")
+ return response
+
# ===== WEBSOCKET
async def __ws_handler(self, request: aiohttp.web.Request) -> aiohttp.web.WebSocketResponse:
@@ -243,7 +283,7 @@ class Server: # pylint: disable=too-many-instance-attributes
"reset": self.__atx.click_reset,
}.get(button)
if not clicker:
- raise BadRequest("Missing or invalid 'button=%s'" % (button))
+ raise BadRequest("Invalid param 'button'")
await self.__broadcast_event("atx_click", button=button) # type: ignore
await clicker()
await self.__broadcast_event("atx_click", button=None) # type: ignore
@@ -266,7 +306,7 @@ class Server: # pylint: disable=too-many-instance-attributes
state = self.__msd.get_state()
await self.__broadcast_event("msd_state", **state)
else:
- raise BadRequest("Missing or invalid 'to=%s'" % (to))
+ raise BadRequest("Invalid param 'to'")
return _json(state)
@_wrap_exceptions_for_web("Can't write data to mass-storage device")
@@ -314,13 +354,7 @@ class Server: # pylint: disable=too-many-instance-attributes
async def __streamer_set_params_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
quality = request.query.get("quality")
if quality:
- try:
- quality_int = int(quality)
- if not (1 <= quality_int <= 100):
- raise ValueError()
- except Exception:
- raise BadRequest("Invalid quality %r" % (quality))
- self.__streamer_quality = quality_int
+ self.__streamer_quality = _valid_int("quality", quality, 1, 100)
return _json()
async def __streamer_reset_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
diff --git a/requirements.txt b/requirements.txt
index 6ed9b5aa..6ca74376 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,3 +5,4 @@ pyudev
pyyaml
pyserial
setproctitle
+systemd-python
diff --git a/testenv/Dockerfile b/testenv/Dockerfile
index 6a16cf77..ce00df0a 100644
--- a/testenv/Dockerfile
+++ b/testenv/Dockerfile
@@ -36,6 +36,7 @@ RUN pacman -Syy \
&& user-packer -S --noconfirm \
python \
python-pip \
+ python-systemd \
nginx-mainline \
ustreamer \
socat \
diff --git a/testenv/kvmd.yaml b/testenv/kvmd.yaml
index df418139..cb981f0a 100644
--- a/testenv/kvmd.yaml
+++ b/testenv/kvmd.yaml
@@ -4,11 +4,15 @@ kvmd:
port: 8081
heartbeat: 3.0
+ log:
+ services:
+ - kvmd.service
+
hid:
pinout:
reset: 4
- device: /dev/ttyAMA0
+ device: /dev/ttyS10
speed: 115200
reset_delay: 0.1
diff --git a/testenv/requirements.txt b/testenv/requirements.txt
index de538a71..54109c60 100644
--- a/testenv/requirements.txt
+++ b/testenv/requirements.txt
@@ -5,5 +5,6 @@ pyudev
pyyaml
pyserial
setproctitle
+systemd-python
bumpversion
tox
diff --git a/web/index.html b/web/index.html
index 6b9979bb..8e49aed7 100644
--- a/web/index.html
+++ b/web/index.html
@@ -112,6 +112,10 @@
<button disabled data-force-hide-menu id="hid-reset-button">&bull; Reset keyboard &amp; mouse</button>
<button disabled data-force-hide-menu id="msd-reset-button">&bull; Reset mass storage</button>
</div>
+ <hr>
+ <div class="ctl-dropdown-content-buttons">
+ <button data-force-hide-menu onclick="window.open('kvmd/log?seek=3600&follow=1', '_blank');">&bull; View log</button>
+ </div>
</div>
</div>
</li>