1 /**
2  * Copyright © 2021 IBM Corporation
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #include "sdbusplus.hpp"
18 
19 #include <CLI/CLI.hpp>
20 #include <sdbusplus/bus.hpp>
21 
22 #include <iomanip>
23 #include <iostream>
24 
25 using SDBusPlus = phosphor::fan::util::SDBusPlus;
26 
27 constexpr auto phosphorServiceName = "phosphor-fan-control@0.service";
28 constexpr auto systemdMgrIface = "org.freedesktop.systemd1.Manager";
29 constexpr auto systemdPath = "/org/freedesktop/systemd1";
30 constexpr auto systemdService = "org.freedesktop.systemd1";
31 
32 enum
33 {
34     FAN_NAMES = 0,
35     PATH_MAP = 1,
36     IFACES = 2,
37     METHOD = 3
38 };
39 
40 /**
41  * @function extracts fan name from dbus path string (last token where
42  * delimiter is the / character), with proper bounds checking.
43  * @param[in] path - D-Bus path
44  * @return just the fan name.
45  */
46 
47 std::string justFanName(std::string const& path)
48 {
49     std::string fanName;
50 
51     auto itr = path.rfind("/");
52     if (itr != std::string::npos && itr < path.size())
53     {
54         fanName = path.substr(1 + itr);
55     }
56 
57     return fanName;
58 }
59 
60 /**
61  * @function produces subtree paths whose names match fan token names.
62  * @param[in] path - D-Bus path to obtain subtree from
63  * @param[in] iface - interface to obtain subTreePaths from
64  * @param[in] fans - label matching tokens to filter by
65  * @param[in] shortPath - flag to shorten fan token
66  * @return map of paths by fan name
67  */
68 
69 std::map<std::string, std::vector<std::string>>
70     getPathsFromIface(const std::string& path, const std::string& iface,
71                       const std::vector<std::string>& fans,
72                       bool shortPath = false)
73 {
74     std::map<std::string, std::vector<std::string>> dest;
75 
76     for (auto& path :
77          SDBusPlus::getSubTreePathsRaw(SDBusPlus::getBus(), path, iface, 0))
78     {
79         for (auto& fan : fans)
80         {
81             if (shortPath)
82             {
83                 if (fan == justFanName(path))
84                 {
85                     dest[fan].push_back(path);
86                 }
87             }
88             else if (std::string::npos != path.find(fan + "_"))
89             {
90                 dest[fan].push_back(path);
91             }
92         }
93     }
94 
95     return dest;
96 }
97 
98 /**
99  * @function consolidated function to load dbus paths and fan names
100  */
101 auto loadDBusData()
102 {
103     auto& bus{SDBusPlus::getBus()};
104 
105     std::vector<std::string> fanNames;
106 
107     // paths by D-bus interface,fan name
108     std::map<std::string, std::map<std::string, std::vector<std::string>>>
109         pathMap;
110 
111     std::string method("RPM");
112 
113     std::map<const std::string, const std::string> interfaces{
114         {"FanSpeed", "xyz.openbmc_project.Control.FanSpeed"},
115         {"FanPwm", "xyz.openbmc_project.Control.FanPwm"},
116         {"SensorValue", "xyz.openbmc_project.Sensor.Value"},
117         {"Item", "xyz.openbmc_project.Inventory.Item"},
118         {"OpStatus", "xyz.openbmc_project.State.Decorator.OperationalStatus"}};
119 
120     std::map<const std::string, const std::string> paths{
121         {"motherboard",
122          "/xyz/openbmc_project/inventory/system/chassis/motherboard"},
123         {"tach", "/xyz/openbmc_project/sensors/fan_tach"}};
124 
125     // build a list of all fans
126     for (auto& path : SDBusPlus::getSubTreePathsRaw(bus, paths["tach"],
127                                                     interfaces["FanSpeed"], 0))
128     {
129         // special case where we build the list of fans
130         auto fan = justFanName(path);
131         fan = fan.substr(0, fan.rfind("_"));
132         fanNames.push_back(fan);
133     }
134 
135     // retry with PWM mode if none found
136     if (0 == fanNames.size())
137     {
138         method = "PWM";
139 
140         for (auto& path : SDBusPlus::getSubTreePathsRaw(
141                  bus, paths["tach"], interfaces["FanPwm"], 0))
142         {
143             // special case where we build the list of fans
144             auto fan = justFanName(path);
145             fan = fan.substr(0, fan.rfind("_"));
146             fanNames.push_back(fan);
147         }
148     }
149 
150     // load tach sensor paths for each fan
151     pathMap["tach"] =
152         getPathsFromIface(paths["tach"], interfaces["SensorValue"], fanNames);
153 
154     // load inventory Item data for each fan
155     pathMap["inventory"] = getPathsFromIface(
156         paths["motherboard"], interfaces["Item"], fanNames, true);
157 
158     // load operational status data for each fan
159     pathMap["opstatus"] = getPathsFromIface(
160         paths["motherboard"], interfaces["OpStatus"], fanNames, true);
161 
162     return std::make_tuple(fanNames, pathMap, interfaces, method);
163 }
164 
165 /**
166  * @function gets the states of phosphor-fanctl. equivalent to
167  * "systemctl status phosphor-fan-control@0"
168  * @return a list of several (sub)states of fanctl (loaded,
169  * active, running) as well as D-Bus properties representing
170  * BMC states (bmc state,chassis power state, host state)
171  */
172 
173 std::array<std::string, 6> getStates()
174 {
175     using DBusTuple =
176         std::tuple<std::string, std::string, std::string, std::string,
177                    std::string, std::string, sdbusplus::message::object_path,
178                    uint32_t, std::string, sdbusplus::message::object_path>;
179 
180     std::array<std::string, 6> ret;
181 
182     std::vector<std::string> services{phosphorServiceName};
183 
184     try
185     {
186         auto fields{SDBusPlus::callMethodAndRead<std::vector<DBusTuple>>(
187             systemdService, systemdPath, systemdMgrIface, "ListUnitsByNames",
188             services)};
189 
190         if (fields.size() > 0)
191         {
192             ret[0] = std::get<2>(fields[0]);
193             ret[1] = std::get<3>(fields[0]);
194             ret[2] = std::get<4>(fields[0]);
195         }
196         else
197         {
198             std::cout << "No units found for systemd service: " << services[0]
199                       << std::endl;
200         }
201     }
202     catch (const std::exception& e)
203     {
204         std::cerr << "Failure retrieving phosphor-fan-control states: "
205                   << e.what() << std::endl;
206     }
207 
208     std::string path("/xyz/openbmc_project/state/bmc0");
209     std::string iface("xyz.openbmc_project.State.BMC");
210     ret[3] =
211         SDBusPlus::getProperty<std::string>(path, iface, "CurrentBMCState");
212 
213     path = "/xyz/openbmc_project/state/chassis0";
214     iface = "xyz.openbmc_project.State.Chassis";
215     ret[4] =
216         SDBusPlus::getProperty<std::string>(path, iface, "CurrentPowerState");
217 
218     path = "/xyz/openbmc_project/state/host0";
219     iface = "xyz.openbmc_project.State.Host";
220     ret[5] =
221         SDBusPlus::getProperty<std::string>(path, iface, "CurrentHostState");
222 
223     return ret;
224 }
225 
226 /**
227  * @function helper to determine interface type from a given control method
228  */
229 std::string ifaceTypeFromMethod(const std::string& method)
230 {
231     return (method == "RPM" ? "FanSpeed" : "FanPwm");
232 }
233 
234 /**
235  * @function performs the "status" command from the cmdline.
236  * get states and sensor data and output to the console
237  */
238 void status()
239 {
240     using std::cout;
241     using std::endl;
242     using std::setw;
243 
244     auto busData = loadDBusData();
245     auto& method = std::get<METHOD>(busData);
246 
247     std::string property;
248 
249     // get the state,substate of fan-control and obmc
250     auto states = getStates();
251 
252     // print the header
253     cout << "Fan Control Service State   : " << states[0] << ", " << states[1]
254          << "(" << states[2] << ")" << endl;
255     cout << endl;
256     cout << "CurrentBMCState     : " << states[3] << endl;
257     cout << "CurrentPowerState   : " << states[4] << endl;
258     cout << "CurrentHostState    : " << states[5] << endl;
259     cout << endl;
260     cout << " FAN        "
261          << "TARGET(" << method << ")  FEEDBACKS(RPMS)   PRESENT"
262          << "   FUNCTIONAL" << endl;
263     cout << "==============================================================="
264          << endl;
265 
266     auto& fanNames{std::get<FAN_NAMES>(busData)};
267     auto& pathMap{std::get<PATH_MAP>(busData)};
268     auto& interfaces{std::get<IFACES>(busData)};
269 
270     for (auto& fan : fanNames)
271     {
272         cout << " " << fan << setw(18);
273 
274         // get the target RPM
275         property = "Target";
276         cout << SDBusPlus::getProperty<uint64_t>(
277                     pathMap["tach"][fan][0],
278                     interfaces[ifaceTypeFromMethod(method)], property)
279              << setw(11);
280 
281         // get the sensor RPM
282         property = "Value";
283 
284         int numRotors = pathMap["tach"][fan].size();
285         // print tach readings for each rotor
286         for (auto& path : pathMap["tach"][fan])
287         {
288             cout << SDBusPlus::getProperty<double>(
289                 path, interfaces["SensorValue"], property);
290 
291             // dont print slash on last rotor
292             if (--numRotors)
293                 cout << "/";
294         }
295         cout << setw(10);
296 
297         // print the Present property
298         property = "Present";
299         std::string val;
300         for (auto& path : pathMap["inventory"][fan])
301         {
302             try
303             {
304                 if (SDBusPlus::getProperty<bool>(path, interfaces["Item"],
305                                                  property))
306                 {
307                     val = "true";
308                 }
309                 else
310                 {
311                     val = "false";
312                 }
313             }
314             catch (const phosphor::fan::util::DBusPropertyError&)
315             {
316                 val = "Unknown";
317             }
318             cout << val;
319         }
320 
321         cout << setw(13);
322 
323         // and the functional property
324         property = "Functional";
325         for (auto& path : pathMap["opstatus"][fan])
326         {
327             try
328             {
329                 if (SDBusPlus::getProperty<bool>(path, interfaces["OpStatus"],
330                                                  property))
331                 {
332                     val = "true";
333                 }
334                 else
335                 {
336                     val = "false";
337                 }
338             }
339             catch (const phosphor::fan::util::DBusPropertyError&)
340             {
341                 val = "Unknown";
342             }
343             cout << val;
344         }
345 
346         cout << endl;
347     }
348 }
349 
350 /**
351  * @function print target RPM/PWM and tach readings from each fan
352  */
353 void get()
354 {
355     using std::cout;
356     using std::endl;
357     using std::setw;
358 
359     auto busData = loadDBusData();
360 
361     auto& fanNames{std::get<FAN_NAMES>(busData)};
362     auto& pathMap{std::get<PATH_MAP>(busData)};
363     auto& interfaces{std::get<IFACES>(busData)};
364     auto& method = std::get<METHOD>(busData);
365 
366     std::string property;
367 
368     // print the header
369     cout << "TARGET SENSOR" << setw(11) << "TARGET(" << method
370          << ")   FEEDBACK SENSOR   ";
371     cout << "FEEDBACK(" << method << ")" << endl;
372     cout << "==============================================================="
373          << endl;
374 
375     for (auto& fan : fanNames)
376     {
377         if (pathMap["tach"][fan].size() == 0)
378             continue;
379         // print just the sensor name
380         auto shortPath = pathMap["tach"][fan][0];
381         shortPath = justFanName(shortPath);
382         cout << shortPath << setw(22);
383 
384         // print its target RPM/PWM
385         property = "Target";
386         cout << SDBusPlus::getProperty<uint64_t>(
387                     pathMap["tach"][fan][0],
388                     interfaces[ifaceTypeFromMethod(method)], property)
389              << setw(12) << " ";
390 
391         // print readings for each rotor
392         property = "Value";
393 
394         auto indent = 0U;
395         for (auto& path : pathMap["tach"][fan])
396         {
397             cout << setw(indent);
398             cout << justFanName(path) << setw(17)
399                  << SDBusPlus::getProperty<double>(
400                         path, interfaces["SensorValue"], property)
401                  << endl;
402 
403             if (0 == indent)
404                 indent = 46;
405         }
406     }
407 }
408 
409 /**
410  * @function set fan[s] to a target RPM
411  */
412 void set(uint64_t target, std::vector<std::string> fanList)
413 {
414     auto busData = loadDBusData();
415 
416     auto& bus{SDBusPlus::getBus()};
417     auto& pathMap{std::get<PATH_MAP>(busData)};
418     auto& interfaces{std::get<IFACES>(busData)};
419     auto& method = std::get<METHOD>(busData);
420 
421     std::string ifaceType(method == "RPM" ? "FanSpeed" : "FanPwm");
422 
423     // stop the fan-control service
424     auto retval = SDBusPlus::callMethodAndRead<sdbusplus::message::object_path>(
425         systemdService, systemdPath, systemdMgrIface, "StopUnit",
426         phosphorServiceName, "replace");
427 
428     if (fanList.size() == 0)
429     {
430         fanList = std::get<FAN_NAMES>(busData);
431     }
432 
433     for (auto& fan : fanList)
434     {
435         try
436         {
437             auto paths(pathMap["tach"].find(fan));
438 
439             if (pathMap["tach"].end() == paths)
440             {
441                 // try again, maybe it was a sensor name instead of a fan name
442                 for (const auto& [fanName, sensors] : pathMap["tach"])
443                 {
444                     for (const auto& path : sensors)
445                     {
446                         std::string sensor(path.substr(path.rfind("/")));
447 
448                         if (sensor.size() > 0)
449                         {
450                             sensor = sensor.substr(1);
451 
452                             if (sensor == fan)
453                             {
454                                 paths = pathMap["tach"].find(fanName);
455 
456                                 break;
457                             }
458                         }
459                     }
460                 }
461             }
462 
463             if (pathMap["tach"].end() == paths)
464             {
465                 std::cout << "Could not find tach path for fan: " << fan
466                           << std::endl;
467                 continue;
468             }
469 
470             // set the target RPM
471             SDBusPlus::setProperty<uint64_t>(bus, paths->second[0],
472                                              interfaces[ifaceType], "Target",
473                                              std::move(target));
474         }
475         catch (const phosphor::fan::util::DBusPropertyError& e)
476         {
477             std::cerr << "Cannot set target rpm for " << fan
478                       << " caught D-Bus exception: " << e.what() << std::endl;
479         }
480     }
481 }
482 
483 /**
484  * @function restart fan-control to allow it to manage fan speeds
485  */
486 void resume()
487 {
488     try
489     {
490         auto retval =
491             SDBusPlus::callMethodAndRead<sdbusplus::message::object_path>(
492                 systemdService, systemdPath, systemdMgrIface, "StartUnit",
493                 phosphorServiceName, "replace");
494     }
495     catch (const phosphor::fan::util::DBusMethodError& e)
496     {
497         std::cerr << "Unable to start fan control: " << e.what() << std::endl;
498     }
499 }
500 
501 /**
502  * @function main entry point for the application
503  */
504 int main(int argc, char* argv[])
505 {
506     auto rc = 0;
507     uint64_t target{0U};
508     std::vector<std::string> fanList;
509 
510     try
511     {
512         CLI::App app{R"(Manually control, get fan tachs, view status, and resume
513              automatic control of all fans within a chassis.)"};
514 
515         app.set_help_flag("-h,--help", "Print this help page and exit.");
516 
517         // App requires only 1 subcommand to be given
518         app.require_subcommand(1);
519 
520         // This represents the command given
521         auto commands = app.add_option_group("Commands");
522 
523         // status method
524         auto cmdStatus = commands->add_subcommand(
525             "status",
526             "Get the fan tach targets/values and fan-control service status");
527         cmdStatus->set_help_flag(
528             "-h, --help", "Prints fan target/tach readings, present/functional "
529                           "states, and fan-monitor/BMC/Power service status");
530         cmdStatus->require_option(0);
531 
532         // get method
533         auto cmdGet = commands->add_subcommand(
534             "get",
535             "Get the current fan target and feedback speeds for all rotors");
536         cmdGet->set_help_flag(
537             "-h, --help",
538             "Get the current fan target and feedback speeds for all rotors");
539         cmdGet->require_option(0);
540 
541         // set method
542         auto cmdSet = commands->add_subcommand(
543             "set", "Set fan(s) target speed for all rotors");
544         cmdSet->add_option("target", target, "RPM/PWM target to set the fans");
545         cmdSet->add_option("fan list", fanList,
546                            "[optional] list of fans to set target RPM");
547 
548         std::string strHelp = "Resume running phosphor-fan-control";
549         auto cmdResume = commands->add_subcommand("resume", strHelp);
550         cmdResume->set_help_flag("-h, --help", strHelp);
551         cmdResume->require_option(0);
552 
553         auto setHelp(R"(set <TARGET> [\"TARGET SENSOR LIST\"]
554       <TARGET>
555           - RPM/PWM target to set the fans
556 [TARGET SENSOR LIST]
557   - list of target sensors to set)");
558         cmdSet->set_help_flag("-h, --help", setHelp);
559         cmdSet->require_option();
560 
561         CLI11_PARSE(app, argc, argv);
562 
563         if (app.got_subcommand("status"))
564         {
565             status();
566         }
567         else if (app.got_subcommand("set"))
568         {
569             set(target, fanList);
570         }
571         else if (app.got_subcommand("get"))
572         {
573             get();
574         }
575         else if (app.got_subcommand("resume"))
576         {
577             resume();
578         }
579     }
580     catch (const std::exception& e)
581     {
582         rc = -1;
583         std::cerr << argv[0] << " failed: " << e.what() << std::endl;
584     }
585 
586     return rc;
587 }
588