blob: c701817f0a61723463d0037878d2861d6609a87f [file] [log] [blame]
Mike Capps385b1982021-06-28 10:40:42 -04001/**
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
25using SDBusPlus = phosphor::fan::util::SDBusPlus;
26
Mike Capps16aab352021-06-30 10:17:21 -040027constexpr auto phosphorServiceName = "phosphor-fan-control@0.service";
28constexpr auto systemdMgrIface = "org.freedesktop.systemd1.Manager";
29constexpr auto systemdPath = "/org/freedesktop/systemd1";
30constexpr auto systemdService = "org.freedesktop.systemd1";
31
32enum
33{
34 FAN_NAMES = 0,
35 PATH_MAP = 1,
36 IFACES = 2,
37 METHOD = 3
38};
39
Mike Capps385b1982021-06-28 10:40:42 -040040/**
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
47std::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
69std::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/**
Mike Capps530c6552021-06-29 09:50:38 -040099 * @function consolidated function to load dbus paths and fan names
100 */
101auto 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/**
Mike Capps385b1982021-06-28 10:40:42 -0400166 * @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
173std::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
Mike Capps16aab352021-06-30 10:17:21 -0400182 std::vector<std::string> services{phosphorServiceName};
Mike Capps385b1982021-06-28 10:40:42 -0400183
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 }
Mike Capps530c6552021-06-29 09:50:38 -0400202 catch (const std::exception& e)
Mike Capps385b1982021-06-28 10:40:42 -0400203 {
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/**
Mike Capps530c6552021-06-29 09:50:38 -0400227 * @function helper to determine interface type from a given control method
228 */
229std::string ifaceTypeFromMethod(const std::string& method)
230{
231 return (method == "RPM" ? "FanSpeed" : "FanPwm");
232}
233
234/**
Mike Capps385b1982021-06-28 10:40:42 -0400235 * @function performs the "status" command from the cmdline.
236 * get states and sensor data and output to the console
237 */
238void status()
239{
240 using std::cout;
241 using std::endl;
242 using std::setw;
243
Mike Capps530c6552021-06-29 09:50:38 -0400244 auto busData = loadDBusData();
Mike Capps16aab352021-06-30 10:17:21 -0400245 auto& method = std::get<METHOD>(busData);
Mike Capps385b1982021-06-28 10:40:42 -0400246
Mike Capps530c6552021-06-29 09:50:38 -0400247 std::string property;
Mike Capps385b1982021-06-28 10:40:42 -0400248
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 "
Mike Capps530c6552021-06-29 09:50:38 -0400261 << "TARGET(" << method << ") FEEDBACKS(RPMS) PRESENT"
Mike Capps385b1982021-06-28 10:40:42 -0400262 << " FUNCTIONAL" << endl;
263 cout << "==============================================================="
264 << endl;
265
Mike Capps16aab352021-06-30 10:17:21 -0400266 auto& fanNames{std::get<FAN_NAMES>(busData)};
267 auto& pathMap{std::get<PATH_MAP>(busData)};
268 auto& interfaces{std::get<IFACES>(busData)};
Mike Capps530c6552021-06-29 09:50:38 -0400269
Mike Capps385b1982021-06-28 10:40:42 -0400270 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>(
Mike Capps530c6552021-06-29 09:50:38 -0400277 pathMap["tach"][fan][0],
278 interfaces[ifaceTypeFromMethod(method)], property)
Mike Capps385b1982021-06-28 10:40:42 -0400279 << setw(11);
280
281 // get the sensor RPM
282 property = "Value";
Mike Capps530c6552021-06-29 09:50:38 -0400283
Mike Capps385b1982021-06-28 10:40:42 -0400284 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
Mike Capps530c6552021-06-29 09:50:38 -0400297 // print the Present property
Mike Capps385b1982021-06-28 10:40:42 -0400298 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 }
Patrick Williamsddb773b2021-10-06 11:24:49 -0500314 catch (const phosphor::fan::util::DBusPropertyError&)
Mike Capps385b1982021-06-28 10:40:42 -0400315 {
316 val = "Unknown";
317 }
318 cout << val;
319 }
320
321 cout << setw(13);
322
Mike Capps530c6552021-06-29 09:50:38 -0400323 // and the functional property
Mike Capps385b1982021-06-28 10:40:42 -0400324 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 }
Patrick Williamsddb773b2021-10-06 11:24:49 -0500339 catch (const phosphor::fan::util::DBusPropertyError&)
Mike Capps385b1982021-06-28 10:40:42 -0400340 {
341 val = "Unknown";
342 }
343 cout << val;
344 }
345
346 cout << endl;
347 }
348}
349
350/**
Mike Capps530c6552021-06-29 09:50:38 -0400351 * @function print target RPM/PWM and tach readings from each fan
352 */
353void get()
354{
355 using std::cout;
356 using std::endl;
357 using std::setw;
358
359 auto busData = loadDBusData();
360
Mike Capps16aab352021-06-30 10:17:21 -0400361 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);
Mike Capps530c6552021-06-29 09:50:38 -0400365
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/**
Mike Capps16aab352021-06-30 10:17:21 -0400410 * @function set fan[s] to a target RPM
411 */
412void 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 */
486void 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/**
Mike Capps385b1982021-06-28 10:40:42 -0400502 * @function main entry point for the application
503 */
504int main(int argc, char* argv[])
505{
506 auto rc = 0;
Mike Capps16aab352021-06-30 10:17:21 -0400507 uint64_t target{0U};
508 std::vector<std::string> fanList;
Mike Capps385b1982021-06-28 10:40:42 -0400509
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
Mike Capps16aab352021-06-30 10:17:21 -0400523 // status method
Mike Capps385b1982021-06-28 10:40:42 -0400524 auto cmdStatus = commands->add_subcommand(
525 "status",
526 "Get the fan tach targets/values and fan-control service status");
Mike Capps385b1982021-06-28 10:40:42 -0400527 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
Mike Capps16aab352021-06-30 10:17:21 -0400532 // get method
Mike Capps530c6552021-06-29 09:50:38 -0400533 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
Mike Capps16aab352021-06-30 10:17:21 -0400541 // 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
Mike Capps385b1982021-06-28 10:40:42 -0400561 CLI11_PARSE(app, argc, argv);
562
563 if (app.got_subcommand("status"))
564 {
565 status();
566 }
Mike Capps16aab352021-06-30 10:17:21 -0400567 else if (app.got_subcommand("set"))
568 {
569 set(target, fanList);
570 }
Mike Capps530c6552021-06-29 09:50:38 -0400571 else if (app.got_subcommand("get"))
572 {
573 get();
574 }
Mike Capps16aab352021-06-30 10:17:21 -0400575 else if (app.got_subcommand("resume"))
576 {
577 resume();
578 }
Mike Capps385b1982021-06-28 10:40:42 -0400579 }
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}