Add in-tree OEM IPMI provider infrastructure

Change-Id: I341af91571b95f74e12def273c6cddf2b72c7884
Signed-off-by: Vernon Mauery <vernon.mauery@gmail.com>
diff --git a/oem/example/apphandler.cpp b/oem/example/apphandler.cpp
new file mode 100644
index 0000000..678f71a
--- /dev/null
+++ b/oem/example/apphandler.cpp
@@ -0,0 +1,431 @@
+#include "config.h"
+
+#include <ipmid/api.hpp>
+#include <ipmid/types.hpp>
+#include <ipmid/utils.hpp>
+#include <nlohmann/json.hpp>
+#include <phosphor-logging/elog-errors.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/message/types.hpp>
+#include <xyz/openbmc_project/Common/error.hpp>
+#include <xyz/openbmc_project/Software/Activation/server.hpp>
+#include <xyz/openbmc_project/Software/Version/server.hpp>
+#include <xyz/openbmc_project/State/BMC/server.hpp>
+
+#include <algorithm>
+#include <array>
+#include <charconv>
+#include <cstddef>
+#include <cstdint>
+#include <filesystem>
+#include <fstream>
+#include <memory>
+#include <regex>
+#include <string>
+#include <string_view>
+#include <tuple>
+#include <vector>
+
+constexpr auto bmcStateInterface = "xyz.openbmc_project.State.BMC";
+constexpr auto bmcStateProperty = "CurrentBMCState";
+
+static constexpr auto redundancyIntf =
+    "xyz.openbmc_project.Software.RedundancyPriority";
+static constexpr auto versionIntf = "xyz.openbmc_project.Software.Version";
+static constexpr auto activationIntf =
+    "xyz.openbmc_project.Software.Activation";
+static constexpr auto softwareRoot = "/xyz/openbmc_project/software";
+
+void registerNetFnAppFunctions() __attribute__((constructor));
+
+using namespace phosphor::logging;
+using namespace sdbusplus::error::xyz::openbmc_project::common;
+using Version = sdbusplus::server::xyz::openbmc_project::software::Version;
+using Activation =
+    sdbusplus::server::xyz::openbmc_project::software::Activation;
+using BMC = sdbusplus::server::xyz::openbmc_project::state::BMC;
+namespace fs = std::filesystem;
+
+/**
+ * @brief Returns the Version info from primary s/w object
+ *
+ * Get the Version info from the active s/w object which is having high
+ * "Priority" value(a smaller number is a higher priority) and "Purpose"
+ * is "BMC" from the list of all s/w objects those are implementing
+ * RedundancyPriority interface from the given softwareRoot path.
+ *
+ * @return On success returns the Version info from primary s/w object.
+ *
+ */
+std::string getActiveSoftwareVersionInfo(ipmi::Context::ptr ctx)
+{
+    std::string revision{};
+    ipmi::ObjectTree objectTree;
+    try
+    {
+        objectTree =
+            ipmi::getAllDbusObjects(*ctx->bus, softwareRoot, redundancyIntf);
+    }
+    catch (const sdbusplus::exception_t& e)
+    {
+        lg2::error("Failed to fetch redundancy object from dbus, "
+                   "interface: {INTERFACE},  error: {ERROR}",
+                   "INTERFACE", redundancyIntf, "ERROR", e);
+        elog<InternalFailure>();
+    }
+
+    auto objectFound = false;
+    for (auto& softObject : objectTree)
+    {
+        auto service =
+            ipmi::getService(*ctx->bus, redundancyIntf, softObject.first);
+        auto objValueTree =
+            ipmi::getManagedObjects(*ctx->bus, service, softwareRoot);
+
+        auto minPriority = 0xFF;
+        for (const auto& objIter : objValueTree)
+        {
+            try
+            {
+                auto& intfMap = objIter.second;
+                auto& redundancyPriorityProps = intfMap.at(redundancyIntf);
+                auto& versionProps = intfMap.at(versionIntf);
+                auto& activationProps = intfMap.at(activationIntf);
+                auto priority =
+                    std::get<uint8_t>(redundancyPriorityProps.at("Priority"));
+                auto purpose =
+                    std::get<std::string>(versionProps.at("Purpose"));
+                auto activation =
+                    std::get<std::string>(activationProps.at("Activation"));
+                auto version =
+                    std::get<std::string>(versionProps.at("Version"));
+                if ((Version::convertVersionPurposeFromString(purpose) ==
+                     Version::VersionPurpose::BMC) &&
+                    (Activation::convertActivationsFromString(activation) ==
+                     Activation::Activations::Active))
+                {
+                    if (priority < minPriority)
+                    {
+                        minPriority = priority;
+                        objectFound = true;
+                        revision = std::move(version);
+                    }
+                }
+            }
+            catch (const std::exception& e)
+            {
+                lg2::error("error message: {ERROR}", "ERROR", e);
+            }
+        }
+    }
+
+    if (!objectFound)
+    {
+        lg2::error("Could not found an BMC software Object");
+        elog<InternalFailure>();
+    }
+
+    return revision;
+}
+
+bool getCurrentBmcStateWithFallback(ipmi::Context::ptr& ctx,
+                                    const bool fallbackAvailability)
+{
+    // Get the Inventory object implementing the BMC interface
+    ipmi::DbusObjectInfo bmcObject{};
+    boost::system::error_code ec =
+        ipmi::getDbusObject(ctx, bmcStateInterface, bmcObject);
+    std::string bmcState{};
+    if (ec.value())
+    {
+        return fallbackAvailability;
+    }
+    ec = ipmi::getDbusProperty(ctx, bmcObject.second, bmcObject.first,
+                               bmcStateInterface, bmcStateProperty, bmcState);
+    if (!ec.value())
+    {
+        return fallbackAvailability;
+    }
+    return BMC::convertBMCStateFromString(bmcState) == BMC::BMCState::Ready;
+}
+
+typedef struct
+{
+    char major;
+    char minor;
+    uint8_t aux[4];
+} Revision;
+
+/* Use regular expression searching matched pattern X.Y, and convert it to  */
+/* Major (X) and Minor (Y) version.                                         */
+/* Example:                                                                 */
+/* version = 2.14.0-dev                                                     */
+/*           ^ ^                                                            */
+/*           | |---------------- Minor                                      */
+/*           |------------------ Major                                      */
+/*                                                                          */
+/* Default regex string only tries to match Major and Minor version.        */
+/*                                                                          */
+/* To match more firmware version info, platforms need to define it own     */
+/* regex string to match more strings, and assign correct mapping index in  */
+/* matches array.                                                           */
+/*                                                                          */
+/* matches[0]: matched index for major ver                                  */
+/* matches[1]: matched index for minor ver                                  */
+/* matches[2]: matched index for aux[0] (set 0 to skip)                     */
+/* matches[3]: matched index for aux[1] (set 0 to skip)                     */
+/* matches[4]: matched index for aux[2] (set 0 to skip)                     */
+/* matches[5]: matched index for aux[3] (set 0 to skip)                     */
+/* Example:                                                                 */
+/* regex = "([\d]+).([\d]+).([\d]+)-dev-([\d]+)-g([0-9a-fA-F]{2})           */
+/*          ([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})"               */
+/* matches = {1,2,5,6,7,8}                                                  */
+/* version = 2.14.0-dev-750-g37a7c5ad1-dirty                                */
+/*           ^ ^  ^     ^    ^ ^ ^ ^                                        */
+/*           | |  |     |    | | | |                                        */
+/*           | |  |     |    | | | |-- Aux byte 3 (0xAD), index 8           */
+/*           | |  |     |    | | |---- Aux byte 2 (0xC5), index 7           */
+/*           | |  |     |    | |------ Aux byte 1 (0xA7), index 6           */
+/*           | |  |     |    |-------- Aux byte 0 (0x37), index 5           */
+/*           | |  |     |------------- Not used, index 4                    */
+/*           | |  |------------------- Not used, index 3                    */
+/*           | |---------------------- Minor (14), index 2                  */
+/*           |------------------------ Major (2), index 1                   */
+int convertVersion(std::string s, Revision& rev)
+{
+    static const std::vector<size_t> matches = {
+        MAJOR_MATCH_INDEX, MINOR_MATCH_INDEX, AUX_0_MATCH_INDEX,
+        AUX_1_MATCH_INDEX, AUX_2_MATCH_INDEX, AUX_3_MATCH_INDEX};
+    std::regex fw_regex(FW_VER_REGEX);
+    std::smatch m;
+    Revision r = {0};
+    size_t val;
+
+    if (std::regex_search(s, m, fw_regex))
+    {
+        if (m.size() < *std::max_element(matches.begin(), matches.end()))
+        { // max index higher than match count
+            return -1;
+        }
+
+        // convert major
+        {
+            std::string str = m[matches[0]].str();
+            const auto& [ptr, ec] =
+                std::from_chars(str.data(), str.data() + str.size(), val);
+            if (ec != std::errc() || ptr != str.data() + str.size())
+            { // failed to convert major string
+                return -1;
+            }
+
+            if (val >= 2000)
+            { // For the platforms use year as major version, it would expect to
+              // have major version between 0 - 99. If the major version is
+              // greater than or equal to 2000, it is treated as a year and
+              // converted to 0 - 99.
+                r.major = val % 100;
+            }
+            else
+            {
+                r.major = val & 0x7F;
+            }
+        }
+
+        // convert minor
+        {
+            std::string str = m[matches[1]].str();
+            const auto& [ptr, ec] =
+                std::from_chars(str.data(), str.data() + str.size(), val);
+            if (ec != std::errc() || ptr != str.data() + str.size())
+            { // failed to convert minor string
+                return -1;
+            }
+            r.minor = val & 0xFF;
+        }
+
+        // convert aux bytes
+        {
+            size_t i;
+            for (i = 0; i < 4; i++)
+            {
+                if (matches[i + 2] == 0)
+                {
+                    continue;
+                }
+
+                std::string str = m[matches[i + 2]].str();
+                const char* cstr = str.c_str();
+                auto [ptr,
+                      ec] = std::from_chars(cstr, cstr + str.size(), val, 16);
+                if (ec != std::errc() || ptr != cstr + str.size())
+                { // failed to convert aux byte string
+                    break;
+                }
+
+                r.aux[i] = val & 0xFF;
+            }
+
+            if (i != 4)
+            { // something wrong durign converting aux bytes
+                return -1;
+            }
+        }
+
+        // all matched
+        rev = r;
+        return 0;
+    }
+
+    return -1;
+}
+
+/* @brief: Implement the Get Device ID IPMI command per the IPMI spec
+ *  @param[in] ctx - shared_ptr to an IPMI context struct
+ *
+ *  @returns IPMI completion code plus response data
+ *   - Device ID (manufacturer defined)
+ *   - Device revision[4 bits]; reserved[3 bits]; SDR support[1 bit]
+ *   - FW revision major[7 bits] (binary encoded); available[1 bit]
+ *   - FW Revision minor (BCD encoded)
+ *   - IPMI version (0x02 for IPMI 2.0)
+ *   - device support (bitfield of supported options)
+ *   - MFG IANA ID (3 bytes)
+ *   - product ID (2 bytes)
+ *   - AUX info (4 bytes)
+ */
+ipmi::RspType<uint8_t,  // Device ID
+              uint8_t,  // Device Revision
+              uint8_t,  // Firmware Revision Major
+              uint8_t,  // Firmware Revision minor
+              uint8_t,  // IPMI version
+              uint8_t,  // Additional device support
+              uint24_t, // MFG ID
+              uint16_t, // Product ID
+              uint32_t  // AUX info
+              >
+    ipmiAppGetDeviceId([[maybe_unused]] ipmi::Context::ptr ctx)
+{
+    static struct
+    {
+        uint8_t id;
+        uint8_t revision;
+        uint8_t fw[2];
+        uint8_t ipmiVer;
+        uint8_t addnDevSupport;
+        uint24_t manufId;
+        uint16_t prodId;
+        uint32_t aux;
+    } devId;
+    static bool dev_id_initialized = false;
+    static bool defaultActivationSetting = true;
+    const char* filename = "/usr/share/ipmi-providers/dev_id.json";
+    constexpr auto ipmiDevIdStateShift = 7;
+    constexpr auto ipmiDevIdFw1Mask = ~(1 << ipmiDevIdStateShift);
+
+    static bool haveBMCVersion = false;
+    if (!haveBMCVersion || !dev_id_initialized)
+    {
+        int r = -1;
+        Revision rev = {0, 0, {0, 0, 0, 0}};
+        try
+        {
+            auto version = getActiveSoftwareVersionInfo(ctx);
+            r = convertVersion(version, rev);
+        }
+        catch (const std::exception& e)
+        {
+            lg2::error("error message: {ERROR}", "ERROR", e);
+        }
+
+        if (r >= 0)
+        {
+            // bit7 identifies if the device is available
+            // 0=normal operation
+            // 1=device firmware, SDR update,
+            // or self-initialization in progress.
+            // The availability may change in run time, so mask here
+            // and initialize later.
+            devId.fw[0] = rev.major & ipmiDevIdFw1Mask;
+
+            rev.minor = (rev.minor > 99 ? 99 : rev.minor);
+            devId.fw[1] = rev.minor % 10 + (rev.minor / 10) * 16;
+            std::memcpy(&devId.aux, rev.aux, sizeof(rev.aux));
+            haveBMCVersion = true;
+        }
+    }
+    if (!dev_id_initialized)
+    {
+        // IPMI Spec version 2.0
+        devId.ipmiVer = 2;
+
+        std::ifstream devIdFile(filename);
+        if (devIdFile.is_open())
+        {
+            auto data = nlohmann::json::parse(devIdFile, nullptr, false);
+            if (!data.is_discarded())
+            {
+                devId.id = data.value("id", 0);
+                devId.revision = data.value("revision", 0);
+                devId.addnDevSupport = data.value("addn_dev_support", 0);
+                devId.manufId = data.value("manuf_id", 0);
+                devId.prodId = data.value("prod_id", 0);
+                if (!(AUX_0_MATCH_INDEX || AUX_1_MATCH_INDEX ||
+                      AUX_2_MATCH_INDEX || AUX_3_MATCH_INDEX))
+                {
+                    devId.aux = data.value("aux", 0);
+                }
+
+                if (data.contains("firmware_revision"))
+                {
+                    const auto& firmwareRevision = data.at("firmware_revision");
+                    if (firmwareRevision.contains("major"))
+                    {
+                        firmwareRevision.at("major").get_to(devId.fw[0]);
+                    }
+                    if (firmwareRevision.contains("minor"))
+                    {
+                        firmwareRevision.at("minor").get_to(devId.fw[1]);
+                    }
+                }
+
+                // Set the availablitity of the BMC.
+                defaultActivationSetting = data.value("availability", true);
+
+                // Don't read the file every time if successful
+                dev_id_initialized = true;
+            }
+            else
+            {
+                lg2::error("Device ID JSON parser failure");
+                return ipmi::responseUnspecifiedError();
+            }
+        }
+        else
+        {
+            lg2::error("Device ID file not found");
+            return ipmi::responseUnspecifiedError();
+        }
+    }
+
+    // Set availability to the actual current BMC state
+    devId.fw[0] &= ipmiDevIdFw1Mask;
+    if (!getCurrentBmcStateWithFallback(ctx, defaultActivationSetting))
+    {
+        devId.fw[0] |= (1 << ipmiDevIdStateShift);
+    }
+
+    return ipmi::responseSuccess(
+        devId.id, devId.revision, devId.fw[0], devId.fw[1], devId.ipmiVer,
+        devId.addnDevSupport, devId.manufId, devId.prodId, devId.aux);
+}
+
+void registerNetFnAppFunctions()
+{
+    // OEM libraries should use ipmi::prioOemBase to override default
+    // implementation of IPMI commands that use ipmi::prioOpenBmcBase
+
+    // <Get Device ID>
+    ipmi::registerHandler(ipmi::prioOemBase, ipmi::netFnApp,
+                          ipmi::app::cmdGetDeviceId, ipmi::Privilege::User,
+                          ipmiAppGetDeviceId);
+}
diff --git a/oem/example/meson.build b/oem/example/meson.build
new file mode 100644
index 0000000..d21b62a
--- /dev/null
+++ b/oem/example/meson.build
@@ -0,0 +1,26 @@
+# example OEM library build
+
+example_oem_src = ['apphandler.cpp']
+
+example_oem_deps = [
+    boost,
+    crypto,
+    ipmid_dep,
+    nlohmann_json_dep,
+    phosphor_dbus_interfaces_dep,
+    phosphor_logging_dep,
+    sdbusplus_dep,
+    stdplus_dep,
+]
+
+
+example_oem_lib = library(
+    'example-oem',
+    example_oem_src,
+    dependencies: example_oem_deps,
+    include_directories: [root_inc],
+    install: true,
+    install_dir: get_option('libdir') / 'ipmid-providers',
+    version: meson.project_version(),
+    override_options: ipmi_plugin_options,
+)