input: Systemd unit monitor input

Depends-on: https://gerrit.cesnet.cz/c/CzechLight/dependencies/+/2869
Change-Id: Ia22b4cf620a58624357549bd903455fc94c76931
diff --git a/src/inputs/DbusSystemdInput.cpp b/src/inputs/DbusSystemdInput.cpp
new file mode 100644
index 0000000..9549809
--- /dev/null
+++ b/src/inputs/DbusSystemdInput.cpp
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@fit.cvut.cz>
+ *
+*/
+#include "DbusSystemdInput.h"
+#include "utils/log.h"
+
+namespace velia {
+
+/** @brief Construct the systemd unit watcher for arbitrary dbus object. Mainly for tests. */
+DbusSystemdInput::DbusSystemdInput(std::shared_ptr<AbstractManager> manager, sdbus::IConnection& connection, const std::string& busname, const std::string& managerObjectPath, const std::string& managerIface, const std::string& unitIface)
+    : AbstractInput(std::move(manager))
+    , m_log(spdlog::get("input"))
+    , m_busName(busname)
+    , m_unitIface(unitIface)
+    , m_proxyManager(sdbus::createProxy(connection, m_busName, managerObjectPath))
+{
+    // Subscribe to systemd events. Systemd may not generate signals unless explicitly called
+    m_proxyManager->callMethod("Subscribe").onInterface(managerIface).withArguments().dontExpectReply();
+
+    // Register to a signal introducing new unit
+    m_proxyManager->uponSignal("UnitNew").onInterface(managerIface).call([&](const std::string& unitName, const sdbus::ObjectPath& unitObjectPath) {
+        if (m_proxyUnits.find(unitObjectPath) == m_proxyUnits.end()) {
+            registerSystemdUnit(connection, unitName, unitObjectPath);
+        }
+    });
+    m_proxyManager->finishRegistration();
+
+    /* Track all current units. Method ListUnits(a(ssssssouso) out) method returns a dbus struct type with information
+     * about the unit (see https://www.freedesktop.org/wiki/Software/systemd/dbus/#Manager-ListUnits).
+     * In our code we need only the first (index 0, the unit name) field and seventh (index 6, unit object path) field.
+     */
+    std::vector<sdbus::Struct<std::string, std::string, std::string, std::string, std::string, std::string, sdbus::ObjectPath, uint32_t, std::string, sdbus::ObjectPath>> units;
+    m_proxyManager->callMethod("ListUnits").onInterface(managerIface).storeResultsTo(units);
+    for (const auto& unit : units) {
+        registerSystemdUnit(connection, unit.get<0>(), unit.get<6>());
+    }
+}
+
+/** @brief Construct the systemd watcher for well-known systemd paths. */
+DbusSystemdInput::DbusSystemdInput(std::shared_ptr<AbstractManager> manager, sdbus::IConnection& connection)
+    : DbusSystemdInput(std::move(manager), connection, "org.freedesktop.systemd1", "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager", "org.freedesktop.systemd1.Unit")
+{
+}
+
+/** @brief Registers a systemd unit by its unit name and unit dbus objectpath. */
+void DbusSystemdInput::registerSystemdUnit(sdbus::IConnection& connection, const std::string& unitName, const sdbus::ObjectPath& unitObjectPath)
+{
+    auto proxyUnit = sdbus::createProxy(connection, m_busName, unitObjectPath);
+    proxyUnit->uponSignal("PropertiesChanged").onInterface("org.freedesktop.DBus.Properties").call([&, unitName](const std::string& iface, const std::map<std::string, sdbus::Variant>& changed, [[maybe_unused]] const std::vector<std::string>& invalidated) {
+        if (iface != m_unitIface) {
+            return;
+        }
+
+        std::string nActiveState, nSubState;
+        if (auto it = changed.find("ActiveState"); it != changed.end()) {
+            nActiveState = it->second.get<std::string>();
+        }
+        if (auto it = changed.find("SubState"); it != changed.end()) {
+            nSubState = it->second.get<std::string>();
+        }
+
+        onUnitStateChange(unitName, nActiveState, nSubState);
+        m_log->trace("Systemd unit '{}' changed state ({} {})", unitName, nActiveState, nSubState);
+    });
+    proxyUnit->finishRegistration();
+
+    // Query the current state of this unit
+    std::string nActiveState = proxyUnit->getProperty("ActiveState").onInterface(m_unitIface);
+    std::string nSubState = proxyUnit->getProperty("SubState").onInterface(m_unitIface);
+    onUnitStateChange(unitName, nActiveState, nSubState);
+
+    m_proxyUnits.emplace(std::make_pair(unitObjectPath, std::move(proxyUnit)));
+    m_log->trace("Registered systemd unit watcher for '{}' ({} {})", unitName, nActiveState, nSubState);
+}
+
+/** @brief Callback for unit state change */
+void DbusSystemdInput::onUnitStateChange(const std::string& name, const std::string& activeState, const std::string& subState)
+{
+    if (activeState == "failed" || (activeState == "activating" && subState == "auto-restart")) {
+        m_failedUnits.insert(name); // this unit is in failed state
+    } else {
+        m_failedUnits.erase(name); // this unit is now OK
+    }
+
+    updateState(m_failedUnits.empty() ? State::OK : State::ERROR);
+}
+
+DbusSystemdInput::~DbusSystemdInput() = default;
+
+}
\ No newline at end of file
diff --git a/src/inputs/DbusSystemdInput.h b/src/inputs/DbusSystemdInput.h
new file mode 100644
index 0000000..3da35da
--- /dev/null
+++ b/src/inputs/DbusSystemdInput.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@fit.cvut.cz>
+ *
+*/
+#pragma once
+
+#include <memory>
+#include <sdbus-c++/sdbus-c++.h>
+#include <set>
+#include "inputs/AbstractInput.h"
+#include "manager/StateManager.h"
+
+namespace velia {
+
+/**
+ * Register
+ */
+class DbusSystemdInput : public AbstractInput {
+public:
+    DbusSystemdInput(std::shared_ptr<AbstractManager> manager, sdbus::IConnection& connection);
+    DbusSystemdInput(std::shared_ptr<AbstractManager> manager, sdbus::IConnection& connection, const std::string& busname, const std::string& managerObjectPath, const std::string& managerIface, const std::string& unitIface);
+    ~DbusSystemdInput() override;
+
+private:
+    velia::Log m_log;
+
+    std::string m_busName;
+    std::string m_unitIface;
+    std::unique_ptr<sdbus::IProxy> m_proxyManager;
+
+    /** List of registered unit watchers */
+    std::map<sdbus::ObjectPath, std::unique_ptr<sdbus::IProxy>> m_proxyUnits;
+
+    /** List of units in failed state. */
+    std::set<std::string> m_failedUnits;
+
+    void registerSystemdUnit(sdbus::IConnection& connection, const std::string& unitName, const sdbus::ObjectPath& unitObjectPath);
+    void onUnitStateChange(const std::string& name, const std::string& activeState, const std::string& nSubState);
+};
+
+}
\ No newline at end of file