From 542fd4006a4accb530581aa374f84e6136f4f306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Tue, 16 Dec 2025 13:31:09 +0100 Subject: [PATCH] Bump sysrepo,lignetconf2, libyang and netopeer2 This update got pretty messy, since libyang had some breaking changes. And unfortunatly rousette, libyang-cpp and sysrepo-cpp has not made any new release yet. --- buildroot | 2 +- doc/ChangeLog.md | 3 + .../klish-plugin-sysrepo.hash | 2 +- .../klish-plugin-sysrepo.mk | 2 +- ...0001-Implement-SchemaNode-actionRpcs.patch | 2 +- .../0002-CI-pin-to-libyang-v3.patch | 38 + .../0003-wrap-lyd_validate_op.patch | 216 +++++ .../libyang-cpp/0004-port-to-libyang-v4.patch | 435 +++++++++ .../0005-Add-SchemaNode-isOutput-method.patch | 77 ++ ...006-adapt-to-changes-in-libyang-v4.2.patch | 364 +++++++ ...fault-when-listing-module-s-features.patch | 112 +++ ...P-headers-and-the-input-data-payload.patch | 6 +- ...-throwing-exception-in-withRestconfE.patch | 9 +- ...-switch-to-the-new-cloud-s-Swift-URL.patch | 31 + ...stead-of-a-deadlock-when-the-handle.patch} | 6 +- ...alling-this-an-almost-RESTCONF-serve.patch | 29 + ...e-long-lived-connections-on-SIGTERM.patch} | 9 +- ... 0007-restconf-refactor-server-stop.patch} | 9 +- ...008-tests-use-std-string-starts_with.patch | 33 + ...s-add-SSE-event-watcher-for-comments.patch | 172 ++++ ...d-keep-alive-pings-from-EventStream.patch} | 9 +- ...vent-streams-use-named-constructors.patch} | 9 +- ...-convention-for-weak_from_this-lock.patch} | 9 +- ...-log-level-LEVEL-command-line-option.patch | 78 -- ...x-a-possible-bad_weak_ptr-exception.patch} | 9 +- ...d-optional-callbacks-to-EventStream.patch} | 9 +- ...add-internal-RPC-handler-dispatcher.patch} | 9 +- ...sing-incomplete-events-in-SSE-client.patch | 120 +++ ...am-fix-possible-heap-use-after-free.patch} | 9 +- ...se-a-working-version-of-nghttp2-asio.patch | 52 + .../0019-ping-use-a-monotonic-timer.patch | 61 ++ .../0020-tests-use-a-monotonic-timer.patch | 81 ++ .../rousette/0021-port-to-libyang-v4.patch | 360 +++++++ ...estconf-prevent-lock-order-inversion.patch | 63 ++ ...pollute-scope-with-unnecessary-varia.patch | 39 + ...onf-validate-input-for-internal-rpcs.patch | 89 ++ ...-from-parts-ietf-subscribed-notifica.patch | 71 ++ ...sh-subscription-RPC-for-subscribed-n.patch | 679 +++++++++++++ ...factor-restconf-stream-request-types.patch | 165 ++++ ...ndpoint-for-subscribed-notifications.patch | 893 ++++++++++++++++++ ...ynamic-subscriptions-shutdown-method.patch | 69 ++ ...te-subsc.-notification-streams-after.patch | 319 +++++++ ...nf-add-kill-delete-subscription-RPCs.patch | 360 +++++++ ...d-subscribed-notifications-filtering.patch | 232 +++++ .../rousette/0033-cmake-wrap-long-line.patch | 36 + ...-replays-in-subscribed-notifications.patch | 322 +++++++ ...factor-getting-datastore-from-string.patch | 95 ++ .../0036-build-remove-duplicate-line.patch | 31 + .../0037-remove-obsolete-targets.patch | 156 +++ .../rousette/0038-port-to-libyang-v4.2.patch | 156 +++ .../0001-CI-pin-to-libyang-v3.patch | 47 + ...-fix-implicit-dependency-on-optional.patch | 33 + ...x-include-for-for-our-public-headers.patch | 31 + ...r-of-files-which-are-named-utils.hpp.patch | 82 ++ ...Fix-a-different-function-return-type.patch | 60 ++ ...-raw-strings-and-macros-break-gcc-13.patch | 61 ++ .../sysrepo-cpp/0007-Port-to-libyang-v4.patch | 305 ++++++ ...nOperGet-unconsistent-with-C-library.patch | 90 ++ .../0009-Update-to-libyang-v4.2.patch | 121 +++ patches/frr/9.1.3/0001-libyang-compat.patch | 94 +- .../do-not-fail-without-certficates.patch | 22 + ...IX-printed-context-align-all-sizes-a.patch | 741 +++++++++++++++ ...context-BUGFIX-missing-mem-alignment.patch | 46 + .../0003-lyb-BUGFIX-big-endian-fixes.patch | 102 ++ .../0004-bits-BUGFIX-operator-priority.patch | 41 + ...nteger-BUGFIX-big-endian-print-fixes.patch | 223 +++++ ...ntext-BUGFIX-unexpected-context-node.patch | 32 + ...as-copy-from-only-in-rpc-copy-config.patch | 6 +- ...0002-Do-not-generate-data-in-sysrepo.patch | 6 +- ...-support-having-local-users-disabled.patch | 56 ++ ...nstall-YANG-models-we-do-it-in-confd.patch | 30 + ...add-support-for-running-in-foregroun.patch | 4 +- ...NE-to-return-any-error-to-sysrepocfg.patch | 32 +- ...3-Allow-to-copy-from-factory-default.patch | 10 +- ...sysrepoctl-to-install-factory-config.patch | 40 +- ...e-new-log-level-SEC-for-audit-trails.patch | 6 +- ...ail-for-high-priority-system-changes.patch | 14 +- ...sr_shmsub_listen_thread-exit-process.patch | 8 +- .../4.2.10/0008-Cross-compile-fixes.patch | 37 + 79 files changed, 8340 insertions(+), 187 deletions(-) create mode 100644 package/libyang-cpp/0002-CI-pin-to-libyang-v3.patch create mode 100644 package/libyang-cpp/0003-wrap-lyd_validate_op.patch create mode 100644 package/libyang-cpp/0004-port-to-libyang-v4.patch create mode 100644 package/libyang-cpp/0005-Add-SchemaNode-isOutput-method.patch create mode 100644 package/libyang-cpp/0006-adapt-to-changes-in-libyang-v4.2.patch create mode 100644 package/libyang-cpp/0007-don-t-segfault-when-listing-module-s-features.patch create mode 100644 package/rousette/0003-CI-switch-to-the-new-cloud-s-Swift-URL.patch rename package/rousette/{0003-restconf-crash-instead-of-a-deadlock-when-the-handle.patch => 0004-restconf-crash-instead-of-a-deadlock-when-the-handle.patch} (97%) create mode 100644 package/rousette/0005-doc-let-s-stop-calling-this-an-almost-RESTCONF-serve.patch rename package/rousette/{0004-close-long-lived-connections-on-SIGTERM.patch => 0006-close-long-lived-connections-on-SIGTERM.patch} (98%) rename package/rousette/{0005-restconf-refactor-server-stop.patch => 0007-restconf-refactor-server-stop.patch} (87%) create mode 100644 package/rousette/0008-tests-use-std-string-starts_with.patch create mode 100644 package/rousette/0009-tests-add-SSE-event-watcher-for-comments.patch rename package/rousette/{0006-http-send-keep-alive-pings-from-EventStream.patch => 0010-http-send-keep-alive-pings-from-EventStream.patch} (98%) rename package/rousette/{0007-refactor-event-streams-use-named-constructors.patch => 0011-refactor-event-streams-use-named-constructors.patch} (97%) rename package/rousette/{0008-refactor-a-better-convention-for-weak_from_this-lock.patch => 0012-refactor-a-better-convention-for-weak_from_this-lock.patch} (93%) delete mode 100644 package/rousette/0013-Add-log-level-LEVEL-command-line-option.patch rename package/rousette/{0009-fix-a-possible-bad_weak_ptr-exception.patch => 0013-fix-a-possible-bad_weak_ptr-exception.patch} (79%) rename package/rousette/{0010-http-add-optional-callbacks-to-EventStream.patch => 0014-http-add-optional-callbacks-to-EventStream.patch} (95%) rename package/rousette/{0011-restconf-add-internal-RPC-handler-dispatcher.patch => 0015-restconf-add-internal-RPC-handler-dispatcher.patch} (93%) create mode 100644 package/rousette/0016-tests-processing-incomplete-events-in-SSE-client.patch rename package/rousette/{0012-EventStream-fix-possible-heap-use-after-free.patch => 0017-EventStream-fix-possible-heap-use-after-free.patch} (98%) create mode 100644 package/rousette/0018-Use-a-working-version-of-nghttp2-asio.patch create mode 100644 package/rousette/0019-ping-use-a-monotonic-timer.patch create mode 100644 package/rousette/0020-tests-use-a-monotonic-timer.patch create mode 100644 package/rousette/0021-port-to-libyang-v4.patch create mode 100644 package/rousette/0022-restconf-prevent-lock-order-inversion.patch create mode 100644 package/rousette/0023-refactor-do-not-pollute-scope-with-unnecessary-varia.patch create mode 100644 package/rousette/0024-restconf-validate-input-for-internal-rpcs.patch create mode 100644 package/rousette/0025-tests-allow-read-from-parts-ietf-subscribed-notifica.patch create mode 100644 package/rousette/0026-restconf-establish-subscription-RPC-for-subscribed-n.patch create mode 100644 package/rousette/0027-refactor-restconf-stream-request-types.patch create mode 100644 package/rousette/0028-restconf-endpoint-for-subscribed-notifications.patch create mode 100644 package/rousette/0029-restconf-dynamic-subscriptions-shutdown-method.patch create mode 100644 package/rousette/0030-restconf-terminate-subsc.-notification-streams-after.patch create mode 100644 package/rousette/0031-restconf-add-kill-delete-subscription-RPCs.patch create mode 100644 package/rousette/0032-restconf-add-subscribed-notifications-filtering.patch create mode 100644 package/rousette/0033-cmake-wrap-long-line.patch create mode 100644 package/rousette/0034-restconf-support-replays-in-subscribed-notifications.patch create mode 100644 package/rousette/0035-restconf-refactor-getting-datastore-from-string.patch create mode 100644 package/rousette/0036-build-remove-duplicate-line.patch create mode 100644 package/rousette/0037-remove-obsolete-targets.patch create mode 100644 package/rousette/0038-port-to-libyang-v4.2.patch create mode 100644 package/sysrepo-cpp/0001-CI-pin-to-libyang-v3.patch create mode 100644 package/sysrepo-cpp/0002-fix-implicit-dependency-on-optional.patch create mode 100644 package/sysrepo-cpp/0003-fix-include-for-for-our-public-headers.patch create mode 100644 package/sysrepo-cpp/0004-reduce-number-of-files-which-are-named-utils.hpp.patch create mode 100644 package/sysrepo-cpp/0005-Fix-a-different-function-return-type.patch create mode 100644 package/sysrepo-cpp/0006-fix-raw-strings-and-macros-break-gcc-13.patch create mode 100644 package/sysrepo-cpp/0007-Port-to-libyang-v4.patch create mode 100644 package/sysrepo-cpp/0008-fix-onOperGet-unconsistent-with-C-library.patch create mode 100644 package/sysrepo-cpp/0009-Update-to-libyang-v4.2.patch create mode 100644 patches/libnetconf2/4.1.2/do-not-fail-without-certficates.patch create mode 100644 patches/libyang/4.2.2/0001-plugins-ext-BUGFIX-printed-context-align-all-sizes-a.patch create mode 100644 patches/libyang/4.2.2/0002-printer-context-BUGFIX-missing-mem-alignment.patch create mode 100644 patches/libyang/4.2.2/0003-lyb-BUGFIX-big-endian-fixes.patch create mode 100644 patches/libyang/4.2.2/0004-bits-BUGFIX-operator-priority.patch create mode 100644 patches/libyang/4.2.2/0005-integer-BUGFIX-big-endian-print-fixes.patch create mode 100644 patches/libyang/4.2.2/0006-context-BUGFIX-unexpected-context-node.patch rename patches/netopeer2/{2.4.5 => 2.7.0}/0001-Allow-factory-as-copy-from-only-in-rpc-copy-config.patch (85%) rename patches/netopeer2/{2.4.5 => 2.7.0}/0002-Do-not-generate-data-in-sysrepo.patch (95%) create mode 100644 patches/netopeer2/2.7.0/0003-main-UPDATE-support-having-local-users-disabled.patch create mode 100644 patches/netopeer2/2.7.0/0004-Do-not-install-YANG-models-we-do-it-in-confd.patch rename patches/sysrepo/{3.7.11 => 4.2.10}/0001-sysrepo-plugind-add-support-for-running-in-foregroun.patch (95%) rename patches/sysrepo/{3.7.11 => 4.2.10}/0002-Allow-SR_EV_DONE-to-return-any-error-to-sysrepocfg.patch (89%) rename patches/sysrepo/{3.7.11 => 4.2.10}/0003-Allow-to-copy-from-factory-default.patch (77%) rename patches/sysrepo/{3.7.11 => 4.2.10}/0004-Add-z-switch-to-sysrepoctl-to-install-factory-config.patch (84%) rename patches/sysrepo/{3.7.11 => 4.2.10}/0005-Introduce-new-log-level-SEC-for-audit-trails.patch (96%) rename patches/sysrepo/{3.7.11 => 4.2.10}/0006-Add-audit-trail-for-high-priority-system-changes.patch (85%) rename patches/sysrepo/{3.7.11 => 4.2.10}/0007-On-error-in-sr_shmsub_listen_thread-exit-process.patch (81%) create mode 100644 patches/sysrepo/4.2.10/0008-Cross-compile-fixes.patch diff --git a/buildroot b/buildroot index 5baee16c..4166c59a 160000 --- a/buildroot +++ b/buildroot @@ -1 +1 @@ -Subproject commit 5baee16c62b087f06b196c1896846f6f9a2b3cd3 +Subproject commit 4166c59afd84cdd17311b8705f5c4d94e8af32a8 diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 90218984..123dc50f 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -9,6 +9,9 @@ All notable changes to the project are documented in this file. ### Changes - Upgrade Linux kernel to 6.12.62 (LTS) +- Upgrade libyang to 4.2.2 +- Upgrade sysrepo to 4.2.10 +- Upgrade netopeer2 (NETCONF) to 2.7.0 - Add RIPv2 routing support, issue #582 - Add support for configurable OSPF debug logging, issue #1281. Debug options can now be enabled per category (bfd, packet, ism, nsm, default-information, diff --git a/package/klish-plugin-sysrepo/klish-plugin-sysrepo.hash b/package/klish-plugin-sysrepo/klish-plugin-sysrepo.hash index 64eef92a..f6dcef91 100644 --- a/package/klish-plugin-sysrepo/klish-plugin-sysrepo.hash +++ b/package/klish-plugin-sysrepo/klish-plugin-sysrepo.hash @@ -1,3 +1,3 @@ # Locally calculated sha256 9d9d33b873917ca5d0bdcc47a36d2fd385971ab0c045d1472fcadf95ee5bcf5b LICENCE -sha256 cfe1064849a158f3eb9e82f12a709723115fc9bc4d30056e0e96ea0ba03672a2 klish-plugin-sysrepo-4e86a836c9b09a14c9625ea3d3b56b6578be4833-git4.tar.gz +sha256 71802708c96521a8bf43e38d6e5ac36de5481e087eaabdbebbc282ec0e29b8d3 klish-plugin-sysrepo-0846fa6b8d5d665eebfc7b1664542646915a79f7-git4.tar.gz diff --git a/package/klish-plugin-sysrepo/klish-plugin-sysrepo.mk b/package/klish-plugin-sysrepo/klish-plugin-sysrepo.mk index ddd22ffb..359f140d 100644 --- a/package/klish-plugin-sysrepo/klish-plugin-sysrepo.mk +++ b/package/klish-plugin-sysrepo/klish-plugin-sysrepo.mk @@ -4,7 +4,7 @@ # ################################################################################ -KLISH_PLUGIN_SYSREPO_VERSION = 4e86a836c9b09a14c9625ea3d3b56b6578be4833 +KLISH_PLUGIN_SYSREPO_VERSION = 0846fa6b8d5d665eebfc7b1664542646915a79f7 KLISH_PLUGIN_SYSREPO_SITE = https://github.com/kernelkit/klish-plugin-sysrepo.git #KLISH_PLUGIN_SYSREPO_VERSION = cdd3eb51a7f7ee0ed5bd925fa636061d3b1b85fb #KLISH_PLUGIN_SYSREPO_SITE = https://src.libcode.org/pkun/klish-plugin-sysrepo.git diff --git a/package/libyang-cpp/0001-Implement-SchemaNode-actionRpcs.patch b/package/libyang-cpp/0001-Implement-SchemaNode-actionRpcs.patch index 5dfc89b4..d7a78e08 100644 --- a/package/libyang-cpp/0001-Implement-SchemaNode-actionRpcs.patch +++ b/package/libyang-cpp/0001-Implement-SchemaNode-actionRpcs.patch @@ -1,7 +1,7 @@ From 22b41e7ed8268b94ccc139138e2037c390a3a616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= Date: Thu, 10 Jul 2025 15:00:45 +0200 -Subject: [PATCH] Implement `SchemaNode::actionRpcs()` +Subject: [PATCH 1/7] Implement `SchemaNode::actionRpcs()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit diff --git a/package/libyang-cpp/0002-CI-pin-to-libyang-v3.patch b/package/libyang-cpp/0002-CI-pin-to-libyang-v3.patch new file mode 100644 index 00000000..cc29661a --- /dev/null +++ b/package/libyang-cpp/0002-CI-pin-to-libyang-v3.patch @@ -0,0 +1,38 @@ +From 1f8e502cf0308faa0336b1077308184de6a83ee0 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Mon, 20 Oct 2025 11:45:06 +0200 +Subject: [PATCH 2/7] CI: pin to libyang v3 +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Change-Id: Idba085ad9c9c137874801f1d4089f8ee6bb390c7 +Signed-off-by: Mattias Walström +--- + .zuul.yaml | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/.zuul.yaml b/.zuul.yaml +index 7b92766..fd89549 100644 +--- a/.zuul.yaml ++++ b/.zuul.yaml +@@ -4,13 +4,13 @@ + - f38-gcc-cover: + required-projects: + - name: github/CESNET/libyang +- override-checkout: devel ++ override-checkout: cesnet/2025-08-07 + - name: github/doctest/doctest + override-checkout: v2.3.6 + - f38-clang-asan-ubsan: + required-projects: &projects + - name: github/CESNET/libyang +- override-checkout: devel ++ override-checkout: cesnet/2025-08-07 + - name: github/doctest/doctest + override-checkout: v2.4.11 + - f38-clang-tsan: +-- +2.43.0 + diff --git a/package/libyang-cpp/0003-wrap-lyd_validate_op.patch b/package/libyang-cpp/0003-wrap-lyd_validate_op.patch new file mode 100644 index 00000000..edbcded5 --- /dev/null +++ b/package/libyang-cpp/0003-wrap-lyd_validate_op.patch @@ -0,0 +1,216 @@ +From b27a65d5d5f6ffffbd2c26c1b5bea57a6de584d0 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Mon, 20 Oct 2025 18:46:38 +0200 +Subject: [PATCH 3/7] wrap lyd_validate_op +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +This patch wraps lyd_validate_op from libyang. Downstream users can now +validate operations (RPC input, replies, and notifications). + +Change-Id: Ib03070bbc3e1d0dccd6bf38eda82e944db1093b1 +Signed-off-by: Mattias Walström +--- + include/libyang-cpp/DataNode.hpp | 2 + + src/DataNode.cpp | 24 +++++++++++ + tests/context.cpp | 2 +- + tests/data_node.cpp | 72 ++++++++++++++++++++++++++++++++ + tests/example_schema.hpp | 22 ++++++++++ + 5 files changed, 121 insertions(+), 1 deletion(-) + +diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp +index 50d6c0e..e202a96 100644 +--- a/include/libyang-cpp/DataNode.hpp ++++ b/include/libyang-cpp/DataNode.hpp +@@ -59,6 +59,7 @@ template + void handleLyTreeOperation(DataNode* affectedNode, Operation operation, Siblings siblings, std::shared_ptr newRefs); + + LIBYANG_CPP_EXPORT void validateAll(std::optional& node, const std::optional& opts = std::nullopt); ++LIBYANG_CPP_EXPORT void validateOp(libyang::DataNode& input, const std::optional& opsTree, OperationType opType); + + LIBYANG_CPP_EXPORT Set findXPathAt( + const std::optional& contextNode, +@@ -147,6 +148,7 @@ public: + friend LIBYANG_CPP_EXPORT lyd_node* getRawNode(DataNode node); + + friend LIBYANG_CPP_EXPORT void validateAll(std::optional& node, const std::optional& opts); ++ friend LIBYANG_CPP_EXPORT void validateOp(libyang::DataNode& input, const std::optional& opsTree, OperationType opType); + friend LIBYANG_CPP_EXPORT Set findXPathAt(const std::optional& contextNode, const libyang::DataNode& forest, const std::string& xpath); + + bool operator==(const DataNode& node) const; +diff --git a/src/DataNode.cpp b/src/DataNode.cpp +index 7e87917..e28ef30 100644 +--- a/src/DataNode.cpp ++++ b/src/DataNode.cpp +@@ -1226,6 +1226,30 @@ void validateAll(std::optional& node, const std::optional& opsTree, OperationType opType) ++{ ++ if (opType == OperationType::RpcYang || opType == OperationType::RpcRestconf || opType == OperationType::RpcNetconf) { ++ opType = OperationType::RpcYang; ++ } else if (opType == OperationType::ReplyYang || opType == OperationType::ReplyRestconf || opType == OperationType::ReplyNetconf) { ++ opType = OperationType::ReplyYang; ++ } else if (opType == OperationType::NotificationYang || opType == OperationType::NotificationRestconf || opType == OperationType::NotificationNetconf) { ++ opType = OperationType::NotificationYang; ++ } else { ++ throw Error("validateOp: DataYang datatype is not supported"); ++ } ++ ++ auto ret = lyd_validate_op(input.m_node, opsTree ? opsTree->m_node : nullptr, utils::toOpType(opType), nullptr); ++ throwIfError(ret, "libyang:validateOp: lyd_validate_op failed"); ++} ++ + /** @short Find instances matching the provided XPath + * + * @param contextNode The node which serves as the "context node" for XPath evaluation. Use nullopt to start at root. +diff --git a/tests/context.cpp b/tests/context.cpp +index 6c5dde8..c0b7e09 100644 +--- a/tests/context.cpp ++++ b/tests/context.cpp +@@ -154,7 +154,7 @@ TEST_CASE("context") + { + auto mod = ctx->parseModule(example_schema, libyang::SchemaFormat::YANG); + auto rpcs = mod.actionRpcs(); +- REQUIRE(rpcs.size() == 1); ++ REQUIRE(rpcs.size() == 2); + REQUIRE(rpcs[0].module().name() == "example-schema"); + REQUIRE(rpcs[0].name() == "myRpc"); + +diff --git a/tests/data_node.cpp b/tests/data_node.cpp +index db5a28e..9215b12 100644 +--- a/tests/data_node.cpp ++++ b/tests/data_node.cpp +@@ -2430,6 +2430,78 @@ TEST_CASE("Data Node manipulation") + "Can't parse into operation data tree: LY_EVALID", libyang::Error); + } + } ++ ++ DOCTEST_SUBCASE("Validation") ++ { ++ DOCTEST_SUBCASE("Valid input") ++ { ++ std::string rpcInput; ++ std::string rpcPath; ++ std::string expected; ++ std::optional depTree; ++ ++ DOCTEST_SUBCASE("RPC") ++ { ++ rpcInput = R"({"example-schema:input": { "number": 42 } })"; ++ rpcPath = "/example-schema:rpc-with-choice"; ++ expected = R"({ ++ "example-schema:rpc-with-choice": { ++ "number": 42 ++ } ++} ++)"; ++ } ++ ++ DOCTEST_SUBCASE("Action") ++ { ++ rpcInput = R"({ "example-schema:input": { "friend": "Kuba" } })"; ++ rpcPath = "/example-schema:person[name='Franta']/poke-a-friend"; ++ expected = R"({ ++ "example-schema:poke-a-friend": { ++ "friend": "Kuba" ++ } ++} ++)"; ++ depTree = ctx.newPath("/example-schema:person[name='Kuba']"); ++ } ++ ++ auto [parent, rpcTree] = ctx.newPath2(rpcPath); ++ auto rpcOp = rpcTree->parseOp(rpcInput, dataTypeFor(rpcInput), libyang::OperationType::RpcRestconf); ++ ++ REQUIRE(!rpcOp.op); ++ REQUIRE(rpcOp.tree); ++ ++ libyang::validateOp(*rpcTree, depTree, libyang::OperationType::RpcRestconf); ++ REQUIRE(*rpcTree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == expected); ++ } ++ ++ DOCTEST_SUBCASE("Nodes in disjunctive cases defined together") ++ { ++ auto rpcInput = R"({ "example-schema:input": { "number": 42, "text": "The ultimate answer" } })"; ++ ++ auto rpcTree = ctx.newPath("/example-schema:rpc-with-choice"); ++ auto rpcOp = rpcTree.parseOp(rpcInput, dataTypeFor(rpcInput), libyang::OperationType::RpcRestconf); ++ ++ REQUIRE(!rpcOp.op); ++ REQUIRE(rpcOp.tree); ++ ++ REQUIRE_THROWS_WITH_AS(libyang::validateOp(rpcTree, std::nullopt, libyang::OperationType::RpcRestconf), "libyang:validateOp: lyd_validate_op failed: LY_EVALID", libyang::Error); ++ } ++ ++ DOCTEST_SUBCASE("Action without the leafref node") ++ { ++ auto rpcInput = R"({ "example-schema:input": { "friend": "Kuba" } })"; ++ auto rpcPath = "/example-schema:person[name='Franta']/poke-a-friend"; ++ ++ auto [parent, rpcTree] = ctx.newPath2(rpcPath); ++ auto rpcOp = rpcTree->parseOp(rpcInput, dataTypeFor(rpcInput), libyang::OperationType::RpcRestconf); ++ ++ REQUIRE(!rpcOp.op); ++ REQUIRE(rpcOp.tree); ++ ++ REQUIRE_THROWS_WITH_AS(libyang::validateOp(*rpcTree, std::nullopt, libyang::OperationType::RpcRestconf), "libyang:validateOp: lyd_validate_op failed: LY_EVALID", libyang::Error); ++ } ++ } + } + + DOCTEST_SUBCASE("comparing") { +diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp +index 02d3d74..acc6ecc 100644 +--- a/tests/example_schema.hpp ++++ b/tests/example_schema.hpp +@@ -108,6 +108,16 @@ module example-schema { + } + + action poke { } ++ ++ action poke-a-friend { ++ input { ++ leaf friend { ++ type leafref { ++ path '../../../person/name'; ++ } ++ } ++ } ++ } + } + + leaf bossPerson { +@@ -284,6 +294,18 @@ module example-schema { + } + } + } ++ rpc rpc-with-choice { ++ input { ++ choice the-impossible-choice { ++ leaf text { ++ type string; ++ } ++ leaf number { ++ type int32; ++ } ++ } ++ } ++ } + + anydata myData { + } +-- +2.43.0 + diff --git a/package/libyang-cpp/0004-port-to-libyang-v4.patch b/package/libyang-cpp/0004-port-to-libyang-v4.patch new file mode 100644 index 00000000..c1a95091 --- /dev/null +++ b/package/libyang-cpp/0004-port-to-libyang-v4.patch @@ -0,0 +1,435 @@ +From 4c00c24dee6bbc694d62e7174b7ac14f8aa95f14 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Thu, 30 Oct 2025 21:16:13 +0100 +Subject: [PATCH 4/7] port to libyang v4 +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Apart from the usual API/ABI changes, the default values of a `leaf` or +a `leaf-list` are no longer canonicalized. + +This used to be missing from libyang v4, but it was added recently. +Sill, chances are that the form which was used in the YANG model's +source code is actually *the* intended presentation, so let's skip +canonicalization altogether. + +Upstream has removed some flags and defines, so let's sync their list +with whatever is currently available in the `devel` branch. + +Change-Id: I52dcec88eeee003b45dd6cf400f4d6875abbcc05 +Signed-off-by: Mattias Walström +--- + .zuul.yaml | 4 ++-- + CMakeLists.txt | 4 ++-- + README.md | 2 +- + include/libyang-cpp/Context.hpp | 6 +++++- + include/libyang-cpp/DataNode.hpp | 5 ++++- + include/libyang-cpp/Enum.hpp | 15 +++++++++++++-- + src/Context.cpp | 12 ++++++++++-- + src/DataNode.cpp | 11 +++++++++-- + src/SchemaNode.cpp | 6 +++--- + src/Type.cpp | 2 +- + src/utils/enum.hpp | 21 +++++++++++++++++++-- + tests/context.cpp | 32 +++++++++++++++++--------------- + tests/data_node.cpp | 2 +- + tests/schema_node.cpp | 4 ++-- + 14 files changed, 89 insertions(+), 37 deletions(-) + +diff --git a/.zuul.yaml b/.zuul.yaml +index fd89549..7b92766 100644 +--- a/.zuul.yaml ++++ b/.zuul.yaml +@@ -4,13 +4,13 @@ + - f38-gcc-cover: + required-projects: + - name: github/CESNET/libyang +- override-checkout: cesnet/2025-08-07 ++ override-checkout: devel + - name: github/doctest/doctest + override-checkout: v2.3.6 + - f38-clang-asan-ubsan: + required-projects: &projects + - name: github/CESNET/libyang +- override-checkout: cesnet/2025-08-07 ++ override-checkout: devel + - name: github/doctest/doctest + override-checkout: v2.4.11 + - f38-clang-tsan: +diff --git a/CMakeLists.txt b/CMakeLists.txt +index c6db320..90f9aaa 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -20,7 +20,7 @@ add_custom_target(libyang-cpp-version-cmake + cmake/ProjectGitVersionRunner.cmake + ) + include(cmake/ProjectGitVersion.cmake) +-set(LIBYANG_CPP_PKG_VERSION "4") ++set(LIBYANG_CPP_PKG_VERSION "5") + prepare_git_version(LIBYANG_CPP_VERSION ${LIBYANG_CPP_PKG_VERSION}) + + find_package(Doxygen) +@@ -28,7 +28,7 @@ option(WITH_DOCS "Create and install internal documentation (needs Doxygen)" ${D + option(BUILD_SHARED_LIBS "By default, shared libs are enabled. Turn off for a static build." ON) + + find_package(PkgConfig REQUIRED) +-pkg_check_modules(LIBYANG REQUIRED libyang>=3.10.1 IMPORTED_TARGET) ++pkg_check_modules(LIBYANG REQUIRED libyang>=4.1.0 IMPORTED_TARGET) + + # FIXME from gcc 14.1 on we should be able to use the calendar/time from libstdc++ and thus remove the date dependency + find_package(date) +diff --git a/README.md b/README.md +index 8291e00..0cef2ff 100644 +--- a/README.md ++++ b/README.md +@@ -8,7 +8,7 @@ + Object lifetimes are managed automatically via RAII. + + ## Dependencies +-- [libyang v3](https://github.com/CESNET/libyang) - the `devel` branch (even for the `master` branch of *libyang-cpp*) ++- [libyang v4](https://github.com/CESNET/libyang) - the `devel` branch (even for the `master` branch of *libyang-cpp*) + - C++20 compiler (e.g., GCC 10.x+, clang 10+) + - CMake 3.19+ + - optionally for built-in tests, [Doctest](https://github.com/doctest/doctest/) as a C++ unit test framework +diff --git a/include/libyang-cpp/Context.hpp b/include/libyang-cpp/Context.hpp +index ca89063..e695914 100644 +--- a/include/libyang-cpp/Context.hpp ++++ b/include/libyang-cpp/Context.hpp +@@ -108,7 +108,11 @@ public: + std::optional getSubmodule(const std::string& name, const std::optional& revision) const; + void registerModuleCallback(std::function callback); + +- ParsedOp parseOp(const std::string& input, const DataFormat format, const OperationType opType) const; ++ ParsedOp parseOp( ++ const std::string& input, ++ const DataFormat format, ++ const OperationType opType, ++ const std::optional parseOpts = std::nullopt) const; + + DataNode newPath(const std::string& path, const std::optional& value = std::nullopt, const std::optional options = std::nullopt) const; + CreatedNodes newPath2(const std::string& path, const std::optional& value = std::nullopt, const std::optional options = std::nullopt) const; +diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp +index e202a96..35e5936 100644 +--- a/include/libyang-cpp/DataNode.hpp ++++ b/include/libyang-cpp/DataNode.hpp +@@ -123,7 +123,10 @@ public: + Collection siblings() const; + Collection immediateChildren() const; + +- ParsedOp parseOp(const std::string& input, const DataFormat format, const OperationType opType) const; ++ ParsedOp parseOp(const std::string& input, ++ const DataFormat format, ++ const OperationType opType, ++ const std::optional parseOpts = std::nullopt) const; + + void parseSubtree( + const std::string& data, +diff --git a/include/libyang-cpp/Enum.hpp b/include/libyang-cpp/Enum.hpp +index 85dcc83..32142f9 100644 +--- a/include/libyang-cpp/Enum.hpp ++++ b/include/libyang-cpp/Enum.hpp +@@ -136,6 +136,7 @@ enum class DuplicationOptions : uint32_t { + WithFlags = 0x08, + NoExt = 0x10, + WithPriv = 0x20, ++ NoLyds = 0x40, + }; + + enum class NodeType : uint16_t { +@@ -167,6 +168,13 @@ enum class ContextOptions : uint16_t { + PreferSearchDirs = 0x20, + SetPrivParsed = 0x40, + ExplicitCompile = 0x80, ++ EnableImpFeatures = 0x100, ++ CompileObsolete = 0x200, ++ LybHashes = 0x400, ++ LeafrefExtended = 0x800, ++ LeafrefLinking = 0x1000, ++ BuiltinPluginsOnly = 0x2000, ++ StaticPluginsOnly = 0x4000, + }; + + /** +@@ -203,7 +211,6 @@ enum class AnydataValueType : uint32_t { + String, + XML, + JSON, +- LYB + }; + + /** +@@ -252,6 +259,7 @@ enum class ValidationOptions { + MultiError = 0x0004, + Operational = 0x0008, + NoDefaults = 0x0010, ++ NotFinal = 0x0020, + }; + + /** +@@ -262,11 +270,14 @@ enum class ParseOptions { + Strict = 0x020000, + Opaque = 0x040000, + NoState = 0x080000, +- LybModUpdate = 0x100000, ++ LybSkipCtxCheck = 0x100000, + Ordered = 0x200000, + Subtree = 0x400000, /**< Do not use this one for parsing of data subtrees */ + WhenTrue = 0x800000, + NoNew = 0x1000000, ++ StoreOnly = 0x2010000, ++ JsonNull = 0x4000000, ++ JsonStringDataTypes = 0x8000000, + }; + + /** +diff --git a/src/Context.cpp b/src/Context.cpp +index fec2f27..f5bdc71 100644 +--- a/src/Context.cpp ++++ b/src/Context.cpp +@@ -239,7 +239,7 @@ std::optional Context::parseExtData( + * Note: to parse a NETCONF RPC reply, you MUST parse the original NETCONF RPC request (that is, you have to use + * this method with OperationType::RpcNetconf). + */ +-ParsedOp Context::parseOp(const std::string& input, const DataFormat format, const OperationType opType) const ++ParsedOp Context::parseOp(const std::string& input, const DataFormat format, const OperationType opType, const std::optional parseOpts) const + { + auto in = wrap_ly_in_new_memory(input); + +@@ -251,7 +251,15 @@ ParsedOp Context::parseOp(const std::string& input, const DataFormat format, con + case OperationType::NotificationYang: { + lyd_node* op = nullptr; + lyd_node* tree = nullptr; +- auto err = lyd_parse_op(m_ctx.get(), nullptr, in.get(), utils::toLydFormat(format), utils::toOpType(opType), &tree, &op); ++ auto err = lyd_parse_op( ++ m_ctx.get(), ++ nullptr, ++ in.get(), ++ utils::toLydFormat(format), ++ utils::toOpType(opType), ++ parseOpts ? utils::toParseOptions(*parseOpts) : 0, ++ &tree, ++ &op); + + ParsedOp res; + res.tree = tree ? std::optional{libyang::wrapRawNode(tree)} : std::nullopt; +diff --git a/src/DataNode.cpp b/src/DataNode.cpp +index e28ef30..edabf9b 100644 +--- a/src/DataNode.cpp ++++ b/src/DataNode.cpp +@@ -391,7 +391,7 @@ DataNodeAny DataNode::asAny() const + * + * Wraps `lyd_parse_op`. + */ +-ParsedOp DataNode::parseOp(const std::string& input, const DataFormat format, const OperationType opType) const ++ParsedOp DataNode::parseOp(const std::string& input, const DataFormat format, const OperationType opType, const std::optional parseOpts) const + { + auto in = wrap_ly_in_new_memory(input); + +@@ -401,7 +401,14 @@ ParsedOp DataNode::parseOp(const std::string& input, const DataFormat format, co + case OperationType::ReplyRestconf: { + lyd_node* op = nullptr; + lyd_node* tree = nullptr; +- auto err = lyd_parse_op(m_node->schema->module->ctx, m_node, in.get(), utils::toLydFormat(format), utils::toOpType(opType), &tree, nullptr); ++ auto err = lyd_parse_op(m_node->schema->module->ctx, ++ m_node, ++ in.get(), ++ utils::toLydFormat(format), ++ utils::toOpType(opType), ++ parseOpts ? utils::toParseOptions(*parseOpts) : 0, ++ &tree, ++ nullptr); + ParsedOp res{ + .tree = tree ? std::optional{libyang::wrapRawNode(tree)} : std::nullopt, + .op = op ? std::optional{libyang::wrapRawNode(op)} : std::nullopt +diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp +index ce24203..2e0351a 100644 +--- a/src/SchemaNode.cpp ++++ b/src/SchemaNode.cpp +@@ -564,7 +564,7 @@ std::vector LeafList::defaultValuesStr() const + auto dflts = reinterpret_cast(m_node)->dflts; + std::vector res; + for (const auto& it : std::span(dflts, LY_ARRAY_COUNT(dflts))) { +- res.emplace_back(lyd_value_get_canonical(m_ctx.get(), it)); ++ res.emplace_back(it.str); + } + return res; + } +@@ -660,8 +660,8 @@ bool LeafList::isUserOrdered() const + std::optional Leaf::defaultValueStr() const + { + auto dflt = reinterpret_cast(m_node)->dflt; +- if (dflt) { +- return lyd_value_get_canonical(m_ctx.get(), dflt); ++ if (dflt.str) { ++ return std::string{dflt.str}; + } else { + return std::nullopt; + } +diff --git a/src/Type.cpp b/src/Type.cpp +index 01b2c4f..4d2db46 100644 +--- a/src/Type.cpp ++++ b/src/Type.cpp +@@ -240,7 +240,7 @@ std::optional Type::description() const + */ + std::string Type::internalPluginId() const + { +- return m_type->plugin->id; ++ return lysc_get_type_plugin(m_type->plugin_ref)->id; + } + + /** +diff --git a/src/utils/enum.hpp b/src/utils/enum.hpp +index 5e02d24..0b2e2d7 100644 +--- a/src/utils/enum.hpp ++++ b/src/utils/enum.hpp +@@ -91,6 +91,7 @@ static_assert(LYD_DUP_WITH_FLAGS == toDuplicationOptions(DuplicationOptions::Wit + static_assert(LYD_DUP_WITH_PARENTS == toDuplicationOptions(DuplicationOptions::WithParents)); + static_assert(LYD_DUP_NO_EXT == toDuplicationOptions(DuplicationOptions::NoExt)); + static_assert(LYD_DUP_WITH_PRIV == toDuplicationOptions(DuplicationOptions::WithPriv)); ++static_assert(LYD_DUP_NO_LYDS == toDuplicationOptions(DuplicationOptions::NoLyds)); + + static_assert((LYD_DUP_NO_META | LYD_DUP_NO_EXT) == + toDuplicationOptions(DuplicationOptions::NoMeta | DuplicationOptions::NoExt)); +@@ -131,6 +132,13 @@ static_assert(toContextOptions(ContextOptions::DisableSearchCwd) == LY_CTX_DISAB + static_assert(toContextOptions(ContextOptions::PreferSearchDirs) == LY_CTX_PREFER_SEARCHDIRS); + static_assert(toContextOptions(ContextOptions::SetPrivParsed) == LY_CTX_SET_PRIV_PARSED); + static_assert(toContextOptions(ContextOptions::ExplicitCompile) == LY_CTX_EXPLICIT_COMPILE); ++static_assert(toContextOptions(ContextOptions::EnableImpFeatures) == LY_CTX_ENABLE_IMP_FEATURES); ++static_assert(toContextOptions(ContextOptions::CompileObsolete) == LY_CTX_COMPILE_OBSOLETE); ++static_assert(toContextOptions(ContextOptions::LybHashes) == LY_CTX_LYB_HASHES); ++static_assert(toContextOptions(ContextOptions::LeafrefExtended) == LY_CTX_LEAFREF_EXTENDED); ++static_assert(toContextOptions(ContextOptions::LeafrefLinking) == LY_CTX_LEAFREF_LINKING); ++static_assert(toContextOptions(ContextOptions::BuiltinPluginsOnly) == LY_CTX_BUILTIN_PLUGINS_ONLY); ++static_assert(toContextOptions(ContextOptions::StaticPluginsOnly) == LY_CTX_STATIC_PLUGINS_ONLY); + + constexpr uint16_t toLogOptions(const LogOptions options) + { +@@ -198,6 +206,10 @@ constexpr uint32_t toValidationOptions(const ValidationOptions opts) + + static_assert(toValidationOptions(ValidationOptions::NoState) == LYD_VALIDATE_NO_STATE); + static_assert(toValidationOptions(ValidationOptions::Present) == LYD_VALIDATE_PRESENT); ++static_assert(toValidationOptions(ValidationOptions::MultiError) == LYD_VALIDATE_MULTI_ERROR); ++static_assert(toValidationOptions(ValidationOptions::Operational) == LYD_VALIDATE_OPERATIONAL); ++static_assert(toValidationOptions(ValidationOptions::NoDefaults) == LYD_VALIDATE_NO_DEFAULTS); ++static_assert(toValidationOptions(ValidationOptions::NotFinal) == LYD_VALIDATE_NOT_FINAL); + + constexpr uint32_t toParseOptions(const ParseOptions opts) + { +@@ -208,8 +220,14 @@ static_assert(toParseOptions(ParseOptions::ParseOnly) == LYD_PARSE_ONLY); + static_assert(toParseOptions(ParseOptions::Strict) == LYD_PARSE_STRICT); + static_assert(toParseOptions(ParseOptions::Opaque) == LYD_PARSE_OPAQ); + static_assert(toParseOptions(ParseOptions::NoState) == LYD_PARSE_NO_STATE); +-static_assert(toParseOptions(ParseOptions::LybModUpdate) == LYD_PARSE_LYB_MOD_UPDATE); ++static_assert(toParseOptions(ParseOptions::LybSkipCtxCheck) == LYD_PARSE_LYB_SKIP_CTX_CHECK); + static_assert(toParseOptions(ParseOptions::Ordered) == LYD_PARSE_ORDERED); ++static_assert(toParseOptions(ParseOptions::Subtree) == LYD_PARSE_SUBTREE); ++static_assert(toParseOptions(ParseOptions::WhenTrue) == LYD_PARSE_WHEN_TRUE); ++static_assert(toParseOptions(ParseOptions::NoNew) == LYD_PARSE_NO_NEW); ++static_assert(toParseOptions(ParseOptions::StoreOnly) == LYD_PARSE_STORE_ONLY); ++static_assert(toParseOptions(ParseOptions::JsonNull) == LYD_PARSE_JSON_NULL); ++static_assert(toParseOptions(ParseOptions::JsonStringDataTypes) == LYD_PARSE_JSON_STRING_DATATYPES); + + constexpr lyd_type toOpType(const OperationType type) + { +@@ -242,7 +260,6 @@ static_assert(toAnydataValueType(AnydataValueType::DataTree) == LYD_ANYDATA_DATA + static_assert(toAnydataValueType(AnydataValueType::String) == LYD_ANYDATA_STRING); + static_assert(toAnydataValueType(AnydataValueType::XML) == LYD_ANYDATA_XML); + static_assert(toAnydataValueType(AnydataValueType::JSON) == LYD_ANYDATA_JSON); +-static_assert(toAnydataValueType(AnydataValueType::LYB) == LYD_ANYDATA_LYB); + + constexpr LYS_OUTFORMAT toLysOutFormat(const SchemaOutputFormat format) + { +diff --git a/tests/context.cpp b/tests/context.cpp +index c0b7e09..eaedebf 100644 +--- a/tests/context.cpp ++++ b/tests/context.cpp +@@ -359,25 +359,27 @@ TEST_CASE("context") + ctx->loadModule("mod1", std::nullopt, {}); + ctx->parseModule(valid_yang_model, libyang::SchemaFormat::YANG); + auto modules = ctx->modules(); +- REQUIRE(modules.size() == 8); ++ REQUIRE(modules.size() == 9); + REQUIRE(modules.at(0).name() == "ietf-yang-metadata"); + REQUIRE(modules.at(0).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-metadata"); + REQUIRE(modules.at(1).name() == "yang"); + REQUIRE(modules.at(1).ns() == "urn:ietf:params:xml:ns:yang:1"); +- REQUIRE(modules.at(2).name() == "ietf-inet-types"); +- REQUIRE(modules.at(2).ns() == "urn:ietf:params:xml:ns:yang:ietf-inet-types"); +- REQUIRE(modules.at(3).name() == "ietf-yang-types"); +- REQUIRE(modules.at(3).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-types"); +- REQUIRE(modules.at(4).name() == "ietf-yang-schema-mount"); +- REQUIRE(modules.at(4).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-schema-mount"); +- REQUIRE(modules.at(5).name() == "ietf-yang-structure-ext"); +- REQUIRE(modules.at(5).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-structure-ext"); +- REQUIRE(modules.at(6).name() == "mod1"); +- REQUIRE(modules.at(6).ns() == "http://example.com"); +- REQUIRE(*modules.at(6).revision() == "2021-11-15"); +- REQUIRE(modules.at(7).name() == "test"); ++ REQUIRE(modules.at(2).name() == "default"); ++ REQUIRE(modules.at(2).ns() == "urn:ietf:params:xml:ns:netconf:default:1.0"); ++ REQUIRE(modules.at(3).name() == "ietf-inet-types"); ++ REQUIRE(modules.at(3).ns() == "urn:ietf:params:xml:ns:yang:ietf-inet-types"); ++ REQUIRE(modules.at(4).name() == "ietf-yang-types"); ++ REQUIRE(modules.at(4).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-types"); ++ REQUIRE(modules.at(5).name() == "ietf-yang-schema-mount"); ++ REQUIRE(modules.at(5).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-schema-mount"); ++ REQUIRE(modules.at(6).name() == "ietf-yang-structure-ext"); ++ REQUIRE(modules.at(6).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-structure-ext"); ++ REQUIRE(modules.at(7).name() == "mod1"); + REQUIRE(modules.at(7).ns() == "http://example.com"); +- REQUIRE(modules.at(7).revision() == std::nullopt); ++ REQUIRE(*modules.at(7).revision() == "2021-11-15"); ++ REQUIRE(modules.at(8).name() == "test"); ++ REQUIRE(modules.at(8).ns() == "http://example.com"); ++ REQUIRE(modules.at(8).revision() == std::nullopt); + } + + DOCTEST_SUBCASE("Module comparison") +@@ -704,7 +706,7 @@ TEST_CASE("context") + + DOCTEST_SUBCASE("schema printing") + { +- std::optional ctx_pp{std::in_place, std::nullopt, libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd | libyang::ContextOptions::SetPrivParsed}; ++ std::optional ctx_pp{std::in_place, std::nullopt, libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd | libyang::ContextOptions::SetPrivParsed | libyang::ContextOptions::CompileObsolete}; + auto mod = ctx_pp->parseModule(type_module, libyang::SchemaFormat::YANG); + + REQUIRE(mod.printStr(libyang::SchemaOutputFormat::Tree) == R"(module: type_module +diff --git a/tests/data_node.cpp b/tests/data_node.cpp +index 9215b12..88ca5b9 100644 +--- a/tests/data_node.cpp ++++ b/tests/data_node.cpp +@@ -2426,7 +2426,7 @@ TEST_CASE("Data Node manipulation") + "WTF": "foo bar baz" + } + } +- )", libyang::DataFormat::JSON, libyang::OperationType::RpcRestconf), ++ )", libyang::DataFormat::JSON, libyang::OperationType::RpcRestconf, libyang::ParseOptions::Strict), + "Can't parse into operation data tree: LY_EVALID", libyang::Error); + } + } +diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp +index 65ef462..58152a4 100644 +--- a/tests/schema_node.cpp ++++ b/tests/schema_node.cpp +@@ -19,7 +19,7 @@ using namespace std::string_literals; + TEST_CASE("SchemaNode") + { + std::optional ctx{std::in_place, std::nullopt, +- libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd}; ++ libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd | libyang::ContextOptions::CompileObsolete}; + std::optional ctxWithParsed{std::in_place, std::nullopt, + libyang::ContextOptions::SetPrivParsed | libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd}; + ctx->parseModule(example_schema, libyang::SchemaFormat::YANG); +@@ -841,7 +841,7 @@ TEST_CASE("SchemaNode") + + DOCTEST_SUBCASE("LeafList::defaultValuesStr") + { +- REQUIRE(ctx->findPath("/type_module:leafListWithDefault").asLeafList().defaultValuesStr() == std::vector{"-1", "512", "1024", "2048"}); ++ REQUIRE(ctx->findPath("/type_module:leafListWithDefault").asLeafList().defaultValuesStr() == std::vector{"-1", "+512", "0x400", "04000"}); + REQUIRE(ctx->findPath("/type_module:leafListBasic").asLeafList().defaultValuesStr().size() == 0); + } + +-- +2.43.0 + diff --git a/package/libyang-cpp/0005-Add-SchemaNode-isOutput-method.patch b/package/libyang-cpp/0005-Add-SchemaNode-isOutput-method.patch new file mode 100644 index 00000000..17b2417d --- /dev/null +++ b/package/libyang-cpp/0005-Add-SchemaNode-isOutput-method.patch @@ -0,0 +1,77 @@ +From 51d7457bd281e34bac92ff8231978766c7784657 Mon Sep 17 00:00:00 2001 +From: Martin Bohal +Date: Tue, 4 Nov 2025 14:26:05 +0100 +Subject: [PATCH 5/7] Add SchemaNode::isOutput() method +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +This commit introduces the `isOutput()` method to the `SchemaNode` +class, which checks whether the schema node is part of an output +statement subtree. + +It complements an already existing `isInput()` method. + +Change-Id: I8fe75f2e607c0159d8295d685a59d301155ca92e +Signed-off-by: Mattias Walström +--- + include/libyang-cpp/SchemaNode.hpp | 1 + + src/SchemaNode.cpp | 10 ++++++++++ + tests/schema_node.cpp | 7 +++++++ + 3 files changed, 18 insertions(+) + +diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp +index 9f49fd7..db5a63e 100644 +--- a/include/libyang-cpp/SchemaNode.hpp ++++ b/include/libyang-cpp/SchemaNode.hpp +@@ -58,6 +58,7 @@ public: + Status status() const; + Config config() const; + bool isInput() const; ++ bool isOutput() const; + NodeType nodeType() const; + // It is possible to cast SchemaNode to another type via the following methods. The types are children classes of + // SchemaNode. No problems with slicing can occur, because these types are value-based and aren't constructible +diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp +index 2e0351a..9241b23 100644 +--- a/src/SchemaNode.cpp ++++ b/src/SchemaNode.cpp +@@ -193,6 +193,16 @@ bool SchemaNode::isInput() const + return m_node->flags & LYS_INPUT; + } + ++/** ++ * @brief Checks whether this node is inside a subtree of an output statement. ++ * ++ * Wraps `LYS_OUTPUT`. ++ */ ++bool SchemaNode::isOutput() const ++{ ++ return m_node->flags & LYS_OUTPUT; ++} ++ + /** + * Returns the node type of this node (e.g. leaf, container...). + * +diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp +index 58152a4..01c2f19 100644 +--- a/tests/schema_node.cpp ++++ b/tests/schema_node.cpp +@@ -135,6 +135,13 @@ TEST_CASE("SchemaNode") + REQUIRE(!ctx->findPath("/type_module:leafString").isInput()); + } + ++ DOCTEST_SUBCASE("SchemaNode::isOutput") ++ { ++ REQUIRE(ctx->findPath("/example-schema:myRpc/outputLeaf", libyang::InputOutputNodes::Output).isOutput()); ++ REQUIRE(!ctx->findPath("/example-schema:myRpc/inputLeaf").isOutput()); ++ REQUIRE(!ctx->findPath("/type_module:leafString").isOutput()); ++ } ++ + DOCTEST_SUBCASE("SchemaNode::module") + { + REQUIRE(ctx->findPath("/type_module:leafString").module().name() == "type_module"); +-- +2.43.0 + diff --git a/package/libyang-cpp/0006-adapt-to-changes-in-libyang-v4.2.patch b/package/libyang-cpp/0006-adapt-to-changes-in-libyang-v4.2.patch new file mode 100644 index 00000000..c16768a7 --- /dev/null +++ b/package/libyang-cpp/0006-adapt-to-changes-in-libyang-v4.2.patch @@ -0,0 +1,364 @@ +From 38d5c10009df602594e1b9a53bac41111a9ba430 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Thu, 20 Nov 2025 15:32:03 +0100 +Subject: [PATCH 6/7] adapt to changes in libyang v4.2 +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +I was *so* tempted to leave the C++ enums as-is, just to save us from +this needless sed exercise, but hey, let's not start confusing any +possible downstream consumers now that upstream has changed these +identifiers. + +Change-Id: I9b34585822b41be0ca1578051f1c0eb8588a0a51 +Signed-off-by: Mattias Walström +--- + CMakeLists.txt | 4 +-- + README.md | 2 +- + include/libyang-cpp/Enum.hpp | 4 +-- + src/utils/enum.hpp | 4 +-- + tests/context.cpp | 10 +++---- + tests/data_node.cpp | 58 ++++++++++++++++++------------------ + 6 files changed, 41 insertions(+), 41 deletions(-) + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 90f9aaa..cbaf82a 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -20,7 +20,7 @@ add_custom_target(libyang-cpp-version-cmake + cmake/ProjectGitVersionRunner.cmake + ) + include(cmake/ProjectGitVersion.cmake) +-set(LIBYANG_CPP_PKG_VERSION "5") ++set(LIBYANG_CPP_PKG_VERSION "6") + prepare_git_version(LIBYANG_CPP_VERSION ${LIBYANG_CPP_PKG_VERSION}) + + find_package(Doxygen) +@@ -28,7 +28,7 @@ option(WITH_DOCS "Create and install internal documentation (needs Doxygen)" ${D + option(BUILD_SHARED_LIBS "By default, shared libs are enabled. Turn off for a static build." ON) + + find_package(PkgConfig REQUIRED) +-pkg_check_modules(LIBYANG REQUIRED libyang>=4.1.0 IMPORTED_TARGET) ++pkg_check_modules(LIBYANG REQUIRED libyang>=4.2.1 IMPORTED_TARGET) + + # FIXME from gcc 14.1 on we should be able to use the calendar/time from libstdc++ and thus remove the date dependency + find_package(date) +diff --git a/README.md b/README.md +index 0cef2ff..842573f 100644 +--- a/README.md ++++ b/README.md +@@ -8,7 +8,7 @@ + Object lifetimes are managed automatically via RAII. + + ## Dependencies +-- [libyang v4](https://github.com/CESNET/libyang) - the `devel` branch (even for the `master` branch of *libyang-cpp*) ++- [libyang v4.2+](https://github.com/CESNET/libyang) - the `devel` branch (even for the `master` branch of *libyang-cpp*) + - C++20 compiler (e.g., GCC 10.x+, clang 10+) + - CMake 3.19+ + - optionally for built-in tests, [Doctest](https://github.com/doctest/doctest/) as a C++ unit test framework +diff --git a/include/libyang-cpp/Enum.hpp b/include/libyang-cpp/Enum.hpp +index 32142f9..096e030 100644 +--- a/include/libyang-cpp/Enum.hpp ++++ b/include/libyang-cpp/Enum.hpp +@@ -67,9 +67,9 @@ enum class OperationType : uint32_t { + */ + enum class PrintFlags : uint32_t { + WithDefaultsExplicit = 0x00, +- WithSiblings = 0x01, ++ Siblings = 0x01, + Shrink = 0x02, +- KeepEmptyCont = 0x04, ++ EmptyContainers = 0x04, + WithDefaultsTrim = 0x10, + WithDefaultsAll = 0x20, + WithDefaultsAllTag = 0x40, +diff --git a/src/utils/enum.hpp b/src/utils/enum.hpp +index 0b2e2d7..a8902ff 100644 +--- a/src/utils/enum.hpp ++++ b/src/utils/enum.hpp +@@ -33,7 +33,7 @@ constexpr uint32_t toPrintFlags(const PrintFlags flags) + } + // These tests ensure that I used the right numbers when defining my enum. + // TODO: add asserts for operator|(PrintFlags, PrintFlags) +-static_assert(LYD_PRINT_KEEPEMPTYCONT == toPrintFlags(PrintFlags::KeepEmptyCont)); ++static_assert(LYD_PRINT_EMPTY_CONT == toPrintFlags(PrintFlags::EmptyContainers)); + static_assert(LYD_PRINT_SHRINK == toPrintFlags(PrintFlags::Shrink)); + static_assert(LYD_PRINT_WD_ALL == toPrintFlags(PrintFlags::WithDefaultsAll)); + static_assert(LYD_PRINT_WD_ALL_TAG == toPrintFlags(PrintFlags::WithDefaultsAllTag)); +@@ -41,7 +41,7 @@ static_assert(LYD_PRINT_WD_EXPLICIT == toPrintFlags(PrintFlags::WithDefaultsExpl + static_assert(LYD_PRINT_WD_IMPL_TAG == toPrintFlags(PrintFlags::WithDefaultsImplicitTag)); + static_assert(LYD_PRINT_WD_MASK == toPrintFlags(PrintFlags::WithDefaultsMask)); + static_assert(LYD_PRINT_WD_TRIM == toPrintFlags(PrintFlags::WithDefaultsTrim)); +-static_assert(LYD_PRINT_WITHSIBLINGS == toPrintFlags(PrintFlags::WithSiblings)); ++static_assert(LYD_PRINT_SIBLINGS == toPrintFlags(PrintFlags::Siblings)); + + #ifndef _MSC_VER + // MSVC doesn't respect the underlying enum size +diff --git a/tests/context.cpp b/tests/context.cpp +index eaedebf..355860b 100644 +--- a/tests/context.cpp ++++ b/tests/context.cpp +@@ -502,7 +502,7 @@ TEST_CASE("context") + auto errorsNode = node->findXPath("/ietf-restconf:errors"); + REQUIRE(errorsNode.size() == 1); + REQUIRE(errorsNode.begin()->path() == "/ietf-restconf:errors"); +- REQUIRE(*errorsNode.begin()->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont) == R"({ ++ REQUIRE(*errorsNode.begin()->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers) == R"({ + "ietf-restconf:errors": { + "error": [ + { +@@ -589,7 +589,7 @@ TEST_CASE("context") + + auto firstValue = edits.begin()->findPath("value"); + REQUIRE(firstValue); +- REQUIRE(*firstValue->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({ ++ REQUIRE(*firstValue->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == R"({ + "ietf-yang-patch:value": { + "example-schema:person": { + "name": "John" +@@ -597,7 +597,7 @@ TEST_CASE("context") + } + } + )"); +- REQUIRE(*firstValue->printStr(libyang::DataFormat::XML, libyang::PrintFlags::KeepEmptyCont) == R"( ++ REQUIRE(*firstValue->printStr(libyang::DataFormat::XML, libyang::PrintFlags::EmptyContainers) == R"( + + John + +@@ -607,8 +607,8 @@ TEST_CASE("context") + auto secondValueNode = (edits.begin() + 1)->findPath("value"); + REQUIRE(secondValueNode); + auto secondValue = std::get(secondValueNode->asAny().releaseValue().value()); +- REQUIRE(*secondValue.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == "{\n \"example-schema:dummy\": \"I am a dummy\"\n}\n"); +- REQUIRE(*secondValue.printStr(libyang::DataFormat::XML, libyang::PrintFlags::KeepEmptyCont) == "I am a dummy\n"); ++ REQUIRE(*secondValue.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == "{\n \"example-schema:dummy\": \"I am a dummy\"\n}\n"); ++ REQUIRE(*secondValue.printStr(libyang::DataFormat::XML, libyang::PrintFlags::EmptyContainers) == "I am a dummy\n"); + } + } + +diff --git a/tests/data_node.cpp b/tests/data_node.cpp +index 88ca5b9..3f874de 100644 +--- a/tests/data_node.cpp ++++ b/tests/data_node.cpp +@@ -137,7 +137,7 @@ TEST_CASE("Data Node manipulation") + DOCTEST_SUBCASE("Printing") + { + auto node = ctx.parseData(data, libyang::DataFormat::JSON); +- auto str = node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont); ++ auto str = node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers); + const auto expected = R"({ + "example-schema:leafInt32": 420, + "example-schema:first": { +@@ -158,7 +158,7 @@ TEST_CASE("Data Node manipulation") + REQUIRE(str == expected); + + auto emptyCont = ctx.newPath("/example-schema:first"); +- REQUIRE(emptyCont.printStr(libyang::DataFormat::XML, libyang::PrintFlags::WithSiblings) == std::nullopt); ++ REQUIRE(emptyCont.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Siblings) == std::nullopt); + } + + DOCTEST_SUBCASE("Overwriting a tree with a different tree") +@@ -504,7 +504,7 @@ TEST_CASE("Data Node manipulation") + { + auto node = std::optional{ctx.newPath("/example-schema:leafInt32", "420")}; + libyang::validateAll(node, libyang::ValidationOptions::NoState); +- auto str = node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont); ++ auto str = node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers); + REQUIRE(str == data); + } + +@@ -725,7 +725,7 @@ TEST_CASE("Data Node manipulation") + } + + // The original tree should still be accesible. +- node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings); ++ node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings); + } + } + +@@ -855,7 +855,7 @@ TEST_CASE("Data Node manipulation") + auto cont = ctx.newPath2("/example-schema2:contWithTwoNodes").createdNode; + data->unlinkWithSiblings(); + cont->insertChild(*data); +- REQUIRE(*cont->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) == R"({ ++ REQUIRE(*cont->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) == R"({ + "example-schema2:contWithTwoNodes": { + "one": 333, + "two": 666 +@@ -1501,9 +1501,9 @@ TEST_CASE("Data Node manipulation") + REQUIRE(std::holds_alternative(rawVal)); + auto retrieved = std::get(rawVal); + REQUIRE(retrieved.path() == "/key"); +- REQUIRE(*retrieved.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings) ++ REQUIRE(*retrieved.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings) + == R"|({"key":"value"})|"); +- REQUIRE(*retrieved.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings) ++ REQUIRE(*retrieved.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings) + == R"|(value)|"); + } + } +@@ -1525,9 +1525,9 @@ TEST_CASE("Data Node manipulation") + REQUIRE(std::holds_alternative(rawVal)); + auto retrieved = std::get(rawVal); + REQUIRE(retrieved.path() == "/something"); +- REQUIRE(*retrieved.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings) ++ REQUIRE(*retrieved.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings) + == R"|(lol)|"); +- REQUIRE(*retrieved.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings) ++ REQUIRE(*retrieved.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings) + == R"|({"something":"lol"})|"); + } + } +@@ -1572,9 +1572,9 @@ TEST_CASE("Data Node manipulation") + val = jsonAnyXmlNode.createdNode->asAny().releaseValue(); + } + +- REQUIRE(*jsonAnyXmlNode.createdNode->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings) ++ REQUIRE(*jsonAnyXmlNode.createdNode->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings) + == R"|({"example-schema:ax":[1,2,3]})|"s); +- REQUIRE(*jsonAnyXmlNode.createdNode->printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings) ++ REQUIRE(*jsonAnyXmlNode.createdNode->printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings) + == R"|()|"s + origJSON + ""); + } + +@@ -1603,9 +1603,9 @@ TEST_CASE("Data Node manipulation") + } + + REQUIRE(root); +- REQUIRE(*root->printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings) ++ REQUIRE(*root->printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings) + == origXML); +- REQUIRE(*root->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings) ++ REQUIRE(*root->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings) + == origJSON); + + auto node = root->findPath("/example-schema:ax"); +@@ -1665,9 +1665,9 @@ TEST_CASE("Data Node manipulation") + auto retrieved = std::get(*val); + val.reset(); + REQUIRE(retrieved.path() == "/a"); +- REQUIRE(*retrieved.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings) ++ REQUIRE(*retrieved.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings) + == origXML); +- REQUIRE(*retrieved.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings) ++ REQUIRE(*retrieved.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::Siblings) + == origJSON); + } + } +@@ -1806,7 +1806,7 @@ TEST_CASE("Data Node manipulation") + + nodeX.parseSubtree(data, libyang::DataFormat::JSON, + libyang::ParseOptions::Strict | libyang::ParseOptions::NoState | libyang::ParseOptions::ParseOnly); +- REQUIRE(*nodeX.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) == R"({ ++ REQUIRE(*nodeX.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) == R"({ + "example-schema5:x": { + "x_b": { + "x_b_leaf": 666 +@@ -1948,7 +1948,7 @@ TEST_CASE("Data Node manipulation") + { + netconfDeletePresenceCont.newMeta(netconf, "operation", "delete"); + netconfDeletePresenceCont.newMeta(ietfOrigin, "origin", "ietf-origin:default"); +- REQUIRE(*netconfDeletePresenceCont.printStr(libyang::DataFormat::XML, libyang::PrintFlags::WithSiblings) ++ REQUIRE(*netconfDeletePresenceCont.printStr(libyang::DataFormat::XML, libyang::PrintFlags::Siblings) + == R"()" "\n"); + } + +@@ -1957,7 +1957,7 @@ TEST_CASE("Data Node manipulation") + auto opaqueLeaf = ctx.newPath("/example-schema:leafInt32", std::nullopt, libyang::CreationOptions::Opaque); + REQUIRE_THROWS(opaqueLeaf.newMeta(netconf, "operation", "delete")); + opaqueLeaf.newAttrOpaqueJSON("ietf-netconf", "operation", "delete"); +- REQUIRE(*opaqueLeaf.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) ++ REQUIRE(*opaqueLeaf.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) + == R"({ + "example-schema:leafInt32": "", + "@example-schema:leafInt32": { +@@ -1976,7 +1976,7 @@ TEST_CASE("Data Node manipulation") + auto discard2 = ctx.newOpaqueJSON(libyang::OpaqueName{"sysrepo", "sysrepo", "discard-items"}, libyang::JSON{"/example-schema:b"}); + REQUIRE(!!discard2); + discard1->insertSibling(*discard2); +- REQUIRE(*discard1->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) ++ REQUIRE(*discard1->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) + == R"({ + "sysrepo:discard-items": "/example-schema:a", + "sysrepo:discard-items": "/example-schema:b" +@@ -1991,7 +1991,7 @@ TEST_CASE("Data Node manipulation") + + auto leafInt16 = ctx.newPath("/example-schema:leafInt16", "666"); + leafInt16.insertSibling(*discard1); +- REQUIRE(*discard1->firstSibling().printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) ++ REQUIRE(*discard1->firstSibling().printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) + == R"({ + "example-schema:leafInt16": 666, + "sysrepo:discard-items": "/example-schema:a", +@@ -2020,7 +2020,7 @@ TEST_CASE("Data Node manipulation") + + dummy.insertSibling(*discard3); + leafInt16.insertSibling(dummy); +- REQUIRE(*discard1->firstSibling().printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) ++ REQUIRE(*discard1->firstSibling().printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) + == R"({ + "example-schema:dummy": "blah", + "example-schema:leafInt16": 666, +@@ -2112,8 +2112,8 @@ TEST_CASE("Data Node manipulation") + data->unlinkWithSiblings(); + out->insertChild(*data); + +- REQUIRE(*out->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) == expectedJson); +- REQUIRE(*out->printStr(libyang::DataFormat::XML, libyang::PrintFlags::WithSiblings) == expectedXml); ++ REQUIRE(*out->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) == expectedJson); ++ REQUIRE(*out->printStr(libyang::DataFormat::XML, libyang::PrintFlags::Siblings) == expectedXml); + } + + DOCTEST_SUBCASE("libyang internal metadata") +@@ -2148,7 +2148,7 @@ TEST_CASE("Data Node manipulation") + auto node = ctx.newExtPath(ext, "/ietf-restconf:errors", std::nullopt, std::nullopt); + REQUIRE(node); + REQUIRE(node->schema().name() == "errors"); +- REQUIRE(*node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont) == R"({ ++ REQUIRE(*node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers) == R"({ + "ietf-restconf:errors": {} + } + )"); +@@ -2157,7 +2157,7 @@ TEST_CASE("Data Node manipulation") + REQUIRE(node->newPath("ietf-restconf:error[1]/error-tag", "invalid-attribute")); + REQUIRE(node->newExtPath(ext, "/ietf-restconf:errors/error[1]/error-message", "ahoj")); + REQUIRE_THROWS_WITH(node->newPath("ietf-restconf:error[1]/error-message", "duplicate create"), "Couldn't create a node with path 'ietf-restconf:error[1]/error-message': LY_EEXIST"); +- REQUIRE(*node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont) == R"({ ++ REQUIRE(*node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers) == R"({ + "ietf-restconf:errors": { + "error": [ + { +@@ -2173,7 +2173,7 @@ TEST_CASE("Data Node manipulation") + REQUIRE(node->newExtPath(ext, "/ietf-restconf:errors/error[2]/error-type", "transport")); + REQUIRE(node->newExtPath(ext, "/ietf-restconf:errors/error[2]/error-tag", "invalid-attribute")); + REQUIRE(node->newPath("ietf-restconf:error[2]/error-message", "aaa")); +- REQUIRE(*node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont) == R"({ ++ REQUIRE(*node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers) == R"({ + "ietf-restconf:errors": { + "error": [ + { +@@ -2397,8 +2397,8 @@ TEST_CASE("Data Node manipulation") + REQUIRE(response.tree->path() == "/example-schema:output"); + REQUIRE(response.tree->isOpaque()); + REQUIRE(!response.tree->child()); // nothing gets "parsed" here, the result is put into the tree that parseOp() operated on (!) +- CAPTURE(*response.tree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont)); +- CAPTURE(*replyTree.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont)); ++ CAPTURE(*response.tree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers)); ++ CAPTURE(*replyTree.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers)); + + node = replyTree.findPath("/example-schema:myRpc/outputLeaf", libyang::InputOutputNodes::Output); + REQUIRE(!!node); +@@ -2472,7 +2472,7 @@ TEST_CASE("Data Node manipulation") + REQUIRE(rpcOp.tree); + + libyang::validateOp(*rpcTree, depTree, libyang::OperationType::RpcRestconf); +- REQUIRE(*rpcTree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == expected); ++ REQUIRE(*rpcTree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == expected); + } + + DOCTEST_SUBCASE("Nodes in disjunctive cases defined together") +-- +2.43.0 + diff --git a/package/libyang-cpp/0007-don-t-segfault-when-listing-module-s-features.patch b/package/libyang-cpp/0007-don-t-segfault-when-listing-module-s-features.patch new file mode 100644 index 00000000..a05ee827 --- /dev/null +++ b/package/libyang-cpp/0007-don-t-segfault-when-listing-module-s-features.patch @@ -0,0 +1,112 @@ +From af187d1bc27a7390b358d7ad3b9f8f44cabe41ad Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Thu, 20 Nov 2025 12:46:49 +0100 +Subject: [PATCH 7/7] don't segfault when listing module's features +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +This is especially relevant when working with the "printed context" from +sysrepo, because that context has all the parsed info removed. As a +result, it is not possible to query a module's list of features. Let's +stop dereferencing a null pointer in that case. + +Bug: https://github.com/sysrepo/sysrepo/issues/3695 +Change-Id: I6258f580b4e5aa02cae2d60260b84593aefcf587 +Signed-off-by: Mattias Walström +--- + CMakeLists.txt | 1 + + include/libyang-cpp/Utils.hpp | 1 + + src/Module.cpp | 3 +++ + src/utils/exception.cpp | 5 +++++ + tests/context.cpp | 12 ++++++++++++ + 5 files changed, 22 insertions(+) + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index cbaf82a..b9da66b 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -118,6 +118,7 @@ if(BUILD_TESTING) + endfunction() + + libyang_cpp_test(context) ++ target_link_libraries(test_context PkgConfig::LIBYANG) + libyang_cpp_test(data_node) + target_link_libraries(test_data_node PkgConfig::LIBYANG) + libyang_cpp_test(schema_node) +diff --git a/include/libyang-cpp/Utils.hpp b/include/libyang-cpp/Utils.hpp +index 934714d..428b265 100644 +--- a/include/libyang-cpp/Utils.hpp ++++ b/include/libyang-cpp/Utils.hpp +@@ -35,6 +35,7 @@ public: + class LIBYANG_CPP_EXPORT ParsedInfoUnavailable : public Error { + public: + explicit ParsedInfoUnavailable(); ++ explicit ParsedInfoUnavailable(const std::string& what); + }; + + /** +diff --git a/src/Module.cpp b/src/Module.cpp +index d6d4023..8eb8fb4 100644 +--- a/src/Module.cpp ++++ b/src/Module.cpp +@@ -155,6 +155,9 @@ void Module::setImplemented(const AllFeatures) + */ + std::vector Module::features() const + { ++ if (!m_module->parsed) { ++ throw ParsedInfoUnavailable{"Module::features: lys_module::parsed is not available"}; ++ } + std::vector res; + for (const auto& feature : std::span(m_module->parsed->features, LY_ARRAY_COUNT(m_module->parsed->features))) { + res.emplace_back(Feature{&feature, m_ctx}); +diff --git a/src/utils/exception.cpp b/src/utils/exception.cpp +index ecd2b85..a8d908b 100644 +--- a/src/utils/exception.cpp ++++ b/src/utils/exception.cpp +@@ -22,6 +22,11 @@ ParsedInfoUnavailable::ParsedInfoUnavailable() + { + } + ++ParsedInfoUnavailable::ParsedInfoUnavailable(const std::string& what) ++ : Error(what) ++{ ++} ++ + ErrorCode ErrorWithCode::code() const + { + return m_errCode; +diff --git a/tests/context.cpp b/tests/context.cpp +index 355860b..5fce0f4 100644 +--- a/tests/context.cpp ++++ b/tests/context.cpp +@@ -10,6 +10,7 @@ + #include + #include + #include ++#include + #include "example_schema.hpp" + #include "pretty_printers.hpp" + #include "test_vars.hpp" +@@ -333,6 +334,17 @@ TEST_CASE("context") + REQUIRE(enabledFeatures == expectedEnabledFeatures); + } + ++ DOCTEST_SUBCASE("printed context") ++ { ++ ctx->setSearchDir(TESTS_DIR / "yang"); ++ auto mod = ctx->loadModule("mod1", std::nullopt, {"*"}); ++ REQUIRE(mod.features().size() == 3); ++ ly_ctx_free_parsed(retrieveContext(*ctx)); ++ REQUIRE_THROWS_WITH_AS(mod.features(), ++ "Module::features: lys_module::parsed is not available", ++ libyang::ParsedInfoUnavailable); ++ } ++ + DOCTEST_SUBCASE("Module::setImplemented") + { + ctx->setSearchDir(TESTS_DIR / "yang"); +-- +2.43.0 + diff --git a/package/rousette/0001-Log-HTTP-headers-and-the-input-data-payload.patch b/package/rousette/0001-Log-HTTP-headers-and-the-input-data-payload.patch index e4a1f7dd..42e9504b 100644 --- a/package/rousette/0001-Log-HTTP-headers-and-the-input-data-payload.patch +++ b/package/rousette/0001-Log-HTTP-headers-and-the-input-data-payload.patch @@ -1,7 +1,7 @@ -From c0d13b8e426608a30b1eea382f908fb6447fd01d Mon Sep 17 00:00:00 2001 +From a4136d889237dadb9253ea7eb668a525dd779e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Thu, 12 Jun 2025 10:33:42 +0100 -Subject: [PATCH 01/13] Log HTTP headers and the input data payload +Subject: [PATCH 01/38] Log HTTP headers and the input data payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -11,7 +11,7 @@ Organization: Wires Hažlinský just found. Change-Id: I2a930a02c7d30c051390fe73e6af9849edd580b4 -Signed-off-by: Joachim Wiberg +Signed-off-by: Mattias Walström --- src/restconf/Server.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/package/rousette/0002-restconf-prevent-throwing-exception-in-withRestconfE.patch b/package/rousette/0002-restconf-prevent-throwing-exception-in-withRestconfE.patch index e11b6366..4aa9089c 100644 --- a/package/rousette/0002-restconf-prevent-throwing-exception-in-withRestconfE.patch +++ b/package/rousette/0002-restconf-prevent-throwing-exception-in-withRestconfE.patch @@ -1,8 +1,11 @@ -From 027a47ac3ff6c369caab7c007a5a171a68270829 Mon Sep 17 00:00:00 2001 +From 6c5b482ea5c9fbc1149a0864b05d1bb1fa7100bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Fri, 13 Jun 2025 10:47:55 +0200 -Subject: [PATCH 02/13] restconf: prevent throwing exception in +Subject: [PATCH 02/38] restconf: prevent throwing exception in withRestconfExceptions wrapper +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit Organization: Wires In case the catch block of withRestconfExceptions is reached and an @@ -19,7 +22,7 @@ handle such situations. Bug: https://github.com/CESNET/rousette/issues/19 Change-Id: Ifbd74b9bdc0ca66c4e5449a7673ef2f12ae9215e -Signed-off-by: Joachim Wiberg +Signed-off-by: Mattias Walström --- src/restconf/Server.cpp | 2 +- tests/restconf-plain-patch.cpp | 24 ++++++++++++++++++++++++ diff --git a/package/rousette/0003-CI-switch-to-the-new-cloud-s-Swift-URL.patch b/package/rousette/0003-CI-switch-to-the-new-cloud-s-Swift-URL.patch new file mode 100644 index 00000000..272c73c0 --- /dev/null +++ b/package/rousette/0003-CI-switch-to-the-new-cloud-s-Swift-URL.patch @@ -0,0 +1,31 @@ +From 41c9d9cab47a88ee6c70ab8009b789226c0982fe Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Tue, 17 Jun 2025 12:46:27 +0200 +Subject: [PATCH 03/38] CI: switch to the new cloud's Swift URL +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Change-Id: I69f8351394262a2a9b691422592741bfb40a8e38 +Signed-off-by: Mattias Walström +--- + ci/build.sh | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/ci/build.sh b/ci/build.sh +index d49bd56..59b31ab 100755 +--- a/ci/build.sh ++++ b/ci/build.sh +@@ -67,7 +67,7 @@ if [[ -z "${ARTIFACT_URL}" ]]; then + # nothing ahead in the pipeline -> fallback to the latest promoted artifact + DEPSRCDIR=$(jq < ~/zuul-env.json -e -r ".projects[] | select(.name == \"CzechLight/dependencies\").src_dir") + DEP_SUBMODULE_COMMIT=$(git --git-dir ${HOME}/${DEPSRCDIR}/.git rev-parse HEAD) +- ARTIFACT_URL="https://object-store.cloud.muni.cz/swift/v1/ci-artifacts-${ZUUL_TENANT}/${ZUUL_GERRIT_HOSTNAME}/CzechLight/dependencies/deps-${ZUUL_JOB_NAME_NO_PROJECT%%-cover?(-previous)}/${DEP_SUBMODULE_COMMIT}.tar.zst" ++ ARTIFACT_URL="https://object-store.brno.openstack.cloud.e-infra.cz/swift/v1/KEY_b637b9c937414b29b3e277b4a85cc658/ci-artifacts-${ZUUL_TENANT}/${ZUUL_GERRIT_HOSTNAME}/CzechLight/dependencies/deps-${ZUUL_JOB_NAME_NO_PROJECT%%-cover?(-previous)}/${DEP_SUBMODULE_COMMIT}.tar.zst" + fi + + curl ${ARTIFACT_URL} | unzstd --stdout | tar -C ${PREFIX} -xf - +-- +2.43.0 + diff --git a/package/rousette/0003-restconf-crash-instead-of-a-deadlock-when-the-handle.patch b/package/rousette/0004-restconf-crash-instead-of-a-deadlock-when-the-handle.patch similarity index 97% rename from package/rousette/0003-restconf-crash-instead-of-a-deadlock-when-the-handle.patch rename to package/rousette/0004-restconf-crash-instead-of-a-deadlock-when-the-handle.patch index bb0bb2c0..6086d311 100644 --- a/package/rousette/0003-restconf-crash-instead-of-a-deadlock-when-the-handle.patch +++ b/package/rousette/0004-restconf-crash-instead-of-a-deadlock-when-the-handle.patch @@ -1,7 +1,7 @@ -From d1c56f14b54b113df8fba759924bb47cfa27366c Mon Sep 17 00:00:00 2001 +From ec8673126929b6459fcd99c84a79993a725b40e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Fri, 13 Jun 2025 10:47:55 +0200 -Subject: [PATCH 03/13] restconf: crash instead of a deadlock when the handler +Subject: [PATCH 04/38] restconf: crash instead of a deadlock when the handler throws MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -37,7 +37,7 @@ called twice [3]. Signed-off-by: Jan Kundrát Bug: https://github.com/CESNET/rousette/issues/19 Change-Id: I2c090b9a76b062101ba422a7d50e8e699779e203 -Signed-off-by: Joachim Wiberg +Signed-off-by: Mattias Walström --- src/restconf/Server.cpp | 28 ++++++++++++++++++++++++++++ src/restconf/Server.h | 4 ++++ diff --git a/package/rousette/0005-doc-let-s-stop-calling-this-an-almost-RESTCONF-serve.patch b/package/rousette/0005-doc-let-s-stop-calling-this-an-almost-RESTCONF-serve.patch new file mode 100644 index 00000000..301a57ec --- /dev/null +++ b/package/rousette/0005-doc-let-s-stop-calling-this-an-almost-RESTCONF-serve.patch @@ -0,0 +1,29 @@ +From 37ca95c387d76c3f296a4e44b211772a1ca155ab Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Wed, 18 Jun 2025 12:01:04 +0200 +Subject: [PATCH 05/38] doc: let's stop calling this "an almost-RESTCONF + server" +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Change-Id: If55ace481c78d838a811ded76a564f8fb59f9233 +Signed-off-by: Mattias Walström +--- + README.md | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/README.md b/README.md +index 0796dce..a42c64d 100644 +--- a/README.md ++++ b/README.md +@@ -1,4 +1,4 @@ +-# An almost-RESTCONF server ++# A RESTCONF server for sysrepo + + ![License](https://img.shields.io/github/license/cesnet/rousette) + [![Gerrit](https://img.shields.io/badge/patches-via%20Gerrit-blue)](https://gerrit.cesnet.cz/q/project:CzechLight/rousette) +-- +2.43.0 + diff --git a/package/rousette/0004-close-long-lived-connections-on-SIGTERM.patch b/package/rousette/0006-close-long-lived-connections-on-SIGTERM.patch similarity index 98% rename from package/rousette/0004-close-long-lived-connections-on-SIGTERM.patch rename to package/rousette/0006-close-long-lived-connections-on-SIGTERM.patch index 6bff80ac..37ffabac 100644 --- a/package/rousette/0004-close-long-lived-connections-on-SIGTERM.patch +++ b/package/rousette/0006-close-long-lived-connections-on-SIGTERM.patch @@ -1,7 +1,10 @@ -From db64d7b3fc0a0373067e6c5ac9e41b93351778a1 Mon Sep 17 00:00:00 2001 +From 4eae6200aa812950ebbac1660a1899f4edf41e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Wed, 7 Aug 2024 19:07:35 +0200 -Subject: [PATCH 04/13] close long-lived connections on SIGTERM +Subject: [PATCH 06/38] close long-lived connections on SIGTERM +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit Organization: Wires Without this patch, all the ongoing SSE streams would be left alive for @@ -20,7 +23,7 @@ hope it is). Change-Id: If442134783ba1d699de47c51a9068378f53e8339 Co-authored-by: Tomas Pecka -Signed-off-by: Joachim Wiberg +Signed-off-by: Mattias Walström --- CMakeLists.txt | 1 + src/clock.cpp | 7 +-- diff --git a/package/rousette/0005-restconf-refactor-server-stop.patch b/package/rousette/0007-restconf-refactor-server-stop.patch similarity index 87% rename from package/rousette/0005-restconf-refactor-server-stop.patch rename to package/rousette/0007-restconf-refactor-server-stop.patch index 1830546f..065c2c21 100644 --- a/package/rousette/0005-restconf-refactor-server-stop.patch +++ b/package/rousette/0007-restconf-refactor-server-stop.patch @@ -1,7 +1,10 @@ -From 5c8a139d04d9b3dfb28b8c0e1a4aa7b945a1e8fc Mon Sep 17 00:00:00 2001 +From ca2894d4888c673d227fc196a25f83ded20e8f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Mon, 30 Jun 2025 15:38:02 +0200 -Subject: [PATCH 05/13] restconf: refactor server stop +Subject: [PATCH 07/38] restconf: refactor server stop +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit Organization: Wires In 94b24795 ("server: simplify listening/stopping") we needed to call @@ -17,7 +20,7 @@ does not seem like it should be called from *every* io_service.) [2] https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/reference/post.html Change-Id: I2f33c38a78dce4081a03326c9a9bb25817fc9d2f -Signed-off-by: Joachim Wiberg +Signed-off-by: Mattias Walström --- src/restconf/Server.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/package/rousette/0008-tests-use-std-string-starts_with.patch b/package/rousette/0008-tests-use-std-string-starts_with.patch new file mode 100644 index 00000000..a397d617 --- /dev/null +++ b/package/rousette/0008-tests-use-std-string-starts_with.patch @@ -0,0 +1,33 @@ +From 9961430bacdd8dbac64a01b2a2ffb6a4b7e806b6 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Tue, 13 May 2025 13:48:35 +0200 +Subject: [PATCH 08/38] tests: use std::string::starts_with +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +We are C++20, so we can use line, which is more readable. + +Change-Id: I40d4038b421f6bc1fcf320f609b50d5ce7018a45 +Signed-off-by: Mattias Walström +--- + tests/restconf_utils.cpp | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp +index cbfce2c..08b9623 100644 +--- a/tests/restconf_utils.cpp ++++ b/tests/restconf_utils.cpp +@@ -228,7 +228,7 @@ std::vector SSEClient::parseEvents(const std::string& msg) + std::string event; + + while (std::getline(iss, line)) { +- if (line.compare(0, prefix.size(), prefix) == 0) { ++ if (line.starts_with(prefix)) { + event += line.substr(prefix.size()); + } else if (line.empty()) { + res.emplace_back(std::move(event)); +-- +2.43.0 + diff --git a/package/rousette/0009-tests-add-SSE-event-watcher-for-comments.patch b/package/rousette/0009-tests-add-SSE-event-watcher-for-comments.patch new file mode 100644 index 00000000..5558b313 --- /dev/null +++ b/package/rousette/0009-tests-add-SSE-event-watcher-for-comments.patch @@ -0,0 +1,172 @@ +From 746c0cdfef6808f393be7946630b8acbb0636706 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Tue, 22 Jul 2025 17:47:50 +0200 +Subject: [PATCH 09/38] tests: add SSE event watcher for comments +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +I want to test whether client receives even the Server Sent Events +which should be ignored, i.e., lines starting with colon. + +Change-Id: If54f0af05b4884aab01325f12fd0a6859791b41b +Signed-off-by: Mattias Walström +--- + tests/event_watchers.cpp | 7 ++++++- + tests/event_watchers.h | 4 +++- + tests/restconf_utils.cpp | 36 ++++++++++++++++++++---------------- + tests/restconf_utils.h | 12 +++++++++--- + 4 files changed, 38 insertions(+), 21 deletions(-) + +diff --git a/tests/event_watchers.cpp b/tests/event_watchers.cpp +index 690338e..26ba951 100644 +--- a/tests/event_watchers.cpp ++++ b/tests/event_watchers.cpp +@@ -64,7 +64,7 @@ void RestconfNotificationWatcher::setDataFormat(const libyang::DataFormat dataFo + this->dataFormat = dataFormat; + } + +-void RestconfNotificationWatcher::operator()(const std::string& msg) const ++void RestconfNotificationWatcher::dataEvent(const std::string& msg) const + { + spdlog::trace("Client received data: {}", msg); + auto notifDataNode = ctx.parseOp(msg, +@@ -79,3 +79,8 @@ void RestconfNotificationWatcher::operator()(const std::string& msg) const + + data(*dataRoot->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink)); + } ++ ++void RestconfNotificationWatcher::commentEvent(const std::string& msg) const ++{ ++ comment(msg); ++} +diff --git a/tests/event_watchers.h b/tests/event_watchers.h +index 3b533ba..7022b64 100644 +--- a/tests/event_watchers.h ++++ b/tests/event_watchers.h +@@ -39,8 +39,10 @@ struct RestconfNotificationWatcher { + + RestconfNotificationWatcher(const libyang::Context& ctx); + void setDataFormat(const libyang::DataFormat dataFormat); +- void operator()(const std::string& msg) const; ++ void dataEvent(const std::string& msg) const; ++ void commentEvent(const std::string& msg) const; + ++ MAKE_CONST_MOCK1(comment, void(const std::string&)); + MAKE_CONST_MOCK1(data, void(const std::string&)); + }; + +diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp +index 08b9623..5b1ebe3 100644 +--- a/tests/restconf_utils.cpp ++++ b/tests/restconf_utils.cpp +@@ -169,10 +169,11 @@ SSEClient::SSEClient( + const std::string& server_address, + const std::string& server_port, + std::binary_semaphore& requestSent, +- const RestconfNotificationWatcher& notification, ++ const RestconfNotificationWatcher& eventWatcher, + const std::string& uri, + const std::map& headers, +- const boost::posix_time::seconds silenceTimeout) ++ const boost::posix_time::seconds silenceTimeout, ++ const ReportIgnoredLines reportIgnoredLines) + : client(std::make_shared(io, server_address, server_port)) + , t(io, silenceTimeout) + { +@@ -191,17 +192,15 @@ SSEClient::SSEClient( + } + }); + +- client->on_connect([&, uri, reqHeaders, silenceTimeout, server_address, server_port](auto) { ++ client->on_connect([&, uri, reqHeaders, silenceTimeout, server_address, server_port, reportIgnoredLines](auto) { + boost::system::error_code ec; + + auto req = client->submit(ec, "GET", serverAddressAndPort(server_address, server_port) + uri, "", reqHeaders); +- req->on_response([&, silenceTimeout](const ng_client::response& res) { ++ req->on_response([&, silenceTimeout, reportIgnoredLines](const ng_client::response& res) { + requestSent.release(); +- res.on_data([&, silenceTimeout](const uint8_t* data, std::size_t len) { ++ res.on_data([&, silenceTimeout, reportIgnoredLines](const uint8_t* data, std::size_t len) { + // not a production-ready code. In real-life condition the data received in one callback might probably be incomplete +- for (const auto& event : parseEvents(std::string(reinterpret_cast(data), len))) { +- notification(event); +- } ++ parseEvents(std::string(reinterpret_cast(data), len), eventWatcher, reportIgnoredLines); + t.expires_from_now(silenceTimeout); + }); + }); +@@ -218,25 +217,30 @@ SSEClient::SSEClient( + }); + } + +-std::vector SSEClient::parseEvents(const std::string& msg) ++void SSEClient::parseEvents(const std::string& msg, const RestconfNotificationWatcher& eventWatcher, const ReportIgnoredLines reportIgnoredLines) + { +- static const std::string prefix = "data:"; ++ static const std::string dataPrefix = "data:"; ++ static const std::string ignorePrefix = ":"; + +- std::vector res; + std::istringstream iss(msg); + std::string line; + std::string event; + + while (std::getline(iss, line)) { +- if (line.starts_with(prefix)) { +- event += line.substr(prefix.size()); +- } else if (line.empty()) { +- res.emplace_back(std::move(event)); ++ if (line.starts_with(ignorePrefix) && reportIgnoredLines == ReportIgnoredLines::Yes) { ++ eventWatcher.commentEvent(line); ++ } else if (line.starts_with(ignorePrefix)) { ++ continue; ++ } else if (line.starts_with(dataPrefix)) { ++ event += line.substr(dataPrefix.size()); ++ } else if (line.empty() && !event.empty()) { ++ eventWatcher.dataEvent(event); + event.clear(); ++ } else if (line.empty()) { ++ continue; + } else { + CAPTURE(msg); + FAIL("Unprefixed response"); + } + } +- return res; + } +diff --git a/tests/restconf_utils.h b/tests/restconf_utils.h +index 9efe398..9dde10b 100644 +--- a/tests/restconf_utils.h ++++ b/tests/restconf_utils.h +@@ -88,17 +88,23 @@ struct SSEClient { + std::shared_ptr client; + boost::asio::deadline_timer t; + ++ enum class ReportIgnoredLines { ++ No, ++ Yes, ++ }; ++ + SSEClient( + boost::asio::io_service& io, + const std::string& server_address, + const std::string& server_port, + std::binary_semaphore& requestSent, +- const RestconfNotificationWatcher& notification, ++ const RestconfNotificationWatcher& eventWatcher, + const std::string& uri, + const std::map& headers, +- const boost::posix_time::seconds silenceTimeout = boost::posix_time::seconds(1)); // test code; the server should respond "soon" ++ const boost::posix_time::seconds silenceTimeout = boost::posix_time::seconds(1), // test code; the server should respond "soon" ++ const ReportIgnoredLines reportIgnoredLines = ReportIgnoredLines::No); + +- static std::vector parseEvents(const std::string& msg); ++ static void parseEvents(const std::string& msg, const RestconfNotificationWatcher& eventWatcher, const ReportIgnoredLines reportIgnoredLines); + }; + + #define PREPARE_LOOP_WITH_EXCEPTIONS \ +-- +2.43.0 + diff --git a/package/rousette/0006-http-send-keep-alive-pings-from-EventStream.patch b/package/rousette/0010-http-send-keep-alive-pings-from-EventStream.patch similarity index 98% rename from package/rousette/0006-http-send-keep-alive-pings-from-EventStream.patch rename to package/rousette/0010-http-send-keep-alive-pings-from-EventStream.patch index d26a23a3..696b1d1d 100644 --- a/package/rousette/0006-http-send-keep-alive-pings-from-EventStream.patch +++ b/package/rousette/0010-http-send-keep-alive-pings-from-EventStream.patch @@ -1,7 +1,10 @@ -From 086cfe1847e406df90852dc3cb52ece3d0cbe22b Mon Sep 17 00:00:00 2001 +From 5becffe8a1dd47c8836ce1800a1b72acdf86021f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Mon, 12 May 2025 14:56:54 +0200 -Subject: [PATCH 06/13] http: send keep-alive pings from EventStream +Subject: [PATCH 10/38] http: send keep-alive pings from EventStream +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit Organization: Wires The nghttp2-asio server disconnects client after 60 seconds of no @@ -28,7 +31,7 @@ to the server when the server sends these "keep-alive comments". [3] https://html.spec.whatwg.org/multipage/server-sent-events.html#authoring-notes Change-Id: I57e510d0b61ac7ed032c582779780c64768b7d53 -Signed-off-by: Joachim Wiberg +Signed-off-by: Mattias Walström --- src/clock.cpp | 4 +- src/http/EventStream.cpp | 44 +++++++++++++-- diff --git a/package/rousette/0007-refactor-event-streams-use-named-constructors.patch b/package/rousette/0011-refactor-event-streams-use-named-constructors.patch similarity index 97% rename from package/rousette/0007-refactor-event-streams-use-named-constructors.patch rename to package/rousette/0011-refactor-event-streams-use-named-constructors.patch index abe346d5..35d5fe1a 100644 --- a/package/rousette/0007-refactor-event-streams-use-named-constructors.patch +++ b/package/rousette/0011-refactor-event-streams-use-named-constructors.patch @@ -1,7 +1,10 @@ -From 06e2751e4da052195db96aaa90549fba398208bd Mon Sep 17 00:00:00 2001 +From ff4ff1c193083feca76d9f0f4485e4b175c373c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Wed, 23 Jul 2025 14:27:26 +0200 -Subject: [PATCH 07/13] refactor: event streams use named constructors +Subject: [PATCH 11/38] refactor: event streams use named constructors +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit Organization: Wires This commit refactors the EventStream and NotificationStream to use @@ -14,7 +17,7 @@ named constructors which perform the double initialization themselves. This way, the code is more readable and the intention should be clearer. Change-Id: Iac96c49c20670dfe924d7c8db33328ed9c2fc9dd -Signed-off-by: Joachim Wiberg +Signed-off-by: Mattias Walström --- src/clock.cpp | 3 +-- src/http/EventStream.cpp | 20 ++++++++++++++++++++ diff --git a/package/rousette/0008-refactor-a-better-convention-for-weak_from_this-lock.patch b/package/rousette/0012-refactor-a-better-convention-for-weak_from_this-lock.patch similarity index 93% rename from package/rousette/0008-refactor-a-better-convention-for-weak_from_this-lock.patch rename to package/rousette/0012-refactor-a-better-convention-for-weak_from_this-lock.patch index d02e9056..9c5b51ee 100644 --- a/package/rousette/0008-refactor-a-better-convention-for-weak_from_this-lock.patch +++ b/package/rousette/0012-refactor-a-better-convention-for-weak_from_this-lock.patch @@ -1,13 +1,16 @@ -From e98e079d0fdedf48facbfcf187950946c1ffed7a Mon Sep 17 00:00:00 2001 +From f4602a03adc9134a9b7a9d338e900b40557da6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Thu, 7 Aug 2025 12:14:02 +0200 -Subject: [PATCH 08/13] refactor: a better convention for weak_from_this->lock +Subject: [PATCH 12/38] refactor: a better convention for weak_from_this->lock +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit Organization: Wires It is not a "client", so let's stop calling it a "client". My bad. Change-Id: Id8dc4d92c3ade8d86697366d0102e84bd466f504 -Signed-off-by: Joachim Wiberg +Signed-off-by: Mattias Walström --- src/http/EventStream.cpp | 44 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/package/rousette/0013-Add-log-level-LEVEL-command-line-option.patch b/package/rousette/0013-Add-log-level-LEVEL-command-line-option.patch deleted file mode 100644 index b2c24d5e..00000000 --- a/package/rousette/0013-Add-log-level-LEVEL-command-line-option.patch +++ /dev/null @@ -1,78 +0,0 @@ -From ba43271df7cc83806a08d6937719544786fd331d Mon Sep 17 00:00:00 2001 -From: Joachim Wiberg -Date: Sun, 28 Sep 2025 06:04:12 +0200 -Subject: [PATCH 13/13] Add '--log-level LEVEL' command line option -Organization: Wires - -The default 'trace' log level is quite verbose for production systems. -This commit changes the default to 'info' and adds a command line option -to control the log level. - -Signed-off-by: Joachim Wiberg ---- - src/restconf/main.cpp | 27 +++++++++++++++++++++++++-- - 1 file changed, 25 insertions(+), 2 deletions(-) - -diff --git a/src/restconf/main.cpp b/src/restconf/main.cpp -index 477e846..f253421 100644 ---- a/src/restconf/main.cpp -+++ b/src/restconf/main.cpp -@@ -26,11 +26,12 @@ - static const char usage[] = - R"(Rousette - RESTCONF server - Usage: -- rousette [--syslog] [--timeout ] [--help] -+ rousette [--syslog] [--timeout ] [--log-level ] [--help] - Options: - -h --help Show this screen. - -t --timeout Change default timeout in sysrepo (if not set, use sysrepo internal). - --syslog Log to syslog. -+ --log-level Set log level (trace, debug, info, warn, error, critical, off) [default: info]. - )"; - #ifdef HAVE_SYSTEMD - -@@ -75,10 +76,32 @@ int main(int argc, char* argv []) - { - auto args = docopt::docopt(usage, {argv + 1, argv + argc}, true,""/* version */, true); - auto timeout = std::chrono::milliseconds{0}; -+ auto logLevel = spdlog::level::info; - - if (args["--timeout"]) { - timeout = std::chrono::milliseconds{args["--timeout"].asLong() * 1000}; - } -+ -+ if (args["--log-level"]) { -+ auto levelStr = args["--log-level"].asString(); -+ if (levelStr == "trace") { -+ logLevel = spdlog::level::trace; -+ } else if (levelStr == "debug") { -+ logLevel = spdlog::level::debug; -+ } else if (levelStr == "info") { -+ logLevel = spdlog::level::info; -+ } else if (levelStr == "warn" || levelStr == "warning") { -+ logLevel = spdlog::level::warn; -+ } else if (levelStr == "error" || levelStr == "err") { -+ logLevel = spdlog::level::err; -+ } else if (levelStr == "critical") { -+ logLevel = spdlog::level::critical; -+ } else if (levelStr == "off") { -+ logLevel = spdlog::level::off; -+ } else { -+ throw std::runtime_error("Invalid log level: " + levelStr + ". Valid levels: trace, debug, info, warn, error, critical, off"); -+ } -+ } - if (args["--syslog"].asBool()) { - auto syslog_sink = std::make_shared("rousette", LOG_PID, LOG_USER, true); - auto logger = std::make_shared("rousette", syslog_sink); -@@ -94,7 +117,7 @@ int main(int argc, char* argv []) - auto logger = std::make_shared("rousette", stdout_sink); - spdlog::set_default_logger(logger); - } -- spdlog::set_level(spdlog::level::trace); -+ spdlog::set_level(logLevel); - - /* We will parse URIs using boost::spirit's alnum/alpha/... matchers which are locale-dependent. - * Let's use something stable no matter what the system is using --- -2.43.0 - diff --git a/package/rousette/0009-fix-a-possible-bad_weak_ptr-exception.patch b/package/rousette/0013-fix-a-possible-bad_weak_ptr-exception.patch similarity index 79% rename from package/rousette/0009-fix-a-possible-bad_weak_ptr-exception.patch rename to package/rousette/0013-fix-a-possible-bad_weak_ptr-exception.patch index 99cbeade..4a4380d9 100644 --- a/package/rousette/0009-fix-a-possible-bad_weak_ptr-exception.patch +++ b/package/rousette/0013-fix-a-possible-bad_weak_ptr-exception.patch @@ -1,12 +1,15 @@ -From de7c55a1e8d3dda20ee3dc408612f2ddb309e888 Mon Sep 17 00:00:00 2001 +From 4e9b535a59861f25c0602eaa1fc39126d7cd9899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Thu, 7 Aug 2025 12:21:54 +0200 -Subject: [PATCH 09/13] fix a possible bad_weak_ptr exception +Subject: [PATCH 13/38] fix a possible bad_weak_ptr exception +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit Organization: Wires Change-Id: I8c7f7a943a1d848f15527988cb76c2a0a10089e6 Fixes: 4eae6200 (close long-lived connections on SIGTERM) -Signed-off-by: Joachim Wiberg +Signed-off-by: Mattias Walström --- src/http/EventStream.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/rousette/0010-http-add-optional-callbacks-to-EventStream.patch b/package/rousette/0014-http-add-optional-callbacks-to-EventStream.patch similarity index 95% rename from package/rousette/0010-http-add-optional-callbacks-to-EventStream.patch rename to package/rousette/0014-http-add-optional-callbacks-to-EventStream.patch index 5ffb3bee..50abb38b 100644 --- a/package/rousette/0010-http-add-optional-callbacks-to-EventStream.patch +++ b/package/rousette/0014-http-add-optional-callbacks-to-EventStream.patch @@ -1,7 +1,10 @@ -From 0f034d6da3e6d0a91bbe09717cb33aaf3a3ed747 Mon Sep 17 00:00:00 2001 +From 6bca750f866b5b14c4d9c3da68e5c8f1e4eee36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Mon, 19 May 2025 12:11:09 +0200 -Subject: [PATCH 10/13] http: add optional callbacks to EventStream +Subject: [PATCH 14/38] http: add optional callbacks to EventStream +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit Organization: Wires In some cases, it might be useful to have a callback that is called when @@ -11,7 +14,7 @@ implementation where we would like to clean up stuff after client disconnects. Change-Id: Icfc2959e38b812b7c18f45976415209b29151c7b -Signed-off-by: Joachim Wiberg +Signed-off-by: Mattias Walström --- src/http/EventStream.cpp | 18 +++++++++++++++--- src/http/EventStream.h | 10 ++++++++-- diff --git a/package/rousette/0011-restconf-add-internal-RPC-handler-dispatcher.patch b/package/rousette/0015-restconf-add-internal-RPC-handler-dispatcher.patch similarity index 93% rename from package/rousette/0011-restconf-add-internal-RPC-handler-dispatcher.patch rename to package/rousette/0015-restconf-add-internal-RPC-handler-dispatcher.patch index 4aa75f32..2a151cc5 100644 --- a/package/rousette/0011-restconf-add-internal-RPC-handler-dispatcher.patch +++ b/package/rousette/0015-restconf-add-internal-RPC-handler-dispatcher.patch @@ -1,7 +1,10 @@ -From d72adb3da31cb509e64c8f33174e375fa9468b0b Mon Sep 17 00:00:00 2001 +From f6ee629de8abef42a24c42b185052b0c8e78bd6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Mon, 19 May 2025 12:23:16 +0200 -Subject: [PATCH 11/13] restconf: add internal RPC handler dispatcher +Subject: [PATCH 15/38] restconf: add internal RPC handler dispatcher +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit Organization: Wires This code adds a new internal RPC handler dispatcher that checks if user @@ -11,7 +14,7 @@ So far, there are no RPCs processed, but support for the ietf-subscribed-notifications:establish-subscription RPC is coming soon. Change-Id: I99121a511011229e4098f95e91601b39d333444a -Signed-off-by: Joachim Wiberg +Signed-off-by: Mattias Walström --- src/restconf/Server.cpp | 24 ++++++++++++++++++++---- tests/restconf-rpc.cpp | 18 ++++++++++++++++++ diff --git a/package/rousette/0016-tests-processing-incomplete-events-in-SSE-client.patch b/package/rousette/0016-tests-processing-incomplete-events-in-SSE-client.patch new file mode 100644 index 00000000..5b462f46 --- /dev/null +++ b/package/rousette/0016-tests-processing-incomplete-events-in-SSE-client.patch @@ -0,0 +1,120 @@ +From b7966613b43b01402c9f0af286a0b3237161779d Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Tue, 2 Sep 2025 15:33:43 +0200 +Subject: [PATCH 16/38] tests: processing incomplete events in SSE client +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +During my testing I noticed that I obtained incomplete message in +on_data callback. The message was split into two callbacks. +This commit fixes the FIXME by buffering the data and processing +a complete event, once available. + +Change-Id: Ied07e69e8b518f20fcc82134a4c041e7ec3a06d6 +Signed-off-by: Mattias Walström +--- + tests/restconf_utils.cpp | 53 +++++++++++++++++++++++----------------- + tests/restconf_utils.h | 3 ++- + 2 files changed, 33 insertions(+), 23 deletions(-) + +diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp +index 5b1ebe3..740b1fa 100644 +--- a/tests/restconf_utils.cpp ++++ b/tests/restconf_utils.cpp +@@ -199,8 +199,8 @@ SSEClient::SSEClient( + req->on_response([&, silenceTimeout, reportIgnoredLines](const ng_client::response& res) { + requestSent.release(); + res.on_data([&, silenceTimeout, reportIgnoredLines](const uint8_t* data, std::size_t len) { +- // not a production-ready code. In real-life condition the data received in one callback might probably be incomplete +- parseEvents(std::string(reinterpret_cast(data), len), eventWatcher, reportIgnoredLines); ++ dataBuffer.append(std::string(reinterpret_cast(data), len)); ++ parseEvents(eventWatcher, reportIgnoredLines); + t.expires_from_now(silenceTimeout); + }); + }); +@@ -217,30 +217,39 @@ SSEClient::SSEClient( + }); + } + +-void SSEClient::parseEvents(const std::string& msg, const RestconfNotificationWatcher& eventWatcher, const ReportIgnoredLines reportIgnoredLines) ++void SSEClient::parseEvents(const RestconfNotificationWatcher& eventWatcher, const ReportIgnoredLines reportIgnoredLines) + { + static const std::string dataPrefix = "data:"; + static const std::string ignorePrefix = ":"; + +- std::istringstream iss(msg); +- std::string line; +- std::string event; +- +- while (std::getline(iss, line)) { +- if (line.starts_with(ignorePrefix) && reportIgnoredLines == ReportIgnoredLines::Yes) { +- eventWatcher.commentEvent(line); +- } else if (line.starts_with(ignorePrefix)) { +- continue; +- } else if (line.starts_with(dataPrefix)) { +- event += line.substr(dataPrefix.size()); +- } else if (line.empty() && !event.empty()) { +- eventWatcher.dataEvent(event); +- event.clear(); +- } else if (line.empty()) { +- continue; +- } else { +- CAPTURE(msg); +- FAIL("Unprefixed response"); ++ std::size_t pos = 0; ++ constexpr auto EVENT_SEPARATOR = "\n\n"; // FIXME: Not a production-ready code; does not deal with all possible newline combinations of CR and LF ++ ++ while ((pos = dataBuffer.find(EVENT_SEPARATOR)) != std::string::npos) { ++ // extract event ++ auto rawEvent = dataBuffer.substr(0, pos + std::char_traits::length(EVENT_SEPARATOR)); ++ std::istringstream stream(rawEvent); ++ dataBuffer.erase(0, pos + std::char_traits::length(EVENT_SEPARATOR)); ++ ++ // split on newlines ++ std::string line; ++ std::string event; ++ while (std::getline(stream, line)) { ++ if (line.starts_with(ignorePrefix) && reportIgnoredLines == ReportIgnoredLines::Yes) { ++ eventWatcher.commentEvent(line); ++ } else if (line.starts_with(ignorePrefix)) { ++ continue; ++ } else if (line.starts_with(dataPrefix)) { ++ event += line.substr(dataPrefix.size()); ++ } else if (line.empty() && !event.empty()) { ++ eventWatcher.dataEvent(event); ++ event.clear(); ++ } else if (line.empty()) { ++ continue; ++ } else { ++ CAPTURE(rawEvent); ++ FAIL("Unprefixed response"); ++ } + } + } + } +diff --git a/tests/restconf_utils.h b/tests/restconf_utils.h +index 9dde10b..c35df54 100644 +--- a/tests/restconf_utils.h ++++ b/tests/restconf_utils.h +@@ -87,6 +87,7 @@ void setupRealNacm(sysrepo::Session session); + struct SSEClient { + std::shared_ptr client; + boost::asio::deadline_timer t; ++ std::string dataBuffer; + + enum class ReportIgnoredLines { + No, +@@ -104,7 +105,7 @@ struct SSEClient { + const boost::posix_time::seconds silenceTimeout = boost::posix_time::seconds(1), // test code; the server should respond "soon" + const ReportIgnoredLines reportIgnoredLines = ReportIgnoredLines::No); + +- static void parseEvents(const std::string& msg, const RestconfNotificationWatcher& eventWatcher, const ReportIgnoredLines reportIgnoredLines); ++ void parseEvents(const RestconfNotificationWatcher& eventWatcher, const ReportIgnoredLines reportIgnoredLines); + }; + + #define PREPARE_LOOP_WITH_EXCEPTIONS \ +-- +2.43.0 + diff --git a/package/rousette/0012-EventStream-fix-possible-heap-use-after-free.patch b/package/rousette/0017-EventStream-fix-possible-heap-use-after-free.patch similarity index 98% rename from package/rousette/0012-EventStream-fix-possible-heap-use-after-free.patch rename to package/rousette/0017-EventStream-fix-possible-heap-use-after-free.patch index e0069ede..5bc8e345 100644 --- a/package/rousette/0012-EventStream-fix-possible-heap-use-after-free.patch +++ b/package/rousette/0017-EventStream-fix-possible-heap-use-after-free.patch @@ -1,7 +1,10 @@ -From 86ea8a1fdc2cb8baa1edbd32bd4f8b45acdef816 Mon Sep 17 00:00:00 2001 +From 1067e05674633b97d64b428686aff44822230c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Mon, 8 Sep 2025 20:24:15 +0200 -Subject: [PATCH 12/13] EventStream: fix possible heap-use-after-free +Subject: [PATCH 17/38] EventStream: fix possible heap-use-after-free +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit Organization: Wires I have started getting heap-use-after-free errors from sanitizers (see @@ -61,7 +64,7 @@ before actually resuming the response. (...) Change-Id: Ifdb1f8610cacffca3bb49da17aa9b1d267cdd472 -Signed-off-by: Joachim Wiberg +Signed-off-by: Mattias Walström --- src/http/EventStream.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/package/rousette/0018-Use-a-working-version-of-nghttp2-asio.patch b/package/rousette/0018-Use-a-working-version-of-nghttp2-asio.patch new file mode 100644 index 00000000..620c7af2 --- /dev/null +++ b/package/rousette/0018-Use-a-working-version-of-nghttp2-asio.patch @@ -0,0 +1,52 @@ +From fff342945eb3c8deb6f7aa67aca54f9c9f87f3ec Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Thu, 11 Sep 2025 18:56:31 +0200 +Subject: [PATCH 18/38] Use a working version of nghttp2-asio +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +The latest upstream version does not build on Boost 1.87, and there's a +nasty crash in the destructor which led to a nice little heap of hair on +our desks. It's time to fork that thing. + +Link: https://github.com/nghttp2/nghttp2-asio/pull/9 +Link: https://github.com/nghttp2/nghttp2-asio/pull/25 +Change-Id: Iec9619ce45e7e76f0781d39966d0b6c7cc6fa778 +Signed-off-by: Mattias Walström +--- + .zuul.yaml | 2 +- + README.md | 3 ++- + 2 files changed, 3 insertions(+), 2 deletions(-) + +diff --git a/.zuul.yaml b/.zuul.yaml +index e5a87b6..a994f34 100644 +--- a/.zuul.yaml ++++ b/.zuul.yaml +@@ -6,7 +6,7 @@ + required-projects: &projects + - CzechLight/dependencies + - name: github/nghttp2/nghttp2-asio +- override-checkout: main ++ override-checkout: cesnet/main + vars: + zuul_work_dir: "{{ zuul.projects[zuul.project.canonical_hostname + '/CzechLight/rousette'].src_dir }}" + +diff --git a/README.md b/README.md +index a42c64d..437afd3 100644 +--- a/README.md ++++ b/README.md +@@ -84,7 +84,8 @@ In practical terms, this means that the NACM access rules for the following XPat + + ## Dependencies + +-- [nghttp2-asio](https://github.com/nghttp2/nghttp2-asio) - asynchronous C++ library for HTTP/2 ++- [nghttp2-asio](https://github.com/CESNET/nghttp2-asio) - asynchronous C++ library for HTTP/2 ++ - we strongly recommend using [our fork](https://github.com/CESNET/nghttp2-asio) which appears to be better maintained, and which contains important bugfixes which have not been merged upstream yet + - [sysrepo-cpp](https://github.com/sysrepo/sysrepo-cpp) - object-oriented bindings of the [*sysrepo*](https://github.com/sysrepo/sysrepo) library + - [libyang-cpp](https://github.com/CESNET/libyang-cpp) - C++ bindings for *libyang* + - [PAM](http://www.linux-pam.org/) - for authentication +-- +2.43.0 + diff --git a/package/rousette/0019-ping-use-a-monotonic-timer.patch b/package/rousette/0019-ping-use-a-monotonic-timer.patch new file mode 100644 index 00000000..7f628043 --- /dev/null +++ b/package/rousette/0019-ping-use-a-monotonic-timer.patch @@ -0,0 +1,61 @@ +From ffc52b2e454ae4ac4fa1948cb76a9469026d453e Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Mon, 6 Oct 2025 22:35:27 +0200 +Subject: [PATCH 19/38] ping: use a monotonic timer +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +The deadline_timer uses system's UTC clock which is not monotonic (i.e., +it jumps when the wall clock is adjusted). Let's switch to a monotonic +timer. + +Given that boost::asio::steady_timer::expires_from_now() is marked as +deprecated, use the other function. + +Change-Id: I40383721ecb0f12bfcb3f638124a32150a29bf48 +Signed-off-by: Mattias Walström +--- + src/http/EventStream.cpp | 2 +- + src/http/EventStream.h | 4 ++-- + 2 files changed, 3 insertions(+), 3 deletions(-) + +diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp +index f97d953..280addd 100644 +--- a/src/http/EventStream.cpp ++++ b/src/http/EventStream.cpp +@@ -180,7 +180,7 @@ void EventStream::enqueue(const std::string& fieldName, const std::string& what) + + void EventStream::start_ping() + { +- ping.expires_from_now(boost::posix_time::seconds(m_keepAlivePingInterval.count())); ++ ping.expires_after(m_keepAlivePingInterval); + ping.async_wait([weak = weak_from_this()](const boost::system::error_code& ec) { + auto myself = weak.lock(); + if (!myself) { +diff --git a/src/http/EventStream.h b/src/http/EventStream.h +index 87d8c82..f9f0dd6 100644 +--- a/src/http/EventStream.h ++++ b/src/http/EventStream.h +@@ -7,7 +7,7 @@ + + #pragma once + +-#include ++#include + #include + #include + #include +@@ -50,7 +50,7 @@ private: + }; + + State state = WaitingForEvents; +- boost::asio::deadline_timer ping; ++ boost::asio::steady_timer ping; + std::list queue; + mutable std::mutex mtx; // for `state` and `queue` + boost::signals2::scoped_connection eventSub, terminateSub; +-- +2.43.0 + diff --git a/package/rousette/0020-tests-use-a-monotonic-timer.patch b/package/rousette/0020-tests-use-a-monotonic-timer.patch new file mode 100644 index 00000000..9b7f7d69 --- /dev/null +++ b/package/rousette/0020-tests-use-a-monotonic-timer.patch @@ -0,0 +1,81 @@ +From 4493316404f31354ea18bc92a1706a3834ad972b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Mon, 6 Oct 2025 22:50:11 +0200 +Subject: [PATCH 20/38] tests: use a monotonic timer +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Change-Id: I89cdd08082b025643dac81788ca174d3c2177089 +Signed-off-by: Mattias Walström +--- + tests/restconf-eventstream.cpp | 2 +- + tests/restconf_utils.cpp | 6 +++--- + tests/restconf_utils.h | 4 ++-- + 3 files changed, 6 insertions(+), 6 deletions(-) + +diff --git a/tests/restconf-eventstream.cpp b/tests/restconf-eventstream.cpp +index ad1040d..ecb86e3 100644 +--- a/tests/restconf-eventstream.cpp ++++ b/tests/restconf-eventstream.cpp +@@ -101,7 +101,7 @@ TEST_CASE("Event stream tests") + netconfWatcher, + "/streams/NETCONF/JSON", + std::map{AUTH_ROOT}, +- boost::posix_time::seconds{5}, ++ std::chrono::seconds{5}, + SSEClient::ReportIgnoredLines::Yes); + + RUN_LOOP_WITH_EXCEPTIONS; +diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp +index 740b1fa..cb4e26a 100644 +--- a/tests/restconf_utils.cpp ++++ b/tests/restconf_utils.cpp +@@ -172,10 +172,10 @@ SSEClient::SSEClient( + const RestconfNotificationWatcher& eventWatcher, + const std::string& uri, + const std::map& headers, +- const boost::posix_time::seconds silenceTimeout, ++ const std::chrono::seconds silenceTimeout, + const ReportIgnoredLines reportIgnoredLines) + : client(std::make_shared(io, server_address, server_port)) +- , t(io, silenceTimeout) ++ , t(io, std::chrono::seconds(silenceTimeout)) + { + ng::header_map reqHeaders; + for (const auto& [name, value] : headers) { +@@ -201,7 +201,7 @@ SSEClient::SSEClient( + res.on_data([&, silenceTimeout, reportIgnoredLines](const uint8_t* data, std::size_t len) { + dataBuffer.append(std::string(reinterpret_cast(data), len)); + parseEvents(eventWatcher, reportIgnoredLines); +- t.expires_from_now(silenceTimeout); ++ t.expires_after(std::chrono::seconds(silenceTimeout)); + }); + }); + +diff --git a/tests/restconf_utils.h b/tests/restconf_utils.h +index c35df54..a5676f4 100644 +--- a/tests/restconf_utils.h ++++ b/tests/restconf_utils.h +@@ -86,7 +86,7 @@ void setupRealNacm(sysrepo::Session session); + + struct SSEClient { + std::shared_ptr client; +- boost::asio::deadline_timer t; ++ boost::asio::steady_timer t; + std::string dataBuffer; + + enum class ReportIgnoredLines { +@@ -102,7 +102,7 @@ struct SSEClient { + const RestconfNotificationWatcher& eventWatcher, + const std::string& uri, + const std::map& headers, +- const boost::posix_time::seconds silenceTimeout = boost::posix_time::seconds(1), // test code; the server should respond "soon" ++ const std::chrono::seconds silenceTimeout = std::chrono::seconds{1}, // test code; the server should respond "soon" + const ReportIgnoredLines reportIgnoredLines = ReportIgnoredLines::No); + + void parseEvents(const RestconfNotificationWatcher& eventWatcher, const ReportIgnoredLines reportIgnoredLines); +-- +2.43.0 + diff --git a/package/rousette/0021-port-to-libyang-v4.patch b/package/rousette/0021-port-to-libyang-v4.patch new file mode 100644 index 00000000..34db989e --- /dev/null +++ b/package/rousette/0021-port-to-libyang-v4.patch @@ -0,0 +1,360 @@ +From 8077a63ac97529d56782d2315f23c44f6036b85f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Thu, 23 Oct 2025 18:17:37 +0200 +Subject: [PATCH 21/38] port to libyang v4 +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +- the prefix of the "ietf-netconf-with-defaults" module has changed, +- RPC parsing needs a flag to reject unknown input data nodes, +- due to the "printed context" mess, we now need an extra flag to ensure + that we can still access the YANG schema source. + +The last item is interesting. We could also read directly from the +on-disk location, but that looks a bit meh. It's also a bit annoying +that one has to change this thing over a lot of places, but I think that +this is much nicer than trying to force a change over an existing +connection (e.g., via a connection-specific call in the Server::Server +ctor which might hit some internal locks depending on how the sysrepo +state look like). + +I was considering adding a wrapper for `ly_ctx_get_options()` and a +check in Server::Server, but then I decided that I won't bother. + +Oh, and this "disable printed context" is also needed for the RESTCONF +*client* which is used in the unit test. The `lyd_parse_op` cannot parse +notifications *iff* the `notifications` module from RFC 5277 is missing +*and* when the libyang context does not have the "parsed" info from the +`ietf-yang-types` available. The printed context, as provided by +sysrepo, does not have that data available. This is all papered over as +long as the context flag setting works, which is the case with +post-4.2.4 version of sysrepo (still untagged). Yay. + +We still set these flags to a single value in all of the unit tests and +in the main() as well to maintain the remaining bits of our sanity, of +course. + +Change-Id: I142f650affb53411b5c1dd81311400a2aa6dec20 +Depends-on: https://gerrit.cesnet.cz/c/CzechLight/dependencies/+/8990 +Bug: https://github.com/CESNET/libyang/pull/2448 +Depends-on: https://github.com/sysrepo/sysrepo/commit/6dc5641762962b93d54a54443e2fd43aa319a7a6 +Signed-off-by: Mattias Walström +--- + CMakeLists.txt | 6 +++--- + src/restconf/Server.cpp | 2 +- + src/restconf/main.cpp | 4 ++++ + tests/restconf-defaults.cpp | 2 ++ + tests/restconf-delete.cpp | 2 ++ + tests/restconf-eventstream.cpp | 1 + + tests/restconf-nacm.cpp | 2 ++ + tests/restconf-notifications.cpp | 1 + + tests/restconf-plain-patch.cpp | 2 ++ + tests/restconf-reading.cpp | 12 +++++++----- + tests/restconf-rpc.cpp | 2 ++ + tests/restconf-writing.cpp | 2 ++ + tests/restconf-yang-patch.cpp | 2 ++ + tests/restconf-yang-schema.cpp | 2 ++ + 14 files changed, 33 insertions(+), 9 deletions(-) + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index c0304af..1223ded 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -74,9 +74,9 @@ find_package(PkgConfig) + pkg_check_modules(nghttp2 REQUIRED IMPORTED_TARGET libnghttp2_asio>=0.0.90 libnghttp2) # To compile under boost 1.87 you have to patch nghttp2-asio using https://github.com/nghttp2/nghttp2-asio/issues/23 + find_package(Boost 1.66 REQUIRED CONFIG COMPONENTS system thread) + +-pkg_check_modules(SYSREPO REQUIRED sysrepo>=3.6.5 IMPORTED_TARGET) +-pkg_check_modules(SYSREPO-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=6) +-pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=4) ++pkg_check_modules(SYSREPO REQUIRED sysrepo IMPORTED_TARGET) ++pkg_check_modules(SYSREPO-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=7) ++pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=5) + pkg_check_modules(SYSTEMD IMPORTED_TARGET libsystemd) + pkg_check_modules(PAM REQUIRED IMPORTED_TARGET pam) + pkg_check_modules(DOCOPT REQUIRED IMPORTED_TARGET docopt) +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 74c06f7..633f0bb 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -487,7 +487,7 @@ void processActionOrRPC(std::shared_ptr requestCtx, const std::c + auto [parent, rpcNode] = ctx.newPath2(requestCtx->restconfRequest.path); + + if (!requestCtx->payload.empty()) { +- rpcNode->parseOp(requestCtx->payload, *requestCtx->dataFormat.request, libyang::OperationType::RpcRestconf); ++ rpcNode->parseOp(requestCtx->payload, *requestCtx->dataFormat.request, libyang::OperationType::RpcRestconf, libyang::ParseOptions::Strict); + } + + std::optional rpcReply; +diff --git a/src/restconf/main.cpp b/src/restconf/main.cpp +index 477e846..b1e2bc6 100644 +--- a/src/restconf/main.cpp ++++ b/src/restconf/main.cpp +@@ -22,6 +22,7 @@ + #include + #include + #include ++#include + #include "restconf/Server.h" + static const char usage[] = + R"(Rousette - RESTCONF server +@@ -103,6 +104,9 @@ int main(int argc, char* argv []) + throw std::runtime_error("Could not set locale C.UTF-8"); + } + ++ // schema access is required ++ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate); ++ + auto conn = sysrepo::Connection{}; + auto server = rousette::restconf::Server{conn, "::1", "10080", timeout}; + +diff --git a/tests/restconf-defaults.cpp b/tests/restconf-defaults.cpp +index 89083f1..e783e59 100644 +--- a/tests/restconf-defaults.cpp ++++ b/tests/restconf-defaults.cpp +@@ -9,6 +9,7 @@ + static const auto SERVER_PORT = "10087"; + #include + #include ++#include + #include "restconf/Server.h" + #include "tests/aux-utils.h" + #include "tests/event_watchers.h" +@@ -18,6 +19,7 @@ TEST_CASE("default handling") + trompeloeil::sequence seq1; + + spdlog::set_level(spdlog::level::trace); ++ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate); + auto srConn = sysrepo::Connection{}; + auto srSess = srConn.sessionStart(sysrepo::Datastore::Running); + auto nacmGuard = manageNacm(srSess); +diff --git a/tests/restconf-delete.cpp b/tests/restconf-delete.cpp +index 4818ff3..3b716bd 100644 +--- a/tests/restconf-delete.cpp ++++ b/tests/restconf-delete.cpp +@@ -8,6 +8,7 @@ + static const auto SERVER_PORT = "10086"; + #include + #include ++#include + #include "restconf/Server.h" + #include "tests/aux-utils.h" + #include "tests/event_watchers.h" +@@ -16,6 +17,7 @@ static const auto SERVER_PORT = "10086"; + TEST_CASE("deleting data") + { + spdlog::set_level(spdlog::level::trace); ++ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate); + auto srConn = sysrepo::Connection{}; + auto srSess = srConn.sessionStart(sysrepo::Datastore::Running); + auto nacmGuard = manageNacm(srSess); +diff --git a/tests/restconf-eventstream.cpp b/tests/restconf-eventstream.cpp +index ecb86e3..a099f54 100644 +--- a/tests/restconf-eventstream.cpp ++++ b/tests/restconf-eventstream.cpp +@@ -29,6 +29,7 @@ TEST_CASE("Event stream tests") + + std::vector> expectations; + ++ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate); + auto srConn = sysrepo::Connection{}; + auto srSess = srConn.sessionStart(sysrepo::Datastore::Running); + srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset")); +diff --git a/tests/restconf-nacm.cpp b/tests/restconf-nacm.cpp +index 7799fe8..0e06d56 100644 +--- a/tests/restconf-nacm.cpp ++++ b/tests/restconf-nacm.cpp +@@ -10,11 +10,13 @@ static const auto SERVER_PORT = "10082"; + #include "tests/aux-utils.h" + #include + #include ++#include + #include "restconf/Server.h" + + TEST_CASE("NACM") + { + spdlog::set_level(spdlog::level::trace); ++ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate); + auto srConn = sysrepo::Connection{}; + auto srSess = srConn.sessionStart(sysrepo::Datastore::Running); + auto nacmGuard = manageNacm(srSess); +diff --git a/tests/restconf-notifications.cpp b/tests/restconf-notifications.cpp +index 04131d7..ac355d8 100644 +--- a/tests/restconf-notifications.cpp ++++ b/tests/restconf-notifications.cpp +@@ -29,6 +29,7 @@ TEST_CASE("NETCONF notification streams") + + std::vector> expectations; + ++ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate); + auto srConn = sysrepo::Connection{}; + auto srSess = srConn.sessionStart(sysrepo::Datastore::Running); + srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset")); +diff --git a/tests/restconf-plain-patch.cpp b/tests/restconf-plain-patch.cpp +index 34813d1..c7e035d 100644 +--- a/tests/restconf-plain-patch.cpp ++++ b/tests/restconf-plain-patch.cpp +@@ -8,6 +8,7 @@ + static const auto SERVER_PORT = "10089"; + #include + #include ++#include + #include "restconf/Server.h" + #include "tests/aux-utils.h" + #include "tests/event_watchers.h" +@@ -16,6 +17,7 @@ static const auto SERVER_PORT = "10089"; + TEST_CASE("Plain patch") + { + spdlog::set_level(spdlog::level::trace); ++ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate); + auto srConn = sysrepo::Connection{}; + auto srSess = srConn.sessionStart(sysrepo::Datastore::Running); + auto nacmGuard = manageNacm(srSess); +diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp +index 4f0d2ee..b32a598 100644 +--- a/tests/restconf-reading.cpp ++++ b/tests/restconf-reading.cpp +@@ -10,12 +10,14 @@ static const auto SERVER_PORT = "10081"; + #include "tests/aux-utils.h" + #include + #include ++#include + #include "restconf/Server.h" + #include "tests/event_watchers.h" + + TEST_CASE("reading data") + { + spdlog::set_level(spdlog::level::trace); ++ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate); + auto srConn = sysrepo::Connection{}; + auto srSess = srConn.sessionStart(sysrepo::Datastore::Running); + srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset")); +@@ -794,7 +796,7 @@ TEST_CASE("reading data") + "c": { + "enabled": true, + "@enabled": { +- "ietf-netconf-with-defaults:default": true ++ "default:default": true + } + } + }, +@@ -803,7 +805,7 @@ TEST_CASE("reading data") + "c": { + "enabled": true, + "@enabled": { +- "ietf-netconf-with-defaults:default": true ++ "default:default": true + } + } + } +@@ -868,7 +870,7 @@ TEST_CASE("reading data") + "c": { + "enabled": true, + "@enabled": { +- "ietf-netconf-with-defaults:default": true ++ "default:default": true + } + } + }, +@@ -877,7 +879,7 @@ TEST_CASE("reading data") + "c": { + "enabled": true, + "@enabled": { +- "ietf-netconf-with-defaults:default": true ++ "default:default": true + } + } + } +@@ -932,7 +934,7 @@ TEST_CASE("reading data") + "c": { + "enabled": true, + "@enabled": { +- "ietf-netconf-with-defaults:default": true ++ "default:default": true + } + } + } +diff --git a/tests/restconf-rpc.cpp b/tests/restconf-rpc.cpp +index 43aaed4..bcbd397 100644 +--- a/tests/restconf-rpc.cpp ++++ b/tests/restconf-rpc.cpp +@@ -8,6 +8,7 @@ + static const auto SERVER_PORT = "10084"; + #include + #include ++#include + #include "restconf/Server.h" + #include "tests/aux-utils.h" + #include "tests/event_watchers.h" +@@ -33,6 +34,7 @@ std::map nodesToMap(libyang::DataNode node) + TEST_CASE("invoking actions and rpcs") + { + spdlog::set_level(spdlog::level::trace); ++ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate); + auto srConn = sysrepo::Connection{}; + auto srSess = srConn.sessionStart(sysrepo::Datastore::Running); + SUBSCRIBE_MODULE(sub1, srSess, "example"); +diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp +index d37ab03..3797b72 100644 +--- a/tests/restconf-writing.cpp ++++ b/tests/restconf-writing.cpp +@@ -8,6 +8,7 @@ + static const auto SERVER_PORT = "10083"; + #include + #include ++#include + #include "restconf/Server.h" + #include "tests/aux-utils.h" + #include "tests/event_watchers.h" +@@ -16,6 +17,7 @@ static const auto SERVER_PORT = "10083"; + TEST_CASE("writing data") + { + spdlog::set_level(spdlog::level::trace); ++ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate); + auto srConn = sysrepo::Connection{}; + auto srSess = srConn.sessionStart(sysrepo::Datastore::Running); + auto nacmGuard = manageNacm(srSess); +diff --git a/tests/restconf-yang-patch.cpp b/tests/restconf-yang-patch.cpp +index 2b35c59..19e0405 100644 +--- a/tests/restconf-yang-patch.cpp ++++ b/tests/restconf-yang-patch.cpp +@@ -8,6 +8,7 @@ + static const auto SERVER_PORT = "10090"; + #include + #include ++#include + #include "restconf/Server.h" + #include "tests/aux-utils.h" + #include "tests/event_watchers.h" +@@ -16,6 +17,7 @@ static const auto SERVER_PORT = "10090"; + TEST_CASE("YANG patch") + { + spdlog::set_level(spdlog::level::trace); ++ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate); + auto srConn = sysrepo::Connection{}; + auto srSess = srConn.sessionStart(sysrepo::Datastore::Running); + auto nacmGuard = manageNacm(srSess); +diff --git a/tests/restconf-yang-schema.cpp b/tests/restconf-yang-schema.cpp +index 6e374b1..b56367d 100644 +--- a/tests/restconf-yang-schema.cpp ++++ b/tests/restconf-yang-schema.cpp +@@ -9,12 +9,14 @@ + static const auto SERVER_PORT = "10085"; + #include + #include ++#include + #include "restconf/Server.h" + #include "tests/aux-utils.h" + + TEST_CASE("obtaining YANG schemas") + { + spdlog::set_level(spdlog::level::trace); ++ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate); + auto srConn = sysrepo::Connection{}; + auto srSess = srConn.sessionStart(sysrepo::Datastore::Running); + srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset")); +-- +2.43.0 + diff --git a/package/rousette/0022-restconf-prevent-lock-order-inversion.patch b/package/rousette/0022-restconf-prevent-lock-order-inversion.patch new file mode 100644 index 00000000..1e05a8c1 --- /dev/null +++ b/package/rousette/0022-restconf-prevent-lock-order-inversion.patch @@ -0,0 +1,63 @@ +From b4bf655502d949a5f351e6acd313f47eecefb062 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Mon, 13 Oct 2025 12:26:46 +0200 +Subject: [PATCH 22/38] restconf: prevent lock order inversion +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +The locked section in EventStream's implementation of nghttp's on_close +callback. Without doing that, we might get a lock order inversion in +case somebody tries to lock the mutex inside onClientDisconnectedCb. And +this would indeed happen with the next patch. If that is applied, then +there are indeed two different mutexes involved and the lock order +inversion might happen. + +- Case 1: A thread acquires the mutex (let's call it M1) in +DynamicNotificationHTTPStream::awaitNextNotification and +then sends the notification signal, which acquires the mutex inside +EventStream (mutex M2). + +- Case 2: the nghttp's on_close callback acquired M2 and +then proceeds to call onClientDisconnectedCb which, in our case, +acquires M1. + +One can view this as a fixup of commit 6bca750 ("http: add optional +callbacks to EventStream"), I am not sure. + +See-also: 6bca750f866b5b14c4d9c3da68e5c8f1e4eee36c ("http: add optional callbacks to EventStream") +Change-Id: Ib6626c127ff2eb5feb1d17dece28e70265749df4 +Signed-off-by: Mattias Walström +--- + src/http/EventStream.cpp | 14 +++++++++----- + 1 file changed, 9 insertions(+), 5 deletions(-) + +diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp +index 280addd..9f19030 100644 +--- a/src/http/EventStream.cpp ++++ b/src/http/EventStream.cpp +@@ -82,11 +82,15 @@ void EventStream::activate() + + res.on_close([myself](const auto ec) { + spdlog::debug("{}: closed ({})", myself->peer, nghttp2_http2_strerror(ec)); +- std::lock_guard lock{myself->mtx}; +- myself->ping.cancel(); +- myself->eventSub.disconnect(); +- myself->terminateSub.disconnect(); +- myself->state = Closed; ++ ++ { ++ std::lock_guard lock{myself->mtx}; ++ myself->ping.cancel(); ++ myself->eventSub.disconnect(); ++ myself->terminateSub.disconnect(); ++ myself->state = Closed; ++ } ++ + if (myself->onClientDisconnectedCb) { + myself->onClientDisconnectedCb(); + } +-- +2.43.0 + diff --git a/package/rousette/0023-refactor-do-not-pollute-scope-with-unnecessary-varia.patch b/package/rousette/0023-refactor-do-not-pollute-scope-with-unnecessary-varia.patch new file mode 100644 index 00000000..38ebfb38 --- /dev/null +++ b/package/rousette/0023-refactor-do-not-pollute-scope-with-unnecessary-varia.patch @@ -0,0 +1,39 @@ +From 27cbe6750bdfc9f346421e55ebe555b8c036d77c Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Tue, 11 Nov 2025 15:01:21 +0100 +Subject: [PATCH 23/38] refactor: do not pollute scope with unnecessary + variables +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +The parent variable was not used in the function, it contributed +nothing. + +Change-Id: I5b9b1a077d44d088084c3b2b93359ab7464ec8aa +Signed-off-by: Mattias Walström +--- + src/restconf/Server.cpp | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 633f0bb..77ae1be 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -446,9 +446,9 @@ std::optional processInternalRPC(sysrepo::Session& sess, cons + const auto rpcPath = rpcInput.path(); + + // Is the user authorized to call the operation? +- auto [parent, rpcNode] = sess.getContext().newPath2(rpcPath, std::nullopt); +- if (!sess.checkNacmOperation(*rpcNode)) { +- throw ErrorResponse(403, "application", "access-denied", "Access denied.", rpcNode->path()); ++ auto rpcNode = sess.getContext().newPath2(rpcPath, std::nullopt); ++ if (!sess.checkNacmOperation(*rpcNode.createdNode)) { ++ throw ErrorResponse(403, "application", "access-denied", "Access denied.", rpcNode.createdNode->path()); + } + + if (auto it = handlers.find(rpcPath); it != handlers.end()) { +-- +2.43.0 + diff --git a/package/rousette/0024-restconf-validate-input-for-internal-rpcs.patch b/package/rousette/0024-restconf-validate-input-for-internal-rpcs.patch new file mode 100644 index 00000000..f2fcc9f5 --- /dev/null +++ b/package/rousette/0024-restconf-validate-input-for-internal-rpcs.patch @@ -0,0 +1,89 @@ +From 5592ae49cd7607fdcb2c9e554a521b53545e761b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Tue, 21 Oct 2025 17:23:44 +0200 +Subject: [PATCH 24/38] restconf: validate input for internal rpcs +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Jan found an issue where invalid input to internal rpcs was accepted. It +turns out that the issue was in parsing, but not validating input for +internally processed RPCs (those RPCs for subscribed notifications). + +This patch adds the validation using a wrapper for lyd_validate_op from +libyang-cpp. + +I shuffled a little bit of the code around together with this change to +make it cleaner. +Also, to make the code generic, we store the name of the internal RPC +and a XPath for fetching data required for validation (see +lyd_validate_op docs). The code validates the input data before processing +the RPC. + +Change-Id: I55296895e6993b4ca27d21ce2a64ec2d159a35fc +Signed-off-by: Mattias Walström +--- + src/restconf/Server.cpp | 36 +++++++++++++++++++++++++++++------- + 1 file changed, 29 insertions(+), 7 deletions(-) + +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 77ae1be..7e8db58 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -438,9 +438,12 @@ libyang::CreatedNodes createEditForPutAndPatch(libyang::Context& ctx, const std: + return {editNode, replacementNode}; + } + +-std::optional processInternalRPC(sysrepo::Session& sess, const libyang::DataNode& rpcInput, const libyang::DataFormat requestEncoding) ++std::optional processInternalRPC(sysrepo::Session& sess, libyang::DataNode& rpcInput, const libyang::DataFormat requestEncoding) + { +- using InternalRPCHandler = std::function; ++ struct InternalRPCHandler { ++ std::optional validationDataXPath; ///< XPath to data used for RPC input validation ++ std::function rpcHandler; // The function that processes the RPC ++ }; + const std::map handlers; + + const auto rpcPath = rpcInput.path(); +@@ -451,13 +454,32 @@ std::optional processInternalRPC(sysrepo::Session& sess, cons + throw ErrorResponse(403, "application", "access-denied", "Access denied.", rpcNode.createdNode->path()); + } + +- if (auto it = handlers.find(rpcPath); it != handlers.end()) { +- auto [parent, rpcOutput] = sess.getContext().newPath2(rpcPath, std::nullopt); +- it->second(sess, requestEncoding, rpcInput, *rpcOutput); +- return *parent; ++ auto handlerIt = handlers.find(rpcPath); ++ if (handlerIt == handlers.end()) { ++ throw ErrorResponse(501, "application", "operation-not-supported", "Unsupported RPC call to " + rpcPath, rpcPath); ++ } ++ ++ // RPC input validation. ++ sess.switchDatastore(sysrepo::Datastore::Operational); ++ auto validationData = handlerIt->second.validationDataXPath ? sess.getData(*handlerIt->second.validationDataXPath) : std::nullopt; ++ ++ try { ++ libyang::validateOp(rpcInput, validationData, libyang::OperationType::RpcRestconf); ++ } catch (const libyang::ErrorWithCode& exc) { ++ const auto errors = sess.getContext().getErrors(); ++ ++ if (!errors.empty()) { ++ const auto& error = errors.back(); ++ throw ErrorResponse(400, "protocol", "invalid-value", error.message, error.dataPath.value_or(rpcInput.path())); ++ } else { ++ // Fallback if no detailed error is available ++ throw ErrorResponse(400, "protocol", "invalid-value", "Input validation failed"s, rpcInput.path()); ++ } + } + +- throw ErrorResponse(501, "application", "operation-not-supported", "Unsupported RPC call to " + rpcPath, rpcPath); ++ auto [parent, rpcOutput] = sess.getContext().newPath2(rpcPath, std::nullopt); ++ handlerIt->second.rpcHandler(sess, requestEncoding, rpcInput, *rpcOutput); ++ return *parent; + } + + void processActionOrRPC(std::shared_ptr requestCtx, const std::chrono::milliseconds timeout) +-- +2.43.0 + diff --git a/package/rousette/0025-tests-allow-read-from-parts-ietf-subscribed-notifica.patch b/package/rousette/0025-tests-allow-read-from-parts-ietf-subscribed-notifica.patch new file mode 100644 index 00000000..00a0ba90 --- /dev/null +++ b/package/rousette/0025-tests-allow-read-from-parts-ietf-subscribed-notifica.patch @@ -0,0 +1,71 @@ +From c2413237501eea3125fe31a9701919d14402737f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Tue, 11 Nov 2025 17:30:33 +0100 +Subject: [PATCH 25/38] tests: allow read from parts + ietf-subscribed-notifications for anonymous user +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +The subsequent commits will add more tests for subscribed notifications. +Because the validation of +ietf-subscribed-notifications:establish-subscription will require read +access to ietf-subscribed-notifications:filters, we need to allow the +access for users. + +Change-Id: I50e49c8c60b7e6be47c51f3d8cd546fbb6f49294 +Signed-off-by: Mattias Walström +--- + tests/restconf-reading.cpp | 5 ++++- + tests/restconf_utils.cpp | 4 ++++ + 2 files changed, 8 insertions(+), 1 deletion(-) + +diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp +index b32a598..b11bc48 100644 +--- a/tests/restconf-reading.cpp ++++ b/tests/restconf-reading.cpp +@@ -110,6 +110,7 @@ TEST_CASE("reading data") + ] + } + }, ++ "ietf-subscribed-notifications:filters": {}, + "ietf-system:system": { + "contact": "contact", + "hostname": "hostname", +@@ -168,6 +169,7 @@ TEST_CASE("reading data") + ] + } + }, ++ "ietf-subscribed-notifications:filters": {}, + "ietf-system:system": { + "contact": "contact", + "hostname": "hostname", +@@ -195,7 +197,8 @@ TEST_CASE("reading data") + "example:config-nonconfig": { + "config-node": "foo-config-true" + }, +- "example:ordered-lists": {} ++ "example:ordered-lists": {}, ++ "ietf-subscribed-notifications:filters": {} + } + )"}); + } +diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp +index cb4e26a..80dca05 100644 +--- a/tests/restconf_utils.cpp ++++ b/tests/restconf_utils.cpp +@@ -156,6 +156,10 @@ void setupRealNacm(sysrepo::Session session) + session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='16']/action", "permit"); + session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='16']/access-operations", "exec"); + session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='16']/rpc-name", "establish-subscription"); ++ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='17']/module-name", "ietf-subscribed-notifications"); ++ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='17']/action", "permit"); ++ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='17']/access-operations", "read"); ++ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='17']/path", "/ietf-subscribed-notifications:filters"); + session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='99']/module-name", "*"); + session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='99']/action", "deny"); + session.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/group[.='optics']", ""); +-- +2.43.0 + diff --git a/package/rousette/0026-restconf-establish-subscription-RPC-for-subscribed-n.patch b/package/rousette/0026-restconf-establish-subscription-RPC-for-subscribed-n.patch new file mode 100644 index 00000000..1231a32e --- /dev/null +++ b/package/rousette/0026-restconf-establish-subscription-RPC-for-subscribed-n.patch @@ -0,0 +1,679 @@ +From aa04b0a99fb51aed998aa9f1ab0a527cc09e647f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Mon, 19 May 2025 12:11:09 +0200 +Subject: [PATCH 26/38] restconf: establish-subscription RPC for subscribed + notifications +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +This is a very bare-bones implementation of the subscribed +notifications concept from RFC 8639 [1]. We only support the +establish-subscription RPC to the NETCONF event stream. + +This patch *does not* ship the code which implements the endpoint for +client connection because this patch is already too large and the +endpoint is not strictly necessary for the RPC to work. It will come in +the follow-up patch. + +[1] https://datatracker.ietf.org/doc/rfc8639/ + +Change-Id: I0217e5abc56cfd73859dbcc610fb1f342dc33a10 +Signed-off-by: Mattias Walström +--- + CMakeLists.txt | 8 +- + src/restconf/DynamicSubscriptions.cpp | 177 ++++++++++++++ + src/restconf/DynamicSubscriptions.h | 57 +++++ + src/restconf/Server.cpp | 25 +- + src/restconf/Server.h | 2 + + tests/restconf-subscribed-notifications.cpp | 253 ++++++++++++++++++++ + 6 files changed, 513 insertions(+), 9 deletions(-) + create mode 100644 src/restconf/DynamicSubscriptions.cpp + create mode 100644 src/restconf/DynamicSubscriptions.h + create mode 100644 tests/restconf-subscribed-notifications.cpp + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 1223ded..18af5ba 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -110,6 +110,7 @@ add_library(rousette-auth STATIC + target_link_libraries(rousette-auth PUBLIC spdlog::spdlog PkgConfig::SYSREPO-CPP PkgConfig::PAM rousette-auth-pam PkgConfig::nghttp2) + + add_library(rousette-restconf STATIC ++ src/restconf/DynamicSubscriptions.cpp + src/restconf/NotificationStream.cpp + src/restconf/Server.cpp + src/restconf/YangSchemaLocations.cpp +@@ -211,7 +212,11 @@ if(BUILD_TESTING) + --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-restconf@2017-01-26.yang + --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-restconf-monitoring@2017-01-26.yang + --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-yang-patch@2017-02-22.yang +- --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-subscribed-notifications@2019-09-09.yang ++ --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/iana-if-type@2014-05-08.yang ++ --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-interfaces@2018-02-20.yang ++ --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-ip@2018-02-22.yang ++ --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-network-instance@2019-01-21.yang ++ --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-subscribed-notifications@2019-09-09.yang --enable-feature replay --enable-feature encode-xml --enable-feature encode-json + --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-restconf-subscribed-notifications@2019-11-17.yang + --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example.yang --enable-feature f1 + --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-delete.yang +@@ -228,6 +233,7 @@ if(BUILD_TESTING) + rousette_test(NAME restconf-plain-patch LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM) + rousette_test(NAME restconf-yang-patch LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM) + rousette_test(NAME restconf-eventstream LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM) ++ rousette_test(NAME restconf-subscribed-notifications LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM) + set(nested-models + ${common-models} + --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/root-mod.yang) +diff --git a/src/restconf/DynamicSubscriptions.cpp b/src/restconf/DynamicSubscriptions.cpp +new file mode 100644 +index 0000000..7b13990 +--- /dev/null ++++ b/src/restconf/DynamicSubscriptions.cpp +@@ -0,0 +1,177 @@ ++/* ++ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/ ++ * ++ * Written by Tomáš Pecka ++ * ++*/ ++#include ++#include ++#include ++#include ++#include ++#include ++#include "restconf/DynamicSubscriptions.h" ++#include "restconf/Exceptions.h" ++ ++namespace { ++ ++/** @brief Parses the YANG date-and-time attribute from the RPC input, if present ++ * ++ * @param rpcInput The RPC input node. ++ * @param path The path to the YANG leaf. ++ */ ++std::optional optionalTime(const libyang::DataNode& rpcInput, const std::string& path) ++{ ++ if (auto stopTimeNode = rpcInput.findPath(path)) { ++ return libyang::fromYangTimeFormat(stopTimeNode->asTerm().valueStr()); ++ } ++ ++ return std::nullopt; ++} ++ ++libyang::DataFormat getEncoding(const libyang::DataNode& rpcInput, const libyang::DataFormat requestEncoding) ++{ ++ /* FIXME: So far we allow only encode-json or encode-xml encoding values and not their derived values. ++ * We do not know what those derived values might mean and how do they change the meaning of the encoding leaf. ++ */ ++ if (auto encodingNode = rpcInput.findPath("encoding")) { ++ const auto encodingStr = encodingNode->asTerm().valueStr(); ++ if (encodingStr == "ietf-subscribed-notifications:encode-json") { ++ return libyang::DataFormat::JSON; ++ } else if (encodingStr == "ietf-subscribed-notifications:encode-xml") { ++ return libyang::DataFormat::XML; ++ } else { ++ throw rousette::restconf::ErrorResponse(400, "application", "invalid-attribute", "Unsupported encoding in establish-subscription: '" + encodingStr + "'. Currently we support only 'encode-xml' and 'encode-json' identities."); ++ } ++ } ++ ++ return requestEncoding; ++} ++ ++sysrepo::DynamicSubscription makeStreamSubscription(sysrepo::Session& session, const libyang::DataNode& rpcInput) ++{ ++ auto streamNode = rpcInput.findPath("stream"); ++ ++ if (!streamNode) { ++ throw rousette::restconf::ErrorResponse(400, "application", "invalid-attribute", "Stream is required"); ++ } ++ ++ if (rpcInput.findPath("stream-filter-name")) { ++ throw rousette::restconf::ErrorResponse(400, "application", "invalid-attribute", "Stream filtering is not supported"); ++ } ++ ++ auto stopTime = optionalTime(rpcInput, "stop-time"); ++ ++ return session.subscribeNotifications( ++ std::nullopt /* TODO xpath filter */, ++ streamNode->asTerm().valueStr(), ++ stopTime, ++ std::nullopt /* TODO replayStart */); ++} ++} ++ ++namespace rousette::restconf { ++ ++DynamicSubscriptions::DynamicSubscriptions(const std::string& streamRootUri) ++ : m_restconfStreamUri(streamRootUri) ++ , m_uuidGenerator(boost::uuids::random_generator()) ++{ ++} ++ ++DynamicSubscriptions::~DynamicSubscriptions() = default; ++ ++void DynamicSubscriptions::establishSubscription(sysrepo::Session& session, const libyang::DataFormat requestEncoding, const libyang::DataNode& rpcInput, libyang::DataNode& rpcOutput) ++{ ++ // Generate a new UUID associated with the subscription. The UUID will be used as a part of the URI so that the URI is not predictable (RFC 8650, section 5) ++ auto uuid = makeUUID(); ++ ++ auto dataFormat = getEncoding(rpcInput, requestEncoding); ++ ++ try { ++ auto sub = makeStreamSubscription(session, rpcInput); ++ ++ rpcOutput.newPath("id", std::to_string(sub.subscriptionId()), libyang::CreationOptions::Output); ++ rpcOutput.newPath("ietf-restconf-subscribed-notifications:uri", m_restconfStreamUri + "subscribed/" + boost::uuids::to_string(uuid), libyang::CreationOptions::Output); ++ ++ std::lock_guard lock(m_mutex); ++ m_subscriptions[uuid] = std::make_shared( ++ std::move(sub), ++ dataFormat, ++ uuid, ++ *session.getNacmUser()); ++ } catch (const sysrepo::ErrorWithCode& e) { ++ throw ErrorResponse(400, "application", "invalid-attribute", e.what()); ++ } ++} ++ ++void DynamicSubscriptions::terminateSubscription(const uint32_t subId) ++{ ++ std::lock_guard lock(m_mutex); ++ ++ auto it = std::find_if(m_subscriptions.begin(), m_subscriptions.end(), [subId](const auto& entry) { ++ return entry.second->subscription.subscriptionId() == subId; ++ }); ++ ++ if (it == m_subscriptions.end()) { ++ spdlog::warn("Requested termination of subscription with id {}, but subscription not found", subId); ++ return; ++ } ++ ++ const auto& [uuid, subscriptionData] = *it; ++ spdlog::debug("{}: termination requested", fmt::streamed(*subscriptionData)); ++ subscriptionData->subscription.terminate("ietf-subscribed-notifications:no-such-subscription"); ++ m_subscriptions.erase(uuid); ++} ++ ++/** @brief Returns the subscription data for the given UUID and user. ++ * ++ * @param uuid The UUID of the subscription. ++ * @return A shared pointer to the SubscriptionData object if found and user is the one who established the subscription (or NACM recovery user), otherwise nullptr. ++ */ ++std::shared_ptr DynamicSubscriptions::getSubscriptionForUser(const boost::uuids::uuid& uuid, const std::optional& user) ++{ ++ std::lock_guard lock(m_mutex); ++ if (auto it = m_subscriptions.find(uuid); it != m_subscriptions.end() && (it->second->user == user || user == sysrepo::Session::getNacmRecoveryUser())) { ++ return it->second; ++ } ++ ++ return nullptr; ++} ++ ++boost::uuids::uuid DynamicSubscriptions::makeUUID() ++{ ++ // uuid generator instance accesses must be synchronized (https://www.boost.org/doc/libs/1_88_0/libs/uuid/doc/html/uuid.html#design_notes) ++ std::lock_guard lock(m_mutex); ++ return m_uuidGenerator(); ++} ++ ++DynamicSubscriptions::SubscriptionData::SubscriptionData( ++ sysrepo::DynamicSubscription sub, ++ libyang::DataFormat format, ++ boost::uuids::uuid uuid, ++ const std::string& user) ++ : subscription(std::move(sub)) ++ , dataFormat(format) ++ , uuid(uuid) ++ , user(user) ++{ ++ spdlog::debug("{}: created", fmt::streamed(*this)); ++} ++ ++DynamicSubscriptions::SubscriptionData::~SubscriptionData() ++{ ++ try { ++ subscription.terminate(); ++ } catch (const sysrepo::ErrorWithCode& e) { // Maybe it was already terminated (stop-time). ++ spdlog::warn("Failed to terminate {}: {}", fmt::streamed(*this), e.what()); ++ } ++} ++ ++std::ostream& operator<<(std::ostream& os, const DynamicSubscriptions::SubscriptionData& sub) ++{ ++ return os << "dynamic subscription (id " << sub.subscription.subscriptionId() ++ << ", user " << sub.user ++ << ", uuid " << boost::uuids::to_string(sub.uuid) ++ << ")"; ++} ++} +diff --git a/src/restconf/DynamicSubscriptions.h b/src/restconf/DynamicSubscriptions.h +new file mode 100644 +index 0000000..366d1a1 +--- /dev/null ++++ b/src/restconf/DynamicSubscriptions.h +@@ -0,0 +1,57 @@ ++/* ++ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/ ++ * ++ * Written by Tomáš Pecka ++ * ++*/ ++#pragma once ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace libyang { ++enum class DataFormat; ++} ++ ++namespace rousette::restconf { ++ ++/** Dynamic subscriptions manager. ++ * ++ * Stores all dynamic subscriptions and provides a way to retrieve them by the UUID. ++ * */ ++class DynamicSubscriptions { ++public: ++ struct SubscriptionData : public std::enable_shared_from_this { ++ sysrepo::DynamicSubscription subscription; ++ libyang::DataFormat dataFormat; ///< Encoding of the notification stream ++ boost::uuids::uuid uuid; ///< UUID is part of the GET URI, it identifies subscriptions for clients ++ std::string user; ///< User who initiated the establish-subscription RPC ++ ++ SubscriptionData( ++ sysrepo::DynamicSubscription sub, ++ libyang::DataFormat format, ++ boost::uuids::uuid uuid, ++ const std::string& user); ++ ~SubscriptionData(); ++ }; ++ ++ DynamicSubscriptions(const std::string& streamRootUri); ++ ~DynamicSubscriptions(); ++ std::shared_ptr getSubscriptionForUser(const boost::uuids::uuid& uuid, const std::optional& user); ++ void establishSubscription(sysrepo::Session& session, const libyang::DataFormat requestEncoding, const libyang::DataNode& rpcInput, libyang::DataNode& rpcOutput); ++ ++private: ++ std::mutex m_mutex; ///< Lock for shared data (subscriptions storage and uuid generator) ++ std::string m_restconfStreamUri; ++ std::map> m_subscriptions; ++ boost::uuids::random_generator m_uuidGenerator; ++ ++ void terminateSubscription(const uint32_t subId); ++ ++ boost::uuids::uuid makeUUID(); ++}; ++} +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 7e8db58..7287976 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -438,13 +438,16 @@ libyang::CreatedNodes createEditForPutAndPatch(libyang::Context& ctx, const std: + return {editNode, replacementNode}; + } + +-std::optional processInternalRPC(sysrepo::Session& sess, libyang::DataNode& rpcInput, const libyang::DataFormat requestEncoding) ++std::optional processInternalRPC(sysrepo::Session& sess, libyang::DataNode& rpcInput, const libyang::DataFormat requestEncoding, DynamicSubscriptions& dynamicSubscriptions) + { + struct InternalRPCHandler { + std::optional validationDataXPath; ///< XPath to data used for RPC input validation + std::function rpcHandler; // The function that processes the RPC + }; +- const std::map handlers; ++ const std::map handlers{ ++ {"/ietf-subscribed-notifications:establish-subscription", ++ {"/ietf-subscribed-notifications:filters", [&dynamicSubscriptions](auto&&... args) { return dynamicSubscriptions.establishSubscription(std::forward(args)...); }}}, ++ }; + + const auto rpcPath = rpcInput.path(); + +@@ -482,7 +485,7 @@ std::optional processInternalRPC(sysrepo::Session& sess, liby + return *parent; + } + +-void processActionOrRPC(std::shared_ptr requestCtx, const std::chrono::milliseconds timeout) ++void processActionOrRPC(std::shared_ptr requestCtx, const std::chrono::milliseconds timeout, DynamicSubscriptions& dynamicSubscriptions) + { + requestCtx->sess.switchDatastore(sysrepo::Datastore::Operational); + auto ctx = requestCtx->sess.getContext(); +@@ -516,7 +519,7 @@ void processActionOrRPC(std::shared_ptr requestCtx, const std::c + if (requestCtx->restconfRequest.type == RestconfRequest::Type::Execute) { + rpcReply = requestCtx->sess.sendRPC(*rpcNode, timeout); + } else if (requestCtx->restconfRequest.type == RestconfRequest::Type::ExecuteInternal) { +- rpcReply = processInternalRPC(requestCtx->sess, *rpcNode, *requestCtx->dataFormat.request); ++ rpcReply = processInternalRPC(requestCtx->sess, *rpcNode, *requestCtx->dataFormat.request, dynamicSubscriptions); + } + + if (!rpcReply || rpcReply->immediateChildren().empty()) { +@@ -902,10 +905,16 @@ std::vector> Server::io_services() cons + return server->io_services(); + } + +-Server::Server(sysrepo::Connection conn, const std::string& address, const std::string& port, const std::chrono::milliseconds timeout, const std::chrono::seconds keepAlivePingInterval) ++Server::Server( ++ sysrepo::Connection conn, ++ const std::string& address, ++ const std::string& port, ++ const std::chrono::milliseconds timeout, ++ const std::chrono::seconds keepAlivePingInterval) + : m_monitoringSession(conn.sessionStart(sysrepo::Datastore::Operational)) + , nacm(conn) + , server{std::make_unique()} ++ , m_dynamicSubscriptions(netconfStreamRoot) + , dwdmEvents{std::make_unique(conn.sessionStart())} + { + server->num_threads(1); // we only use one thread for the server, so we can call join() right away +@@ -917,7 +926,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: + {"ietf-netconf", "", {}}, + {"ietf-yang-library", "2019-01-04", {}}, + {"ietf-yang-patch", "2017-02-22", {}}, +- {"ietf-subscribed-notifications", "2019-09-09", {}}, ++ {"ietf-subscribed-notifications", "2019-09-09", {"encode-xml", "encode-json"}}, + {"ietf-restconf-subscribed-notifications", "2019-11-17", {}}, + }) { + if (auto mod = m_monitoringSession.getContext().getModuleImplemented(module)) { +@@ -1217,12 +1226,12 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: + case RestconfRequest::Type::ExecuteInternal: { + auto requestCtx = std::make_shared(req, res, dataFormat, sess, restconfRequest); + +- req.on_data([requestCtx, timeout, peer=http::peer_from_request(req)](const uint8_t* data, std::size_t length) { ++ req.on_data([this, requestCtx, timeout, peer=http::peer_from_request(req)](const uint8_t* data, std::size_t length) { + if (length > 0) { + requestCtx->payload.append(reinterpret_cast(data), length); + } else { + spdlog::trace("{}: HTTP payload: {}", peer, requestCtx->payload); +- WITH_RESTCONF_EXCEPTIONS(processActionOrRPC, rejectWithError)(requestCtx, timeout); ++ WITH_RESTCONF_EXCEPTIONS(processActionOrRPC, rejectWithError)(requestCtx, timeout, m_dynamicSubscriptions); + } + }); + break; +diff --git a/src/restconf/Server.h b/src/restconf/Server.h +index 0720c8d..c98f736 100644 +--- a/src/restconf/Server.h ++++ b/src/restconf/Server.h +@@ -10,6 +10,7 @@ + #include + #include "auth/Nacm.h" + #include "http/EventStream.h" ++#include "restconf/DynamicSubscriptions.h" + + namespace nghttp2::asio_http2::server { + class http2; +@@ -43,6 +44,7 @@ private: + std::optional m_monitoringOperSub; + auth::Nacm nacm; + std::unique_ptr server; ++ DynamicSubscriptions m_dynamicSubscriptions; + std::unique_ptr dwdmEvents; + using JsonDiffSignal = boost::signals2::signal; + JsonDiffSignal opticsChange; +diff --git a/tests/restconf-subscribed-notifications.cpp b/tests/restconf-subscribed-notifications.cpp +new file mode 100644 +index 0000000..3e8e011 +--- /dev/null ++++ b/tests/restconf-subscribed-notifications.cpp +@@ -0,0 +1,253 @@ ++/* ++ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/ ++ * ++ * Written by Tomáš Pecka ++ * ++ */ ++ ++#include "trompeloeil_doctest.h" ++static const auto SERVER_PORT = "10092"; ++#include ++#include ++#include ++#include ++#include ++#include "restconf/Server.h" ++#include "tests/aux-utils.h" ++#include "tests/event_watchers.h" ++#include "tests/pretty_printers.h" ++ ++#define SEND_NOTIFICATION(DATA) notifSession.sendNotification(*ctx.parseOp(DATA, libyang::DataFormat::JSON, libyang::OperationType::NotificationYang).op, sysrepo::Wait::No); ++ ++constexpr auto uuidV4Regex = "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[89abAB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}"; ++ ++using namespace std::chrono_literals; ++using namespace std::string_literals; ++ ++/** @brief Calls establish-subscription rpc, returns the url of the stream associated with the created subscription */ ++std::string establishSubscription( ++ const libyang::Context& ctx, ++ const libyang::DataFormat rpcEncoding, ++ const std::optional>& rpcRequestAuthHeader, ++ const std::optional& encodingLeafValue) ++{ ++ constexpr auto jsonPrefix = "ietf-subscribed-notifications"; ++ constexpr auto xmlNamespace = "urn:ietf:params:xml:ns:yang:ietf-subscribed-notifications"; ++ ++ auto stopTime = libyang::yangTimeFormat(std::chrono::system_clock::now() + 5s, libyang::TimezoneInterpretation::Local); ++ std::map requestHeaders; ++ ng::header_map expectedHeaders; ++ ++ if (rpcRequestAuthHeader) { ++ requestHeaders.insert(*rpcRequestAuthHeader); ++ } ++ ++ std::optional envelope; ++ auto rpcTree = ctx.newPath("/ietf-subscribed-notifications:establish-subscription"); ++ rpcTree.newPath("stream", "NETCONF"); ++ rpcTree.newPath("stop-time", stopTime); ++ ++ if (encodingLeafValue) { ++ rpcTree.newPath("encoding", *encodingLeafValue); ++ } ++ ++ switch (rpcEncoding) { ++ case libyang::DataFormat::JSON: ++ requestHeaders.insert(CONTENT_TYPE_JSON); ++ expectedHeaders = jsonHeaders; ++ envelope = ctx.newOpaqueJSON({jsonPrefix, jsonPrefix, "input"}, std::nullopt); ++ break; ++ case libyang::DataFormat::XML: ++ requestHeaders.insert(CONTENT_TYPE_XML); ++ expectedHeaders = xmlHeaders; ++ envelope = ctx.newOpaqueXML({xmlNamespace, jsonPrefix, "input"}, std::nullopt); ++ break; ++ default: ++ FAIL("Unhandled libyang DataFormat"); ++ break; ++ } ++ ++ // reconnect everything ++ auto data = rpcTree.child(); ++ data->unlinkWithSiblings(); ++ envelope->insertChild(*data); ++ ++ auto body = *envelope->printStr(rpcEncoding, libyang::PrintFlags::WithSiblings); ++ auto resp = post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription", requestHeaders, body); ++ REQUIRE(resp.equalStatusCodeAndHeaders(Response{200, expectedHeaders, ""})); ++ ++ auto reply = ctx.newPath("/ietf-subscribed-notifications:establish-subscription"); ++ REQUIRE(reply.parseOp(resp.data, rpcEncoding, libyang::OperationType::ReplyRestconf).tree); ++ ++ auto urlNode = reply.findPath("ietf-restconf-subscribed-notifications:uri", libyang::InputOutputNodes::Output); ++ REQUIRE(urlNode); ++ ++ return urlNode->asTerm().valueStr(); ++} ++ ++TEST_CASE("RESTCONF subscribed notifications") ++{ ++ trompeloeil::sequence seq1, seq2; ++ sysrepo::setLogLevelStderr(sysrepo::LogLevel::Information); ++ spdlog::set_level(spdlog::level::trace); ++ ++ auto srConn = sysrepo::Connection{}; ++ auto srSess = srConn.sessionStart(sysrepo::Datastore::Running); ++ srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset")); ++ ++ auto nacmGuard = manageNacm(srSess); ++ auto server = rousette::restconf::Server{srConn, SERVER_ADDRESS, SERVER_PORT}; ++ setupRealNacm(srSess); ++ ++ RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext()); ++ ++ libyang::DataFormat rpcRequestEncoding = libyang::DataFormat::JSON; ++ std::optional rpcSubscriptionEncoding; ++ std::optional> rpcRequestAuthHeader; ++ ++ SECTION("NACM authorization") ++ { ++ SECTION("Anonymous access for establish-subscription is disabled") ++ { ++ /* Remove anonymous user's permission to execute RPCs in ietf-subscribed-notifications. ++ * Intentionally a SECTION. We can remove NACM rule for a while in order to test access denied. ++ * The rule gets automatically restored for the rest of the tests. ++ */ ++ srSess.switchDatastore(sysrepo::Datastore::Running); ++ srSess.deleteItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='16']"); ++ srSess.applyChanges(); ++ ++ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription", {CONTENT_TYPE_JSON}, R"###({ "ietf-subscribed-notifications:input": { "stream": "NETCONF" } })###") ++ == Response{403, jsonHeaders, R"###({ ++ "ietf-restconf:errors": { ++ "error": [ ++ { ++ "error-type": "application", ++ "error-tag": "access-denied", ++ "error-path": "/ietf-subscribed-notifications:establish-subscription", ++ "error-message": "Access denied." ++ } ++ ] ++ } ++} ++)###"}); ++ } ++ } ++ ++ SECTION("Invalid establish-subscription requests") ++ { ++ // stop-time in the past ++ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription", {CONTENT_TYPE_JSON}, R"###({ "ietf-subscribed-notifications:input": { "stream": "NETCONF", "stop-time": "1999-09-09T09:09:09Z" } })###") ++ == Response{400, jsonHeaders, R"###({ ++ "ietf-restconf:errors": { ++ "error": [ ++ { ++ "error-type": "application", ++ "error-tag": "invalid-attribute", ++ "error-message": "Couldn't create notification subscription: SR_ERR_INVAL_ARG\u000A Specified \"stop-time\" is in the past. (SR_ERR_INVAL_ARG)" ++ } ++ ] ++ } ++} ++)###"}); ++ ++ // invalid stream ++ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription", {CONTENT_TYPE_JSON}, R"###({ "ietf-subscribed-notifications:input": { "stream": "ajsdhauisds" } })###") ++ == Response{400, jsonHeaders, R"###({ ++ "ietf-restconf:errors": { ++ "error": [ ++ { ++ "error-type": "application", ++ "error-tag": "invalid-attribute", ++ "error-message": "Couldn't create notification subscription: SR_ERR_NOT_FOUND\u000A Failed to collect modules to subscribe to, invalid stream and/or XPath filter (Item not found). (SR_ERR_NOT_FOUND)" ++ } ++ ] ++ } ++} ++)###"}); ++ ++ // stream-filter-name is unsupported, but leafref validation triggers first ++ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription", {CONTENT_TYPE_JSON}, R"###({ "ietf-subscribed-notifications:input": { "stream": "NETCONF", "stream-filter-name": "xyz" } })###") ++ == Response{400, jsonHeaders, R"###({ ++ "ietf-restconf:errors": { ++ "error": [ ++ { ++ "error-type": "protocol", ++ "error-tag": "invalid-value", ++ "error-path": "/ietf-subscribed-notifications:establish-subscription/stream-filter-name", ++ "error-message": "Invalid leafref value \"xyz\" - no target instance \"/sn:filters/sn:stream-filter/sn:name\" with the same value." ++ } ++ ] ++ } ++} ++)###"}); ++ } ++ ++ SECTION("Valid requests") ++ { ++ SECTION("XML stream") ++ { ++ netconfWatcher.setDataFormat(libyang::DataFormat::XML); ++ rpcRequestAuthHeader = AUTH_ROOT; ++ ++ SECTION("Stream encoding inferred from request content-type") ++ { ++ rpcRequestEncoding = libyang::DataFormat::XML; ++ } ++ ++ SECTION("Explicitly asked for XML stream encoding") ++ { ++ rpcSubscriptionEncoding = "encode-xml"; ++ ++ SECTION("Request content-type JSON") ++ { ++ rpcRequestEncoding = libyang::DataFormat::JSON; ++ } ++ ++ SECTION("Request content-type XML") ++ { ++ rpcRequestEncoding = libyang::DataFormat::XML; ++ } ++ } ++ } ++ ++ SECTION("JSON stream") ++ { ++ netconfWatcher.setDataFormat(libyang::DataFormat::JSON); ++ ++ SECTION("NACM: anonymous user cannot read example-notif module") ++ { ++ rpcRequestAuthHeader = std::nullopt; ++ rpcRequestEncoding = libyang::DataFormat::JSON; ++ rpcSubscriptionEncoding = "encode-json"; ++ } ++ ++ SECTION("Content-type with set encode leaf") ++ { ++ rpcRequestAuthHeader = AUTH_ROOT; ++ ++ SECTION("Stream encoding inferred from request content-type") ++ { ++ rpcRequestEncoding = libyang::DataFormat::JSON; ++ } ++ ++ SECTION("Explicitly asked for JSON stream encoding") ++ { ++ rpcSubscriptionEncoding = "encode-json"; ++ SECTION("Request content-type JSON") ++ { ++ rpcRequestEncoding = libyang::DataFormat::JSON; ++ } ++ ++ SECTION("Request content-type XML") ++ { ++ rpcRequestEncoding = libyang::DataFormat::XML; ++ } ++ } ++ } ++ } ++ ++ auto uri = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding); ++ REQUIRE(std::regex_match(uri, std::regex("/streams/subscribed/"s + uuidV4Regex))); ++ } ++} +-- +2.43.0 + diff --git a/package/rousette/0027-refactor-restconf-stream-request-types.patch b/package/rousette/0027-refactor-restconf-stream-request-types.patch new file mode 100644 index 00000000..bff321f0 --- /dev/null +++ b/package/rousette/0027-refactor-restconf-stream-request-types.patch @@ -0,0 +1,165 @@ +From e2c8036130a2265476579ffa9f4354741b54bbd5 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Mon, 13 Oct 2025 16:10:51 +0200 +Subject: [PATCH 27/38] refactor: restconf stream request types +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +In commit 97ceef1 ("restconf: refactor uri parser for stream URIs") we +prepared the URI parser for the new subscribed stream notifications. +What I did not think through was that the subscribed notifications do +not have query parameters. However, I refactored the types in a way +that the query parameters member is now in the common base type. + +This patch fixes that by editing the types in a way that only the +stream notifications will have the query parameters member. The +type for subscribed notifications (which will come in a followup patch) +will not have it. + +See-also: 97ceef119c900c37bbaa27860c3b43cfa6d69f95 ("restconf: refactor uri parser for stream URIs") +Change-Id: Idc1e05f7ff05cea84b5806f60e217605f75e7a9f +Signed-off-by: Mattias Walström +--- + src/restconf/Server.cpp | 2 +- + src/restconf/uri.cpp | 15 ++++++++------- + src/restconf/uri.h | 14 +++++++------- + src/restconf/uri_impl.h | 2 +- + tests/uri-parser.cpp | 12 ++++++------ + 5 files changed, 23 insertions(+), 22 deletions(-) + +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 7287976..616d7d4 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -1020,7 +1020,7 @@ Server::Server( + shutdownRequested, + keepAlivePingInterval, + sess, +- streamRequest.type.encoding, ++ streamRequest.encoding, + xpathFilter, + startTime, + stopTime); +diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp +index 956c155..6ee011c 100644 +--- a/src/restconf/uri.cpp ++++ b/src/restconf/uri.cpp +@@ -58,11 +58,11 @@ const auto resources = x3::rule{"resources"} = + const auto uriGrammar = x3::rule{"grammar"} = x3::lit("/restconf") >> resources; + + +-const auto netconfStream = x3::rule{"netconfStream"} = ++const auto netconfStream = x3::rule{"netconfStream"} = + x3::lit("/NETCONF") >> + ((x3::lit("/XML") >> x3::attr(libyang::DataFormat::XML)) | + (x3::lit("/JSON") >> x3::attr(libyang::DataFormat::JSON))); +-const auto streamUriGrammar = x3::rule{"streamsGrammar"} = x3::lit("/streams") >> netconfStream; ++const auto streamUriGrammar = x3::rule{"streamsGrammar"} = x3::lit("/streams") >> netconfStream; + + // clang-format on + } +@@ -204,9 +204,9 @@ std::optional parseQueryParams(const std::string& quer + return ret; + } + +-std::optional parseStreamUri(const std::string& uriPath) ++std::optional parseStreamUri(const std::string& uriPath) + { +- std::optional ret; ++ std::optional ret; + + if (!x3::parse(std::begin(uriPath), std::end(uriPath), streamUriGrammar >> x3::eoi, ret)) { + return std::nullopt; +@@ -668,8 +668,8 @@ std::optional> asYangMod + return std::nullopt; + } + +-RestconfStreamRequest::NetconfStream::NetconfStream() = default; +-RestconfStreamRequest::NetconfStream::NetconfStream(const libyang::DataFormat& encoding) ++NetconfStreamRequest::NetconfStreamRequest() = default; ++NetconfStreamRequest::NetconfStreamRequest(const libyang::DataFormat& encoding) + : encoding(encoding) + { + } +@@ -692,7 +692,8 @@ RestconfStreamRequest asRestconfStreamRequest(const std::string& httpMethod, con + + validateQueryParametersForStream(*queryParameters); + +- return {*type, *queryParameters}; ++ type->queryParams = *queryParameters; ++ return *type; + } + + /** @brief Returns a set of allowed HTTP methods for given URI. Usable for the 'allow' header */ +diff --git a/src/restconf/uri.h b/src/restconf/uri.h +index d9dd29f..f6c63a7 100644 +--- a/src/restconf/uri.h ++++ b/src/restconf/uri.h +@@ -197,16 +197,16 @@ struct RestconfRequest { + RestconfRequest(Type type, const boost::optional& datastore, const std::string& path, const queryParams::QueryParams& queryParams); + }; + +-struct RestconfStreamRequest { +- struct NetconfStream { +- libyang::DataFormat encoding; +- +- NetconfStream(); +- NetconfStream(const libyang::DataFormat& encoding); +- } type; ++struct NetconfStreamRequest { ++ libyang::DataFormat encoding; + queryParams::QueryParams queryParams; ++ ++ NetconfStreamRequest(); ++ NetconfStreamRequest(const libyang::DataFormat& encoding); + }; + ++using RestconfStreamRequest = NetconfStreamRequest; ++ + RestconfRequest asRestconfRequest(const libyang::Context& ctx, const std::string& httpMethod, const std::string& uriPath, const std::string& uriQueryString = ""); + std::optional asLibyangSchemaNode(const libyang::Context& ctx, const std::vector& pathSegments); + std::pair asLibyangPathSplit(const libyang::Context& ctx, const std::string& uriPath); +diff --git a/src/restconf/uri_impl.h b/src/restconf/uri_impl.h +index 00b22f6..bc36cca 100644 +--- a/src/restconf/uri_impl.h ++++ b/src/restconf/uri_impl.h +@@ -66,7 +66,7 @@ BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::impl::YangModule, name, revision); + BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::PathSegment, apiIdent, keys); + BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::ApiIdentifier, prefix, identifier); + +-BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::RestconfStreamRequest::NetconfStream, encoding); ++BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::NetconfStreamRequest, encoding); + + BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::ParenExpr, lhs, rhs); + BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::SlashExpr, lhs, rhs); +diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp +index 23c2b79..38ca80f 100644 +--- a/tests/uri-parser.cpp ++++ b/tests/uri-parser.cpp +@@ -1082,15 +1082,15 @@ TEST_CASE("URI path parser") + using rousette::restconf::RestconfStreamRequest; + + { +- auto [type, queryParams] = asRestconfStreamRequest("GET", "/streams/NETCONF/XML", ""); +- REQUIRE(type.encoding == libyang::DataFormat::XML); +- REQUIRE(queryParams.empty()); ++ auto req = asRestconfStreamRequest("GET", "/streams/NETCONF/XML", ""); ++ REQUIRE(req.encoding == libyang::DataFormat::XML); ++ REQUIRE(req.queryParams.empty()); + } + + { +- auto [type, queryParams] = asRestconfStreamRequest("GET", "/streams/NETCONF/JSON", ""); +- REQUIRE(type.encoding == libyang::DataFormat::JSON); +- REQUIRE(queryParams.empty()); ++ auto req = asRestconfStreamRequest("GET", "/streams/NETCONF/JSON", ""); ++ REQUIRE(req.encoding == libyang::DataFormat::JSON); ++ REQUIRE(req.queryParams.empty()); + } + + REQUIRE_THROWS_WITH_AS(asRestconfStreamRequest("GET", "/streams/NETCONF", ""), +-- +2.43.0 + diff --git a/package/rousette/0028-restconf-endpoint-for-subscribed-notifications.patch b/package/rousette/0028-restconf-endpoint-for-subscribed-notifications.patch new file mode 100644 index 00000000..23879906 --- /dev/null +++ b/package/rousette/0028-restconf-endpoint-for-subscribed-notifications.patch @@ -0,0 +1,893 @@ +From b3a55ae0a1ef9e092b0c8f0d7c3dbd7baa708cc8 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Tue, 29 Jul 2025 11:39:21 +0200 +Subject: [PATCH 28/38] restconf: endpoint for subscribed notifications +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +This patch creates a new endpoint for the subscribed notification +streams in the RESTCONF server. The previous commit added the +establish-subscription RPC [1], which returned a stream URI, that was +not accessible in that patch. The patch was extra large even without +that functionality so I have split it into two commits. + +There is one more catch worth mentioning both here and in the comments: +In case sysrepo sends events fast enough it is possible that the +function responsible for reading them blocks the event loop and the +function responsible for sending the actual data to the client is not +called. To mitigate that, I added limit to the number of events read +in one go. +However, we are not sure if that is enough. Is it possible that the +reading function might get called several times in a row? My knowledge +of boost::asio is not enough. + +[1] https://datatracker.ietf.org/doc/rfc8639/ + +Change-Id: I07dcdd3fb9ef4f05f93ae6dda0e0d71094a0bbfc +Signed-off-by: Mattias Walström +--- + CMakeLists.txt | 1 + + src/restconf/DynamicSubscriptions.cpp | 127 ++++++++++++- + src/restconf/DynamicSubscriptions.h | 46 +++++ + src/restconf/Server.cpp | 51 +++-- + src/restconf/uri.cpp | 46 ++++- + src/restconf/uri.h | 10 +- + src/restconf/uri_impl.h | 1 + + src/restconf/utils/io.cpp | 30 +++ + src/restconf/utils/io.h | 12 ++ + tests/restconf-subscribed-notifications.cpp | 199 ++++++++++++++++++++ + tests/uri-parser.cpp | 41 +++- + 11 files changed, 527 insertions(+), 37 deletions(-) + create mode 100644 src/restconf/utils/io.cpp + create mode 100644 src/restconf/utils/io.h + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 18af5ba..5d3b7ce 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -116,6 +116,7 @@ add_library(rousette-restconf STATIC + src/restconf/YangSchemaLocations.cpp + src/restconf/uri.cpp + src/restconf/utils/dataformat.cpp ++ src/restconf/utils/io.cpp + src/restconf/utils/sysrepo.cpp + src/restconf/utils/yang.cpp + ) +diff --git a/src/restconf/DynamicSubscriptions.cpp b/src/restconf/DynamicSubscriptions.cpp +index 7b13990..7f4b171 100644 +--- a/src/restconf/DynamicSubscriptions.cpp ++++ b/src/restconf/DynamicSubscriptions.cpp +@@ -12,6 +12,8 @@ + #include + #include "restconf/DynamicSubscriptions.h" + #include "restconf/Exceptions.h" ++#include "restconf/utils/io.h" ++#include "restconf/utils/yang.h" + + namespace { + +@@ -119,7 +121,7 @@ void DynamicSubscriptions::terminateSubscription(const uint32_t subId) + + const auto& [uuid, subscriptionData] = *it; + spdlog::debug("{}: termination requested", fmt::streamed(*subscriptionData)); +- subscriptionData->subscription.terminate("ietf-subscribed-notifications:no-such-subscription"); ++ subscriptionData->terminate("ietf-subscribed-notifications:no-such-subscription"); + m_subscriptions.erase(uuid); + } + +@@ -154,14 +156,54 @@ DynamicSubscriptions::SubscriptionData::SubscriptionData( + , dataFormat(format) + , uuid(uuid) + , user(user) ++ , state(State::Start) + { + spdlog::debug("{}: created", fmt::streamed(*this)); + } + + DynamicSubscriptions::SubscriptionData::~SubscriptionData() + { ++ terminate(); ++} ++ ++void DynamicSubscriptions::SubscriptionData::clientDisconnected() ++{ ++ spdlog::debug("{}: client disconnected", fmt::streamed(*this)); ++ ++ std::lock_guard lock(mutex); ++ if (state == State::Terminating) { ++ return; ++ } ++ ++ state = State::Start; ++} ++ ++void DynamicSubscriptions::SubscriptionData::clientConnected() ++{ ++ spdlog::debug("{}: client connected", fmt::streamed(*this)); ++ std::lock_guard lock(mutex); ++ state = State::ReceiverActive; ++} ++ ++bool DynamicSubscriptions::SubscriptionData::isReadyToAcceptClient() const ++{ ++ std::lock_guard lock(mutex); ++ return state == State::Start; ++} ++ ++void DynamicSubscriptions::SubscriptionData::terminate(const std::optional& reason) ++{ ++ std::lock_guard lock(mutex); ++ ++ // already terminating, do nothing ++ if (state == State::Terminating) { ++ return; ++ } ++ ++ state = State::Terminating; ++ spdlog::debug("{}: terminating subscription ({})", fmt::streamed(*this), reason.value_or("")); + try { +- subscription.terminate(); ++ subscription.terminate(reason); + } catch (const sysrepo::ErrorWithCode& e) { // Maybe it was already terminated (stop-time). + spdlog::warn("Failed to terminate {}: {}", fmt::streamed(*this), e.what()); + } +@@ -174,4 +216,85 @@ std::ostream& operator<<(std::ostream& os, const DynamicSubscriptions::Subscript + << ", uuid " << boost::uuids::to_string(sub.uuid) + << ")"; + } ++ ++DynamicSubscriptionHttpStream::DynamicSubscriptionHttpStream( ++ const nghttp2::asio_http2::server::request& req, ++ const nghttp2::asio_http2::server::response& res, ++ rousette::http::EventStream::Termination& termination, ++ std::shared_ptr signal, ++ const std::chrono::seconds keepAlivePingInterval, ++ const std::shared_ptr& subscriptionData) ++ : EventStream( ++ req, ++ res, ++ termination, ++ *signal, ++ keepAlivePingInterval, ++ std::nullopt /* no initial event */, ++ [this]() { m_subscriptionData->terminate("ietf-subscribed-notifications:no-such-subscription"); }, ++ [this]() { m_subscriptionData->clientDisconnected(); }) ++ , m_subscriptionData(subscriptionData) ++ , m_signal(signal) ++ , m_stream(res.io_service(), m_subscriptionData->subscription.fd()) ++{ ++} ++ ++DynamicSubscriptionHttpStream::~DynamicSubscriptionHttpStream() ++{ ++ // The stream does not own the file descriptor, sysrepo does. It will be closed when the subscription terminates. ++ m_stream.release(); ++} ++ ++/** @brief Waits for the next notifications and process them */ ++void DynamicSubscriptionHttpStream::awaitNextNotification() ++{ ++ constexpr auto MAX_EVENTS = 50; ++ ++ m_stream.async_wait(boost::asio::posix::stream_descriptor::wait_read, [this](const boost::system::error_code& err) { ++ // Unfortunately wait_read does not return operation_aborted when the file descriptor is closed and poll results in POLLHUP ++ if (err == boost::asio::error::operation_aborted || utils::pipeIsClosedAndNoData(m_subscriptionData->subscription.fd())) { ++ return; ++ } ++ ++ size_t eventsProcessed = 0; ++ /* Process all the available notifications, but at most N ++ * In case sysrepo is providing the events fast enough, this loop would still run inside the event loop ++ * and the event responsible for sending the data to the client would not get to be processed. ++ * TODO: Is this enough? What if this async_wait keeps getting called and nothing gets sent? ++ */ ++ while (++eventsProcessed < MAX_EVENTS && utils::pipeHasData(m_subscriptionData->subscription.fd())) { ++ std::lock_guard lock(m_subscriptionData->mutex); // sysrepo-cpp's processEvent and terminate is not thread safe ++ m_subscriptionData->subscription.processEvent([&](const std::optional& notificationTree, const sysrepo::NotificationTimeStamp& time) { ++ (*m_signal)(rousette::restconf::as_restconf_notification( ++ m_subscriptionData->subscription.getSession().getContext(), ++ m_subscriptionData->dataFormat, ++ *notificationTree, ++ time)); ++ }); ++ } ++ ++ // and wait for more ++ awaitNextNotification(); ++ }); ++} ++ ++void DynamicSubscriptionHttpStream::activate() ++{ ++ m_subscriptionData->clientConnected(); ++ EventStream::activate(); ++ awaitNextNotification(); ++} ++ ++std::shared_ptr DynamicSubscriptionHttpStream::create( ++ const nghttp2::asio_http2::server::request& req, ++ const nghttp2::asio_http2::server::response& res, ++ rousette::http::EventStream::Termination& termination, ++ const std::chrono::seconds keepAlivePingInterval, ++ const std::shared_ptr& subscriptionData) ++{ ++ auto signal = std::make_shared(); ++ auto stream = std::shared_ptr(new DynamicSubscriptionHttpStream(req, res, termination, signal, keepAlivePingInterval, subscriptionData)); ++ stream->activate(); ++ return stream; ++} + } +diff --git a/src/restconf/DynamicSubscriptions.h b/src/restconf/DynamicSubscriptions.h +index 366d1a1..1874390 100644 +--- a/src/restconf/DynamicSubscriptions.h ++++ b/src/restconf/DynamicSubscriptions.h +@@ -12,6 +12,7 @@ + #include + #include + #include ++#include "http/EventStream.h" + + namespace libyang { + enum class DataFormat; +@@ -26,17 +27,28 @@ namespace rousette::restconf { + class DynamicSubscriptions { + public: + struct SubscriptionData : public std::enable_shared_from_this { ++ mutable std::mutex mutex; + sysrepo::DynamicSubscription subscription; + libyang::DataFormat dataFormat; ///< Encoding of the notification stream + boost::uuids::uuid uuid; ///< UUID is part of the GET URI, it identifies subscriptions for clients + std::string user; ///< User who initiated the establish-subscription RPC + ++ enum class State { ++ Start, ///< Subscription is ready to be consumed by a client ++ ReceiverActive, ///< Subscription is being consumed by a client ++ Terminating, ///< Subscription is being terminated by the server shutdown ++ } state; ++ + SubscriptionData( + sysrepo::DynamicSubscription sub, + libyang::DataFormat format, + boost::uuids::uuid uuid, + const std::string& user); + ~SubscriptionData(); ++ void clientDisconnected(); ++ void clientConnected(); ++ void terminate(const std::optional& reason = std::nullopt); ++ bool isReadyToAcceptClient() const; + }; + + DynamicSubscriptions(const std::string& streamRootUri); +@@ -54,4 +66,38 @@ private: + + boost::uuids::uuid makeUUID(); + }; ++ ++/** @brief Subscribes to sysrepo's subscribed notification and sends the notifications via HTTP/2 Event stream. ++ * ++ * @see rousette::http::EventStream ++ * @see rousette::http::NotificationStream ++ * */ ++class DynamicSubscriptionHttpStream : public http::EventStream { ++public: ++ ~DynamicSubscriptionHttpStream(); ++ ++ static std::shared_ptr create( ++ const nghttp2::asio_http2::server::request& req, ++ const nghttp2::asio_http2::server::response& res, ++ rousette::http::EventStream::Termination& termination, ++ const std::chrono::seconds keepAlivePingInterval, ++ const std::shared_ptr& subscriptionData); ++ ++private: ++ std::shared_ptr m_subscriptionData; ++ std::shared_ptr m_signal; ++ boost::asio::posix::stream_descriptor m_stream; ++ ++ void awaitNextNotification(); ++ ++protected: ++ DynamicSubscriptionHttpStream( ++ const nghttp2::asio_http2::server::request& req, ++ const nghttp2::asio_http2::server::response& res, ++ rousette::http::EventStream::Termination& termination, ++ std::shared_ptr signal, ++ const std::chrono::seconds keepAlivePingInterval, ++ const std::shared_ptr& subscriptionData); ++ void activate(); ++}; + } +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 616d7d4..2a8ad33 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -1003,27 +1003,40 @@ Server::Server( + + auto streamRequest = asRestconfStreamRequest(req.method(), req.uri().path, req.uri().raw_query); + +- if (auto it = streamRequest.queryParams.find("filter"); it != streamRequest.queryParams.end()) { +- xpathFilter = std::get(it->second); +- } ++ if (auto *request = std::get_if(&streamRequest)) { ++ if (auto sub = m_dynamicSubscriptions.getSubscriptionForUser(request->uuid, sess.getNacmUser())) { ++ if (!sub->isReadyToAcceptClient()) { ++ throw ErrorResponse(409, "application", "resource-denied", "There is already another GET request on this subscription."); ++ } + +- if (auto it = streamRequest.queryParams.find("start-time"); it != streamRequest.queryParams.end()) { +- startTime = libyang::fromYangTimeFormat(std::get(it->second)); +- } +- if (auto it = streamRequest.queryParams.find("stop-time"); it != streamRequest.queryParams.end()) { +- stopTime = libyang::fromYangTimeFormat(std::get(it->second)); +- } ++ DynamicSubscriptionHttpStream::create(req, res, shutdownRequested, keepAlivePingInterval, sub); ++ } else { ++ throw ErrorResponse(404, "application", "invalid-value", "Subscription not found."); ++ } ++ } else if (auto *request = std::get_if(&streamRequest)) { ++ if (auto it = request->queryParams.find("filter"); it != request->queryParams.end()) { ++ xpathFilter = std::get(it->second); ++ } ++ if (auto it = request->queryParams.find("start-time"); it != request->queryParams.end()) { ++ startTime = libyang::fromYangTimeFormat(std::get(it->second)); ++ } ++ if (auto it = request->queryParams.find("stop-time"); it != request->queryParams.end()) { ++ stopTime = libyang::fromYangTimeFormat(std::get(it->second)); ++ } + +- NotificationStream::create( +- req, +- res, +- shutdownRequested, +- keepAlivePingInterval, +- sess, +- streamRequest.encoding, +- xpathFilter, +- startTime, +- stopTime); ++ NotificationStream::create( ++ req, ++ res, ++ shutdownRequested, ++ keepAlivePingInterval, ++ sess, ++ request->encoding, ++ xpathFilter, ++ startTime, ++ stopTime); ++ } else { ++ throw ErrorResponse(500, "protocol", "invalid-value", "Invalid request."); // should not happen ++ } + } catch (const auth::Error& e) { + processAuthError(req, res, e, [&res]() { + res.write_head(401, {TEXT_PLAIN, CORS}); +diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp +index 6ee011c..99c3d1f 100644 +--- a/src/restconf/uri.cpp ++++ b/src/restconf/uri.cpp +@@ -8,6 +8,8 @@ + #include + #include + #include ++#include ++#include + #include + #include + #include +@@ -57,12 +59,30 @@ const auto resources = x3::rule{"resources"} = + ((x3::lit("/") | x3::eps) /* /restconf/ and /restconf */ >> x3::attr(URIPrefix{URIPrefix::Type::RestconfRoot}) >> x3::attr(std::vector{})); + const auto uriGrammar = x3::rule{"grammar"} = x3::lit("/restconf") >> resources; + ++const auto string_to_uuid = [](auto& ctx) { ++ try { ++ _val(ctx) = boost::uuids::string_generator()(_attr(ctx)); ++ _pass(ctx) = true; ++ } catch (const std::runtime_error&) { ++ _pass(ctx) = false; ++ } ++}; ++ ++const auto uuid_impl = x3::rule{"uuid_impl"} = ++ x3::repeat(8)[x3::xdigit] >> x3::char_('-') >> ++ x3::repeat(4)[x3::xdigit] >> x3::char_('-') >> ++ x3::repeat(4)[x3::xdigit] >> x3::char_('-') >> ++ x3::repeat(4)[x3::xdigit] >> x3::char_('-') >> ++ x3::repeat(12)[x3::xdigit]; ++const auto uuid = x3::rule{"uuid"} = uuid_impl[string_to_uuid]; ++const auto subscribedStream = x3::rule{"subscribedStream"} = x3::lit("/subscribed") >> "/" >> uuid; + + const auto netconfStream = x3::rule{"netconfStream"} = + x3::lit("/NETCONF") >> + ((x3::lit("/XML") >> x3::attr(libyang::DataFormat::XML)) | + (x3::lit("/JSON") >> x3::attr(libyang::DataFormat::JSON))); +-const auto streamUriGrammar = x3::rule{"streamsGrammar"} = x3::lit("/streams") >> netconfStream; ++const auto streamUriGrammar = x3::rule>{"streamsGrammar"} = ++ x3::lit("/streams") >> (netconfStream | subscribedStream); + + // clang-format on + } +@@ -674,6 +694,16 @@ NetconfStreamRequest::NetconfStreamRequest(const libyang::DataFormat& encoding) + { + } + ++SubscribedStreamRequest::SubscribedStreamRequest() ++ : uuid(boost::uuids::nil_uuid()) ++{ ++} ++ ++SubscribedStreamRequest::SubscribedStreamRequest(const boost::uuids::uuid& uuid) ++ : uuid(uuid) ++{ ++} ++ + RestconfStreamRequest asRestconfStreamRequest(const std::string& httpMethod, const std::string& uriPath, const std::string& uriQueryString) + { + if (httpMethod != "GET" && httpMethod != "HEAD") { +@@ -685,14 +715,16 @@ RestconfStreamRequest asRestconfStreamRequest(const std::string& httpMethod, con + throw ErrorResponse(404, "application", "invalid-value", "Invalid stream"); + } + +- auto queryParameters = impl::parseQueryParams(uriQueryString); +- if (!queryParameters) { +- throw ErrorResponse(400, "protocol", "invalid-value", "Query parameters syntax error"); +- } ++ if (auto* request = std::get_if(&type.value())) { ++ auto queryParameters = impl::parseQueryParams(uriQueryString); ++ if (!queryParameters) { ++ throw ErrorResponse(400, "protocol", "invalid-value", "Query parameters syntax error"); ++ } + +- validateQueryParametersForStream(*queryParameters); ++ validateQueryParametersForStream(*queryParameters); ++ request->queryParams = *queryParameters; ++ } + +- type->queryParams = *queryParameters; + return *type; + } + +diff --git a/src/restconf/uri.h b/src/restconf/uri.h +index f6c63a7..7893c6f 100644 +--- a/src/restconf/uri.h ++++ b/src/restconf/uri.h +@@ -6,6 +6,7 @@ + + #pragma once + #include ++#include + #include + #include + #include +@@ -205,7 +206,14 @@ struct NetconfStreamRequest { + NetconfStreamRequest(const libyang::DataFormat& encoding); + }; + +-using RestconfStreamRequest = NetconfStreamRequest; ++struct SubscribedStreamRequest { ++ boost::uuids::uuid uuid; ++ ++ SubscribedStreamRequest(); ++ SubscribedStreamRequest(const boost::uuids::uuid& uuid); ++}; ++ ++using RestconfStreamRequest = std::variant; + + RestconfRequest asRestconfRequest(const libyang::Context& ctx, const std::string& httpMethod, const std::string& uriPath, const std::string& uriQueryString = ""); + std::optional asLibyangSchemaNode(const libyang::Context& ctx, const std::vector& pathSegments); +diff --git a/src/restconf/uri_impl.h b/src/restconf/uri_impl.h +index bc36cca..cf57859 100644 +--- a/src/restconf/uri_impl.h ++++ b/src/restconf/uri_impl.h +@@ -67,6 +67,7 @@ BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::PathSegment, apiIdent, keys); + BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::ApiIdentifier, prefix, identifier); + + BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::NetconfStreamRequest, encoding); ++BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::SubscribedStreamRequest, uuid); + + BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::ParenExpr, lhs, rhs); + BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::SlashExpr, lhs, rhs); +diff --git a/src/restconf/utils/io.cpp b/src/restconf/utils/io.cpp +new file mode 100644 +index 0000000..d657096 +--- /dev/null ++++ b/src/restconf/utils/io.cpp +@@ -0,0 +1,30 @@ ++/* ++ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/ ++ * ++ * Written by Tomáš Pecka ++ */ ++ ++#include ++#include "restconf/utils/io.h" ++ ++namespace rousette::restconf::utils { ++bool pipeHasData(const int fd) ++{ ++ pollfd fds = { ++ .fd = fd, ++ .events = POLLIN | POLLHUP, ++ .revents = 0}; ++ ++ return poll(&fds, 1, 0) == 1 && fds.revents & POLLIN; ++} ++ ++bool pipeIsClosedAndNoData(const int fd) ++{ ++ pollfd fds = { ++ .fd = fd, ++ .events = POLLIN | POLLHUP, ++ .revents = 0}; ++ ++ return poll(&fds, 1, 0) == 1 && fds.revents & POLLHUP && !(fds.revents & POLLIN); ++} ++} +diff --git a/src/restconf/utils/io.h b/src/restconf/utils/io.h +new file mode 100644 +index 0000000..ac9f2f8 +--- /dev/null ++++ b/src/restconf/utils/io.h +@@ -0,0 +1,12 @@ ++/* ++ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/ ++ * ++ * Written by Tomáš Pecka ++ */ ++ ++ ++namespace rousette::restconf::utils { ++ ++bool pipeHasData(const int fd); ++bool pipeIsClosedAndNoData(const int fd); ++} +diff --git a/tests/restconf-subscribed-notifications.cpp b/tests/restconf-subscribed-notifications.cpp +index 3e8e011..11dd992 100644 +--- a/tests/restconf-subscribed-notifications.cpp ++++ b/tests/restconf-subscribed-notifications.cpp +@@ -91,6 +91,7 @@ TEST_CASE("RESTCONF subscribed notifications") + sysrepo::setLogLevelStderr(sysrepo::LogLevel::Information); + spdlog::set_level(spdlog::level::trace); + ++ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate); + auto srConn = sysrepo::Connection{}; + auto srSess = srConn.sessionStart(sysrepo::Datastore::Running); + srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset")); +@@ -99,6 +100,19 @@ TEST_CASE("RESTCONF subscribed notifications") + auto server = rousette::restconf::Server{srConn, SERVER_ADDRESS, SERVER_PORT}; + setupRealNacm(srSess); + ++ // parent for nested notification ++ srSess.switchDatastore(sysrepo::Datastore::Operational); ++ srSess.setItem("/example:tlc/list[name='k1']/choice1", "something must me here"); ++ srSess.applyChanges(); ++ std::vector notificationsJSON{ ++ R"({"example:eventA":{"message":"blabla","progress":11}})", ++ R"({"example:eventB":{}})", ++ R"({"example-notif:something-happened":{}})", ++ R"({"example:eventA":{"message":"almost finished","progress":99}})", ++ R"({"example:tlc":{"list":[{"name":"k1","notif":{"message":"nested"}}]}})", ++ }; ++ std::vector> expectations; ++ + RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext()); + + libyang::DataFormat rpcRequestEncoding = libyang::DataFormat::JSON; +@@ -132,6 +146,78 @@ TEST_CASE("RESTCONF subscribed notifications") + } + )###"}); + } ++ ++ SECTION("User DWDM establishes subscription") ++ { ++ std::pair user = AUTH_DWDM; ++ auto uri = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, user, std::nullopt); ++ ++ std::map headers; ++ ++ SECTION("Users who cannot GET") ++ { ++ SECTION("anonymous") { } ++ SECTION("norules") { headers.insert(AUTH_NORULES); } ++ REQUIRE(get(uri, headers) == Response{404, plaintextHeaders, "Subscription not found."}); ++ } ++ ++ SECTION("Users who can GET") ++ { ++ SECTION("root") { headers.insert(AUTH_ROOT); } ++ SECTION("dwdm") { headers.insert(AUTH_DWDM); } ++ REQUIRE(head(uri, headers) == Response{200, eventStreamHeaders, ""}); ++ } ++ ++ SECTION("GET on the same subscription concurently") ++ { ++ std::map headers; ++ std::string response; ++ int status; ++ ++ SECTION("Allowed users") ++ { ++ response = "There is already another GET request on this subscription."; ++ status = 409; ++ ++ SECTION("Same user") ++ { ++ headers.insert(AUTH_DWDM); ++ } ++ SECTION("Different user") ++ { ++ headers.insert(AUTH_ROOT); ++ } ++ } ++ SECTION("Disallowed user") ++ { ++ headers.insert(AUTH_NORULES); ++ response = "Subscription not found."; ++ status = 404; ++ } ++ ++ PREPARE_LOOP_WITH_EXCEPTIONS ++ auto thr = std::jthread(wrap_exceptions_and_asio(bg, io, [&]() { ++ WAIT_UNTIL_SSE_CLIENT_REQUESTS; ++ REQUIRE(get(uri, headers) == Response{status, plaintextHeaders, response}); ++ })); ++ ++ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, {AUTH_DWDM}); ++ RUN_LOOP_WITH_EXCEPTIONS; ++ } ++ ++ SECTION("GET on the same subscription sequentially") ++ { ++ boost::asio::io_service io; ++ { ++ std::binary_semaphore requestSent(0); // SSEClient needs to notify when the request is sent, but we don't care about it here. ++ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, {AUTH_DWDM}); ++ } ++ { ++ std::binary_semaphore requestSent(0); ++ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, {AUTH_DWDM}); ++ } ++ } ++ } + } + + SECTION("Invalid establish-subscription requests") +@@ -209,6 +295,12 @@ TEST_CASE("RESTCONF subscribed notifications") + rpcRequestEncoding = libyang::DataFormat::XML; + } + } ++ ++ EXPECT_NOTIFICATION(notificationsJSON[0], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[1], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[2], seq2); ++ EXPECT_NOTIFICATION(notificationsJSON[3], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[4], seq1); + } + + SECTION("JSON stream") +@@ -220,6 +312,11 @@ TEST_CASE("RESTCONF subscribed notifications") + rpcRequestAuthHeader = std::nullopt; + rpcRequestEncoding = libyang::DataFormat::JSON; + rpcSubscriptionEncoding = "encode-json"; ++ ++ EXPECT_NOTIFICATION(notificationsJSON[0], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[1], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[3], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[4], seq1); + } + + SECTION("Content-type with set encode leaf") +@@ -244,10 +341,112 @@ TEST_CASE("RESTCONF subscribed notifications") + rpcRequestEncoding = libyang::DataFormat::XML; + } + } ++ ++ EXPECT_NOTIFICATION(notificationsJSON[0], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[1], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[2], seq2); ++ EXPECT_NOTIFICATION(notificationsJSON[3], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[4], seq1); + } + } + + auto uri = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding); + REQUIRE(std::regex_match(uri, std::regex("/streams/subscribed/"s + uuidV4Regex))); ++ ++ PREPARE_LOOP_WITH_EXCEPTIONS ++ ++ // Here's how these two threads work together. ++ // ++ // The main test thread (this one): ++ // - sets up all the expectations ++ // - has an HTTP client which calls/spends the expectations based on the incoming SSE data ++ // - blocks while it runs the ASIO event loop ++ // ++ // The auxiliary thread (the notificationThread): ++ // - waits for the HTTP client having issued its long-lived HTTP GET ++ // - sends a bunch of notifications to sysrepo ++ // - waits for all the expectations getting spent, and then terminates the ASIO event loop cleanly ++ ++ std::jthread notificationThread = std::jthread(wrap_exceptions_and_asio(bg, io, [&]() { ++ auto notifSession = sysrepo::Connection{}.sessionStart(); ++ auto ctx = notifSession.getContext(); ++ ++ WAIT_UNTIL_SSE_CLIENT_REQUESTS; ++ ++ SEND_NOTIFICATION(notificationsJSON[0]); ++ SEND_NOTIFICATION(notificationsJSON[1]); ++ std::this_thread::sleep_for(500ms); // simulate some delays; server might be slow in creating notifications, client should still remain connected ++ SEND_NOTIFICATION(notificationsJSON[2]); ++ SEND_NOTIFICATION(notificationsJSON[3]); ++ std::this_thread::sleep_for(500ms); ++ SEND_NOTIFICATION(notificationsJSON[4]); ++ ++ // once the main thread has processed all the notifications, stop the ASIO loop ++ waitForCompletionAndBitMore(seq1); ++ waitForCompletionAndBitMore(seq2); ++ })); ++ ++ std::map streamHeaders; ++ if (rpcRequestAuthHeader) { ++ streamHeaders.insert(*rpcRequestAuthHeader); ++ } ++ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, streamHeaders); ++ RUN_LOOP_WITH_EXCEPTIONS; + } + } ++ ++TEST_CASE("Terminating server under notification load") ++{ ++ trompeloeil::sequence seq1; ++ sysrepo::setLogLevelStderr(sysrepo::LogLevel::Information); ++ spdlog::set_level(spdlog::level::trace); ++ ++ auto srConn = sysrepo::Connection{}; ++ auto srSess = srConn.sessionStart(sysrepo::Datastore::Running); ++ srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset")); ++ ++ auto nacmGuard = manageNacm(srSess); ++ auto server = std::make_unique(srConn, SERVER_ADDRESS, SERVER_PORT); ++ setupRealNacm(srSess); ++ ++ RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext()); ++ constexpr auto notif = R"({"example:eventB":{}})"; ++ ++ std::optional rpcSubscriptionEncoding; ++ std::optional> rpcRequestAuthHeader; ++ ++ std::pair auth = AUTH_ROOT; ++ auto uri = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, auth, std::nullopt); ++ ++ PREPARE_LOOP_WITH_EXCEPTIONS; ++ ++ std::atomic serverRunning = true; ++ std::atomic notificationsReceived = 0; ++ constexpr size_t NOTIFICATIONS_BEFORE_TERMINATE = 50; ++ ++ auto notificationThread = std::jthread(wrap_exceptions_and_asio(bg, io, [&]() { ++ auto notifSession = sysrepo::Connection{}.sessionStart(); ++ auto ctx = notifSession.getContext(); ++ ++ WAIT_UNTIL_SSE_CLIENT_REQUESTS; ++ ++ while (serverRunning) { ++ SEND_NOTIFICATION(notif); ++ } ++ })); ++ ++ auto serverShutdownThread = std::jthread([&]() { ++ while (notificationsReceived <= NOTIFICATIONS_BEFORE_TERMINATE) { ++ // A condition variable would be more elegant, but this is just a test... ++ std::this_thread::sleep_for(20ms); ++ } ++ server.reset(); ++ serverRunning = false; ++ }); ++ ++ netconfWatcher.setDataFormat(libyang::DataFormat::JSON); ++ REQUIRE_CALL(netconfWatcher, data(notif)).IN_SEQUENCE(seq1).TIMES(AT_LEAST(NOTIFICATIONS_BEFORE_TERMINATE)).LR_SIDE_EFFECT(notificationsReceived++); ++ REQUIRE_CALL(netconfWatcher, data(R"({ietf-subscribed-notifications:subscription-terminated":{"id":1,"reason":"no-such-subscription"}})")).IN_SEQUENCE(seq1).TIMES(AT_MOST(1)); ++ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, {auth}); ++ RUN_LOOP_WITH_EXCEPTIONS; ++} +diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp +index 38ca80f..2ad244f 100644 +--- a/tests/uri-parser.cpp ++++ b/tests/uri-parser.cpp +@@ -7,6 +7,7 @@ + */ + + #include "trompeloeil_doctest.h" ++#include + #include + #include + #include +@@ -1042,8 +1043,11 @@ TEST_CASE("URI path parser") + + SECTION("filter") + { ++ using rousette::restconf::NetconfStreamRequest; ++ + auto resp = asRestconfStreamRequest("GET", "/streams/NETCONF/XML", "filter=/asd"); +- REQUIRE(resp.queryParams == QueryParams({{"filter", "/asd"s}})); ++ REQUIRE(std::holds_alternative(resp)); ++ REQUIRE(std::get(resp).queryParams == QueryParams({{"filter", "/asd"s}})); + + REQUIRE_THROWS_WITH_AS(asRestconfRequest(ctx, "GET", "/restconf/data/example:ordered-lists", "filter=something"), + serializeErrorResponse(400, "protocol", "invalid-value", "Query parameter 'filter' can be used only with streams").c_str(), +@@ -1052,8 +1056,11 @@ TEST_CASE("URI path parser") + + SECTION("start-time") + { ++ using rousette::restconf::NetconfStreamRequest; ++ + auto resp = asRestconfStreamRequest("GET", "/streams/NETCONF/XML", "start-time=2024-01-01T01:01:01Z"); +- REQUIRE(resp.queryParams == QueryParams({{"start-time", "2024-01-01T01:01:01Z"s}})); ++ REQUIRE(std::holds_alternative(resp)); ++ REQUIRE(std::get(resp).queryParams == QueryParams({{"start-time", "2024-01-01T01:01:01Z"s}})); + + REQUIRE_THROWS_WITH_AS(asRestconfRequest(ctx, "GET", "/restconf/data/example:ordered-lists", "start-time=2024-01-01T01:01:01Z"), + serializeErrorResponse(400, "protocol", "invalid-value", "Query parameter 'start-time' can be used only with streams").c_str(), +@@ -1062,8 +1069,11 @@ TEST_CASE("URI path parser") + + SECTION("stop-time") + { ++ using rousette::restconf::NetconfStreamRequest; ++ + auto resp = asRestconfStreamRequest("GET", "/streams/NETCONF/XML", "stop-time=2024-01-01T01:01:01Z"); +- REQUIRE(resp.queryParams == QueryParams({{"stop-time", "2024-01-01T01:01:01Z"s}})); ++ REQUIRE(std::holds_alternative(resp)); ++ REQUIRE(std::get(resp).queryParams == QueryParams({{"stop-time", "2024-01-01T01:01:01Z"s}})); + + REQUIRE_THROWS_WITH_AS(asRestconfRequest(ctx, "GET", "/restconf/data/example:ordered-lists", "stop-time=2024-01-01T01:01:01Z"), + serializeErrorResponse(400, "protocol", "invalid-value", "Query parameter 'stop-time' can be used only with streams").c_str(), +@@ -1079,18 +1089,27 @@ TEST_CASE("URI path parser") + SECTION("Streams") + { + using rousette::restconf::asRestconfStreamRequest; +- using rousette::restconf::RestconfStreamRequest; ++ using rousette::restconf::NetconfStreamRequest; ++ using rousette::restconf::SubscribedStreamRequest; + + { + auto req = asRestconfStreamRequest("GET", "/streams/NETCONF/XML", ""); +- REQUIRE(req.encoding == libyang::DataFormat::XML); +- REQUIRE(req.queryParams.empty()); ++ REQUIRE(std::holds_alternative(req)); ++ REQUIRE(std::get(req).encoding == libyang::DataFormat::XML); ++ REQUIRE(std::get(req).queryParams.empty()); + } + + { + auto req = asRestconfStreamRequest("GET", "/streams/NETCONF/JSON", ""); +- REQUIRE(req.encoding == libyang::DataFormat::JSON); +- REQUIRE(req.queryParams.empty()); ++ REQUIRE(std::holds_alternative(req)); ++ REQUIRE(std::get(req).encoding == libyang::DataFormat::JSON); ++ REQUIRE(std::get(req).queryParams.empty()); ++ } ++ ++ { ++ auto req = asRestconfStreamRequest("GET", "/streams/subscribed/a40f0a50-061a-4832-a6ac-c4db7df81a10", ""); ++ REQUIRE(std::holds_alternative(req)); ++ REQUIRE(std::get(req).uuid == boost::uuids::string_generator()("a40f0a50-061a-4832-a6ac-c4db7df81a10")); + } + + REQUIRE_THROWS_WITH_AS(asRestconfStreamRequest("GET", "/streams/NETCONF", ""), +@@ -1105,6 +1124,12 @@ TEST_CASE("URI path parser") + REQUIRE_THROWS_WITH_AS(asRestconfStreamRequest("GET", "/streams/NETCONF/XM", ""), + serializeErrorResponse(404, "application", "invalid-value", "Invalid stream").c_str(), + rousette::restconf::ErrorResponse); ++ REQUIRE_THROWS_WITH_AS(asRestconfStreamRequest("GET", "/streams/subscribed", ""), ++ serializeErrorResponse(404, "application", "invalid-value", "Invalid stream").c_str(), ++ rousette::restconf::ErrorResponse); ++ REQUIRE_THROWS_WITH_AS(asRestconfStreamRequest("GET", "/streams/subscribed/123-456-789", ""), ++ serializeErrorResponse(404, "application", "invalid-value", "Invalid stream").c_str(), ++ rousette::restconf::ErrorResponse); + + + for (const auto& httpMethod : {"OPTIONS", "PATCH", "DELETE", "POST", "PUT"}) { +-- +2.43.0 + diff --git a/package/rousette/0029-restconf-dynamic-subscriptions-shutdown-method.patch b/package/rousette/0029-restconf-dynamic-subscriptions-shutdown-method.patch new file mode 100644 index 00000000..009ad42b --- /dev/null +++ b/package/rousette/0029-restconf-dynamic-subscriptions-shutdown-method.patch @@ -0,0 +1,69 @@ +From 9c39cdd8a8a6bb8fb94d4f943313ec8c4a1823d7 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Mon, 10 Nov 2025 19:58:36 +0100 +Subject: [PATCH 29/38] restconf: dynamic subscriptions shutdown method +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Dynamic subscriptions now have a proper shutdown method that sets a +Terminating state on all the subscriptions so that each subscription +cannot call its terminate() method. + +Change-Id: I4532bbafff7c7386a18ed0636f1102842d2749fa +Signed-off-by: Mattias Walström +--- + src/restconf/DynamicSubscriptions.cpp | 10 ++++++++++ + src/restconf/DynamicSubscriptions.h | 1 + + src/restconf/Server.cpp | 1 + + 3 files changed, 12 insertions(+) + +diff --git a/src/restconf/DynamicSubscriptions.cpp b/src/restconf/DynamicSubscriptions.cpp +index 7f4b171..9c195f4 100644 +--- a/src/restconf/DynamicSubscriptions.cpp ++++ b/src/restconf/DynamicSubscriptions.cpp +@@ -82,6 +82,16 @@ DynamicSubscriptions::DynamicSubscriptions(const std::string& streamRootUri) + + DynamicSubscriptions::~DynamicSubscriptions() = default; + ++void DynamicSubscriptions::stop() ++{ ++ std::lock_guard lock(m_mutex); ++ for (const auto& [uuid, subscriptionData] : m_subscriptions) { ++ // We are already terminating and will destroy via destructor, calls to terminate() will do nothing ++ std::lock_guard lock(subscriptionData->mutex); ++ subscriptionData->state = SubscriptionData::State::Terminating; ++ } ++} ++ + void DynamicSubscriptions::establishSubscription(sysrepo::Session& session, const libyang::DataFormat requestEncoding, const libyang::DataNode& rpcInput, libyang::DataNode& rpcOutput) + { + // Generate a new UUID associated with the subscription. The UUID will be used as a part of the URI so that the URI is not predictable (RFC 8650, section 5) +diff --git a/src/restconf/DynamicSubscriptions.h b/src/restconf/DynamicSubscriptions.h +index 1874390..0309cad 100644 +--- a/src/restconf/DynamicSubscriptions.h ++++ b/src/restconf/DynamicSubscriptions.h +@@ -55,6 +55,7 @@ public: + ~DynamicSubscriptions(); + std::shared_ptr getSubscriptionForUser(const boost::uuids::uuid& uuid, const std::optional& user); + void establishSubscription(sysrepo::Session& session, const libyang::DataFormat requestEncoding, const libyang::DataNode& rpcInput, libyang::DataNode& rpcOutput); ++ void stop(); + + private: + std::mutex m_mutex; ///< Lock for shared data (subscriptions storage and uuid generator) +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 2a8ad33..5486e99 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -884,6 +884,7 @@ void Server::stop() + }); + + shutdownRequested(); ++ m_dynamicSubscriptions.stop(); + } + + void Server::join() +-- +2.43.0 + diff --git a/package/rousette/0030-restconf-terminate-subsc.-notification-streams-after.patch b/package/rousette/0030-restconf-terminate-subsc.-notification-streams-after.patch new file mode 100644 index 00000000..494ed1a1 --- /dev/null +++ b/package/rousette/0030-restconf-terminate-subsc.-notification-streams-after.patch @@ -0,0 +1,319 @@ +From 3154d9209dc876fee43d9e83907ab44e06008c65 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Tue, 14 Oct 2025 12:25:20 +0200 +Subject: [PATCH 30/38] restconf: terminate subsc. notification streams after + 60s with no client +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +We allow anonymous users to create notification streams. However, such +users will not have permissions to delete the subscriptions which opens +a potential for resource exhaustion. To prevent this, we now terminate +the notification streams after 60 seconds of inactivity. + +The period is configurable via the Server constructor, but the default +is 60 seconds. + +Change-Id: Id399d108069704fa8ab79b9ef6ab855dd62cfc8d +Signed-off-by: Mattias Walström +--- + src/restconf/DynamicSubscriptions.cpp | 72 ++++++++++++++++++--- + src/restconf/DynamicSubscriptions.h | 22 ++++++- + src/restconf/Server.cpp | 5 +- + src/restconf/Server.h | 3 +- + tests/restconf-subscribed-notifications.cpp | 34 ++++++++++ + 5 files changed, 122 insertions(+), 14 deletions(-) + +diff --git a/src/restconf/DynamicSubscriptions.cpp b/src/restconf/DynamicSubscriptions.cpp +index 9c195f4..f6defb4 100644 +--- a/src/restconf/DynamicSubscriptions.cpp ++++ b/src/restconf/DynamicSubscriptions.cpp +@@ -74,9 +74,11 @@ sysrepo::DynamicSubscription makeStreamSubscription(sysrepo::Session& session, c + + namespace rousette::restconf { + +-DynamicSubscriptions::DynamicSubscriptions(const std::string& streamRootUri) ++DynamicSubscriptions::DynamicSubscriptions(const std::string& streamRootUri, const nghttp2::asio_http2::server::http2& server, const std::chrono::seconds inactivityTimeout) + : m_restconfStreamUri(streamRootUri) ++ , m_server(server) + , m_uuidGenerator(boost::uuids::random_generator()) ++ , m_inactivityTimeout(inactivityTimeout) + { + } + +@@ -86,9 +88,7 @@ void DynamicSubscriptions::stop() + { + std::lock_guard lock(m_mutex); + for (const auto& [uuid, subscriptionData] : m_subscriptions) { +- // We are already terminating and will destroy via destructor, calls to terminate() will do nothing +- std::lock_guard lock(subscriptionData->mutex); +- subscriptionData->state = SubscriptionData::State::Terminating; ++ subscriptionData->stop(); + } + } + +@@ -110,7 +110,10 @@ void DynamicSubscriptions::establishSubscription(sysrepo::Session& session, cons + std::move(sub), + dataFormat, + uuid, +- *session.getNacmUser()); ++ *session.getNacmUser(), ++ *m_server.io_services().at(0), ++ m_inactivityTimeout, ++ [this, subId = sub.subscriptionId()]() { terminateSubscription(subId); }); + } catch (const sysrepo::ErrorWithCode& e) { + throw ErrorResponse(400, "application", "invalid-attribute", e.what()); + } +@@ -161,18 +164,29 @@ DynamicSubscriptions::SubscriptionData::SubscriptionData( + sysrepo::DynamicSubscription sub, + libyang::DataFormat format, + boost::uuids::uuid uuid, +- const std::string& user) ++ const std::string& user, ++ boost::asio::io_context& io, ++ std::chrono::seconds inactivityTimeout, ++ std::function onClientInactiveCallback) + : subscription(std::move(sub)) + , dataFormat(format) + , uuid(uuid) + , user(user) + , state(State::Start) ++ , inactivityTimeout(inactivityTimeout) ++ , clientInactiveTimer(io) ++ , onClientInactiveCallback(std::move(onClientInactiveCallback)) + { + spdlog::debug("{}: created", fmt::streamed(*this)); ++ ++ std::lock_guard lock(mutex); ++ inactivityStart(); + } + + DynamicSubscriptions::SubscriptionData::~SubscriptionData() + { ++ std::lock_guard lock(mutex); ++ inactivityCancel(); + terminate(); + } + +@@ -186,12 +200,14 @@ void DynamicSubscriptions::SubscriptionData::clientDisconnected() + } + + state = State::Start; ++ inactivityStart(); + } + + void DynamicSubscriptions::SubscriptionData::clientConnected() + { + spdlog::debug("{}: client connected", fmt::streamed(*this)); + std::lock_guard lock(mutex); ++ inactivityCancel(); + state = State::ReceiverActive; + } + +@@ -201,10 +217,37 @@ bool DynamicSubscriptions::SubscriptionData::isReadyToAcceptClient() const + return state == State::Start; + } + +-void DynamicSubscriptions::SubscriptionData::terminate(const std::optional& reason) ++/** @pre The mutex must be locked by the caller. */ ++void DynamicSubscriptions::SubscriptionData::inactivityStart() + { +- std::lock_guard lock(mutex); ++ if (state == State::Terminating) { ++ return; ++ } ++ ++ spdlog::trace("{}: starting inactivity timer", fmt::streamed(*this)); ++ ++ clientInactiveTimer.expires_after(inactivityTimeout); ++ clientInactiveTimer.async_wait([weakThis = weak_from_this()](const boost::system::error_code& err) { ++ auto self = weakThis.lock(); ++ if (!self || err == boost::asio::error::operation_aborted) { ++ return; ++ } + ++ spdlog::trace("{}: client inactive, perform inactivity callback", fmt::streamed(*self)); ++ self->onClientInactiveCallback(); ++ }); ++} ++ ++/** @pre The mutex must be locked by the caller. */ ++void DynamicSubscriptions::SubscriptionData::inactivityCancel() ++{ ++ spdlog::trace("{}: cancelling inactivity timer", fmt::streamed(*this)); ++ clientInactiveTimer.cancel(); ++} ++ ++/** @pre The mutex must be locked by the caller. */ ++void DynamicSubscriptions::SubscriptionData::terminate(const std::optional& reason) ++{ + // already terminating, do nothing + if (state == State::Terminating) { + return; +@@ -219,6 +262,14 @@ void DynamicSubscriptions::SubscriptionData::terminate(const std::optionalterminate("ietf-subscribed-notifications:no-such-subscription"); }, ++ [this]() { ++ std::lock_guard lock(m_subscriptionData->mutex); ++ m_subscriptionData->terminate("ietf-subscribed-notifications:no-such-subscription"); ++ }, + [this]() { m_subscriptionData->clientDisconnected(); }) + , m_subscriptionData(subscriptionData) + , m_signal(signal) +diff --git a/src/restconf/DynamicSubscriptions.h b/src/restconf/DynamicSubscriptions.h +index 0309cad..0422947 100644 +--- a/src/restconf/DynamicSubscriptions.h ++++ b/src/restconf/DynamicSubscriptions.h +@@ -18,6 +18,10 @@ namespace libyang { + enum class DataFormat; + } + ++namespace nghttp2::asio_http2::server { ++class http2; ++} ++ + namespace rousette::restconf { + + /** Dynamic subscriptions manager. +@@ -39,19 +43,31 @@ public: + Terminating, ///< Subscription is being terminated by the server shutdown + } state; + ++ std::chrono::seconds inactivityTimeout; ///< Time after which the subscription is considered inactive and can be removed if no client is connected ++ boost::asio::steady_timer clientInactiveTimer; ///< Timer used for auto-destruction of subscriptions that are unused ++ std::function onClientInactiveCallback; ++ + SubscriptionData( + sysrepo::DynamicSubscription sub, + libyang::DataFormat format, + boost::uuids::uuid uuid, +- const std::string& user); ++ const std::string& user, ++ boost::asio::io_context& io, ++ std::chrono::seconds inactivityTimeout, ++ std::function onClientInactiveCallback); + ~SubscriptionData(); + void clientDisconnected(); + void clientConnected(); + void terminate(const std::optional& reason = std::nullopt); + bool isReadyToAcceptClient() const; ++ void stop(); ++ ++ private: ++ void inactivityStart(); ++ void inactivityCancel(); + }; + +- DynamicSubscriptions(const std::string& streamRootUri); ++ DynamicSubscriptions(const std::string& streamRootUri, const nghttp2::asio_http2::server::http2& server, const std::chrono::seconds inactivityTimeout); + ~DynamicSubscriptions(); + std::shared_ptr getSubscriptionForUser(const boost::uuids::uuid& uuid, const std::optional& user); + void establishSubscription(sysrepo::Session& session, const libyang::DataFormat requestEncoding, const libyang::DataNode& rpcInput, libyang::DataNode& rpcOutput); +@@ -60,8 +76,10 @@ public: + private: + std::mutex m_mutex; ///< Lock for shared data (subscriptions storage and uuid generator) + std::string m_restconfStreamUri; ++ const nghttp2::asio_http2::server::http2& m_server; + std::map> m_subscriptions; + boost::uuids::random_generator m_uuidGenerator; ++ std::chrono::seconds m_inactivityTimeout; + + void terminateSubscription(const uint32_t subId); + +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 5486e99..0c7e855 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -911,11 +911,12 @@ Server::Server( + const std::string& address, + const std::string& port, + const std::chrono::milliseconds timeout, +- const std::chrono::seconds keepAlivePingInterval) ++ const std::chrono::seconds keepAlivePingInterval, ++ const std::chrono::seconds subNotifInactivityTimeout) + : m_monitoringSession(conn.sessionStart(sysrepo::Datastore::Operational)) + , nacm(conn) + , server{std::make_unique()} +- , m_dynamicSubscriptions(netconfStreamRoot) ++ , m_dynamicSubscriptions(netconfStreamRoot, *server, subNotifInactivityTimeout) + , dwdmEvents{std::make_unique(conn.sessionStart())} + { + server->num_threads(1); // we only use one thread for the server, so we can call join() right away +diff --git a/src/restconf/Server.h b/src/restconf/Server.h +index c98f736..b4db948 100644 +--- a/src/restconf/Server.h ++++ b/src/restconf/Server.h +@@ -33,7 +33,8 @@ public: + const std::string& address, + const std::string& port, + const std::chrono::milliseconds timeout = std::chrono::milliseconds{0}, +- const std::chrono::seconds keepAlivePingInterval = std::chrono::seconds{55}); ++ const std::chrono::seconds keepAlivePingInterval = std::chrono::seconds{55}, ++ const std::chrono::seconds subNotifInactivityTimeout = std::chrono::seconds{60}); + ~Server(); + void join(); + void stop(); +diff --git a/tests/restconf-subscribed-notifications.cpp b/tests/restconf-subscribed-notifications.cpp +index 11dd992..68e9286 100644 +--- a/tests/restconf-subscribed-notifications.cpp ++++ b/tests/restconf-subscribed-notifications.cpp +@@ -450,3 +450,37 @@ TEST_CASE("Terminating server under notification load") + SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, {auth}); + RUN_LOOP_WITH_EXCEPTIONS; + } ++ ++TEST_CASE("Cleaning up inactive subscriptions") ++{ ++ trompeloeil::sequence seq1, seq2; ++ sysrepo::setLogLevelStderr(sysrepo::LogLevel::Information); ++ spdlog::set_level(spdlog::level::trace); ++ ++ auto srConn = sysrepo::Connection{}; ++ auto srSess = srConn.sessionStart(sysrepo::Datastore::Running); ++ srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset")); ++ ++ RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext()); ++ ++ auto nacmGuard = manageNacm(srSess); ++ constexpr auto inactivityTimeout = 2s; ++ auto server = rousette::restconf::Server{srConn, SERVER_ADDRESS, SERVER_PORT, 0ms, 55s, inactivityTimeout}; ++ ++ auto uri = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, {AUTH_ROOT}, std::nullopt); ++ ++ SECTION("Client connects and disconnects") ++ { ++ PREPARE_LOOP_WITH_EXCEPTIONS ++ auto thr = std::jthread(wrap_exceptions_and_asio(bg, io, [&]() { ++ WAIT_UNTIL_SSE_CLIENT_REQUESTS; ++ std::this_thread::sleep_for(1s); ++ })); ++ ++ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, {AUTH_ROOT}); ++ RUN_LOOP_WITH_EXCEPTIONS; ++ } ++ ++ std::this_thread::sleep_for(inactivityTimeout + 1500ms); ++ REQUIRE(get(uri, {AUTH_ROOT}) == Response{404, plaintextHeaders, "Subscription not found."}); ++} +-- +2.43.0 + diff --git a/package/rousette/0031-restconf-add-kill-delete-subscription-RPCs.patch b/package/rousette/0031-restconf-add-kill-delete-subscription-RPCs.patch new file mode 100644 index 00000000..49589dd5 --- /dev/null +++ b/package/rousette/0031-restconf-add-kill-delete-subscription-RPCs.patch @@ -0,0 +1,360 @@ +From 22cfcbf52be604afe0caafc892f4b7befe03b9db Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Tue, 8 Apr 2025 17:00:59 +0200 +Subject: [PATCH 31/38] restconf: add {kill,delete}-subscription RPCs +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Add implementations for deleting and killing subscriptions [1,2]. + +Rousette allows establishing subscriptions by anonymous users. Anonymous +users can not call delete-subscription RPC though because we do not know +if it is the same anonymous user who initiated the subscription. +The subscriptions are killed after a minute of inactivity, so the +subscription will get deleted by rousette automatically after the client +disconnects. + +[1] https://datatracker.ietf.org/doc/html/rfc8639.html#section-2.4.4 +[2] https://datatracker.ietf.org/doc/html/rfc8639.html#section-2.4.5 + +Change-Id: I443d6fd88f2797045b5ae0a7d7e1a761fe74e7cb +Signed-off-by: Mattias Walström +--- + src/restconf/DynamicSubscriptions.cpp | 44 +++++ + src/restconf/DynamicSubscriptions.h | 2 + + src/restconf/Server.cpp | 4 + + tests/restconf-subscribed-notifications.cpp | 178 +++++++++++++++++++- + 4 files changed, 222 insertions(+), 6 deletions(-) + +diff --git a/src/restconf/DynamicSubscriptions.cpp b/src/restconf/DynamicSubscriptions.cpp +index f6defb4..fabc226 100644 +--- a/src/restconf/DynamicSubscriptions.cpp ++++ b/src/restconf/DynamicSubscriptions.cpp +@@ -119,6 +119,32 @@ void DynamicSubscriptions::establishSubscription(sysrepo::Session& session, cons + } + } + ++void DynamicSubscriptions::deleteSubscription(sysrepo::Session& session, const libyang::DataFormat, const libyang::DataNode& rpcInput, libyang::DataNode&) ++{ ++ const auto isKill = rpcInput.findPath("/ietf-subscribed-notifications:kill-subscription") != std::nullopt; ++ const auto subId = std::get(rpcInput.findPath("id")->asTerm().value()); ++ ++ // The RPC is already NACM-checked. Now, retrieve the subscription, if the current user has permission for it ++ auto subscriptionData = getSubscriptionForUser(subId, session.getNacmUser()); ++ if (!subscriptionData) { ++ throw ErrorResponse(404, "application", "invalid-value", "Subscription not found.", rpcInput.path()); ++ } ++ ++ /* I *think* the RFC 8639 says that root can use delete-subscription only for subscriptions created by root. ++ * This checks if the current user is root and the subscription was created by a different user. If so, reject the request. ++ */ ++ if (!isKill && session.getNacmUser() == session.getNacmRecoveryUser() && subscriptionData->user != session.getNacmRecoveryUser()) { ++ //FIXME: pass additional error info (rc:yang-data delete-subscription-error-info from RFC 8639) ++ throw ErrorResponse(400, "application", "invalid-attribute", "Trying to delete subscription not created by root. Use kill-subscription instead.", rpcInput.path()); ++ } ++ ++ spdlog::debug("Terminating subscription id {}", subId); ++ subscriptionData->subscription.terminate("ietf-subscribed-notifications:no-such-subscription"); ++ ++ std::unique_lock lock(m_mutex); ++ m_subscriptions.erase(subscriptionData->uuid); ++} ++ + void DynamicSubscriptions::terminateSubscription(const uint32_t subId) + { + std::lock_guard lock(m_mutex); +@@ -153,6 +179,24 @@ std::shared_ptr DynamicSubscriptions::ge + return nullptr; + } + ++/** @brief Returns the subscription data for the given subscription id and user. ++ * ++ * @param id The ID of the subscription. ++ * @return A shared pointer to the SubscriptionData object if found and user is the one who established the subscription (or NACM recovery user), otherwise nullptr. ++ */ ++std::shared_ptr DynamicSubscriptions::getSubscriptionForUser(const uint32_t subId, const std::optional& user) ++{ ++ std::unique_lock lock(m_mutex); ++ ++ // FIXME: This is linear search. Maybe use something like boost::multi_index? ++ if (auto it = std::find_if(m_subscriptions.begin(), m_subscriptions.end(), [subId](const auto& entry) { return entry.second->subscription.subscriptionId() == subId; }); ++ it != m_subscriptions.end() && (it->second->user == user || user == sysrepo::Session::getNacmRecoveryUser())) { ++ return it->second; ++ } ++ ++ return nullptr; ++} ++ + boost::uuids::uuid DynamicSubscriptions::makeUUID() + { + // uuid generator instance accesses must be synchronized (https://www.boost.org/doc/libs/1_88_0/libs/uuid/doc/html/uuid.html#design_notes) +diff --git a/src/restconf/DynamicSubscriptions.h b/src/restconf/DynamicSubscriptions.h +index 0422947..8057b17 100644 +--- a/src/restconf/DynamicSubscriptions.h ++++ b/src/restconf/DynamicSubscriptions.h +@@ -70,8 +70,10 @@ public: + DynamicSubscriptions(const std::string& streamRootUri, const nghttp2::asio_http2::server::http2& server, const std::chrono::seconds inactivityTimeout); + ~DynamicSubscriptions(); + std::shared_ptr getSubscriptionForUser(const boost::uuids::uuid& uuid, const std::optional& user); ++ std::shared_ptr getSubscriptionForUser(const uint32_t subId, const std::optional& user); + void establishSubscription(sysrepo::Session& session, const libyang::DataFormat requestEncoding, const libyang::DataNode& rpcInput, libyang::DataNode& rpcOutput); + void stop(); ++ void deleteSubscription(sysrepo::Session& session, const libyang::DataFormat, const libyang::DataNode& rpcInput, libyang::DataNode&); + + private: + std::mutex m_mutex; ///< Lock for shared data (subscriptions storage and uuid generator) +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 0c7e855..04cdf56 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -447,6 +447,10 @@ std::optional processInternalRPC(sysrepo::Session& sess, liby + const std::map handlers{ + {"/ietf-subscribed-notifications:establish-subscription", + {"/ietf-subscribed-notifications:filters", [&dynamicSubscriptions](auto&&... args) { return dynamicSubscriptions.establishSubscription(std::forward(args)...); }}}, ++ {"/ietf-subscribed-notifications:kill-subscription", ++ {std::nullopt, [&dynamicSubscriptions](auto&&... args) { return dynamicSubscriptions.deleteSubscription(std::forward(args)...); }}}, ++ {"/ietf-subscribed-notifications:delete-subscription", ++ {std::nullopt, [&dynamicSubscriptions](auto&&... args) { return dynamicSubscriptions.deleteSubscription(std::forward(args)...); }}}, + }; + + const auto rpcPath = rpcInput.path(); +diff --git a/tests/restconf-subscribed-notifications.cpp b/tests/restconf-subscribed-notifications.cpp +index 68e9286..7c7de4b 100644 +--- a/tests/restconf-subscribed-notifications.cpp ++++ b/tests/restconf-subscribed-notifications.cpp +@@ -24,8 +24,13 @@ constexpr auto uuidV4Regex = "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[89a + using namespace std::chrono_literals; + using namespace std::string_literals; + ++struct EstablishSubscriptionResult { ++ uint32_t id; ++ std::string url; ++}; ++ + /** @brief Calls establish-subscription rpc, returns the url of the stream associated with the created subscription */ +-std::string establishSubscription( ++EstablishSubscriptionResult establishSubscription( + const libyang::Context& ctx, + const libyang::DataFormat rpcEncoding, + const std::optional>& rpcRequestAuthHeader, +@@ -79,10 +84,16 @@ std::string establishSubscription( + auto reply = ctx.newPath("/ietf-subscribed-notifications:establish-subscription"); + REQUIRE(reply.parseOp(resp.data, rpcEncoding, libyang::OperationType::ReplyRestconf).tree); + ++ auto idNode = reply.findPath("id", libyang::InputOutputNodes::Output); ++ REQUIRE(idNode); ++ + auto urlNode = reply.findPath("ietf-restconf-subscribed-notifications:uri", libyang::InputOutputNodes::Output); + REQUIRE(urlNode); + +- return urlNode->asTerm().valueStr(); ++ return { ++ std::get(idNode->asTerm().value()), ++ urlNode->asTerm().valueStr(), ++ }; + } + + TEST_CASE("RESTCONF subscribed notifications") +@@ -150,7 +161,7 @@ TEST_CASE("RESTCONF subscribed notifications") + SECTION("User DWDM establishes subscription") + { + std::pair user = AUTH_DWDM; +- auto uri = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, user, std::nullopt); ++ auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, user, std::nullopt); + + std::map headers; + +@@ -350,7 +361,7 @@ TEST_CASE("RESTCONF subscribed notifications") + } + } + +- auto uri = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding); ++ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding); + REQUIRE(std::regex_match(uri, std::regex("/streams/subscribed/"s + uuidV4Regex))); + + PREPARE_LOOP_WITH_EXCEPTIONS +@@ -393,6 +404,161 @@ TEST_CASE("RESTCONF subscribed notifications") + SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, streamHeaders); + RUN_LOOP_WITH_EXCEPTIONS; + } ++ ++ SECTION("delete-subscription") ++ { ++ std::optional expectedResponse; ++ std::map headers; ++ headers.insert(CONTENT_TYPE_JSON); ++ ++ SECTION("Subscription created by dwdm") ++ { ++ rpcRequestAuthHeader = AUTH_DWDM; ++ ++ SECTION("dwdm (author) can delete") ++ { ++ headers.insert(AUTH_DWDM); ++ expectedResponse = Response{204, noContentTypeHeaders, ""}; ++ } ++ ++ SECTION("Anonymous cannot delete anything because of NACM") ++ { ++ expectedResponse = Response{403, jsonHeaders, R"({ ++ "ietf-restconf:errors": { ++ "error": [ ++ { ++ "error-type": "application", ++ "error-tag": "access-denied", ++ "error-path": "/ietf-subscribed-notifications:delete-subscription", ++ "error-message": "Access denied." ++ } ++ ] ++ } ++} ++)"}; ++ } ++ ++ SECTION("norules user") ++ { ++ headers.insert(AUTH_NORULES); ++ expectedResponse = Response{404, jsonHeaders, R"({ ++ "ietf-restconf:errors": { ++ "error": [ ++ { ++ "error-type": "application", ++ "error-tag": "invalid-value", ++ "error-path": "/ietf-subscribed-notifications:delete-subscription", ++ "error-message": "Subscription not found." ++ } ++ ] ++ } ++} ++)"}; ++ } ++ ++ SECTION("root user") ++ { ++ headers.insert(AUTH_ROOT); ++ expectedResponse = Response{400, jsonHeaders, R"({ ++ "ietf-restconf:errors": { ++ "error": [ ++ { ++ "error-type": "application", ++ "error-tag": "invalid-attribute", ++ "error-path": "/ietf-subscribed-notifications:delete-subscription", ++ "error-message": "Trying to delete subscription not created by root. Use kill-subscription instead." ++ } ++ ] ++ } ++} ++)"}; ++ } ++ ++ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding); ++ auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}"; ++ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:delete-subscription", headers, body) == expectedResponse); ++ } ++ ++ SECTION("Anonymous user cannot delete subscription craeted by anonymous user") ++ { ++ rpcRequestAuthHeader = std::nullopt; ++ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding); ++ auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}"; ++ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:delete-subscription", headers, body) == Response{403, jsonHeaders, R"({ ++ "ietf-restconf:errors": { ++ "error": [ ++ { ++ "error-type": "application", ++ "error-tag": "access-denied", ++ "error-path": "/ietf-subscribed-notifications:delete-subscription", ++ "error-message": "Access denied." ++ } ++ ] ++ } ++} ++)"}); ++ } ++ } ++ ++ SECTION("kill-subscription") ++ { ++ std::optional expectedResponse; ++ std::map headers; ++ headers.insert(CONTENT_TYPE_JSON); ++ ++ SECTION("User cannot kill") ++ { ++ SECTION("dwdm (author)") ++ { ++ headers.insert(AUTH_DWDM); ++ } ++ ++ SECTION("anonymous") ++ { ++ } ++ ++ expectedResponse = Response{403, jsonHeaders, R"({ ++ "ietf-restconf:errors": { ++ "error": [ ++ { ++ "error-type": "application", ++ "error-tag": "access-denied", ++ "error-path": "/ietf-subscribed-notifications:kill-subscription", ++ "error-message": "Access denied." ++ } ++ ] ++ } ++} ++)"}; ++ } ++ ++ SECTION("root") ++ { ++ headers.insert(AUTH_ROOT); ++ expectedResponse = Response{204, noContentTypeHeaders, ""}; ++ } ++ ++ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding); ++ auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}"; ++ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:kill-subscription", headers, body) == expectedResponse); ++ } ++ ++ SECTION("Invalid kill/delete-subscription requests") ++ { ++ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:kill-subscription", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"ietf-subscribed-notifications:input": {}})") == Response{400, jsonHeaders, R"({ ++ "ietf-restconf:errors": { ++ "error": [ ++ { ++ "error-type": "protocol", ++ "error-tag": "invalid-value", ++ "error-path": "/ietf-subscribed-notifications:kill-subscription", ++ "error-message": "Mandatory node \"id\" instance does not exist." ++ } ++ ] ++ } ++} ++)"}); ++ } + } + + TEST_CASE("Terminating server under notification load") +@@ -416,7 +582,7 @@ TEST_CASE("Terminating server under notification load") + std::optional> rpcRequestAuthHeader; + + std::pair auth = AUTH_ROOT; +- auto uri = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, auth, std::nullopt); ++ auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, auth, std::nullopt); + + PREPARE_LOOP_WITH_EXCEPTIONS; + +@@ -467,7 +633,7 @@ TEST_CASE("Cleaning up inactive subscriptions") + constexpr auto inactivityTimeout = 2s; + auto server = rousette::restconf::Server{srConn, SERVER_ADDRESS, SERVER_PORT, 0ms, 55s, inactivityTimeout}; + +- auto uri = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, {AUTH_ROOT}, std::nullopt); ++ auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, {AUTH_ROOT}, std::nullopt); + + SECTION("Client connects and disconnects") + { +-- +2.43.0 + diff --git a/package/rousette/0032-restconf-add-subscribed-notifications-filtering.patch b/package/rousette/0032-restconf-add-subscribed-notifications-filtering.patch new file mode 100644 index 00000000..86a6f7c0 --- /dev/null +++ b/package/rousette/0032-restconf-add-subscribed-notifications-filtering.patch @@ -0,0 +1,232 @@ +From c5558b12ee67280b6281b639c3d8652cec3c7fa6 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Wed, 19 Mar 2025 14:39:05 +0100 +Subject: [PATCH 32/38] restconf: add subscribed notifications filtering +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Implement filtering of subscribed notifications using xpath and subtree +filters. + +Change-Id: Iee973a6a2d03b4a0d90f952afe5436ce701787ae +Signed-off-by: Mattias Walström +--- + CMakeLists.txt | 2 +- + src/restconf/DynamicSubscriptions.cpp | 13 ++++- + src/restconf/Server.cpp | 2 +- + tests/restconf-subscribed-notifications.cpp | 65 ++++++++++++++++++--- + 4 files changed, 70 insertions(+), 12 deletions(-) + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 5d3b7ce..28696b3 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -217,7 +217,7 @@ if(BUILD_TESTING) + --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-interfaces@2018-02-20.yang + --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-ip@2018-02-22.yang + --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-network-instance@2019-01-21.yang +- --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-subscribed-notifications@2019-09-09.yang --enable-feature replay --enable-feature encode-xml --enable-feature encode-json ++ --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-subscribed-notifications@2019-09-09.yang --enable-feature replay --enable-feature encode-xml --enable-feature encode-json --enable-feature xpath --enable-feature subtree + --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-restconf-subscribed-notifications@2019-11-17.yang + --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example.yang --enable-feature f1 + --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-delete.yang +diff --git a/src/restconf/DynamicSubscriptions.cpp b/src/restconf/DynamicSubscriptions.cpp +index fabc226..a13d344 100644 +--- a/src/restconf/DynamicSubscriptions.cpp ++++ b/src/restconf/DynamicSubscriptions.cpp +@@ -59,13 +59,22 @@ sysrepo::DynamicSubscription makeStreamSubscription(sysrepo::Session& session, c + } + + if (rpcInput.findPath("stream-filter-name")) { +- throw rousette::restconf::ErrorResponse(400, "application", "invalid-attribute", "Stream filtering is not supported"); ++ /* TODO: This requires support for modifying subscriptions first; a change of entry in filters container must change all ++ * subscriptions using this stream-filter, see for instance https://datatracker.ietf.org/doc/html/rfc8639.html#section-2.7.2 */ ++ throw rousette::restconf::ErrorResponse(400, "application", "invalid-attribute", "Stream filtering with predefined filters is not supported"); + } + + auto stopTime = optionalTime(rpcInput, "stop-time"); + ++ std::optional> filter; ++ if (auto node = rpcInput.findPath("stream-xpath-filter")) { ++ filter = node->asTerm().valueStr(); ++ } else if (auto node = rpcInput.findPath("stream-subtree-filter")) { ++ filter = node->asAny(); ++ } ++ + return session.subscribeNotifications( +- std::nullopt /* TODO xpath filter */, ++ filter, + streamNode->asTerm().valueStr(), + stopTime, + std::nullopt /* TODO replayStart */); +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 04cdf56..76603b3 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -932,7 +932,7 @@ Server::Server( + {"ietf-netconf", "", {}}, + {"ietf-yang-library", "2019-01-04", {}}, + {"ietf-yang-patch", "2017-02-22", {}}, +- {"ietf-subscribed-notifications", "2019-09-09", {"encode-xml", "encode-json"}}, ++ {"ietf-subscribed-notifications", "2019-09-09", {"encode-xml", "encode-json", "xpath", "subtree"}}, + {"ietf-restconf-subscribed-notifications", "2019-11-17", {}}, + }) { + if (auto mod = m_monitoringSession.getContext().getModuleImplemented(module)) { +diff --git a/tests/restconf-subscribed-notifications.cpp b/tests/restconf-subscribed-notifications.cpp +index 7c7de4b..7fcf549 100644 +--- a/tests/restconf-subscribed-notifications.cpp ++++ b/tests/restconf-subscribed-notifications.cpp +@@ -34,7 +34,8 @@ EstablishSubscriptionResult establishSubscription( + const libyang::Context& ctx, + const libyang::DataFormat rpcEncoding, + const std::optional>& rpcRequestAuthHeader, +- const std::optional& encodingLeafValue) ++ const std::optional& encodingLeafValue, ++ const std::variant& filter) + { + constexpr auto jsonPrefix = "ietf-subscribed-notifications"; + constexpr auto xmlNamespace = "urn:ietf:params:xml:ns:yang:ietf-subscribed-notifications"; +@@ -56,6 +57,14 @@ EstablishSubscriptionResult establishSubscription( + rpcTree.newPath("encoding", *encodingLeafValue); + } + ++ if (std::holds_alternative(filter)) { ++ rpcTree.newPath("stream-xpath-filter", std::get(filter)); ++ } ++ ++ if (std::holds_alternative(filter)) { ++ rpcTree.newPath2("stream-subtree-filter", std::get(filter)); ++ } ++ + switch (rpcEncoding) { + case libyang::DataFormat::JSON: + requestHeaders.insert(CONTENT_TYPE_JSON); +@@ -128,6 +137,7 @@ TEST_CASE("RESTCONF subscribed notifications") + + libyang::DataFormat rpcRequestEncoding = libyang::DataFormat::JSON; + std::optional rpcSubscriptionEncoding; ++ std::variant rpcFilter; + std::optional> rpcRequestAuthHeader; + + SECTION("NACM authorization") +@@ -161,7 +171,7 @@ TEST_CASE("RESTCONF subscribed notifications") + SECTION("User DWDM establishes subscription") + { + std::pair user = AUTH_DWDM; +- auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, user, std::nullopt); ++ auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, user, std::nullopt, rpcFilter); + + std::map headers; + +@@ -277,6 +287,23 @@ TEST_CASE("RESTCONF subscribed notifications") + ] + } + } ++)###"}); ++ ++ srSess.switchDatastore(sysrepo::Datastore::Operational); ++ srSess.setItem("/ietf-subscribed-notifications:filters/stream-filter[name='xyz']/stream-xpath-filter", "/example:eventA"); ++ srSess.applyChanges(); ++ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription", {CONTENT_TYPE_JSON}, R"###({ "ietf-subscribed-notifications:input": { "stream": "NETCONF", "stream-filter-name": "xyz" } })###") ++ == Response{400, jsonHeaders, R"###({ ++ "ietf-restconf:errors": { ++ "error": [ ++ { ++ "error-type": "application", ++ "error-tag": "invalid-attribute", ++ "error-message": "Stream filtering with predefined filters is not supported" ++ } ++ ] ++ } ++} + )###"}); + } + +@@ -330,6 +357,28 @@ TEST_CASE("RESTCONF subscribed notifications") + EXPECT_NOTIFICATION(notificationsJSON[4], seq1); + } + ++ SECTION("XPath filter set") ++ { ++ rpcRequestAuthHeader = AUTH_ROOT; ++ rpcRequestEncoding = libyang::DataFormat::JSON; ++ rpcFilter = "/example:eventA | /example:eventB"; ++ rpcSubscriptionEncoding = "encode-json"; ++ EXPECT_NOTIFICATION(notificationsJSON[0], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[1], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[3], seq1); ++ } ++ ++ SECTION("Subtree filter set") ++ { ++ rpcRequestAuthHeader = AUTH_ROOT; ++ rpcRequestEncoding = libyang::DataFormat::JSON; ++ // Constructing the filter as XML is only an implementation detail. The tree is then constructed as JSON in establishSubscription ++ rpcFilter = libyang::XML{""}; ++ rpcSubscriptionEncoding = "encode-json"; ++ EXPECT_NOTIFICATION(notificationsJSON[0], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[3], seq1); ++ } ++ + SECTION("Content-type with set encode leaf") + { + rpcRequestAuthHeader = AUTH_ROOT; +@@ -361,7 +410,7 @@ TEST_CASE("RESTCONF subscribed notifications") + } + } + +- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding); ++ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter); + REQUIRE(std::regex_match(uri, std::regex("/streams/subscribed/"s + uuidV4Regex))); + + PREPARE_LOOP_WITH_EXCEPTIONS +@@ -474,7 +523,7 @@ TEST_CASE("RESTCONF subscribed notifications") + )"}; + } + +- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding); ++ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter); + auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}"; + REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:delete-subscription", headers, body) == expectedResponse); + } +@@ -482,7 +531,7 @@ TEST_CASE("RESTCONF subscribed notifications") + SECTION("Anonymous user cannot delete subscription craeted by anonymous user") + { + rpcRequestAuthHeader = std::nullopt; +- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding); ++ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter); + auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}"; + REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:delete-subscription", headers, body) == Response{403, jsonHeaders, R"({ + "ietf-restconf:errors": { +@@ -538,7 +587,7 @@ TEST_CASE("RESTCONF subscribed notifications") + expectedResponse = Response{204, noContentTypeHeaders, ""}; + } + +- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding); ++ auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter); + auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}"; + REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:kill-subscription", headers, body) == expectedResponse); + } +@@ -582,7 +631,7 @@ TEST_CASE("Terminating server under notification load") + std::optional> rpcRequestAuthHeader; + + std::pair auth = AUTH_ROOT; +- auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, auth, std::nullopt); ++ auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, auth, std::nullopt, {}); + + PREPARE_LOOP_WITH_EXCEPTIONS; + +@@ -633,7 +682,7 @@ TEST_CASE("Cleaning up inactive subscriptions") + constexpr auto inactivityTimeout = 2s; + auto server = rousette::restconf::Server{srConn, SERVER_ADDRESS, SERVER_PORT, 0ms, 55s, inactivityTimeout}; + +- auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, {AUTH_ROOT}, std::nullopt); ++ auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, {AUTH_ROOT}, std::nullopt, {}); + + SECTION("Client connects and disconnects") + { +-- +2.43.0 + diff --git a/package/rousette/0033-cmake-wrap-long-line.patch b/package/rousette/0033-cmake-wrap-long-line.patch new file mode 100644 index 00000000..d4850a26 --- /dev/null +++ b/package/rousette/0033-cmake-wrap-long-line.patch @@ -0,0 +1,36 @@ +From fe91af1eb38888d7eacb36cb5a299bd3163f7c60 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Tue, 14 Oct 2025 17:22:00 +0200 +Subject: [PATCH 33/38] cmake: wrap long line +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Change-Id: Id94e18d581dbfbe2e6d386be6a232544f9344eec +Signed-off-by: Mattias Walström +--- + CMakeLists.txt | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 28696b3..3d79dad 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -217,7 +217,12 @@ if(BUILD_TESTING) + --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-interfaces@2018-02-20.yang + --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-ip@2018-02-22.yang + --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-network-instance@2019-01-21.yang +- --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-subscribed-notifications@2019-09-09.yang --enable-feature replay --enable-feature encode-xml --enable-feature encode-json --enable-feature xpath --enable-feature subtree ++ --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-subscribed-notifications@2019-09-09.yang ++ --enable-feature replay ++ --enable-feature encode-xml ++ --enable-feature encode-json ++ --enable-feature xpath ++ --enable-feature subtree + --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-restconf-subscribed-notifications@2019-11-17.yang + --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example.yang --enable-feature f1 + --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-delete.yang +-- +2.43.0 + diff --git a/package/rousette/0034-restconf-support-replays-in-subscribed-notifications.patch b/package/rousette/0034-restconf-support-replays-in-subscribed-notifications.patch new file mode 100644 index 00000000..c3d211e1 --- /dev/null +++ b/package/rousette/0034-restconf-support-replays-in-subscribed-notifications.patch @@ -0,0 +1,322 @@ +From 817debab72d10f2f30e83f1630bebdcdf3ec9455 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Mon, 10 Mar 2025 15:33:08 +0100 +Subject: [PATCH 34/38] restconf: support replays in subscribed notifications +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Change-Id: Idfc3ceb99c8111c5575c1c8b94d539f234fa43be +Signed-off-by: Mattias Walström +--- + CMakeLists.txt | 1 + + src/restconf/DynamicSubscriptions.cpp | 24 ++++- + src/restconf/Server.cpp | 2 +- + tests/restconf-subscribed-notifications.cpp | 102 ++++++++++++++++++-- + 4 files changed, 115 insertions(+), 14 deletions(-) + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 3d79dad..f98a64b 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -223,6 +223,7 @@ if(BUILD_TESTING) + --enable-feature encode-json + --enable-feature xpath + --enable-feature subtree ++ --enable-feature replay + --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-restconf-subscribed-notifications@2019-11-17.yang + --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example.yang --enable-feature f1 + --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-delete.yang +diff --git a/src/restconf/DynamicSubscriptions.cpp b/src/restconf/DynamicSubscriptions.cpp +index a13d344..9be3e92 100644 +--- a/src/restconf/DynamicSubscriptions.cpp ++++ b/src/restconf/DynamicSubscriptions.cpp +@@ -50,7 +50,7 @@ libyang::DataFormat getEncoding(const libyang::DataNode& rpcInput, const libyang + return requestEncoding; + } + +-sysrepo::DynamicSubscription makeStreamSubscription(sysrepo::Session& session, const libyang::DataNode& rpcInput) ++sysrepo::DynamicSubscription makeStreamSubscription(sysrepo::Session& session, const libyang::DataNode& rpcInput, libyang::DataNode& rpcOutput) + { + auto streamNode = rpcInput.findPath("stream"); + +@@ -73,11 +73,27 @@ sysrepo::DynamicSubscription makeStreamSubscription(sysrepo::Session& session, c + filter = node->asAny(); + } + +- return session.subscribeNotifications( ++ std::optional replayStartTime; ++ if (auto node = rpcInput.findPath("replay-start-time")) { ++ replayStartTime = libyang::fromYangTimeFormat(node->asTerm().valueStr()); ++ } ++ ++ auto sub = session.subscribeNotifications( + filter, + streamNode->asTerm().valueStr(), + stopTime, +- std::nullopt /* TODO replayStart */); ++ replayStartTime); ++ ++ /* Node replay-start-time-revision should be set only if time was revised to be different than the requested start time, ++ * i.e. when the "replay-start-time" contains a value that is earlier than what a publisher's retained history. ++ * Then the actual publisher's revised start time MUST be set in the returned "replay-start-time-revision" object. ++ * (RFC 8639, 2.4.2.1) ++ * */ ++ if (auto replayStartTimeRevision = sub.replayStartTime(); replayStartTimeRevision && replayStartTime) { ++ rpcOutput.newPath("replay-start-time-revision", libyang::yangTimeFormat(*replayStartTimeRevision, libyang::TimezoneInterpretation::Local), libyang::CreationOptions::Output); ++ } ++ ++ return sub; + } + } + +@@ -109,7 +125,7 @@ void DynamicSubscriptions::establishSubscription(sysrepo::Session& session, cons + auto dataFormat = getEncoding(rpcInput, requestEncoding); + + try { +- auto sub = makeStreamSubscription(session, rpcInput); ++ auto sub = makeStreamSubscription(session, rpcInput, rpcOutput); + + rpcOutput.newPath("id", std::to_string(sub.subscriptionId()), libyang::CreationOptions::Output); + rpcOutput.newPath("ietf-restconf-subscribed-notifications:uri", m_restconfStreamUri + "subscribed/" + boost::uuids::to_string(uuid), libyang::CreationOptions::Output); +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 76603b3..d166236 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -932,7 +932,7 @@ Server::Server( + {"ietf-netconf", "", {}}, + {"ietf-yang-library", "2019-01-04", {}}, + {"ietf-yang-patch", "2017-02-22", {}}, +- {"ietf-subscribed-notifications", "2019-09-09", {"encode-xml", "encode-json", "xpath", "subtree"}}, ++ {"ietf-subscribed-notifications", "2019-09-09", {"encode-xml", "encode-json", "xpath", "subtree", "replay"}}, + {"ietf-restconf-subscribed-notifications", "2019-11-17", {}}, + }) { + if (auto mod = m_monitoringSession.getContext().getModuleImplemented(module)) { +diff --git a/tests/restconf-subscribed-notifications.cpp b/tests/restconf-subscribed-notifications.cpp +index 7fcf549..d42fa96 100644 +--- a/tests/restconf-subscribed-notifications.cpp ++++ b/tests/restconf-subscribed-notifications.cpp +@@ -18,7 +18,7 @@ static const auto SERVER_PORT = "10092"; + #include "tests/pretty_printers.h" + + #define SEND_NOTIFICATION(DATA) notifSession.sendNotification(*ctx.parseOp(DATA, libyang::DataFormat::JSON, libyang::OperationType::NotificationYang).op, sysrepo::Wait::No); +- ++#define REPLAY_COMPLETED trompeloeil::re(R"(^\{"ietf-subscribed-notifications:replay-completed":\{"id":[0-9]+\}\}$)") + constexpr auto uuidV4Regex = "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[89abAB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}"; + + using namespace std::chrono_literals; +@@ -27,6 +27,7 @@ using namespace std::string_literals; + struct EstablishSubscriptionResult { + uint32_t id; + std::string url; ++ std::optional replayStartTimeRevision; + }; + + /** @brief Calls establish-subscription rpc, returns the url of the stream associated with the created subscription */ +@@ -35,7 +36,8 @@ EstablishSubscriptionResult establishSubscription( + const libyang::DataFormat rpcEncoding, + const std::optional>& rpcRequestAuthHeader, + const std::optional& encodingLeafValue, +- const std::variant& filter) ++ const std::variant& filter, ++ const std::optional& replayStartTime) + { + constexpr auto jsonPrefix = "ietf-subscribed-notifications"; + constexpr auto xmlNamespace = "urn:ietf:params:xml:ns:yang:ietf-subscribed-notifications"; +@@ -65,6 +67,10 @@ EstablishSubscriptionResult establishSubscription( + rpcTree.newPath2("stream-subtree-filter", std::get(filter)); + } + ++ if (replayStartTime) { ++ rpcTree.newPath("replay-start-time", libyang::yangTimeFormat(*replayStartTime, libyang::TimezoneInterpretation::Local)); ++ } ++ + switch (rpcEncoding) { + case libyang::DataFormat::JSON: + requestHeaders.insert(CONTENT_TYPE_JSON); +@@ -99,9 +105,15 @@ EstablishSubscriptionResult establishSubscription( + auto urlNode = reply.findPath("ietf-restconf-subscribed-notifications:uri", libyang::InputOutputNodes::Output); + REQUIRE(urlNode); + ++ std::optional replayStartTimeRevision; ++ if (auto node = reply.findPath("ietf-subscribed-notifications:replay-start-time-revision", libyang::InputOutputNodes::Output)) { ++ replayStartTimeRevision = libyang::fromYangTimeFormat(node->asTerm().valueStr()); ++ } ++ + return { + std::get(idNode->asTerm().value()), + urlNode->asTerm().valueStr(), ++ replayStartTimeRevision + }; + } + +@@ -131,6 +143,8 @@ TEST_CASE("RESTCONF subscribed notifications") + R"({"example:eventA":{"message":"almost finished","progress":99}})", + R"({"example:tlc":{"list":[{"name":"k1","notif":{"message":"nested"}}]}})", + }; ++ const std::string notificationForReplayJSON = R"({"example:eventA":{"message":"this-should-be-sent-very-early","progress":0}})"; ++ + std::vector> expectations; + + RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext()); +@@ -138,7 +152,10 @@ TEST_CASE("RESTCONF subscribed notifications") + libyang::DataFormat rpcRequestEncoding = libyang::DataFormat::JSON; + std::optional rpcSubscriptionEncoding; + std::variant rpcFilter; ++ std::optional rpcReplayStart; + std::optional> rpcRequestAuthHeader; ++ std::pair replayedNotificationSendInterval; // bounds for replayed notification event time ++ bool shouldReviseStartTime = false; + + SECTION("NACM authorization") + { +@@ -171,7 +188,7 @@ TEST_CASE("RESTCONF subscribed notifications") + SECTION("User DWDM establishes subscription") + { + std::pair user = AUTH_DWDM; +- auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, user, std::nullopt, rpcFilter); ++ auto [id, uri, replayStartTimeRevision] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, user, std::nullopt, rpcFilter, rpcReplayStart); + + std::map headers; + +@@ -304,6 +321,23 @@ TEST_CASE("RESTCONF subscribed notifications") + ] + } + } ++)###"}); ++ ++ // replay-start-time > stop-time ++ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription", ++ {CONTENT_TYPE_JSON}, ++ R"###({ "ietf-subscribed-notifications:input": { "stream": "NETCONF", "replay-start-time": "2000-11-11T11:22:33Z", "stop-time": "2000-01-01T00:00:00Z" } })###") ++ == Response{400, jsonHeaders, R"###({ ++ "ietf-restconf:errors": { ++ "error": [ ++ { ++ "error-type": "application", ++ "error-tag": "invalid-attribute", ++ "error-message": "Couldn't create notification subscription: SR_ERR_INVAL_ARG\u000A Specified \"stop-time\" is earlier than \"start-time\". (SR_ERR_INVAL_ARG)" ++ } ++ ] ++ } ++} + )###"}); + } + +@@ -379,6 +413,48 @@ TEST_CASE("RESTCONF subscribed notifications") + EXPECT_NOTIFICATION(notificationsJSON[3], seq1); + } + ++ SECTION("Replays") ++ { ++ // Announce replay support and send one notification before the client connects ++ srConn.setModuleReplaySupport("example", true); ++ ++ { ++ auto notifSession = sysrepo::Connection{}.sessionStart(); ++ auto ctx = notifSession.getContext(); ++ replayedNotificationSendInterval.first = std::chrono::system_clock::now(); ++ SEND_NOTIFICATION(notificationForReplayJSON); ++ replayedNotificationSendInterval.second = std::chrono::system_clock::now(); ++ } ++ ++ rpcRequestAuthHeader = AUTH_ROOT; ++ rpcRequestEncoding = libyang::DataFormat::JSON; ++ rpcSubscriptionEncoding = "encode-json"; ++ ++ SECTION("replay-start-time-revision is announced to the client") ++ { ++ rpcReplayStart = std::chrono::system_clock::now() - 666s /* Reasonable time in the past, earlier than the replayed notification was sent */; ++ ++ EXPECT_NOTIFICATION(notificationForReplayJSON, seq1); ++ EXPECT_NOTIFICATION(REPLAY_COMPLETED, seq1); ++ shouldReviseStartTime = true; ++ } ++ ++ SECTION("replay-start-time-revision not announced") ++ { ++ /* Ask for replay since the time when the first notification was sent. This should not revise the start time ++ * because we are not asking for the start *before* our history. (RFC 8639, 2.4.2.1) */ ++ rpcReplayStart = replayedNotificationSendInterval.second; ++ ++ EXPECT_NOTIFICATION(REPLAY_COMPLETED, seq1); ++ } ++ ++ EXPECT_NOTIFICATION(notificationsJSON[0], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[1], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[2], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[3], seq1); ++ EXPECT_NOTIFICATION(notificationsJSON[4], seq1); ++ } ++ + SECTION("Content-type with set encode leaf") + { + rpcRequestAuthHeader = AUTH_ROOT; +@@ -410,9 +486,17 @@ TEST_CASE("RESTCONF subscribed notifications") + } + } + +- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter); ++ auto [id, uri, replayStartTimeRevision] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter, rpcReplayStart); + REQUIRE(std::regex_match(uri, std::regex("/streams/subscribed/"s + uuidV4Regex))); + ++ if (shouldReviseStartTime) { ++ REQUIRE(replayStartTimeRevision); ++ REQUIRE(replayedNotificationSendInterval.first <= *replayStartTimeRevision); ++ REQUIRE(*replayStartTimeRevision <= replayedNotificationSendInterval.second); ++ } else { ++ REQUIRE(!replayStartTimeRevision); ++ } ++ + PREPARE_LOOP_WITH_EXCEPTIONS + + // Here's how these two threads work together. +@@ -523,7 +607,7 @@ TEST_CASE("RESTCONF subscribed notifications") + )"}; + } + +- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter); ++ auto [id, uri, replayStartTimeRevision] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter, rpcReplayStart); + auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}"; + REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:delete-subscription", headers, body) == expectedResponse); + } +@@ -531,7 +615,7 @@ TEST_CASE("RESTCONF subscribed notifications") + SECTION("Anonymous user cannot delete subscription craeted by anonymous user") + { + rpcRequestAuthHeader = std::nullopt; +- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter); ++ auto [id, uri, replayStartTimeRevision] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter, rpcReplayStart); + auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}"; + REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:delete-subscription", headers, body) == Response{403, jsonHeaders, R"({ + "ietf-restconf:errors": { +@@ -587,7 +671,7 @@ TEST_CASE("RESTCONF subscribed notifications") + expectedResponse = Response{204, noContentTypeHeaders, ""}; + } + +- auto [id, uri] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter); ++ auto [id, uri, replayStartTimeRevision] = establishSubscription(srSess.getContext(), rpcRequestEncoding, rpcRequestAuthHeader, rpcSubscriptionEncoding, rpcFilter, rpcReplayStart); + auto body = R"({"ietf-subscribed-notifications:input": { "id": )" + std::to_string(id) + "}}"; + REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:kill-subscription", headers, body) == expectedResponse); + } +@@ -631,7 +715,7 @@ TEST_CASE("Terminating server under notification load") + std::optional> rpcRequestAuthHeader; + + std::pair auth = AUTH_ROOT; +- auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, auth, std::nullopt, {}); ++ auto [id, uri, replayStartTimeRevision] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, auth, std::nullopt, {}, std::nullopt); + + PREPARE_LOOP_WITH_EXCEPTIONS; + +@@ -682,7 +766,7 @@ TEST_CASE("Cleaning up inactive subscriptions") + constexpr auto inactivityTimeout = 2s; + auto server = rousette::restconf::Server{srConn, SERVER_ADDRESS, SERVER_PORT, 0ms, 55s, inactivityTimeout}; + +- auto [id, uri] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, {AUTH_ROOT}, std::nullopt, {}); ++ auto [id, uri, replayStartTimeRevision] = establishSubscription(srSess.getContext(), libyang::DataFormat::JSON, {AUTH_ROOT}, std::nullopt, {}, std::nullopt); + + SECTION("Client connects and disconnects") + { +-- +2.43.0 + diff --git a/package/rousette/0035-restconf-refactor-getting-datastore-from-string.patch b/package/rousette/0035-restconf-refactor-getting-datastore-from-string.patch new file mode 100644 index 00000000..42fe80fc --- /dev/null +++ b/package/rousette/0035-restconf-refactor-getting-datastore-from-string.patch @@ -0,0 +1,95 @@ +From 7d6b1a463fdacefaf7a7057564f6f4d4a8cb4632 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= +Date: Tue, 25 Mar 2025 11:06:26 +0100 +Subject: [PATCH 35/38] restconf: refactor getting datastore from string +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +This will be useful when processing YANG push requests. + +Change-Id: I1e76335c8d674e81890d6cd1e973adaf521eb205 +Signed-off-by: Mattias Walström +--- + src/restconf/uri.cpp | 19 +++++-------------- + src/restconf/utils/sysrepo.cpp | 16 ++++++++++++++++ + src/restconf/utils/sysrepo.h | 1 + + 3 files changed, 22 insertions(+), 14 deletions(-) + +diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp +index 99c3d1f..6143435 100644 +--- a/src/restconf/uri.cpp ++++ b/src/restconf/uri.cpp +@@ -17,6 +17,7 @@ + #include "restconf/Exceptions.h" + #include "restconf/uri.h" + #include "restconf/uri_impl.h" ++#include "restconf/utils/sysrepo.h" + #include "restconf/utils/yang.h" + + using namespace std::string_literals; +@@ -298,21 +299,11 @@ std::optional datastoreFromApiIdentifier(const boost::option + return std::nullopt; + } + +- if (*datastore->prefix == "ietf-datastores") { +- if (datastore->identifier == "running") { +- return sysrepo::Datastore::Running; +- } else if (datastore->identifier == "operational") { +- return sysrepo::Datastore::Operational; +- } else if (datastore->identifier == "candidate") { +- return sysrepo::Datastore::Candidate; +- } else if (datastore->identifier == "startup") { +- return sysrepo::Datastore::Startup; +- } else if (datastore->identifier == "factory-default") { +- return sysrepo::Datastore::FactoryDefault; +- } ++ try { ++ return datastoreFromString(*datastore->prefix + ":"s + datastore->identifier); ++ } catch (const std::runtime_error&) { ++ throw ErrorResponse(400, "application", "operation-failed", "Unsupported datastore " + *datastore->prefix + ":" + datastore->identifier); + } +- +- throw ErrorResponse(400, "application", "operation-failed", "Unsupported datastore " + *datastore->prefix + ":" + datastore->identifier); + } + + std::optional> getModuleOrSubmodule(const libyang::Context& ctx, const std::string& name, const std::optional& revision) +diff --git a/src/restconf/utils/sysrepo.cpp b/src/restconf/utils/sysrepo.cpp +index 622ab27..9f14cc7 100644 +--- a/src/restconf/utils/sysrepo.cpp ++++ b/src/restconf/utils/sysrepo.cpp +@@ -22,4 +22,20 @@ ScopedDatastoreSwitch::~ScopedDatastoreSwitch() + m_session.switchDatastore(m_oldDatastore); + } + ++sysrepo::Datastore datastoreFromString(const std::string& datastore) ++{ ++ if (datastore == "ietf-datastores:running") { ++ return sysrepo::Datastore::Running; ++ } else if (datastore == "ietf-datastores:operational") { ++ return sysrepo::Datastore::Operational; ++ } else if (datastore == "ietf-datastores:candidate") { ++ return sysrepo::Datastore::Candidate; ++ } else if (datastore == "ietf-datastores:startup") { ++ return sysrepo::Datastore::Startup; ++ } else if (datastore == "ietf-datastores:factory-default") { ++ return sysrepo::Datastore::FactoryDefault; ++ } ++ ++ throw std::runtime_error("Unknown datastore '" + datastore + "'"); ++} + } +diff --git a/src/restconf/utils/sysrepo.h b/src/restconf/utils/sysrepo.h +index da6c359..0eced9e 100644 +--- a/src/restconf/utils/sysrepo.h ++++ b/src/restconf/utils/sysrepo.h +@@ -26,4 +26,5 @@ public: + ScopedDatastoreSwitch& operator=(ScopedDatastoreSwitch&&) = delete; + }; + ++sysrepo::Datastore datastoreFromString(const std::string& datastore); + } +-- +2.43.0 + diff --git a/package/rousette/0036-build-remove-duplicate-line.patch b/package/rousette/0036-build-remove-duplicate-line.patch new file mode 100644 index 00000000..2b3090e1 --- /dev/null +++ b/package/rousette/0036-build-remove-duplicate-line.patch @@ -0,0 +1,31 @@ +From c1d5d143020ddc51c3886b82e87d463cf3211fca Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Fri, 21 Nov 2025 10:44:47 +0100 +Subject: [PATCH 36/38] build: remove duplicate line +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Fixes: 817debab restconf: support replays in subscribed notifications +Change-Id: I4d2ea8429a4cb7b77f8739aff7a26a03cf535756 +Signed-off-by: Mattias Walström +--- + CMakeLists.txt | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index f98a64b..3d79dad 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -223,7 +223,6 @@ if(BUILD_TESTING) + --enable-feature encode-json + --enable-feature xpath + --enable-feature subtree +- --enable-feature replay + --install ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-restconf-subscribed-notifications@2019-11-17.yang + --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example.yang --enable-feature f1 + --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-delete.yang +-- +2.43.0 + diff --git a/package/rousette/0037-remove-obsolete-targets.patch b/package/rousette/0037-remove-obsolete-targets.patch new file mode 100644 index 00000000..4320d152 --- /dev/null +++ b/package/rousette/0037-remove-obsolete-targets.patch @@ -0,0 +1,156 @@ +From ed62a496744de0e0dc31171167d5d6f69f54af93 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Fri, 21 Nov 2025 12:54:44 +0100 +Subject: [PATCH 37/38] remove obsolete targets +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Change-Id: Idde217a4ae1bc6f03657d9609e0ec1b0a47a9747 +Signed-off-by: Mattias Walström +--- + CMakeLists.txt | 9 -------- + src/clock.cpp | 62 -------------------------------------------------- + src/events.cpp | 33 --------------------------- + 3 files changed, 104 deletions(-) + delete mode 100644 src/clock.cpp + delete mode 100644 src/events.cpp + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 3d79dad..4a91128 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -125,14 +125,6 @@ target_link_libraries(rousette-restconf PUBLIC rousette-http rousette-sysrepo ro + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/src/auth/NacmIdentities.h.in ${CMAKE_CURRENT_BINARY_DIR}/NacmIdentities.h @ONLY) + include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +-add_executable(clock-demo src/clock.cpp) +-target_link_libraries(clock-demo rousette-http Boost::system Threads::Threads) +- +-add_executable(watch-operational-ds +- src/events.cpp +-) +-target_link_libraries(watch-operational-ds PUBLIC rousette-sysrepo) +- + add_executable(rousette src/restconf/main.cpp) + target_link_libraries(rousette PUBLIC rousette-restconf PkgConfig::DOCOPT) + if(SYSTEMD_FOUND) +@@ -140,7 +132,6 @@ if(SYSTEMD_FOUND) + endif() + + install(TARGETS +- # clock-demo + # watch-operational-ds + rousette + RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/) +diff --git a/src/clock.cpp b/src/clock.cpp +deleted file mode 100644 +index aa72849..0000000 +--- a/src/clock.cpp ++++ /dev/null +@@ -1,62 +0,0 @@ +-/* +- * Copyright (C) 2016-2021 CESNET, https://photonics.cesnet.cz/ +- * +- * Written by Jan Kundrát +- * +-*/ +- +-#include +-#include +-#include +-#include +-#include +-#include "http/EventStream.h" +- +-using namespace std::literals; +- +-constexpr auto keepAlivePingInterval = std::chrono::seconds{55}; +- +-int main(int argc [[maybe_unused]], char** argv [[maybe_unused]]) +-{ +- spdlog::set_level(spdlog::level::trace); +- +- rousette::http::EventStream::Termination shutdown; +- rousette::http::EventStream::EventSignal sig; +- std::jthread timer{[&sig]() { +- for (int i = 0; /* forever */; ++i) { +- std::this_thread::sleep_for(666ms); +- spdlog::info("tick: {}", i); +- sig("ping #" + std::to_string(i)); +- } +- }}; +- +- nghttp2::asio_http2::server::http2 server; +- server.num_threads(4); +- +- server.handle("/events", [&shutdown, &sig](const auto& req, const auto& res) { +- rousette::http::EventStream::create(req, res, shutdown, sig, keepAlivePingInterval); +- }); +- +- server.handle("/", [](const auto& req, const auto& resp) { +- spdlog::info("{}: {} {}", boost::lexical_cast(req.remote_endpoint()), req.method(), req.uri().raw_path); +- resp.write_head(200, {{"content-type", {"text/html", false}}}); +- resp.end(R"(nghttp2 event stream +-

events

    +- +- +-)"); +- }); +- +- boost::system::error_code ec; +- if (server.listen_and_serve(ec, "::", "10080")) { +- return 1; +- } +- return 0; +-} +diff --git a/src/events.cpp b/src/events.cpp +deleted file mode 100644 +index dc0477a..0000000 +--- a/src/events.cpp ++++ /dev/null +@@ -1,33 +0,0 @@ +-/* +- * Copyright (C) 2016-2021 CESNET, https://photonics.cesnet.cz/ +- * +- * Written by Jan Kundrát +- * +-*/ +- +-#include +-#include +-#include +-#include +-#include "sr/AllEvents.h" +- +-using namespace rousette::sr; +- +-int main(int argc [[maybe_unused]], char* argv [[maybe_unused]] []) +-{ +- spdlog::set_level(spdlog::level::trace); +- +- auto sess = sysrepo::Connection{}.sessionStart(); +- auto e = AllEvents{ +- sess, +- /* AllEvents::WithAttributes::All, */ +- AllEvents::WithAttributes::RemoveEmptyOperationAndOrigin, +- /* AllEvents::WithAttributes::None, */ +- }; +- +- signal(SIGTERM, [](int) {}); +- signal(SIGINT, [](int) {}); +- pause(); +- +- return 0; +-} +-- +2.43.0 + diff --git a/package/rousette/0038-port-to-libyang-v4.2.patch b/package/rousette/0038-port-to-libyang-v4.2.patch new file mode 100644 index 00000000..36494c21 --- /dev/null +++ b/package/rousette/0038-port-to-libyang-v4.2.patch @@ -0,0 +1,156 @@ +From 5a1d0dac28c1a78e90d3906693228584adcf3ab1 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Thu, 20 Nov 2025 16:13:43 +0100 +Subject: [PATCH 38/38] port to libyang v4.2 +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Depends-on: https://gerrit.cesnet.cz/c/CzechLight/dependencies/+/9012 +Change-Id: I1ac6b25a3c1034f06917594732f066fd77d4b26b +Signed-off-by: Mattias Walström +--- + CMakeLists.txt | 2 +- + src/restconf/Server.cpp | 10 +++++----- + src/restconf/utils/yang.cpp | 2 +- + src/sr/AllEvents.cpp | 4 ++-- + src/sr/OpticalEvents.cpp | 2 +- + tests/event_watchers.cpp | 2 +- + tests/restconf-subscribed-notifications.cpp | 2 +- + 7 files changed, 12 insertions(+), 12 deletions(-) + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 4a91128..4bb029b 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -76,7 +76,7 @@ find_package(Boost 1.66 REQUIRED CONFIG COMPONENTS system thread) + + pkg_check_modules(SYSREPO REQUIRED sysrepo IMPORTED_TARGET) + pkg_check_modules(SYSREPO-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=7) +-pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=5) ++pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=6) + pkg_check_modules(SYSTEMD IMPORTED_TARGET libsystemd) + pkg_check_modules(PAM REQUIRED IMPORTED_TARGET pam) + pkg_check_modules(DOCOPT REQUIRED IMPORTED_TARGET docopt) +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index d166236..3c7c3c7 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -111,7 +111,7 @@ void rejectWithErrorImpl(libyang::Context ctx, const libyang::DataFormat& dataFo + } + + res.write_head(code, headers); +- res.end(*parent.printStr(dataFormat, libyang::PrintFlags::WithSiblings)); ++ res.end(*parent.printStr(dataFormat, libyang::PrintFlags::Siblings)); + } + + void rejectWithError(libyang::Context ctx, const libyang::DataFormat& dataFormat, const request& req, const response& res, const int code, const std::string errorType, const std::string& errorTag, const std::string& errorMessage, const std::optional& errorPath) +@@ -543,7 +543,7 @@ void processActionOrRPC(std::shared_ptr requestCtx, const std::c + contentType(requestCtx->dataFormat.response), + CORS, + }); +- requestCtx->res.end(*envelope->printStr(requestCtx->dataFormat.response, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont)); ++ requestCtx->res.end(*envelope->printStr(requestCtx->dataFormat.response, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers)); + } + + void processPost(std::shared_ptr requestCtx, const std::chrono::milliseconds timeout) +@@ -709,7 +709,7 @@ void processYangPatch(std::shared_ptr requestCtx, const std::chr + yangPatchStatus->newExtPath(yangPatchStatusExt, "/ietf-yang-patch:yang-patch-status/ok", std::nullopt); + + requestCtx->res.write_head(200, {contentType(requestCtx->dataFormat.response), CORS}); +- requestCtx->res.end(*yangPatchStatus->printStr(requestCtx->dataFormat.response, libyang::PrintFlags::WithSiblings)); ++ requestCtx->res.end(*yangPatchStatus->printStr(requestCtx->dataFormat.response, libyang::PrintFlags::Siblings)); + } + + void processPutOrPlainPatch(std::shared_ptr requestCtx, const std::chrono::milliseconds timeout) +@@ -842,7 +842,7 @@ libyang::PrintFlags libyangPrintFlags(const libyang::DataNode& dataNode, const s + } catch(const libyang::Error& e) { + } + +- libyang::PrintFlags ret = libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont; ++ libyang::PrintFlags ret = libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers; + + if (!withDefaults && node && (node->schema().nodeType() == libyang::NodeType::Leaf || node->schema().nodeType() == libyang::NodeType::Leaflist) && node->asTerm().isImplicitDefault()) { + return ret | libyang::PrintFlags::WithDefaultsAll; +@@ -1116,7 +1116,7 @@ Server::Server( + case RestconfRequest::Type::ListRPC: + res.write_head(200, {contentType(dataFormat.response), CORS}); + res.end(*apiResource(sess.getContext(), restconfRequest.type, dataFormat.response) +- .printStr(dataFormat.response, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont)); ++ .printStr(dataFormat.response, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers)); + break; + + case RestconfRequest::Type::GetData: { +diff --git a/src/restconf/utils/yang.cpp b/src/restconf/utils/yang.cpp +index 9358840..5c311c1 100644 +--- a/src/restconf/utils/yang.cpp ++++ b/src/restconf/utils/yang.cpp +@@ -112,7 +112,7 @@ std::string as_restconf_notification(const libyang::Context& ctx, libyang::DataF + envelope->insertChild(*eventTime); + envelope->insertChild(notification); + +- auto res = *envelope->printStr(dataFormat, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont); ++ auto res = *envelope->printStr(dataFormat, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers); + + // notification node comes from sysrepo and sysrepo will free this; if not unlinked then envelope destructor would try to free this as well + notification.unlink(); +diff --git a/src/sr/AllEvents.cpp b/src/sr/AllEvents.cpp +index 7b6226b..2202b36 100644 +--- a/src/sr/AllEvents.cpp ++++ b/src/sr/AllEvents.cpp +@@ -110,10 +110,10 @@ sysrepo::ErrorCode AllEvents::onChange(sysrepo::Session session, const std::stri + break; + } + }; +- auto json = *copy.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont); ++ auto json = *copy.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers); + spdlog::info("JSON: {}", json); + spdlog::warn("FULL JSON: {}", +- *session.getData('/' + module + ":*")->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)); ++ *session.getData('/' + module + ":*")->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings)); + change(module, json); + } + +diff --git a/src/sr/OpticalEvents.cpp b/src/sr/OpticalEvents.cpp +index 25906a8..3f78ddc 100644 +--- a/src/sr/OpticalEvents.cpp ++++ b/src/sr/OpticalEvents.cpp +@@ -15,7 +15,7 @@ + namespace { + std::string dumpDataFrom(sysrepo::Session session, const std::string& module) + { +- return *session.getData('/' + module + ":*")->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont); ++ return *session.getData('/' + module + ":*")->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings | libyang::PrintFlags::EmptyContainers); + } + } + +diff --git a/tests/event_watchers.cpp b/tests/event_watchers.cpp +index 26ba951..38ee7be 100644 +--- a/tests/event_watchers.cpp ++++ b/tests/event_watchers.cpp +@@ -23,7 +23,7 @@ void datastoreChanges(auto session, auto& dsChangesMock, auto path) + + void datastoreNewState(auto session, auto& dsChangesMock, auto path) + { +- dsChangesMock.contentAfterChange(session.getData(path)->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)); ++ dsChangesMock.contentAfterChange(session.getData(path)->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings)); + } + } + +diff --git a/tests/restconf-subscribed-notifications.cpp b/tests/restconf-subscribed-notifications.cpp +index d42fa96..73b9fab 100644 +--- a/tests/restconf-subscribed-notifications.cpp ++++ b/tests/restconf-subscribed-notifications.cpp +@@ -92,7 +92,7 @@ EstablishSubscriptionResult establishSubscription( + data->unlinkWithSiblings(); + envelope->insertChild(*data); + +- auto body = *envelope->printStr(rpcEncoding, libyang::PrintFlags::WithSiblings); ++ auto body = *envelope->printStr(rpcEncoding, libyang::PrintFlags::Siblings); + auto resp = post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:establish-subscription", requestHeaders, body); + REQUIRE(resp.equalStatusCodeAndHeaders(Response{200, expectedHeaders, ""})); + +-- +2.43.0 + diff --git a/package/sysrepo-cpp/0001-CI-pin-to-libyang-v3.patch b/package/sysrepo-cpp/0001-CI-pin-to-libyang-v3.patch new file mode 100644 index 00000000..91d49243 --- /dev/null +++ b/package/sysrepo-cpp/0001-CI-pin-to-libyang-v3.patch @@ -0,0 +1,47 @@ +From ee0ce2ea31f3ecba4a4753a761df08f0698c2726 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Mon, 20 Oct 2025 11:47:26 +0200 +Subject: [PATCH 1/9] CI: pin to libyang v3 +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Depends-on: https://gerrit.cesnet.cz/c/CzechLight/libyang-cpp/+/8978 +Change-Id: I2521ad3a62d6197e757e636b88478deeb3bb5592 +Signed-off-by: Mattias Walström +--- + .zuul.yaml | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +diff --git a/.zuul.yaml b/.zuul.yaml +index 7ada6f7..cef174f 100644 +--- a/.zuul.yaml ++++ b/.zuul.yaml +@@ -4,9 +4,9 @@ + - f38-gcc-cover: + required-projects: + - name: github/CESNET/libyang +- override-checkout: devel ++ override-checkout: cesnet/2025-07-08 + - name: github/sysrepo/sysrepo +- override-checkout: devel ++ override-checkout: cesnet/2025-06-02 + - name: github/doctest/doctest + override-checkout: v2.4.8 + - name: github/rollbear/trompeloeil +@@ -15,9 +15,9 @@ + - f38-clang-asan-ubsan: + required-projects: &projects + - name: github/CESNET/libyang +- override-checkout: devel ++ override-checkout: cesnet/2025-07-08 + - name: github/sysrepo/sysrepo +- override-checkout: devel ++ override-checkout: cesnet/2025-06-02 + - name: github/doctest/doctest + override-checkout: v2.4.11 + - name: github/rollbear/trompeloeil +-- +2.43.0 + diff --git a/package/sysrepo-cpp/0002-fix-implicit-dependency-on-optional.patch b/package/sysrepo-cpp/0002-fix-implicit-dependency-on-optional.patch new file mode 100644 index 00000000..36c1f4b6 --- /dev/null +++ b/package/sysrepo-cpp/0002-fix-implicit-dependency-on-optional.patch @@ -0,0 +1,33 @@ +From e49463aba8b44b932fdd342d1c1d117e70414aba Mon Sep 17 00:00:00 2001 +From: Edoardo Bortolozzo +Date: Fri, 17 Oct 2025 18:08:47 +0200 +Subject: [PATCH 2/9] fix: implicit dependency on optional +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +chrono imports optional in gcc >=13. +added the explicit include to compile in gcc <=12. + +Change-Id: I18cc1a45ab522ee6738d4bc7080696cba8284903 +Signed-off-by: Mattias Walström +--- + include/sysrepo-cpp/Connection.hpp | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/include/sysrepo-cpp/Connection.hpp b/include/sysrepo-cpp/Connection.hpp +index bf72bcd..a469a2c 100644 +--- a/include/sysrepo-cpp/Connection.hpp ++++ b/include/sysrepo-cpp/Connection.hpp +@@ -7,6 +7,7 @@ + */ + #pragma once + ++#include + #include + #include + #include +-- +2.43.0 + diff --git a/package/sysrepo-cpp/0003-fix-include-for-for-our-public-headers.patch b/package/sysrepo-cpp/0003-fix-include-for-for-our-public-headers.patch new file mode 100644 index 00000000..64f9e970 --- /dev/null +++ b/package/sysrepo-cpp/0003-fix-include-for-for-our-public-headers.patch @@ -0,0 +1,31 @@ +From b8e349aacae4c7063517a1bc652e9e93f90e28ba Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Fri, 24 Oct 2025 17:05:52 +0200 +Subject: [PATCH 3/9] fix #include for for our public headers +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Change-Id: Idcbfe38348e3ca2e4331fc1fad542ae759b8d0f1 +Signed-off-by: Mattias Walström +--- + src/utils/utils.cpp | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp +index ff4a32c..c13b394 100644 +--- a/src/utils/utils.cpp ++++ b/src/utils/utils.cpp +@@ -7,7 +7,7 @@ + */ + + #include +-#include "sysrepo-cpp/utils/exception.hpp" ++#include + extern "C" { + #include + } +-- +2.43.0 + diff --git a/package/sysrepo-cpp/0004-reduce-number-of-files-which-are-named-utils.hpp.patch b/package/sysrepo-cpp/0004-reduce-number-of-files-which-are-named-utils.hpp.patch new file mode 100644 index 00000000..aad1e008 --- /dev/null +++ b/package/sysrepo-cpp/0004-reduce-number-of-files-which-are-named-utils.hpp.patch @@ -0,0 +1,82 @@ +From 753a3e6eb346f2a9f6263b2d9fcd1c6dbb508247 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Fri, 24 Oct 2025 17:06:56 +0200 +Subject: [PATCH 4/9] reduce number of files which are named "utils.hpp" +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +We used to have three (one in publicheaders, one in src/utils/, one +below tests/). Let's cut that by one to increase our collective sanity. + +Change-Id: I9e3d93c65dffb29ba18b3ae325ddd7712a8be7a4 +Signed-off-by: Mattias Walström +--- + src/Connection.cpp | 2 +- + src/Session.cpp | 2 +- + src/Subscription.cpp | 2 +- + src/utils/{utils.hpp => misc.hpp} | 0 + src/utils/utils.cpp | 2 +- + 5 files changed, 4 insertions(+), 4 deletions(-) + rename src/utils/{utils.hpp => misc.hpp} (100%) + +diff --git a/src/Connection.cpp b/src/Connection.cpp +index 0297ebe..1717b9a 100644 +--- a/src/Connection.cpp ++++ b/src/Connection.cpp +@@ -13,7 +13,7 @@ extern "C" { + #include + #include "utils/enum.hpp" + #include "utils/exception.hpp" +-#include "utils/utils.hpp" ++#include "utils/misc.hpp" + + namespace sysrepo { + +diff --git a/src/Session.cpp b/src/Session.cpp +index dfe1044..994ce87 100644 +--- a/src/Session.cpp ++++ b/src/Session.cpp +@@ -21,7 +21,7 @@ extern "C" { + #include + #include "utils/enum.hpp" + #include "utils/exception.hpp" +-#include "utils/utils.hpp" ++#include "utils/misc.hpp" + + using namespace std::string_literals; + namespace sysrepo { +diff --git a/src/Subscription.cpp b/src/Subscription.cpp +index 303b54c..5bb724e 100644 +--- a/src/Subscription.cpp ++++ b/src/Subscription.cpp +@@ -16,7 +16,7 @@ extern "C" { + } + #include "utils/enum.hpp" + #include "utils/exception.hpp" +-#include "utils/utils.hpp" ++#include "utils/misc.hpp" + + namespace sysrepo { + /** +diff --git a/src/utils/utils.hpp b/src/utils/misc.hpp +similarity index 100% +rename from src/utils/utils.hpp +rename to src/utils/misc.hpp +diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp +index c13b394..2e8d8b4 100644 +--- a/src/utils/utils.cpp ++++ b/src/utils/utils.cpp +@@ -12,7 +12,7 @@ extern "C" { + #include + } + #include "enum.hpp" +-#include "utils.hpp" ++#include "misc.hpp" + + namespace sysrepo { + /** +-- +2.43.0 + diff --git a/package/sysrepo-cpp/0005-Fix-a-different-function-return-type.patch b/package/sysrepo-cpp/0005-Fix-a-different-function-return-type.patch new file mode 100644 index 00000000..646c382f --- /dev/null +++ b/package/sysrepo-cpp/0005-Fix-a-different-function-return-type.patch @@ -0,0 +1,60 @@ +From dc118efa44b13508e1af47d924ad02ce58c58cea Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Fri, 24 Oct 2025 17:18:49 +0200 +Subject: [PATCH 5/9] Fix a different function return type +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +The prototype that was specified in the header file did not match what +was used in the corresponding .cpp implementation. This has not been a +problem because that implementation .cpp file was not including the +actual header, so this was not detected. Then, at runtime, this was not +a problem due to the way how derived types are laid out in memory. + +Fixes: 6ed5442 Utilities for working with existing sysrepo:discard-items nodes +Change-Id: Ie4cf8c0e39e9c8486a6289c0328510ad6f7360fb +Signed-off-by: Mattias Walström +--- + src/utils/utils.cpp | 7 ++++--- + 1 file changed, 4 insertions(+), 3 deletions(-) + +diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp +index 2e8d8b4..61d0c7f 100644 +--- a/src/utils/utils.cpp ++++ b/src/utils/utils.cpp +@@ -8,6 +8,7 @@ + + #include + #include ++#include + extern "C" { + #include + } +@@ -78,7 +79,7 @@ void checkNoThreadFlag(const SubscribeOptions opts, const std::optional findMatchingDiscard(libyang::DataNode root, const std::string& xpath) ++std::optional findMatchingDiscard(libyang::DataNode root, const std::string& xpath) + { + auto discard = root.firstOpaqueSibling(); + while (discard) { +@@ -97,11 +98,11 @@ std::optional findMatchingDiscard(libyang::DataNode root, con + /** + * @short Find all sysrepo:discard-items nodes which match the given XPath or the descendants of this XPath + */ +-std::vector findMatchingDiscardPrefixes(libyang::DataNode root, const std::string& xpathPrefix) ++std::vector findMatchingDiscardPrefixes(libyang::DataNode root, const std::string& xpathPrefix) + { + auto withSlash = (xpathPrefix.empty() || xpathPrefix[xpathPrefix.size() - 1] == '/') ? xpathPrefix : xpathPrefix + '/'; + auto withBracket = (xpathPrefix.empty() || xpathPrefix[xpathPrefix.size() - 1] == '[') ? xpathPrefix : xpathPrefix + '['; +- std::vector res; ++ std::vector res; + auto discard = root.firstOpaqueSibling(); + while (discard) { + if (discard->name().matches("sysrepo", "discard-items")) { +-- +2.43.0 + diff --git a/package/sysrepo-cpp/0006-fix-raw-strings-and-macros-break-gcc-13.patch b/package/sysrepo-cpp/0006-fix-raw-strings-and-macros-break-gcc-13.patch new file mode 100644 index 00000000..6e9ab6bd --- /dev/null +++ b/package/sysrepo-cpp/0006-fix-raw-strings-and-macros-break-gcc-13.patch @@ -0,0 +1,61 @@ +From b943faea3cb00c5e86f21710647a2ca370e41f33 Mon Sep 17 00:00:00 2001 +From: Edoardo Bortolozzo +Date: Fri, 24 Oct 2025 13:45:00 +0200 +Subject: [PATCH 6/9] fix: raw strings and macros break gcc < 13 +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +see bug report: +https://gcc.gnu.org/bugzilla/show_bug.cgi?id=55971 + +Change-Id: I04afc2b3bee86a7e4b038b277aad0e3a150a1940 +Signed-off-by: Mattias Walström +--- + tests/subscriptions-dynamic.cpp | 21 ++++++++++++++------- + 1 file changed, 14 insertions(+), 7 deletions(-) + +diff --git a/tests/subscriptions-dynamic.cpp b/tests/subscriptions-dynamic.cpp +index 5ed173b..561df0c 100644 +--- a/tests/subscriptions-dynamic.cpp ++++ b/tests/subscriptions-dynamic.cpp +@@ -39,21 +39,28 @@ + REQUIRE(pipeStatus((SUBSCRIPTION).fd(), -1) == PipeStatus::DataReady); \ + (SUBSCRIPTION).processEvent(cbNotif); + +-#define SUBSCRIPTION_TERMINATED(SUBSCRIPTION) R"({ ++auto SUBSCRIPTION_TERMINATED(const auto& SUBSCRIPTION) ++{ ++ return R"({ + "ietf-subscribed-notifications:subscription-terminated": { +- "id": )" + std::to_string((SUBSCRIPTION).subscriptionId()) + R"(, ++ "id": )" ++ + std::to_string((SUBSCRIPTION).subscriptionId()) + R"(, + "reason": "no-such-subscription" + } + } +-)" ++)"; ++}; + +-#define REPLAY_COMPLETED(SUBSCRIPTION) R"({ ++auto REPLAY_COMPLETED(const auto& SUBSCRIPTION) ++{ ++ return R"({ + "ietf-subscribed-notifications:replay-completed": { +- "id": )" \ +- + std::to_string((SUBSCRIPTION).subscriptionId()) + R"( ++ "id": )" ++ + std::to_string((SUBSCRIPTION).subscriptionId()) + R"( + } + } +-)" ++)"; ++}; + + #define REQUIRE_YANG_PUSH_UPDATE(SUBSCRIPTION, NOTIFICATION) \ + TROMPELOEIL_REQUIRE_CALL(rec, recordYangPushUpdate((SUBSCRIPTION).subscriptionId(), NOTIFICATION)).IN_SEQUENCE(seq); +-- +2.43.0 + diff --git a/package/sysrepo-cpp/0007-Port-to-libyang-v4.patch b/package/sysrepo-cpp/0007-Port-to-libyang-v4.patch new file mode 100644 index 00000000..22a28e44 --- /dev/null +++ b/package/sysrepo-cpp/0007-Port-to-libyang-v4.patch @@ -0,0 +1,305 @@ +From 5fd8cf4fb8db1d65b6a20d2777bad22647e7f5e0 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Thu, 23 Oct 2025 16:04:10 +0200 +Subject: [PATCH 7/9] Port to libyang v4 +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +I'm rather disappointed with the upstream changes that implement that +shared libyang "printed context" mapped to a fixed address, I think it's +effectively a step backwards. Upstream is happy with their solution, so +here we are. + +Change-Id: I6b89422ec66bc694dc2c6ecd0d40aa1e7f76a741 +Depends-on: https://gerrit.cesnet.cz/c/CzechLight/libyang-cpp/+/8984 +Signed-off-by: Mattias Walström +--- + .zuul.yaml | 8 +++--- + CMakeLists.txt | 6 ++--- + ci/build.sh | 4 +++ + include/sysrepo-cpp/Enum.hpp | 18 ++++++++++--- + include/sysrepo-cpp/utils/utils.hpp | 5 ++++ + src/Connection.cpp | 3 +-- + src/Session.cpp | 2 +- + src/utils/enum.hpp | 18 +++++++------ + src/utils/utils.cpp | 18 +++++++++++++ + tests/session.cpp | 39 +++++++++++++++++++++-------- + 10 files changed, 90 insertions(+), 31 deletions(-) + +diff --git a/.zuul.yaml b/.zuul.yaml +index cef174f..7ada6f7 100644 +--- a/.zuul.yaml ++++ b/.zuul.yaml +@@ -4,9 +4,9 @@ + - f38-gcc-cover: + required-projects: + - name: github/CESNET/libyang +- override-checkout: cesnet/2025-07-08 ++ override-checkout: devel + - name: github/sysrepo/sysrepo +- override-checkout: cesnet/2025-06-02 ++ override-checkout: devel + - name: github/doctest/doctest + override-checkout: v2.4.8 + - name: github/rollbear/trompeloeil +@@ -15,9 +15,9 @@ + - f38-clang-asan-ubsan: + required-projects: &projects + - name: github/CESNET/libyang +- override-checkout: cesnet/2025-07-08 ++ override-checkout: devel + - name: github/sysrepo/sysrepo +- override-checkout: cesnet/2025-06-02 ++ override-checkout: devel + - name: github/doctest/doctest + override-checkout: v2.4.11 + - name: github/rollbear/trompeloeil +diff --git a/CMakeLists.txt b/CMakeLists.txt +index f2a9b09..49338dd 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -19,7 +19,7 @@ add_custom_target(sysrepo-cpp-version-cmake + cmake/ProjectGitVersionRunner.cmake + ) + include(cmake/ProjectGitVersion.cmake) +-set(SYSREPO_CPP_PKG_VERSION "6") ++set(SYSREPO_CPP_PKG_VERSION "7") + prepare_git_version(SYSREPO_CPP_VERSION ${SYSREPO_CPP_PKG_VERSION}) + + find_package(Doxygen) +@@ -27,8 +27,8 @@ option(WITH_DOCS "Create and install internal documentation (needs Doxygen)" ${D + option(WITH_EXAMPLES "Build examples" ON) + + find_package(PkgConfig) +-pkg_check_modules(LIBYANG_CPP REQUIRED libyang-cpp>=3 IMPORTED_TARGET) +-pkg_check_modules(SYSREPO REQUIRED sysrepo>=3.7.4 IMPORTED_TARGET) ++pkg_check_modules(LIBYANG_CPP REQUIRED libyang-cpp>=5 IMPORTED_TARGET) ++pkg_check_modules(SYSREPO REQUIRED sysrepo>=4.2.4 IMPORTED_TARGET) + + include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) + +diff --git a/ci/build.sh b/ci/build.sh +index 785a811..67bc7d3 100755 +--- a/ci/build.sh ++++ b/ci/build.sh +@@ -47,6 +47,10 @@ if [[ $ZUUL_JOB_NAME =~ .*-tsan ]]; then + # Our TSAN does not have interceptors for a variety of "less common" functions such as pthread_mutex_clocklock. + # Disable all functions which are optional in sysrepo/libnetconf2/Netopeer2. + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DHAVE_PTHREAD_MUTEX_TIMEDLOCK=OFF -DHAVE_PTHREAD_MUTEX_CLOCKLOCK=OFF -DHAVE_PTHREAD_RWLOCK_CLOCKRDLOCK=OFF -DHAVE_PTHREAD_RWLOCK_CLOCKWRLOCK=OFF -DHAVE_PTHREAD_COND_CLOCKWAIT=OFF" ++ ++ # This is currently broken on TSAN ("Failed to map the printed context (Operation not permitted)."). ++ # Even if it wasn't broken, I want at least one build in the matrix with this (mis)feature disabled. ++ CMAKE_OPTIONS="${CMAKE_OPTIONS} -DPRINTED_CONTEXT_ADDRESS=0" + fi + + if [[ $ZUUL_JOB_NAME =~ .*-cover.* ]]; then +diff --git a/include/sysrepo-cpp/Enum.hpp b/include/sysrepo-cpp/Enum.hpp +index 48467c6..505797a 100644 +--- a/include/sysrepo-cpp/Enum.hpp ++++ b/include/sysrepo-cpp/Enum.hpp +@@ -176,12 +176,24 @@ enum class NotificationType : uint32_t { + }; + + /** +- * Wraps `sr_conn_flag_e`. ++ * Wraps `sr_context_flag_t`. ++ */ ++enum class ContextFlags : uint32_t { ++ Default = 0x00, /**< SR_CTX_DEFAULT */ ++ NoPrinted = 0x01, /**< SR_CTX_NO_PRINTED */ ++ LibYangPrivParsed = 0x02, /**< SR_CTX_SET_PRIV_PARSED */ ++}; ++ ++constexpr ContextFlags operator|(const ContextFlags a, const ContextFlags b) ++{ ++ return implEnumBitOr(a, b); ++} ++ ++/** ++ * Wraps `sr_conn_flag_t`. + */ + enum class ConnectionFlags : uint32_t { + Default = 0x00, /**< SR_CONN_DEFAULT */ +- CacheRunning = 0x01, /**< SR_CONN_CACHE_RUNNING */ +- LibYangPrivParsed = 0x02, /**< SR_CONN_CTX_SET_PRIV_PARSED */ + }; + + constexpr ConnectionFlags operator|(const ConnectionFlags a, const ConnectionFlags b) +diff --git a/include/sysrepo-cpp/utils/utils.hpp b/include/sysrepo-cpp/utils/utils.hpp +index 64b1c2e..6f2446f 100644 +--- a/include/sysrepo-cpp/utils/utils.hpp ++++ b/include/sysrepo-cpp/utils/utils.hpp +@@ -12,6 +12,11 @@ + namespace sysrepo { + Session wrapUnmanagedSession(sr_session_ctx_s* session); + void setLogLevelStderr(const LogLevel); ++enum class GlobalContextEffect { ++ Lazy, /**< Set these flags the next time the shared context is applied */ ++ Immediate, /**< Apply immediately */ ++}; ++ContextFlags setGlobalContextOptions(const ContextFlags flags, const GlobalContextEffect when); + std::optional findMatchingDiscard(libyang::DataNode root, const std::string& xpath); + std::vector findMatchingDiscardPrefixes(libyang::DataNode root, const std::string& xpathPrefix); + void unlinkFromForest(std::optional& root, libyang::DataNode node); +diff --git a/src/Connection.cpp b/src/Connection.cpp +index 1717b9a..96e340c 100644 +--- a/src/Connection.cpp ++++ b/src/Connection.cpp +@@ -26,7 +26,7 @@ Connection::Connection(const ConnectionFlags options) + : ctx(nullptr) + { + sr_conn_ctx_t* ctx; +- auto res = sr_connect(static_cast(options), &ctx); ++ auto res = sr_connect(static_cast(options), &ctx); + + throwIfError(res, "Couldn't connect to sysrepo"); + this->ctx = std::shared_ptr(ctx, sr_disconnect); +@@ -96,5 +96,4 @@ uint32_t Connection::getId() const + { + return sr_get_cid(ctx.get()); + } +- + } +diff --git a/src/Session.cpp b/src/Session.cpp +index 994ce87..95b8f24 100644 +--- a/src/Session.cpp ++++ b/src/Session.cpp +@@ -241,7 +241,7 @@ std::optional Session::operationalChanges(const std::optional + void Session::discardOperationalChanges(const std::optional& moduleName, std::chrono::milliseconds timeout) + { + SYSREPO_CPP_SESSION_MTX; +- auto res = sr_discard_oper_changes(nullptr, m_sess.get(), moduleName ? nullptr : moduleName->c_str(), timeout.count()); ++ auto res = sr_discard_oper_changes(m_sess.get(), moduleName ? nullptr : moduleName->c_str(), timeout.count()); + throwIfError(res, "Session::discardOoperationalChanges: Couldn't discard "s + (moduleName ? "for module \"" + *moduleName + "\"" : "globally"s), m_sess.get()); + } + +diff --git a/src/utils/enum.hpp b/src/utils/enum.hpp +index 2d3a298..e917b21 100644 +--- a/src/utils/enum.hpp ++++ b/src/utils/enum.hpp +@@ -151,13 +151,16 @@ static_assert(toNotificationType(SR_EV_NOTIF_MODIFIED) == NotificationType::Modi + static_assert(toNotificationType(SR_EV_NOTIF_SUSPENDED) == NotificationType::Suspended); + static_assert(toNotificationType(SR_EV_NOTIF_RESUMED) == NotificationType::Resumed); + +-static_assert(std::is_same_v>); ++static_assert(std::is_same_v, std::underlying_type_t>); + static_assert(static_cast(SR_CONN_DEFAULT) == ConnectionFlags::Default); +-static_assert(static_cast(SR_CONN_CACHE_RUNNING) == ConnectionFlags::CacheRunning); +-static_assert(static_cast(SR_CONN_CTX_SET_PRIV_PARSED) == ConnectionFlags::LibYangPrivParsed); +-static_assert(static_cast(SR_CONN_CACHE_RUNNING | SR_CONN_CTX_SET_PRIV_PARSED) == (ConnectionFlags::CacheRunning | ConnectionFlags::LibYangPrivParsed)); + +-static_assert(std::is_same_v>); ++static_assert(std::is_same_v, std::underlying_type_t>); ++static_assert(static_cast(SR_CTX_DEFAULT) == ContextFlags::Default); ++static_assert(static_cast(SR_CTX_SET_PRIV_PARSED) == ContextFlags::LibYangPrivParsed); ++static_assert(static_cast(SR_CTX_NO_PRINTED) == ContextFlags::NoPrinted); ++static_assert(static_cast(SR_CTX_SET_PRIV_PARSED | SR_CTX_NO_PRINTED) == (ContextFlags::LibYangPrivParsed | ContextFlags::NoPrinted)); ++ ++// upstream project provides no dedicated type for these + static_assert(static_cast(SR_OPER_DEFAULT) == GetOptions::Default); + static_assert(static_cast(SR_OPER_NO_STATE) == GetOptions::OperNoState); + static_assert(static_cast(SR_OPER_NO_CONFIG) == GetOptions::OperNoConfig); +@@ -168,9 +171,10 @@ static_assert(static_cast(SR_OPER_NO_POLL_CACHED) == GetOptions::Ope + static_assert(static_cast(SR_OPER_NO_RUN_CACHED) == GetOptions::OperNoRunningCached); + static_assert(static_cast(SR_GET_NO_FILTER) == GetOptions::NoFilter); + +-constexpr sr_get_options_t toGetOptions(const GetOptions opts) ++// upstream project provides no dedicated type for these ++constexpr uint32_t toGetOptions(const GetOptions opts) + { +- return static_cast(opts); ++ return static_cast(opts); + } + + static_assert(static_cast(SRSN_YP_CHANGE_CREATE) == YangPushChange::Create); +diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp +index 61d0c7f..dbbc5ce 100644 +--- a/src/utils/utils.cpp ++++ b/src/utils/utils.cpp +@@ -13,6 +13,7 @@ extern "C" { + #include + } + #include "enum.hpp" ++#include "exception.hpp" + #include "misc.hpp" + + namespace sysrepo { +@@ -37,6 +38,23 @@ void setLogLevelStderr(const LogLevel level) + sr_log_stderr(toLogLevel(level)); + } + ++/** ++ * @brief Set global sysrepo-level context options ++ * ++ * Be advised of consequences of manipulating a shared global state, especially when using multiple connections. ++ * ++ * Wraps `sr_context_options`. ++ */ ++ContextFlags setGlobalContextOptions(const ContextFlags flags, const GlobalContextEffect when) ++{ ++ uint32_t old; ++ throwIfError( ++ sr_context_options(static_cast(flags), when == GlobalContextEffect::Immediate, &old), ++ "sr_context_options failed", ++ nullptr); ++ return static_cast(old); ++} ++ + std::timespec toTimespec(std::chrono::time_point tp) + { + // https://embeddedartistry.com/blog/2019/01/31/converting-between-timespec-stdchrono#-std-chrono-time_point-to-timespec- +diff --git a/tests/session.cpp b/tests/session.cpp +index 47b5631..a709a09 100644 +--- a/tests/session.cpp ++++ b/tests/session.cpp +@@ -627,17 +627,34 @@ TEST_CASE("session") + + DOCTEST_SUBCASE("libyang context flags") + { +- sess.setItem("/test_module:popelnice/s", "666"); +- REQUIRE(sess.getOneNode("/test_module:popelnice/s").asTerm().valueStr() == "666"); +- // Parsed type info is not preserved by libyang unless its context is constructed with a flag, +- // and that flag is not used by sysrepo by default... +- REQUIRE_THROWS_AS(sess.getOneNode("/test_module:popelnice/s").schema().asLeaf().valueType().asString().length(), libyang::ParsedInfoUnavailable); +- +- // ...unless we pass that flag explicitly as a parameter to the connection. +- auto sess2 = sysrepo::Connection{sysrepo::ConnectionFlags::LibYangPrivParsed}.sessionStart(); +- sess2.setItem("/test_module:popelnice/s", "333"); +- REQUIRE(sess2.getOneNode("/test_module:popelnice/s").asTerm().valueStr() == "333"); +- REQUIRE(sess2.getOneNode("/test_module:popelnice/s").schema().asLeaf().valueType().asString().length().parts[0].max == 10); ++ DOCTEST_SUBCASE("default") ++ { ++ sess.setItem("/test_module:popelnice/s", "666"); ++ REQUIRE(sess.getOneNode("/test_module:popelnice/s").asTerm().valueStr() == "666"); ++ // Parsed type info is not preserved by libyang unless its context is constructed with a flag, ++ // and that flag is not used by sysrepo by default. ++ REQUIRE_THROWS_AS(sess.getOneNode("/test_module:popelnice/s").schema().asLeaf().valueType().asString().length(), libyang::ParsedInfoUnavailable); ++ ++ // pending changes hold the context lock, so we cannot change the global context options ++ REQUIRE_THROWS_WITH_AS( ++ sysrepo::setGlobalContextOptions( ++ sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, ++ sysrepo::GlobalContextEffect::Immediate), ++ "sr_context_options failed: SR_ERR_TIME_OUT", ++ sysrepo::ErrorWithCode); ++ } ++ ++ // Since this is actually a shared global state, changing it is "tricky". The best bet is to actually ++ // ensure that there are no other connections (which we cannot do here, we already have a connection opened ++ // from the common code at the very top of this test case), and especially no internal sysrepo-level locks ++ // on the shared libyang context. For example, no uncommited changes -- hence a new subcase. ++ DOCTEST_SUBCASE("Connection::setGlobalContextOptions LibYangPrivParsed") ++ { ++ sysrepo::setGlobalContextOptions(sysrepo::ContextFlags::LibYangPrivParsed | sysrepo::ContextFlags::NoPrinted, sysrepo::GlobalContextEffect::Immediate); ++ sess.setItem("/test_module:popelnice/s", "333"); ++ REQUIRE(sess.getOneNode("/test_module:popelnice/s").asTerm().valueStr() == "333"); ++ REQUIRE(sess.getOneNode("/test_module:popelnice/s").schema().asLeaf().valueType().asString().length().parts[0].max == 10); ++ } + } + + DOCTEST_SUBCASE("replay support") +-- +2.43.0 + diff --git a/package/sysrepo-cpp/0008-fix-onOperGet-unconsistent-with-C-library.patch b/package/sysrepo-cpp/0008-fix-onOperGet-unconsistent-with-C-library.patch new file mode 100644 index 00000000..d864533d --- /dev/null +++ b/package/sysrepo-cpp/0008-fix-onOperGet-unconsistent-with-C-library.patch @@ -0,0 +1,90 @@ +From 6e9dbd8513ce75e51f2f714df69c08af5b556d1d Mon Sep 17 00:00:00 2001 +From: Edoardo Bortolozzo +Date: Tue, 28 Oct 2025 13:24:13 +0100 +Subject: [PATCH 8/9] fix: onOperGet unconsistent with C library. +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Fixes: https://github.com/sysrepo/sysrepo-cpp/issues/29 +Change-Id: I5a21069889c59466df8e9457c3a316b58e2571bb +Signed-off-by: Mattias Walström +--- + include/sysrepo-cpp/Session.hpp | 2 +- + include/sysrepo-cpp/Subscription.hpp | 2 +- + src/Session.cpp | 4 ++-- + src/Subscription.cpp | 4 ++-- + 4 files changed, 6 insertions(+), 6 deletions(-) + +diff --git a/include/sysrepo-cpp/Session.hpp b/include/sysrepo-cpp/Session.hpp +index aae7068..3af4b10 100644 +--- a/include/sysrepo-cpp/Session.hpp ++++ b/include/sysrepo-cpp/Session.hpp +@@ -127,7 +127,7 @@ public: + [[nodiscard]] Subscription onOperGet( + const std::string& moduleName, + OperGetCb cb, +- const std::optional& xpath = std::nullopt, ++ const std::string& path, + const SubscribeOptions opts = SubscribeOptions::Default, + ExceptionHandler handler = nullptr, + const std::optional& callbacks = std::nullopt); +diff --git a/include/sysrepo-cpp/Subscription.hpp b/include/sysrepo-cpp/Subscription.hpp +index c8f3ff8..14ecce4 100644 +--- a/include/sysrepo-cpp/Subscription.hpp ++++ b/include/sysrepo-cpp/Subscription.hpp +@@ -32,7 +32,7 @@ public: + Subscription& operator=(Subscription&&) noexcept; + + void onModuleChange(const std::string& moduleName, ModuleChangeCb cb, const std::optional& xpath = std::nullopt, uint32_t priority = 0, const SubscribeOptions opts = SubscribeOptions::Default); +- void onOperGet(const std::string& moduleName, OperGetCb cb, const std::optional& xpath, const SubscribeOptions opts = SubscribeOptions::Default); ++ void onOperGet(const std::string& moduleName, OperGetCb cb, const std::string& path, const SubscribeOptions opts = SubscribeOptions::Default); + void onRPCAction(const std::string& xpath, RpcActionCb cb, uint32_t priority = 0, const SubscribeOptions opts = SubscribeOptions::Default); + void onNotification( + const std::string& moduleName, +diff --git a/src/Session.cpp b/src/Session.cpp +index 95b8f24..6f0327d 100644 +--- a/src/Session.cpp ++++ b/src/Session.cpp +@@ -505,14 +505,14 @@ Subscription Session::onModuleChange( + Subscription Session::onOperGet( + const std::string& moduleName, + OperGetCb cb, +- const std::optional& xpath, ++ const std::string& path, + const SubscribeOptions opts, + ExceptionHandler handler, + const std::optional& callbacks) + { + checkNoThreadFlag(opts, callbacks); + auto sub = Subscription{*this, handler, callbacks}; +- sub.onOperGet(moduleName, cb, xpath, opts); ++ sub.onOperGet(moduleName, cb, path, opts); + return sub; + } + +diff --git a/src/Subscription.cpp b/src/Subscription.cpp +index 5bb724e..dca1176 100644 +--- a/src/Subscription.cpp ++++ b/src/Subscription.cpp +@@ -215,14 +215,14 @@ void Subscription::onModuleChange(const std::string& moduleName, ModuleChangeCb + * @param xpath XPath that identifies which data this subscription is able to provide. + * @param opts Options further changing the behavior of this method. + */ +-void Subscription::onOperGet(const std::string& moduleName, OperGetCb cb, const std::optional& xpath, const SubscribeOptions opts) ++void Subscription::onOperGet(const std::string& moduleName, OperGetCb cb, const std::string& path, const SubscribeOptions opts) + { + SYSREPO_CPP_SESSION_MTX_OF(m_sess); + checkNoThreadFlag(opts, m_customEventLoopCbs); + + auto& privRef = m_operGetCbs.emplace_back(PrivData{cb, m_exceptionHandler.get()}); + sr_subscription_ctx_s* ctx = m_sub.get(); +- auto res = sr_oper_get_subscribe(m_sess.m_sess.get(), moduleName.c_str(), xpath ? xpath->c_str() : nullptr, operGetItemsCb, reinterpret_cast(&privRef), toSubscribeOptions(opts), &ctx); ++ auto res = sr_oper_get_subscribe(m_sess.m_sess.get(), moduleName.c_str(), path.c_str(), operGetItemsCb, reinterpret_cast(&privRef), toSubscribeOptions(opts), &ctx); + throwIfError(res, "Couldn't create operational get items subscription", m_sess.m_sess.get()); + + saveContext(ctx); +-- +2.43.0 + diff --git a/package/sysrepo-cpp/0009-Update-to-libyang-v4.2.patch b/package/sysrepo-cpp/0009-Update-to-libyang-v4.2.patch new file mode 100644 index 00000000..49489be0 --- /dev/null +++ b/package/sysrepo-cpp/0009-Update-to-libyang-v4.2.patch @@ -0,0 +1,121 @@ +From 60d729e4791e5447cf0d969be6da9f8f1878af64 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= +Date: Thu, 20 Nov 2025 15:38:04 +0100 +Subject: [PATCH 9/9] Update to libyang v4.2 +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Depends-on: https://gerrit.cesnet.cz/c/CzechLight/libyang-cpp/+/9009 +Change-Id: Ia4c53dba0befa8dade38e728489edc6473aa5f4f +Signed-off-by: Mattias Walström +--- + CMakeLists.txt | 4 ++-- + tests/session.cpp | 14 +++++++------- + tests/subscriptions-dynamic.cpp | 4 ++-- + 3 files changed, 11 insertions(+), 11 deletions(-) + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 49338dd..8234464 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -27,8 +27,8 @@ option(WITH_DOCS "Create and install internal documentation (needs Doxygen)" ${D + option(WITH_EXAMPLES "Build examples" ON) + + find_package(PkgConfig) +-pkg_check_modules(LIBYANG_CPP REQUIRED libyang-cpp>=5 IMPORTED_TARGET) +-pkg_check_modules(SYSREPO REQUIRED sysrepo>=4.2.4 IMPORTED_TARGET) ++pkg_check_modules(LIBYANG_CPP REQUIRED libyang-cpp>=6 IMPORTED_TARGET) ++pkg_check_modules(SYSREPO REQUIRED sysrepo>=4.2.8 IMPORTED_TARGET) + + include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) + +diff --git a/tests/session.cpp b/tests/session.cpp +index a709a09..0e92e65 100644 +--- a/tests/session.cpp ++++ b/tests/session.cpp +@@ -105,7 +105,7 @@ TEST_CASE("session") + + auto data = sess.getData("/test_module:popelnice", 0); + REQUIRE(data); +- REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({ ++ REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == R"({ + "test_module:popelnice": { + "content": { + "trash": [ +@@ -126,14 +126,14 @@ TEST_CASE("session") + + data = sess.getData("/test_module:popelnice", 1); + REQUIRE(data); +- REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({ ++ REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == R"({ + "test_module:popelnice": {} + } + )"); + + data = sess.getData("/test_module:popelnice", 2); + REQUIRE(data); +- REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({ ++ REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == R"({ + "test_module:popelnice": { + "content": {} + } +@@ -143,7 +143,7 @@ TEST_CASE("session") + // If a list should be returned, its keys are always returned as well. + data = sess.getData("/test_module:popelnice", 3); + REQUIRE(data); +- REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({ ++ REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == R"({ + "test_module:popelnice": { + "content": { + "trash": [ +@@ -161,7 +161,7 @@ TEST_CASE("session") + + data = sess.getData("/test_module:popelnice", 4); + REQUIRE(data); +- REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({ ++ REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::EmptyContainers) == R"({ + "test_module:popelnice": { + "content": { + "trash": [ +@@ -497,7 +497,7 @@ TEST_CASE("session") + { + sess.switchDatastore(sysrepo::Datastore::FactoryDefault); + auto data = sess.getData("/*"); +- REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) == "{\n\n}\n"); ++ REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) == "{\n\n}\n"); + REQUIRE_THROWS_AS(sess.setItem(leaf, "123"), sysrepo::ErrorWithCode); + } + +@@ -622,7 +622,7 @@ TEST_CASE("session") + } + + // the original tree is not corrupted +- REQUIRE(*conf->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) != ""); ++ REQUIRE(*conf->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) != ""); + } + + DOCTEST_SUBCASE("libyang context flags") +diff --git a/tests/subscriptions-dynamic.cpp b/tests/subscriptions-dynamic.cpp +index 561df0c..0d4ce7c 100644 +--- a/tests/subscriptions-dynamic.cpp ++++ b/tests/subscriptions-dynamic.cpp +@@ -140,12 +140,12 @@ TEST_CASE("Dynamic subscriptions") + const auto subId = std::get(idNode->asTerm().value()); + idNode->unlink(); + +- rec.recordYangPushUpdate(subId, tree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)); ++ rec.recordYangPushUpdate(subId, tree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings)); + }; + + auto cbNotif = [&](const std::optional& tree, auto) { + REQUIRE(tree); +- rec.recordNotification(tree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)); ++ rec.recordNotification(tree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings)); + }; + + // write some initial data +-- +2.43.0 + diff --git a/patches/frr/9.1.3/0001-libyang-compat.patch b/patches/frr/9.1.3/0001-libyang-compat.patch index 0efbda97..ebb5b950 100644 --- a/patches/frr/9.1.3/0001-libyang-compat.patch +++ b/patches/frr/9.1.3/0001-libyang-compat.patch @@ -1,6 +1,77 @@ -diff -urN frr-9.1.3.orig/lib/yang.c frr-9.1.3/lib/yang.c ---- frr-9.1.3.orig/lib/yang.c 2024-12-27 22:06:42.000000000 +0100 -+++ frr-9.1.3/lib/yang.c 2025-03-04 12:23:06.723640114 +0100 +diff --git a/lib/northbound.c b/lib/northbound.c +index 6ff5c24bd1..c6cca523b2 100644 +--- a/lib/northbound.c ++++ b/lib/northbound.c +@@ -581,7 +581,7 @@ void nb_config_diff(const struct nb_config *config1, + char *s; + + if (!lyd_print_mem(&s, diff, LYD_JSON, +- LYD_PRINT_WITHSIBLINGS | LYD_PRINT_WD_ALL)) { ++ LYD_PRINT_SIBLINGS | LYD_PRINT_WD_ALL)) { + zlog_debug("%s: %s", __func__, s); + free(s); + } +@@ -2283,7 +2283,7 @@ bool nb_operation_is_valid(enum nb_operation operation, + if (sleaf->when) + return true; + if (CHECK_FLAG(sleaf->flags, LYS_MAND_TRUE) +- || sleaf->dflt) ++ || sleaf->dflt.str) + return false; + break; + case LYS_CONTAINER: +diff --git a/lib/northbound_cli.c b/lib/northbound_cli.c +index 8003679ed5..0997b5f8c5 100644 +--- a/lib/northbound_cli.c ++++ b/lib/northbound_cli.c +@@ -634,7 +634,7 @@ static int nb_cli_show_config_libyang(struct vty *vty, LYD_FORMAT format, + return CMD_WARNING; + } + +- SET_FLAG(options, LYD_PRINT_WITHSIBLINGS); ++ SET_FLAG(options, LYD_PRINT_SIBLINGS); + if (with_defaults) + SET_FLAG(options, LYD_PRINT_WD_ALL); + else +@@ -1443,7 +1443,7 @@ DEFPY (show_yang_operational_data, + struct ly_ctx *ly_ctx; + struct lyd_node *dnode; + char *strp; +- uint32_t print_options = LYD_PRINT_WITHSIBLINGS; ++ uint32_t print_options = LYD_PRINT_SIBLINGS; + int ret; + + if (xml) +diff --git a/lib/northbound_db.c b/lib/northbound_db.c +index 74abcde955..7d51f39291 100644 +--- a/lib/northbound_db.c ++++ b/lib/northbound_db.c +@@ -79,7 +79,7 @@ int nb_db_transaction_save(const struct nb_transaction *transaction, + * values too, as this covers the case where defaults may change. + */ + if (lyd_print_mem(&config_str, transaction->config->dnode, LYD_XML, +- LYD_PRINT_WITHSIBLINGS | LYD_PRINT_WD_ALL) ++ LYD_PRINT_SIBLINGS | LYD_PRINT_WD_ALL) + != 0) + goto exit; + +diff --git a/lib/northbound_grpc.cpp b/lib/northbound_grpc.cpp +index 6c33351cef..b3e14828f3 100644 +--- a/lib/northbound_grpc.cpp ++++ b/lib/northbound_grpc.cpp +@@ -370,7 +370,7 @@ static LY_ERR data_tree_from_dnode(frr::DataTree *dt, + char *strp; + int options = 0; + +- SET_FLAG(options, LYD_PRINT_WITHSIBLINGS); ++ SET_FLAG(options, LYD_PRINT_SIBLINGS); + if (with_defaults) + SET_FLAG(options, LYD_PRINT_WD_ALL); + else +diff --git a/lib/yang.c b/lib/yang.c +index 4dd8654217..44563d4922 100644 +--- a/lib/yang.c ++++ b/lib/yang.c @@ -10,11 +10,23 @@ #include "lib_errors.h" #include "yang.h" @@ -25,7 +96,18 @@ diff -urN frr-9.1.3.orig/lib/yang.c frr-9.1.3/lib/yang.c /* libyang container. */ struct ly_ctx *ly_native_ctx; -@@ -657,7 +669,12 @@ +@@ -329,9 +341,7 @@ const char *yang_snode_get_default(const struct lysc_node *snode) + switch (snode->nodetype) { + case LYS_LEAF: + sleaf = (const struct lysc_node_leaf *)snode; +- return sleaf->dflt ? lyd_value_get_canonical(sleaf->module->ctx, +- sleaf->dflt) +- : NULL; ++ return sleaf->dflt.str; + case LYS_LEAFLIST: + /* TODO: check leaf-list default values */ + return NULL; +@@ -657,7 +667,12 @@ struct yang_data *yang_data_list_find(const struct list *list, } /* Make libyang log its errors using FRR logging infrastructure. */ @@ -39,7 +121,7 @@ diff -urN frr-9.1.3.orig/lib/yang.c frr-9.1.3/lib/yang.c { int priority = LOG_ERR; -@@ -674,8 +691,14 @@ +@@ -674,8 +689,14 @@ static void ly_log_cb(LY_LOG_LEVEL level, const char *msg, const char *path) break; } @@ -56,7 +138,7 @@ diff -urN frr-9.1.3.orig/lib/yang.c frr-9.1.3/lib/yang.c else zlog(priority, "libyang: %s", msg); } -@@ -752,7 +775,7 @@ +@@ -752,7 +773,7 @@ struct ly_ctx *yang_ctx_new_setup(bool embedded_modules, bool explicit_compile) void yang_init(bool embedded_modules, bool defer_compile) { /* Initialize libyang global parameters that affect all containers. */ diff --git a/patches/libnetconf2/4.1.2/do-not-fail-without-certficates.patch b/patches/libnetconf2/4.1.2/do-not-fail-without-certficates.patch new file mode 100644 index 00000000..95c684a9 --- /dev/null +++ b/patches/libnetconf2/4.1.2/do-not-fail-without-certficates.patch @@ -0,0 +1,22 @@ +diff --git a/src/server_config.c b/src/server_config.c +index 48d362e..dc9f59d 100644 +--- a/src/server_config.c ++++ b/src/server_config.c +@@ -4151,6 +4151,8 @@ config_asymmetric_key_certs(const struct lyd_node *node, enum nc_operation paren + struct lyd_node *n; + enum nc_operation op; + ++ if (!node) ++ return 0; + NC_NODE_GET_OP(node, parent_op, &op); + + /* configure all certificates */ +@@ -4225,7 +4227,7 @@ config_asymmetric_key(const struct lyd_node *node, enum nc_operation parent_op, + } + + /* config asymmetric key certificates */ +- NC_CHECK_RET(nc_lyd_find_child(node, "certificates", 1, &n)); ++ NC_CHECK_RET(nc_lyd_find_child(node, "certificates", 0, &n)); + NC_CHECK_RET(config_asymmetric_key_certs(n, op, entry)); + + /* config generate csr */ diff --git a/patches/libyang/4.2.2/0001-plugins-ext-BUGFIX-printed-context-align-all-sizes-a.patch b/patches/libyang/4.2.2/0001-plugins-ext-BUGFIX-printed-context-align-all-sizes-a.patch new file mode 100644 index 00000000..7a8b4fae --- /dev/null +++ b/patches/libyang/4.2.2/0001-plugins-ext-BUGFIX-printed-context-align-all-sizes-a.patch @@ -0,0 +1,741 @@ +From 545473073552cd4cf5e0fc839e7170d4678ef646 Mon Sep 17 00:00:00 2001 +From: Michal Vasko +Date: Wed, 10 Dec 2025 09:52:13 +0100 +Subject: [PATCH 1/6] plugins ext BUGFIX printed context align all sizes and + mem +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Refs #2455 + +Signed-off-by: Mattias Walström +--- + src/plugins_exts.h | 17 +++- + src/plugins_exts/metadata.c | 4 +- + src/plugins_exts/schema_mount.c | 24 +++--- + src/plugins_exts/structure.c | 8 +- + src/printer_context.c | 148 +++++++++++++++----------------- + 5 files changed, 100 insertions(+), 101 deletions(-) + +diff --git a/src/plugins_exts.h b/src/plugins_exts.h +index f260ff787..e912fe908 100644 +--- a/src/plugins_exts.h ++++ b/src/plugins_exts.h +@@ -953,13 +953,26 @@ LIBYANG_API_DECL void lyplg_ext_cfree_instance_substatements(const struct ly_ctx + * compiled size + */ + ++/** ++ * @brief Alignment of all the printed data, ensures all memory access is aligned to this number (B) ++ */ ++#define LY_CTXP_MEM_ALIGN 8 ++ ++/** ++ * @brief Adjust data size to an aligned size to make sure the following data is aligned. ++ * ++ * @param[in,out] SIZE Data size that is adjusted. ++ */ ++#define LY_CTXP_MEM_SIZE(SIZE) ((SIZE) + ((~(SIZE) + 1) & (LY_CTXP_MEM_ALIGN - 1))) ++ + /** + * @brief Callback to return the size of the custom compiled structure and substmts array. If there are none, do not + * define this callback. + * + * @param[in] ext Compiled extension structure. + * @param[in,out] addr_ht Hash table with addresses of shared structures that were already accounted for, can be added to. +- * @return Total size of the custom compiled structures and the substmts array; ++ * @return Total size of the custom compiled structures and the substmts array. Always use ::LY_CTXP_MEM_SIZE when ++ * adding to the size for correct alignment. + * @return -1 on error. + */ + typedef int (*lyplg_ext_compiled_size_clb)(const struct lysc_ext_instance *ext, struct ly_ht *addr_ht); +@@ -984,7 +997,7 @@ LIBYANG_API_DECL int lyplg_ext_compiled_stmts_storage_size(const struct lysc_ext + * @param[in] orig_ext Compiled extension structure to print. + * @param[in,out] ext Printed extension structure to modify. + * @param[in,out] mem Memory chunk of the size returned by ::lyplg_ext_compiled_size_clb() to print into, is moved +- * after all the printed data. ++ * after all the printed data. Always use ::LY_CTXP_MEM_SIZE when moving @p mem for correct alignment. + * @param[in,out] ptr_set Set with pointers to set to printed addresses. + * @param[in,out] addr_ht Hash table with pairs of addresses of shared structures to be printed and their printed + * addresses, can be added to. +diff --git a/src/plugins_exts/metadata.c b/src/plugins_exts/metadata.c +index d5492839e..c73c8f9a8 100644 +--- a/src/plugins_exts/metadata.c ++++ b/src/plugins_exts/metadata.c +@@ -220,7 +220,7 @@ annotation_compiled_size(const struct lysc_ext_instance *ext, struct ly_ht *addr + { + int size = 0; + +- size += sizeof(struct lysc_ext_metadata); ++ size += LY_CTXP_MEM_SIZE(sizeof(struct lysc_ext_metadata)); + size += lyplg_ext_compiled_stmts_storage_size(ext->substmts, addr_ht); + + return size; +@@ -233,7 +233,7 @@ annotation_compiled_print(const struct lysc_ext_instance *orig_ext, struct lysc_ + struct lysc_ext_metadata *ann_cdata; + + ann_cdata = ext->compiled = *mem; +- *mem = (char *)*mem + sizeof *ann_cdata; ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *ann_cdata); + memset(ann_cdata, 0, sizeof *ann_cdata); + + ext->substmts[1].storage_p = (void **)&ann_cdata->units; +diff --git a/src/plugins_exts/schema_mount.c b/src/plugins_exts/schema_mount.c +index df736e5ad..3dd23f992 100644 +--- a/src/plugins_exts/schema_mount.c ++++ b/src/plugins_exts/schema_mount.c +@@ -79,8 +79,6 @@ struct sprinter_tree_priv { + lyplg_ext_compile_log(cctx, ext, LY_LLERR, LY_EINT, "Internal error (%s:%d).", __FILE__, __LINE__); \ + return LY_EINT + +-#define CTXP_MEM_SIZE(SIZE) ((SIZE) + ((~(SIZE) + 1) & (8 - 1))) +- + /** + * @brief Check if given mount point is unique among its siblings + * +@@ -1468,7 +1466,7 @@ schema_mount_compiled_size(const struct lysc_ext_instance *ext, struct ly_ht *ad + return 0; + } + +- size += CTXP_MEM_SIZE(sizeof *sm_data); ++ size += LY_CTXP_MEM_SIZE(sizeof *sm_data); + + /* ht addr check, make sure the shared context is stored only once */ + hash = lyht_hash((const char *)&sm_data->shared, sizeof sm_data->shared); +@@ -1476,12 +1474,12 @@ schema_mount_compiled_size(const struct lysc_ext_instance *ext, struct ly_ht *ad + return size; + } + +- size += CTXP_MEM_SIZE(sizeof *sm_data->shared); +- size += CTXP_MEM_SIZE(sm_data->shared->schema_count * sizeof *sm_data->shared->schemas); ++ size += LY_CTXP_MEM_SIZE(sizeof *sm_data->shared); ++ size += LY_CTXP_MEM_SIZE(sm_data->shared->schema_count * sizeof *sm_data->shared->schemas); + for (i = 0; i < sm_data->shared->schema_count; ++i) { +- size += CTXP_MEM_SIZE(ly_ctx_compiled_size(sm_data->shared->schemas[i].ctx)); +- size += CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].mount_point) + 1); +- size += CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].content_id) + 1); ++ size += LY_CTXP_MEM_SIZE(ly_ctx_compiled_size(sm_data->shared->schemas[i].ctx)); ++ size += LY_CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].mount_point) + 1); ++ size += LY_CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].content_id) + 1); + } + + /* inlined contexts cannot be reused and will not be printed */ +@@ -1501,7 +1499,7 @@ schema_mount_compiled_print(const struct lysc_ext_instance *orig_ext, struct lys + + /* sm_data */ + ext->compiled = sm_data = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *sm_data); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *sm_data); + + pthread_mutexattr_init(&attr); + pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); +@@ -1517,7 +1515,7 @@ schema_mount_compiled_print(const struct lysc_ext_instance *orig_ext, struct lys + + /* sm_data->shared */ + sm_data->shared = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *sm_data->shared); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *sm_data->shared); + + sm_data->shared->schema_count = orig_sm_data->shared->schema_count; + sm_data->shared->ref_count = orig_sm_data->shared->ref_count; +@@ -1525,7 +1523,7 @@ schema_mount_compiled_print(const struct lysc_ext_instance *orig_ext, struct lys + /* sm_data->shared->schemas */ + if (orig_sm_data->shared->schemas) { + sm_data->shared->schemas = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sm_data->shared->schema_count * sizeof *sm_data->shared->schemas); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sm_data->shared->schema_count * sizeof *sm_data->shared->schemas); + + for (i = 0; i < sm_data->shared->schema_count; ++i) { + /* ctx */ +@@ -1538,12 +1536,12 @@ schema_mount_compiled_print(const struct lysc_ext_instance *orig_ext, struct lys + /* mount_point */ + strcpy(*mem, orig_sm_data->shared->schemas[i].mount_point); + sm_data->shared->schemas[i].mount_point = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].mount_point) + 1); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].mount_point) + 1); + + /* content_id */ + strcpy(*mem, orig_sm_data->shared->schemas[i].content_id); + sm_data->shared->schemas[i].content_id = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].content_id) + 1); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(strlen(sm_data->shared->schemas[i].content_id) + 1); + } + } + +diff --git a/src/plugins_exts/structure.c b/src/plugins_exts/structure.c +index b88c9a9a5..6309a2c30 100644 +--- a/src/plugins_exts/structure.c ++++ b/src/plugins_exts/structure.c +@@ -329,9 +329,9 @@ structure_compiled_size(const struct lysc_ext_instance *ext, struct ly_ht *addr_ + + struct_cdata = ext->compiled; + +- size += sizeof *struct_cdata; ++ size += LY_CTXP_MEM_SIZE(sizeof *struct_cdata); + size += lyplg_ext_compiled_stmts_storage_size(ext->substmts, addr_ht); +- size += sizeof *struct_cdata->top_cont; ++ size += LY_CTXP_MEM_SIZE(sizeof *struct_cdata->top_cont); + + return size; + } +@@ -347,12 +347,12 @@ structure_compiled_print(const struct lysc_ext_instance *orig_ext, struct lysc_e + + /* ext structure */ + struct_cdata = ext->compiled = *mem; +- *mem = (char *)*mem + sizeof *struct_cdata; ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *struct_cdata); + memset(struct_cdata, 0, sizeof *struct_cdata); + + /* top_cont */ + struct_cdata->top_cont = *mem; +- *mem = (char *)*mem + sizeof *struct_cdata->top_cont; ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *struct_cdata->top_cont); + lyplg_ext_compiled_print_add_addr(addr_ht, orig_cdata->top_cont, struct_cdata->top_cont); + memset(struct_cdata->top_cont, 0, sizeof *struct_cdata->top_cont); + +diff --git a/src/printer_context.c b/src/printer_context.c +index 57d5f90e1..3fb50ac0a 100644 +--- a/src/printer_context.c ++++ b/src/printer_context.c +@@ -46,10 +46,10 @@ + if (ORIG_ARRAY) { \ + LY_ARRAY_COUNT_TYPE count = LY_ARRAY_COUNT(ORIG_ARRAY); \ + memcpy(*MEM, &count, sizeof count); \ +- *MEM = (char *)*MEM + CTXP_MEM_SIZE(sizeof count); \ ++ *MEM = (char *)*MEM + LY_CTXP_MEM_SIZE(sizeof count); \ + \ + ARRAY = *MEM; \ +- *MEM = (char *)*MEM + CTXP_MEM_SIZE(count * sizeof *ORIG_ARRAY); \ ++ *MEM = (char *)*MEM + LY_CTXP_MEM_SIZE(count * sizeof *ORIG_ARRAY); \ + } else { \ + ARRAY = NULL; \ + } +@@ -67,24 +67,12 @@ + #define CTXP_OPTIONAL_STRUCT(FUNC, ORIG_PTR, PTR, ADDR_HT, PTR_SET, MEM) \ + if (ORIG_PTR) { \ + PTR = *MEM; \ +- *MEM = (char *)*MEM + CTXP_MEM_SIZE(sizeof *PTR); \ ++ *MEM = (char *)*MEM + LY_CTXP_MEM_SIZE(sizeof *PTR); \ + FUNC(ORIG_PTR, PTR, ADDR_HT, PTR_SET, MEM); \ + } else { \ + PTR = NULL; \ + } + +-/** +- * @brief Alignment of all the printed data, ensures all memory access is aligned to this number (B) +- */ +-#define CTXP_MEM_ALIGN 8 +- +-/** +- * @brief Adjust data size to an aligned size to make sure the following data is aligned. +- * +- * @param[in,out] SIZE Data size that is adjusted. +- */ +-#define CTXP_MEM_SIZE(SIZE) ((SIZE) + ((~(SIZE) + 1) & (CTXP_MEM_ALIGN - 1))) +- + static void + ctxs_dict_strings(const struct ly_ht *ht, int *size) + { +@@ -96,7 +84,7 @@ ctxs_dict_strings(const struct ly_ht *ht, int *size) + dict_rec = (struct ly_dict_rec *)&rec->val; + + /* strings */ +- *size += CTXP_MEM_SIZE(strlen(dict_rec->value) + 1); ++ *size += LY_CTXP_MEM_SIZE(strlen(dict_rec->value) + 1); + } + } + +@@ -117,7 +105,7 @@ ctxs_exts(const struct lysc_ext_instance *exts, struct ly_ht *ht, int *size) + + /* compiled, substmts storage */ + if (exts[u].def->plugin_ref && (ext_plg = LYSC_GET_EXT_PLG(exts[u].def->plugin_ref)) && ext_plg->compiled_size) { +- *size += CTXP_MEM_SIZE(ext_plg->compiled_size(&exts[u], ht)); ++ *size += LY_CTXP_MEM_SIZE(ext_plg->compiled_size(&exts[u], ht)); + } + } + } +@@ -131,7 +119,7 @@ ctxs_prefixes(const struct lysc_prefix *prefixes, int *size) + LY_ARRAY_FOR(prefixes, u) { + /* string not in the dictionary */ + if (prefixes[u].prefix) { +- *size += CTXP_MEM_SIZE(strlen(prefixes[u].prefix) + 1); ++ *size += LY_CTXP_MEM_SIZE(strlen(prefixes[u].prefix) + 1); + } + } + } +@@ -141,20 +129,20 @@ ctxs_expr(const struct lyxp_expr *exp, int *size) + { + uint32_t i, j; + +- *size += CTXP_MEM_SIZE(sizeof *exp); ++ *size += LY_CTXP_MEM_SIZE(sizeof *exp); + +- *size += CTXP_MEM_SIZE(strlen(exp->expr) + 1); +- *size += CTXP_MEM_SIZE(exp->used * sizeof *exp->tokens); +- *size += CTXP_MEM_SIZE(exp->used * sizeof *exp->tok_pos); +- *size += CTXP_MEM_SIZE(exp->used * sizeof *exp->tok_len); +- *size += CTXP_MEM_SIZE(exp->used * sizeof *exp->repeat); ++ *size += LY_CTXP_MEM_SIZE(strlen(exp->expr) + 1); ++ *size += LY_CTXP_MEM_SIZE(exp->used * sizeof *exp->tokens); ++ *size += LY_CTXP_MEM_SIZE(exp->used * sizeof *exp->tok_pos); ++ *size += LY_CTXP_MEM_SIZE(exp->used * sizeof *exp->tok_len); ++ *size += LY_CTXP_MEM_SIZE(exp->used * sizeof *exp->repeat); + for (i = 0; i < exp->used; ++i) { + if (!exp->repeat[i]) { + continue; + } + + for (j = 0; exp->repeat[i][j]; ++j) {} +- *size += CTXP_MEM_SIZE((j + 1) * sizeof **exp->repeat); ++ *size += LY_CTXP_MEM_SIZE((j + 1) * sizeof **exp->repeat); + } + } + +@@ -182,7 +170,7 @@ ctxs_when(const struct lysc_when *when, struct ly_ht *ht, int *size) + return; + } + +- *size += CTXP_MEM_SIZE(sizeof *when); ++ *size += LY_CTXP_MEM_SIZE(sizeof *when); + + ctxs_expr(when->cond, size); + ctxs_prefixes(when->prefixes, size); +@@ -207,7 +195,7 @@ ctxs_range(const struct lysc_range *range, struct ly_ht *ht, int *size) + return; + } + +- *size += CTXP_MEM_SIZE(sizeof *range); ++ *size += LY_CTXP_MEM_SIZE(sizeof *range); + + *size += CTXS_SIZED_ARRAY(range->parts); + ctxs_exts(range->exts, ht, size); +@@ -227,7 +215,7 @@ ctxs_patterns(const struct lysc_pattern **patterns, struct ly_ht *ht, int *size) + continue; + } + +- *size += CTXP_MEM_SIZE(sizeof *patterns[u]); ++ *size += LY_CTXP_MEM_SIZE(sizeof *patterns[u]); + ctxs_exts(patterns[u]->exts, ht, size); + } + } +@@ -270,7 +258,7 @@ ctxs_type(const struct lysc_type *type, struct ly_ht *ht, int *size) + switch (type->basetype) { + case LY_TYPE_BINARY: + type_bin = (const struct lysc_type_bin *)type; +- *size += CTXP_MEM_SIZE(sizeof *type_bin); ++ *size += LY_CTXP_MEM_SIZE(sizeof *type_bin); + + ctxs_range(type_bin->length, ht, size); + break; +@@ -283,13 +271,13 @@ ctxs_type(const struct lysc_type *type, struct ly_ht *ht, int *size) + case LY_TYPE_INT32: + case LY_TYPE_INT64: + type_num = (const struct lysc_type_num *)type; +- *size += CTXP_MEM_SIZE(sizeof *type_num); ++ *size += LY_CTXP_MEM_SIZE(sizeof *type_num); + + ctxs_range(type_num->range, ht, size); + break; + case LY_TYPE_STRING: + type_str = (const struct lysc_type_str *)type; +- *size += CTXP_MEM_SIZE(sizeof *type_str); ++ *size += LY_CTXP_MEM_SIZE(sizeof *type_str); + + ctxs_range(type_str->length, ht, size); + ctxs_patterns((const struct lysc_pattern **)type_str->patterns, ht, size); +@@ -297,33 +285,33 @@ ctxs_type(const struct lysc_type *type, struct ly_ht *ht, int *size) + case LY_TYPE_BITS: + case LY_TYPE_ENUM: + type_enum_bits = (const struct lysc_type_enum *)type; +- *size += CTXP_MEM_SIZE(sizeof *type_enum_bits); ++ *size += LY_CTXP_MEM_SIZE(sizeof *type_enum_bits); + + ctxs_enums(type_enum_bits->enums, ht, size); + break; + case LY_TYPE_BOOL: + case LY_TYPE_EMPTY: +- *size += CTXP_MEM_SIZE(sizeof *type); ++ *size += LY_CTXP_MEM_SIZE(sizeof *type); + break; + case LY_TYPE_DEC64: + type_dec = (const struct lysc_type_dec *)type; +- *size += CTXP_MEM_SIZE(sizeof *type_dec); ++ *size += LY_CTXP_MEM_SIZE(sizeof *type_dec); + + ctxs_range(type_dec->range, ht, size); + break; + case LY_TYPE_IDENT: + type_identref = (const struct lysc_type_identityref *)type; +- *size += CTXP_MEM_SIZE(sizeof *type_identref); ++ *size += LY_CTXP_MEM_SIZE(sizeof *type_identref); + + *size += CTXS_SIZED_ARRAY(type_identref->bases); + break; + case LY_TYPE_INST: + type_instid = (const struct lysc_type_instanceid *)type; +- *size += CTXP_MEM_SIZE(sizeof *type_instid); ++ *size += LY_CTXP_MEM_SIZE(sizeof *type_instid); + break; + case LY_TYPE_LEAFREF: + type_lref = (const struct lysc_type_leafref *)type; +- *size += CTXP_MEM_SIZE(sizeof *type_lref); ++ *size += LY_CTXP_MEM_SIZE(sizeof *type_lref); + + ctxs_expr(type_lref->path, size); + ctxs_prefixes(type_lref->prefixes, size); +@@ -331,7 +319,7 @@ ctxs_type(const struct lysc_type *type, struct ly_ht *ht, int *size) + break; + case LY_TYPE_UNION: + type_union = (const struct lysc_type_union *)type; +- *size += CTXP_MEM_SIZE(sizeof *type_union); ++ *size += LY_CTXP_MEM_SIZE(sizeof *type_union); + + *size += CTXS_SIZED_ARRAY(type_union->types); + LY_ARRAY_FOR(type_union->types, u) { +@@ -366,7 +354,7 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size) + switch (node->nodetype) { + case LYS_CONTAINER: + cont = (const struct lysc_node_container *)node; +- *size += CTXP_MEM_SIZE(sizeof *cont); ++ *size += LY_CTXP_MEM_SIZE(sizeof *cont); + + LY_LIST_FOR(cont->child, child) { + ctxs_node(child, ht, size); +@@ -382,7 +370,7 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size) + break; + case LYS_CHOICE: + choic = (const struct lysc_node_choice *)node; +- *size += CTXP_MEM_SIZE(sizeof *choic); ++ *size += LY_CTXP_MEM_SIZE(sizeof *choic); + + LY_LIST_FOR((const struct lysc_node *)choic->cases, child) { + ctxs_node(child, ht, size); +@@ -391,7 +379,7 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size) + break; + case LYS_LEAF: + leaf = (const struct lysc_node_leaf *)node; +- *size += CTXP_MEM_SIZE(sizeof *leaf); ++ *size += LY_CTXP_MEM_SIZE(sizeof *leaf); + + ctxs_musts(leaf->musts, ht, size); + ctxs_whens((const struct lysc_when **)leaf->when, ht, size); +@@ -400,7 +388,7 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size) + break; + case LYS_LEAFLIST: + llist = (const struct lysc_node_leaflist *)node; +- *size += CTXP_MEM_SIZE(sizeof *llist); ++ *size += LY_CTXP_MEM_SIZE(sizeof *llist); + + ctxs_musts(llist->musts, ht, size); + ctxs_whens((const struct lysc_when **)llist->when, ht, size); +@@ -412,7 +400,7 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size) + break; + case LYS_LIST: + list = (const struct lysc_node_list *)node; +- *size += CTXP_MEM_SIZE(sizeof *list); ++ *size += LY_CTXP_MEM_SIZE(sizeof *list); + + LY_LIST_FOR(list->child, child) { + ctxs_node(child, ht, size); +@@ -427,21 +415,21 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size) + } + *size += CTXS_SIZED_ARRAY(list->uniques); + LY_ARRAY_FOR(list->uniques, u) { +- *size += CTXP_MEM_SIZE(sizeof(LY_ARRAY_COUNT_TYPE)) + +- CTXP_MEM_SIZE(LY_ARRAY_COUNT(list->uniques[u]) * sizeof **list->uniques); ++ *size += LY_CTXP_MEM_SIZE(sizeof(LY_ARRAY_COUNT_TYPE)) + ++ LY_CTXP_MEM_SIZE(LY_ARRAY_COUNT(list->uniques[u]) * sizeof **list->uniques); + } + break; + case LYS_ANYXML: + case LYS_ANYDATA: + any = (const struct lysc_node_anydata *)node; +- *size += CTXP_MEM_SIZE(sizeof *any); ++ *size += LY_CTXP_MEM_SIZE(sizeof *any); + + ctxs_musts(any->musts, ht, size); + ctxs_whens((const struct lysc_when **)any->when, ht, size); + break; + case LYS_CASE: + cas = (const struct lysc_node_case *)node; +- *size += CTXP_MEM_SIZE(sizeof *cas); ++ *size += LY_CTXP_MEM_SIZE(sizeof *cas); + + LY_LIST_FOR(cas->child, child) { + ctxs_node(child, ht, size); +@@ -451,7 +439,7 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size) + case LYS_RPC: + case LYS_ACTION: + act = (const struct lysc_node_action *)node; +- *size += CTXP_MEM_SIZE(sizeof *act); ++ *size += LY_CTXP_MEM_SIZE(sizeof *act); + + ctxs_whens((const struct lysc_when **)act->when, ht, size); + ctxs_node((struct lysc_node *)&act->input, ht, size); +@@ -469,7 +457,7 @@ ctxs_node(const struct lysc_node *node, struct ly_ht *ht, int *size) + break; + case LYS_NOTIF: + notif = (const struct lysc_node_notif *)node; +- *size += CTXP_MEM_SIZE(sizeof *notif); ++ *size += LY_CTXP_MEM_SIZE(sizeof *notif); + + LY_LIST_FOR(notif->child, child) { + ctxs_node(child, ht, size); +@@ -493,7 +481,7 @@ ctxs_compiled(const struct lysc_module *compiled, struct ly_ht *ht, int *size) + } + + /* compiled module */ +- *size += CTXP_MEM_SIZE(sizeof *compiled); ++ *size += LY_CTXP_MEM_SIZE(sizeof *compiled); + + *size += CTXS_SIZED_ARRAY(compiled->features); + LY_LIST_FOR(compiled->data, node) { +@@ -539,7 +527,7 @@ static void + ctxs_module(const struct lys_module *mod, struct ly_ht *ht, int *size) + { + /* module */ +- *size += CTXP_MEM_SIZE(sizeof *mod); ++ *size += LY_CTXP_MEM_SIZE(sizeof *mod); + + /* compiled module */ + ctxs_compiled(mod->compiled, ht, size); +@@ -561,13 +549,13 @@ ly_ctx_compiled_size_context(const struct ly_ctx *ctx, struct ly_ht *addr_ht, in + const struct lys_module *mod; + + /* context */ +- *size += CTXP_MEM_SIZE(sizeof *ctx); ++ *size += LY_CTXP_MEM_SIZE(sizeof *ctx); + + /* all the strings in the dictionary */ + ctxs_dict_strings(ctx->dict.hash_tab, size); + + /* module set */ +- *size += CTXP_MEM_SIZE(ctx->modules.count * sizeof ctx->modules.objs); ++ *size += LY_CTXP_MEM_SIZE(ctx->modules.count * sizeof ctx->modules.objs); + for (i = 0; i < ctx->modules.count; ++i) { + mod = ctx->modules.objs[i]; + +@@ -720,7 +708,7 @@ ctxp_set(const struct ly_set *orig_set, struct ly_set *set, void **mem) + set->size = orig_set->count; + set->count = orig_set->count; + set->objs = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(set->count * sizeof set->objs); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(set->count * sizeof set->objs); + } + + static void +@@ -741,7 +729,7 @@ ctxp_dict_strings(const struct ly_ht *orig_dict, struct ly_ht *addr_ht, void **m + orig_str = dict_rec->value; + + str = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(len); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(len); + + memcpy(str, orig_str, len); + +@@ -801,30 +789,30 @@ ctxp_expr(const struct lyxp_expr *orig_exp, struct lyxp_expr *exp, void **mem) + uint32_t i, len; + + exp->expr = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(strlen(orig_exp->expr) + 1); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(strlen(orig_exp->expr) + 1); + memcpy(exp->expr, orig_exp->expr, strlen(orig_exp->expr) + 1); + + exp->tokens = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->tokens); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->tokens); + memcpy(exp->tokens, orig_exp->tokens, orig_exp->used * sizeof *exp->tokens); + + exp->tok_pos = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->tok_pos); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->tok_pos); + memcpy(exp->tok_pos, orig_exp->tok_pos, orig_exp->used * sizeof *exp->tok_pos); + + exp->tok_len = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->tok_len); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->tok_len); + memcpy(exp->tok_len, orig_exp->tok_len, orig_exp->used * sizeof *exp->tok_len); + + exp->repeat = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->repeat); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(orig_exp->used * sizeof *exp->repeat); + for (i = 0; i < orig_exp->used; ++i) { + if (orig_exp->repeat[i]) { + for (len = 0; orig_exp->repeat[i][len]; ++len) {} + ++len; + + exp->repeat[i] = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(len * sizeof **exp->repeat); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(len * sizeof **exp->repeat); + memcpy(exp->repeat[i], orig_exp->repeat[i], len * sizeof **exp->repeat); + } else { + exp->repeat[i] = NULL; +@@ -845,7 +833,7 @@ ctxp_prefix(const struct lysc_prefix *orig_prefix, struct lysc_prefix *prefix, s + size = strlen(orig_prefix->prefix) + 1; + + prefix->prefix = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(size); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(size); + memcpy(prefix->prefix, orig_prefix->prefix, size); + } else { + prefix->prefix = NULL; +@@ -867,7 +855,7 @@ ctxp_must(const struct lysc_must *orig_must, struct lysc_must *must, struct ly_h + } + + must->cond = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *must->cond); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *must->cond); + ctxp_expr(orig_must->cond, must->cond, mem); + + CTXP_SIZED_ARRAY(orig_must->prefixes, must->prefixes, mem); +@@ -901,7 +889,7 @@ ctxp_when(const struct lysc_when *orig_when, struct lysc_when **when, struct ly_ + } + + w = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *w); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *w); + + if (orig_when->exts) { + /* may be referenced in the parent */ +@@ -909,7 +897,7 @@ ctxp_when(const struct lysc_when *orig_when, struct lysc_when **when, struct ly_ + } + + w->cond = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *w->cond); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *w->cond); + ctxp_expr(orig_when->cond, w->cond, mem); + + w->context = ly_ctx_compiled_addr_ht_get(addr_ht, orig_when->context, 0); +@@ -980,7 +968,7 @@ ctxp_children(const struct lysc_node *orig_child, struct lysc_node **child, stru + } + + ch = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(node_size); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(node_size); + + /* referenced by pointers */ + ly_ctx_compiled_addr_ht_add(addr_ht, orig_child, ch); +@@ -1035,7 +1023,7 @@ ctxp_pattern(const struct lysc_pattern *orig_pattern, struct lysc_pattern **patt + } + + p = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *p); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *p); + + if (orig_pattern->exts) { + /* may be referenced in the parent */ +@@ -1122,7 +1110,7 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_ + case LY_TYPE_BINARY: + orig_type_bin = (const struct lysc_type_bin *)orig_type; + t = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_bin); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_bin); + t_bin = (struct lysc_type_bin *)t; + + CTXP_OPTIONAL_STRUCT(ctxp_range, orig_type_bin->length, t_bin->length, addr_ht, ptr_set, mem); +@@ -1137,7 +1125,7 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_ + case LY_TYPE_INT64: + orig_type_num = (const struct lysc_type_num *)orig_type; + t = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_num); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_num); + t_num = (struct lysc_type_num *)t; + + CTXP_OPTIONAL_STRUCT(ctxp_range, orig_type_num->range, t_num->range, addr_ht, ptr_set, mem); +@@ -1145,7 +1133,7 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_ + case LY_TYPE_STRING: + orig_type_str = (const struct lysc_type_str *)orig_type; + t = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_str); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_str); + t_str = (struct lysc_type_str *)t; + + CTXP_OPTIONAL_STRUCT(ctxp_range, orig_type_str->length, t_str->length, addr_ht, ptr_set, mem); +@@ -1158,7 +1146,7 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_ + case LY_TYPE_ENUM: + orig_type_enum_bits = (const struct lysc_type_enum *)orig_type; + t = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_enum_bits); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_enum_bits); + t_enum_bits = (struct lysc_type_enum *)t; + + CTXP_SIZED_ARRAY(orig_type_enum_bits->enums, t_enum_bits->enums, mem); +@@ -1170,12 +1158,12 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_ + case LY_TYPE_EMPTY: + /* no additional members */ + t = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type); + break; + case LY_TYPE_DEC64: + orig_type_dec = (const struct lysc_type_dec *)orig_type; + t = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_dec); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_dec); + t_dec = (struct lysc_type_dec *)t; + + t_dec->fraction_digits = orig_type_dec->fraction_digits; +@@ -1184,7 +1172,7 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_ + case LY_TYPE_IDENT: + orig_type_identref = (const struct lysc_type_identityref *)orig_type; + t = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_identref); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_identref); + t_identref = (struct lysc_type_identityref *)t; + + CTXP_SIZED_ARRAY(orig_type_identref->bases, t_identref->bases, mem); +@@ -1197,7 +1185,7 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_ + case LY_TYPE_INST: + orig_type_instid = (const struct lysc_type_instanceid *)orig_type; + t = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_instid); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_instid); + t_instid = (struct lysc_type_instanceid *)t; + + t_instid->require_instance = orig_type_instid->require_instance; +@@ -1205,11 +1193,11 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_ + case LY_TYPE_LEAFREF: + orig_type_lref = (const struct lysc_type_leafref *)orig_type; + t = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_lref); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_lref); + t_lref = (struct lysc_type_leafref *)t; + + t_lref->path = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *t_lref->path); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *t_lref->path); + ctxp_expr(orig_type_lref->path, t_lref->path, mem); + + CTXP_SIZED_ARRAY(orig_type_lref->prefixes, t_lref->prefixes, mem); +@@ -1223,7 +1211,7 @@ ctxp_type(const struct lysc_type *orig_type, struct lysc_type **type, struct ly_ + case LY_TYPE_UNION: + orig_type_union = (const struct lysc_type_union *)orig_type; + t = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof *orig_type_union); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof *orig_type_union); + t_union = (struct lysc_type_union *)t; + + CTXP_SIZED_ARRAY(orig_type_union->types, t_union->types, mem); +@@ -1736,7 +1724,7 @@ ly_ctx_compiled_print_context(const struct ly_ctx *orig_ctx, struct ly_ctx *ctx, + for (i = 0; i < ctx->modules.count; ++i) { + /* allocate the shared module and store its new address in HT so it can be referenced */ + ctx->modules.objs[i] = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof(struct lys_module)); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof(struct lys_module)); + ly_ctx_compiled_addr_ht_add(addr_ht, orig_ctx->modules.objs[i], ctx->modules.objs[i]); + } + for (i = 0; i < ctx->modules.count; ++i) { +@@ -1864,7 +1852,7 @@ ly_ctx_compiled_ext_stmts_storage_print(const struct lysc_ext_substmt *orig_subs + case LY_STMT_LENGTH: + case LY_STMT_RANGE: + *substmts[u].storage_p = *mem; +- *mem = (char *)*mem + CTXP_MEM_SIZE(sizeof(struct lysc_range)); ++ *mem = (char *)*mem + LY_CTXP_MEM_SIZE(sizeof(struct lysc_range)); + ctxp_range(*orig_substmts[u].storage_p, *substmts[u].storage_p, addr_ht, ptr_set, mem); + break; + case LY_STMT_MAX_ELEMENTS: +-- +2.43.0 + diff --git a/patches/libyang/4.2.2/0002-printer-context-BUGFIX-missing-mem-alignment.patch b/patches/libyang/4.2.2/0002-printer-context-BUGFIX-missing-mem-alignment.patch new file mode 100644 index 00000000..9c6dabc8 --- /dev/null +++ b/patches/libyang/4.2.2/0002-printer-context-BUGFIX-missing-mem-alignment.patch @@ -0,0 +1,46 @@ +From 4d32b003a7aaad5ecca9a013db2dbaa36ef79692 Mon Sep 17 00:00:00 2001 +From: Michal Vasko +Date: Wed, 10 Dec 2025 10:13:54 +0100 +Subject: [PATCH 2/6] printer context BUGFIX missing mem alignment +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Refs #2455 + +Signed-off-by: Mattias Walström +--- + src/context.c | 2 +- + src/printer_context.c | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/context.c b/src/context.c +index 88f4678b3..52a4ae46d 100644 +--- a/src/context.c ++++ b/src/context.c +@@ -1459,7 +1459,7 @@ ly_ctx_compiled_print(const struct ly_ctx *ctx, void *mem, void **mem_end) + + /* context, referenced */ + pctx = mem; +- mem = (char *)mem + sizeof *pctx; ++ mem = (char *)mem + LY_CTXP_MEM_SIZE(sizeof *pctx); + ly_ctx_compiled_addr_ht_add(addr_ht, ctx, pctx); + + /* members */ +diff --git a/src/printer_context.c b/src/printer_context.c +index 3fb50ac0a..ade0a210d 100644 +--- a/src/printer_context.c ++++ b/src/printer_context.c +@@ -33,7 +33,7 @@ + * @return Size of @p ARRAY. + */ + #define CTXS_SIZED_ARRAY(ARRAY) \ +- (ARRAY ? sizeof(LY_ARRAY_COUNT_TYPE) : 0) + LY_ARRAY_COUNT(ARRAY) * sizeof *ARRAY ++ (ARRAY ? LY_CTXP_MEM_SIZE(sizeof(LY_ARRAY_COUNT_TYPE)) : 0) + LY_CTXP_MEM_SIZE(LY_ARRAY_COUNT(ARRAY) * sizeof *ARRAY) + + /** + * @brief Print (serialize) a sized array. +-- +2.43.0 + diff --git a/patches/libyang/4.2.2/0003-lyb-BUGFIX-big-endian-fixes.patch b/patches/libyang/4.2.2/0003-lyb-BUGFIX-big-endian-fixes.patch new file mode 100644 index 00000000..386475d9 --- /dev/null +++ b/patches/libyang/4.2.2/0003-lyb-BUGFIX-big-endian-fixes.patch @@ -0,0 +1,102 @@ +From 51f257fe100adde66efc4d93a5e40b2e5a9a9fab Mon Sep 17 00:00:00 2001 +From: Michal Vasko +Date: Thu, 11 Dec 2025 12:05:02 +0100 +Subject: [PATCH 3/6] lyb BUGFIX big-endian fixes +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Refs #2455 + +Signed-off-by: Mattias Walström +--- + src/parser_lyb.c | 8 +++++++- + src/printer_lyb.c | 18 ++++++++++++------ + 2 files changed, 19 insertions(+), 7 deletions(-) + +diff --git a/src/parser_lyb.c b/src/parser_lyb.c +index 43ab02f95..82c759c3d 100644 +--- a/src/parser_lyb.c ++++ b/src/parser_lyb.c +@@ -1425,7 +1425,10 @@ lyb_parse_node(struct lyd_lyb_ctx *lybctx, struct lyd_node *parent, struct lyd_n + char *mod_name = NULL, mod_rev[LY_REV_SIZE]; + + /* read node type */ +- lyb_read((uint64_t *)&lyb_type, LYB_NODE_TYPE_BITS, lybctx->parse_ctx); ++ lyb_read(&lyb_type, LYB_NODE_TYPE_BITS, lybctx->parse_ctx); ++ ++ /* correct byte order */ ++ lyb_type = le32toh(lyb_type); + + switch (lyb_type) { + case LYB_NODE_END: +@@ -1543,6 +1546,9 @@ lyb_parse_header(struct lyd_lyb_ctx *lybctx) + data_hash = 0; + lyb_read((uint8_t *)&data_hash, LYB_HEADER_CTX_HASH_BITS, pctx); + ++ /* correct byte order */ ++ data_hash = le32toh(data_hash); ++ + if (!data_hash) { + /* fine for no data */ + pctx->empty_hash = 1; +diff --git a/src/printer_lyb.c b/src/printer_lyb.c +index b35d3c0fe..7094467a7 100644 +--- a/src/printer_lyb.c ++++ b/src/printer_lyb.c +@@ -447,12 +447,12 @@ lyb_write_count(uint32_t count, struct lylyb_print_ctx *lybctx) + return LY_EINT; + } + +- /* correct byte order */ +- count = htole32(count); +- + /* copy count to buf */ + buf |= count << prefix_b; + ++ /* correct byte order */ ++ buf = htole32(buf); ++ + return lyb_write(&buf, prefix_b + num_b, lybctx); + } + +@@ -492,12 +492,12 @@ lyb_write_size(uint32_t size, struct lylyb_print_ctx *lybctx) + num_b = 32; + } + +- /* correct byte order */ +- size = htole32(size); +- + /* copy size to buf */ + buf |= size << prefix_b; + ++ /* correct byte order */ ++ buf = htole32(buf); ++ + return lyb_write(&buf, prefix_b + num_b, lybctx); + } + +@@ -635,6 +635,9 @@ lyb_print_header(struct lylyb_print_ctx *lybctx) + /* context hash (is truncated), if not printing empty data */ + if (lybctx->ctx) { + hash = lyb_truncate_hash_nonzero(ly_ctx_get_modules_hash(lybctx->ctx), LYB_HEADER_CTX_HASH_BITS); ++ ++ /* correct byte order */ ++ hash = htole32(hash); + } else { + hash = 0; + } +@@ -1019,6 +1022,9 @@ lyb_print_lyb_type(const struct lyd_node *node, struct lylyb_print_ctx *lybctx) + lyb_type = LYB_NODE_CHILD; + } + ++ /* correct byte order */ ++ lyb_type = htole32(lyb_type); ++ + LY_CHECK_RET(lyb_write(&lyb_type, LYB_NODE_TYPE_BITS, lybctx)); + + return LY_SUCCESS; +-- +2.43.0 + diff --git a/patches/libyang/4.2.2/0004-bits-BUGFIX-operator-priority.patch b/patches/libyang/4.2.2/0004-bits-BUGFIX-operator-priority.patch new file mode 100644 index 00000000..7ed17bfa --- /dev/null +++ b/patches/libyang/4.2.2/0004-bits-BUGFIX-operator-priority.patch @@ -0,0 +1,41 @@ +From a44103219029ba70b67a87d5a33cfb8aec1e03f3 Mon Sep 17 00:00:00 2001 +From: Michal Vasko +Date: Thu, 11 Dec 2025 12:05:51 +0100 +Subject: [PATCH 4/6] bits BUGFIX operator priority +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Refs #2455 + +Signed-off-by: Mattias Walström +--- + src/plugins_types/bits.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/plugins_types/bits.c b/src/plugins_types/bits.c +index ab7acb527..f5fa6dccb 100644 +--- a/src/plugins_types/bits.c ++++ b/src/plugins_types/bits.c +@@ -61,7 +61,7 @@ lyplg_type_bits_is_bit_set(const char *bitmap, uint32_t size_bits, uint32_t bit_ + uint32_t size; + + /* get size in bytes */ +- size = size_bits / 8 + (size_bits % 8) ? 1 : 0; ++ size = size_bits / 8 + ((size_bits % 8) ? 1 : 0); + + /* find the byte with our bit */ + (void)size; +@@ -93,7 +93,7 @@ bits_bit_set(char *bitmap, uint32_t size_bits, uint32_t bit_position) + uint32_t size; + + /* get size in bytes */ +- size = size_bits / 8 + (size_bits % 8) ? 1 : 0; ++ size = size_bits / 8 + ((size_bits % 8) ? 1 : 0); + + /* find the byte with our bit */ + (void)size; +-- +2.43.0 + diff --git a/patches/libyang/4.2.2/0005-integer-BUGFIX-big-endian-print-fixes.patch b/patches/libyang/4.2.2/0005-integer-BUGFIX-big-endian-print-fixes.patch new file mode 100644 index 00000000..787052e1 --- /dev/null +++ b/patches/libyang/4.2.2/0005-integer-BUGFIX-big-endian-print-fixes.patch @@ -0,0 +1,223 @@ +From e2c06476b886642411181e0a236b95de14145940 Mon Sep 17 00:00:00 2001 +From: Michal Vasko +Date: Thu, 11 Dec 2025 12:06:15 +0100 +Subject: [PATCH 5/6] integer BUGFIX big-endian print fixes +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Refs #2455 + +Signed-off-by: Mattias Walström +--- + src/plugins_types/integer.c | 96 ++++++++++--------------------------- + 1 file changed, 24 insertions(+), 72 deletions(-) + +diff --git a/src/plugins_types/integer.c b/src/plugins_types/integer.c +index 945d93f86..f05096970 100644 +--- a/src/plugins_types/integer.c ++++ b/src/plugins_types/integer.c +@@ -291,64 +291,6 @@ lyplg_type_sort_int(const struct ly_ctx *UNUSED(ctx), const struct lyd_value *va + return 0; + } + +-static const void * +-lyplg_type_print_int(const struct ly_ctx *UNUSED(ctx), const struct lyd_value *value, LY_VALUE_FORMAT format, +- void *UNUSED(prefix_data), ly_bool *dynamic, uint32_t *value_size_bits) +-{ +- int64_t prev_num = 0, num = 0; +- void *buf; +- +- if (format == LY_VALUE_LYB) { +- switch (value->realtype->basetype) { +- case LY_TYPE_INT8: +- prev_num = num = value->int8; +- break; +- case LY_TYPE_INT16: +- prev_num = num = value->int16; +- break; +- case LY_TYPE_INT32: +- prev_num = num = value->int32; +- break; +- case LY_TYPE_INT64: +- prev_num = num = value->int64; +- break; +- default: +- break; +- } +- +- num = htole64(num); +- if (num == prev_num) { +- /* values are equal, little-endian or int8 */ +- *dynamic = 0; +- if (value_size_bits) { +- /* the least amount of bits that can hold the number */ +- *value_size_bits = lyplg_type_get_highest_set_bit_pos(num); +- } +- return &value->int64; +- } else { +- /* values differ, big-endian */ +- buf = calloc(1, LYPLG_BITS2BYTES(lyplg_type_get_highest_set_bit_pos(num))); +- LY_CHECK_RET(!buf, NULL); +- +- *dynamic = 1; +- if (value_size_bits) { +- *value_size_bits = lyplg_type_get_highest_set_bit_pos(num); +- } +- memcpy(buf, &num, LYPLG_BITS2BYTES(lyplg_type_get_highest_set_bit_pos(num))); +- return buf; +- } +- } +- +- /* use the cached canonical value */ +- if (dynamic) { +- *dynamic = 0; +- } +- if (value_size_bits) { +- *value_size_bits = strlen(value->_canonical) * 8; +- } +- return value->_canonical; +-} +- + static LY_ERR + lyplg_type_store_uint(const struct ly_ctx *ctx, const struct lysc_type *type, const void *value, uint32_t value_size_bits, + uint32_t options, LY_VALUE_FORMAT format, void *UNUSED(prefix_data), uint32_t hints, +@@ -573,31 +515,37 @@ lyplg_type_sort_uint(const struct ly_ctx *UNUSED(ctx), const struct lyd_value *v + } + + static const void * +-lyplg_type_print_uint(const struct ly_ctx *UNUSED(ctx), const struct lyd_value *value, LY_VALUE_FORMAT format, ++lyplg_type_print_u_int(const struct ly_ctx *UNUSED(ctx), const struct lyd_value *value, LY_VALUE_FORMAT format, + void *UNUSED(prefix_data), ly_bool *dynamic, uint32_t *value_size_bits) + { + uint64_t num = 0; ++ uint8_t bytes_used; ++ uint16_t bits_used; + void *buf; + + if (format == LY_VALUE_LYB) { + switch (value->realtype->basetype) { + case LY_TYPE_UINT8: ++ case LY_TYPE_INT8: + num = value->uint8; + break; + case LY_TYPE_UINT16: ++ case LY_TYPE_INT16: + num = value->uint16; + break; + case LY_TYPE_UINT32: ++ case LY_TYPE_INT32: + num = value->uint32; + break; + case LY_TYPE_UINT64: ++ case LY_TYPE_INT64: + num = value->uint64; + break; + default: + break; + } +- num = htole64(num); +- if (num == value->uint64) { ++ ++ if (htole64(num) == value->uint64) { + /* values are equal, little-endian or uint8 */ + *dynamic = 0; + if (value_size_bits) { +@@ -607,14 +555,18 @@ lyplg_type_print_uint(const struct ly_ctx *UNUSED(ctx), const struct lyd_value * + return &value->uint64; + } else { + /* values differ, big-endian */ +- buf = calloc(1, LYPLG_BITS2BYTES(lyplg_type_get_highest_set_bit_pos(num))); ++ bits_used = lyplg_type_get_highest_set_bit_pos(num); ++ bytes_used = LYPLG_BITS2BYTES(bits_used); ++ ++ buf = calloc(1, bytes_used); + LY_CHECK_RET(!buf, NULL); ++ num = htole64(num); ++ memcpy(buf, &num, bytes_used); + + *dynamic = 1; + if (value_size_bits) { +- *value_size_bits = lyplg_type_get_highest_set_bit_pos(num); ++ *value_size_bits = bits_used; + } +- memcpy(buf, &num, LYPLG_BITS2BYTES(lyplg_type_get_highest_set_bit_pos(num))); + return buf; + } + } +@@ -649,7 +601,7 @@ const struct lyplg_type_record plugins_integer[] = { + .plugin.validate_tree = NULL, + .plugin.compare = lyplg_type_compare_uint, + .plugin.sort = lyplg_type_sort_uint, +- .plugin.print = lyplg_type_print_uint, ++ .plugin.print = lyplg_type_print_u_int, + .plugin.duplicate = lyplg_type_dup_simple, + .plugin.free = lyplg_type_free_simple, + }, { +@@ -664,7 +616,7 @@ const struct lyplg_type_record plugins_integer[] = { + .plugin.validate_tree = NULL, + .plugin.compare = lyplg_type_compare_uint, + .plugin.sort = lyplg_type_sort_uint, +- .plugin.print = lyplg_type_print_uint, ++ .plugin.print = lyplg_type_print_u_int, + .plugin.duplicate = lyplg_type_dup_simple, + .plugin.free = lyplg_type_free_simple, + }, { +@@ -679,7 +631,7 @@ const struct lyplg_type_record plugins_integer[] = { + .plugin.validate_tree = NULL, + .plugin.compare = lyplg_type_compare_uint, + .plugin.sort = lyplg_type_sort_uint, +- .plugin.print = lyplg_type_print_uint, ++ .plugin.print = lyplg_type_print_u_int, + .plugin.duplicate = lyplg_type_dup_simple, + .plugin.free = lyplg_type_free_simple, + }, { +@@ -694,7 +646,7 @@ const struct lyplg_type_record plugins_integer[] = { + .plugin.validate_tree = NULL, + .plugin.compare = lyplg_type_compare_uint, + .plugin.sort = lyplg_type_sort_uint, +- .plugin.print = lyplg_type_print_uint, ++ .plugin.print = lyplg_type_print_u_int, + .plugin.duplicate = lyplg_type_dup_simple, + .plugin.free = lyplg_type_free_simple, + }, { +@@ -709,7 +661,7 @@ const struct lyplg_type_record plugins_integer[] = { + .plugin.validate_tree = NULL, + .plugin.compare = lyplg_type_compare_int, + .plugin.sort = lyplg_type_sort_int, +- .plugin.print = lyplg_type_print_int, ++ .plugin.print = lyplg_type_print_u_int, + .plugin.duplicate = lyplg_type_dup_simple, + .plugin.free = lyplg_type_free_simple, + }, { +@@ -724,7 +676,7 @@ const struct lyplg_type_record plugins_integer[] = { + .plugin.validate_tree = NULL, + .plugin.compare = lyplg_type_compare_int, + .plugin.sort = lyplg_type_sort_int, +- .plugin.print = lyplg_type_print_int, ++ .plugin.print = lyplg_type_print_u_int, + .plugin.duplicate = lyplg_type_dup_simple, + .plugin.free = lyplg_type_free_simple, + }, { +@@ -739,7 +691,7 @@ const struct lyplg_type_record plugins_integer[] = { + .plugin.validate_tree = NULL, + .plugin.compare = lyplg_type_compare_int, + .plugin.sort = lyplg_type_sort_int, +- .plugin.print = lyplg_type_print_int, ++ .plugin.print = lyplg_type_print_u_int, + .plugin.duplicate = lyplg_type_dup_simple, + .plugin.free = lyplg_type_free_simple, + }, { +@@ -754,7 +706,7 @@ const struct lyplg_type_record plugins_integer[] = { + .plugin.validate_tree = NULL, + .plugin.compare = lyplg_type_compare_int, + .plugin.sort = lyplg_type_sort_int, +- .plugin.print = lyplg_type_print_int, ++ .plugin.print = lyplg_type_print_u_int, + .plugin.duplicate = lyplg_type_dup_simple, + .plugin.free = lyplg_type_free_simple, + }, +-- +2.43.0 + diff --git a/patches/libyang/4.2.2/0006-context-BUGFIX-unexpected-context-node.patch b/patches/libyang/4.2.2/0006-context-BUGFIX-unexpected-context-node.patch new file mode 100644 index 00000000..a367a24f --- /dev/null +++ b/patches/libyang/4.2.2/0006-context-BUGFIX-unexpected-context-node.patch @@ -0,0 +1,32 @@ +From 0e911b67177252160f7f1361e2481a88200debc3 Mon Sep 17 00:00:00 2001 +From: Michal Vasko +Date: Thu, 11 Dec 2025 15:20:28 +0100 +Subject: [PATCH 6/6] context BUGFIX unexpected context node +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Fixes #2457 + +Signed-off-by: Mattias Walström +--- + src/context.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/context.c b/src/context.c +index 52a4ae46d..1c287d9f2 100644 +--- a/src/context.c ++++ b/src/context.c +@@ -370,7 +370,7 @@ ly_ctx_new_yl_legacy(struct ly_ctx *ctx, const struct lyd_node *yltree) + LY_ERR ret = LY_SUCCESS; + uint32_t i, j; + +- LY_CHECK_RET(ret = lyd_find_xpath(yltree, "modules-state/module", &set)); ++ LY_CHECK_RET(ret = lyd_find_xpath(yltree, "/ietf-yang-library:modules-state/module", &set)); + + if (!set->count) { + ret = LY_ENOTFOUND; +-- +2.43.0 + diff --git a/patches/netopeer2/2.4.5/0001-Allow-factory-as-copy-from-only-in-rpc-copy-config.patch b/patches/netopeer2/2.7.0/0001-Allow-factory-as-copy-from-only-in-rpc-copy-config.patch similarity index 85% rename from patches/netopeer2/2.4.5/0001-Allow-factory-as-copy-from-only-in-rpc-copy-config.patch rename to patches/netopeer2/2.7.0/0001-Allow-factory-as-copy-from-only-in-rpc-copy-config.patch index ec65a91d..28c60cf2 100644 --- a/patches/netopeer2/2.4.5/0001-Allow-factory-as-copy-from-only-in-rpc-copy-config.patch +++ b/patches/netopeer2/2.7.0/0001-Allow-factory-as-copy-from-only-in-rpc-copy-config.patch @@ -1,7 +1,7 @@ -From 8406098242751deb3de1172ceda382251fd2c8f5 Mon Sep 17 00:00:00 2001 +From 84a8003b97caaf602be1406e8291cea10d5cb7c1 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 22 Jun 2023 10:24:57 +0200 -Subject: [PATCH 1/2] Allow 'factory' as copy-from (only) in rpc copy-config +Subject: [PATCH 1/4] Allow 'factory' as copy-from (only) in rpc copy-config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -14,7 +14,7 @@ Signed-off-by: Mattias Walström 1 file changed, 2 insertions(+) diff --git a/src/netconf.c b/src/netconf.c -index 1d9ba79..2a6f1a4 100644 +index 764f1f6..084e1d4 100644 --- a/src/netconf.c +++ b/src/netconf.c @@ -334,6 +334,8 @@ np2srv_rpc_copyconfig_cb(const struct lyd_node *rpc, struct np_user_sess *user_s diff --git a/patches/netopeer2/2.4.5/0002-Do-not-generate-data-in-sysrepo.patch b/patches/netopeer2/2.7.0/0002-Do-not-generate-data-in-sysrepo.patch similarity index 95% rename from patches/netopeer2/2.4.5/0002-Do-not-generate-data-in-sysrepo.patch rename to patches/netopeer2/2.7.0/0002-Do-not-generate-data-in-sysrepo.patch index 484c0bbb..e72e530a 100644 --- a/patches/netopeer2/2.4.5/0002-Do-not-generate-data-in-sysrepo.patch +++ b/patches/netopeer2/2.7.0/0002-Do-not-generate-data-in-sysrepo.patch @@ -1,7 +1,7 @@ -From 6ad71e6359453b8dcecbf9f33f59bd736111ecf3 Mon Sep 17 00:00:00 2001 +From 010621423617d87b87f3484188ce8d6b960b50b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Tue, 4 Feb 2025 20:14:50 +0100 -Subject: [PATCH 2/2] Do not generate data in sysrepo +Subject: [PATCH 2/4] Do not generate data in sysrepo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -16,7 +16,7 @@ Signed-off-by: Mattias Walström 1 file changed, 40 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt -index cf97275..95b82ae 100644 +index 71c8bdd..594363f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -395,47 +395,7 @@ if(SYSREPO_SETUP) diff --git a/patches/netopeer2/2.7.0/0003-main-UPDATE-support-having-local-users-disabled.patch b/patches/netopeer2/2.7.0/0003-main-UPDATE-support-having-local-users-disabled.patch new file mode 100644 index 00000000..2756c072 --- /dev/null +++ b/patches/netopeer2/2.7.0/0003-main-UPDATE-support-having-local-users-disabled.patch @@ -0,0 +1,56 @@ +From d3a5114761d077ad72171998045d37cbf46bb7d0 Mon Sep 17 00:00:00 2001 +From: Michal Vasko +Date: Thu, 4 Dec 2025 14:13:10 +0100 +Subject: [PATCH 3/4] main UPDATE support having local users disabled +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Signed-off-by: Mattias Walström +--- + src/main.c | 21 +++++++++++++++------ + 1 file changed, 15 insertions(+), 6 deletions(-) + +diff --git a/src/main.c b/src/main.c +index 64ffaaa..7492b43 100644 +--- a/src/main.c ++++ b/src/main.c +@@ -1158,6 +1158,8 @@ server_data_subscribe(void) + { + int rc; + const char *mod_name, *xpath; ++ const struct lys_module *mod; ++ const struct ly_ctx *ly_ctx; + + #define SR_OPER_SUBSCR(mod_name, xpath, cb) \ + rc = sr_oper_get_subscribe(np2srv.sr_sess, mod_name, xpath, cb, NULL, 0, &np2srv.sr_data_sub); \ +@@ -1203,12 +1205,19 @@ server_data_subscribe(void) + mod_name = "ietf-tls-common"; + SR_OPER_SUBSCR(mod_name, "/ietf-tls-common:supported-algorithms", np2srv_tls_algs_oper_cb); + +- /* password last modified oper data for both listen + call-home SSH users */ +- mod_name = "ietf-netconf-server"; +- SR_OPER_SUBSCR(mod_name, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint/ssh/" +- "ssh-server-parameters/client-authentication/users/user/password/last-modified", np2srv_password_last_modified_oper_cb); +- SR_OPER_SUBSCR(mod_name, "/ietf-netconf-server:netconf-server/call-home/netconf-client/endpoints/endpoint/ssh/" +- "ssh-server-parameters/client-authentication/users/user/password/last-modified", np2srv_password_last_modified_oper_cb); ++ ly_ctx = sr_acquire_context(np2srv.sr_conn); ++ mod = ly_ctx_get_module_implemented(ly_ctx, "ietf-ssh-server"); ++ sr_release_context(np2srv.sr_conn); ++ if (!lys_feature_value(mod, "local-users-supported")) { ++ /* password last modified oper data for both listen + call-home SSH users */ ++ mod_name = "ietf-netconf-server"; ++ SR_OPER_SUBSCR(mod_name, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint/ssh/" ++ "ssh-server-parameters/client-authentication/users/user/password/last-modified", ++ np2srv_password_last_modified_oper_cb); ++ SR_OPER_SUBSCR(mod_name, "/ietf-netconf-server:netconf-server/call-home/netconf-client/endpoints/endpoint/ssh/" ++ "ssh-server-parameters/client-authentication/users/user/password/last-modified", ++ np2srv_password_last_modified_oper_cb); ++ } + #endif /* NC_ENABLED_SSH_TLS */ + + /* subscriptions to running DS */ +-- +2.43.0 + diff --git a/patches/netopeer2/2.7.0/0004-Do-not-install-YANG-models-we-do-it-in-confd.patch b/patches/netopeer2/2.7.0/0004-Do-not-install-YANG-models-we-do-it-in-confd.patch new file mode 100644 index 00000000..5ef20756 --- /dev/null +++ b/patches/netopeer2/2.7.0/0004-Do-not-install-YANG-models-we-do-it-in-confd.patch @@ -0,0 +1,30 @@ +From d23806c941a16a8a12e006de240fd16afd963f47 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= +Date: Tue, 16 Dec 2025 08:15:54 +0100 +Subject: [PATCH 4/4] Do not install YANG models, we do it in confd. +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Signed-off-by: Mattias Walström +--- + CMakeLists.txt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 594363f..d449230 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -86,7 +86,7 @@ set(YANG_MODULE_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/yang/modul + set(DATA_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/netopeer2" CACHE STRING "Directory with shared netopeer2 data") + + # script options +-option(SYSREPO_SETUP "Install required modules with their default configuration into sysrepo using a script" ON) ++option(SYSREPO_SETUP "Install required modules with their default configuration into sysrepo using a script" OFF) + set(MODULES_PERMS 600 CACHE STRING "File access permissions set for all the server modules") + if(NOT MODULES_OWNER) + execute_process(COMMAND id -un RESULT_VARIABLE RET +-- +2.43.0 + diff --git a/patches/sysrepo/3.7.11/0001-sysrepo-plugind-add-support-for-running-in-foregroun.patch b/patches/sysrepo/4.2.10/0001-sysrepo-plugind-add-support-for-running-in-foregroun.patch similarity index 95% rename from patches/sysrepo/3.7.11/0001-sysrepo-plugind-add-support-for-running-in-foregroun.patch rename to patches/sysrepo/4.2.10/0001-sysrepo-plugind-add-support-for-running-in-foregroun.patch index 1a99b4ee..2622693e 100644 --- a/patches/sysrepo/3.7.11/0001-sysrepo-plugind-add-support-for-running-in-foregroun.patch +++ b/patches/sysrepo/4.2.10/0001-sysrepo-plugind-add-support-for-running-in-foregroun.patch @@ -1,7 +1,7 @@ -From e1ebfda6318d25804f3344caa89ce3f723b576e4 Mon Sep 17 00:00:00 2001 +From efe7706fd7397c2feb384afea00ee97e74287df0 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 28 Mar 2023 10:37:53 +0200 -Subject: [PATCH 1/7] sysrepo-plugind: add support for running in foreground +Subject: [PATCH 1/8] sysrepo-plugind: add support for running in foreground with syslog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 diff --git a/patches/sysrepo/3.7.11/0002-Allow-SR_EV_DONE-to-return-any-error-to-sysrepocfg.patch b/patches/sysrepo/4.2.10/0002-Allow-SR_EV_DONE-to-return-any-error-to-sysrepocfg.patch similarity index 89% rename from patches/sysrepo/3.7.11/0002-Allow-SR_EV_DONE-to-return-any-error-to-sysrepocfg.patch rename to patches/sysrepo/4.2.10/0002-Allow-SR_EV_DONE-to-return-any-error-to-sysrepocfg.patch index 8a42a7c9..b1f40820 100644 --- a/patches/sysrepo/3.7.11/0002-Allow-SR_EV_DONE-to-return-any-error-to-sysrepocfg.patch +++ b/patches/sysrepo/4.2.10/0002-Allow-SR_EV_DONE-to-return-any-error-to-sysrepocfg.patch @@ -1,7 +1,7 @@ -From a190da4e967fb7ed65bc06794e8d775b4e1a7b84 Mon Sep 17 00:00:00 2001 +From 11b9938206cf2bafc456bb22e14c7f85a604760c Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 7 May 2024 15:41:53 +0200 -Subject: [PATCH 2/7] Allow SR_EV_DONE to return any error to sysrepocfg +Subject: [PATCH 2/8] Allow SR_EV_DONE to return any error to sysrepocfg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -28,10 +28,10 @@ Signed-off-by: Mattias Walström 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/shm_sub.c b/src/shm_sub.c -index 61aab4c6..6ba1cd4d 100644 +index db0a2e0f..f64b85d6 100644 --- a/src/shm_sub.c +++ b/src/shm_sub.c -@@ -1832,7 +1832,7 @@ cleanup: +@@ -1775,7 +1775,7 @@ cleanup: sr_error_info_t * sr_shmsub_change_notify_change_done(struct sr_mod_info_s *mod_info, const char *orig_name, const void *orig_data, @@ -40,7 +40,7 @@ index 61aab4c6..6ba1cd4d 100644 { sr_error_info_t *err_info = NULL; struct sr_mod_info_mod_s *mod = NULL; -@@ -1980,12 +1980,19 @@ sr_shmsub_change_notify_change_done(struct sr_mod_info_s *mod_info, const char * +@@ -1923,12 +1923,19 @@ sr_shmsub_change_notify_change_done(struct sr_mod_info_s *mod_info, const char * sr_rwunlock(&nsub->sub_shm->lock, 0, SR_LOCK_WRITE, cid, __func__); nsub->lock = SR_LOCK_NONE; @@ -63,7 +63,7 @@ index 61aab4c6..6ba1cd4d 100644 nsub->pending_event = 0; } } while (1); -@@ -3557,7 +3564,7 @@ sr_shmsub_change_listen_relock(sr_sub_shm_t *sub_shm, sr_lock_mode_t mode, struc +@@ -3562,7 +3569,7 @@ sr_shmsub_change_listen_relock(sr_sub_shm_t *sub_shm, sr_lock_mode_t mode, struc sr_error_info_t * sr_shmsub_change_listen_process_module_events(struct modsub_change_s *change_subs, sr_conn_ctx_t *conn) { @@ -72,7 +72,7 @@ index 61aab4c6..6ba1cd4d 100644 uint32_t i, data_len = 0, valid_subscr_count, remaining_subs; char *data = NULL, *shm_data_ptr; int ret = SR_ERR_OK, filter_valid; -@@ -3579,7 +3586,6 @@ sr_shmsub_change_listen_process_module_events(struct modsub_change_s *change_sub +@@ -3584,7 +3591,6 @@ sr_shmsub_change_listen_process_module_events(struct modsub_change_s *change_sub } } if (i == change_subs->sub_count) { @@ -80,7 +80,7 @@ index 61aab4c6..6ba1cd4d 100644 goto cleanup; } -@@ -3686,6 +3692,11 @@ process_event: +@@ -3697,6 +3703,11 @@ process_event: } break; } @@ -92,7 +92,7 @@ index 61aab4c6..6ba1cd4d 100644 } /* subscription processed this event */ -@@ -3718,6 +3729,11 @@ process_event: +@@ -3729,6 +3740,11 @@ process_event: } break; case SR_SUB_EV_DONE: @@ -104,7 +104,7 @@ index 61aab4c6..6ba1cd4d 100644 case SR_SUB_EV_ABORT: /* nothing to do */ break; -@@ -3742,14 +3758,20 @@ process_event: +@@ -3753,14 +3769,20 @@ process_event: /* SUB WRITE URGE LOCK */ if (sr_shmsub_change_listen_relock(sub_shm, SR_LOCK_WRITE_URGE, &sub_info, change_sub, change_subs->module_name, @@ -127,7 +127,7 @@ index 61aab4c6..6ba1cd4d 100644 } else { SR_LOG_DBG("EV LISTEN: \"%s\" \"%s\" ID %" PRIu32 " priority %" PRIu32 " success (remaining %" PRIu32 " subscribers).", change_subs->module_name, sr_ev2str(sub_info.event), sub_info.operation_id, sub_info.priority, remaining_subs); -@@ -4164,7 +4186,7 @@ finish_iter: +@@ -4176,7 +4198,7 @@ finish_iter: sr_errinfo_free(&cb_err_info); /* publish "done" event */ @@ -137,10 +137,10 @@ index 61aab4c6..6ba1cd4d 100644 } diff --git a/src/shm_sub.h b/src/shm_sub.h -index 0cd22884..bd697ccc 100644 +index de21a30f..66fc7b69 100644 --- a/src/shm_sub.h +++ b/src/shm_sub.h -@@ -159,7 +159,7 @@ sr_error_info_t *sr_shmsub_change_notify_change(struct sr_mod_info_s *mod_info, +@@ -147,7 +147,7 @@ sr_error_info_t *sr_shmsub_change_notify_change(struct sr_mod_info_s *mod_info, * @return err_info, NULL on success. */ sr_error_info_t *sr_shmsub_change_notify_change_done(struct sr_mod_info_s *mod_info, const char *orig_name, @@ -150,10 +150,10 @@ index 0cd22884..bd697ccc 100644 /** * @brief Notify about (generate) a change "abort" event. diff --git a/src/sysrepo.c b/src/sysrepo.c -index 068585d7..72dad00c 100644 +index fa7b5360..542a55ba 100644 --- a/src/sysrepo.c +++ b/src/sysrepo.c -@@ -4313,7 +4313,7 @@ sr_error_info_t * +@@ -4397,7 +4397,7 @@ sr_error_info_t * sr_changes_notify_store(struct sr_mod_info_s *mod_info, sr_session_ctx_t *session, int shmmod_session_del, uint32_t timeout_ms, sr_lock_mode_t has_change_sub_lock, sr_error_info_t **err_info2) { @@ -162,7 +162,7 @@ index 068585d7..72dad00c 100644 struct sr_denied denied = {0}; sr_lock_mode_t change_sub_lock = has_change_sub_lock; uint32_t sid = 0, err_count; -@@ -4470,7 +4470,7 @@ store: +@@ -4559,7 +4559,7 @@ store: } /* publish "done" event, all changes were applied */ diff --git a/patches/sysrepo/3.7.11/0003-Allow-to-copy-from-factory-default.patch b/patches/sysrepo/4.2.10/0003-Allow-to-copy-from-factory-default.patch similarity index 77% rename from patches/sysrepo/3.7.11/0003-Allow-to-copy-from-factory-default.patch rename to patches/sysrepo/4.2.10/0003-Allow-to-copy-from-factory-default.patch index e3f1fb05..ac1749d6 100644 --- a/patches/sysrepo/3.7.11/0003-Allow-to-copy-from-factory-default.patch +++ b/patches/sysrepo/4.2.10/0003-Allow-to-copy-from-factory-default.patch @@ -1,7 +1,7 @@ -From 81b0b781fa41a9e1c7ab2a94e6fc5aa4c52d365d Mon Sep 17 00:00:00 2001 +From 9e0267d4f20733b2a26df6d0ee0bc4019db8b13f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Wed, 8 May 2024 17:00:50 +0200 -Subject: [PATCH 3/7] Allow to copy from factory default +Subject: [PATCH 3/8] Allow to copy from factory default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -13,12 +13,12 @@ Signed-off-by: Mattias Walström 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sysrepo.c b/src/sysrepo.c -index 72dad00c..1b54319f 100644 +index 542a55ba..80bfb42c 100644 --- a/src/sysrepo.c +++ b/src/sysrepo.c -@@ -4895,7 +4895,7 @@ sr_copy_config(sr_session_ctx_t *session, const char *module_name, sr_datastore_ - struct sr_mod_info_s mod_info; +@@ -5023,7 +5023,7 @@ sr_copy_config(sr_session_ctx_t *session, const char *module_name, sr_datastore_ const struct lys_module *ly_mod = NULL; + sr_datastore_t ds1, ds2; - SR_CHECK_ARG_APIRET(!session || !SR_IS_CONVENTIONAL_DS(src_datastore) || !SR_IS_CONVENTIONAL_DS(session->ds), + SR_CHECK_ARG_APIRET(!session || (!SR_IS_CONVENTIONAL_DS(src_datastore) && (src_datastore != SR_DS_FACTORY_DEFAULT)) || !SR_IS_CONVENTIONAL_DS(session->ds), diff --git a/patches/sysrepo/3.7.11/0004-Add-z-switch-to-sysrepoctl-to-install-factory-config.patch b/patches/sysrepo/4.2.10/0004-Add-z-switch-to-sysrepoctl-to-install-factory-config.patch similarity index 84% rename from patches/sysrepo/3.7.11/0004-Add-z-switch-to-sysrepoctl-to-install-factory-config.patch rename to patches/sysrepo/4.2.10/0004-Add-z-switch-to-sysrepoctl-to-install-factory-config.patch index b6d0ba8f..f5aa25f8 100644 --- a/patches/sysrepo/3.7.11/0004-Add-z-switch-to-sysrepoctl-to-install-factory-config.patch +++ b/patches/sysrepo/4.2.10/0004-Add-z-switch-to-sysrepoctl-to-install-factory-config.patch @@ -1,7 +1,7 @@ -From 496305b14423d0e0344cd4e52a80699773cd1a34 Mon Sep 17 00:00:00 2001 +From e0c899ba266b959544d7cc08c917cebba7ac91c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Mon, 6 May 2024 14:49:32 +0200 -Subject: [PATCH 4/7] Add -z switch to sysrepoctl to install factory config +Subject: [PATCH 4/8] Add -z switch to sysrepoctl to install factory config from a json file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -19,10 +19,10 @@ Signed-off-by: Mattias Walström 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/executables/sysrepoctl.c b/src/executables/sysrepoctl.c -index b33a60cd..b60d17af 100644 +index 510eec2a..497c2cbf 100644 --- a/src/executables/sysrepoctl.c +++ b/src/executables/sysrepoctl.c -@@ -664,6 +664,7 @@ main(int argc, char **argv) +@@ -659,6 +659,7 @@ main(int argc, char **argv) {"uninstall", required_argument, NULL, 'u'}, {"change", required_argument, NULL, 'c'}, {"update", required_argument, NULL, 'U'}, @@ -30,7 +30,7 @@ index b33a60cd..b60d17af 100644 {"plugin-list", no_argument, NULL, 'L'}, {"plugin-install", required_argument, NULL, 'P'}, {"search-dirs", required_argument, NULL, 's'}, -@@ -688,7 +689,7 @@ main(int argc, char **argv) +@@ -683,7 +684,7 @@ main(int argc, char **argv) /* process options */ opterr = 0; @@ -39,7 +39,7 @@ index b33a60cd..b60d17af 100644 switch (opt) { case 'h': /* help */ -@@ -880,6 +881,15 @@ main(int argc, char **argv) +@@ -875,6 +876,15 @@ main(int argc, char **argv) goto cleanup; } break; @@ -55,7 +55,7 @@ index b33a60cd..b60d17af 100644 case 'I': /* init-data */ if (operation == 'i') { -@@ -939,9 +949,15 @@ main(int argc, char **argv) +@@ -934,9 +944,15 @@ main(int argc, char **argv) goto cleanup; } } @@ -73,7 +73,7 @@ index b33a60cd..b60d17af 100644 /* list */ if ((r = srctl_list(conn))) { diff --git a/src/lyd_mods.h b/src/lyd_mods.h -index f3a9b215..b86f24b4 100644 +index c1c2ea7a..1cdf2436 100644 --- a/src/lyd_mods.h +++ b/src/lyd_mods.h @@ -103,4 +103,11 @@ sr_error_info_t *sr_lydmods_change_chng_feature(const struct ly_ctx *ly_ctx, con @@ -89,10 +89,10 @@ index f3a9b215..b86f24b4 100644 + #endif diff --git a/src/sysrepo.c b/src/sysrepo.c -index 1b54319f..ca2026b2 100644 +index 80bfb42c..22fcbc3c 100644 --- a/src/sysrepo.c +++ b/src/sysrepo.c -@@ -1626,6 +1626,55 @@ sr_free_int_install_mods(sr_int_install_mod_t *new_mods, uint32_t new_mod_count) +@@ -1689,6 +1689,55 @@ sr_free_int_install_mods(sr_int_install_mod_t *new_mods, uint32_t new_mod_count) free(new_mods); } @@ -103,15 +103,15 @@ index 1b54319f..ca2026b2 100644 + struct lyd_node *sr_mods = NULL; + int initialized = 0; + struct lyd_node *mod_data = NULL; -+ struct sr_data_update_s data_info = {0}; ++ struct sr_lycc_ds_data_s data_info = {0}; + + /* create new temporary context */ -+ if ((err_info = sr_ly_ctx_init(conn, &new_ctx))) { ++ if ((err_info = sr_ly_ctx_new(&new_ctx))) { + goto cleanup; + } + + /* use temporary context to load current modules */ -+ if ((err_info = sr_shmmod_ctx_load_modules(SR_CONN_MOD_SHM(conn), new_ctx, NULL))) { ++ if ((err_info = sr_shmmod_ctx_load_modules(SR_CTX_MOD_SHM(sr_yang_ctx), new_ctx, NULL))) { + goto cleanup; + } + @@ -130,18 +130,18 @@ index 1b54319f..ca2026b2 100644 + } + data_info.new.fdflt = mod_data; + mod_data = NULL; -+ if ((err_info = sr_lycc_store_data_if_differ(conn, new_ctx, sr_mods, &data_info))) { ++ if ((err_info = sr_lycc_store_ds_data_if_differ(conn, new_ctx, sr_mods, &data_info))) { + goto cleanup; + } + -+ /* update content ID and safely switch the context */ ++ /* update content ID */ + SR_CONN_MAIN_SHM(conn)->content_id = ly_ctx_get_modules_hash(new_ctx); -+ sr_conn_ctx_switch(conn, &new_ctx, NULL); +cleanup: -+ ly_ctx_destroy(new_ctx); -+ sr_lycc_update_data_clear(&data_info); ++ sr_lycc_ds_data_set_clear(&data_info.old); ++ sr_lycc_ds_data_set_clear(&data_info.new); + lyd_free_siblings(sr_mods); + lyd_free_siblings(mod_data); ++ ly_ctx_destroy(new_ctx); + return sr_api_ret(NULL, err_info); +} + @@ -149,7 +149,7 @@ index 1b54319f..ca2026b2 100644 sr_install_module(sr_conn_ctx_t *conn, const char *schema_path, const char *search_dirs, const char **features) { diff --git a/src/sysrepo.h b/src/sysrepo.h -index 466c8294..ee1b47a9 100644 +index 82620c80..6fdf2b29 100644 --- a/src/sysrepo.h +++ b/src/sysrepo.h @@ -34,7 +34,6 @@ extern "C" { @@ -160,7 +160,7 @@ index 466c8294..ee1b47a9 100644 /** * @defgroup log_api Logging API * @{ -@@ -725,6 +724,15 @@ int sr_get_module_info(sr_conn_ctx_t *conn, sr_data_t **sysrepo_data); +@@ -787,6 +786,15 @@ int sr_get_module_info(sr_conn_ctx_t *conn, sr_data_t **sysrepo_data); */ int sr_is_module_internal(const struct lys_module *ly_mod); diff --git a/patches/sysrepo/3.7.11/0005-Introduce-new-log-level-SEC-for-audit-trails.patch b/patches/sysrepo/4.2.10/0005-Introduce-new-log-level-SEC-for-audit-trails.patch similarity index 96% rename from patches/sysrepo/3.7.11/0005-Introduce-new-log-level-SEC-for-audit-trails.patch rename to patches/sysrepo/4.2.10/0005-Introduce-new-log-level-SEC-for-audit-trails.patch index 71616506..9ff33c23 100644 --- a/patches/sysrepo/3.7.11/0005-Introduce-new-log-level-SEC-for-audit-trails.patch +++ b/patches/sysrepo/4.2.10/0005-Introduce-new-log-level-SEC-for-audit-trails.patch @@ -1,7 +1,7 @@ -From 6543a03c98fe32180dc95a47d113a3bd2dd369de Mon Sep 17 00:00:00 2001 +From c7602dc8eabb941e0a163208aaf4de92dd5ef526 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 21 Aug 2024 16:00:35 +0200 -Subject: [PATCH 5/7] Introduce new log level [SEC] for audit trails +Subject: [PATCH 5/8] Introduce new log level [SEC] for audit trails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -86,7 +86,7 @@ index b4f62e7c..62279675 100644 #define SR_CHECK_MEM_GOTO(cond, err_info, go) if (cond) { SR_ERRINFO_MEM(&(err_info)); goto go; } diff --git a/src/sysrepo_types.h b/src/sysrepo_types.h -index 165e255f..60df80cf 100644 +index d71af2d4..32990fac 100644 --- a/src/sysrepo_types.h +++ b/src/sysrepo_types.h @@ -65,7 +65,8 @@ typedef enum { diff --git a/patches/sysrepo/3.7.11/0006-Add-audit-trail-for-high-priority-system-changes.patch b/patches/sysrepo/4.2.10/0006-Add-audit-trail-for-high-priority-system-changes.patch similarity index 85% rename from patches/sysrepo/3.7.11/0006-Add-audit-trail-for-high-priority-system-changes.patch rename to patches/sysrepo/4.2.10/0006-Add-audit-trail-for-high-priority-system-changes.patch index cb5b88e5..a7c377af 100644 --- a/patches/sysrepo/3.7.11/0006-Add-audit-trail-for-high-priority-system-changes.patch +++ b/patches/sysrepo/4.2.10/0006-Add-audit-trail-for-high-priority-system-changes.patch @@ -1,7 +1,7 @@ -From 0ff2b7562154289de39abb10bcec2bc0d744a64d Mon Sep 17 00:00:00 2001 +From a86dfdd4a5cb74c1f8c90c8d5aea6f5505c1b88c Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 21 Aug 2024 16:04:43 +0200 -Subject: [PATCH 6/7] Add audit trail for high priority system changes +Subject: [PATCH 6/8] Add audit trail for high priority system changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -22,10 +22,10 @@ Signed-off-by: Mattias Walström 1 file changed, 12 insertions(+) diff --git a/src/sysrepo.c b/src/sysrepo.c -index ca2026b2..2ea546e9 100644 +index 22fcbc3c..a1d14eda 100644 --- a/src/sysrepo.c +++ b/src/sysrepo.c -@@ -4518,6 +4518,9 @@ store: +@@ -4607,6 +4607,9 @@ store: goto cleanup; } @@ -35,7 +35,7 @@ index ca2026b2..2ea546e9 100644 /* publish "done" event, all changes were applied */ if ((err_info = sr_shmsub_change_notify_change_done(mod_info, orig_name, orig_data, timeout_ms, &cb_err_info))) { goto cleanup; -@@ -4529,6 +4532,9 @@ store: +@@ -4618,6 +4621,9 @@ store: } cleanup: @@ -45,7 +45,7 @@ index ca2026b2..2ea546e9 100644 if (change_sub_lock && !has_change_sub_lock) { assert(change_sub_lock == SR_LOCK_READ); -@@ -5031,6 +5037,9 @@ sr_copy_config(sr_session_ctx_t *session, const char *module_name, sr_datastore_ +@@ -5164,6 +5170,9 @@ sr_copy_config(sr_session_ctx_t *session, const char *module_name, sr_datastore_ } } @@ -55,7 +55,7 @@ index ca2026b2..2ea546e9 100644 cleanup: /* MODULES UNLOCK */ sr_shmmod_modinfo_unlock(&mod_info); -@@ -7562,6 +7571,9 @@ sr_rpc_send_tree(sr_session_ctx_t *session, struct lyd_node *input, uint32_t tim +@@ -7772,6 +7781,9 @@ sr_rpc_send_tree_internal(sr_session_ctx_t *session, struct lyd_node *input, uin } } diff --git a/patches/sysrepo/3.7.11/0007-On-error-in-sr_shmsub_listen_thread-exit-process.patch b/patches/sysrepo/4.2.10/0007-On-error-in-sr_shmsub_listen_thread-exit-process.patch similarity index 81% rename from patches/sysrepo/3.7.11/0007-On-error-in-sr_shmsub_listen_thread-exit-process.patch rename to patches/sysrepo/4.2.10/0007-On-error-in-sr_shmsub_listen_thread-exit-process.patch index f49a9baa..ac6067ca 100644 --- a/patches/sysrepo/3.7.11/0007-On-error-in-sr_shmsub_listen_thread-exit-process.patch +++ b/patches/sysrepo/4.2.10/0007-On-error-in-sr_shmsub_listen_thread-exit-process.patch @@ -1,7 +1,7 @@ -From 83e58e43fc1c968a6e007ab0c39d99aeffc8fbce Mon Sep 17 00:00:00 2001 +From dbf08c67d8f17bdf98466b18fd72a230269e5d46 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 23 Aug 2024 12:22:06 +0200 -Subject: [PATCH 7/7] On error in sr_shmsub_listen_thread(), exit process +Subject: [PATCH 7/8] On error in sr_shmsub_listen_thread(), exit process MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -18,10 +18,10 @@ Signed-off-by: Mattias Walström 1 file changed, 2 insertions(+) diff --git a/src/shm_sub.c b/src/shm_sub.c -index 6ba1cd4d..5c006a82 100644 +index f64b85d6..c1a4b960 100644 --- a/src/shm_sub.c +++ b/src/shm_sub.c -@@ -5049,5 +5049,7 @@ error: +@@ -5061,5 +5061,7 @@ error: /* free our own resources */ ATOMIC_STORE_RELAXED(subscr->thread_running, 0); pthread_detach(pthread_self()); diff --git a/patches/sysrepo/4.2.10/0008-Cross-compile-fixes.patch b/patches/sysrepo/4.2.10/0008-Cross-compile-fixes.patch new file mode 100644 index 00000000..7b94019f --- /dev/null +++ b/patches/sysrepo/4.2.10/0008-Cross-compile-fixes.patch @@ -0,0 +1,37 @@ +From 2549c966c090dd38a7a09907d27d13107d15aedd Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= +Date: Tue, 16 Dec 2025 08:18:32 +0100 +Subject: [PATCH 8/8] Cross compile fixes +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Signed-off-by: Mattias Walström +--- + CMakeModules/SetupPrintedContext.cmake | 9 +++++++++ + 1 file changed, 9 insertions(+) + +diff --git a/CMakeModules/SetupPrintedContext.cmake b/CMakeModules/SetupPrintedContext.cmake +index f31ddff6..bfce7a5f 100644 +--- a/CMakeModules/SetupPrintedContext.cmake ++++ b/CMakeModules/SetupPrintedContext.cmake +@@ -59,6 +59,15 @@ function(SETUP_PRINTED_CONTEXT) + return() + endif() + ++ # cannot run test programs when cross-compiling ++ if(CMAKE_CROSSCOMPILING) ++ message(WARNING "Cannot calculate printed context address when cross-compiling. " ++ "Provide PRINTED_CONTEXT_ADDRESS manually to enable printed context.") ++ message(STATUS "Printed context: disabled") ++ set(PRINTED_CONTEXT_ADDRESS 0 CACHE STRING "${PRINTED_CONTEXT_ADDRESS_DOCSTRING}" FORCE) ++ return() ++ endif() ++ + # compile and run the address calculator + try_run( + exec_result compile_result +-- +2.43.0 +