blob: 5439d09e46a984fcfe3aeddca23b47e857cc73af [file] [log] [blame]
Tomáš Pecka292bc9c2021-01-11 22:03:11 +01001/*
2 * Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/
3 *
4 * Written by Tomáš Pecka <tomas.pecka@fit.cvut.cz>
5 *
6 */
7
Tomáš Pecka79344c82021-09-16 18:25:59 +02008#include <arpa/inet.h>
Tomáš Pecka292bc9c2021-01-11 22:03:11 +01009#include <boost/algorithm/string/predicate.hpp>
10#include <fstream>
Jan Kundrát1c3b8812021-05-17 13:06:03 +020011#include <optional>
Tomáš Peckaf976c5b2021-01-23 21:19:52 +010012#include "IETFSystem.h"
Tomáš Pecka879a6032021-02-03 17:21:48 +010013#include "system_vars.h"
14#include "utils/exec.h"
Tomáš Pecka292bc9c2021-01-11 22:03:11 +010015#include "utils/io.h"
16#include "utils/log.h"
Tomáš Pecka272abaf2021-01-24 12:28:43 +010017#include "utils/sysrepo.h"
Jan Kundrát51f1d6d2021-02-19 00:41:28 +010018#include "utils/time.h"
Tomáš Pecka292bc9c2021-01-11 22:03:11 +010019
20using namespace std::literals;
21
22namespace {
23
24const auto IETF_SYSTEM_MODULE_NAME = "ietf-system"s;
25const auto IETF_SYSTEM_STATE_MODULE_PREFIX = "/"s + IETF_SYSTEM_MODULE_NAME + ":system-state/"s;
Václav Kubernát566c9402021-02-10 08:33:54 +010026const auto IETF_SYSTEM_HOSTNAME_PATH = "/ietf-system:system/hostname";
Tomáš Pecka79344c82021-09-16 18:25:59 +020027const auto IETF_SYSTEM_DNS_PATH = "/ietf-system:system/dns-resolver";
Jan Kundrát51f1d6d2021-02-19 00:41:28 +010028const auto IETF_SYSTEM_STATE_CLOCK_PATH = "/ietf-system:system-state/clock";
Tomáš Pecka292bc9c2021-01-11 22:03:11 +010029
30/** @brief Returns key=value pairs from (e.g. /etc/os-release) as a std::map */
31std::map<std::string, std::string> parseKeyValueFile(const std::filesystem::path& path)
32{
33 std::map<std::string, std::string> res;
34 std::ifstream ifs(path);
35 if (!ifs.is_open())
36 throw std::invalid_argument("File '" + std::string(path) + "' not found.");
37
38 std::string line;
39 while (std::getline(ifs, line)) {
40 // man os-release: Lines beginning with "#" shall be ignored as comments. Blank lines are permitted and ignored.
41 if (line.empty() || boost::algorithm::starts_with(line, "#")) {
42 continue;
43 }
44
45 size_t equalSignPos = line.find_first_of('=');
46 if (equalSignPos != std::string::npos) {
47 std::string key = line.substr(0, equalSignPos);
48 std::string val = line.substr(equalSignPos + 1);
49
50 // remove quotes from value
51 if (val.length() >= 2 && val.front() == '"' && val.front() == val.back()) {
52 val = val.substr(1, val.length() - 2);
53 }
54
55 res[key] = val;
56 } else { // when there is no = sign, treat the value as empty string
57 res[line] = "";
58 }
59 }
60
61 return res;
62}
63
Václav Kubernát7efd6d52021-11-09 01:31:11 +010064std::optional<std::string> getHostnameFromChange(const sysrepo::Session session)
Václav Kubernát566c9402021-02-10 08:33:54 +010065{
66 std::optional<std::string> res;
67
Václav Kubernát7efd6d52021-11-09 01:31:11 +010068 auto data = session.getData(IETF_SYSTEM_HOSTNAME_PATH);
Václav Kubernát566c9402021-02-10 08:33:54 +010069 if (data) {
Václav Kubernát7efd6d52021-11-09 01:31:11 +010070 auto hostnameNode = data->findPath(IETF_SYSTEM_HOSTNAME_PATH);
71 res = hostnameNode->asTerm().valueStr();
Václav Kubernát566c9402021-02-10 08:33:54 +010072 }
73
74 return res;
75}
Tomáš Pecka79344c82021-09-16 18:25:59 +020076
77/** @brief Returns list of IP addresses (coded as a string) that serve as the DNS servers.
78 *
79 * We query the addresses from systemd-resolved D-Bus interface (see https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html#Properties
80 * and possibly also https://www.freedesktop.org/software/systemd/man/resolved.conf.html).
81 * We use the value of DnsEx property on the Manager object. In case that DnsEx is empty we fallback to FallbackDnsEx property.
82 *
83 * Note that the returns not only the system-wide setting, but also the DNS resolvers that are configured per-interface. We chose not to ignore them despite ietf-system
84 * YANG model inability to distinguish between system-wide and per-interface type. Hence the resolver is listed as a system-wide one.
85 */
86std::vector<std::string> getDNSResolvers(sdbus::IConnection& connection, const std::string& dbusName)
87{
88 static const auto DBUS_RESOLVE1_MANAGER_PATH = "/org/freedesktop/resolve1";
89 static const auto DBUS_RESOLVE1_MANAGER_INTERFACE = "org.freedesktop.resolve1.Manager";
90
91 auto proxy = sdbus::createProxy(connection, dbusName, DBUS_RESOLVE1_MANAGER_PATH);
92
93 for (const auto& propertyName : {"DNSEx", "FallbackDNSEx"}) {
94 sdbus::Variant store = proxy->getProperty(propertyName).onInterface(DBUS_RESOLVE1_MANAGER_INTERFACE);
95
96 // DBus type of the DNSEx and FallbackDNSEx properties is "a(iiayqs)" ~ Array of [ Struct of (Int32, Int32, Array of [Byte], Uint16, String) ]
97 // i.e., <ifindex (0 for system-wide), addrtype, address as a bytearray, port (0 for unspecified), server name>,
98 auto replyObjects = store.get<std::vector<sdbus::Struct<int32_t, int32_t, std::vector<uint8_t>, uint16_t, std::string>>>();
99
100 if (!replyObjects.empty() > 0) {
101 std::vector<std::string> res;
102
103 for (const auto& e : replyObjects) {
104 auto addrType = e.get<1>();
105 auto addrBytes = e.get<2>();
106
107 std::array<char, std::max(INET_ADDRSTRLEN, INET6_ADDRSTRLEN)> buf{};
108 inet_ntop(addrType, addrBytes.data(), buf.data(), buf.size());
109
110 res.emplace_back(buf.data());
111 }
112
113 return res;
114 }
115 }
116
117 return {};
118}
Tomáš Pecka292bc9c2021-01-11 22:03:11 +0100119}
120
121namespace velia::system {
122
Václav Kubernátd2927cc2021-02-18 04:36:26 +0100123void IETFSystem::initStaticProperties(const std::filesystem::path& osRelease)
Tomáš Pecka292bc9c2021-01-11 22:03:11 +0100124{
Václav Kubernát566c9402021-02-10 08:33:54 +0100125 utils::ensureModuleImplemented(m_srSession, IETF_SYSTEM_MODULE_NAME, "2014-08-06");
126
Tomáš Pecka292bc9c2021-01-11 22:03:11 +0100127 std::map<std::string, std::string> osReleaseContents = parseKeyValueFile(osRelease);
128
129 std::map<std::string, std::string> opsSystemStateData {
130 {IETF_SYSTEM_STATE_MODULE_PREFIX + "platform/os-name", osReleaseContents.at("NAME")},
131 {IETF_SYSTEM_STATE_MODULE_PREFIX + "platform/os-release", osReleaseContents.at("VERSION")},
132 {IETF_SYSTEM_STATE_MODULE_PREFIX + "platform/os-version", osReleaseContents.at("VERSION")},
133 };
134
Jan Kundrát498c3f82023-05-24 19:25:48 +0200135 utils::valuesPush(opsSystemStateData, {}, {}, m_srSession, sysrepo::Datastore::Operational);
Václav Kubernátd2927cc2021-02-18 04:36:26 +0100136}
Tomáš Pecka879a6032021-02-03 17:21:48 +0100137
Václav Kubernátd2927cc2021-02-18 04:36:26 +0100138void IETFSystem::initSystemRestart()
139{
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100140 sysrepo::RpcActionCb cb = [this](auto session, auto, auto, auto, auto, auto, auto) {
Tomáš Pecka879a6032021-02-03 17:21:48 +0100141 try {
142 velia::utils::execAndWait(m_log, SYSTEMCTL_EXECUTABLE, {"reboot"}, "", {});
143 } catch(const std::runtime_error& e) {
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100144 utils::setErrors(session, "Reboot procedure failed.");
145 return sysrepo::ErrorCode::OperationFailed;
Tomáš Pecka879a6032021-02-03 17:21:48 +0100146 }
147
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100148 return sysrepo::ErrorCode::Ok;
149 };
150
Jan Kundrátb3e99982022-03-18 17:38:20 +0100151 m_srSubscribe = m_srSession.onRPCAction("/" + IETF_SYSTEM_MODULE_NAME + ":system-restart", cb);
Tomáš Pecka292bc9c2021-01-11 22:03:11 +0100152}
Václav Kubernátd2927cc2021-02-18 04:36:26 +0100153
Václav Kubernát566c9402021-02-10 08:33:54 +0100154void IETFSystem::initHostname()
155{
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100156 sysrepo::ModuleChangeCb hostNameCbRunning = [this] (auto session, auto, auto, auto, auto, auto) {
Václav Kubernát566c9402021-02-10 08:33:54 +0100157 if (auto newHostname = getHostnameFromChange(session)) {
158 velia::utils::execAndWait(m_log, HOSTNAMECTL_EXECUTABLE, {"set-hostname", *newHostname}, "");
159 }
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100160 return sysrepo::ErrorCode::Ok;
Václav Kubernát566c9402021-02-10 08:33:54 +0100161 };
162
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100163 sysrepo::ModuleChangeCb hostNameCbStartup = [] (auto session, auto, auto, auto, auto, auto) {
Václav Kubernát566c9402021-02-10 08:33:54 +0100164 if (auto newHostname = getHostnameFromChange(session)) {
165 utils::safeWriteFile(BACKUP_ETC_HOSTNAME_FILE, *newHostname);
166 }
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100167 return sysrepo::ErrorCode::Ok;
Václav Kubernát566c9402021-02-10 08:33:54 +0100168 };
169
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100170 sysrepo::OperGetCb hostNameCbOperational = [] (auto, auto, auto, auto, auto, auto, auto& parent) {
Václav Kubernát566c9402021-02-10 08:33:54 +0100171 // + 1 for null-terminating byte, HOST_NAME_MAX doesn't count that
Václav Kubernáte407f742021-05-18 10:47:13 +0200172 std::array<char, HOST_NAME_MAX + 1> buffer{};
Václav Kubernát566c9402021-02-10 08:33:54 +0100173
174 if (gethostname(buffer.data(), buffer.size()) != 0) {
175 throw std::system_error(errno, std::system_category(), "gethostname() failed");
176 }
177
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100178 parent->newPath( IETF_SYSTEM_HOSTNAME_PATH, buffer.data());
Václav Kubernát566c9402021-02-10 08:33:54 +0100179
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100180 return sysrepo::ErrorCode::Ok;
Václav Kubernát566c9402021-02-10 08:33:54 +0100181 };
182
Jan Kundrát80113ed2022-04-27 18:10:52 +0200183 m_srSubscribe->onModuleChange(IETF_SYSTEM_MODULE_NAME, hostNameCbRunning, IETF_SYSTEM_HOSTNAME_PATH, 0, sysrepo::SubscribeOptions::DoneOnly | sysrepo::SubscribeOptions::Enabled);
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100184 m_srSession.switchDatastore(sysrepo::Datastore::Startup);
Jan Kundrát80113ed2022-04-27 18:10:52 +0200185 m_srSubscribe->onModuleChange(IETF_SYSTEM_MODULE_NAME, hostNameCbStartup, IETF_SYSTEM_HOSTNAME_PATH, 0, sysrepo::SubscribeOptions::DoneOnly);
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100186 m_srSession.switchDatastore(sysrepo::Datastore::Operational);
Jan Kundrátb3e99982022-03-18 17:38:20 +0100187 m_srSubscribe->onOperGet(IETF_SYSTEM_MODULE_NAME, hostNameCbOperational, IETF_SYSTEM_HOSTNAME_PATH);
Václav Kubernát566c9402021-02-10 08:33:54 +0100188}
189
Jan Kundrátdcd50f02021-02-18 23:28:26 +0100190/** @short Acknowledge writes to dummy fields so that they're visible in the operational DS */
191void IETFSystem::initDummies()
192{
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100193 m_srSession.switchDatastore(sysrepo::Datastore::Running);
194 sysrepo::ModuleChangeCb ignore = [] (auto, auto, auto, auto, auto, auto) {
195 return sysrepo::ErrorCode::Ok;
Jan Kundrátdcd50f02021-02-18 23:28:26 +0100196 };
197 for (const auto xpath : {"/ietf-system:system/location", "/ietf-system:system/contact"}) {
Jan Kundrát80113ed2022-04-27 18:10:52 +0200198 m_srSubscribe->onModuleChange(IETF_SYSTEM_MODULE_NAME, ignore, xpath, 0, sysrepo::SubscribeOptions::DoneOnly /* it's a dummy write, no need for SubscribeOptions::Enabled */);
Jan Kundrátdcd50f02021-02-18 23:28:26 +0100199 }
200}
201
Jan Kundrát51f1d6d2021-02-19 00:41:28 +0100202/** @short Time and clock callbacks */
203void IETFSystem::initClock()
204{
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100205 sysrepo::OperGetCb cb = [] (auto, auto, auto, auto, auto, auto, auto& parent) {
Jan Kundrátb3e99982022-03-18 17:38:20 +0100206 parent->newPath(IETF_SYSTEM_STATE_CLOCK_PATH + "/current-datetime"s, utils::yangTimeFormat(std::chrono::system_clock::now()));
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100207 return sysrepo::ErrorCode::Ok;
208 };
209
Jan Kundrátb3e99982022-03-18 17:38:20 +0100210 m_srSubscribe->onOperGet(IETF_SYSTEM_MODULE_NAME, cb, IETF_SYSTEM_STATE_CLOCK_PATH, sysrepo::SubscribeOptions::OperMerge);
Jan Kundrát51f1d6d2021-02-19 00:41:28 +0100211}
212
Tomáš Pecka79344c82021-09-16 18:25:59 +0200213/** @short DNS resolver callbacks */
214void IETFSystem::initDNS(sdbus::IConnection& connection, const std::string& dbusName) {
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100215 sysrepo::OperGetCb dnsOper = [&connection, dbusName] (auto session, auto, auto, auto, auto, auto, auto& parent) {
Tomáš Pecka79344c82021-09-16 18:25:59 +0200216 std::map<std::string, std::string> values;
217
218 /* RFC 7317 specifies that key leaf 'name' contains "An arbitrary name for the DNS server".
219 We use the IP address which is unique. If the server is returned multiple times (e.g. once as system-wide and once
220 for some specific ifindex, it doesn't matter that it is listed only once. */
221 for (const auto& e : getDNSResolvers(connection, dbusName)) {
222 values[IETF_SYSTEM_DNS_PATH + "/server[name='"s + e + "']/udp-and-tcp/address"] = e;
223 }
224
Jan Kundrát498c3f82023-05-24 19:25:48 +0200225 utils::valuesToYang(values, {}, {}, session, parent);
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100226 return sysrepo::ErrorCode::Ok;
Tomáš Pecka79344c82021-09-16 18:25:59 +0200227 };
228
Jan Kundrátb3e99982022-03-18 17:38:20 +0100229 m_srSubscribe->onOperGet(IETF_SYSTEM_MODULE_NAME, dnsOper, IETF_SYSTEM_DNS_PATH);
Tomáš Pecka79344c82021-09-16 18:25:59 +0200230}
231
Václav Kubernát372d2772021-02-18 04:42:16 +0100232/** This class handles multiple system properties and publishes them via the ietf-system model:
233 * - OS-identification data from osRelease file
234 * - Rebooting
Václav Kubernát566c9402021-02-10 08:33:54 +0100235 * - Hostname
Václav Kubernát372d2772021-02-18 04:42:16 +0100236 */
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100237IETFSystem::IETFSystem(::sysrepo::Session srSession, const std::filesystem::path& osRelease, sdbus::IConnection& connection, const std::string& dbusName)
238 : m_srSession(srSession)
239 , m_srSubscribe()
Václav Kubernátd2927cc2021-02-18 04:36:26 +0100240 , m_log(spdlog::get("system"))
241{
242 initStaticProperties(osRelease);
243 initSystemRestart();
Václav Kubernát566c9402021-02-10 08:33:54 +0100244 initHostname();
Jan Kundrátdcd50f02021-02-18 23:28:26 +0100245 initDummies();
Jan Kundrát51f1d6d2021-02-19 00:41:28 +0100246 initClock();
Tomáš Pecka79344c82021-09-16 18:25:59 +0200247 initDNS(connection, dbusName);
Václav Kubernátd2927cc2021-02-18 04:36:26 +0100248}
Tomáš Pecka292bc9c2021-01-11 22:03:11 +0100249}