Merge "docs: this is now more than just a health monitor"
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 76a79ae..720aa6b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -39,8 +39,8 @@
     message(FATAL_ERROR "Systemd 245 has a bug affecting DBus method calls using sdbus-c++. Please see https://github.com/Kistler-Group/sdbus-cpp/issues/106.")
 endif()
 
-pkg_check_modules(SYSREPO REQUIRED sysrepo-cpp>=1.4.100 IMPORTED_TARGET sysrepo)
-pkg_check_modules(LIBYANG REQUIRED libyang-cpp>=1.0.209 IMPORTED_TARGET libyang)
+pkg_check_modules(SYSREPO REQUIRED sysrepo-cpp>=1.4.103 IMPORTED_TARGET sysrepo)
+pkg_check_modules(LIBYANG REQUIRED libyang-cpp>=1.0.215 IMPORTED_TARGET libyang)
 
 include(GNUInstallDirs)
 
@@ -55,6 +55,9 @@
     yang/ietf-netconf-acm@2018-02-14.yang
     yang/ietf-system@2014-08-06.yang
     yang/ietf-yang-types@2013-07-15.yang
+    yang/ietf-ethertypes@2019-03-04.yang
+    yang/ietf-interfaces@2018-02-20.yang
+    yang/ietf-packet-fields@2019-03-04.yang
     yang/ietf-access-control-list@2019-03-04.yang
     yang/czechlight-firewall@2021-01-25.yang
     )
@@ -131,6 +134,8 @@
 add_library(velia-system STATIC
         src/system/RAUC.cpp
         src/system/RAUC.h
+        src/system/CzechlightSystem.cpp
+        src/system/CzechlightSystem.h
         src/system/IETFSystem.cpp
         src/system/IETFSystem.h
         )
@@ -156,20 +161,18 @@
 
 
 # - daemons
-add_executable(veliad
-        src/main.cpp
-        src/Factory.cpp
-        src/Factory.h
-        )
-target_link_libraries(veliad
-        PUBLIC
+add_executable(veliad-health
+    src/main-health.cpp
+    src/health/Factory.cpp
+    src/health/Factory.h
+    )
+target_link_libraries(veliad-health
+    PUBLIC
         velia-health
-        velia-ietf-hardware
-        velia-ietf-hardware-sysrepo
         docopt
-        )
-add_dependencies(veliad target-VELIA_VERSION)
-target_include_directories(veliad PUBLIC ${CMAKE_BINARY_DIR})
+    )
+add_dependencies(veliad-health target-VELIA_VERSION)
+target_include_directories(veliad-health PUBLIC ${CMAKE_BINARY_DIR})
 
 add_executable(veliad-system
     src/main-system.cpp
@@ -195,6 +198,20 @@
 add_dependencies(veliad-firewall target-VELIA_VERSION)
 target_include_directories(veliad-firewall PUBLIC ${CMAKE_BINARY_DIR})
 
+add_executable(veliad-hardware
+    src/main-hardware.cpp
+    src/ietf-hardware/Factory.cpp
+    src/ietf-hardware/Factory.h
+    )
+target_link_libraries(veliad-hardware
+    PUBLIC
+        velia-ietf-hardware
+        velia-ietf-hardware-sysrepo
+        docopt
+    )
+add_dependencies(veliad-hardware target-VELIA_VERSION)
+target_include_directories(veliad-hardware PUBLIC ${CMAKE_BINARY_DIR})
+
 # Testing
 include(CTest)
 if(BUILD_TESTING)
@@ -212,6 +229,12 @@
     target_include_directories(DoctestIntegration PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/tests/)
     target_compile_definitions(DoctestIntegration PUBLIC DOCTEST_CONFIG_SUPER_FAST_ASSERTS)
 
+    add_library(SysrepoTesting STATIC
+        tests/mock/sysrepo/events.cpp
+        tests/mock/sysrepo/events.h
+        )
+    target_link_libraries(SysrepoTesting DoctestIntegration PkgConfig::SYSREPO)
+
     add_library(DbusTesting STATIC
         tests/dbus-helpers/dbus_rauc_server.cpp
         tests/dbus-helpers/dbus_rauc_server.h
@@ -279,6 +302,10 @@
     velia_test(health_output-led velia-health)
 
     velia_test(system_rauc velia-system DbusTesting)
+    set_tests_properties(
+            test-system_rauc
+            PROPERTIES RESOURCE_LOCK dbus-rauc
+    )
 
     velia_test(hardware_emmc velia-ietf-hardware FsTestUtils)
     velia_test(hardware_hwmon velia-ietf-hardware FsTestUtils)
@@ -289,6 +316,15 @@
             test-sysrepo_system-ietfsystem
             PROPERTIES FIXTURES_REQUIRED sysrepo:env:sysrepo-ietf-system
             RESOURCE_LOCK sysrepo
+)
+
+    sysrepo_fixture_env(sysrepo-czechlight-system YANG ${CMAKE_CURRENT_SOURCE_DIR}/yang/czechlight-system@2021-01-13.yang)
+    velia_test(sysrepo_system-czechlightsystem velia-system DbusTesting)
+    target_link_libraries(test-sysrepo_system-czechlightsystem SysrepoTesting)
+    set_tests_properties(
+            test-sysrepo_system-czechlightsystem
+            PROPERTIES FIXTURES_REQUIRED sysrepo:env:sysrepo-czechlight-system
+            RESOURCE_LOCK "sysrepo;dbus-rauc"
     )
 
     sysrepo_fixture_env(sysrepo-ietf-hardware YANG ${CMAKE_CURRENT_SOURCE_DIR}/yang/iana-hardware@2018-03-13.yang YANG ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-hardware@2018-03-13.yang FEATURE hardware-sensor)
@@ -365,5 +401,7 @@
 
 set(YANG_DIR ${CMAKE_INSTALL_PREFIX}/share/velia/yang)
 install(FILES ${YANG_SRCS} DESTINATION ${YANG_DIR})
-install(TARGETS veliad RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/)
+install(TARGETS veliad-health RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/)
 install(TARGETS veliad-system RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/)
+install(TARGETS veliad-hardware RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/)
+install(TARGETS veliad-firewall RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/)
diff --git a/ci/build.sh b/ci/build.sh
index 5af1e6a..bfd3457 100755
--- a/ci/build.sh
+++ b/ci/build.sh
@@ -31,6 +31,7 @@
     export CFLAGS="-fsanitize=undefined ${CFLAGS}"
     export CXXFLAGS="-fsanitize=undefined ${CXXFLAGS}"
     export LDFLAGS="-fsanitize=undefined ${LDFLAGS}"
+    export UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1
 fi
 
 if [[ $ZUUL_JOB_NAME =~ .*-asan ]]; then
@@ -43,7 +44,7 @@
     export CFLAGS="-fsanitize=thread ${CFLAGS}"
     export CXXFLAGS="-fsanitize=thread ${CXXFLAGS}"
     export LDFLAGS="-fsanitize=thread ${LDFLAGS}"
-    export TSAN_OPTIONS="suppressions=$HOME/target/tsan.supp:detect_deadlocks=0"
+    export TSAN_OPTIONS="suppressions=$HOME/target/tsan.supp"
 fi
 
 if [[ $ZUUL_JOB_NAME =~ .*-cover.* ]]; then
diff --git a/src/Factory.h b/src/Factory.h
deleted file mode 100644
index 521bc4f..0000000
--- a/src/Factory.h
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma once
-#include <memory>
-#include "health/State.h"
-#include "health/outputs/SlotWrapper.h"
-
-namespace velia::ietf_hardware {
-class IETFHardware;
-}
-
-namespace velia::ietf_hardware {
-std::shared_ptr<ietf_hardware::IETFHardware> create(const std::string& applianceName);
-}
-
-namespace velia::health {
-boost::signals2::SlotWrapper<void, health::State> createOutput(const std::string& applianceName);
-}
diff --git a/src/health/Factory.cpp b/src/health/Factory.cpp
new file mode 100644
index 0000000..607a985
--- /dev/null
+++ b/src/health/Factory.cpp
@@ -0,0 +1,18 @@
+#include "Factory.h"
+#include "health/outputs/LedSysfsDriver.h"
+#include "health/outputs/SlotWrapper.h"
+#include "health/outputs/callables.h"
+
+namespace velia::health {
+boost::signals2::SlotWrapper<void, health::State> createOutput(const std::string& applianceName)
+{
+    if (applianceName == "czechlight-clearfog") {
+        return boost::signals2::SlotWrapper<void, State>(std::make_shared<LedOutputCallback>(
+            std::make_shared<LedSysfsDriver>("/sys/class/leds/status:red/"),
+            std::make_shared<LedSysfsDriver>("/sys/class/leds/status:green/"),
+            std::make_shared<LedSysfsDriver>("/sys/class/leds/status:blue/")));
+    } else {
+        throw std::runtime_error("Unknown appliance '" + applianceName + "'");
+    }
+}
+}
diff --git a/src/health/Factory.h b/src/health/Factory.h
new file mode 100644
index 0000000..c4207a2
--- /dev/null
+++ b/src/health/Factory.h
@@ -0,0 +1,8 @@
+#pragma once
+#include <memory>
+#include "health/State.h"
+#include "health/outputs/SlotWrapper.h"
+
+namespace velia::health {
+boost::signals2::SlotWrapper<void, health::State> createOutput(const std::string& applianceName);
+}
diff --git a/src/Factory.cpp b/src/ietf-hardware/Factory.cpp
similarity index 79%
rename from src/Factory.cpp
rename to src/ietf-hardware/Factory.cpp
index 408e06d..c214c85 100644
--- a/src/Factory.cpp
+++ b/src/ietf-hardware/Factory.cpp
@@ -1,7 +1,4 @@
 #include "Factory.h"
-#include "health/outputs/LedSysfsDriver.h"
-#include "health/outputs/SlotWrapper.h"
-#include "health/outputs/callables.h"
 #include "ietf-hardware/IETFHardware.h"
 #include "ietf-hardware/sysfs/EMMC.h"
 #include "ietf-hardware/sysfs/HWMon.h"
@@ -41,17 +38,3 @@
 }
 
 }
-
-namespace velia::health {
-boost::signals2::SlotWrapper<void, health::State> createOutput(const std::string& applianceName)
-{
-    if (applianceName == "czechlight-clearfog") {
-        return boost::signals2::SlotWrapper<void, State>(std::make_shared<LedOutputCallback>(
-            std::make_shared<LedSysfsDriver>("/sys/class/leds/status:red/"),
-            std::make_shared<LedSysfsDriver>("/sys/class/leds/status:green/"),
-            std::make_shared<LedSysfsDriver>("/sys/class/leds/status:blue/")));
-    } else {
-        throw std::runtime_error("Unknown appliance '" + applianceName + "'");
-    }
-}
-}
diff --git a/src/ietf-hardware/Factory.h b/src/ietf-hardware/Factory.h
new file mode 100644
index 0000000..fcd83f5
--- /dev/null
+++ b/src/ietf-hardware/Factory.h
@@ -0,0 +1,10 @@
+#pragma once
+#include <memory>
+
+namespace velia::ietf_hardware {
+class IETFHardware;
+}
+
+namespace velia::ietf_hardware {
+std::shared_ptr<ietf_hardware::IETFHardware> create(const std::string& applianceName);
+}
diff --git a/src/main-firewall.cpp b/src/main-firewall.cpp
index aca2184..db6b33d 100644
--- a/src/main-firewall.cpp
+++ b/src/main-firewall.cpp
@@ -54,7 +54,7 @@
         loggingSink = std::make_shared<spdlog::sinks::ansicolor_stderr_sink_mt>();
     }
 
-    auto args = docopt::docopt(usage, {argv + 1, argv + argc}, true, "veliad " VELIA_VERSION, true);
+    auto args = docopt::docopt(usage, {argv + 1, argv + argc}, true, "veliad-firewall " VELIA_VERSION, true);
 
     velia::utils::initLogs(loggingSink);
     spdlog::set_level(spdlog::level::info);
diff --git a/src/main.cpp b/src/main-hardware.cpp
similarity index 66%
copy from src/main.cpp
copy to src/main-hardware.cpp
index cc8f0e6..7b0bd2e 100644
--- a/src/main.cpp
+++ b/src/main-hardware.cpp
@@ -1,19 +1,15 @@
 #include <docopt.h>
-#include <future>
-#include <sdbus-c++/sdbus-c++.h>
 #include <spdlog/sinks/ansicolor_sink.h>
 #include <spdlog/spdlog.h>
 #include <sysrepo-cpp/Session.hpp>
-#include "Factory.h"
 #include "VELIA_VERSION.h"
-#include "health/inputs/DbusSystemdInput.h"
-#include "health/manager/StateManager.h"
-#include "health/outputs/callables.h"
+#include "ietf-hardware/Factory.h"
+#include "ietf-hardware/IETFHardware.h"
 #include "ietf-hardware/sysrepo/Sysrepo.h"
-#include "main.h"
 #include "utils/exceptions.h"
 #include "utils/journal.h"
 #include "utils/log-init.h"
+#include "utils/waitUntilSignalled.h"
 
 /** @short Extract log level from a CLI option */
 spdlog::level::level_enum parseLogLevel(const std::string& name, const docopt::value& option)
@@ -33,18 +29,16 @@
 }
 
 static const char usage[] =
-    R"(Monitor system health status.
+    R"(Hardware monitoring via Sysrepo.
 
 Usage:
-  veliad
+  veliad-hardware
     [--appliance=<Model>]
     [--log-level=<Level>]
-    [--health-log-level=<Level>]
     [--sysrepo-log-level=<Level>]
     [--hardware-log-level=<Level>]
-    [--systemd-ignore-unit=<Unit>]...
-  veliad (-h | --help)
-  veliad --version
+  veliad-hardware (-h | --help)
+  veliad-hardware --version
 
 Options:
   -h --help                         Show this screen.
@@ -53,14 +47,10 @@
   --log-level=<N>                   Log level for everything [default: 3]
                                     (0 -> critical, 1 -> error, 2 -> warning, 3 -> info,
                                     4 -> debug, 5 -> trace)
-  --health-log-level=<N>            Log level for the health monitoring [default: 3]
   --sysrepo-log-level=<N>           Log level for the sysrepo library [default: 3]
   --hardware-log-level=<N>          Log level for the hardware drivers [default: 3]
-  --systemd-ignore-unit=<Unit>      Ignore state of systemd's unit in systemd state tracker. Can be specified multiple times.
 )";
 
-DBUS_EVENTLOOP_INIT
-
 int main(int argc, char* argv[])
 {
     std::shared_ptr<spdlog::sinks::sink> loggingSink;
@@ -70,15 +60,15 @@
         loggingSink = std::make_shared<spdlog::sinks::ansicolor_stderr_sink_mt>();
     }
 
-    auto args = docopt::docopt(usage, {argv + 1, argv + argc}, true, "veliad " VELIA_VERSION, true);
+    auto args = docopt::docopt(usage, {argv + 1, argv + argc}, true, "veliad-system " VELIA_VERSION, true);
 
     velia::utils::initLogs(loggingSink);
     spdlog::set_level(spdlog::level::info);
 
     try {
         spdlog::set_level(parseLogLevel("Generic", args["--log-level"]));
-        spdlog::get("hardware")->set_level(parseLogLevel("Hardware loggers", args["--hardware-log-level"]));
         spdlog::get("sysrepo")->set_level(parseLogLevel("Sysrepo library", args["--sysrepo-log-level"]));
+        spdlog::get("hardware")->set_level(parseLogLevel("Hardware loggers", args["--hardware-log-level"]));
 
         spdlog::get("main")->debug("Opening Sysrepo connection");
         auto srConn = std::make_shared<sysrepo::Connection>();
@@ -97,30 +87,7 @@
         spdlog::get("main")->debug("Initializing Sysrepo ietf-hardware callback");
         auto sysrepoIETFHardware = velia::ietf_hardware::sysrepo::Sysrepo(srSubscription, ietfHardware);
 
-        DBUS_EVENTLOOP_START
-
-        // health
-        auto manager = std::make_shared<velia::health::StateManager>();
-
-        // output configuration
-        spdlog::get("main")->debug("Initializing LED drivers");
-        if (const auto& appliance = args["--appliance"]) {
-            manager->m_outputSignal.connect(velia::health::createOutput(appliance.asString()));
-        }
-
-        spdlog::get("main")->debug("All outputs initialized.");
-
-        // input configuration
-        std::set<std::string> ignoredUnits(args["--systemd-ignore-unit"].asStringList().begin(), args["--systemd-ignore-unit"].asStringList().end());
-        spdlog::get("main")->debug("Starting DBus systemd watcher");
-        if (!ignoredUnits.empty()) {
-            spdlog::get("main")->debug("Systemd input will ignore changes of the following units: {}", args["--systemd-ignore-unit"]);
-        }
-        auto inputSystemdDbus = std::make_shared<velia::health::DbusSystemdInput>(manager, ignoredUnits, *g_dbusConnection);
-
-        spdlog::get("main")->debug("All inputs initialized.");
-
-        DBUS_EVENTLOOP_END;
+        waitUntilSignaled();
 
         return 0;
     } catch (std::exception& e) {
diff --git a/src/main.cpp b/src/main-health.cpp
similarity index 78%
rename from src/main.cpp
rename to src/main-health.cpp
index cc8f0e6..c3e4475 100644
--- a/src/main.cpp
+++ b/src/main-health.cpp
@@ -4,12 +4,11 @@
 #include <spdlog/sinks/ansicolor_sink.h>
 #include <spdlog/spdlog.h>
 #include <sysrepo-cpp/Session.hpp>
-#include "Factory.h"
 #include "VELIA_VERSION.h"
+#include "health/Factory.h"
 #include "health/inputs/DbusSystemdInput.h"
 #include "health/manager/StateManager.h"
 #include "health/outputs/callables.h"
-#include "ietf-hardware/sysrepo/Sysrepo.h"
 #include "main.h"
 #include "utils/exceptions.h"
 #include "utils/journal.h"
@@ -36,26 +35,23 @@
     R"(Monitor system health status.
 
 Usage:
-  veliad
+  veliad-health
     [--appliance=<Model>]
     [--log-level=<Level>]
     [--health-log-level=<Level>]
     [--sysrepo-log-level=<Level>]
-    [--hardware-log-level=<Level>]
     [--systemd-ignore-unit=<Unit>]...
-  veliad (-h | --help)
-  veliad --version
+  veliad-health (-h | --help)
+  veliad-health --version
 
 Options:
   -h --help                         Show this screen.
   --version                         Show version.
-  --appliance=<Model>               Initialize IETF Hardware and outputs for specific appliance.
   --log-level=<N>                   Log level for everything [default: 3]
                                     (0 -> critical, 1 -> error, 2 -> warning, 3 -> info,
                                     4 -> debug, 5 -> trace)
   --health-log-level=<N>            Log level for the health monitoring [default: 3]
   --sysrepo-log-level=<N>           Log level for the sysrepo library [default: 3]
-  --hardware-log-level=<N>          Log level for the hardware drivers [default: 3]
   --systemd-ignore-unit=<Unit>      Ignore state of systemd's unit in systemd state tracker. Can be specified multiple times.
 )";
 
@@ -70,14 +66,13 @@
         loggingSink = std::make_shared<spdlog::sinks::ansicolor_stderr_sink_mt>();
     }
 
-    auto args = docopt::docopt(usage, {argv + 1, argv + argc}, true, "veliad " VELIA_VERSION, true);
+    auto args = docopt::docopt(usage, {argv + 1, argv + argc}, true, "veliad-health " VELIA_VERSION, true);
 
     velia::utils::initLogs(loggingSink);
     spdlog::set_level(spdlog::level::info);
 
     try {
         spdlog::set_level(parseLogLevel("Generic", args["--log-level"]));
-        spdlog::get("hardware")->set_level(parseLogLevel("Hardware loggers", args["--hardware-log-level"]));
         spdlog::get("sysrepo")->set_level(parseLogLevel("Sysrepo library", args["--sysrepo-log-level"]));
 
         spdlog::get("main")->debug("Opening Sysrepo connection");
@@ -85,18 +80,6 @@
         auto srSess = std::make_shared<sysrepo::Session>(srConn);
         auto srSubscription = std::make_shared<sysrepo::Subscribe>(srSess);
 
-        // initialize ietf-hardware
-        spdlog::get("main")->debug("Initializing IETFHardware module");
-        std::shared_ptr<velia::ietf_hardware::IETFHardware> ietfHardware;
-        if (const auto& appliance = args["--appliance"]) {
-            ietfHardware = velia::ietf_hardware::create(appliance.asString());
-        } else {
-            ietfHardware = std::make_shared<velia::ietf_hardware::IETFHardware>();
-        }
-
-        spdlog::get("main")->debug("Initializing Sysrepo ietf-hardware callback");
-        auto sysrepoIETFHardware = velia::ietf_hardware::sysrepo::Sysrepo(srSubscription, ietfHardware);
-
         DBUS_EVENTLOOP_START
 
         // health
diff --git a/src/main-system.cpp b/src/main-system.cpp
index 3f94091..843c19e 100644
--- a/src/main-system.cpp
+++ b/src/main-system.cpp
@@ -4,6 +4,7 @@
 #include <sysrepo-cpp/Session.hpp>
 #include "VELIA_VERSION.h"
 #include "main.h"
+#include "system/CzechlightSystem.h"
 #include "system/IETFSystem.h"
 #include "utils/exceptions.h"
 #include "utils/journal.h"
@@ -76,6 +77,7 @@
         // initialize ietf-system
         spdlog::get("main")->debug("Initializing Sysrepo for system models");
         auto sysrepoIETFSystem = velia::system::IETFSystem(srSess, "/etc/os-release");
+        auto sysrepoCzechlightSystem = velia::system::CzechlightSystem(srConn, *g_dbusConnection);
 
         DBUS_EVENTLOOP_END
         return 0;
diff --git a/src/system/CzechlightSystem.cpp b/src/system/CzechlightSystem.cpp
new file mode 100644
index 0000000..4256015
--- /dev/null
+++ b/src/system/CzechlightSystem.cpp
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
+ *
+ */
+
+#include "CzechlightSystem.h"
+#include "utils/log.h"
+#include "utils/sysrepo.h"
+
+using namespace std::literals;
+
+namespace {
+
+const auto CZECHLIGHT_SYSTEM_MODULE_NAME = "czechlight-system"s;
+const auto CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX = "/"s + CZECHLIGHT_SYSTEM_MODULE_NAME + ":firmware/"s;
+
+}
+
+namespace velia::system {
+
+CzechlightSystem::CzechlightSystem(std::shared_ptr<::sysrepo::Connection> srConn, sdbus::IConnection& dbusConnection)
+    : m_srConn(std::move(srConn))
+    , m_srSession(std::make_shared<::sysrepo::Session>(m_srConn))
+    , m_srSubscribe(std::make_shared<::sysrepo::Subscribe>(m_srSession))
+    , m_rauc(std::make_shared<RAUC>(
+          dbusConnection,
+          [this](const std::string& operation) {
+              if (operation == "installing") {
+                  std::map<std::string, std::string> data {
+                      {CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/status", "in-progress"},
+                  };
+
+                  utils::valuesPush(data, std::make_shared<::sysrepo::Session>(m_srConn, SR_DS_OPERATIONAL));
+              }
+          },
+          [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::map<std::string, std::string> data {
+                  {CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/message", lastError},
+                  {CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/status", retVal == 0 ? "succeeded" : "failed"},
+              };
+
+              utils::valuesPush(data, std::make_shared<::sysrepo::Session>(m_srConn, SR_DS_OPERATIONAL));
+          }))
+    , m_log(spdlog::get("system"))
+{
+    {
+        auto raucOperation = m_rauc->operation();
+        auto raucLastError = m_rauc->lastError();
+        std::map<std::string, std::string> data {
+            {CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/message", raucLastError},
+        };
+
+        if (raucOperation == "installing") {
+            data[CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/status"] = "in-progress";
+        } else if (!raucLastError.empty()) {
+            data[CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/status"] = "failed";
+        } else {
+            data[CZECHLIGHT_SYSTEM_FIRMWARE_MODULE_PREFIX + "installation/status"] = "none";
+        }
+
+        utils::valuesPush(data, m_srSession, SR_DS_OPERATIONAL);
+    }
+
+    m_srSubscribe->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) {
+            try {
+                std::string source = input->val(0)->val_to_string();
+                m_rauc->install(source);
+            } catch (sdbus::Error& e) {
+                m_log->warn("RAUC install error: '{}'", e.what());
+                session->set_error(e.getMessage().c_str(), nullptr);
+                return SR_ERR_OPERATION_FAILED;
+            }
+            return SR_ERR_OK;
+        },
+        0,
+        SR_SUBSCR_CTX_REUSE);
+}
+}
diff --git a/src/system/CzechlightSystem.h b/src/system/CzechlightSystem.h
new file mode 100644
index 0000000..f03173f
--- /dev/null
+++ b/src/system/CzechlightSystem.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
+ *
+ */
+#pragma once
+
+#include <filesystem>
+#include <sdbus-c++/sdbus-c++.h>
+#include <sysrepo-cpp/Session.hpp>
+#include "system/RAUC.h"
+#include "utils/log-fwd.h"
+
+namespace velia::system {
+
+class CzechlightSystem {
+public:
+    CzechlightSystem(std::shared_ptr<::sysrepo::Connection> srConn, sdbus::IConnection& dbusConnection);
+
+private:
+    std::shared_ptr<::sysrepo::Connection> m_srConn;
+    std::shared_ptr<::sysrepo::Session> m_srSession;
+    std::shared_ptr<::sysrepo::Subscribe> m_srSubscribe;
+    std::shared_ptr<RAUC> m_rauc;
+    velia::Log m_log;
+};
+}
diff --git a/tests/mock/sysrepo/events.cpp b/tests/mock/sysrepo/events.cpp
new file mode 100644
index 0000000..7ba2123
--- /dev/null
+++ b/tests/mock/sysrepo/events.cpp
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2016-2018 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Jan Kundrát <jan.kundrat@cesnet.cz>
+ *
+*/
+
+#include "events.h"
+
+namespace {
+std::string sr_ev_notif_type_to_string(const sr_ev_notif_type_t notif_type)
+{
+    switch (notif_type) {
+    case SR_EV_NOTIF_REALTIME:
+        return "SR_EV_NOTIF_REALTIME";
+    case SR_EV_NOTIF_REPLAY:
+        return "SR_EV_NOTIF_REPLAY";
+    case SR_EV_NOTIF_REPLAY_COMPLETE:
+        return "SR_EV_NOTIF_REPLAY_COMPLETE";
+    case SR_EV_NOTIF_STOP:
+        return "SR_EV_NOTIF_STOP";
+    default:
+        return "[unknown event type]";
+    }
+}
+}
+
+EventWatcher::~EventWatcher()
+{
+}
+
+void EventWatcher::operator()(
+    [[maybe_unused]] ::sysrepo::S_Session session,
+    const sr_ev_notif_type_t notif_type,
+    const char* xpath,
+    const ::sysrepo::S_Vals vals,
+    time_t timestamp)
+{
+    Event e;
+    e.xPath = xpath;
+    e.received = std::chrono::steady_clock::now();
+
+    auto log = spdlog::get("main");
+    log->info("SR event {} {} {}", sr_ev_notif_type_to_string(notif_type), timestamp, xpath);
+
+    for (size_t i = 0; i < vals->val_cnt(); ++i) {
+        const auto& v = vals->val(i);
+        auto s = v->val_to_string();
+        log->debug(" {}: {}", v->xpath(), s);
+        e.data[v->xpath()] = s;
+    }
+
+    std::lock_guard<std::mutex> lock(*mutex);
+    events->push_back(e);
+}
+
+std::vector<EventWatcher::Event>::size_type EventWatcher::count() const
+{
+    std::lock_guard<std::mutex> lock(*mutex);
+    return events->size();
+}
+
+EventWatcher::Event EventWatcher::peek(const std::vector<EventWatcher::Event>::size_type index) const
+{
+    std::lock_guard<std::mutex> lock(*mutex);
+    return (*events)[index];
+}
diff --git a/tests/mock/sysrepo/events.h b/tests/mock/sysrepo/events.h
new file mode 100644
index 0000000..7177dce
--- /dev/null
+++ b/tests/mock/sysrepo/events.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016-2018 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Jan Kundrát <jan.kundrat@cesnet.cz>
+ *
+*/
+
+#pragma once
+
+#include <chrono>
+#include <mutex>
+#include <sysrepo-cpp/Session.hpp>
+#include "test_log_setup.h"
+
+class EventWatcher {
+public:
+    ~EventWatcher();
+    void operator()(::sysrepo::S_Session session, const sr_ev_notif_type_t notif_type, const char* xpath, const ::sysrepo::S_Vals vals, time_t timestamp);
+
+    struct Event {
+        std::string xPath;
+        std::map<std::string, std::string> data;
+        std::chrono::time_point<std::chrono::steady_clock> received;
+    };
+
+    Event peek(std::vector<Event>::size_type index) const;
+    std::vector<Event>::size_type count() const;
+
+private:
+    mutable std::shared_ptr<std::mutex> mutex = std::make_shared<std::mutex>();
+    std::shared_ptr<std::vector<Event>> events = std::make_shared<std::vector<Event>>();
+};
diff --git a/tests/sysrepo_system-czechlightsystem.cpp b/tests/sysrepo_system-czechlightsystem.cpp
new file mode 100644
index 0000000..fe0f2a2
--- /dev/null
+++ b/tests/sysrepo_system-czechlightsystem.cpp
@@ -0,0 +1,176 @@
+#include "trompeloeil_doctest.h"
+#include "dbus-helpers/dbus_rauc_server.h"
+#include "pretty_printers.h"
+#include "system/CzechlightSystem.h"
+#include "test_log_setup.h"
+#include "test_sysrepo_helpers.h"
+#include "tests/configure.cmake.h"
+#include "tests/mock/sysrepo/events.h"
+
+using namespace std::literals;
+
+TEST_CASE("czechlight-system")
+{
+    trompeloeil::sequence seq1;
+
+    TEST_SYSREPO_INIT_LOGS;
+    TEST_SYSREPO_INIT;
+    TEST_SYSREPO_INIT_CLIENT;
+
+    auto dbusServerConnection = sdbus::createSessionBusConnection("de.pengutronix.rauc");
+    auto dbusClientConnection = sdbus::createSessionBusConnection();
+    dbusClientConnection->enterEventLoopAsync();
+    dbusServerConnection->enterEventLoopAsync();
+
+    std::map<std::string, velia::system::RAUC::SlotProperties> dbusRaucStatus = {
+        {"rootfs.1", {
+                         {"activated.count", uint32_t {39}},
+                         {"activated.timestamp", "2021-01-13T17:20:18Z"},
+                         {"bootname", "B"},
+                         {"boot-status", "good"},
+                         {"bundle.compatible", "czechlight-clearfog"},
+                         {"bundle.version", "v4-103-g34d2f48"},
+                         {"class", "rootfs"},
+                         {"device", "/dev/mmcblk0p3"},
+                         {"installed.count", uint32_t {39}},
+                         {"installed.timestamp", "2021-01-13T17:20:15Z"},
+                         {"mountpoint", "/"},
+                         {"sha256", "07b30d065c7aad64d2006ce99fd339c929d3ca97b666fca4584b9ef726469fc4"},
+                         {"size", uint64_t {45601892}},
+                         {"state", "booted"},
+                         {"status", "ok"},
+                         {"type", "ext4"},
+                     }},
+        {"rootfs.0", {
+                         {"activated.count", uint32_t {41}},
+                         {"activated.timestamp", "2021-01-13T17:15:54Z"},
+                         {"bootname", "A"},
+                         {"boot-status", "bad"},
+                         {"bundle.compatible", "czechlight-clearfog"},
+                         {"bundle.version", "v4-104-ge80fcd4"},
+                         {"class", "rootfs"},
+                         {"device", "/dev/mmcblk0p1"},
+                         {"installed.count", uint32_t {41}},
+                         {"installed.timestamp", "2021-01-13T17:15:50Z"},
+                         {"sha256", "6d81e8f341edd17c127811f7347c7e23d18c2fc25c0bdc29ac56999cc9c25629"},
+                         {"size", uint64_t {45647664}},
+                         {"state", "inactive"},
+                         {"status", "ok"},
+                         {"type", "ext4"},
+                     }},
+        {"cfg.1", {
+                      {"bundle.compatible", "czechlight-clearfog"},
+                      {"bundle.version", "v4-103-g34d2f48"},
+                      {"class", "cfg"},
+                      {"device", "/dev/mmcblk0p4"},
+                      {"installed.count", uint32_t {39}},
+                      {"installed.timestamp", "2021-01-13T17:20:18Z"},
+                      {"mountpoint", "/cfg"},
+                      {"parent", "rootfs.1"},
+                      {"sha256", "5ca1b6c461fc194055d52b181f57c63dc1d34c19d041f6395e6f6abc039692bb"},
+                      {"size", uint64_t {108}},
+                      {"state", "active"},
+                      {"status", "ok"},
+                      {"type", "ext4"},
+                  }},
+        {"cfg.0", {
+                      {"bundle.compatible", "czechlight-clearfog"},
+                      {"bundle.version", "v4-104-ge80fcd4"},
+                      {"class", "cfg"},
+                      {"device", "/dev/mmcblk0p2"},
+                      {"installed.count", uint32_t {41}},
+                      {"installed.timestamp", "2021-01-13T17:15:54Z"},
+                      {"parent", "rootfs.0"},
+                      {"sha256", "5ca1b6c461fc194055d52b181f57c63dc1d34c19d041f6395e6f6abc039692bb"},
+                      {"size", uint64_t {108}},
+                      {"state", "inactive"},
+                      {"status", "ok"},
+                      {"type", "ext4"},
+                  }},
+    };
+    auto raucServer = DBusRAUCServer(*dbusServerConnection, "rootfs.1", dbusRaucStatus);
+    auto sysrepo = std::make_shared<velia::system::CzechlightSystem>(srConn, *dbusClientConnection);
+
+    REQUIRE(dataFromSysrepo(client, "/czechlight-system:firmware", SR_DS_OPERATIONAL) == std::map<std::string, std::string>{
+        {"/installation", ""},
+        {"/installation/message", ""},
+        {"/installation/status", "none"},
+    });
+
+    SECTION("Firmware install RPC")
+    {
+        auto rpcInput = std::make_shared<sysrepo::Vals>(1);
+        rpcInput->val(0)->set("/czechlight-system:firmware/installation/install/url", "/path/to/bundle/update.raucb");
+
+        SECTION("Installation runs")
+        {
+            DBusRAUCServer::InstallBehaviour installType;
+            std::map<std::string, std::string> expectedFinished, expectedInProgress = {
+                {"/installation", ""},
+                {"/installation/message", ""},
+                {"/installation/status", "in-progress"},
+            };
+            size_t expectedNotificationsCount;
+            std::string expectedLastNotificationMsg;
+
+            // subscribe to notifications
+            EventWatcher events;
+            subscription->event_notif_subscribe("czechlight-system", events, "/czechlight-system:firmware/installation/update");
+
+            SECTION("Successfull install")
+            {
+                installType = DBusRAUCServer::InstallBehaviour::OK;
+                expectedFinished = {
+                    {"/installation", ""},
+                    {"/installation/message", ""},
+                    {"/installation/status", "succeeded"},
+                };
+                expectedNotificationsCount = 22;
+                expectedLastNotificationMsg = "Installing done.";
+            }
+
+            SECTION("Unsuccessfull install")
+            {
+                installType = DBusRAUCServer::InstallBehaviour::FAILURE;
+                expectedFinished = {
+                    {"/installation", ""},
+                    {"/installation/message", "Failed to download bundle https://10.88.3.11:8000/update.raucb: Transfer failed: error:1408F10B:SSL routines:ssl3_get_record:wrong version number"},
+                    {"/installation/status", "failed"},
+                };
+                expectedNotificationsCount = 6;
+                expectedLastNotificationMsg = "Installing failed.";
+            }
+
+            raucServer.installBundleBehaviour(installType);
+            auto res = client->rpc_send("/czechlight-system:firmware/installation/install", rpcInput);
+            REQUIRE(res->val_cnt() == 0);
+
+            std::this_thread::sleep_for(10ms); // lets wait a while, so the RAUC's callback for operation changed takes place
+            REQUIRE(dataFromSysrepo(client, "/czechlight-system:firmware", SR_DS_OPERATIONAL) == expectedInProgress);
+
+            std::this_thread::sleep_for(2s); // lets wait a while, so the installation can finish
+            REQUIRE(dataFromSysrepo(client, "/czechlight-system:firmware", SR_DS_OPERATIONAL) == expectedFinished);
+
+            // check updates notification count and that at least some of them are reasonable
+            REQUIRE(events.count() == expectedNotificationsCount);
+            REQUIRE(events.peek(0).data["/czechlight-system:firmware/installation/update/message"] == "Installing");
+            REQUIRE(events.peek(0).data["/czechlight-system:firmware/installation/update/progress"] == "0");
+            REQUIRE(events.peek(events.count() - 1).data["/czechlight-system:firmware/installation/update/message"] == expectedLastNotificationMsg);
+            REQUIRE(events.peek(events.count() - 1).data["/czechlight-system:firmware/installation/update/progress"] == "100");
+
+            // check updates notification progress is an increasing sequence
+            for (size_t i = 1; i < events.count(); i++) {
+                auto prevProgress = std::stoi(events.peek(i - 1).data["/czechlight-system:firmware/installation/update/progress"]);
+                auto currProgress = std::stoi(events.peek(i).data["/czechlight-system:firmware/installation/update/progress"]);
+                REQUIRE(prevProgress <= currProgress);
+            }
+        }
+
+        SECTION("Invoke another installation before the first finishes")
+        {
+            client->rpc_send("/czechlight-system:firmware/installation/install", rpcInput);
+            std::this_thread::sleep_for(10ms);
+            REQUIRE_THROWS_WITH_AS(client->rpc_send("/czechlight-system:firmware/installation/install", rpcInput), "User callback failed", sysrepo::sysrepo_exception);
+        }
+    }
+}
diff --git a/tests/system_rauc.cpp b/tests/system_rauc.cpp
index a60d0fe..1c13846 100644
--- a/tests/system_rauc.cpp
+++ b/tests/system_rauc.cpp
@@ -95,7 +95,7 @@
         *clientConnection,
         [&fakeRaucInstallCb](const std::string& operation) { fakeRaucInstallCb.operationCallback(operation); },
         [&fakeRaucInstallCb](int32_t perc, const std::string& msg) { fakeRaucInstallCb.progressCallback(perc, msg); },
-        [&fakeRaucInstallCb](int32_t retval, const std::string& lastError) { fakeRaucInstallCb.completedCallback(retval, lastError); });
+        [&fakeRaucInstallCb](int32_t retVal, const std::string& lastError) { fakeRaucInstallCb.completedCallback(retVal, lastError); });
 
     REQUIRE(rauc->lastError() == "");
     REQUIRE(rauc->operation() == "idle");
diff --git a/tests/test_sysrepo_helpers.h b/tests/test_sysrepo_helpers.h
index bf8bf5f..54d2d71 100644
--- a/tests/test_sysrepo_helpers.h
+++ b/tests/test_sysrepo_helpers.h
@@ -44,6 +44,7 @@
     auto srSess = std::make_shared<sysrepo::Session>(srConn); \
     auto srSubs = std::make_shared<sysrepo::Subscribe>(srSess);
 
-#define TEST_SYSREPO_INIT_CLIENT                               \
-    auto clientConn = std::make_shared<sysrepo::Connection>(); \
-    auto client = std::make_shared<sysrepo::Session>(clientConn);
+#define TEST_SYSREPO_INIT_CLIENT                                  \
+    auto clientConn = std::make_shared<sysrepo::Connection>();    \
+    auto client = std::make_shared<sysrepo::Session>(clientConn); \
+    auto subscription = std::make_shared<sysrepo::Subscribe>(client);