diff options
author | Devaev Maxim <[email protected]> | 2019-09-12 01:53:25 +0300 |
---|---|---|
committer | Devaev Maxim <[email protected]> | 2019-09-12 01:53:25 +0300 |
commit | ab7a16a4f7afb2393f69a7ac9b3f5cea7c3ed4d6 (patch) | |
tree | f94b97528ba2ffc18dacc2473908f0759e0e7329 | |
parent | ca2eabc01f7294bd6fe35c734580ada91a2230a1 (diff) |
msd plugins
-rw-r--r-- | configs/kvmd/main/v0-hdmi.yaml | 2 | ||||
-rw-r--r-- | configs/kvmd/main/v0-vga.yaml | 2 | ||||
-rw-r--r-- | kvmd/apps/__init__.py | 16 | ||||
-rw-r--r-- | kvmd/apps/cleanup/__init__.py | 23 | ||||
-rw-r--r-- | kvmd/apps/kvmd/__init__.py | 4 | ||||
-rw-r--r-- | kvmd/apps/kvmd/server.py | 10 | ||||
-rw-r--r-- | kvmd/plugins/msd/__init__.py | 113 | ||||
-rw-r--r-- | kvmd/plugins/msd/none.py | 86 | ||||
-rw-r--r-- | kvmd/plugins/msd/relay.py (renamed from kvmd/apps/kvmd/msd.py) | 135 |
9 files changed, 283 insertions, 108 deletions
diff --git a/configs/kvmd/main/v0-hdmi.yaml b/configs/kvmd/main/v0-hdmi.yaml index 2271c757..44469158 100644 --- a/configs/kvmd/main/v0-hdmi.yaml +++ b/configs/kvmd/main/v0-hdmi.yaml @@ -25,7 +25,7 @@ kvmd: reset_switch_pin: 27 msd: - enabled: false + type: none streamer: desired_fps: 30 diff --git a/configs/kvmd/main/v0-vga.yaml b/configs/kvmd/main/v0-vga.yaml index 9f2d895c..0d510743 100644 --- a/configs/kvmd/main/v0-vga.yaml +++ b/configs/kvmd/main/v0-vga.yaml @@ -25,7 +25,7 @@ kvmd: reset_switch_pin: 27 msd: - enabled: false + type: none streamer: unix: /run/kvmd/ustreamer.sock diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py index fd4ddec7..60df70fc 100644 --- a/kvmd/apps/__init__.py +++ b/kvmd/apps/__init__.py @@ -39,6 +39,7 @@ from ..plugins import UnknownPluginError from ..plugins.auth import get_auth_service_class from ..plugins.hid import get_hid_class from ..plugins.atx import get_atx_class +from ..plugins.msd import get_msd_class from ..yamlconf import ConfigError from ..yamlconf import make_config @@ -50,7 +51,6 @@ from ..yamlconf.loader import load_yaml_file from ..validators.basic import valid_bool from ..validators.basic import valid_number -from ..validators.basic import valid_int_f1 from ..validators.basic import valid_float_f01 from ..validators.auth import valid_users_list @@ -66,7 +66,6 @@ from ..validators.net import valid_port from ..validators.kvm import valid_stream_quality from ..validators.kvm import valid_stream_fps -from ..validators.hw import valid_gpio_pin from ..validators.hw import valid_gpio_pin_optional @@ -119,6 +118,7 @@ def _init_config(config_path: str, sections: List[str], override_options: List[s scheme["kvmd"]["hid"].update(get_hid_class(config.kvmd.hid.type).get_plugin_options()) scheme["kvmd"]["atx"].update(get_atx_class(config.kvmd.atx.type).get_plugin_options()) + scheme["kvmd"]["msd"].update(get_msd_class(config.kvmd.msd.type).get_plugin_options()) config = make_config(raw_config, scheme) @@ -189,17 +189,7 @@ def _get_config_scheme(sections: List[str]) -> Dict: }, "msd": { - "enabled": Option(True, type=valid_bool), - - "target_pin": Option(-1, type=valid_gpio_pin, only_if="enabled"), - "reset_pin": Option(-1, type=valid_gpio_pin, only_if="enabled"), - - "device": Option("", type=valid_abs_path, only_if="enabled", unpack_as="device_path"), - "init_delay": Option(1.0, type=valid_float_f01), - "init_retries": Option(5, type=valid_int_f1), - "reset_delay": Option(1.0, type=valid_float_f01), - "write_meta": Option(True, type=valid_bool), - "chunk_size": Option(65536, type=(lambda arg: valid_number(arg, min=1024))), + "type": Option("relay"), }, "streamer": { diff --git a/kvmd/apps/cleanup/__init__.py b/kvmd/apps/cleanup/__init__.py index 680f080b..d7790b67 100644 --- a/kvmd/apps/cleanup/__init__.py +++ b/kvmd/apps/cleanup/__init__.py @@ -47,20 +47,25 @@ def main(argv: Optional[List[str]]=None) -> None: logger.info("Cleaning up ...") with gpio.bcm(): - for (name, pin, enabled) in [ + for (name, pin) in [ *([ - ("hid_reset_pin", config.hid.reset_pin, True), + ("tty_hid_reset_pin", config.hid.reset_pin), ] if config.hid.type == "tty" else []), + *([ - ("atx_power_switch_pin", config.atx.power_switch_pin, True), - ("atx_reset_switch_pin", config.atx.reset_switch_pin, True), + ("gpio_atx_power_switch_pin", config.atx.power_switch_pin), + ("gpio_atx_reset_switch_pin", config.atx.reset_switch_pin), ] if config.atx.type == "gpio" else []), - ("msd_target_pin", config.msd.target_pin, config.msd.enabled), - ("msd_reset_pin", config.msd.reset_pin, config.msd.enabled), - ("streamer_cap_pin", config.streamer.cap_pin, True), - ("streamer_conv_pin", config.streamer.conv_pin, True), + + *([ + ("relay_msd_target_pin", config.msd.target_pin), + ("relay_msd_reset_pin", config.msd.reset_pin), + ] if config.msd.type == "relay" else []), + + ("streamer_cap_pin", config.streamer.cap_pin), + ("streamer_conv_pin", config.streamer.conv_pin), ]: - if enabled and pin >= 0: + if pin >= 0: logger.info("Writing value=0 to GPIO pin=%d (%s)", pin, name) try: gpio.set_output(pin, initial=False) diff --git a/kvmd/apps/kvmd/__init__.py b/kvmd/apps/kvmd/__init__.py index bc26e442..f44dc669 100644 --- a/kvmd/apps/kvmd/__init__.py +++ b/kvmd/apps/kvmd/__init__.py @@ -29,13 +29,13 @@ from ... import gpio from ...plugins.hid import get_hid_class from ...plugins.atx import get_atx_class +from ...plugins.msd import get_msd_class from .. import init from .auth import AuthManager from .info import InfoManager from .logreader import LogReader -from .msd import MassStorageDevice from .streamer import Streamer from .server import Server @@ -64,7 +64,7 @@ def main(argv: Optional[List[str]]=None) -> None: hid=get_hid_class(config.hid.type)(**config.hid._unpack(ignore=["type"])), atx=get_atx_class(config.atx.type)(**config.atx._unpack(ignore=["type"])), - msd=MassStorageDevice(**config.msd._unpack()), + msd=get_msd_class(config.msd.type)(**config.msd._unpack(ignore=["type"])), streamer=Streamer(**config.streamer._unpack()), ).run(**config.server._unpack()) diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py index 21e30f0f..a0c14224 100644 --- a/kvmd/apps/kvmd/server.py +++ b/kvmd/apps/kvmd/server.py @@ -49,6 +49,9 @@ from ...plugins.hid import BaseHid from ...plugins.atx import AtxOperationError from ...plugins.atx import BaseAtx +from ...plugins.msd import MsdOperationError +from ...plugins.msd import BaseMsd + from ...validators import ValidatorError from ...validators.basic import valid_bool @@ -75,8 +78,6 @@ from ... import __version__ from .auth import AuthManager from .info import InfoManager from .logreader import LogReader -from .msd import MsdOperationError -from .msd import MassStorageDevice from .streamer import Streamer @@ -234,7 +235,7 @@ class Server: # pylint: disable=too-many-instance-attributes hid: BaseHid, atx: BaseAtx, - msd: MassStorageDevice, + msd: BaseMsd, streamer: Streamer, ) -> None: @@ -486,8 +487,9 @@ class Server: # pylint: disable=too-many-instance-attributes logger.info("Writing image %r to MSD ...", image_name) await self.__msd.write_image_info(image_name, False) + chunk_size = self.__msd.get_chunk_size() while True: - chunk = await data_field.read_chunk(self.__msd.chunk_size) + chunk = await data_field.read_chunk(chunk_size) if not chunk: break written = await self.__msd.write_image_chunk(chunk) diff --git a/kvmd/plugins/msd/__init__.py b/kvmd/plugins/msd/__init__.py new file mode 100644 index 00000000..64d339eb --- /dev/null +++ b/kvmd/plugins/msd/__init__.py @@ -0,0 +1,113 @@ +# ========================================================================== # +# # +# KVMD - The main Pi-KVM daemon. # +# # +# Copyright (C) 2018 Maxim Devaev <[email protected]> # +# # +# 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 types + +from typing import Dict +from typing import Type +from typing import AsyncGenerator + +from ... import aioregion + +from .. import BasePlugin +from .. import get_plugin_class + + +# ===== +class MsdError(Exception): + pass + + +class MsdOperationError(MsdError): + pass + + +class MsdOfflineError(MsdOperationError): + def __init__(self) -> None: + super().__init__("MSD is not found") + + +class MsdAlreadyOnServerError(MsdOperationError): + def __init__(self) -> None: + super().__init__("MSD is already connected to Server") + + +class MsdAlreadyOnKvmError(MsdOperationError): + def __init__(self) -> None: + super().__init__("MSD is already connected to KVM") + + +class MsdNotOnKvmError(MsdOperationError): + def __init__(self) -> None: + super().__init__("MSD is not connected to KVM") + + +class MsdIsBusyError(MsdOperationError, aioregion.RegionIsBusyError): + pass + + +# ===== +class BaseMsd(BasePlugin): + def get_state(self) -> Dict: + raise NotImplementedError + + async def poll_state(self) -> AsyncGenerator[Dict, None]: + yield {} + raise NotImplementedError + + async def cleanup(self) -> None: + pass + + async def connect_to_kvm(self) -> Dict: + raise NotImplementedError + + async def connect_to_server(self) -> Dict: + raise NotImplementedError + + async def reset(self) -> None: + raise NotImplementedError + + async def __aenter__(self) -> "BaseMsd": + raise NotImplementedError + + def get_chunk_size(self) -> int: + raise NotImplementedError + + async def write_image_info(self, name: str, complete: bool) -> None: + raise NotImplementedError + + async def write_image_chunk(self, chunk: bytes) -> int: + raise NotImplementedError + + async def __aexit__( + self, + _exc_type: Type[BaseException], + _exc: BaseException, + _tb: types.TracebackType, + ) -> None: + + raise NotImplementedError + + +# ===== +def get_msd_class(name: str) -> Type[BaseMsd]: + return get_plugin_class("msd", (name or "none")) # type: ignore diff --git a/kvmd/plugins/msd/none.py b/kvmd/plugins/msd/none.py new file mode 100644 index 00000000..dab0f406 --- /dev/null +++ b/kvmd/plugins/msd/none.py @@ -0,0 +1,86 @@ +# ========================================================================== # +# # +# KVMD - The main Pi-KVM daemon. # +# # +# Copyright (C) 2018 Maxim Devaev <[email protected]> # +# # +# 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 +import types + +from typing import Dict +from typing import Type +from typing import AsyncGenerator + +from . import MsdOperationError +from . import BaseMsd + + +# ===== +class MsdDisabledError(MsdOperationError): + def __init__(self) -> None: + super().__init__("MSD is disabled") + + +# ===== +class Plugin(BaseMsd): + def get_state(self) -> Dict: + return { + "enabled": False, + "online": False, + "busy": False, + "uploading": False, + "written": False, + "info": None, + "connected_to": None, + } + + async def poll_state(self) -> AsyncGenerator[Dict, None]: + while True: + yield self.get_state() + await asyncio.sleep(60) + + async def connect_to_kvm(self) -> Dict: + raise MsdDisabledError() + + async def connect_to_server(self) -> Dict: + raise MsdDisabledError() + + async def reset(self) -> None: + raise MsdDisabledError() + + async def __aenter__(self) -> BaseMsd: + raise MsdDisabledError() + + def get_chunk_size(self) -> int: + raise MsdDisabledError() + + async def write_image_info(self, name: str, complete: bool) -> None: + raise MsdDisabledError() + + async def write_image_chunk(self, chunk: bytes) -> int: + raise MsdDisabledError() + + async def __aexit__( + self, + _exc_type: Type[BaseException], + _exc: BaseException, + _tb: types.TracebackType, + ) -> None: + + raise MsdDisabledError() diff --git a/kvmd/apps/kvmd/msd.py b/kvmd/plugins/msd/relay.py index cb26e520..8c9285de 100644 --- a/kvmd/apps/kvmd/msd.py +++ b/kvmd/plugins/msd/relay.py @@ -46,43 +46,24 @@ from ... import aiotools from ... import aioregion from ... import gpio +from ...yamlconf import Option -# ===== -class MsdError(Exception): - pass - - -class MsdOperationError(MsdError): - pass - - -class MsdDisabledError(MsdOperationError): - def __init__(self) -> None: - super().__init__("MSD is disabled") - - -class MsdOfflineError(MsdOperationError): - def __init__(self) -> None: - super().__init__("MSD is not found") - - -class MsdAlreadyOnServerError(MsdOperationError): - def __init__(self) -> None: - super().__init__("MSD is already connected to Server") - - -class MsdAlreadyOnKvmError(MsdOperationError): - def __init__(self) -> None: - super().__init__("MSD is already connected to KVM") - +from ...validators.basic import valid_bool +from ...validators.basic import valid_number +from ...validators.basic import valid_int_f1 +from ...validators.basic import valid_float_f01 -class MsdNotOnKvmError(MsdOperationError): - def __init__(self) -> None: - super().__init__("MSD is not connected to KVM") +from ...validators.os import valid_abs_path +from ...validators.hw import valid_gpio_pin -class MsdIsBusyError(MsdOperationError, aioregion.RegionIsBusyError): - pass +from . import MsdError +from . import MsdOfflineError +from . import MsdAlreadyOnServerError +from . import MsdAlreadyOnKvmError +from . import MsdNotOnKvmError +from . import MsdIsBusyError +from . import BaseMsd # ===== @@ -94,7 +75,7 @@ class _ImageInfo: @dataclasses.dataclass(frozen=True) -class _MassStorageDeviceInfo: +class _DeviceInfo: path: str real: str size: int @@ -154,7 +135,7 @@ def _ioctl_uint32(device_file: IO, request: int) -> int: return result -def _explore_device(device_path: str) -> _MassStorageDeviceInfo: +def _explore_device(device_path: str) -> _DeviceInfo: if not stat.S_ISBLK(os.stat(device_path).st_mode): raise RuntimeError(f"Not a block device: {device_path}") @@ -164,7 +145,7 @@ def _explore_device(device_path: str) -> _MassStorageDeviceInfo: device_file.seek(size - _IMAGE_INFO_SIZE) image_info = _parse_image_info_bytes(device_file.read()) - return _MassStorageDeviceInfo( + return _DeviceInfo( path=device_path, real=os.path.realpath(device_path), size=size, @@ -173,20 +154,16 @@ def _explore_device(device_path: str) -> _MassStorageDeviceInfo: def _msd_working(method: Callable) -> Callable: - async def wrapper(self: "MassStorageDevice", *args: Any, **kwargs: Any) -> Any: - if not self._enabled: # pylint: disable=protected-access - raise MsdDisabledError() + async def wrapper(self: "Plugin", *args: Any, **kwargs: Any) -> Any: if not self._device_info: # pylint: disable=protected-access raise MsdOfflineError() return (await method(self, *args, **kwargs)) return wrapper -class MassStorageDevice: # pylint: disable=too-many-instance-attributes - def __init__( +class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes + def __init__( # pylint: disable=super-init-not-called self, - enabled: bool, - target_pin: int, reset_pin: int, @@ -198,26 +175,19 @@ class MassStorageDevice: # pylint: disable=too-many-instance-attributes chunk_size: int, ) -> None: - self._enabled = enabled - - if self._enabled: - self.__target_pin = gpio.set_output(target_pin) - self.__reset_pin = gpio.set_output(reset_pin) - assert bool(device_path) - else: - self.__target_pin = -1 - self.__reset_pin = -1 + self.__target_pin = gpio.set_output(target_pin) + self.__reset_pin = gpio.set_output(reset_pin) self.__device_path = device_path self.__init_delay = init_delay self.__init_retries = init_retries self.__reset_delay = reset_delay self.__write_meta = write_meta - self.chunk_size = chunk_size + self.__chunk_size = chunk_size self.__region = aioregion.AioExclusiveRegion(MsdIsBusyError) - self._device_info: Optional[_MassStorageDeviceInfo] = None + self._device_info: Optional[_DeviceInfo] = None self.__device_file: Optional[aiofiles.base.AiofilesContextManager] = None self.__written = 0 self.__on_kvm = True @@ -225,22 +195,33 @@ class MassStorageDevice: # pylint: disable=too-many-instance-attributes self.__state_queue: asyncio.queues.Queue = asyncio.Queue() logger = get_logger(0) - if self._enabled: - logger.info("Using %r as MSD", self.__device_path) - try: - aiotools.run_sync(self.__load_device_info()) - if self.__write_meta: - logger.info("Enabled image metadata writing") - except Exception as err: - log = (logger.error if isinstance(err, MsdError) else logger.exception) - log("MSD is offline: %s", err) - else: - logger.info("MSD is disabled") + logger.info("Using %r as MSD", self.__device_path) + try: + aiotools.run_sync(self.__load_device_info()) + if self.__write_meta: + logger.info("Enabled image metadata writing") + except Exception as err: + log = (logger.error if isinstance(err, MsdError) else logger.exception) + log("MSD is offline: %s", err) + + @classmethod + def get_plugin_options(cls) -> Dict[str, Option]: + return { + "target_pin": Option(-1, type=valid_gpio_pin), + "reset_pin": Option(-1, type=valid_gpio_pin), + + "device": Option("", type=valid_abs_path, unpack_as="device_path"), + "init_delay": Option(1.0, type=valid_float_f01), + "init_retries": Option(5, type=valid_int_f1), + "reset_delay": Option(1.0, type=valid_float_f01), + "write_meta": Option(True, type=valid_bool), + "chunk_size": Option(65536, type=(lambda arg: valid_number(arg, min=1024))), + } def get_state(self) -> Dict: - online = (self._enabled and bool(self._device_info)) + online = bool(self._device_info) return { - "enabled": self._enabled, + "enabled": True, "online": online, "busy": self.__region.is_busy(), "uploading": bool(self.__device_file), @@ -251,17 +232,13 @@ class MassStorageDevice: # pylint: disable=too-many-instance-attributes async def poll_state(self) -> AsyncGenerator[Dict, None]: while True: - if self._enabled: - yield (await self.__state_queue.get()) - else: - await asyncio.sleep(60) + yield (await self.__state_queue.get()) @aiotools.atomic async def cleanup(self) -> None: - if self._enabled: - await self.__close_device_file() - gpio.write(self.__target_pin, False) - gpio.write(self.__reset_pin, False) + await self.__close_device_file() + gpio.write(self.__target_pin, False) + gpio.write(self.__reset_pin, False) @_msd_working @aiotools.atomic @@ -313,8 +290,6 @@ class MassStorageDevice: # pylint: disable=too-many-instance-attributes @aiotools.atomic async def reset(self) -> None: - if not self._enabled: - raise MsdDisabledError() with aiotools.unregion_only_on_exception(self.__region): await self.__inner_reset() @@ -342,7 +317,7 @@ class MassStorageDevice: # pylint: disable=too-many-instance-attributes @_msd_working @aiotools.atomic - async def __aenter__(self) -> "MassStorageDevice": + async def __aenter__(self) -> "Plugin": assert self._device_info self.__region.enter() try: @@ -357,6 +332,9 @@ class MassStorageDevice: # pylint: disable=too-many-instance-attributes finally: await self.__state_queue.put(self.get_state()) + def get_chunk_size(self) -> int: + return self.__chunk_size + @aiotools.atomic async def write_image_info(self, name: str, complete: bool) -> None: assert self.__device_file @@ -382,6 +360,7 @@ class MassStorageDevice: # pylint: disable=too-many-instance-attributes _exc: BaseException, _tb: types.TracebackType, ) -> None: + try: await self.__close_device_file() await self.__load_device_info() |