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 enum 33 { 34 FAN_NAMES = 0, 35 PATH_MAP = 1, 36 IFACES = 2, 37 METHOD = 3 38 }; 39 40 /** 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 47 std::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 69 std::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 /** 99 * @function consolidated function to load dbus paths and fan names 100 */ 101 auto 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 /** 166 * @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 173 std::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 182 std::vector<std::string> services{phosphorServiceName}; 183 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 } 202 catch (const std::exception& e) 203 { 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 /** 227 * @function helper to determine interface type from a given control method 228 */ 229 std::string ifaceTypeFromMethod(const std::string& method) 230 { 231 return (method == "RPM" ? "FanSpeed" : "FanPwm"); 232 } 233 234 /** 235 * @function performs the "status" command from the cmdline. 236 * get states and sensor data and output to the console 237 */ 238 void status() 239 { 240 using std::cout; 241 using std::endl; 242 using std::setw; 243 244 auto busData = loadDBusData(); 245 auto& method = std::get<METHOD>(busData); 246 247 std::string property; 248 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 " 261 << "TARGET(" << method << ") FEEDBACKS(RPMS) PRESENT" 262 << " FUNCTIONAL" << endl; 263 cout << "===============================================================" 264 << endl; 265 266 auto& fanNames{std::get<FAN_NAMES>(busData)}; 267 auto& pathMap{std::get<PATH_MAP>(busData)}; 268 auto& interfaces{std::get<IFACES>(busData)}; 269 270 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>( 277 pathMap["tach"][fan][0], 278 interfaces[ifaceTypeFromMethod(method)], property) 279 << setw(11); 280 281 // get the sensor RPM 282 property = "Value"; 283 284 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 297 // print the Present property 298 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 } 314 catch (const phosphor::fan::util::DBusPropertyError&) 315 { 316 val = "Unknown"; 317 } 318 cout << val; 319 } 320 321 cout << setw(13); 322 323 // and the functional property 324 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 } 339 catch (const phosphor::fan::util::DBusPropertyError&) 340 { 341 val = "Unknown"; 342 } 343 cout << val; 344 } 345 346 cout << endl; 347 } 348 } 349 350 /** 351 * @function print target RPM/PWM and tach readings from each fan 352 */ 353 void get() 354 { 355 using std::cout; 356 using std::endl; 357 using std::setw; 358 359 auto busData = loadDBusData(); 360 361 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); 365 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 /** 410 * @function set fan[s] to a target RPM 411 */ 412 void 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 */ 486 void 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 /** 502 * @function main entry point for the application 503 */ 504 int main(int argc, char* argv[]) 505 { 506 auto rc = 0; 507 uint64_t target{0U}; 508 std::vector<std::string> fanList; 509 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 523 // status method 524 auto cmdStatus = commands->add_subcommand( 525 "status", 526 "Get the fan tach targets/values and fan-control service status"); 527 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 532 // get method 533 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 541 // 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 561 CLI11_PARSE(app, argc, argv); 562 563 if (app.got_subcommand("status")) 564 { 565 status(); 566 } 567 else if (app.got_subcommand("set")) 568 { 569 set(target, fanList); 570 } 571 else if (app.got_subcommand("get")) 572 { 573 get(); 574 } 575 else if (app.got_subcommand("resume")) 576 { 577 resume(); 578 } 579 } 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 } 588