diff options
author | Devaev Maxim <[email protected]> | 2019-04-28 08:31:37 +0300 |
---|---|---|
committer | Devaev Maxim <[email protected]> | 2019-04-28 08:31:37 +0300 |
commit | 0bde12e24ddb3aad02e9d39c031bb66b04bfd997 (patch) | |
tree | dfb3639a6b0f32242047677d580916ece046f7ba /kvmd/apps | |
parent | 380b1d15e3c8fcb7cc324cebf209bfe38cbb4011 (diff) |
ipmi bmc proxy
Diffstat (limited to 'kvmd/apps')
-rw-r--r-- | kvmd/apps/__init__.py | 29 | ||||
-rw-r--r-- | kvmd/apps/ipmi/__init__.py | 48 | ||||
-rw-r--r-- | kvmd/apps/ipmi/__main__.py | 24 | ||||
-rw-r--r-- | kvmd/apps/ipmi/auth.py | 84 | ||||
-rw-r--r-- | kvmd/apps/ipmi/server.py | 190 |
5 files changed, 371 insertions, 4 deletions
diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py index 65d30f2e..e4b10127 100644 --- a/kvmd/apps/__init__.py +++ b/kvmd/apps/__init__.py @@ -110,11 +110,13 @@ def _init_config(config_path: str, sections: List[str], override_options: List[s _merge_dicts(raw_config, build_raw_from_options(override_options)) config = make_config(raw_config, scheme) - scheme["kvmd"]["auth"]["internal"] = get_auth_service_class(config.kvmd.auth.internal_type).get_options() - if config.kvmd.auth.external_type: - scheme["kvmd"]["auth"]["external"] = get_auth_service_class(config.kvmd.auth.external_type).get_options() + if "kvmd" in sections: + scheme["kvmd"]["auth"]["internal"] = get_auth_service_class(config.kvmd.auth.internal_type).get_options() + if config.kvmd.auth.external_type: + scheme["kvmd"]["auth"]["external"] = get_auth_service_class(config.kvmd.auth.external_type).get_options() + config = make_config(raw_config, scheme) - return make_config(raw_config, scheme) + return config except (ConfigError, UnknownPluginError) as err: raise SystemExit("Config error: %s" % (str(err))) @@ -233,6 +235,25 @@ def _get_config_scheme(sections: List[str]) -> Dict: "cmd": Option(["/bin/true"], type=valid_command), }, }, + + "ipmi": { + "server": { + "host": Option("::", type=valid_ip_or_host), + "port": Option(623, type=valid_port), + "timeout": Option(10.0, type=valid_float_f01), + }, + + "kvmd": { + "host": Option("localhost", type=valid_ip_or_host, unpack_as="kvmd_host"), + "port": Option(0, type=valid_port, unpack_as="kvmd_port"), + "unix": Option("", type=valid_abs_path, only_if="!port", unpack_as="kvmd_unix_path"), + "timeout": Option(5.0, type=valid_float_f01, unpack_as="kvmd_timeout"), + }, + + "auth": { + "file": Option("/etc/kvmd/ipmipasswd", type=valid_abs_path_exists, unpack_as="path"), + }, + }, } if sections: diff --git a/kvmd/apps/ipmi/__init__.py b/kvmd/apps/ipmi/__init__.py new file mode 100644 index 00000000..e2b56b1a --- /dev/null +++ b/kvmd/apps/ipmi/__init__.py @@ -0,0 +1,48 @@ +# ========================================================================== # +# # +# 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/>. # +# # +# ========================================================================== # + + +from typing import List +from typing import Optional + +from .. import init + +from .auth import IpmiAuthManager +from .server import IpmiServer + + +# ===== +def main(argv: Optional[List[str]]=None) -> None: + config = init( + prog="kvmd-ipmi", + description="IPMI to KVMD proxy", + sections=["logging", "ipmi"], + argv=argv, + )[2].ipmi + + # pylint: disable=protected-access + IpmiServer( + auth_manager=IpmiAuthManager(**config.auth._unpack()), + **{ # Dirty mypy hack + **config.server._unpack(), + **config.kvmd._unpack(), + }, + ).run() # type: ignore diff --git a/kvmd/apps/ipmi/__main__.py b/kvmd/apps/ipmi/__main__.py new file mode 100644 index 00000000..77f4e294 --- /dev/null +++ b/kvmd/apps/ipmi/__main__.py @@ -0,0 +1,24 @@ +# ========================================================================== # +# # +# 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/>. # +# # +# ========================================================================== # + + +from . import main +main() diff --git a/kvmd/apps/ipmi/auth.py b/kvmd/apps/ipmi/auth.py new file mode 100644 index 00000000..4b95abf5 --- /dev/null +++ b/kvmd/apps/ipmi/auth.py @@ -0,0 +1,84 @@ +# ========================================================================== # +# # +# 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/>. # +# # +# ========================================================================== # + + +from typing import List +from typing import Dict +from typing import NamedTuple + + +# ===== +class IpmiPasswdError(Exception): + def __init__(self, msg: str) -> None: + super().__init__("Incorrect IPMI passwd file: " + msg) + + +class IpmiUserCredentials(NamedTuple): + ipmi_user: str + ipmi_passwd: str + kvmd_user: str + kvmd_passwd: str + + +class IpmiAuthManager: + def __init__(self, path: str) -> None: + with open(path) as passwd_file: + self.__credentials = self.__parse_passwd_file(passwd_file.read().split("\n")) + + def __contains__(self, ipmi_user: str) -> bool: + return (ipmi_user in self.__credentials) + + def __getitem__(self, ipmi_user: str) -> str: + return self.__credentials[ipmi_user].ipmi_passwd + + def get_credentials(self, ipmi_user: str) -> IpmiUserCredentials: + return self.__credentials[ipmi_user] + + def __parse_passwd_file(self, lines: List[str]) -> Dict[str, IpmiUserCredentials]: + credentials: Dict[str, IpmiUserCredentials] = {} + for (number, line) in enumerate(lines): + if len(line.strip()) == 0 or line.lstrip().startswith("#"): + continue + + if " -> " not in line: + raise IpmiPasswdError("Missing ' -> ' operator at line #%d" % (number)) + + (left, right) = map(str.lstrip, line.split(" -> ", 1)) + for (name, pair) in [("left", left), ("right", right)]: + if ":" not in pair: + raise IpmiPasswdError("Missing ':' operator in %s credentials at line #%d" % (name, number)) + + (ipmi_user, ipmi_passwd) = left.split(":") + ipmi_user = ipmi_user.strip() + + (kvmd_user, kvmd_passwd) = right.split(":") + kvmd_user = kvmd_user.strip() + + if ipmi_user in credentials: + raise IpmiPasswdError("Found duplicating user %r (left) at line #%d" % (ipmi_user, number)) + + credentials[ipmi_user] = IpmiUserCredentials( + ipmi_user=ipmi_user, + ipmi_passwd=ipmi_passwd, + kvmd_user=kvmd_passwd, + kvmd_passwd=kvmd_passwd, + ) + return credentials diff --git a/kvmd/apps/ipmi/server.py b/kvmd/apps/ipmi/server.py new file mode 100644 index 00000000..f71ce85c --- /dev/null +++ b/kvmd/apps/ipmi/server.py @@ -0,0 +1,190 @@ +# ========================================================================== # +# # +# 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 sys +import asyncio +import threading + +from typing import Tuple +from typing import Dict +from typing import Optional + +import aiohttp + +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 + +from ...logging import get_logger + +from ... import __version__ + +from .auth import IpmiAuthManager + + +# ===== +class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attributes,abstract-method + # https://www.intel.com/content/dam/www/public/us/en/documents/product-briefs/ipmi-second-gen-interface-spec-v2-rev1-1.pdf + # https://www.thomas-krenn.com/en/wiki/IPMI_Basics + + def __init__( + self, + auth_manager: IpmiAuthManager, + + host: str, + port: str, + timeout: float, + + kvmd_host: str, + kvmd_port: int, + kvmd_unix_path: str, + kvmd_timeout: float, + ) -> None: + + super().__init__(authdata=auth_manager, address=host, port=port) + + self.__auth_manager = auth_manager + + self.__host = host + self.__port = port + self.__timeout = timeout + + self.__kvmd_host = kvmd_host + self.__kvmd_port = kvmd_port + self.__kvmd_unix_path = kvmd_unix_path + self.__kvmd_timeout = kvmd_timeout + + def run(self) -> None: + logger = get_logger(0) + logger.info("Listening IPMI on UPD [%s]:%d ...", self.__host, self.__port) + try: + while True: + IpmiSession.wait_for_rsp(self.__timeout) + except (SystemExit, KeyboardInterrupt): + pass + logger.info("Bye-bye") + + # ===== + + def handle_raw_request(self, request: Dict, session: IpmiServerSession) -> None: + handler = { + (6, 1): lambda _, session: self.send_device_id(session), # Get device ID + (0, 1): self.__get_chassis_status_handler, # Get chassis status + (0, 2): self.__chassis_control_handler, # Chassis control + }.get((request["netfn"], request["command"])) + if handler is not None: + try: + handler(request, session) + except (aiohttp.ClientError, asyncio.TimeoutError): + session.send_ipmi_response(code=0xFF) + except Exception: + get_logger(0).exception("Unexpected exception while handling IPMI request: netfn=%d; command=%d", + request["netfn"], request["command"]) + session.send_ipmi_response(code=0xFF) + else: + session.send_ipmi_response(code=0xC1) + + def __get_chassis_status_handler(self, _: Dict, session: IpmiServerSession) -> None: + result = self.__make_request("GET", "/atx", session)[1] + data = [int(result["leds"]["power"]), 0, 0] + session.send_ipmi_response(data=data) + + def __chassis_control_handler(self, request: Dict, session: IpmiServerSession) -> None: + handle = { + 0: "/atx/power?action=off", + 1: "/atx/power?action=on", + 3: "/atx/power?action=reset", + 5: "/atx/power?action=off_soft", + }.get(request["data"][0], "") + if handle: + if self.__make_request("POST", handle, session)[0] == 409: + code = 0xC0 # Try again later + else: + code = 0 + else: + code = 0xCC # Invalid request + session.send_ipmi_response(code=code) + + # ===== + + def __make_request(self, method: str, handle: str, ipmi_session: IpmiServerSession) -> Tuple[int, Dict]: + result: Optional[Tuple[int, Dict]] = None + exc_info = None + + def make_request() -> None: + nonlocal result + nonlocal exc_info + + loop = asyncio.new_event_loop() + try: + result = loop.run_until_complete(self.__make_request_async(method, handle, ipmi_session)) + except: # noqa: E722 # pylint: disable=bare-except + exc_info = sys.exc_info() + finally: + loop.close() + + thread = threading.Thread(target=make_request, daemon=True) + thread.start() + thread.join() + if exc_info is not None: + raise exc_info[1].with_traceback(exc_info[2]) # type: ignore # pylint: disable=unsubscriptable-object + assert result is not None + # Dirty pylint hack + return (result[0], result[1]) # pylint: disable=unsubscriptable-object + + async def __make_request_async(self, method: str, handle: str, ipmi_session: IpmiServerSession) -> Tuple[int, Dict]: + logger = get_logger(0) + + assert handle.startswith("/") + url = "http://%s:%d%s" % (self.__kvmd_host, self.__kvmd_port, handle) + + credentials = self.__auth_manager.get_credentials(ipmi_session.username.decode()) + logger.info("Performing %r request to %r from user %r (IPMI) as %r (KVMD)", + method, url, credentials.ipmi_user, credentials.kvmd_user) + + async with self.__make_http_session_async() as http_session: + try: + async with http_session.request( + method=method, + url=url, + headers={ + "X-KVMD-User": credentials.kvmd_user, + "X-KVMD-Passwd": credentials.kvmd_passwd, + "User-Agent": "KVMD-IPMI/%s" % (__version__), + }, + timeout=self.__kvmd_timeout, + ) as response: + if response.status != 409: + response.raise_for_status() + return (response.status, (await response.json())["result"]) + except (aiohttp.ClientError, asyncio.TimeoutError) as err: + logger.error("Can't perform %r request to %r: %s: %s", method, url, type(err).__name__, str(err)) + raise + except Exception: + logger.exception("Unexpected exception while performing %r request to %r", method, url) + raise + + def __make_http_session_async(self) -> aiohttp.ClientSession: + if self.__kvmd_unix_path: + return aiohttp.ClientSession(connector=aiohttp.UnixConnector(path=self.__kvmd_unix_path)) + else: + return aiohttp.ClientSession() |