summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--configs/nginx/kvmd.ctx-server.conf6
-rw-r--r--kvmd/apps/kvmd/api/redfish.py127
-rw-r--r--kvmd/apps/kvmd/http.py5
-rw-r--r--kvmd/apps/kvmd/server.py8
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"}],
+ "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)