Tomáš Pecka | 292bc9c | 2021-01-11 22:03:11 +0100 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/ |
| 3 | * |
| 4 | * Written by Tomáš Pecka <tomas.pecka@fit.cvut.cz> |
| 5 | * |
| 6 | */ |
| 7 | |
Tomáš Pecka | 79344c8 | 2021-09-16 18:25:59 +0200 | [diff] [blame] | 8 | #include <arpa/inet.h> |
Tomáš Pecka | 292bc9c | 2021-01-11 22:03:11 +0100 | [diff] [blame] | 9 | #include <boost/algorithm/string/predicate.hpp> |
| 10 | #include <fstream> |
Jan Kundrát | 1c3b881 | 2021-05-17 13:06:03 +0200 | [diff] [blame] | 11 | #include <optional> |
Tomáš Pecka | f976c5b | 2021-01-23 21:19:52 +0100 | [diff] [blame] | 12 | #include "IETFSystem.h" |
Tomáš Pecka | 879a603 | 2021-02-03 17:21:48 +0100 | [diff] [blame] | 13 | #include "system_vars.h" |
| 14 | #include "utils/exec.h" |
Tomáš Pecka | 292bc9c | 2021-01-11 22:03:11 +0100 | [diff] [blame] | 15 | #include "utils/io.h" |
| 16 | #include "utils/log.h" |
Tomáš Pecka | 272abaf | 2021-01-24 12:28:43 +0100 | [diff] [blame] | 17 | #include "utils/sysrepo.h" |
Jan Kundrát | 51f1d6d | 2021-02-19 00:41:28 +0100 | [diff] [blame] | 18 | #include "utils/time.h" |
Tomáš Pecka | 292bc9c | 2021-01-11 22:03:11 +0100 | [diff] [blame] | 19 | |
| 20 | using namespace std::literals; |
| 21 | |
| 22 | namespace { |
| 23 | |
| 24 | const auto IETF_SYSTEM_MODULE_NAME = "ietf-system"s; |
| 25 | const auto IETF_SYSTEM_STATE_MODULE_PREFIX = "/"s + IETF_SYSTEM_MODULE_NAME + ":system-state/"s; |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 26 | const auto IETF_SYSTEM_HOSTNAME_PATH = "/ietf-system:system/hostname"; |
Tomáš Pecka | 79344c8 | 2021-09-16 18:25:59 +0200 | [diff] [blame] | 27 | const auto IETF_SYSTEM_DNS_PATH = "/ietf-system:system/dns-resolver"; |
Jan Kundrát | 51f1d6d | 2021-02-19 00:41:28 +0100 | [diff] [blame] | 28 | const auto IETF_SYSTEM_STATE_CLOCK_PATH = "/ietf-system:system-state/clock"; |
Tomáš Pecka | 292bc9c | 2021-01-11 22:03:11 +0100 | [diff] [blame] | 29 | |
| 30 | /** @brief Returns key=value pairs from (e.g. /etc/os-release) as a std::map */ |
| 31 | std::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át | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 64 | std::optional<std::string> getHostnameFromChange(const sysrepo::Session session) |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 65 | { |
| 66 | std::optional<std::string> res; |
| 67 | |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 68 | auto data = session.getData(IETF_SYSTEM_HOSTNAME_PATH); |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 69 | if (data) { |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 70 | auto hostnameNode = data->findPath(IETF_SYSTEM_HOSTNAME_PATH); |
| 71 | res = hostnameNode->asTerm().valueStr(); |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 72 | } |
| 73 | |
| 74 | return res; |
| 75 | } |
Tomáš Pecka | 79344c8 | 2021-09-16 18:25:59 +0200 | [diff] [blame] | 76 | |
| 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 | */ |
| 86 | std::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áš Pecka | 292bc9c | 2021-01-11 22:03:11 +0100 | [diff] [blame] | 119 | } |
| 120 | |
| 121 | namespace velia::system { |
| 122 | |
Václav Kubernát | d2927cc | 2021-02-18 04:36:26 +0100 | [diff] [blame] | 123 | void IETFSystem::initStaticProperties(const std::filesystem::path& osRelease) |
Tomáš Pecka | 292bc9c | 2021-01-11 22:03:11 +0100 | [diff] [blame] | 124 | { |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 125 | utils::ensureModuleImplemented(m_srSession, IETF_SYSTEM_MODULE_NAME, "2014-08-06"); |
| 126 | |
Tomáš Pecka | 292bc9c | 2021-01-11 22:03:11 +0100 | [diff] [blame] | 127 | 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át | 498c3f8 | 2023-05-24 19:25:48 +0200 | [diff] [blame^] | 135 | utils::valuesPush(opsSystemStateData, {}, {}, m_srSession, sysrepo::Datastore::Operational); |
Václav Kubernát | d2927cc | 2021-02-18 04:36:26 +0100 | [diff] [blame] | 136 | } |
Tomáš Pecka | 879a603 | 2021-02-03 17:21:48 +0100 | [diff] [blame] | 137 | |
Václav Kubernát | d2927cc | 2021-02-18 04:36:26 +0100 | [diff] [blame] | 138 | void IETFSystem::initSystemRestart() |
| 139 | { |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 140 | sysrepo::RpcActionCb cb = [this](auto session, auto, auto, auto, auto, auto, auto) { |
Tomáš Pecka | 879a603 | 2021-02-03 17:21:48 +0100 | [diff] [blame] | 141 | try { |
| 142 | velia::utils::execAndWait(m_log, SYSTEMCTL_EXECUTABLE, {"reboot"}, "", {}); |
| 143 | } catch(const std::runtime_error& e) { |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 144 | utils::setErrors(session, "Reboot procedure failed."); |
| 145 | return sysrepo::ErrorCode::OperationFailed; |
Tomáš Pecka | 879a603 | 2021-02-03 17:21:48 +0100 | [diff] [blame] | 146 | } |
| 147 | |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 148 | return sysrepo::ErrorCode::Ok; |
| 149 | }; |
| 150 | |
Jan Kundrát | b3e9998 | 2022-03-18 17:38:20 +0100 | [diff] [blame] | 151 | m_srSubscribe = m_srSession.onRPCAction("/" + IETF_SYSTEM_MODULE_NAME + ":system-restart", cb); |
Tomáš Pecka | 292bc9c | 2021-01-11 22:03:11 +0100 | [diff] [blame] | 152 | } |
Václav Kubernát | d2927cc | 2021-02-18 04:36:26 +0100 | [diff] [blame] | 153 | |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 154 | void IETFSystem::initHostname() |
| 155 | { |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 156 | sysrepo::ModuleChangeCb hostNameCbRunning = [this] (auto session, auto, auto, auto, auto, auto) { |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 157 | if (auto newHostname = getHostnameFromChange(session)) { |
| 158 | velia::utils::execAndWait(m_log, HOSTNAMECTL_EXECUTABLE, {"set-hostname", *newHostname}, ""); |
| 159 | } |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 160 | return sysrepo::ErrorCode::Ok; |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 161 | }; |
| 162 | |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 163 | sysrepo::ModuleChangeCb hostNameCbStartup = [] (auto session, auto, auto, auto, auto, auto) { |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 164 | if (auto newHostname = getHostnameFromChange(session)) { |
| 165 | utils::safeWriteFile(BACKUP_ETC_HOSTNAME_FILE, *newHostname); |
| 166 | } |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 167 | return sysrepo::ErrorCode::Ok; |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 168 | }; |
| 169 | |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 170 | sysrepo::OperGetCb hostNameCbOperational = [] (auto, auto, auto, auto, auto, auto, auto& parent) { |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 171 | // + 1 for null-terminating byte, HOST_NAME_MAX doesn't count that |
Václav Kubernát | e407f74 | 2021-05-18 10:47:13 +0200 | [diff] [blame] | 172 | std::array<char, HOST_NAME_MAX + 1> buffer{}; |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 173 | |
| 174 | if (gethostname(buffer.data(), buffer.size()) != 0) { |
| 175 | throw std::system_error(errno, std::system_category(), "gethostname() failed"); |
| 176 | } |
| 177 | |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 178 | parent->newPath( IETF_SYSTEM_HOSTNAME_PATH, buffer.data()); |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 179 | |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 180 | return sysrepo::ErrorCode::Ok; |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 181 | }; |
| 182 | |
Jan Kundrát | 80113ed | 2022-04-27 18:10:52 +0200 | [diff] [blame] | 183 | m_srSubscribe->onModuleChange(IETF_SYSTEM_MODULE_NAME, hostNameCbRunning, IETF_SYSTEM_HOSTNAME_PATH, 0, sysrepo::SubscribeOptions::DoneOnly | sysrepo::SubscribeOptions::Enabled); |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 184 | m_srSession.switchDatastore(sysrepo::Datastore::Startup); |
Jan Kundrát | 80113ed | 2022-04-27 18:10:52 +0200 | [diff] [blame] | 185 | m_srSubscribe->onModuleChange(IETF_SYSTEM_MODULE_NAME, hostNameCbStartup, IETF_SYSTEM_HOSTNAME_PATH, 0, sysrepo::SubscribeOptions::DoneOnly); |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 186 | m_srSession.switchDatastore(sysrepo::Datastore::Operational); |
Jan Kundrát | b3e9998 | 2022-03-18 17:38:20 +0100 | [diff] [blame] | 187 | m_srSubscribe->onOperGet(IETF_SYSTEM_MODULE_NAME, hostNameCbOperational, IETF_SYSTEM_HOSTNAME_PATH); |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 188 | } |
| 189 | |
Jan Kundrát | dcd50f0 | 2021-02-18 23:28:26 +0100 | [diff] [blame] | 190 | /** @short Acknowledge writes to dummy fields so that they're visible in the operational DS */ |
| 191 | void IETFSystem::initDummies() |
| 192 | { |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 193 | m_srSession.switchDatastore(sysrepo::Datastore::Running); |
| 194 | sysrepo::ModuleChangeCb ignore = [] (auto, auto, auto, auto, auto, auto) { |
| 195 | return sysrepo::ErrorCode::Ok; |
Jan Kundrát | dcd50f0 | 2021-02-18 23:28:26 +0100 | [diff] [blame] | 196 | }; |
| 197 | for (const auto xpath : {"/ietf-system:system/location", "/ietf-system:system/contact"}) { |
Jan Kundrát | 80113ed | 2022-04-27 18:10:52 +0200 | [diff] [blame] | 198 | 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át | dcd50f0 | 2021-02-18 23:28:26 +0100 | [diff] [blame] | 199 | } |
| 200 | } |
| 201 | |
Jan Kundrát | 51f1d6d | 2021-02-19 00:41:28 +0100 | [diff] [blame] | 202 | /** @short Time and clock callbacks */ |
| 203 | void IETFSystem::initClock() |
| 204 | { |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 205 | sysrepo::OperGetCb cb = [] (auto, auto, auto, auto, auto, auto, auto& parent) { |
Jan Kundrát | b3e9998 | 2022-03-18 17:38:20 +0100 | [diff] [blame] | 206 | parent->newPath(IETF_SYSTEM_STATE_CLOCK_PATH + "/current-datetime"s, utils::yangTimeFormat(std::chrono::system_clock::now())); |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 207 | return sysrepo::ErrorCode::Ok; |
| 208 | }; |
| 209 | |
Jan Kundrát | b3e9998 | 2022-03-18 17:38:20 +0100 | [diff] [blame] | 210 | m_srSubscribe->onOperGet(IETF_SYSTEM_MODULE_NAME, cb, IETF_SYSTEM_STATE_CLOCK_PATH, sysrepo::SubscribeOptions::OperMerge); |
Jan Kundrát | 51f1d6d | 2021-02-19 00:41:28 +0100 | [diff] [blame] | 211 | } |
| 212 | |
Tomáš Pecka | 79344c8 | 2021-09-16 18:25:59 +0200 | [diff] [blame] | 213 | /** @short DNS resolver callbacks */ |
| 214 | void IETFSystem::initDNS(sdbus::IConnection& connection, const std::string& dbusName) { |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 215 | sysrepo::OperGetCb dnsOper = [&connection, dbusName] (auto session, auto, auto, auto, auto, auto, auto& parent) { |
Tomáš Pecka | 79344c8 | 2021-09-16 18:25:59 +0200 | [diff] [blame] | 216 | 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át | 498c3f8 | 2023-05-24 19:25:48 +0200 | [diff] [blame^] | 225 | utils::valuesToYang(values, {}, {}, session, parent); |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 226 | return sysrepo::ErrorCode::Ok; |
Tomáš Pecka | 79344c8 | 2021-09-16 18:25:59 +0200 | [diff] [blame] | 227 | }; |
| 228 | |
Jan Kundrát | b3e9998 | 2022-03-18 17:38:20 +0100 | [diff] [blame] | 229 | m_srSubscribe->onOperGet(IETF_SYSTEM_MODULE_NAME, dnsOper, IETF_SYSTEM_DNS_PATH); |
Tomáš Pecka | 79344c8 | 2021-09-16 18:25:59 +0200 | [diff] [blame] | 230 | } |
| 231 | |
Václav Kubernát | 372d277 | 2021-02-18 04:42:16 +0100 | [diff] [blame] | 232 | /** 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át | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 235 | * - Hostname |
Václav Kubernát | 372d277 | 2021-02-18 04:42:16 +0100 | [diff] [blame] | 236 | */ |
Václav Kubernát | 7efd6d5 | 2021-11-09 01:31:11 +0100 | [diff] [blame] | 237 | IETFSystem::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át | d2927cc | 2021-02-18 04:36:26 +0100 | [diff] [blame] | 240 | , m_log(spdlog::get("system")) |
| 241 | { |
| 242 | initStaticProperties(osRelease); |
| 243 | initSystemRestart(); |
Václav Kubernát | 566c940 | 2021-02-10 08:33:54 +0100 | [diff] [blame] | 244 | initHostname(); |
Jan Kundrát | dcd50f0 | 2021-02-18 23:28:26 +0100 | [diff] [blame] | 245 | initDummies(); |
Jan Kundrát | 51f1d6d | 2021-02-19 00:41:28 +0100 | [diff] [blame] | 246 | initClock(); |
Tomáš Pecka | 79344c8 | 2021-09-16 18:25:59 +0200 | [diff] [blame] | 247 | initDNS(connection, dbusName); |
Václav Kubernát | d2927cc | 2021-02-18 04:36:26 +0100 | [diff] [blame] | 248 | } |
Tomáš Pecka | 292bc9c | 2021-01-11 22:03:11 +0100 | [diff] [blame] | 249 | } |