fanctl: Add query_dump cmd to search dump contents

Add a 'query_dump' subcommand to fanctl to assist in looking at the
content of the dump file /tmp/fan_control.json generated by the 'fanctl
dump' command.  This dump file contains the flight recorder as well as
fan control's objects, service, and parameter cache.

The command's output is in JSON.

It has the following parameters:
--section
   The section of the dump: objects, parameters, or services

--name
   The name of the top level JSON dictionary key to match on.
   This can match on substrings, like 'sensors' for all sensors.
   Optional - if not provided it matches on every name.

--properties
   List of property names to match on.
   Geared toward printing property values from the objects cache.
   Optional, if not provided the whole JSON objects under the
   name is printed.

Examples:

Print the flight recorder:
fanctl query_dump --section flight_recorder
{
    "0": "Oct 29 03:41:29.468444: main: Startup"
}

Print the altitude:
fanctl query_dump -s objects -n Altitude -p Value
{
    "/xyz/openbmc_project/sensors/altitude/Altitude": {
        "Value": 1032.82
    }
}

Print all temperatures:
fanctl query_dump -s objects -n sensors/temp -p Value
{
    "/xyz/openbmc_project/sensors/temperature/Ambient_Virtual_Temp": {
        "Value": 25.0
    },
    ...
}

Print every interface and property of the ambient sensor
fanctl query_dump -s objects -n Ambient
{
   ... interfaces with their property names and values ...
}

Print every Value property in the cache:
fanctl query_dump -s objects -p Value
{
    "/xyz/openbmc_project/sensors/altitude/Altitude": {
        "Value": 1032.82
    },
    "/xyz/openbmc_project/sensors/temperature/Ambient_Virtual_Temp": {
        "Value": 25.0
    }
}

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: If66a74b432481f337f02813f2e61ae5767a57d1d
diff --git a/.gitignore b/.gitignore
index 81c7e91..0b4cff4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,7 +20,7 @@
 *.o

 phosphor-fan-presence-tach

 phosphor-fan-control

-fanctl

+control/fanctl

 fan_zone_defs.cpp

 phosphor-fan-monitor

 phosphor-cooling-type

diff --git a/control/fanctl.cpp b/control/fanctl.cpp
index 34677f1..909ea0e 100644
--- a/control/fanctl.cpp
+++ b/control/fanctl.cpp
@@ -19,8 +19,11 @@
 #include "sdbusplus.hpp"
 
 #include <CLI/CLI.hpp>
+#include <nlohmann/json.hpp>
 #include <sdbusplus/bus.hpp>
 
+#include <chrono>
+#include <filesystem>
 #include <iomanip>
 #include <iostream>
 
@@ -30,6 +33,7 @@
 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
 {
@@ -39,6 +43,13 @@
     METHOD = 3
 };
 
+struct DumpQuery
+{
+    std::string section;
+    std::string name;
+    std::vector<std::string> properties;
+};
+
 /**
  * @function extracts fan name from dbus path string (last token where
  * delimiter is the / character), with proper bounds checking.
@@ -531,8 +542,7 @@
     {
         SDBusPlus::callMethod(systemdService, systemdPath, systemdMgrIface,
                               "KillUnit", phosphorServiceName, "main", SIGUSR1);
-        std::cout << "Fan control dump written to: /tmp/fan_control_dump.json"
-                  << std::endl;
+        std::cout << "Fan control dump written to: " << dumpFile << std::endl;
     }
     catch (const phosphor::fan::util::DBusPropertyError& e)
     {
@@ -541,9 +551,79 @@
 }
 
 /**
+ * @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);
+
+    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)
+void initCLI(CLI::App& app, uint64_t& target, std::vector<std::string>& fanList,
+             DumpQuery& dq)
 {
     app.set_help_flag("-h,--help", "Print this help page and exit.");
 
@@ -598,6 +678,20 @@
     cmdDump->set_help_flag("-h, --help",
                            "Dump the FlightRecorder diagnostic log");
     cmdDump->require_option(0);
+
+#ifdef CONTROL_USE_JSON
+    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");
+#endif
 }
 
 /**
@@ -608,6 +702,7 @@
     auto rc = 0;
     uint64_t target{0U};
     std::vector<std::string> fanList;
+    DumpQuery dq;
 
     try
     {
@@ -617,7 +712,7 @@
                      "https://github.com/openbmc/phosphor-fan-presence/tree/"
                      "master/docs/control/fanctl"};
 
-        initCLI(app, target, fanList);
+        initCLI(app, target, fanList, dq);
 
         CLI11_PARSE(app, argc, argv);
 
@@ -645,6 +740,12 @@
         {
             dumpFanControl();
         }
+#ifdef CONTROL_USE_JSON
+        else if (app.got_subcommand("query_dump"))
+        {
+            queryDumpFile(dq);
+        }
+#endif
     }
     catch (const std::exception& e)
     {
diff --git a/docs/control/fanctl/README.md b/docs/control/fanctl/README.md
index cb41620..0abc7b3 100644
--- a/docs/control/fanctl/README.md
+++ b/docs/control/fanctl/README.md
@@ -49,6 +49,10 @@
     * Note: In the case where a system does not have an active fan control
       algorithm enabled yet, an intended safe fan target should be set
       prior to resuming
+dump
+    - Tell fan control to dump its caches and flight recorder.
+query_dump
+    - Provides arguments to search the dump file.
 help
     - Display this help and exit
 ```
@@ -112,3 +116,14 @@
 
     > fanctl reload
 
+- Tell the fan control daemon to dump debug data to /tmp/fan\_control\_dump.json
+    > fanctl dump
+
+- Print all temperatures in the fan control cache after running 'fanctl dump':
+    > fanctl query_dump -s objects -n sensors/temperature -p Value
+
+- Print every interface and property in the Ambient temp sensor's cache entry:
+    > fanctl query_dump -s objects -n Ambient
+
+- Print the flight recorder after running 'fanctl dump':
+    > fanctl query_dump -s flight_recorder