PEL: SRC Details Python parser support

This change enables the calling of python3 parsers for SRC details.
Modules will be searched under the namespace "srcparsers" from python3
sys.path directories (including the current directory).

Example:

  /usr/lib/python3.8/site-packages/srcparsers/bsrc/bsrc.py

  or

  ./srcparsers/bsrc/bsrc.py

  where

  b = Creator Subsystem ID for Hostboot in ASCII lowercase

The parsers will need to provide a function called "parseSRCToJson" with
input parameters:

  1. (str) ASCII string (Hex Word 1)
  2. (str) Hex Word 2
  3. (str) Hex Word 3
  4. (str) Hex Word 4
  5. (str) Hex Word 5
  6. (str) Hex Word 6
  7. (str) Hex Word 7
  8. (str) Hex Word 8
  9. (str) Hex Word 9

The return value must be a valid JSON string.

Signed-off-by: Harisuddin Mohamed Isa <harisuddin@gmail.com>
Change-Id: I2cb0248e5ee2803869eab28d3232b414e2d743af
diff --git a/extensions/openpower-pels/openpower-pels.mk b/extensions/openpower-pels/openpower-pels.mk
index d2fa114..9c6ec61 100644
--- a/extensions/openpower-pels/openpower-pels.mk
+++ b/extensions/openpower-pels/openpower-pels.mk
@@ -4,6 +4,7 @@
 	extensions/openpower-pels/manager.cpp \
 	extensions/openpower-pels/pldm_interface.cpp \
 	extensions/openpower-pels/repository.cpp \
+	extensions/openpower-pels/src.cpp \
 	extensions/openpower-pels/user_data.cpp
 
 phosphor_log_manager_LDADD = \
@@ -39,7 +40,6 @@
 	extensions/openpower-pels/pel_values.cpp \
 	extensions/openpower-pels/private_header.cpp \
 	extensions/openpower-pels/registry.cpp \
-	extensions/openpower-pels/src.cpp \
 	extensions/openpower-pels/section_factory.cpp \
 	extensions/openpower-pels/service_indicators.cpp \
 	extensions/openpower-pels/severity.cpp \
@@ -72,6 +72,7 @@
 
 peltool_SOURCES = \
 	extensions/openpower-pels/tools/peltool.cpp \
+	extensions/openpower-pels/src.cpp \
 	extensions/openpower-pels/user_data.cpp \
 	extensions/openpower-pels/user_data_json.cpp
 peltool_LDFLAGS = "-lpython$(PYTHON_VERSION)"
diff --git a/extensions/openpower-pels/pel.cpp b/extensions/openpower-pels/pel.cpp
index d23bf92..85ab741 100644
--- a/extensions/openpower-pels/pel.cpp
+++ b/extensions/openpower-pels/pel.cpp
@@ -291,7 +291,7 @@
         std::optional<std::string> json;
         if (sectionID == "PS" || sectionID == "SS")
         {
-            json = section.getJSON(registry);
+            json = section.getJSON(registry, plugins, creatorID);
         }
         else if (sectionID == "UD")
         {
diff --git a/extensions/openpower-pels/section.hpp b/extensions/openpower-pels/section.hpp
index 4a11d77..15ddcff 100644
--- a/extensions/openpower-pels/section.hpp
+++ b/extensions/openpower-pels/section.hpp
@@ -62,11 +62,15 @@
     /**
      * @brief Get section in JSON. Derived classes to override when required to.
      * @param[in] registry - Registry object reference
+     * @param[in] plugins - Vector of strings of plugins found in filesystem
+     * @param[in] creatorID - Creator Subsystem ID from Private Header
      * @return std::optional<std::string> - If a section comes with a JSON
      * representation, this would return the string for it.
      */
     virtual std::optional<std::string>
-        getJSON(message::Registry& registry) const
+        getJSON(message::Registry& registry,
+                const std::vector<std::string>& plugins,
+                uint8_t creatorID) const
     {
         return std::nullopt;
     }
diff --git a/extensions/openpower-pels/src.cpp b/extensions/openpower-pels/src.cpp
index b3a7693..7252b46 100644
--- a/extensions/openpower-pels/src.cpp
+++ b/extensions/openpower-pels/src.cpp
@@ -19,7 +19,13 @@
 #include "json_utils.hpp"
 #include "paths.hpp"
 #include "pel_values.hpp"
+#ifdef PELTOOL
+#include <Python.h>
 
+#include <fifo_map.hpp>
+#include <nlohmann/json.hpp>
+#include <sstream>
+#endif
 #include <phosphor-logging/log.hpp>
 
 namespace openpower
@@ -33,6 +39,194 @@
 
 constexpr size_t ccinSize = 4;
 
+#ifdef PELTOOL
+// Use fifo_map as nlohmann::json's map. We are just ignoring the 'less'
+// compare.  With this map the keys are kept in FIFO order.
+template <class K, class V, class dummy_compare, class A>
+using fifoMap = nlohmann::fifo_map<K, V, nlohmann::fifo_map_compare<K>, A>;
+using fifoJSON = nlohmann::basic_json<fifoMap>;
+
+void pyDecRef(PyObject* pyObj)
+{
+    Py_XDECREF(pyObj);
+}
+
+/**
+ * @brief Returns a JSON string to append to SRC section.
+ *
+ * The returning string will contain a JSON object, but without
+ * the outer {}.  If the input JSON isn't a JSON object (dict), then
+ * one will be created with the input added to a 'SRC Details' key.
+ *
+ * @param[in] json - The JSON to convert to a string
+ *
+ * @return std::string - The JSON string
+ */
+std::string prettyJSON(const fifoJSON& json)
+{
+    fifoJSON output;
+    if (!json.is_object())
+    {
+        output["SRC Details"] = json;
+    }
+    else
+    {
+        for (const auto& [key, value] : json.items())
+        {
+            output[key] = value;
+        }
+    }
+
+    // Let nlohmann do the pretty printing.
+    std::stringstream stream;
+    stream << std::setw(4) << output;
+
+    auto jsonString = stream.str();
+
+    // Now it looks like:
+    // {
+    //     "Key": "Value",
+    //     ...
+    // }
+
+    // Replace the { and the following newline, and the } and its
+    // preceeding newline.
+    jsonString.erase(0, 2);
+
+    auto pos = jsonString.find_last_of('}');
+    jsonString.erase(pos - 1);
+
+    return jsonString;
+}
+
+/**
+ * @brief Call Python modules to parse the data into a JSON string
+ *
+ * The module to call is based on the Creator Subsystem ID under the namespace
+ * "srcparsers". For example: "srcparsers.xsrc.xsrc" where "x" is the Creator
+ * Subsystem ID in ASCII lowercase.
+ *
+ * All modules must provide the following:
+ * Function: parseSRCToJson
+ * Argument list:
+ *    1. (str) ASCII string (Hex Word 1)
+ *    2. (str) Hex Word 2
+ *    3. (str) Hex Word 3
+ *    4. (str) Hex Word 4
+ *    5. (str) Hex Word 5
+ *    6. (str) Hex Word 6
+ *    7. (str) Hex Word 7
+ *    8. (str) Hex Word 8
+ *    9. (str) Hex Word 9
+ *-Return data:
+ *    1. (str) JSON string
+ *
+ * @param[in] hexwords - Vector of strings of Hexwords 1-9
+ * @param[in] creatorID - The creatorID from the Private Header section
+ * @return std::optional<std::string> - The JSON string if it could be created,
+ *                                      else std::nullopt
+ */
+std::optional<std::string> getPythonJSON(std::vector<std::string>& hexwords,
+                                         uint8_t creatorID)
+{
+    PyObject *pName, *pModule, *pDict, *pFunc, *pArgs, *pResult, *pBytes,
+        *eType, *eValue, *eTraceback;
+    std::string pErrStr;
+    std::string module = getNumberString("%c", tolower(creatorID)) + "src";
+    pName = PyUnicode_FromString(
+        std::string("srcparsers." + module + "." + module).c_str());
+    std::unique_ptr<PyObject, decltype(&pyDecRef)> modNamePtr(pName, &pyDecRef);
+    pModule = PyImport_Import(pName);
+    std::unique_ptr<PyObject, decltype(&pyDecRef)> modPtr(pModule, &pyDecRef);
+    if (pModule == NULL)
+    {
+        pErrStr = "No error string found";
+        PyErr_Fetch(&eType, &eValue, &eTraceback);
+        if (eValue)
+        {
+            PyObject* pStr = PyObject_Str(eValue);
+            if (pStr)
+            {
+                pErrStr = PyUnicode_AsUTF8(pStr);
+            }
+            Py_XDECREF(pStr);
+        }
+    }
+    else
+    {
+        pDict = PyModule_GetDict(pModule);
+        pFunc = PyDict_GetItemString(pDict, "parseSRCToJson");
+        if (PyCallable_Check(pFunc))
+        {
+            pArgs = PyTuple_New(9);
+            std::unique_ptr<PyObject, decltype(&pyDecRef)> argPtr(pArgs,
+                                                                  &pyDecRef);
+            for (size_t i = 0; i < 9; i++)
+            {
+                if (i < hexwords.size())
+                {
+                    auto arg = hexwords[i];
+                    PyTuple_SetItem(pArgs, i,
+                                    Py_BuildValue("s#", arg.c_str(), 8));
+                }
+                else
+                {
+                    PyTuple_SetItem(pArgs, i, Py_BuildValue("s", "00000000"));
+                }
+            }
+            pResult = PyObject_CallObject(pFunc, pArgs);
+            std::unique_ptr<PyObject, decltype(&pyDecRef)> resPtr(pResult,
+                                                                  &pyDecRef);
+            if (pResult)
+            {
+                pBytes = PyUnicode_AsEncodedString(pResult, "utf-8", "~E~");
+                std::unique_ptr<PyObject, decltype(&pyDecRef)> pyBytePtr(
+                    pBytes, &pyDecRef);
+                const char* output = PyBytes_AS_STRING(pBytes);
+                try
+                {
+                    fifoJSON json = nlohmann::json::parse(output);
+                    return prettyJSON(json);
+                }
+                catch (std::exception& e)
+                {
+                    log<level::ERR>("Bad JSON from parser",
+                                    entry("ERROR=%s", e.what()),
+                                    entry("SRC=%s", hexwords.front().c_str()),
+                                    entry("PARSER_MODULE=%s", module.c_str()));
+                    return std::nullopt;
+                }
+            }
+            else
+            {
+                pErrStr = "No error string found";
+                PyErr_Fetch(&eType, &eValue, &eTraceback);
+                if (eValue)
+                {
+                    PyObject* pStr = PyObject_Str(eValue);
+                    if (pStr)
+                    {
+                        pErrStr = PyUnicode_AsUTF8(pStr);
+                    }
+                    Py_XDECREF(pStr);
+                }
+            }
+        }
+    }
+    if (!pErrStr.empty())
+    {
+        log<level::ERR>("Python exception thrown by parser",
+                        entry("ERROR=%s", pErrStr.c_str()),
+                        entry("SRC=%s", hexwords.front().c_str()),
+                        entry("PARSER_MODULE=%s", module.c_str()));
+    }
+    Py_XDECREF(eType);
+    Py_XDECREF(eValue);
+    Py_XDECREF(eTraceback);
+    return std::nullopt;
+}
+#endif
+
 void SRC::unflatten(Stream& stream)
 {
     stream >> _header >> _version >> _flags >> _reserved1B >> _wordCount >>
@@ -437,9 +631,12 @@
     return printOut;
 }
 
-std::optional<std::string> SRC::getJSON(message::Registry& registry) const
+std::optional<std::string> SRC::getJSON(message::Registry& registry,
+                                        const std::vector<std::string>& plugins,
+                                        uint8_t creatorID) const
 {
     std::string ps;
+    std::vector<std::string> hexwords;
     jsonInsert(ps, pv::sectionVer, getNumberString("%d", _header.version), 1);
     jsonInsert(ps, pv::subSection, getNumberString("%d", _header.subType), 1);
     jsonInsert(ps, pv::createdBy, getNumberString("0x%X", _header.componentID),
@@ -477,6 +674,7 @@
     jsonInsert(ps, "Valid Word Count", getNumberString("0x%02X", _wordCount),
                1);
     std::string refcode = asciiString();
+    hexwords.push_back(refcode);
     std::string extRefcode;
     size_t pos = refcode.find(0x20);
     if (pos != std::string::npos)
@@ -495,16 +693,32 @@
     }
     for (size_t i = 2; i <= _wordCount; i++)
     {
-        jsonInsert(
-            ps, "Hex Word " + std::to_string(i),
-            getNumberString("%08X", _hexData[getWordIndexFromWordNum(i)]), 1);
+        std::string tmpWord =
+            getNumberString("%08X", _hexData[getWordIndexFromWordNum(i)]);
+        jsonInsert(ps, "Hex Word " + std::to_string(i), tmpWord, 1);
+        hexwords.push_back(tmpWord);
     }
     auto calloutJson = getCallouts();
     if (calloutJson)
     {
         ps.append(calloutJson.value());
+        ps.append(",\n");
     }
-    else
+    std::string subsystem = getNumberString("%c", tolower(creatorID));
+    bool srcDetailExists = false;
+#ifdef PELTOOL
+    if (std::find(plugins.begin(), plugins.end(), subsystem + "src") !=
+        plugins.end())
+    {
+        auto pyJson = getPythonJSON(hexwords, creatorID);
+        if (pyJson)
+        {
+            ps.append(pyJson.value());
+            srcDetailExists = true;
+        }
+    }
+#endif
+    if (!srcDetailExists)
     {
         ps.erase(ps.size() - 2);
     }
diff --git a/extensions/openpower-pels/src.hpp b/extensions/openpower-pels/src.hpp
index b9c8199..44540b0 100644
--- a/extensions/openpower-pels/src.hpp
+++ b/extensions/openpower-pels/src.hpp
@@ -226,10 +226,13 @@
     /**
      * @brief Get section in JSON.
      * @param[in] registry - Registry object reference
+     * @param[in] plugins - Vector of strings of plugins found in filesystem
+     * @param[in] creatorID - Creator Subsystem ID from Private Header
      * @return std::optional<std::string> - SRC section's JSON
      */
-    std::optional<std::string>
-        getJSON(message::Registry& registry) const override;
+    std::optional<std::string> getJSON(message::Registry& registry,
+                                       const std::vector<std::string>& plugins,
+                                       uint8_t creatorID) const override;
 
     /**
      * @brief Get error details based on refcode and hexwords
diff --git a/extensions/openpower-pels/tools/peltool.cpp b/extensions/openpower-pels/tools/peltool.cpp
index 983d589..da82932 100644
--- a/extensions/openpower-pels/tools/peltool.cpp
+++ b/extensions/openpower-pels/tools/peltool.cpp
@@ -219,6 +219,7 @@
     Py_Initialize();
     std::vector<std::string> plugins;
     std::vector<std::string> siteDirs;
+    std::array<std::string, 2> parserDirs = {"udparsers", "srcparsers"};
     PyObject* pName = PyUnicode_FromString("sys");
     PyObject* pModule = PyImport_Import(pName);
     Py_XDECREF(pName);
@@ -240,15 +241,19 @@
     }
     for (const auto& dir : siteDirs)
     {
-        if (fs::exists(dir + "/udparsers"))
+        for (const auto& parserDir : parserDirs)
         {
-            for (const auto& entry : fs::directory_iterator(dir + "/udparsers"))
+            if (fs::exists(dir + "/" + parserDir))
             {
-                if (entry.is_directory() and
-                    fs::exists(entry.path().string() + "/" +
-                               entry.path().stem().string() + ".py"))
+                for (const auto& entry :
+                     fs::directory_iterator(dir + "/" + parserDir))
                 {
-                    plugins.push_back(entry.path().stem());
+                    if (entry.is_directory() and
+                        fs::exists(entry.path().string() + "/" +
+                                   entry.path().stem().string() + ".py"))
+                    {
+                        plugins.push_back(entry.path().stem());
+                    }
                 }
             }
         }