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;
+};
+
+}