msd plugins

This commit is contained in:
Devaev Maxim
2019-09-12 01:53:25 +03:00
parent ca2eabc01f
commit ab7a16a4f7
9 changed files with 283 additions and 108 deletions

View File

@@ -25,7 +25,7 @@ kvmd:
reset_switch_pin: 27 reset_switch_pin: 27
msd: msd:
enabled: false type: none
streamer: streamer:
desired_fps: 30 desired_fps: 30

View File

@@ -25,7 +25,7 @@ kvmd:
reset_switch_pin: 27 reset_switch_pin: 27
msd: msd:
enabled: false type: none
streamer: streamer:
unix: /run/kvmd/ustreamer.sock unix: /run/kvmd/ustreamer.sock

View File

@@ -39,6 +39,7 @@ from ..plugins import UnknownPluginError
from ..plugins.auth import get_auth_service_class from ..plugins.auth import get_auth_service_class
from ..plugins.hid import get_hid_class from ..plugins.hid import get_hid_class
from ..plugins.atx import get_atx_class from ..plugins.atx import get_atx_class
from ..plugins.msd import get_msd_class
from ..yamlconf import ConfigError from ..yamlconf import ConfigError
from ..yamlconf import make_config 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_bool
from ..validators.basic import valid_number from ..validators.basic import valid_number
from ..validators.basic import valid_int_f1
from ..validators.basic import valid_float_f01 from ..validators.basic import valid_float_f01
from ..validators.auth import valid_users_list 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_quality
from ..validators.kvm import valid_stream_fps from ..validators.kvm import valid_stream_fps
from ..validators.hw import valid_gpio_pin
from ..validators.hw import valid_gpio_pin_optional 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"]["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"]["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) config = make_config(raw_config, scheme)
@@ -189,17 +189,7 @@ def _get_config_scheme(sections: List[str]) -> Dict:
}, },
"msd": { "msd": {
"enabled": Option(True, type=valid_bool), "type": Option("relay"),
"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))),
}, },
"streamer": { "streamer": {

View File

@@ -47,20 +47,25 @@ def main(argv: Optional[List[str]]=None) -> None:
logger.info("Cleaning up ...") logger.info("Cleaning up ...")
with gpio.bcm(): 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 []), ] if config.hid.type == "tty" else []),
*([ *([
("atx_power_switch_pin", config.atx.power_switch_pin, True), ("gpio_atx_power_switch_pin", config.atx.power_switch_pin),
("atx_reset_switch_pin", config.atx.reset_switch_pin, True), ("gpio_atx_reset_switch_pin", config.atx.reset_switch_pin),
] if config.atx.type == "gpio" else []), ] 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), ("relay_msd_target_pin", config.msd.target_pin),
("streamer_conv_pin", config.streamer.conv_pin, True), ("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) logger.info("Writing value=0 to GPIO pin=%d (%s)", pin, name)
try: try:
gpio.set_output(pin, initial=False) gpio.set_output(pin, initial=False)

View File

@@ -29,13 +29,13 @@ from ... import gpio
from ...plugins.hid import get_hid_class from ...plugins.hid import get_hid_class
from ...plugins.atx import get_atx_class from ...plugins.atx import get_atx_class
from ...plugins.msd import get_msd_class
from .. import init from .. import init
from .auth import AuthManager from .auth import AuthManager
from .info import InfoManager from .info import InfoManager
from .logreader import LogReader from .logreader import LogReader
from .msd import MassStorageDevice
from .streamer import Streamer from .streamer import Streamer
from .server import Server 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"])), hid=get_hid_class(config.hid.type)(**config.hid._unpack(ignore=["type"])),
atx=get_atx_class(config.atx.type)(**config.atx._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()), streamer=Streamer(**config.streamer._unpack()),
).run(**config.server._unpack()) ).run(**config.server._unpack())

View File

@@ -49,6 +49,9 @@ from ...plugins.hid import BaseHid
from ...plugins.atx import AtxOperationError from ...plugins.atx import AtxOperationError
from ...plugins.atx import BaseAtx from ...plugins.atx import BaseAtx
from ...plugins.msd import MsdOperationError
from ...plugins.msd import BaseMsd
from ...validators import ValidatorError from ...validators import ValidatorError
from ...validators.basic import valid_bool from ...validators.basic import valid_bool
@@ -75,8 +78,6 @@ from ... import __version__
from .auth import AuthManager from .auth import AuthManager
from .info import InfoManager from .info import InfoManager
from .logreader import LogReader from .logreader import LogReader
from .msd import MsdOperationError
from .msd import MassStorageDevice
from .streamer import Streamer from .streamer import Streamer
@@ -234,7 +235,7 @@ class Server: # pylint: disable=too-many-instance-attributes
hid: BaseHid, hid: BaseHid,
atx: BaseAtx, atx: BaseAtx,
msd: MassStorageDevice, msd: BaseMsd,
streamer: Streamer, streamer: Streamer,
) -> None: ) -> None:
@@ -486,8 +487,9 @@ class Server: # pylint: disable=too-many-instance-attributes
logger.info("Writing image %r to MSD ...", image_name) logger.info("Writing image %r to MSD ...", image_name)
await self.__msd.write_image_info(image_name, False) await self.__msd.write_image_info(image_name, False)
chunk_size = self.__msd.get_chunk_size()
while True: while True:
chunk = await data_field.read_chunk(self.__msd.chunk_size) chunk = await data_field.read_chunk(chunk_size)
if not chunk: if not chunk:
break break
written = await self.__msd.write_image_chunk(chunk) written = await self.__msd.write_image_chunk(chunk)

View File

@@ -0,0 +1,113 @@
# ========================================================================== #
# #
# 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 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

86
kvmd/plugins/msd/none.py Normal file
View File

@@ -0,0 +1,86 @@
# ========================================================================== #
# #
# 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
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()

View File

@@ -46,43 +46,24 @@ from ... import aiotools
from ... import aioregion from ... import aioregion
from ... import gpio from ... import gpio
from ...yamlconf import Option
# ===== from ...validators.basic import valid_bool
class MsdError(Exception): from ...validators.basic import valid_number
pass from ...validators.basic import valid_int_f1
from ...validators.basic import valid_float_f01
from ...validators.os import valid_abs_path
class MsdOperationError(MsdError): from ...validators.hw import valid_gpio_pin
pass
from . import MsdError
class MsdDisabledError(MsdOperationError): from . import MsdOfflineError
def __init__(self) -> None: from . import MsdAlreadyOnServerError
super().__init__("MSD is disabled") from . import MsdAlreadyOnKvmError
from . import MsdNotOnKvmError
from . import MsdIsBusyError
class MsdOfflineError(MsdOperationError): from . import BaseMsd
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
# ===== # =====
@@ -94,7 +75,7 @@ class _ImageInfo:
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class _MassStorageDeviceInfo: class _DeviceInfo:
path: str path: str
real: str real: str
size: int size: int
@@ -154,7 +135,7 @@ def _ioctl_uint32(device_file: IO, request: int) -> int:
return result 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): if not stat.S_ISBLK(os.stat(device_path).st_mode):
raise RuntimeError(f"Not a block device: {device_path}") 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) device_file.seek(size - _IMAGE_INFO_SIZE)
image_info = _parse_image_info_bytes(device_file.read()) image_info = _parse_image_info_bytes(device_file.read())
return _MassStorageDeviceInfo( return _DeviceInfo(
path=device_path, path=device_path,
real=os.path.realpath(device_path), real=os.path.realpath(device_path),
size=size, size=size,
@@ -173,20 +154,16 @@ def _explore_device(device_path: str) -> _MassStorageDeviceInfo:
def _msd_working(method: Callable) -> Callable: def _msd_working(method: Callable) -> Callable:
async def wrapper(self: "MassStorageDevice", *args: Any, **kwargs: Any) -> Any: async def wrapper(self: "Plugin", *args: Any, **kwargs: Any) -> Any:
if not self._enabled: # pylint: disable=protected-access
raise MsdDisabledError()
if not self._device_info: # pylint: disable=protected-access if not self._device_info: # pylint: disable=protected-access
raise MsdOfflineError() raise MsdOfflineError()
return (await method(self, *args, **kwargs)) return (await method(self, *args, **kwargs))
return wrapper return wrapper
class MassStorageDevice: # pylint: disable=too-many-instance-attributes class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
def __init__( def __init__( # pylint: disable=super-init-not-called
self, self,
enabled: bool,
target_pin: int, target_pin: int,
reset_pin: int, reset_pin: int,
@@ -198,26 +175,19 @@ class MassStorageDevice: # pylint: disable=too-many-instance-attributes
chunk_size: int, chunk_size: int,
) -> None: ) -> None:
self._enabled = enabled self.__target_pin = gpio.set_output(target_pin)
self.__reset_pin = gpio.set_output(reset_pin)
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.__device_path = device_path self.__device_path = device_path
self.__init_delay = init_delay self.__init_delay = init_delay
self.__init_retries = init_retries self.__init_retries = init_retries
self.__reset_delay = reset_delay self.__reset_delay = reset_delay
self.__write_meta = write_meta self.__write_meta = write_meta
self.chunk_size = chunk_size self.__chunk_size = chunk_size
self.__region = aioregion.AioExclusiveRegion(MsdIsBusyError) 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.__device_file: Optional[aiofiles.base.AiofilesContextManager] = None
self.__written = 0 self.__written = 0
self.__on_kvm = True self.__on_kvm = True
@@ -225,22 +195,33 @@ class MassStorageDevice: # pylint: disable=too-many-instance-attributes
self.__state_queue: asyncio.queues.Queue = asyncio.Queue() self.__state_queue: asyncio.queues.Queue = asyncio.Queue()
logger = get_logger(0) logger = get_logger(0)
if self._enabled: logger.info("Using %r as MSD", self.__device_path)
logger.info("Using %r as MSD", self.__device_path) try:
try: aiotools.run_sync(self.__load_device_info())
aiotools.run_sync(self.__load_device_info()) if self.__write_meta:
if self.__write_meta: logger.info("Enabled image metadata writing")
logger.info("Enabled image metadata writing") except Exception as err:
except Exception as err: log = (logger.error if isinstance(err, MsdError) else logger.exception)
log = (logger.error if isinstance(err, MsdError) else logger.exception) log("MSD is offline: %s", err)
log("MSD is offline: %s", err)
else: @classmethod
logger.info("MSD is disabled") 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: def get_state(self) -> Dict:
online = (self._enabled and bool(self._device_info)) online = bool(self._device_info)
return { return {
"enabled": self._enabled, "enabled": True,
"online": online, "online": online,
"busy": self.__region.is_busy(), "busy": self.__region.is_busy(),
"uploading": bool(self.__device_file), "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]: async def poll_state(self) -> AsyncGenerator[Dict, None]:
while True: while True:
if self._enabled: yield (await self.__state_queue.get())
yield (await self.__state_queue.get())
else:
await asyncio.sleep(60)
@aiotools.atomic @aiotools.atomic
async def cleanup(self) -> None: async def cleanup(self) -> None:
if self._enabled: await self.__close_device_file()
await self.__close_device_file() gpio.write(self.__target_pin, False)
gpio.write(self.__target_pin, False) gpio.write(self.__reset_pin, False)
gpio.write(self.__reset_pin, False)
@_msd_working @_msd_working
@aiotools.atomic @aiotools.atomic
@@ -313,8 +290,6 @@ class MassStorageDevice: # pylint: disable=too-many-instance-attributes
@aiotools.atomic @aiotools.atomic
async def reset(self) -> None: async def reset(self) -> None:
if not self._enabled:
raise MsdDisabledError()
with aiotools.unregion_only_on_exception(self.__region): with aiotools.unregion_only_on_exception(self.__region):
await self.__inner_reset() await self.__inner_reset()
@@ -342,7 +317,7 @@ class MassStorageDevice: # pylint: disable=too-many-instance-attributes
@_msd_working @_msd_working
@aiotools.atomic @aiotools.atomic
async def __aenter__(self) -> "MassStorageDevice": async def __aenter__(self) -> "Plugin":
assert self._device_info assert self._device_info
self.__region.enter() self.__region.enter()
try: try:
@@ -357,6 +332,9 @@ class MassStorageDevice: # pylint: disable=too-many-instance-attributes
finally: finally:
await self.__state_queue.put(self.get_state()) await self.__state_queue.put(self.get_state())
def get_chunk_size(self) -> int:
return self.__chunk_size
@aiotools.atomic @aiotools.atomic
async def write_image_info(self, name: str, complete: bool) -> None: async def write_image_info(self, name: str, complete: bool) -> None:
assert self.__device_file assert self.__device_file
@@ -382,6 +360,7 @@ class MassStorageDevice: # pylint: disable=too-many-instance-attributes
_exc: BaseException, _exc: BaseException,
_tb: types.TracebackType, _tb: types.TracebackType,
) -> None: ) -> None:
try: try:
await self.__close_device_file() await self.__close_device_file()
await self.__load_device_info() await self.__load_device_info()