diff --git a/board/aarch32/raspberrypi-rpi2/rootfs/usr/share/product/raspberrypi,2-model-b/etc/factory-config.cfg b/board/aarch32/raspberrypi-rpi2/rootfs/usr/share/product/raspberrypi,2-model-b/etc/factory-config.cfg index d7a28efe..fcc963b9 100644 --- a/board/aarch32/raspberrypi-rpi2/rootfs/usr/share/product/raspberrypi,2-model-b/etc/factory-config.cfg +++ b/board/aarch32/raspberrypi-rpi2/rootfs/usr/share/product/raspberrypi,2-model-b/etc/factory-config.cfg @@ -41,14 +41,31 @@ "ietf-ip:ipv4": { "infix-dhcp-client:dhcp": { "option": [ - {"id": "netmask"}, - {"id": "broadcast"}, - {"id": "router"}, - {"id": "domain"}, - {"id": "hostname"}, - {"id": "dns-server"}, - {"id": "ntp-server"}, - {"id": "vendor-class", "value": "Raspberry Pi 2 Model B"} + { + "id": "netmask" + }, + { + "id": "broadcast" + }, + { + "id": "router" + }, + { + "id": "domain" + }, + { + "id": "hostname" + }, + { + "id": "dns-server" + }, + { + "id": "ntp-server" + }, + { + "id": "vendor-class", + "value": "Raspberry Pi 2 Model B" + } ] } } @@ -122,7 +139,11 @@ "name": "default-ssh", "ssh": { "tcp-server-parameters": { - "local-address": "::" + "local-bind": [ + { + "local-address": "::" + } + ] }, "ssh-server-parameters": { "server-identity": { @@ -168,7 +189,7 @@ "infix-system:motd-banner": "Li0tLS0tLS0uCnwgIC4gLiAgfCBJbmZpeCBPUyDigJQgSW1tdXRhYmxlLkZyaWVuZGx5LlNlY3VyZQp8LS4gdiAuLXwgaHR0cHM6Ly9rZXJuZWxraXQub3JnCictJy0tLSctJwo=" }, "infix-meta:meta": { - "version": "1.6" + "version": "1.7" }, "infix-services:mdns": { "enabled": true diff --git a/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3/etc/factory-config.cfg b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3/etc/factory-config.cfg index 259cc996..e23da3d7 100644 --- a/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3/etc/factory-config.cfg +++ b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3/etc/factory-config.cfg @@ -102,24 +102,49 @@ "ietf-ip:ipv4": { "infix-dhcp-client:dhcp": { "option": [ - {"id": "ntp-server"}, - {"id": "broadcast"}, - {"id": "domain"}, - {"id": "hostname"}, - {"id": "dns-server"}, - {"id": "router"}, - {"id": "netmask"}, - {"id": "vendor-class", "value": "Banana Pi BPI-R3"} + { + "id": "ntp-server" + }, + { + "id": "broadcast" + }, + { + "id": "domain" + }, + { + "id": "hostname" + }, + { + "id": "dns-server" + }, + { + "id": "router" + }, + { + "id": "netmask" + }, + { + "id": "vendor-class", + "value": "Banana Pi BPI-R3" + } ] } }, "ietf-ip:ipv6": { "infix-dhcpv6-client:dhcp": { "option": [ - {"id": "ntp-server"}, - {"id": "client-fqdn"}, - {"id": "domain-search"}, - {"id": "dns-server"} + { + "id": "ntp-server" + }, + { + "id": "client-fqdn" + }, + { + "id": "domain-search" + }, + { + "id": "dns-server" + } ] } } @@ -135,18 +160,18 @@ ] }, "ietf-keystore:keystore": { - "asymmetric-keys": { - "asymmetric-key": [ - { - "name": "genkey", - "public-key-format": "infix-crypto-types:ssh-public-key-format", - "public-key": "", - "private-key-format": "infix-crypto-types:rsa-private-key-format", - "cleartext-private-key": "", - "certificates": {} - } - ] + "asymmetric-keys": { + "asymmetric-key": [ + { + "name": "genkey", + "public-key-format": "infix-crypto-types:ssh-public-key-format", + "public-key": "", + "private-key-format": "infix-crypto-types:rsa-private-key-format", + "cleartext-private-key": "", + "certificates": {} } + ] + } }, "ietf-netconf-acm:nacm": { "enable-nacm": true, @@ -201,7 +226,11 @@ "name": "default-ssh", "ssh": { "tcp-server-parameters": { - "local-address": "::" + "local-bind": [ + { + "local-address": "::" + } + ] }, "ssh-server-parameters": { "server-identity": { @@ -293,7 +322,7 @@ "policy": [ { "name": "lan-to-wan", - "action": "accept", + "action": "accept", "ingress": [ "lan" ], @@ -305,7 +334,7 @@ ] }, "infix-meta:meta": { - "version": "1.6" + "version": "1.7" }, "infix-services:mdns": { "enabled": true @@ -341,4 +370,3 @@ } } } - diff --git a/board/aarch64/friendlyarm-nanopi-r2s/rootfs/usr/share/product/friendlyarm,nanopi-r2s/etc/factory-config.cfg b/board/aarch64/friendlyarm-nanopi-r2s/rootfs/usr/share/product/friendlyarm,nanopi-r2s/etc/factory-config.cfg index d300780d..a76a2291 100644 --- a/board/aarch64/friendlyarm-nanopi-r2s/rootfs/usr/share/product/friendlyarm,nanopi-r2s/etc/factory-config.cfg +++ b/board/aarch64/friendlyarm-nanopi-r2s/rootfs/usr/share/product/friendlyarm,nanopi-r2s/etc/factory-config.cfg @@ -61,24 +61,49 @@ "ietf-ip:ipv4": { "infix-dhcp-client:dhcp": { "option": [ - {"id": "ntp-server"}, - {"id": "broadcast"}, - {"id": "domain"}, - {"id": "hostname"}, - {"id": "dns-server"}, - {"id": "router"}, - {"id": "netmask"}, - {"id": "vendor-class", "value": "NanoPi R2S"} + { + "id": "ntp-server" + }, + { + "id": "broadcast" + }, + { + "id": "domain" + }, + { + "id": "hostname" + }, + { + "id": "dns-server" + }, + { + "id": "router" + }, + { + "id": "netmask" + }, + { + "id": "vendor-class", + "value": "NanoPi R2S" + } ] } }, "ietf-ip:ipv6": { "infix-dhcpv6-client:dhcp": { "option": [ - {"id": "ntp-server"}, - {"id": "client-fqdn"}, - {"id": "domain-search"}, - {"id": "dns-server"} + { + "id": "ntp-server" + }, + { + "id": "client-fqdn" + }, + { + "id": "domain-search" + }, + { + "id": "dns-server" + } ] } } @@ -86,18 +111,18 @@ ] }, "ietf-keystore:keystore": { - "asymmetric-keys": { - "asymmetric-key": [ - { - "name": "genkey", - "public-key-format": "infix-crypto-types:ssh-public-key-format", - "public-key": "", - "private-key-format": "infix-crypto-types:rsa-private-key-format", - "cleartext-private-key": "", - "certificates": {} - } - ] + "asymmetric-keys": { + "asymmetric-key": [ + { + "name": "genkey", + "public-key-format": "infix-crypto-types:ssh-public-key-format", + "public-key": "", + "private-key-format": "infix-crypto-types:rsa-private-key-format", + "cleartext-private-key": "", + "certificates": {} } + ] + } }, "ietf-netconf-acm:nacm": { "enable-nacm": true, @@ -152,7 +177,11 @@ "name": "default-ssh", "ssh": { "tcp-server-parameters": { - "local-address": "::" + "local-bind": [ + { + "local-address": "::" + } + ] }, "ssh-server-parameters": { "server-identity": { @@ -244,7 +273,7 @@ "policy": [ { "name": "lan-to-wan", - "action": "accept", + "action": "accept", "ingress": [ "lan" ], @@ -256,7 +285,7 @@ ] }, "infix-meta:meta": { - "version": "1.6" + "version": "1.7" }, "infix-services:mdns": { "enabled": true diff --git a/board/aarch64/raspberrypi-rpi64/README.md b/board/aarch64/raspberrypi-rpi64/README.md index 919c539c..14be9f13 100644 --- a/board/aarch64/raspberrypi-rpi64/README.md +++ b/board/aarch64/raspberrypi-rpi64/README.md @@ -108,7 +108,7 @@ To configure WiFi as a client, first store your WiFi password in the keystore: admin@infix:/> configure admin@infix:/config/> edit keystore symmetric-key mywifi admin@infix:/config/keystore/…/mywifi/> set key-format wifi-preshared-key-format -admin@infix:/config/keystore/…/mywifi/> set cleartext-key YourWiFiPassword +admin@infix:/config/keystore/…/mywifi/> set cleartext-symmetric-key YourWiFiPassword admin@infix:/config/keystore/…/mywifi/> leave ``` diff --git a/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg b/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg index a4959b99..78bc0980 100644 --- a/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg +++ b/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg @@ -41,14 +41,31 @@ "ietf-ip:ipv4": { "infix-dhcp-client:dhcp": { "option": [ - {"id": "netmask"}, - {"id": "broadcast"}, - {"id": "router"}, - {"id": "domain"}, - {"id": "hostname"}, - {"id": "dns-server"}, - {"id": "ntp-server"}, - {"id": "vendor-class", "value": "Raspberry Pi 4 Model B"} + { + "id": "netmask" + }, + { + "id": "broadcast" + }, + { + "id": "router" + }, + { + "id": "domain" + }, + { + "id": "hostname" + }, + { + "id": "dns-server" + }, + { + "id": "ntp-server" + }, + { + "id": "vendor-class", + "value": "Raspberry Pi 4 Model B" + } ] } } @@ -126,7 +143,11 @@ "name": "default-ssh", "ssh": { "tcp-server-parameters": { - "local-address": "::" + "local-bind": [ + { + "local-address": "::" + } + ] }, "ssh-server-parameters": { "server-identity": { @@ -172,7 +193,7 @@ "infix-system:motd-banner": "Li0tLS0tLS0uCnwgIC4gLiAgfCBJbmZpeCBPUyDigJQgSW1tdXRhYmxlLkZyaWVuZGx5LlNlY3VyZQp8LS4gdiAuLXwgaHR0cHM6Ly9rZXJuZWxraXQub3JnCictJy0tLSctJwo=" }, "infix-meta:meta": { - "version": "1.6" + "version": "1.7" }, "infix-services:mdns": { "enabled": true 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/doc/wifi.md b/doc/wifi.md index 6b9051e7..1ab89625 100644 --- a/doc/wifi.md +++ b/doc/wifi.md @@ -84,7 +84,7 @@ and 63 characters admin@example:/> configure admin@example:/config/> edit keystore symmetric-key example admin@example:/config/keystore/…/example/> set key-format wifi-preshared-key-format -admin@example:/config/keystore/…/example/> set cleartext-key mysecret +admin@example:/config/keystore/…/example/> set cleartext-symmetric-key mysecret admin@example:/config/keystore/…/example/> leave admin@example:/> ``` diff --git a/package/confd/confd.mk b/package/confd/confd.mk index ed74875b..7cfb2347 100644 --- a/package/confd/confd.mk +++ b/package/confd/confd.mk @@ -4,13 +4,13 @@ # ################################################################################ -CONFD_VERSION = 1.6 +CONFD_VERSION = 1.7 CONFD_SITE_METHOD = local CONFD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/confd CONFD_LICENSE = BSD-3-Clause CONFD_LICENSE_FILES = LICENSE CONFD_REDISTRIBUTE = NO -CONFD_DEPENDENCIES = host-sysrepo sysrepo netopeer2 jansson libite sysrepo libsrx libglib2 +CONFD_DEPENDENCIES = host-sysrepo sysrepo rousette netopeer2 jansson libite sysrepo libsrx libglib2 CONFD_AUTORECONF = YES CONFD_CONF_OPTS += --disable-silent-rules --with-crypt=$(BR2_PACKAGE_CONFD_DEFAULT_CRYPT) CONFD_SYSREPO_SHM_PREFIX = sr_buildroot$(subst /,_,$(CONFIG_DIR))_confd 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/python-libyang/python-libyang.hash b/package/python-libyang/python-libyang.hash index 9c9897ed..95f7a4ec 100644 --- a/package/python-libyang/python-libyang.hash +++ b/package/python-libyang/python-libyang.hash @@ -1,5 +1,5 @@ # md5, sha256 from https://pypi.org/pypi/libyang/json md5 46332891cac852441b523f7e71a507f7 libyang-3.0.1.tar.gz -sha256 57f8b234131fb073518313351288194e535d74cc8fdff7f0ff7e31c0ab33e663 libyang-3.0.1.tar.gz +sha256 97da2c908a8a3607ac834f2acec224ea53a4b9e9770c13e5598513641decf637 libyang-3.3.0.tar.gz # Locally computed sha256 checksums sha256 c8d554f918d32b1467f35829e1cfb753764dd683c6edcecc877a4d7e9f05b2a7 LICENSE diff --git a/package/python-libyang/python-libyang.mk b/package/python-libyang/python-libyang.mk index e8cca429..70dffe0c 100644 --- a/package/python-libyang/python-libyang.mk +++ b/package/python-libyang/python-libyang.mk @@ -4,9 +4,9 @@ # ################################################################################ -PYTHON_LIBYANG_VERSION = 3.0.1 +PYTHON_LIBYANG_VERSION = 3.3.0 PYTHON_LIBYANG_SOURCE = libyang-$(PYTHON_LIBYANG_VERSION).tar.gz -PYTHON_LIBYANG_SITE = https://files.pythonhosted.org/packages/91/2e/ff13ee16c874d232d5d3fdff83f629cbc9ac47f9aaf2b59256b6a1bdbc16 +PYTHON_LIBYANG_SITE = https://files.pythonhosted.org/packages/34/d8/ebe9de6d29eaf62a684b34d30118294e48a05d7fbd1bb0af002b7627140d PYTHON_LIBYANG_SETUP_TYPE = setuptools PYTHON_LIBYANG_LICENSE = MIT PYTHON_LIBYANG_LICENSE_FILES = LICENSE 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/python-libyang/3.3.0/8c246567c03471e20cd9574f9c95a179712c3e3f.patch b/patches/python-libyang/3.3.0/8c246567c03471e20cd9574f9c95a179712c3e3f.patch new file mode 100644 index 00000000..b3f2df81 --- /dev/null +++ b/patches/python-libyang/3.3.0/8c246567c03471e20cd9574f9c95a179712c3e3f.patch @@ -0,0 +1,876 @@ +From 8c246567c03471e20cd9574f9c95a179712c3e3f Mon Sep 17 00:00:00 2001 +From: Stefan Gula +Date: Sun, 2 Nov 2025 21:26:55 +0100 +Subject: [PATCH] cffi: allows to usage of libyang v4.2.2 + +This patch adjust cffi definitions and also all associated functions +based on libyang v4.2.2. + +Signed-off-by: Stefan Gula +--- + cffi/cdefs.h | 61 ++++++++++++++++++++++----------------- + cffi/source.c | 4 +-- + libyang/context.py | 25 ++++++++++------ + libyang/data.py | 7 ++--- + libyang/extension.py | 24 +++++++++++----- + libyang/schema.py | 66 +++++++++++++++++++++++++++++++++--------- + tests/test_context.py | 6 +++- + tests/test_data.py | 67 ++++++++++++++++++++++++------------------- + tests/test_diff.py | 4 ++- + tests/test_schema.py | 4 +-- + 10 files changed, 172 insertions(+), 96 deletions(-) + +diff --git a/cffi/cdefs.h b/cffi/cdefs.h +index 8b6d426..82ec974 100644 +--- a/cffi/cdefs.h ++++ b/cffi/cdefs.h +@@ -17,6 +17,7 @@ struct ly_ctx; + #define LY_CTX_LEAFREF_EXTENDED ... + #define LY_CTX_LEAFREF_LINKING ... + #define LY_CTX_BUILTIN_PLUGINS_ONLY ... ++#define LY_CTX_COMPILE_OBSOLETE ... + + + typedef enum { +@@ -304,20 +305,19 @@ enum lyd_type { + LYD_TYPE_REPLY_RESTCONF + }; + +-#define LYD_PRINT_KEEPEMPTYCONT ... + #define LYD_PRINT_SHRINK ... ++#define LYD_PRINT_EMPTY_CONT ... + #define LYD_PRINT_WD_ALL ... + #define LYD_PRINT_WD_ALL_TAG ... + #define LYD_PRINT_WD_EXPLICIT ... + #define LYD_PRINT_WD_IMPL_TAG ... + #define LYD_PRINT_WD_MASK ... +-#define LYD_PRINT_WITHSIBLINGS ... ++#define LYD_PRINT_SIBLINGS ... + #define LYD_PRINT_WD_TRIM ... + LY_ERR lyd_print_mem(char **, const struct lyd_node *, LYD_FORMAT, uint32_t); + LY_ERR lyd_print_tree(struct ly_out *, const struct lyd_node *, LYD_FORMAT, uint32_t); + LY_ERR lyd_print_all(struct ly_out *, const struct lyd_node *, LYD_FORMAT, uint32_t); + +-#define LYD_PARSE_LYB_MOD_UPDATE ... + #define LYD_PARSE_NO_STATE ... + #define LYD_PARSE_STORE_ONLY ... + #define LYD_PARSE_JSON_NULL ... +@@ -351,7 +351,7 @@ LY_ERR ly_out_new_file(FILE *, struct ly_out **); + LY_ERR ly_out_new_fd(int, struct ly_out **); + + LY_ERR lyd_parse_data(const struct ly_ctx *, struct lyd_node *, struct ly_in *, LYD_FORMAT, uint32_t, uint32_t, struct lyd_node **); +-LY_ERR lyd_parse_op(const struct ly_ctx *, struct lyd_node *, struct ly_in *, LYD_FORMAT, enum lyd_type, struct lyd_node **, struct lyd_node **); ++LY_ERR lyd_parse_op(const struct ly_ctx *, struct lyd_node *, struct ly_in *, LYD_FORMAT, enum lyd_type, uint32_t, struct lyd_node **, struct lyd_node **); + + typedef enum { + LYS_OUT_UNKNOWN, +@@ -369,6 +369,7 @@ LY_ERR lys_print_module(struct ly_out *, const struct lys_module *, LYS_OUTFORMA + + struct lysc_module { + struct lys_module *mod; ++ const char **features; + struct lysc_node *data; + struct lysc_node_action *rpcs; + struct lysc_node_notif *notifs; +@@ -388,13 +389,15 @@ struct lys_module { + const char *ref; + struct lysp_module *parsed; + struct lysc_module *compiled; ++ struct lysc_ext *extensions; + struct lysc_ident *identities; ++ struct lysc_submodule *submodules; + struct lys_module **augmented_by; + struct lys_module **deviated_by; + ly_bool implemented; + ly_bool to_compile; +- uint8_t latest_revision; +- ...; ++ uint8_t version : 2; ++ uint8_t latest_revision : 4; + }; + + struct lysp_module { +@@ -467,12 +470,11 @@ struct lysp_ext_instance { + const char *argument; + LY_VALUE_FORMAT format; + void *prefix_data; +- struct lysp_ext *def; ++ uintptr_t plugin_ref; + void *parent; + enum ly_stmt parent_stmt; + uint64_t parent_stmt_index; + uint16_t flags; +- const struct lyplg_ext_record *record; + struct lysp_ext_substmt *substmts; + void *parsed; + struct lysp_stmt *child; +@@ -610,6 +612,11 @@ struct lysp_node_container { + ...; + }; + ++struct lysc_value { ++ const char *str; ++ struct lysc_prefix *prefixes; ++}; ++ + struct lysc_node_leaf { + union { + struct lysc_node node; +@@ -623,7 +630,7 @@ struct lysc_node_leaf { + struct lysc_when **when; + struct lysc_type *type; + const char *units; +- struct lyd_value *dflt; ++ struct lysc_value dflt; + ...; + }; + +@@ -653,7 +660,7 @@ struct lysc_node_leaflist { + struct lysc_when **when; + struct lysc_type *type; + const char *units; +- struct lyd_value **dflts; ++ struct lysc_value *dflts; + uint32_t min; + uint32_t max; + ...; +@@ -783,7 +790,7 @@ struct lysp_node_augment { + struct lysc_type { + const char *name; + struct lysc_ext_instance *exts; +- struct lyplg_type *plugin; ++ uintptr_t plugin_ref; + LY_DATA_TYPE basetype; + uint32_t refcount; + }; +@@ -859,7 +866,7 @@ struct lysc_ext { + const char *name; + const char *argname; + struct lysc_ext_instance *exts; +- struct lyplg_ext *plugin; ++ uintptr_t plugin_ref; + struct lys_module *module; + uint16_t flags; + }; +@@ -982,7 +989,6 @@ typedef struct pcre2_real_code pcre2_code; + + struct lysc_pattern { + const char *expr; +- pcre2_code *code; + const char *dsc; + const char *ref; + const char *emsg; +@@ -1017,7 +1023,7 @@ struct lysc_ident { + struct lysc_type_num { + const char *name; + struct lysc_ext_instance *exts; +- struct lyplg_type *plugin; ++ uintptr_t plugin_ref; + LY_DATA_TYPE basetype; + uint32_t refcount; + struct lysc_range *range; +@@ -1026,7 +1032,7 @@ struct lysc_type_num { + struct lysc_type_dec { + const char *name; + struct lysc_ext_instance *exts; +- struct lyplg_type *plugin; ++ uintptr_t plugin_ref; + LY_DATA_TYPE basetype; + uint32_t refcount; + uint8_t fraction_digits; +@@ -1036,7 +1042,7 @@ struct lysc_type_dec { + struct lysc_type_str { + const char *name; + struct lysc_ext_instance *exts; +- struct lyplg_type *plugin; ++ uintptr_t plugin_ref; + LY_DATA_TYPE basetype; + uint32_t refcount; + struct lysc_range *length; +@@ -1058,7 +1064,7 @@ struct lysc_type_bitenum_item { + struct lysc_type_enum { + const char *name; + struct lysc_ext_instance *exts; +- struct lyplg_type *plugin; ++ uintptr_t plugin_ref; + LY_DATA_TYPE basetype; + uint32_t refcount; + struct lysc_type_bitenum_item *enums; +@@ -1067,7 +1073,7 @@ struct lysc_type_enum { + struct lysc_type_bits { + const char *name; + struct lysc_ext_instance *exts; +- struct lyplg_type *plugin; ++ uintptr_t plugin_ref; + LY_DATA_TYPE basetype; + uint32_t refcount; + struct lysc_type_bitenum_item *bits; +@@ -1076,7 +1082,7 @@ struct lysc_type_bits { + struct lysc_type_leafref { + const char *name; + struct lysc_ext_instance *exts; +- struct lyplg_type *plugin; ++ uintptr_t plugin_ref; + LY_DATA_TYPE basetype; + uint32_t refcount; + struct lyxp_expr *path; +@@ -1088,7 +1094,7 @@ struct lysc_type_leafref { + struct lysc_type_identityref { + const char *name; + struct lysc_ext_instance *exts; +- struct lyplg_type *plugin; ++ uintptr_t plugin_ref; + LY_DATA_TYPE basetype; + uint32_t refcount; + struct lysc_ident **bases; +@@ -1097,7 +1103,7 @@ struct lysc_type_identityref { + struct lysc_type_instanceid { + const char *name; + struct lysc_ext_instance *exts; +- struct lyplg_type *plugin; ++ uintptr_t plugin_ref; + LY_DATA_TYPE basetype; + uint32_t refcount; + uint8_t require_instance; +@@ -1106,7 +1112,7 @@ struct lysc_type_instanceid { + struct lysc_type_union { + const char *name; + struct lysc_ext_instance *exts; +- struct lyplg_type *plugin; ++ uintptr_t plugin_ref; + LY_DATA_TYPE basetype; + uint32_t refcount; + struct lysc_type **types; +@@ -1115,7 +1121,7 @@ struct lysc_type_union { + struct lysc_type_bin { + const char *name; + struct lysc_ext_instance *exts; +- struct lyplg_type *plugin; ++ uintptr_t plugin_ref; + LY_DATA_TYPE basetype; + uint32_t refcount; + struct lysc_range *length; +@@ -1159,8 +1165,7 @@ typedef enum { + LYD_ANYDATA_DATATREE, + LYD_ANYDATA_STRING, + LYD_ANYDATA_XML, +- LYD_ANYDATA_JSON, +- LYD_ANYDATA_LYB ++ LYD_ANYDATA_JSON + } LYD_ANYDATA_VALUETYPE; + + union lyd_any_value { +@@ -1168,7 +1173,6 @@ union lyd_any_value { + const char *str; + const char *xml; + const char *json; +- char *mem; + }; + + struct lyd_node_any { +@@ -1320,6 +1324,8 @@ struct lyd_attr { + LY_ERR lyd_new_attr(struct lyd_node *, const char *, const char *, const char *, struct lyd_attr **); + void lyd_free_attr_single(const struct ly_ctx *ctx, struct lyd_attr *attr); + ++LY_ERR lyd_value_validate_dflt(const struct lysc_node *, const char *, struct lysc_prefix *, const struct lyd_node *, const struct lysc_type **, const char **); ++ + struct lyd_leafref_links_rec { + const struct lyd_node_term *node; + const struct lyd_node_term **leafref_nodes; +@@ -1328,13 +1334,14 @@ struct lyd_leafref_links_rec { + + LY_ERR lyd_leafref_get_links(const struct lyd_node_term *, const struct lyd_leafref_links_rec **); + LY_ERR lyd_leafref_link_node_tree(struct lyd_node *); ++struct lyplg_ext *lysc_get_ext_plugin(uintptr_t); + const char *lyplg_ext_stmt2str(enum ly_stmt stmt); + const struct lysp_module *lyplg_ext_parse_get_cur_pmod(const struct lysp_ctx *); + struct ly_ctx *lyplg_ext_compile_get_ctx(const struct lysc_ctx *); + void lyplg_ext_parse_log(const struct lysp_ctx *, const struct lysp_ext_instance *, LY_LOG_LEVEL, LY_ERR, const char *, ...); + void lyplg_ext_compile_log(const struct lysc_ctx *, const struct lysc_ext_instance *, LY_LOG_LEVEL, LY_ERR, const char *, ...); + LY_ERR lyplg_ext_parse_extension_instance(struct lysp_ctx *, struct lysp_ext_instance *); +-LY_ERR lyplg_ext_compile_extension_instance(struct lysc_ctx *, const struct lysp_ext_instance *, struct lysc_ext_instance *); ++LY_ERR lyplg_ext_compile_extension_instance(struct lysc_ctx *, const struct lysp_ext_instance *, struct lysc_ext_instance *, struct lysc_node *); + void lyplg_ext_pfree_instance_substatements(const struct ly_ctx *ctx, struct lysp_ext_substmt *substmts); + void lyplg_ext_cfree_instance_substatements(const struct ly_ctx *ctx, struct lysc_ext_substmt *substmts); + typedef LY_ERR (*lyplg_ext_parse_clb)(struct lysp_ctx *, struct lysp_ext_instance *); +diff --git a/cffi/source.c b/cffi/source.c +index 34e18b0..3c76e98 100644 +--- a/cffi/source.c ++++ b/cffi/source.c +@@ -6,6 +6,6 @@ + #include + #include + +-#if LY_VERSION_MAJOR * 10000 + LY_VERSION_MINOR * 100 + LY_VERSION_MICRO < 30801 +-#error "This version of libyang bindings only works with libyang soversion 3.8.1+" ++#if LY_VERSION_MAJOR * 10000 + LY_VERSION_MINOR * 100 + LY_VERSION_MICRO < 40202 ++#error "This version of libyang bindings only works with libyang soversion 4.2.2+" + #endif +diff --git a/libyang/context.py b/libyang/context.py +index 4489680..21db162 100644 +--- a/libyang/context.py ++++ b/libyang/context.py +@@ -205,6 +205,7 @@ def __init__( + builtin_plugins_only: bool = False, + all_implemented: bool = False, + enable_imp_features: bool = False, ++ compile_obsolete: bool = False, + yanglib_path: Optional[str] = None, + yanglib_fmt: str = "json", + cdata=None, # C type: "struct ly_ctx *" +@@ -231,6 +232,8 @@ def __init__( + options |= lib.LY_CTX_ALL_IMPLEMENTED + if enable_imp_features: + options |= lib.LY_CTX_ENABLE_IMP_FEATURES ++ if compile_obsolete: ++ options |= lib.LY_CTX_COMPILE_OBSOLETE + # force priv parsed + options |= lib.LY_CTX_SET_PRIV_PARSED + +@@ -259,6 +262,7 @@ def __init__( + fmt = lib.LYD_JSON + else: + fmt = lib.LYD_XML ++ print("steweg", search_path, yanglib_path, yanglib_fmt, options) + ret = lib.ly_ctx_new_ylpath( + str2c(search_path), str2c(yanglib_path), fmt, options, ctx + ) +@@ -542,6 +546,8 @@ def parse_op( + in_data: Union[IO, str], + dtype: DataType, + parent: DNode = None, ++ opaq: bool = False, ++ strict: bool = False, + ) -> DNode: + fmt = data_format(fmt) + data = ffi.new("struct ly_in **") +@@ -551,13 +557,14 @@ def parse_op( + if ret != lib.LY_SUCCESS: + raise self.error("failed to read input data") + ++ flags = parser_flags(opaq=opaq, strict=strict) + tree = ffi.new("struct lyd_node **", ffi.NULL) + op = ffi.new("struct lyd_node **", ffi.NULL) + par = ffi.new("struct lyd_node **", ffi.NULL) + if parent is not None: + par[0] = parent.cdata + +- ret = lib.lyd_parse_op(self.cdata, par[0], data[0], fmt, dtype, tree, op) ++ ret = lib.lyd_parse_op(self.cdata, par[0], data[0], fmt, dtype, flags, tree, op) + lib.ly_in_free(data[0], 0) + if ret != lib.LY_SUCCESS: + raise self.error("failed to parse input data") +@@ -570,9 +577,17 @@ def parse_op_mem( + data: str, + dtype: DataType = DataType.DATA_YANG, + parent: DNode = None, ++ opaq: bool = False, ++ strict: bool = False, + ): + return self.parse_op( +- fmt, in_type=IOType.MEMORY, in_data=data, dtype=dtype, parent=parent ++ fmt, ++ in_type=IOType.MEMORY, ++ in_data=data, ++ dtype=dtype, ++ parent=parent, ++ opaq=opaq, ++ strict=strict, + ) + + def parse_data( +@@ -581,7 +596,6 @@ def parse_data( + in_type: IOType, + in_data: Union[str, bytes, IO], + parent: DNode = None, +- lyb_mod_update: bool = False, + no_state: bool = False, + parse_only: bool = False, + opaq: bool = False, +@@ -596,7 +610,6 @@ def parse_data( + if self.cdata is None: + raise RuntimeError("context already destroyed") + parser_flgs = parser_flags( +- lyb_mod_update=lyb_mod_update, + no_state=no_state, + parse_only=parse_only, + opaq=opaq, +@@ -640,7 +653,6 @@ def parse_data_mem( + data: Union[str, bytes], + fmt: str, + parent: DNode = None, +- lyb_mod_update: bool = False, + no_state: bool = False, + parse_only: bool = False, + opaq: bool = False, +@@ -657,7 +669,6 @@ def parse_data_mem( + in_type=IOType.MEMORY, + in_data=data, + parent=parent, +- lyb_mod_update=lyb_mod_update, + no_state=no_state, + parse_only=parse_only, + opaq=opaq, +@@ -675,7 +686,6 @@ def parse_data_file( + fileobj: IO, + fmt: str, + parent: DNode = None, +- lyb_mod_update: bool = False, + no_state: bool = False, + parse_only: bool = False, + opaq: bool = False, +@@ -692,7 +702,6 @@ def parse_data_file( + in_type=IOType.FD, + in_data=fileobj, + parent=parent, +- lyb_mod_update=lyb_mod_update, + no_state=no_state, + parse_only=parse_only, + opaq=opaq, +diff --git a/libyang/data.py b/libyang/data.py +index 7a5887b..230c51a 100644 +--- a/libyang/data.py ++++ b/libyang/data.py +@@ -53,11 +53,11 @@ def printer_flags( + ) -> int: + flags = 0 + if with_siblings: +- flags |= lib.LYD_PRINT_WITHSIBLINGS ++ flags |= lib.LYD_PRINT_SIBLINGS + if not pretty: + flags |= lib.LYD_PRINT_SHRINK + if keep_empty_containers: +- flags |= lib.LYD_PRINT_KEEPEMPTYCONT ++ flags |= lib.LYD_PRINT_EMPTY_CONT + if trim_default_values: + flags |= lib.LYD_PRINT_WD_TRIM + if include_implicit_defaults: +@@ -109,7 +109,6 @@ def newval_flags( + + # ------------------------------------------------------------------------------------- + def parser_flags( +- lyb_mod_update: bool = False, + no_state: bool = False, + parse_only: bool = False, + opaq: bool = False, +@@ -120,8 +119,6 @@ def parser_flags( + json_string_datatypes: bool = False, + ) -> int: + flags = 0 +- if lyb_mod_update: +- flags |= lib.LYD_PARSE_LYB_MOD_UPDATE + if no_state: + flags |= lib.LYD_PARSE_NO_STATE + if parse_only: +diff --git a/libyang/extension.py b/libyang/extension.py +index 57f7cb2..f7e4ba9 100644 +--- a/libyang/extension.py ++++ b/libyang/extension.py +@@ -8,7 +8,7 @@ + from _libyang import ffi, lib + from .context import Context + from .log import get_libyang_level +-from .schema import ExtensionCompiled, ExtensionParsed, Module ++from .schema import ExtensionCompiled, ExtensionParsed, Module, SNode + from .util import LibyangError, c2str, str2c + + +@@ -25,7 +25,7 @@ def __init__(self, message: str, ret: int, log_level: int) -> None: + + @ffi.def_extern(name="lypy_lyplg_ext_parse_clb") + def libyang_c_lyplg_ext_parse_clb(pctx, pext): +- plugin = extensions_plugins[pext.record.plugin] ++ plugin = extensions_plugins[lib.lysc_get_ext_plugin(pext.plugin_ref)] + module_cdata = lib.lyplg_ext_parse_get_cur_pmod(pctx).mod + context = Context(cdata=module_cdata.ctx) + module = Module(context, module_cdata) +@@ -46,7 +46,7 @@ def libyang_c_lyplg_ext_parse_clb(pctx, pext): + + @ffi.def_extern(name="lypy_lyplg_ext_compile_clb") + def libyang_c_lyplg_ext_compile_clb(cctx, pext, cext): +- plugin = extensions_plugins[pext.record.plugin] ++ plugin = extensions_plugins[lib.lysc_get_ext_plugin(pext.plugin_ref)] + context = Context(cdata=lib.lyplg_ext_compile_get_ctx(cctx)) + module = Module(context, cext.module) + parsed_ext = ExtensionParsed(context, pext, module) +@@ -67,7 +67,7 @@ def libyang_c_lyplg_ext_compile_clb(cctx, pext, cext): + + @ffi.def_extern(name="lypy_lyplg_ext_parse_free_clb") + def libyang_c_lyplg_ext_parse_free_clb(ctx, pext): +- plugin = extensions_plugins[pext.record.plugin] ++ plugin = extensions_plugins[lib.lysc_get_ext_plugin(pext.plugin_ref)] + context = Context(cdata=ctx) + parsed_ext = ExtensionParsed(context, pext, None) + plugin.parse_free_clb(parsed_ext) +@@ -75,7 +75,9 @@ def libyang_c_lyplg_ext_parse_free_clb(ctx, pext): + + @ffi.def_extern(name="lypy_lyplg_ext_compile_free_clb") + def libyang_c_lyplg_ext_compile_free_clb(ctx, cext): +- plugin = extensions_plugins[getattr(cext, "def").plugin] ++ plugin = extensions_plugins[ ++ lib.lysc_get_ext_plugin(getattr(cext, "def").plugin_ref) ++ ] + context = Context(cdata=ctx) + compiled_ext = ExtensionCompiled(context, cext) + plugin.compile_free_clb(compiled_ext) +@@ -200,9 +202,17 @@ def set_compile_ctx(self, cctx) -> None: + def parse_substmts(self, ext: ExtensionParsed) -> int: + return lib.lyplg_ext_parse_extension_instance(self._pctx, ext.cdata) + +- def compile_substmts(self, pext: ExtensionParsed, cext: ExtensionCompiled) -> int: ++ def compile_substmts( ++ self, ++ pext: ExtensionParsed, ++ cext: ExtensionCompiled, ++ parent: Optional[SNode] = None, ++ ) -> int: + return lib.lyplg_ext_compile_extension_instance( +- self._cctx, pext.cdata, cext.cdata ++ self._cctx, ++ pext.cdata, ++ cext.cdata, ++ ffi.NULL if parent is None else parent.cdata, + ) + + def free_parse_substmts(self, ext: ExtensionParsed) -> None: +diff --git a/libyang/schema.py b/libyang/schema.py +index 23c1b1b..f104a87 100644 +--- a/libyang/schema.py ++++ b/libyang/schema.py +@@ -1522,21 +1522,35 @@ def new(context: "libyang.Context", cdata) -> "SNode": + # ------------------------------------------------------------------------------------- + @SNode.register(SNode.LEAF) + class SLeaf(SNode): +- __slots__ = ("cdata_leaf", "cdata_leaf_parsed") ++ __slots__ = ("cdata_leaf", "cdata_leaf_parsed", "cdata_default_realtype") + + def __init__(self, context: "libyang.Context", cdata): + super().__init__(context, cdata) + self.cdata_leaf = ffi.cast("struct lysc_node_leaf *", cdata) + self.cdata_leaf_parsed = ffi.cast("struct lysp_node_leaf *", self.cdata_parsed) ++ self.cdata_default_realtype = None + + def default(self) -> Union[None, bool, int, str, float]: +- if not self.cdata_leaf.dflt: ++ if not self.cdata_leaf.dflt.str: + return None +- val = lib.lyd_value_get_canonical(self.context.cdata, self.cdata_leaf.dflt) +- if not val: +- return None +- val = c2str(val) +- val_type = Type(self.context, self.cdata_leaf.dflt.realtype, None) ++ ++ if self.cdata_default_realtype is None: ++ # calculate real type of default value just once ++ val_type_cdata = ffi.new("struct lysc_type **", ffi.NULL) ++ ret = lib.lyd_value_validate_dflt( ++ self.cdata, ++ self.cdata_leaf.dflt.str, ++ self.cdata_leaf.dflt.prefixes, ++ ffi.NULL, ++ val_type_cdata, ++ ffi.NULL, ++ ) ++ if ret != lib.LY_SUCCESS: ++ raise self.context.error("Unable to get real type of default value") ++ self.cdata_default_realtype = Type(self.context, val_type_cdata[0], None) ++ ++ val = c2str(self.cdata_leaf.dflt.str) ++ val_type = self.cdata_default_realtype + if val_type.base() == Type.BOOL: + return val == "true" + if val_type.base() in Type.NUM_TYPES: +@@ -1563,7 +1577,7 @@ def __str__(self): + # ------------------------------------------------------------------------------------- + @SNode.register(SNode.LEAFLIST) + class SLeafList(SNode): +- __slots__ = ("cdata_leaflist", "cdata_leaflist_parsed") ++ __slots__ = ("cdata_leaflist", "cdata_leaflist_parsed", "cdata_default_realtypes") + + def __init__(self, context: "libyang.Context", cdata): + super().__init__(context, cdata) +@@ -1571,6 +1585,7 @@ def __init__(self, context: "libyang.Context", cdata): + self.cdata_leaflist_parsed = ffi.cast( + "struct lysp_node_leaflist *", self.cdata_parsed + ) ++ self.cdata_default_realtypes = None + + def ordered(self) -> bool: + return bool(self.cdata_parsed.flags & lib.LYS_ORDBY_USER) +@@ -1586,12 +1601,37 @@ def type(self) -> Type: + def defaults(self) -> Iterator[Union[None, bool, int, str, float]]: + if self.cdata_leaflist.dflts == ffi.NULL: + return +- for dflt in ly_array_iter(self.cdata_leaflist.dflts): +- val = lib.lyd_value_get_canonical(self.context.cdata, dflt) +- if not val: ++ ++ if self.cdata_default_realtypes is None: ++ # calculate real types of default values just once ++ val_type_cdata = ffi.new("struct lysc_type **", ffi.NULL) ++ self.cdata_default_realtypes = [] ++ for dflt in ly_array_iter(self.cdata_leaflist.dflts): ++ if not dflt.str: ++ self.cdata_default_realtypes.append(None) ++ continue ++ val_type_cdata[0] = ffi.NULL ++ ret = lib.lyd_value_validate_dflt( ++ self.cdata, ++ dflt.str, ++ dflt.prefixes, ++ ffi.NULL, ++ val_type_cdata, ++ ffi.NULL, ++ ) ++ if ret != lib.LY_SUCCESS: ++ raise self.context.error("Unable to get real type of default value") ++ self.cdata_default_realtypes.append( ++ Type(self.context, val_type_cdata[0], None) ++ ) ++ ++ for dflt, val_type in zip( ++ ly_array_iter(self.cdata_leaflist.dflts), self.cdata_default_realtypes ++ ): ++ if not dflt.str: + yield None +- val = c2str(val) +- val_type = Type(self.context, dflt.realtype, None) ++ continue ++ val = c2str(dflt.str) + if val_type.base() == Type.BOOL: + yield val == "true" + elif val_type.base() in Type.NUM_TYPES: +diff --git a/tests/test_context.py b/tests/test_context.py +index 61c0ae8..8ffd045 100644 +--- a/tests/test_context.py ++++ b/tests/test_context.py +@@ -20,7 +20,11 @@ def test_ctx_no_dir(self): + self.assertIsNot(ctx, None) + + def test_ctx_yanglib(self): +- ctx = Context(YANG_DIR, yanglib_path=YANG_DIR + "/yang-library.json") ++ ctx = Context( ++ YANG_DIR, ++ yanglib_path=YANG_DIR + "/yang-library.json", ++ compile_obsolete=True, ++ ) + ctx.load_module("yolo-system") + dnode = ctx.get_yanglib_data() + j = dnode.print_mem("json", with_siblings=True) +diff --git a/tests/test_data.py b/tests/test_data.py +index 430873a..8f56dc1 100644 +--- a/tests/test_data.py ++++ b/tests/test_data.py +@@ -33,7 +33,7 @@ + # ------------------------------------------------------------------------------------- + class DataTest(unittest.TestCase): + def setUp(self): +- self.ctx = Context(YANG_DIR) ++ self.ctx = Context(YANG_DIR, compile_obsolete=True) + modules = [ + self.ctx.load_module("ietf-netconf"), + self.ctx.load_module("yolo-system"), +@@ -51,18 +51,18 @@ def tearDown(self): + "yolo-system:conf": { + "hostname": "foo", + "url": [ +- { +- "proto": "https", +- "host": "github.com", +- "path": "/CESNET/libyang-python", +- "enabled": false +- }, + { + "proto": "http", + "host": "foobar.com", + "port": 8080, + "path": "/index.html", + "enabled": true ++ }, ++ { ++ "proto": "https", ++ "host": "github.com", ++ "path": "/CESNET/libyang-python", ++ "enabled": false + } + ], + "number": [ +@@ -76,7 +76,9 @@ def tearDown(self): + """ + + def test_data_parse_config_json(self): +- dnode = self.ctx.parse_data_mem(self.JSON_CONFIG, "json", no_state=True) ++ dnode = self.ctx.parse_data_mem( ++ self.JSON_CONFIG, "json", no_state=True, ordered=True ++ ) + self.assertIsInstance(dnode, DContainer) + try: + j = dnode.print_mem("json", with_siblings=True) +@@ -91,18 +93,18 @@ def test_data_parse_config_json(self): + "yolo-system:conf": { + "hostname": "foo", + "url": [ +- { +- "proto": "https", +- "host": "github.com", +- "path": "/CESNET/libyang-python", +- "enabled": false +- }, + { + "proto": "http", + "host": "foobar.com", + "port": 8080, + "path": "/index.html", + "enabled": true ++ }, ++ { ++ "proto": "https", ++ "host": "github.com", ++ "path": "/CESNET/libyang-python", ++ "enabled": false + } + ], + "number": [ +@@ -128,12 +130,6 @@ def test_data_parse_config_json_without_yang_lib(self): + "yolo-system:conf": { + "hostname": "foo", + "url": [ +- { +- "proto": "https", +- "host": "github.com", +- "path": "/CESNET/libyang-python", +- "enabled": false +- }, + { + "proto": "http", + "host": "barfoo.com", +@@ -145,6 +141,12 @@ def test_data_parse_config_json_without_yang_lib(self): + "port": 8080, + "path": "/index.html", + "enabled": true ++ }, ++ { ++ "proto": "https", ++ "host": "github.com", ++ "path": "/CESNET/libyang-python", ++ "enabled": false + } + ], + "number": [ +@@ -254,7 +256,9 @@ def test_data_parse_state_json(self): + """ + + def test_data_parse_config_xml(self): +- dnode = self.ctx.parse_data_mem(self.XML_CONFIG, "xml", validate_present=True) ++ dnode = self.ctx.parse_data_mem( ++ self.XML_CONFIG, "xml", validate_present=True, ordered=True ++ ) + self.assertIsInstance(dnode, DContainer) + try: + xml = dnode.print_mem("xml", with_siblings=True, trim_default_values=True) +@@ -509,6 +513,7 @@ def test_data_from_dict_module_free_func(self): + } + + def test_data_from_dict_module_with_prefix(self): ++ self.maxDiff = None + module = self.ctx.get_module("yolo-system") + dnode = module.parse_data_dict( + self.DICT_CONFIG_WITH_PREFIX, strict=True, validate_present=True +@@ -853,17 +858,17 @@ def test_data_diff(self): + TREE = [ + "/yolo-system:conf", + "/yolo-system:conf/hostname", +- "/yolo-system:conf/url[proto='https'][host='github.com']", +- "/yolo-system:conf/url[proto='https'][host='github.com']/proto", +- "/yolo-system:conf/url[proto='https'][host='github.com']/host", +- "/yolo-system:conf/url[proto='https'][host='github.com']/path", +- "/yolo-system:conf/url[proto='https'][host='github.com']/enabled", + "/yolo-system:conf/url[proto='http'][host='foobar.com']", + "/yolo-system:conf/url[proto='http'][host='foobar.com']/proto", + "/yolo-system:conf/url[proto='http'][host='foobar.com']/host", + "/yolo-system:conf/url[proto='http'][host='foobar.com']/port", + "/yolo-system:conf/url[proto='http'][host='foobar.com']/path", + "/yolo-system:conf/url[proto='http'][host='foobar.com']/enabled", ++ "/yolo-system:conf/url[proto='https'][host='github.com']", ++ "/yolo-system:conf/url[proto='https'][host='github.com']/proto", ++ "/yolo-system:conf/url[proto='https'][host='github.com']/host", ++ "/yolo-system:conf/url[proto='https'][host='github.com']/path", ++ "/yolo-system:conf/url[proto='https'][host='github.com']/enabled", + "/yolo-system:conf/number[.='1000']", + "/yolo-system:conf/number[.='2000']", + "/yolo-system:conf/number[.='3000']", +@@ -906,7 +911,7 @@ def test_find_all(self): + } + ] + } +- self.assertEqual(urls[0].print_dict(absolute=False), expected_url) ++ self.assertEqual(urls[1].print_dict(absolute=False), expected_url) + finally: + dnode.free() + +@@ -1090,7 +1095,9 @@ def test_dnode_leafref_linking(self): + "yolo-leafref-extended:ref1": "val1" + }""" + self.ctx.destroy() +- self.ctx = Context(YANG_DIR, leafref_extended=True, leafref_linking=True) ++ self.ctx = Context( ++ YANG_DIR, leafref_extended=True, leafref_linking=True, compile_obsolete=True ++ ) + mod = self.ctx.load_module("yolo-leafref-extended") + self.assertIsInstance(mod, Module) + dnode1 = self.ctx.parse_data_mem(MAIN, "json", parse_only=True) +@@ -1121,7 +1128,7 @@ def test_dnode_builtin_plugins_only(self): + MAIN = {"yolo-nodetypes:ip-address": "test"} + self.tearDown() + gc.collect() +- self.ctx = Context(YANG_DIR, builtin_plugins_only=True) ++ self.ctx = Context(YANG_DIR, builtin_plugins_only=True, compile_obsolete=True) + module = self.ctx.load_module("yolo-nodetypes") + dnode = dict_to_dnode(MAIN, module, None, validate=False, store_only=True) + self.assertIsInstance(dnode, DLeaf) +@@ -1140,7 +1147,7 @@ def test_merge_builtin_plugins_only(self): + MAIN = {"yolo-nodetypes:ip-address": "test"} + self.tearDown() + gc.collect() +- self.ctx = Context(YANG_DIR, builtin_plugins_only=True) ++ self.ctx = Context(YANG_DIR, builtin_plugins_only=True, compile_obsolete=True) + module = self.ctx.load_module("yolo-nodetypes") + dnode = module.parse_data_dict(MAIN, validate=False, store_only=True) + self.assertIsInstance(dnode, DLeaf) +diff --git a/tests/test_diff.py b/tests/test_diff.py +index d4b7e87..2263e34 100644 +--- a/tests/test_diff.py ++++ b/tests/test_diff.py +@@ -97,7 +97,9 @@ class DiffTest(unittest.TestCase): + ) + + def test_diff(self): +- with Context(OLD_YANG_DIR) as ctx_old, Context(NEW_YANG_DIR) as ctx_new: ++ with Context(OLD_YANG_DIR, compile_obsolete=True) as ctx_old, Context( ++ NEW_YANG_DIR, compile_obsolete=True ++ ) as ctx_new: + mod = ctx_old.load_module("yolo-system") + mod.feature_enable_all() + mod = ctx_new.load_module("yolo-system") +diff --git a/tests/test_schema.py b/tests/test_schema.py +index b1862ab..a1ca412 100644 +--- a/tests/test_schema.py ++++ b/tests/test_schema.py +@@ -272,7 +272,7 @@ def test_iffeature_dump(self): + # ------------------------------------------------------------------------------------- + class ContainerTest(unittest.TestCase): + def setUp(self): +- self.ctx = Context(YANG_DIR) ++ self.ctx = Context(YANG_DIR, compile_obsolete=True) + mod = self.ctx.load_module("yolo-system") + mod.feature_enable_all() + self.container = next(self.ctx.find_path("/yolo-system:conf")) +@@ -549,7 +549,7 @@ def test_rpc_parsed(self): + # ------------------------------------------------------------------------------------- + class LeafTypeTest(unittest.TestCase): + def setUp(self): +- self.ctx = Context(YANG_DIR) ++ self.ctx = Context(YANG_DIR, compile_obsolete=True) + self.ctx.load_module("yolo-system") + + def tearDown(self): 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 + diff --git a/src/bin/copy.c b/src/bin/copy.c index f9b7d2a1..338b8079 100644 --- a/src/bin/copy.c +++ b/src/bin/copy.c @@ -291,7 +291,7 @@ static int sysrepo_export(const struct infix_ds *ds, const char *path) return err; } - err = lyd_print_path(path, data->tree, LYD_JSON, LYD_PRINT_WITHSIBLINGS); + err = lyd_print_path(path, data->tree, LYD_JSON, LYD_PRINT_SIBLINGS); sr_release_data(data); if (err) { sysrepo_print_error(sess); diff --git a/src/confd/configure.ac b/src/confd/configure.ac index 0c48c804..6266615b 100644 --- a/src/confd/configure.ac +++ b/src/confd/configure.ac @@ -1,6 +1,6 @@ AC_PREREQ(2.61) # confd version is same as system YANG model version, step on breaking changes -AC_INIT([confd], [1.6], [https://github.com/kernelkit/infix/issues]) +AC_INIT([confd], [1.7], [https://github.com/kernelkit/infix/issues]) AM_INIT_AUTOMAKE(1.11 foreign subdir-objects) AM_SILENT_RULES(yes) @@ -20,10 +20,10 @@ AC_CONFIG_FILES([ share/migrate/1.4/Makefile share/migrate/1.5/Makefile share/migrate/1.6/Makefile + share/migrate/1.7/Makefile yang/Makefile yang/confd/Makefile yang/test-mode/Makefile - yang/rousette/Makefile src/Makefile ]) diff --git a/src/confd/share/factory.d/10-netconf-server.json b/src/confd/share/factory.d/10-netconf-server.json index 4bd392c9..0dcfdd2c 100644 --- a/src/confd/share/factory.d/10-netconf-server.json +++ b/src/confd/share/factory.d/10-netconf-server.json @@ -21,7 +21,11 @@ "name": "default-ssh", "ssh": { "tcp-server-parameters": { - "local-address": "::" + "local-bind": [ + { + "local-address": "::" + } + ] }, "ssh-server-parameters": { "server-identity": { diff --git a/src/confd/share/migrate/1.7/10-netconf-server-tcp-params.sh b/src/confd/share/migrate/1.7/10-netconf-server-tcp-params.sh new file mode 100755 index 00000000..1ebab54d --- /dev/null +++ b/src/confd/share/migrate/1.7/10-netconf-server-tcp-params.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# Migrate NETCONF server TCP parameters to new YANG schema (RFC 9643) +# The 'local-address' leaf has been moved into a 'local-bind' list +# Old: tcp-server-parameters/local-address +# New: tcp-server-parameters/local-bind[]/local-address + +file=$1 +temp=${file}.tmp + +jq ' +if .["ietf-netconf-server:netconf-server"]?.listen?.endpoints?.endpoint then + .["ietf-netconf-server:netconf-server"].listen.endpoints.endpoint |= map( + if .ssh?."tcp-server-parameters"?."local-address" then + # Extract and remove the old local-address value, then add new structure + .ssh."tcp-server-parameters"."local-address" as $addr | + .ssh."tcp-server-parameters" |= (del(."local-address") | . + { + "local-bind": [{ + "local-address": $addr + }] + }) + else + . + end + ) +else + . +end +' "$file" > "$temp" && mv "$temp" "$file" diff --git a/src/confd/share/migrate/1.7/20-keystore-cleartext-key-rename.sh b/src/confd/share/migrate/1.7/20-keystore-cleartext-key-rename.sh new file mode 100755 index 00000000..2343e5f8 --- /dev/null +++ b/src/confd/share/migrate/1.7/20-keystore-cleartext-key-rename.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# Migrate keystore symmetric key syntax to new YANG schema (RFC 9643) +# The 'cleartext-key' leaf has been renamed to 'cleartext-symmetric-key' +# Old: symmetric-key[]/key-type/cleartext-key +# New: symmetric-key[]/key-type/cleartext-symmetric-key + +file=$1 +temp=${file}.tmp + +jq ' +if .["ietf-keystore:keystore"]?."symmetric-keys"?."symmetric-key" then + .["ietf-keystore:keystore"]."symmetric-keys"."symmetric-key" |= map( + if ."key-type"?."cleartext-key" then + # Rename cleartext-key to cleartext-symmetric-key + ."key-type"."cleartext-key" as $key_value | + ."key-type" |= (del(."cleartext-key") | . + { + "cleartext-symmetric-key": $key_value + }) + else + . + end + ) +else + . +end +' "$file" > "$temp" && mv "$temp" "$file" diff --git a/src/confd/share/migrate/1.7/Makefile.am b/src/confd/share/migrate/1.7/Makefile.am new file mode 100644 index 00000000..488c2765 --- /dev/null +++ b/src/confd/share/migrate/1.7/Makefile.am @@ -0,0 +1,3 @@ +migratedir = $(pkgdatadir)/migrate/1.7 +dist_migrate_DATA = 10-netconf-server-tcp-params.sh \ + 20-keystore-cleartext-key-rename.sh diff --git a/src/confd/share/migrate/Makefile.am b/src/confd/share/migrate/Makefile.am index c5d75c31..0a7c2c82 100644 --- a/src/confd/share/migrate/Makefile.am +++ b/src/confd/share/migrate/Makefile.am @@ -1,2 +1,2 @@ -SUBDIRS = 1.0 1.1 1.2 1.3 1.4 1.5 1.6 +SUBDIRS = 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 migratedir = $(pkgdatadir)/migrate diff --git a/src/confd/src/core.c b/src/confd/src/core.c index e24dca9e..9460de1a 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -184,7 +184,7 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod /* Debug: print diff to file */ FILE *f = fopen("/tmp/confd-diff.json", "w"); if (f) { - lyd_print_file(f, diff, LYD_JSON, LYD_PRINT_WITHSIBLINGS); + lyd_print_file(f, diff, LYD_JSON, LYD_PRINT_SIBLINGS); fclose(f); } #endif diff --git a/src/confd/src/if-wifi.c b/src/confd/src/if-wifi.c index ab934a84..edd40535 100644 --- a/src/confd/src/if-wifi.c +++ b/src/confd/src/if-wifi.c @@ -87,7 +87,7 @@ int wifi_gen(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net) country = lydx_get_cattr(wifi, "country-code"); encryption = lydx_get_cattr(wifi, "encryption"); secret_node = lydx_get_xpathf(cif, "../../keystore/symmetric-keys/symmetric-key[name='%s']", secret_name); - secret = lydx_get_cattr(secret_node, "cleartext-key"); + secret = lydx_get_cattr(secret_node, "cleartext-symmetric-key"); if (!enabled) return wifi_gen_del(cif, net); diff --git a/src/confd/yang/Makefile.am b/src/confd/yang/Makefile.am index ca1f9fb8..25d1740d 100644 --- a/src/confd/yang/Makefile.am +++ b/src/confd/yang/Makefile.am @@ -1 +1 @@ -SUBDIRS = confd test-mode rousette +SUBDIRS = confd test-mode diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 67a282a2..4841eb7a 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -18,7 +18,7 @@ MODULES=( "ietf-bfd-ip-sh@2022-09-22.yang" "iana-if-type@2023-01-26.yang" # NOTE: ietf-tls-client must be version matched with ietf-tls-server, used by netopeer2! - "ietf-tls-client@2023-12-28.yang" +# "ietf-tls-client@2023-12-28.yang" "ietf-syslog@2024-03-21.yang -e file-action -e file-limit-size -e remote-action -e select-adv-compare -e select-match" "infix-syslog@2025-11-17.yang" "iana-hardware@2018-03-13.yang" @@ -37,9 +37,9 @@ MODULES=( "infix-firewall@2025-04-26.yang" "infix-firewall-services@2025-04-26.yang" "infix-firewall-icmp-types@2025-04-26.yang" - "infix-meta@2024-10-18.yang" + "infix-meta@2025-12-10.yang" "infix-system@2025-10-18.yang" - "infix-services@2024-12-03.yang" + "infix-services@2025-12-10.yang" "ieee802-ethernet-interface@2019-06-21.yang" "infix-ethernet-interface@2024-02-27.yang" "infix-factory-default@2023-06-28.yang" @@ -47,5 +47,5 @@ MODULES=( "ietf-crypto-types -e cleartext-symmetric-keys" "infix-crypto-types@2025-06-17.yang" "ietf-keystore -e symmetric-keys" - "infix-keystore@2025-06-17.yang" + "infix-keystore@2025-12-10.yang" ) diff --git a/src/confd/yang/confd/infix-if-wifi.yang b/src/confd/yang/confd/infix-if-wifi.yang index 85a3ba34..e3f5e39b 100644 --- a/src/confd/yang/confd/infix-if-wifi.yang +++ b/src/confd/yang/confd/infix-if-wifi.yang @@ -37,6 +37,11 @@ submodule infix-if-wifi { It supports WiFi client mode and enables comprehensive management of wireless connections, including encryption, country codes, and scanning."; + revision 2025-12-12 { + description "Adapt to new revision of model ietf-keystore."; + reference "internal"; + } + revision 2025-05-27 { description "Initial revision."; reference "internal"; @@ -121,7 +126,7 @@ submodule infix-if-wifi { } leaf secret { - type ks:symmetric-key-ref; + type ks:central-symmetric-key-ref; mandatory true; must "../encryption != 'disabled'" { error-message diff --git a/src/confd/yang/confd/infix-if-wifi@2025-05-27.yang b/src/confd/yang/confd/infix-if-wifi@2025-12-10.yang similarity index 100% rename from src/confd/yang/confd/infix-if-wifi@2025-05-27.yang rename to src/confd/yang/confd/infix-if-wifi@2025-12-10.yang diff --git a/src/confd/yang/confd/infix-keystore.yang b/src/confd/yang/confd/infix-keystore.yang index 97ae2b70..705ccb0a 100644 --- a/src/confd/yang/confd/infix-keystore.yang +++ b/src/confd/yang/confd/infix-keystore.yang @@ -11,6 +11,10 @@ module infix-keystore { import infix-crypto-types { prefix infix-ct; } + + revision 2025-12-10 { + description "Adapt to changes in final version of ietf-keystore"; + } revision 2025-06-17 { description "Add Wi-Fi secrets support"; } @@ -46,14 +50,14 @@ module infix-keystore { wifi-preshared-key-format - WiFi preshared key"; } } - deviation "/ks:keystore/ks:symmetric-keys/ks:symmetric-key/ks:key-type/ks:cleartext-key/ks:cleartext-key" { + deviation "/ks:keystore/ks:symmetric-keys/ks:symmetric-key/ks:key-type/ks:cleartext-symmetric-key" { deviate not-supported; } augment "/ks:keystore/ks:symmetric-keys/ks:symmetric-key/ks:key-type" { - case cleartext-key { - leaf cleartext-key { + case cleartext-symmetric-key { + leaf cleartext-symmetric-key { type string; - must "../../ks:key-format != 'infix-ct:wifi-preshared-key-format' or " + + must "../infix-ks:key-format != 'infix-ct:wifi-preshared-key-format' or " + "(string-length(.) >= 8 and string-length(.) <= 63)" { error-message "WiFi pre-shared key must be 8-63 characters long"; } diff --git a/src/confd/yang/confd/infix-keystore@2025-06-17.yang b/src/confd/yang/confd/infix-keystore@2025-12-10.yang similarity index 100% rename from src/confd/yang/confd/infix-keystore@2025-06-17.yang rename to src/confd/yang/confd/infix-keystore@2025-12-10.yang diff --git a/src/confd/yang/confd/infix-meta.yang b/src/confd/yang/confd/infix-meta.yang index e81b91fd..da7f96b1 100644 --- a/src/confd/yang/confd/infix-meta.yang +++ b/src/confd/yang/confd/infix-meta.yang @@ -12,6 +12,10 @@ module infix-meta { description "Infix metadata."; + revision 2025-12-10 { + description "Remove the obsolete, not allowed in libyang."; + reference "internal"; + } revision 2024-10-18 { description "Mark entire meta container obsolete."; reference "internal"; @@ -29,10 +33,8 @@ module infix-meta { container meta { description "Meta data"; - status obsolete; leaf version { - status obsolete; // Ensure frontends don't show this, used for migration. description "Configuration file format version, automatically generated."; type string; } diff --git a/src/confd/yang/confd/infix-meta@2024-10-18.yang b/src/confd/yang/confd/infix-meta@2025-12-10.yang similarity index 100% rename from src/confd/yang/confd/infix-meta@2024-10-18.yang rename to src/confd/yang/confd/infix-meta@2025-12-10.yang diff --git a/src/confd/yang/confd/infix-routing.yang b/src/confd/yang/confd/infix-routing.yang index c17f7d56..0b56ff04 100644 --- a/src/confd/yang/confd/infix-routing.yang +++ b/src/confd/yang/confd/infix-routing.yang @@ -167,6 +167,7 @@ module infix-routing { base infix-routing-type; description "Static route"; } + deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/rt:type" { deviate replace { type identityref { @@ -858,5 +859,4 @@ module infix-routing { This is useful for networks where multiple addresses exist on an interface."; } } - } diff --git a/src/confd/yang/confd/infix-services.yang b/src/confd/yang/confd/infix-services.yang index 86ce2059..a08135ac 100644 --- a/src/confd/yang/confd/infix-services.yang +++ b/src/confd/yang/confd/infix-services.yang @@ -25,6 +25,10 @@ module infix-services { contact "kernelkit@googlegroups.com"; description "Infix services, generic."; + revision 2025-12-10 { + description "Adapt to changes in final version of ietf-keystore"; + reference "internal"; + } revision 2024-12-03 { description "Add support for SSH server configuration"; reference "internal"; @@ -143,7 +147,7 @@ module infix-services { + "derived-from-or-self(deref(.)/../ks:private-key-format, 'infix-ct:rsa-private-key-format'))" { error-message "Only RSA hostkeys are supported."; } - type ks:asymmetric-key-ref; + type ks:central-asymmetric-key-ref; } list listen { diff --git a/src/confd/yang/confd/infix-services@2024-12-03.yang b/src/confd/yang/confd/infix-services@2025-12-10.yang similarity index 100% rename from src/confd/yang/confd/infix-services@2024-12-03.yang rename to src/confd/yang/confd/infix-services@2025-12-10.yang diff --git a/src/confd/yang/libnetconf2.inc b/src/confd/yang/libnetconf2.inc index 2a1fa268..3f0080b1 100644 --- a/src/confd/yang/libnetconf2.inc +++ b/src/confd/yang/libnetconf2.inc @@ -3,23 +3,23 @@ # INFO: CHANGED FEATURE FLAGS FROM ORIGINAL: ietf-keystore and ietf-ssh-server MODULES=( - "iana-ssh-encryption-algs@2022-06-16.yang" - "iana-ssh-key-exchange-algs@2022-06-16.yang" - "iana-ssh-mac-algs@2022-06-16.yang" - "iana-ssh-public-key-algs@2022-06-16.yang" - "iana-tls-cipher-suite-algs@2022-06-16.yang" + "iana-ssh-encryption-algs@2024-10-16.yang" + "iana-ssh-key-exchange-algs@2024-10-16.yang" + "iana-ssh-mac-algs@2024-10-16.yang" + "iana-ssh-public-key-algs@2024-10-16.yang" + "iana-tls-cipher-suite-algs@2024-10-16.yang" "ietf-x509-cert-to-name@2014-12-10.yang" "iana-crypt-hash@2014-04-04.yang -e crypt-hash-md5 -e crypt-hash-sha-256 -e crypt-hash-sha-512" - "ietf-crypto-types@2023-12-28.yang -e cleartext-passwords -e cleartext-private-keys" - "ietf-keystore@2023-12-28.yang -e central-keystore-supported -e asymmetric-keys" - "ietf-truststore@2023-12-28.yang -e central-truststore-supported -e certificates -e public-keys" - "ietf-tcp-common@2023-12-28.yang -e keepalives-supported" - "ietf-tcp-server@2023-12-28.yang -e tcp-server-keepalives" - "ietf-tcp-client@2023-12-28.yang -e local-binding-supported -e tcp-client-keepalives" - "ietf-ssh-common@2023-12-28.yang -e transport-params" - "ietf-ssh-server@2023-12-28.yang" - "ietf-tls-common@2023-12-28.yang -e tls10 -e tls11 -e tls12 -e tls13 -e hello-params" - "ietf-tls-server@2023-12-28.yang -e server-ident-x509-cert -e client-auth-supported -e client-auth-x509-cert" - "ietf-netconf-server@2023-12-28.yang -e ssh-listen -e tls-listen -e ssh-call-home -e tls-call-home -e central-netconf-server-supported" - "libnetconf2-netconf-server@2025-06-02.yang" + "ietf-crypto-types@2024-10-10.yang -e cleartext-passwords -e cleartext-private-keys" + "ietf-keystore@2024-10-10.yang -e central-keystore-supported -e inline-definitions-supported -e asymmetric-keys" + "ietf-truststore@2024-10-10.yang -e central-truststore-supported -e inline-definitions-supported -e certificates -e public-keys" + "ietf-tcp-common@2024-10-10.yang -e keepalives-supported" + "ietf-tcp-server@2024-10-10.yang -e tcp-server-keepalives" + "ietf-tcp-client@2024-10-10.yang -e local-binding-supported -e tcp-client-keepalives" + "ietf-ssh-common@2024-10-10.yang -e algorithm-discovery -e transport-params" + "ietf-ssh-server@2024-10-10.yang" + "ietf-tls-common@2024-10-10.yang -e algorithm-discovery -e tls12 -e tls13 -e hello-params" + "ietf-tls-server@2024-10-10.yang -e server-ident-x509-cert -e client-auth-supported -e client-auth-x509-cert" + "ietf-netconf-server@2025-04-24.yang -e ssh-listen -e tls-listen -e ssh-call-home -e tls-call-home -e central-netconf-server-supported" + "libnetconf2-netconf-server@2025-11-11.yang" ) diff --git a/src/confd/yang/rousette.inc b/src/confd/yang/rousette.inc index 1c1de132..f70041cd 100644 --- a/src/confd/yang/rousette.inc +++ b/src/confd/yang/rousette.inc @@ -2,6 +2,6 @@ MODULES=( "ietf-restconf@2017-01-26.yang" "ietf-yang-patch@2017-02-22.yang" "ietf-restconf-monitoring@2017-01-26.yang" - "ietf-subscribed-notifications@2019-09-09.yang" + "ietf-subscribed-notifications@2019-09-09.yang -e encode-json" "ietf-restconf-subscribed-notifications@2019-11-17.yang" ) diff --git a/src/confd/yang/rousette/Makefile.am b/src/confd/yang/rousette/Makefile.am deleted file mode 100644 index 42519ffb..00000000 --- a/src/confd/yang/rousette/Makefile.am +++ /dev/null @@ -1,2 +0,0 @@ -yangdir = $(YANGDIR)/rousette -yang_DATA = $(wildcard *@*.yang) diff --git a/src/confd/yang/rousette/ietf-restconf-monitoring@2017-01-26.yang b/src/confd/yang/rousette/ietf-restconf-monitoring@2017-01-26.yang deleted file mode 100644 index e87b00ba..00000000 --- a/src/confd/yang/rousette/ietf-restconf-monitoring@2017-01-26.yang +++ /dev/null @@ -1,150 +0,0 @@ -module ietf-restconf-monitoring { - namespace "urn:ietf:params:xml:ns:yang:ietf-restconf-monitoring"; - prefix "rcmon"; - - import ietf-yang-types { prefix yang; } - import ietf-inet-types { prefix inet; } - - organization - "IETF NETCONF (Network Configuration) Working Group"; - - contact - "WG Web: - WG List: - - Author: Andy Bierman - - - Author: Martin Bjorklund - - - Author: Kent Watsen - "; - - description - "This module contains monitoring information for the - RESTCONF protocol. - - Copyright (c) 2017 IETF Trust and the persons identified as - authors of the code. All rights reserved. - - Redistribution and use in source and binary forms, with or - without modification, is permitted pursuant to, and subject - to the license terms contained in, the Simplified BSD License - set forth in Section 4.c of the IETF Trust's Legal Provisions - Relating to IETF Documents - (http://trustee.ietf.org/license-info). - - This version of this YANG module is part of RFC 8040; see - the RFC itself for full legal notices."; - - revision 2017-01-26 { - description - "Initial revision."; - reference - "RFC 8040: RESTCONF Protocol."; - } - - container restconf-state { - config false; - description - "Contains RESTCONF protocol monitoring information."; - - container capabilities { - description - "Contains a list of protocol capability URIs."; - - leaf-list capability { - type inet:uri; - description - "A RESTCONF protocol capability URI."; - } - } - - container streams { - description - "Container representing the notification event streams - supported by the server."; - reference - "RFC 5277, Section 3.4, element."; - - list stream { - key name; - description - "Each entry describes an event stream supported by - the server."; - - leaf name { - type string; - description - "The stream name."; - reference - "RFC 5277, Section 3.4, element."; - } - - leaf description { - type string; - description - "Description of stream content."; - reference - "RFC 5277, Section 3.4, element."; - } - - leaf replay-support { - type boolean; - default false; - description - "Indicates if replay buffer is supported for this stream. - If 'true', then the server MUST support the 'start-time' - and 'stop-time' query parameters for this stream."; - reference - "RFC 5277, Section 3.4, element."; - } - - leaf replay-log-creation-time { - when "../replay-support" { - description - "Only present if notification replay is supported."; - } - type yang:date-and-time; - description - "Indicates the time the replay log for this stream - was created."; - reference - "RFC 5277, Section 3.4, - element."; - } - - list access { - key encoding; - min-elements 1; - description - "The server will create an entry in this list for each - encoding format that is supported for this stream. - The media type 'text/event-stream' is expected - for all event streams. This list identifies the - subtypes supported for this stream."; - - leaf encoding { - type string; - description - "This is the secondary encoding format within the - 'text/event-stream' encoding used by all streams. - The type 'xml' is supported for XML encoding. - The type 'json' is supported for JSON encoding."; - } - - leaf location { - type inet:uri; - mandatory true; - description - "Contains a URL that represents the entry point - for establishing notification delivery via - server-sent events."; - } - } - } - } - } - -} diff --git a/src/confd/yang/rousette/ietf-restconf-subscribed-notifications@2019-11-17.yang b/src/confd/yang/rousette/ietf-restconf-subscribed-notifications@2019-11-17.yang deleted file mode 100644 index 98a79908..00000000 --- a/src/confd/yang/rousette/ietf-restconf-subscribed-notifications@2019-11-17.yang +++ /dev/null @@ -1,85 +0,0 @@ -module ietf-restconf-subscribed-notifications { - yang-version 1.1; - namespace "urn:ietf:params:xml:ns:yang:" - + "ietf-restconf-subscribed-notifications"; - prefix rsn; - - import ietf-subscribed-notifications { - prefix sn; - } - import ietf-inet-types { - prefix inet; - } - - organization - "IETF NETCONF (Network Configuration) Working Group"; - contact - "WG Web: - WG List: - - Editor: Eric Voit - - - Editor: Alexander Clemm - - - Editor: Reshad Rahman - "; - description - "Defines RESTCONF as a supported transport for subscribed - event notifications. - - Copyright (c) 2019 IETF Trust and the persons identified - as authors of the code. All rights reserved. - - Redistribution and use in source and binary forms, with or - without modification, is permitted pursuant to, and subject to - the license terms contained in, the Simplified BSD License set - forth in Section 4.c of the IETF Trust's Legal Provisions - Relating to IETF Documents - (https://trustee.ietf.org/license-info). - - This version of this YANG module is part of RFC 8650; see the - RFC itself for full legal notices."; - - revision 2019-11-17 { - description - "Initial version"; - reference - "RFC 8650: Dynamic Subscription to YANG Events and Datastores - over RESTCONF"; - } - - grouping uri { - description - "Provides a reusable description of a URI."; - leaf uri { - type inet:uri; - config false; - description - "Location of a subscription-specific URI on the publisher."; - } - } - - augment "/sn:establish-subscription/sn:output" { - description - "This augmentation allows RESTCONF-specific parameters for a - response to a publisher's subscription request."; - uses uri; - } - - augment "/sn:subscriptions/sn:subscription" { - description - "This augmentation allows RESTCONF-specific parameters to be - exposed for a subscription."; - uses uri; - } - - augment "/sn:subscription-modified" { - description - "This augmentation allows RESTCONF-specific parameters to be - included as part of the notification that a subscription has - been modified."; - uses uri; - } -} diff --git a/src/confd/yang/rousette/ietf-restconf@2017-01-26.yang b/src/confd/yang/rousette/ietf-restconf@2017-01-26.yang deleted file mode 100644 index b47455b8..00000000 --- a/src/confd/yang/rousette/ietf-restconf@2017-01-26.yang +++ /dev/null @@ -1,278 +0,0 @@ -module ietf-restconf { - yang-version 1.1; - namespace "urn:ietf:params:xml:ns:yang:ietf-restconf"; - prefix "rc"; - - organization - "IETF NETCONF (Network Configuration) Working Group"; - - contact - "WG Web: - WG List: - - Author: Andy Bierman - - - Author: Martin Bjorklund - - - Author: Kent Watsen - "; - - description - "This module contains conceptual YANG specifications - for basic RESTCONF media type definitions used in - RESTCONF protocol messages. - - Note that the YANG definitions within this module do not - represent configuration data of any kind. - The 'restconf-media-type' YANG extension statement - provides a normative syntax for XML and JSON - message-encoding purposes. - - Copyright (c) 2017 IETF Trust and the persons identified as - authors of the code. All rights reserved. - - Redistribution and use in source and binary forms, with or - without modification, is permitted pursuant to, and subject - to the license terms contained in, the Simplified BSD License - set forth in Section 4.c of the IETF Trust's Legal Provisions - Relating to IETF Documents - (http://trustee.ietf.org/license-info). - - This version of this YANG module is part of RFC 8040; see - the RFC itself for full legal notices."; - - revision 2017-01-26 { - description - "Initial revision."; - reference - "RFC 8040: RESTCONF Protocol."; - } - - extension yang-data { - argument name { - yin-element true; - } - description - "This extension is used to specify a YANG data template that - represents conceptual data defined in YANG. It is - intended to describe hierarchical data independent of - protocol context or specific message-encoding format. - Data definition statements within a yang-data extension - specify the generic syntax for the specific YANG data - template, whose name is the argument of the 'yang-data' - extension statement. - - Note that this extension does not define a media type. - A specification using this extension MUST specify the - message-encoding rules, including the content media type. - - The mandatory 'name' parameter value identifies the YANG - data template that is being defined. It contains the - template name. - - This extension is ignored unless it appears as a top-level - statement. It MUST contain data definition statements - that result in exactly one container data node definition. - An instance of a YANG data template can thus be translated - into an XML instance document, whose top-level element - corresponds to the top-level container. - The module name and namespace values for the YANG module using - the extension statement are assigned to instance document data - conforming to the data definition statements within - this extension. - - The substatements of this extension MUST follow the - 'data-def-stmt' rule in the YANG ABNF. - - The XPath document root is the extension statement itself, - such that the child nodes of the document root are - represented by the data-def-stmt substatements within - this extension. This conceptual document is the context - for the following YANG statements: - - - must-stmt - - when-stmt - - path-stmt - - min-elements-stmt - - max-elements-stmt - - mandatory-stmt - - unique-stmt - - ordered-by - - instance-identifier data type - - The following data-def-stmt substatements are constrained - when used within a 'yang-data' extension statement. - - - The list-stmt is not required to have a key-stmt defined. - - The if-feature-stmt is ignored if present. - - The config-stmt is ignored if present. - - The available identity values for any 'identityref' - leaf or leaf-list nodes are limited to the module - containing this extension statement and the modules - imported into that module. - "; - } - - rc:yang-data yang-errors { - uses errors; - } - - rc:yang-data yang-api { - uses restconf; - } - - grouping errors { - description - "A grouping that contains a YANG container - representing the syntax and semantics of a - YANG Patch error report within a response message."; - - container errors { - description - "Represents an error report returned by the server if - a request results in an error."; - - list error { - description - "An entry containing information about one - specific error that occurred while processing - a RESTCONF request."; - reference - "RFC 6241, Section 4.3."; - - leaf error-type { - type enumeration { - enum transport { - description - "The transport layer."; - } - enum rpc { - description - "The rpc or notification layer."; - } - enum protocol { - description - "The protocol operation layer."; - } - enum application { - description - "The server application layer."; - } - } - mandatory true; - description - "The protocol layer where the error occurred."; - } - - leaf error-tag { - type string; - mandatory true; - description - "The enumerated error-tag."; - } - - leaf error-app-tag { - type string; - description - "The application-specific error-tag."; - } - - leaf error-path { - type instance-identifier; - description - "The YANG instance identifier associated - with the error node."; - } - - leaf error-message { - type string; - description - "A message describing the error."; - } - - anydata error-info { - description - "This anydata value MUST represent a container with - zero or more data nodes representing additional - error information."; - } - } - } - } - - grouping restconf { - description - "Conceptual grouping representing the RESTCONF - root resource."; - - container restconf { - description - "Conceptual container representing the RESTCONF - root resource."; - - container data { - description - "Container representing the datastore resource. - Represents the conceptual root of all state data - and configuration data supported by the server. - The child nodes of this container can be any data - resources that are defined as top-level data nodes - from the YANG modules advertised by the server in - the 'ietf-yang-library' module."; - } - - container operations { - description - "Container for all operation resources. - - Each resource is represented as an empty leaf with the - name of the RPC operation from the YANG 'rpc' statement. - - For example, the 'system-restart' RPC operation defined - in the 'ietf-system' module would be represented as - an empty leaf in the 'ietf-system' namespace. This is - a conceptual leaf and will not actually be found in - the module: - - module ietf-system { - leaf system-reset { - type empty; - } - } - - To invoke the 'system-restart' RPC operation: - - POST /restconf/operations/ietf-system:system-restart - - To discover the RPC operations supported by the server: - - GET /restconf/operations - - In XML, the YANG module namespace identifies the module: - - - - In JSON, the YANG module name identifies the module: - - { 'ietf-system:system-restart' : [null] } - "; - } - leaf yang-library-version { - type string { - pattern '\d{4}-\d{2}-\d{2}'; - } - config false; - mandatory true; - description - "Identifies the revision date of the 'ietf-yang-library' - module that is implemented by this RESTCONF server. - Indicates the year, month, and day in YYYY-MM-DD - numeric format."; - } - } - } - -} diff --git a/src/confd/yang/rousette/ietf-subscribed-notifications@2019-09-09.yang b/src/confd/yang/rousette/ietf-subscribed-notifications@2019-09-09.yang deleted file mode 100644 index e04593c3..00000000 --- a/src/confd/yang/rousette/ietf-subscribed-notifications@2019-09-09.yang +++ /dev/null @@ -1,1350 +0,0 @@ -module ietf-subscribed-notifications { - yang-version 1.1; - namespace "urn:ietf:params:xml:ns:yang:ietf-subscribed-notifications"; - prefix sn; - - import ietf-inet-types { - prefix inet; - reference - "RFC 6991: Common YANG Data Types"; - } - import ietf-interfaces { - prefix if; - reference - "RFC 8343: A YANG Data Model for Interface Management"; - } - import ietf-netconf-acm { - prefix nacm; - reference - "RFC 8341: Network Configuration Access Control Model"; - } - import ietf-network-instance { - prefix ni; - reference - "RFC 8529: YANG Data Model for Network Instances"; - } - import ietf-restconf { - prefix rc; - reference - "RFC 8040: RESTCONF Protocol"; - } - import ietf-yang-types { - prefix yang; - reference - "RFC 6991: Common YANG Data Types"; - } - - organization - "IETF NETCONF (Network Configuration) Working Group"; - contact - "WG Web: - WG List: - - Author: Alexander Clemm - - - Author: Eric Voit - - - Author: Alberto Gonzalez Prieto - - - Author: Einar Nilsen-Nygaard - - - Author: Ambika Prasad Tripathy - "; - description - "This module defines a YANG data model for subscribing to event - records and receiving matching content in notification messages. - - The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL', 'SHALL - NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', 'NOT RECOMMENDED', - 'MAY', and 'OPTIONAL' in this document are to be interpreted as - described in BCP 14 (RFC 2119) (RFC 8174) when, and only when, - they appear in all capitals, as shown here. - - Copyright (c) 2019 IETF Trust and the persons identified as - authors of the code. All rights reserved. - - Redistribution and use in source and binary forms, with or - without modification, is permitted pursuant to, and subject to - the license terms contained in, the Simplified BSD License set - forth in Section 4.c of the IETF Trust's Legal Provisions - Relating to IETF Documents - (https://trustee.ietf.org/license-info). - - This version of this YANG module is part of RFC 8639; see the - RFC itself for full legal notices."; - - revision 2019-09-09 { - description - "Initial version."; - reference - "RFC 8639: A YANG Data Model for Subscriptions to - Event Notifications"; - } - - /* - * FEATURES - */ - - feature configured { - description - "This feature indicates that configuration of subscriptions is - supported."; - } - - feature dscp { - description - "This feature indicates that a publisher supports the ability - to set the Differentiated Services Code Point (DSCP) value in - outgoing packets."; - } - - feature encode-json { - description - "This feature indicates that JSON encoding of notification - messages is supported."; - } - - feature encode-xml { - description - "This feature indicates that XML encoding of notification - messages is supported."; - } - - feature interface-designation { - description - "This feature indicates that a publisher supports sourcing all - receiver interactions for a configured subscription from a - single designated egress interface."; - } - - feature qos { - description - "This feature indicates that a publisher supports absolute - dependencies of one subscription's traffic over another - as well as weighted bandwidth sharing between subscriptions. - Both of these are Quality of Service (QoS) features that allow - differentiated treatment of notification messages between a - publisher and a specific receiver."; - } - - feature replay { - description - "This feature indicates that historical event record replay is - supported. With replay, it is possible for past event records - to be streamed in chronological order."; - } - - feature subtree { - description - "This feature indicates support for YANG subtree filtering."; - reference - "RFC 6241: Network Configuration Protocol (NETCONF), - Section 6"; - } - - feature supports-vrf { - description - "This feature indicates that a publisher supports VRF - configuration for configured subscriptions. VRF support for - dynamic subscriptions does not require this feature."; - reference - "RFC 8529: YANG Data Model for Network Instances, - Section 6"; - } - - feature xpath { - description - "This feature indicates support for XPath filtering."; - reference - "XML Path Language (XPath) Version 1.0 - (https://www.w3.org/TR/1999/REC-xpath-19991116)"; - } - - /* - * EXTENSIONS - */ - - extension subscription-state-notification { - description - "This statement applies only to notifications. It indicates - that the notification is a subscription state change - notification. Therefore, it does not participate in a regular - event stream and does not need to be specifically subscribed - to in order to be received. This statement can only occur as - a substatement of the YANG 'notification' statement. This - statement is not for use outside of this YANG module."; - } - - /* - * IDENTITIES - */ - /* Identities for RPC and notification errors */ - - identity delete-subscription-error { - description - "Base identity for the problem found while attempting to - fulfill either a 'delete-subscription' RPC request or a - 'kill-subscription' RPC request."; - } - - identity establish-subscription-error { - description - "Base identity for the problem found while attempting to - fulfill an 'establish-subscription' RPC request."; - } - - identity modify-subscription-error { - description - "Base identity for the problem found while attempting to - fulfill a 'modify-subscription' RPC request."; - } - - identity subscription-suspended-reason { - description - "Base identity for the problem condition communicated to a - receiver as part of a 'subscription-suspended' - notification."; - } - - identity subscription-terminated-reason { - description - "Base identity for the problem condition communicated to a - receiver as part of a 'subscription-terminated' - notification."; - } - - identity dscp-unavailable { - base establish-subscription-error; - if-feature "dscp"; - description - "The publisher is unable to mark notification messages with - prioritization information in a way that will be respected - during network transit."; - } - - identity encoding-unsupported { - base establish-subscription-error; - description - "Unable to encode notification messages in the desired - format."; - } - - identity filter-unavailable { - base subscription-terminated-reason; - description - "Referenced filter does not exist. This means a receiver is - referencing a filter that doesn't exist or to which it - does not have access permissions."; - } - - identity filter-unsupported { - base establish-subscription-error; - base modify-subscription-error; - description - "Cannot parse syntax in the filter. This failure can be from - a syntax error or a syntax too complex to be processed by the - publisher."; - } - - identity insufficient-resources { - base establish-subscription-error; - base modify-subscription-error; - base subscription-suspended-reason; - description - "The publisher does not have sufficient resources to support - the requested subscription. An example might be that - allocated CPU is too limited to generate the desired set of - notification messages."; - } - - identity no-such-subscription { - base modify-subscription-error; - base delete-subscription-error; - base subscription-terminated-reason; - description - "Referenced subscription doesn't exist. This may be as a - result of a nonexistent subscription ID, an ID that belongs to - another subscriber, or an ID for a configured subscription."; - } - - identity replay-unsupported { - base establish-subscription-error; - if-feature "replay"; - description - "Replay cannot be performed for this subscription. This means - the publisher will not provide the requested historic - information from the event stream via replay to this - receiver."; - } - - identity stream-unavailable { - base subscription-terminated-reason; - description - "Not a subscribable event stream. This means the referenced - event stream is not available for subscription by the - receiver."; - } - - identity suspension-timeout { - base subscription-terminated-reason; - description - "Termination of a previously suspended subscription. The - publisher has eliminated the subscription, as it exceeded a - time limit for suspension."; - } - - identity unsupportable-volume { - base subscription-suspended-reason; - description - "The publisher does not have the network bandwidth needed to - get the volume of generated information intended for a - receiver."; - } - - /* Identities for encodings */ - - identity configurable-encoding { - description - "If a transport identity derives from this identity, it means - that it supports configurable encodings. An example of a - configurable encoding might be a new identity such as - 'encode-cbor'. Such an identity could use - 'configurable-encoding' as its base. This would allow a - dynamic subscription encoded in JSON (RFC 8259) to request - that notification messages be encoded via the Concise Binary - Object Representation (CBOR) (RFC 7049). Further details for - any specific configurable encoding would be explored in a - transport document based on this specification."; - reference - "RFC 8259: The JavaScript Object Notation (JSON) Data - Interchange Format - RFC 7049: Concise Binary Object Representation (CBOR)"; - } - - identity encoding { - description - "Base identity to represent data encodings."; - } - - identity encode-xml { - base encoding; - if-feature "encode-xml"; - description - "Encode data using XML as described in RFC 7950."; - reference - "RFC 7950: The YANG 1.1 Data Modeling Language"; - } - - identity encode-json { - base encoding; - if-feature "encode-json"; - description - "Encode data using JSON as described in RFC 7951."; - reference - "RFC 7951: JSON Encoding of Data Modeled with YANG"; - } - - /* Identities for transports */ - - identity transport { - description - "An identity that represents the underlying mechanism for - passing notification messages."; - } - - /* - * TYPEDEFs - */ - - typedef encoding { - type identityref { - base encoding; - } - description - "Specifies a data encoding, e.g., for a data subscription."; - } - - typedef stream-filter-ref { - type leafref { - path "/sn:filters/sn:stream-filter/sn:name"; - } - description - "This type is used to reference an event stream filter."; - } - - typedef stream-ref { - type leafref { - path "/sn:streams/sn:stream/sn:name"; - } - description - "This type is used to reference a system-provided - event stream."; - } - - typedef subscription-id { - type uint32; - description - "A type for subscription identifiers."; - } - - typedef transport { - type identityref { - base transport; - } - description - "Specifies the transport used to send notification messages - to a receiver."; - } - - /* - * GROUPINGS - */ - - grouping stream-filter-elements { - description - "This grouping defines the base for filters applied to event - streams."; - choice filter-spec { - description - "The content filter specification for this request."; - anydata stream-subtree-filter { - if-feature "subtree"; - description - "Event stream evaluation criteria encoded in the syntax of - a subtree filter as defined in RFC 6241, Section 6. - - The subtree filter is applied to the representation of - individual, delineated event records as contained in the - event stream. - - If the subtree filter returns a non-empty node set, the - filter matches the event record, and the event record is - included in the notification message sent to the - receivers."; - reference - "RFC 6241: Network Configuration Protocol (NETCONF), - Section 6"; - } - leaf stream-xpath-filter { - if-feature "xpath"; - type yang:xpath1.0; - description - "Event stream evaluation criteria encoded in the syntax of - an XPath 1.0 expression. - - The XPath expression is evaluated on the representation of - individual, delineated event records as contained in - the event stream. - - The result of the XPath expression is converted to a - boolean value using the standard XPath 1.0 rules. If the - boolean value is 'true', the filter matches the event - record, and the event record is included in the - notification message sent to the receivers. - - The expression is evaluated in the following XPath - context: - - o The set of namespace declarations is the set of - prefix and namespace pairs for all YANG modules - implemented by the server, where the prefix is the - YANG module name and the namespace is as defined by - the 'namespace' statement in the YANG module. - - If the leaf is encoded in XML, all namespace - declarations in scope on the 'stream-xpath-filter' - leaf element are added to the set of namespace - declarations. If a prefix found in the XML is - already present in the set of namespace - declarations, the namespace in the XML is used. - - o The set of variable bindings is empty. - - o The function library is comprised of the core - function library and the XPath functions defined in - Section 10 in RFC 7950. - - o The context node is the root node."; - reference - "XML Path Language (XPath) Version 1.0 - (https://www.w3.org/TR/1999/REC-xpath-19991116) - RFC 7950: The YANG 1.1 Data Modeling Language, - Section 10"; - } - } - } - - grouping update-qos { - description - "This grouping describes QoS information concerning a - subscription. This information is passed to lower layers - for transport prioritization and treatment."; - leaf dscp { - if-feature "dscp"; - type inet:dscp; - default "0"; - description - "The desired network transport priority level. This is the - priority set on notification messages encapsulating the - results of the subscription. This transport priority is - shared for all receivers of a given subscription."; - } - leaf weighting { - if-feature "qos"; - type uint8 { - range "0 .. 255"; - } - description - "Relative weighting for a subscription. Larger weights get - more resources. Allows an underlying transport layer to - perform informed load-balance allocations between various - subscriptions."; - reference - "RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2), - Section 5.3.2"; - } - leaf dependency { - if-feature "qos"; - type subscription-id; - description - "Provides the 'subscription-id' of a parent subscription. - The parent subscription has absolute precedence should - that parent have push updates ready to egress the publisher. - In other words, there should be no streaming of objects from - the current subscription if the parent has something ready - to push. - - If a dependency is asserted via configuration or via an RPC - but the referenced 'subscription-id' does not exist, the - dependency is silently discarded. If a referenced - subscription is deleted, this dependency is removed."; - reference - "RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2), - Section 5.3.1"; - } - } - - grouping subscription-policy-modifiable { - description - "This grouping describes all objects that may be changed - in a subscription."; - choice target { - mandatory true; - description - "Identifies the source of information against which a - subscription is being applied as well as specifics on the - subset of information desired from that source."; - case stream { - choice stream-filter { - description - "An event stream filter can be applied to a subscription. - That filter will either come referenced from a global - list or be provided in the subscription itself."; - case by-reference { - description - "Apply a filter that has been configured separately."; - leaf stream-filter-name { - type stream-filter-ref; - mandatory true; - description - "References an existing event stream filter that is - to be applied to an event stream for the - subscription."; - } - } - case within-subscription { - description - "A local definition allows a filter to have the same - lifecycle as the subscription."; - uses stream-filter-elements; - } - } - } - } - leaf stop-time { - type yang:date-and-time; - description - "Identifies a time after which notification messages for a - subscription should not be sent. If 'stop-time' is not - present, the notification messages will continue until the - subscription is terminated. If 'replay-start-time' exists, - 'stop-time' must be for a subsequent time. If - 'replay-start-time' doesn't exist, 'stop-time', when - established, must be for a future time."; - } - } - - grouping subscription-policy-dynamic { - description - "This grouping describes the only information concerning a - subscription that can be passed over the RPCs defined in this - data model."; - uses subscription-policy-modifiable { - augment "target/stream" { - description - "Adds additional objects that can be modified by an RPC."; - leaf stream { - type stream-ref { - require-instance false; - } - mandatory true; - description - "Indicates the event stream to be considered for - this subscription."; - } - leaf replay-start-time { - if-feature "replay"; - type yang:date-and-time; - config false; - description - "Used to trigger the 'replay' feature for a dynamic - subscription, where event records that are selected - need to be at or after the specified starting time. If - 'replay-start-time' is not present, this is not a replay - subscription and event record push should start - immediately. It is never valid to specify start times - that are later than or equal to the current time."; - } - } - } - uses update-qos; - } - - grouping subscription-policy { - description - "This grouping describes the full set of policy information - concerning both dynamic and configured subscriptions, with the - exclusion of both receivers and networking information - specific to the publisher, such as what interface should be - used to transmit notification messages."; - uses subscription-policy-dynamic; - leaf transport { - if-feature "configured"; - type transport; - description - "For a configured subscription, this leaf specifies the - transport used to deliver messages destined for all - receivers of that subscription."; - } - leaf encoding { - when 'not(../transport) or derived-from(../transport, - "sn:configurable-encoding")'; - type encoding; - description - "The type of encoding for notification messages. For a - dynamic subscription, if not included as part of an - 'establish-subscription' RPC, the encoding will be populated - with the encoding used by that RPC. For a configured - subscription, if not explicitly configured, the encoding - will be the default encoding for an underlying transport."; - } - leaf purpose { - if-feature "configured"; - type string; - description - "Open text allowing a configuring entity to embed the - originator or other specifics of this subscription."; - } - } - - /* - * RPCs - */ - - rpc establish-subscription { - description - "This RPC allows a subscriber to create (and possibly - negotiate) a subscription on its own behalf. If successful, - the subscription remains in effect for the duration of the - subscriber's association with the publisher or until the - subscription is terminated. If an error occurs or the - publisher cannot meet the terms of a subscription, an RPC - error is returned, and the subscription is not created. - In that case, the RPC reply's 'error-info' MAY include - suggested parameter settings that would have a higher - likelihood of succeeding in a subsequent - 'establish-subscription' request."; - input { - uses subscription-policy-dynamic; - leaf encoding { - type encoding; - description - "The type of encoding for the subscribed data. If not - included as part of the RPC, the encoding MUST be set by - the publisher to be the encoding used by this RPC."; - } - } - output { - leaf id { - type subscription-id; - mandatory true; - description - "Identifier used for this subscription."; - } - leaf replay-start-time-revision { - if-feature "replay"; - type yang:date-and-time; - description - "If a replay has been requested, this object represents - the earliest time covered by the event buffer for the - requested event stream. The value of this object is the - 'replay-log-aged-time' if it exists. Otherwise, it is - the 'replay-log-creation-time'. All buffered event - records after this time will be replayed to a receiver. - This object will only be sent if the starting time has - been revised to be later than the time requested by the - subscriber."; - } - } - } - - rc:yang-data establish-subscription-stream-error-info { - container establish-subscription-stream-error-info { - description - "If any 'establish-subscription' RPC parameters are - unsupportable against the event stream, a subscription - is not created and the RPC error response MUST indicate the - reason why the subscription failed to be created. This - yang-data MAY be inserted as structured data in a - subscription's RPC error response to indicate the reason for - the failure. This yang-data MUST be inserted if hints are - to be provided back to the subscriber."; - leaf reason { - type identityref { - base establish-subscription-error; - } - description - "Indicates the reason why the subscription has failed to - be created to a targeted event stream."; - } - leaf filter-failure-hint { - type string; - description - "Information describing where and/or why a provided - filter was unsupportable for a subscription. The - syntax and semantics of this hint are - implementation specific."; - } - } - } - - rpc modify-subscription { - description - "This RPC allows a subscriber to modify a dynamic - subscription's parameters. If successful, the changed - subscription parameters remain in effect for the duration of - the subscription, until the subscription is again modified, or - until the subscription is terminated. In the case of an error - or an inability to meet the modified parameters, the - subscription is not modified and the original subscription - parameters remain in effect. In that case, the RPC error MAY - include 'error-info' suggested parameter hints that would have - a high likelihood of succeeding in a subsequent - 'modify-subscription' request. A successful - 'modify-subscription' will return a suspended subscription to - the 'active' state."; - input { - leaf id { - type subscription-id; - mandatory true; - description - "Identifier to use for this subscription."; - } - uses subscription-policy-modifiable; - } - } - - rc:yang-data modify-subscription-stream-error-info { - container modify-subscription-stream-error-info { - description - "This yang-data MAY be provided as part of a subscription's - RPC error response when there is a failure of a - 'modify-subscription' RPC that has been made against an - event stream. This yang-data MUST be used if hints are to - be provided back to the subscriber."; - leaf reason { - type identityref { - base modify-subscription-error; - } - description - "Information in a 'modify-subscription' RPC error response - that indicates the reason why the subscription to an event - stream has failed to be modified."; - } - leaf filter-failure-hint { - type string; - description - "Information describing where and/or why a provided - filter was unsupportable for a subscription. The syntax - and semantics of this hint are - implementation specific."; - } - } - } - - rpc delete-subscription { - description - "This RPC allows a subscriber to delete a subscription that - was previously created by that same subscriber using the - 'establish-subscription' RPC. - - If an error occurs, the server replies with an 'rpc-error' - where the 'error-info' field MAY contain a - 'delete-subscription-error-info' structure."; - input { - leaf id { - type subscription-id; - mandatory true; - description - "Identifier of the subscription that is to be deleted. - Only subscriptions that were created using - 'establish-subscription' from the same origin as this RPC - can be deleted via this RPC."; - } - } - } - - rpc kill-subscription { - nacm:default-deny-all; - description - "This RPC allows an operator to delete a dynamic subscription - without restrictions on the originating subscriber or - underlying transport session. - - If an error occurs, the server replies with an 'rpc-error' - where the 'error-info' field MAY contain a - 'delete-subscription-error-info' structure."; - input { - leaf id { - type subscription-id; - mandatory true; - description - "Identifier of the subscription that is to be deleted. - Only subscriptions that were created using - 'establish-subscription' can be deleted via this RPC."; - } - } - } - - rc:yang-data delete-subscription-error-info { - container delete-subscription-error-info { - description - "If a 'delete-subscription' RPC or a 'kill-subscription' RPC - fails, the subscription is not deleted and the RPC error - response MUST indicate the reason for this failure. This - yang-data MAY be inserted as structured data in a - subscription's RPC error response to indicate the reason - for the failure."; - leaf reason { - type identityref { - base delete-subscription-error; - } - mandatory true; - description - "Indicates the reason why the subscription has failed to be - deleted."; - } - } - } - - /* - * NOTIFICATIONS - */ - - notification replay-completed { - sn:subscription-state-notification; - if-feature "replay"; - description - "This notification is sent to indicate that all of the replay - notifications have been sent."; - leaf id { - type subscription-id; - mandatory true; - description - "This references the affected subscription."; - } - } - - notification subscription-completed { - sn:subscription-state-notification; - if-feature "configured"; - description - "This notification is sent to indicate that a subscription has - finished passing event records, as the 'stop-time' has been - reached."; - leaf id { - type subscription-id; - mandatory true; - description - "This references the gracefully completed subscription."; - } - } - - notification subscription-modified { - sn:subscription-state-notification; - description - "This notification indicates that a subscription has been - modified. Notification messages sent from this point on will - conform to the modified terms of the subscription. For - completeness, this subscription state change notification - includes both modified and unmodified aspects of a - subscription."; - leaf id { - type subscription-id; - mandatory true; - description - "This references the affected subscription."; - } - uses subscription-policy { - refine "target/stream/stream-filter/within-subscription" { - description - "Filter applied to the subscription. If the - 'stream-filter-name' is populated, the filter in the - subscription came from the 'filters' container. - Otherwise, it is populated in-line as part of the - subscription."; - } - } - } - - notification subscription-resumed { - sn:subscription-state-notification; - description - "This notification indicates that a subscription that had - previously been suspended has resumed. Notifications will - once again be sent. In addition, a 'subscription-resumed' - indicates that no modification of parameters has occurred - since the last time event records have been sent."; - leaf id { - type subscription-id; - mandatory true; - description - "This references the affected subscription."; - } - } - - notification subscription-started { - sn:subscription-state-notification; - if-feature "configured"; - description - "This notification indicates that a subscription has started - and notifications will now be sent."; - leaf id { - type subscription-id; - mandatory true; - description - "This references the affected subscription."; - } - uses subscription-policy { - refine "target/stream/replay-start-time" { - description - "Indicates the time that a replay is using for the - streaming of buffered event records. This will be - populated with the most recent of the following: - the event time of the previous event record sent to a - receiver, the 'replay-log-creation-time', the - 'replay-log-aged-time', or the most recent publisher - boot time."; - } - refine "target/stream/stream-filter/within-subscription" { - description - "Filter applied to the subscription. If the - 'stream-filter-name' is populated, the filter in the - subscription came from the 'filters' container. - Otherwise, it is populated in-line as part of the - subscription."; - } - augment "target/stream" { - description - "This augmentation adds additional parameters specific to a - 'subscription-started' notification."; - leaf replay-previous-event-time { - when '../replay-start-time'; - if-feature "replay"; - type yang:date-and-time; - description - "If there is at least one event in the replay buffer - prior to 'replay-start-time', this gives the time of - the event generated immediately prior to the - 'replay-start-time'. - - If a receiver previously received event records for - this configured subscription, it can compare this time - to the last event record previously received. If the - two are not the same (perhaps due to a reboot), then a - dynamic replay can be initiated to acquire any missing - event records."; - } - } - } - } - - notification subscription-suspended { - sn:subscription-state-notification; - description - "This notification indicates that a suspension of the - subscription by the publisher has occurred. No further - notifications will be sent until the subscription resumes. - This notification shall only be sent to receivers of a - subscription; it does not constitute a general-purpose - notification."; - leaf id { - type subscription-id; - mandatory true; - description - "This references the affected subscription."; - } - leaf reason { - type identityref { - base subscription-suspended-reason; - } - mandatory true; - description - "Identifies the condition that resulted in the suspension."; - } - } - - notification subscription-terminated { - sn:subscription-state-notification; - description - "This notification indicates that a subscription has been - terminated."; - leaf id { - type subscription-id; - mandatory true; - description - "This references the affected subscription."; - } - leaf reason { - type identityref { - base subscription-terminated-reason; - } - mandatory true; - description - "Identifies the condition that resulted in the termination."; - } - } - - /* - * DATA NODES - */ - - container streams { - config false; - description - "Contains information on the built-in event streams provided by - the publisher."; - list stream { - key "name"; - description - "Identifies the built-in event streams that are supported by - the publisher."; - leaf name { - type string; - description - "A handle for a system-provided event stream made up of a - sequential set of event records, each of which is - characterized by its own domain and semantics."; - } - leaf description { - type string; - description - "A description of the event stream, including such - information as the type of event records that are - available in this event stream."; - } - leaf replay-support { - if-feature "replay"; - type empty; - description - "Indicates that event record replay is available on this - event stream."; - } - leaf replay-log-creation-time { - when '../replay-support'; - if-feature "replay"; - type yang:date-and-time; - mandatory true; - description - "The timestamp of the creation of the log used to support - the replay function on this event stream. This time - might be earlier than the earliest available information - contained in the log. This object is updated if the log - resets for some reason."; - } - leaf replay-log-aged-time { - when '../replay-support'; - if-feature "replay"; - type yang:date-and-time; - description - "The timestamp associated with the last event record that - has been aged out of the log. This timestamp identifies - how far back in history this replay log extends, if it - doesn't extend back to the 'replay-log-creation-time'. - This object MUST be present if replay is supported and any - event records have been aged out of the log."; - } - } - } - container filters { - description - "Contains a list of configurable filters that can be applied to - subscriptions. This facilitates the reuse of complex filters - once defined."; - list stream-filter { - key "name"; - description - "A list of preconfigured filters that can be applied to - subscriptions."; - leaf name { - type string; - description - "A name to differentiate between filters."; - } - uses stream-filter-elements; - } - } - container subscriptions { - description - "Contains the list of currently active subscriptions, i.e., - subscriptions that are currently in effect, used for - subscription management and monitoring purposes. This - includes subscriptions that have been set up via - RPC primitives as well as subscriptions that have been - established via configuration."; - list subscription { - key "id"; - description - "The identity and specific parameters of a subscription. - Subscriptions in this list can be created using a control - channel or RPC or can be established through configuration. - - If the 'kill-subscription' RPC or configuration operations - are used to delete a subscription, a - 'subscription-terminated' message is sent to any active or - suspended receivers."; - leaf id { - type subscription-id; - description - "Identifier of a subscription; unique in a given - publisher."; - } - uses subscription-policy { - refine "target/stream/stream" { - description - "Indicates the event stream to be considered for this - subscription. If an event stream has been removed - and can no longer be referenced by an active - subscription, send a 'subscription-terminated' - notification with 'stream-unavailable' as the reason. - If a configured subscription refers to a nonexistent - event stream, move that subscription to the - 'invalid' state."; - } - refine "transport" { - description - "For a configured subscription, this leaf specifies the - transport used to deliver messages destined for all - receivers of that subscription. This object is - mandatory for subscriptions in the configuration - datastore. This object (1) is not mandatory for dynamic - subscriptions in the operational state datastore and - (2) should not be present for other types of dynamic - subscriptions."; - } - augment "target/stream" { - description - "Enables objects to be added to a configured stream - subscription."; - leaf configured-replay { - if-feature "configured"; - if-feature "replay"; - type empty; - description - "The presence of this leaf indicates that replay for - the configured subscription should start at the - earliest time in the event log or at the publisher - boot time, whichever is later."; - } - } - } - choice notification-message-origin { - if-feature "configured"; - description - "Identifies the egress interface on the publisher - from which notification messages are to be sent."; - case interface-originated { - description - "When notification messages are to egress a specific, - designated interface on the publisher."; - leaf source-interface { - if-feature "interface-designation"; - type if:interface-ref; - description - "References the interface for notification messages."; - } - } - case address-originated { - description - "When notification messages are to depart from a - publisher using a specific originating address and/or - routing context information."; - leaf source-vrf { - if-feature "supports-vrf"; - type leafref { - path "/ni:network-instances/ni:network-instance/ni:name"; - } - description - "VRF from which notification messages should egress a - publisher."; - } - leaf source-address { - type inet:ip-address-no-zone; - description - "The source address for the notification messages. - If a source VRF exists but this object doesn't, a - publisher's default address for that VRF must - be used."; - } - } - } - leaf configured-subscription-state { - if-feature "configured"; - type enumeration { - enum valid { - value 1; - description - "The subscription is supportable with its current - parameters."; - } - enum invalid { - value 2; - description - "The subscription as a whole is unsupportable with its - current parameters."; - } - enum concluded { - value 3; - description - "A subscription is inactive, as it has hit a - stop time. It no longer has receivers in the - 'active' or 'suspended' state, but the subscription - has not yet been removed from configuration."; - } - } - config false; - description - "The presence of this leaf indicates that the subscription - originated from configuration, not through a control - channel or RPC. The value indicates the state of the - subscription as established by the publisher."; - } - container receivers { - description - "Set of receivers in a subscription."; - list receiver { - key "name"; - min-elements 1; - description - "A host intended as a recipient for the notification - messages of a subscription. For configured - subscriptions, transport-specific network parameters - (or a leafref to those parameters) may be augmented to a - specific receiver in this list."; - leaf name { - type string; - description - "Identifies a unique receiver for a subscription."; - } - leaf sent-event-records { - type yang:zero-based-counter64; - config false; - description - "The number of event records sent to the receiver. The - count is initialized when a dynamic subscription is - established or when a configured receiver - transitions to the 'valid' state."; - } - leaf excluded-event-records { - type yang:zero-based-counter64; - config false; - description - "The number of event records explicitly removed via - either an event stream filter or an access control - filter so that they are not passed to a receiver. - This count is set to zero each time - 'sent-event-records' is initialized."; - } - leaf state { - type enumeration { - enum active { - value 1; - description - "The receiver is currently being sent any - applicable notification messages for the - subscription."; - } - enum suspended { - value 2; - description - "The receiver state is 'suspended', so the - publisher is currently unable to provide - notification messages for the subscription."; - } - enum connecting { - value 3; - if-feature "configured"; - description - "A subscription has been configured, but a - 'subscription-started' subscription state change - notification needs to be successfully received - before notification messages are sent. - - If the 'reset' action is invoked for a receiver of - an active configured subscription, the state - must be moved to 'connecting'."; - } - enum disconnected { - value 4; - if-feature "configured"; - description - "A subscription has failed to send a - 'subscription-started' state change to the - receiver. Additional connection attempts are not - currently being made."; - } - } - config false; - mandatory true; - description - "Specifies the state of a subscription from the - perspective of a particular receiver. With this - information, it is possible to determine whether a - publisher is currently generating notification - messages intended for that receiver."; - } - action reset { - if-feature "configured"; - description - "Allows the reset of this configured subscription's - receiver to the 'connecting' state. This enables the - connection process to be reinitiated."; - output { - leaf time { - type yang:date-and-time; - mandatory true; - description - "Time at which a publisher returned the receiver to - the 'connecting' state."; - } - } - } - } - } - } - } -} diff --git a/src/confd/yang/rousette/ietf-yang-patch@2017-02-22.yang b/src/confd/yang/rousette/ietf-yang-patch@2017-02-22.yang deleted file mode 100644 index d0029ed2..00000000 --- a/src/confd/yang/rousette/ietf-yang-patch@2017-02-22.yang +++ /dev/null @@ -1,390 +0,0 @@ -module ietf-yang-patch { - yang-version 1.1; - namespace "urn:ietf:params:xml:ns:yang:ietf-yang-patch"; - prefix "ypatch"; - - import ietf-restconf { prefix rc; } - - organization - "IETF NETCONF (Network Configuration) Working Group"; - - contact - "WG Web: - WG List: - - Author: Andy Bierman - - - Author: Martin Bjorklund - - - Author: Kent Watsen - "; - - description - "This module contains conceptual YANG specifications - for the YANG Patch and YANG Patch Status data structures. - - Note that the YANG definitions within this module do not - represent configuration data of any kind. - The YANG grouping statements provide a normative syntax - for XML and JSON message-encoding purposes. - - Copyright (c) 2017 IETF Trust and the persons identified as - authors of the code. All rights reserved. - - Redistribution and use in source and binary forms, with or - without modification, is permitted pursuant to, and subject - to the license terms contained in, the Simplified BSD License - set forth in Section 4.c of the IETF Trust's Legal Provisions - Relating to IETF Documents - (http://trustee.ietf.org/license-info). - - This version of this YANG module is part of RFC 8072; see - the RFC itself for full legal notices."; - - revision 2017-02-22 { - description - "Initial revision."; - reference - "RFC 8072: YANG Patch Media Type."; - } - - typedef target-resource-offset { - type string; - description - "Contains a data resource identifier string representing - a sub-resource within the target resource. - The document root for this expression is the - target resource that is specified in the - protocol operation (e.g., the URI for the PATCH request). - - This string is encoded according to the same rules as those - for a data resource identifier in a RESTCONF request URI."; - reference - "RFC 8040, Section 3.5.3."; - } - - rc:yang-data "yang-patch" { - uses yang-patch; - } - - rc:yang-data "yang-patch-status" { - uses yang-patch-status; - } - - grouping yang-patch { - - description - "A grouping that contains a YANG container representing the - syntax and semantics of a YANG Patch edit request message."; - - container yang-patch { - description - "Represents a conceptual sequence of datastore edits, - called a patch. Each patch is given a client-assigned - patch identifier. Each edit MUST be applied - in ascending order, and all edits MUST be applied. - If any errors occur, then the target datastore MUST NOT - be changed by the YANG Patch operation. - - It is possible for a datastore constraint violation to occur - due to any node in the datastore, including nodes not - included in the 'edit' list. Any validation errors MUST - be reported in the reply message."; - - reference - "RFC 7950, Section 8.3."; - - leaf patch-id { - type string; - mandatory true; - description - "An arbitrary string provided by the client to identify - the entire patch. Error messages returned by the server - that pertain to this patch will be identified by this - 'patch-id' value. A client SHOULD attempt to generate - unique 'patch-id' values to distinguish between - transactions from multiple clients in any audit logs - maintained by the server."; - } - - leaf comment { - type string; - description - "An arbitrary string provided by the client to describe - the entire patch. This value SHOULD be present in any - audit logging records generated by the server for the - patch."; - } - - list edit { - key edit-id; - ordered-by user; - - description - "Represents one edit within the YANG Patch request message. - The 'edit' list is applied in the following manner: - - - The first edit is conceptually applied to a copy - of the existing target datastore, e.g., the - running configuration datastore. - - Each ascending edit is conceptually applied to - the result of the previous edit(s). - - After all edits have been successfully processed, - the result is validated according to YANG constraints. - - If successful, the server will attempt to apply - the result to the target datastore."; - - leaf edit-id { - type string; - description - "Arbitrary string index for the edit. - Error messages returned by the server that pertain - to a specific edit will be identified by this value."; - } - - leaf operation { - type enumeration { - enum create { - description - "The target data node is created using the supplied - value, only if it does not already exist. The - 'target' leaf identifies the data node to be - created, not the parent data node."; - } - enum delete { - description - "Delete the target node, only if the data resource - currently exists; otherwise, return an error."; - } - - enum insert { - description - "Insert the supplied value into a user-ordered - list or leaf-list entry. The target node must - represent a new data resource. If the 'where' - parameter is set to 'before' or 'after', then - the 'point' parameter identifies the insertion - point for the target node."; - } - enum merge { - description - "The supplied value is merged with the target data - node."; - } - enum move { - description - "Move the target node. Reorder a user-ordered - list or leaf-list. The target node must represent - an existing data resource. If the 'where' parameter - is set to 'before' or 'after', then the 'point' - parameter identifies the insertion point to move - the target node."; - } - enum replace { - description - "The supplied value is used to replace the target - data node."; - } - enum remove { - description - "Delete the target node if it currently exists."; - } - } - mandatory true; - description - "The datastore operation requested for the associated - 'edit' entry."; - } - - leaf target { - type target-resource-offset; - mandatory true; - description - "Identifies the target data node for the edit - operation. If the target has the value '/', then - the target data node is the target resource. - The target node MUST identify a data resource, - not the datastore resource."; - } - - leaf point { - when "(../operation = 'insert' or ../operation = 'move')" - + "and (../where = 'before' or ../where = 'after')" { - description - "This leaf only applies for 'insert' or 'move' - operations, before or after an existing entry."; - } - type target-resource-offset; - description - "The absolute URL path for the data node that is being - used as the insertion point or move point for the - target of this 'edit' entry."; - } - - leaf where { - when "../operation = 'insert' or ../operation = 'move'" { - description - "This leaf only applies for 'insert' or 'move' - operations."; - } - type enumeration { - enum before { - description - "Insert or move a data node before the data resource - identified by the 'point' parameter."; - } - enum after { - description - "Insert or move a data node after the data resource - identified by the 'point' parameter."; - } - - enum first { - description - "Insert or move a data node so it becomes ordered - as the first entry."; - } - enum last { - description - "Insert or move a data node so it becomes ordered - as the last entry."; - } - } - default last; - description - "Identifies where a data resource will be inserted - or moved. YANG only allows these operations for - list and leaf-list data nodes that are - 'ordered-by user'."; - } - - anydata value { - when "../operation = 'create' " - + "or ../operation = 'merge' " - + "or ../operation = 'replace' " - + "or ../operation = 'insert'" { - description - "The anydata 'value' is only used for 'create', - 'merge', 'replace', and 'insert' operations."; - } - description - "Value used for this edit operation. The anydata 'value' - contains the target resource associated with the - 'target' leaf. - - For example, suppose the target node is a YANG container - named foo: - - container foo { - leaf a { type string; } - leaf b { type int32; } - } - - The 'value' node contains one instance of foo: - - - - some value - 42 - - - "; - } - } - } - - } // grouping yang-patch - - grouping yang-patch-status { - - description - "A grouping that contains a YANG container representing the - syntax and semantics of a YANG Patch Status response - message."; - - container yang-patch-status { - description - "A container representing the response message sent by the - server after a YANG Patch edit request message has been - processed."; - - leaf patch-id { - type string; - mandatory true; - description - "The 'patch-id' value used in the request."; - } - - choice global-status { - description - "Report global errors or complete success. - If there is no case selected, then errors - are reported in the 'edit-status' container."; - - case global-errors { - uses rc:errors; - description - "This container will be present if global errors that - are unrelated to a specific edit occurred."; - } - leaf ok { - type empty; - description - "This leaf will be present if the request succeeded - and there are no errors reported in the 'edit-status' - container."; - } - } - - container edit-status { - description - "This container will be present if there are - edit-specific status responses to report. - If all edits succeeded and the 'global-status' - returned is 'ok', then a server MAY omit this - container."; - - list edit { - key edit-id; - - description - "Represents a list of status responses, - corresponding to edits in the YANG Patch - request message. If an 'edit' entry was - skipped or not reached by the server, - then this list will not contain a corresponding - entry for that edit."; - - leaf edit-id { - type string; - description - "Response status is for the 'edit' list entry - with this 'edit-id' value."; - } - - choice edit-status-choice { - description - "A choice between different types of status - responses for each 'edit' entry."; - leaf ok { - type empty; - description - "This 'edit' entry was invoked without any - errors detected by the server associated - with this edit."; - } - case errors { - uses rc:errors; - description - "The server detected errors associated with the - edit identified by the same 'edit-id' value."; - } - } - } - } - } - } // grouping yang-patch-status - -} diff --git a/src/confd/yang/sysrepo.inc b/src/confd/yang/sysrepo.inc index 7728fedf..fdf667cf 100644 --- a/src/confd/yang/sysrepo.inc +++ b/src/confd/yang/sysrepo.inc @@ -2,7 +2,7 @@ # Modules from sysrepo MODULES=( - "sysrepo@2024-10-25.yang" + "sysrepo@2025-04-04.yang" "sysrepo-factory-default@2025-03-18.yang" "sysrepo-monitoring@2023-08-11.yang" "sysrepo-notifications@2025-01-10.yang" diff --git a/src/libsrx/src/srx_val.c b/src/libsrx/src/srx_val.c index ac820458..6ac1fcd2 100644 --- a/src/libsrx/src/srx_val.c +++ b/src/libsrx/src/srx_val.c @@ -149,7 +149,7 @@ bool srx_isset(sr_session_ctx_t *session, const char *fmt, ...) return isset; } -static int set_vaitem(sr_session_ctx_t *session, const sr_val_t *val, sr_edit_options_t opts, +static int set_vaitem(sr_session_ctx_t *session, const sr_val_t *val, uint32_t opts, const char *fmt, va_list ap) { va_list apdup; @@ -171,7 +171,7 @@ static int set_vaitem(sr_session_ctx_t *session, const sr_val_t *val, sr_edit_op return sr_set_item(session, xpath, val, opts); } -int srx_set_item(sr_session_ctx_t *session, const sr_val_t *val, sr_edit_options_t opts, +int srx_set_item(sr_session_ctx_t *session, const sr_val_t *val, uint32_t opts, const char *fmt, ...) { va_list ap; @@ -184,7 +184,7 @@ int srx_set_item(sr_session_ctx_t *session, const sr_val_t *val, sr_edit_options return rc; } -int srx_set_bool(sr_session_ctx_t *session, bool ena, sr_edit_options_t opts, +int srx_set_bool(sr_session_ctx_t *session, bool ena, uint32_t opts, const char *fmt, ...) { sr_val_t val = { @@ -202,7 +202,7 @@ int srx_set_bool(sr_session_ctx_t *session, bool ena, sr_edit_options_t opts, } -int srx_set_str(sr_session_ctx_t *session, const char *str, sr_edit_options_t opts, +int srx_set_str(sr_session_ctx_t *session, const char *str, uint32_t opts, const char *fmt, ...) { char *xpath; diff --git a/src/libsrx/src/srx_val.h b/src/libsrx/src/srx_val.h index 1b2d1ada..82378f0d 100644 --- a/src/libsrx/src/srx_val.h +++ b/src/libsrx/src/srx_val.h @@ -16,11 +16,11 @@ int srx_free_changes(struct lyd_node *tree); bool srx_isset(sr_session_ctx_t *session, const char *fmt, ...); -int srx_set_item(sr_session_ctx_t *, const sr_val_t *, sr_edit_options_t, const char *, ...) +int srx_set_item(sr_session_ctx_t *, const sr_val_t *, uint32_t, const char *, ...) __attribute__ ((format (printf, 4, 5))); -int srx_set_str(sr_session_ctx_t *, const char *, sr_edit_options_t, const char *fmt, ...) +int srx_set_str(sr_session_ctx_t *, const char *, uint32_t, const char *fmt, ...) __attribute__ ((format (printf, 4, 5))); -int srx_set_bool(sr_session_ctx_t *session, bool ena, sr_edit_options_t opts, const char *fmt, ...) +int srx_set_bool(sr_session_ctx_t *session, bool ena, uint32_t opts, const char *fmt, ...) __attribute__ ((format (printf, 4, 5))); char *srx_get_str (sr_session_ctx_t *session, const char *fmt, ...) diff --git a/src/statd/journal.c b/src/statd/journal.c index a869e13d..b515c1f1 100644 --- a/src/statd/journal.c +++ b/src/statd/journal.c @@ -81,7 +81,7 @@ static int create_snapshot(const struct lyd_node *tree) int ret; /* Write latest snapshot as uncompressed operational.json for easy access */ - ret = lyd_print_path(DUMP_FILE, tree, LYD_JSON, LYD_PRINT_WITHSIBLINGS); + ret = lyd_print_path(DUMP_FILE, tree, LYD_JSON, LYD_PRINT_SIBLINGS); if (ret != LY_SUCCESS) { ERROR("Error, writing operational.json: %d", ret); return -1;