bm_instance: Create a new handler

This OEM handler will get properties from tmpfs to serve over IPMI.

Tested:
~# cat /run/bm-instance/asset-tag
12345
~# ipmitool raw 0x2e 0x32 0x79 0x2b 0x00 0x17 0x00
 79 2b 00 17 05 31 32 33 34 35
...
...
(Verified this works for all 7 sub commands)

Signed-off-by: Brandon Kim <brandonkim@google.com>
Change-Id: I191b6f4994d91ada49a3332a8e93a3a305561904
diff --git a/README.md b/README.md
index 39c8866..71a213b 100644
--- a/README.md
+++ b/README.md
@@ -509,3 +509,25 @@
 | Byte(s) | Value | Data       |
 | ------- | ----- | ---------- |
 | 0x00    | 0x16  | Subcommand |
+
+### GetBMInstanceProperty - SubCommand 0x17
+
+Read a BM instance property, specify the property to read following the enum.
+
+The response contains the length of the property string to read in bytes
+followed by the property string which does **NOT** have a NULL terminator.
+
+Request
+
+| Byte(s) | Value | Data                                                                                                                                 |
+| ------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------ |
+| 0x00    | 0x17  | Subcommand                                                                                                                           |
+| 0x01    |       | 0x0: AssetTag <br>0x1: BoardSerialNumber <br>0x2: Family <br>0x3: ProductName <br>0x4: SKU <br>0x5: SystemSerialNumber <br>0x6: UUID |
+
+Response
+
+| Byte(s)           | Value              | Data                                                             |
+| ----------------- | ------------------ | ---------------------------------------------------------------- |
+| 0x00              | 0x17               | Subcommand                                                       |
+| 0x01              | String Length (N)  | Number of bytes to read for property string - Size limited to 64 |
+| 0x2..0x02 + N - 1 | String of property | String, not null-terminated                                      |
diff --git a/bm_instance.cpp b/bm_instance.cpp
new file mode 100644
index 0000000..d3fbd8e
--- /dev/null
+++ b/bm_instance.cpp
@@ -0,0 +1,76 @@
+// Copyright 2024 Google LLC
+//
+// 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 "bm_instance.hpp"
+
+#include "commands.hpp"
+#include "errors.hpp"
+#include "handler.hpp"
+
+#include <ipmid/api-types.hpp>
+#include <stdplus/print.hpp>
+
+#include <span>
+#include <vector>
+
+namespace google
+{
+namespace ipmi
+{
+
+namespace
+{
+#ifndef MAX_IPMI_BUFFER
+#define MAX_IPMI_BUFFER 64
+#endif
+} // namespace
+
+struct BMInstancePropertyRequest
+{
+    std::uint8_t bmInstancePropertyType;
+} __attribute__((packed));
+
+Resp getBMInstanceProperty(std::span<const uint8_t> data,
+                           HandlerInterface* handler)
+{
+    if (data.size() < sizeof(struct BMInstancePropertyRequest))
+    {
+        stdplus::print(stderr, "Invalid command length: {}\n",
+                       static_cast<uint32_t>(data.size()));
+        return ::ipmi::responseReqDataLenInvalid();
+    }
+
+    std::string bmInstanceProperty =
+        handler->getBMInstanceProperty(/*type=*/data[0]);
+
+    const size_t length = sizeof(struct BMInstancePropertyReply) +
+                          bmInstanceProperty.size();
+
+    if (length > MAX_IPMI_BUFFER)
+    {
+        stdplus::print(stderr, "Response would overflow response buffer\n");
+        return ::ipmi::responseInvalidCommand();
+    }
+
+    std::vector<std::uint8_t> reply;
+    reply.reserve(length);
+    reply.emplace_back(bmInstanceProperty.size());
+    reply.insert(reply.end(), bmInstanceProperty.begin(),
+                 bmInstanceProperty.end());
+
+    return ::ipmi::responseSuccess(SysOEMCommands::SysGetBMInstanceProperty,
+                                   reply);
+}
+} // namespace ipmi
+} // namespace google
diff --git a/bm_instance.hpp b/bm_instance.hpp
new file mode 100644
index 0000000..9852a78
--- /dev/null
+++ b/bm_instance.hpp
@@ -0,0 +1,35 @@
+// Copyright 2024 Google LLC
+//
+// 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.
+
+#pragma once
+
+#include "handler.hpp"
+
+#include <span>
+
+namespace google
+{
+namespace ipmi
+{
+
+struct BMInstancePropertyReply
+{
+    uint8_t bmInstancePropertyLength;
+} __attribute__((packed));
+
+Resp getBMInstanceProperty(std::span<const uint8_t> data,
+                           HandlerInterface* handler);
+
+} // namespace ipmi
+} // namespace google
diff --git a/commands.hpp b/commands.hpp
index cc22a23..0dc80e6 100644
--- a/commands.hpp
+++ b/commands.hpp
@@ -67,6 +67,8 @@
     SysGetAccelVrSettings = 21,
     // Google CustomAccel Set VR Settings
     SysSetAccelVrSettings = 22,
+    // Get BM instance property info
+    SysGetBMInstanceProperty = 23,
 };
 
 } // namespace ipmi
diff --git a/handler.cpp b/handler.cpp
index d4a8897..bc6323d 100644
--- a/handler.cpp
+++ b/handler.cpp
@@ -16,6 +16,7 @@
 
 #include "bm_config.h"
 
+#include "bm_instance.hpp"
 #include "bmc_mode_enum.hpp"
 #include "errors.hpp"
 #include "handler_impl.hpp"
@@ -39,6 +40,7 @@
 #include <cstdio>
 #include <filesystem>
 #include <fstream>
+#include <iomanip>
 #include <map>
 #include <sstream>
 #include <string>
@@ -767,5 +769,48 @@
 
     return static_cast<uint16_t>(std::get<double>(value));
 }
+
+std::string Handler::getBMInstanceProperty(uint8_t propertyType) const
+{
+    std::string propertyTypeString;
+    if (auto it = bmInstanceTypeStringMap.find(propertyType);
+        it == bmInstanceTypeStringMap.end())
+    {
+        stdplus::print(stderr, "PropertyType: '{}' is invalid.\n",
+                       propertyType);
+        throw IpmiException(::ipmi::ccInvalidFieldRequest);
+    }
+    else
+    {
+        propertyTypeString = it->second;
+    }
+    std::string opath = std::format("/run/bm-instance/{}", propertyTypeString);
+    // Check for file
+
+    std::error_code ec;
+    // TODO(brandonkim@google.com): Fix this to use stdplus::ManagedFd
+    if (!this->getFs()->exists(opath, ec))
+    {
+        stdplus::print(stderr, "Path: '{}' doesn't exist.\n", opath);
+        throw IpmiException(::ipmi::ccInvalidFieldRequest);
+    }
+
+    // If file exists, read up to 64 bytes (normally shouldn't be more than 32)
+    std::ifstream ifs;
+    ifs.exceptions(std::ifstream::failbit);
+    std::string property;
+    try
+    {
+        ifs.open(opath);
+        ifs >> std::setw(64) >> property;
+    }
+    catch (std::ios_base::failure& fail)
+    {
+        stdplus::print(stderr, "Failed to read: '{}'.\n", opath);
+        throw IpmiException(::ipmi::ccUnspecifiedError);
+    }
+    return property;
+}
+
 } // namespace ipmi
 } // namespace google
diff --git a/handler.hpp b/handler.hpp
index b06d986..50bfb02 100644
--- a/handler.hpp
+++ b/handler.hpp
@@ -233,6 +233,14 @@
     virtual uint16_t accelGetVrSettings(::ipmi::Context::ptr ctx,
                                         uint8_t chip_id,
                                         uint8_t settings_id) const = 0;
+
+    /**
+     * Get the BM instance property from /run/<propertyType>
+     *
+     * @param[in] propertyType  - BM instance property type
+     * @return - string of the requested BM instance property
+     */
+    virtual std::string getBMInstanceProperty(uint8_t propertyType) const = 0;
 };
 
 } // namespace ipmi
diff --git a/handler_impl.hpp b/handler_impl.hpp
index b1f48bc..dc00051 100644
--- a/handler_impl.hpp
+++ b/handler_impl.hpp
@@ -78,6 +78,7 @@
                             uint8_t settings_id, uint16_t value) const override;
     uint16_t accelGetVrSettings(::ipmi::Context::ptr ctx, uint8_t chip_id,
                                 uint8_t settings_id) const override;
+    std::string getBMInstanceProperty(uint8_t propertyType) const override;
 
   protected:
     // Exposed for dependency injection
@@ -109,6 +110,12 @@
     const std::unordered_map<uint8_t, std::string> _vrSettingsMap{
         {0, "idle_mode_"}, {1, "power_brake_"}, {2, "loadline_"}};
 
+    const std::unordered_map<uint8_t, std::string> bmInstanceTypeStringMap = {
+        {0x00, "asset-tag"}, {0x01, "board-serial-number"},
+        {0x02, "family"},    {0x03, "product-name"},
+        {0x04, "sku"},       {0x05, "system-serial-number"},
+        {0x06, "uuid"}};
+
     nlohmann::json _entityConfig{};
 
     std::vector<std::tuple<uint32_t, std::string>> _pcie_i2c_map;
diff --git a/ipmi.cpp b/ipmi.cpp
index d8d94e9..5c0b916 100644
--- a/ipmi.cpp
+++ b/ipmi.cpp
@@ -14,6 +14,7 @@
 
 #include "ipmi.hpp"
 
+#include "bm_instance.hpp"
 #include "bmc_mode.hpp"
 #include "cable.hpp"
 #include "commands.hpp"
@@ -91,6 +92,8 @@
             return accelGetVrSettings(ctx, data, handler);
         case SysSetAccelVrSettings:
             return accelSetVrSettings(ctx, data, handler);
+        case SysGetBMInstanceProperty:
+            return getBMInstanceProperty(data, handler);
         default:
             stdplus::print(stderr, "Invalid subcommand: {:#x}\n", cmd);
             return ::ipmi::responseInvalidCommand();
diff --git a/meson.build b/meson.build
index fcbfedd..b101757 100644
--- a/meson.build
+++ b/meson.build
@@ -44,6 +44,7 @@
 
 sys_lib = static_library(
   'sys',
+  'bm_instance.cpp',
   'bmc_mode.cpp',
   'cable.cpp',
   'cpld.cpp',
diff --git a/test/bm_instance_unittest.cpp b/test/bm_instance_unittest.cpp
new file mode 100644
index 0000000..2c603c4
--- /dev/null
+++ b/test/bm_instance_unittest.cpp
@@ -0,0 +1,76 @@
+// Copyright 2024 Google LLC
+//
+// 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 "bm_instance.hpp"
+#include "commands.hpp"
+#include "handler_mock.hpp"
+#include "helper.hpp"
+
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace google
+{
+namespace ipmi
+{
+
+using testing::_;
+using ::testing::Return;
+using ::testing::StrEq;
+
+TEST(GetBMInstancePropertyTest, InvalidRequestSize)
+{
+    std::vector<uint8_t> request = {};
+
+    HandlerMock hMock;
+    EXPECT_EQ(::ipmi::responseReqDataLenInvalid(),
+              getBMInstanceProperty(request, &hMock));
+}
+
+TEST(GetBMInstancePropertyTest, InvalidCommand)
+{
+    std::vector<uint8_t> request = {1};
+    std::string expectedOutput(64, 'a');
+
+    HandlerMock hMock;
+    EXPECT_CALL(hMock, getBMInstanceProperty(1))
+        .WillOnce(Return(expectedOutput));
+    EXPECT_EQ(::ipmi::responseInvalidCommand(),
+              getBMInstanceProperty(request, &hMock));
+}
+
+TEST(GetBMInstancePropertyTest, ValidRequest)
+{
+    std::vector<uint8_t> request = {2};
+    std::string expectedOutput = "asdf";
+
+    HandlerMock hMock;
+    EXPECT_CALL(hMock, getBMInstanceProperty(2))
+        .WillOnce(Return(expectedOutput));
+
+    auto reply = getBMInstanceProperty(request, &hMock);
+    auto result = ValidateReply(reply);
+    auto& data = result.second;
+
+    EXPECT_EQ(sizeof(struct BMInstancePropertyReply) + expectedOutput.size(),
+              data.size());
+    EXPECT_EQ(SysOEMCommands::SysGetBMInstanceProperty, result.first);
+    EXPECT_THAT(std::string(data.begin() + 1, data.end()),
+                StrEq(expectedOutput));
+}
+
+} // namespace ipmi
+} // namespace google
diff --git a/test/handler_mock.hpp b/test/handler_mock.hpp
index 579dfc6..b08cd5e 100644
--- a/test/handler_mock.hpp
+++ b/test/handler_mock.hpp
@@ -71,6 +71,8 @@
                 (const, override));
     MOCK_METHOD(uint16_t, accelGetVrSettings,
                 (::ipmi::Context::ptr, uint8_t, uint8_t), (const, override));
+    MOCK_METHOD(std::string, getBMInstanceProperty, (uint8_t),
+                (const, override));
 };
 
 } // namespace ipmi
diff --git a/test/handler_unittest.cpp b/test/handler_unittest.cpp
index 549f1d0..2fb470e 100644
--- a/test/handler_unittest.cpp
+++ b/test/handler_unittest.cpp
@@ -642,6 +642,18 @@
     }
 }
 
+TEST(HandlerTest, BmInstanceFailCase)
+{
+    StrictMock<sdbusplus::SdBusMock> mock;
+    MockDbusHandler h(mock);
+
+    // Invalid enum
+    EXPECT_THROW(h.getBMInstanceProperty(0x07), IpmiException);
+
+    // Valid enum but no path exists
+    EXPECT_THROW(h.getBMInstanceProperty(0x00), IpmiException);
+}
+
 // TODO: Add checks for other functions of handler.
 
 } // namespace ipmi
diff --git a/test/meson.build b/test/meson.build
index c3387dd..f7e3bb9 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -31,6 +31,7 @@
   'bmc_mode',
   'linux_boot_done',
   'bm_mode_transition',
+  'bm_instance',
 ]
 
 foreach t : tests