PEL: Create SRC FRU callout for inventory path

When a BMC application creates an event log with the
CALLOUT_INVENTORY_PATH=<FRU inventory path> entry in the AdditionalData
property, create a FRU callout section for that FRU in the SRC.

Obtain the location code, part number, serial number, and CCIN VPD
keyword for that FRU to add to the callout.  If these aren't available,
as detected by catching an sdbusplus exception, create a callout with a
'no VPD for FRU' maintenance procedure instead.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I0d6680d87c325703df9e2a6d2e2715b18498fabc
diff --git a/extensions/openpower-pels/src.cpp b/extensions/openpower-pels/src.cpp
index 8d2fd0b..b15c777 100644
--- a/extensions/openpower-pels/src.cpp
+++ b/extensions/openpower-pels/src.cpp
@@ -120,7 +120,7 @@
 
     _asciiString = std::make_unique<src::AsciiString>(regEntry);
 
-    // TODO: add callouts using the Callouts object
+    addCallouts(additionalData, dataIface);
 
     _size = baseSRCSize;
     _size += _callouts ? _callouts->flattenedSize() : 0;
@@ -502,5 +502,49 @@
     return ps;
 }
 
+void SRC::addCallouts(const AdditionalData& additionalData,
+                      const DataInterfaceBase& dataIface)
+{
+    auto item = additionalData.getValue("CALLOUT_INVENTORY_PATH");
+    if (item)
+    {
+        addInventoryCallout(*item, dataIface);
+    }
+
+    // TODO: CALLOUT_DEVICE_PATH
+}
+
+void SRC::addInventoryCallout(const std::string& inventoryPath,
+                              const DataInterfaceBase& dataIface)
+{
+    std::string locCode;
+    std::string fn;
+    std::string ccin;
+    std::string sn;
+    std::unique_ptr<src::Callout> callout;
+
+    createCalloutsObject();
+
+    try
+    {
+        dataIface.getHWCalloutFields(inventoryPath, locCode, fn, ccin, sn);
+
+        callout = std::make_unique<src::Callout>(CalloutPriority::high, locCode,
+                                                 fn, ccin, sn);
+    }
+    catch (const SdBusError& e)
+    {
+        log<level::INFO>("No VPD found for FRU callout",
+                         entry("PATH=%s", inventoryPath.c_str()));
+
+        // Use the 'NoVPDforFRU' maintenance procedure instead
+        callout = std::make_unique<src::Callout>(CalloutPriority::high,
+                                                 MaintProcedure::noVPDforFRU);
+    }
+
+    _callouts->addCallout(std::move(callout));
+
+} // namespace pels
+
 } // namespace pels
 } // namespace openpower
diff --git a/extensions/openpower-pels/src.hpp b/extensions/openpower-pels/src.hpp
index 51ded24..a9fecf8 100644
--- a/extensions/openpower-pels/src.hpp
+++ b/extensions/openpower-pels/src.hpp
@@ -339,6 +339,41 @@
     std::optional<std::string> getCallouts() const;
 
     /**
+     * @brief Checks the AdditionalData property and the message registry
+     *        JSON and adds any necessary callouts.
+     *
+     * The callout sources are the AdditionalData event log property
+     * and the message registry JSON.
+     *
+     * @param[in] additionalData - the AdditionalData values
+     * @param[in] dataIface - The DataInterface object
+     */
+    void addCallouts(const AdditionalData& additionalData,
+                     const DataInterfaceBase& dataIface);
+
+    /**
+     * @brief Adds a FRU callout based on an inventory path
+     *
+     * @param[in] inventoryPath - The inventory item to call out
+     * @param[in] dataIface - The DataInterface object
+     */
+    void addInventoryCallout(const std::string& inventoryPath,
+                             const DataInterfaceBase& dataIface);
+
+    /**
+     * @brief Creates the Callouts object _callouts
+     *        so that callouts can be added to it.
+     */
+    void createCalloutsObject()
+    {
+        if (!_callouts)
+        {
+            _callouts = std::make_unique<src::Callouts>();
+            _flags |= additionalSections;
+        }
+    }
+
+    /**
      * @brief The SRC version field
      */
     uint8_t _version;
diff --git a/test/openpower-pels/src_test.cpp b/test/openpower-pels/src_test.cpp
index 383c7e1..c982ca5 100644
--- a/test/openpower-pels/src_test.cpp
+++ b/test/openpower-pels/src_test.cpp
@@ -22,8 +22,11 @@
 #include <gtest/gtest.h>
 
 using namespace openpower::pels;
+using ::testing::_;
+using ::testing::InvokeWithoutArgs;
 using ::testing::NiceMock;
 using ::testing::Return;
+using ::testing::SetArgReferee;
 namespace fs = std::filesystem;
 
 const auto testRegistry = R"(
@@ -311,3 +314,100 @@
         errorDetails.value(),
         "Comp 0x1 failed 0x4 times over 0x1E secs with ErrorCode 0x1ABCDEF");
 }
+
+// Test that an inventory path callout string is
+// converted into the appropriate FRU callout.
+TEST_F(SRCTest, InventoryCalloutTest)
+{
+    message::Entry entry;
+    entry.src.type = 0xBD;
+    entry.src.reasonCode = 0xABCD;
+    entry.subsystem = 0x42;
+    entry.src.powerFault = false;
+
+    std::vector<std::string> adData{"CALLOUT_INVENTORY_PATH=motherboard"};
+    AdditionalData ad{adData};
+    NiceMock<MockDataInterface> dataIface;
+
+    EXPECT_CALL(dataIface, getHWCalloutFields("motherboard", _, _, _, _))
+        .Times(1)
+        .WillOnce(DoAll(SetArgReferee<1>("UTMS-P1"),
+                        SetArgReferee<2>("1234567"), SetArgReferee<3>("CCCC"),
+                        SetArgReferee<4>("123456789ABC")));
+
+    SRC src{entry, ad, dataIface};
+    EXPECT_TRUE(src.valid());
+
+    ASSERT_TRUE(src.callouts());
+
+    EXPECT_EQ(src.callouts()->callouts().size(), 1);
+
+    auto& callout = src.callouts()->callouts().front();
+
+    EXPECT_EQ(callout->locationCode(), "UTMS-P1");
+
+    auto& fru = callout->fruIdentity();
+
+    EXPECT_EQ(fru->getPN().value(), "1234567");
+    EXPECT_EQ(fru->getCCIN().value(), "CCCC");
+    EXPECT_EQ(fru->getSN().value(), "123456789ABC");
+
+    // flatten and unflatten
+    std::vector<uint8_t> data;
+    Stream stream{data};
+    src.flatten(stream);
+
+    stream.offset(0);
+    SRC newSRC{stream};
+    EXPECT_TRUE(newSRC.valid());
+    ASSERT_TRUE(src.callouts());
+    EXPECT_EQ(src.callouts()->callouts().size(), 1);
+}
+
+// Test that when the FRU fields can't be obtained that
+// a procedure callout is used.
+TEST_F(SRCTest, InventoryCalloutNoVPDTest)
+{
+    message::Entry entry;
+    entry.src.type = 0xBD;
+    entry.src.reasonCode = 0xABCD;
+    entry.subsystem = 0x42;
+    entry.src.powerFault = false;
+
+    std::vector<std::string> adData{"CALLOUT_INVENTORY_PATH=motherboard"};
+    AdditionalData ad{adData};
+    NiceMock<MockDataInterface> dataIface;
+
+    auto func = []() { throw sdbusplus::exception::SdBusError(5, "Error"); };
+
+    EXPECT_CALL(dataIface, getHWCalloutFields("motherboard", _, _, _, _))
+        .Times(1)
+        .WillOnce(InvokeWithoutArgs(func));
+
+    SRC src{entry, ad, dataIface};
+    EXPECT_TRUE(src.valid());
+
+    ASSERT_TRUE(src.callouts());
+
+    EXPECT_EQ(src.callouts()->callouts().size(), 1);
+
+    auto& callout = src.callouts()->callouts().front();
+
+    auto& fru = callout->fruIdentity();
+
+    EXPECT_EQ(fru->getMaintProc().value(), "BMCSP01");
+    EXPECT_FALSE(fru->getPN());
+    EXPECT_FALSE(fru->getSN());
+    EXPECT_FALSE(fru->getCCIN());
+    //
+    // flatten and unflatten
+    std::vector<uint8_t> data;
+    Stream stream{data};
+    src.flatten(stream);
+
+    stream.offset(0);
+    SRC newSRC{stream};
+    EXPECT_TRUE(newSRC.valid());
+    ASSERT_TRUE(src.callouts());
+    EXPECT_EQ(src.callouts()->callouts().size(), 1);
+}