2121
2222
2323import asyncio
24+ import re
2425
2526from aiohttp .web import Request
2627from aiohttp .web import Response
3132
3233from ....plugins .atx import BaseAtx
3334
34- from ....validators import ValidatorError
3535from ....validators import check_string_in_list
36+ from ....validators .basic import valid_int_f0
3637
3738from ..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 )
0 commit comments