Add suppport for retrieving machine name

This adds support for reading /etc/os-release, parsing out
OPENBMC_TARGET_MACHINE and returning this to the caller.

Change-Id: If2a419b9a77597686f5137efce97b1150142f181
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/Makefile.am b/Makefile.am
index 43f65aa..34e60bd 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -47,6 +47,7 @@
 	psu.cpp \
 	pcie_i2c.cpp \
 	entity_name.cpp \
+	machine_name.cpp \
 	handler.cpp \
 	util.cpp \
 	ipmi.cpp
diff --git a/README.md b/README.md
index c13e717..e0a09d4 100644
--- a/README.md
+++ b/README.md
@@ -162,3 +162,22 @@
 |0x00|0x06|Subcommand
 |0x01|Entity name length (say N)|Entity name length
 |0x02...0x02 + N - 1|Entity name|Entity name without null terminator
+
+### GetMachineName - SubCommand 0x07
+
+The BMC parses /etc/os-release for the OPENBMC_TARGET_MACHINE field and returns
+its value.
+
+Request
+
+|Byte(s) |Value |Data
+|--------|------|----
+|0x00|0x06|Subcommand
+
+Response
+
+|Byte(s) |Value |Data
+|--------|------|----
+|0x00|0x06|Subcommand
+|0x01|Model name length (say N)|Model name length
+|0x02...0x02 + N - 1|Model name|Model name without null terminator
diff --git a/commands.hpp b/commands.hpp
index dfe8f29..e8f1efa 100644
--- a/commands.hpp
+++ b/commands.hpp
@@ -21,6 +21,8 @@
     SysPcieSlotI2cBusMapping = 5,
     // The Sys "entity id:entity instance" to entity name mapping command.
     SysEntityName = 6,
+    // Returns the machine name of the image
+    SysMachineName = 7,
 };
 
 } // namespace ipmi
diff --git a/handler.cpp b/handler.cpp
index 4605e2f..028c97b 100644
--- a/handler.cpp
+++ b/handler.cpp
@@ -33,6 +33,7 @@
 #include <sdbusplus/bus.hpp>
 #include <sstream>
 #include <string>
+#include <string_view>
 #include <tuple>
 #include <xyz/openbmc_project/Common/error.hpp>
 
@@ -233,6 +234,47 @@
     return entityName;
 }
 
+std::string Handler::getMachineName()
+{
+    const char* path = "/etc/os-release";
+    std::ifstream ifs(path);
+    if (ifs.fail())
+    {
+        std::fprintf(stderr, "Failed to open: %s\n", path);
+        throw IpmiException(IPMI_CC_UNSPECIFIED_ERROR);
+    }
+
+    std::string line;
+    while (true)
+    {
+        std::getline(ifs, line);
+        if (ifs.eof())
+        {
+            std::fprintf(stderr, "Failed to find OPENBMC_TARGET_MACHINE: %s\n",
+                         path);
+            throw IpmiException(IPMI_CC_INVALID);
+        }
+        if (ifs.fail())
+        {
+            std::fprintf(stderr, "Failed to read: %s\n", path);
+            throw IpmiException(IPMI_CC_UNSPECIFIED_ERROR);
+        }
+        std::string_view lineView(line);
+        constexpr std::string_view prefix = "OPENBMC_TARGET_MACHINE=";
+        if (lineView.substr(0, prefix.size()) != prefix)
+        {
+            continue;
+        }
+        lineView.remove_prefix(prefix.size());
+        lineView.remove_prefix(
+            std::min(lineView.find_first_not_of('"'), lineView.size()));
+        lineView.remove_suffix(
+            lineView.size() - 1 -
+            std::min(lineView.find_last_not_of('"'), lineView.size() - 1));
+        return std::string(lineView);
+    }
+}
+
 std::string readNameFromConfig(const std::string& type, uint8_t instance,
                                const Json& config)
 {
diff --git a/handler.hpp b/handler.hpp
index 19fd1ea..766ddde 100644
--- a/handler.hpp
+++ b/handler.hpp
@@ -67,6 +67,14 @@
                                       std::uint8_t instance) = 0;
 
     /**
+     * Return the name of the machine, parsed from release information.
+     *
+     * @return the machine name
+     * @throw IpmiException on failure.
+     */
+    virtual std::string getMachineName() = 0;
+
+    /**
      * Populate the i2c-pcie mapping vector.
      */
     virtual void buildI2cPcieMapping() = 0;
diff --git a/handler_impl.hpp b/handler_impl.hpp
index b9946a1..130a4b0 100644
--- a/handler_impl.hpp
+++ b/handler_impl.hpp
@@ -28,6 +28,7 @@
     VersionTuple getCpldVersion(unsigned int id) const override;
     void psuResetDelay(std::uint32_t delay) const override;
     std::string getEntityName(std::uint8_t id, std::uint8_t instance) override;
+    std::string getMachineName() override;
     void buildI2cPcieMapping() override;
     size_t getI2cPcieMappingSize() const override;
     std::tuple<std::uint32_t, std::string>
diff --git a/ipmi.cpp b/ipmi.cpp
index d297efc..509ec15 100644
--- a/ipmi.cpp
+++ b/ipmi.cpp
@@ -22,6 +22,7 @@
 #include "entity_name.hpp"
 #include "eth.hpp"
 #include "handler.hpp"
+#include "machine_name.hpp"
 #include "pcie_i2c.hpp"
 #include "psu.hpp"
 
@@ -63,6 +64,8 @@
             return pcieSlotI2cBusMapping(reqBuf, replyCmdBuf, dataLen, handler);
         case SysEntityName:
             return getEntityName(reqBuf, replyCmdBuf, dataLen, handler);
+        case SysMachineName:
+            return getMachineName(reqBuf, replyCmdBuf, dataLen, handler);
         default:
             std::fprintf(stderr, "Invalid subcommand: 0x%x\n", reqBuf[0]);
             return IPMI_CC_INVALID;
diff --git a/machine_name.cpp b/machine_name.cpp
new file mode 100644
index 0000000..9fcbd10
--- /dev/null
+++ b/machine_name.cpp
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "machine_name.hpp"
+
+#include "errors.hpp"
+
+#include <cstddef>
+#include <cstdio>
+#include <cstring>
+#include <optional>
+#include <string>
+
+namespace google
+{
+namespace ipmi
+{
+
+struct GetMachineNameRequest
+{
+    uint8_t subcommand;
+} __attribute__((packed));
+
+struct GetMachineNameReply
+{
+    uint8_t subcommand;
+    uint8_t machineNameLength;
+    uint8_t machineName[0];
+} __attribute__((packed));
+
+ipmi_ret_t getMachineName(const uint8_t* reqBuf, uint8_t* replyBuf,
+                          size_t* dataLen, HandlerInterface* handler)
+{
+    GetMachineNameRequest request;
+    if (*dataLen < sizeof(request))
+    {
+        std::fprintf(stderr, "Invalid command length: %zu\n", *dataLen);
+        return IPMI_CC_REQ_DATA_LEN_INVALID;
+    }
+    std::memcpy(&request, reqBuf, sizeof(request));
+
+    static std::optional<std::string> machineName;
+    if (!machineName)
+    {
+        try
+        {
+            machineName = handler->getMachineName();
+        }
+        catch (const IpmiException& e)
+        {
+            return e.getIpmiError();
+        }
+    }
+
+    GetMachineNameReply reply;
+    size_t len = sizeof(reply) + machineName->size();
+    if (len > MAX_IPMI_BUFFER)
+    {
+        std::fprintf(stderr, "Response would overflow response buffer\n");
+        return IPMI_CC_INVALID;
+    }
+    reply.subcommand = request.subcommand;
+    reply.machineNameLength = machineName->size();
+    std::memcpy(replyBuf, &reply, sizeof(reply));
+    std::memcpy(replyBuf + sizeof(reply), machineName->data(),
+                machineName->size());
+    (*dataLen) = len;
+    return IPMI_CC_OK;
+}
+
+} // namespace ipmi
+} // namespace google
diff --git a/machine_name.hpp b/machine_name.hpp
new file mode 100644
index 0000000..7412929
--- /dev/null
+++ b/machine_name.hpp
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "handler.hpp"
+
+#include <ipmid/api.h>
+
+namespace google
+{
+namespace ipmi
+{
+
+// Handle the machine name command.
+ipmi_ret_t getMachineName(const uint8_t* reqBuf, uint8_t* replyBuf,
+                          size_t* dataLen, HandlerInterface* handler);
+
+} // namespace ipmi
+} // namespace google
diff --git a/test/Makefile.am b/test/Makefile.am
index 08b5a1c..d10f4ba 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -41,6 +41,10 @@
 entity_unittest_SOURCES = entity_unittest.cpp
 entity_unittest_LDADD = $(top_builddir)/libsyscmds_common.la
 
+check_PROGRAMS += machine_unittest
+machine_unittest_SOURCES = machine_unittest.cpp
+machine_unittest_LDADD = $(top_builddir)/libsyscmds_common.la
+
 check_PROGRAMS += pcie_unittest
 pcie_unittest_SOURCES = pcie_unittest.cpp
 pcie_unittest_LDADD = $(top_builddir)/libsyscmds_common.la
diff --git a/test/handler_mock.hpp b/test/handler_mock.hpp
index 1643a1f..f214db8 100644
--- a/test/handler_mock.hpp
+++ b/test/handler_mock.hpp
@@ -27,6 +27,7 @@
                                   std::uint8_t>(unsigned int));
     MOCK_CONST_METHOD1(psuResetDelay, void(std::uint32_t));
     MOCK_METHOD2(getEntityName, std::string(std::uint8_t, std::uint8_t));
+    MOCK_METHOD0(getMachineName, std::string());
     MOCK_METHOD0(buildI2cPcieMapping, void());
     MOCK_CONST_METHOD0(getI2cPcieMappingSize, size_t());
     MOCK_CONST_METHOD1(getI2cEntry,
diff --git a/test/machine_unittest.cpp b/test/machine_unittest.cpp
new file mode 100644
index 0000000..0e50a69
--- /dev/null
+++ b/test/machine_unittest.cpp
@@ -0,0 +1,73 @@
+#include "commands.hpp"
+#include "errors.hpp"
+#include "handler_mock.hpp"
+#include "machine_name.hpp"
+
+#include <cstdint>
+#include <cstring>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+#define MAX_IPMI_BUFFER 64
+
+using ::testing::Return;
+using ::testing::Throw;
+
+namespace google
+{
+namespace ipmi
+{
+
+TEST(MachineNameCommandTest, InvalidCommandLength)
+{
+    std::vector<std::uint8_t> request = {};
+    size_t dataLen = request.size();
+    std::uint8_t reply[MAX_IPMI_BUFFER];
+
+    ::testing::StrictMock<HandlerMock> hMock;
+
+    EXPECT_EQ(IPMI_CC_REQ_DATA_LEN_INVALID,
+              getMachineName(request.data(), reply, &dataLen, &hMock));
+}
+
+TEST(MachineNameCommandTest, InvalidFile)
+{
+    std::vector<std::uint8_t> request = {SysOEMCommands::SysMachineName};
+    size_t dataLen = request.size();
+    std::uint8_t reply[MAX_IPMI_BUFFER];
+
+    ::testing::StrictMock<HandlerMock> hMock;
+    EXPECT_CALL(hMock, getMachineName()).WillOnce(Throw(IpmiException(5)));
+
+    EXPECT_EQ(5, getMachineName(request.data(), reply, &dataLen, &hMock));
+}
+
+TEST(MachineNameCommandTest, CachesValidRequest)
+{
+    std::vector<std::uint8_t> request = {SysOEMCommands::SysMachineName};
+    size_t dataLen = request.size();
+    std::uint8_t reply[MAX_IPMI_BUFFER];
+    const std::string ret = "Machine";
+
+    ::testing::StrictMock<HandlerMock> hMock;
+    EXPECT_CALL(hMock, getMachineName()).WillOnce(Return(ret));
+
+    EXPECT_EQ(IPMI_CC_OK,
+              getMachineName(request.data(), reply, &dataLen, &hMock));
+    EXPECT_EQ(SysOEMCommands::SysMachineName, reply[0]);
+    EXPECT_EQ(ret.size(), reply[1]);
+    EXPECT_EQ(0, std::memcmp(&reply[2], ret.data(), ret.size()));
+
+    dataLen = request.size();
+    memset(reply, 0, sizeof(reply));
+    EXPECT_EQ(IPMI_CC_OK,
+              getMachineName(request.data(), reply, &dataLen, &hMock));
+    EXPECT_EQ(SysOEMCommands::SysMachineName, reply[0]);
+    EXPECT_EQ(ret.size(), reply[1]);
+    EXPECT_EQ(0, std::memcmp(&reply[2], ret.data(), ret.size()));
+}
+
+} // namespace ipmi
+} // namespace google