Skip to content
/ kvmd Public

Commit abf497a

Browse files
committed
redfish: switch integration
1 parent 9cc95d4 commit abf497a

File tree

2 files changed

+107
-42
lines changed

2 files changed

+107
-42
lines changed

kvmd/apps/kvmd/api/redfish.py

Lines changed: 106 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222

2323
import asyncio
24+
import re
2425

2526
from aiohttp.web import Request
2627
from aiohttp.web import Response
@@ -31,10 +32,11 @@
3132

3233
from ....plugins.atx import BaseAtx
3334

34-
from ....validators import ValidatorError
3535
from ....validators import check_string_in_list
36+
from ....validators.basic import valid_int_f0
3637

3738
from ..info import InfoManager
39+
from ..switch import Switch
3840

3941

4042
# =====
@@ -50,86 +52,149 @@ class RedfishApi:
5052
# redfishtool -S Never -u admin -p admin -r localhost:8080 Systems
5153
# redfishtool -S Never -u admin -p admin -r localhost:8080 Systems reset ForceOff
5254

53-
def __init__(self, info_manager: InfoManager, atx: BaseAtx) -> None:
55+
__SWITCH_PREFIX = "SwitchPort"
56+
57+
def __init__(self, info_manager: InfoManager, atx: BaseAtx, switch: Switch) -> None:
5458
self.__info_manager = info_manager
55-
self.__atx = atx
5659

57-
self.__actions = {
58-
"On": self.__atx.power_on,
59-
"ForceOff": self.__atx.power_off_hard,
60+
self.__atx = atx
61+
self.__atx_actions = {
62+
"On": self.__atx.power_on,
63+
"ForceOff": self.__atx.power_off_hard,
6064
"GracefulShutdown": self.__atx.power_off,
61-
"ForceRestart": self.__atx.power_reset_hard,
62-
"ForceOn": self.__atx.power_on,
63-
"PushPowerButton": self.__atx.click_power,
65+
"ForceRestart": self.__atx.power_reset_hard,
66+
"ForceOn": self.__atx.power_on,
67+
"PushPowerButton": self.__atx.click_power,
68+
}
69+
70+
self.__switch = switch
71+
self.__switch_actions = {
72+
"On": self.__switch.atx_power_on,
73+
"ForceOff": self.__switch.atx_power_off_hard,
74+
"GracefulShutdown": self.__switch.atx_power_off,
75+
"ForceRestart": self.__switch.atx_power_reset_hard,
76+
"ForceOn": self.__switch.atx_power_on,
77+
"PushPowerButton": self.__switch.atx_click_power,
6478
}
6579

80+
assert set(self.__atx_actions) == set(self.__switch_actions)
81+
6682
# =====
6783

6884
@exposed_http("GET", "/redfish/v1", auth_required=False)
6985
async def __root_handler(self, _: Request) -> Response:
7086
return make_json_response({
71-
"@odata.id": "/redfish/v1",
72-
"@odata.type": "#ServiceRoot.v1_6_0.ServiceRoot",
73-
"Id": "RootService",
74-
"Name": "Root Service",
87+
"@odata.id": "/redfish/v1",
88+
"@odata.type": "#ServiceRoot.v1_6_0.ServiceRoot",
89+
"Id": "RootService",
90+
"Name": "Root Service",
7591
"RedfishVersion": "1.6.0",
76-
"Systems": {"@odata.id": "/redfish/v1/Systems"},
92+
"Systems": {"@odata.id": "/redfish/v1/Systems"}, # ATX
7793
}, wrap_result=False)
7894

95+
# ===== ATX =====
96+
7997
@exposed_http("GET", "/redfish/v1/Systems")
8098
async def __systems_handler(self, _: Request) -> Response:
99+
(atx_state, switch_state) = await asyncio.gather(*[
100+
self.__atx.get_state(),
101+
self.__switch.get_state(),
102+
])
103+
104+
members: list[str] = []
105+
if atx_state["enabled"]:
106+
members.append("0")
107+
108+
members.extend(
109+
f"{self.__SWITCH_PREFIX}{port}"
110+
for port in range(len(switch_state["model"]["ports"]))
111+
)
112+
81113
return make_json_response({
82-
"@odata.id": "/redfish/v1/Systems",
114+
"@odata.id": "/redfish/v1/Systems",
83115
"@odata.type": "#ComputerSystemCollection.ComputerSystemCollection",
84-
"Members": [{"@odata.id": "/redfish/v1/Systems/0"}],
85-
"Members@odata.count": 1,
86-
"Name": "Computer System Collection",
116+
"Name": "Computer System Collection",
117+
"Members": [
118+
{"@odata.id": f"/redfish/v1/Systems/{member}"}
119+
for member in members
120+
],
121+
"Members@odata.count": len(members),
87122
}, wrap_result=False)
88123

89-
@exposed_http("GET", "/redfish/v1/Systems/0")
90-
async def __server_handler(self, _: Request) -> Response:
91-
(atx_state, meta_host) = await asyncio.gather(*[
92-
self.__atx.get_state(),
93-
self.__info_manager.get_meta_server_host(),
94-
])
124+
@exposed_http("GET", "/redfish/v1/Systems/{sid}")
125+
async def __systems_server_handler(self, req: Request) -> Response:
126+
(sid, port) = self.__valid_server_id(req)
127+
if port < 0:
128+
(atx_state, host) = await asyncio.gather(*[
129+
self.__atx.get_state(),
130+
self.__info_manager.get_meta_server_host(),
131+
])
132+
power = atx_state["leds"]["power"] # type: ignore
133+
134+
else:
135+
switch_state = await self.__switch.get_state()
136+
if port >= len(switch_state["model"]["ports"]):
137+
raise HttpError("Non-existent Switch Port ID", 400)
138+
host = str(switch_state["model"]["ports"][port]["name"] or sid) # Makes mypy happy
139+
power = switch_state["atx"]["leds"]["power"][port]
140+
141+
host = re.sub(r"[^a-zA-Z0-9_\.]", "_", host)
95142
return make_json_response({
96-
"@odata.id": "/redfish/v1/Systems/0",
143+
"@odata.id": f"/redfish/v1/Systems/{sid}",
97144
"@odata.type": "#ComputerSystem.v1_10_0.ComputerSystem",
145+
"Id": sid,
146+
"HostName": host,
147+
"PowerState": ("On" if power else "Off"),
98148
"Actions": {
99-
"#ComputerSystem.Reset": {
100-
"ResetType@Redfish.AllowableValues": list(self.__actions),
101-
"target": "/redfish/v1/Systems/0/Actions/ComputerSystem.Reset",
149+
"#ComputerSystem.Reset": { # XXX: Same actions list for ATX and Switch
150+
"ResetType@Redfish.AllowableValues": list(self.__atx_actions),
151+
"target": f"/redfish/v1/Systems/{sid}/Actions/ComputerSystem.Reset",
102152
},
103153
"#ComputerSystem.SetDefaultBootOrder": { # https://github.com/pikvm/pikvm/issues/1525
104-
"target": "/redfish/v1/Systems/0/Actions/ComputerSystem.SetDefaultBootOrder",
154+
"target": f"/redfish/v1/Systems/{sid}/Actions/ComputerSystem.SetDefaultBootOrder",
105155
},
106156
},
107-
"Id": "0",
108-
"HostName": meta_host,
109-
"PowerState": ("On" if atx_state["leds"]["power"] else "Off"), # type: ignore
110157
"Boot": {
111158
"BootSourceOverrideEnabled": "Disabled",
112159
"BootSourceOverrideTarget": None,
113160
},
114161
}, wrap_result=False)
115162

116-
@exposed_http("PATCH", "/redfish/v1/Systems/0")
117-
async def __patch_handler(self, _: Request) -> Response:
163+
@exposed_http("PATCH", "/redfish/v1/Systems/{sid}")
164+
async def __systems_server_patch_handler(self, _: Request) -> Response:
118165
# https://github.com/pikvm/pikvm/issues/1525
166+
# XXX: We don't care about sid validation here, because nothing to do
119167
return Response(body=None, status=204)
120168

121-
@exposed_http("POST", "/redfish/v1/Systems/0/Actions/ComputerSystem.Reset")
122-
async def __power_handler(self, req: Request) -> Response:
169+
@exposed_http("POST", "/redfish/v1/Systems/{sid}/Actions/ComputerSystem.Reset")
170+
async def __systems_server_power_handler(self, req: Request) -> Response:
171+
(_, port) = self.__valid_server_id(req)
123172
try:
173+
# XXX: Same actions list for ATX and Switch
124174
action = check_string_in_list(
125175
arg=(await req.json()).get("ResetType"),
176+
variants=set(self.__atx_actions),
126177
name="Redfish ResetType",
127-
variants=set(self.__actions),
128178
lower=False,
129179
)
130-
except ValidatorError:
131-
raise
132180
except Exception:
133-
raise HttpError("Missing Redfish ResetType", 400)
134-
await self.__actions[action](False)
181+
raise HttpError("Missing or invalid ResetType", 400)
182+
if port < 0:
183+
if (await self.__atx.get_state())["enabled"]:
184+
await self.__atx_actions[action](False)
185+
else:
186+
await self.__switch_actions[action](port)
135187
return Response(body=None, status=204)
188+
189+
def __valid_server_id(self, req: Request) -> tuple[str, int]:
190+
try:
191+
sid = req.match_info["sid"].strip()
192+
if sid == "0": # Legacy name for PiKVM itself
193+
return ("0", -1)
194+
if sid.startswith(self.__SWITCH_PREFIX):
195+
sid = sid[len(self.__SWITCH_PREFIX):]
196+
port = valid_int_f0(sid)
197+
return (f"{self.__SWITCH_PREFIX}{port}", port)
198+
except Exception:
199+
pass
200+
raise HttpError("Missing or invalid Server ID", 400)

kvmd/apps/kvmd/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def __init__( # pylint: disable=too-many-arguments,too-many-locals
184184
StreamerApi(streamer, ocr),
185185
SwitchApi(switch),
186186
ExportApi(info_manager, atx, user_gpio),
187-
RedfishApi(info_manager, atx),
187+
RedfishApi(info_manager, atx, switch),
188188
]
189189
self.__subsystems = [
190190
_Subsystem.make(auth_manager, "Auth manager"),

0 commit comments

Comments
 (0)