control: command line tool to retrieve fan status (status/help functions)

This is part one of a multipart commit to create a fan control
command line utility to report service status, show target/
actual RPM/PWM info, and manually control single fans.
Currently this only implements "status" and "help" commands
(and makefile). Further functionality will come in subsequent
commits.

Signed-off-by: Mike Capps <mikepcapps@gmail.com>
Signed-off-by: Matthew Barth <msbarth@us.ibm.com>
Change-Id: I9a9fc5c50edf7981075b59bb526e967e50e67c86
diff --git a/control/fanctl.cpp b/control/fanctl.cpp
new file mode 100644
index 0000000..f5e77f2
--- /dev/null
+++ b/control/fanctl.cpp
@@ -0,0 +1,361 @@
+/**
+ * Copyright © 2021 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 "sdbusplus.hpp"
+
+#include <CLI/CLI.hpp>
+#include <sdbusplus/bus.hpp>
+
+#include <iomanip>
+#include <iostream>
+
+using SDBusPlus = phosphor::fan::util::SDBusPlus;
+
+constexpr auto phosphorServiceName = "phosphor-fan-control@0.service";
+constexpr auto systemdMgrIface = "org.freedesktop.systemd1.Manager";
+constexpr auto systemdPath = "/org/freedesktop/systemd1";
+constexpr auto systemdService = "org.freedesktop.systemd1";
+
+std::map<std::string, 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<std::string, std::string> paths{
+    {"motherboard",
+     "/xyz/openbmc_project/inventory/system/chassis/motherboard"},
+    {"tach", "/xyz/openbmc_project/sensors/fan_tach"}};
+
+// paths by D-bus interface,fan name
+std::map<std::string, std::map<std::string, std::vector<std::string>>> pathMap;
+
+/**
+ * @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(std::string const& 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 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 (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 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& bus{SDBusPlus::getBus()};
+
+    std::string tmethod("RPM"), fmethod("RPMS"), property;
+
+    std::vector<std::string> sensorPaths, fanNames;
+
+    // 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 if none found
+    if (0 == fanNames.size())
+    {
+        tmethod = "PWM";
+        fmethod = "PWM";
+
+        for (auto& path : SDBusPlus::getSubTreePathsRaw(
+                 bus, paths["tach"], interfaces["FanPwm"], 0))
+        {
+            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 speed sensor paths for each fan
+    pathMap["speed"] = pathMap["tach"];
+
+    // 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);
+
+    // 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(" << tmethod << ")  FEEDBACKS(RPMS)   PRESENT"
+         << "   FUNCTIONAL" << endl;
+    cout << "==============================================================="
+         << endl;
+
+    for (auto& fan : fanNames)
+    {
+        cout << " " << fan << setw(18);
+
+        // get the target RPM
+        property = "Target";
+        cout << SDBusPlus::getProperty<uint64_t>(
+                    pathMap["speed"][fan][0], interfaces["FanSpeed"], property)
+             << setw(11);
+
+        // get the sensor RPM
+        property = "Value";
+        int numRotors = pathMap["tach"][fan].size();
+        // print tach readings for each rotor
+        for (auto& path : pathMap["tach"][fan])
+        {
+            cout << SDBusPlus::getProperty<double>(
+                path, interfaces["SensorValue"], property);
+
+            // dont print slash on last rotor
+            if (--numRotors)
+                cout << "/";
+        }
+        cout << setw(10);
+
+        // get the present property
+        property = "Present";
+        std::string val;
+        for (auto& path : pathMap["inventory"][fan])
+        {
+            try
+            {
+                if (SDBusPlus::getProperty<bool>(path, interfaces["Item"],
+                                                 property))
+                {
+                    val = "true";
+                }
+                else
+                {
+                    val = "false";
+                }
+            }
+            catch (phosphor::fan::util::DBusPropertyError&)
+            {
+                val = "Unknown";
+            }
+            cout << val;
+        }
+
+        cout << setw(13);
+
+        // get the functional property
+        property = "Functional";
+        for (auto& path : pathMap["opstatus"][fan])
+        {
+            try
+            {
+                if (SDBusPlus::getProperty<bool>(path, interfaces["OpStatus"],
+                                                 property))
+                {
+                    val = "true";
+                }
+                else
+                {
+                    val = "false";
+                }
+            }
+            catch (phosphor::fan::util::DBusPropertyError&)
+            {
+                val = "Unknown";
+            }
+            cout << val;
+        }
+
+        cout << endl;
+    }
+}
+
+/**
+ * @function main entry point for the application
+ */
+int main(int argc, char* argv[])
+{
+    auto rc = 0;
+
+    try
+    {
+        CLI::App app{R"(Manually control, get fan tachs, view status, and resume
+             automatic control of all fans within a chassis.)"};
+
+        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");
+
+        // Configure method
+        auto cmdStatus = commands->add_subcommand(
+            "status",
+            "Get the fan tach targets/values and fan-control service status");
+
+        cmdStatus->set_help_flag(
+            "-h, --help", "Prints fan target/tach readings, present/functional "
+                          "states, and fan-monitor/BMC/Power service status");
+        cmdStatus->require_option(0);
+
+        CLI11_PARSE(app, argc, argv);
+
+        if (app.got_subcommand("status"))
+        {
+            status();
+        }
+    }
+    catch (const std::exception& e)
+    {
+        rc = -1;
+        std::cerr << argv[0] << " failed: " << e.what() << std::endl;
+    }
+
+    return rc;
+}