system: Implement interface statistics in IETFInterfaces

The YANG model ietf-interfaces [1] models interface statistics. This
commit implements some of the interface statistics that are available
via libnl.

Also require newer sysrepo. This version fixes deadlock between push and
pull ops data manipulation from multiple threads [2].

[1] https://tools.ietf.org/html/rfc8343
[2] https://github.com/sysrepo/sysrepo/issues/2375

Bug: https://github.com/sysrepo/sysrepo/issues/2375
Change-Id: I6daa798af25181cb49cb199631f853520c0e91ce
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a6cdb63..002dbe9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -40,7 +40,7 @@
     message(FATAL_ERROR "Systemd 245 has a bug affecting DBus method calls using sdbus-c++. Please see https://github.com/Kistler-Group/sdbus-cpp/issues/106.")
 endif()
 
-pkg_check_modules(SYSREPO REQUIRED sysrepo-cpp>=1.4.126 IMPORTED_TARGET sysrepo)
+pkg_check_modules(SYSREPO REQUIRED sysrepo-cpp>=1.4.127 IMPORTED_TARGET sysrepo)
 pkg_check_modules(LIBYANG REQUIRED libyang-cpp>=1.0.215 IMPORTED_TARGET libyang)
 pkg_check_modules(LIBNL REQUIRED IMPORTED_TARGET libnl-route-3.0)
 
diff --git a/src/system/IETFInterfaces.cpp b/src/system/IETFInterfaces.cpp
index 23a636d..d82f5e5 100644
--- a/src/system/IETFInterfaces.cpp
+++ b/src/system/IETFInterfaces.cpp
@@ -125,6 +125,7 @@
 
 IETFInterfaces::IETFInterfaces(std::shared_ptr<::sysrepo::Session> srSess)
     : m_srSession(std::move(srSess))
+    , m_srSubscribe(std::make_shared<::sysrepo::Subscribe>(m_srSession))
     , m_log(spdlog::get("system"))
     , m_rtnetlink(std::make_shared<Rtnetlink>(
           [this](rtnl_link* link, int action) { onLinkUpdate(link, action); },
@@ -137,6 +138,25 @@
 
     m_rtnetlink->invokeInitialCallbacks();
     // TODO: Implement /ietf-routing:routing/interfaces and /ietf-routing:routing/router-id
+
+    m_srSubscribe->oper_get_items_subscribe(
+        IETF_INTERFACES_MODULE_NAME.c_str(), [this](auto session, auto, auto, auto, auto, auto& parent) {
+            std::map<std::string, std::string> values;
+            for (const auto& link : m_rtnetlink->getLinks()) {
+                const auto yangPrefix = IETF_INTERFACES + "/interface[name='" + rtnl_link_get_name(link.get()) + "']/statistics";
+
+                values[yangPrefix + "/in-octets"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_RX_BYTES));
+                values[yangPrefix + "/out-octets"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_TX_BYTES));
+                values[yangPrefix + "/in-discards"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_RX_DROPPED));
+                values[yangPrefix + "/out-discards"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_TX_DROPPED));
+                values[yangPrefix + "/in-errors"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_RX_ERRORS));
+                values[yangPrefix + "/out-errors"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_TX_ERRORS));
+            }
+
+            utils::valuesToYang(values, {}, session, parent);
+            return SR_ERR_OK;
+        },
+        (IETF_INTERFACES + "/interface/statistics").c_str());
 }
 
 void IETFInterfaces::onLinkUpdate(rtnl_link* link, int action)
diff --git a/src/system/IETFInterfaces.h b/src/system/IETFInterfaces.h
index 16dbd31..3e5e145 100644
--- a/src/system/IETFInterfaces.h
+++ b/src/system/IETFInterfaces.h
@@ -27,6 +27,7 @@
     void onRouteUpdate(rtnl_route* addr, int action);
 
     std::shared_ptr<::sysrepo::Session> m_srSession;
+    std::shared_ptr<::sysrepo::Subscribe> m_srSubscribe;
     velia::Log m_log;
     std::shared_ptr<Rtnetlink> m_rtnetlink; // first to destroy, because the callback to rtnetlink uses m_srSession and m_log
 };
diff --git a/tests/sysrepo_system-ietfinterfaces.cpp b/tests/sysrepo_system-ietfinterfaces.cpp
index 1497f49..766396f 100644
--- a/tests/sysrepo_system-ietfinterfaces.cpp
+++ b/tests/sysrepo_system-ietfinterfaces.cpp
@@ -16,13 +16,23 @@
 {
     TEST_SYSREPO_INIT_LOGS;
     TEST_SYSREPO_INIT;
+
     TEST_SYSREPO_INIT_CLIENT;
 
     auto network = std::make_shared<velia::system::IETFInterfaces>(srSess);
 
     // We didn't came up with some way of mocking netlink. At least check that there is the loopback
     // interface with expected values. It is *probably* safe to assume that there is at least the lo device.
-    REQUIRE(dataFromSysrepo(client, "/ietf-interfaces:interfaces/interface[name='lo']", SR_DS_OPERATIONAL) == std::map<std::string, std::string> {
+    auto lo = dataFromSysrepo(client, "/ietf-interfaces:interfaces/interface[name='lo']", SR_DS_OPERATIONAL);
+
+    // ensure statistics keys exist and remove them ; we can't really predict the content
+    for (const auto& key : {"/statistics", "/statistics/in-discards", "/statistics/in-errors", "/statistics/in-octets", "/statistics/out-discards", "/statistics/out-errors", "/statistics/out-octets"}) {
+        auto it = lo.find(key);
+        REQUIRE(it != lo.end());
+        lo.erase(it);
+    }
+
+    REQUIRE(lo == std::map<std::string, std::string> {
                 {"/name", "lo"},
                 {"/type", "iana-if-type:softwareLoopback"},
                 {"/phys-address", "00:00:00:00:00:00"},
@@ -37,6 +47,5 @@
                 {"/ietf-ip:ipv6/address[ip='::1']/prefix-length", "128"},
                 {"/ietf-ip:ipv6/ietf-ipv6-unicast-routing:ipv6-router-advertisements", ""},
                 {"/ietf-ip:ipv6/ietf-ipv6-unicast-routing:ipv6-router-advertisements/prefix-list", ""},
-                {"/statistics", ""},
             });
 }