blob: 558e6a24d04dbd4b6c5cad8967b443f8c9b5e3c9 [file] [log] [blame]
/*
* Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/
*
* Written by Tomáš Pecka <tomas.pecka@fit.cvut.cz>
*
*/
#include <arpa/inet.h>
#include <boost/algorithm/string/predicate.hpp>
#include <fstream>
#include <optional>
#include "IETFSystem.h"
#include "system_vars.h"
#include "utils/exec.h"
#include "utils/io.h"
#include "utils/log.h"
#include "utils/sysrepo.h"
#include "utils/time.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;
const auto IETF_SYSTEM_HOSTNAME_PATH = "/ietf-system:system/hostname";
const auto IETF_SYSTEM_DNS_PATH = "/ietf-system:system/dns-resolver";
const auto IETF_SYSTEM_STATE_CLOCK_PATH = "/ietf-system:system-state/clock";
/** @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;
}
std::optional<std::string> getHostnameFromChange(const std::shared_ptr<sysrepo::Session> session)
{
std::optional<std::string> res;
auto data = session->get_data(IETF_SYSTEM_HOSTNAME_PATH);
if (data) {
auto hostnameNode = data->find_path(IETF_SYSTEM_HOSTNAME_PATH)->data().front();
auto leaf = std::make_shared<libyang::Data_Node_Leaf_List>(hostnameNode);
res = leaf->value_str();
}
return res;
}
/** @brief Returns list of IP addresses (coded as a string) that serve as the DNS servers.
*
* We query the addresses from systemd-resolved D-Bus interface (see https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html#Properties
* and possibly also https://www.freedesktop.org/software/systemd/man/resolved.conf.html).
* We use the value of DnsEx property on the Manager object. In case that DnsEx is empty we fallback to FallbackDnsEx property.
*
* 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
* YANG model inability to distinguish between system-wide and per-interface type. Hence the resolver is listed as a system-wide one.
*/
std::vector<std::string> getDNSResolvers(sdbus::IConnection& connection, const std::string& dbusName)
{
static const auto DBUS_RESOLVE1_MANAGER_PATH = "/org/freedesktop/resolve1";
static const auto DBUS_RESOLVE1_MANAGER_INTERFACE = "org.freedesktop.resolve1.Manager";
auto proxy = sdbus::createProxy(connection, dbusName, DBUS_RESOLVE1_MANAGER_PATH);
for (const auto& propertyName : {"DNSEx", "FallbackDNSEx"}) {
sdbus::Variant store = proxy->getProperty(propertyName).onInterface(DBUS_RESOLVE1_MANAGER_INTERFACE);
// DBus type of the DNSEx and FallbackDNSEx properties is "a(iiayqs)" ~ Array of [ Struct of (Int32, Int32, Array of [Byte], Uint16, String) ]
// i.e., <ifindex (0 for system-wide), addrtype, address as a bytearray, port (0 for unspecified), server name>,
auto replyObjects = store.get<std::vector<sdbus::Struct<int32_t, int32_t, std::vector<uint8_t>, uint16_t, std::string>>>();
if (!replyObjects.empty() > 0) {
std::vector<std::string> res;
for (const auto& e : replyObjects) {
auto addrType = e.get<1>();
auto addrBytes = e.get<2>();
std::array<char, std::max(INET_ADDRSTRLEN, INET6_ADDRSTRLEN)> buf{};
inet_ntop(addrType, addrBytes.data(), buf.data(), buf.size());
res.emplace_back(buf.data());
}
return res;
}
}
return {};
}
}
namespace velia::system {
void IETFSystem::initStaticProperties(const std::filesystem::path& osRelease)
{
utils::ensureModuleImplemented(m_srSession, IETF_SYSTEM_MODULE_NAME, "2014-08-06");
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")},
};
utils::valuesPush(opsSystemStateData, {}, m_srSession, SR_DS_OPERATIONAL);
}
void IETFSystem::initSystemRestart()
{
m_srSubscribe->rpc_subscribe(
("/" + IETF_SYSTEM_MODULE_NAME + ":system-restart").c_str(),
[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) {
try {
velia::utils::execAndWait(m_log, SYSTEMCTL_EXECUTABLE, {"reboot"}, "", {});
} catch(const std::runtime_error& e) {
session->set_error("Reboot procedure failed.", nullptr);
return SR_ERR_OPERATION_FAILED;
}
return SR_ERR_OK;
},
0,
SR_SUBSCR_CTX_REUSE);
}
void IETFSystem::initHostname()
{
sysrepo::ModuleChangeCb hostNameCbRunning = [this] (auto session, auto, auto, auto, auto) {
if (auto newHostname = getHostnameFromChange(session)) {
velia::utils::execAndWait(m_log, HOSTNAMECTL_EXECUTABLE, {"set-hostname", *newHostname}, "");
}
return SR_ERR_OK;
};
sysrepo::ModuleChangeCb hostNameCbStartup = [] (auto session, auto, auto, auto, auto) {
if (auto newHostname = getHostnameFromChange(session)) {
utils::safeWriteFile(BACKUP_ETC_HOSTNAME_FILE, *newHostname);
}
return SR_ERR_OK;
};
sysrepo::OperGetItemsCb hostNameCbOperational = [] (auto session, auto, auto, auto, auto, auto& parent) {
// + 1 for null-terminating byte, HOST_NAME_MAX doesn't count that
std::array<char, HOST_NAME_MAX + 1> buffer{};
if (gethostname(buffer.data(), buffer.size()) != 0) {
throw std::system_error(errno, std::system_category(), "gethostname() failed");
}
parent->new_path(
session->get_context(),
IETF_SYSTEM_HOSTNAME_PATH,
buffer.data(),
LYD_ANYDATA_CONSTSTRING,
0);
return SR_ERR_OK;
};
m_srSubscribe->module_change_subscribe(IETF_SYSTEM_MODULE_NAME.c_str(), hostNameCbRunning, IETF_SYSTEM_HOSTNAME_PATH, 0, SR_SUBSCR_DONE_ONLY);
m_srSession->session_switch_ds(SR_DS_STARTUP);
m_srSubscribe->module_change_subscribe(IETF_SYSTEM_MODULE_NAME.c_str(), hostNameCbStartup, IETF_SYSTEM_HOSTNAME_PATH, 0, SR_SUBSCR_DONE_ONLY);
m_srSession->session_switch_ds(SR_DS_OPERATIONAL);
m_srSubscribe->oper_get_items_subscribe(IETF_SYSTEM_MODULE_NAME.c_str(), hostNameCbOperational, IETF_SYSTEM_HOSTNAME_PATH);
}
/** @short Acknowledge writes to dummy fields so that they're visible in the operational DS */
void IETFSystem::initDummies()
{
m_srSession->session_switch_ds(SR_DS_RUNNING);
auto ignore = [] (auto, auto, auto, auto, auto) {
return SR_ERR_OK;
};
for (const auto xpath : {"/ietf-system:system/location", "/ietf-system:system/contact"}) {
m_srSubscribe->module_change_subscribe(IETF_SYSTEM_MODULE_NAME.c_str(), ignore, xpath, 0, SR_SUBSCR_CTX_REUSE | SR_SUBSCR_DONE_ONLY);
}
}
/** @short Time and clock callbacks */
void IETFSystem::initClock()
{
m_srSubscribe->oper_get_items_subscribe(IETF_SYSTEM_MODULE_NAME.c_str(),
[] (auto session, auto, auto, auto, auto, auto& parent) {
parent->new_path(session->get_context(),
(IETF_SYSTEM_STATE_CLOCK_PATH + "/current-datetime"s).c_str(),
utils::yangTimeFormat(std::chrono::system_clock::now()).c_str(),
LYD_ANYDATA_CONSTSTRING,
0);
return SR_ERR_OK;
},
IETF_SYSTEM_STATE_CLOCK_PATH,
SR_SUBSCR_OPER_MERGE);
}
/** @short DNS resolver callbacks */
void IETFSystem::initDNS(sdbus::IConnection& connection, const std::string& dbusName) {
sysrepo::OperGetItemsCb dnsOper = [&connection, dbusName] (auto session, auto, auto, auto, auto, auto& parent) {
std::map<std::string, std::string> values;
/* RFC 7317 specifies that key leaf 'name' contains "An arbitrary name for the DNS server".
We use the IP address which is unique. If the server is returned multiple times (e.g. once as system-wide and once
for some specific ifindex, it doesn't matter that it is listed only once. */
for (const auto& e : getDNSResolvers(connection, dbusName)) {
values[IETF_SYSTEM_DNS_PATH + "/server[name='"s + e + "']/udp-and-tcp/address"] = e;
}
utils::valuesToYang(values, {}, session, parent);
return SR_ERR_OK;
};
m_srSubscribe->oper_get_items_subscribe(IETF_SYSTEM_MODULE_NAME.c_str(), dnsOper, IETF_SYSTEM_DNS_PATH);
}
/** This class handles multiple system properties and publishes them via the ietf-system model:
* - OS-identification data from osRelease file
* - Rebooting
* - Hostname
*/
IETFSystem::IETFSystem(std::shared_ptr<::sysrepo::Session> srSession, const std::filesystem::path& osRelease, sdbus::IConnection& connection, const std::string& dbusName)
: m_srSession(std::move(srSession))
, m_srSubscribe(std::make_shared<::sysrepo::Subscribe>(m_srSession))
, m_log(spdlog::get("system"))
{
initStaticProperties(osRelease);
initSystemRestart();
initHostname();
initDummies();
initClock();
initDNS(connection, dbusName);
}
}