diff options
author | Devaev Maxim <[email protected]> | 2019-11-29 01:35:38 +0300 |
---|---|---|
committer | Devaev Maxim <[email protected]> | 2019-11-29 01:35:38 +0300 |
commit | 3d8f16b9c60cc2ef8547e62f968b5faedf1d8094 (patch) | |
tree | 4addbab851f2ad68932d88249ea812ac74b1d957 /kvmd/apps | |
parent | 51e15d01c28556f47be849604ee5e52f18ffdd5d (diff) |
wake-on-lan back
Diffstat (limited to 'kvmd/apps')
-rw-r--r-- | kvmd/apps/__init__.py | 8 | ||||
-rw-r--r-- | kvmd/apps/kvmd/__init__.py | 2 | ||||
-rw-r--r-- | kvmd/apps/kvmd/server.py | 20 | ||||
-rw-r--r-- | kvmd/apps/kvmd/wol.py | 88 |
4 files changed, 117 insertions, 1 deletions
diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py index 1654ec4c..d06e09a3 100644 --- a/kvmd/apps/__init__.py +++ b/kvmd/apps/__init__.py @@ -66,7 +66,9 @@ from ..validators.os import valid_unix_mode from ..validators.os import valid_command from ..validators.net import valid_ip_or_host +from ..validators.net import valid_ip from ..validators.net import valid_port +from ..validators.net import valid_mac from ..validators.kvm import valid_stream_quality from ..validators.kvm import valid_stream_fps @@ -212,6 +214,12 @@ def _get_config_scheme() -> Dict: "extras": Option("/usr/share/kvmd/extras", type=valid_abs_dir, unpack_as="extras_path"), }, + "wol": { + "ip": Option("255.255.255.255", type=(lambda arg: valid_ip(arg, v6=False))), + "port": Option(9, type=valid_port), + "mac": Option("", type=(lambda arg: (valid_mac(arg) if arg else ""))), + }, + "hid": { "type": Option("", type=valid_stripped_string_not_empty), # Dynamic content diff --git a/kvmd/apps/kvmd/__init__.py b/kvmd/apps/kvmd/__init__.py index 05e17dd4..1b498a4e 100644 --- a/kvmd/apps/kvmd/__init__.py +++ b/kvmd/apps/kvmd/__init__.py @@ -36,6 +36,7 @@ from .. import init from .auth import AuthManager from .info import InfoManager from .logreader import LogReader +from .wol import WakeOnLan from .streamer import Streamer from .server import Server @@ -71,6 +72,7 @@ def main(argv: Optional[List[str]]=None) -> None: ), info_manager=InfoManager(**config.info._unpack()), log_reader=LogReader(), + wol=WakeOnLan(**config.wol._unpack()), hid=get_hid_class(config.hid.type)(**config.hid._unpack(ignore=["type"])), atx=get_atx_class(config.atx.type)(**config.atx._unpack(ignore=["type"])), diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py index b4e1ad22..bd40d76f 100644 --- a/kvmd/apps/kvmd/server.py +++ b/kvmd/apps/kvmd/server.py @@ -82,6 +82,9 @@ from .info import InfoManager from .logreader import LogReader from .streamer import Streamer +from .wol import WolDisabledError +from .wol import WakeOnLan + # ===== try: @@ -191,7 +194,7 @@ def _exposed(http_method: str, path: str, auth_required: bool=True) -> Callable: except (AtxIsBusyError, MsdIsBusyError) as err: return _json_exception(err, 409) - except (ValidatorError, AtxOperationError, MsdOperationError) as err: + except (ValidatorError, AtxOperationError, MsdOperationError, WolDisabledError) as err: return _json_exception(err, 400) except UnauthorizedError as err: return _json_exception(err, 401) @@ -222,6 +225,7 @@ def _system_task(method: Callable) -> Callable: class _Events(Enum): INFO_STATE = "info_state" + WOL_STATE = "wol_state" HID_STATE = "hid_state" ATX_STATE = "atx_state" MSD_STATE = "msd_state" @@ -234,6 +238,7 @@ class Server: # pylint: disable=too-many-instance-attributes auth_manager: AuthManager, info_manager: InfoManager, log_reader: LogReader, + wol: WakeOnLan, hid: BaseHid, atx: BaseAtx, @@ -244,6 +249,7 @@ class Server: # pylint: disable=too-many-instance-attributes self._auth_manager = auth_manager self.__info_manager = info_manager self.__log_reader = log_reader + self.__wol = wol self.__hid = hid self.__atx = atx @@ -355,6 +361,17 @@ class Server: # pylint: disable=too-many-instance-attributes )).encode("utf-8") + b"\r\n") return response + # ===== Wake-on-LAN + + @_exposed("GET", "/wol") + async def __wol_state_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response: + return _json(self.__wol.get_state()) + + @_exposed("POST", "/wol/wakeup") + async def __wol_wakeup_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response: + await self.__wol.wakeup() + return _json() + # ===== WEBSOCKET @_exposed("GET", "/ws") @@ -366,6 +383,7 @@ class Server: # pylint: disable=too-many-instance-attributes await self.__register_socket(ws) await asyncio.gather(*[ self.__broadcast_event(_Events.INFO_STATE, (await self.__make_info())), + self.__broadcast_event(_Events.WOL_STATE, self.__wol.get_state()), self.__broadcast_event(_Events.HID_STATE, self.__hid.get_state()), self.__broadcast_event(_Events.ATX_STATE, self.__atx.get_state()), self.__broadcast_event(_Events.MSD_STATE, (await self.__msd.get_state())), diff --git a/kvmd/apps/kvmd/wol.py b/kvmd/apps/kvmd/wol.py new file mode 100644 index 00000000..40c40d1d --- /dev/null +++ b/kvmd/apps/kvmd/wol.py @@ -0,0 +1,88 @@ +# ========================================================================== # +# # +# 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 socket + +from typing import Dict +from typing import Optional + +from ...logging import get_logger + +from ... import aiotools + + +# ===== +class WolDisabledError(Exception): + def __init__(self) -> None: + super().__init__("WoL is disabled") + + +# ===== +class WakeOnLan: + def __init__(self, ip: str, port: int, mac: str) -> None: + self.__ip = ip + self.__port = port + self.__mac = mac + self.__magic = b"" + + if mac: + assert len(mac) == 17, mac + self.__magic = bytes.fromhex("FF" * 6 + mac.replace(":", "") * 16) + + def get_state(self) -> Dict: + return { + "enabled": bool(self.__magic), + "target": { + "ip": self.__ip, + "port": self.__port, + "mac": self.__mac, + }, + } + + @aiotools.atomic + async def wakeup(self) -> None: + if not self.__magic: + raise WolDisabledError() + await self.__inner_wakeup() + + @aiotools.tasked + @aiotools.muted("Can't perform Wake-on-LAN or operation was not completed") + async def __inner_wakeup(self) -> None: + logger = get_logger(0) + logger.info("Waking up %s (%s:%s) using Wake-on-LAN ...", self.__mac, self.__ip, self.__port) + sock: Optional[socket.socket] = None + try: + # TODO: IPv6 support: http://lists.cluenet.de/pipermail/ipv6-ops/2014-September/010139.html + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.connect((self.__ip, self.__port)) + sock.send(self.__magic) + except Exception: + logger.exception("Can't send Wake-on-LAN packet") + else: + logger.info("Wake-on-LAN packet sent") + finally: + if sock: + try: + sock.close() + except Exception: + pass |