1 /** 2 * Copyright © 2022 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 <nlohmann/json.hpp> 23 #include <sdbusplus/bus.hpp> 24 25 #include <chrono> 26 #include <filesystem> 27 #include <iomanip> 28 #include <iostream> 29 30 using SDBusPlus = phosphor::fan::util::SDBusPlus; 31 32 constexpr auto systemdMgrIface = "org.freedesktop.systemd1.Manager"; 33 constexpr auto systemdPath = "/org/freedesktop/systemd1"; 34 constexpr auto systemdService = "org.freedesktop.systemd1"; 35 constexpr auto phosphorServiceName = "phosphor-fan-control@0.service"; 36 constexpr auto dumpFile = "/tmp/fan_control_dump.json"; 37 38 enum 39 { 40 FAN_NAMES = 0, 41 PATH_MAP = 1, 42 IFACES = 2, 43 METHOD = 3 44 }; 45 46 struct DumpQuery 47 { 48 std::string section; 49 std::string name; 50 std::vector<std::string> properties; 51 bool dump{false}; 52 }; 53 54 /** 55 * @function extracts fan name from dbus path string (last token where 56 * delimiter is the / character), with proper bounds checking. 57 * @param[in] path - D-Bus path 58 * @return just the fan name. 59 */ 60 61 std::string justFanName(const std::string& path) 62 { 63 std::string fanName; 64 65 auto itr = path.rfind("/"); 66 if (itr != std::string::npos && itr < path.size()) 67 { 68 fanName = path.substr(1 + itr); 69 } 70 71 return fanName; 72 } 73 74 /** 75 * @function produces subtree paths whose names match fan token names. 76 * @param[in] path - D-Bus path to obtain subtree from 77 * @param[in] iface - interface to obtain subTreePaths from 78 * @param[in] fans - label matching tokens to filter by 79 * @param[in] shortPath - flag to shorten fan token 80 * @return map of paths by fan name 81 */ 82 83 std::map<std::string, std::vector<std::string>> 84 getPathsFromIface(const std::string& path, const std::string& iface, 85 const std::vector<std::string>& fans, 86 bool shortPath = false) 87 { 88 std::map<std::string, std::vector<std::string>> dest; 89 90 for (auto& path : 91 SDBusPlus::getSubTreePathsRaw(SDBusPlus::getBus(), path, iface, 0)) 92 { 93 for (auto& fan : fans) 94 { 95 if (shortPath) 96 { 97 if (fan == justFanName(path)) 98 { 99 dest[fan].push_back(path); 100 } 101 } 102 else if (std::string::npos != path.find(fan + "_")) 103 { 104 dest[fan].push_back(path); 105 } 106 } 107 } 108 109 return dest; 110 } 111 112 /** 113 * @function consolidated function to load dbus paths and fan names 114 */ 115 auto loadDBusData() 116 { 117 auto& bus{SDBusPlus::getBus()}; 118 119 std::vector<std::string> fanNames; 120 121 // paths by D-bus interface,fan name 122 std::map<std::string, std::map<std::string, std::vector<std::string>>> 123 pathMap; 124 125 std::string method("RPM"); 126 127 std::map<const std::string, const std::string> interfaces{ 128 {"FanSpeed", "xyz.openbmc_project.Control.FanSpeed"}, 129 {"FanPwm", "xyz.openbmc_project.Control.FanPwm"}, 130 {"SensorValue", "xyz.openbmc_project.Sensor.Value"}, 131 {"Item", "xyz.openbmc_project.Inventory.Item"}, 132 {"OpStatus", "xyz.openbmc_project.State.Decorator.OperationalStatus"}}; 133 134 std::map<const std::string, const std::string> paths{ 135 {"motherboard", 136 "/xyz/openbmc_project/inventory/system/chassis/motherboard"}, 137 {"tach", "/xyz/openbmc_project/sensors/fan_tach"}}; 138 139 // build a list of all fans 140 for (auto& path : SDBusPlus::getSubTreePathsRaw(bus, paths["tach"], 141 interfaces["FanSpeed"], 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 // retry with PWM mode if none found 150 if (0 == fanNames.size()) 151 { 152 method = "PWM"; 153 154 for (auto& path : SDBusPlus::getSubTreePathsRaw( 155 bus, paths["tach"], interfaces["FanPwm"], 0)) 156 { 157 // special case where we build the list of fans 158 auto fan = justFanName(path); 159 fan = fan.substr(0, fan.rfind("_")); 160 fanNames.push_back(fan); 161 } 162 } 163 164 // load tach sensor paths for each fan 165 pathMap["tach"] = getPathsFromIface(paths["tach"], 166 interfaces["SensorValue"], fanNames); 167 168 // load inventory Item data for each fan 169 pathMap["inventory"] = getPathsFromIface( 170 paths["motherboard"], interfaces["Item"], fanNames, true); 171 172 // load operational status data for each fan 173 pathMap["opstatus"] = getPathsFromIface( 174 paths["motherboard"], interfaces["OpStatus"], fanNames, true); 175 176 return std::make_tuple(fanNames, pathMap, interfaces, method); 177 } 178 179 /** 180 * @function gets the states of phosphor-fanctl. equivalent to 181 * "systemctl status phosphor-fan-control@0" 182 * @return a list of several (sub)states of fanctl (loaded, 183 * active, running) as well as D-Bus properties representing 184 * BMC states (bmc state,chassis power state, host state) 185 */ 186 187 std::array<std::string, 6> getStates() 188 { 189 using DBusTuple = 190 std::tuple<std::string, std::string, std::string, std::string, 191 std::string, std::string, sdbusplus::message::object_path, 192 uint32_t, std::string, sdbusplus::message::object_path>; 193 194 std::array<std::string, 6> ret; 195 196 std::vector<std::string> services{phosphorServiceName}; 197 198 try 199 { 200 auto fields{SDBusPlus::callMethodAndRead<std::vector<DBusTuple>>( 201 systemdService, systemdPath, systemdMgrIface, "ListUnitsByNames", 202 services)}; 203 204 if (fields.size() > 0) 205 { 206 ret[0] = std::get<2>(fields[0]); 207 ret[1] = std::get<3>(fields[0]); 208 ret[2] = std::get<4>(fields[0]); 209 } 210 else 211 { 212 std::cout << "No units found for systemd service: " << services[0] 213 << std::endl; 214 } 215 } 216 catch (const std::exception& e) 217 { 218 std::cerr << "Failure retrieving phosphor-fan-control states: " 219 << e.what() << std::endl; 220 } 221 222 std::string path("/xyz/openbmc_project/state/bmc0"); 223 std::string iface("xyz.openbmc_project.State.BMC"); 224 ret[3] = SDBusPlus::getProperty<std::string>(path, iface, 225 "CurrentBMCState"); 226 227 path = "/xyz/openbmc_project/state/chassis0"; 228 iface = "xyz.openbmc_project.State.Chassis"; 229 ret[4] = SDBusPlus::getProperty<std::string>(path, iface, 230 "CurrentPowerState"); 231 232 path = "/xyz/openbmc_project/state/host0"; 233 iface = "xyz.openbmc_project.State.Host"; 234 ret[5] = SDBusPlus::getProperty<std::string>(path, iface, 235 "CurrentHostState"); 236 237 return ret; 238 } 239 240 /** 241 * @function helper to determine interface type from a given control method 242 */ 243 std::string ifaceTypeFromMethod(const std::string& method) 244 { 245 return (method == "RPM" ? "FanSpeed" : "FanPwm"); 246 } 247 248 /** 249 * @function performs the "status" command from the cmdline. 250 * get states and sensor data and output to the console 251 */ 252 void status() 253 { 254 using std::cout; 255 using std::endl; 256 using std::setw; 257 258 auto busData = loadDBusData(); 259 auto& method = std::get<METHOD>(busData); 260 261 std::string property; 262 263 // get the state,substate of fan-control and obmc 264 auto states = getStates(); 265 266 // print the header 267 cout << "Fan Control Service State : " << states[0] << ", " << states[1] 268 << "(" << states[2] << ")" << endl; 269 cout << endl; 270 cout << "CurrentBMCState : " << states[3] << endl; 271 cout << "CurrentPowerState : " << states[4] << endl; 272 cout << "CurrentHostState : " << states[5] << endl; 273 cout << endl; 274 cout << "FAN " 275 << "TARGET(" << method << ") FEEDBACKS(RPM) PRESENT" 276 << " FUNCTIONAL" << endl; 277 cout << "===============================================================" 278 << endl; 279 280 auto& fanNames{std::get<FAN_NAMES>(busData)}; 281 auto& pathMap{std::get<PATH_MAP>(busData)}; 282 auto& interfaces{std::get<IFACES>(busData)}; 283 284 for (auto& fan : fanNames) 285 { 286 cout << setw(8) << std::left << fan << std::right << setw(13); 287 288 // get the target RPM 289 property = "Target"; 290 cout << SDBusPlus::getProperty<uint64_t>( 291 pathMap["tach"][fan][0], 292 interfaces[ifaceTypeFromMethod(method)], property) 293 << setw(19); 294 295 // get the sensor RPM 296 property = "Value"; 297 298 std::ostringstream output; 299 int numRotors = pathMap["tach"][fan].size(); 300 // print tach readings for each rotor 301 for (auto& path : pathMap["tach"][fan]) 302 { 303 output << SDBusPlus::getProperty<double>( 304 path, interfaces["SensorValue"], property); 305 306 // dont print slash on last rotor 307 if (--numRotors) 308 output << "/"; 309 } 310 cout << output.str() << setw(10); 311 312 // print the Present property 313 property = "Present"; 314 auto itFan = pathMap["inventory"].find(fan); 315 if (itFan != pathMap["inventory"].end()) 316 { 317 for (auto& path : itFan->second) 318 { 319 try 320 { 321 cout << std::boolalpha 322 << SDBusPlus::getProperty<bool>( 323 path, interfaces["Item"], property); 324 } 325 catch (const phosphor::fan::util::DBusError&) 326 { 327 cout << "Unknown"; 328 } 329 } 330 } 331 else 332 { 333 cout << "Unknown"; 334 } 335 336 cout << setw(13); 337 338 // and the functional property 339 property = "Functional"; 340 itFan = pathMap["opstatus"].find(fan); 341 if (itFan != pathMap["opstatus"].end()) 342 { 343 for (auto& path : itFan->second) 344 { 345 try 346 { 347 cout << std::boolalpha 348 << SDBusPlus::getProperty<bool>( 349 path, interfaces["OpStatus"], property); 350 } 351 catch (const phosphor::fan::util::DBusError&) 352 { 353 cout << "Unknown"; 354 } 355 } 356 } 357 else 358 { 359 cout << "Unknown"; 360 } 361 362 cout << endl; 363 } 364 } 365 366 /** 367 * @function print target RPM/PWM and tach readings from each fan 368 */ 369 void get() 370 { 371 using std::cout; 372 using std::endl; 373 using std::setw; 374 375 auto busData = loadDBusData(); 376 377 auto& fanNames{std::get<FAN_NAMES>(busData)}; 378 auto& pathMap{std::get<PATH_MAP>(busData)}; 379 auto& interfaces{std::get<IFACES>(busData)}; 380 auto& method = std::get<METHOD>(busData); 381 382 std::string property; 383 384 // print the header 385 cout << "TARGET SENSOR" << setw(11) << "TARGET(" << method 386 << ") FEEDBACK SENSOR FEEDBACK(RPM)" << endl; 387 cout << "===============================================================" 388 << endl; 389 390 for (auto& fan : fanNames) 391 { 392 if (pathMap["tach"][fan].size() == 0) 393 continue; 394 // print just the sensor name 395 auto shortPath = pathMap["tach"][fan][0]; 396 shortPath = justFanName(shortPath); 397 cout << setw(13) << std::left << shortPath << std::right << setw(15); 398 399 // print its target RPM/PWM 400 property = "Target"; 401 cout << SDBusPlus::getProperty<uint64_t>( 402 pathMap["tach"][fan][0], interfaces[ifaceTypeFromMethod(method)], 403 property); 404 405 // print readings for each rotor 406 property = "Value"; 407 408 auto indent = 0; 409 for (auto& path : pathMap["tach"][fan]) 410 { 411 cout << setw(18 + indent) << justFanName(path) << setw(17) 412 << SDBusPlus::getProperty<double>( 413 path, interfaces["SensorValue"], property) 414 << endl; 415 416 if (0 == indent) 417 indent = 28; 418 } 419 } 420 } 421 422 /** 423 * @function set fan[s] to a target RPM 424 */ 425 void set(uint64_t target, std::vector<std::string>& fanList) 426 { 427 auto busData = loadDBusData(); 428 auto& bus{SDBusPlus::getBus()}; 429 auto& pathMap{std::get<PATH_MAP>(busData)}; 430 auto& interfaces{std::get<IFACES>(busData)}; 431 auto& method = std::get<METHOD>(busData); 432 433 std::string ifaceType(method == "RPM" ? "FanSpeed" : "FanPwm"); 434 435 // stop the fan-control service 436 SDBusPlus::callMethodAndRead<sdbusplus::message::object_path>( 437 systemdService, systemdPath, systemdMgrIface, "StopUnit", 438 phosphorServiceName, "replace"); 439 440 if (fanList.size() == 0) 441 { 442 fanList = std::get<FAN_NAMES>(busData); 443 } 444 445 for (auto& fan : fanList) 446 { 447 try 448 { 449 auto paths(pathMap["tach"].find(fan)); 450 451 if (pathMap["tach"].end() == paths) 452 { 453 // try again, maybe it was a sensor name instead of a fan name 454 for (const auto& [fanName, sensors] : pathMap["tach"]) 455 { 456 for (const auto& path : sensors) 457 { 458 std::string sensor(path.substr(path.rfind("/"))); 459 460 if (sensor.size() > 0) 461 { 462 sensor = sensor.substr(1); 463 464 if (sensor == fan) 465 { 466 paths = pathMap["tach"].find(fanName); 467 468 break; 469 } 470 } 471 } 472 } 473 } 474 475 if (pathMap["tach"].end() == paths) 476 { 477 std::cout << "Could not find tach path for fan: " << fan 478 << std::endl; 479 continue; 480 } 481 482 // set the target RPM 483 SDBusPlus::setProperty<uint64_t>(bus, paths->second[0], 484 interfaces[ifaceType], "Target", 485 std::move(target)); 486 } 487 catch (const phosphor::fan::util::DBusPropertyError& e) 488 { 489 std::cerr << "Cannot set target rpm for " << fan 490 << " caught D-Bus exception: " << e.what() << std::endl; 491 } 492 } 493 } 494 495 /** 496 * @function restart fan-control to allow it to manage fan speeds 497 */ 498 void resume() 499 { 500 try 501 { 502 auto retval = 503 SDBusPlus::callMethodAndRead<sdbusplus::message::object_path>( 504 systemdService, systemdPath, systemdMgrIface, "StartUnit", 505 phosphorServiceName, "replace"); 506 } 507 catch (const phosphor::fan::util::DBusMethodError& e) 508 { 509 std::cerr << "Unable to start fan control: " << e.what() << std::endl; 510 } 511 } 512 513 /** 514 * @function force reload of control files by sending HUP signal 515 */ 516 void reload() 517 { 518 try 519 { 520 SDBusPlus::callMethod(systemdService, systemdPath, systemdMgrIface, 521 "KillUnit", phosphorServiceName, "main", SIGHUP); 522 } 523 catch (const phosphor::fan::util::DBusPropertyError& e) 524 { 525 std::cerr << "Unable to reload configuration files: " << e.what() 526 << std::endl; 527 } 528 } 529 530 /** 531 * @function dump debug data 532 */ 533 void dumpFanControl() 534 { 535 namespace fs = std::filesystem; 536 537 try 538 { 539 // delete existing file 540 if (fs::exists(dumpFile)) 541 { 542 std::filesystem::remove(dumpFile); 543 } 544 545 SDBusPlus::callMethod(systemdService, systemdPath, systemdMgrIface, 546 "KillUnit", phosphorServiceName, "main", SIGUSR1); 547 548 bool done = false; 549 size_t tries = 0; 550 const size_t maxTries = 30; 551 552 do 553 { 554 // wait for file to be detected 555 sleep(1); 556 557 if (fs::exists(dumpFile)) 558 { 559 try 560 { 561 auto unused{nlohmann::json::parse(std::ifstream{dumpFile})}; 562 done = true; 563 } 564 catch (...) 565 {} 566 } 567 568 if (++tries > maxTries) 569 { 570 std::cerr << "Timed out waiting for fan control dump.\n"; 571 return; 572 } 573 } while (!done); 574 575 std::cout << "Fan control dump written to: " << dumpFile << std::endl; 576 } 577 catch (const phosphor::fan::util::DBusPropertyError& e) 578 { 579 std::cerr << "Unable to dump fan control: " << e.what() << std::endl; 580 } 581 } 582 583 /** 584 * @function Query items in the dump file 585 */ 586 void queryDumpFile(const DumpQuery& dq) 587 { 588 nlohmann::json output; 589 std::ifstream file{dumpFile}; 590 591 if (!file.good()) 592 { 593 std::cerr << "Unable to open dump file, please run 'fanctl dump'.\n"; 594 return; 595 } 596 597 auto dumpData = nlohmann::json::parse(file); 598 599 if (!dumpData.contains(dq.section)) 600 { 601 std::cerr << "Error: Dump file does not contain " << dq.section 602 << " section" 603 << "\n"; 604 return; 605 } 606 607 const auto& section = dumpData.at(dq.section); 608 609 if (section.is_array()) 610 { 611 for (const auto& entry : section) 612 { 613 if (!entry.is_string() || dq.name.empty() || 614 (entry.get<std::string>().find(dq.name) != std::string::npos)) 615 { 616 output[dq.section].push_back(entry); 617 } 618 } 619 std::cout << std::setw(4) << output << "\n"; 620 return; 621 } 622 623 for (const auto& [key1, values1] : section.items()) 624 { 625 if (dq.name.empty() || (key1.find(dq.name) != std::string::npos)) 626 { 627 // If no properties specified, print the whole JSON value 628 if (dq.properties.empty()) 629 { 630 output[key1] = values1; 631 continue; 632 } 633 634 // Look for properties both one and two levels down. 635 // Future improvement: Use recursion. 636 for (const auto& [key2, values2] : values1.items()) 637 { 638 for (const auto& prop : dq.properties) 639 { 640 if (prop == key2) 641 { 642 output[key1][prop] = values2; 643 } 644 } 645 646 for (const auto& [key3, values3] : values2.items()) 647 { 648 for (const auto& prop : dq.properties) 649 { 650 if (prop == key3) 651 { 652 output[key1][prop] = values3; 653 } 654 } 655 } 656 } 657 } 658 } 659 660 if (!output.empty()) 661 { 662 std::cout << std::setw(4) << output << "\n"; 663 } 664 } 665 666 /** 667 * @function setup the CLI object to accept all options 668 */ 669 void initCLI(CLI::App& app, uint64_t& target, std::vector<std::string>& fanList, 670 [[maybe_unused]] DumpQuery& dq) 671 { 672 app.set_help_flag("-h,--help", "Print this help page and exit."); 673 674 // App requires only 1 subcommand to be given 675 app.require_subcommand(1); 676 677 // This represents the command given 678 auto commands = app.add_option_group("Commands"); 679 680 // status method 681 std::string strHelp("Prints fan target/tach readings, present/functional " 682 "states, and fan-monitor/BMC/Power service status"); 683 684 auto cmdStatus = commands->add_subcommand("status", strHelp); 685 cmdStatus->set_help_flag("-h, --help", strHelp); 686 cmdStatus->require_option(0); 687 688 // get method 689 strHelp = "Get the current fan target and feedback speeds for all rotors"; 690 auto cmdGet = commands->add_subcommand("get", strHelp); 691 cmdGet->set_help_flag("-h, --help", strHelp); 692 cmdGet->require_option(0); 693 694 // set method 695 strHelp = "Set target (all rotors) for one-or-more fans"; 696 auto cmdSet = commands->add_subcommand("set", strHelp); 697 strHelp = R"(set <TARGET> [TARGET SENSOR(S)] 698 <TARGET> 699 - RPM/PWM target to set the fans 700 [TARGET SENSOR LIST] 701 - list of target sensors to set)"; 702 cmdSet->set_help_flag("-h, --help", strHelp); 703 cmdSet->add_option("target", target, "RPM/PWM target to set the fans"); 704 cmdSet->add_option( 705 "fan list", fanList, 706 "[optional] list of 1+ fans to set target RPM/PWM (default: all)"); 707 cmdSet->require_option(); 708 709 #ifdef CONTROL_USE_JSON 710 strHelp = "Reload phosphor-fan configuration files"; 711 auto cmdReload = commands->add_subcommand("reload", strHelp); 712 cmdReload->set_help_flag("-h, --help", strHelp); 713 cmdReload->require_option(0); 714 #endif 715 716 strHelp = "Resume running phosphor-fan-control"; 717 auto cmdResume = commands->add_subcommand("resume", strHelp); 718 cmdResume->set_help_flag("-h, --help", strHelp); 719 cmdResume->require_option(0); 720 721 // Dump method 722 auto cmdDump = commands->add_subcommand("dump", "Dump debug data"); 723 cmdDump->set_help_flag("-h, --help", "Dump debug data"); 724 cmdDump->require_option(0); 725 726 #ifdef CONTROL_USE_JSON 727 // Query dump 728 auto cmdDumpQuery = commands->add_subcommand("query_dump", 729 "Query the dump file"); 730 731 cmdDumpQuery->set_help_flag("-h, --help", "Query the dump file"); 732 cmdDumpQuery 733 ->add_option("-s, --section", dq.section, "Dump file section name") 734 ->required(); 735 cmdDumpQuery->add_option("-n, --name", dq.name, 736 "Optional dump file entry name (or substring)"); 737 cmdDumpQuery->add_option("-p, --properties", dq.properties, 738 "Optional list of dump file property names"); 739 cmdDumpQuery->add_flag("-d, --dump", dq.dump, 740 "Force a dump before the query"); 741 #endif 742 } 743 744 /** 745 * @function main entry point for the application 746 */ 747 int main(int argc, char* argv[]) 748 { 749 auto rc = 0; 750 uint64_t target{0U}; 751 std::vector<std::string> fanList; 752 DumpQuery dq; 753 754 try 755 { 756 CLI::App app{"Manually control, get fan tachs, view status, and resume " 757 "automatic control of all fans within a chassis. Full " 758 "documentation can be found at the readme:\n" 759 "https://github.com/openbmc/phosphor-fan-presence/tree/" 760 "master/docs/control/fanctl"}; 761 762 initCLI(app, target, fanList, dq); 763 764 CLI11_PARSE(app, argc, argv); 765 766 if (app.got_subcommand("get")) 767 { 768 get(); 769 } 770 else if (app.got_subcommand("set")) 771 { 772 set(target, fanList); 773 } 774 #ifdef CONTROL_USE_JSON 775 else if (app.got_subcommand("reload")) 776 { 777 reload(); 778 } 779 #endif 780 else if (app.got_subcommand("resume")) 781 { 782 resume(); 783 } 784 else if (app.got_subcommand("status")) 785 { 786 status(); 787 } 788 else if (app.got_subcommand("dump")) 789 { 790 #ifdef CONTROL_USE_JSON 791 dumpFanControl(); 792 #else 793 std::ofstream(dumpFile) 794 << "{\n\"msg\": \"Unable to create dump on " 795 "non-JSON config based system\"\n}"; 796 #endif 797 } 798 #ifdef CONTROL_USE_JSON 799 else if (app.got_subcommand("query_dump")) 800 { 801 if (dq.dump) 802 { 803 dumpFanControl(); 804 } 805 queryDumpFile(dq); 806 } 807 #endif 808 } 809 catch (const std::exception& e) 810 { 811 rc = -1; 812 std::cerr << argv[0] << " failed: " << e.what() << std::endl; 813 } 814 815 return rc; 816 } 817