summaryrefslogtreecommitdiff
path: root/kvmd
diff options
context:
space:
mode:
authorDevaev Maxim <[email protected]>2019-11-29 01:35:38 +0300
committerDevaev Maxim <[email protected]>2019-11-29 01:35:38 +0300
commit3d8f16b9c60cc2ef8547e62f968b5faedf1d8094 (patch)
tree4addbab851f2ad68932d88249ea812ac74b1d957 /kvmd
parent51e15d01c28556f47be849604ee5e52f18ffdd5d (diff)
wake-on-lan back
Diffstat (limited to 'kvmd')
-rw-r--r--kvmd/apps/__init__.py8
-rw-r--r--kvmd/apps/kvmd/__init__.py2
-rw-r--r--kvmd/apps/kvmd/server.py20
-rw-r--r--kvmd/apps/kvmd/wol.py88
-rw-r--r--kvmd/validators/net.py20
5 files changed, 132 insertions, 6 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
diff --git a/kvmd/validators/net.py b/kvmd/validators/net.py
index ce875242..79d46a15 100644
--- a/kvmd/validators/net.py
+++ b/kvmd/validators/net.py
@@ -22,6 +22,8 @@
import socket
+from typing import List
+from typing import Callable
from typing import Any
from . import check_re_match
@@ -44,15 +46,18 @@ def valid_ip_or_host(arg: Any) -> str:
)
-def valid_ip(arg: Any) -> str:
+def valid_ip(arg: Any, v4: bool=True, v6: bool=True) -> str:
+ assert v4 or v6
+ validators: List[Callable] = []
+ if v4:
+ validators.append(lambda arg: (arg, socket.inet_pton(socket.AF_INET, arg))[0])
+ if v6:
+ validators.append(lambda arg: (arg, socket.inet_pton(socket.AF_INET6, arg))[0])
name = "IP address"
return check_any(
arg=valid_stripped_string_not_empty(arg, name),
name=name,
- validators=[
- lambda arg: (arg, socket.inet_pton(socket.AF_INET, arg))[0],
- lambda arg: (arg, socket.inet_pton(socket.AF_INET6, arg))[0],
- ],
+ validators=validators,
)
@@ -65,3 +70,8 @@ def valid_rfc_host(arg: Any) -> str:
def valid_port(arg: Any) -> int:
return int(valid_number(arg, min=0, max=65535, name="TCP/UDP port"))
+
+
+def valid_mac(arg: Any) -> str:
+ pattern = ":".join([r"[0-9a-fA-F]{2}"] * 6)
+ return check_re_match(arg, "MAC address", pattern).lower()