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