Add Intel-specific IPMI FRU commands

Includes commands that are required to support the 'ipmitool
fru print' command.

Change-Id: If7768de1441cc620ad6db4c99c4254305cf5bbbc
Signed-off-by: Jason M. Bills <jason.m.bills@linux.intel.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4362cc7..eb4a366 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -95,4 +95,12 @@
 target_link_libraries (sensorcommands ${CMAKE_THREAD_LIBS_INIT})
 target_link_libraries (sensorcommands sdbusplus)
 
-install (TARGETS oemcmds sensorcommands DESTINATION lib/ipmid-providers)
+add_library (zstoragecommands SHARED src/storagecommands.cpp)
+set_target_properties (zstoragecommands PROPERTIES VERSION "0.1.0")
+set_target_properties (zstoragecommands PROPERTIES SOVERSION "0")
+target_link_libraries (zstoragecommands ${CMAKE_THREAD_LIBS_INIT})
+
+install (
+    TARGETS oemcmds sensorcommands zstoragecommands
+    DESTINATION lib/ipmid-providers
+)
diff --git a/include/storagecommands.hpp b/include/storagecommands.hpp
index ef32c74..0d32ea5 100644
--- a/include/storagecommands.hpp
+++ b/include/storagecommands.hpp
@@ -66,8 +66,35 @@
     uint8_t allocUnitLargestFreeMSB;
     uint8_t maxRecordSize;
 };
+
+struct GetFRUAreaReq
+{
+    uint8_t fruDeviceID;
+    uint16_t fruInventoryOffset;
+    uint8_t countToRead;
+};
+
+struct GetFRUAreaResp
+{
+    uint8_t inventorySizeLSB;
+    uint8_t inventorySizeMSB;
+    uint8_t accessType;
+};
+
+struct WriteFRUDataReq
+{
+    uint8_t fruDeviceID;
+    uint16_t fruInventoryOffset;
+    uint8_t data[];
+};
 #pragma pack(pop)
 
+enum class GetFRUAreaAccessType : uint8_t
+{
+    byte = 0x0,
+    words = 0x1
+};
+
 enum class SensorTypeCodes : uint8_t
 {
     reserved = 0x0,
@@ -107,6 +134,20 @@
     ipmiCmdSetSELTime = 0x49,
 };
 
+#pragma pack(push, 1)
+struct FRUHeader
+{
+    uint8_t commonHeaderFormat;
+    uint8_t internalOffset;
+    uint8_t chassisOffset;
+    uint8_t boardOffset;
+    uint8_t productOffset;
+    uint8_t multiRecordOffset;
+    uint8_t pad;
+    uint8_t checksum;
+};
+#pragma pack(pop)
+
 namespace ipmi
 {
 namespace storage
diff --git a/src/storagecommands.cpp b/src/storagecommands.cpp
index e71d47f..6a8da91 100644
--- a/src/storagecommands.cpp
+++ b/src/storagecommands.cpp
@@ -30,6 +30,7 @@
 namespace storage
 {
 
+constexpr static const size_t maxMessageSize = 64;
 constexpr static const size_t maxFruSdrNameSize = 16;
 using ManagedObjectType = boost::container::flat_map<
     sdbusplus::message::object_path,
@@ -41,6 +42,7 @@
         std::string, boost::container::flat_map<std::string, DbusVariant>>>;
 
 constexpr static const char* fruDeviceServiceName = "com.intel.FruDevice";
+constexpr static const size_t cacheTimeoutSeconds = 10;
 
 static std::vector<uint8_t> fruCache;
 static uint8_t cacheBus = 0xFF;
@@ -52,6 +54,7 @@
 // collision to verify our dev-id
 boost::container::flat_map<uint8_t, std::pair<uint8_t, uint8_t>> deviceHashes;
 
+void registerStorageFunctions() __attribute__((constructor));
 static sdbusplus::bus::bus dbus(ipmid_get_sd_bus_connection());
 
 bool writeFru()
@@ -74,6 +77,14 @@
     return true;
 }
 
+void createTimer()
+{
+    if (cacheTimer == nullptr)
+    {
+        cacheTimer = std::make_unique<phosphor::Timer>(writeFru);
+    }
+}
+
 ipmi_ret_t replaceCacheFru(uint8_t devId)
 {
     static uint8_t lastDevId = 0xFF;
@@ -194,6 +205,179 @@
     return IPMI_CC_OK;
 }
 
+ipmi_ret_t ipmiStorageReadFRUData(ipmi_netfn_t netfn, ipmi_cmd_t cmd,
+                                  ipmi_request_t request,
+                                  ipmi_response_t response,
+                                  ipmi_data_len_t dataLen,
+                                  ipmi_context_t context)
+{
+    if (*dataLen != 4)
+    {
+        *dataLen = 0;
+        return IPMI_CC_REQ_DATA_LEN_INVALID;
+    }
+    *dataLen = 0; // default to 0 in case of an error
+
+    auto req = static_cast<GetFRUAreaReq*>(request);
+
+    if (req->countToRead > maxMessageSize - 1)
+    {
+        return IPMI_CC_INVALID_FIELD_REQUEST;
+    }
+    ipmi_ret_t status = replaceCacheFru(req->fruDeviceID);
+
+    if (status != IPMI_CC_OK)
+    {
+        return status;
+    }
+
+    size_t fromFRUByteLen = 0;
+    if (req->countToRead + req->fruInventoryOffset < fruCache.size())
+    {
+        fromFRUByteLen = req->countToRead;
+    }
+    else if (fruCache.size() > req->fruInventoryOffset)
+    {
+        fromFRUByteLen = fruCache.size() - req->fruInventoryOffset;
+    }
+    size_t padByteLen = req->countToRead - fromFRUByteLen;
+    uint8_t* respPtr = static_cast<uint8_t*>(response);
+    *respPtr = req->countToRead;
+    std::copy(fruCache.begin() + req->fruInventoryOffset,
+              fruCache.begin() + req->fruInventoryOffset + fromFRUByteLen,
+              ++respPtr);
+    // if longer than the fru is requested, fill with 0xFF
+    if (padByteLen)
+    {
+        respPtr += fromFRUByteLen;
+        std::fill(respPtr, respPtr + padByteLen, 0xFF);
+    }
+    *dataLen = fromFRUByteLen + 1;
+
+    return IPMI_CC_OK;
+}
+
+ipmi_ret_t ipmiStorageWriteFRUData(ipmi_netfn_t netfn, ipmi_cmd_t cmd,
+                                   ipmi_request_t request,
+                                   ipmi_response_t response,
+                                   ipmi_data_len_t dataLen,
+                                   ipmi_context_t context)
+{
+    if (*dataLen < 4 ||
+        *dataLen >=
+            0xFF + 3) // count written return is one byte, so limit to one byte
+                      // of data after the three request data bytes
+    {
+        *dataLen = 0;
+        return IPMI_CC_REQ_DATA_LEN_INVALID;
+    }
+
+    auto req = static_cast<WriteFRUDataReq*>(request);
+    size_t writeLen = *dataLen - 3;
+    *dataLen = 0; // default to 0 in case of an error
+
+    ipmi_ret_t status = replaceCacheFru(req->fruDeviceID);
+    if (status != IPMI_CC_OK)
+    {
+        return status;
+    }
+    int lastWriteAddr = req->fruInventoryOffset + writeLen;
+    if (fruCache.size() < lastWriteAddr)
+    {
+        fruCache.resize(req->fruInventoryOffset + writeLen);
+    }
+
+    std::copy(req->data, req->data + writeLen,
+              fruCache.begin() + req->fruInventoryOffset);
+
+    bool atEnd = false;
+
+    if (fruCache.size() >= sizeof(FRUHeader))
+    {
+
+        FRUHeader* header = reinterpret_cast<FRUHeader*>(fruCache.data());
+
+        int lastRecordStart = std::max(
+            header->internalOffset,
+            std::max(header->chassisOffset,
+                     std::max(header->boardOffset, header->productOffset)));
+        // TODO: Handle Multi-Record FRUs?
+
+        lastRecordStart *= 8; // header starts in are multiples of 8 bytes
+
+        // get the length of the area in multiples of 8 bytes
+        if (lastWriteAddr > (lastRecordStart + 1))
+        {
+            // second byte in record area is the length
+            int areaLength(fruCache[lastRecordStart + 1]);
+            areaLength *= 8; // it is in multiples of 8 bytes
+
+            if (lastWriteAddr >= (areaLength + lastRecordStart))
+            {
+                atEnd = true;
+            }
+        }
+    }
+    uint8_t* respPtr = static_cast<uint8_t*>(response);
+    if (atEnd)
+    {
+        // cancel timer, we're at the end so might as well send it
+        cacheTimer->stop();
+        if (!writeFru())
+        {
+            return IPMI_CC_INVALID_FIELD_REQUEST;
+        }
+        *respPtr = std::min(fruCache.size(), static_cast<size_t>(0xFF));
+    }
+    else
+    {
+        // start a timer, if no further data is sent in cacheTimeoutSeconds
+        // seconds, check to see if it is valid
+        createTimer();
+        cacheTimer->start(std::chrono::duration_cast<std::chrono::microseconds>(
+            std::chrono::seconds(cacheTimeoutSeconds)));
+        *respPtr = 0;
+    }
+
+    *dataLen = 1;
+
+    return IPMI_CC_OK;
+}
+
+ipmi_ret_t ipmiStorageGetFRUInvAreaInfo(ipmi_netfn_t netfn, ipmi_cmd_t cmd,
+                                        ipmi_request_t request,
+                                        ipmi_response_t response,
+                                        ipmi_data_len_t dataLen,
+                                        ipmi_context_t context)
+{
+    if (*dataLen != 1)
+    {
+        *dataLen = 0;
+        return IPMI_CC_REQ_DATA_LEN_INVALID;
+    }
+    *dataLen = 0; // default to 0 in case of an error
+
+    uint8_t reqDev = *(static_cast<uint8_t*>(request));
+    if (reqDev == 0xFF)
+    {
+        return IPMI_CC_INVALID_FIELD_REQUEST;
+    }
+    ipmi_ret_t status = replaceCacheFru(reqDev);
+
+    if (status != IPMI_CC_OK)
+    {
+        return status;
+    }
+
+    GetFRUAreaResp* respPtr = static_cast<GetFRUAreaResp*>(response);
+    respPtr->inventorySizeLSB = fruCache.size() & 0xFF;
+    respPtr->inventorySizeMSB = fruCache.size() >> 8;
+    respPtr->accessType = static_cast<uint8_t>(GetFRUAreaAccessType::byte);
+
+    *dataLen = sizeof(GetFRUAreaResp);
+    return IPMI_CC_OK;
+}
+
 ipmi_ret_t getFruSdrCount(size_t& count)
 {
     ipmi_ret_t ret = replaceCacheFru(0);
@@ -311,5 +495,26 @@
 
     return IPMI_CC_OK;
 }
+
+void registerStorageFunctions()
+{
+    // <Get FRU Inventory Area Info>
+    ipmiPrintAndRegister(
+        NETFUN_STORAGE,
+        static_cast<ipmi_cmd_t>(IPMINetfnStorageCmds::ipmiCmdGetFRUInvAreaInfo),
+        NULL, ipmiStorageGetFRUInvAreaInfo, PRIVILEGE_OPERATOR);
+
+    // <Add READ FRU Data
+    ipmiPrintAndRegister(
+        NETFUN_STORAGE,
+        static_cast<ipmi_cmd_t>(IPMINetfnStorageCmds::ipmiCmdReadFRUData), NULL,
+        ipmiStorageReadFRUData, PRIVILEGE_OPERATOR);
+
+    // <Add WRITE FRU Data
+    ipmiPrintAndRegister(
+        NETFUN_STORAGE,
+        static_cast<ipmi_cmd_t>(IPMINetfnStorageCmds::ipmiCmdWriteFRUData),
+        NULL, ipmiStorageWriteFRUData, PRIVILEGE_OPERATOR);
+}
 } // namespace storage
 } // namespace ipmi
\ No newline at end of file