system: implement firmware status in czechlight-system

This patch implements filling the data in the
czechlight-system:firmware/firmware-slot list. The list is supposed to
hold the basic information about the current firmware slot OS version
and boot status. All the data are provided by RAUC.

Because RAUC provides the slot data vis D-Bus only when explicitly asked
by GetSlotStatus function over and there are no signals (or signals about
property changes), we have to actually call the function to retrieve the
data.

However, there is one catch. RAUC will not provide us with the data when
there is another operation in progress (i.e., when somebody invoked
installation). Therefore, we cache the data and we provide the caller
with the cached data whenever we can't fetch fresh data.

There is, however, another catch. The data can change any time, not only
after installation of new bundle. They can also change whenever somebody
marks a slot bad/good. We can't capture that change unless we are
querying the RAUC periodically, which we do not want to.

Let's say, somebody marks the slot bad and then run an installation.
During the installation he/she asks for the slot data. However, RAUC
won't let us fetch the data (because it is fully devoted to the
installation process), so we would return the old data, where the slot
is still good.

So, at least, we update the cache also before and after any installation,
because that is the only long-running operation in RAUC.

Change-Id: I890bf6813f15f9629a0c6acd516216e88d6cd986
diff --git a/src/system/Firmware.cpp b/src/system/Firmware.cpp
index 68fd860..42628d7 100644
--- a/src/system/Firmware.cpp
+++ b/src/system/Firmware.cpp
@@ -15,50 +15,51 @@
 
 const auto CZECHLIGHT_SYSTEM_MODULE_NAME = "czechlight-system"s;
 const auto CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX = "/"s + CZECHLIGHT_SYSTEM_MODULE_NAME + ":firmware/"s;
+const auto FIRMWARE_SLOTS = {"rootfs.0"s, "rootfs.1"s};
 
 }
 
 namespace velia::system {
 
 Firmware::Firmware(std::shared_ptr<::sysrepo::Connection> srConn, sdbus::IConnection& dbusConnectionSignals, sdbus::IConnection& dbusConnectionMethods)
-    : m_srConn(std::move(srConn))
+    : m_rauc(std::make_shared<RAUC>(
+        dbusConnectionSignals,
+        dbusConnectionMethods,
+        [this](const std::string& operation) {
+            if (operation == "installing") {
+                std::lock_guard<std::mutex> lck(m_mtx);
+                m_installStatus = "in-progress";
+            }
+        },
+        [this](int32_t perc, const std::string& msg) {
+            std::map<std::string, std::string> data = {
+                {CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/update/message", msg},
+                {CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/update/progress", std::to_string(perc)},
+            };
+
+            libyang::S_Data_Node dataNode;
+            auto session = std::make_shared<::sysrepo::Session>(m_srConn);
+
+            utils::valuesToYang(data, session, dataNode);
+            session->event_notif_send(dataNode);
+        },
+        [this](int32_t retVal, const std::string& lastError) {
+            auto lock = updateSlotStatus();
+            m_installStatus = retVal == 0 ? "succeeded" : "failed";
+            m_installMessage = lastError;
+        }))
+    , m_log(spdlog::get("system"))
+    , m_srConn(std::move(srConn))
     , m_srSessionOps(std::make_shared<::sysrepo::Session>(m_srConn))
     , m_srSessionRPC(std::make_shared<::sysrepo::Session>(m_srConn))
     , m_srSubscribeOps(std::make_shared<::sysrepo::Subscribe>(m_srSessionOps))
     , m_srSubscribeRPC(std::make_shared<::sysrepo::Subscribe>(m_srSessionRPC))
-    , m_rauc(std::make_shared<RAUC>(
-          dbusConnectionSignals,
-          dbusConnectionMethods,
-          [this](const std::string& operation) {
-              if (operation == "installing") {
-                  std::lock_guard<std::mutex> lck(m_mtx);
-                  m_installStatus = "in-progress";
-              }
-          },
-          [this](int32_t perc, const std::string& msg) {
-              std::map<std::string, std::string> data = {
-                  {CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/update/message", msg},
-                  {CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/update/progress", std::to_string(perc)},
-              };
-
-              libyang::S_Data_Node dataNode;
-              auto session = std::make_shared<::sysrepo::Session>(m_srConn);
-
-              utils::valuesToYang(data, session, dataNode);
-              session->event_notif_send(dataNode);
-          },
-          [this](int32_t retVal, const std::string& lastError) {
-              std::lock_guard<std::mutex> lck(m_mtx);
-              m_installStatus = retVal == 0 ? "succeeded" : "failed";
-              m_installMessage = lastError;
-          }))
-    , m_log(spdlog::get("system"))
 {
     {
         auto raucOperation = m_rauc->operation();
         auto raucLastError = m_rauc->lastError();
 
-        std::lock_guard<std::mutex> lck(m_mtx);
+        auto lock = updateSlotStatus();
 
         m_installMessage = raucLastError;
 
@@ -74,6 +75,8 @@
     m_srSubscribeRPC->rpc_subscribe(
         (CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/install").c_str(),
         [this](::sysrepo::S_Session session, [[maybe_unused]] const char* op_path, const ::sysrepo::S_Vals input, [[maybe_unused]] sr_event_t event, [[maybe_unused]] uint32_t request_id, [[maybe_unused]] ::sysrepo::S_Vals_Holder output) {
+            auto lock = updateSlotStatus();
+
             try {
                 std::string source = input->val(0)->val_to_string();
                 m_rauc->install(source);
@@ -91,12 +94,13 @@
         CZECHLIGHT_SYSTEM_MODULE_NAME.c_str(),
         [this](::sysrepo::S_Session session, [[maybe_unused]] const char* module_name, [[maybe_unused]] const char* path, [[maybe_unused]] const char* request_xpath, [[maybe_unused]] uint32_t request_id, libyang::S_Data_Node& parent) {
             std::map<std::string, std::string> data;
+
             {
-                std::lock_guard<std::mutex> lck(m_mtx);
-                data = {
-                    {CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/status", m_installStatus},
-                    {CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/message", m_installMessage},
-                };
+                auto lock = updateSlotStatus();
+
+                data.insert(m_slotStatusCache.begin(), m_slotStatusCache.end());
+                data[CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/status"] = m_installStatus;
+                data[CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/message"] = m_installMessage;
             }
 
             utils::valuesToYang(data, session, parent);
@@ -105,4 +109,38 @@
         (CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "*").c_str(),
         SR_SUBSCR_PASSIVE | SR_SUBSCR_OPER_MERGE | SR_SUBSCR_CTX_REUSE);
 }
+
+/** @brief Updates the slot status cache with the new data obtained via RAUC
+ *
+ * Gets current slot status data from RAUC and updates the local slot status cache if new data are available.
+ * The methods manipulates with the local cache which is shared among multiple thread.
+ *
+ * @return an unique_lock (in locked state) that can be further used to manipulate with the local cache
+ * */
+std::unique_lock<std::mutex> Firmware::updateSlotStatus()
+{
+    std::map<std::string, velia::system::RAUC::SlotProperties> slotStatus;
+
+    try {
+        slotStatus = m_rauc->slotStatus();
+    } catch (sdbus::Error& e) {
+        m_log->warn("Could not fetch RAUC slot status data: {}", e.getMessage());
+    }
+
+    std::unique_lock<std::mutex> lck(m_mtx);
+
+    for (const auto& slotName : FIRMWARE_SLOTS) {
+        if (auto it = slotStatus.find(slotName); it != slotStatus.end()) { // if there is an update for the slot "slotName"
+            const auto& props = it->second;
+            auto xpathPrefix = CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "firmware-slot[name='" + std::get<std::string>(props.at("bootname")) + "']/";
+
+            m_slotStatusCache[xpathPrefix + "state"] = std::get<std::string>(props.at("state"));
+            m_slotStatusCache[xpathPrefix + "version"] = std::get<std::string>(props.at("bundle.version"));
+            m_slotStatusCache[xpathPrefix + "installed"] = std::get<std::string>(props.at("installed.timestamp"));
+            m_slotStatusCache[xpathPrefix + "boot-status"] = std::get<std::string>(props.at("boot-status"));
+        }
+    }
+
+    return lck;
+}
 }
diff --git a/src/system/Firmware.h b/src/system/Firmware.h
index 5ff6723..0d81b02 100644
--- a/src/system/Firmware.h
+++ b/src/system/Firmware.h
@@ -20,12 +20,19 @@
     Firmware(std::shared_ptr<::sysrepo::Connection> srConn, sdbus::IConnection& dbusConnectionSignals, sdbus::IConnection& dbusConnectionMethods);
 
 private:
-    std::shared_ptr<::sysrepo::Connection> m_srConn;
-    std::shared_ptr<::sysrepo::Session> m_srSessionOps, m_srSessionRPC;
-    std::shared_ptr<::sysrepo::Subscribe> m_srSubscribeOps, m_srSubscribeRPC;
     std::shared_ptr<RAUC> m_rauc;
     std::mutex m_mtx; //! @brief locks access to cached elements that are shared from multiple threads
     std::string m_installStatus, m_installMessage;
+    std::map<std::string, std::string> m_slotStatusCache;
     velia::Log m_log;
+    std::shared_ptr<::sysrepo::Connection> m_srConn;
+    std::shared_ptr<::sysrepo::Session> m_srSessionOps, m_srSessionRPC;
+    /* Subscribe objects must be destroyed before shared_ptr<RAUC> and other objects that are used in the callbacks
+     * (m_slotStatusCache, m_installStatus, m_installMessage). If they're not, the objects might be already destroyed
+     * while executing the callback.
+     */
+    std::shared_ptr<::sysrepo::Subscribe> m_srSubscribeOps, m_srSubscribeRPC;
+
+    std::unique_lock<std::mutex> updateSlotStatus();
 };
 }