tools/handler: Read the running version

A host tool would read the running firmware version through polling the
version blob state.

Signed-off-by: Jie Yang <jjy@google.com>
Change-Id: I0d68fff6527cd52360abee1cb225a8f228d68392
diff --git a/tools/handler.cpp b/tools/handler.cpp
index d175e85..8cd7a99 100644
--- a/tools/handler.cpp
+++ b/tools/handler.cpp
@@ -141,6 +141,57 @@
     return (success == true);
 }
 
+std::vector<uint8_t> UpdateHandler::readVersion(const std::string& versionBlob)
+{
+    std::uint16_t session;
+
+    try
+    {
+        session = blob->openBlob(
+            versionBlob, static_cast<std::uint16_t>(
+                             ipmi_flash::FirmwareFlags::UpdateFlags::openRead));
+    }
+    catch (const ipmiblob::BlobException& b)
+    {
+        throw ToolException("blob exception received: " +
+                            std::string(b.what()));
+    }
+
+    std::fprintf(stderr, "Calling stat on %s session to check status\n",
+                 versionBlob.c_str());
+    std::vector<uint8_t> data;
+
+    /* TODO: call readBytes multiple times in case IPMI message length exceeds
+     * IPMI_MAX_MSG_LENGTH.
+     */
+    auto pollResp = pollReadReady(session, blob);
+    if (pollResp.first)
+    {
+        std::fprintf(stderr, "Returned success\n");
+        if (pollResp.second > 0)
+        {
+            try
+            {
+                data = blob->readBytes(session, 0, pollResp.second);
+            }
+            catch (const ipmiblob::BlobException& b)
+            {
+                blob->closeBlob(session);
+                throw ToolException("blob exception received: " +
+                                    std::string(b.what()));
+            }
+        }
+    }
+    else
+    {
+        std::fprintf(stderr, "Returned non-success (could still "
+                             "be running (unlikely))\n");
+    }
+
+    blob->closeBlob(session);
+    return data;
+}
+
 void UpdateHandler::cleanArtifacts()
 {
     /* open(), commit(), close() */
diff --git a/tools/handler.hpp b/tools/handler.hpp
index 5820fba..f4bf843 100644
--- a/tools/handler.hpp
+++ b/tools/handler.hpp
@@ -44,6 +44,15 @@
     virtual bool verifyFile(const std::string& target, bool ignoreStatus) = 0;
 
     /**
+     * Read the active firmware version.
+     *
+     * @param[in] versionBlob - the version blob id within the version handler.
+     * @return firmware version
+     */
+    virtual std::vector<uint8_t>
+        readVersion(const std::string& versionBlob) = 0;
+
+    /**
      * Cleanup the artifacts by triggering this action.
      */
     virtual void cleanArtifacts() = 0;
@@ -71,6 +80,8 @@
      */
     bool verifyFile(const std::string& target, bool ignoreStatus) override;
 
+    std::vector<uint8_t> readVersion(const std::string& versionBlob) override;
+
     void cleanArtifacts() override;
 
   private:
diff --git a/tools/helper.cpp b/tools/helper.cpp
index d3790de..232f482 100644
--- a/tools/helper.cpp
+++ b/tools/helper.cpp
@@ -19,10 +19,13 @@
 #include "status.hpp"
 #include "tool_errors.hpp"
 
+#include <blobs-ipmid/blobs.hpp>
 #include <ipmiblob/blob_errors.hpp>
+#include <ipmiblob/blob_interface.hpp>
 
 #include <chrono>
 #include <thread>
+#include <utility>
 
 namespace host_tool
 {
@@ -110,6 +113,67 @@
     return (result == ipmi_flash::ActionStatus::success);
 }
 
+/* Poll an open blob session for reading.
+ *
+ * The committing bit indicates that the blob is not available for reading now
+ * and the reader might come back and check the state later.
+ *
+ * Polling finishes under the following conditions:
+ * - The open_read bit set -> stat successful
+ * - The open_read and committing bits unset -> stat failed;
+ * - Blob exception was received;
+ * - Time ran out.
+ * Polling continues when the open_read bit unset and committing bit set.
+ * If the blob is not open_read and not committing, then it is an error to the
+ * reader.
+ */
+std::pair<bool, uint32_t> pollReadReady(std::uint16_t session,
+                                        ipmiblob::BlobInterface* blob)
+{
+    using namespace std::chrono_literals;
+    static constexpr auto pollingSleep = 5s;
+    ipmiblob::StatResponse blobStatResp;
+
+    try
+    {
+        /* Polling lasts 5 minutes. When opening a version blob, the system
+         * unit defined in the version handler will extract the running version
+         * from the image on the flash.
+         */
+        static constexpr int commandAttempts = 60;
+        int attempts = 0;
+
+        while (attempts++ < commandAttempts)
+        {
+            blobStatResp = blob->getStat(session);
+
+            if (blobStatResp.blob_state & blobs::StateFlags::open_read)
+            {
+                std::fprintf(stderr, "success\n");
+                return std::make_pair(true, blobStatResp.size);
+            }
+            else if (blobStatResp.blob_state & blobs::StateFlags::committing)
+            {
+                std::fprintf(stderr, "running\n");
+            }
+            else
+            {
+                std::fprintf(stderr, "failed\n");
+                return std::make_pair(false, 0);
+            }
+
+            std::this_thread::sleep_for(pollingSleep);
+        }
+    }
+    catch (const ipmiblob::BlobException& b)
+    {
+        throw ToolException("blob exception received: " +
+                            std::string(b.what()));
+    }
+
+    return std::make_pair(false, 0);
+}
+
 void* memcpyAligned(void* destination, const void* source, std::size_t size)
 {
     std::size_t i = 0;
diff --git a/tools/helper.hpp b/tools/helper.hpp
index 9acf58d..9b987ac 100644
--- a/tools/helper.hpp
+++ b/tools/helper.hpp
@@ -12,11 +12,21 @@
  *
  * @param[in] session - the open verification session
  * @param[in] blob - pointer to blob interface implementation object.
- * @return true if the verification was successul.
+ * @return true if the verification was successful.
  */
 bool pollStatus(std::uint16_t session, ipmiblob::BlobInterface* blob);
 
 /**
+ * Poll an open firmware version blob session and check if it ready to read.
+ *
+ * @param[in] session - the open firmware version blob session
+ * @param[in] blob - pointer to blob interface implementation object
+ * @return the polling status and blob buffer size
+ */
+std::pair<bool, uint32_t> pollReadReady(std::uint16_t session,
+                                        ipmiblob::BlobInterface* blob);
+
+/**
  * Aligned memcpy
  * @param[out] destination - destination memory pointer
  * @param[in] source - source memory pointer
diff --git a/tools/test/tools_helper_unittest.cpp b/tools/test/tools_helper_unittest.cpp
index 9aac01a..2bee66d 100644
--- a/tools/test/tools_helper_unittest.cpp
+++ b/tools/test/tools_helper_unittest.cpp
@@ -1,6 +1,7 @@
 #include "helper.hpp"
 #include "status.hpp"
 
+#include <blobs-ipmid/blobs.hpp>
 #include <ipmiblob/test/blob_interface_mock.hpp>
 
 #include <cstdint>
@@ -45,6 +46,65 @@
     EXPECT_FALSE(pollStatus(session, &blobMock));
 }
 
+TEST_F(HelperTest, PollReadReadyReturnsAfterSuccess)
+{
+    ipmiblob::StatResponse blobResponse = {};
+    /* the other details of the response are ignored, and should be. */
+    blobResponse.blob_state =
+        blobs::StateFlags::open_read | blobs::StateFlags::committed;
+
+    EXPECT_CALL(blobMock, getStat(TypedEq<std::uint16_t>(session)))
+        .WillOnce(Return(blobResponse));
+
+    EXPECT_TRUE(pollReadReady(session, &blobMock).first);
+}
+
+TEST_F(HelperTest, PollReadReadyReturnsAfterFailure)
+{
+    ipmiblob::StatResponse blobResponse = {};
+    /* the other details of the response are ignored, and should be. */
+    blobResponse.blob_state = blobs::StateFlags::commit_error;
+
+    EXPECT_CALL(blobMock, getStat(TypedEq<std::uint16_t>(session)))
+        .WillOnce(Return(blobResponse));
+
+    EXPECT_FALSE(pollReadReady(session, &blobMock).first);
+}
+
+TEST_F(HelperTest, PollReadReadyReturnsAfterRetrySuccess)
+{
+    ipmiblob::StatResponse blobResponseRunning = {};
+    /* the other details of the response are ignored, and should be. */
+    blobResponseRunning.blob_state = blobs::StateFlags::committing;
+
+    ipmiblob::StatResponse blobResponseReadReady = {};
+    /* the other details of the response are ignored, and should be. */
+    blobResponseReadReady.blob_state = blobs::StateFlags::open_read;
+
+    EXPECT_CALL(blobMock, getStat(TypedEq<std::uint16_t>(session)))
+        .WillOnce(Return(blobResponseRunning))
+        .WillOnce(Return(blobResponseReadReady));
+
+    EXPECT_TRUE(pollReadReady(session, &blobMock).first);
+}
+
+TEST_F(HelperTest, PollReadReadyReturnsAfterRetryFailure)
+{
+    ipmiblob::StatResponse blobResponseRunning = {};
+    /* the other details of the response are ignored, and should be. */
+    blobResponseRunning.blob_state = blobs::StateFlags::committing;
+
+    ipmiblob::StatResponse blobResponseError = {};
+    /* the other details of the response are ignored, and should be. */
+    blobResponseError.blob_state = blobs::StateFlags::commit_error;
+
+    EXPECT_CALL(blobMock, getStat(TypedEq<std::uint16_t>(session)))
+        .WillOnce(Return(blobResponseRunning))
+        .WillOnce(Return(blobResponseError));
+
+    EXPECT_FALSE(pollReadReady(session, &blobMock).first);
+}
+
 TEST_F(HelperTest, MemcpyAlignedOneByte)
 {
     const char source = 'a';
diff --git a/tools/test/tools_updater_unittest.cpp b/tools/test/tools_updater_unittest.cpp
index bb28ecb..824cec9 100644
--- a/tools/test/tools_updater_unittest.cpp
+++ b/tools/test/tools_updater_unittest.cpp
@@ -6,6 +6,7 @@
 #include "updater_mock.hpp"
 #include "util.hpp"
 
+#include <blobs-ipmid/blobs.hpp>
 #include <ipmiblob/blob_errors.hpp>
 #include <ipmiblob/test/blob_interface_mock.hpp>
 
@@ -20,6 +21,7 @@
 
 using ::testing::_;
 using ::testing::Eq;
+using ::testing::IsEmpty;
 using ::testing::Return;
 using ::testing::Throw;
 using ::testing::TypedEq;
@@ -172,6 +174,64 @@
                  ToolException);
 }
 
+TEST_F(UpdateHandlerTest, ReadVerisonReturnExpected)
+{
+    /* It can return as expected, when polling and readBytes succeeds. */
+    EXPECT_CALL(blobMock, openBlob(ipmi_flash::biosVersionBlobId, _))
+        .WillOnce(Return(session));
+    ipmiblob::StatResponse readVersionResponse = {};
+    readVersionResponse.blob_state =
+        blobs::StateFlags::open_read | blobs::StateFlags::committed;
+    readVersionResponse.size = 10;
+    EXPECT_CALL(blobMock, getStat(TypedEq<std::uint16_t>(session)))
+        .WillOnce(Return(readVersionResponse));
+    std::vector<uint8_t> resp = {0x2d, 0xfe};
+    EXPECT_CALL(blobMock, readBytes(session, 0, _)).WillOnce(Return(resp));
+
+    EXPECT_CALL(blobMock, closeBlob(session)).WillOnce(Return());
+    EXPECT_EQ(resp, updater.readVersion(ipmi_flash::biosVersionBlobId));
+}
+
+TEST_F(UpdateHandlerTest, ReadVersionExceptionWhenPollingSucceedsReadBytesFails)
+{
+    /* On readBytes, it can except. */
+    EXPECT_CALL(blobMock, openBlob(ipmi_flash::biosVersionBlobId, _))
+        .WillOnce(Return(session));
+    ipmiblob::StatResponse readVersionResponse = {};
+    readVersionResponse.blob_state =
+        blobs::StateFlags::open_read | blobs::StateFlags::committed;
+    readVersionResponse.size = 10;
+    EXPECT_CALL(blobMock, getStat(TypedEq<std::uint16_t>(session)))
+        .WillOnce(Return(readVersionResponse));
+    EXPECT_CALL(blobMock, readBytes(session, 0, _))
+        .WillOnce(Throw(ipmiblob::BlobException("asdf")));
+    EXPECT_CALL(blobMock, closeBlob(session)).WillOnce(Return());
+    EXPECT_THROW(updater.readVersion(ipmi_flash::biosVersionBlobId),
+                 ToolException);
+}
+
+TEST_F(UpdateHandlerTest, ReadVersionReturnsEmptyIfPollingFails)
+{
+    /* It can return an empty result, when polling fails. */
+    EXPECT_CALL(blobMock, openBlob(ipmi_flash::biosVersionBlobId, _))
+        .WillOnce(Return(session));
+    ipmiblob::StatResponse readVersionResponse = {};
+    readVersionResponse.blob_state = blobs::StateFlags::commit_error;
+    EXPECT_CALL(blobMock, getStat(TypedEq<std::uint16_t>(session)))
+        .WillOnce(Return(readVersionResponse));
+    EXPECT_CALL(blobMock, closeBlob(session)).WillOnce(Return());
+    EXPECT_THAT(updater.readVersion(ipmi_flash::biosVersionBlobId), IsEmpty());
+}
+
+TEST_F(UpdateHandlerTest, ReadVersionCovertsOpenBlobExceptionToToolException)
+{
+    /* On open, it can except and this is converted to a ToolException. */
+    EXPECT_CALL(blobMock, openBlob(ipmi_flash::biosVersionBlobId, _))
+        .WillOnce(Throw(ipmiblob::BlobException("asdf")));
+    EXPECT_THROW(updater.readVersion(ipmi_flash::biosVersionBlobId),
+                 ToolException);
+}
+
 TEST_F(UpdateHandlerTest, CleanArtifactsSkipsCleanupIfUnableToOpen)
 {
     /* It only tries to commit if it's able to open the blob.  However, if
diff --git a/tools/test/updater_mock.hpp b/tools/test/updater_mock.hpp
index 9469c62..05527b2 100644
--- a/tools/test/updater_mock.hpp
+++ b/tools/test/updater_mock.hpp
@@ -2,7 +2,9 @@
 
 #include "updater.hpp"
 
+#include <cstdint>
 #include <string>
+#include <vector>
 
 #include <gmock/gmock.h>
 
@@ -13,6 +15,7 @@
 {
   public:
     MOCK_METHOD1(checkAvailable, bool(const std::string&));
+    MOCK_METHOD1(readVersion, std::vector<uint8_t>(const std::string&));
     MOCK_METHOD2(sendFile, void(const std::string&, const std::string&));
     MOCK_METHOD2(verifyFile, bool(const std::string&, bool));
     MOCK_METHOD0(cleanArtifacts, void());