mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 00:51:53 +08:00
wake-on-lan back
This commit is contained in:
@@ -8,6 +8,7 @@ Group=kvmd
|
|||||||
Type=simple
|
Type=simple
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
|
AmbientCapabilities=CAP_NET_RAW
|
||||||
|
|
||||||
ExecStart=/usr/bin/kvmd
|
ExecStart=/usr/bin/kvmd
|
||||||
ExecStopPost=/usr/bin/kvmd-cleanup
|
ExecStopPost=/usr/bin/kvmd-cleanup
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ from ..validators.os import valid_unix_mode
|
|||||||
from ..validators.os import valid_command
|
from ..validators.os import valid_command
|
||||||
|
|
||||||
from ..validators.net import valid_ip_or_host
|
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_port
|
||||||
|
from ..validators.net import valid_mac
|
||||||
|
|
||||||
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
|
||||||
@@ -212,6 +214,12 @@ def _get_config_scheme() -> Dict:
|
|||||||
"extras": Option("/usr/share/kvmd/extras", type=valid_abs_dir, unpack_as="extras_path"),
|
"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": {
|
"hid": {
|
||||||
"type": Option("", type=valid_stripped_string_not_empty),
|
"type": Option("", type=valid_stripped_string_not_empty),
|
||||||
# Dynamic content
|
# Dynamic content
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ 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 .wol import WakeOnLan
|
||||||
from .streamer import Streamer
|
from .streamer import Streamer
|
||||||
from .server import Server
|
from .server import Server
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ def main(argv: Optional[List[str]]=None) -> None:
|
|||||||
),
|
),
|
||||||
info_manager=InfoManager(**config.info._unpack()),
|
info_manager=InfoManager(**config.info._unpack()),
|
||||||
log_reader=LogReader(),
|
log_reader=LogReader(),
|
||||||
|
wol=WakeOnLan(**config.wol._unpack()),
|
||||||
|
|
||||||
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"])),
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ from .info import InfoManager
|
|||||||
from .logreader import LogReader
|
from .logreader import LogReader
|
||||||
from .streamer import Streamer
|
from .streamer import Streamer
|
||||||
|
|
||||||
|
from .wol import WolDisabledError
|
||||||
|
from .wol import WakeOnLan
|
||||||
|
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
try:
|
try:
|
||||||
@@ -191,7 +194,7 @@ def _exposed(http_method: str, path: str, auth_required: bool=True) -> Callable:
|
|||||||
|
|
||||||
except (AtxIsBusyError, MsdIsBusyError) as err:
|
except (AtxIsBusyError, MsdIsBusyError) as err:
|
||||||
return _json_exception(err, 409)
|
return _json_exception(err, 409)
|
||||||
except (ValidatorError, AtxOperationError, MsdOperationError) as err:
|
except (ValidatorError, AtxOperationError, MsdOperationError, WolDisabledError) as err:
|
||||||
return _json_exception(err, 400)
|
return _json_exception(err, 400)
|
||||||
except UnauthorizedError as err:
|
except UnauthorizedError as err:
|
||||||
return _json_exception(err, 401)
|
return _json_exception(err, 401)
|
||||||
@@ -222,6 +225,7 @@ def _system_task(method: Callable) -> Callable:
|
|||||||
|
|
||||||
class _Events(Enum):
|
class _Events(Enum):
|
||||||
INFO_STATE = "info_state"
|
INFO_STATE = "info_state"
|
||||||
|
WOL_STATE = "wol_state"
|
||||||
HID_STATE = "hid_state"
|
HID_STATE = "hid_state"
|
||||||
ATX_STATE = "atx_state"
|
ATX_STATE = "atx_state"
|
||||||
MSD_STATE = "msd_state"
|
MSD_STATE = "msd_state"
|
||||||
@@ -234,6 +238,7 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||||||
auth_manager: AuthManager,
|
auth_manager: AuthManager,
|
||||||
info_manager: InfoManager,
|
info_manager: InfoManager,
|
||||||
log_reader: LogReader,
|
log_reader: LogReader,
|
||||||
|
wol: WakeOnLan,
|
||||||
|
|
||||||
hid: BaseHid,
|
hid: BaseHid,
|
||||||
atx: BaseAtx,
|
atx: BaseAtx,
|
||||||
@@ -244,6 +249,7 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||||||
self._auth_manager = auth_manager
|
self._auth_manager = auth_manager
|
||||||
self.__info_manager = info_manager
|
self.__info_manager = info_manager
|
||||||
self.__log_reader = log_reader
|
self.__log_reader = log_reader
|
||||||
|
self.__wol = wol
|
||||||
|
|
||||||
self.__hid = hid
|
self.__hid = hid
|
||||||
self.__atx = atx
|
self.__atx = atx
|
||||||
@@ -355,6 +361,17 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||||||
)).encode("utf-8") + b"\r\n")
|
)).encode("utf-8") + b"\r\n")
|
||||||
return response
|
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
|
# ===== WEBSOCKET
|
||||||
|
|
||||||
@_exposed("GET", "/ws")
|
@_exposed("GET", "/ws")
|
||||||
@@ -366,6 +383,7 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||||||
await self.__register_socket(ws)
|
await self.__register_socket(ws)
|
||||||
await asyncio.gather(*[
|
await asyncio.gather(*[
|
||||||
self.__broadcast_event(_Events.INFO_STATE, (await self.__make_info())),
|
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.HID_STATE, self.__hid.get_state()),
|
||||||
self.__broadcast_event(_Events.ATX_STATE, self.__atx.get_state()),
|
self.__broadcast_event(_Events.ATX_STATE, self.__atx.get_state()),
|
||||||
self.__broadcast_event(_Events.MSD_STATE, (await self.__msd.get_state())),
|
self.__broadcast_event(_Events.MSD_STATE, (await self.__msd.get_state())),
|
||||||
|
|||||||
88
kvmd/apps/kvmd/wol.py
Normal file
88
kvmd/apps/kvmd/wol.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# ========================================================================== #
|
||||||
|
# #
|
||||||
|
# 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 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
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
|
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
from typing import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from . import check_re_match
|
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"
|
name = "IP address"
|
||||||
return check_any(
|
return check_any(
|
||||||
arg=valid_stripped_string_not_empty(arg, name),
|
arg=valid_stripped_string_not_empty(arg, name),
|
||||||
name=name,
|
name=name,
|
||||||
validators=[
|
validators=validators,
|
||||||
lambda arg: (arg, socket.inet_pton(socket.AF_INET, arg))[0],
|
|
||||||
lambda arg: (arg, socket.inet_pton(socket.AF_INET6, arg))[0],
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -65,3 +70,8 @@ def valid_rfc_host(arg: Any) -> str:
|
|||||||
|
|
||||||
def valid_port(arg: Any) -> int:
|
def valid_port(arg: Any) -> int:
|
||||||
return int(valid_number(arg, min=0, max=65535, name="TCP/UDP port"))
|
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()
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from kvmd.validators.net import valid_ip_or_host
|
|||||||
from kvmd.validators.net import valid_ip
|
from kvmd.validators.net import valid_ip
|
||||||
from kvmd.validators.net import valid_rfc_host
|
from kvmd.validators.net import valid_rfc_host
|
||||||
from kvmd.validators.net import valid_port
|
from kvmd.validators.net import valid_port
|
||||||
|
from kvmd.validators.net import valid_mac
|
||||||
|
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
@@ -120,3 +121,24 @@ def test_ok__valid_port(arg: Any) -> None:
|
|||||||
def test_fail__valid_port(arg: Any) -> None:
|
def test_fail__valid_port(arg: Any) -> None:
|
||||||
with pytest.raises(ValidatorError):
|
with pytest.raises(ValidatorError):
|
||||||
print(valid_port(arg))
|
print(valid_port(arg))
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
" 00:00:00:00:00:00 ",
|
||||||
|
" 9f:00:00:00:00:00 ",
|
||||||
|
" FF:FF:FF:FF:FF:FF ",
|
||||||
|
])
|
||||||
|
def test_ok__valid_mac(arg: Any) -> None:
|
||||||
|
assert valid_mac(arg) == arg.strip().lower()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
"00:00:00:00:00:0",
|
||||||
|
"9x:00:00:00:00:00",
|
||||||
|
"",
|
||||||
|
None,
|
||||||
|
])
|
||||||
|
def test_fail__valid_mac(arg: Any) -> None:
|
||||||
|
with pytest.raises(ValidatorError):
|
||||||
|
print(valid_mac(arg))
|
||||||
|
|||||||
Reference in New Issue
Block a user