mirror of
https://github.com/kernelkit/infix.git
synced 2026-08-01 13:23:01 +02:00
415 lines
18 KiB
Diff
415 lines
18 KiB
Diff
From 9d812203b3c0550995be8d60e7b11761bdbac04d Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
|
|
Date: Tue, 1 Apr 2025 21:22:04 +0200
|
|
Subject: [PATCH 13/20] add subtree filtering to notifications subscription
|
|
MIME-Version: 1.0
|
|
Content-Type: text/plain; charset=UTF-8
|
|
Content-Transfer-Encoding: 8bit
|
|
Organization: Wires
|
|
|
|
Change-Id: Ie547b5f478cc8e3b09ea2f324b62854576787e1b
|
|
Signed-off-by: Mattias Walström <lazzer@gmail.com>
|
|
---
|
|
CMakeLists.txt | 2 +-
|
|
include/sysrepo-cpp/Session.hpp | 6 +-
|
|
src/Session.cpp | 58 +++++++++--
|
|
tests/subscriptions-dynamic.cpp | 173 ++++++++++++++++++++++++++++++--
|
|
4 files changed, 213 insertions(+), 26 deletions(-)
|
|
|
|
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
|
index 01ec967..47472d2 100644
|
|
--- a/CMakeLists.txt
|
|
+++ b/CMakeLists.txt
|
|
@@ -82,7 +82,7 @@ if(BUILD_TESTING)
|
|
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-interfaces@2018-02-20.yang
|
|
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-ip@2018-02-22.yang
|
|
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-network-instance@2019-01-21.yang
|
|
- --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-subscribed-notifications@2019-09-09.yang -e replay
|
|
+ --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-subscribed-notifications@2019-09-09.yang -e replay -e subtree
|
|
--install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-yang-push@2019-09-09.yang -e on-change
|
|
)
|
|
|
|
diff --git a/include/sysrepo-cpp/Session.hpp b/include/sysrepo-cpp/Session.hpp
|
|
index 255f861..0781890 100644
|
|
--- a/include/sysrepo-cpp/Session.hpp
|
|
+++ b/include/sysrepo-cpp/Session.hpp
|
|
@@ -137,17 +137,17 @@ public:
|
|
const std::optional<FDHandling>& callbacks = std::nullopt);
|
|
|
|
[[nodiscard]] DynamicSubscription yangPushPeriodic(
|
|
- const std::optional<std::string>& xpathFilter,
|
|
+ const std::optional<std::variant<std::string, libyang::DataNodeAny>>& filter,
|
|
std::chrono::milliseconds periodTime,
|
|
const std::optional<NotificationTimeStamp>& anchorTime = std::nullopt,
|
|
const std::optional<NotificationTimeStamp>& stopTime = std::nullopt);
|
|
[[nodiscard]] DynamicSubscription yangPushOnChange(
|
|
- const std::optional<std::string>& xpathFilter,
|
|
+ const std::optional<std::variant<std::string, libyang::DataNodeAny>>& filter,
|
|
const std::optional<std::chrono::milliseconds>& dampeningPeriod = std::nullopt,
|
|
SyncOnStart syncOnStart = SyncOnStart::No,
|
|
const std::optional<NotificationTimeStamp>& stopTime = std::nullopt);
|
|
[[nodiscard]] DynamicSubscription subscribeNotifications(
|
|
- const std::optional<std::string>& xpathFilter,
|
|
+ const std::optional<std::variant<std::string, libyang::DataNodeAny>>& filter,
|
|
const std::optional<std::string>& stream = std::nullopt,
|
|
const std::optional<NotificationTimeStamp>& stopTime = std::nullopt,
|
|
const std::optional<NotificationTimeStamp>& startTime = std::nullopt);
|
|
diff --git a/src/Session.cpp b/src/Session.cpp
|
|
index 2c27c92..1d65d70 100644
|
|
--- a/src/Session.cpp
|
|
+++ b/src/Session.cpp
|
|
@@ -42,6 +42,37 @@ libyang::DataNode wrapSrData(std::shared_ptr<sr_session_ctx_s> sess, sr_data_t*
|
|
sr_release_data(data);
|
|
}));
|
|
}
|
|
+
|
|
+std::optional<std::string> constructXPathFilter(const std::optional<std::variant<std::string, libyang::DataNodeAny>>& filter)
|
|
+{
|
|
+ if (!filter) {
|
|
+ return std::nullopt;
|
|
+ }
|
|
+
|
|
+ if (std::holds_alternative<std::string>(*filter)) {
|
|
+ return std::get<std::string>(*filter);
|
|
+ }
|
|
+
|
|
+ auto node = std::get<libyang::DataNodeAny>(*filter);
|
|
+ auto value = node.releaseValue();
|
|
+
|
|
+ if (!value) {
|
|
+ return "/"; // select nothing, RFC 6241, 6.4.2
|
|
+ }
|
|
+
|
|
+ if (std::holds_alternative<libyang::DataNode>(*value)) {
|
|
+ char* str;
|
|
+
|
|
+ auto filterTree = std::get<libyang::DataNode>(*value);
|
|
+ auto res = srsn_filter_subtree2xpath(libyang::getRawNode(filterTree), nullptr, &str);
|
|
+ std::unique_ptr<char, decltype([](auto* p) constexpr { std::free(p); })> strDeleter(str); // pass ownership of c-string to the deleter
|
|
+
|
|
+ throwIfError(res, "Unable to convert subtree filter to xpath");
|
|
+ return str;
|
|
+ }
|
|
+
|
|
+ throw Error("Subtree filter anydata node must contain (possibly empty) libyang tree");
|
|
+}
|
|
}
|
|
|
|
/**
|
|
@@ -526,9 +557,9 @@ Subscription Session::onNotification(
|
|
/**
|
|
* Subscribe for receiving notifications according to 'ietf-yang-push' YANG periodic subscriptions.
|
|
*
|
|
- * Wraps `srsn_yang_push_periodic`.
|
|
+ * Wraps `srsn_subscribe` and `srsn_filter_subtree2xpath` for subtree filters.
|
|
*
|
|
- * @param xpathFilter Optional XPath that filters received notification.
|
|
+ * @param filter Optional filter for received notification, xpath filter for string type, subtree filter for libyang::DataNodeAny
|
|
* @param periodTime Notification period.
|
|
* @param anchorTime Optional anchor time for the period. Anchor time acts as a reference point for the period.
|
|
* @param stopTime Optional stop time ending the notification subscription.
|
|
@@ -536,7 +567,7 @@ Subscription Session::onNotification(
|
|
* @return A YangPushSubscription handle.
|
|
*/
|
|
DynamicSubscription Session::yangPushPeriodic(
|
|
- const std::optional<std::string>& xpathFilter,
|
|
+ const std::optional<std::variant<std::string, libyang::DataNodeAny>>& filter,
|
|
std::chrono::milliseconds periodTime,
|
|
const std::optional<NotificationTimeStamp>& anchorTime,
|
|
const std::optional<NotificationTimeStamp>& stopTime)
|
|
@@ -545,6 +576,8 @@ DynamicSubscription Session::yangPushPeriodic(
|
|
uint32_t subId;
|
|
auto stopSpec = stopTime ? std::optional{toTimespec(*stopTime)} : std::nullopt;
|
|
auto anchorSpec = anchorTime ? std::optional{toTimespec(*anchorTime)} : std::nullopt;
|
|
+ auto xpathFilter = constructXPathFilter(filter);
|
|
+
|
|
auto res = srsn_yang_push_periodic(m_sess.get(),
|
|
toDatastore(activeDatastore()),
|
|
xpathFilter ? xpathFilter->c_str() : nullptr,
|
|
@@ -561,9 +594,9 @@ DynamicSubscription Session::yangPushPeriodic(
|
|
/**
|
|
* Subscribe for receiving notifications according to 'ietf-yang-push' YANG on-change subscriptions.
|
|
*
|
|
- * Wraps `srsn_yang_push_on_change`.
|
|
+ * Wraps `srsn_subscribe` and `srsn_filter_subtree2xpath` for subtree filters.
|
|
*
|
|
- * @param xpathFilter Optional XPath that filters received notification.
|
|
+ * @param filter Optional filter for received notification, xpath filter for string type, subtree filter for libyang::DataNodeAny
|
|
* @param dampeningPeriod Optional dampening period.
|
|
* @param syncOnStart Whether to start with a notification of the current state.
|
|
* @param stopTime Optional stop time ending the notification subscription.
|
|
@@ -571,7 +604,7 @@ DynamicSubscription Session::yangPushPeriodic(
|
|
* @return A YangPushSubscription handle.
|
|
*/
|
|
DynamicSubscription Session::yangPushOnChange(
|
|
- const std::optional<std::string>& xpathFilter,
|
|
+ const std::optional<std::variant<std::string, libyang::DataNodeAny>>& filter,
|
|
const std::optional<std::chrono::milliseconds>& dampeningPeriod,
|
|
SyncOnStart syncOnStart,
|
|
const std::optional<NotificationTimeStamp>& stopTime)
|
|
@@ -579,6 +612,8 @@ DynamicSubscription Session::yangPushOnChange(
|
|
int fd;
|
|
uint32_t subId;
|
|
auto stopSpec = stopTime ? std::optional{toTimespec(*stopTime)} : std::nullopt;
|
|
+ auto xpathFilter = constructXPathFilter(filter);
|
|
+
|
|
auto res = srsn_yang_push_on_change(m_sess.get(),
|
|
toDatastore(activeDatastore()),
|
|
xpathFilter ? xpathFilter->c_str() : nullptr,
|
|
@@ -596,11 +631,11 @@ DynamicSubscription Session::yangPushOnChange(
|
|
}
|
|
|
|
/**
|
|
- * Subscribe for receiving notifications according to 'ietf-subscribed-notifications'
|
|
+ * Subscribe for receiving notifications according to 'ietf-subscribed-notifications'.
|
|
*
|
|
- * Wraps `srsn_subscribe.
|
|
+ * Wraps `srsn_subscribe` and `srsn_filter_subtree2xpath` for subtree filters.
|
|
*
|
|
- * @param xpathFilter Optional XPath that filters received notification.
|
|
+ * @param filter Optional filter for received notification, xpath filter for string type, subtree filter for libyang::DataNodeAny
|
|
* @param stream Optional stream to subscribe to.
|
|
* @param stopTime Optional stop time ending the subscription.
|
|
* @param startTime Optional start time of the subscription, used for replaying stored notifications.
|
|
@@ -608,7 +643,7 @@ DynamicSubscription Session::yangPushOnChange(
|
|
* @return A YangPushSubscription handle.
|
|
*/
|
|
DynamicSubscription Session::subscribeNotifications(
|
|
- const std::optional<std::string>& xpathFilter,
|
|
+ const std::optional<std::variant<std::string, libyang::DataNodeAny>>& filter,
|
|
const std::optional<std::string>& stream,
|
|
const std::optional<NotificationTimeStamp>& stopTime,
|
|
const std::optional<NotificationTimeStamp>& startTime)
|
|
@@ -618,10 +653,11 @@ DynamicSubscription Session::subscribeNotifications(
|
|
auto stopSpec = stopTime ? std::optional{toTimespec(*stopTime)} : std::nullopt;
|
|
auto startSpec = startTime ? std::optional{toTimespec(*startTime)} : std::nullopt;
|
|
struct timespec replayStartSpec;
|
|
+ auto xpathFilter = constructXPathFilter(filter);
|
|
|
|
auto res = srsn_subscribe(m_sess.get(),
|
|
stream ? stream->c_str() : nullptr,
|
|
- xpathFilter ? xpathFilter->c_str() : nullptr,
|
|
+ xpathFilter ? xpathFilter->data() : nullptr,
|
|
stopSpec ? &stopSpec.value() : nullptr,
|
|
startSpec ? &startSpec.value() : nullptr,
|
|
false,
|
|
diff --git a/tests/subscriptions-dynamic.cpp b/tests/subscriptions-dynamic.cpp
|
|
index a44bfba..84a0880 100644
|
|
--- a/tests/subscriptions-dynamic.cpp
|
|
+++ b/tests/subscriptions-dynamic.cpp
|
|
@@ -28,6 +28,9 @@
|
|
#define REQUIRE_NOTIFICATION(SUBSCRIPTION, NOTIFICATION) \
|
|
TROMPELOEIL_REQUIRE_CALL(rec, recordNotification(NOTIFICATION)).IN_SEQUENCE(seq);
|
|
|
|
+#define REQUIRE_NAMED_NOTIFICATION(SUBSCRIPTION, NOTIFICATION) \
|
|
+ expectations.emplace_back(TROMPELOEIL_NAMED_REQUIRE_CALL(rec, recordNotification(NOTIFICATION)).IN_SEQUENCE(seq));
|
|
+
|
|
#define READ_NOTIFICATION(SUBSCRIPTION) \
|
|
REQUIRE(pipeStatus((SUBSCRIPTION).fd()) == PipeStatus::DataReady); \
|
|
(SUBSCRIPTION).processEvent(cbNotif);
|
|
@@ -276,6 +279,89 @@ TEST_CASE("Dynamic subscriptions")
|
|
const auto excMessage = "Couldn't terminate yang-push subscription with id " + std::to_string(sub->subscriptionId()) + ": SR_ERR_NOT_FOUND";
|
|
REQUIRE_THROWS_WITH_AS(sub->terminate(), excMessage.c_str(), sysrepo::ErrorWithCode);
|
|
}
|
|
+
|
|
+ DOCTEST_SUBCASE("Filtering")
|
|
+ {
|
|
+ std::optional<sysrepo::DynamicSubscription> sub;
|
|
+ std::vector<std::unique_ptr<trompeloeil::expectation>> expectations;
|
|
+
|
|
+ DOCTEST_SUBCASE("xpath filter")
|
|
+ {
|
|
+ sub = sess.subscribeNotifications("/test_module:ping");
|
|
+
|
|
+ REQUIRE_NAMED_NOTIFICATION(sub, notifications[0]);
|
|
+ }
|
|
+
|
|
+ DOCTEST_SUBCASE("subtree filter")
|
|
+ {
|
|
+ libyang::CreatedNodes createdNodes;
|
|
+
|
|
+ DOCTEST_SUBCASE("filter a node")
|
|
+ {
|
|
+ DOCTEST_SUBCASE("XML")
|
|
+ {
|
|
+ createdNodes = sess.getContext().newPath2(
|
|
+ "/ietf-subscribed-notifications:establish-subscription/stream-subtree-filter",
|
|
+ libyang::XML{"<ping xmlns='urn:ietf:params:xml:ns:yang:test_module' />"});
|
|
+ }
|
|
+
|
|
+ DOCTEST_SUBCASE("JSON")
|
|
+ {
|
|
+ createdNodes = sess.getContext().newPath2(
|
|
+ "/ietf-subscribed-notifications:establish-subscription/stream-subtree-filter",
|
|
+ libyang::JSON{R"({"test_module:ping": {}})"});
|
|
+ }
|
|
+
|
|
+ REQUIRE_NAMED_NOTIFICATION(sub, notifications[0]);
|
|
+ }
|
|
+
|
|
+ DOCTEST_SUBCASE("filter more top level nodes")
|
|
+ {
|
|
+ DOCTEST_SUBCASE("XML")
|
|
+ {
|
|
+ createdNodes = sess.getContext().newPath2(
|
|
+ "/ietf-subscribed-notifications:establish-subscription/stream-subtree-filter",
|
|
+ libyang::XML{"<ping xmlns='urn:ietf:params:xml:ns:yang:test_module' />"
|
|
+ "<silent-ping xmlns='urn:ietf:params:xml:ns:yang:test_module' />"});
|
|
+ }
|
|
+
|
|
+ DOCTEST_SUBCASE("JSON")
|
|
+ {
|
|
+ createdNodes = sess.getContext().newPath2(
|
|
+ "/ietf-subscribed-notifications:establish-subscription/stream-subtree-filter",
|
|
+ libyang::JSON{R"({
|
|
+ "test_module:ping": {},
|
|
+ "test_module:silent-ping": {}
|
|
+ })"});
|
|
+ }
|
|
+
|
|
+ REQUIRE_NAMED_NOTIFICATION(sub, notifications[0]);
|
|
+ REQUIRE_NAMED_NOTIFICATION(sub, notifications[1]);
|
|
+ }
|
|
+
|
|
+ DOCTEST_SUBCASE("empty filter selects nothing")
|
|
+ {
|
|
+ createdNodes = sess.getContext().newPath2(
|
|
+ "/ietf-subscribed-notifications:establish-subscription/stream-subtree-filter",
|
|
+ std::nullopt);
|
|
+ }
|
|
+
|
|
+ sub = sess.subscribeNotifications(createdNodes.createdNode->asAny());
|
|
+ }
|
|
+
|
|
+ CLIENT_SEND_NOTIFICATION(notifications[0]);
|
|
+ CLIENT_SEND_NOTIFICATION(notifications[1]);
|
|
+
|
|
+ // read as many notifications as we expect
|
|
+ for (size_t i = 0; i < expectations.size(); ++i) {
|
|
+ READ_NOTIFICATION_BLOCKING(*sub);
|
|
+ }
|
|
+
|
|
+ sub->terminate();
|
|
+
|
|
+ // ensure no more notifications were sent
|
|
+ REQUIRE_PIPE_HANGUP(*sub);
|
|
+ }
|
|
}
|
|
|
|
DOCTEST_SUBCASE("YANG Push on change")
|
|
@@ -285,9 +371,73 @@ TEST_CASE("Dynamic subscriptions")
|
|
* between writing to sysrepo and reading the notifications.
|
|
*/
|
|
|
|
- auto sub = sess.yangPushOnChange(std::nullopt, std::nullopt, sysrepo::SyncOnStart::Yes);
|
|
+ DOCTEST_SUBCASE("Filters")
|
|
+ {
|
|
+ std::optional<sysrepo::DynamicSubscription> sub;
|
|
+
|
|
+ DOCTEST_SUBCASE("XPath filter")
|
|
+ {
|
|
+ sub = sess.yangPushOnChange("/test_module:leafInt32 | /test_module:popelnice/content/trash[name='asd']");
|
|
+ }
|
|
+
|
|
+ DOCTEST_SUBCASE("Subtree filter")
|
|
+ {
|
|
+ auto createdNodes = sess.getContext().newPath2(
|
|
+ "/ietf-subscribed-notifications:establish-subscription/ietf-yang-push:datastore-subtree-filter",
|
|
+ libyang::XML{"<leafInt32 xmlns='http://example.com/' />"
|
|
+ "<popelnice xmlns='http://example.com/'><content><trash><name>asd</name></trash></content></popelnice>"});
|
|
+ sub = sess.yangPushOnChange(createdNodes.createdNode->asAny());
|
|
+ }
|
|
+
|
|
+ client.setItem("/test_module:leafInt32", "42");
|
|
+ client.setItem("/test_module:popelnice/s", "asd");
|
|
+ client.setItem("/test_module:popelnice/content/trash[name='asd']", std::nullopt);
|
|
+ client.applyChanges();
|
|
|
|
- REQUIRE_YANG_PUSH_UPDATE(sub, R"({
|
|
+ client.deleteItem("/test_module:popelnice/s");
|
|
+ client.applyChanges();
|
|
+
|
|
+ REQUIRE_YANG_PUSH_UPDATE(*sub, R"({
|
|
+ "ietf-yang-push:push-change-update": {
|
|
+ "datastore-changes": {
|
|
+ "yang-patch": {
|
|
+ "patch-id": "patch-1",
|
|
+ "edit": [
|
|
+ {
|
|
+ "edit-id": "edit-1",
|
|
+ "operation": "create",
|
|
+ "target": "/test_module:leafInt32",
|
|
+ "value": {
|
|
+ "test_module:leafInt32": 42
|
|
+ }
|
|
+ },
|
|
+ {
|
|
+ "edit-id": "edit-2",
|
|
+ "operation": "create",
|
|
+ "target": "/test_module:popelnice/content/trash[name='asd']",
|
|
+ "value": {
|
|
+ "test_module:trash": {
|
|
+ "name": "asd"
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ ]
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
+)");
|
|
+ READ_YANG_PUSH_UPDATE(*sub);
|
|
+
|
|
+ sub->terminate();
|
|
+ REQUIRE_PIPE_HANGUP(*sub);
|
|
+ }
|
|
+
|
|
+ DOCTEST_SUBCASE("Sync on start")
|
|
+ {
|
|
+ auto sub = sess.yangPushOnChange(std::nullopt, std::nullopt, sysrepo::SyncOnStart::Yes);
|
|
+
|
|
+ REQUIRE_YANG_PUSH_UPDATE(sub, R"({
|
|
"ietf-yang-push:push-update": {
|
|
"datastore-contents": {
|
|
"test_module:values": [
|
|
@@ -298,14 +448,14 @@ TEST_CASE("Dynamic subscriptions")
|
|
}
|
|
}
|
|
)");
|
|
- READ_YANG_PUSH_UPDATE(sub);
|
|
+ READ_YANG_PUSH_UPDATE(sub);
|
|
|
|
- client.setItem("/test_module:leafInt32", "123");
|
|
- client.setItem("/test_module:values[.='5']", std::nullopt);
|
|
- client.deleteItem("/test_module:values[.='3']");
|
|
- client.applyChanges();
|
|
+ client.setItem("/test_module:leafInt32", "123");
|
|
+ client.setItem("/test_module:values[.='5']", std::nullopt);
|
|
+ client.deleteItem("/test_module:values[.='3']");
|
|
+ client.applyChanges();
|
|
|
|
- REQUIRE_YANG_PUSH_UPDATE(sub, R"({
|
|
+ REQUIRE_YANG_PUSH_UPDATE(sub, R"({
|
|
"ietf-yang-push:push-change-update": {
|
|
"datastore-changes": {
|
|
"yang-patch": {
|
|
@@ -342,10 +492,11 @@ TEST_CASE("Dynamic subscriptions")
|
|
}
|
|
}
|
|
)");
|
|
- READ_YANG_PUSH_UPDATE(sub);
|
|
+ READ_YANG_PUSH_UPDATE(sub);
|
|
|
|
- sub.terminate();
|
|
- REQUIRE_PIPE_HANGUP(sub);
|
|
+ sub.terminate();
|
|
+ REQUIRE_PIPE_HANGUP(sub);
|
|
+ }
|
|
}
|
|
|
|
DOCTEST_SUBCASE("YANG Push periodic")
|
|
--
|
|
2.43.0
|
|
|