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