summaryrefslogtreecommitdiff
path: root/kvmd/apps
diff options
context:
space:
mode:
authorDevaev Maxim <[email protected]>2020-10-28 05:12:03 +0300
committerDevaev Maxim <[email protected]>2020-10-28 05:21:19 +0300
commite9eb6460d34e48753cef2eaf6761a2642decfe24 (patch)
tree2f329224e1aed7ec4d3203edb8dc373ee8dca281 /kvmd/apps
parente7cbf1e1e213338a81cda874be4933472b142eec (diff)
ipmi sol
Diffstat (limited to 'kvmd/apps')
-rw-r--r--kvmd/apps/__init__.py8
-rw-r--r--kvmd/apps/ipmi/__init__.py5
-rw-r--r--kvmd/apps/ipmi/server.py119
3 files changed, 128 insertions, 4 deletions
diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py
index 6d133ac9..820278ce 100644
--- a/kvmd/apps/__init__.py
+++ b/kvmd/apps/__init__.py
@@ -93,6 +93,7 @@ from ..validators.kvm import valid_ugpio_channel
from ..validators.kvm import valid_ugpio_mode
from ..validators.kvm import valid_ugpio_view_table
+from ..validators.hw import valid_tty_speed
from ..validators.hw import valid_gpio_pin
from ..validators.hw import valid_otg_gadget
from ..validators.hw import valid_otg_id
@@ -496,6 +497,13 @@ def _get_config_scheme() -> Dict:
"auth": {
"file": Option("/etc/kvmd/ipmipasswd", type=valid_abs_file, unpack_as="path"),
},
+
+ "sol": {
+ "device": Option("", type=(lambda arg: (valid_abs_path(arg) if arg else "")), unpack_as="sol_device_path"),
+ "speed": Option(115200, type=valid_tty_speed, unpack_as="sol_speed"),
+ "select_timeout": Option(0.1, type=valid_float_f01, unpack_as="sol_select_timeout"),
+ "proxy_port": Option(0, type=valid_port, unpack_as="sol_proxy_port"),
+ },
},
"vnc": {
diff --git a/kvmd/apps/ipmi/__init__.py b/kvmd/apps/ipmi/__init__.py
index 0d1189c8..0abb57dc 100644
--- a/kvmd/apps/ipmi/__init__.py
+++ b/kvmd/apps/ipmi/__init__.py
@@ -48,5 +48,8 @@ def main(argv: Optional[List[str]]=None) -> None:
user_agent=htclient.make_user_agent("KVMD-IPMI"),
**config.kvmd._unpack(),
),
- **config.server._unpack(),
+ **{ # Makes mypy happy (too many arguments for IpmiServer)
+ **config.server._unpack(),
+ **config.sol._unpack(),
+ },
).run()
diff --git a/kvmd/apps/ipmi/server.py b/kvmd/apps/ipmi/server.py
index 87f090a3..b6224cba 100644
--- a/kvmd/apps/ipmi/server.py
+++ b/kvmd/apps/ipmi/server.py
@@ -20,13 +20,21 @@
# ========================================================================== #
+import os
+import select
import asyncio
+import threading
+import multiprocessing
import functools
+import queue
from typing import Dict
+from typing import Optional
import aiohttp
+import serial
+from pyghmi.ipmi.console import ServerConsole as IpmiConsole
from pyghmi.ipmi.private.session import Session as IpmiSession
from pyghmi.ipmi.private.serversession import IpmiServer as BaseIpmiServer
from pyghmi.ipmi.private.serversession import ServerSession as IpmiServerSession
@@ -51,8 +59,13 @@ class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attribute
kvmd: KvmdClient,
host: str,
- port: str,
+ port: int,
timeout: float,
+
+ sol_device_path: str,
+ sol_speed: int,
+ sol_select_timeout: float,
+ sol_proxy_port: int,
) -> None:
super().__init__(authdata=auth_manager, address=host, port=port)
@@ -64,6 +77,16 @@ class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attribute
self.__port = port
self.__timeout = timeout
+ self.__sol_device_path = sol_device_path
+ self.__sol_speed = sol_speed
+ self.__sol_select_timeout = sol_select_timeout
+ self.__sol_proxy_port = (sol_proxy_port or port)
+
+ self.__sol_lock = threading.Lock()
+ self.__sol_console: Optional[IpmiConsole] = None
+ self.__sol_thread: Optional[threading.Thread] = None
+ self.__sol_stop = False
+
def run(self) -> None:
logger = get_logger(0)
logger.info("Listening IPMI on UPD [%s]:%d ...", self.__host, self.__port)
@@ -72,6 +95,7 @@ class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attribute
IpmiSession.wait_for_rsp(self.__timeout)
except (SystemExit, KeyboardInterrupt):
pass
+ self.__stop_sol_worker()
logger.info("Bye-bye")
# =====
@@ -84,6 +108,8 @@ class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attribute
(6, 0x38): (lambda _, session: session.send_ipmi_response()), # Get channel auth types
(0, 1): self.__get_chassis_status_handler, # Get chassis status
(0, 2): self.__chassis_control_handler, # Chassis control
+ (6, 0x48): self.__activate_sol_handler, # Enable SOL
+ (6, 0x49): self.__deactivate_sol_handler, # Disable SOL
}.get((request["netfn"], request["command"]))
if handler is not None:
try:
@@ -97,6 +123,8 @@ class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attribute
else:
session.send_ipmi_response(code=0xC1)
+ # =====
+
def __get_power_state_handler(self, _: Dict, session: IpmiServerSession) -> None:
# https://github.com/arcress0/ipmiutil/blob/e2f6e95127d22e555f959f136d9bb9543c763896/util/ireset.c#L654
result = self.__make_request(session, "atx.get_state() [power]", "atx.get_state")
@@ -133,8 +161,6 @@ class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attribute
code = 0xCC # Invalid request
session.send_ipmi_response(code=code)
- # =====
-
def __make_request(self, session: IpmiServerSession, name: str, method_path: str, **kwargs): # type: ignore
async def runner(): # type: ignore
logger = get_logger(0)
@@ -150,3 +176,90 @@ class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attribute
raise
return aiotools.run_sync(runner())
+
+ # =====
+
+ def __activate_sol_handler(self, _: Dict, session: IpmiServerSession) -> None:
+ with self.__sol_lock:
+ if not self.__sol_device_path:
+ session.send_ipmi_response(code=0x81) # SOL disabled
+ elif not os.access(self.__sol_device_path, os.R_OK | os.W_OK):
+ get_logger(0).info("Can't activate SOL because %s is unavailable", self.__sol_device_path)
+ session.send_ipmi_response(code=0x81) # SOL disabled
+ elif self.__is_sol_activated():
+ session.send_ipmi_response(code=0x80) # Already activated
+ else:
+ get_logger(0).info("Activating SOL ...")
+ self.__stop_sol_worker() # Join if dead
+ session.send_ipmi_response(data=[
+ 0, 0, 0, 0, 1, 0, 1, 0,
+ (self.__sol_proxy_port >> 8 & 0xFF), (self.__sol_proxy_port & 0xFF),
+ 0xFF, 0xFF,
+ ])
+ self.__start_sol_worker(session)
+
+ def __deactivate_sol_handler(self, _: Dict, session: IpmiServerSession) -> None:
+ with self.__sol_lock:
+ if not self.__sol_device_path:
+ session.send_ipmi_response(code=0x81)
+ elif not self.__is_sol_activated():
+ session.send_ipmi_response(code=0x80)
+ else:
+ get_logger(0).info("Deactivating SOL ...")
+ self.__stop_sol_worker()
+
+ def __is_sol_activated(self) -> bool:
+ return (self.__sol_thread is not None and self.__sol_thread.is_alive())
+
+ def __start_sol_worker(self, session: IpmiServerSession) -> None:
+ assert self.__sol_console is None
+ assert self.__sol_thread is None
+ user_queue: "multiprocessing.Queue[bytes]" = multiprocessing.Queue() # Only for select()
+ self.__sol_console = IpmiConsole(session, user_queue.put_nowait)
+ self.__sol_thread = threading.Thread(target=self.__sol_worker, args=(user_queue,), daemon=True)
+ self.__sol_thread.start()
+
+ def __stop_sol_worker(self) -> None:
+ if self.__sol_thread is not None:
+ if self.__sol_thread.is_alive():
+ self.__sol_stop = True
+ self.__sol_thread.join()
+ self.__sol_stop = False
+ self.__sol_thread = None
+ self.__close_sol_console()
+
+ def __close_sol_console(self) -> None:
+ if self.__sol_console is not None:
+ self.__sol_console.close()
+ self.__sol_console = None
+ get_logger(0).info("SOL closed")
+
+ def __sol_worker(self, user_queue: "multiprocessing.Queue[bytes]") -> None:
+ logger = get_logger(0)
+ logger.info("Starting SOL worker ...")
+ try:
+ assert self.__sol_console is not None
+ with serial.Serial(self.__sol_device_path, self.__sol_speed) as tty:
+ logger.info("Opened SOL port %s at speed=%d", self.__sol_device_path, self.__sol_speed)
+ qr = user_queue._reader # type: ignore # pylint: disable=protected-access
+ try:
+ while not self.__sol_stop:
+ ready = select.select([qr, tty], [], [], self.__sol_select_timeout)[0]
+ if qr in ready:
+ data = b""
+ for _ in range(user_queue.qsize()): # Don't hold on this with [not empty()]
+ try:
+ data += user_queue.get_nowait()
+ except queue.Empty:
+ break
+ if data:
+ tty.write(data)
+ if tty in ready:
+ self.__sol_console.send_data(tty.read_all())
+ finally:
+ logger.info("Closed SOL port %s", self.__sol_device_path)
+ except Exception:
+ logger.exception("SOL worker error")
+ self.__close_sol_console()
+ finally:
+ logger.info("SOL worker finished")