tools: implement method to send ipmi packet to BMC

Implement method to send ipmi packets to BMC.  Only currently supports
the system interface.

Tested: Verified on a host that has the BLOB handler installed on the
BMC, that it received a blob count of 0, as there were no handlers
installed at that test.  Further functional tests will verify cases with
the firmware handler installed, however, it is verified that this code
correctly speaks IPMI to the BLOB handler registered.

Change-Id: I696f3915930cc9fa5e768fbdeed484ea6cb707a2
Signed-off-by: Patrick Venture <venture@google.com>
diff --git a/test/ipmi_interface_mock.hpp b/test/ipmi_interface_mock.hpp
index 409ef44..374dc93 100644
--- a/test/ipmi_interface_mock.hpp
+++ b/test/ipmi_interface_mock.hpp
@@ -12,7 +12,7 @@
   public:
     virtual ~IpmiInterfaceMock() = default;
     MOCK_METHOD1(sendPacket,
-                 std::vector<std::uint8_t>(const std::vector<std::uint8_t>&));
+                 std::vector<std::uint8_t>(std::vector<std::uint8_t>&));
 };
 
 } // namespace host_tool
diff --git a/test/tools_ipmi_unittest.cpp b/test/tools_ipmi_unittest.cpp
index 8a72489..d0c9911 100644
--- a/test/tools_ipmi_unittest.cpp
+++ b/test/tools_ipmi_unittest.cpp
@@ -1,4 +1,5 @@
 #include "internal_sys_mock.hpp"
+#include "ipmi_errors.hpp"
 #include "ipmi_handler.hpp"
 
 namespace host_tool
@@ -14,7 +15,7 @@
     IpmiHandler ipmi(&sysMock);
 
     EXPECT_CALL(sysMock, open(_, _)).WillRepeatedly(Return(-1));
-    EXPECT_EQ(false, ipmi.open());
+    EXPECT_THROW(ipmi.open(), IpmiException);
 }
 
 } // namespace host_tool
diff --git a/tools/ipmi_errors.hpp b/tools/ipmi_errors.hpp
index 994f837..210ef53 100644
--- a/tools/ipmi_errors.hpp
+++ b/tools/ipmi_errors.hpp
@@ -16,6 +16,7 @@
         smessage << "Received IPMI_CC: " << cc;
         message = smessage.str();
     }
+    explicit IpmiException(const std::string& message) : message(message){};
 
     virtual const char* what() const noexcept override
     {
diff --git a/tools/ipmi_handler.cpp b/tools/ipmi_handler.cpp
index e1e0529..9278338 100644
--- a/tools/ipmi_handler.cpp
+++ b/tools/ipmi_handler.cpp
@@ -16,8 +16,16 @@
 
 #include "ipmi_handler.hpp"
 
-#include <fcntl.h>
+#include "ipmi_errors.hpp"
 
+#include <fcntl.h>
+#include <linux/ipmi.h>
+#include <linux/ipmi_msgdefs.h>
+#include <sys/ioctl.h>
+
+#include <array>
+#include <cstdint>
+#include <cstring>
 #include <sstream>
 #include <string>
 #include <vector>
@@ -25,7 +33,7 @@
 namespace host_tool
 {
 
-bool IpmiHandler::open()
+void IpmiHandler::open()
 {
     const int device = 0;
     const std::vector<std::string> formats = {"/dev/ipmi", "/dev/ipmi/",
@@ -44,16 +52,114 @@
         break;
     }
 
-    /* Should we throw an exception here, since we exhausted these options?
-     * Maybe. :D
-     */
-    return !(fd < 0);
+    if (fd < 0)
+    {
+        throw IpmiException("Unable to open any ipmi devices");
+    }
 }
 
 std::vector<std::uint8_t>
-    IpmiHandler::sendPacket(const std::vector<std::uint8_t>& data)
+    IpmiHandler::sendPacket(std::vector<std::uint8_t>& data)
 {
-    return {};
+    if (fd < 0)
+    {
+        open();
+    }
+
+    constexpr int ipmiOEMNetFn = 46;
+    constexpr int ipmiOEMLun = 0;
+    /* /openbmc/phosphor-host-ipmid/blob/master/host-ipmid/oemopenbmc.hpp */
+    constexpr int ipmiOEMBlobCmd = 128;
+    constexpr int fifteenMs = 15 * 1000;
+    constexpr int ipmiReadTimeout = fifteenMs;
+    constexpr int ipmiResponseBufferLen = IPMI_MAX_MSG_LENGTH;
+    constexpr int ipmiOk = 0;
+
+    /* We have a handle to the IPMI device. */
+    std::array<std::uint8_t, ipmiResponseBufferLen> responseBuffer;
+
+    /* Build address. */
+    struct ipmi_system_interface_addr systemAddress;
+    systemAddress.addr_type = IPMI_SYSTEM_INTERFACE_ADDR_TYPE;
+    systemAddress.channel = IPMI_BMC_CHANNEL;
+    systemAddress.lun = ipmiOEMLun;
+
+    /* Build request. */
+    struct ipmi_req request;
+    std::memset(&request, 0, sizeof(request));
+    request.addr = reinterpret_cast<unsigned char*>(&systemAddress);
+    request.addr_len = sizeof(systemAddress);
+    request.msgid = sequence++;
+    request.msg.data = reinterpret_cast<unsigned char*>(data.data());
+    request.msg.data_len = data.size();
+    request.msg.netfn = ipmiOEMNetFn;
+    request.msg.cmd = ipmiOEMBlobCmd;
+
+    struct ipmi_recv reply;
+    reply.addr = reinterpret_cast<unsigned char*>(&systemAddress);
+    reply.addr_len = sizeof(systemAddress);
+    reply.msg.data = reinterpret_cast<unsigned char*>(responseBuffer.data());
+    reply.msg.data_len = responseBuffer.size();
+
+    /* Try to send request. */
+    int rc = sys->ioctl(fd, IPMICTL_SEND_COMMAND, &request);
+    if (rc < 0)
+    {
+        throw IpmiException("Unable to send IPMI request.");
+    }
+
+    /* Could use sdeventplus, but for only one type of event is it worth it? */
+    struct pollfd pfd;
+    pfd.fd = fd;
+    pfd.events = POLLIN;
+
+    do
+    {
+        rc = sys->poll(&pfd, 1, ipmiReadTimeout);
+        if (rc < 0)
+        {
+            if (errno == EINTR)
+            {
+                continue;
+            }
+            throw IpmiException("Error occurred.");
+        }
+        else if (rc == 0)
+        {
+            throw IpmiException("Timeout waiting for reply.");
+        }
+
+        /* Yay, happy case! */
+        rc = sys->ioctl(fd, IPMICTL_RECEIVE_MSG_TRUNC, &reply);
+        if (rc < 0)
+        {
+            throw IpmiException("Unable to read reply.");
+        }
+
+        if (request.msgid != reply.msgid)
+        {
+            std::fprintf(stderr, "Received wrong message, trying again.\n");
+        }
+    } while (request.msgid != reply.msgid);
+
+    if (responseBuffer[0] != ipmiOk)
+    {
+        throw IpmiException(static_cast<int>(responseBuffer[0]));
+    }
+
+    std::vector<std::uint8_t> returning;
+    auto dataLen = reply.msg.data_len - 1;
+
+    returning.insert(returning.begin(), responseBuffer.begin() + 1,
+                     responseBuffer.begin() + dataLen + 1);
+
+    for (const auto& byte : returning)
+    {
+        std::fprintf(stderr, "0x%02x ", byte);
+    }
+    std::fprintf(stderr, "\n");
+
+    return returning;
 }
 
 } // namespace host_tool
diff --git a/tools/ipmi_handler.hpp b/tools/ipmi_handler.hpp
index 48e6def..1c91bff 100644
--- a/tools/ipmi_handler.hpp
+++ b/tools/ipmi_handler.hpp
@@ -3,6 +3,8 @@
 #include "internal/sys.hpp"
 #include "ipmi_interface.hpp"
 
+#include <vector>
+
 namespace host_tool
 {
 
@@ -21,12 +23,15 @@
     /**
      * Attempt to open the device node.
      *
-     * @return true on success, failure otherwise.
+     * @throws IpmiException on failure.
      */
-    bool open();
+    void open();
 
+    /**
+     * @throws IpmiException on failure.
+     */
     std::vector<std::uint8_t>
-        sendPacket(const std::vector<std::uint8_t>& data) override;
+        sendPacket(std::vector<std::uint8_t>& data) override;
 
   private:
     const internal::Sys* sys;
@@ -34,6 +39,8 @@
      * allow moving this object.
      */
     int fd = -1;
+    /* The last IPMI sequence number we used. */
+    int sequence = 0;
 };
 
 } // namespace host_tool
diff --git a/tools/ipmi_interface.hpp b/tools/ipmi_interface.hpp
index 8be624c..6bad7db 100644
--- a/tools/ipmi_interface.hpp
+++ b/tools/ipmi_interface.hpp
@@ -19,7 +19,7 @@
      * @throws IpmiException on failure.
      */
     virtual std::vector<std::uint8_t>
-        sendPacket(const std::vector<std::uint8_t>& data) = 0;
+        sendPacket(std::vector<std::uint8_t>& data) = 0;
 };
 
 } // namespace host_tool