PEL: Find message registry callouts to use

Add Registry::getCallouts() to find, based on the system type and
AdditionalData property, the callouts that apply for the callout JSON
passed in.  This will be used to choose the callouts to add to a PEL
based on the system and current failure informaton.

These files describe the actual JSON format:
* extensions/openpower-pels/registry/README.md
* extensions/openpower-pels/registry/schema/schema.json

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I5901e459981b10f6c338d3ef827d9128338dca50
diff --git a/extensions/openpower-pels/registry.cpp b/extensions/openpower-pels/registry.cpp
index 8b7ac18..830a013 100644
--- a/extensions/openpower-pels/registry.cpp
+++ b/extensions/openpower-pels/registry.cpp
@@ -262,6 +262,262 @@
     return id;
 }
 
+/**
+ * @brief Says if the JSON is the format that contains AdditionalData keys
+ *        as in index into them.
+ *
+ * @param[in] json - The highest level callout JSON
+ *
+ * @return bool - If it is the AdditionalData format or not
+ */
+bool calloutUsesAdditionalData(const nlohmann::json& json)
+{
+    return (json.contains("ADName") &&
+            json.contains("CalloutsWithTheirADValues"));
+}
+
+/**
+ * @brief Finds the callouts to use when there is no AdditionalData,
+ *        but the system type may be used as a key.
+ *
+ * One entry in the array looks like the following.  The System key
+ * is optional and if not present it means that entry applies to
+ * every configuration that doesn't have another entry with a matching
+ * System key.
+ *
+ *    {
+ *        "System": "system1",
+ *        "CalloutList":
+ *        [
+ *            {
+ *                "Priority": "high",
+ *                "LocCode": "P1-C1"
+ *            },
+ *            {
+ *                "Priority": "low",
+ *                "LocCode": "P1"
+ *            }
+ *        ]
+ *    }
+ */
+const nlohmann::json& findCalloutList(const nlohmann::json& json,
+                                      const std::string& systemType)
+{
+    const nlohmann::json* callouts = nullptr;
+
+    if (!json.is_array())
+    {
+        throw std::runtime_error{"findCalloutList was not passed a JSON array"};
+    }
+
+    // The entry with the system type match will take precedence over the entry
+    // without any "System" field in it at all, which will match all other
+    // cases.
+    for (const auto& calloutList : json)
+    {
+        if (calloutList.contains("System"))
+        {
+            if (systemType == calloutList["System"].get<std::string>())
+            {
+                callouts = &calloutList["CalloutList"];
+                break;
+            }
+        }
+        else
+        {
+            // Any entry with no System key
+            callouts = &calloutList["CalloutList"];
+        }
+    }
+
+    if (!callouts)
+    {
+        log<level::WARNING>(
+            "No matching system type entry or default system type entry "
+            " for PEL callout list",
+            entry("SYSTEMTYPE=%s", systemType.c_str()));
+
+        throw std::runtime_error{
+            "Could not find a CalloutList JSON for this error and system type"};
+    }
+
+    return *callouts;
+}
+
+/**
+ * @brief Creates a RegistryCallout based on the input JSON.
+ *
+ * The JSON looks like:
+ *     {
+ *          "Priority": "high",
+ *          "LocCode": "E1"
+ *          ...
+ *     }
+ *
+ * Schema validation enforces what keys are present.
+ *
+ * @param[in] json - The JSON dictionary entry for a callout
+ *
+ * @return RegistryCallout - A filled in RegistryCallout
+ */
+RegistryCallout makeRegistryCallout(const nlohmann::json& json)
+{
+    RegistryCallout callout;
+
+    callout.priority = "high";
+
+    if (json.contains("Priority"))
+    {
+        callout.priority = json["Priority"].get<std::string>();
+    }
+
+    if (json.contains("LocCode"))
+    {
+        callout.locCode = json["LocCode"].get<std::string>();
+    }
+
+    if (json.contains("Procedure"))
+    {
+        callout.procedure = json["Procedure"].get<std::string>();
+    }
+    else if (json.contains("SymbolicFRU"))
+    {
+        callout.symbolicFRU = json["SymbolicFRU"].get<std::string>();
+    }
+    else if (json.contains("SymbolicFRUTrusted"))
+    {
+        callout.symbolicFRUTrusted =
+            json["SymbolicFRUTrusted"].get<std::string>();
+    }
+
+    return callout;
+}
+
+/**
+ * @brief Returns the callouts to use when an AdditionalData key is
+ *        required to find the correct entries.
+ *
+ *       The System property is used to find which CalloutList to use.
+ *       If System is missing, then that CalloutList is valid for
+ *       everything.
+ *
+ * The JSON looks like:
+ *    [
+ *        {
+ *            "System": "systemA",
+ *            "CalloutList":
+ *            [
+ *                {
+ *                    "Priority": "high",
+ *                    "LocCode": "P1-C5"
+ *                }
+ *            ]
+ *         }
+ *    ]
+ *
+ * @param[in] json - The callout JSON
+ * @param[in] systemType - The system type from EntityManager
+ *
+ * @return std::vector<RegistryCallout> - The callouts to use
+ */
+std::vector<RegistryCallout> getCalloutsWithoutAD(const nlohmann::json& json,
+                                                  const std::string& systemType)
+{
+    std::vector<RegistryCallout> calloutEntries;
+
+    // Find the CalloutList to use based on the system type
+    const auto& calloutList = findCalloutList(json, systemType);
+
+    // We finally found the callouts, make the objects.
+    for (const auto& callout : calloutList)
+    {
+        calloutEntries.push_back(std::move(makeRegistryCallout(callout)));
+    }
+
+    return calloutEntries;
+}
+
+/**
+ * @brief Returns the callouts to use when an AdditionalData key is
+ *        required to find the correct entries.
+ *
+ * The JSON looks like:
+ *    {
+ *        "ADName": "PROC_NUM",
+ *        "CalloutsWithTheirADValues":
+ *        [
+ *            {
+ *                "ADValue": "0",
+ *                "Callouts":
+ *                [
+ *                    {
+ *                        "CalloutList":
+ *                        [
+ *                            {
+ *                                "Priority": "high",
+ *                                "LocCode": "P1-C5"
+ *                            }
+ *                        ]
+ *                    }
+ *                ]
+ *            }
+ *        ]
+ *     }
+ *
+ * Note that the "Callouts" entry above is the same as the top level
+ * entry used when there is no AdditionalData key.
+ *
+ * @param[in] json - The callout JSON
+ * @param[in] systemType - The system type from EntityManager
+ * @param[in] additionalData - The AdditionalData property
+ *
+ * @return std::vector<RegistryCallout> - The callouts to use
+ */
+std::vector<RegistryCallout>
+    getCalloutsUsingAD(const nlohmann::json& json,
+                       const std::string& systemType,
+                       const AdditionalData& additionalData)
+{
+    // This indicates which AD field we'll be using
+    auto keyName = json["ADName"].get<std::string>();
+
+    // Get the actual value from the AD data
+    auto adValue = additionalData.getValue(keyName);
+
+    if (!adValue)
+    {
+        // The AdditionalData did not contain the necessary key
+        log<level::WARNING>(
+            "The PEL message registry callouts JSON "
+            "said to use an AdditionalData key that isn't in the "
+            "AdditionalData event log property",
+            entry("ADNAME=%s\n", keyName.c_str()));
+        throw std::runtime_error{
+            "Missing AdditionalData entry for this callout"};
+    }
+
+    const auto& callouts = json["CalloutsWithTheirADValues"];
+
+    // find the entry with that AD value
+    auto it = std::find_if(
+        callouts.begin(), callouts.end(), [adValue](const nlohmann::json& j) {
+            return *adValue == j["ADValue"].get<std::string>();
+        });
+
+    if (it == callouts.end())
+    {
+        log<level::WARNING>(
+            "No callout entry found for the AdditionalData value used",
+            entry("AD_VALUE=%s", adValue->c_str()));
+
+        throw std::runtime_error{
+            "No callout entry found for the AdditionalData value used"};
+    }
+
+    // Proceed to find the callouts possibly based on system type.
+    return getCalloutsWithoutAD((*it)["Callouts"], systemType);
+}
+
 } // namespace helper
 
 std::optional<Entry> Registry::lookup(const std::string& name, LookupType type,
@@ -432,6 +688,22 @@
     return registry;
 }
 
+std::vector<RegistryCallout>
+    Registry::getCallouts(const nlohmann::json& calloutJSON,
+                          const std::string& systemType,
+                          const AdditionalData& additionalData)
+{
+    // The JSON may either use an AdditionalData key
+    // as an index, or not.
+    if (helper::calloutUsesAdditionalData(calloutJSON))
+    {
+        return helper::getCalloutsUsingAD(calloutJSON, systemType,
+                                          additionalData);
+    }
+
+    return helper::getCalloutsWithoutAD(calloutJSON, systemType);
+}
+
 } // namespace message
 } // namespace pels
 } // namespace openpower
diff --git a/extensions/openpower-pels/registry.hpp b/extensions/openpower-pels/registry.hpp
index 09666f3..047853d 100644
--- a/extensions/openpower-pels/registry.hpp
+++ b/extensions/openpower-pels/registry.hpp
@@ -1,4 +1,6 @@
 #pragma once
+#include "additional_data.hpp"
+
 #include <filesystem>
 #include <nlohmann/json.hpp>
 #include <optional>
@@ -169,6 +171,18 @@
 };
 
 /**
+ * @brief Holds callout information pulled out of the JSON.
+ */
+struct RegistryCallout
+{
+    std::string priority;
+    std::string locCode;
+    std::string procedure;
+    std::string symbolicFRU;
+    std::string symbolicFRUTrusted;
+};
+
+/**
  * @class Registry
  *
  * This class wraps the message registry JSON data and allows one to find
@@ -235,6 +249,26 @@
     std::optional<Entry> lookup(const std::string& name, LookupType type,
                                 bool toCache = false);
 
+    /**
+     * @brief Find the callouts to put into the PEL based on the calloutJSON
+     *        data.
+     *
+     * The system type and AdditionalData are used to index into the correct
+     * callout table.
+     *
+     * Throws exceptions on failures.
+     *
+     * @param[in] calloutJSON - Where to look up the  callouts
+     * @param[in] systemType - The system type from EntityManager
+     * @param[in] additionalData - The AdditionalData property
+     *
+     * @return std::vector<RegistryCallout> - The callouts to use
+     */
+    static std::vector<RegistryCallout>
+        getCallouts(const nlohmann::json& calloutJSON,
+                    const std::string& systemType,
+                    const AdditionalData& additionalData);
+
   private:
     /**
      * @brief Parse message registry file using nlohmann::json
diff --git a/test/openpower-pels/registry_test.cpp b/test/openpower-pels/registry_test.cpp
index 2944ce0..92c1b14 100644
--- a/test/openpower-pels/registry_test.cpp
+++ b/test/openpower-pels/registry_test.cpp
@@ -22,6 +22,7 @@
 #include <gtest/gtest.h>
 
 using namespace openpower::pels::message;
+using namespace openpower::pels;
 namespace fs = std::filesystem;
 
 const auto registryData = R"(
@@ -334,3 +335,270 @@
     EXPECT_THROW(getComponentID(0x11, 0x8800, R"({})"_json, "foo"),
                  std::runtime_error);
 }
+
+// Test when callouts are in the JSON.
+TEST_F(RegistryTest, TestGetCallouts)
+{
+    {
+        // Callouts without AD, that depend on system type,
+        // where there is a default entry without a system type.
+        auto json = R"(
+        [
+        {
+            "System": "system1",
+            "CalloutList":
+            [
+                {
+                    "Priority": "high",
+                    "LocCode": "P1-C1"
+                },
+                {
+                    "Priority": "low",
+                    "LocCode": "P1"
+                },
+                {
+                    "Priority": "low",
+                    "SymbolicFRU": "service_docs"
+                }
+            ]
+        },
+        {
+            "CalloutList":
+            [
+                {
+                    "Priority": "medium",
+                    "Procedure": "no_vpd_for_fru"
+                },
+                {
+                    "Priority": "low",
+                    "LocCode": "P3-C8",
+                    "SymbolicFRUTrusted": "service_docs"
+                }
+            ]
+
+        }
+        ])"_json;
+
+        AdditionalData ad;
+
+        auto callouts = Registry::getCallouts(json, "system1", ad);
+        EXPECT_EQ(callouts.size(), 3);
+        EXPECT_EQ(callouts[0].priority, "high");
+        EXPECT_EQ(callouts[0].locCode, "P1-C1");
+        EXPECT_EQ(callouts[0].procedure, "");
+        EXPECT_EQ(callouts[0].symbolicFRU, "");
+        EXPECT_EQ(callouts[0].symbolicFRUTrusted, "");
+        EXPECT_EQ(callouts[1].priority, "low");
+        EXPECT_EQ(callouts[1].locCode, "P1");
+        EXPECT_EQ(callouts[1].procedure, "");
+        EXPECT_EQ(callouts[1].symbolicFRU, "");
+        EXPECT_EQ(callouts[1].symbolicFRUTrusted, "");
+        EXPECT_EQ(callouts[2].priority, "low");
+        EXPECT_EQ(callouts[2].locCode, "");
+        EXPECT_EQ(callouts[2].procedure, "");
+        EXPECT_EQ(callouts[2].symbolicFRU, "service_docs");
+        EXPECT_EQ(callouts[2].symbolicFRUTrusted, "");
+
+        // system2 isn't in the JSON, so it will pick the default one
+        callouts = Registry::getCallouts(json, "system2", ad);
+        EXPECT_EQ(callouts.size(), 2);
+        EXPECT_EQ(callouts[0].priority, "medium");
+        EXPECT_EQ(callouts[0].locCode, "");
+        EXPECT_EQ(callouts[0].procedure, "no_vpd_for_fru");
+        EXPECT_EQ(callouts[0].symbolicFRU, "");
+        EXPECT_EQ(callouts[1].priority, "low");
+        EXPECT_EQ(callouts[1].locCode, "P3-C8");
+        EXPECT_EQ(callouts[1].procedure, "");
+        EXPECT_EQ(callouts[1].symbolicFRU, "");
+        EXPECT_EQ(callouts[1].symbolicFRUTrusted, "service_docs");
+    }
+
+    // Empty JSON array (treated as an error)
+    {
+        auto json = R"([])"_json;
+        AdditionalData ad;
+        EXPECT_THROW(Registry::getCallouts(json, "system1", ad),
+                     std::runtime_error);
+    }
+
+    {
+        // Callouts without AD, that depend on system type,
+        // where there isn't a default entry without a system type.
+        auto json = R"(
+        [
+        {
+            "System": "system1",
+            "CalloutList":
+            [
+                {
+                    "Priority": "high",
+                    "LocCode": "P1-C1"
+                },
+                {
+                    "Priority": "low",
+                    "LocCode": "P1",
+                    "SymbolicFRU": "1234567"
+                }
+            ]
+        },
+        {
+            "System": "system2",
+            "CalloutList":
+            [
+                {
+                    "Priority": "medium",
+                    "LocCode": "P7",
+                    "CalloutType": "tool_fru"
+                }
+            ]
+
+        }
+        ])"_json;
+
+        AdditionalData ad;
+
+        auto callouts = Registry::getCallouts(json, "system1", ad);
+        EXPECT_EQ(callouts.size(), 2);
+        EXPECT_EQ(callouts[0].priority, "high");
+        EXPECT_EQ(callouts[0].locCode, "P1-C1");
+        EXPECT_EQ(callouts[0].procedure, "");
+        EXPECT_EQ(callouts[0].symbolicFRU, "");
+        EXPECT_EQ(callouts[0].symbolicFRUTrusted, "");
+        EXPECT_EQ(callouts[1].priority, "low");
+        EXPECT_EQ(callouts[1].locCode, "P1");
+        EXPECT_EQ(callouts[1].procedure, "");
+        EXPECT_EQ(callouts[1].symbolicFRU, "1234567");
+        EXPECT_EQ(callouts[1].symbolicFRUTrusted, "");
+
+        callouts = Registry::getCallouts(json, "system2", ad);
+        EXPECT_EQ(callouts.size(), 1);
+        EXPECT_EQ(callouts[0].priority, "medium");
+        EXPECT_EQ(callouts[0].locCode, "P7");
+        EXPECT_EQ(callouts[0].procedure, "");
+        EXPECT_EQ(callouts[0].symbolicFRU, "");
+        EXPECT_EQ(callouts[0].symbolicFRUTrusted, "");
+
+        // There is no entry for system3 or a default system,
+        // so this should fail.
+        EXPECT_THROW(Registry::getCallouts(json, "system3", ad),
+                     std::runtime_error);
+    }
+
+    {
+        // Callouts that use the AdditionalData key PROC_NUM
+        // as an index into them, along with a system type.
+        // It supports PROC_NUMs 0 and 1.
+        auto json = R"(
+        {
+            "ADName": "PROC_NUM",
+            "CalloutsWithTheirADValues":
+            [
+                {
+                    "ADValue": "0",
+                    "Callouts":
+                    [
+                        {
+                            "System": "system3",
+                            "CalloutList":
+                            [
+                                {
+                                    "Priority": "high",
+                                    "LocCode": "P1-C5"
+                                },
+                                {
+                                    "Priority": "medium",
+                                    "LocCode": "P1-C6",
+                                    "SymbolicFRU": "1234567"
+                                },
+                                {
+                                    "Priority": "low",
+                                    "Procedure": "no_vpd_for_fru",
+                                    "CalloutType": "config_procedure"
+                                }
+                            ]
+                        },
+                        {
+                            "CalloutList":
+                            [
+                                {
+                                    "Priority": "low",
+                                    "LocCode": "P55"
+                                }
+                            ]
+                        }
+                    ]
+                },
+                {
+                    "ADValue": "1",
+                    "Callouts":
+                    [
+                        {
+                            "CalloutList":
+                            [
+                                {
+                                    "Priority": "high",
+                                    "LocCode": "P1-C6",
+                                    "CalloutType": "external_fru"
+                                }
+                            ]
+                        }
+                    ]
+                }
+            ]
+        })"_json;
+
+        {
+            // Find callouts for PROC_NUM 0 on system3
+            std::vector<std::string> adData{"PROC_NUM=0"};
+            AdditionalData ad{adData};
+
+            auto callouts = Registry::getCallouts(json, "system3", ad);
+            EXPECT_EQ(callouts.size(), 3);
+            EXPECT_EQ(callouts[0].priority, "high");
+            EXPECT_EQ(callouts[0].locCode, "P1-C5");
+            EXPECT_EQ(callouts[0].procedure, "");
+            EXPECT_EQ(callouts[0].symbolicFRU, "");
+            EXPECT_EQ(callouts[0].symbolicFRUTrusted, "");
+            EXPECT_EQ(callouts[1].priority, "medium");
+            EXPECT_EQ(callouts[1].locCode, "P1-C6");
+            EXPECT_EQ(callouts[1].procedure, "");
+            EXPECT_EQ(callouts[1].symbolicFRU, "1234567");
+            EXPECT_EQ(callouts[1].symbolicFRUTrusted, "");
+            EXPECT_EQ(callouts[2].priority, "low");
+            EXPECT_EQ(callouts[2].locCode, "");
+            EXPECT_EQ(callouts[2].procedure, "no_vpd_for_fru");
+            EXPECT_EQ(callouts[2].symbolicFRU, "");
+            EXPECT_EQ(callouts[2].symbolicFRUTrusted, "");
+
+            // Find callouts for PROC_NUM 0 that uses the default system entry.
+            callouts = Registry::getCallouts(json, "system99", ad);
+            EXPECT_EQ(callouts.size(), 1);
+            EXPECT_EQ(callouts[0].priority, "low");
+            EXPECT_EQ(callouts[0].locCode, "P55");
+            EXPECT_EQ(callouts[0].procedure, "");
+            EXPECT_EQ(callouts[0].symbolicFRU, "");
+            EXPECT_EQ(callouts[0].symbolicFRUTrusted, "");
+        }
+        {
+            // Find callouts for PROC_NUM 1 that uses a default system entry.
+            std::vector<std::string> adData{"PROC_NUM=1"};
+            AdditionalData ad{adData};
+
+            auto callouts = Registry::getCallouts(json, "system1", ad);
+            EXPECT_EQ(callouts.size(), 1);
+            EXPECT_EQ(callouts[0].priority, "high");
+            EXPECT_EQ(callouts[0].locCode, "P1-C6");
+            EXPECT_EQ(callouts[0].procedure, "");
+            EXPECT_EQ(callouts[0].symbolicFRU, "");
+            EXPECT_EQ(callouts[0].symbolicFRUTrusted, "");
+        }
+        {
+            // There is no entry for PROC_NUM 2, it will fail.
+            std::vector<std::string> adData{"PROC_NUM=2"};
+            AdditionalData ad{adData};
+
+            EXPECT_THROW(Registry::getCallouts(json, "system1", ad),
+                         std::runtime_error);
+        }
+    }
+}