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