system: Move lldp-systemd-networkd-sysrepo to velia
We agreed that maintaining standalone lldp-systemd-networkd-sysrepo
project is excessive. For example, many compilation units in the
project are copied from velia. Also, velia manages a lot of
system (and network) related things so why should the access to LLDP
neighbours via sysrepo be a standalone project?
This commit copies lldp-systemd-networkd-sysrepo project into velia.
The source repository commit is
39bd89ff7e57cd7bf4ebfdab5fce41fb4fdedcf0.
Only necessary stuff was changed (some naming and compilation issues).
We *might* be changing this code soon if our patch for LLDP neighbours
extraction in JSON format[1] is merged into systemd.
Of course, this also needs a br2-external update because this commit
effectively deprecates lldp-systemd-networkd-sysrepo project and it
should not be used in br2-external anymore.
[1] https://github.com/systemd/systemd/pull/20333
Change-Id: Ide2ed4e5eb8f1c3b10f0e2af7820f83c04cb81e8
diff --git a/src/main-system.cpp b/src/main-system.cpp
index 9a9c18d..fe367db 100644
--- a/src/main-system.cpp
+++ b/src/main-system.cpp
@@ -4,13 +4,15 @@
#include <sysrepo-cpp/Session.hpp>
#include "VELIA_VERSION.h"
#include "main.h"
+#include "system_vars.h"
#include "system/Authentication.h"
#include "system/Firmware.h"
#include "system/IETFInterfaces.h"
#include "system/IETFInterfacesConfig.h"
#include "system/IETFSystem.h"
#include "system/LED.h"
-#include "system_vars.h"
+#include "system/LLDP.h"
+#include "system/LLDPCallback.h"
#include "utils/exceptions.h"
#include "utils/exec.h"
#include "utils/journal.h"
@@ -104,6 +106,10 @@
auto leds = velia::system::LED(srConn, "/sys/class/leds");
+ auto lldp = std::make_shared<velia::system::LLDPDataProvider>("/run/systemd/netif/lldp", *g_dbusConnection, "org.freedesktop.network1");
+ auto srSubs = std::make_shared<sysrepo::Subscribe>(srSess);
+ srSubs->oper_get_items_subscribe("czechlight-lldp", velia::system::LLDPCallback(lldp), "/czechlight-lldp:nbr-list");
+
DBUS_EVENTLOOP_END
return 0;
} catch (std::exception& e) {
diff --git a/src/system/LLDP.cpp b/src/system/LLDP.cpp
new file mode 100644
index 0000000..452cff3
--- /dev/null
+++ b/src/system/LLDP.cpp
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@fit.cvut.cz>
+ *
+ */
+#include <netinet/ether.h>
+#include <spdlog/spdlog.h>
+#include "LLDP.h"
+#include "utils/log.h"
+
+namespace velia::system {
+
+namespace impl {
+
+static const std::string systemdNetworkdDbusInterface = "org.freedesktop.network1.Manager";
+static const sdbus::ObjectPath systemdNetworkdDbusManagerObjectPath = "/org/freedesktop/network1";
+
+/** @brief LLDP capabilities identifiers ordered by their appearence in YANG schema 'czechlight-lldp' */
+const char* SYSTEM_CAPABILITIES[] = {
+ "other",
+ "repeater",
+ "bridge",
+ "wlan-access-point",
+ "router",
+ "telephone",
+ "docsis-cable-device",
+ "station-only",
+ "cvlan-component",
+ "svlan-component",
+ "two-port-mac-relay",
+};
+
+/** @brief Converts systemd's capabilities bitset to YANG's (named) bits.
+ *
+ * Apparently, libyang's parser requires the bits to be specified as string of names separated by whitespace.
+ * See libyang's src/parser.c (function lyp_parse_value, switch-case LY_TYPE_BITS) and tests/test_sec9_7.c
+ *
+ * The names of individual bits should appear in the order they are defined in the YANG schema. At least that is how
+ * I understand libyang's comment 'identifiers appear ordered by their position' in src/parser.c.
+ * Systemd and our YANG model czechlight-lldp define the bits in the same order so this function does not have to care
+ * about it.
+ */
+std::string toBitsYANG(uint16_t bits)
+{
+ std::string res;
+
+ unsigned idx = 0;
+ while (bits) {
+ if (bits % 2) {
+ if (!res.empty()) {
+ res += " ";
+ }
+ res += SYSTEM_CAPABILITIES[idx];
+ }
+
+ bits /= 2;
+ idx += 1;
+ }
+
+ return res;
+}
+
+/** @brief sd_lldp_neighbor requires deletion by invoking sd_lldp_neighbor_unrefp */
+struct sd_lldp_neighbor_deleter {
+ void operator()(sd_lldp_neighbor* e) const
+ {
+ sd_lldp_neighbor_unrefp(&e);
+ }
+};
+using sd_lldp_neighbor_managed = std::unique_ptr<sd_lldp_neighbor, sd_lldp_neighbor_deleter>;
+
+/* @brief Reads a LLDP neighbour entry from systemd's binary LLDP files.
+*
+* Inspired systemd's networkctl code.
+*/
+sd_lldp_neighbor_managed nextNeighbor(std::ifstream& ifs)
+{
+ size_t size;
+
+ // read neighbor size
+ /* Systemd allows the LLDP frame to be at most 4 KiB long. The comment in networkctl.c states that
+ * "each LLDP packet is at most MTU size, but let's allow up to 4KiB just in case".
+ * This comment may be misleading a bit because Ethernet Jumbo Frames can be up to 9000 B long.
+ * However, LLDP frames should still be at most 1500 B long.
+ * (see https://www.cisco.com/c/en/us/td/docs/routers/ncs4000/software/configure/guide/configurationguide/configurationguide_chapter_0111011.pdf)
+ */
+ {
+ uint64_t rawSz; // get neighbour size in bytes
+ ifs.read(reinterpret_cast<char*>(&rawSz), sizeof(rawSz));
+ size = le64toh(rawSz);
+
+ if (size_t rd = ifs.gcount(); (rd == 0 && ifs.eof()) || rd != sizeof(rawSz) || size >= 4096) {
+ return nullptr;
+ }
+ }
+
+ std::vector<uint8_t> raw;
+ raw.resize(size);
+
+ ifs.read(reinterpret_cast<char*>(raw.data()), size);
+ if (static_cast<size_t>(ifs.gcount()) != size) { // typecast is safe here (see std::streamsize)
+ return nullptr;
+ }
+
+ // let systemd parse from raw
+ sd_lldp_neighbor* tmp = nullptr;
+ if (sd_lldp_neighbor_from_raw(&tmp, raw.data(), size) < 0) {
+ return nullptr;
+ }
+
+ return sd_lldp_neighbor_managed(tmp);
+}
+
+/* @brief Lists links using networkd dbus interface and returns them as a list of pairs <link_id, link_name>. */
+auto listLinks(sdbus::IProxy* networkdManagerProxy)
+{
+ std::vector<sdbus::Struct<int, std::string, sdbus::ObjectPath>> links;
+ std::vector<std::pair<int, std::string>> res; // we only want to return pairs (linkId, linkName), we do not need dbus object path
+
+ networkdManagerProxy->callMethod("ListLinks").onInterface(impl::systemdNetworkdDbusInterface).storeResultsTo(links);
+
+ std::transform(links.begin(), links.end(), std::back_inserter(res), [](const auto& e) { return std::make_pair(std::get<0>(e), std::get<1>(e)); });
+ return res;
+}
+
+} /* namespace impl */
+
+LLDPDataProvider::LLDPDataProvider(std::filesystem::path dataDirectory, sdbus::IConnection& dbusConnection, const std::string& dbusNetworkdBus)
+ : m_log(spdlog::get("system"))
+ , m_dataDirectory(std::move(dataDirectory))
+ , m_networkdDbusProxy(sdbus::createProxy(dbusConnection, dbusNetworkdBus, impl::systemdNetworkdDbusManagerObjectPath))
+{
+}
+
+std::vector<NeighborEntry> LLDPDataProvider::getNeighbors() const
+{
+ std::vector<NeighborEntry> res;
+
+ for (const auto& [linkId, linkName] : impl::listLinks(m_networkdDbusProxy.get())) {
+ m_log->debug("LLDP: Collecting neighbours on '{}' (id {})", linkName, linkId);
+
+ // open lldp datafile
+ std::filesystem::path lldpFilename = m_dataDirectory / std::to_string(linkId);
+ std::ifstream ifs(lldpFilename, std::ios::binary);
+
+ if (!ifs.is_open()) {
+ // TODO: As of now, we are querying systemd-networkd for *all* links, not just those that have LLDP enabled.
+ // TODO: Create a patch for systemd that queries *only* links with LLDP enabled and change severity of this debug log to warning/error.
+ m_log->debug(" failed to open ({})", lldpFilename);
+ continue;
+ }
+
+ while (auto n = impl::nextNeighbor(ifs)) {
+ NeighborEntry ne;
+ ne.m_portId = linkName;
+
+ if (const char* system_name = nullptr; sd_lldp_neighbor_get_system_name(n.get(), &system_name) >= 0) {
+ ne.m_properties["remoteSysName"] = system_name;
+ }
+ if (const char* port_id = nullptr; sd_lldp_neighbor_get_port_id_as_string(n.get(), &port_id) >= 0) {
+ ne.m_properties["remotePortId"] = port_id;
+ }
+ if (const char* chassis_id = nullptr; sd_lldp_neighbor_get_chassis_id_as_string(n.get(), &chassis_id) >= 0) {
+ ne.m_properties["remoteChassisId"] = chassis_id;
+ }
+ if (ether_addr* addr = nullptr; sd_lldp_neighbor_get_destination_address(n.get(), addr) >= 0) {
+ ne.m_properties["remoteMgmtAddress"] = ether_ntoa(addr);
+ }
+
+ if (uint16_t cap = 0; sd_lldp_neighbor_get_system_capabilities(n.get(), &cap) >= 0) {
+ ne.m_properties["systemCapabilitiesSupported"] = impl::toBitsYANG(cap);
+ }
+
+ if (uint16_t cap = 0; sd_lldp_neighbor_get_enabled_capabilities(n.get(), &cap) >= 0) {
+ ne.m_properties["systemCapabilitiesEnabled"] = impl::toBitsYANG(cap);
+ }
+
+ m_log->trace(" found neighbor {}", ne);
+ res.push_back(ne);
+ }
+ }
+
+ return res;
+}
+
+std::ostream& operator<<(std::ostream& os, const NeighborEntry& entry)
+{
+ os << "NeighborEntry(" << entry.m_portId << ": {";
+
+ for (auto it = entry.m_properties.begin(); it != entry.m_properties.end(); ++it) {
+ if (it != entry.m_properties.begin()) {
+ os << ", ";
+ }
+
+ os << it->first << ": " << it->second;
+ }
+
+ return os << "}";
+}
+
+}
diff --git a/src/system/LLDP.h b/src/system/LLDP.h
new file mode 100644
index 0000000..35c0ce0
--- /dev/null
+++ b/src/system/LLDP.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@fit.cvut.cz>
+ *
+ */
+
+#pragma once
+
+#include <filesystem>
+#include <fstream>
+#include <map>
+#include <sdbus-c++/sdbus-c++.h>
+#include <spdlog/fmt/ostr.h> // allow spdlog to use operator<<(ostream, NeighborEntry)
+#include <string>
+#include <systemd/sd-lldp.h>
+#include <vector>
+#include "utils/log-fwd.h"
+
+namespace velia::system {
+
+struct NeighborEntry {
+ std::string m_portId;
+ std::map<std::string, std::string> m_properties;
+};
+std::ostream& operator<<(std::ostream& os, const NeighborEntry& entry);
+
+class LLDPDataProvider {
+public:
+ LLDPDataProvider(std::filesystem::path dataDirectory, sdbus::IConnection& dbusConnection, const std::string& dbusNetworkdBus);
+ std::vector<NeighborEntry> getNeighbors() const;
+
+private:
+ velia::Log m_log;
+ std::filesystem::path m_dataDirectory;
+ std::unique_ptr<sdbus::IProxy> m_networkdDbusProxy;
+};
+
+}
diff --git a/src/system/LLDPCallback.cpp b/src/system/LLDPCallback.cpp
new file mode 100644
index 0000000..a38705b
--- /dev/null
+++ b/src/system/LLDPCallback.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@fit.cvut.cz>
+ *
+ */
+
+#include <numeric>
+#include "LLDPCallback.h"
+#include "utils/log.h"
+
+namespace velia::system {
+
+LLDPCallback::LLDPCallback(std::shared_ptr<LLDPDataProvider> lldp)
+ : m_log(spdlog::get("system"))
+ , m_lldp(std::move(lldp))
+ , m_lastRequestId(0)
+{
+}
+
+int LLDPCallback::operator()(std::shared_ptr<::sysrepo::Session> session, const char* module_name, const char* xpath, const char* request_xpath, uint32_t request_id, std::shared_ptr<libyang::Data_Node>& parent)
+{
+ m_log->trace("operational data callback: XPath {} req {} orig-XPath {}", xpath, request_id, request_xpath);
+
+ // when asking for something in the subtree of THIS request
+ if (m_lastRequestId == request_id) {
+ m_log->trace(" ops data request already handled");
+ return SR_ERR_OK;
+ }
+ m_lastRequestId = request_id;
+
+ auto ctx = session->get_context();
+ auto mod = ctx->get_module(module_name);
+
+ parent = std::make_shared<libyang::Data_Node>(ctx, "/czechlight-lldp:nbr-list", nullptr, LYD_ANYDATA_CONSTSTRING, 0);
+
+ for (const auto& n : m_lldp->getNeighbors()) {
+ auto ifc = std::make_shared<libyang::Data_Node>(parent, mod, "neighbors");
+
+ auto ifName = std::make_shared<libyang::Data_Node>(ifc, mod, "ifName", n.m_portId.c_str());
+
+ for (const auto& [key, val] : n.m_properties) { // garbage properties in, garbage out
+ auto prop = std::make_shared<libyang::Data_Node>(ifc, mod, key.c_str(), val.c_str());
+ }
+ }
+
+ m_log->trace("Pushing to sysrepo (JSON): {}", parent->print_mem(LYD_FORMAT::LYD_JSON, 0));
+
+ return SR_ERR_OK;
+}
+
+}
diff --git a/src/system/LLDPCallback.h b/src/system/LLDPCallback.h
new file mode 100644
index 0000000..77daca8
--- /dev/null
+++ b/src/system/LLDPCallback.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@fit.cvut.cz>
+ *
+ */
+
+#pragma once
+
+#include <functional>
+#include <map>
+#include <memory>
+#include <optional>
+#include <sysrepo-cpp/Session.hpp>
+#include "LLDP.h"
+#include "utils/log-fwd.h"
+
+namespace velia::system {
+
+class LLDPCallback {
+public:
+ explicit LLDPCallback(std::shared_ptr<LLDPDataProvider> lldp);
+ int operator()(std::shared_ptr<::sysrepo::Session> session, const char* module_name, const char* path, const char* request_xpath, uint32_t request_id, std::shared_ptr<libyang::Data_Node>& parent);
+
+private:
+ velia::Log m_log;
+ std::shared_ptr<LLDPDataProvider> m_lldp;
+ uint64_t m_lastRequestId;
+};
+
+}