xref: /openbmc/bmcweb/features/redfish/lib/fan.hpp (revision 1da1ff5fc9d51096bd8cc6c6693d5a41629d861b)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #pragma once
4 
5 #include "app.hpp"
6 #include "async_resp.hpp"
7 #include "dbus_utility.hpp"
8 #include "error_messages.hpp"
9 #include "generated/enums/resource.hpp"
10 #include "http_request.hpp"
11 #include "led.hpp"
12 #include "logging.hpp"
13 #include "query.hpp"
14 #include "registries/privilege_registry.hpp"
15 #include "utils/chassis_utils.hpp"
16 #include "utils/dbus_utils.hpp"
17 #include "utils/json_utils.hpp"
18 
19 #include <asm-generic/errno.h>
20 
21 #include <boost/beast/http/field.hpp>
22 #include <boost/beast/http/verb.hpp>
23 #include <boost/system/error_code.hpp>
24 #include <boost/url/format.hpp>
25 #include <nlohmann/json.hpp>
26 #include <sdbusplus/unpack_properties.hpp>
27 
28 #include <array>
29 #include <functional>
30 #include <memory>
31 #include <optional>
32 #include <string>
33 #include <string_view>
34 #include <utility>
35 
36 namespace redfish
37 {
38 constexpr std::array<std::string_view, 1> fanInterface = {
39     "xyz.openbmc_project.Inventory.Item.Fan"};
40 
41 inline void updateFanList(
42     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
43     const std::string& chassisId,
44     const dbus::utility::MapperGetSubTreePathsResponse& fanPaths)
45 {
46     nlohmann::json& fanList = asyncResp->res.jsonValue["Members"];
47     for (const std::string& fanPath : fanPaths)
48     {
49         std::string fanName =
50             sdbusplus::message::object_path(fanPath).filename();
51         if (fanName.empty())
52         {
53             continue;
54         }
55 
56         nlohmann::json item = nlohmann::json::object();
57         item["@odata.id"] = boost::urls::format(
58             "/redfish/v1/Chassis/{}/ThermalSubsystem/Fans/{}", chassisId,
59             fanName);
60 
61         fanList.emplace_back(std::move(item));
62     }
63     asyncResp->res.jsonValue["Members@odata.count"] = fanList.size();
64 }
65 
66 inline void getFanPaths(
67     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
68     const std::string& validChassisPath,
69     const std::function<void(const dbus::utility::MapperGetSubTreePathsResponse&
70                                  fanPaths)>& callback)
71 {
72     sdbusplus::message::object_path endpointPath{validChassisPath};
73     endpointPath /= "cooled_by";
74 
75     dbus::utility::getAssociatedSubTreePaths(
76         endpointPath,
77         sdbusplus::message::object_path("/xyz/openbmc_project/inventory"), 0,
78         fanInterface,
79         [asyncResp, callback](
80             const boost::system::error_code& ec,
81             const dbus::utility::MapperGetSubTreePathsResponse& subtreePaths) {
82             if (ec)
83             {
84                 if (ec.value() != EBADR)
85                 {
86                     BMCWEB_LOG_ERROR(
87                         "DBUS response error for getAssociatedSubTreePaths {}",
88                         ec.value());
89                     messages::internalError(asyncResp->res);
90                 }
91                 return;
92             }
93             callback(subtreePaths);
94         });
95 }
96 
97 inline void doFanCollection(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
98                             const std::string& chassisId,
99                             const std::optional<std::string>& validChassisPath)
100 {
101     if (!validChassisPath)
102     {
103         messages::resourceNotFound(asyncResp->res, "Chassis", chassisId);
104         return;
105     }
106 
107     asyncResp->res.addHeader(
108         boost::beast::http::field::link,
109         "</redfish/v1/JsonSchemas/FanCollection/FanCollection.json>; rel=describedby");
110     asyncResp->res.jsonValue["@odata.type"] = "#FanCollection.FanCollection";
111     asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
112         "/redfish/v1/Chassis/{}/ThermalSubsystem/Fans", chassisId);
113     asyncResp->res.jsonValue["Name"] = "Fan Collection";
114     asyncResp->res.jsonValue["Description"] =
115         "The collection of Fan resource instances " + chassisId;
116     asyncResp->res.jsonValue["Members"] = nlohmann::json::array();
117     asyncResp->res.jsonValue["Members@odata.count"] = 0;
118 
119     getFanPaths(asyncResp, *validChassisPath,
120                 std::bind_front(updateFanList, asyncResp, chassisId));
121 }
122 
123 inline void handleFanCollectionHead(
124     App& app, const crow::Request& req,
125     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
126     const std::string& chassisId)
127 {
128     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
129     {
130         return;
131     }
132 
133     redfish::chassis_utils::getValidChassisPath(
134         asyncResp, chassisId,
135         [asyncResp,
136          chassisId](const std::optional<std::string>& validChassisPath) {
137             if (!validChassisPath)
138             {
139                 messages::resourceNotFound(asyncResp->res, "Chassis",
140                                            chassisId);
141                 return;
142             }
143             asyncResp->res.addHeader(
144                 boost::beast::http::field::link,
145                 "</redfish/v1/JsonSchemas/FanCollection/FanCollection.json>; rel=describedby");
146         });
147 }
148 
149 inline void handleFanCollectionGet(
150     App& app, const crow::Request& req,
151     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
152     const std::string& chassisId)
153 {
154     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
155     {
156         return;
157     }
158 
159     redfish::chassis_utils::getValidChassisPath(
160         asyncResp, chassisId,
161         std::bind_front(doFanCollection, asyncResp, chassisId));
162 }
163 
164 inline void requestRoutesFanCollection(App& app)
165 {
166     BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/ThermalSubsystem/Fans/")
167         .privileges(redfish::privileges::headFanCollection)
168         .methods(boost::beast::http::verb::head)(
169             std::bind_front(handleFanCollectionHead, std::ref(app)));
170 
171     BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/ThermalSubsystem/Fans/")
172         .privileges(redfish::privileges::getFanCollection)
173         .methods(boost::beast::http::verb::get)(
174             std::bind_front(handleFanCollectionGet, std::ref(app)));
175 }
176 
177 inline bool checkFanId(const std::string& fanPath, const std::string& fanId)
178 {
179     std::string fanName = sdbusplus::message::object_path(fanPath).filename();
180 
181     return !(fanName.empty() || fanName != fanId);
182 }
183 
184 inline void handleFanPath(
185     const std::string& fanId,
186     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
187     const dbus::utility::MapperGetSubTreePathsResponse& fanPaths,
188     const std::function<void(const std::string& fanPath,
189                              const std::string& service)>& callback)
190 {
191     for (const auto& fanPath : fanPaths)
192     {
193         if (!checkFanId(fanPath, fanId))
194         {
195             continue;
196         }
197         dbus::utility::getDbusObject(
198             fanPath, fanInterface,
199             [fanPath, asyncResp,
200              callback](const boost::system::error_code& ec,
201                        const dbus::utility::MapperGetObject& object) {
202                 if (ec || object.empty())
203                 {
204                     BMCWEB_LOG_ERROR("DBUS response error on getDbusObject {}",
205                                      ec.value());
206                     messages::internalError(asyncResp->res);
207                     return;
208                 }
209                 callback(fanPath, object.begin()->first);
210             });
211 
212         return;
213     }
214     BMCWEB_LOG_WARNING("Fan not found {}", fanId);
215     messages::resourceNotFound(asyncResp->res, "Fan", fanId);
216 }
217 
218 inline void getValidFanObject(
219     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
220     const std::string& validChassisPath, const std::string& fanId,
221     const std::function<void(const std::string& fanPath,
222                              const std::string& service)>& callback)
223 {
224     getFanPaths(
225         asyncResp, validChassisPath,
226         [fanId, asyncResp, callback](
227             const dbus::utility::MapperGetSubTreePathsResponse& fanPaths) {
228             handleFanPath(fanId, asyncResp, fanPaths, callback);
229         });
230 }
231 
232 inline void addFanCommonProperties(crow::Response& resp,
233                                    const std::string& chassisId,
234                                    const std::string& fanId)
235 {
236     resp.addHeader(boost::beast::http::field::link,
237                    "</redfish/v1/JsonSchemas/Fan/Fan.json>; rel=describedby");
238     resp.jsonValue["@odata.type"] = "#Fan.v1_3_0.Fan";
239     resp.jsonValue["Name"] = "Fan";
240     resp.jsonValue["Id"] = fanId;
241     resp.jsonValue["@odata.id"] = boost::urls::format(
242         "/redfish/v1/Chassis/{}/ThermalSubsystem/Fans/{}", chassisId, fanId);
243     resp.jsonValue["Status"]["State"] = resource::State::Enabled;
244     resp.jsonValue["Status"]["Health"] = resource::Health::OK;
245 }
246 
247 inline void getFanHealth(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
248                          const std::string& fanPath, const std::string& service)
249 {
250     dbus::utility::getProperty<bool>(
251         service, fanPath,
252         "xyz.openbmc_project.State.Decorator.OperationalStatus", "Functional",
253         [asyncResp](const boost::system::error_code& ec, const bool value) {
254             if (ec)
255             {
256                 if (ec.value() != EBADR)
257                 {
258                     BMCWEB_LOG_ERROR("DBUS response error for Health {}",
259                                      ec.value());
260                     messages::internalError(asyncResp->res);
261                 }
262                 return;
263             }
264 
265             if (!value)
266             {
267                 asyncResp->res.jsonValue["Status"]["Health"] =
268                     resource::Health::Critical;
269             }
270         });
271 }
272 
273 inline void getFanState(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
274                         const std::string& fanPath, const std::string& service)
275 {
276     dbus::utility::getProperty<bool>(
277         service, fanPath, "xyz.openbmc_project.Inventory.Item", "Present",
278         [asyncResp](const boost::system::error_code& ec, const bool value) {
279             if (ec)
280             {
281                 if (ec.value() != EBADR)
282                 {
283                     BMCWEB_LOG_ERROR("DBUS response error for State {}",
284                                      ec.value());
285                     messages::internalError(asyncResp->res);
286                 }
287                 return;
288             }
289 
290             if (!value)
291             {
292                 asyncResp->res.jsonValue["Status"]["State"] =
293                     resource::State::Absent;
294             }
295         });
296 }
297 
298 inline void getFanAsset(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
299                         const std::string& fanPath, const std::string& service)
300 {
301     dbus::utility::getAllProperties(
302         service, fanPath, "xyz.openbmc_project.Inventory.Decorator.Asset",
303         [fanPath, asyncResp{asyncResp}](
304             const boost::system::error_code& ec,
305             const dbus::utility::DBusPropertiesMap& assetList) {
306             if (ec)
307             {
308                 if (ec.value() != EBADR)
309                 {
310                     BMCWEB_LOG_ERROR("DBUS response error for Properties{}",
311                                      ec.value());
312                     messages::internalError(asyncResp->res);
313                 }
314                 return;
315             }
316             const std::string* manufacturer = nullptr;
317             const std::string* model = nullptr;
318             const std::string* partNumber = nullptr;
319             const std::string* serialNumber = nullptr;
320             const std::string* sparePartNumber = nullptr;
321 
322             const bool success = sdbusplus::unpackPropertiesNoThrow(
323                 dbus_utils::UnpackErrorPrinter(), assetList, "Manufacturer",
324                 manufacturer, "Model", model, "PartNumber", partNumber,
325                 "SerialNumber", serialNumber, "SparePartNumber",
326                 sparePartNumber);
327             if (!success)
328             {
329                 messages::internalError(asyncResp->res);
330                 return;
331             }
332             if (manufacturer != nullptr)
333             {
334                 asyncResp->res.jsonValue["Manufacturer"] = *manufacturer;
335             }
336             if (model != nullptr)
337             {
338                 asyncResp->res.jsonValue["Model"] = *model;
339             }
340             if (partNumber != nullptr)
341             {
342                 asyncResp->res.jsonValue["PartNumber"] = *partNumber;
343             }
344             if (serialNumber != nullptr)
345             {
346                 asyncResp->res.jsonValue["SerialNumber"] = *serialNumber;
347             }
348             if (sparePartNumber != nullptr && !sparePartNumber->empty())
349             {
350                 asyncResp->res.jsonValue["SparePartNumber"] = *sparePartNumber;
351             }
352         });
353 }
354 
355 inline void getFanLocation(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
356                            const std::string& fanPath,
357                            const std::string& service)
358 {
359     dbus::utility::getProperty<std::string>(
360         service, fanPath,
361         "xyz.openbmc_project.Inventory.Decorator.LocationCode", "LocationCode",
362         [asyncResp](const boost::system::error_code& ec,
363                     const std::string& property) {
364             if (ec)
365             {
366                 if (ec.value() != EBADR)
367                 {
368                     BMCWEB_LOG_ERROR("DBUS response error for Location{}",
369                                      ec.value());
370                     messages::internalError(asyncResp->res);
371                 }
372                 return;
373             }
374             asyncResp->res
375                 .jsonValue["Location"]["PartLocation"]["ServiceLabel"] =
376                 property;
377         });
378 }
379 
380 inline void afterGetValidFanObject(
381     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
382     const std::string& chassisId, const std::string& fanId,
383     const std::string& fanPath, const std::string& service)
384 {
385     addFanCommonProperties(asyncResp->res, chassisId, fanId);
386     getFanState(asyncResp, fanPath, service);
387     getFanHealth(asyncResp, fanPath, service);
388     getFanAsset(asyncResp, fanPath, service);
389     getFanLocation(asyncResp, fanPath, service);
390     getLocationIndicatorActive(asyncResp, fanPath);
391 }
392 
393 inline void doFanGet(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
394                      const std::string& chassisId, const std::string& fanId,
395                      const std::optional<std::string>& validChassisPath)
396 {
397     if (!validChassisPath)
398     {
399         messages::resourceNotFound(asyncResp->res, "Chassis", chassisId);
400         return;
401     }
402 
403     getValidFanObject(
404         asyncResp, *validChassisPath, fanId,
405         std::bind_front(afterGetValidFanObject, asyncResp, chassisId, fanId));
406 }
407 
408 inline void handleFanHead(App& app, const crow::Request& req,
409                           const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
410                           const std::string& chassisId,
411                           const std::string& fanId)
412 {
413     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
414     {
415         return;
416     }
417 
418     redfish::chassis_utils::getValidChassisPath(
419         asyncResp, chassisId,
420         [asyncResp, chassisId,
421          fanId](const std::optional<std::string>& validChassisPath) {
422             if (!validChassisPath)
423             {
424                 messages::resourceNotFound(asyncResp->res, "Chassis",
425                                            chassisId);
426                 return;
427             }
428             getValidFanObject(
429                 asyncResp, *validChassisPath, fanId,
430                 [asyncResp](const std::string&, const std::string&) {
431                     asyncResp->res.addHeader(
432                         boost::beast::http::field::link,
433                         "</redfish/v1/JsonSchemas/Fan/Fan.json>; rel=describedby");
434                 });
435         });
436 }
437 
438 inline void handleFanGet(App& app, const crow::Request& req,
439                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
440                          const std::string& chassisId, const std::string& fanId)
441 {
442     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
443     {
444         return;
445     }
446 
447     redfish::chassis_utils::getValidChassisPath(
448         asyncResp, chassisId,
449         std::bind_front(doFanGet, asyncResp, chassisId, fanId));
450 }
451 
452 inline void handleSetFanPathById(
453     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
454     const std::string& chassisId, const std::string& fanId,
455     bool locationIndicatorActive, const boost::system::error_code& ec,
456     const dbus::utility::MapperGetSubTreePathsResponse& fanPaths)
457 {
458     if (ec)
459     {
460         if (ec.value() == boost::system::errc::io_error)
461         {
462             BMCWEB_LOG_WARNING("Chassis {} not found", chassisId);
463             messages::resourceNotFound(asyncResp->res, "Chassis", chassisId);
464             return;
465         }
466 
467         BMCWEB_LOG_ERROR("DBUS response error {}", ec.value());
468         messages::internalError(asyncResp->res);
469         return;
470     }
471 
472     for (const auto& fanPath : fanPaths)
473     {
474         if (checkFanId(fanPath, fanId))
475         {
476             setLocationIndicatorActive(asyncResp, fanPath,
477                                        locationIndicatorActive);
478             return;
479         }
480     }
481     BMCWEB_LOG_WARNING("Fan {} not found", fanId);
482     messages::resourceNotFound(asyncResp->res, "Fan", fanId);
483 }
484 
485 inline void handleFanPatch(App& app, const crow::Request& req,
486                            const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
487                            const std::string& chassisId,
488                            const std::string& fanId)
489 {
490     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
491     {
492         return;
493     }
494 
495     std::optional<bool> locationIndicatorActive;
496     if (!json_util::readJsonPatch(req, asyncResp->res,
497                                   "LocationIndicatorActive",
498                                   locationIndicatorActive))
499     {
500         return;
501     }
502 
503     if (locationIndicatorActive)
504     {
505         dbus::utility::getAssociatedSubTreePathsById(
506             chassisId, "/xyz/openbmc_project/inventory", chassisInterfaces,
507             "cooled_by", fanInterface,
508             [asyncResp, chassisId, fanId, locationIndicatorActive](
509                 const boost::system::error_code& ec,
510                 const dbus::utility::MapperGetSubTreePathsResponse&
511                     subtreePaths) {
512                 handleSetFanPathById(asyncResp, chassisId, fanId,
513                                      *locationIndicatorActive, ec,
514                                      subtreePaths);
515             });
516     }
517 }
518 
519 inline void requestRoutesFan(App& app)
520 {
521     BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/ThermalSubsystem/Fans/<str>/")
522         .privileges(redfish::privileges::headFan)
523         .methods(boost::beast::http::verb::head)(
524             std::bind_front(handleFanHead, std::ref(app)));
525 
526     BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/ThermalSubsystem/Fans/<str>/")
527         .privileges(redfish::privileges::getFan)
528         .methods(boost::beast::http::verb::get)(
529             std::bind_front(handleFanGet, std::ref(app)));
530 
531     BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/ThermalSubsystem/Fans/<str>/")
532         .privileges(redfish::privileges::patchFan)
533         .methods(boost::beast::http::verb::patch)(
534             std::bind_front(handleFanPatch, std::ref(app)));
535 }
536 
537 } // namespace redfish
538