Support IPMI OEM command to edit BMC MAC Address

Implement the IPMI command to edit BMC MAC Address.
The workflow of this design is described as below:
1. Get the bus and address of the baseboard FRU device by calling
GetManagedObjects method of xyz.openbmc_project.FruDevice. The baseboard
is indicated by the CHASSIS_TYPE is "23". If the baseboard doesn't have
"CHASSIS_TYPE" field or it is not "23", users can indicate the FRU's bus
and address by manual via "mac-address-fru-bus" and
"mac-address-fru-addr" options.
2. Get the FRU raw data by calling GetRawFru method of
xyz.openbmc_project.FruDevice with input parameters are bus and address
in the step 1.
3. Update the new MAC address to FRU data which are read in step 2 and
recalculate checksum.
4. Write new FRU data to EEPROM by calling WriteFru method
xyz.openbmc_project.FruDevice.

Tested:
   1. Update BMC MAC Address to 70:E2:84:86:76:C0
    $ ipmitool raw 0x3c 0x01 0x70 0xe2 0x84 0x86 0x76 0xc0
   2. Check FRU for the new MAC Address in Board Extra
    $ ipmitool fru print 0 | grep "Board Extra"
   3. Reboot BMC and check if eth1 has new BMC MAC Address
    $ ifconfig eth1 | grep HWaddr
      eth1      Link encap:Ethernet  HWaddr 70:E2:84:86:76:C0

Signed-off-by: Thang Tran <thuutran@amperecomputing.com>
Change-Id: I08f6f6eb9dd701406dc81b18110cbe2d98321e59
diff --git a/include/oemcommands.hpp b/include/oemcommands.hpp
index 6179ab7..b758844 100644
--- a/include/oemcommands.hpp
+++ b/include/oemcommands.hpp
@@ -27,6 +27,7 @@
 } // namespace ampere
 namespace general
 {
+constexpr uint8_t cmdEditBmcMacAdr = 0x01;
 constexpr uint8_t cmdSyncRtcTime = 0xf9;
 } // namespace general
-} // namespace ipmi
\ No newline at end of file
+} // namespace ipmi
diff --git a/meson.build b/meson.build
index acf5ec1..6f55020 100644
--- a/meson.build
+++ b/meson.build
@@ -34,6 +34,20 @@
 channellayer_dep = cpp.find_library('channellayer', required: true)
 userlayer_dep = cpp.find_library('userlayer', required: true)
 
+mac_fru_bus = get_option('mac-address-fru-bus')
+if mac_fru_bus >= 0
+  add_project_arguments(
+    '-DMAC_ADDRESS_FRU_BUS=' + mac_fru_bus.to_string(),
+    language : 'cpp')
+endif
+
+mac_fru_bus = get_option('mac-address-fru-addr')
+if mac_fru_bus >= 8 and mac_fru_bus <=119
+  add_project_arguments(
+    '-DMAC_ADDRESS_FRU_ADDR=' + mac_fru_bus.to_string(),
+    language : 'cpp')
+endif
+
 zampoemcmds_pre = declare_dependency(
   include_directories: root_inc,
   dependencies: [
@@ -54,4 +68,4 @@
   version: meson.project_version(),
   override_options: ['b_lundef=false'],
   install: true,
-  install_dir: get_option('libdir') / 'ipmid-providers')
\ No newline at end of file
+  install_dir: get_option('libdir') / 'ipmid-providers')
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..cbd6df2
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,10 @@
+option('mac-address-fru-bus', 
+       type : 'integer', 
+       value : -1, 
+       description: 'The i2c bus index of FRU device which includes MAC ADDRESS information')
+
+option('mac-address-fru-addr',
+       type : 'integer',
+       value : 0,
+       description: 'The i2c address (0x08->0x77)  of FRU device which includes MAC ADDRESS information')
+
diff --git a/src/oemcommands.cpp b/src/oemcommands.cpp
index deea8e2..53a35a1 100644
--- a/src/oemcommands.cpp
+++ b/src/oemcommands.cpp
@@ -14,15 +14,32 @@
  * limitations under the License.
  */
 
+#include "oemcommands.hpp"
+
+#include <boost/container/flat_map.hpp>
 #include <ipmid/api.hpp>
 #include <ipmid/types.hpp>
 #include <ipmid/utils.hpp>
 #include <phosphor-logging/log.hpp>
-#include "oemcommands.hpp"
+
 #include <cstdlib>
 
 using namespace phosphor::logging;
 
+using BasicVariantType =
+    std::variant<std::vector<std::string>, std::string, int64_t, uint64_t,
+                 double, int32_t, uint32_t, int16_t, uint16_t, uint8_t, bool>;
+using FruObjectType = boost::container::flat_map<
+    sdbusplus::message::object_path,
+    boost::container::flat_map<
+        std::string,
+        boost::container::flat_map<std::string, BasicVariantType>>>;
+
+constexpr static const char* fruDeviceServiceName =
+    "xyz.openbmc_project.FruDevice";
+
+constexpr static const char* chassisTypeRackMount = "23";
+
 static inline auto response(uint8_t cc)
 {
     return std::make_tuple(cc, std::nullopt);
@@ -33,22 +50,274 @@
     return response(responseFail);
 }
 
+/** @brief get Baseboard FRU's address
+ *  @param - busIdx, address of I2C device
+ *  @returns - true if successfully, false if fail
+ */
+[[maybe_unused]] static bool getBaseBoardFRUAddr(uint8_t& busIdx, uint8_t& addr)
+{
+    bool retVal = false;
+    sd_bus* bus = NULL;
+    FruObjectType fruObjects;
+
+    /*
+     * Read all managed objects of FRU device
+     */
+    int ret = sd_bus_default_system(&bus);
+    if (ret < 0)
+    {
+        phosphor::logging::log<phosphor::logging::level::ERR>(
+            "Failed to connect to system bus");
+        sd_bus_unref(bus);
+        return false;
+    }
+    sdbusplus::bus::bus dbus(bus);
+    auto mapperCall = dbus.new_method_call(fruDeviceServiceName, "/",
+                                           "org.freedesktop.DBus.ObjectManager",
+                                           "GetManagedObjects");
+
+    try
+    {
+        auto mapperReply = dbus.call(mapperCall);
+        mapperReply.read(fruObjects);
+    }
+    catch (sdbusplus::exception_t& e)
+    {
+        log<level::ERR>("Fail to call GetManagedObjects method");
+
+        sd_bus_unref(bus);
+        return false;
+    }
+
+    /*
+     * Scan all FRU objects to find out baseboard FRU device.
+     * The basedboard FRU device is indecate by chassis type
+     * is Rack Mount - "23"
+     */
+    for (const auto& fruObj : fruObjects)
+    {
+        auto fruDeviceInf = fruObj.second.find("xyz.openbmc_project.FruDevice");
+
+        if (fruDeviceInf != fruObj.second.end())
+        {
+            auto chassisProperty = fruDeviceInf->second.find("CHASSIS_TYPE");
+
+            if (chassisProperty != fruDeviceInf->second.end())
+            {
+                std::string chassisType =
+                    std::get<std::string>(chassisProperty->second);
+                auto busProperty = fruDeviceInf->second.find("BUS");
+                auto addrProperty = fruDeviceInf->second.find("ADDRESS");
+
+                if ((0 == chassisType.compare(chassisTypeRackMount)) &&
+                    (busProperty != fruDeviceInf->second.end()) &&
+                    (addrProperty != fruDeviceInf->second.end()))
+                {
+                    busIdx = (uint8_t)std::get<uint32_t>(busProperty->second);
+                    addr = (uint8_t)std::get<uint32_t>(addrProperty->second);
+                    retVal = true;
+                    break;
+                }
+            }
+        }
+    }
+
+    sd_bus_unref(bus);
+    return retVal;
+}
+
+/** @brief get Raw FRU's data
+ *  @param - busIdx, address of I2C device.
+ *         - fruData: data have been read
+ *  @returns - true if successfully, false if fail
+ */
+static bool getRawFruData(uint8_t busIdx, uint8_t addr,
+                          std::vector<uint8_t>& fruData)
+{
+    bool retVal = false;
+    sd_bus* bus = NULL;
+    int ret = sd_bus_default_system(&bus);
+
+    if (ret < 0)
+    {
+        phosphor::logging::log<phosphor::logging::level::ERR>(
+            "Failed to connect to system bus",
+            phosphor::logging::entry("ERRNO=0x%X", -ret));
+    }
+    else
+    {
+        sdbusplus::bus::bus dbus(bus);
+        auto MapperCall = dbus.new_method_call(
+            fruDeviceServiceName, "/xyz/openbmc_project/FruDevice",
+            "xyz.openbmc_project.FruDeviceManager", "GetRawFru");
+
+        MapperCall.append(busIdx, addr);
+
+        try
+        {
+            auto mapperReply = dbus.call(MapperCall);
+            mapperReply.read(fruData);
+            retVal = true;
+        }
+        catch (sdbusplus::exception_t& e)
+        {
+            log<level::ERR>("Fail to read Raw FRU data from system bus\n");
+        }
+    }
+
+    sd_bus_unref(bus);
+
+    return retVal;
+}
+
+/** @brief update MAC address information in FRU data
+ *  @param - fruData: FRU data
+ *         - macAddress: MAC address information
+ *  @returns - true if successfully, false if fail
+ */
+static bool updateMACAddrInFRU(std::vector<uint8_t>& fruData,
+                               std::vector<uint8_t> macAddress)
+{
+    bool retVal = false;
+    uint32_t areaOffset = fruData[3] * 8; /* Board area start offset */
+    char macAddressStr[18];
+    uint32_t boardLeng = 0;
+    uint8_t checkSumVal = 0;
+
+    /*
+     * Update MAC address at first custom field of Board Information Area.
+     */
+    if (areaOffset != 0)
+    {
+        /*
+         * The Board Manufacturer type/length byte is stored
+         * at byte 0x06 of Board area.
+         */
+        uint32_t fieldOffset = areaOffset + 6;
+
+        /*
+         * Scan all 5 predefined fields of Board area to jump to
+         * first Custom field.
+         */
+        for (uint32_t i = 0; i < 5; i++)
+        {
+            fieldOffset += (fruData[fieldOffset] & 0x3f) + 1;
+        }
+
+        /*
+         * Update the MAC address information when type/length is not
+         * EndOfField byte and the length of Custom field is 17.
+         */
+        if ((fruData[fieldOffset] != 0xc1) &&
+            ((uint8_t)17 == (fruData[fieldOffset] & (uint8_t)0x3f)))
+        {
+            sprintf(macAddressStr, "%02X:%02X:%02X:%02X:%02X:%02X",
+                    macAddress[0], macAddress[1], macAddress[2], macAddress[3],
+                    macAddress[4], macAddress[5]);
+
+            /*
+             * Update 17 bytes of MAC address information
+             */
+            fieldOffset++;
+            for (uint32_t i = 0; i < 17; i++)
+            {
+                fruData[fieldOffset + i] = macAddressStr[i];
+            }
+
+            /*
+             * Re-caculate the checksum of Board Information Area.
+             */
+            boardLeng = fruData[areaOffset + 1] * 8;
+            for (uint32_t i = 0; i < boardLeng - 1; i++)
+            {
+                checkSumVal += fruData[areaOffset + i];
+            }
+
+            checkSumVal = ~checkSumVal + 1;
+            fruData[areaOffset + boardLeng - 1] = checkSumVal;
+
+            retVal = true;
+        }
+        else
+        {
+            phosphor::logging::log<phosphor::logging::level::ERR>(
+                "FRU does not include MAC address information");
+        }
+    }
+    else
+    {
+        phosphor::logging::log<phosphor::logging::level::ERR>(
+            "FRU does not include Board Information Area");
+    }
+
+    return retVal;
+}
+
+/** @brief write FRU data to EEPROM
+ *  @param - busIdx and address of I2C device.
+ *         - fruData: FRU data
+ *  @returns - true if successfully, false if fail
+ */
+static bool writeFruData(uint8_t busIdx, uint8_t addr,
+                         std::vector<uint8_t>& fruData)
+{
+    bool retVal = false;
+    sd_bus* bus = NULL;
+    int ret = sd_bus_default_system(&bus);
+
+    if (ret < 0)
+    {
+        phosphor::logging::log<phosphor::logging::level::ERR>(
+            "Failed to connect to system bus",
+            phosphor::logging::entry("ERRNO=0x%X", -ret));
+    }
+    else
+    {
+        sdbusplus::bus::bus dbus(bus);
+        auto MapperCall = dbus.new_method_call(
+            fruDeviceServiceName, "/xyz/openbmc_project/FruDevice",
+            "xyz.openbmc_project.FruDeviceManager", "WriteFru");
+
+        MapperCall.append(busIdx, addr, fruData);
+
+        try
+        {
+            auto mapperReply = dbus.call(MapperCall);
+            retVal = true;
+        }
+        catch (sdbusplus::exception_t& e)
+        {
+            log<level::ERR>("Fail to Write FRU data via system bus\n");
+        }
+    }
+
+    sd_bus_unref(bus);
+
+    return retVal;
+}
+
 /** @brief execute a command and get the output of the command
  *  @param[in] the command
  *  @returns output of the command
  */
-std::string exec(const char* cmd) {
+std::string exec(const char* cmd)
+{
     char buffer[128];
     std::string result = "";
     /* Pipe stream from a command */
     FILE* pipe = popen(cmd, "r");
-    if (!pipe) throw std::runtime_error("popen() failed!");
-    try {
+    if (!pipe)
+        throw std::runtime_error("popen() failed!");
+    try
+    {
         /* Reads a line from the specified stream and stores it */
-        while (fgets(buffer, sizeof buffer, pipe) != NULL) {
+        while (fgets(buffer, sizeof buffer, pipe) != NULL)
+        {
             result += buffer;
         }
-    } catch (...) {
+    }
+    catch (...)
+    {
         pclose(pipe);
         throw;
     }
@@ -89,7 +358,7 @@
             }
         }
     }
-    catch(const std::exception& e)
+    catch (const std::exception& e)
     {
         log<level::ERR>(e.what());
         return responseFailure();
@@ -98,10 +367,65 @@
     return ipmi::responseSuccess();
 }
 
+/** @brief implements ipmi oem command edit MAC address
+ *  @param - new macAddress
+ *  @returns - Fail or Success.
+ */
+ipmi::RspType<uint8_t> ipmiDocmdSetMacAddress(std::vector<uint8_t> macAddress)
+{
+    std::vector<uint8_t> fruData;
+    uint8_t busIdx = 0;
+    uint8_t addrss = 0;
+
+    if (macAddress.size() != 6)
+    {
+        log<level::ERR>("new MAC address is invalid");
+        return responseFailure();
+    }
+
+#if defined(MAC_ADDRESS_FRU_BUS) && defined(MAC_ADDRESS_FRU_ADDR)
+    /* Set BUS and Address of FRU device that includes MAC address */
+    busIdx = MAC_ADDRESS_FRU_BUS;
+    addrss = MAC_ADDRESS_FRU_ADDR;
+#else
+    /* Calculate BUS and Address of FRU device that includes MAC address */
+    if (!getBaseBoardFRUAddr(busIdx, addrss))
+    {
+        log<level::ERR>(
+            "Can not get the bus and address of baseboard FRU device");
+        return responseFailure();
+    }
+#endif
+
+    if (!getRawFruData(busIdx, addrss, fruData))
+    {
+        log<level::ERR>("Can not get raw FRU data");
+        return responseFailure();
+    }
+
+    if (!updateMACAddrInFRU(fruData, macAddress))
+    {
+        log<level::ERR>("Can not update MAC address");
+        return responseFailure();
+    }
+
+    if (!writeFruData(busIdx, addrss, fruData))
+    {
+        log<level::ERR>("Can not Write FRU data");
+        return responseFailure();
+    }
+
+    return ipmi::responseSuccess(macAddress.size());
+}
+
 void registerOEMFunctions() __attribute__((constructor));
 void registerOEMFunctions()
 {
     ipmi::registerHandler(ipmi::prioOemBase, ipmi::ampere::netFnAmpere,
-                          ipmi::general::cmdSyncRtcTime,
-                          ipmi::Privilege::User, ipmiSyncRTCTimeToBMC);
+                          ipmi::general::cmdSyncRtcTime, ipmi::Privilege::User,
+                          ipmiSyncRTCTimeToBMC);
+
+    ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::ampere::netFnAmpere,
+                          ipmi::general::cmdEditBmcMacAdr,
+                          ipmi::Privilege::User, ipmiDocmdSetMacAddress);
 }