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/tools/peltool.cpp b/extensions/openpower-pels/tools/peltool.cpp
index 3abdc74..983d589 100644
--- a/extensions/openpower-pels/tools/peltool.cpp
+++ b/extensions/openpower-pels/tools/peltool.cpp
@@ -22,6 +22,8 @@
 #include "../pel_types.hpp"
 #include "../pel_values.hpp"
 
+#include <Python.h>
+
 #include <CLI/CLI.hpp>
 #include <bitset>
 #include <fstream>
@@ -205,6 +207,56 @@
 }
 
 /**
+ * @brief Initialize Python interpreter and gather all UD parser modules under
+ *        the paths found in Python sys.path and the current user directory.
+ *        This is to prevent calling a non-existant module which causes Python
+ *        to print an import error message and breaking JSON output.
+ *
+ * @return std::vector<std::string> Vector of plugins found in filesystem
+ */
+std::vector<std::string> getPlugins()
+{
+    Py_Initialize();
+    std::vector<std::string> plugins;
+    std::vector<std::string> siteDirs;
+    PyObject* pName = PyUnicode_FromString("sys");
+    PyObject* pModule = PyImport_Import(pName);
+    Py_XDECREF(pName);
+    PyObject* pDict = PyModule_GetDict(pModule);
+    Py_XDECREF(pModule);
+    PyObject* pResult = PyDict_GetItemString(pDict, "path");
+    PyObject* pValue = PyUnicode_FromString(".");
+    PyList_Append(pResult, pValue);
+    Py_XDECREF(pValue);
+    auto list_size = PyList_Size(pResult);
+    for (auto i = 0; i < list_size; i++)
+    {
+        PyObject* item = PyList_GetItem(pResult, i);
+        PyObject* pBytes = PyUnicode_AsEncodedString(item, "utf-8", "~E~");
+        const char* output = PyBytes_AS_STRING(pBytes);
+        Py_XDECREF(pBytes);
+        std::string tmpStr(output);
+        siteDirs.push_back(tmpStr);
+    }
+    for (const auto& dir : siteDirs)
+    {
+        if (fs::exists(dir + "/udparsers"))
+        {
+            for (const auto& entry : fs::directory_iterator(dir + "/udparsers"))
+            {
+                if (entry.is_directory() and
+                    fs::exists(entry.path().string() + "/" +
+                               entry.path().stem().string() + ".py"))
+                {
+                    plugins.push_back(entry.path().stem());
+                }
+            }
+        }
+    }
+    return plugins;
+}
+
+/**
  * @brief Creates JSON string of a PEL entry if fullPEL is false or prints to
  *        stdout the full PEL in JSON if fullPEL is true
  * @param[in] itr - std::map iterator of <uint32_t, BCDTime>
@@ -213,12 +265,14 @@
  * @param[in] fullPEL - Boolean to print full JSON representation of PEL
  * @param[in] foundPEL - Boolean to check if any PEL is present
  * @param[in] scrubRegex - SRC regex object
+ * @param[in] plugins - Vector of strings of plugins found in filesystem
  * @return std::string - JSON string of PEL entry (empty if fullPEL is true)
  */
 template <typename T>
 std::string genPELJSON(T itr, bool hidden, bool includeInfo, bool fullPEL,
                        bool& foundPEL,
-                       const std::optional<std::regex>& scrubRegex)
+                       const std::optional<std::regex>& scrubRegex,
+                       const std::vector<std::string>& plugins)
 {
     std::size_t found;
     std::string val;
@@ -274,7 +328,7 @@
             {
                 std::cout << ",\n\n";
             }
-            pel.toJSON(registry);
+            pel.toJSON(registry, plugins);
         }
         else
         {
@@ -365,6 +419,7 @@
 {
     std::string listStr;
     std::map<uint32_t, BCDTime> PELs;
+    std::vector<std::string> plugins;
     listStr = "{\n";
     for (auto it = fs::directory_iterator(EXTENSION_PERSIST_DIR "/pels/logs");
          it != fs::directory_iterator(); ++it)
@@ -380,10 +435,14 @@
         }
     }
     bool foundPEL = false;
+    if (fullPEL)
+    {
+        plugins = getPlugins();
+    }
     auto buildJSON = [&listStr, &hidden, &includeInfo, &fullPEL, &foundPEL,
-                      &scrubRegex](const auto& i) {
-        listStr +=
-            genPELJSON(i, hidden, includeInfo, fullPEL, foundPEL, scrubRegex);
+                      &scrubRegex, &plugins](const auto& i) {
+        listStr += genPELJSON(i, hidden, includeInfo, fullPEL, foundPEL,
+                              scrubRegex, plugins);
     };
     if (order)
     {
@@ -544,7 +603,8 @@
 {
     if (pel.valid())
     {
-        pel.toJSON(registry);
+        auto plugins = getPlugins();
+        pel.toJSON(registry, plugins);
     }
     else
     {
@@ -724,8 +784,9 @@
         std::vector<uint8_t> data = getFileData(fileName);
         if (!data.empty())
         {
+            auto plugins = getPlugins();
             PEL pel{data};
-            pel.toJSON(registry);
+            pel.toJSON(registry, plugins);
         }
         else
         {
@@ -765,5 +826,6 @@
     {
         std::cout << app.help("", CLI::AppFormatMode::All) << std::endl;
     }
+    Py_Finalize();
     return 0;
 }