system: Implement ietf-ip neighbors

The YANG model ietf-ip [1] models mapping of IP addresses to link layer
addresses per interface.
We ask for the link layer neighbors via libnl.

[1] https://tools.ietf.org/html/rfc8344

Change-Id: I831bcdbb430c657ae9a08da9f0d5a999e302e3da
diff --git a/src/system/IETFInterfaces.cpp b/src/system/IETFInterfaces.cpp
index d82f5e5..75978d2 100644
--- a/src/system/IETFInterfaces.cpp
+++ b/src/system/IETFInterfaces.cpp
@@ -119,6 +119,38 @@
     }
 }
 
+/** @brief Returns YANG structure for ietf-ip:ipv(4|6)/neighbours. Set requestedAddrFamily to required ip version (AF_INET for ipv4 or AF_INET6 for ipv6). */
+std::map<std::string, std::string> collectNeighboursIP(std::shared_ptr<velia::system::Rtnetlink> rtnetlink, int requestedAddrFamily, velia::Log log)
+{
+    std::map<std::string, std::string> values;
+
+    for (const auto& [neigh, link] : rtnetlink->getNeighbours()) {
+        if (rtnl_neigh_get_state(neigh.get()) == NUD_NOARP) {
+            continue;
+        }
+
+        auto linkName = rtnl_link_get_name(link.get());
+
+        auto ipAddr = rtnl_neigh_get_dst(neigh.get());
+        auto ipAddrFamily = nl_addr_get_family(ipAddr);
+
+        if (ipAddrFamily != requestedAddrFamily) {
+            continue;
+        }
+
+        auto ipAddress = binaddrToString(nl_addr_get_binary_addr(ipAddr), ipAddrFamily);
+
+        auto llAddr = rtnl_neigh_get_lladdr(neigh.get());
+        std::array<char, PHYS_ADDR_BUF_SIZE> llAddrBuf {};
+        if (auto llAddress = nl_addr2str(llAddr, llAddrBuf.data(), llAddrBuf.size()); llAddress != "none"s) {
+            values[IETF_INTERFACES + "/interface[name='" + linkName + "']/ietf-ip:" + getIPVersion(ipAddrFamily) + "/neighbor[ip='" + ipAddress + "']/link-layer-address"] = llAddress;
+        } else {
+            log->warn("Neighbor '{}' on link '{}' returned link layer address 'none'", ipAddress, linkName);
+        }
+    }
+
+    return values;
+}
 }
 
 namespace velia::system {
@@ -157,6 +189,20 @@
             return SR_ERR_OK;
         },
         (IETF_INTERFACES + "/interface/statistics").c_str());
+
+    m_srSubscribe->oper_get_items_subscribe(
+        IETF_INTERFACES_MODULE_NAME.c_str(), [this](auto session, auto, auto, auto, auto, auto& parent) {
+            utils::valuesToYang(collectNeighboursIP(m_rtnetlink, AF_INET, m_log), {}, session, parent);
+            return SR_ERR_OK;
+        },
+        (IETF_INTERFACES + "/interface/ietf-ip:ipv4/neighbor").c_str());
+
+    m_srSubscribe->oper_get_items_subscribe(
+        IETF_INTERFACES_MODULE_NAME.c_str(), [this](auto session, auto, auto, auto, auto, auto& parent) {
+            utils::valuesToYang(collectNeighboursIP(m_rtnetlink, AF_INET6, m_log), {}, session, parent);
+            return SR_ERR_OK;
+        },
+        (IETF_INTERFACES + "/interface/ietf-ip:ipv6/neighbor").c_str());
 }
 
 void IETFInterfaces::onLinkUpdate(rtnl_link* link, int action)
diff --git a/tests/sysrepo_system-ietfinterfaces.cpp b/tests/sysrepo_system-ietfinterfaces.cpp
index 766396f..a72bd62 100644
--- a/tests/sysrepo_system-ietfinterfaces.cpp
+++ b/tests/sysrepo_system-ietfinterfaces.cpp
@@ -48,4 +48,5 @@
                 {"/ietf-ip:ipv6/ietf-ipv6-unicast-routing:ipv6-router-advertisements", ""},
                 {"/ietf-ip:ipv6/ietf-ipv6-unicast-routing:ipv6-router-advertisements/prefix-list", ""},
             });
+    // NOTE: There are no neighbours on loopback
 }