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/CMakeLists.txt b/CMakeLists.txt
index 76e6078..f07caac 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -122,6 +122,18 @@
PkgConfig::LIBYANG
)
+# - ietf-system
+add_library(velia-system STATIC
+ src/system/Sysrepo.cpp
+ src/system/Sysrepo.h
+ )
+target_link_libraries(velia-system
+ PUBLIC
+ velia-utils
+ PkgConfig::SYSREPO
+ PkgConfig::LIBYANG
+ )
+
# - daemon
add_executable(veliad
src/main.cpp
@@ -133,6 +145,7 @@
velia-health
velia-ietf-hardware
velia-ietf-hardware-sysrepo
+ velia-system
docopt
)
add_dependencies(veliad target-VELIA_VERSION)
@@ -222,6 +235,14 @@
velia_test(hardware_emmc velia-ietf-hardware FsTestUtils)
velia_test(hardware_hwmon velia-ietf-hardware FsTestUtils)
+ sysrepo_fixture_env(sysrepo-ietf-system YANG ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-system@2014-08-06.yang)
+ velia_test(sysrepo_system velia-system DbusTesting)
+ set_tests_properties(
+ test-sysrepo_system
+ PROPERTIES FIXTURES_REQUIRED sysrepo:env:sysrepo-ietf-system
+ RESOURCE_LOCK sysrepo
+ )
+
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)
velia_test(hardware_ietf-hardware velia-ietf-hardware velia-ietf-hardware-sysrepo)
set_tests_properties(
diff --git a/src/main.cpp b/src/main.cpp
index 23571b2..c0128df 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -11,6 +11,7 @@
#include "health/manager/StateManager.h"
#include "health/outputs/callables.h"
#include "ietf-hardware/sysrepo/Sysrepo.h"
+#include "system/Sysrepo.h"
#include "utils/exceptions.h"
#include "utils/journal.h"
#include "utils/log-init.h"
@@ -102,6 +103,10 @@
spdlog::get("main")->debug("Initializing Sysrepo ietf-hardware callback");
auto sysrepoIETFHardware = velia::ietf_hardware::sysrepo::Sysrepo(srSubscription, ietfHardware);
+ // initialize ietf-system
+ spdlog::get("main")->debug("Initializing Sysrepo for system models");
+ auto sysrepoSystem = velia::system::Sysrepo(srSess, "/etc/os-release");
+
// Gracefully leave dbus event loop on SIGTERM
struct sigaction sigact;
memset(&sigact, 0, sizeof(sigact));
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;
+};
+}
diff --git a/src/utils/log-init.cpp b/src/utils/log-init.cpp
index 7b6724f..6a74f26 100644
--- a/src/utils/log-init.cpp
+++ b/src/utils/log-init.cpp
@@ -17,7 +17,7 @@
*/
void initLogs(std::shared_ptr<spdlog::sinks::sink> sink)
{
- for (const auto& name : {"main", "health", "hardware", "sysrepo"}) {
+ for (const auto& name : {"main", "health", "hardware", "sysrepo", "system"}) {
spdlog::register_logger(std::make_shared<spdlog::logger>(name, sink));
}
}
diff --git a/tests/sysrepo_system.cpp b/tests/sysrepo_system.cpp
new file mode 100644
index 0000000..f10a3e9
--- /dev/null
+++ b/tests/sysrepo_system.cpp
@@ -0,0 +1,72 @@
+#include "trompeloeil_doctest.h"
+#include "pretty_printers.h"
+#include "system/Sysrepo.h"
+#include "test_log_setup.h"
+#include "test_sysrepo_helpers.h"
+#include "tests/configure.cmake.h"
+
+using namespace std::literals;
+
+TEST_CASE("System stuff in Sysrepo")
+{
+ trompeloeil::sequence seq1;
+
+ TEST_SYSREPO_INIT_LOGS;
+ TEST_SYSREPO_INIT;
+
+ SECTION("Test system-state")
+ {
+ TEST_SYSREPO_INIT_CLIENT;
+ static const auto modulePrefix = "/ietf-system:system-state"s;
+
+ SECTION("Valid data")
+ {
+ std::filesystem::path file;
+ std::map<std::string, std::string> expected;
+
+ SECTION("Real data")
+ {
+ file = CMAKE_CURRENT_SOURCE_DIR "/tests/system/os-release";
+ expected = {
+ {"/clock", ""},
+ {"/platform", ""},
+ {"/platform/os-name", "CzechLight"},
+ {"/platform/os-release", "v4-105-g8294175-dirty"},
+ {"/platform/os-version", "v4-105-g8294175-dirty"},
+ };
+ }
+
+ SECTION("Missing =")
+ {
+ file = CMAKE_CURRENT_SOURCE_DIR "/tests/system/missing-equal";
+ expected = {
+ {"/clock", ""},
+ {"/platform", ""},
+ {"/platform/os-name", ""},
+ {"/platform/os-release", ""},
+ {"/platform/os-version", ""},
+ };
+ }
+
+ SECTION("Empty values")
+ {
+ file = CMAKE_CURRENT_SOURCE_DIR "/tests/system/empty-values";
+ expected = {
+ {"/clock", ""},
+ {"/platform", ""},
+ {"/platform/os-name", ""},
+ {"/platform/os-release", ""},
+ {"/platform/os-version", ""},
+ };
+ }
+
+ auto sysrepo = std::make_shared<velia::system::Sysrepo>(srSess, file);
+ REQUIRE(dataFromSysrepo(client, modulePrefix, SR_DS_OPERATIONAL) == expected);
+ }
+
+ SECTION("Invalid data (missing VERSION and NAME keys)")
+ {
+ REQUIRE_THROWS_AS(std::make_shared<velia::system::Sysrepo>(srSess, CMAKE_CURRENT_SOURCE_DIR "/tests/system/missing-keys"), std::out_of_range);
+ }
+ }
+}
diff --git a/tests/system/empty-values b/tests/system/empty-values
new file mode 100644
index 0000000..910749b
--- /dev/null
+++ b/tests/system/empty-values
@@ -0,0 +1,2 @@
+VERSION=
+NAME=
diff --git a/tests/system/missing-equal b/tests/system/missing-equal
new file mode 100644
index 0000000..ad16e1c
--- /dev/null
+++ b/tests/system/missing-equal
@@ -0,0 +1,2 @@
+VERSION
+NAME
diff --git a/tests/system/missing-keys b/tests/system/missing-keys
new file mode 100644
index 0000000..ee45a08
--- /dev/null
+++ b/tests/system/missing-keys
@@ -0,0 +1,5 @@
+# asd
+# asd
+PRETTY_NAME="Czech Light v4-105-g8294175-dirty"
+
+# asd
diff --git a/tests/system/os-release b/tests/system/os-release
new file mode 100644
index 0000000..c586c33
--- /dev/null
+++ b/tests/system/os-release
@@ -0,0 +1,15 @@
+BUILDROOT_VERSION=2020.11-rc1-45-g386283e33b
+ID=buildroot
+BUILDROOT_VERSION_ID=2020.11-rc1
+NAME=CzechLight
+# When building under CI, these git revisions might not necessarily refer to
+# something that is available from Gerrit's git repositories. If the job which
+# produced this image is a result of a Zuul job tree with speculatively merged
+# changes, then these refs are private to Zuul mergers.
+PRETTY_NAME="Czech Light v4-105-g8294175-dirty"
+
+VERSION=v4-105-g8294175-dirty
+CLA_SYSREPO_VERSION=v4-170-g082749ea-dirty
+NETCONF_CLI_VERSION=0651966
+CPP_DEPENDENCIES_VERSION=cd88475
+GAMMARUS_VERSION=ee4affa