sysrepo: Implement parts of ietf-system module

Implement announcing OS name and OS release through ietf-system YANG
model [1], specifically via its top-level container system-state.
The properties are obtained from /etc/os-release file.

[1] https://tools.ietf.org/html/rfc7317

Change-Id: I82d5fd54659ea365232a3a9455dd73f84b8fd0d1
diff --git a/src/system/Sysrepo.cpp b/src/system/Sysrepo.cpp
new file mode 100644
index 0000000..c9ce0fa
--- /dev/null
+++ b/src/system/Sysrepo.cpp
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@fit.cvut.cz>
+ *
+ */
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <fstream>
+#include "Sysrepo.h"
+#include "utils/io.h"
+#include "utils/log.h"
+
+using namespace std::literals;
+
+namespace {
+
+const auto IETF_SYSTEM_MODULE_NAME = "ietf-system"s;
+const auto IETF_SYSTEM_STATE_MODULE_PREFIX = "/"s + IETF_SYSTEM_MODULE_NAME + ":system-state/"s;
+
+/** @brief Returns key=value pairs from (e.g. /etc/os-release) as a std::map */
+std::map<std::string, std::string> parseKeyValueFile(const std::filesystem::path& path)
+{
+    std::map<std::string, std::string> res;
+    std::ifstream ifs(path);
+    if (!ifs.is_open())
+        throw std::invalid_argument("File '" + std::string(path) + "' not found.");
+
+    std::string line;
+    while (std::getline(ifs, line)) {
+        // man os-release: Lines beginning with "#" shall be ignored as comments. Blank lines are permitted and ignored.
+        if (line.empty() || boost::algorithm::starts_with(line, "#")) {
+            continue;
+        }
+
+        size_t equalSignPos = line.find_first_of('=');
+        if (equalSignPos != std::string::npos) {
+            std::string key = line.substr(0, equalSignPos);
+            std::string val = line.substr(equalSignPos + 1);
+
+            // remove quotes from value
+            if (val.length() >= 2 && val.front() == '"' && val.front() == val.back()) {
+                val = val.substr(1, val.length() - 2);
+            }
+
+            res[key] = val;
+        } else { // when there is no = sign, treat the value as empty string
+            res[line] = "";
+        }
+    }
+
+    return res;
+}
+
+}
+
+namespace velia::system {
+
+/** @brief Reads some OS-identification data from osRelease file and publishes them via ietf-system model */
+Sysrepo::Sysrepo(std::shared_ptr<::sysrepo::Session> srSession, const std::filesystem::path& osRelease)
+    : m_srSession(std::move(srSession))
+    , m_log(spdlog::get("system"))
+{
+    std::map<std::string, std::string> osReleaseContents = parseKeyValueFile(osRelease);
+
+    std::map<std::string, std::string> opsSystemStateData {
+        {IETF_SYSTEM_STATE_MODULE_PREFIX + "platform/os-name", osReleaseContents.at("NAME")},
+        {IETF_SYSTEM_STATE_MODULE_PREFIX + "platform/os-release", osReleaseContents.at("VERSION")},
+        {IETF_SYSTEM_STATE_MODULE_PREFIX + "platform/os-version", osReleaseContents.at("VERSION")},
+    };
+
+    sr_datastore_t oldDatastore = m_srSession->session_get_ds();
+    m_srSession->session_switch_ds(SR_DS_OPERATIONAL);
+
+    for (const auto& [k, v] : opsSystemStateData) {
+        m_log->debug("Pushing to sysrepo: {} = {}", k, v);
+        m_srSession->set_item_str(k.c_str(), v.c_str());
+    }
+
+    m_srSession->apply_changes();
+    m_srSession->session_switch_ds(oldDatastore);
+}
+}
diff --git a/src/system/Sysrepo.h b/src/system/Sysrepo.h
new file mode 100644
index 0000000..0aeda9a
--- /dev/null
+++ b/src/system/Sysrepo.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@fit.cvut.cz>
+ *
+ */
+#pragma once
+
+#include <filesystem>
+#include <sysrepo-cpp/Session.hpp>
+#include "utils/log-fwd.h"
+
+namespace velia::system {
+
+class Sysrepo {
+public:
+    explicit Sysrepo(std::shared_ptr<::sysrepo::Session> srSession, const std::filesystem::path& osRelease);
+
+private:
+    std::shared_ptr<::sysrepo::Session> m_srSession;
+    velia::Log m_log;
+};
+}