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 /**
28  * @function extracts fan name from dbus path string (last token where
29  * delimiter is the / character), with proper bounds checking.
30  * @param[in] path - D-Bus path
31  * @return just the fan name.
32  */
33 
34 std::string justFanName(std::string const& path)
35 {
36     std::string fanName;
37 
38     auto itr = path.rfind("/");
39     if (itr != std::string::npos && itr < path.size())
40     {
41         fanName = path.substr(1 + itr);
42     }
43 
44     return fanName;
45 }
46 
47 /**
48  * @function produces subtree paths whose names match fan token names.
49  * @param[in] path - D-Bus path to obtain subtree from
50  * @param[in] iface - interface to obtain subTreePaths from
51  * @param[in] fans - label matching tokens to filter by
52  * @param[in] shortPath - flag to shorten fan token
53  * @return map of paths by fan name
54  */
55 
56 std::map<std::string, std::vector<std::string>>
57     getPathsFromIface(const std::string& path, const std::string& iface,
58                       const std::vector<std::string>& fans,
59                       bool shortPath = false)
60 {
61     std::map<std::string, std::vector<std::string>> dest;
62 
63     for (auto& path :
64          SDBusPlus::getSubTreePathsRaw(SDBusPlus::getBus(), path, iface, 0))
65     {
66         for (auto& fan : fans)
67         {
68             if (shortPath)
69             {
70                 if (fan == justFanName(path))
71                 {
72                     dest[fan].push_back(path);
73                 }
74             }
75             else if (std::string::npos != path.find(fan + "_"))
76             {
77                 dest[fan].push_back(path);
78             }
79         }
80     }
81 
82     return dest;
83 }
84 
85 /**
86  * @function consolidated function to load dbus paths and fan names
87  */
88 auto loadDBusData()
89 {
90     auto& bus{SDBusPlus::getBus()};
91 
92     std::vector<std::string> fanNames;
93 
94     // paths by D-bus interface,fan name
95     std::map<std::string, std::map<std::string, std::vector<std::string>>>
96         pathMap;
97 
98     std::string method("RPM");
99 
100     std::map<const std::string, const std::string> interfaces{
101         {"FanSpeed", "xyz.openbmc_project.Control.FanSpeed"},
102         {"FanPwm", "xyz.openbmc_project.Control.FanPwm"},
103         {"SensorValue", "xyz.openbmc_project.Sensor.Value"},
104         {"Item", "xyz.openbmc_project.Inventory.Item"},
105         {"OpStatus", "xyz.openbmc_project.State.Decorator.OperationalStatus"}};
106 
107     std::map<const std::string, const std::string> paths{
108         {"motherboard",
109          "/xyz/openbmc_project/inventory/system/chassis/motherboard"},
110         {"tach", "/xyz/openbmc_project/sensors/fan_tach"}};
111 
112     // build a list of all fans
113     for (auto& path : SDBusPlus::getSubTreePathsRaw(bus, paths["tach"],
114                                                     interfaces["FanSpeed"], 0))
115     {
116         // special case where we build the list of fans
117         auto fan = justFanName(path);
118         fan = fan.substr(0, fan.rfind("_"));
119         fanNames.push_back(fan);
120     }
121 
122     // retry with PWM mode if none found
123     if (0 == fanNames.size())
124     {
125         method = "PWM";
126 
127         for (auto& path : SDBusPlus::getSubTreePathsRaw(
128                  bus, paths["tach"], interfaces["FanPwm"], 0))
129         {
130             // special case where we build the list of fans
131             auto fan = justFanName(path);
132             fan = fan.substr(0, fan.rfind("_"));
133             fanNames.push_back(fan);
134         }
135     }
136 
137     // load tach sensor paths for each fan
138     pathMap["tach"] =
139         getPathsFromIface(paths["tach"], interfaces["SensorValue"], fanNames);
140 
141     // load inventory Item data for each fan
142     pathMap["inventory"] = getPathsFromIface(
143         paths["motherboard"], interfaces["Item"], fanNames, true);
144 
145     // load operational status data for each fan
146     pathMap["opstatus"] = getPathsFromIface(
147         paths["motherboard"], interfaces["OpStatus"], fanNames, true);
148 
149     return std::make_tuple(fanNames, pathMap, interfaces, method);
150 }
151 
152 /**
153  * @function gets the states of phosphor-fanctl. equivalent to
154  * "systemctl status phosphor-fan-control@0"
155  * @return a list of several (sub)states of fanctl (loaded,
156  * active, running) as well as D-Bus properties representing
157  * BMC states (bmc state,chassis power state, host state)
158  */
159 
160 std::array<std::string, 6> getStates()
161 {
162     using DBusTuple =
163         std::tuple<std::string, std::string, std::string, std::string,
164                    std::string, std::string, sdbusplus::message::object_path,
165                    uint32_t, std::string, sdbusplus::message::object_path>;
166 
167     constexpr auto systemdMgrIface = "org.freedesktop.systemd1.Manager";
168     constexpr auto systemdPath = "/org/freedesktop/systemd1";
169     constexpr auto systemdService = "org.freedesktop.systemd1";
170 
171     std::array<std::string, 6> ret;
172 
173     std::vector<std::string> services{"phosphor-fan-control@0.service"};
174 
175     try
176     {
177         auto fields{SDBusPlus::callMethodAndRead<std::vector<DBusTuple>>(
178             systemdService, systemdPath, systemdMgrIface, "ListUnitsByNames",
179             services)};
180 
181         if (fields.size() > 0)
182         {
183             ret[0] = std::get<2>(fields[0]);
184             ret[1] = std::get<3>(fields[0]);
185             ret[2] = std::get<4>(fields[0]);
186         }
187         else
188         {
189             std::cout << "No units found for systemd service: " << services[0]
190                       << std::endl;
191         }
192     }
193     catch (const std::exception& e)
194     {
195         std::cerr << "Failure retrieving phosphor-fan-control states: "
196                   << e.what() << std::endl;
197     }
198 
199     std::string path("/xyz/openbmc_project/state/bmc0");
200     std::string iface("xyz.openbmc_project.State.BMC");
201     ret[3] =
202         SDBusPlus::getProperty<std::string>(path, iface, "CurrentBMCState");
203 
204     path = "/xyz/openbmc_project/state/chassis0";
205     iface = "xyz.openbmc_project.State.Chassis";
206     ret[4] =
207         SDBusPlus::getProperty<std::string>(path, iface, "CurrentPowerState");
208 
209     path = "/xyz/openbmc_project/state/host0";
210     iface = "xyz.openbmc_project.State.Host";
211     ret[5] =
212         SDBusPlus::getProperty<std::string>(path, iface, "CurrentHostState");
213 
214     return ret;
215 }
216 
217 /**
218  * @function helper to determine interface type from a given control method
219  */
220 std::string ifaceTypeFromMethod(const std::string& method)
221 {
222     return (method == "RPM" ? "FanSpeed" : "FanPwm");
223 }
224 
225 /**
226  * @function performs the "status" command from the cmdline.
227  * get states and sensor data and output to the console
228  */
229 void status()
230 {
231     using std::cout;
232     using std::endl;
233     using std::setw;
234 
235     auto busData = loadDBusData();
236     auto& method = std::get<3>(busData);
237 
238     std::string property;
239 
240     // get the state,substate of fan-control and obmc
241     auto states = getStates();
242 
243     // print the header
244     cout << "Fan Control Service State   : " << states[0] << ", " << states[1]
245          << "(" << states[2] << ")" << endl;
246     cout << endl;
247     cout << "CurrentBMCState     : " << states[3] << endl;
248     cout << "CurrentPowerState   : " << states[4] << endl;
249     cout << "CurrentHostState    : " << states[5] << endl;
250     cout << endl;
251     cout << " FAN        "
252          << "TARGET(" << method << ")  FEEDBACKS(RPMS)   PRESENT"
253          << "   FUNCTIONAL" << endl;
254     cout << "==============================================================="
255          << endl;
256 
257     auto& fanNames{std::get<0>(busData)};
258     auto& pathMap{std::get<1>(busData)};
259     auto& interfaces{std::get<2>(busData)};
260 
261     for (auto& fan : fanNames)
262     {
263         cout << " " << fan << setw(18);
264 
265         // get the target RPM
266         property = "Target";
267         cout << SDBusPlus::getProperty<uint64_t>(
268                     pathMap["tach"][fan][0],
269                     interfaces[ifaceTypeFromMethod(method)], property)
270              << setw(11);
271 
272         // get the sensor RPM
273         property = "Value";
274 
275         int numRotors = pathMap["tach"][fan].size();
276         // print tach readings for each rotor
277         for (auto& path : pathMap["tach"][fan])
278         {
279             cout << SDBusPlus::getProperty<double>(
280                 path, interfaces["SensorValue"], property);
281 
282             // dont print slash on last rotor
283             if (--numRotors)
284                 cout << "/";
285         }
286         cout << setw(10);
287 
288         // print the Present property
289         property = "Present";
290         std::string val;
291         for (auto& path : pathMap["inventory"][fan])
292         {
293             try
294             {
295                 if (SDBusPlus::getProperty<bool>(path, interfaces["Item"],
296                                                  property))
297                 {
298                     val = "true";
299                 }
300                 else
301                 {
302                     val = "false";
303                 }
304             }
305             catch (phosphor::fan::util::DBusPropertyError&)
306             {
307                 val = "Unknown";
308             }
309             cout << val;
310         }
311 
312         cout << setw(13);
313 
314         // and the functional property
315         property = "Functional";
316         for (auto& path : pathMap["opstatus"][fan])
317         {
318             try
319             {
320                 if (SDBusPlus::getProperty<bool>(path, interfaces["OpStatus"],
321                                                  property))
322                 {
323                     val = "true";
324                 }
325                 else
326                 {
327                     val = "false";
328                 }
329             }
330             catch (phosphor::fan::util::DBusPropertyError&)
331             {
332                 val = "Unknown";
333             }
334             cout << val;
335         }
336 
337         cout << endl;
338     }
339 }
340 
341 /**
342  * @function print target RPM/PWM and tach readings from each fan
343  */
344 void get()
345 {
346     using std::cout;
347     using std::endl;
348     using std::setw;
349 
350     auto busData = loadDBusData();
351 
352     auto& fanNames{std::get<0>(busData)};
353     auto& pathMap{std::get<1>(busData)};
354     auto& interfaces{std::get<2>(busData)};
355     auto& method = std::get<3>(busData);
356 
357     std::string property;
358 
359     // print the header
360     cout << "TARGET SENSOR" << setw(11) << "TARGET(" << method
361          << ")   FEEDBACK SENSOR   ";
362     cout << "FEEDBACK(" << method << ")" << endl;
363     cout << "==============================================================="
364          << endl;
365 
366     for (auto& fan : fanNames)
367     {
368         if (pathMap["tach"][fan].size() == 0)
369             continue;
370         // print just the sensor name
371         auto shortPath = pathMap["tach"][fan][0];
372         shortPath = justFanName(shortPath);
373         cout << shortPath << setw(22);
374 
375         // print its target RPM/PWM
376         property = "Target";
377         cout << SDBusPlus::getProperty<uint64_t>(
378                     pathMap["tach"][fan][0],
379                     interfaces[ifaceTypeFromMethod(method)], property)
380              << setw(12) << " ";
381 
382         // print readings for each rotor
383         property = "Value";
384 
385         auto indent = 0U;
386         for (auto& path : pathMap["tach"][fan])
387         {
388             cout << setw(indent);
389             cout << justFanName(path) << setw(17)
390                  << SDBusPlus::getProperty<double>(
391                         path, interfaces["SensorValue"], property)
392                  << endl;
393 
394             if (0 == indent)
395                 indent = 46;
396         }
397     }
398 }
399 
400 /**
401  * @function main entry point for the application
402  */
403 int main(int argc, char* argv[])
404 {
405     auto rc = 0;
406 
407     try
408     {
409         CLI::App app{R"(Manually control, get fan tachs, view status, and resume
410              automatic control of all fans within a chassis.)"};
411 
412         app.set_help_flag("-h,--help", "Print this help page and exit.");
413 
414         // App requires only 1 subcommand to be given
415         app.require_subcommand(1);
416 
417         // This represents the command given
418         auto commands = app.add_option_group("Commands");
419 
420         // Configure method
421         auto cmdStatus = commands->add_subcommand(
422             "status",
423             "Get the fan tach targets/values and fan-control service status");
424 
425         cmdStatus->set_help_flag(
426             "-h, --help", "Prints fan target/tach readings, present/functional "
427                           "states, and fan-monitor/BMC/Power service status");
428         cmdStatus->require_option(0);
429 
430         // Get method
431         auto cmdGet = commands->add_subcommand(
432             "get",
433             "Get the current fan target and feedback speeds for all rotors");
434         cmdGet->set_help_flag(
435             "-h, --help",
436             "Get the current fan target and feedback speeds for all rotors");
437         cmdGet->require_option(0);
438 
439         CLI11_PARSE(app, argc, argv);
440 
441         if (app.got_subcommand("status"))
442         {
443             status();
444         }
445         else if (app.got_subcommand("get"))
446         {
447             get();
448         }
449     }
450     catch (const std::exception& e)
451     {
452         rc = -1;
453         std::cerr << argv[0] << " failed: " << e.what() << std::endl;
454     }
455 
456     return rc;
457 }
458