PEL: Add device path callouts in SRC section

When the SRC section is created and it has either CALLOUT_DEVICE_PATH or
(CALLOUT_IIC_BUS, CALLOUT_IIC_ADDR) in the AdditionalData property, look
up the device callouts to add to the section using
device_callouts::getCallouts().

CALLOUT_IIC_BUS can either be the plain bus number, like "5", or a /dev
path like "/dev/i2c-5".  CALLOUT_IIC_ADDR is a 7 bit I2C address and can
either be in decimal, like "16", or in hex like "0x10".

For now, it just stores any MRUs that come back in the section debug
data that will be added to a UserData section.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I33ae6a3dbc75e2892b29f37cfac414ca9747a2f9
diff --git a/extensions/openpower-pels/src.cpp b/extensions/openpower-pels/src.cpp
index eb103bb..cdafce3 100644
--- a/extensions/openpower-pels/src.cpp
+++ b/extensions/openpower-pels/src.cpp
@@ -15,6 +15,7 @@
  */
 #include "src.hpp"
 
+#include "device_callouts.hpp"
 #include "json_utils.hpp"
 #include "paths.hpp"
 #include "pel_values.hpp"
@@ -513,7 +514,7 @@
         addInventoryCallout(*item, std::nullopt, std::nullopt, dataIface);
     }
 
-    // TODO: CALLOUT_DEVICE_PATH
+    addDevicePathCallouts(additionalData, dataIface);
 
     if (regEntry.callouts)
     {
@@ -684,5 +685,126 @@
     }
 }
 
+void SRC::addDevicePathCallouts(const AdditionalData& additionalData,
+                                const DataInterfaceBase& dataIface)
+{
+    std::vector<device_callouts::Callout> callouts;
+    auto i2cBus = additionalData.getValue("CALLOUT_IIC_BUS");
+    auto i2cAddr = additionalData.getValue("CALLOUT_IIC_ADDR");
+    auto devPath = additionalData.getValue("CALLOUT_DEVICE_PATH");
+
+    // A device callout contains either:
+    // * CALLOUT_ERRNO, CALLOUT_DEVICE_PATH
+    // * CALLOUT_ERRNO, CALLOUT_IIC_BUS, CALLOUT_IIC_ADDR
+    // We don't care about the errno.
+
+    if (devPath)
+    {
+        try
+        {
+            callouts = device_callouts::getCallouts(*devPath,
+                                                    dataIface.getSystemNames());
+        }
+        catch (const std::exception& e)
+        {
+            addDebugData(e.what());
+            callouts.clear();
+        }
+    }
+    else if (i2cBus && i2cAddr)
+    {
+        size_t bus;
+        uint8_t address;
+
+        try
+        {
+            // If /dev/i2c- is prepended, remove it
+            if (i2cBus->find("/dev/i2c-") != std::string::npos)
+            {
+                *i2cBus = i2cBus->substr(9);
+            }
+
+            bus = stoul(*i2cBus, nullptr, 0);
+            address = stoul(*i2cAddr, nullptr, 0);
+        }
+        catch (const std::exception& e)
+        {
+            std::string msg = "Invalid CALLOUT_IIC_BUS " + *i2cBus +
+                              " or CALLOUT_IIC_ADDR " + *i2cAddr +
+                              " in AdditionalData property";
+            addDebugData(msg);
+            return;
+        }
+
+        try
+        {
+            callouts = device_callouts::getI2CCallouts(
+                bus, address, dataIface.getSystemNames());
+        }
+        catch (const std::exception& e)
+        {
+            addDebugData(e.what());
+            callouts.clear();
+        }
+    }
+
+    for (const auto& callout : callouts)
+    {
+        // The priority shouldn't be invalid, but check just in case.
+        CalloutPriority priority = CalloutPriority::high;
+
+        if (!callout.priority.empty())
+        {
+            auto p = pel_values::findByValue(
+                static_cast<uint32_t>(callout.priority[0]),
+                pel_values::calloutPriorityValues);
+
+            if (p != pel_values::calloutPriorityValues.end())
+            {
+                priority = static_cast<CalloutPriority>(callout.priority[0]);
+            }
+            else
+            {
+                std::string msg =
+                    "Invalid priority found in dev callout JSON: " +
+                    callout.priority[0];
+                addDebugData(msg);
+            }
+        }
+
+        try
+        {
+            auto inventoryPath =
+                dataIface.getInventoryFromLocCode(callout.locationCode, 0);
+
+            addInventoryCallout(inventoryPath, priority, std::nullopt,
+                                dataIface);
+        }
+        catch (const std::exception& e)
+        {
+            std::string msg =
+                "Unable to get inventory path from location code: " +
+                callout.locationCode + ": " + e.what();
+            addDebugData(msg);
+        }
+
+        // Until the code is there to convert these MRU value strings to
+        // the official MRU values in the callout objects, just store
+        // the MRU name in the debug UserData section.
+        if (!callout.mru.empty())
+        {
+            std::string msg = "MRU: " + callout.mru;
+            addDebugData(msg);
+        }
+
+        // getCallouts() may have generated some debug data it stored
+        // in a callout object.  Save it as well.
+        if (!callout.debug.empty())
+        {
+            addDebugData(callout.debug);
+        }
+    }
+}
+
 } // namespace pels
 } // namespace openpower
diff --git a/extensions/openpower-pels/src.hpp b/extensions/openpower-pels/src.hpp
index cc9b1c7..b9c8199 100644
--- a/extensions/openpower-pels/src.hpp
+++ b/extensions/openpower-pels/src.hpp
@@ -400,6 +400,16 @@
     }
 
     /**
+     * @brief Adds any FRU callouts based on a device path in the
+     *        AdditionalData parameter.
+     *
+     * @param[in] additionalData - The AdditionalData values
+     * @param[in] dataIface - The DataInterface object
+     */
+    void addDevicePathCallouts(const AdditionalData& additionalData,
+                               const DataInterfaceBase& dataIface);
+
+    /**
      * @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 5c5c868..cac653a 100644
--- a/test/openpower-pels/src_test.cpp
+++ b/test/openpower-pels/src_test.cpp
@@ -348,6 +348,7 @@
     auto& callout = src.callouts()->callouts().front();
 
     EXPECT_EQ(callout->locationCode(), "UTMS-P1");
+    EXPECT_EQ(callout->priority(), 'H');
 
     auto& fru = callout->fruIdentity();
 
@@ -401,6 +402,7 @@
 
     auto& callout = src.callouts()->callouts().front();
     EXPECT_EQ(callout->locationCodeSize(), 0);
+    EXPECT_EQ(callout->priority(), 'H');
 
     auto& fru = callout->fruIdentity();
 
@@ -452,6 +454,7 @@
 
     auto& callout = src.callouts()->callouts().front();
     EXPECT_EQ(callout->locationCode(), "UTMS-P10");
+    EXPECT_EQ(callout->priority(), 'H');
 
     auto& fru = callout->fruIdentity();
 
@@ -665,3 +668,209 @@
         EXPECT_EQ(fru2->getSN().value(), "23456789ABCD");
     }
 }
+
+// Test looking up device path fails in the callout jSON.
+TEST_F(SRCTest, DevicePathCalloutTest)
+{
+    message::Entry entry;
+    entry.src.type = 0xBD;
+    entry.src.reasonCode = 0xABCD;
+    entry.subsystem = 0x42;
+    entry.src.powerFault = false;
+
+    const auto calloutJSON = R"(
+    {
+        "I2C":
+        {
+            "14":
+            {
+                "114":
+                {
+                    "Callouts":[
+                    {
+                        "Name": "/chassis/motherboard/cpu0",
+                        "LocationCode": "P1-C40",
+                        "Priority": "H"
+                    },
+                    {
+                        "Name": "/chassis/motherboard",
+                        "LocationCode": "P1",
+                        "Priority": "M"
+                    },
+                    {
+                        "Name": "/chassis/motherboard/bmc",
+                        "LocationCode": "P1-C15",
+                        "Priority": "L"
+                    }
+                    ],
+                    "Dest": "proc 0 target"
+                }
+            }
+        }
+    })";
+
+    auto dataPath = getPELReadOnlyDataPath();
+    std::ofstream file{dataPath / "systemA_dev_callouts.json"};
+    file << calloutJSON;
+    file.close();
+
+    NiceMock<MockDataInterface> dataIface;
+    std::vector<std::string> names{"systemA"};
+
+    EXPECT_CALL(dataIface, getSystemNames)
+        .Times(5)
+        .WillRepeatedly(ReturnRef(names));
+
+    EXPECT_CALL(dataIface, getInventoryFromLocCode("P1-C40", 0))
+        .Times(3)
+        .WillRepeatedly(
+            Return("/xyz/openbmc_project/inventory/chassis/motherboard/cpu0"));
+
+    EXPECT_CALL(dataIface, getInventoryFromLocCode("P1", 0))
+        .Times(3)
+        .WillRepeatedly(
+            Return("/xyz/openbmc_project/inventory/chassis/motherboard"));
+
+    EXPECT_CALL(dataIface, getInventoryFromLocCode("P1-C15", 0))
+        .Times(3)
+        .WillRepeatedly(
+            Return("/xyz/openbmc_project/inventory/chassis/motherboard/bmc"));
+
+    EXPECT_CALL(dataIface,
+                getLocationCode(
+                    "/xyz/openbmc_project/inventory/chassis/motherboard/cpu0"))
+        .Times(3)
+        .WillRepeatedly(Return("Ufcs-P1-C40"));
+    EXPECT_CALL(
+        dataIface,
+        getLocationCode("/xyz/openbmc_project/inventory/chassis/motherboard"))
+        .Times(3)
+        .WillRepeatedly(Return("Ufcs-P1"));
+    EXPECT_CALL(dataIface,
+                getLocationCode(
+                    "/xyz/openbmc_project/inventory/chassis/motherboard/bmc"))
+        .Times(3)
+        .WillRepeatedly(Return("Ufcs-P1-C15"));
+
+    EXPECT_CALL(
+        dataIface,
+        getHWCalloutFields(
+            "/xyz/openbmc_project/inventory/chassis/motherboard/cpu0", _, _, _))
+        .Times(3)
+        .WillRepeatedly(DoAll(SetArgReferee<1>("1234567"),
+                              SetArgReferee<2>("CCCC"),
+                              SetArgReferee<3>("123456789ABC")));
+    EXPECT_CALL(
+        dataIface,
+        getHWCalloutFields("/xyz/openbmc_project/inventory/chassis/motherboard",
+                           _, _, _))
+        .Times(3)
+        .WillRepeatedly(DoAll(SetArgReferee<1>("7654321"),
+                              SetArgReferee<2>("MMMM"),
+                              SetArgReferee<3>("CBA987654321")));
+    EXPECT_CALL(
+        dataIface,
+        getHWCalloutFields(
+            "/xyz/openbmc_project/inventory/chassis/motherboard/bmc", _, _, _))
+        .Times(3)
+        .WillRepeatedly(DoAll(SetArgReferee<1>("7123456"),
+                              SetArgReferee<2>("BBBB"),
+                              SetArgReferee<3>("C123456789AB")));
+
+    // Call this below with different AdditionalData values that
+    // result in the same callouts.
+    auto checkCallouts = [&entry, &dataIface](const auto& items) {
+        AdditionalData ad{items};
+        SRC src{entry, ad, dataIface};
+
+        ASSERT_TRUE(src.callouts());
+        auto& callouts = src.callouts()->callouts();
+
+        ASSERT_EQ(callouts.size(), 3);
+
+        {
+            EXPECT_EQ(callouts[0]->priority(), 'H');
+            EXPECT_EQ(callouts[0]->locationCode(), "Ufcs-P1-C40");
+
+            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(callouts[1]->priority(), 'M');
+            EXPECT_EQ(callouts[1]->locationCode(), "Ufcs-P1");
+
+            auto& fru = callouts[1]->fruIdentity();
+            EXPECT_EQ(fru->getPN().value(), "7654321");
+            EXPECT_EQ(fru->getCCIN().value(), "MMMM");
+            EXPECT_EQ(fru->getSN().value(), "CBA987654321");
+        }
+        {
+            EXPECT_EQ(callouts[2]->priority(), 'L');
+            EXPECT_EQ(callouts[2]->locationCode(), "Ufcs-P1-C15");
+
+            auto& fru = callouts[2]->fruIdentity();
+            EXPECT_EQ(fru->getPN().value(), "7123456");
+            EXPECT_EQ(fru->getCCIN().value(), "BBBB");
+            EXPECT_EQ(fru->getSN().value(), "C123456789AB");
+        }
+    };
+
+    {
+        // Callouts based on the device path
+        std::vector<std::string> items{
+            "CALLOUT_ERRNO=5",
+            "CALLOUT_DEVICE_PATH=/sys/devices/platform/ahb/ahb:apb/"
+            "ahb:apb:bus@1e78a000/1e78a340.i2c-bus/i2c-14/14-0072"};
+
+        checkCallouts(items);
+    }
+
+    {
+        // Callouts based on the I2C bus and address
+        std::vector<std::string> items{"CALLOUT_ERRNO=5", "CALLOUT_IIC_BUS=14",
+                                       "CALLOUT_IIC_ADDR=0x72"};
+        checkCallouts(items);
+    }
+
+    {
+        // Also based on I2C bus and address, but with bus = /dev/i2c-14
+        std::vector<std::string> items{"CALLOUT_ERRNO=5", "CALLOUT_IIC_BUS=14",
+                                       "CALLOUT_IIC_ADDR=0x72"};
+        checkCallouts(items);
+    }
+
+    {
+        // Callout not found
+        std::vector<std::string> items{
+            "CALLOUT_ERRNO=5",
+            "CALLOUT_DEVICE_PATH=/sys/devices/platform/ahb/ahb:apb/"
+            "ahb:apb:bus@1e78a000/1e78a340.i2c-bus/i2c-24/24-0012"};
+
+        AdditionalData ad{items};
+        SRC src{entry, ad, dataIface};
+
+        EXPECT_FALSE(src.callouts());
+        ASSERT_EQ(src.getDebugData().size(), 1);
+        EXPECT_EQ(src.getDebugData()[0],
+                  "Problem looking up I2C callouts on 24 18: "
+                  "[json.exception.out_of_range.403] key '24' not found");
+    }
+
+    {
+        // Callout not found
+        std::vector<std::string> items{"CALLOUT_ERRNO=5", "CALLOUT_IIC_BUS=22",
+                                       "CALLOUT_IIC_ADDR=0x99"};
+        AdditionalData ad{items};
+        SRC src{entry, ad, dataIface};
+
+        EXPECT_FALSE(src.callouts());
+        ASSERT_EQ(src.getDebugData().size(), 1);
+        EXPECT_EQ(src.getDebugData()[0],
+                  "Problem looking up I2C callouts on 22 153: "
+                  "[json.exception.out_of_range.403] key '22' not found");
+    }
+
+    fs::remove_all(dataPath);
+}