sched-host-tran: implement host transition process

Set the scheduled time and host transition to trigger power on/off.

Tested:
1. Check the state first
 $ curl -k -H "X-Auth-Token: $token" https://$bmc/xyz/openbmc_project/state/host0
 {
   "data": {
     "AttemptsLeft": 3,
     "BootProgress": "xyz.openbmc_project.State.Boot.Progress.ProgressStages.Unspecified",
     "CurrentHostState": "xyz.openbmc_project.State.Host.HostState.Off",
     "OperatingSystemState": "xyz.openbmc_project.State.OperatingSystem.Status.OSStatus.Inactive",
     "RequestedHostTransition": "xyz.openbmc_project.State.Host.Transition.Off",
     "RequestedTransition": "xyz.openbmc_project.State.Host.Transition.On",
     "ScheduledTime": 0
   },
   "message": "200 OK",
   "status": "ok"
 }
2. Set a time in future
 # busctl set-property xyz.openbmc_project.State.ScheduledHostTransition \
   /xyz/openbmc_project/state/host0 \
   xyz.openbmc_project.State.ScheduledHostTransition ScheduledTime t 1582184830
 # busctl get-property xyz.openbmc_project.State.ScheduledHostTransition \
   /xyz/openbmc_project/state/host0 \
   xyz.openbmc_project.State.ScheduledHostTransition ScheduledTime
   t 1582184830
3. Check the state again after scheduled time
 Jan 15 06:38:20 WS-Seq-FW-2 phosphor-host-state-manager[442]: Host State transaction request
 $ curl -k -H "X-Auth-Token: $token" https://$bmc/xyz/openbmc_project/state/host0
 {
   "data": {
     "AttemptsLeft": 3,
     "BootProgress": "xyz.openbmc_project.State.Boot.Progress.ProgressStages.Unspecified",
     "CurrentHostState": "xyz.openbmc_project.State.Host.HostState.Running",
     "OperatingSystemState": "xyz.openbmc_project.State.OperatingSystem.Status.OSStatus.Inactive",
     "RequestedHostTransition": "xyz.openbmc_project.State.Host.Transition.On",
     "RequestedTransition": "xyz.openbmc_project.State.Host.Transition.On",
     "ScheduledTime": 0
   },
   "message": "200 OK",
   "status": "ok"
 }
4. Set quested transition to off
 # busctl set-property xyz.openbmc_project.State.ScheduledHostTransition \
   /xyz/openbmc_project/state/host0 \
   xyz.openbmc_project.State.ScheduledHostTransition RequestedTransition \
   s "xyz.openbmc_project.State.Host.Transition.Off"
 # busctl set-property xyz.openbmc_project.State.ScheduledHostTransition \
   /xyz/openbmc_project/state/host0 \
   xyz.openbmc_project.State.ScheduledHostTransition ScheduledTime t 1582250580
 $ curl -k -H "X-Auth-Token: $token" https://$bmc/xyz/openbmc_project/state/host0
 {
   "data": {
   "AttemptsLeft": 3,
   "BootProgress": "xyz.openbmc_project.State.Boot.Progress.ProgressStages.Unspecified",
   "CurrentHostState": "xyz.openbmc_project.State.Host.HostState.Off",
   "OperatingSystemState": "xyz.openbmc_project.State.OperatingSystem.Status.OSStatus.Inactive",
   "RequestedHostTransition": "xyz.openbmc_project.State.Host.Transition.Off"
   "RequestedTransition": "xyz.openbmc_project.State.Host.Transition.Off",
   "ScheduledTime": 0
   },
 "message": "200 OK",
 "status": "ok"
 }

Change-Id: Ib9f3a3984005d9187a9b98603ec1598d8992869e
Signed-off-by: Carol Wang <wangkair@cn.ibm.com>
diff --git a/scheduled_host_transition.cpp b/scheduled_host_transition.cpp
index 4d59ff9..b65d097 100644
--- a/scheduled_host_transition.cpp
+++ b/scheduled_host_transition.cpp
@@ -16,33 +16,50 @@
 using namespace std::chrono;
 using namespace phosphor::logging;
 using namespace xyz::openbmc_project::ScheduledTime;
+using sdbusplus::exception::SdBusError;
 using InvalidTimeError =
     sdbusplus::xyz::openbmc_project::ScheduledTime::Error::InvalidTime;
 using HostTransition =
     sdbusplus::xyz::openbmc_project::State::server::ScheduledHostTransition;
 
+constexpr auto MAPPER_BUSNAME = "xyz.openbmc_project.ObjectMapper";
+constexpr auto MAPPER_PATH = "/xyz/openbmc_project/object_mapper";
+constexpr auto MAPPER_INTERFACE = "xyz.openbmc_project.ObjectMapper";
+constexpr auto PROPERTY_INTERFACE = "org.freedesktop.DBus.Properties";
+constexpr auto PROPERTY_TRANSITION = "RequestedHostTransition";
+
 uint64_t ScheduledHostTransition::scheduledTime(uint64_t value)
 {
     if (value == 0)
     {
         // 0 means the function Scheduled Host Transition is disabled
-        // to do: check timer, stop timer
-        log<level::INFO>("The function Scheduled Host Transition is disabled.");
-        return HostTransition::scheduledTime(value);
-    }
+        // Stop the timer if it's running
+        if (timer.isEnabled())
+        {
+            timer.setEnabled(false);
+        }
 
-    auto deltaTime = seconds(value) - getTime();
-    if (deltaTime < seconds(0))
-    {
-        log<level::ERR>("Scheduled time is earlier than current time. Fail to "
-                        "do host transition.");
-        elog<InvalidTimeError>(
-            InvalidTime::REASON("Scheduled time is in the past"));
+        log<level::INFO>("The function Scheduled Host Transition is disabled.");
     }
     else
     {
-        // start timer
+        auto deltaTime = seconds(value) - getTime();
+        if (deltaTime < seconds(0))
+        {
+            log<level::ERR>(
+                "Scheduled time is earlier than current time. Fail to "
+                "schedule host transition.");
+            elog<InvalidTimeError>(
+                InvalidTime::REASON("Scheduled time is in the past"));
+        }
+        else
+        {
+            // Start a timer to do host transition at scheduled time
+            timer.restart(deltaTime);
+        }
     }
+
+    // Set and return the scheduled time
     return HostTransition::scheduledTime(value);
 }
 
@@ -52,6 +69,78 @@
     return duration_cast<seconds>(now.time_since_epoch());
 }
 
+std::string getService(sdbusplus::bus::bus& bus, std::string path,
+                       std::string interface)
+{
+    auto mapper = bus.new_method_call(MAPPER_BUSNAME, MAPPER_PATH,
+                                      MAPPER_INTERFACE, "GetObject");
+
+    mapper.append(path, std::vector<std::string>({interface}));
+
+    std::vector<std::pair<std::string, std::vector<std::string>>>
+        mapperResponse;
+
+    try
+    {
+        auto mapperResponseMsg = bus.call(mapper);
+
+        mapperResponseMsg.read(mapperResponse);
+        if (mapperResponse.empty())
+        {
+            log<level::ERR>("Error no matching service",
+                            entry("PATH=%s", path.c_str()),
+                            entry("INTERFACE=%s", interface.c_str()));
+            throw std::runtime_error("Error no matching service");
+        }
+    }
+    catch (const SdBusError& e)
+    {
+        log<level::ERR>("Error in mapper call", entry("ERROR=%s", e.what()),
+                        entry("PATH=%s", path.c_str()),
+                        entry("INTERFACE=%s", interface.c_str()));
+        throw;
+    }
+
+    return mapperResponse.begin()->first;
+}
+
+void setProperty(sdbusplus::bus::bus& bus, const std::string& path,
+                 const std::string& interface, const std::string& property,
+                 const std::string& value)
+{
+    sdbusplus::message::variant<std::string> variantValue = value;
+    std::string service = getService(bus, path, interface);
+
+    auto method = bus.new_method_call(service.c_str(), path.c_str(),
+                                      PROPERTY_INTERFACE, "Set");
+
+    method.append(interface, property, variantValue);
+    bus.call_noreply(method);
+
+    return;
+}
+
+void ScheduledHostTransition::hostTransition()
+{
+    auto hostPath = std::string{HOST_OBJPATH} + '0';
+
+    auto reqTrans = convertForMessage(HostTransition::scheduledTransition());
+
+    setProperty(bus, hostPath, HOST_BUSNAME, PROPERTY_TRANSITION, reqTrans);
+
+    log<level::INFO>("Set requestedTransition",
+                     entry("REQUESTED_TRANSITION=%s", reqTrans.c_str()));
+}
+
+void ScheduledHostTransition::callback()
+{
+    // Stop timer, since we need to do host transition once only
+    timer.setEnabled(false);
+    hostTransition();
+    // Set scheduledTime to 0 to disable host transition
+    HostTransition::scheduledTime(0);
+}
+
 } // namespace manager
 } // namespace state
 } // namespace phosphor
diff --git a/scheduled_host_transition.hpp b/scheduled_host_transition.hpp
index c125638..1cb2591 100644
--- a/scheduled_host_transition.hpp
+++ b/scheduled_host_transition.hpp
@@ -2,7 +2,10 @@
 
 #include <sdbusplus/bus.hpp>
 #include <phosphor-logging/log.hpp>
+#include <sdeventplus/event.hpp>
+#include <sdeventplus/utility/timer.hpp>
 #include <xyz/openbmc_project/State/ScheduledHostTransition/server.hpp>
+#include "config.h"
 
 class TestScheduledHostTransition;
 
@@ -13,6 +16,8 @@
 namespace manager
 {
 
+using Transition =
+    sdbusplus::xyz::openbmc_project::State::server::Host::Transition;
 using ScheduledHostTransitionInherit = sdbusplus::server::object::object<
     sdbusplus::xyz::openbmc_project::State::server::ScheduledHostTransition>;
 
@@ -24,8 +29,11 @@
 class ScheduledHostTransition : public ScheduledHostTransitionInherit
 {
   public:
-    ScheduledHostTransition(sdbusplus::bus::bus& bus, const char* objPath) :
-        ScheduledHostTransitionInherit(bus, objPath)
+    ScheduledHostTransition(sdbusplus::bus::bus& bus, const char* objPath,
+                            const sdeventplus::Event& event) :
+        ScheduledHostTransitionInherit(bus, objPath),
+        bus(bus),
+        timer(event, std::bind(&ScheduledHostTransition::callback, this))
     {
     }
 
@@ -43,11 +51,28 @@
 
   private:
     friend class TestScheduledHostTransition;
+
+    /** @brief sdbusplus bus client connection */
+    sdbusplus::bus::bus& bus;
+
+    /** @brief Timer used for host transition with seconds */
+    sdeventplus::utility::Timer<sdeventplus::ClockId::RealTime> timer;
+
     /** @brief Get current time
      *
      *  @return - return current epoch time
      */
     std::chrono::seconds getTime();
+
+    /** @brief Implement host transition
+     *
+     *  @return - Does not return anything. Error will result in exception
+     *            being thrown
+     */
+    void hostTransition();
+
+    /** @brief Used by the timer to do host transition */
+    void callback();
 };
 } // namespace manager
 } // namespace state
diff --git a/scheduled_host_transition_main.cpp b/scheduled_host_transition_main.cpp
index 40a3d4d..4cd2436 100644
--- a/scheduled_host_transition_main.cpp
+++ b/scheduled_host_transition_main.cpp
@@ -1,5 +1,4 @@
 #include <cstdlib>
-#include <iostream>
 #include <exception>
 #include <sdbusplus/bus.hpp>
 #include "config.h"
@@ -7,6 +6,10 @@
 
 int main()
 {
+    // Get a default event loop
+    auto event = sdeventplus::Event::get_default();
+
+    // Get a handle to system dbus
     auto bus = sdbusplus::bus::new_default();
 
     // For now, we only have one instance of the host
@@ -16,14 +19,13 @@
     sdbusplus::server::manager::manager objManager(bus, objPathInst.c_str());
 
     phosphor::state::manager::ScheduledHostTransition manager(
-        bus, objPathInst.c_str());
+        bus, objPathInst.c_str(), event);
 
     bus.request_name(SCHEDULED_HOST_TRANSITION_BUSNAME);
 
-    while (true)
-    {
-        bus.process_discard();
-        bus.wait();
-    }
+    // Attach the bus to sd_event to service user requests
+    bus.attach_event(event.get(), SD_EVENT_PRIORITY_NORMAL);
+    event.loop();
+
     return 0;
 }
diff --git a/test/test_scheduled_host_transition.cpp b/test/test_scheduled_host_transition.cpp
index f5ffa2c..58e7aea 100644
--- a/test/test_scheduled_host_transition.cpp
+++ b/test/test_scheduled_host_transition.cpp
@@ -4,6 +4,7 @@
 #include <gtest/gtest.h>
 #include <sdbusplus/bus.hpp>
 #include <sdbusplus/test/sdbus_mock.hpp>
+#include <sdeventplus/event.hpp>
 #include <xyz/openbmc_project/ScheduledTime/error.hpp>
 
 namespace phosphor
@@ -20,11 +21,14 @@
 class TestScheduledHostTransition : public testing::Test
 {
   public:
+    sdeventplus::Event event;
     sdbusplus::SdBusMock sdbusMock;
     sdbusplus::bus::bus mockedBus = sdbusplus::get_mocked_new(&sdbusMock);
     ScheduledHostTransition scheduledHostTransition;
 
-    TestScheduledHostTransition() : scheduledHostTransition(mockedBus, "")
+    TestScheduledHostTransition() :
+        event(sdeventplus::Event::get_default()),
+        scheduledHostTransition(mockedBus, "", event)
     {
         // Empty
     }
@@ -33,11 +37,17 @@
     {
         return scheduledHostTransition.getTime();
     }
+
+    bool isTimerEnabled()
+    {
+        return scheduledHostTransition.timer.isEnabled();
+    }
 };
 
 TEST_F(TestScheduledHostTransition, disableHostTransition)
 {
     EXPECT_EQ(scheduledHostTransition.scheduledTime(0), 0);
+    EXPECT_FALSE(isTimerEnabled());
 }
 
 TEST_F(TestScheduledHostTransition, invalidScheduledTime)
@@ -49,6 +59,25 @@
                  InvalidTimeError);
 }
 
+TEST_F(TestScheduledHostTransition, validScheduledTime)
+{
+    // scheduled time is 1 min later than current time
+    uint64_t schTime =
+        static_cast<uint64_t>((getCurrentTime() + seconds(60)).count());
+    EXPECT_EQ(scheduledHostTransition.scheduledTime(schTime), schTime);
+    EXPECT_TRUE(isTimerEnabled());
+}
+
+TEST_F(TestScheduledHostTransition, hostTransitionStatus)
+{
+    // set requested transition to be on
+    scheduledHostTransition.scheduledTransition(Transition::On);
+    EXPECT_EQ(scheduledHostTransition.scheduledTransition(), Transition::On);
+    // set requested transition to be off
+    scheduledHostTransition.scheduledTransition(Transition::Off);
+    EXPECT_EQ(scheduledHostTransition.scheduledTransition(), Transition::Off);
+}
+
 } // namespace manager
 } // namespace state
 } // namespace phosphor