Add new google ipmi sys command: SysHostPowerOff

New google ipmi sys command to let the BMC knows host shutdown,
allow host to gracefully shutdown and disable the watchdog with given
time delay.

Signed-off-by: Yunyun Lin <linyuny@google.com>
Change-Id: I02171c9cfed57ae5d10d66b515e4ab7ee8856466
diff --git a/Makefile.am b/Makefile.am
index 5016449..ec1dfaf 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -2,7 +2,8 @@
 
 if HAVE_SYSTEMD
 systemdsystemunit_DATA = \
-  gbmc-psu-hardreset.target
+  gbmc-psu-hardreset.target \
+  gbmc-host-poweroff.target
 endif
 
 # Ignore system headers
@@ -51,7 +52,8 @@
 	machine_name.cpp \
 	handler.cpp \
 	util.cpp \
-	ipmi.cpp
+	ipmi.cpp \
+	host_power_off.cpp
 libsyscmds_common_la_CXXFLAGS = \
 	$(SDBUSPLUS_CFLAGS) \
 	$(PHOSPHOR_LOGGING_CFLAGS) \
diff --git a/README.md b/README.md
index 25ae8e0..853eebe 100644
--- a/README.md
+++ b/README.md
@@ -215,3 +215,21 @@
 |--------|------|----
 |0x00|0x09|Subcommand
 |0x01...0x04|Flash size|Flash size
+
+### HostPowerOff - SubCommand 0x0A
+Sys command needs to be able to let the BMC knows host attempt S5 shutdown,
+it need power-off the Host gracefully and disable the watchdog with given time
+delay.
+
+Request
+
+|Byte(s) |Value  |Data
+|--------|-------|----
+|0x00|0x0A|Subcommand
+|0x01..0x04| |Seconds to delay (uint32)
+
+Response
+
+|Byte(s) |Value  |Data
+|--------|-------|----
+|0x00|0x0A|Subcommand
diff --git a/commands.hpp b/commands.hpp
index d9efb4f..08ab21b 100644
--- a/commands.hpp
+++ b/commands.hpp
@@ -27,6 +27,8 @@
     SysPsuHardResetOnShutdown = 8,
     // The Sys get flash size command
     SysGetFlashSize = 9,
+    // The Sys Host Power Off with disabled fallback watchdog
+    SysHostPowerOff = 10,
 };
 
 } // namespace ipmi
diff --git a/gbmc-host-poweroff.target b/gbmc-host-poweroff.target
new file mode 100644
index 0000000..521e840
--- /dev/null
+++ b/gbmc-host-poweroff.target
@@ -0,0 +1,2 @@
+[Unit]
+Description=Sys Host Power Off
diff --git a/handler.cpp b/handler.cpp
index ef48842..e99bfff 100644
--- a/handler.cpp
+++ b/handler.cpp
@@ -308,6 +308,48 @@
     }
 }
 
+static constexpr auto HOST_TIME_DELAY_FILENAME = "/run/host_poweroff_delay";
+static constexpr auto HOST_POWEROFF_TARGET = "gbmc-host-poweroff.target";
+
+void Handler::hostPowerOffDelay(std::uint32_t delay) const
+{
+    // Set time delay
+    std::ofstream ofs;
+    ofs.open(HOST_TIME_DELAY_FILENAME, std::ofstream::out);
+    if (!ofs.good())
+    {
+        std::fprintf(stderr, "Unable to open file for output.\n");
+        throw IpmiException(IPMI_CC_UNSPECIFIED_ERROR);
+    }
+
+    ofs << "HOST_POWEROFF_DELAY=" << delay << std::endl;
+    ofs.close();
+    if (ofs.fail())
+    {
+        std::fprintf(stderr, "Write failed\n");
+        throw IpmiException(IPMI_CC_UNSPECIFIED_ERROR);
+    }
+
+    // Write succeeded, please continue.
+    auto bus = sdbusplus::bus::new_default();
+    auto method = bus.new_method_call(SYSTEMD_SERVICE, SYSTEMD_ROOT,
+                                      SYSTEMD_INTERFACE, "StartUnit");
+
+    method.append(HOST_POWEROFF_TARGET);
+    method.append("replace");
+
+    try
+    {
+        bus.call_noreply(method);
+    }
+    catch (const sdbusplus::exception::SdBusError& ex)
+    {
+        log<level::ERR>("Failed to call Power Off",
+                        entry("WHAT=%s", ex.what()));
+        throw IpmiException(IPMI_CC_UNSPECIFIED_ERROR);
+    }
+}
+
 std::string readNameFromConfig(const std::string& type, uint8_t instance,
                                const Json& config)
 {
diff --git a/handler.hpp b/handler.hpp
index 1a2cbae..f4b1bbd 100644
--- a/handler.hpp
+++ b/handler.hpp
@@ -109,6 +109,14 @@
      */
     virtual std::tuple<std::uint32_t, std::string>
         getI2cEntry(unsigned int entry) const = 0;
+
+    /**
+     * Set the Host Power Off delay.
+     *
+     * @param[in] delay - delay in seconds.
+     * @throw IpmiException on failure.
+     */
+    virtual void hostPowerOffDelay(std::uint32_t delay) const = 0;
 };
 
 } // namespace ipmi
diff --git a/handler_impl.hpp b/handler_impl.hpp
index 677efa2..807114e 100644
--- a/handler_impl.hpp
+++ b/handler_impl.hpp
@@ -34,6 +34,7 @@
     std::string getMachineName() override;
     void buildI2cPcieMapping() override;
     size_t getI2cPcieMappingSize() const override;
+    void hostPowerOffDelay(std::uint32_t delay) const override;
     std::tuple<std::uint32_t, std::string>
         getI2cEntry(unsigned int entry) const override;
 
diff --git a/host_power_off.cpp b/host_power_off.cpp
new file mode 100644
index 0000000..57e45a1
--- /dev/null
+++ b/host_power_off.cpp
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2021 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 "host_power_off.hpp"
+
+#include "commands.hpp"
+#include "errors.hpp"
+#include "handler.hpp"
+
+#include <cstdint>
+#include <cstring>
+
+namespace google
+{
+namespace ipmi
+{
+
+ipmi_ret_t hostPowerOff(const uint8_t* reqBuf, uint8_t* replyBuf,
+                        size_t* dataLen, const HandlerInterface* handler)
+{
+    struct HostPowerOffRequest request;
+
+    if ((*dataLen) < sizeof(request))
+    {
+        std::fprintf(stderr, "Invalid command length: %u\n",
+                     static_cast<uint32_t>(*dataLen));
+        return IPMI_CC_REQ_DATA_LEN_INVALID;
+    }
+
+    std::memcpy(&request, &reqBuf[0], sizeof(struct HostPowerOffRequest));
+    try
+    {
+        handler->hostPowerOffDelay(request.delay);
+    }
+    catch (const IpmiException& e)
+    {
+        return e.getIpmiError();
+    }
+
+    replyBuf[0] = SysHostPowerOff;
+    (*dataLen) = sizeof(uint8_t);
+
+    return IPMI_CC_OK;
+}
+} // namespace ipmi
+} // namespace google
diff --git a/host_power_off.hpp b/host_power_off.hpp
new file mode 100644
index 0000000..e399007
--- /dev/null
+++ b/host_power_off.hpp
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "handler.hpp"
+
+#include <ipmid/api.h>
+
+namespace google
+{
+namespace ipmi
+{
+
+struct HostPowerOffRequest
+{
+    uint8_t subcommand;
+    // Delay in seconds.
+    uint32_t delay;
+} __attribute__((packed));
+
+// Disable the fallback watchdog with given time delay and Power Off Host
+ipmi_ret_t hostPowerOff(const uint8_t* reqBuf, uint8_t* replyBuf,
+                        size_t* dataLen, const HandlerInterface* handler);
+
+} // namespace ipmi
+} // namespace google
diff --git a/ipmi.cpp b/ipmi.cpp
index 2b71315..bcd2b4d 100644
--- a/ipmi.cpp
+++ b/ipmi.cpp
@@ -23,6 +23,7 @@
 #include "eth.hpp"
 #include "flash_size.hpp"
 #include "handler.hpp"
+#include "host_power_off.hpp"
 #include "machine_name.hpp"
 #include "pcie_i2c.hpp"
 #include "psu.hpp"
@@ -72,6 +73,8 @@
                                           handler);
         case SysGetFlashSize:
             return getFlashSize(reqBuf, replyCmdBuf, dataLen, handler);
+        case SysHostPowerOff:
+            return hostPowerOff(reqBuf, replyCmdBuf, dataLen, handler);
         default:
             std::fprintf(stderr, "Invalid subcommand: 0x%x\n", reqBuf[0]);
             return IPMI_CC_INVALID;
diff --git a/test/Makefile.am b/test/Makefile.am
index c97f480..a502e77 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -52,3 +52,7 @@
 check_PROGRAMS += flash_unittest
 flash_unittest_SOURCES = flash_unittest.cpp
 flash_unittest_LDADD = $(top_builddir)/libsyscmds_common.la
+
+check_PROGRAMS += poweroff_unittest
+poweroff_unittest_SOURCES = poweroff_unittest.cpp
+poweroff_unittest_LDADD = $(top_builddir)/libsyscmds_common.la
diff --git a/test/handler_mock.hpp b/test/handler_mock.hpp
index d539f7b..edd2aec 100644
--- a/test/handler_mock.hpp
+++ b/test/handler_mock.hpp
@@ -34,6 +34,7 @@
     MOCK_CONST_METHOD0(getI2cPcieMappingSize, size_t());
     MOCK_CONST_METHOD1(getI2cEntry,
                        std::tuple<std::uint32_t, std::string>(unsigned int));
+    MOCK_CONST_METHOD1(hostPowerOffDelay, void(std::uint32_t));
 };
 
 } // namespace ipmi
diff --git a/test/poweroff_unittest.cpp b/test/poweroff_unittest.cpp
new file mode 100644
index 0000000..ce9501b
--- /dev/null
+++ b/test/poweroff_unittest.cpp
@@ -0,0 +1,49 @@
+#include "commands.hpp"
+#include "handler_mock.hpp"
+#include "host_power_off.hpp"
+
+#include <cstdint>
+#include <cstring>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+#define MAX_IPMI_BUFFER 64
+
+namespace google
+{
+namespace ipmi
+{
+
+TEST(PowerOffCommandTest, InvalidRequestLength)
+{
+    std::vector<std::uint8_t> request = {SysOEMCommands::SysHostPowerOff};
+    size_t dataLen = request.size();
+    std::uint8_t reply[MAX_IPMI_BUFFER];
+
+    HandlerMock hMock;
+    EXPECT_EQ(IPMI_CC_REQ_DATA_LEN_INVALID,
+              hostPowerOff(request.data(), reply, &dataLen, &hMock));
+}
+
+TEST(PowerOffCommandTest, ValidRequest)
+{
+    // Set the dealy to 15 mins
+    std::uint32_t delayValue = 0x384;
+    struct HostPowerOffRequest requestContents;
+    requestContents.subcommand = SysOEMCommands::SysHostPowerOff;
+    requestContents.delay = delayValue;
+
+    std::vector<std::uint8_t> request(sizeof(requestContents));
+    std::memcpy(request.data(), &requestContents, sizeof(requestContents));
+    size_t dataLen = request.size();
+    std::uint8_t reply[MAX_IPMI_BUFFER];
+
+    HandlerMock hMock;
+    EXPECT_CALL(hMock, hostPowerOffDelay(delayValue));
+    EXPECT_EQ(IPMI_CC_OK,
+              hostPowerOff(request.data(), reply, &dataLen, &hMock));
+}
+
+} // namespace ipmi
+} // namespace google