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