blob: 9789c24c0513c7c5360b723bda24e277e377ee42 [file] [log] [blame]
/**
* 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 <CLI/CLI.hpp>
#include <nlohmann/json.hpp>
#include <sdbusplus/bus.hpp>
#include <chrono>
#include <filesystem>
#include <iomanip>
#include <iostream>
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<std::string> 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<std::string, std::vector<std::string>> getPathsFromIface(
const std::string& path, const std::string& iface,
const std::vector<std::string>& fans, bool shortPath = false)
{
std::map<std::string, std::vector<std::string>> 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<std::string> fanNames;
// paths by D-bus interface,fan name
std::map<std::string, std::map<std::string, std::vector<std::string>>>
pathMap;
std::string method("RPM");
std::map<const std::string, const std::string> 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<const std::string, const std::string> 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<std::string, 6> getStates()
{
using DBusTuple =
std::tuple<std::string, std::string, std::string, std::string,
std::string, std::string, sdbusplus::message::object_path,
uint32_t, std::string, sdbusplus::message::object_path>;
std::array<std::string, 6> ret;
std::vector<std::string> services{phosphorServiceName};
try
{
auto fields{SDBusPlus::callMethodAndRead<std::vector<DBusTuple>>(
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<std::string>(path, iface, "CurrentBMCState");
path = "/xyz/openbmc_project/state/chassis0";
iface = "xyz.openbmc_project.State.Chassis";
ret[4] =
SDBusPlus::getProperty<std::string>(path, iface, "CurrentPowerState");
path = "/xyz/openbmc_project/state/host0";
iface = "xyz.openbmc_project.State.Host";
ret[5] =
SDBusPlus::getProperty<std::string>(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<METHOD>(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<FAN_NAMES>(busData)};
auto& pathMap{std::get<PATH_MAP>(busData)};
auto& interfaces{std::get<IFACES>(busData)};
for (auto& fan : fanNames)
{
cout << setw(8) << std::left << fan << std::right << setw(13);
// get the target RPM
property = "Target";
cout << SDBusPlus::getProperty<uint64_t>(
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<double>(
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<bool>(
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<bool>(
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<FAN_NAMES>(busData)};
auto& pathMap{std::get<PATH_MAP>(busData)};
auto& interfaces{std::get<IFACES>(busData)};
auto& method = std::get<METHOD>(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<uint64_t>(
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<double>(
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<std::string>& fanList)
{
auto busData = loadDBusData();
auto& bus{SDBusPlus::getBus()};
auto& pathMap{std::get<PATH_MAP>(busData)};
auto& interfaces{std::get<IFACES>(busData)};
auto& method = std::get<METHOD>(busData);
std::string ifaceType(method == "RPM" ? "FanSpeed" : "FanPwm");
// stop the fan-control service
SDBusPlus::callMethodAndRead<sdbusplus::message::object_path>(
systemdService, systemdPath, systemdMgrIface, "StopUnit",
phosphorServiceName, "replace");
if (fanList.size() == 0)
{
fanList = std::get<FAN_NAMES>(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<uint64_t>(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<sdbusplus::message::object_path>(
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<std::string>().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<std::string>& 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> [TARGET SENSOR(S)]
<TARGET>
- 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<std::string> 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;
}