PEL: devcallouts: Get callouts from the JSON

Using the functions that extract search keys from the device path, look
up the callouts in the appriopriate JSON entry and return them as a
vector of Callout objects.

The first callout will have the 'debug' field filled in with the search
keys used and the MRW target name of the destination device.  In the
future this will be added into a UserData section of a PEL with these
callouts to make debugging easier - i.e. making it easier to know what
device it was that failed when looking at the PEL.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: Ic4df1ce95fbc5ed277b43a1ffca08ce477d5f8c6
diff --git a/extensions/openpower-pels/device_callouts.cpp b/extensions/openpower-pels/device_callouts.cpp
index b343424..7e89228 100644
--- a/extensions/openpower-pels/device_callouts.cpp
+++ b/extensions/openpower-pels/device_callouts.cpp
@@ -194,14 +194,255 @@
     return {std::move(links), std::move(bus)};
 }
 
+/**
+ * @brief Pull the callouts out of the JSON callout array passed in
+ *
+ * Create a vector of Callout objects based on the JSON.
+ *
+ * This will also fill in the 'debug' member on the first callout
+ * in the list, which could contain things like the I2C address and
+ * bus extracted from the device path.
+ *
+ * The callouts are in the order they should be added to the PEL.
+ *
+ * @param[in] calloutJSON - The Callouts JSON array to extract from
+ * @param[in] debug - The debug message to add to the first callout
+ *
+ * @return std::vector<Callout> - The Callout objects
+ */
+std::vector<Callout> extractCallouts(const nlohmann::json& calloutJSON,
+                                     const std::string& debug)
+{
+    std::vector<Callout> callouts;
+    bool addDebug = true;
+
+    // The JSON element passed in is the array of callouts
+    if (!calloutJSON.is_array())
+    {
+        throw std::runtime_error(
+            "Dev path callout JSON entry doesn't contain a 'Callouts' array");
+    }
+
+    for (auto& callout : calloutJSON)
+    {
+        Callout c;
+
+        // Add any debug data to the first callout
+        if (addDebug && !debug.empty())
+        {
+            addDebug = false;
+            c.debug = debug;
+        }
+
+        try
+        {
+            c.locationCode = callout.at("LocationCode").get<std::string>();
+            c.name = callout.at("Name").get<std::string>();
+            c.priority = callout.at("Priority").get<std::string>();
+
+            if (callout.contains("MRU"))
+            {
+                c.mru = callout.at("MRU").get<std::string>();
+            }
+        }
+        catch (const nlohmann::json::out_of_range& e)
+        {
+            std::string msg =
+                "Callout entry missing either LocationCode, Name, or Priority "
+                "properties: " +
+                callout.dump();
+            throw std::runtime_error(msg.c_str());
+        }
+
+        callouts.push_back(c);
+    }
+
+    return callouts;
+}
+
+/**
+ * @brief Looks up the callouts in the JSON using the I2C keys.
+ *
+ * @param[in] i2cBus - The I2C bus
+ * @param[in] i2cAddress - The I2C address
+ * @param[in] calloutJSON - The JSON containing the callouts
+ *
+ * @return std::vector<Callout> - The callouts
+ */
 std::vector<device_callouts::Callout>
     calloutI2C(size_t i2cBus, uint8_t i2cAddress,
                const nlohmann::json& calloutJSON)
 {
-    // TODO
-    return {};
+    auto busString = std::to_string(i2cBus);
+    auto addrString = std::to_string(i2cAddress);
+
+    try
+    {
+        const auto& callouts =
+            calloutJSON.at("I2C").at(busString).at(addrString).at("Callouts");
+
+        auto dest = calloutJSON.at("I2C")
+                        .at(busString)
+                        .at(addrString)
+                        .at("Dest")
+                        .get<std::string>();
+
+        std::string msg = "I2C: bus: " + busString + " address: " + addrString +
+                          " dest: " + dest;
+
+        return extractCallouts(callouts, msg);
+    }
+    catch (const nlohmann::json::out_of_range& e)
+    {
+        std::string msg = "Problem looking up I2C callouts on " + busString +
+                          " " + addrString + ": " + std::string{e.what()};
+        throw std::invalid_argument(msg.c_str());
+    }
 }
 
+/**
+ * @brief Looks up the callouts in the JSON for this I2C path.
+ *
+ * @param[in] devPath - The device path
+ * @param[in] calloutJSON - The JSON containing the callouts
+ *
+ * @return std::vector<Callout> - The callouts
+ */
+std::vector<device_callouts::Callout>
+    calloutI2CUsingPath(const std::string& devPath,
+                        const nlohmann::json& calloutJSON)
+{
+    auto [bus, address] = getI2CSearchKeys(devPath);
+
+    return calloutI2C(bus, address, calloutJSON);
+}
+
+/**
+ * @brief Looks up the callouts in the JSON for this FSI path.
+ *
+ * @param[in] devPath - The device path
+ * @param[in] calloutJSON - The JSON containing the callouts
+ *
+ * @return std::vector<Callout> - The callouts
+ */
+std::vector<device_callouts::Callout>
+    calloutFSI(const std::string& devPath, const nlohmann::json& calloutJSON)
+{
+    auto links = getFSISearchKeys(devPath);
+
+    try
+    {
+        const auto& callouts = calloutJSON.at("FSI").at(links).at("Callouts");
+
+        auto dest =
+            calloutJSON.at("FSI").at(links).at("Dest").get<std::string>();
+
+        std::string msg = "FSI: links: " + links + " dest: " + dest;
+
+        return extractCallouts(callouts, msg);
+    }
+    catch (const nlohmann::json::out_of_range& e)
+    {
+        std::string msg = "Problem looking up FSI callouts on " + links + ": " +
+                          std::string{e.what()};
+        throw std::invalid_argument(msg.c_str());
+    }
+}
+
+/**
+ * @brief Looks up the callouts in the JSON for this FSI-I2C path.
+ *
+ * @param[in] devPath - The device path
+ * @param[in] calloutJSON - The JSON containing the callouts
+ *
+ * @return std::vector<Callout> - The callouts
+ */
+std::vector<device_callouts::Callout>
+    calloutFSII2C(const std::string& devPath, const nlohmann::json& calloutJSON)
+{
+    auto linksAndI2C = getFSII2CSearchKeys(devPath);
+    auto links = std::get<std::string>(linksAndI2C);
+    const auto& busAndAddr = std::get<1>(linksAndI2C);
+
+    auto busString = std::to_string(std::get<size_t>(busAndAddr));
+    auto addrString = std::to_string(std::get<uint8_t>(busAndAddr));
+
+    try
+    {
+        auto& callouts = calloutJSON.at("FSI-I2C")
+                             .at(links)
+                             .at(busString)
+                             .at(addrString)
+                             .at("Callouts");
+
+        auto dest = calloutJSON.at("FSI-I2C")
+                        .at(links)
+                        .at(busString)
+                        .at(addrString)
+                        .at("Dest")
+                        .get<std::string>();
+
+        std::string msg = "FSI-I2C: links: " + links + " bus: " + busString +
+                          " addr: " + addrString + " dest: " + dest;
+
+        return extractCallouts(callouts, msg);
+    }
+    catch (const nlohmann::json::out_of_range& e)
+    {
+        std::string msg = "Problem looking up FSI-I2C callouts on " + links +
+                          " " + busString + " " + addrString + ": " + e.what();
+        throw std::invalid_argument(msg.c_str());
+    }
+}
+
+/**
+ * @brief Looks up the callouts in the JSON for this FSI-SPI path.
+ *
+ * @param[in] devPath - The device path
+ * @param[in] calloutJSON - The JSON containing the callouts
+ *
+ * @return std::vector<Callout> - The callouts
+ */
+std::vector<device_callouts::Callout>
+    calloutFSISPI(const std::string& devPath, const nlohmann::json& calloutJSON)
+{
+    auto linksAndSPI = getFSISPISearchKeys(devPath);
+    auto links = std::get<std::string>(linksAndSPI);
+    auto busString = std::to_string(std::get<size_t>(linksAndSPI));
+
+    try
+    {
+        auto& callouts =
+            calloutJSON.at("FSI-SPI").at(links).at(busString).at("Callouts");
+
+        auto dest = calloutJSON.at("FSI-SPI")
+                        .at(links)
+                        .at(busString)
+                        .at("Dest")
+                        .get<std::string>();
+
+        std::string msg = "FSI-SPI: links: " + links + " bus: " + busString +
+                          " dest: " + dest;
+
+        return extractCallouts(callouts, msg);
+    }
+    catch (const nlohmann::json::out_of_range& e)
+    {
+        std::string msg = "Problem looking up FSI-SPI callouts on " + links +
+                          " " + busString + ": " + std::string{e.what()};
+        throw std::invalid_argument(msg.c_str());
+    }
+}
+
+/**
+ * @brief Returns the callouts from the JSON based on the input
+ *        device path.
+ *
+ * @param[in] devPath - The device path
+ * @param[in] json - The callout JSON
+ *
+ * @return std::vector<Callout> - The list of callouts
+ */
 std::vector<device_callouts::Callout> findCallouts(const std::string& devPath,
                                                    const nlohmann::json& json)
 {
@@ -222,16 +463,16 @@
     switch (util::getCalloutType(path))
     {
         case util::CalloutType::i2c:
-            // callouts = calloutI2CUsingPath(errnoValue, path, json);
+            callouts = calloutI2CUsingPath(path, json);
             break;
         case util::CalloutType::fsi:
-            // callouts = calloutFSI(errnoValue, path, json);
+            callouts = calloutFSI(path, json);
             break;
         case util::CalloutType::fsii2c:
-            // callouts = calloutFSII2C(errnoValue, path, json);
+            callouts = calloutFSII2C(path, json);
             break;
         case util::CalloutType::fsispi:
-            // callouts = calloutFSISPI(errnoValue, path, json);
+            callouts = calloutFSISPI(path, json);
             break;
         default:
             std::string msg =
diff --git a/test/openpower-pels/device_callouts_test.cpp b/test/openpower-pels/device_callouts_test.cpp
index 44c2256..eacf4b9 100644
--- a/test/openpower-pels/device_callouts_test.cpp
+++ b/test/openpower-pels/device_callouts_test.cpp
@@ -73,6 +73,16 @@
                     }
                 ],
                 "Dest": "proc-0 target"
+            },
+            "90":
+            {
+                "Callouts":[
+                    {
+                       "Name": "This is missing the location code",
+                       "Priority": "H"
+                    }
+                ],
+                "Dest": "proc-0 target"
             }
         },
         "14":
@@ -94,7 +104,8 @@
                     {
                        "Name": "/chassis/motherboard/cpu0",
                        "LocationCode": "P1-C19",
-                       "Priority": "H"
+                       "Priority": "H",
+                       "MRU": "core0"
                     },
                     {
                        "Name": "/chassis/motherboard",
@@ -192,9 +203,9 @@
             {
                 "Callouts":[
                     {
-                       "Name": "/chassis/motherboard/cpu0",
-                       "LocationCode": "P1-C19",
-                       "Priority": "H"
+                       "Name": "/chassis/motherboard/cpu2",
+                       "LocationCode": "P1-C12",
+                       "Priority": "M"
                     }
                 ],
                 "Dest": "proc-0 target"
@@ -225,6 +236,39 @@
 std::string DeviceCalloutsTest::filename = "systemA_dev_callouts.json";
 fs::path DeviceCalloutsTest::dataPath;
 
+namespace openpower::pels::device_callouts
+{
+
+// Helpers to compair vectors of Callout objects
+bool operator!=(const Callout& left, const Callout& right)
+{
+    return (left.priority != right.priority) ||
+           (left.locationCode != right.locationCode) ||
+           (left.name != right.name) || (left.mru != right.mru) ||
+           (left.debug != right.debug);
+}
+
+bool operator==(const std::vector<Callout>& left,
+                const std::vector<Callout>& right)
+{
+    if (left.size() != right.size())
+    {
+        return false;
+    }
+
+    for (size_t i = 0; i < left.size(); i++)
+    {
+        if (left[i] != right[i])
+        {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+} // namespace openpower::pels::device_callouts
+
 // Test looking up the JSON file based on the system compatible names
 TEST_F(DeviceCalloutsTest, getJSONFilenameTest)
 {
@@ -255,7 +299,7 @@
     {
         EXPECT_EQ(util::getCalloutType(
                       "/sys/devices/platform/ahb/ahb:apb/ahb:apb:bus@1e78a000/"
-                      "1e78a340.i2c-bus/i2c-10/10-0022"),
+                      "1e78a340.i2c-bus/i2c-14/14-0072"),
                   util::CalloutType::i2c);
     }
 
@@ -400,3 +444,122 @@
                      std::invalid_argument);
     }
 }
+
+TEST_F(DeviceCalloutsTest, getCalloutsTest)
+{
+    std::vector<std::string> systemTypes{"systemA", "systemB"};
+
+    // A really bogus path
+    {
+        EXPECT_THROW(getCallouts("/bad/path", systemTypes),
+                     std::invalid_argument);
+    }
+
+    // I2C
+    {
+        auto callouts = getCallouts(
+            "/sys/devices/platform/ahb/ahb:apb/ahb:apb:bus@1e78a000/"
+            "1e78a340.i2c-bus/i2c-14/14-0072",
+            systemTypes);
+
+        std::vector<Callout> expected{
+            {"H", "P1-C19", "/chassis/motherboard/cpu0", "core0",
+             "I2C: bus: 14 address: 114 dest: proc-0 target"},
+            {"M", "P1", "/chassis/motherboard", "", ""}};
+
+        EXPECT_EQ(callouts, expected);
+
+        // Use the bus/address API instead of the device path one
+        callouts = getI2CCallouts(14, 0x72, systemTypes);
+        EXPECT_EQ(callouts, expected);
+
+        // I2C address not in JSON
+        EXPECT_THROW(
+            getCallouts(
+                "/sys/devices/platform/ahb/ahb:apb/ahb:apb:bus@1e78a000/"
+                "1e78a340.i2c-bus/i2c-14/14-0099",
+                systemTypes),
+            std::invalid_argument);
+
+        // A bad JSON entry, missing the location code
+        EXPECT_THROW(
+            getCallouts(
+                "/sys/devices/platform/ahb/ahb:apb/ahb:apb:bus@1e78a000/"
+                "1e78a340.i2c-bus/i2c-0/0-005a",
+                systemTypes),
+            std::runtime_error);
+    }
+
+    // FSI
+    {
+        auto callouts = getCallouts(
+            "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
+            "fsi-master/fsi0/slave@00:00/00:00:00:0a/fsi-master/fsi1/"
+            "slave@01:00/01:01:00:06/sbefifo2-dev0/occ-hwmon.2",
+            systemTypes);
+
+        std::vector<Callout> expected{{"H", "P1-C19",
+                                       "/chassis/motherboard/cpu0", "core",
+                                       "FSI: links: 0-1 dest: proc-0 target"}};
+
+        EXPECT_EQ(callouts, expected);
+
+        // link 9-1 not in JSON
+        EXPECT_THROW(
+            getCallouts(
+                "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
+                "fsi-master/fsi0/slave@09:00/00:00:00:0a/fsi-master/fsi1/"
+                "slave@01:00/01:01:00:06/sbefifo2-dev0/occ-hwmon.2",
+                systemTypes),
+            std::invalid_argument);
+    }
+
+    // FSI-I2C
+    {
+        auto callouts = getCallouts(
+            "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
+            "fsi-master/fsi0/slave@00:00/00:00:00:0a/fsi-master/fsi1/"
+            "slave@03:00/01:01:00:03/i2c-207/207-0019",
+            systemTypes);
+
+        std::vector<Callout> expected{
+            {"H", "P1-C25", "/chassis/motherboard/cpu5", "",
+             "FSI-I2C: links: 0-3 bus: 7 addr: 25 dest: proc-5 target"},
+            {"M", "P1", "/chassis/motherboard", "", ""},
+            {"L", "P2", "/chassis/motherboard/bmc", "", ""}};
+
+        EXPECT_EQ(callouts, expected);
+
+        // Bus 2 not in JSON
+        EXPECT_THROW(
+            getCallouts(
+                "/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
+                "fsi-master/fsi0/slave@00:00/00:00:00:0a/fsi-master/fsi1/"
+                "slave@03:00/01:01:00:03/i2c-202/202-0019",
+                systemTypes),
+            std::invalid_argument);
+    }
+
+    // FSI-SPI
+    {
+        auto callouts =
+            getCallouts("/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
+                        "fsi-master/fsi0/slave@08:00/00:00:00:04/spi_master/"
+                        "spi3/spi3.0/spi3.00/nvmem",
+                        systemTypes);
+
+        std::vector<Callout> expected{
+            {"H", "P1-C19", "/chassis/motherboard/cpu0", "",
+             "FSI-SPI: links: 8 bus: 3 dest: proc-0 target"}};
+
+        EXPECT_EQ(callouts, expected);
+
+        // Bus 7 not in the JSON
+        EXPECT_THROW(
+            getCallouts("/sys/devices/platform/ahb/ahb:apb/1e79b000.fsi/"
+                        "fsi-master/fsi0/slave@08:00/00:00:00:04/spi_master/"
+                        "spi7/spi7.0/spi7.00/nvmem",
+                        systemTypes),
+            std::invalid_argument);
+    }
+}