blob: 558e6a24d04dbd4b6c5cad8967b443f8c9b5e3c9 [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át566c9402021-02-10 08:33:54 +010064std::optional<std::string> getHostnameFromChange(const std::shared_ptr<sysrepo::Session> session)
65{
66 std::optional<std::string> res;
67
68 auto data = session->get_data(IETF_SYSTEM_HOSTNAME_PATH);
69 if (data) {
70 auto hostnameNode = data->find_path(IETF_SYSTEM_HOSTNAME_PATH)->data().front();
71 auto leaf = std::make_shared<libyang::Data_Node_Leaf_List>(hostnameNode);
72 res = leaf->value_str();
73 }
74
75 return res;
76}
Tomáš Pecka79344c82021-09-16 18:25:59 +020077
78/** @brief Returns list of IP addresses (coded as a string) that serve as the DNS servers.
79 *
80 * We query the addresses from systemd-resolved D-Bus interface (see https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html#Properties
81 * and possibly also https://www.freedesktop.org/software/systemd/man/resolved.conf.html).
82 * We use the value of DnsEx property on the Manager object. In case that DnsEx is empty we fallback to FallbackDnsEx property.
83 *
84 * 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
85 * YANG model inability to distinguish between system-wide and per-interface type. Hence the resolver is listed as a system-wide one.
86 */
87std::vector<std::string> getDNSResolvers(sdbus::IConnection& connection, const std::string& dbusName)
88{
89 static const auto DBUS_RESOLVE1_MANAGER_PATH = "/org/freedesktop/resolve1";
90 static const auto DBUS_RESOLVE1_MANAGER_INTERFACE = "org.freedesktop.resolve1.Manager";
91
92 auto proxy = sdbus::createProxy(connection, dbusName, DBUS_RESOLVE1_MANAGER_PATH);
93
94 for (const auto& propertyName : {"DNSEx", "FallbackDNSEx"}) {
95 sdbus::Variant store = proxy->getProperty(propertyName).onInterface(DBUS_RESOLVE1_MANAGER_INTERFACE);
96
97 // DBus type of the DNSEx and FallbackDNSEx properties is "a(iiayqs)" ~ Array of [ Struct of (Int32, Int32, Array of [Byte], Uint16, String) ]
98 // i.e., <ifindex (0 for system-wide), addrtype, address as a bytearray, port (0 for unspecified), server name>,
99 auto replyObjects = store.get<std::vector<sdbus::Struct<int32_t, int32_t, std::vector<uint8_t>, uint16_t, std::string>>>();
100
101 if (!replyObjects.empty() > 0) {
102 std::vector<std::string> res;
103
104 for (const auto& e : replyObjects) {
105 auto addrType = e.get<1>();
106 auto addrBytes = e.get<2>();
107
108 std::array<char, std::max(INET_ADDRSTRLEN, INET6_ADDRSTRLEN)> buf{};
109 inet_ntop(addrType, addrBytes.data(), buf.data(), buf.size());
110
111 res.emplace_back(buf.data());
112 }
113
114 return res;
115 }
116 }
117
118 return {};
119}
Tomáš Pecka292bc9c2021-01-11 22:03:11 +0100120}
121
122namespace velia::system {
123
Václav Kubernátd2927cc2021-02-18 04:36:26 +0100124void IETFSystem::initStaticProperties(const std::filesystem::path& osRelease)
Tomáš Pecka292bc9c2021-01-11 22:03:11 +0100125{
Václav Kubernát566c9402021-02-10 08:33:54 +0100126 utils::ensureModuleImplemented(m_srSession, IETF_SYSTEM_MODULE_NAME, "2014-08-06");
127
Tomáš Pecka292bc9c2021-01-11 22:03:11 +0100128 std::map<std::string, std::string> osReleaseContents = parseKeyValueFile(osRelease);
129
130 std::map<std::string, std::string> opsSystemStateData {
131 {IETF_SYSTEM_STATE_MODULE_PREFIX + "platform/os-name", osReleaseContents.at("NAME")},
132 {IETF_SYSTEM_STATE_MODULE_PREFIX + "platform/os-release", osReleaseContents.at("VERSION")},
133 {IETF_SYSTEM_STATE_MODULE_PREFIX + "platform/os-version", osReleaseContents.at("VERSION")},
134 };
135
Tomáš Pecka498e91c2021-03-02 17:46:47 +0100136 utils::valuesPush(opsSystemStateData, {}, m_srSession, SR_DS_OPERATIONAL);
Václav Kubernátd2927cc2021-02-18 04:36:26 +0100137}
Tomáš Pecka879a6032021-02-03 17:21:48 +0100138
Václav Kubernátd2927cc2021-02-18 04:36:26 +0100139void IETFSystem::initSystemRestart()
140{
Tomáš Pecka879a6032021-02-03 17:21:48 +0100141 m_srSubscribe->rpc_subscribe(
142 ("/" + IETF_SYSTEM_MODULE_NAME + ":system-restart").c_str(),
143 [this](::sysrepo::S_Session session, [[maybe_unused]] const char* op_path, [[maybe_unused]] const ::sysrepo::S_Vals input, [[maybe_unused]] sr_event_t event, [[maybe_unused]] uint32_t request_id, [[maybe_unused]] ::sysrepo::S_Vals_Holder output) {
144 try {
145 velia::utils::execAndWait(m_log, SYSTEMCTL_EXECUTABLE, {"reboot"}, "", {});
146 } catch(const std::runtime_error& e) {
147 session->set_error("Reboot procedure failed.", nullptr);
148 return SR_ERR_OPERATION_FAILED;
149 }
150
151 return SR_ERR_OK;
152 },
153 0,
154 SR_SUBSCR_CTX_REUSE);
Tomáš Pecka292bc9c2021-01-11 22:03:11 +0100155}
Václav Kubernátd2927cc2021-02-18 04:36:26 +0100156
Václav Kubernát566c9402021-02-10 08:33:54 +0100157void IETFSystem::initHostname()
158{
159 sysrepo::ModuleChangeCb hostNameCbRunning = [this] (auto session, auto, auto, auto, auto) {
160 if (auto newHostname = getHostnameFromChange(session)) {
161 velia::utils::execAndWait(m_log, HOSTNAMECTL_EXECUTABLE, {"set-hostname", *newHostname}, "");
162 }
163 return SR_ERR_OK;
164 };
165
166 sysrepo::ModuleChangeCb hostNameCbStartup = [] (auto session, auto, auto, auto, auto) {
167 if (auto newHostname = getHostnameFromChange(session)) {
168 utils::safeWriteFile(BACKUP_ETC_HOSTNAME_FILE, *newHostname);
169 }
170 return SR_ERR_OK;
171 };
172
173 sysrepo::OperGetItemsCb hostNameCbOperational = [] (auto session, auto, auto, auto, auto, auto& parent) {
174 // + 1 for null-terminating byte, HOST_NAME_MAX doesn't count that
Václav Kubernáte407f742021-05-18 10:47:13 +0200175 std::array<char, HOST_NAME_MAX + 1> buffer{};
Václav Kubernát566c9402021-02-10 08:33:54 +0100176
177 if (gethostname(buffer.data(), buffer.size()) != 0) {
178 throw std::system_error(errno, std::system_category(), "gethostname() failed");
179 }
180
181 parent->new_path(
182 session->get_context(),
183 IETF_SYSTEM_HOSTNAME_PATH,
184 buffer.data(),
185 LYD_ANYDATA_CONSTSTRING,
186 0);
187
188 return SR_ERR_OK;
189 };
190
191 m_srSubscribe->module_change_subscribe(IETF_SYSTEM_MODULE_NAME.c_str(), hostNameCbRunning, IETF_SYSTEM_HOSTNAME_PATH, 0, SR_SUBSCR_DONE_ONLY);
192 m_srSession->session_switch_ds(SR_DS_STARTUP);
193 m_srSubscribe->module_change_subscribe(IETF_SYSTEM_MODULE_NAME.c_str(), hostNameCbStartup, IETF_SYSTEM_HOSTNAME_PATH, 0, SR_SUBSCR_DONE_ONLY);
194 m_srSession->session_switch_ds(SR_DS_OPERATIONAL);
195 m_srSubscribe->oper_get_items_subscribe(IETF_SYSTEM_MODULE_NAME.c_str(), hostNameCbOperational, IETF_SYSTEM_HOSTNAME_PATH);
196}
197
Jan Kundrátdcd50f02021-02-18 23:28:26 +0100198/** @short Acknowledge writes to dummy fields so that they're visible in the operational DS */
199void IETFSystem::initDummies()
200{
201 m_srSession->session_switch_ds(SR_DS_RUNNING);
202 auto ignore = [] (auto, auto, auto, auto, auto) {
203 return SR_ERR_OK;
204 };
205 for (const auto xpath : {"/ietf-system:system/location", "/ietf-system:system/contact"}) {
206 m_srSubscribe->module_change_subscribe(IETF_SYSTEM_MODULE_NAME.c_str(), ignore, xpath, 0, SR_SUBSCR_CTX_REUSE | SR_SUBSCR_DONE_ONLY);
207 }
208}
209
Jan Kundrát51f1d6d2021-02-19 00:41:28 +0100210/** @short Time and clock callbacks */
211void IETFSystem::initClock()
212{
213 m_srSubscribe->oper_get_items_subscribe(IETF_SYSTEM_MODULE_NAME.c_str(),
Jan Kundrátf333b132021-04-27 15:40:41 +0200214 [] (auto session, auto, auto, auto, auto, auto& parent) {
Jan Kundrát51f1d6d2021-02-19 00:41:28 +0100215 parent->new_path(session->get_context(),
216 (IETF_SYSTEM_STATE_CLOCK_PATH + "/current-datetime"s).c_str(),
217 utils::yangTimeFormat(std::chrono::system_clock::now()).c_str(),
218 LYD_ANYDATA_CONSTSTRING,
219 0);
220 return SR_ERR_OK;
221 },
222 IETF_SYSTEM_STATE_CLOCK_PATH,
223 SR_SUBSCR_OPER_MERGE);
224}
225
Tomáš Pecka79344c82021-09-16 18:25:59 +0200226/** @short DNS resolver callbacks */
227void IETFSystem::initDNS(sdbus::IConnection& connection, const std::string& dbusName) {
228 sysrepo::OperGetItemsCb dnsOper = [&connection, dbusName] (auto session, auto, auto, auto, auto, auto& parent) {
229 std::map<std::string, std::string> values;
230
231 /* RFC 7317 specifies that key leaf 'name' contains "An arbitrary name for the DNS server".
232 We use the IP address which is unique. If the server is returned multiple times (e.g. once as system-wide and once
233 for some specific ifindex, it doesn't matter that it is listed only once. */
234 for (const auto& e : getDNSResolvers(connection, dbusName)) {
235 values[IETF_SYSTEM_DNS_PATH + "/server[name='"s + e + "']/udp-and-tcp/address"] = e;
236 }
237
238 utils::valuesToYang(values, {}, session, parent);
239 return SR_ERR_OK;
240 };
241
242 m_srSubscribe->oper_get_items_subscribe(IETF_SYSTEM_MODULE_NAME.c_str(), dnsOper, IETF_SYSTEM_DNS_PATH);
243}
244
Václav Kubernát372d2772021-02-18 04:42:16 +0100245/** This class handles multiple system properties and publishes them via the ietf-system model:
246 * - OS-identification data from osRelease file
247 * - Rebooting
Václav Kubernát566c9402021-02-10 08:33:54 +0100248 * - Hostname
Václav Kubernát372d2772021-02-18 04:42:16 +0100249 */
Tomáš Pecka79344c82021-09-16 18:25:59 +0200250IETFSystem::IETFSystem(std::shared_ptr<::sysrepo::Session> srSession, const std::filesystem::path& osRelease, sdbus::IConnection& connection, const std::string& dbusName)
Václav Kubernátd2927cc2021-02-18 04:36:26 +0100251 : m_srSession(std::move(srSession))
252 , m_srSubscribe(std::make_shared<::sysrepo::Subscribe>(m_srSession))
253 , m_log(spdlog::get("system"))
254{
255 initStaticProperties(osRelease);
256 initSystemRestart();
Václav Kubernát566c9402021-02-10 08:33:54 +0100257 initHostname();
Jan Kundrátdcd50f02021-02-18 23:28:26 +0100258 initDummies();
Jan Kundrát51f1d6d2021-02-19 00:41:28 +0100259 initClock();
Tomáš Pecka79344c82021-09-16 18:25:59 +0200260 initDNS(connection, dbusName);
Václav Kubernátd2927cc2021-02-18 04:36:26 +0100261}
Tomáš Pecka292bc9c2021-01-11 22:03:11 +0100262}