/** * Copyright © 2022 IBM Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "config.h" #include "sdbusplus.hpp" #include #include #include #include #include #include #include using SDBusPlus = phosphor::fan::util::SDBusPlus; constexpr auto systemdMgrIface = "org.freedesktop.systemd1.Manager"; constexpr auto systemdPath = "/org/freedesktop/systemd1"; constexpr auto systemdService = "org.freedesktop.systemd1"; constexpr auto phosphorServiceName = "phosphor-fan-control@0.service"; constexpr auto dumpFile = "/tmp/fan_control_dump.json"; enum { FAN_NAMES = 0, PATH_MAP = 1, IFACES = 2, METHOD = 3 }; struct DumpQuery { std::string section; std::string name; std::vector properties; bool dump{false}; }; /** * @function extracts fan name from dbus path string (last token where * delimiter is the / character), with proper bounds checking. * @param[in] path - D-Bus path * @return just the fan name. */ std::string justFanName(const std::string& path) { std::string fanName; auto itr = path.rfind("/"); if (itr != std::string::npos && itr < path.size()) { fanName = path.substr(1 + itr); } return fanName; } /** * @function produces subtree paths whose names match fan token names. * @param[in] path - D-Bus path to obtain subtree from * @param[in] iface - interface to obtain subTreePaths from * @param[in] fans - label matching tokens to filter by * @param[in] shortPath - flag to shorten fan token * @return map of paths by fan name */ std::map> getPathsFromIface(const std::string& path, const std::string& iface, const std::vector& fans, bool shortPath = false) { std::map> dest; for (auto& path : SDBusPlus::getSubTreePathsRaw(SDBusPlus::getBus(), path, iface, 0)) { for (auto& fan : fans) { if (shortPath) { if (fan == justFanName(path)) { dest[fan].push_back(path); } } else if (std::string::npos != path.find(fan + "_")) { dest[fan].push_back(path); } } } return dest; } /** * @function consolidated function to load dbus paths and fan names */ auto loadDBusData() { auto& bus{SDBusPlus::getBus()}; std::vector fanNames; // paths by D-bus interface,fan name std::map>> pathMap; std::string method("RPM"); std::map interfaces{ {"FanSpeed", "xyz.openbmc_project.Control.FanSpeed"}, {"FanPwm", "xyz.openbmc_project.Control.FanPwm"}, {"SensorValue", "xyz.openbmc_project.Sensor.Value"}, {"Item", "xyz.openbmc_project.Inventory.Item"}, {"OpStatus", "xyz.openbmc_project.State.Decorator.OperationalStatus"}}; std::map paths{ {"motherboard", "/xyz/openbmc_project/inventory/system/chassis/motherboard"}, {"tach", "/xyz/openbmc_project/sensors/fan_tach"}}; // build a list of all fans for (auto& path : SDBusPlus::getSubTreePathsRaw(bus, paths["tach"], interfaces["FanSpeed"], 0)) { // special case where we build the list of fans auto fan = justFanName(path); fan = fan.substr(0, fan.rfind("_")); fanNames.push_back(fan); } // retry with PWM mode if none found if (0 == fanNames.size()) { method = "PWM"; for (auto& path : SDBusPlus::getSubTreePathsRaw( bus, paths["tach"], interfaces["FanPwm"], 0)) { // special case where we build the list of fans auto fan = justFanName(path); fan = fan.substr(0, fan.rfind("_")); fanNames.push_back(fan); } } // load tach sensor paths for each fan pathMap["tach"] = getPathsFromIface(paths["tach"], interfaces["SensorValue"], fanNames); // load inventory Item data for each fan pathMap["inventory"] = getPathsFromIface( paths["motherboard"], interfaces["Item"], fanNames, true); // load operational status data for each fan pathMap["opstatus"] = getPathsFromIface( paths["motherboard"], interfaces["OpStatus"], fanNames, true); return std::make_tuple(fanNames, pathMap, interfaces, method); } /** * @function gets the states of phosphor-fanctl. equivalent to * "systemctl status phosphor-fan-control@0" * @return a list of several (sub)states of fanctl (loaded, * active, running) as well as D-Bus properties representing * BMC states (bmc state,chassis power state, host state) */ std::array getStates() { using DBusTuple = std::tuple; std::array ret; std::vector services{phosphorServiceName}; try { auto fields{SDBusPlus::callMethodAndRead>( systemdService, systemdPath, systemdMgrIface, "ListUnitsByNames", services)}; if (fields.size() > 0) { ret[0] = std::get<2>(fields[0]); ret[1] = std::get<3>(fields[0]); ret[2] = std::get<4>(fields[0]); } else { std::cout << "No units found for systemd service: " << services[0] << std::endl; } } catch (const std::exception& e) { std::cerr << "Failure retrieving phosphor-fan-control states: " << e.what() << std::endl; } std::string path("/xyz/openbmc_project/state/bmc0"); std::string iface("xyz.openbmc_project.State.BMC"); ret[3] = SDBusPlus::getProperty(path, iface, "CurrentBMCState"); path = "/xyz/openbmc_project/state/chassis0"; iface = "xyz.openbmc_project.State.Chassis"; ret[4] = SDBusPlus::getProperty(path, iface, "CurrentPowerState"); path = "/xyz/openbmc_project/state/host0"; iface = "xyz.openbmc_project.State.Host"; ret[5] = SDBusPlus::getProperty(path, iface, "CurrentHostState"); return ret; } /** * @function helper to determine interface type from a given control method */ std::string ifaceTypeFromMethod(const std::string& method) { return (method == "RPM" ? "FanSpeed" : "FanPwm"); } /** * @function performs the "status" command from the cmdline. * get states and sensor data and output to the console */ void status() { using std::cout; using std::endl; using std::setw; auto busData = loadDBusData(); auto& method = std::get(busData); std::string property; // get the state,substate of fan-control and obmc auto states = getStates(); // print the header cout << "Fan Control Service State : " << states[0] << ", " << states[1] << "(" << states[2] << ")" << endl; cout << endl; cout << "CurrentBMCState : " << states[3] << endl; cout << "CurrentPowerState : " << states[4] << endl; cout << "CurrentHostState : " << states[5] << endl; cout << endl; cout << "FAN " << "TARGET(" << method << ") FEEDBACKS(RPM) PRESENT" << " FUNCTIONAL" << endl; cout << "===============================================================" << endl; auto& fanNames{std::get(busData)}; auto& pathMap{std::get(busData)}; auto& interfaces{std::get(busData)}; for (auto& fan : fanNames) { cout << setw(8) << std::left << fan << std::right << setw(13); // get the target RPM property = "Target"; cout << SDBusPlus::getProperty( pathMap["tach"][fan][0], interfaces[ifaceTypeFromMethod(method)], property) << setw(19); // get the sensor RPM property = "Value"; std::ostringstream output; int numRotors = pathMap["tach"][fan].size(); // print tach readings for each rotor for (auto& path : pathMap["tach"][fan]) { output << SDBusPlus::getProperty( path, interfaces["SensorValue"], property); // dont print slash on last rotor if (--numRotors) output << "/"; } cout << output.str() << setw(10); // print the Present property property = "Present"; auto itFan = pathMap["inventory"].find(fan); if (itFan != pathMap["inventory"].end()) { for (auto& path : itFan->second) { try { cout << std::boolalpha << SDBusPlus::getProperty( path, interfaces["Item"], property); } catch (const phosphor::fan::util::DBusError&) { cout << "Unknown"; } } } else { cout << "Unknown"; } cout << setw(13); // and the functional property property = "Functional"; itFan = pathMap["opstatus"].find(fan); if (itFan != pathMap["opstatus"].end()) { for (auto& path : itFan->second) { try { cout << std::boolalpha << SDBusPlus::getProperty( path, interfaces["OpStatus"], property); } catch (const phosphor::fan::util::DBusError&) { cout << "Unknown"; } } } else { cout << "Unknown"; } cout << endl; } } /** * @function print target RPM/PWM and tach readings from each fan */ void get() { using std::cout; using std::endl; using std::setw; auto busData = loadDBusData(); auto& fanNames{std::get(busData)}; auto& pathMap{std::get(busData)}; auto& interfaces{std::get(busData)}; auto& method = std::get(busData); std::string property; // print the header cout << "TARGET SENSOR" << setw(11) << "TARGET(" << method << ") FEEDBACK SENSOR FEEDBACK(RPM)" << endl; cout << "===============================================================" << endl; for (auto& fan : fanNames) { if (pathMap["tach"][fan].size() == 0) continue; // print just the sensor name auto shortPath = pathMap["tach"][fan][0]; shortPath = justFanName(shortPath); cout << setw(13) << std::left << shortPath << std::right << setw(15); // print its target RPM/PWM property = "Target"; cout << SDBusPlus::getProperty( pathMap["tach"][fan][0], interfaces[ifaceTypeFromMethod(method)], property); // print readings for each rotor property = "Value"; auto indent = 0; for (auto& path : pathMap["tach"][fan]) { cout << setw(18 + indent) << justFanName(path) << setw(17) << SDBusPlus::getProperty( path, interfaces["SensorValue"], property) << endl; if (0 == indent) indent = 28; } } } /** * @function set fan[s] to a target RPM */ void set(uint64_t target, std::vector& fanList) { auto busData = loadDBusData(); auto& bus{SDBusPlus::getBus()}; auto& pathMap{std::get(busData)}; auto& interfaces{std::get(busData)}; auto& method = std::get(busData); std::string ifaceType(method == "RPM" ? "FanSpeed" : "FanPwm"); // stop the fan-control service SDBusPlus::callMethodAndRead( systemdService, systemdPath, systemdMgrIface, "StopUnit", phosphorServiceName, "replace"); if (fanList.size() == 0) { fanList = std::get(busData); } for (auto& fan : fanList) { try { auto paths(pathMap["tach"].find(fan)); if (pathMap["tach"].end() == paths) { // try again, maybe it was a sensor name instead of a fan name for (const auto& [fanName, sensors] : pathMap["tach"]) { for (const auto& path : sensors) { std::string sensor(path.substr(path.rfind("/"))); if (sensor.size() > 0) { sensor = sensor.substr(1); if (sensor == fan) { paths = pathMap["tach"].find(fanName); break; } } } } } if (pathMap["tach"].end() == paths) { std::cout << "Could not find tach path for fan: " << fan << std::endl; continue; } // set the target RPM SDBusPlus::setProperty(bus, paths->second[0], interfaces[ifaceType], "Target", std::move(target)); } catch (const phosphor::fan::util::DBusPropertyError& e) { std::cerr << "Cannot set target rpm for " << fan << " caught D-Bus exception: " << e.what() << std::endl; } } } /** * @function restart fan-control to allow it to manage fan speeds */ void resume() { try { auto retval = SDBusPlus::callMethodAndRead( systemdService, systemdPath, systemdMgrIface, "StartUnit", phosphorServiceName, "replace"); } catch (const phosphor::fan::util::DBusMethodError& e) { std::cerr << "Unable to start fan control: " << e.what() << std::endl; } } /** * @function force reload of control files by sending HUP signal */ void reload() { try { SDBusPlus::callMethod(systemdService, systemdPath, systemdMgrIface, "KillUnit", phosphorServiceName, "main", SIGHUP); } catch (const phosphor::fan::util::DBusPropertyError& e) { std::cerr << "Unable to reload configuration files: " << e.what() << std::endl; } } /** * @function dump debug data */ void dumpFanControl() { namespace fs = std::filesystem; try { // delete existing file if (fs::exists(dumpFile)) { std::filesystem::remove(dumpFile); } SDBusPlus::callMethod(systemdService, systemdPath, systemdMgrIface, "KillUnit", phosphorServiceName, "main", SIGUSR1); bool done = false; size_t tries = 0; const size_t maxTries = 30; do { // wait for file to be detected sleep(1); if (fs::exists(dumpFile)) { try { auto unused{nlohmann::json::parse(std::ifstream{dumpFile})}; done = true; } catch (...) {} } if (++tries > maxTries) { std::cerr << "Timed out waiting for fan control dump.\n"; return; } } while (!done); std::cout << "Fan control dump written to: " << dumpFile << std::endl; } catch (const phosphor::fan::util::DBusPropertyError& e) { std::cerr << "Unable to dump fan control: " << e.what() << std::endl; } } /** * @function Query items in the dump file */ void queryDumpFile(const DumpQuery& dq) { nlohmann::json output; std::ifstream file{dumpFile}; if (!file.good()) { std::cerr << "Unable to open dump file, please run 'fanctl dump'.\n"; return; } auto dumpData = nlohmann::json::parse(file); if (!dumpData.contains(dq.section)) { std::cerr << "Error: Dump file does not contain " << dq.section << " section" << "\n"; return; } const auto& section = dumpData.at(dq.section); if (section.is_array()) { for (const auto& entry : section) { if (!entry.is_string() || dq.name.empty() || (entry.get().find(dq.name) != std::string::npos)) { output[dq.section].push_back(entry); } } std::cout << std::setw(4) << output << "\n"; return; } for (const auto& [key1, values1] : section.items()) { if (dq.name.empty() || (key1.find(dq.name) != std::string::npos)) { // If no properties specified, print the whole JSON value if (dq.properties.empty()) { output[key1] = values1; continue; } // Look for properties both one and two levels down. // Future improvement: Use recursion. for (const auto& [key2, values2] : values1.items()) { for (const auto& prop : dq.properties) { if (prop == key2) { output[key1][prop] = values2; } } for (const auto& [key3, values3] : values2.items()) { for (const auto& prop : dq.properties) { if (prop == key3) { output[key1][prop] = values3; } } } } } } if (!output.empty()) { std::cout << std::setw(4) << output << "\n"; } } /** * @function setup the CLI object to accept all options */ void initCLI(CLI::App& app, uint64_t& target, std::vector& fanList, [[maybe_unused]] DumpQuery& dq) { app.set_help_flag("-h,--help", "Print this help page and exit."); // App requires only 1 subcommand to be given app.require_subcommand(1); // This represents the command given auto commands = app.add_option_group("Commands"); // status method std::string strHelp("Prints fan target/tach readings, present/functional " "states, and fan-monitor/BMC/Power service status"); auto cmdStatus = commands->add_subcommand("status", strHelp); cmdStatus->set_help_flag("-h, --help", strHelp); cmdStatus->require_option(0); // get method strHelp = "Get the current fan target and feedback speeds for all rotors"; auto cmdGet = commands->add_subcommand("get", strHelp); cmdGet->set_help_flag("-h, --help", strHelp); cmdGet->require_option(0); // set method strHelp = "Set target (all rotors) for one-or-more fans"; auto cmdSet = commands->add_subcommand("set", strHelp); strHelp = R"(set [TARGET SENSOR(S)] - RPM/PWM target to set the fans [TARGET SENSOR LIST] - list of target sensors to set)"; cmdSet->set_help_flag("-h, --help", strHelp); cmdSet->add_option("target", target, "RPM/PWM target to set the fans"); cmdSet->add_option( "fan list", fanList, "[optional] list of 1+ fans to set target RPM/PWM (default: all)"); cmdSet->require_option(); #ifdef CONTROL_USE_JSON strHelp = "Reload phosphor-fan configuration files"; auto cmdReload = commands->add_subcommand("reload", strHelp); cmdReload->set_help_flag("-h, --help", strHelp); cmdReload->require_option(0); #endif strHelp = "Resume running phosphor-fan-control"; auto cmdResume = commands->add_subcommand("resume", strHelp); cmdResume->set_help_flag("-h, --help", strHelp); cmdResume->require_option(0); // Dump method auto cmdDump = commands->add_subcommand("dump", "Dump debug data"); cmdDump->set_help_flag("-h, --help", "Dump debug data"); cmdDump->require_option(0); #ifdef CONTROL_USE_JSON // Query dump auto cmdDumpQuery = commands->add_subcommand("query_dump", "Query the dump file"); cmdDumpQuery->set_help_flag("-h, --help", "Query the dump file"); cmdDumpQuery ->add_option("-s, --section", dq.section, "Dump file section name") ->required(); cmdDumpQuery->add_option("-n, --name", dq.name, "Optional dump file entry name (or substring)"); cmdDumpQuery->add_option("-p, --properties", dq.properties, "Optional list of dump file property names"); cmdDumpQuery->add_flag("-d, --dump", dq.dump, "Force a dump before the query"); #endif } /** * @function main entry point for the application */ int main(int argc, char* argv[]) { auto rc = 0; uint64_t target{0U}; std::vector fanList; DumpQuery dq; try { CLI::App app{"Manually control, get fan tachs, view status, and resume " "automatic control of all fans within a chassis. Full " "documentation can be found at the readme:\n" "https://github.com/openbmc/phosphor-fan-presence/tree/" "master/docs/control/fanctl"}; initCLI(app, target, fanList, dq); CLI11_PARSE(app, argc, argv); if (app.got_subcommand("get")) { get(); } else if (app.got_subcommand("set")) { set(target, fanList); } #ifdef CONTROL_USE_JSON else if (app.got_subcommand("reload")) { reload(); } #endif else if (app.got_subcommand("resume")) { resume(); } else if (app.got_subcommand("status")) { status(); } else if (app.got_subcommand("dump")) { #ifdef CONTROL_USE_JSON dumpFanControl(); #else std::ofstream(dumpFile) << "{\n\"msg\": \"Unable to create dump on " "non-JSON config based system\"\n}"; #endif } #ifdef CONTROL_USE_JSON else if (app.got_subcommand("query_dump")) { if (dq.dump) { dumpFanControl(); } queryDumpFile(dq); } #endif } catch (const std::exception& e) { rc = -1; std::cerr << argv[0] << " failed: " << e.what() << std::endl; } return rc; }