diff options
-rw-r--r-- | configs/nginx/kvmd.ctx-server.conf | 6 | ||||
-rw-r--r-- | kvmd/apps/kvmd/api/redfish.py | 127 | ||||
-rw-r--r-- | kvmd/apps/kvmd/http.py | 5 | ||||
-rw-r--r-- | kvmd/apps/kvmd/server.py | 8 |
4 files changed, 143 insertions, 3 deletions
diff --git a/configs/nginx/kvmd.ctx-server.conf b/configs/nginx/kvmd.ctx-server.conf index 86f3ee47..1eb37842 100644 --- a/configs/nginx/kvmd.ctx-server.conf +++ b/configs/nginx/kvmd.ctx-server.conf @@ -93,3 +93,9 @@ location /streamer { proxy_buffering off; proxy_ignore_headers X-Accel-Buffering; } + +location /redfish { + proxy_pass http://kvmd; + include /etc/kvmd/nginx/loc-proxy.conf; + auth_request off; +} diff --git a/kvmd/apps/kvmd/api/redfish.py b/kvmd/apps/kvmd/api/redfish.py new file mode 100644 index 00000000..52cc5610 --- /dev/null +++ b/kvmd/apps/kvmd/api/redfish.py @@ -0,0 +1,127 @@ +# ========================================================================== # +# # +# 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 asyncio + +from aiohttp.web import Request +from aiohttp.web import Response + +from ....plugins.atx import BaseAtx + +from ....validators import ValidatorError +from ....validators import check_string_in_list + +from ..info import InfoManager + +from ..http import HttpError +from ..http import exposed_http +from ..http import make_json_response + + +# ===== +class RedfishApi: + # https://github.com/DMTF/Redfishtool + # https://github.com/DMTF/Redfish-Mockup-Server + # https://redfish.dmtf.org/redfish/v1 + # https://www.dmtf.org/documents/redfish-spmf/redfish-mockup-bundle-20191 + # https://www.dmtf.org/sites/default/files/Redfish_School-Sessions.pdf + # https://www.ibm.com/support/knowledgecenter/POWER9/p9ej4/p9ej4_kickoff.htm + # + # Quick examples: + # redfishtool -S Never -u admin -p admin -r localhost:8080 Systems + # redfishtool -S Never -u admin -p admin -r localhost:8080 Systems reset ForceOff + + def __init__(self, info_manager: InfoManager, atx: BaseAtx) -> None: + self.__info_manager = info_manager + self.__atx = atx + + self.__actions = { + "On": self.__atx.power_on, + "ForceOff": self.__atx.power_off_hard, + "GracefulShutdown": self.__atx.power_off, + "ForceRestart": self.__atx.power_reset_hard, + "ForceOn": self.__atx.power_on, + "PushPowerButton": self.__atx.click_power, + } + + # ===== + + @exposed_http("GET", "/redfish/v1", auth_required=False) + async def __root_handler(self, _: Request) -> Response: + return make_json_response({ + "@odata.id": "/redfish/v1", + "@odata.type": "#ServiceRoot.v1_6_0.ServiceRoot", + "Id": "RootService", + "Name": "Root Service", + "RedfishVersion": "1.6.0", + "Systems": {"@odata.id": "/redfish/v1/Systems"}, + }, wrap_result=False) + + @exposed_http("GET", "/redfish/v1/Systems") + async def __systems_handler(self, _: Request) -> Response: + return make_json_response({ + "@odata.id": "/redfish/v1/Systems", + "@odata.type": "#ComputerSystemCollection.ComputerSystemCollection", + "Members": [{"@odata.id": "/redfish/v1/Systems/0"}], + "[email protected]": 1, + "Name": "Computer System Collection", + }, wrap_result=False) + + @exposed_http("GET", "/redfish/v1/Systems/0") + async def __server_handler(self, _: Request) -> Response: + (atx_state, meta_state) = await asyncio.gather(*[ + self.__atx.get_state(), + self.__info_manager.get_submanager("meta").get_state(), + ]) + try: + host = meta_state.get("server", {})["host"] + except Exception: + host = "" + return make_json_response({ + "@odata.id": "/redfish/v1/Systems/0", + "@odata.type": "#ComputerSystem.v1_10_0.ComputerSystem", + "Actions": { + "#ComputerSystem.Reset": { + "[email protected]": list(self.__actions), + "target": "/redfish/v1/Systems/0/Actions/ComputerSystem.Reset" + }, + }, + "Id": "0", + "HostName": host, + "PowerState": ("On" if atx_state["leds"]["power"] else "Off"), + }, wrap_result=False) + + @exposed_http("POST", "/redfish/v1/Systems/0/Actions/ComputerSystem.Reset") + async def __power_handler(self, request: Request) -> Response: + try: + action = check_string_in_list( + arg=(await request.json())["ResetType"], + name="Redfish ResetType", + variants=set(self.__actions), + lower=False, + ) + except Exception as err: + if isinstance(err, ValidatorError): + raise + raise HttpError("Missing Redfish ResetType", 400) + await self.__actions[action](False) + return Response(body=None, status=204) diff --git a/kvmd/apps/kvmd/http.py b/kvmd/apps/kvmd/http.py index e13198b1..21077af9 100644 --- a/kvmd/apps/kvmd/http.py +++ b/kvmd/apps/kvmd/http.py @@ -117,13 +117,14 @@ def make_json_response( result: Optional[Dict]=None, status: int=200, set_cookies: Optional[Dict[str, str]]=None, + wrap_result: bool=True, ) -> aiohttp.web.Response: response = aiohttp.web.Response( - text=json.dumps({ + text=json.dumps(({ "ok": (status == 200), "result": (result or {}), - }, sort_keys=True, indent=4), + } if wrap_result else result), sort_keys=True, indent=4), status=status, content_type="application/json", ) diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py index 1b3e2363..0ff4c49d 100644 --- a/kvmd/apps/kvmd/server.py +++ b/kvmd/apps/kvmd/server.py @@ -91,6 +91,7 @@ from .api.atx import AtxApi from .api.msd import MsdApi from .api.streamer import StreamerApi from .api.export import ExportApi +from .api.redfish import RedfishApi # ===== @@ -198,6 +199,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins MsdApi(msd, sync_chunk_size), StreamerApi(streamer), ExportApi(info_manager, atx, user_gpio), + RedfishApi(info_manager, atx), ] self.__ws_handlers: Dict[str, Callable] = {} @@ -291,7 +293,11 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins super().run(**kwargs) async def _make_app(self) -> aiohttp.web.Application: - app = aiohttp.web.Application() + app = aiohttp.web.Application(middlewares=[aiohttp.web.normalize_path_middleware( + append_slash=False, + remove_slash=True, + merge_slashes=True, + )]) app.on_shutdown.append(self.__on_shutdown) app.on_cleanup.append(self.__on_cleanup) |