PEL: Parse and add JSON callouts

In the SRC class, create callout objects for the callouts that were
contained in the incoming JSON array.  These can be procedure, symbolic
FRU, or normal hardware FRU callouts.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I525f0f700508693c17b9987f555c0d917cf72fa6
diff --git a/extensions/openpower-pels/src.cpp b/extensions/openpower-pels/src.cpp
index c5d8ed8..6462598 100644
--- a/extensions/openpower-pels/src.cpp
+++ b/extensions/openpower-pels/src.cpp
@@ -1066,7 +1066,125 @@
 void SRC::addJSONCallout(const nlohmann::json& jsonCallout,
                          const DataInterfaceBase& dataIface)
 {
-    // TODO
+    auto priority = getPriorityFromJSON(jsonCallout);
+    std::string locCode;
+    std::string unexpandedLocCode;
+    std::unique_ptr<src::Callout> callout;
+
+    // Expand the location code if it's there
+    if (jsonCallout.contains("LocationCode"))
+    {
+        unexpandedLocCode = jsonCallout.at("LocationCode").get<std::string>();
+
+        try
+        {
+            locCode = dataIface.expandLocationCode(unexpandedLocCode, 0);
+        }
+        catch (const std::exception& e)
+        {
+            addDebugData(fmt::format("Unable to expand location code {}: {}",
+                                     unexpandedLocCode, e.what()));
+            // Use the value from the JSON so at least there's something
+            locCode = unexpandedLocCode;
+        }
+    }
+
+    // Create either a procedure, symbolic FRU, or normal FRU callout.
+    if (jsonCallout.contains("Procedure"))
+    {
+        auto procedure = jsonCallout.at("Procedure").get<std::string>();
+
+        callout = std::make_unique<src::Callout>(
+            static_cast<CalloutPriority>(priority), procedure,
+            src::CalloutValueType::raw);
+    }
+    else if (jsonCallout.contains("SymbolicFRU"))
+    {
+        auto fru = jsonCallout.at("SymbolicFRU").get<std::string>();
+
+        bool trusted = false;
+        if (jsonCallout.contains("TrustedLocationCode") && !locCode.empty())
+        {
+            trusted = jsonCallout.at("TrustedLocationCode").get<bool>();
+        }
+
+        callout = std::make_unique<src::Callout>(
+            static_cast<CalloutPriority>(priority), fru,
+            src::CalloutValueType::raw, locCode, trusted);
+    }
+    else
+    {
+        // A hardware FRU
+        std::string inventoryPath;
+
+        if (jsonCallout.contains("InventoryPath"))
+        {
+            inventoryPath = jsonCallout.at("InventoryPath").get<std::string>();
+        }
+        else
+        {
+            if (unexpandedLocCode.empty())
+            {
+                throw std::runtime_error{"JSON callout needs either an "
+                                         "inventory path or location code"};
+            }
+
+            try
+            {
+                inventoryPath = dataIface.getInventoryFromLocCode(
+                    unexpandedLocCode, 0, false);
+            }
+            catch (const std::exception& e)
+            {
+                throw std::runtime_error{
+                    fmt::format("Unable to get inventory path from "
+                                "location code: {}: {}",
+                                unexpandedLocCode, e.what())};
+            }
+        }
+
+        // If the location code was also passed in, use that here too
+        // so addInventoryCallout doesn't have to look it up.
+        std::optional<std::string> lc;
+        if (!locCode.empty())
+        {
+            lc = locCode;
+        }
+
+        addInventoryCallout(inventoryPath, priority, lc, dataIface);
+    }
+
+    if (callout)
+    {
+        createCalloutsObject();
+        _callouts->addCallout(std::move(callout));
+    }
+}
+
+CalloutPriority SRC::getPriorityFromJSON(const nlohmann::json& json)
+{
+    // Looks like:
+    // {
+    //     "Priority": "H"
+    // }
+    auto p = json.at("Priority").get<std::string>();
+    if (p.empty())
+    {
+        throw std::runtime_error{"Priority field in callout is empty"};
+    }
+
+    auto priority = static_cast<CalloutPriority>(p.front());
+
+    // Validate it
+    auto priorityIt = pv::findByValue(static_cast<uint32_t>(priority),
+                                      pv::calloutPriorityValues);
+    if (priorityIt == pv::calloutPriorityValues.end())
+    {
+        throw std::runtime_error{
+            fmt::format("Invalid priority '{}' found in JSON callout", p)};
+    }
+
+    return priority;
 }
 
 } // namespace pels
diff --git a/extensions/openpower-pels/src.hpp b/extensions/openpower-pels/src.hpp
index 063701c..0ec01f6 100644
--- a/extensions/openpower-pels/src.hpp
+++ b/extensions/openpower-pels/src.hpp
@@ -450,6 +450,17 @@
      */
     void addJSONCallout(const nlohmann::json& jsonCallout,
                         const DataInterfaceBase& dataIface);
+
+    /**
+     * @brief Extracts a CalloutPriority value from the json
+     *        using the 'Priority' key.
+     *
+     * @param[in] json - A JSON object that contains the priority key
+     *
+     * @return CalloutPriority - The priority value
+     */
+    CalloutPriority getPriorityFromJSON(const nlohmann::json& json);
+
     /**
      * @brief The SRC version field
      */
diff --git a/test/openpower-pels/src_test.cpp b/test/openpower-pels/src_test.cpp
index c4c9d40..c4b80a4 100644
--- a/test/openpower-pels/src_test.cpp
+++ b/test/openpower-pels/src_test.cpp
@@ -28,6 +28,7 @@
 using ::testing::Return;
 using ::testing::ReturnRef;
 using ::testing::SetArgReferee;
+using ::testing::Throw;
 namespace fs = std::filesystem;
 
 const auto testRegistry = R"(
@@ -874,3 +875,277 @@
 
     fs::remove_all(dataPath);
 }
+
+// Test when callouts are passed in via JSON
+TEST_F(SRCTest, JsonCalloutsTest)
+{
+    const auto jsonCallouts = R"(
+        [
+            {
+                "LocationCode": "P0-C1",
+                "Priority": "H",
+                "MRUs": [
+                    {
+                        "ID": 42,
+                        "Priority": "H"
+                    },
+                    {
+                        "ID": 43,
+                        "Priority": "M"
+                    }
+                ]
+            },
+            {
+                "InventoryPath": "/inv/system/chassis/motherboard/cpu0",
+                "Priority": "M",
+                "Guarded": true,
+                "Deconfigured": true
+            },
+            {
+                "Procedure": "PROCEDU",
+                "Priority": "A"
+            },
+            {
+                "SymbolicFRU": "TRUSTED",
+                "Priority": "B",
+                "TrustedLocationCode": true,
+                "LocationCode": "P1-C23"
+            },
+            {
+                "SymbolicFRU": "FRUTST1",
+                "Priority": "C",
+                "LocationCode": "P1-C24"
+            },
+            {
+                "SymbolicFRU": "FRUTST2LONG",
+                "Priority": "L"
+            }
+        ]
+    )"_json;
+
+    message::Entry entry;
+    entry.src.type = 0xBD;
+    entry.src.reasonCode = 0xABCD;
+    entry.subsystem = 0x42;
+    entry.src.powerFault = false;
+
+    AdditionalData ad;
+    NiceMock<MockDataInterface> dataIface;
+
+    // Callout 0 mock calls
+    {
+        EXPECT_CALL(dataIface, expandLocationCode("P0-C1", 0))
+            .Times(1)
+            .WillOnce(Return("UXXX-P0-C1"));
+        EXPECT_CALL(dataIface, getInventoryFromLocCode("P0-C1", 0, false))
+            .Times(1)
+            .WillOnce(Return("/inv/system/chassis/motherboard/bmc"));
+        EXPECT_CALL(
+            dataIface,
+            getHWCalloutFields("/inv/system/chassis/motherboard/bmc", _, _, _))
+            .Times(1)
+            .WillOnce(DoAll(SetArgReferee<1>("1234567"),
+                            SetArgReferee<2>("CCCC"),
+                            SetArgReferee<3>("123456789ABC")));
+    }
+    // Callout 1 mock calls
+    {
+        EXPECT_CALL(dataIface,
+                    getLocationCode("/inv/system/chassis/motherboard/cpu0"))
+            .WillOnce(Return("UYYY-P5"));
+        EXPECT_CALL(
+            dataIface,
+            getHWCalloutFields("/inv/system/chassis/motherboard/cpu0", _, _, _))
+            .Times(1)
+            .WillOnce(DoAll(SetArgReferee<1>("2345678"),
+                            SetArgReferee<2>("DDDD"),
+                            SetArgReferee<3>("23456789ABCD")));
+    }
+    // Callout 3 mock calls
+    {
+        EXPECT_CALL(dataIface, expandLocationCode("P1-C23", 0))
+            .Times(1)
+            .WillOnce(Return("UXXX-P1-C23"));
+    }
+    // Callout 4 mock calls
+    {
+        EXPECT_CALL(dataIface, expandLocationCode("P1-C24", 0))
+            .Times(1)
+            .WillOnce(Return("UXXX-P1-C24"));
+    }
+
+    SRC src{entry, ad, jsonCallouts, dataIface};
+    ASSERT_TRUE(src.callouts());
+
+    const auto& callouts = src.callouts()->callouts();
+    ASSERT_EQ(callouts.size(), 6);
+
+    // Check callout 0
+    {
+        EXPECT_EQ(callouts[0]->priority(), 'H');
+        EXPECT_EQ(callouts[0]->locationCode(), "UXXX-P0-C1");
+
+        auto& fru = callouts[0]->fruIdentity();
+        EXPECT_EQ(fru->getPN().value(), "1234567");
+        EXPECT_EQ(fru->getCCIN().value(), "CCCC");
+        EXPECT_EQ(fru->getSN().value(), "123456789ABC");
+        EXPECT_EQ(fru->failingComponentType(), src::FRUIdentity::hardwareFRU);
+    }
+
+    // Check callout 1
+    {
+        EXPECT_EQ(callouts[1]->priority(), 'M');
+        EXPECT_EQ(callouts[1]->locationCode(), "UYYY-P5");
+
+        auto& fru = callouts[1]->fruIdentity();
+        EXPECT_EQ(fru->getPN().value(), "2345678");
+        EXPECT_EQ(fru->getCCIN().value(), "DDDD");
+        EXPECT_EQ(fru->getSN().value(), "23456789ABCD");
+        EXPECT_EQ(fru->failingComponentType(), src::FRUIdentity::hardwareFRU);
+    }
+
+    // Check callout 2
+    {
+        EXPECT_EQ(callouts[2]->priority(), 'A');
+        EXPECT_EQ(callouts[2]->locationCode(), "");
+
+        auto& fru = callouts[2]->fruIdentity();
+        EXPECT_EQ(fru->getMaintProc().value(), "PROCEDU");
+        EXPECT_EQ(fru->failingComponentType(),
+                  src::FRUIdentity::maintenanceProc);
+    }
+
+    // Check callout 3
+    {
+        EXPECT_EQ(callouts[3]->priority(), 'B');
+        EXPECT_EQ(callouts[3]->locationCode(), "UXXX-P1-C23");
+
+        auto& fru = callouts[3]->fruIdentity();
+        EXPECT_EQ(fru->getPN().value(), "TRUSTED");
+        EXPECT_EQ(fru->failingComponentType(),
+                  src::FRUIdentity::symbolicFRUTrustedLocCode);
+    }
+
+    // Check callout 4
+    {
+        EXPECT_EQ(callouts[4]->priority(), 'C');
+        EXPECT_EQ(callouts[4]->locationCode(), "UXXX-P1-C24");
+
+        auto& fru = callouts[4]->fruIdentity();
+        EXPECT_EQ(fru->getPN().value(), "FRUTST1");
+        EXPECT_EQ(fru->failingComponentType(), src::FRUIdentity::symbolicFRU);
+    }
+
+    // Check callout 5
+    {
+        EXPECT_EQ(callouts[5]->priority(), 'L');
+        EXPECT_EQ(callouts[5]->locationCode(), "");
+
+        auto& fru = callouts[5]->fruIdentity();
+        EXPECT_EQ(fru->getPN().value(), "FRUTST2");
+        EXPECT_EQ(fru->failingComponentType(), src::FRUIdentity::symbolicFRU);
+    }
+
+    // Check that it didn't find any errors
+    const auto& data = src.getDebugData();
+    EXPECT_TRUE(data.empty());
+}
+
+TEST_F(SRCTest, JsonBadCalloutsTest)
+{
+    // The first call will have a Throw in a mock call.
+    // The second will have a different Throw in a mock call.
+    // The others have issues with the Priority field.
+    const auto jsonCallouts = R"(
+        [
+            {
+                "LocationCode": "P0-C1",
+                "Priority": "H"
+            },
+            {
+                "LocationCode": "P0-C2",
+                "Priority": "H"
+            },
+            {
+                "LocationCode": "P0-C3"
+            },
+            {
+                "LocationCode": "P0-C4",
+                "Priority": "X"
+            }
+        ]
+    )"_json;
+
+    message::Entry entry;
+    entry.src.type = 0xBD;
+    entry.src.reasonCode = 0xABCD;
+    entry.subsystem = 0x42;
+    entry.src.powerFault = false;
+
+    AdditionalData ad;
+    NiceMock<MockDataInterface> dataIface;
+
+    // Callout 0 mock calls
+    // Expand location code will fail, so the unexpanded location
+    // code should show up in the callout instead.
+    {
+        EXPECT_CALL(dataIface, expandLocationCode("P0-C1", 0))
+            .WillOnce(Throw(std::runtime_error("Fail")));
+
+        EXPECT_CALL(dataIface, getInventoryFromLocCode("P0-C1", 0, false))
+            .Times(1)
+            .WillOnce(Return("/inv/system/chassis/motherboard/bmc"));
+        EXPECT_CALL(
+            dataIface,
+            getHWCalloutFields("/inv/system/chassis/motherboard/bmc", _, _, _))
+            .Times(1)
+            .WillOnce(DoAll(SetArgReferee<1>("1234567"),
+                            SetArgReferee<2>("CCCC"),
+                            SetArgReferee<3>("123456789ABC")));
+    }
+
+    // Callout 1 mock calls
+    // getInventoryFromLocCode will fail
+    {
+        EXPECT_CALL(dataIface, expandLocationCode("P0-C2", 0))
+            .Times(1)
+            .WillOnce(Return("UXXX-P0-C2"));
+
+        EXPECT_CALL(dataIface, getInventoryFromLocCode("P0-C2", 0, false))
+            .Times(1)
+            .WillOnce(Throw(std::runtime_error("Fail")));
+    }
+
+    SRC src{entry, ad, jsonCallouts, dataIface};
+
+    ASSERT_TRUE(src.callouts());
+
+    const auto& callouts = src.callouts()->callouts();
+
+    // Only the first callout was successful
+    ASSERT_EQ(callouts.size(), 1);
+
+    {
+        EXPECT_EQ(callouts[0]->priority(), 'H');
+        EXPECT_EQ(callouts[0]->locationCode(), "P0-C1");
+
+        auto& fru = callouts[0]->fruIdentity();
+        EXPECT_EQ(fru->getPN().value(), "1234567");
+        EXPECT_EQ(fru->getCCIN().value(), "CCCC");
+        EXPECT_EQ(fru->getSN().value(), "123456789ABC");
+        EXPECT_EQ(fru->failingComponentType(), src::FRUIdentity::hardwareFRU);
+    }
+
+    const auto& data = src.getDebugData();
+    ASSERT_EQ(data.size(), 4);
+    EXPECT_STREQ(data[0].c_str(), "Unable to expand location code P0-C1: Fail");
+    EXPECT_STREQ(data[1].c_str(),
+                 "Failed extracting callout data from JSON: Unable to "
+                 "get inventory path from location code: P0-C2: Fail");
+    EXPECT_STREQ(data[2].c_str(),
+                 "Failed extracting callout data from JSON: "
+                 "[json.exception.out_of_range.403] key 'Priority' not found");
+    EXPECT_STREQ(data[3].c_str(),
+                 "Failed extracting callout data from JSON: Invalid "
+                 "priority 'X' found in JSON callout");
+}