PEL: Python3 module support for UserData parsing

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

Example:

  /usr/lib/python3.8/site-packages/udparsers/bxxxx/bxxxx.py

  or

  ./udparsers/bxxxx/bxxxx.py

  where

  b = Creator Subsystem ID for Hostboot
  xxxx = Component ID of UserData section

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

1. (int) Sub-section type
2. (int) Section version
3. (memoryview): Data

The return value needs to be a valid JSON string.

Signed-off-by: Harisuddin Mohamed Isa <harisuddin@gmail.com>
Change-Id: I4d3523083bc48ad0c329c525d83c4a61d36ff611
diff --git a/extensions/openpower-pels/user_data_json.cpp b/extensions/openpower-pels/user_data_json.cpp
index e133cdd..408f5c3 100644
--- a/extensions/openpower-pels/user_data_json.cpp
+++ b/extensions/openpower-pels/user_data_json.cpp
@@ -22,6 +22,8 @@
 #include "stream.hpp"
 #include "user_data_formats.hpp"
 
+#include <Python.h>
+
 #include <fifo_map.hpp>
 #include <iomanip>
 #include <nlohmann/json.hpp>
@@ -39,6 +41,11 @@
 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 for use by PEL::printSectionInJSON().
  *
@@ -228,19 +235,151 @@
     return std::nullopt;
 }
 
+/**
+ * @brief Call Python modules to parse the data into a JSON string
+ *
+ * The module to call is based on the Creator Subsystem ID and the Component
+ * ID under the namespace "udparsers". For example: "udparsers.xyyyy.xyyyy"
+ * where "x" is the Creator Subsystem ID and "yyyy" is the Component ID.
+ *
+ * All modules must provide the following:
+ * Function: parseUDToJson
+ * Argument list:
+ *    1. (int) Sub-section type
+ *    2. (int) Section version
+ *    3. (memoryview): Data
+ *-Return data:
+ *    1. (str) JSON string
+ *
+ * @param[in] componentID - The comp ID from the UserData section header
+ * @param[in] subType - The subtype from the UserData section header
+ * @param[in] version - The version from the UserData section header
+ * @param[in] data - The data itself
+ * @param[in] creatorID - The creatorID from the PrivateHeader section
+ * @return std::optional<std::string> - The JSON string if it could be created,
+ *                                      else std::nullopt
+ */
+std::optional<std::string> getPythonJSON(uint16_t componentID, uint8_t subType,
+                                         uint8_t version,
+                                         const std::vector<uint8_t>& data,
+                                         uint8_t creatorID)
+{
+    PyObject *pName, *pModule, *pDict, *pFunc, *pArgs, *pData, *pResult,
+        *pBytes, *eType, *eValue, *eTraceback;
+    std::string pErrStr;
+    std::string module = getNumberString("%c", tolower(creatorID)) +
+                         getNumberString("%04x", componentID);
+    pName = PyUnicode_FromString(
+        std::string("udparsers." + 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, "parseUDToJson");
+        if (PyCallable_Check(pFunc))
+        {
+            auto ud = data.data();
+            pArgs = PyTuple_New(3);
+            std::unique_ptr<PyObject, decltype(&pyDecRef)> argPtr(pArgs,
+                                                                  &pyDecRef);
+            PyTuple_SetItem(pArgs, 0,
+                            PyLong_FromUnsignedLong((unsigned long)subType));
+            PyTuple_SetItem(pArgs, 1,
+                            PyLong_FromUnsignedLong((unsigned long)version));
+            pData = PyMemoryView_FromMemory(
+                reinterpret_cast<char*>(const_cast<unsigned char*>(ud)),
+                data.size(), PyBUF_READ);
+            std::unique_ptr<PyObject, decltype(&pyDecRef)> dataPtr(pData,
+                                                                   &pyDecRef);
+            PyTuple_SetItem(pArgs, 2, pData);
+            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(componentID, subType, version, json);
+                }
+                catch (std::exception& e)
+                {
+                    log<level::ERR>("Bad JSON from parser",
+                                    entry("ERROR=%s", e.what()),
+                                    entry("PARSER_MODULE=%s", module.c_str()),
+                                    entry("SUBTYPE=0x%X", subType),
+                                    entry("VERSION=%d", version),
+                                    entry("DATA_LENGTH=%lu\n", data.size()));
+                    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("PARSER_MODULE=%s", module.c_str()),
+                        entry("SUBTYPE=0x%X", subType),
+                        entry("VERSION=%d", version),
+                        entry("DATA_LENGTH=%lu\n", data.size()));
+    }
+    Py_XDECREF(eType);
+    Py_XDECREF(eValue);
+    Py_XDECREF(eTraceback);
+    return std::nullopt;
+}
+
 std::optional<std::string> getJSON(uint16_t componentID, uint8_t subType,
                                    uint8_t version,
-                                   const std::vector<uint8_t>& data)
+                                   const std::vector<uint8_t>& data,
+                                   uint8_t creatorID)
 {
     try
     {
-        switch (componentID)
+        if (pv::creatorIDs.at(getNumberString("%c", creatorID)) == "BMC" &&
+            componentID == static_cast<uint16_t>(ComponentID::phosphorLogging))
         {
-            case static_cast<uint16_t>(ComponentID::phosphorLogging):
-                return getBuiltinFormatJSON(componentID, subType, version,
-                                            data);
-            default:
-                break;
+            return getBuiltinFormatJSON(componentID, subType, version, data);
+        }
+        else
+        {
+            return getPythonJSON(componentID, subType, version, data,
+                                 creatorID);
         }
     }
     catch (std::exception& e)