PEL: Add APIs to look up device path callouts
This commit adds the interfaces device_callouts::getCallouts() and
device_callouts::getI2CCallouts() that will be used to look up the FRU
callouts to add to PELs for errors stemming from accessing devices,
either by a sysfs path, or in the case of the latter interface an I2C
bus and address.
Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I573d04632fd9fc6356a0ff53f85c2a2c13038962
diff --git a/extensions/openpower-pels/device_callouts.cpp b/extensions/openpower-pels/device_callouts.cpp
new file mode 100644
index 0000000..44226c9
--- /dev/null
+++ b/extensions/openpower-pels/device_callouts.cpp
@@ -0,0 +1,127 @@
+/**
+ * Copyright © 2020 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "device_callouts.hpp"
+
+#include "paths.hpp"
+
+#include <fstream>
+#include <phosphor-logging/log.hpp>
+#include <regex>
+
+namespace openpower::pels::device_callouts
+{
+
+constexpr auto debugFilePath = "/etc/phosphor-logging/";
+constexpr auto calloutFileSuffix = "_dev_callouts.json";
+
+namespace fs = std::filesystem;
+using namespace phosphor::logging;
+
+namespace util
+{
+
+fs::path getJSONFilename(const std::vector<std::string>& compatibleList)
+{
+ auto basePath = getPELReadOnlyDataPath();
+ fs::path fullPath;
+
+ // Find an entry in the list of compatible system names that
+ // matches a filename we have.
+
+ for (const auto& name : compatibleList)
+ {
+ fs::path filename = name + calloutFileSuffix;
+
+ // Check the debug path first
+ fs::path path{fs::path{debugFilePath} / filename};
+
+ if (fs::exists(path))
+ {
+ log<level::INFO>("Found device callout debug file");
+ fullPath = path;
+ break;
+ }
+
+ path = basePath / filename;
+
+ if (fs::exists(path))
+ {
+ fullPath = path;
+ break;
+ }
+ }
+
+ if (fullPath.empty())
+ {
+ throw std::invalid_argument(
+ "No JSON dev path callout file for this system");
+ }
+
+ return fullPath;
+}
+
+/**
+ * @brief Reads the callout JSON into an object based on the
+ * compatible system names list.
+ *
+ * @param[in] compatibleList - The list of compatible names for this
+ * system.
+ *
+ * @return nlohmann::json - The JSON object
+ */
+nlohmann::json loadJSON(const std::vector<std::string>& compatibleList)
+{
+ auto filename = getJSONFilename(compatibleList);
+ std::ifstream file{filename};
+ return nlohmann::json::parse(file);
+}
+
+std::vector<device_callouts::Callout>
+ calloutI2C(size_t i2cBus, uint8_t i2cAddress,
+ const nlohmann::json& calloutJSON)
+{
+ // TODO
+ return {};
+}
+
+std::vector<device_callouts::Callout> findCallouts(const std::string& devPath,
+ const nlohmann::json& json)
+{
+ std::vector<Callout> callouts;
+
+ // TODO
+
+ return callouts;
+}
+
+} // namespace util
+
+std::vector<Callout> getCallouts(const std::string& devPath,
+ const std::vector<std::string>& compatibleList)
+{
+ auto json = util::loadJSON(compatibleList);
+ return util::findCallouts(devPath, json);
+}
+
+std::vector<Callout>
+ getI2CCallouts(size_t i2cBus, uint8_t i2cAddress,
+ const std::vector<std::string>& compatibleList)
+{
+ auto json = util::loadJSON(compatibleList);
+ return util::calloutI2C(i2cBus, i2cAddress, json);
+}
+
+} // namespace openpower::pels::device_callouts
diff --git a/extensions/openpower-pels/device_callouts.hpp b/extensions/openpower-pels/device_callouts.hpp
new file mode 100644
index 0000000..2945f25
--- /dev/null
+++ b/extensions/openpower-pels/device_callouts.hpp
@@ -0,0 +1,149 @@
+#pragma once
+
+#include "pel_types.hpp"
+
+#include <filesystem>
+#include <nlohmann/json.hpp>
+#include <string>
+#include <tuple>
+#include <vector>
+
+/**
+ * @file device_callouts.hpp
+ *
+ * @brief Looks up FRU callouts, which are D-Bus inventory paths,
+ * in JSON for device sysfs paths.
+ *
+ * The code will extract the search keys from the sysfs path to
+ * use to look up the callout list in the JSON. The callouts will
+ * be sorted by priority as defined in th PEL spec.
+ *
+ * The JSON is normally generated from the MRW, and contains
+ * sections for the following types of callouts:
+ * * I2C (also based on bus/addr as well as the path)
+ * * FSI
+ * * FSI-I2C
+ * * FSI-SPI
+ *
+ * The JSON looks like:
+ *
+ * "I2C":
+ * "<bus>":
+ * "<address>":
+ * "Callouts": [
+ * {
+ * "LocationCode": "<location code>",
+ * "Name": "<inventory path>",
+ * "Priority": "<priority=H/M/L>",
+ * "MRU": "<optional MRU name>"
+ * },
+ * ...
+ * ],
+ * "Dest": "<destination MRW target>"
+ *
+ * "FSI":
+ * "<fsi link>":
+ * "Callouts": [
+ * ...
+ * ],
+ * "Dest": "<destination MRW target>"
+ *
+ * "FSI-I2C":
+ * "<fsi link>":
+ * "<bus>":
+ * "<address>":
+ * "Callouts": [
+ * ...
+ * ],
+ * "Dest": "<destination MRW target>"
+ *
+ * "FSI-SPI":
+ * "<fsi link>":
+ * "<bus>":
+ * "Callouts": [
+ * ...
+ * ],
+ * "Dest": "<destination MRW target>"
+ *
+ */
+
+namespace openpower::pels::device_callouts
+{
+
+/**
+ * @brief Represents a callout in the device JSON.
+ *
+ * The debug field will only be filled in for the first
+ * callout in the list of them and contains additional
+ * information about what happened when looking up the
+ * callouts that is meant to aid in debug.
+ */
+struct Callout
+{
+ std::string priority;
+ std::string locationCode;
+ std::string name;
+ std::string mru;
+ std::string debug;
+};
+
+/**
+ * @brief Looks up the callouts in a JSON File to add to a PEL
+ * for when the path between the BMC and the device specified
+ * by the passed in device path needs to be called out.
+ *
+ * The path is the path used to access the device in sysfs. It
+ * can be either a directory path or a file path.
+ *
+ * @param[in] devPath - The device path
+ * @param[in] compatibleList - The list of compatible names for this
+ * system.
+ * @return std::vector<Callout> - The list of callouts
+ */
+std::vector<Callout>
+ getCallouts(const std::string& devPath,
+ const std::vector<std::string>& compatibleList);
+
+/**
+ * @brief Looks up the callouts to add to a PEL for when the path
+ * between the BMC and the device specified by the passed in
+ * I2C bus and address needs to be called out.
+ *
+ * @param[in] i2cBus - The I2C bus
+ * @param[in] i2cAddress - The I2C address
+ * @param[in] compatibleList - The list of compatible names for this
+ * system.
+ * @return std::vector<Callout> - The list of callouts
+ */
+std::vector<Callout>
+ getI2CCallouts(size_t i2cBus, uint8_t i2cAddress,
+ const std::vector<std::string>& compatibleList);
+
+namespace util
+{
+
+/**
+ * @brief Returns the path to the JSON file to look up callouts in.
+ *
+ * @param[in] compatibleList - The list of compatible names for this
+ * system.
+ *
+ * @return path - The path to the file.
+ */
+std::filesystem::path
+ getJSONFilename(const std::vector<std::string>& compatibleList);
+
+/**
+ * @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);
+} // namespace util
+} // namespace openpower::pels::device_callouts
diff --git a/extensions/openpower-pels/openpower-pels.mk b/extensions/openpower-pels/openpower-pels.mk
index cd1ab10..8e5893e 100644
--- a/extensions/openpower-pels/openpower-pels.mk
+++ b/extensions/openpower-pels/openpower-pels.mk
@@ -23,6 +23,7 @@
extensions/openpower-pels/callout.cpp \
extensions/openpower-pels/callouts.cpp \
extensions/openpower-pels/data_interface.cpp \
+ extensions/openpower-pels/device_callouts.cpp \
extensions/openpower-pels/extended_user_header.cpp \
extensions/openpower-pels/failing_mtms.cpp \
extensions/openpower-pels/fru_identity.cpp \
diff --git a/test/openpower-pels/Makefile.include b/test/openpower-pels/Makefile.include
index 34db4e5..0ee298b 100644
--- a/test/openpower-pels/Makefile.include
+++ b/test/openpower-pels/Makefile.include
@@ -4,6 +4,7 @@
additional_data_test \
ascii_string_test \
bcd_time_test \
+ device_callouts_test \
event_logger_test \
extended_user_header_test \
failing_mtms_test \
@@ -37,6 +38,7 @@
$(top_builddir)/extensions/openpower-pels/bcd_time.o \
$(top_builddir)/extensions/openpower-pels/callout.o \
$(top_builddir)/extensions/openpower-pels/callouts.o \
+ $(top_builddir)/extensions/openpower-pels/device_callouts.o \
$(top_builddir)/extensions/openpower-pels/extended_user_header.o \
$(top_builddir)/extensions/openpower-pels/failing_mtms.o \
$(top_builddir)/extensions/openpower-pels/fru_identity.o \
@@ -309,6 +311,7 @@
$(top_builddir)/extensions/openpower-pels/ascii_string.o \
$(top_builddir)/extensions/openpower-pels/callout.o \
$(top_builddir)/extensions/openpower-pels/callouts.o \
+ $(top_builddir)/extensions/openpower-pels/device_callouts.o \
$(top_builddir)/extensions/openpower-pels/fru_identity.o \
$(top_builddir)/extensions/openpower-pels/json_utils.o \
$(top_builddir)/extensions/openpower-pels/mru.o \
@@ -330,6 +333,7 @@
$(top_builddir)/extensions/openpower-pels/bcd_time.o \
$(top_builddir)/extensions/openpower-pels/callout.o \
$(top_builddir)/extensions/openpower-pels/callouts.o \
+ $(top_builddir)/extensions/openpower-pels/device_callouts.o \
$(top_builddir)/extensions/openpower-pels/data_interface.o \
$(top_builddir)/extensions/openpower-pels/extended_user_header.o \
$(top_builddir)/extensions/openpower-pels/fru_identity.o \
@@ -377,3 +381,13 @@
event_logger_test_LDADD = \
$(test_ldadd)
event_logger_test_LDFLAGS = $(test_ldflags) $(SDEVENTPLUS_LIBS)
+
+device_callouts_test_SOURCES = \
+ %reldir%/device_callouts_test.cpp
+device_callouts_test_CPPFLAGS = $(test_cppflags)
+device_callouts_test_CXXFLAGS = $(test_cxxflags)
+device_callouts_test_LDADD = \
+ $(test_ldadd) \
+ $(pel_test_utils_ldadd) \
+ $(top_builddir)/extensions/openpower-pels/device_callouts.o
+device_callouts_test_LDFLAGS = $(test_ldflags)
diff --git a/test/openpower-pels/device_callouts_test.cpp b/test/openpower-pels/device_callouts_test.cpp
new file mode 100644
index 0000000..d2947a1
--- /dev/null
+++ b/test/openpower-pels/device_callouts_test.cpp
@@ -0,0 +1,242 @@
+/**
+ * Copyright © 2020 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "extensions/openpower-pels/device_callouts.hpp"
+#include "extensions/openpower-pels/paths.hpp"
+
+#include <fstream>
+
+#include <gtest/gtest.h>
+
+using namespace openpower::pels;
+using namespace openpower::pels::device_callouts;
+namespace fs = std::filesystem;
+
+// The callout JSON looks like:
+// "I2C":
+// "<bus>":
+// "<address>":
+// "Callouts": ...
+//
+// "FSI":
+// "<fsi link>":
+// "Callouts": ...
+//
+// "FSI-I2C":
+// "<fsi link>":
+// "<bus>":
+// "<address>":
+// "Callouts": ...
+//
+// "FSI-SPI":
+// "<fsi link>":
+// "<bus>":
+// "Callouts": ...
+
+const auto calloutJSON = R"(
+{
+ "I2C":
+ {
+ "0":
+ {
+ "32":
+ {
+ "Callouts":[
+ {
+ "Name": "/chassis/motherboard/cpu0",
+ "LocationCode": "P1-C19",
+ "Priority": "H"
+ }
+ ],
+ "Dest": "proc-0 target"
+ },
+ "81":
+ {
+ "Callouts":[
+ {
+ "Name": "/chassis/motherboard/cpu0",
+ "LocationCode": "P1-C19",
+ "Priority": "H"
+ }
+ ],
+ "Dest": "proc-0 target"
+ }
+ },
+ "14":
+ {
+ "112":
+ {
+ "Callouts":[
+ {
+ "Name": "/chassis/motherboard/cpu0",
+ "LocationCode": "P1-C19",
+ "Priority": "H"
+ }
+ ],
+ "Dest": "proc-0 target"
+ },
+ "114":
+ {
+ "Callouts":[
+ {
+ "Name": "/chassis/motherboard/cpu0",
+ "LocationCode": "P1-C19",
+ "Priority": "H"
+ },
+ {
+ "Name": "/chassis/motherboard",
+ "LocationCode": "P1",
+ "Priority": "M"
+ }
+ ],
+ "Dest": "proc-0 target"
+ }
+ }
+ },
+ "FSI":
+ {
+ "0":
+ {
+ "Callouts":[
+ {
+ "Name": "/chassis/motherboard/cpu0",
+ "LocationCode": "P1-C19",
+ "Priority": "H"
+ }
+ ],
+ "Dest": "proc-0 target"
+ },
+ "0-1":
+ {
+ "Callouts":[
+ {
+ "Name": "/chassis/motherboard/cpu0",
+ "LocationCode": "P1-C19",
+ "Priority": "H",
+ "MRU": "core"
+ }
+ ],
+ "Dest": "proc-0 target"
+ }
+ },
+ "FSI-I2C":
+ {
+ "0-3":
+ {
+ "7":
+ {
+ "24":
+ {
+ "Callouts":[
+ {
+ "Name": "/chassis/motherboard/cpu0",
+ "LocationCode": "P1-C19",
+ "Priority": "H"
+ }
+ ],
+ "Dest": "proc-0 target"
+ },
+ "25":
+ {
+ "Callouts":[
+ {
+ "Name": "/chassis/motherboard/cpu5",
+ "LocationCode": "P1-C25",
+ "Priority": "H"
+ },
+ {
+ "Name": "/chassis/motherboard",
+ "LocationCode": "P1",
+ "Priority": "M"
+ },
+ {
+ "Name": "/chassis/motherboard/bmc",
+ "LocationCode": "P2",
+ "Priority": "L"
+ }
+ ],
+ "Dest": "proc-5 target"
+ }
+ }
+ }
+ },
+ "FSI-SPI":
+ {
+ "8":
+ {
+ "3":
+ {
+ "Callouts":[
+ {
+ "Name": "/chassis/motherboard/cpu0",
+ "LocationCode": "P1-C19",
+ "Priority": "H"
+ }
+ ],
+ "Dest": "proc-0 target"
+ },
+ "4":
+ {
+ "Callouts":[
+ {
+ "Name": "/chassis/motherboard/cpu0",
+ "LocationCode": "P1-C19",
+ "Priority": "H"
+ }
+ ],
+ "Dest": "proc-0 target"
+ }
+ }
+ }
+})"_json;
+
+class DeviceCalloutsTest : public ::testing::Test
+{
+ public:
+ static void SetUpTestCase()
+ {
+ dataPath = getPELReadOnlyDataPath();
+ std::ofstream file{dataPath / filename};
+ file << calloutJSON.dump();
+ }
+
+ static void TearDownTestCase()
+ {
+ fs::remove_all(dataPath);
+ }
+
+ static std::string filename;
+ static fs::path dataPath;
+};
+
+std::string DeviceCalloutsTest::filename = "systemA_dev_callouts.json";
+fs::path DeviceCalloutsTest::dataPath;
+
+// Test looking up the JSON file based on the system compatible names
+TEST_F(DeviceCalloutsTest, getJSONFilenameTest)
+{
+ {
+ std::vector<std::string> compatibles{"system1", "systemA", "system3"};
+ EXPECT_EQ(util::getJSONFilename(compatibles),
+ fs::path{dataPath / filename});
+ }
+
+ // Actual filename not in compatibles
+ {
+ std::vector<std::string> compatibles{"system5", "system6"};
+ EXPECT_THROW(util::getJSONFilename(compatibles), std::invalid_argument);
+ }
+}