diff --git a/buildroot b/buildroot index 2bf7726b..d1ff7600 160000 --- a/buildroot +++ b/buildroot @@ -1 +1 @@ -Subproject commit 2bf7726bb1ad67b9d96f407d67f1c25ba69cfc7f +Subproject commit d1ff76003648fcd659f96d31364ba844980b6abd diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 7219a726..a89d8945 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -9,6 +9,10 @@ All notable changes to the project are documented in this file. ### Changes - Upgrade Buildroot to 2025.02.6 (LTS) - Upgrade Linux kernel to 6.12.46 (LTS) +- Upgrade libyang to 3.13.5 +- Upgrade sysrepo to 3.7.11 +- Upgrade netopeer2 (NETCONF) to 2.4.5 +- Upgrade rousette (RESTCONF) to v2 - Add support for [Banana Pi R3][BPI-R3], a 7 port switch with 2 WiFi chip - Add neofetch system information tool for system introspection, issue #1143 - Add mtr and iperf3 network diagnostic tools, issue #1144 diff --git a/package/libyang-cpp/0001-Implement-SchemaNode-actionRpcs.patch b/package/libyang-cpp/0001-Implement-SchemaNode-actionRpcs.patch new file mode 100644 index 00000000..5dfc89b4 --- /dev/null +++ b/package/libyang-cpp/0001-Implement-SchemaNode-actionRpcs.patch @@ -0,0 +1,135 @@ +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()` +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +This function allows to access list of actions +within schema node. + +It is implemented same as `Module::actionRpcs` +to follow same approach, so there is no need to use +collections. + +Change-Id: Ie99908cfdd334433fd9ae6f1a909f196e61139fd +Signed-off-by: Jan Kundrát +Signed-off-by: Mattias Walström +--- + include/libyang-cpp/SchemaNode.hpp | 1 + + src/SchemaNode.cpp | 12 ++++++++++++ + tests/example_schema.hpp | 28 ++++++++++++++++++++++++++++ + tests/schema_node.cpp | 21 +++++++++++++++++++++ + 4 files changed, 62 insertions(+) + +diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp +index 0f1a4c4..9f49fd7 100644 +--- a/include/libyang-cpp/SchemaNode.hpp ++++ b/include/libyang-cpp/SchemaNode.hpp +@@ -79,6 +79,7 @@ public: + Collection siblings() const; + Collection immediateChildren() const; + std::vector extensionInstances() const; ++ std::vector actionRpcs() const; + + std::vector when() const; + +diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp +index 26b5099..ce24203 100644 +--- a/src/SchemaNode.cpp ++++ b/src/SchemaNode.cpp +@@ -117,6 +117,18 @@ Collection SchemaNode::immediateChildren() c + return c ? c->siblings() : Collection{nullptr, nullptr}; + } + ++/** ++ * @brief Returns a collection of action nodes (not RPC nodes) as SchemaNode ++ */ ++std::vector SchemaNode::actionRpcs() const ++{ ++ std::vector res; ++ for (auto action = reinterpret_cast(lysc_node_actions(m_node)); action; action = action->next) { ++ res.emplace_back(SchemaNode{action, m_ctx}); ++ } ++ return res; ++} ++ + /** + * Returns the YANG description of the node. + * +diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp +index 0d8acb9..02d3d74 100644 +--- a/tests/example_schema.hpp ++++ b/tests/example_schema.hpp +@@ -214,6 +214,34 @@ module example-schema { + } + + container bigTree { ++ action firstAction { ++ input { ++ leaf inputLeaf1 { ++ type string; ++ } ++ } ++ ++ output { ++ leaf outputLeaf1 { ++ type string; ++ } ++ } ++ } ++ ++ action secondAction { ++ input { ++ leaf inputLeaf2 { ++ type string; ++ } ++ } ++ ++ output { ++ leaf outputLeaf2 { ++ type string; ++ } ++ } ++ } ++ + container one { + leaf myLeaf { + type string; +diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp +index 0001377..65ef462 100644 +--- a/tests/schema_node.cpp ++++ b/tests/schema_node.cpp +@@ -544,6 +544,27 @@ TEST_CASE("SchemaNode") + REQUIRE(elem.extensionInstances()[2].argument() == "last-modified"); + } + ++ DOCTEST_SUBCASE("SchemaNode::actionRpcs") ++ { ++ DOCTEST_SUBCASE("no actions") ++ { ++ auto actions = ctx->findPath("/example-schema:presenceContainer").actionRpcs(); ++ REQUIRE(actions.size() == 0); ++ } ++ ++ DOCTEST_SUBCASE("two actions") ++ { ++ auto actions = ctx->findPath("/example-schema:bigTree").actionRpcs(); ++ REQUIRE(actions.size() == 2); ++ REQUIRE(actions[0].asActionRpc().name() == "firstAction"); ++ REQUIRE(actions[0].asActionRpc().input().child()->name() == "inputLeaf1"); ++ REQUIRE(actions[0].asActionRpc().output().child()->name() == "outputLeaf1"); ++ REQUIRE(actions[1].asActionRpc().name() == "secondAction"); ++ REQUIRE(actions[1].asActionRpc().input().child()->name() == "inputLeaf2"); ++ REQUIRE(actions[1].asActionRpc().output().child()->name() == "outputLeaf2"); ++ } ++ } ++ + DOCTEST_SUBCASE("SchemaNode::operator==") + { + auto a = ctx->findPath("/type_module:leafString"); +-- +2.43.0 + diff --git a/package/libyang-cpp/0001-added-support-for-RpcYang-in-Context-parseOp.patch b/package/libyang-cpp/0001-added-support-for-RpcYang-in-Context-parseOp.patch deleted file mode 100644 index e654e938..00000000 --- a/package/libyang-cpp/0001-added-support-for-RpcYang-in-Context-parseOp.patch +++ /dev/null @@ -1,82 +0,0 @@ -From d0f6422fee7a46fcb7445c88f499f61b3eb0ead0 Mon Sep 17 00:00:00 2001 -From: Adam Piecek -Date: Wed, 23 Oct 2024 14:37:09 +0200 -Subject: [PATCH 01/18] added support for RpcYang in Context::parseOp -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: I25182ea2d042be1e6e4246e18aee260cc032e547 -Signed-off-by: Mattias Walström ---- - src/Context.cpp | 6 ++++-- - tests/context.cpp | 21 +++++++++++++++++++++ - 2 files changed, 25 insertions(+), 2 deletions(-) - -diff --git a/src/Context.cpp b/src/Context.cpp -index b844bd9..287f8c8 100644 ---- a/src/Context.cpp -+++ b/src/Context.cpp -@@ -221,7 +221,8 @@ std::optional Context::parseExtData( - * - a NETCONF RPC, - * - a NETCONF notification, - * - a RESTCONF notification, -- * - a YANG notification. -+ * - a YANG notification, -+ * - a YANG RPC. - * - * Parsing any of these requires just the schema (which is available through the Context), and the textual payload. - * All the other information are encoded in the textual payload as per the standard. -@@ -243,6 +244,7 @@ ParsedOp Context::parseOp(const std::string& input, const DataFormat format, con - auto in = wrap_ly_in_new_memory(input); - - switch (opType) { -+ case OperationType::RpcYang: - case OperationType::RpcNetconf: - case OperationType::NotificationNetconf: - case OperationType::NotificationRestconf: -@@ -254,7 +256,7 @@ ParsedOp Context::parseOp(const std::string& input, const DataFormat format, con - ParsedOp res; - res.tree = tree ? std::optional{libyang::wrapRawNode(tree)} : std::nullopt; - -- if (opType == OperationType::NotificationYang) { -+ if ((opType == OperationType::NotificationYang) || (opType == OperationType::RpcYang)) { - res.op = op && tree ? std::optional{DataNode(op, res.tree->m_refs)} : std::nullopt; - } else { - res.op = op ? std::optional{libyang::wrapRawNode(op)} : std::nullopt; -diff --git a/tests/context.cpp b/tests/context.cpp -index 71ae873..11019eb 100644 ---- a/tests/context.cpp -+++ b/tests/context.cpp -@@ -464,6 +464,27 @@ TEST_CASE("context") - REQUIRE(data->findPath("/example-schema:leafInt8")->asTerm().valueStr() == "-43"); - } - -+ DOCTEST_SUBCASE("Context::parseOp") -+ { -+ DOCTEST_SUBCASE("RPC") -+ { -+ ctx->parseModule(example_schema, libyang::SchemaFormat::YANG); -+ std::string dataJson = R"({"example-schema:myRpc":{"inputLeaf":"str"}})"; -+ auto pop = ctx->parseOp(dataJson, libyang::DataFormat::JSON, libyang::OperationType::RpcYang); -+ REQUIRE(pop.op->schema().name() == "myRpc"); -+ REQUIRE(pop.tree->findPath("/example-schema:myRpc/inputLeaf")->asTerm().valueStr() == "str"); -+ } -+ DOCTEST_SUBCASE("action") -+ { -+ ctx->parseModule(example_schema, libyang::SchemaFormat::YANG); -+ std::string datajson = R"({"example-schema:person":[{"name":"john", "poke":{}}]})"; -+ auto pop = ctx->parseOp(datajson, libyang::DataFormat::JSON, libyang::OperationType::RpcYang); -+ REQUIRE(pop.op->schema().name() == "poke"); -+ REQUIRE(pop.tree->findPath("/example-schema:person[name='john']/poke")->schema().nodeType() == libyang::NodeType::Action); -+ } -+ } -+ -+ - DOCTEST_SUBCASE("Context::parseExt") - { - ctx->setSearchDir(TESTS_DIR / "yang"); --- -2.43.0 - diff --git a/package/libyang-cpp/0002-throw-when-lyd_validate_all-returns-error.patch b/package/libyang-cpp/0002-throw-when-lyd_validate_all-returns-error.patch deleted file mode 100644 index 949da45a..00000000 --- a/package/libyang-cpp/0002-throw-when-lyd_validate_all-returns-error.patch +++ /dev/null @@ -1,53 +0,0 @@ -From 7e015f3486bdbb54f1dcc2e2ce51102b1d623081 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Wed, 23 Oct 2024 12:52:24 +0200 -Subject: [PATCH 02/18] throw when lyd_validate_all returns error -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Bug: https://github.com/CESNET/libyang-cpp/issues/20 -Change-Id: I005a2f1b057978573a4046e7b4cc31d77e36fde3 -Signed-off-by: Mattias Walström ---- - src/DataNode.cpp | 4 +++- - tests/data_node.cpp | 7 +++++++ - 2 files changed, 10 insertions(+), 1 deletion(-) - -diff --git a/src/DataNode.cpp b/src/DataNode.cpp -index 51c86c8..2ef17f2 100644 ---- a/src/DataNode.cpp -+++ b/src/DataNode.cpp -@@ -1170,7 +1170,9 @@ void validateAll(std::optional& node, const std::optionalm_node : nullptr, nullptr, opts ? utils::toValidationOptions(*opts) : 0, nullptr); -+ auto ret = lyd_validate_all(node ? &node->m_node : nullptr, nullptr, opts ? utils::toValidationOptions(*opts) : 0, nullptr); -+ throwIfError(ret, "libyang:validateAll: lyd_validate_all failed"); -+ - if (!node->m_node) { - node = std::nullopt; - } -diff --git a/tests/data_node.cpp b/tests/data_node.cpp -index a23e4c2..8a2610e 100644 ---- a/tests/data_node.cpp -+++ b/tests/data_node.cpp -@@ -489,6 +489,13 @@ TEST_CASE("Data Node manipulation") - REQUIRE_THROWS_WITH_AS(libyang::validateAll(node, libyang::ValidationOptions::NoState), "validateAll: Node is not a unique reference", libyang::Error); - } - -+ DOCTEST_SUBCASE("validateAll throws on validation failure") -+ { -+ ctx.parseModule(type_module, libyang::SchemaFormat::YANG); -+ auto node = std::optional{ctx.newPath("/type_module:leafWithConfigFalse", "hi")}; -+ REQUIRE_THROWS_WITH_AS(libyang::validateAll(node, libyang::ValidationOptions::NoState), "libyang:validateAll: lyd_validate_all failed: LY_EVALID", libyang::ErrorWithCode); -+ } -+ - DOCTEST_SUBCASE("unlink") - { - auto root = ctx.parseData(data2, libyang::DataFormat::JSON); --- -2.43.0 - diff --git a/package/libyang-cpp/0003-remove-a-misleading-comment.patch b/package/libyang-cpp/0003-remove-a-misleading-comment.patch deleted file mode 100644 index 668666fa..00000000 --- a/package/libyang-cpp/0003-remove-a-misleading-comment.patch +++ /dev/null @@ -1,38 +0,0 @@ -From 490d8bb242d33213b948485f5b94c55e22cf86a6 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Thu, 21 Nov 2024 11:32:44 +0100 -Subject: [PATCH 03/18] remove a misleading comment -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -The whole intention within action's input/output handling here was to -put some emphasis on the fact that we aren't tracking the input/output -nodes directly. However, looking at all the other classes this is a bit -redundant, we're using a pattern like this all the time. Just drop the -comment. - -Change-Id: Ibd9bf9f1e83c650dda3bc43ef48e61dd6d95da5a -Signed-off-by: Mattias Walström ---- - src/SchemaNode.cpp | 3 --- - 1 file changed, 3 deletions(-) - -diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp -index 81e938f..9934cea 100644 ---- a/src/SchemaNode.cpp -+++ b/src/SchemaNode.cpp -@@ -640,9 +640,6 @@ bool List::isUserOrdered() const - */ - ActionRpcInput ActionRpc::input() const - { -- // I need a lysc_node* for ActionRpcInput, but m_node->input is a lysp_node_action_inout. lysp_node_action_inout is -- // still just a lysc_node, so I'll just convert to lysc_node. -- // This is not very pretty, but I don't want to introduce another member for ActionRpcInput and ActionRpcOutput. - return ActionRpcInput{reinterpret_cast(&reinterpret_cast(m_node)->input), m_ctx}; - } - --- -2.43.0 - diff --git a/package/libyang-cpp/0004-schema-improve-List-keys-not-to-use-std-move.patch b/package/libyang-cpp/0004-schema-improve-List-keys-not-to-use-std-move.patch deleted file mode 100644 index 58e57672..00000000 --- a/package/libyang-cpp/0004-schema-improve-List-keys-not-to-use-std-move.patch +++ /dev/null @@ -1,35 +0,0 @@ -From e1b17386cf61048d2fe27fffb3b763981a225f52 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= -Date: Wed, 27 Nov 2024 09:47:47 +0100 -Subject: [PATCH 04/18] schema: improve `List::keys()` not to use `std::move` -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -`List::keys()` used `std::move` while iterating over array of leafs. -This was solved without using `std::move`. - -Change-Id: I8cbf8780ecd8848e46c1de5d4123a08624536bba -Signed-off-by: Mattias Walström ---- - src/SchemaNode.cpp | 3 +-- - 1 file changed, 1 insertion(+), 2 deletions(-) - -diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp -index 9934cea..20e2aff 100644 ---- a/src/SchemaNode.cpp -+++ b/src/SchemaNode.cpp -@@ -593,8 +593,7 @@ std::vector List::keys() const - LY_LIST_FOR(list->child, elem) - { - if (lysc_is_key(elem)) { -- Leaf leaf(elem, m_ctx); -- res.emplace_back(std::move(leaf)); -+ res.emplace_back(Leaf(elem, m_ctx)); - } - } - --- -2.43.0 - diff --git a/package/libyang-cpp/0005-schema-make-leaf-list-s-default-statement-available.patch b/package/libyang-cpp/0005-schema-make-leaf-list-s-default-statement-available.patch deleted file mode 100644 index 9839dd9d..00000000 --- a/package/libyang-cpp/0005-schema-make-leaf-list-s-default-statement-available.patch +++ /dev/null @@ -1,124 +0,0 @@ -From 1102ecdcafbc9206f59b383769687e418557838e Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= -Date: Mon, 25 Nov 2024 15:54:02 +0100 -Subject: [PATCH 05/18] schema: make leaf-list's `default` statement available -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Make leaf-list's `default` statement available so that it can be -accessed if end-user requires reading schema nodes. - -`LeafList::defaultValuesStr()` returns array of canonized string default -values. - -Change-Id: Idc42cd877f1fd3d717d491d09c46b59492527bff -Signed-off-by: Mattias Walström ---- - include/libyang-cpp/SchemaNode.hpp | 1 + - src/SchemaNode.cpp | 17 +++++++++++++++++ - tests/context.cpp | 1 + - tests/example_schema.hpp | 8 ++++++++ - tests/schema_node.cpp | 7 +++++++ - 5 files changed, 34 insertions(+) - -diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp -index 83afc06..8ddf9be 100644 ---- a/include/libyang-cpp/SchemaNode.hpp -+++ b/include/libyang-cpp/SchemaNode.hpp -@@ -169,6 +169,7 @@ class LIBYANG_CPP_EXPORT LeafList : public SchemaNode { - public: - bool isMandatory() const; - types::Type valueType() const; -+ std::vector defaultValuesStr() const; - libyang::types::constraints::ListSize maxElements() const; - libyang::types::constraints::ListSize minElements() const; - std::optional units() const; -diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp -index 9934cea..95bc09b 100644 ---- a/src/SchemaNode.cpp -+++ b/src/SchemaNode.cpp -@@ -472,6 +472,23 @@ types::Type LeafList::valueType() const - return types::Type{reinterpret_cast(m_node)->type, typeParsed, m_ctx}; - } - -+/** -+ * @brief Retrieves the default string values for this leaf-list. -+ * -+ * @return The default values, or an empty vector if the leaf-list does not have default values. -+ * -+ * Wraps `lysc_node_leaflist::dflts`. -+ */ -+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)); -+ } -+ return res; -+} -+ - /** - * @brief Retrieves the units for this leaf. - * @return The units, or std::nullopt if no units are available. -diff --git a/tests/context.cpp b/tests/context.cpp -index 11019eb..902faf6 100644 ---- a/tests/context.cpp -+++ b/tests/context.cpp -@@ -733,6 +733,7 @@ TEST_CASE("context") - +--rw iid-valid? instance-identifier - +--rw iid-relaxed? instance-identifier - +--rw leafListBasic* string -+ +--rw leafListWithDefault* int32 - +--rw leafListWithMinMaxElements* int32 - +--rw leafListWithUnits* int32 - +--rw listBasic* [primary-key] -diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp -index c093f50..2861b1a 100644 ---- a/tests/example_schema.hpp -+++ b/tests/example_schema.hpp -@@ -525,6 +525,14 @@ module type_module { - ordered-by user; - } - -+ leaf-list leafListWithDefault { -+ type int32; -+ default -1; -+ default +512; -+ default 0x400; -+ default 04000; -+ } -+ - leaf-list leafListWithMinMaxElements { - type int32; - min-elements 1; -diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp -index a86900d..80c7407 100644 ---- a/tests/schema_node.cpp -+++ b/tests/schema_node.cpp -@@ -200,6 +200,7 @@ TEST_CASE("SchemaNode") - "/type_module:iid-valid", - "/type_module:iid-relaxed", - "/type_module:leafListBasic", -+ "/type_module:leafListWithDefault", - "/type_module:leafListWithMinMaxElements", - "/type_module:leafListWithUnits", - "/type_module:listBasic", -@@ -606,6 +607,12 @@ TEST_CASE("SchemaNode") - REQUIRE(!ctx->findPath("/type_module:leafListBasic").asLeafList().isMandatory()); - } - -+ DOCTEST_SUBCASE("LeafList::defaultValuesStr") -+ { -+ REQUIRE(ctx->findPath("/type_module:leafListWithDefault").asLeafList().defaultValuesStr() == std::vector{"-1", "512", "1024", "2048"}); -+ REQUIRE(ctx->findPath("/type_module:leafListBasic").asLeafList().defaultValuesStr().size() == 0); -+ } -+ - DOCTEST_SUBCASE("LeafList::maxElements") - { - REQUIRE(ctx->findPath("/type_module:leafListWithMinMaxElements").asLeafList().maxElements() == 5); --- -2.43.0 - diff --git a/package/libyang-cpp/0006-schema-Make-choice-and-case-statements-available.patch b/package/libyang-cpp/0006-schema-Make-choice-and-case-statements-available.patch deleted file mode 100644 index c5c9009f..00000000 --- a/package/libyang-cpp/0006-schema-Make-choice-and-case-statements-available.patch +++ /dev/null @@ -1,434 +0,0 @@ -From 01f2633cef60495d5cafc4b4b1f25273b03ab3cd Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= -Date: Tue, 22 Oct 2024 15:11:30 +0200 -Subject: [PATCH 06/18] schema: Make choice and case statements available -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Make choice and case statements available so that they can be accessed -if end-user requires reading schema nodes. - -By design, choice and case statements do not exist in data tree directly. -Only children of one case can be present in the data tree at one time. -That means that choice and case children are not instantiable, thus -`SchemaNode::immediateChildren` must be used (instead of -`SchemaNode::childInstantibles`) if end-user wants to access choice -and case substatements. - -Change-Id: Ib089672ad21dda8a0344895835d92d3432fcccb8 -Co-authored-by: Jan Kundrát -Signed-off-by: Mattias Walström ---- - include/libyang-cpp/SchemaNode.hpp | 34 +++++++++++++ - src/SchemaNode.cpp | 68 ++++++++++++++++++++++++++ - tests/context.cpp | 77 +++++++++++++++++++----------- - tests/example_schema.hpp | 60 +++++++++++++++++++++++ - tests/schema_node.cpp | 72 ++++++++++++++++++++++++++++ - 5 files changed, 284 insertions(+), 27 deletions(-) - -diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp -index 8ddf9be..0f1a4c4 100644 ---- a/include/libyang-cpp/SchemaNode.hpp -+++ b/include/libyang-cpp/SchemaNode.hpp -@@ -22,6 +22,8 @@ class AnyDataAnyXML; - class ActionRpc; - class ActionRpcInput; - class ActionRpcOutput; -+class Case; -+class Choice; - class Container; - class Leaf; - class LeafList; -@@ -62,6 +64,8 @@ public: - // drectly by the user. - // TODO: turn these into a templated `as<>` method. - AnyDataAnyXML asAnyDataAnyXML() const; -+ Case asCase() const; -+ Choice asChoice() const; - Container asContainer() const; - Leaf asLeaf() const; - LeafList asLeafList() const; -@@ -129,6 +133,36 @@ private: - using SchemaNode::SchemaNode; - }; - -+/** -+ * @brief Class representing a schema definition of a `case` node. -+ * -+ * Wraps `lysc_node_case`. -+ */ -+class LIBYANG_CPP_EXPORT Case : public SchemaNode { -+public: -+ friend SchemaNode; -+ friend Choice; -+ -+private: -+ using SchemaNode::SchemaNode; -+}; -+ -+/** -+ * @brief Class representing a schema definition of a `choice` node. -+ * -+ * Wraps `lysc_node_choice`. -+ */ -+class LIBYANG_CPP_EXPORT Choice : public SchemaNode { -+public: -+ bool isMandatory() const; -+ std::vector cases() const; -+ std::optional defaultCase() const; -+ friend SchemaNode; -+ -+private: -+ using SchemaNode::SchemaNode; -+}; -+ - /** - * @brief Class representing a schema definition of a `container` node. - */ -diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp -index bd20402..26b5099 100644 ---- a/src/SchemaNode.cpp -+++ b/src/SchemaNode.cpp -@@ -191,6 +191,32 @@ NodeType SchemaNode::nodeType() const - return utils::toNodeType(m_node->nodetype); - } - -+/** -+ * @brief Try to cast this SchemaNode to a Case node. -+ * @throws Error If this node is not a case. -+ */ -+Case SchemaNode::asCase() const -+{ -+ if (nodeType() != NodeType::Case) { -+ throw Error("Schema node is not a case: " + path()); -+ } -+ -+ return Case{m_node, m_ctx}; -+} -+ -+/** -+ * @brief Try to cast this SchemaNode to a Choice node. -+ * @throws Error If this node is not a choice. -+ */ -+Choice SchemaNode::asChoice() const -+{ -+ if (nodeType() != NodeType::Choice) { -+ throw Error("Schema node is not a choice: " + path()); -+ } -+ -+ return Choice{m_node, m_ctx}; -+} -+ - /** - * @brief Try to cast this SchemaNode to a Container node. - * @throws Error If this node is not a container. -@@ -401,6 +427,48 @@ bool AnyDataAnyXML::isMandatory() const - return m_node->flags & LYS_MAND_TRUE; - } - -+/** -+ * @brief Checks whether this choice is mandatory. -+ * -+ * Wraps flag `LYS_MAND_TRUE`. -+ */ -+bool Choice::isMandatory() const -+{ -+ return m_node->flags & LYS_MAND_TRUE; -+} -+ -+/** -+ * @brief Retrieves the list of cases for this choice. -+ * -+ * Wraps `lysc_node_choice::cases`. -+ */ -+std::vector Choice::cases() const -+{ -+ auto choice = reinterpret_cast(m_node); -+ auto cases = reinterpret_cast(choice->cases); -+ std::vector res; -+ lysc_node* elem; -+ LY_LIST_FOR(cases, elem) -+ { -+ res.emplace_back(Case(elem, m_ctx)); -+ } -+ return res; -+} -+ -+/** -+ * @brief Retrieves the default case for this choice. -+ * -+ * Wraps `lysc_node_choice::dflt`. -+ */ -+std::optional Choice::defaultCase() const -+{ -+ auto choice = reinterpret_cast(m_node); -+ if (!choice->dflt) { -+ return std::nullopt; -+ } -+ return Case{reinterpret_cast(choice->dflt), m_ctx}; -+} -+ - /** - * @brief Checks whether this container is mandatory. - * -diff --git a/tests/context.cpp b/tests/context.cpp -index 902faf6..5929b75 100644 ---- a/tests/context.cpp -+++ b/tests/context.cpp -@@ -709,33 +709,56 @@ TEST_CASE("context") - auto mod = ctx_pp->parseModule(type_module, libyang::SchemaFormat::YANG); - - REQUIRE(mod.printStr(libyang::SchemaOutputFormat::Tree) == R"(module: type_module -- +--rw anydataBasic? anydata -- +--rw anydataWithMandatoryChild anydata -- +--rw anyxmlBasic? anyxml -- +--rw anyxmlWithMandatoryChild anyxml -- +--rw leafBinary? binary -- +--rw leafBits? bits -- +--rw leafEnum? enumeration -- +--rw leafEnum2? enumeration -- +--rw leafNumber? int32 -- +--rw leafRef? -> /custom-prefix:listAdvancedWithOneKey/lol -- +--rw leafRefRelaxed? -> /custom-prefix:listAdvancedWithOneKey/lol -- +--rw leafString? string -- +--rw leafUnion? union -- +--rw meal? identityref -- +--ro leafWithConfigFalse? string -- +--rw leafWithDefaultValue? string -- +--rw leafWithDescription? string -- +--rw leafWithMandatoryTrue string -- x--rw leafWithStatusDeprecated? string -- o--rw leafWithStatusObsolete? string -- +--rw leafWithUnits? int32 -- +--rw iid-valid? instance-identifier -- +--rw iid-relaxed? instance-identifier -- +--rw leafListBasic* string -- +--rw leafListWithDefault* int32 -- +--rw leafListWithMinMaxElements* int32 -- +--rw leafListWithUnits* int32 -+ +--rw anydataBasic? anydata -+ +--rw anydataWithMandatoryChild anydata -+ +--rw anyxmlBasic? anyxml -+ +--rw anyxmlWithMandatoryChild anyxml -+ +--rw choiceBasicContainer -+ | +--rw (choiceBasic)? -+ | +--:(case1) -+ | | +--rw l? string -+ | | +--rw ll* string -+ | +--:(case2) -+ | +--rw l2? string -+ +--rw choiceWithMandatoryContainer -+ | +--rw (choiceWithMandatory) -+ | +--:(case3) -+ | | +--rw l3? string -+ | +--:(case4) -+ | +--rw l4? string -+ +--rw choiceWithDefaultContainer -+ | +--rw (choiceWithDefault)? -+ | +--:(case5) -+ | | +--rw l5? string -+ | +--:(case6) -+ | +--rw l6? string -+ +--rw implicitCaseContainer -+ | +--rw (implicitCase)? -+ | +--:(implicitLeaf) -+ | +--rw implicitLeaf? string -+ +--rw leafBinary? binary -+ +--rw leafBits? bits -+ +--rw leafEnum? enumeration -+ +--rw leafEnum2? enumeration -+ +--rw leafNumber? int32 -+ +--rw leafRef? -> /custom-prefix:listAdvancedWithOneKey/lol -+ +--rw leafRefRelaxed? -> /custom-prefix:listAdvancedWithOneKey/lol -+ +--rw leafString? string -+ +--rw leafUnion? union -+ +--rw meal? identityref -+ +--ro leafWithConfigFalse? string -+ +--rw leafWithDefaultValue? string -+ +--rw leafWithDescription? string -+ +--rw leafWithMandatoryTrue string -+ x--rw leafWithStatusDeprecated? string -+ o--rw leafWithStatusObsolete? string -+ +--rw leafWithUnits? int32 -+ +--rw iid-valid? instance-identifier -+ +--rw iid-relaxed? instance-identifier -+ +--rw leafListBasic* string -+ +--rw leafListWithDefault* int32 -+ +--rw leafListWithMinMaxElements* int32 -+ +--rw leafListWithUnits* int32 - +--rw listBasic* [primary-key] - | +--rw primary-key string - +--rw listAdvancedWithOneKey* [lol] -diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp -index 2861b1a..ae3b4de 100644 ---- a/tests/example_schema.hpp -+++ b/tests/example_schema.hpp -@@ -390,6 +390,66 @@ module type_module { - mandatory true; - } - -+ container choiceBasicContainer { -+ choice choiceBasic { -+ case case1 { -+ leaf l { -+ type string; -+ } -+ leaf-list ll { -+ type string; -+ ordered-by user; -+ } -+ } -+ case case2 { -+ leaf l2 { -+ type string; -+ } -+ } -+ } -+ } -+ -+ container choiceWithMandatoryContainer { -+ choice choiceWithMandatory { -+ mandatory true; -+ case case3 { -+ leaf l3 { -+ type string; -+ } -+ } -+ case case4 { -+ leaf l4 { -+ type string; -+ } -+ } -+ } -+ } -+ -+ container choiceWithDefaultContainer { -+ choice choiceWithDefault { -+ default case5; -+ case case5 { -+ leaf l5 { -+ type string; -+ } -+ } -+ case case6 { -+ leaf l6 { -+ type string; -+ } -+ } -+ } -+ } -+ -+ container implicitCaseContainer { -+ choice implicitCase { -+ leaf implicitLeaf { -+ type string; -+ } -+ } -+ } -+ -+ - leaf leafBinary { - type binary; - } -diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp -index 80c7407..8d74bd2 100644 ---- a/tests/schema_node.cpp -+++ b/tests/schema_node.cpp -@@ -58,6 +58,9 @@ TEST_CASE("SchemaNode") - ], - "type_module:anydataWithMandatoryChild": {"content": "test-string"}, - "type_module:anyxmlWithMandatoryChild": {"content": "test-string"}, -+ "type_module:choiceWithMandatoryContainer": { -+ "l4": "test-string" -+ }, - "type_module:containerWithMandatoryChild": { - "leafWithMandatoryTrue": "test-string" - }, -@@ -180,6 +183,10 @@ TEST_CASE("SchemaNode") - "/type_module:anydataWithMandatoryChild", - "/type_module:anyxmlBasic", - "/type_module:anyxmlWithMandatoryChild", -+ "/type_module:choiceBasicContainer", -+ "/type_module:choiceWithMandatoryContainer", -+ "/type_module:choiceWithDefaultContainer", -+ "/type_module:implicitCaseContainer", - "/type_module:leafBinary", - "/type_module:leafBits", - "/type_module:leafEnum", -@@ -417,6 +424,71 @@ TEST_CASE("SchemaNode") - REQUIRE(!ctx->findPath("/type_module:anyxmlBasic").asAnyDataAnyXML().isMandatory()); - } - -+ DOCTEST_SUBCASE("Choice and Case") -+ { -+ std::string xpath; -+ bool isMandatory = false; -+ std::optional defaultCase; -+ std::vector caseNames; -+ std::optional root; -+ -+ DOCTEST_SUBCASE("two cases with nothing fancy") -+ { -+ root = ctx->findPath("/type_module:choiceBasicContainer"); -+ caseNames = {"case1", "case2"}; -+ } -+ -+ DOCTEST_SUBCASE("mandatory choice") { -+ root = ctx->findPath("/type_module:choiceWithMandatoryContainer"); -+ isMandatory = true; -+ caseNames = {"case3", "case4"}; -+ } -+ -+ DOCTEST_SUBCASE("default choice") { -+ root = ctx->findPath("/type_module:choiceWithDefaultContainer"); -+ defaultCase = "case5"; -+ caseNames = {"case5", "case6"}; -+ } -+ -+ DOCTEST_SUBCASE("implicit case") { -+ root = ctx->findPath("/type_module:implicitCaseContainer"); -+ caseNames = {"implicitLeaf"}; -+ } -+ -+ // For testing purposes, we have each choice in its own container. As choice and case are not directly instantiable, -+ // we wrap them in a container to simplify the testing process. It allows us to simply address the choice by its -+ // container and then get the choice from it. It also prevents polluting the test schema with unnecessary nodes -+ // and isolates the choice from other nodes. -+ auto container = root->asContainer(); -+ auto choice = container.immediateChildren().begin()->asChoice(); -+ REQUIRE(choice.isMandatory() == isMandatory); -+ REQUIRE(!!choice.defaultCase() == !!defaultCase); -+ if (defaultCase) { -+ REQUIRE(choice.defaultCase()->name() == *defaultCase); -+ } -+ std::vector actualCaseNames; -+ for (const auto& case_ : choice.cases()) { -+ actualCaseNames.push_back(case_.name()); -+ } -+ REQUIRE(actualCaseNames == caseNames); -+ -+ // Also test child node access for one arbitrary choice/case combination -+ if (root->path() == "/type_module:choiceBasicContainer") { -+ REQUIRE(choice.cases().size() == 2); -+ auto case1 = choice.cases()[0]; -+ auto children = case1.immediateChildren(); -+ auto it = children.begin(); -+ REQUIRE(it->asLeaf().name() == "l"); -+ ++it; -+ REQUIRE(it->asLeafList().name() == "ll"); -+ -+ auto case2 = choice.cases()[1]; -+ children = case2.immediateChildren(); -+ it = children.begin(); -+ REQUIRE(it->asLeaf().name() == "l2"); -+ } -+ } -+ - DOCTEST_SUBCASE("Container::isMandatory") - { - REQUIRE(ctx->findPath("/type_module:containerWithMandatoryChild").asContainer().isMandatory()); --- -2.43.0 - diff --git a/package/libyang-cpp/0007-Wrap-lyd_change_term-for-changing-the-value-for-a-te.patch b/package/libyang-cpp/0007-Wrap-lyd_change_term-for-changing-the-value-for-a-te.patch deleted file mode 100644 index 76cb2206..00000000 --- a/package/libyang-cpp/0007-Wrap-lyd_change_term-for-changing-the-value-for-a-te.patch +++ /dev/null @@ -1,129 +0,0 @@ -From a1acdc794facf8cbf113f73274ecebd5898c81a1 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Tue, 17 Dec 2024 15:08:43 +0100 -Subject: [PATCH 07/18] Wrap lyd_change_term for changing the value for a - terminal node -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Previously, the code would require a newPath(..., -libyang::CreationOptions::Update), which is quite a mouthful. - -Change-Id: I8a908c0fdd3e48dda830819758522a511adedd3b -Signed-off-by: Mattias Walström ---- - include/libyang-cpp/DataNode.hpp | 8 ++++++ - src/DataNode.cpp | 21 ++++++++++++++++ - tests/data_node.cpp | 42 ++++++++++++++++++++++++++------ - 3 files changed, 63 insertions(+), 8 deletions(-) - -diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp -index 2211415..851681b 100644 ---- a/include/libyang-cpp/DataNode.hpp -+++ b/include/libyang-cpp/DataNode.hpp -@@ -212,6 +212,14 @@ public: - Value value() const; - types::Type valueType() const; - -+ /** @brief Was the value changed? */ -+ enum class ValueChange { -+ Changed, /**< Yes, this is an actual change of the stored value */ -+ ExplicitNonDefault, /**< It still holds the default value, but it's been set explicitly now */ -+ EqualValueNotChanged, /**< No change, the previous value is the same as the new one, and it isn't an implicit default */ -+ }; -+ ValueChange changeValue(const std::string value); -+ - private: - using DataNode::DataNode; - }; -diff --git a/src/DataNode.cpp b/src/DataNode.cpp -index 2ef17f2..84591e5 100644 ---- a/src/DataNode.cpp -+++ b/src/DataNode.cpp -@@ -903,6 +903,27 @@ types::Type DataNodeTerm::valueType() const - return impl(reinterpret_cast(m_node)->value); - } - -+/** @short Change the term's value -+ * -+ * Wraps `lyd_change_term`. -+ * */ -+DataNodeTerm::ValueChange DataNodeTerm::changeValue(const std::string value) -+{ -+ auto ret = lyd_change_term(m_node, value.c_str()); -+ -+ switch (ret) { -+ case LY_SUCCESS: -+ return ValueChange::Changed; -+ case LY_EEXIST: -+ return ValueChange::ExplicitNonDefault; -+ case LY_ENOT: -+ return ValueChange::EqualValueNotChanged; -+ default: -+ throwIfError(ret, "DataNodeTerm::changeValue failed"); -+ __builtin_unreachable(); -+ } -+} -+ - /** - * @brief Returns a collection for iterating depth-first over the subtree this instance points to. - * -diff --git a/tests/data_node.cpp b/tests/data_node.cpp -index 8a2610e..45fd6c1 100644 ---- a/tests/data_node.cpp -+++ b/tests/data_node.cpp -@@ -456,15 +456,41 @@ TEST_CASE("Data Node manipulation") - REQUIRE(node.hasDefaultValue()); - REQUIRE(node.isImplicitDefault()); - -- data->newPath("/example-schema3:leafWithDefault", "not-default-value", libyang::CreationOptions::Update); -- node = data->findPath("/example-schema3:leafWithDefault")->asTerm(); -- REQUIRE(!node.hasDefaultValue()); -- REQUIRE(!node.isImplicitDefault()); -+ DOCTEST_SUBCASE("newPath") -+ { -+ data->newPath("/example-schema3:leafWithDefault", "not-default-value", libyang::CreationOptions::Update); -+ node = data->findPath("/example-schema3:leafWithDefault")->asTerm(); -+ REQUIRE(!node.hasDefaultValue()); -+ REQUIRE(!node.isImplicitDefault()); - -- data->newPath("/example-schema3:leafWithDefault", "AHOJ", libyang::CreationOptions::Update); -- node = data->findPath("/example-schema3:leafWithDefault")->asTerm(); -- REQUIRE(node.hasDefaultValue()); -- REQUIRE(!node.isImplicitDefault()); -+ data->newPath("/example-schema3:leafWithDefault", "AHOJ", libyang::CreationOptions::Update); -+ node = data->findPath("/example-schema3:leafWithDefault")->asTerm(); -+ REQUIRE(node.hasDefaultValue()); -+ REQUIRE(!node.isImplicitDefault()); -+ } -+ -+ DOCTEST_SUBCASE("changing values") -+ { -+ auto node = data->findPath("/example-schema3:leafWithDefault"); -+ REQUIRE(!!node); -+ auto term = node->asTerm(); -+ -+ DOCTEST_SUBCASE("to an arbitrary value") { -+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::Changed); -+ } -+ -+ DOCTEST_SUBCASE("from an implicit default to an explicit default") { -+ REQUIRE(term.changeValue("AHOJ") == libyang::DataNodeTerm::ValueChange::ExplicitNonDefault); -+ REQUIRE(term.changeValue("AHOJ") == libyang::DataNodeTerm::ValueChange::EqualValueNotChanged); -+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::Changed); -+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::EqualValueNotChanged); -+ } -+ -+ DOCTEST_SUBCASE("from an implicit default to something else") { -+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::Changed); -+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::EqualValueNotChanged); -+ } -+ } - } - - DOCTEST_SUBCASE("isTerm") --- -2.43.0 - diff --git a/package/libyang-cpp/0008-Add-Module-child-Module-childrenDfs-and-Module-immed.patch b/package/libyang-cpp/0008-Add-Module-child-Module-childrenDfs-and-Module-immed.patch deleted file mode 100644 index 4ffce00e..00000000 --- a/package/libyang-cpp/0008-Add-Module-child-Module-childrenDfs-and-Module-immed.patch +++ /dev/null @@ -1,652 +0,0 @@ -From 32b200ed06e9adb44a8d4ce6771f18812a54d06e Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= -Date: Wed, 20 Nov 2024 10:20:19 +0100 -Subject: [PATCH 08/18] Add `Module::child()`, `Module::childrenDfs()` and - `Module::immediateChildren()` -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Those functions are implemented in the same manner as in `SchemaNode` -and allows to walk through modules children. This is counterpart to -already implemented `Module::childInstantiables()` that returns -instantiables schema nodes. These return all nodes, including -the schema-only nodes such as choice and case if end-user needs -to read its schema. - -While the implementation is inspired by functions in `SchemaNode`, -imlementation of `Module::parent()` and `Module::siblings()` was omitted -as those do no make sense on `Module`. - -Change-Id: I38c8374304f859d65343d04d08302e07deb05f27 -Signed-off-by: Mattias Walström ---- - include/libyang-cpp/Collection.hpp | 1 + - include/libyang-cpp/Module.hpp | 5 + - src/Module.cpp | 40 +++ - tests/context.cpp | 5 + - tests/example_schema.hpp | 21 ++ - tests/schema_node.cpp | 409 +++++++++++++++++++---------- - 6 files changed, 346 insertions(+), 135 deletions(-) - -diff --git a/include/libyang-cpp/Collection.hpp b/include/libyang-cpp/Collection.hpp -index 557a0a2..4324791 100644 ---- a/include/libyang-cpp/Collection.hpp -+++ b/include/libyang-cpp/Collection.hpp -@@ -98,6 +98,7 @@ class LIBYANG_CPP_EXPORT Collection { - public: - friend DataNode; - friend Iterator; -+ friend Module; - friend SchemaNode; - ~Collection(); - Collection(const Collection&); -diff --git a/include/libyang-cpp/Module.hpp b/include/libyang-cpp/Module.hpp -index f10c36f..ab20d36 100644 ---- a/include/libyang-cpp/Module.hpp -+++ b/include/libyang-cpp/Module.hpp -@@ -34,6 +34,8 @@ class ChildInstanstiables; - class Identity; - class SchemaNode; - class SubmoduleParsed; -+template -+class Collection; - - namespace types { - class IdentityRef; -@@ -86,7 +88,10 @@ public: - - std::vector identities() const; - -+ std::optional child() const; - ChildInstanstiables childInstantiables() const; -+ libyang::Collection childrenDfs() const; -+ Collection immediateChildren() const; - std::vector actionRpcs() const; - - std::string printStr(const SchemaOutputFormat format, const std::optional flags = std::nullopt, std::optional lineLength = std::nullopt) const; -diff --git a/src/Module.cpp b/src/Module.cpp -index 4dc9e3b..d6d4023 100644 ---- a/src/Module.cpp -+++ b/src/Module.cpp -@@ -8,6 +8,7 @@ - - #include - #include -+#include - #include - #include - #include -@@ -178,6 +179,23 @@ std::vector Module::identities() const - return res; - } - -+/** -+ * @brief Returns the first child node of this module. -+ * @return The child, or std::nullopt if there are no children. -+ */ -+std::optional Module::child() const -+{ -+ if (!m_module->implemented) { -+ throw Error{"Module::child: module is not implemented"}; -+ } -+ -+ if (!m_module->compiled->data) { -+ return std::nullopt; -+ } -+ -+ return SchemaNode{m_module->compiled->data, m_ctx}; -+} -+ - /** - * @brief Returns a collection of data instantiable top-level nodes of this module. - * -@@ -191,6 +209,28 @@ ChildInstanstiables Module::childInstantiables() const - return ChildInstanstiables{nullptr, m_module->compiled, m_ctx}; - } - -+/** -+ * @brief Returns a collection for iterating depth-first over the subtree this module points to. -+ */ -+Collection Module::childrenDfs() const -+{ -+ if (!m_module->implemented) { -+ throw Error{"Module::childrenDfs: module is not implemented"}; -+ } -+ return Collection{m_module->compiled->data, m_ctx}; -+} -+ -+/** -+ * @brief Returns a collection for iterating over the immediate children of where this module points to. -+ * -+ * This is a convenience function for iterating over this->child().siblings() which does not throw even when module has no children. -+ */ -+Collection Module::immediateChildren() const -+{ -+ auto c = child(); -+ return c ? c->siblings() : Collection{nullptr, nullptr}; -+} -+ - /** - * @brief Returns a collection of RPC nodes (not action nodes) as SchemaNode - * -diff --git a/tests/context.cpp b/tests/context.cpp -index 5929b75..25343db 100644 ---- a/tests/context.cpp -+++ b/tests/context.cpp -@@ -713,6 +713,11 @@ TEST_CASE("context") - +--rw anydataWithMandatoryChild anydata - +--rw anyxmlBasic? anyxml - +--rw anyxmlWithMandatoryChild anyxml -+ +--rw (choiceOnModule)? -+ | +--:(case1) -+ | | +--rw choiceOnModuleLeaf1? string -+ | +--:(case2) -+ | +--rw choiceOnModuleLeaf2? string - +--rw choiceBasicContainer - | +--rw (choiceBasic)? - | +--:(case1) -diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp -index ae3b4de..0d8acb9 100644 ---- a/tests/example_schema.hpp -+++ b/tests/example_schema.hpp -@@ -390,6 +390,19 @@ module type_module { - mandatory true; - } - -+ choice choiceOnModule { -+ case case1 { -+ leaf choiceOnModuleLeaf1 { -+ type string; -+ } -+ } -+ case case2 { -+ leaf choiceOnModuleLeaf2 { -+ type string; -+ } -+ } -+ } -+ - container choiceBasicContainer { - choice choiceBasic { - case case1 { -@@ -787,6 +800,14 @@ module type_module { - } - )"s; - -+const auto empty_module = R"( -+module empty_module { -+ yang-version 1.1; -+ namespace "e"; -+ prefix "e"; -+} -+)"s; -+ - const auto with_inet_types_module = R"( - module with-inet-types { - yang-version 1.1; -diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp -index 8d74bd2..0001377 100644 ---- a/tests/schema_node.cpp -+++ b/tests/schema_node.cpp -@@ -24,8 +24,10 @@ TEST_CASE("SchemaNode") - libyang::ContextOptions::SetPrivParsed | libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd}; - ctx->parseModule(example_schema, libyang::SchemaFormat::YANG); - ctx->parseModule(type_module, libyang::SchemaFormat::YANG); -+ ctx->parseModule(empty_module, libyang::SchemaFormat::YANG); - ctxWithParsed->parseModule(example_schema, libyang::SchemaFormat::YANG); - ctxWithParsed->parseModule(type_module, libyang::SchemaFormat::YANG); -+ ctxWithParsed->parseModule(empty_module, libyang::SchemaFormat::YANG); - - DOCTEST_SUBCASE("context lifetime") - { -@@ -74,10 +76,34 @@ TEST_CASE("SchemaNode") - REQUIRE(node->schema().path() == "/example-schema:person"); - } - -- DOCTEST_SUBCASE("SchemaNode::child") -+ DOCTEST_SUBCASE("child") - { -- REQUIRE(ctx->findPath("/type_module:listAdvancedWithTwoKey").child()->name() == "first"); -- REQUIRE(!ctx->findPath("/type_module:leafString").child().has_value()); -+ DOCTEST_SUBCASE("implemented module") -+ { -+ DOCTEST_SUBCASE("SchemaNode::child") -+ { -+ REQUIRE(ctx->findPath("/type_module:listAdvancedWithTwoKey").child()->name() == "first"); -+ REQUIRE(!ctx->findPath("/type_module:leafString").child().has_value()); -+ } -+ -+ DOCTEST_SUBCASE("Module::child") -+ { -+ REQUIRE(ctx->getModule("type_module", std::nullopt)->child()->name() == "anydataBasic"); -+ REQUIRE(!ctx->getModule("empty_module", std::nullopt)->child()); -+ } -+ } -+ -+ DOCTEST_SUBCASE("unimplemented module") -+ { -+ DOCTEST_SUBCASE("Module::child") -+ { -+ ctx->setSearchDir(TESTS_DIR / "yang"); -+ auto modYangPatch = ctx->loadModule("ietf-yang-patch", std::nullopt); -+ auto modRestconf = ctx->getModule("ietf-restconf", "2017-01-26"); -+ REQUIRE(!modRestconf->implemented()); -+ REQUIRE_THROWS_WITH_AS(modRestconf->child(), "Module::child: module is not implemented", libyang::Error); -+ } -+ } - } - - DOCTEST_SUBCASE("SchemaNode::config") -@@ -160,162 +186,275 @@ TEST_CASE("SchemaNode") - - DOCTEST_SUBCASE("childInstantiables") - { -- std::vector expectedPaths; -- std::optional children; -- -- DOCTEST_SUBCASE("SchemaNode::childInstantiables") -+ DOCTEST_SUBCASE("implemented module") - { -- expectedPaths = { -- "/type_module:listAdvancedWithOneKey/lol", -- "/type_module:listAdvancedWithOneKey/notKey1", -- "/type_module:listAdvancedWithOneKey/notKey2", -- "/type_module:listAdvancedWithOneKey/notKey3", -- "/type_module:listAdvancedWithOneKey/notKey4", -- }; -+ std::vector expectedPaths; -+ std::optional children; -+ -+ DOCTEST_SUBCASE("SchemaNode::childInstantiables") -+ { -+ expectedPaths = { -+ "/type_module:listAdvancedWithOneKey/lol", -+ "/type_module:listAdvancedWithOneKey/notKey1", -+ "/type_module:listAdvancedWithOneKey/notKey2", -+ "/type_module:listAdvancedWithOneKey/notKey3", -+ "/type_module:listAdvancedWithOneKey/notKey4", -+ }; -+ -+ children = ctx->findPath("/type_module:listAdvancedWithOneKey").childInstantiables(); -+ } - -- children = ctx->findPath("/type_module:listAdvancedWithOneKey").childInstantiables(); -- } -+ DOCTEST_SUBCASE("Module::childInstantiables") -+ { -+ expectedPaths = { -+ "/type_module:anydataBasic", -+ "/type_module:anydataWithMandatoryChild", -+ "/type_module:anyxmlBasic", -+ "/type_module:anyxmlWithMandatoryChild", -+ "/type_module:choiceOnModuleLeaf1", -+ "/type_module:choiceOnModuleLeaf2", -+ "/type_module:choiceBasicContainer", -+ "/type_module:choiceWithMandatoryContainer", -+ "/type_module:choiceWithDefaultContainer", -+ "/type_module:implicitCaseContainer", -+ "/type_module:leafBinary", -+ "/type_module:leafBits", -+ "/type_module:leafEnum", -+ "/type_module:leafEnum2", -+ "/type_module:leafNumber", -+ "/type_module:leafRef", -+ "/type_module:leafRefRelaxed", -+ "/type_module:leafString", -+ "/type_module:leafUnion", -+ "/type_module:meal", -+ "/type_module:leafWithConfigFalse", -+ "/type_module:leafWithDefaultValue", -+ "/type_module:leafWithDescription", -+ "/type_module:leafWithMandatoryTrue", -+ "/type_module:leafWithStatusDeprecated", -+ "/type_module:leafWithStatusObsolete", -+ "/type_module:leafWithUnits", -+ "/type_module:iid-valid", -+ "/type_module:iid-relaxed", -+ "/type_module:leafListBasic", -+ "/type_module:leafListWithDefault", -+ "/type_module:leafListWithMinMaxElements", -+ "/type_module:leafListWithUnits", -+ "/type_module:listBasic", -+ "/type_module:listAdvancedWithOneKey", -+ "/type_module:listAdvancedWithTwoKey", -+ "/type_module:listWithMinMaxElements", -+ "/type_module:numeric", -+ "/type_module:container", -+ "/type_module:containerWithMandatoryChild", -+ }; -+ children = ctx->getModule("type_module", std::nullopt)->childInstantiables(); -+ } - -- DOCTEST_SUBCASE("Module::childInstantiables") -- { -- expectedPaths = { -- "/type_module:anydataBasic", -- "/type_module:anydataWithMandatoryChild", -- "/type_module:anyxmlBasic", -- "/type_module:anyxmlWithMandatoryChild", -- "/type_module:choiceBasicContainer", -- "/type_module:choiceWithMandatoryContainer", -- "/type_module:choiceWithDefaultContainer", -- "/type_module:implicitCaseContainer", -- "/type_module:leafBinary", -- "/type_module:leafBits", -- "/type_module:leafEnum", -- "/type_module:leafEnum2", -- "/type_module:leafNumber", -- "/type_module:leafRef", -- "/type_module:leafRefRelaxed", -- "/type_module:leafString", -- "/type_module:leafUnion", -- "/type_module:meal", -- "/type_module:leafWithConfigFalse", -- "/type_module:leafWithDefaultValue", -- "/type_module:leafWithDescription", -- "/type_module:leafWithMandatoryTrue", -- "/type_module:leafWithStatusDeprecated", -- "/type_module:leafWithStatusObsolete", -- "/type_module:leafWithUnits", -- "/type_module:iid-valid", -- "/type_module:iid-relaxed", -- "/type_module:leafListBasic", -- "/type_module:leafListWithDefault", -- "/type_module:leafListWithMinMaxElements", -- "/type_module:leafListWithUnits", -- "/type_module:listBasic", -- "/type_module:listAdvancedWithOneKey", -- "/type_module:listAdvancedWithTwoKey", -- "/type_module:listWithMinMaxElements", -- "/type_module:numeric", -- "/type_module:container", -- "/type_module:containerWithMandatoryChild", -- }; -- children = ctx->getModule("type_module", std::nullopt)->childInstantiables(); -- } -+ std::vector actualPaths; -+ for (const auto& child : *children) { -+ actualPaths.emplace_back(child.path()); -+ } - -- std::vector actualPaths; -- for (const auto& child : *children) { -- actualPaths.emplace_back(child.path()); -+ REQUIRE(expectedPaths == actualPaths); - } - -- REQUIRE(expectedPaths == actualPaths); -+ DOCTEST_SUBCASE("unimplemented module") -+ { -+ DOCTEST_SUBCASE("Module::childInstantiables") -+ { -+ ctx->setSearchDir(TESTS_DIR / "yang"); -+ auto modYangPatch = ctx->loadModule("ietf-yang-patch", std::nullopt); -+ auto modRestconf = ctx->getModule("ietf-restconf", "2017-01-26"); -+ REQUIRE(!modRestconf->implemented()); -+ REQUIRE_THROWS_WITH_AS(modRestconf->childInstantiables(), "Module::childInstantiables: module is not implemented", libyang::Error); -+ } -+ } - } - -- DOCTEST_SUBCASE("SchemaNode::childrenDfs") -+ DOCTEST_SUBCASE("childrenDfs") - { -- std::vector expectedPaths; -+ DOCTEST_SUBCASE("implemented module") -+ { -+ std::vector expectedPaths; -+ std::optional> children; - -- const char* path; -+ DOCTEST_SUBCASE("SchemaNode::childrenDfs") -+ { -+ DOCTEST_SUBCASE("listAdvancedWithTwoKey") -+ { -+ expectedPaths = { -+ "/type_module:listAdvancedWithTwoKey", -+ "/type_module:listAdvancedWithTwoKey/first", -+ "/type_module:listAdvancedWithTwoKey/second", -+ }; -+ children = ctx->findPath("/type_module:listAdvancedWithTwoKey").childrenDfs(); -+ } - -- DOCTEST_SUBCASE("listAdvancedWithTwoKey") -- { -- expectedPaths = { -- "/type_module:listAdvancedWithTwoKey", -- "/type_module:listAdvancedWithTwoKey/first", -- "/type_module:listAdvancedWithTwoKey/second", -- }; -+ DOCTEST_SUBCASE("DFS on a leaf") -+ { -+ expectedPaths = { -+ "/type_module:leafString", -+ }; -+ children = ctx->findPath("/type_module:leafString").childrenDfs(); -+ } -+ } - -- path = "/type_module:listAdvancedWithTwoKey"; -- } -+ DOCTEST_SUBCASE("Module::childrenDfs") -+ { -+ expectedPaths = { -+ "/type_module:anydataBasic", -+ }; -+ children = ctx->getModule("type_module", std::nullopt)->childrenDfs(); -+ } - -- DOCTEST_SUBCASE("DFS on a leaf") -- { -- expectedPaths = { -- "/type_module:leafString", -- }; -+ std::vector actualPaths; -+ for (const auto& it : *children) { -+ actualPaths.emplace_back(it.path()); -+ } - -- path = "/type_module:leafString"; -+ REQUIRE(actualPaths == expectedPaths); - } - -- std::vector actualPaths; -- for (const auto& it : ctx->findPath(path).childrenDfs()) { -- actualPaths.emplace_back(it.path()); -+ DOCTEST_SUBCASE("unimplemented module") -+ { -+ DOCTEST_SUBCASE("Module::childrenDfs") -+ { -+ ctx->setSearchDir(TESTS_DIR / "yang"); -+ auto modYangPatch = ctx->loadModule("ietf-yang-patch", std::nullopt); -+ auto modRestconf = ctx->getModule("ietf-restconf", "2017-01-26"); -+ REQUIRE(!modRestconf->implemented()); -+ REQUIRE_THROWS_WITH_AS(modRestconf->childrenDfs(), "Module::childrenDfs: module is not implemented", libyang::Error); -+ } - } -- -- REQUIRE(actualPaths == expectedPaths); - } - -- DOCTEST_SUBCASE("SchemaNode::immediateChildren") -+ DOCTEST_SUBCASE("immediateChildren") - { -- std::vector expectedPaths; -- const char* path; -- DOCTEST_SUBCASE("listAdvancedWithTwoKey") -- { -- expectedPaths = { -- "/type_module:listAdvancedWithTwoKey/first", -- "/type_module:listAdvancedWithTwoKey/second", -- }; -- path = "/type_module:listAdvancedWithTwoKey"; -- } -- DOCTEST_SUBCASE("leaf") -+ DOCTEST_SUBCASE("implemented module") - { -- expectedPaths = { -- }; -- path = "/type_module:leafString"; -- } -- DOCTEST_SUBCASE("no recursion") -- { -- expectedPaths = { -- "/type_module:container/x", -- "/type_module:container/y", -- "/type_module:container/z", -- }; -- path = "/type_module:container"; -- } -- DOCTEST_SUBCASE("empty container") -- { -- expectedPaths = { -- }; -- path = "/type_module:container/y"; -- } -- DOCTEST_SUBCASE("one item") -- { -- expectedPaths = { -- "/type_module:container/z/z1", -- }; -- path = "/type_module:container/z"; -+ std::vector expectedPaths; -+ std::optional> children; -+ -+ DOCTEST_SUBCASE("SchemaNode::immediateChildren") -+ { -+ DOCTEST_SUBCASE("listAdvancedWithTwoKey") -+ { -+ expectedPaths = { -+ "/type_module:listAdvancedWithTwoKey/first", -+ "/type_module:listAdvancedWithTwoKey/second", -+ }; -+ children = ctx->findPath("/type_module:listAdvancedWithTwoKey").immediateChildren(); -+ } -+ DOCTEST_SUBCASE("leaf") -+ { -+ expectedPaths = {}; -+ children = ctx->findPath("/type_module:leafString").immediateChildren(); -+ } -+ DOCTEST_SUBCASE("no recursion") -+ { -+ expectedPaths = { -+ "/type_module:container/x", -+ "/type_module:container/y", -+ "/type_module:container/z", -+ }; -+ children = ctx->findPath("/type_module:container").immediateChildren(); -+ } -+ DOCTEST_SUBCASE("empty container") -+ { -+ expectedPaths = {}; -+ children = ctx->findPath("/type_module:container/y").immediateChildren(); -+ } -+ DOCTEST_SUBCASE("one item") -+ { -+ expectedPaths = { -+ "/type_module:container/z/z1", -+ }; -+ children = ctx->findPath("/type_module:container/z").immediateChildren(); -+ } -+ DOCTEST_SUBCASE("two items") -+ { -+ expectedPaths = { -+ "/type_module:container/x/x1", -+ "/type_module:container/x/x2", -+ }; -+ children = ctx->findPath("/type_module:container/x").immediateChildren(); -+ } -+ } -+ -+ DOCTEST_SUBCASE("Module::immediateChildren") -+ { -+ expectedPaths = { -+ "/type_module:anydataBasic", -+ "/type_module:anydataWithMandatoryChild", -+ "/type_module:anyxmlBasic", -+ "/type_module:anyxmlWithMandatoryChild", -+ // choiceOnModule is a choice, so it doesn't have path "/type_module:choiceOnModule". -+ // This node is tested at the end of the test subcase. -+ "/", -+ "/type_module:choiceBasicContainer", -+ "/type_module:choiceWithMandatoryContainer", -+ "/type_module:choiceWithDefaultContainer", -+ "/type_module:implicitCaseContainer", -+ "/type_module:leafBinary", -+ "/type_module:leafBits", -+ "/type_module:leafEnum", -+ "/type_module:leafEnum2", -+ "/type_module:leafNumber", -+ "/type_module:leafRef", -+ "/type_module:leafRefRelaxed", -+ "/type_module:leafString", -+ "/type_module:leafUnion", -+ "/type_module:meal", -+ "/type_module:leafWithConfigFalse", -+ "/type_module:leafWithDefaultValue", -+ "/type_module:leafWithDescription", -+ "/type_module:leafWithMandatoryTrue", -+ "/type_module:leafWithStatusDeprecated", -+ "/type_module:leafWithStatusObsolete", -+ "/type_module:leafWithUnits", -+ "/type_module:iid-valid", -+ "/type_module:iid-relaxed", -+ "/type_module:leafListBasic", -+ "/type_module:leafListWithDefault", -+ "/type_module:leafListWithMinMaxElements", -+ "/type_module:leafListWithUnits", -+ "/type_module:listBasic", -+ "/type_module:listAdvancedWithOneKey", -+ "/type_module:listAdvancedWithTwoKey", -+ "/type_module:listWithMinMaxElements", -+ "/type_module:numeric", -+ "/type_module:container", -+ "/type_module:containerWithMandatoryChild", -+ }; -+ children = ctx->getModule("type_module", std::nullopt)->immediateChildren(); -+ -+ std::vector actualNames; -+ for (auto it : children.value()) { -+ actualNames.emplace_back(it.name()); -+ } -+ // choiceOnModule is a choice, so it doesn't have path, just name. -+ REQUIRE(actualNames[4] == "choiceOnModule"); -+ } -+ -+ std::vector actualPaths; -+ for (const auto& it : *children) { -+ actualPaths.emplace_back(it.path()); -+ } -+ REQUIRE(actualPaths == expectedPaths); - } -- DOCTEST_SUBCASE("two items") -+ -+ DOCTEST_SUBCASE("unimplemented module") - { -- expectedPaths = { -- "/type_module:container/x/x1", -- "/type_module:container/x/x2", -- }; -- path = "/type_module:container/x"; -- } -- std::vector actualPaths; -- for (const auto& it : ctx->findPath(path).immediateChildren()) { -- actualPaths.emplace_back(it.path()); -+ DOCTEST_SUBCASE("Module::immediateChildren") -+ { -+ ctx->setSearchDir(TESTS_DIR / "yang"); -+ auto modYangPatch = ctx->loadModule("ietf-yang-patch", std::nullopt); -+ auto modRestconf = ctx->getModule("ietf-restconf", "2017-01-26"); -+ REQUIRE(!modRestconf->implemented()); -+ REQUIRE_THROWS_WITH_AS(modRestconf->immediateChildren(), "Module::child: module is not implemented", libyang::Error); -+ } - } -- REQUIRE(actualPaths == expectedPaths); - } - - DOCTEST_SUBCASE("SchemaNode::siblings") --- -2.43.0 - diff --git a/package/libyang-cpp/0009-upstream-stopped-reporting-schema-mounts-node.patch b/package/libyang-cpp/0009-upstream-stopped-reporting-schema-mounts-node.patch deleted file mode 100644 index 8689db95..00000000 --- a/package/libyang-cpp/0009-upstream-stopped-reporting-schema-mounts-node.patch +++ /dev/null @@ -1,46 +0,0 @@ -From 39c7530caa510144c17521278b721ba1e6d8ff40 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Thu, 9 Jan 2025 15:31:37 +0100 -Subject: [PATCH 09/18] upstream stopped reporting schema-mounts node -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: I940769d38d56fcfda3e1408c92331fdb00c161e9 -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 2 +- - tests/context.cpp | 3 +-- - 2 files changed, 2 insertions(+), 3 deletions(-) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 3d86809..732f52b 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -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.4.2 IMPORTED_TARGET) -+pkg_check_modules(LIBYANG REQUIRED libyang>=3.7.8 IMPORTED_TARGET) - set(LIBYANG_CPP_PKG_VERSION "3") - - # FIXME from gcc 14.1 on we should be able to use the calendar/time from libstdc++ and thus remove the date dependency -diff --git a/tests/context.cpp b/tests/context.cpp -index 5929b75..9d38fea 100644 ---- a/tests/context.cpp -+++ b/tests/context.cpp -@@ -509,8 +509,7 @@ TEST_CASE("context") - "error-message": "hi" - } - ] -- }, -- "ietf-yang-schema-mount:schema-mounts": {} -+ } - } - )"); - } --- -2.43.0 - diff --git a/package/libyang-cpp/0010-CI-temporarily-pin-version-due-to-anyxml-behavior-ch.patch b/package/libyang-cpp/0010-CI-temporarily-pin-version-due-to-anyxml-behavior-ch.patch deleted file mode 100644 index b17e4dd7..00000000 --- a/package/libyang-cpp/0010-CI-temporarily-pin-version-due-to-anyxml-behavior-ch.patch +++ /dev/null @@ -1,39 +0,0 @@ -From 82c963766ad4e4a802db7be656acbedb640745e9 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Thu, 30 Jan 2025 16:34:12 +0100 -Subject: [PATCH 10/18] CI: temporarily pin version due to anyxml behavior - changes upstream -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: Id977f4d045098c1b93656c0efb871c9a1b650e2d -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 b41c490..19f0def 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-01-29 - - name: github/onqtam/doctest - override-checkout: v2.3.6 - - f38-clang-asan-ubsan: - required-projects: &projects - - name: github/CESNET/libyang -- override-checkout: devel -+ override-checkout: cesnet/2025-01-29 - - name: github/onqtam/doctest - override-checkout: v2.4.11 - - f38-clang-tsan: --- -2.43.0 - diff --git a/package/libyang-cpp/0011-build-a-single-place-to-define-package-version.patch b/package/libyang-cpp/0011-build-a-single-place-to-define-package-version.patch deleted file mode 100644 index f1761702..00000000 --- a/package/libyang-cpp/0011-build-a-single-place-to-define-package-version.patch +++ /dev/null @@ -1,40 +0,0 @@ -From 50a216e35a555961f94a32a71bb2d45ac611d0aa Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Wed, 29 Jan 2025 22:49:08 +0100 -Subject: [PATCH 11/18] build: a single place to define package version -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: I2cd7397895ed4852f852e99b97543dde76eaff8f -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 732f52b..c518ca8 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -21,7 +21,8 @@ add_custom_target(libyang-cpp-version-cmake - cmake/ProjectGitVersionRunner.cmake - ) - include(cmake/ProjectGitVersion.cmake) --prepare_git_version(LIBYANG_CPP_VERSION "3") -+set(LIBYANG_CPP_PKG_VERSION "3") -+prepare_git_version(LIBYANG_CPP_VERSION ${LIBYANG_CPP_PKG_VERSION}) - - find_package(Doxygen) - option(WITH_DOCS "Create and install internal documentation (needs Doxygen)" ${DOXYGEN_FOUND}) -@@ -29,7 +30,6 @@ option(BUILD_SHARED_LIBS "By default, shared libs are enabled. Turn off for a st - - find_package(PkgConfig REQUIRED) - pkg_check_modules(LIBYANG REQUIRED libyang>=3.7.8 IMPORTED_TARGET) --set(LIBYANG_CPP_PKG_VERSION "3") - - # 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) --- -2.43.0 - diff --git a/package/libyang-cpp/0012-YANG-flavored-regular-expressions.patch b/package/libyang-cpp/0012-YANG-flavored-regular-expressions.patch deleted file mode 100644 index 75cf753f..00000000 --- a/package/libyang-cpp/0012-YANG-flavored-regular-expressions.patch +++ /dev/null @@ -1,173 +0,0 @@ -From 1533458346b4f395b1187c646b61bbcb1fddc615 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Mon, 4 Nov 2024 14:52:12 +0100 -Subject: [PATCH 12/18] YANG-flavored regular expressions -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: I93b2756d0f470585280c076308df3f384bd7765d -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 3 +++ - include/libyang-cpp/Regex.hpp | 28 ++++++++++++++++++++++ - src/Regex.cpp | 45 +++++++++++++++++++++++++++++++++++ - tests/regex.cpp | 30 +++++++++++++++++++++++ - 4 files changed, 106 insertions(+) - create mode 100644 include/libyang-cpp/Regex.hpp - create mode 100644 src/Regex.cpp - create mode 100644 tests/regex.cpp - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index c518ca8..512af8c 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -54,6 +54,7 @@ add_library(yang-cpp - src/Enum.cpp - src/Collection.cpp - src/Module.cpp -+ src/Regex.cpp - src/SchemaNode.cpp - src/Set.cpp - src/Type.cpp -@@ -83,6 +84,7 @@ if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.24") - libyang-cpp/Enum.hpp - libyang-cpp/ChildInstantiables.hpp - libyang-cpp/Module.hpp -+ libyang-cpp/Regex.hpp - libyang-cpp/Set.hpp - libyang-cpp/SchemaNode.hpp - libyang-cpp/Time.hpp -@@ -119,6 +121,7 @@ if(BUILD_TESTING) - libyang_cpp_test(schema_node) - libyang_cpp_test(unsafe) - target_link_libraries(test_unsafe PkgConfig::LIBYANG) -+ libyang_cpp_test(regex) - - if(date_FOUND) - add_executable(test_time-stl-hhdate tests/time.cpp) -diff --git a/include/libyang-cpp/Regex.hpp b/include/libyang-cpp/Regex.hpp -new file mode 100644 -index 0000000..31935f2 ---- /dev/null -+++ b/include/libyang-cpp/Regex.hpp -@@ -0,0 +1,28 @@ -+/* -+ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/ -+ * -+ * Written by Jan Kundrát -+ * -+ * SPDX-License-Identifier: BSD-3-Clause -+ */ -+#pragma once -+#include -+#include -+ -+namespace libyang { -+class Context; -+ -+/** -+ * @brief A regular expression pattern which uses the YANG-flavored regex engine -+ */ -+class LIBYANG_CPP_EXPORT Regex { -+public: -+ Regex(const std::string& pattern); -+ ~Regex(); -+ bool matches(const std::string& input); -+ -+private: -+ void* code; -+}; -+ -+} -diff --git a/src/Regex.cpp b/src/Regex.cpp -new file mode 100644 -index 0000000..a34fcd5 ---- /dev/null -+++ b/src/Regex.cpp -@@ -0,0 +1,45 @@ -+/* -+ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/ -+ * -+ * Written by Jan Kundrát -+ * -+ * SPDX-License-Identifier: BSD-3-Clause -+ */ -+ -+// The following header MUST be included before anything else which "might" use PCRE2 -+// because that library uses the preprocessor to prepare the "correct" versions of symbols. -+#include -+ -+#include -+#include -+#include "utils/exception.hpp" -+ -+#define THE_PCRE2_CODE_P reinterpret_cast(this->code) -+#define THE_PCRE2_CODE_P_P reinterpret_cast(&this->code) -+ -+namespace libyang { -+ -+Regex::Regex(const std::string& pattern) -+ : code(nullptr) -+{ -+ auto res = ly_pattern_compile(nullptr, pattern.c_str(), THE_PCRE2_CODE_P_P); -+ throwIfError(res, ly_last_logmsg()); -+} -+ -+Regex::~Regex() -+{ -+ pcre2_code_free(THE_PCRE2_CODE_P); -+} -+ -+bool Regex::matches(const std::string& input) -+{ -+ auto res = ly_pattern_match(nullptr, nullptr /* we have a precompiled pattern */, input.c_str(), input.size(), THE_PCRE2_CODE_P_P); -+ if (res == LY_SUCCESS) { -+ return true; -+ } else if (res == LY_ENOT) { -+ return false; -+ } else { -+ throwError(res, ly_last_logmsg()); -+ } -+} -+} -diff --git a/tests/regex.cpp b/tests/regex.cpp -new file mode 100644 -index 0000000..7594f43 ---- /dev/null -+++ b/tests/regex.cpp -@@ -0,0 +1,30 @@ -+/* -+ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/ -+ * -+ * Written by Jan Kundrát -+ * -+ * SPDX-License-Identifier: BSD-3-Clause -+ */ -+ -+#include -+#include -+#include -+ -+TEST_CASE("regex") -+{ -+ using libyang::Regex; -+ using namespace std::string_literals; -+ -+ REQUIRE_THROWS_WITH_AS(Regex{"\\"}, R"(Regular expression "\" is not valid ("": \ at end of pattern).: LY_EVALID)", libyang::ErrorWithCode); -+ -+ Regex re{"ahoj"}; -+ REQUIRE(re.matches("ahoj")); -+ REQUIRE(!re.matches("cau")); -+ REQUIRE(re.matches("ahoj")); // test repeated calls as well -+ REQUIRE(!re.matches("oj")); -+ REQUIRE(!re.matches("aho")); -+ -+ // Testing runtime errors during pattern *matching* is tricky. There's a limit on backtracking, -+ // so testing a pattern like x+x+y on an obscenely long string of "x" characters *will* do the trick, eventually, -+ // but the PCRE2 library has a default limit of 10M attempts. That's a VERY big number to hit during a test :(. -+} --- -2.43.0 - diff --git a/package/libyang-cpp/0013-Adapt-to-upstream-changes-in-anyxml-JSON-printing.patch b/package/libyang-cpp/0013-Adapt-to-upstream-changes-in-anyxml-JSON-printing.patch deleted file mode 100644 index 2feb3b62..00000000 --- a/package/libyang-cpp/0013-Adapt-to-upstream-changes-in-anyxml-JSON-printing.patch +++ /dev/null @@ -1,67 +0,0 @@ -From 8d406728a53c2e77a4fe7393b7e30d42b8f9b9bb Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Thu, 30 Jan 2025 15:40:22 +0100 -Subject: [PATCH 13/18] Adapt to upstream changes in anyxml JSON printing -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: I5f6de28cebc95a446549017c2768b450f4fd6526 -Signed-off-by: Mattias Walström ---- - .zuul.yaml | 4 ++-- - CMakeLists.txt | 3 ++- - tests/data_node.cpp | 2 +- - 3 files changed, 5 insertions(+), 4 deletions(-) - -diff --git a/.zuul.yaml b/.zuul.yaml -index 19f0def..b41c490 100644 ---- a/.zuul.yaml -+++ b/.zuul.yaml -@@ -4,13 +4,13 @@ - - f38-gcc-cover: - required-projects: - - name: github/CESNET/libyang -- override-checkout: cesnet/2025-01-29 -+ override-checkout: devel - - name: github/onqtam/doctest - override-checkout: v2.3.6 - - f38-clang-asan-ubsan: - required-projects: &projects - - name: github/CESNET/libyang -- override-checkout: cesnet/2025-01-29 -+ override-checkout: devel - - name: github/onqtam/doctest - override-checkout: v2.4.11 - - f38-clang-tsan: -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 512af8c..a40fd52 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -29,7 +29,8 @@ 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.7.8 IMPORTED_TARGET) -+# FIXME: it's actually 3.7.12, but that hasn't been released yet -+pkg_check_modules(LIBYANG REQUIRED libyang>=3.7.11 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/tests/data_node.cpp b/tests/data_node.cpp -index 45fd6c1..14470dd 100644 ---- a/tests/data_node.cpp -+++ b/tests/data_node.cpp -@@ -1568,7 +1568,7 @@ TEST_CASE("Data Node manipulation") - REQUIRE(*jsonAnyXmlNode.createdNode->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings) - == R"|({"example-schema:ax":[1,2,3]})|"s); - REQUIRE(*jsonAnyXmlNode.createdNode->printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings) -- == R"|()|"s); -+ == R"|()|"s + origJSON + ""); - } - - REQUIRE(!!val); --- -2.43.0 - diff --git a/package/libyang-cpp/0014-fix-DataNode-insertSibling-return-value.patch b/package/libyang-cpp/0014-fix-DataNode-insertSibling-return-value.patch deleted file mode 100644 index 0808ed38..00000000 --- a/package/libyang-cpp/0014-fix-DataNode-insertSibling-return-value.patch +++ /dev/null @@ -1,57 +0,0 @@ -From f050e7e4a17ef2e221ca000a544042c33c9541fc Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Thu, 13 Mar 2025 19:21:26 +0100 -Subject: [PATCH 14/18] fix DataNode::insertSibling() return value -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -When such an insert happens, the C library returns the node which is now -the first sibling among all of the siblings of the node which was used -as a reference during the insert. Our API was also documented this way, -but we were not doing that. - -Reported-by: Irfan -Bug: https://github.com/CESNET/libyang-cpp/issues/29 -Change-Id: Id7f84a31e50212d6e2cbce9ab03a351a3721f767 -Signed-off-by: Mattias Walström ---- - src/DataNode.cpp | 2 +- - tests/data_node.cpp | 7 +++++++ - 2 files changed, 8 insertions(+), 1 deletion(-) - -diff --git a/src/DataNode.cpp b/src/DataNode.cpp -index 84591e5..b899b18 100644 ---- a/src/DataNode.cpp -+++ b/src/DataNode.cpp -@@ -711,7 +711,7 @@ DataNode DataNode::insertSibling(DataNode toInsert) - lyd_insert_sibling(this->m_node, toInsert.m_node, &firstSibling); - }, toInsert.parent() ? OperationScope::JustThisNode : OperationScope::AffectsFollowingSiblings, m_refs); - -- return DataNode{m_node, m_refs}; -+ return DataNode{firstSibling, m_refs}; - } - - /** -diff --git a/tests/data_node.cpp b/tests/data_node.cpp -index 14470dd..b6ee455 100644 ---- a/tests/data_node.cpp -+++ b/tests/data_node.cpp -@@ -973,6 +973,13 @@ TEST_CASE("Data Node manipulation") - REQUIRE(getNumberOrder() == expected); - } - -+ DOCTEST_SUBCASE("DataNode::insertSibling") -+ { -+ auto node = ctx.newPath("/example-schema:leafUInt8", "10"); -+ REQUIRE(node.insertSibling(ctx.newPath("/example-schema:leafUInt16", "10")).path() == "/example-schema:leafUInt8"); -+ REQUIRE(node.insertSibling(ctx.newPath("/example-schema:dummy", "10")).path() == "/example-schema:dummy"); -+ } -+ - DOCTEST_SUBCASE("DataNode::duplicate") - { - auto root = ctx.parseData(data2, libyang::DataFormat::JSON); --- -2.43.0 - diff --git a/package/libyang-cpp/0015-API-ABI-change-opaque-node-naming.patch b/package/libyang-cpp/0015-API-ABI-change-opaque-node-naming.patch deleted file mode 100644 index 2111e7b5..00000000 --- a/package/libyang-cpp/0015-API-ABI-change-opaque-node-naming.patch +++ /dev/null @@ -1,260 +0,0 @@ -From f958af42bf5d9fbd901ed59ebc1359ac0ddcc00f Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Fri, 14 Mar 2025 11:36:50 +0100 -Subject: [PATCH 15/18] API/ABI change: opaque node naming -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Our C++ API would ignore the "module name or XML prefix", which turns -out to be *the* relevant part when it comes to opaque node naming. The -prefix is, instead, just that string that might have been inherited from -the parent node when parsing the serialized data; it's an optional -thingy which, if not set explicitly, is implicitly inherited. - -Adapt the API for this, and since this *will* break the build, let's -bump the package version. - -Change-Id: I199afe5fa7a571034b744531c63b93b9c656563a -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 5 ++--- - include/libyang-cpp/Context.hpp | 4 ++-- - include/libyang-cpp/DataNode.hpp | 11 ++++++++-- - src/Context.cpp | 32 ++++++++++++++++++++-------- - src/DataNode.cpp | 19 ++++++++++++++--- - tests/data_node.cpp | 36 ++++++++++++++++++++++++++------ - 6 files changed, 82 insertions(+), 25 deletions(-) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index a40fd52..c5fec45 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -21,7 +21,7 @@ add_custom_target(libyang-cpp-version-cmake - cmake/ProjectGitVersionRunner.cmake - ) - include(cmake/ProjectGitVersion.cmake) --set(LIBYANG_CPP_PKG_VERSION "3") -+set(LIBYANG_CPP_PKG_VERSION "4") - prepare_git_version(LIBYANG_CPP_VERSION ${LIBYANG_CPP_PKG_VERSION}) - - find_package(Doxygen) -@@ -29,8 +29,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) --# FIXME: it's actually 3.7.12, but that hasn't been released yet --pkg_check_modules(LIBYANG REQUIRED libyang>=3.7.11 IMPORTED_TARGET) -+pkg_check_modules(LIBYANG REQUIRED libyang>=3.10.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/include/libyang-cpp/Context.hpp b/include/libyang-cpp/Context.hpp -index baa47b3..ca89063 100644 ---- a/include/libyang-cpp/Context.hpp -+++ b/include/libyang-cpp/Context.hpp -@@ -115,8 +115,8 @@ public: - CreatedNodes newPath2(const std::string& path, libyang::JSON json, const std::optional options = std::nullopt) const; - CreatedNodes newPath2(const std::string& path, libyang::XML xml, const std::optional options = std::nullopt) const; - std::optional newExtPath(const ExtensionInstance& ext, const std::string& path, const std::optional& value, const std::optional options = std::nullopt) const; -- std::optional newOpaqueJSON(const std::string& moduleName, const std::string& name, const std::optional& value) const; -- std::optional newOpaqueXML(const std::string& moduleName, const std::string& name, const std::optional& value) const; -+ std::optional newOpaqueJSON(const OpaqueName& name, const std::optional& value) const; -+ std::optional newOpaqueXML(const OpaqueName& name, const std::optional& value) const; - SchemaNode findPath(const std::string& dataPath, const InputOutputNodes inputOutputNodes = InputOutputNodes::Input) const; - Set findXPath(const std::string& path) const; - -diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp -index 851681b..310b5e3 100644 ---- a/include/libyang-cpp/DataNode.hpp -+++ b/include/libyang-cpp/DataNode.hpp -@@ -225,15 +225,22 @@ private: - }; - - /** -- * @brief Contains a (possibly module-qualified) name of an opaque node. -+ * @brief Contains a name of an opaque node. - * -- * This is generic container of a prefix/module string and a name string. -+ * An opaque node always has a name, and a module (or XML namespace) to which this node belongs. -+ * Sometimes, it also has a prefix. -+ * -+ * If the prefix is set *and* the underlying node is an opaque JSON one, then the prefix must be the same as the "module or namespace" name. -+ * If the underlying node is an opaque XML one, then the XML prefix might be something completely different, and in that case the real fun begins. -+ * Review the libayng C manual, this is something that the C++ wrapper doesn't really have under control. - * - * Wraps `ly_opaq_name`. - */ - struct LIBYANG_CPP_EXPORT OpaqueName { -+ std::string moduleOrNamespace; - std::optional prefix; - std::string name; -+ std::string pretty() const; - }; - - /** -diff --git a/src/Context.cpp b/src/Context.cpp -index 287f8c8..fec2f27 100644 ---- a/src/Context.cpp -+++ b/src/Context.cpp -@@ -378,17 +378,25 @@ std::optional Context::newExtPath(const ExtensionInstance& ext, const - * - * Wraps `lyd_new_opaq`. - * -- * @param moduleName Node module name, used as a prefix as well - * @param name Name of the created node - * @param value JSON data blob, if any - * @return Returns the newly created node (if created) - */ --std::optional Context::newOpaqueJSON(const std::string& moduleName, const std::string& name, const std::optional& value) const -+std::optional Context::newOpaqueJSON(const OpaqueName& name, const std::optional& value) const - { -+ if (name.prefix && *name.prefix != name.moduleOrNamespace) { -+ throw Error{"invalid opaque JSON node: prefix \"" + *name.prefix + "\" doesn't match module name \"" + name.moduleOrNamespace + "\""}; -+ } - lyd_node* out; -- auto err = lyd_new_opaq(nullptr, m_ctx.get(), name.c_str(), value ? value->content.c_str() : nullptr, nullptr, moduleName.c_str(), &out); -+ auto err = lyd_new_opaq(nullptr, -+ m_ctx.get(), -+ name.name.c_str(), -+ value ? value->content.c_str() : nullptr, -+ name.prefix ? name.prefix->c_str() : nullptr, -+ name.moduleOrNamespace.c_str(), -+ &out); - -- throwIfError(err, "Couldn't create an opaque JSON node '"s + moduleName + ':' + name + "'"); -+ throwIfError(err, "Couldn't create an opaque JSON node " + name.pretty()); - - if (out) { - return DataNode{out, std::make_shared(m_ctx)}; -@@ -403,17 +411,23 @@ std::optional Context::newOpaqueJSON(const std::string& moduleName, co - * - * Wraps `lyd_new_opaq2`. - * -- * @param xmlNamespace Node module namespace - * @param name Name of the created node - * @param value XML data blob, if any - * @return Returns the newly created node (if created) - */ --std::optional Context::newOpaqueXML(const std::string& xmlNamespace, const std::string& name, const std::optional& value) const -+std::optional Context::newOpaqueXML(const OpaqueName& name, const std::optional& value) const - { -+ // the XML node naming is "complex", we cannot really check the XML namespace for sanity here - lyd_node* out; -- auto err = lyd_new_opaq2(nullptr, m_ctx.get(), name.c_str(), value ? value->content.c_str() : nullptr, nullptr, xmlNamespace.c_str(), &out); -- -- throwIfError(err, "Couldn't create an opaque XML node '"s + name +"' from namespace '" + xmlNamespace + "'"); -+ auto err = lyd_new_opaq2(nullptr, -+ m_ctx.get(), -+ name.name.c_str(), -+ value ? value->content.c_str() : nullptr, -+ name.prefix ? name.prefix->c_str() : nullptr, -+ name.moduleOrNamespace.c_str(), -+ &out); -+ -+ throwIfError(err, "Couldn't create an opaque XML node " + name.pretty()); - - if (out) { - return DataNode{out, std::make_shared(m_ctx)}; -diff --git a/src/DataNode.cpp b/src/DataNode.cpp -index b899b18..344f1b6 100644 ---- a/src/DataNode.cpp -+++ b/src/DataNode.cpp -@@ -1112,9 +1112,9 @@ OpaqueName DataNodeOpaque::name() const - { - auto opaq = reinterpret_cast(m_node); - return OpaqueName{ -- .prefix = opaq->name.prefix ? std::optional(opaq->name.prefix) : std::nullopt, -- .name = opaq->name.name -- }; -+ .moduleOrNamespace = opaq->name.module_name, -+ .prefix = opaq->name.prefix ? std::optional{opaq->name.prefix} : std::nullopt, -+ .name = opaq->name.name}; - } - - std::string DataNodeOpaque::value() const -@@ -1122,6 +1122,19 @@ std::string DataNodeOpaque::value() const - return reinterpret_cast(m_node)->value; - } - -+std::string OpaqueName::pretty() const -+{ -+ if (prefix) { -+ if (*prefix == moduleOrNamespace) { -+ return *prefix + ':' + name; -+ } else { -+ return "{" + moduleOrNamespace + "}, " + *prefix + ':' + name; -+ } -+ } else { -+ return "{" + moduleOrNamespace + "}, " + name; -+ } -+} -+ - /** - * Wraps a raw non-null lyd_node pointer. - * @param node The pointer to be wrapped. Must not be null. -diff --git a/tests/data_node.cpp b/tests/data_node.cpp -index b6ee455..a1096a6 100644 ---- a/tests/data_node.cpp -+++ b/tests/data_node.cpp -@@ -1969,9 +1969,11 @@ TEST_CASE("Data Node manipulation") - - DOCTEST_SUBCASE("opaque nodes for sysrepo ops data discard") - { -- auto discard1 = ctx.newOpaqueJSON("sysrepo", "discard-items", libyang::JSON{"/example-schema:a"}); -+ // the "short" form with no prefix -+ auto discard1 = ctx.newOpaqueJSON(libyang::OpaqueName{"sysrepo", std::nullopt, "discard-items"}, libyang::JSON{"/example-schema:a"}); - REQUIRE(!!discard1); -- auto discard2 = ctx.newOpaqueJSON("sysrepo", "discard-items", libyang::JSON{"/example-schema:b"}); -+ // let's use a prefix form here -+ 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) -@@ -2001,16 +2003,38 @@ TEST_CASE("Data Node manipulation") - auto data = ctx.newPath2("/example-schema:myRpc/outputLeaf", "AHOJ", libyang::CreationOptions::Output).createdNode; - REQUIRE(data); - data->newPath("/example-schema:myRpc/another", "yay", libyang::CreationOptions::Output); -+ std::string prettyName; - -- DOCTEST_SUBCASE("JSON") { -- out = ctx.newOpaqueJSON(data->schema().module().name(), "output", std::nullopt); -+ DOCTEST_SUBCASE("JSON no prefix") { -+ out = ctx.newOpaqueJSON({data->schema().module().name(), std::nullopt, "output"}, std::nullopt); -+ prettyName = "{example-schema}, output"; - } - -- DOCTEST_SUBCASE("XML") { -- out = ctx.newOpaqueXML(data->schema().module().ns(), "output", std::nullopt); -+ DOCTEST_SUBCASE("JSON with prefix") { -+ out = ctx.newOpaqueJSON({data->schema().module().name(), data->schema().module().name(), "output"}, std::nullopt); -+ prettyName = "example-schema:output"; -+ -+ // wrong prefix is detected -+ REQUIRE_THROWS_WITH_AS(ctx.newOpaqueJSON({data->schema().module().name(), "xxx", "output"}, std::nullopt), -+ R"(invalid opaque JSON node: prefix "xxx" doesn't match module name "example-schema")", -+ libyang::Error); -+ } -+ -+ DOCTEST_SUBCASE("XML no prefix") { -+ out = ctx.newOpaqueXML({data->schema().module().ns(), std::nullopt, "output"}, std::nullopt); -+ prettyName = "{http://example.com/coze}, output"; -+ } -+ -+ DOCTEST_SUBCASE("XML with prefix") { -+ out = ctx.newOpaqueXML({data->schema().module().ns(), -+ data->schema().module().name() /* prefix is a module name, not the XML NS*/, -+ "output"}, -+ std::nullopt); -+ prettyName = "{http://example.com/coze}, example-schema:output"; - } - - REQUIRE(out); -+ REQUIRE(prettyName == out->asOpaque().name().pretty()); - data->unlinkWithSiblings(); - out->insertChild(*data); - --- -2.43.0 - diff --git a/package/libyang-cpp/0016-Add-a-helper-for-finding-opaque-nodes.patch b/package/libyang-cpp/0016-Add-a-helper-for-finding-opaque-nodes.patch deleted file mode 100644 index 2ea22505..00000000 --- a/package/libyang-cpp/0016-Add-a-helper-for-finding-opaque-nodes.patch +++ /dev/null @@ -1,462 +0,0 @@ -From 8c59ecc5c687f8ee2ce62825835a378b422185f2 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Fri, 14 Mar 2025 13:41:01 +0100 -Subject: [PATCH 16/18] Add a helper for finding opaque nodes -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -This is needed for sysrepo where we want to search through the -sysrepo:discard-items top-level opaque nodes. - -At first I wanted to simply wrap lyd_find_sibling_opaq_next(), but its -semantics is quite different to what is needed in the code which uses -sysrepo, so here's my attempt at a nice-ish API. - -Change-Id: I2571961e42f6d7a121e27c881cacdcfec0e87762 -Signed-off-by: Mattias Walström ---- - include/libyang-cpp/DataNode.hpp | 2 + - src/DataNode.cpp | 49 ++++++ - tests/data_node.cpp | 74 +++++++++ - tests/yang/sysrepo@2024-10-25.yang | 257 +++++++++++++++++++++++++++++ - 4 files changed, 382 insertions(+) - create mode 100644 tests/yang/sysrepo@2024-10-25.yang - -diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp -index 310b5e3..50d6c0e 100644 ---- a/include/libyang-cpp/DataNode.hpp -+++ b/include/libyang-cpp/DataNode.hpp -@@ -102,6 +102,7 @@ public: - - bool isOpaque() const; - DataNodeOpaque asOpaque() const; -+ std::optional firstOpaqueSibling() const; - - // TODO: allow setting the `parent` argument - DataNode duplicate(const std::optional opts = std::nullopt) const; -@@ -241,6 +242,7 @@ struct LIBYANG_CPP_EXPORT OpaqueName { - std::optional prefix; - std::string name; - std::string pretty() const; -+ bool matches(const std::string& prefixIsh, const std::string& name) const; - }; - - /** -diff --git a/src/DataNode.cpp b/src/DataNode.cpp -index 344f1b6..7e87917 100644 ---- a/src/DataNode.cpp -+++ b/src/DataNode.cpp -@@ -1135,6 +1135,20 @@ std::string OpaqueName::pretty() const - } - } - -+/** -+ * @short Fuzzy-match a real-world name against a combination of "something like a prefix" and "unqualified name" -+ * -+ * Because libyang doesn't propagate inherited prefixes, and because opaque nodes are magic, we seem to require -+ * this "fuzzy matching". It won't properly report a match on opaque nodes with a prefix that's inherited when -+ * using XML namespaces, though. -+ * */ -+bool OpaqueName::matches(const std::string& prefixIsh, const std::string& name) const -+{ -+ return name == this->name -+ && (prefixIsh == moduleOrNamespace -+ || (!!prefix && prefixIsh == *prefix)); -+} -+ - /** - * Wraps a raw non-null lyd_node pointer. - * @param node The pointer to be wrapped. Must not be null. -@@ -1305,4 +1319,39 @@ bool DataNode::siblingsEqual(const libyang::DataNode& other, const DataCompare f - } - } - -+/** -+ * @short Find the first opaque node among the siblings -+ * -+ * This function was inspired by `lyd_find_sibling_opaq_next()`. -+ * */ -+std::optional DataNode::firstOpaqueSibling() const -+{ -+ struct lyd_node *candidate = m_node; -+ -+ // Skip all non-opaque nodes; libyang guarantees to have them first, followed by a (possibly empty) set -+ // of opaque nodes. This is not documented anywhere, but it was explicitly confirmed by the maintainer: -+ // -+ // JK: can I rely on all non-opaque nodes being listed first among the siblings, and then all opaque nodes -+ // in one continuous sequence (but with an unspecified order among the opaque nodes themselves)? -+ // -+ // MV: yep -+ while (candidate && candidate->schema) { -+ candidate = candidate->next; -+ } -+ -+ // walk back through the opaque nodes; however, libyang lists are not your regular linked lists -+ while (candidate -+ && !candidate->prev->schema // don't go from the first opaque node through the non-opaque ones -+ && candidate->prev->next // don't wrap from the first node to the last one in case all of them are opaque -+ ) { -+ candidate = candidate->prev; -+ } -+ -+ if (candidate) { -+ return DataNode{candidate, m_refs}.asOpaque(); -+ } -+ -+ return std::nullopt; -+} -+ - } -diff --git a/tests/data_node.cpp b/tests/data_node.cpp -index a1096a6..db5a28e 100644 ---- a/tests/data_node.cpp -+++ b/tests/data_node.cpp -@@ -1982,6 +1982,80 @@ TEST_CASE("Data Node manipulation") - "sysrepo:discard-items": "/example-schema:b" - } - )"); -+ -+ // check that a list which only consists of opaque nodes doesn't break our iteration -+ REQUIRE(!!discard1->firstOpaqueSibling()); -+ REQUIRE(*discard1->firstOpaqueSibling() == *discard1); -+ REQUIRE(!!discard2->firstOpaqueSibling()); -+ REQUIRE(*discard2->firstOpaqueSibling() == *discard1); -+ -+ auto leafInt16 = ctx.newPath("/example-schema:leafInt16", "666"); -+ leafInt16.insertSibling(*discard1); -+ REQUIRE(*discard1->firstSibling().printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) -+ == R"({ -+ "example-schema:leafInt16": 666, -+ "sysrepo:discard-items": "/example-schema:a", -+ "sysrepo:discard-items": "/example-schema:b" -+} -+)"); -+ REQUIRE(leafInt16.firstSibling().path() == "/example-schema:leafInt16"); -+ REQUIRE(discard1->firstSibling().path() == "/example-schema:leafInt16"); -+ REQUIRE(discard2->firstSibling().path() == "/example-schema:leafInt16"); -+ -+ auto dummy = ctx.newPath("/example-schema:dummy", "blah"); -+ auto opaqueLeaf = ctx.newPath("/example-schema:leafInt32", std::nullopt, libyang::CreationOptions::Opaque); -+ opaqueLeaf.newAttrOpaqueJSON("ietf-netconf", "operation", "delete"); -+ dummy.insertSibling(opaqueLeaf); -+ -+ // FIXME reword this: this one might not be handled by sysrepo, but we want it for our fuzzy matcher testing anyway -+ auto discard3 = ctx.newOpaqueXML(libyang::OpaqueName{"http://www.sysrepo.org/yang/sysrepo", "sysrepo", "discard-items"}, libyang::XML{"/example-schema:c"}); -+ REQUIRE(!!discard3); -+ // notice that it's printed without a proper prefix at first... -+ REQUIRE(*discard3->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink) -+ == R"({"discard-items":"/example-schema:c"})"); -+ // ...but after loading the module, the proper module is added back -+ ctx.parseModule(TESTS_DIR / "yang" / "sysrepo@2024-10-25.yang", libyang::SchemaFormat::YANG); -+ REQUIRE(*discard3->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink) -+ == R"({"sysrepo:discard-items":"/example-schema:c"})"); -+ -+ dummy.insertSibling(*discard3); -+ leafInt16.insertSibling(dummy); -+ REQUIRE(*discard1->firstSibling().printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) -+ == R"({ -+ "example-schema:dummy": "blah", -+ "example-schema:leafInt16": 666, -+ "sysrepo:discard-items": "/example-schema:a", -+ "sysrepo:discard-items": "/example-schema:b", -+ "example-schema:leafInt32": "", -+ "@example-schema:leafInt32": { -+ "ietf-netconf:operation": "delete" -+ }, -+ "sysrepo:discard-items": "/example-schema:c" -+} -+)"); -+ -+ REQUIRE(dummy.firstOpaqueSibling() == discard1); -+ REQUIRE(dummy.firstOpaqueSibling() != discard2); -+ REQUIRE(leafInt16.firstOpaqueSibling() == discard1); -+ REQUIRE(opaqueLeaf.firstOpaqueSibling() == discard1); -+ REQUIRE(discard1->firstOpaqueSibling() == discard1); -+ REQUIRE(discard2->firstOpaqueSibling() == discard1); -+ REQUIRE(discard3->firstOpaqueSibling() == discard1); -+ REQUIRE(discard1->asOpaque().name().matches("sysrepo", "discard-items")); -+ REQUIRE(!discard1->asOpaque().name().matches("http://www.sysrepo.org/yang/sysrepo", "discard-items")); -+ REQUIRE(discard2->asOpaque().name().matches("sysrepo", "discard-items")); -+ REQUIRE(!discard2->asOpaque().name().matches("http://www.sysrepo.org/yang/sysrepo", "discard-items")); -+ REQUIRE(discard3->asOpaque().name().matches("sysrepo", "discard-items")); -+ REQUIRE(discard3->asOpaque().name().matches("http://www.sysrepo.org/yang/sysrepo", "discard-items")); -+ REQUIRE(!opaqueLeaf.asOpaque().name().matches("sysrepo", "discard-items")); -+ -+ REQUIRE(!!dummy.firstOpaqueSibling()->nextSibling()); -+ REQUIRE(dummy.firstOpaqueSibling()->nextSibling() == discard2); -+ REQUIRE(!!dummy.firstOpaqueSibling()->nextSibling()->nextSibling()); -+ REQUIRE(dummy.firstOpaqueSibling()->nextSibling()->nextSibling() == opaqueLeaf); -+ REQUIRE(!!dummy.firstOpaqueSibling()->nextSibling()->nextSibling()->nextSibling()); -+ REQUIRE(dummy.firstOpaqueSibling()->nextSibling()->nextSibling()->nextSibling() == discard3); -+ REQUIRE(!dummy.firstOpaqueSibling()->nextSibling()->nextSibling()->nextSibling()->nextSibling()); - } - - DOCTEST_SUBCASE("RESTCONF RPC output") -diff --git a/tests/yang/sysrepo@2024-10-25.yang b/tests/yang/sysrepo@2024-10-25.yang -new file mode 100644 -index 0000000..f5bc32c ---- /dev/null -+++ b/tests/yang/sysrepo@2024-10-25.yang -@@ -0,0 +1,257 @@ -+module sysrepo { -+ namespace "http://www.sysrepo.org/yang/sysrepo"; -+ prefix sr; -+ -+ yang-version 1.1; -+ -+ import ietf-yang-types { -+ prefix yang; -+ } -+ -+ import ietf-datastores { -+ prefix ds; -+ } -+ -+ import ietf-yang-metadata { -+ prefix md; -+ revision-date 2016-08-05; -+ } -+ -+ organization -+ "CESNET"; -+ -+ contact -+ "Author: Michal Vasko -+ "; -+ -+ description -+ "Sysrepo YANG datastore internal attributes and information."; -+ -+ revision "2024-10-25" { -+ description -+ "Removed redundant metadata used for push operational data."; -+ } -+ -+ revision "2019-07-10" { -+ description -+ "Initial revision."; -+ } -+ -+ typedef module-ref { -+ description -+ "Reference to a module."; -+ type leafref { -+ path "/sysrepo-modules/module/name"; -+ } -+ } -+ -+ md:annotation operation { -+ type enumeration { -+ enum none { -+ description -+ "Node with this operation must exist but does not affect the datastore in any way."; -+ reference -+ "RFC 6241 section 7.2.: default-operation"; -+ } -+ enum ether { -+ description -+ "Node with this operation does not have to exist and does not affect the datastore in any way."; -+ } -+ enum purge { -+ description -+ "Node with this operation represents an arbitrary generic node instance and all -+ the instances will be deleted."; -+ } -+ } -+ description -+ "Additional proprietary operations used internally."; -+ reference -+ "RFC 6241 section 7.2."; -+ } -+ -+ identity notification { -+ base ds:datastore; -+ description -+ "Special datastore for storing notifications for replay."; -+ } -+ -+ grouping module-info-grp { -+ leaf name { -+ type string; -+ description -+ "Module name."; -+ } -+ -+ leaf revision { -+ type string; -+ description -+ "Module revision."; -+ } -+ -+ leaf-list enabled-feature { -+ type string; -+ description -+ "List of all the enabled features."; -+ } -+ -+ list plugin { -+ key "datastore"; -+ description -+ "Module datastore plugin handling specific datastore data."; -+ -+ leaf datastore { -+ type identityref { -+ base ds:datastore; -+ } -+ description -+ "Datastore of this plugin."; -+ } -+ -+ leaf name { -+ type string; -+ mandatory true; -+ description -+ "Specific plugin name as present in the plugin structures."; -+ } -+ } -+ } -+ -+ grouping deps-grp { -+ list lref { -+ description -+ "Dependency of a leafref node."; -+ -+ leaf target-path { -+ type yang:xpath1.0; -+ mandatory true; -+ description -+ "Path identifying the leafref target node."; -+ } -+ -+ leaf target-module { -+ type module-ref; -+ mandatory true; -+ description -+ "Foreign target module of the leafref."; -+ } -+ } -+ -+ list inst-id { -+ description -+ "Dependency of an instance-identifier node."; -+ -+ leaf source-path { -+ type yang:xpath1.0; -+ mandatory true; -+ description -+ "Path identifying the instance-identifier node."; -+ } -+ -+ leaf default-target-path { -+ type yang:xpath1.0; -+ description -+ "Default instance-identifier value."; -+ } -+ } -+ -+ list xpath { -+ description -+ "Dependency of an XPath expression - must or when statement."; -+ -+ leaf expression { -+ type yang:xpath1.0; -+ mandatory true; -+ description -+ "XPath expression of the dependency - must or when statement argument."; -+ } -+ -+ leaf-list target-module { -+ type module-ref; -+ description -+ "Foreign modules with the data needed for evaluation of the XPath."; -+ } -+ } -+ } -+ -+ container sysrepo-modules { -+ config false; -+ description -+ "All installed Sysrepo modules."; -+ -+ leaf content-id { -+ type uint32; -+ mandatory true; -+ description -+ "Sysrepo module-set content-id to be used for its generated yang-library data."; -+ } -+ -+ list module { -+ key "name"; -+ description -+ "Sysrepo module."; -+ -+ uses module-info-grp; -+ -+ leaf replay-support { -+ type yang:date-and-time; -+ description -+ "Present only if the module supports replay. Means the earliest stored notification if any present. -+ Otherwise the time the replay support was switched on."; -+ } -+ -+ container deps { -+ description -+ "Module data dependencies on other modules."; -+ uses deps-grp; -+ } -+ -+ leaf-list inverse-deps { -+ type module-ref; -+ description -+ "List of modules that depend on this module."; -+ } -+ -+ list rpc { -+ key "path"; -+ description -+ "Module RPC/actions."; -+ -+ leaf path { -+ type yang:xpath1.0; -+ description -+ "Path identifying the operation."; -+ } -+ -+ container in { -+ description -+ "Operation input dependencies."; -+ uses deps-grp; -+ } -+ -+ container out { -+ description -+ "Operation output dependencies."; -+ uses deps-grp; -+ } -+ } -+ -+ list notification { -+ key "path"; -+ description -+ "Module notifications."; -+ -+ leaf path { -+ type yang:xpath1.0; -+ description -+ "Path identifying the notification."; -+ } -+ -+ container deps { -+ description -+ "Notification dependencies."; -+ uses deps-grp; -+ } -+ } -+ } -+ } -+} --- -2.43.0 - diff --git a/package/libyang-cpp/0017-build-link-to-libpcre2-explicitly.patch b/package/libyang-cpp/0017-build-link-to-libpcre2-explicitly.patch deleted file mode 100644 index 350fae95..00000000 --- a/package/libyang-cpp/0017-build-link-to-libpcre2-explicitly.patch +++ /dev/null @@ -1,56 +0,0 @@ -From dab8c9aac96063ccb8541d5d1795ee89b75faeee Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Wed, 2 Apr 2025 11:50:24 -0700 -Subject: [PATCH 17/18] build: link to libpcre2 explicitly -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -This is apparently a problem on Mac OS X builds where the transitive -dependency via libyang doesn't take effect: - - Undefined symbols for architecture arm64: - "_pcre2_code_free_8", referenced from: - libyang::Regex::~Regex() in Regex.cpp.o - libyang::Regex::~Regex() in Regex.cpp.o - ld: symbol(s) not found for architecture arm64 - -Let's fix this by linking with libpcre2-8 directly. - -We're using a different approach than libyang; they have their own CMake -wrapper. I find it easier to work with pkg-config modules, so let's try -it that way. - -Change-Id: I1a64407d852161595b7dca654433190002cc3600 -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 5 ++++- - 1 file changed, 4 insertions(+), 1 deletion(-) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index c5fec45..4e009aa 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -34,6 +34,9 @@ pkg_check_modules(LIBYANG REQUIRED libyang>=3.10.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) - -+# libyang::Regex::~Regex() bypasses libyang and calls out to libpcre directly -+pkg_check_modules(LIBPCRE2 REQUIRED libpcre2-8 IMPORTED_TARGET) -+ - include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) - - include(CheckIncludeFileCXX) -@@ -64,7 +67,7 @@ add_library(yang-cpp - src/utils/newPath.cpp - ) - --target_link_libraries(yang-cpp PRIVATE PkgConfig::LIBYANG) -+target_link_libraries(yang-cpp PRIVATE PkgConfig::LIBYANG PkgConfig::LIBPCRE2) - # We do not offer any long-term API/ABI guarantees. To make stuff easier for downstream consumers, - # we will be bumping both API and ABI versions very deliberately. - # There will be no attempts at semver tracking, for example. --- -2.43.0 - diff --git a/package/libyang-cpp/0018-CI-renamed-project-upstream.patch b/package/libyang-cpp/0018-CI-renamed-project-upstream.patch deleted file mode 100644 index f8f39669..00000000 --- a/package/libyang-cpp/0018-CI-renamed-project-upstream.patch +++ /dev/null @@ -1,66 +0,0 @@ -From 7519fcd35216072a6b1eebe2a79e19789be345a9 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Wed, 9 Apr 2025 15:35:37 +0200 -Subject: [PATCH 18/18] CI: renamed project upstream -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: I5447f243297fbfde7c364eb3919b00db239bd069 -Signed-off-by: Mattias Walström ---- - .zuul.yaml | 4 ++-- - README.md | 2 +- - ci/build.sh | 2 +- - 3 files changed, 4 insertions(+), 4 deletions(-) - -diff --git a/.zuul.yaml b/.zuul.yaml -index b41c490..7b92766 100644 ---- a/.zuul.yaml -+++ b/.zuul.yaml -@@ -5,13 +5,13 @@ - required-projects: - - name: github/CESNET/libyang - override-checkout: devel -- - name: github/onqtam/doctest -+ - name: github/doctest/doctest - override-checkout: v2.3.6 - - f38-clang-asan-ubsan: - required-projects: &projects - - name: github/CESNET/libyang - override-checkout: devel -- - name: github/onqtam/doctest -+ - name: github/doctest/doctest - override-checkout: v2.4.11 - - f38-clang-tsan: - required-projects: *projects -diff --git a/README.md b/README.md -index d76975b..8291e00 100644 ---- a/README.md -+++ b/README.md -@@ -11,7 +11,7 @@ Object lifetimes are managed automatically via RAII. - - [libyang v3](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/onqtam/doctest/) as a C++ unit test framework -+- optionally for built-in tests, [Doctest](https://github.com/doctest/doctest/) as a C++ unit test framework - - optionally for the docs, Doxygen - - ## Building -diff --git a/ci/build.sh b/ci/build.sh -index d06b646..0867f07 100755 ---- a/ci/build.sh -+++ b/ci/build.sh -@@ -73,7 +73,7 @@ build_n_test() { - } - - build_n_test github/CESNET/libyang -DENABLE_BUILD_TESTS=ON -DENABLE_VALGRIND_TESTS=OFF --build_n_test github/onqtam/doctest -DDOCTEST_WITH_TESTS=OFF -+build_n_test github/doctest/doctest -DDOCTEST_WITH_TESTS=OFF - build_n_test ${ZUUL_PROJECT_NAME} -DBUILD_TESTING=ON - - pushd ${BUILD_DIR}/${ZUUL_PROJECT_NAME} --- -2.43.0 - diff --git a/package/libyang-cpp/libyang-cpp.hash b/package/libyang-cpp/libyang-cpp.hash index f8a79234..5b32439d 100644 --- a/package/libyang-cpp/libyang-cpp.hash +++ b/package/libyang-cpp/libyang-cpp.hash @@ -1,3 +1,3 @@ # Locally calculated sha256 82e3758011ec44c78e98d0777799d6e12aec5b8a64b32ebb20d0fe50e32488bb LICENSE -sha256 43c84317ba13470f421a90bca74c062cf63a70e488d2e90c432b662c4638a14e libyang-cpp-v3.tar.gz +sha256 70fd0df940026fb930d5407abe679e064caf4701bff35d79465b9ad2c0915808 libyang-cpp-v4.tar.gz diff --git a/package/libyang-cpp/libyang-cpp.mk b/package/libyang-cpp/libyang-cpp.mk index 83df0d4f..01df2afe 100644 --- a/package/libyang-cpp/libyang-cpp.mk +++ b/package/libyang-cpp/libyang-cpp.mk @@ -3,7 +3,7 @@ # CPP bindings for libyang # ################################################################################ -LIBYANG_CPP_VERSION = v3 +LIBYANG_CPP_VERSION = v4 LIBYANG_CPP_SITE = $(call github,CESNET,libyang-cpp,$(LIBYANG_CPP_VERSION)) LIBYANG_CPP_LICENSE = BSD-3-Clause LIBYANG_CPP_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 new file mode 100644 index 00000000..ec9e1889 --- /dev/null +++ b/package/rousette/0001-Log-HTTP-headers-and-the-input-data-payload.patch @@ -0,0 +1,64 @@ +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/17] Log HTTP headers and the input data payload +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +...so that we can debug the results of a nasty lockup that Michal +Hažlinský just found. + +Change-Id: I2a930a02c7d30c051390fe73e6af9849edd580b4 +Signed-off-by: Mattias Walström +--- + src/restconf/Server.cpp | 10 ++++++++-- + 1 file changed, 8 insertions(+), 2 deletions(-) + +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 272ecc1..9821495 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -44,6 +44,9 @@ void logRequest(const auto& request) + { + const auto& peer = http::peer_from_request(request); + spdlog::info("{}: {} {}", peer, request.method(), request.uri().raw_path); ++ for (const auto& hdr: request.header()) { ++ spdlog::trace("{}: header: {}: {}", peer, hdr.first, hdr.second.sensitive ? ""s : hdr.second.value); ++ } + } + + template +@@ -1079,12 +1082,14 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: + + auto requestCtx = std::make_shared(req, res, dataFormat, sess, restconfRequest); + +- req.on_data([requestCtx, restconfRequest /* intentional copy */, timeout](const uint8_t* data, std::size_t length) { ++ req.on_data([requestCtx, restconfRequest /* intentional copy */, timeout, peer=http::peer_from_request(req)](const uint8_t* data, std::size_t length) { + if (length > 0) { // there are still some data to be read + requestCtx->payload.append(reinterpret_cast(data), length); + return; + } + ++ spdlog::trace("{}: HTTP payload: {}", peer, requestCtx->payload); ++ + if (restconfRequest.type == RestconfRequest::Type::CreateChildren) { + WITH_RESTCONF_EXCEPTIONS(processPost, rejectWithError)(requestCtx, timeout); + } else if (restconfRequest.type == RestconfRequest::Type::MergeData && isYangPatch(requestCtx->req)) { +@@ -1143,10 +1148,11 @@ 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](const uint8_t* data, std::size_t length) { ++ req.on_data([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); + } + }); +-- +2.43.0 + diff --git a/package/rousette/0001-restconf-report-the-list-key-values-when-they-differ.patch b/package/rousette/0001-restconf-report-the-list-key-values-when-they-differ.patch deleted file mode 100644 index 67469b1b..00000000 --- a/package/rousette/0001-restconf-report-the-list-key-values-when-they-differ.patch +++ /dev/null @@ -1,199 +0,0 @@ -From 9622a68eba4aeaa60619b4c33d050ce91b27653d Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Tue, 8 Oct 2024 12:22:49 +0200 -Subject: [PATCH 01/44] restconf: report the list key values when they differ - between URI and data -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -For creating (leaf-)list instances with PUT and PATCH methods one has -to specify the list key values in the URI and in the data as well. -Those values must be the same. In case they are not, it is an error. -We reported that the values mismatch in case this happens, but the error -message did not report what the values were. -Knowing that might be beneficial when one is about to insert key values -that can be namespace qualified (like identityrefs) and that are -sometimes manipulated by libyang (e.g., when the identity belongs to the -same namespace as the list node, it is not necessary for it to be -namespace qualified by the client, but libyang adds the namespace -internally). - -Change-Id: Ie0d42511bde01ab4c39d61370b6601f8808e40c5 -Signed-off-by: Mattias Walström ---- - src/restconf/Server.cpp | 29 +++++++++++++++++++++-------- - tests/restconf-plain-patch.cpp | 2 +- - tests/restconf-writing.cpp | 14 +++++++------- - tests/restconf-yang-patch.cpp | 2 +- - 4 files changed, 30 insertions(+), 17 deletions(-) - -diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp -index 53d6625..5f560ed 100644 ---- a/src/restconf/Server.cpp -+++ b/src/restconf/Server.cpp -@@ -154,9 +154,22 @@ auto rejectYangPatch(const std::string& patchId, const std::string& editId) - }; - } - -+struct KeyMismatch { -+ libyang::DataNode offendingNode; -+ std::optional uriKeyValue; -+ -+ std::string message() const { -+ if (uriKeyValue) { -+ return "List key mismatch between URI path ('"s + *uriKeyValue + "') and data ('" + offendingNode.asTerm().valueStr() + "')."; -+ } else { -+ return "List key mismatch (key missing in the data)."; -+ } -+ } -+}; -+ - /** @brief In case node is a (leaf-)list check if the key values are the same as the keys specified in the lastPathSegment. - * @return The node where the mismatch occurs */ --std::optional checkKeysMismatch(const libyang::DataNode& node, const PathSegment& lastPathSegment) -+std::optional checkKeysMismatch(const libyang::DataNode& node, const PathSegment& lastPathSegment) - { - if (node.schema().nodeType() == libyang::NodeType::List) { - const auto& listKeys = node.schema().asList().keys(); -@@ -164,18 +177,18 @@ std::optional checkKeysMismatch(const libyang::DataNode& node - const auto& keyValueURI = lastPathSegment.keys[i]; - auto keyNodeData = node.findPath(listKeys[i].module().name() + ':' + listKeys[i].name()); - if (!keyNodeData) { -- return node; -+ return KeyMismatch{node, std::nullopt}; - } - - const auto& keyValueData = keyNodeData->asTerm().valueStr(); - - if (keyValueURI != keyValueData) { -- return keyNodeData; -+ return KeyMismatch{*keyNodeData, keyValueURI}; - } - } - } else if (node.schema().nodeType() == libyang::NodeType::Leaflist) { - if (lastPathSegment.keys[0] != node.asTerm().valueStr()) { -- return node; -+ return KeyMismatch{node, lastPathSegment.keys[0]}; - } - } - return std::nullopt; -@@ -350,8 +363,8 @@ libyang::CreatedNodes createEditForPutAndPatch(libyang::Context& ctx, const std: - if (isSameNode(child, lastPathSegment)) { - // 1) a single child that is created by parseSubtree(), its name is the same as `lastPathSegment`. - // It could be a list; then we need to check if the keys in provided data match the keys in URI. -- if (auto offendingNode = checkKeysMismatch(child, lastPathSegment)) { -- throw ErrorResponse(400, "protocol", "invalid-value", "List key mismatch between URI path and data.", offendingNode->path()); -+ if (auto keyMismatch = checkKeysMismatch(child, lastPathSegment)) { -+ throw ErrorResponse(400, "protocol", "invalid-value", keyMismatch->message(), keyMismatch->offendingNode.path()); - } - replacementNode = child; - } else if (isKeyNode(*node, child)) { -@@ -373,8 +386,8 @@ libyang::CreatedNodes createEditForPutAndPatch(libyang::Context& ctx, const std: - if (!isSameNode(*replacementNode, lastPathSegment)) { - throw ErrorResponse(400, "protocol", "invalid-value", "Data contains invalid node.", replacementNode->path()); - } -- if (auto offendingNode = checkKeysMismatch(*parent, lastPathSegment)) { -- throw ErrorResponse(400, "protocol", "invalid-value", "List key mismatch between URI path and data.", offendingNode->path()); -+ if (auto keyMismatch = checkKeysMismatch(*parent, lastPathSegment)) { -+ throw ErrorResponse(400, "protocol", "invalid-value", keyMismatch->message(), keyMismatch->offendingNode.path()); - } - } - } -diff --git a/tests/restconf-plain-patch.cpp b/tests/restconf-plain-patch.cpp -index 10d653a..d4f3952 100644 ---- a/tests/restconf-plain-patch.cpp -+++ b/tests/restconf-plain-patch.cpp -@@ -72,7 +72,7 @@ TEST_CASE("Plain patch") - "error-type": "protocol", - "error-tag": "invalid-value", - "error-path": "/example:tlc/list[name='blabla']/name", -- "error-message": "List key mismatch between URI path and data." -+ "error-message": "List key mismatch between URI path ('libyang') and data ('blabla')." - } - ] - } -diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp -index d46952f..96dbb25 100644 ---- a/tests/restconf-writing.cpp -+++ b/tests/restconf-writing.cpp -@@ -432,7 +432,7 @@ TEST_CASE("writing data") - "error-type": "protocol", - "error-tag": "invalid-value", - "error-path": "/example:tlc/list[name='ahoj']/name", -- "error-message": "List key mismatch between URI path and data." -+ "error-message": "List key mismatch between URI path ('netconf') and data ('ahoj')." - } - ] - } -@@ -447,7 +447,7 @@ TEST_CASE("writing data") - "error-type": "protocol", - "error-tag": "invalid-value", - "error-path": "/example:top-level-list[name='ahoj']/name", -- "error-message": "List key mismatch between URI path and data." -+ "error-message": "List key mismatch between URI path ('netconf') and data ('ahoj')." - } - ] - } -@@ -505,7 +505,7 @@ TEST_CASE("writing data") - "error-type": "protocol", - "error-tag": "invalid-value", - "error-path": "/example:tlc/list[name='netconf']/collection[.='666']", -- "error-message": "List key mismatch between URI path and data." -+ "error-message": "List key mismatch between URI path ('667') and data ('666')." - } - ] - } -@@ -520,7 +520,7 @@ TEST_CASE("writing data") - "error-type": "protocol", - "error-tag": "invalid-value", - "error-path": "/example:top-level-leaf-list[.='666']", -- "error-message": "List key mismatch between URI path and data." -+ "error-message": "List key mismatch between URI path ('667') and data ('666')." - } - ] - } -@@ -535,7 +535,7 @@ TEST_CASE("writing data") - "error-type": "protocol", - "error-tag": "invalid-value", - "error-path": "/example:tlc/list[name='sysrepo']/name", -- "error-message": "List key mismatch between URI path and data." -+ "error-message": "List key mismatch between URI path ('netconf') and data ('sysrepo')." - } - ] - } -@@ -550,7 +550,7 @@ TEST_CASE("writing data") - "error-type": "protocol", - "error-tag": "invalid-value", - "error-path": "/example:tlc/list[name='sysrepo']/name", -- "error-message": "List key mismatch between URI path and data." -+ "error-message": "List key mismatch between URI path ('netconf') and data ('sysrepo')." - } - ] - } -@@ -565,7 +565,7 @@ TEST_CASE("writing data") - "error-type": "protocol", - "error-tag": "invalid-value", - "error-path": "/example:tlc/list[name='libyang']/collection[.='42']", -- "error-message": "List key mismatch between URI path and data." -+ "error-message": "List key mismatch between URI path ('5') and data ('42')." - } - ] - } -diff --git a/tests/restconf-yang-patch.cpp b/tests/restconf-yang-patch.cpp -index 9d70912..7cc8946 100644 ---- a/tests/restconf-yang-patch.cpp -+++ b/tests/restconf-yang-patch.cpp -@@ -436,7 +436,7 @@ TEST_CASE("YANG patch") - "error-type": "protocol", - "error-tag": "invalid-value", - "error-path": "/example:tlc/list[name='asdasdauisbdhaijbsdad']/name", -- "error-message": "List key mismatch between URI path and data." -+ "error-message": "List key mismatch between URI path ('libyang') and data ('asdasdauisbdhaijbsdad')." - } - ] - } --- -2.43.0 - diff --git a/package/rousette/0002-restconf-prevent-throwing-exception-in-withRestconfE.patch b/package/rousette/0002-restconf-prevent-throwing-exception-in-withRestconfE.patch new file mode 100644 index 00000000..3b3c8ac9 --- /dev/null +++ b/package/rousette/0002-restconf-prevent-throwing-exception-in-withRestconfE.patch @@ -0,0 +1,140 @@ +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/17] 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 +exception is thrown, rousette will start behaving in a strange way +where no new connections are accepted and already connected clients are +not receiving the content [1]. + +This patch prevents throwing the exception that would slip through our +catch handlers. +An uncaught exception is a problem though, so expect a followup patch to +handle such situations. + +[1] https://github.com/CESNET/rousette/issues/19 + +Bug: https://github.com/CESNET/rousette/issues/19 +Change-Id: Ifbd74b9bdc0ca66c4e5449a7673ef2f12ae9215e +Signed-off-by: Mattias Walström +--- + src/restconf/Server.cpp | 2 +- + tests/restconf-plain-patch.cpp | 24 ++++++++++++++++++++++++ + tests/restconf-reading.cpp | 3 +++ + tests/yang/example.yang | 19 +++++++++++++++++++ + 4 files changed, 47 insertions(+), 1 deletion(-) + +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 9821495..1c2123e 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -350,7 +350,7 @@ constexpr auto withRestconfExceptions(T func, U rejectWithError) + } else if (e.code() == sysrepo::ErrorCode::ItemAlreadyExists) { + rejectWithError(requestCtx->sess.getContext(), requestCtx->dataFormat.response, requestCtx->req, requestCtx->res, 409, "application", "resource-denied", "Resource already exists.", std::nullopt); + } else if (e.code() == sysrepo::ErrorCode::ValidationFailed) { +- bool isAction = requestCtx->sess.getContext().findPath(requestCtx->restconfRequest.path).nodeType() == libyang::NodeType::Action; ++ bool isAction = requestCtx->restconfRequest.path != "/" && requestCtx->sess.getContext().findPath(requestCtx->restconfRequest.path).nodeType() == libyang::NodeType::Action; + /* + * FIXME: This happens on invalid input data (e.g., missing mandatory nodes) or missing action data node. + * The former (invalid input data) should probably be validated by libyang's parseOp but it only parses. +diff --git a/tests/restconf-plain-patch.cpp b/tests/restconf-plain-patch.cpp +index b550f54..34813d1 100644 +--- a/tests/restconf-plain-patch.cpp ++++ b/tests/restconf-plain-patch.cpp +@@ -179,5 +179,29 @@ TEST_CASE("Plain patch") + ] + } + } ++)"}); ++ ++ REQUIRE(patch(RESTCONF_ROOT_DS("running"), {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({ ++ "example:channel-plan": { ++ "channel": [ ++ { ++ "name": "coriant", ++ "lower-frequency": 199999999, ++ "upper-frequency": 191500000 ++ } ++ ] ++ } ++} ++)") == Response{400, jsonHeaders, R"({ ++ "ietf-restconf:errors": { ++ "error": [ ++ { ++ "error-type": "application", ++ "error-tag": "operation-failed", ++ "error-message": "Validation failed. Invalid input data." ++ } ++ ] ++ } ++} + )"}); + } +diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp +index f87c4f5..4f0d2ee 100644 +--- a/tests/restconf-reading.cpp ++++ b/tests/restconf-reading.cpp +@@ -62,6 +62,7 @@ TEST_CASE("reading data") + // this relies on a NACM rule for anonymous access that filters out "a lot of stuff" + REQUIRE(get(RESTCONF_DATA_ROOT, {}) == Response{200, jsonHeaders, R"({ + "example:top-level-leaf": "moo", ++ "example:channel-plan": {}, + "example:tlc": {}, + "example:a": { + "b": { +@@ -119,6 +120,7 @@ TEST_CASE("reading data") + + REQUIRE(get(RESTCONF_ROOT_DS("operational"), {}) == Response{200, jsonHeaders, R"({ + "example:top-level-leaf": "moo", ++ "example:channel-plan": {}, + "example:tlc": {}, + "example:a": { + "b": { +@@ -176,6 +178,7 @@ TEST_CASE("reading data") + + REQUIRE(get(RESTCONF_ROOT_DS("running"), {}) == Response{200, jsonHeaders, R"({ + "example:top-level-leaf": "moo", ++ "example:channel-plan": {}, + "example:tlc": {}, + "example:a": { + "b": { +diff --git a/tests/yang/example.yang b/tests/yang/example.yang +index 5d586a0..1cd12e3 100644 +--- a/tests/yang/example.yang ++++ b/tests/yang/example.yang +@@ -27,6 +27,25 @@ module example { + } + leaf-list top-level-leaf-list { type int32; } + ++ container channel-plan { ++ list channel { ++ key "name"; ++ leaf name { type string; } ++ leaf lower-frequency { ++ type int32; ++ units "Hz"; ++ } ++ leaf upper-frequency { ++ type int32; ++ units "Hz"; ++ } ++ ++ must "lower-frequency < upper-frequency" { ++ description "The lower frequency must be less than the upper frequency."; ++ } ++ } ++ } ++ + container tlc { + if-feature f1; + list list { +-- +2.43.0 + diff --git a/package/rousette/0002-uri-rename-url-encoding-to-percent-encoding.patch b/package/rousette/0002-uri-rename-url-encoding-to-percent-encoding.patch deleted file mode 100644 index e0dc5581..00000000 --- a/package/rousette/0002-uri-rename-url-encoding-to-percent-encoding.patch +++ /dev/null @@ -1,50 +0,0 @@ -From 070cffb48fda789910581930265d4624a7213e1b Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Wed, 16 Oct 2024 11:02:45 +0200 -Subject: [PATCH 02/44] uri: rename url-encoding to percent-encoding -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -RFC 3986 [1] calls it the percent-encoding, let's be consistent. - -[1] https://datatracker.ietf.org/doc/html/rfc3986 - -Change-Id: Iee8b76c980b2694b6643e627b462f8bfc2c21c45 -Signed-off-by: Mattias Walström ---- - src/restconf/uri.cpp | 6 +++--- - 1 file changed, 3 insertions(+), 3 deletions(-) - -diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp -index 6e27168..b144d92 100644 ---- a/src/restconf/uri.cpp -+++ b/src/restconf/uri.cpp -@@ -29,12 +29,12 @@ auto add = [](auto& ctx) { - char c = std::tolower(_attr(ctx)); - _val(ctx) = _val(ctx) * 16 + (c >= 'a' ? c - 'a' + 10 : c - '0'); - }; --const auto urlEncodedChar = x3::rule{"urlEncodedChar"} = x3::lit('%')[set_zero] >> x3::xdigit[add] >> x3::xdigit[add]; -+const auto percentEncodedChar = x3::rule{"percentEncodedChar"} = x3::lit('%')[set_zero] >> x3::xdigit[add] >> x3::xdigit[add]; - - /* reserved characters according to RFC 3986, sec. 2.2 with '%' added. The '%' character is not specified as reserved but it effectively is because - * "Percent sign serves as the indicator for percent-encoded octets, it must be percent-encoded (...)" [RFC 3986, sec. 2.4]. */ - const auto reservedChars = x3::lit(':') | '/' | '?' | '#' | '[' | ']' | '@' | '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ',' | ';' | '=' | '%'; --const auto keyValue = x3::rule{"keyValue"} = *(urlEncodedChar | (x3::char_ - reservedChars)); -+const auto keyValue = x3::rule{"keyValue"} = *(percentEncodedChar | (x3::char_ - reservedChars)); - - const auto keyList = x3::rule>{"keyList"} = keyValue % ','; - const auto identifier = x3::rule{"identifier"} = (x3::alpha | x3::char_('_')) >> *(x3::alnum | x3::char_('_') | x3::char_('-') | x3::char_('.')); -@@ -117,7 +117,7 @@ const auto dateAndTime = x3::rule{"dateAndTime"} - x3::repeat(4)[x3::digit] >> x3::char_('-') >> x3::repeat(2)[x3::digit] >> x3::char_('-') >> x3::repeat(2)[x3::digit] >> x3::char_('T') >> - x3::repeat(2)[x3::digit] >> x3::char_(':') >> x3::repeat(2)[x3::digit] >> x3::char_(':') >> x3::repeat(2)[x3::digit] >> -(x3::char_('.') >> +x3::digit) >> - (x3::char_('Z') | (-(x3::char_('+')|x3::char_('-')) >> x3::repeat(2)[x3::digit] >> x3::char_(':') >> x3::repeat(2)[x3::digit])); --const auto filter = x3::rule{"filter"} = +(urlEncodedChar | (x3::char_ - '&')); -+const auto filter = x3::rule{"filter"} = +(percentEncodedChar | (x3::char_ - '&')); - const auto depthParam = x3::rule{"depthParam"} = x3::uint_[validDepthValues] | (x3::string("unbounded") >> x3::attr(queryParams::UnboundedDepth{})); - const auto queryParamPair = x3::rule>{"queryParamPair"} = - (x3::string("depth") >> "=" >> depthParam) | --- -2.43.0 - 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..202864fe --- /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/17] 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-uri-correct-x3-rule-class-names.patch b/package/rousette/0003-uri-correct-x3-rule-class-names.patch deleted file mode 100644 index c49caea6..00000000 --- a/package/rousette/0003-uri-correct-x3-rule-class-names.patch +++ /dev/null @@ -1,34 +0,0 @@ -From 8a233202647f24538f2bbb8fff1e38d52e3599a4 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Wed, 16 Oct 2024 11:05:09 +0200 -Subject: [PATCH 03/44] uri: correct x3 rule class names -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Fixes: e06d5bf ("server: parser for data resources in URI paths") -Change-Id: Ic953e568d841032113ede1c0e896574361c0ebe2 -Signed-off-by: Mattias Walström ---- - src/restconf/uri.cpp | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - -diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp -index b144d92..8e8dc23 100644 ---- a/src/restconf/uri.cpp -+++ b/src/restconf/uri.cpp -@@ -37,8 +37,8 @@ const auto reservedChars = x3::lit(':') | '/' | '?' | '#' | '[' | ']' | '@' | '! - const auto keyValue = x3::rule{"keyValue"} = *(percentEncodedChar | (x3::char_ - reservedChars)); - - const auto keyList = x3::rule>{"keyList"} = keyValue % ','; --const auto identifier = x3::rule{"identifier"} = (x3::alpha | x3::char_('_')) >> *(x3::alnum | x3::char_('_') | x3::char_('-') | x3::char_('.')); --const auto apiIdentifier = x3::rule{"apiIdentifier"} = -(identifier >> ':') >> identifier; -+const auto identifier = x3::rule{"identifier"} = (x3::alpha | x3::char_('_')) >> *(x3::alnum | x3::char_('_') | x3::char_('-') | x3::char_('.')); -+const auto apiIdentifier = x3::rule{"apiIdentifier"} = -(identifier >> ':') >> identifier; - const auto listInstance = x3::rule{"listInstance"} = apiIdentifier >> -('=' >> keyList); - const auto fullyQualifiedApiIdentifier = x3::rule{"apiIdentifier"} = identifier >> ':' >> identifier; - const auto fullyQualifiedListInstance = x3::rule{"listInstance"} = fullyQualifiedApiIdentifier >> -('=' >> keyList); --- -2.43.0 - diff --git a/package/rousette/0004-restconf-crash-instead-of-a-deadlock-when-the-handle.patch b/package/rousette/0004-restconf-crash-instead-of-a-deadlock-when-the-handle.patch new file mode 100644 index 00000000..452f9039 --- /dev/null +++ b/package/rousette/0004-restconf-crash-instead-of-a-deadlock-when-the-handle.patch @@ -0,0 +1,172 @@ +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 04/17] restconf: crash instead of a deadlock when the handler + throws +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +In case there is an exception thrown in the handler function and it is +not caught by our code, rousette starts to behave in a strange way where +no new connections are accepted and already connected clients are not +receiving the content [1]. + +It turns out, that nghttp2-asio starts the boost::asio::io_service +wrapped inside std::async [2]. This means the thread terminated, and is +waiting for somebody to get the result of the future [3]. But nobody +did that in our code. + +Luckily, the (hot-)fix proposed by Jan is quite simple. Rousette uses +only one io_service, so we can just call server.join() (which itself +calls future::get) instead of the pause() in the main function. If the +handler throws, we pick the result right away and rousette crashes. +(The crash is intentional and in line with the behaviour of our other +daemons in our systems. Daemon crashes, we log it and systemd restarts +it). + +Note that to enable graceful shutdown, it was necessary to guard against +double call to server->join(), because future::get does not like being +called twice [3]. + +[1] https://github.com/CESNET/rousette/issues/19 +[2] https://github.com/nghttp2/nghttp2-asio/blob/a057ffaa207fbe9584565ed503c2ee5c7e609747/lib/asio_io_service_pool.cc#L57 +[3] https://en.cppreference.com/w/cpp/thread/future/get + +Signed-off-by: Jan Kundrát +Bug: https://github.com/CESNET/rousette/issues/19 +Change-Id: I2c090b9a76b062101ba422a7d50e8e699779e203 +Signed-off-by: Mattias Walström +--- + src/restconf/Server.cpp | 28 ++++++++++++++++++++++++++++ + src/restconf/Server.h | 4 ++++ + src/restconf/main.cpp | 15 ++++++++++----- + 3 files changed, 42 insertions(+), 5 deletions(-) + +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 1c2123e..4255540 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -826,6 +826,15 @@ bool isYangPatch(const nghttp2::asio_http2::server::request& req) + } + + Server::~Server() ++{ ++ stop(); ++ ++ if (!joined) { ++ server->join(); ++ } ++} ++ ++void Server::stop() + { + // notification to stop has to go through the asio io_context + for (const auto& service : server->io_services()) { +@@ -836,8 +845,25 @@ Server::~Server() + }); + t.cancel(); + } ++} ++ ++void Server::join() ++{ ++ /* FIXME: nghttp2-asio is calling io.run() wrapped in std::async. ++ * In case the handler function throws, the exception is not propagated to the main thread *until* someone calls server.join() which calls future.get() on all io.run() wrappers. ++ * Thankfully, we have only one thread, so we can just call join() right away. Underlying future.get() blocks until io.run() finishes, either gracefully or upon uncaught exception. ++ * ++ * !!! Will not work server uses multiple threads !!! ++ */ + ++ // main thread waits here + server->join(); ++ joined = true; ++} ++ ++std::vector> Server::io_services() const ++{ ++ return server->io_services(); + } + + Server::Server(sysrepo::Connection conn, const std::string& address, const std::string& port, const std::chrono::milliseconds timeout) +@@ -846,6 +872,8 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: + , server{std::make_unique()} + , 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 ++ + for (const auto& [module, version, features] : { + std::tuple>{"ietf-restconf", "2017-01-26", {}}, + {"ietf-restconf-monitoring", "2017-01-26", {}}, +diff --git a/src/restconf/Server.h b/src/restconf/Server.h +index d1b2187..ad53d4c 100644 +--- a/src/restconf/Server.h ++++ b/src/restconf/Server.h +@@ -30,6 +30,9 @@ class Server { + public: + explicit Server(sysrepo::Connection conn, const std::string& address, const std::string& port, const std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); + ~Server(); ++ void join(); ++ void stop(); ++ std::vector> io_services() const; + + private: + sysrepo::Session m_monitoringSession; +@@ -39,6 +42,7 @@ private: + std::unique_ptr dwdmEvents; + using JsonDiffSignal = boost::signals2::signal; + JsonDiffSignal opticsChange; ++ bool joined = false; // true if the server has been joined, join twice is an error + }; + } + } +diff --git a/src/restconf/main.cpp b/src/restconf/main.cpp +index 6029e08..477e846 100644 +--- a/src/restconf/main.cpp ++++ b/src/restconf/main.cpp +@@ -5,13 +5,13 @@ + * + */ + +-#include + #include + #include + #include + + #include "configure.cmake.h" /* Expose HAVE_SYSTEMD */ + ++#include + #ifdef HAVE_SYSTEMD + #include + #endif +@@ -19,7 +19,6 @@ + #include + #include + #include +-#include + #include + #include + #include +@@ -106,9 +105,15 @@ int main(int argc, char* argv []) + + auto conn = sysrepo::Connection{}; + auto server = rousette::restconf::Server{conn, "::1", "10080", timeout}; +- signal(SIGTERM, [](int) {}); +- signal(SIGINT, [](int) {}); +- pause(); ++ ++ // allow graceful shutdown ++ boost::asio::signal_set signals(*server.io_services()[0], SIGTERM, SIGINT); ++ signals.async_wait([&](const boost::system::error_code& ec, int) { ++ if (!ec) { ++ server.stop(); ++ } ++ }); ++ server.join(); + + return 0; + } +-- +2.43.0 + diff --git a/package/rousette/0004-restconf-parser-should-work-on-raw-percent-encoded-p.patch b/package/rousette/0004-restconf-parser-should-work-on-raw-percent-encoded-p.patch deleted file mode 100644 index d5558e61..00000000 --- a/package/rousette/0004-restconf-parser-should-work-on-raw-percent-encoded-p.patch +++ /dev/null @@ -1,153 +0,0 @@ -From 96cbf730010ee9539d05d0d72697dc960b3a938c Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 7 Oct 2024 20:46:24 +0200 -Subject: [PATCH 04/44] restconf: parser should work on raw percent-encoded - paths -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -The difference between nghttp2's uri_ref::path and raw_path is that the -raw_path keeps the percent encoding, while the path converts the percent -encoded chars to their "normal" form. - -Our parser expects some parts of the URI to be percent encoded so we -have to use raw_paths everywhere. - -I thought about rewriting the parser not to expect percent encoded -characters but that would bring some complications. For instance when -querying lists, the RESTCONF RFC specifies that every key value is -percent encoded and individual key values are delimited by commas [1]. -So, when somebody sends a request like /restconf/data/list=a%2Cb,c the -"normal" form is /restconf/data/list=a,b,c and in that case we obtain -three keys, but the client sent only two, where the first one contained -comma. - -I am adding few tests to check for percent encoded values. - -We realized this while working on one of the reported bugs [1]. The -query sent by the client there is wrong, the ':' char should be -percent-encoded. - -[1] https://github.com/CESNET/rousette/issues/12 - -Bug: https://github.com/CESNET/rousette/issues/12 -Change-Id: I473501cef3c8eae9af0c5d0751393cdad647e23c -Signed-off-by: Mattias Walström ---- - src/restconf/Server.cpp | 8 ++++---- - src/restconf/uri.cpp | 4 ++-- - tests/restconf-reading.cpp | 15 +++++++++++++++ - tests/uri-parser.cpp | 5 +++-- - 4 files changed, 24 insertions(+), 8 deletions(-) - -diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp -index 5f560ed..79d8ff6 100644 ---- a/src/restconf/Server.cpp -+++ b/src/restconf/Server.cpp -@@ -416,7 +416,7 @@ void processActionOrRPC(std::shared_ptr requestCtx, const std::c - * - The data node exists but might get deleted right after this check: Sysrepo throws an error when this happens. - * - The data node does not exist but might get created right after this check: The node was not there when the request was issues so it should not be a problem - */ -- auto [pathToParent, pathSegment] = asLibyangPathSplit(ctx, requestCtx->req.uri().path); -+ auto [pathToParent, pathSegment] = asLibyangPathSplit(ctx, requestCtx->req.uri().raw_path); - if (!requestCtx->sess.getData(pathToParent, 0, sysrepo::GetOptions::Default, timeout)) { - throw ErrorResponse(400, "application", "operation-failed", "Action data node '" + requestCtx->restconfRequest.path + "' does not exist."); - } -@@ -539,7 +539,7 @@ void processYangPatchEdit(const std::shared_ptr& requestCtx, con - auto target = childLeafValue(editContainer, "target"); - auto operation = childLeafValue(editContainer, "operation"); - -- auto [singleEdit, replacementNode] = createEditForPutAndPatch(ctx, requestCtx->req.uri().path + target, yangPatchValueAsJSON(editContainer), libyang::DataFormat::JSON); -+ auto [singleEdit, replacementNode] = createEditForPutAndPatch(ctx, requestCtx->req.uri().raw_path + target, yangPatchValueAsJSON(editContainer), libyang::DataFormat::JSON); - validateInputMetaAttributes(ctx, *singleEdit); - - // insert and move are not defined in RFC6241. sec 7.3 and sysrepo does not support them directly -@@ -658,7 +658,7 @@ void processPutOrPlainPatch(std::shared_ptr requestCtx, const st - throw ErrorResponse(400, "protocol", "invalid-value", "Target resource does not exist"); - } - -- auto [edit, replacementNode] = createEditForPutAndPatch(ctx, requestCtx->req.uri().path, requestCtx->payload, *requestCtx->dataFormat.request /* caller checks if the dataFormat.request is present */); -+ auto [edit, replacementNode] = createEditForPutAndPatch(ctx, requestCtx->req.uri().raw_path, requestCtx->payload, *requestCtx->dataFormat.request /* caller checks if the dataFormat.request is present */); - validateInputMetaAttributes(ctx, *edit); - - if (requestCtx->req.method() == "PUT") { -@@ -954,7 +954,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: - dataFormat = chooseDataEncoding(req.header()); - authorizeRequest(nacm, sess, req); - -- auto restconfRequest = asRestconfRequest(sess.getContext(), req.method(), req.uri().path, req.uri().raw_query); -+ auto restconfRequest = asRestconfRequest(sess.getContext(), req.method(), req.uri().raw_path, req.uri().raw_query); - - switch (restconfRequest.type) { - case RestconfRequest::Type::RestconfRoot: -diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp -index 8e8dc23..ac399b7 100644 ---- a/src/restconf/uri.cpp -+++ b/src/restconf/uri.cpp -@@ -34,9 +34,9 @@ const auto percentEncodedChar = x3::rule{"pe - /* reserved characters according to RFC 3986, sec. 2.2 with '%' added. The '%' character is not specified as reserved but it effectively is because - * "Percent sign serves as the indicator for percent-encoded octets, it must be percent-encoded (...)" [RFC 3986, sec. 2.4]. */ - const auto reservedChars = x3::lit(':') | '/' | '?' | '#' | '[' | ']' | '@' | '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ',' | ';' | '=' | '%'; --const auto keyValue = x3::rule{"keyValue"} = *(percentEncodedChar | (x3::char_ - reservedChars)); -+const auto percentEncodedString = x3::rule{"percentEncodedString"} = *(percentEncodedChar | (x3::char_ - reservedChars)); - --const auto keyList = x3::rule>{"keyList"} = keyValue % ','; -+const auto keyList = x3::rule>{"keyList"} = percentEncodedString % ','; - const auto identifier = x3::rule{"identifier"} = (x3::alpha | x3::char_('_')) >> *(x3::alnum | x3::char_('_') | x3::char_('-') | x3::char_('.')); - const auto apiIdentifier = x3::rule{"apiIdentifier"} = -(identifier >> ':') >> identifier; - const auto listInstance = x3::rule{"listInstance"} = apiIdentifier >> -('=' >> keyList); -diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp -index fa1cbcc..2898839 100644 ---- a/tests/restconf-reading.cpp -+++ b/tests/restconf-reading.cpp -@@ -261,6 +261,21 @@ TEST_CASE("reading data") - } - )"}); - -+ // percent-encoded comma is a part of the key value, it is not a delimiter -+ REQUIRE(get(RESTCONF_DATA_ROOT "/ietf-system:system/radius/server=a%2Cb", {AUTH_DWDM}) == Response{404, jsonHeaders, R"({ -+ "ietf-restconf:errors": { -+ "error": [ -+ { -+ "error-type": "application", -+ "error-tag": "invalid-value", -+ "error-message": "No data from sysrepo." -+ } -+ ] -+ } -+} -+)"}); -+ -+ // comma is a delimiter of list key values - REQUIRE(get(RESTCONF_DATA_ROOT "/ietf-system:system/radius/server=a,b", {AUTH_DWDM}) == Response{400, jsonHeaders, R"({ - "ietf-restconf:errors": { - "error": [ -diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp -index a748e09..5977afc 100644 ---- a/tests/uri-parser.cpp -+++ b/tests/uri-parser.cpp -@@ -158,9 +158,9 @@ TEST_CASE("URI path parser") - {{"prefix", "lst"}, {"key1"}}, - {{"prefix", "leaf"}}, - }}}, -- {"/restconf/data/foo:bar/lst=key1,,key3", {{ -+ {"/restconf/data/foo:bar/lst=module%3Akey1,,key3", {{ - {{"foo", "bar"}}, -- {{"lst"}, {"key1", "", "key3"}}, -+ {{"lst"}, {"module:key1", "", "key3"}}, - }}}, - {"/restconf/data/foo:bar/lst=key%2CWithCommas,,key2C", {{ - {{"foo", "bar"}}, -@@ -240,6 +240,7 @@ TEST_CASE("URI path parser") - "/restconf/data/foo:list=A%2", - "/restconf/data/foo:list=A%2,", - "/restconf/data/foo:bar/list1=%%", -+ "/restconf/data/foo:bar/list1=module:smth", - "/restconf/data/foo:bar/", - "/restconf/data/ foo : bar", - "/rest conf/data / foo:bar", --- -2.43.0 - 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..9985c857 --- /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/17] 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/0005-restconf-list-key-values-checking-must-respect-libya.patch b/package/rousette/0005-restconf-list-key-values-checking-must-respect-libya.patch deleted file mode 100644 index 8d111ac7..00000000 --- a/package/rousette/0005-restconf-list-key-values-checking-must-respect-libya.patch +++ /dev/null @@ -1,428 +0,0 @@ -From 8b13c1e4ccaa61a241674c27063439e257fa88de Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Wed, 2 Oct 2024 20:21:28 +0200 -Subject: [PATCH 05/44] restconf: list key values checking must respect - libyang's canonicalization -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -While dealing with the issue that was fixed in the previous commit we -realized that the issue is deeper. Not only that our parser rejected -the input when someone used identityref with module prefix in the URI, -but also, our internal code for creating/modifying list entries was -wrong. - -In PUT requests we have to check if user entered the same key value in -URI and yang-data payload. However, some values are canonicalized by -libyang (e.g. decimal64 type with fraction-digits=2 or even the -identityrefs) and so if the client entered two different but -canonically equivalent values, we would reject such request. - -Bug: https://github.com/CESNET/rousette/issues/12 -Change-Id: I44245d831e8de6d0e6f991fcd18319c095b49b1d -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 3 +- - src/restconf/Server.cpp | 49 +++++++++++++++++---- - src/restconf/utils/yang.cpp | 5 +++ - src/restconf/utils/yang.h | 1 + - tests/restconf-reading.cpp | 59 +++++++++++++++++++++++++ - tests/restconf-writing.cpp | 82 +++++++++++++++++++++++++++++++++++ - tests/uri-parser.cpp | 2 + - tests/yang/example-types.yang | 13 ++++++ - tests/yang/example.yang | 25 +++++++++++ - 9 files changed, 229 insertions(+), 10 deletions(-) - create mode 100644 tests/yang/example-types.yang - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index c563dcf..465bef9 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -204,7 +204,8 @@ if(BUILD_TESTING) - --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example.yang --enable-feature f1 - --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-delete.yang - --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-augment.yang -- --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-notif.yang) -+ --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-notif.yang -+ --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-types.yang) - rousette_test(NAME restconf-reading LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM) - rousette_test(NAME restconf-writing LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM) - rousette_test(NAME restconf-delete LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM) -diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp -index 79d8ff6..b515d66 100644 ---- a/src/restconf/Server.cpp -+++ b/src/restconf/Server.cpp -@@ -154,6 +154,15 @@ auto rejectYangPatch(const std::string& patchId, const std::string& editId) - }; - } - -+/** @brief Check if these two paths compare the same after path canonicalization */ -+bool compareKeyValue(const libyang::Context& ctx, const std::string& pathA, const std::string& pathB) -+{ -+ auto [parentA, nodeA] = ctx.newPath2(pathA, std::nullopt); -+ auto [parentB, nodeB] = ctx.newPath2(pathB, std::nullopt); -+ -+ return nodeA->asTerm().valueStr() == nodeB->asTerm().valueStr(); -+} -+ - struct KeyMismatch { - libyang::DataNode offendingNode; - std::optional uriKeyValue; -@@ -169,25 +178,46 @@ struct KeyMismatch { - - /** @brief In case node is a (leaf-)list check if the key values are the same as the keys specified in the lastPathSegment. - * @return The node where the mismatch occurs */ --std::optional checkKeysMismatch(const libyang::DataNode& node, const PathSegment& lastPathSegment) -+std::optional checkKeysMismatch(libyang::Context& ctx, const libyang::DataNode& node, const std::string& lyParentPath, const PathSegment& lastPathSegment) - { -+ const auto pathPrefix = (lyParentPath.empty() ? "" : lyParentPath) + "/" + lastPathSegment.apiIdent.name(); -+ - if (node.schema().nodeType() == libyang::NodeType::List) { - const auto& listKeys = node.schema().asList().keys(); - for (size_t i = 0; i < listKeys.size(); ++i) { -- const auto& keyValueURI = lastPathSegment.keys[i]; - auto keyNodeData = node.findPath(listKeys[i].module().name() + ':' + listKeys[i].name()); - if (!keyNodeData) { - return KeyMismatch{node, std::nullopt}; - } - -- const auto& keyValueData = keyNodeData->asTerm().valueStr(); -- -- if (keyValueURI != keyValueData) { -- return KeyMismatch{*keyNodeData, keyValueURI}; -+ /* -+ * If the key's value has a canonical form then libyang makes the value canonical -+ * but there is no guarantee that the client provided the value in the canonical form. -+ * -+ * Let libyang do the work. Create two data nodes, one with the key value from the data and the other -+ * with the key value from the URI. Then compare the values from the two nodes. If they are different, -+ * they certainly mismatch. -+ * -+ * This can happen in cases like -+ * * The key's type is identityref and the client provided the key value as a string without the module name. Libyang will canonicalize the value by adding the module name. -+ * * The key's type is decimal64 with fractional-digits 2; then the client can provide the value as 1.0 or 1.00 and they should be the same. Libyang will canonicalize the value. -+ */ -+ -+ auto keysWithValueFromData = lastPathSegment.keys; -+ keysWithValueFromData[i] = keyNodeData->asTerm().valueStr(); -+ -+ const auto suffix = "/" + listKeys[i].name(); -+ const auto pathFromData = pathPrefix + listKeyPredicate(listKeys, keysWithValueFromData) + suffix; -+ const auto pathFromURI = pathPrefix + listKeyPredicate(listKeys, lastPathSegment.keys) + suffix; -+ -+ if (!compareKeyValue(ctx, pathFromData, pathFromURI)) { -+ return KeyMismatch{*keyNodeData, lastPathSegment.keys[i]}; - } - } - } else if (node.schema().nodeType() == libyang::NodeType::Leaflist) { -- if (lastPathSegment.keys[0] != node.asTerm().valueStr()) { -+ const auto pathFromData = pathPrefix + leaflistKeyPredicate(node.asTerm().valueStr()); -+ const auto pathFromURI = pathPrefix + leaflistKeyPredicate(lastPathSegment.keys[0]); -+ if (!compareKeyValue(ctx, pathFromData, pathFromURI)) { - return KeyMismatch{node, lastPathSegment.keys[0]}; - } - } -@@ -363,7 +393,7 @@ libyang::CreatedNodes createEditForPutAndPatch(libyang::Context& ctx, const std: - if (isSameNode(child, lastPathSegment)) { - // 1) a single child that is created by parseSubtree(), its name is the same as `lastPathSegment`. - // It could be a list; then we need to check if the keys in provided data match the keys in URI. -- if (auto keyMismatch = checkKeysMismatch(child, lastPathSegment)) { -+ if (auto keyMismatch = checkKeysMismatch(ctx, child, lyParentPath, lastPathSegment)) { - throw ErrorResponse(400, "protocol", "invalid-value", keyMismatch->message(), keyMismatch->offendingNode.path()); - } - replacementNode = child; -@@ -386,7 +416,8 @@ libyang::CreatedNodes createEditForPutAndPatch(libyang::Context& ctx, const std: - if (!isSameNode(*replacementNode, lastPathSegment)) { - throw ErrorResponse(400, "protocol", "invalid-value", "Data contains invalid node.", replacementNode->path()); - } -- if (auto keyMismatch = checkKeysMismatch(*parent, lastPathSegment)) { -+ -+ if (auto keyMismatch = checkKeysMismatch(ctx, *parent, lyParentPath, lastPathSegment)) { - throw ErrorResponse(400, "protocol", "invalid-value", keyMismatch->message(), keyMismatch->offendingNode.path()); - } - } -diff --git a/src/restconf/utils/yang.cpp b/src/restconf/utils/yang.cpp -index 15cceb6..4c4d619 100644 ---- a/src/restconf/utils/yang.cpp -+++ b/src/restconf/utils/yang.cpp -@@ -50,6 +50,11 @@ std::string listKeyPredicate(const std::vector& listKeyLeafs, con - return res; - } - -+std::string leaflistKeyPredicate(const std::string& keyValue) -+{ -+ return "[.=" + escapeListKey(keyValue) + ']'; -+} -+ - bool isUserOrderedList(const libyang::DataNode& node) - { - if (node.schema().nodeType() == libyang::NodeType::List) { -diff --git a/src/restconf/utils/yang.h b/src/restconf/utils/yang.h -index 677d049..e91ba8a 100644 ---- a/src/restconf/utils/yang.h -+++ b/src/restconf/utils/yang.h -@@ -16,6 +16,7 @@ namespace rousette::restconf { - - std::string escapeListKey(const std::string& str); - std::string listKeyPredicate(const std::vector& listKeyLeafs, const std::vector& keyValues); -+std::string leaflistKeyPredicate(const std::string& keyValue); - bool isUserOrderedList(const libyang::DataNode& node); - bool isKeyNode(const libyang::DataNode& maybeList, const libyang::DataNode& node); - } -diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp -index 2898839..96c38ab 100644 ---- a/tests/restconf-reading.cpp -+++ b/tests/restconf-reading.cpp -@@ -287,6 +287,65 @@ TEST_CASE("reading data") - ] - } - } -+)"}); -+ -+ srSess.setItem("/example:list-with-identity-key[type='example:derived-identity'][name='name']", std::nullopt); -+ srSess.setItem("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']", std::nullopt); -+ srSess.setItem("/example:tlc/decimal-list[.='1.00']", std::nullopt); -+ srSess.applyChanges(); -+ -+ // dealing with keys which can have prefixes (YANG identities) -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({ -+ "example:list-with-identity-key": [ -+ { -+ "type": "derived-identity", -+ "name": "name" -+ } -+ ] -+} -+)"}); -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example%3Aderived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({ -+ "example:list-with-identity-key": [ -+ { -+ "type": "derived-identity", -+ "name": "name" -+ } -+ ] -+} -+)"}); -+ -+ // an identity from another module must be namespace-qualified -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=another-derived-identity,name", {AUTH_ROOT}) == Response{404, jsonHeaders, R"({ -+ "ietf-restconf:errors": { -+ "error": [ -+ { -+ "error-type": "application", -+ "error-tag": "invalid-value", -+ "error-message": "No data from sysrepo." -+ } -+ ] -+ } -+} -+)"}); -+ -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example-types%3Aanother-derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({ -+ "example:list-with-identity-key": [ -+ { -+ "type": "example-types:another-derived-identity", -+ "name": "name" -+ } -+ ] -+} -+)"}); -+ -+ // test canonicalization of list key values; the key value was inserted as "1.00" -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/decimal-list=1", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({ -+ "example:tlc": { -+ "decimal-list": [ -+ "1.0" -+ ] -+ } -+} - )"}); - } - -diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp -index 96dbb25..8418554 100644 ---- a/tests/restconf-writing.cpp -+++ b/tests/restconf-writing.cpp -@@ -389,6 +389,88 @@ TEST_CASE("writing data") - REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/list=libyang/nested=11,12,13", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:nested": [{"first": "11", "second": 12, "third": "13"}]}]})") == Response{201, noContentTypeHeaders, ""}); - } - -+ SECTION("Test canonicalization of keys") -+ { -+ EXPECT_CHANGE( -+ CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']", std::nullopt), -+ CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/type", "example:derived-identity"), -+ CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/name", "name"), -+ CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/text", "blabla")); -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""}); -+ -+ // prefixed in the URI, not prefixed in the data -+ EXPECT_CHANGE( -+ MODIFIED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/text", "hehe")); -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example%3Aderived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "derived-identity", "text": "hehe"}]}]})") == Response{204, noContentTypeHeaders, ""}); -+ -+ -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=another-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{400, jsonHeaders, R"({ -+ "ietf-restconf:errors": { -+ "error": [ -+ { -+ "error-type": "protocol", -+ "error-tag": "invalid-value", -+ "error-message": "Validation failure: Can't parse data: LY_EVALID" -+ } -+ ] -+ } -+} -+)"}); -+ -+ EXPECT_CHANGE( -+ CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']", std::nullopt), -+ CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']/type", "example-types:another-derived-identity"), -+ CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']/name", "name"), -+ CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']/text", "blabla")); -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "example-types:another-derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""}); -+ -+ // missing namespace in the data -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{400, jsonHeaders, R"({ -+ "ietf-restconf:errors": { -+ "error": [ -+ { -+ "error-type": "protocol", -+ "error-tag": "invalid-value", -+ "error-message": "Validation failure: Can't parse data: LY_EVALID" -+ } -+ ] -+ } -+} -+)"}); -+ -+ EXPECT_CHANGE(CREATED("/example:leaf-list-with-identity-key[.='example-types:another-derived-identity']", "example-types:another-derived-identity")); -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:leaf-list-with-identity-key=example-types%3Aanother-derived-identity", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:leaf-list-with-identity-key": ["example-types:another-derived-identity"]})") == Response{201, noContentTypeHeaders, ""}); -+ -+ // missing namespace in the URI -+ EXPECT_CHANGE(CREATED("/example:leaf-list-with-identity-key[.='example:derived-identity']", "example:derived-identity")); -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:leaf-list-with-identity-key=derived-identity", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:leaf-list-with-identity-key": ["example:derived-identity"]})") == Response{201, noContentTypeHeaders, ""}); -+ -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:leaf-list-with-identity-key=example-types%3Aanother-derived-identity", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:leaf-list-with-identity-key": ["example:derived-identity"]})") == Response{400, jsonHeaders, R"({ -+ "ietf-restconf:errors": { -+ "error": [ -+ { -+ "error-type": "protocol", -+ "error-tag": "invalid-value", -+ "error-path": "/example:leaf-list-with-identity-key[.='example:derived-identity']", -+ "error-message": "List key mismatch between URI path ('example-types:another-derived-identity') and data ('example:derived-identity')." -+ } -+ ] -+ } -+} -+)"}); -+ -+ // value in the URI and in the data have the same canonical form -+ EXPECT_CHANGE(CREATED("/example:tlc/decimal-list[.='1.0']", "1.0")); -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/decimal-list=1.00", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:decimal-list": ["1.0"]})") == Response{201, noContentTypeHeaders, ""}); -+ -+ // nothing is changed, still the same value -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/decimal-list=1.000", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:decimal-list": ["1"]})") == Response{204, noContentTypeHeaders, ""}); -+ -+ // different value -+ EXPECT_CHANGE(CREATED("/example:tlc/decimal-list[.='1.01']", "1.01")); -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/decimal-list=1.010", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:decimal-list": ["1.01"]})") == Response{201, noContentTypeHeaders, ""}); -+ } -+ - SECTION("Modify a leaf in a list entry") - { - EXPECT_CHANGE(MODIFIED("/example:tlc/list[name='libyang']/choice1", "restconf")); -diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp -index 5977afc..7320c3c 100644 ---- a/tests/uri-parser.cpp -+++ b/tests/uri-parser.cpp -@@ -293,6 +293,7 @@ TEST_CASE("URI path parser") - {"/restconf/data/example:tlc/list=eth0/choice1", "/example:tlc/list[name='eth0']/choice1", std::nullopt}, - {"/restconf/data/example:tlc/list=eth0/choice2", "/example:tlc/list[name='eth0']/choice2", std::nullopt}, - {"/restconf/data/example:tlc/list=eth0/collection=val", "/example:tlc/list[name='eth0']/collection[.='val']", std::nullopt}, -+ {"/restconf/data/example:list-with-identity-key=example-types%3Aanother-derived-identity,aaa", "/example:list-with-identity-key[type='example-types:another-derived-identity'][name='aaa']", std::nullopt}, - {"/restconf/data/example:tlc/status", "/example:tlc/status", std::nullopt}, - // container example:a has a container b inserted locally and also via an augment. Check that we return the correct one - {"/restconf/data/example:a/b", "/example:a/b", std::nullopt}, -@@ -327,6 +328,7 @@ TEST_CASE("URI path parser") - {"/restconf/data/example:tlc/status", "/example:tlc", {{"example", "status"}}}, - {"/restconf/data/example:a/example-augment:b/c", "/example:a/example-augment:b", {{"example-augment", "c"}}}, - {"/restconf/ds/ietf-datastores:startup/example:a/example-augment:b/c", "/example:a/example-augment:b", {{"example-augment", "c"}}}, -+ {"/restconf/data/example:list-with-identity-key=example-types%3Aanother-derived-identity,aaa", "", {{"example", "list-with-identity-key"}, {"example-types:another-derived-identity", "aaa"}}}, - }) { - CAPTURE(httpMethod); - CAPTURE(expectedRequestType); -diff --git a/tests/yang/example-types.yang b/tests/yang/example-types.yang -new file mode 100644 -index 0000000..5bc2fb0 ---- /dev/null -+++ b/tests/yang/example-types.yang -@@ -0,0 +1,13 @@ -+module example-types { -+ yang-version 1.1; -+ namespace "http://example.tld/example-types"; -+ prefix ex-types; -+ -+ import example { -+ prefix ex; -+ } -+ -+ identity another-derived-identity { -+ base ex:base-identity; -+ } -+} -diff --git a/tests/yang/example.yang b/tests/yang/example.yang -index df1301f..c46273c 100644 ---- a/tests/yang/example.yang -+++ b/tests/yang/example.yang -@@ -6,6 +6,13 @@ module example { - feature f1 { } - feature f2 { } - -+ identity base-identity { -+ } -+ -+ identity derived-identity { -+ base base-identity; -+ } -+ - leaf top-level-leaf { type string; } - leaf top-level-leaf2 { type string; default "x"; } - -@@ -50,6 +57,11 @@ module example { - config false; - leaf name { type string; } - } -+ leaf-list decimal-list { -+ type decimal64 { -+ fraction-digits 2; -+ } -+ } - leaf status { - type enumeration { - enum on { } -@@ -109,6 +121,19 @@ module example { - } - } - -+ list list-with-identity-key { -+ key "type name"; -+ leaf type { -+ type identityref { base base-identity; } -+ } -+ leaf name { type string; } -+ leaf text { type string; } -+ } -+ -+ leaf-list leaf-list-with-identity-key { -+ type identityref { base base-identity; } -+ } -+ - rpc test-rpc { - input { - leaf i { --- -2.43.0 - diff --git a/package/rousette/0006-close-long-lived-connections-on-SIGTERM.patch b/package/rousette/0006-close-long-lived-connections-on-SIGTERM.patch new file mode 100644 index 00000000..57179a31 --- /dev/null +++ b/package/rousette/0006-close-long-lived-connections-on-SIGTERM.patch @@ -0,0 +1,377 @@ +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 06/17] 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 +about 50s, and only then killed. Fix that by a proper signalling -- once +the intention to shutdown is known, all the EventStreams are +(asynchronously) switched to a new state, and the HTTP2 state machine is +restarted. Once the event loop gets around to processing them, the +callback will just say EOF. + +The clock demo is different because it doesn't wrap the nghttp2 server +like the restconf/Server.cpp does. + +I have no idea how well this works against a Slowloris attack, but I +think that stuff like that should be solved at the HTTP library level (I +hope it is). + +Change-Id: If442134783ba1d699de47c51a9068378f53e8339 +Co-authored-by: Tomas Pecka +Signed-off-by: Mattias Walström +--- + CMakeLists.txt | 1 + + src/clock.cpp | 7 +-- + src/http/EventStream.cpp | 30 +++++++++++-- + src/http/EventStream.h | 10 +++-- + src/restconf/NotificationStream.cpp | 7 +-- + src/restconf/NotificationStream.h | 5 ++- + src/restconf/Server.cpp | 5 ++- + src/restconf/Server.h | 1 + + tests/restconf-eventstream.cpp | 70 +++++++++++++++++++++++++++++ + tests/restconf_utils.cpp | 6 +++ + 10 files changed, 124 insertions(+), 18 deletions(-) + create mode 100644 tests/restconf-eventstream.cpp + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index b99c577..c0304af 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -227,6 +227,7 @@ if(BUILD_TESTING) + rousette_test(NAME restconf-notifications LIBRARIES rousette-restconf FIXTURE common-models WRAP_PAM) + 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) + set(nested-models + ${common-models} + --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/root-mod.yang) +diff --git a/src/clock.cpp b/src/clock.cpp +index 7ac6c3e..c85be19 100644 +--- a/src/clock.cpp ++++ b/src/clock.cpp +@@ -18,7 +18,8 @@ int main(int argc [[maybe_unused]], char** argv [[maybe_unused]]) + { + spdlog::set_level(spdlog::level::trace); + +- rousette::http::EventStream::Signal sig; ++ 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); +@@ -30,8 +31,8 @@ int main(int argc [[maybe_unused]], char** argv [[maybe_unused]]) + nghttp2::asio_http2::server::http2 server; + server.num_threads(4); + +- server.handle("/events", [&sig](const auto& req, const auto& res) { +- auto client = std::make_shared(req, res, sig); ++ server.handle("/events", [&shutdown, &sig](const auto& req, const auto& res) { ++ auto client = std::make_shared(req, res, shutdown, sig); + client->activate(); + }); + +diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp +index 25dbf3f..77c282d 100644 +--- a/src/http/EventStream.cpp ++++ b/src/http/EventStream.cpp +@@ -18,7 +18,7 @@ using namespace nghttp2::asio_http2; + namespace rousette::http { + + /** @short After constructing, make sure to call activate() immediately. */ +-EventStream::EventStream(const server::request& req, const server::response& res, Signal& signal, const std::optional& initialEvent) ++EventStream::EventStream(const server::request& req, const server::response& res, Termination& termination, EventSignal& signal, const std::optional& initialEvent) + : res{res} + , peer{peer_from_request(req)} + { +@@ -26,9 +26,27 @@ EventStream::EventStream(const server::request& req, const server::response& res + enqueue(*initialEvent); + } + +- subscription = signal.connect([this](const auto& msg) { ++ eventSub = signal.connect([this](const auto& msg) { + enqueue(msg); + }); ++ ++ terminateSub = termination.connect([this]() { ++ spdlog::trace("{}: will terminate", peer); ++ std::lock_guard lock{mtx}; ++ if (state == Closed) { // we are late to the party, res is already gone ++ return; ++ } ++ ++ state = WantToClose; ++ boost::asio::post(this->res.io_service(), [maybeClient = std::weak_ptr{shared_from_this()}]() { ++ if (auto client = maybeClient.lock()) { ++ std::lock_guard lock{client->mtx}; ++ if (client->state == WantToClose) { // resume unless somebody closed it before this was picked up by the event loop ++ client->res.resume(); ++ } ++ } ++ }); ++ }); + } + + /** @short Start event processing and data delivery +@@ -47,7 +65,8 @@ void EventStream::activate() + res.on_close([client](const auto ec) { + spdlog::debug("{}: closed ({})", client->peer, nghttp2_http2_strerror(ec)); + std::lock_guard lock{client->mtx}; +- client->subscription.disconnect(); ++ client->eventSub.disconnect(); ++ client->terminateSub.disconnect(); + client->state = Closed; + }); + +@@ -85,6 +104,9 @@ ssize_t EventStream::process(uint8_t* destination, std::size_t len, uint32_t* da + case WaitingForEvents: + spdlog::trace("{}: sleeping", peer); + return NGHTTP2_ERR_DEFERRED; ++ case WantToClose: ++ *data_flags |= NGHTTP2_DATA_FLAG_EOF; ++ return 0; + case Closed: + throw std::logic_error{"response already closed"}; + } +@@ -105,7 +127,7 @@ void EventStream::enqueue(const std::string& what) + buf += '\n'; + + std::lock_guard lock{mtx}; +- if (state == Closed) { ++ if (state == Closed || state == WantToClose) { + spdlog::trace("{}: enqueue: already disconnected", peer); + return; + } +diff --git a/src/http/EventStream.h b/src/http/EventStream.h +index 735b69a..b427fb3 100644 +--- a/src/http/EventStream.h ++++ b/src/http/EventStream.h +@@ -23,13 +23,14 @@ namespace rousette::http { + + /** @short Event delivery via text/event-stream + +-Recieve data from a Signal, and deliver them to an HTTP client via a text/event-stream streamed response. ++Recieve data from an EventSignal, and deliver them to an HTTP client via a text/event-stream streamed response. + */ + class EventStream : public std::enable_shared_from_this { + public: +- using Signal = boost::signals2::signal; ++ using EventSignal = boost::signals2::signal; ++ using Termination = boost::signals2::signal; + +- EventStream(const nghttp2::asio_http2::server::request& req, const nghttp2::asio_http2::server::response& res, Signal& signal, const std::optional& initialEvent = std::nullopt); ++ EventStream(const nghttp2::asio_http2::server::request& req, const nghttp2::asio_http2::server::response& res, Termination& terminate, EventSignal& signal, const std::optional& initialEvent = std::nullopt); + void activate(); + + private: +@@ -37,13 +38,14 @@ private: + enum State { + HasEvents, + WaitingForEvents, ++ WantToClose, + Closed, + }; + + State state = WaitingForEvents; + std::list queue; + mutable std::mutex mtx; // for `state` and `queue` +- boost::signals2::scoped_connection subscription; ++ boost::signals2::scoped_connection eventSub, terminateSub; + const std::string peer; + + size_t send_chunk(uint8_t* destination, std::size_t len, uint32_t* data_flags); +diff --git a/src/restconf/NotificationStream.cpp b/src/restconf/NotificationStream.cpp +index e00c39d..d0ba938 100644 +--- a/src/restconf/NotificationStream.cpp ++++ b/src/restconf/NotificationStream.cpp +@@ -24,7 +24,7 @@ void subscribe( + std::optional& sub, + sysrepo::Session& session, + const std::string& moduleName, +- rousette::http::EventStream::Signal& signal, ++ rousette::http::EventStream::EventSignal& signal, + libyang::DataFormat dataFormat, + const std::optional& filter, + const std::optional& startTime, +@@ -87,13 +87,14 @@ namespace rousette::restconf { + NotificationStream::NotificationStream( + const nghttp2::asio_http2::server::request& req, + const nghttp2::asio_http2::server::response& res, +- std::shared_ptr signal, ++ rousette::http::EventStream::Termination& termination, ++ std::shared_ptr signal, + sysrepo::Session session, + libyang::DataFormat dataFormat, + const std::optional& filter, + const std::optional& startTime, + const std::optional& stopTime) +- : EventStream(req, res, *signal) ++ : EventStream(req, res, termination, *signal) + , m_notificationSignal(signal) + , m_session(std::move(session)) + , m_dataFormat(dataFormat) +diff --git a/src/restconf/NotificationStream.h b/src/restconf/NotificationStream.h +index 3029a0d..46f8416 100644 +--- a/src/restconf/NotificationStream.h ++++ b/src/restconf/NotificationStream.h +@@ -30,7 +30,7 @@ namespace rousette::restconf { + * @see rousette::http::EventStream + * */ + class NotificationStream : public rousette::http::EventStream { +- std::shared_ptr m_notificationSignal; ++ std::shared_ptr m_notificationSignal; + sysrepo::Session m_session; + libyang::DataFormat m_dataFormat; + std::optional m_filter; +@@ -42,7 +42,8 @@ public: + NotificationStream( + const nghttp2::asio_http2::server::request& req, + const nghttp2::asio_http2::server::response& res, +- std::shared_ptr signal, ++ rousette::http::EventStream::Termination& termination, ++ std::shared_ptr signal, + sysrepo::Session sess, + libyang::DataFormat dataFormat, + const std::optional& filter, +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 4255540..2f495f9 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -845,6 +845,7 @@ void Server::stop() + }); + t.cancel(); + } ++ shutdownRequested(); + } + + void Server::join() +@@ -935,7 +936,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: + server->handle("/telemetry/optics", [this](const auto& req, const auto& res) { + logRequest(req); + +- auto client = std::make_shared(req, res, opticsChange, as_restconf_push_update(dwdmEvents->currentData(), std::chrono::system_clock::now())); ++ auto client = std::make_shared(req, res, shutdownRequested, opticsChange, as_restconf_push_update(dwdmEvents->currentData(), std::chrono::system_clock::now())); + client->activate(); + }); + +@@ -972,7 +973,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: + // The signal is constructed outside NotificationStream class because it is required to be passed to + // NotificationStream's parent (EventStream) constructor where it already must be constructed + // Yes, this is a hack. +- auto client = std::make_shared(req, res, std::make_shared(), sess, streamRequest.type.encoding, xpathFilter, startTime, stopTime); ++ auto client = std::make_shared(req, res, shutdownRequested, std::make_shared(), sess, streamRequest.type.encoding, xpathFilter, startTime, stopTime); + client->activate(); + } catch (const auth::Error& e) { + processAuthError(req, res, e, [&res]() { +diff --git a/src/restconf/Server.h b/src/restconf/Server.h +index ad53d4c..112c943 100644 +--- a/src/restconf/Server.h ++++ b/src/restconf/Server.h +@@ -43,6 +43,7 @@ private: + using JsonDiffSignal = boost::signals2::signal; + JsonDiffSignal opticsChange; + bool joined = false; // true if the server has been joined, join twice is an error ++ boost::signals2::signal shutdownRequested; + }; + } + } +diff --git a/tests/restconf-eventstream.cpp b/tests/restconf-eventstream.cpp +new file mode 100644 +index 0000000..3d7fc15 +--- /dev/null ++++ b/tests/restconf-eventstream.cpp +@@ -0,0 +1,70 @@ ++/* ++ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/ ++ * ++ * Written by Tomáš Pecka ++ * ++ */ ++ ++#include "trompeloeil_doctest.h" ++static const auto SERVER_PORT = "10091"; ++#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); ++ ++using namespace std::chrono_literals; ++ ++TEST_CASE("Termination on server shutdown") ++{ ++ trompeloeil::sequence seqMod1; ++ sysrepo::setLogLevelStderr(sysrepo::LogLevel::Information); ++ spdlog::set_level(spdlog::level::trace); ++ ++ std::vector> expectations; ++ ++ 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()); ++ ++ const std::string notification(R"({"example:eventA":{"message":"blabla","progress":11}})"); ++ EXPECT_NOTIFICATION(notification, seqMod1); ++ EXPECT_NOTIFICATION(notification, seqMod1); ++ EXPECT_NOTIFICATION(notification, seqMod1); ++ ++ auto notifSession = sysrepo::Connection{}.sessionStart(); ++ auto ctx = notifSession.getContext(); ++ ++ PREPARE_LOOP_WITH_EXCEPTIONS; ++ auto 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(notification); ++ SEND_NOTIFICATION(notification); ++ SEND_NOTIFICATION(notification); ++ waitForCompletionAndBitMore(seqMod1); ++ ++ auto beforeShutdown = std::chrono::system_clock::now(); ++ server.reset(); ++ auto shutdownDuration = std::chrono::system_clock::now() - beforeShutdown; ++ REQUIRE(shutdownDuration < 5s); ++ })); ++ ++ SSEClient client(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, "/streams/NETCONF/JSON", std::map{AUTH_ROOT}); ++ ++ RUN_LOOP_WITH_EXCEPTIONS; ++} +diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp +index a137463..cbfce2c 100644 +--- a/tests/restconf_utils.cpp ++++ b/tests/restconf_utils.cpp +@@ -205,6 +205,12 @@ SSEClient::SSEClient( + t.expires_from_now(silenceTimeout); + }); + }); ++ ++ req->on_close([maybeClient = std::weak_ptr{client}](auto) { ++ if (auto client = maybeClient.lock()) { ++ client->shutdown(); ++ } ++ }); + }); + + client->on_error([&](const boost::system::error_code& ec) { +-- +2.43.0 + diff --git a/package/rousette/0006-tests-test-querying-lists-with-union-keys.patch b/package/rousette/0006-tests-test-querying-lists-with-union-keys.patch deleted file mode 100644 index 7990f1f1..00000000 --- a/package/rousette/0006-tests-test-querying-lists-with-union-keys.patch +++ /dev/null @@ -1,300 +0,0 @@ -From fda47b6a6cfdaecc24e96c4d6138c6de3ef116e0 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 7 Oct 2024 21:21:22 +0200 -Subject: [PATCH 06/44] tests: test querying lists with union keys -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Test that we correctly work with keys that are unions of something -that can have a module namespace and that must not have it. - -By the way, enum values are not supposed to have a namespace prefix, -good to know [1]. - -[1] https://www.rfc-editor.org/rfc/rfc7951#section-6.4 - -Change-Id: I5a70f18117bb453330b4bb2ce0d2fb47d35b6ea6 -Signed-off-by: Mattias Walström ---- - tests/restconf-reading.cpp | 53 ++++++++++++++++++++++++----- - tests/restconf-writing.cpp | 68 ++++++++++++++++++++++++++++---------- - tests/uri-parser.cpp | 2 +- - tests/yang/example.yang | 26 +++++++++++++-- - 4 files changed, 120 insertions(+), 29 deletions(-) - -diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp -index 96c38ab..2ded3f0 100644 ---- a/tests/restconf-reading.cpp -+++ b/tests/restconf-reading.cpp -@@ -289,14 +289,16 @@ TEST_CASE("reading data") - } - )"}); - -- srSess.setItem("/example:list-with-identity-key[type='example:derived-identity'][name='name']", std::nullopt); -- srSess.setItem("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']", std::nullopt); -+ srSess.setItem("/example:list-with-union-keys[type='example:derived-identity'][name='name']", std::nullopt); -+ srSess.setItem("/example:list-with-union-keys[type='example-types:another-derived-identity'][name='name']", std::nullopt); -+ srSess.setItem("/example:list-with-union-keys[type='fiii'][name='name']", std::nullopt); -+ srSess.setItem("/example:list-with-union-keys[type='zero'][name='name']", std::nullopt); // enum value - srSess.setItem("/example:tlc/decimal-list[.='1.00']", std::nullopt); - srSess.applyChanges(); - - // dealing with keys which can have prefixes (YANG identities) -- REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({ -- "example:list-with-identity-key": [ -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({ -+ "example:list-with-union-keys": [ - { - "type": "derived-identity", - "name": "name" -@@ -304,8 +306,8 @@ TEST_CASE("reading data") - ] - } - )"}); -- REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example%3Aderived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({ -- "example:list-with-identity-key": [ -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example%3Aderived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({ -+ "example:list-with-union-keys": [ - { - "type": "derived-identity", - "name": "name" -@@ -315,7 +317,7 @@ TEST_CASE("reading data") - )"}); - - // an identity from another module must be namespace-qualified -- REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=another-derived-identity,name", {AUTH_ROOT}) == Response{404, jsonHeaders, R"({ -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=another-derived-identity,name", {AUTH_ROOT}) == Response{404, jsonHeaders, R"({ - "ietf-restconf:errors": { - "error": [ - { -@@ -328,8 +330,8 @@ TEST_CASE("reading data") - } - )"}); - -- REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example-types%3Aanother-derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({ -- "example:list-with-identity-key": [ -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example-types%3Aanother-derived-identity,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({ -+ "example:list-with-union-keys": [ - { - "type": "example-types:another-derived-identity", - "name": "name" -@@ -346,6 +348,39 @@ TEST_CASE("reading data") - ] - } - } -+)"}); -+ -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=zero,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({ -+ "example:list-with-union-keys": [ -+ { -+ "type": "zero", -+ "name": "name" -+ } -+ ] -+} -+)"}); -+ -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example%3Azero,name", {AUTH_ROOT}) == Response{404, jsonHeaders, R"({ -+ "ietf-restconf:errors": { -+ "error": [ -+ { -+ "error-type": "application", -+ "error-tag": "invalid-value", -+ "error-message": "No data from sysrepo." -+ } -+ ] -+ } -+} -+)"}); -+ -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:list-with-union-keys=fiii,name", {AUTH_ROOT}) == Response{200, jsonHeaders, R"({ -+ "example:list-with-union-keys": [ -+ { -+ "type": "fiii", -+ "name": "name" -+ } -+ ] -+} - )"}); - } - -diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp -index 8418554..c1a9515 100644 ---- a/tests/restconf-writing.cpp -+++ b/tests/restconf-writing.cpp -@@ -392,46 +392,69 @@ TEST_CASE("writing data") - SECTION("Test canonicalization of keys") - { - EXPECT_CHANGE( -- CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']", std::nullopt), -- CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/type", "example:derived-identity"), -- CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/name", "name"), -- CREATED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/text", "blabla")); -- REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""}); -+ CREATED("/example:list-with-union-keys[type='example:derived-identity'][name='name']", std::nullopt), -+ CREATED("/example:list-with-union-keys[type='example:derived-identity'][name='name']/type", "example:derived-identity"), -+ CREATED("/example:list-with-union-keys[type='example:derived-identity'][name='name']/name", "name"), -+ CREATED("/example:list-with-union-keys[type='example:derived-identity'][name='name']/text", "blabla")); -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""}); - - // prefixed in the URI, not prefixed in the data - EXPECT_CHANGE( -- MODIFIED("/example:list-with-identity-key[type='example:derived-identity'][name='name']/text", "hehe")); -- REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example%3Aderived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "derived-identity", "text": "hehe"}]}]})") == Response{204, noContentTypeHeaders, ""}); -+ MODIFIED("/example:list-with-union-keys[type='example:derived-identity'][name='name']/text", "hehe")); -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example%3Aderived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "derived-identity", "text": "hehe"}]}]})") == Response{204, noContentTypeHeaders, ""}); - -+ // 'another-derived-identity' comes from a different module than the list itself, so this parses as string -+ EXPECT_CHANGE( -+ CREATED("/example:list-with-union-keys[type='another-derived-identity'][name='name']", std::nullopt), -+ CREATED("/example:list-with-union-keys[type='another-derived-identity'][name='name']/type", "another-derived-identity"), -+ CREATED("/example:list-with-union-keys[type='another-derived-identity'][name='name']/name", "name"), -+ CREATED("/example:list-with-union-keys[type='another-derived-identity'][name='name']/text", "blabla")); -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=another-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""}); -+ -+ EXPECT_CHANGE( -+ CREATED("/example:list-with-union-keys[type='example-types:another-derived-identity'][name='name']", std::nullopt), -+ CREATED("/example:list-with-union-keys[type='example-types:another-derived-identity'][name='name']/type", "example-types:another-derived-identity"), -+ CREATED("/example:list-with-union-keys[type='example-types:another-derived-identity'][name='name']/name", "name"), -+ CREATED("/example:list-with-union-keys[type='example-types:another-derived-identity'][name='name']/text", "blabla")); -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "example-types:another-derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""}); - -- REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=another-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{400, jsonHeaders, R"({ -+ // missing namespace in the data -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{400, jsonHeaders, R"({ - "ietf-restconf:errors": { - "error": [ - { - "error-type": "protocol", - "error-tag": "invalid-value", -- "error-message": "Validation failure: Can't parse data: LY_EVALID" -+ "error-path": "/example:list-with-union-keys[type='another-derived-identity'][name='name']/type", -+ "error-message": "List key mismatch between URI path ('example-types:another-derived-identity') and data ('another-derived-identity')." - } - ] - } - } - )"}); - -+ // zero is enum value - EXPECT_CHANGE( -- CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']", std::nullopt), -- CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']/type", "example-types:another-derived-identity"), -- CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']/name", "name"), -- CREATED("/example:list-with-identity-key[type='example-types:another-derived-identity'][name='name']/text", "blabla")); -- REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "example-types:another-derived-identity", "text": "blabla"}]}]})") == Response{201, noContentTypeHeaders, ""}); -+ CREATED("/example:list-with-union-keys[type='zero'][name='name']", std::nullopt), -+ CREATED("/example:list-with-union-keys[type='zero'][name='name']/type", "zero"), -+ CREATED("/example:list-with-union-keys[type='zero'][name='name']/name", "name")); -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=zero,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "zero"}]}]})") == Response{201, noContentTypeHeaders, ""}); - -- // missing namespace in the data -- REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-identity-key=example-types%3Aanother-derived-identity,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-identity-key": [{"name": "name", "type": "another-derived-identity", "text": "blabla"}]}]})") == Response{400, jsonHeaders, R"({ -+ // example:zero is string, enum values are not namespace-prefixed -+ EXPECT_CHANGE( -+ CREATED("/example:list-with-union-keys[type='example:zero'][name='name']", std::nullopt), -+ CREATED("/example:list-with-union-keys[type='example:zero'][name='name']/type", "example:zero"), -+ CREATED("/example:list-with-union-keys[type='example:zero'][name='name']/name", "name")); -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example%3Azero,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "example:zero"}]}]})") == Response{201, noContentTypeHeaders, ""}); -+ -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=zero,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list-with-union-keys": [{"name": "name", "type": "example:zero"}]}]})") == Response{400, jsonHeaders, R"({ - "ietf-restconf:errors": { - "error": [ - { - "error-type": "protocol", - "error-tag": "invalid-value", -- "error-message": "Validation failure: Can't parse data: LY_EVALID" -+ "error-path": "/example:list-with-union-keys[type='example:zero'][name='name']/type", -+ "error-message": "List key mismatch between URI path ('zero') and data ('example:zero')." - } - ] - } -@@ -459,6 +482,17 @@ TEST_CASE("writing data") - } - )"}); - -+ EXPECT_CHANGE(CREATED("/example:fruit-list[.='example:apple']", "example:apple")); -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:fruit-list=example%3Aapple", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:fruit-list": ["apple"]})") == Response{201, noContentTypeHeaders, ""}); -+ -+ // leafref -+ EXPECT_CHANGE( -+ CREATED("/example:list-with-union-keys[type='example:apple'][name='name']", std::nullopt), -+ CREATED("/example:list-with-union-keys[type='example:apple'][name='name']/type", "example:apple"), -+ CREATED("/example:list-with-union-keys[type='example:apple'][name='name']/name", "name")); -+ REQUIRE(put(RESTCONF_DATA_ROOT "/example:list-with-union-keys=example%3Aapple,name", {AUTH_ROOT, CONTENT_TYPE_JSON}, -+ R"({"example:list-with-union-keys": [{"name": "name", "type": "apple"}]}]})") == Response{201, noContentTypeHeaders, ""}); -+ - // value in the URI and in the data have the same canonical form - EXPECT_CHANGE(CREATED("/example:tlc/decimal-list[.='1.0']", "1.0")); - REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/decimal-list=1.00", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:decimal-list": ["1.0"]})") == Response{201, noContentTypeHeaders, ""}); -diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp -index 7320c3c..000fe5d 100644 ---- a/tests/uri-parser.cpp -+++ b/tests/uri-parser.cpp -@@ -293,7 +293,7 @@ TEST_CASE("URI path parser") - {"/restconf/data/example:tlc/list=eth0/choice1", "/example:tlc/list[name='eth0']/choice1", std::nullopt}, - {"/restconf/data/example:tlc/list=eth0/choice2", "/example:tlc/list[name='eth0']/choice2", std::nullopt}, - {"/restconf/data/example:tlc/list=eth0/collection=val", "/example:tlc/list[name='eth0']/collection[.='val']", std::nullopt}, -- {"/restconf/data/example:list-with-identity-key=example-types%3Aanother-derived-identity,aaa", "/example:list-with-identity-key[type='example-types:another-derived-identity'][name='aaa']", std::nullopt}, -+ {"/restconf/data/example:list-with-union-keys=example-types%3Aanother-derived-identity,aaa", "/example:list-with-union-keys[type='example-types:another-derived-identity'][name='aaa']", std::nullopt}, - {"/restconf/data/example:tlc/status", "/example:tlc/status", std::nullopt}, - // container example:a has a container b inserted locally and also via an augment. Check that we return the correct one - {"/restconf/data/example:a/b", "/example:a/b", std::nullopt}, -diff --git a/tests/yang/example.yang b/tests/yang/example.yang -index c46273c..75cd7a6 100644 ---- a/tests/yang/example.yang -+++ b/tests/yang/example.yang -@@ -13,6 +13,11 @@ module example { - base base-identity; - } - -+ identity fruit { } -+ identity apple { -+ base fruit; -+ } -+ - leaf top-level-leaf { type string; } - leaf top-level-leaf2 { type string; default "x"; } - -@@ -121,10 +126,23 @@ module example { - } - } - -- list list-with-identity-key { -+ list list-with-union-keys { - key "type name"; - leaf type { -- type identityref { base base-identity; } -+ type union { -+ type identityref { -+ base base-identity; -+ } -+ type enumeration { -+ enum zero; -+ enum one; -+ } -+ type leafref { -+ require-instance true; -+ path "/fruit-list"; -+ } -+ type string; -+ } - } - leaf name { type string; } - leaf text { type string; } -@@ -134,6 +152,10 @@ module example { - type identityref { base base-identity; } - } - -+ leaf-list fruit-list { -+ type identityref { base fruit; } -+ } -+ - rpc test-rpc { - input { - leaf i { --- -2.43.0 - diff --git a/package/rousette/0007-error-handling-in-sysrepo-has-changed.patch b/package/rousette/0007-error-handling-in-sysrepo-has-changed.patch deleted file mode 100644 index 6b6cb921..00000000 --- a/package/rousette/0007-error-handling-in-sysrepo-has-changed.patch +++ /dev/null @@ -1,32 +0,0 @@ -From 27ef5bc87fdeb70a77609da6ec18ee5c28656bb6 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Tue, 29 Oct 2024 18:54:55 +0100 -Subject: [PATCH 07/44] error handling in sysrepo has changed -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/+/7969 -Change-Id: Id028806ed49114cba4c55e2874bcf3fc98308bdc -Signed-off-by: Mattias Walström ---- - tests/restconf-rpc.cpp | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/tests/restconf-rpc.cpp b/tests/restconf-rpc.cpp -index 4f66f10..9bc1dbc 100644 ---- a/tests/restconf-rpc.cpp -+++ b/tests/restconf-rpc.cpp -@@ -301,7 +301,7 @@ TEST_CASE("invoking actions and rpcs") - { - "error-type": "application", - "error-tag": "operation-failed", -- "error-message": "Internal server error due to sysrepo exception: Couldn't send RPC: SR_ERR_CALLBACK_FAILED\u000A Operation failed (SR_ERR_OPERATION_FAILED)\u000A User callback failed. (SR_ERR_CALLBACK_FAILED)" -+ "error-message": "Internal server error due to sysrepo exception: Couldn't send RPC: SR_ERR_OPERATION_FAILED\u000A Operation failed (SR_ERR_OPERATION_FAILED)" - } - ] - } --- -2.43.0 - diff --git a/package/rousette/0007-restconf-refactor-server-stop.patch b/package/rousette/0007-restconf-refactor-server-stop.patch new file mode 100644 index 00000000..328b1682 --- /dev/null +++ b/package/rousette/0007-restconf-refactor-server-stop.patch @@ -0,0 +1,54 @@ +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 07/17] 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 +server->stop() through event loop and it was proposed to use the +approach introduced in [1], i.e., construct a timer, then cancel it +right away which would invoke the timer handler. +I find that quite complicated, why not just simply call post()? [2] + +(Also, by looking at the http2::asio_http2::server::http2::stop(), it +does not seem like it should be called from *every* io_service.) + +[1] https://stackoverflow.com/questions/17005258/why-does-boost-asio-not-support-an-event-based-interface/17029022#17029022 +[2] https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/reference/post.html + +Change-Id: I2f33c38a78dce4081a03326c9a9bb25817fc9d2f +Signed-off-by: Mattias Walström +--- + src/restconf/Server.cpp | 13 +++++-------- + 1 file changed, 5 insertions(+), 8 deletions(-) + +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 2f495f9..bd2ce0d 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -837,14 +837,11 @@ Server::~Server() + void Server::stop() + { + // notification to stop has to go through the asio io_context +- for (const auto& service : server->io_services()) { +- boost::asio::deadline_timer t{*service, boost::posix_time::pos_infin}; +- t.async_wait([server = this->server.get()](const boost::system::error_code&) { +- spdlog::trace("Stoping HTTP/2 server"); +- server->stop(); +- }); +- t.cancel(); +- } ++ boost::asio::post(*server->io_services().front(), [server = this->server.get()]() { ++ spdlog::trace("Stoping HTTP/2 server"); ++ server->stop(); ++ }); ++ + shutdownRequested(); + } + +-- +2.43.0 + diff --git a/package/rousette/0008-restconf-support-fields-query-parameter.patch b/package/rousette/0008-restconf-support-fields-query-parameter.patch deleted file mode 100644 index f468f422..00000000 --- a/package/rousette/0008-restconf-support-fields-query-parameter.patch +++ /dev/null @@ -1,721 +0,0 @@ -From 6819561d97e38569c319e36ca2e99768036b4032 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Wed, 21 Aug 2024 19:18:08 +0200 -Subject: [PATCH 08/44] restconf: support fields query parameter -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -This patch adds support for fields query parameter [1]. - -I had to modify the original grammar for fields parameter a bit to -allow for the lowest precedence while parsing `;` expression. Also we -allow for more strings than the original grammar specifies to make the -syntax more user friendly. - -The fields expression is parsed into an AST which corresponds 1:1 -to the parse tree. The tree representing the expression could be -simplified but I chose not to as it would complicate the code even -more (although the translation to XPath would be simpler). -The tree is then transformed into a valid XPath 1.0 expression. - -The XPath 1.0 expressions are limited and I could not find a way how to -transform the fields string into a valid XPath. I realized that the -easiest way will be to "unwrap" the expression into individual paths -and join them via union operator. -For example, input `a(b;c/d)` would result into `a/b | a/c/d` XPath. - -[1] https://datatracker.ietf.org/doc/html/rfc8040#section-4.8.3 -[2] https://www.w3.org/TR/1999/REC-xpath-19991116/#section-Expressions - -Change-Id: I3c96bbcf49b38ecf08f56912afd3a8f50c15cd44 -Signed-off-by: Jan Kundrát -Signed-off-by: Mattias Walström ---- - README.md | 1 - - src/restconf/Server.cpp | 9 ++- - src/restconf/uri.cpp | 95 +++++++++++++++++++++++- - src/restconf/uri.h | 57 ++++++++++++++- - src/restconf/uri_impl.h | 4 ++ - tests/pretty_printers.h | 23 ++++++ - tests/restconf-reading.cpp | 144 ++++++++++++++++++++++++++++++++++++- - tests/restconf-writing.cpp | 12 +++- - tests/uri-parser.cpp | 93 ++++++++++++++++++++++++ - tests/yang/example.yang | 10 +++ - 10 files changed, 438 insertions(+), 10 deletions(-) - -diff --git a/README.md b/README.md -index 1689584..3fbfd21 100644 ---- a/README.md -+++ b/README.md -@@ -22,7 +22,6 @@ This is a [RESTCONF](https://datatracker.ietf.org/doc/html/rfc8040.html) server - - TLS certificate authentication (see [Access control model](#access-control-model) below) - - the [`Last-Modified`](https://datatracker.ietf.org/doc/html/rfc8040.html#section-3.4.1.1) and [`ETag`](https://datatracker.ietf.org/doc/html/rfc8040.html#section-3.4.1.2) headers for [edit collision prevention](https://datatracker.ietf.org/doc/html/rfc8040.html#section-3.4.1) in the datastore resource - - the [`Last-Modified`](https://datatracker.ietf.org/doc/html/rfc8040.html#section-3.5.1) and [`ETag`](https://datatracker.ietf.org/doc/html/rfc8040.html#section-3.5.2) headers in the data resource -- - The [`fields`](https://datatracker.ietf.org/doc/html/rfc8040.html#section-4.8.3) query parameter - - [NMDA](https://datatracker.ietf.org/doc/html/rfc8527.html) datastore access - - no [`with-operational-default`](https://datatracker.ietf.org/doc/html/rfc8527#section-3.2.1) capability - - no [`with-origin`](https://datatracker.ietf.org/doc/html/rfc8527#section-3.2.2) capability -diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp -index b515d66..7c66ea4 100644 ---- a/src/restconf/Server.cpp -+++ b/src/restconf/Server.cpp -@@ -834,6 +834,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: - m_monitoringSession.setItem("/ietf-restconf-monitoring:restconf-state/capabilities/capability[2]", "urn:ietf:params:restconf:capability:depth:1.0"); - m_monitoringSession.setItem("/ietf-restconf-monitoring:restconf-state/capabilities/capability[3]", "urn:ietf:params:restconf:capability:with-defaults:1.0"); - m_monitoringSession.setItem("/ietf-restconf-monitoring:restconf-state/capabilities/capability[4]", "urn:ietf:params:restconf:capability:filter:1.0"); -+ m_monitoringSession.setItem("/ietf-restconf-monitoring:restconf-state/capabilities/capability[5]", "urn:ietf:params:restconf:capability:fields:1.0"); - m_monitoringSession.applyChanges(); - - m_monitoringOperSub = m_monitoringSession.onOperGet( -@@ -1017,7 +1018,13 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: - } - } - -- if (auto data = sess.getData(restconfRequest.path, maxDepth, getOptions, timeout); data) { -+ auto xpath = restconfRequest.path; -+ if (auto it = restconfRequest.queryParams.find("fields"); it != restconfRequest.queryParams.end()) { -+ auto fields = std::get(it->second); -+ xpath = fieldsToXPath(sess.getContext(), xpath == "/*" ? "" : xpath, fields); -+ } -+ -+ if (auto data = sess.getData(xpath, maxDepth, getOptions, timeout); data) { - res.write_head( - 200, - { -diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp -index ac399b7..da1e3a5 100644 ---- a/src/restconf/uri.cpp -+++ b/src/restconf/uri.cpp -@@ -4,7 +4,10 @@ - * Written by Tomáš Pecka - */ - -+#include -+#include - #include -+#include - #include - #include - #include -@@ -112,6 +115,28 @@ struct insertTable: x3::symbols { - } - } const insertParam; - -+/* This grammar is implemented a little bit differently than the RFC states. The ABNF from RFC is: -+ * -+ * fields-expr = path "(" fields-expr ")" / path ";" fields-expr / path -+ * path = api-identifier [ "/" path ] -+ * -+ * Firstly, the grammar from the RFC doesn't allow for expression like `a(b);c` but allows for `c;a(b)`. -+ * I think both should be valid (as user I would expect that the order of such expression does not matter). -+ * Hence our grammar allows for more strings than the grammar from RFC. -+ * This issue was already raised on IETF mailing list: https://mailarchive.ietf.org/arch/msg/netconf/TYBpTE_ELzzMOe6amrw6fQF07nE/ -+ * but neither a formal errata was issued nor there was a resolution on the mailing list. -+ */ -+const auto fieldsExpr = x3::rule{"fieldsExpr"}; -+const auto fieldsSemi = x3::rule{"fieldsSemi"}; -+const auto fieldsSlash = x3::rule{"fieldsSlash"}; -+const auto fieldsParen = x3::rule{"fieldsParen"}; -+ -+const auto fieldsSemi_def = fieldsParen >> -(x3::lit(";") >> fieldsSemi); -+const auto fieldsParen_def = fieldsSlash >> -(x3::lit("(") >> fieldsExpr >> x3::lit(")")); -+const auto fieldsSlash_def = apiIdentifier >> -(x3::lit("/") >> fieldsSlash); -+const auto fieldsExpr_def = fieldsSemi; -+BOOST_SPIRIT_DEFINE(fieldsParen, fieldsExpr, fieldsSlash, fieldsSemi); -+ - // early sanity check, this timestamp will be parsed by libyang::fromYangTimeFormat anyways - const auto dateAndTime = x3::rule{"dateAndTime"} = - x3::repeat(4)[x3::digit] >> x3::char_('-') >> x3::repeat(2)[x3::digit] >> x3::char_('-') >> x3::repeat(2)[x3::digit] >> x3::char_('T') >> -@@ -127,7 +152,8 @@ const auto queryParamPair = x3::rule> "=" >> uriPath) | - (x3::string("filter") >> "=" >> filter) | - (x3::string("start-time") >> "=" >> dateAndTime) | -- (x3::string("stop-time") >> "=" >> dateAndTime); -+ (x3::string("stop-time") >> "=" >> dateAndTime) | -+ (x3::string("fields") >> "=" >> fieldsExpr); - - const auto queryParamGrammar = x3::rule{"queryParamGrammar"} = queryParamPair % "&" | x3::eps; - -@@ -384,7 +410,7 @@ void validateQueryParameters(const std::multimap allowedHttpMethodsForUri(const libyang::Context& ctx, cons - - return allowedHttpMethods; - } -+ -+/** @brief Traverses the AST of the fields input expression and collects all the possible paths -+ * -+ * @param expr The fields expressions -+ * @param currentPath The current path in the AST, it serves as a stack for the DFS -+ * @param output The collection of all collected paths -+ * @param end If this is the terminal node, i.e., the last node in the expression. This is needed for the correct handling of the leafs under paren expression, which does not "split" the paths but rather concatenates. -+ * */ -+void fieldsToXPath(const queryParams::fields::Expr& expr, std::vector& currentPath, std::vector& output, bool end = false) -+{ -+ boost::apply_visitor([&](auto&& node) { -+ using T = std::decay_t; -+ -+ if constexpr (std::is_same_v) { -+ // the paths from left and right subtree are concatenated, i.e., the nodes we collect in the left tree -+ // are joined together with the nodes from the right tree -+ fieldsToXPath(node.lhs, currentPath, output, !node.rhs.has_value()); -+ if (node.rhs) { -+ fieldsToXPath(*node.rhs, currentPath, output, end); -+ } -+ } else if constexpr (std::is_same_v) { -+ // the two paths are now independent and nodes from left subtree do not affect the right subtree -+ // hence we need to copy the current path -+ auto pathCopy = currentPath; -+ fieldsToXPath(node.lhs, currentPath, output, !node.rhs.has_value()); -+ if (node.rhs) { -+ fieldsToXPath(*node.rhs, pathCopy, output, false); -+ } -+ } else if constexpr (std::is_same_v) { -+ // the paths from left and right subtree are concatenated, i.e., the the nodes we collect in the left tree -+ // are joined together with the nodes from the right tree, but if this is the terminal node, we need to -+ // add it to the collection of all the gathered paths -+ currentPath.push_back(node.lhs.name()); -+ -+ if (node.rhs) { -+ fieldsToXPath(*node.rhs, currentPath, output, end); -+ } else if (end) { -+ output.emplace_back(boost::algorithm::join(currentPath, "/")); -+ } -+ } -+ }, expr); -+} -+ -+/** @brief Translates the fields expression into a XPath expression and checks for schema validity of the resulting nodes -+ * -+ * The expressions are "unwrapped" into a linear structure and then a union of such paths is made. -+ * E.g., the expression "a(b;c)" is translated into "a/b | a/c". -+ * */ -+std::string fieldsToXPath(const libyang::Context& ctx, const std::string& prefix, const queryParams::fields::Expr& expr) -+{ -+ std::vector currentPath{prefix}; -+ std::vector paths; -+ -+ fieldsToXPath(expr, currentPath, paths); -+ -+ for (auto& xpath : paths) { -+ try { -+ validateMethodForNode("GET", impl::URIPrefix{impl::URIPrefix::Type::BasicRestconfData}, ctx.findPath(xpath)); -+ } catch (const libyang::Error& e) { -+ throw ErrorResponse(400, "application", "operation-failed", "Can't find schema node for '" + xpath + "'"); -+ } -+ } -+ -+ return boost::algorithm::join(paths, " | "); -+} - } -diff --git a/src/restconf/uri.h b/src/restconf/uri.h -index 5e079ef..f6df724 100644 ---- a/src/restconf/uri.h -+++ b/src/restconf/uri.h -@@ -6,6 +6,7 @@ - - #pragma once - #include -+#include - #include - #include - #include -@@ -101,6 +102,57 @@ struct After { - using PointParsed = std::vector; - } - -+namespace fields { -+struct ParenExpr; -+struct SemiExpr; -+struct SlashExpr; -+ -+using Expr = boost::variant, boost::recursive_wrapper, boost::recursive_wrapper>; -+ -+struct ParenExpr { -+ Expr lhs; -+ boost::optional rhs; -+ -+ ParenExpr() = default; -+ ParenExpr(const Expr& lhs, const Expr& rhs) : ParenExpr(lhs, boost::optional(rhs)) {} -+ ParenExpr(const Expr& lhs, const boost::optional& rhs = boost::none) -+ : lhs(lhs) -+ , rhs(rhs) -+ { -+ } -+ -+ bool operator==(const ParenExpr&) const = default; -+}; -+struct SemiExpr { -+ Expr lhs; -+ boost::optional rhs; -+ -+ SemiExpr() = default; -+ SemiExpr(const Expr& lhs, const Expr& rhs) : SemiExpr(lhs, boost::optional(rhs)) {} -+ SemiExpr(const Expr& lhs, const boost::optional& rhs = boost::none) -+ : lhs(lhs) -+ , rhs(rhs) -+ { -+ } -+ -+ bool operator==(const SemiExpr&) const = default; -+}; -+struct SlashExpr { -+ ApiIdentifier lhs; -+ boost::optional rhs; -+ -+ SlashExpr() = default; -+ SlashExpr(const ApiIdentifier& lhs, const Expr& rhs) : SlashExpr(lhs, boost::optional(rhs)) {} -+ SlashExpr(const ApiIdentifier& lhs, const boost::optional& rhs = boost::none) -+ : lhs(lhs) -+ , rhs(rhs) -+ { -+ } -+ -+ bool operator==(const SlashExpr&) const = default; -+}; -+} -+ - using QueryParamValue = std::variant< - UnboundedDepth, - unsigned int, -@@ -116,7 +168,8 @@ using QueryParamValue = std::variant< - insert::Last, - insert::Before, - insert::After, -- insert::PointParsed>; -+ insert::PointParsed, -+ fields::Expr>; - using QueryParams = std::multimap; - } - -@@ -159,4 +212,6 @@ std::vector asPathSegments(const std::string& uriPath); - std::optional> asYangModule(const libyang::Context& ctx, const std::string& uriPath); - RestconfStreamRequest asRestconfStreamRequest(const std::string& httpMethod, const std::string& uriPath, const std::string& uriQueryString); - std::set allowedHttpMethodsForUri(const libyang::Context& ctx, const std::string& uriPath); -+ -+std::string fieldsToXPath(const libyang::Context& ctx, const std::string& prefix, const queryParams::fields::Expr& expr); - } -diff --git a/src/restconf/uri_impl.h b/src/restconf/uri_impl.h -index 8a2e166..2bcdb3f 100644 ---- a/src/restconf/uri_impl.h -+++ b/src/restconf/uri_impl.h -@@ -65,3 +65,7 @@ BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::impl::URIPath, prefix, segments); - 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::queryParams::fields::ParenExpr, lhs, rhs); -+BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::SlashExpr, lhs, rhs); -+BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::SemiExpr, lhs, rhs); -diff --git a/tests/pretty_printers.h b/tests/pretty_printers.h -index ec87250..a2befeb 100644 ---- a/tests/pretty_printers.h -+++ b/tests/pretty_printers.h -@@ -8,6 +8,7 @@ - #pragma once - - #include "trompeloeil_doctest.h" -+#include - #include - #include - #include -@@ -159,6 +160,28 @@ struct StringMaker { - [](const rousette::restconf::queryParams::insert::PointParsed& p) -> std::string { - return ("PointParsed{" + StringMaker::convert(p) + "}").c_str(); - }, -+ [](const rousette::restconf::queryParams::fields::Expr& expr) -> std::string { -+ return boost::apply_visitor([&](auto&& next) { -+ using T = std::decay_t; -+ std::string res; -+ -+ if constexpr (std::is_same_v || std::is_same_v) { -+ if constexpr (std::is_same_v) { -+ res = "ParenExpr{"; -+ } else { -+ res = "SemiExpr{"; -+ } -+ res += StringMaker::convert(next.lhs).c_str(); -+ } else if constexpr (std::is_same_v) { -+ res = "SlashExpr{" + next.lhs.name(); -+ } -+ -+ if (next.rhs) { -+ res += std::string(", ") + StringMaker::convert(*next.rhs).c_str(); -+ } -+ return res += "}"; -+ }, expr); -+ }, - }, obj).c_str(); - } - }; -diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp -index 2ded3f0..d7d507b 100644 ---- a/tests/restconf-reading.cpp -+++ b/tests/restconf-reading.cpp -@@ -72,7 +72,8 @@ TEST_CASE("reading data") - "urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit", - "urn:ietf:params:restconf:capability:depth:1.0", - "urn:ietf:params:restconf:capability:with-defaults:1.0", -- "urn:ietf:params:restconf:capability:filter:1.0" -+ "urn:ietf:params:restconf:capability:filter:1.0", -+ "urn:ietf:params:restconf:capability:fields:1.0" - ] - }, - "streams": { -@@ -116,7 +117,8 @@ TEST_CASE("reading data") - "urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit", - "urn:ietf:params:restconf:capability:depth:1.0", - "urn:ietf:params:restconf:capability:with-defaults:1.0", -- "urn:ietf:params:restconf:capability:filter:1.0" -+ "urn:ietf:params:restconf:capability:filter:1.0", -+ "urn:ietf:params:restconf:capability:fields:1.0" - ] - }, - "streams": { -@@ -672,7 +674,8 @@ TEST_CASE("reading data") - "urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit", - "urn:ietf:params:restconf:capability:depth:1.0", - "urn:ietf:params:restconf:capability:with-defaults:1.0", -- "urn:ietf:params:restconf:capability:filter:1.0" -+ "urn:ietf:params:restconf:capability:filter:1.0", -+ "urn:ietf:params:restconf:capability:fields:1.0" - ] - }, - "streams": { -@@ -894,6 +897,141 @@ TEST_CASE("reading data") - )"}); - } - -+ SECTION("fields filtering") -+ { -+ srSess.switchDatastore(sysrepo::Datastore::Running); -+ srSess.setItem("/example:tlc/list[name='blabla']/choice1", "c1"); -+ srSess.setItem("/example:tlc/list[name='blabla']/collection[.='42']", std::nullopt); -+ srSess.setItem("/example:tlc/list[name='blabla']/nested[first='1'][second='2'][third='3']/fourth", "4"); -+ srSess.setItem("/example:tlc/list[name='blabla']/nested[first='1'][second='2'][third='3']/data/a", "a"); -+ srSess.setItem("/example:tlc/list[name='blabla']/nested[first='1'][second='2'][third='3']/data/other-data/b", "b"); -+ srSess.setItem("/example:tlc/list[name='blabla']/nested[first='1'][second='2'][third='3']/data/other-data/c", "c"); -+ srSess.applyChanges(); -+ -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/list=blabla?fields=choice1;collection", {}) == Response{200, jsonHeaders, R"({ -+ "example:tlc": { -+ "list": [ -+ { -+ "name": "blabla", -+ "collection": [ -+ 42 -+ ], -+ "choice1": "c1" -+ } -+ ] -+ } -+} -+)"}); -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/list=blabla?fields=choice1;choice2;nested/data(a;other-data/b)", {}) == Response{200, jsonHeaders, R"({ -+ "example:tlc": { -+ "list": [ -+ { -+ "name": "blabla", -+ "nested": [ -+ { -+ "first": "1", -+ "second": 2, -+ "third": "3", -+ "data": { -+ "a": "a", -+ "other-data": { -+ "b": "b" -+ } -+ } -+ } -+ ], -+ "choice1": "c1" -+ } -+ ] -+ } -+} -+)"}); -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/list=blabla?fields=hehe", {}) == Response{400, jsonHeaders, R"({ -+ "ietf-restconf:errors": { -+ "error": [ -+ { -+ "error-type": "application", -+ "error-tag": "operation-failed", -+ "error-message": "Can't find schema node for '/example:tlc/list[name='blabla']/hehe'" -+ } -+ ] -+ } -+} -+)"}); -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/list=blabla?fields=nested/data&depth=1", {}) == Response{200, jsonHeaders, R"({ -+ "example:tlc": { -+ "list": [ -+ { -+ "name": "blabla", -+ "nested": [ -+ { -+ "first": "1", -+ "second": 2, -+ "third": "3", -+ "data": { -+ "a": "a", -+ "other-data": {} -+ } -+ } -+ ] -+ } -+ ] -+ } -+} -+)"}); -+ -+ // whole datastore with fields filtering -+ REQUIRE(get(RESTCONF_DATA_ROOT "?fields=example:tlc/list/nested/data&depth=1", {}) == Response{200, jsonHeaders, R"({ -+ "example:tlc": { -+ "list": [ -+ { -+ "name": "blabla", -+ "nested": [ -+ { -+ "first": "1", -+ "second": 2, -+ "third": "3", -+ "data": { -+ "a": "a", -+ "other-data": {} -+ } -+ } -+ ] -+ } -+ ] -+ } -+} -+)"}); -+ -+ // nonexistent schema node in fields: missing prefix in tlc because we query root node so libyang can't infer the prefix from the path -+ REQUIRE(get(RESTCONF_DATA_ROOT "?fields=tlc", {}) == Response{400, jsonHeaders, R"({ -+ "ietf-restconf:errors": { -+ "error": [ -+ { -+ "error-type": "application", -+ "error-tag": "operation-failed", -+ "error-message": "Can't find schema node for '/tlc'" -+ } -+ ] -+ } -+} -+)"}); -+ -+ // nonexistent schema node in fields -+ REQUIRE(get(RESTCONF_DATA_ROOT "?fields=example:tlc/ob-la-di-ob-la-da", {}) == Response{400, jsonHeaders, R"({ -+ "ietf-restconf:errors": { -+ "error": [ -+ { -+ "error-type": "application", -+ "error-tag": "operation-failed", -+ "error-message": "Can't find schema node for '/example:tlc/ob-la-di-ob-la-da'" -+ } -+ ] -+ } -+} -+)"}); -+ } -+ - SECTION("OPTIONS method") - { - // RPC node -diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp -index c1a9515..582a262 100644 ---- a/tests/restconf-writing.cpp -+++ b/tests/restconf-writing.cpp -@@ -375,6 +375,8 @@ TEST_CASE("writing data") - CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/first", "1"), - CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/second", "2"), - CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/third", "3"), -+ CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/data", std::nullopt), -+ CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/data/other-data", std::nullopt), - CREATED("/example:tlc/list[name='large']/choice2", "large")); - REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/list=large", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list":[{"name": "large", "choice2": "large", "example:nested": [{"first": "1", "second": 2, "third": "3"}]}]})") == Response{201, noContentTypeHeaders, ""}); - } -@@ -385,7 +387,9 @@ TEST_CASE("writing data") - CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']", std::nullopt), - CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/first", "11"), - CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/second", "12"), -- CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/third", "13")); -+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/third", "13"), -+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/data", std::nullopt), -+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/data/other-data", std::nullopt)); - REQUIRE(put(RESTCONF_DATA_ROOT "/example:tlc/list=libyang/nested=11,12,13", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:nested": [{"first": "11", "second": 12, "third": "13"}]}]})") == Response{201, noContentTypeHeaders, ""}); - } - -@@ -1339,6 +1343,8 @@ TEST_CASE("writing data") - CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/first", "1"), - CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/second", "2"), - CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/third", "3"), -+ CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/data", std::nullopt), -+ CREATED("/example:tlc/list[name='large']/nested[first='1'][second='2'][third='3']/data/other-data", std::nullopt), - CREATED("/example:tlc/list[name='large']/choice2", "large")); - REQUIRE(post(RESTCONF_DATA_ROOT "/example:tlc", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:list":[{"name": "large", "choice2": "large", "example:nested": [{"first": "1", "second": 2, "third": "3"}]}]})") == Response{201, jsonHeaders, ""}); - } -@@ -1349,7 +1355,9 @@ TEST_CASE("writing data") - CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']", std::nullopt), - CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/first", "11"), - CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/second", "12"), -- CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/third", "13")); -+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/third", "13"), -+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/data", std::nullopt), -+ CREATED("/example:tlc/list[name='libyang']/nested[first='11'][second='12'][third='13']/data/other-data", std::nullopt)); - REQUIRE(post(RESTCONF_DATA_ROOT "/example:tlc/list=libyang", {AUTH_ROOT, CONTENT_TYPE_JSON}, R"({"example:nested": [{"first": "11", "second": 12, "third": "13"}]}]})") == Response{201, jsonHeaders, ""}); - } - -diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp -index 000fe5d..fd5dd8b 100644 ---- a/tests/uri-parser.cpp -+++ b/tests/uri-parser.cpp -@@ -828,6 +828,65 @@ TEST_CASE("URI path parser") - REQUIRE(parseQueryParams("stop-time=2023-05-20T18:30:00") == std::nullopt); - REQUIRE(parseQueryParams("stop-time=20230520T18:30:00Z") == std::nullopt); - REQUIRE(parseQueryParams("stop-time=2023-05-a0T18:30:00+05:30") == std::nullopt); -+ REQUIRE(parseQueryParams("fields=mod:leaf") == QueryParams{{"fields", fields::SemiExpr{fields::ParenExpr{fields::SlashExpr{{"mod", "leaf"}}}}}}); -+ REQUIRE(parseQueryParams("fields=b(c;d);e(f)") == QueryParams{{"fields", -+ fields::SemiExpr{ -+ fields::ParenExpr{ -+ fields::SlashExpr{{"b"}}, -+ fields::SemiExpr{ -+ fields::ParenExpr{ -+ fields::SlashExpr{{"c"}} -+ }, -+ fields::SemiExpr{ -+ fields::ParenExpr{ -+ fields::SlashExpr{{"d"}} -+ } -+ } -+ } -+ }, -+ fields::SemiExpr{ -+ fields::ParenExpr{ -+ fields::SlashExpr{{"e"}}, -+ fields::SemiExpr{ -+ fields::ParenExpr{ -+ fields::SlashExpr{{"f"}} -+ } -+ } -+ } -+ } -+ } -+ }}); -+ REQUIRE(parseQueryParams("fields=(xyz)") == std::nullopt); -+ REQUIRE(parseQueryParams("fields=a;(xyz)") == std::nullopt); -+ REQUIRE(parseQueryParams("fields=") == std::nullopt); -+ -+ for (const auto& [prefix, fields, xpath] : { -+ std::tuple{"/example:a", "b", "/example:a/b"}, -+ {"/example:a", "b/c", "/example:a/b/c"}, -+ {"/example:a/b", "c(enabled;blower)", "/example:a/b/c/enabled | /example:a/b/c/blower"}, -+ {"/example:a", "b(c(enabled;blower))", "/example:a/b/c/enabled | /example:a/b/c/blower"}, -+ {"/example:a", "b(c)", "/example:a/b/c"}, -+ {"/example:a", "example:b;something", "/example:a/example:b | /example:a/something"}, -+ {"/example:a", "something;b1;b(c/enabled;c/blower)", "/example:a/something | /example:a/b1 | /example:a/b/c/enabled | /example:a/b/c/blower"}, -+ {"/example:a", "b(c/enabled;c/blower);something;b1", "/example:a/b/c/enabled | /example:a/b/c/blower | /example:a/something | /example:a/b1"}, // not allowed by RFC 8040 -+ {"", "example:a(b;b1)", "/example:a/b | /example:a/b1"}, -+ }) { -+ CAPTURE(fields); -+ CAPTURE(xpath); -+ auto qp = parseQueryParams("fields=" + fields); -+ REQUIRE(qp); -+ REQUIRE(qp->count("fields") == 1); -+ auto fieldExpr = qp->find("fields")->second; -+ REQUIRE(std::holds_alternative(fieldExpr)); -+ REQUIRE(rousette::restconf::fieldsToXPath(ctx, prefix, std::get(fieldExpr)) == xpath); -+ } -+ -+ auto qp = parseQueryParams("fields=xxx/xyz(a;b)"); -+ REQUIRE(qp); -+ REQUIRE_THROWS_WITH_AS( -+ rousette::restconf::fieldsToXPath(ctx, "/example:a", std::get(qp->find("fields")->second)), -+ serializeErrorResponse(400, "application", "operation-failed", "Can't find schema node for '/example:a/xxx/xyz/a'").c_str(), -+ rousette::restconf::ErrorResponse); - } - - SECTION("Full requests with validation") -@@ -885,6 +944,40 @@ TEST_CASE("URI path parser") - rousette::restconf::ErrorResponse); - } - -+ SECTION("fields") -+ { -+ auto resp = asRestconfRequest(ctx, "GET", "/restconf/data/example:a", "fields=b/c(enabled;blower)"); -+ REQUIRE(resp.queryParams == QueryParams({{"fields", -+ fields::SemiExpr{ -+ fields::ParenExpr{ -+ fields::SlashExpr{ -+ {"b"}, -+ fields::SlashExpr{{"c"}} -+ }, -+ fields::SemiExpr{ -+ fields::ParenExpr{ -+ fields::SlashExpr{{"enabled"}} -+ }, -+ fields::SemiExpr{ -+ fields::ParenExpr{ -+ fields::SlashExpr{{"blower"}} -+ } -+ } -+ } -+ } -+ } -+ } -+ })); -+ -+ REQUIRE_THROWS_WITH_AS(asRestconfRequest(ctx, "POST", "/restconf/data/example:a", "fields=b/c(enabled;blower)"), -+ serializeErrorResponse(400, "protocol", "invalid-value", "Query parameter 'fields' can be used only with GET and HEAD methods").c_str(), -+ rousette::restconf::ErrorResponse); -+ -+ REQUIRE_THROWS_WITH_AS(asRestconfStreamRequest("GET", "/streams/NETCONF/XML", "fields=a"), -+ serializeErrorResponse(400, "protocol", "invalid-value", "Query parameter 'fields' can't be used with streams").c_str(), -+ rousette::restconf::ErrorResponse); -+ } -+ - SECTION("insert first/last") - { - auto resp = asRestconfRequest(ctx, "PUT", "/restconf/data/example:tlc", "insert=first"); -diff --git a/tests/yang/example.yang b/tests/yang/example.yang -index 75cd7a6..5d586a0 100644 ---- a/tests/yang/example.yang -+++ b/tests/yang/example.yang -@@ -38,6 +38,14 @@ module example { - leaf first { type string; } - leaf second { type int32; } - leaf third { type string; } -+ leaf fourth { type string; } -+ container data { -+ leaf a { type string; } -+ container other-data { -+ leaf b { type string; } -+ leaf c { type string; } -+ } -+ } - } - choice choose { - mandatory true; -@@ -92,6 +100,8 @@ module example { - } - } - } -+ container b1 { } -+ leaf something { type string; } - } - - container two-leafs { --- -2.43.0 - 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..8c07b924 --- /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/17] 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-cmake-adhere-to-CMP0167.patch b/package/rousette/0009-cmake-adhere-to-CMP0167.patch deleted file mode 100644 index ef9f3b96..00000000 --- a/package/rousette/0009-cmake-adhere-to-CMP0167.patch +++ /dev/null @@ -1,36 +0,0 @@ -From 48d9b6ba3f3f892b9060b76b505d2f9a3aeb9e02 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 25 Nov 2024 09:15:55 +0100 -Subject: [PATCH 09/44] cmake: adhere to CMP0167 -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -After locally updating to cmake 3.30 I have seen a warning that our way -of finding boost library is deprecated [1]. - -[1] https://cmake.org/cmake/help/latest/policy/CMP0167.html - -Change-Id: I0cfc6cd0077fac48723487a280daac5fe8218ebb -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 465bef9..01dd2c2 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -72,7 +72,7 @@ find_package(spdlog REQUIRED) - find_package(date REQUIRED) # FIXME: Remove when we have STL with __cpp_lib_chrono >= 201907 (gcc 14) - find_package(PkgConfig) - pkg_check_modules(nghttp2 REQUIRED IMPORTED_TARGET libnghttp2_asio>=0.0.90 libnghttp2) --find_package(Boost REQUIRED COMPONENTS system thread) -+find_package(Boost REQUIRED CONFIG COMPONENTS system thread) - - pkg_check_modules(SYSREPO-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=3) - pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=3) --- -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..e83c5868 --- /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/17] 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/0010-Fix-compatibility-with-pam_wrapper-1.1.6.patch b/package/rousette/0010-Fix-compatibility-with-pam_wrapper-1.1.6.patch deleted file mode 100644 index 96de31d1..00000000 --- a/package/rousette/0010-Fix-compatibility-with-pam_wrapper-1.1.6.patch +++ /dev/null @@ -1,45 +0,0 @@ -From 64997543d48236cd2aae417568bc54d32c54df21 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Mon, 2 Dec 2024 14:43:36 +0100 -Subject: [PATCH 10/44] Fix compatibility with pam_wrapper 1.1.6+ -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -An year ago I reported a bug that the pam_wrapper project says that they -use a variable called `PAM_WRAPPER_DISABLE_DEEPBIND`, but in fact they -check `UID_WRAPPER_DISABLE_DEEPBIND`. The upstream listened to me, and -they fixed it [1]. Unfortunately, the old variable name is not read from -as of pam_wrapper 1.1.6, so we require setting *both* variables for -random version compatibility. - -[1] https://git.samba.org/?p=pam_wrapper.git;a=commitdiff;h=9f0cccf7432dd9be1de953f9b13a7f9b06c40442 - -Change-Id: I2959f505f5325950606c68b0b324be7181dd6e4f -Reported-by: Tomáš Pecka -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 3 ++- - 1 file changed, 2 insertions(+), 1 deletion(-) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 01dd2c2..731d7cb 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -179,10 +179,11 @@ if(BUILD_TESTING) - endif() - - if(TEST_WRAP_PAM) -+ # FIXME: remove UID_WRAPPER_... (and keep PAM_WRAPPER_...) once we require pam_wrapper 1.1.6+ - set(TEST_COMMAND - ${UNSHARE_EXECUTABLE} -r -m sh -c "set -ex $ - ${MOUNT_EXECUTABLE} -t tmpfs none /tmp $ -- export LD_PRELOAD=${pam_wrapper_LDFLAGS} PAM_WRAPPER_SERVICE_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/pam PAM_WRAPPER=1 UID_WRAPPER_DISABLE_DEEPBIND=1 $ -+ export LD_PRELOAD=${pam_wrapper_LDFLAGS} PAM_WRAPPER_SERVICE_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/pam PAM_WRAPPER=1 UID_WRAPPER_DISABLE_DEEPBIND=1 PAM_WRAPPER_DISABLE_DEEPBIND=1 $ - $") - else() - set(TEST_COMMAND test-${TEST_NAME}) --- -2.43.0 - diff --git a/package/rousette/0010-http-send-keep-alive-pings-from-EventStream.patch b/package/rousette/0010-http-send-keep-alive-pings-from-EventStream.patch new file mode 100644 index 00000000..6dd69f96 --- /dev/null +++ b/package/rousette/0010-http-send-keep-alive-pings-from-EventStream.patch @@ -0,0 +1,428 @@ +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 10/17] 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 +activity in the stream [1]. In our use-case I can imagine it can be +quite common that no data will flow for a long period of time. If no +data are updated (i.e., no notifications, or no YANG data changes in +sysrepo when we have YANG push), then nothing flows forth and back. + +The 60 second value is configurable [2], but I am not sure about the +value that should be used. 5 minutes? hours? days? + +Let's approach this differently. We can instead send a comment into the +stream every few seconds which will make sure that the connection will +not be dropped by nghttp2-asio. This approach is described in the +HTML Living Standard [3]. +For now, let's resort to sending a ping-alive every 55 seconds and +hardcode 60 seconds no activity timeout. + +I have verified that using this method a curl client keeps connected +to the server when the server sends these "keep-alive comments". + +[1] https://github.com/nghttp2/nghttp2/issues/555 +[2] https://github.com/nghttp2/nghttp2-asio/blob/e877868abe06a83ed0a6ac6e245c07f6f20866b5/lib/asio_server_http2_impl.h#L51 +[3] https://html.spec.whatwg.org/multipage/server-sent-events.html#authoring-notes + +Change-Id: I57e510d0b61ac7ed032c582779780c64768b7d53 +Signed-off-by: Mattias Walström +--- + src/clock.cpp | 4 +- + src/http/EventStream.cpp | 44 +++++++++++++-- + src/http/EventStream.h | 13 ++++- + src/restconf/NotificationStream.cpp | 3 +- + src/restconf/NotificationStream.h | 1 + + src/restconf/Server.cpp | 21 +++++-- + src/restconf/Server.h | 6 +- + tests/restconf-eventstream.cpp | 85 +++++++++++++++++++++-------- + 8 files changed, 139 insertions(+), 38 deletions(-) + +diff --git a/src/clock.cpp b/src/clock.cpp +index c85be19..24e9efb 100644 +--- a/src/clock.cpp ++++ b/src/clock.cpp +@@ -14,6 +14,8 @@ + + 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); +@@ -32,7 +34,7 @@ int main(int argc [[maybe_unused]], char** argv [[maybe_unused]]) + server.num_threads(4); + + server.handle("/events", [&shutdown, &sig](const auto& req, const auto& res) { +- auto client = std::make_shared(req, res, shutdown, sig); ++ auto client = std::make_shared(req, res, shutdown, sig, keepAlivePingInterval); + client->activate(); + }); + +diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp +index 77c282d..82f8b38 100644 +--- a/src/http/EventStream.cpp ++++ b/src/http/EventStream.cpp +@@ -17,17 +17,26 @@ using namespace nghttp2::asio_http2; + + namespace rousette::http { + ++constexpr auto FIELD_DATA = "data"; ++ + /** @short After constructing, make sure to call activate() immediately. */ +-EventStream::EventStream(const server::request& req, const server::response& res, Termination& termination, EventSignal& signal, const std::optional& initialEvent) ++EventStream::EventStream(const server::request& req, ++ const server::response& res, ++ Termination& termination, ++ EventSignal& signal, ++ const std::chrono::seconds keepAlivePingInterval, ++ const std::optional& initialEvent) + : res{res} ++ , ping{res.io_service()} + , peer{peer_from_request(req)} ++ , m_keepAlivePingInterval(keepAlivePingInterval) + { + if (initialEvent) { +- enqueue(*initialEvent); ++ enqueue(FIELD_DATA, *initialEvent); + } + + eventSub = signal.connect([this](const auto& msg) { +- enqueue(msg); ++ enqueue(FIELD_DATA, msg); + }); + + terminateSub = termination.connect([this]() { +@@ -56,6 +65,8 @@ shared_from_this() throws bad_weak_ptr, so we need a two-phase construction. + */ + void EventStream::activate() + { ++ start_ping(); ++ + auto client = shared_from_this(); + res.write_head(200, { + {"content-type", {"text/event-stream", false}}, +@@ -65,6 +76,7 @@ void EventStream::activate() + res.on_close([client](const auto ec) { + spdlog::debug("{}: closed ({})", client->peer, nghttp2_http2_strerror(ec)); + std::lock_guard lock{client->mtx}; ++ client->ping.cancel(); + client->eventSub.disconnect(); + client->terminateSub.disconnect(); + client->state = Closed; +@@ -113,14 +125,15 @@ ssize_t EventStream::process(uint8_t* destination, std::size_t len, uint32_t* da + __builtin_unreachable(); + } + +-void EventStream::enqueue(const std::string& what) ++void EventStream::enqueue(const std::string& fieldName, const std::string& what) + { + std::string buf; + buf.reserve(what.size()); + const std::regex newline{"\n"}; + for (auto it = std::sregex_token_iterator{what.begin(), what.end(), newline, -1}; + it != std::sregex_token_iterator{}; ++it) { +- buf += "data: "; ++ buf += fieldName; ++ buf += ": "; + buf += *it; + buf += '\n'; + } +@@ -139,4 +152,25 @@ void EventStream::enqueue(const std::string& what) + state = HasEvents; + boost::asio::post(res.io_service(), [&res = this->res]() { res.resume(); }); + } ++ ++void EventStream::start_ping() ++{ ++ ping.expires_from_now(boost::posix_time::seconds(m_keepAlivePingInterval.count())); ++ ping.async_wait([maybeClient = weak_from_this()](const boost::system::error_code& ec) { ++ auto client = maybeClient.lock(); ++ if (!client) { ++ spdlog::trace("ping: client already gone"); ++ return; ++ } ++ ++ if (ec == boost::asio::error::operation_aborted) { ++ spdlog::trace("{}: ping scheduler cancelled", client->peer); ++ return; ++ } ++ ++ client->enqueue("", "\n"); ++ spdlog::trace("{}: keep-alive ping enqueued", client->peer); ++ client->start_ping(); ++ }); ++} + } +diff --git a/src/http/EventStream.h b/src/http/EventStream.h +index b427fb3..4b3578a 100644 +--- a/src/http/EventStream.h ++++ b/src/http/EventStream.h +@@ -7,6 +7,7 @@ + + #pragma once + ++#include + #include + #include + #include +@@ -30,7 +31,12 @@ public: + using EventSignal = boost::signals2::signal; + using Termination = boost::signals2::signal; + +- EventStream(const nghttp2::asio_http2::server::request& req, const nghttp2::asio_http2::server::response& res, Termination& terminate, EventSignal& signal, const std::optional& initialEvent = std::nullopt); ++ EventStream(const nghttp2::asio_http2::server::request& req, ++ const nghttp2::asio_http2::server::response& res, ++ Termination& terminate, ++ EventSignal& signal, ++ const std::chrono::seconds keepAlivePingInterval, ++ const std::optional& initialEvent = std::nullopt); + void activate(); + + private: +@@ -43,13 +49,16 @@ private: + }; + + State state = WaitingForEvents; ++ boost::asio::deadline_timer ping; + std::list queue; + mutable std::mutex mtx; // for `state` and `queue` + boost::signals2::scoped_connection eventSub, terminateSub; + const std::string peer; ++ const std::chrono::seconds m_keepAlivePingInterval; + + size_t send_chunk(uint8_t* destination, std::size_t len, uint32_t* data_flags); + ssize_t process(uint8_t* destination, std::size_t len, uint32_t* data_flags); +- void enqueue(const std::string& what); ++ void enqueue(const std::string& fieldName, const std::string& what); ++ void start_ping(); + }; + } +diff --git a/src/restconf/NotificationStream.cpp b/src/restconf/NotificationStream.cpp +index d0ba938..5017f3e 100644 +--- a/src/restconf/NotificationStream.cpp ++++ b/src/restconf/NotificationStream.cpp +@@ -89,12 +89,13 @@ NotificationStream::NotificationStream( + const nghttp2::asio_http2::server::response& res, + rousette::http::EventStream::Termination& termination, + std::shared_ptr signal, ++ const std::chrono::seconds keepAlivePingInterval, + sysrepo::Session session, + libyang::DataFormat dataFormat, + const std::optional& filter, + const std::optional& startTime, + const std::optional& stopTime) +- : EventStream(req, res, termination, *signal) ++ : EventStream(req, res, termination, *signal, keepAlivePingInterval) + , m_notificationSignal(signal) + , m_session(std::move(session)) + , m_dataFormat(dataFormat) +diff --git a/src/restconf/NotificationStream.h b/src/restconf/NotificationStream.h +index 46f8416..daa79d6 100644 +--- a/src/restconf/NotificationStream.h ++++ b/src/restconf/NotificationStream.h +@@ -44,6 +44,7 @@ public: + const nghttp2::asio_http2::server::response& res, + rousette::http::EventStream::Termination& termination, + std::shared_ptr signal, ++ const std::chrono::seconds keepAlivePingInterval, + sysrepo::Session sess, + libyang::DataFormat dataFormat, + const std::optional& filter, +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index bd2ce0d..31b3d0f 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -864,13 +864,14 @@ 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) ++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()} + , 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 ++ server->read_timeout(boost::posix_time::seconds{60}); // terminate connection after 60 seconds of inactivity (this is explicitly setting the default value) + + for (const auto& [module, version, features] : { + std::tuple>{"ietf-restconf", "2017-01-26", {}}, +@@ -930,14 +931,14 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: + res.end(""s); + }); + +- server->handle("/telemetry/optics", [this](const auto& req, const auto& res) { ++ server->handle("/telemetry/optics", [this, keepAlivePingInterval](const auto& req, const auto& res) { + logRequest(req); + +- auto client = std::make_shared(req, res, shutdownRequested, opticsChange, as_restconf_push_update(dwdmEvents->currentData(), std::chrono::system_clock::now())); ++ auto client = std::make_shared(req, res, shutdownRequested, opticsChange, keepAlivePingInterval, as_restconf_push_update(dwdmEvents->currentData(), std::chrono::system_clock::now())); + client->activate(); + }); + +- server->handle(netconfStreamRoot, [this, conn](const auto& req, const auto& res) mutable { ++ server->handle(netconfStreamRoot, [this, conn, keepAlivePingInterval](const auto& req, const auto& res) mutable { + logRequest(req); + + std::optional xpathFilter; +@@ -970,7 +971,17 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: + // The signal is constructed outside NotificationStream class because it is required to be passed to + // NotificationStream's parent (EventStream) constructor where it already must be constructed + // Yes, this is a hack. +- auto client = std::make_shared(req, res, shutdownRequested, std::make_shared(), sess, streamRequest.type.encoding, xpathFilter, startTime, stopTime); ++ auto client = std::make_shared( ++ req, ++ res, ++ shutdownRequested, ++ std::make_shared(), ++ keepAlivePingInterval, ++ sess, ++ streamRequest.type.encoding, ++ xpathFilter, ++ startTime, ++ stopTime); + client->activate(); + } catch (const auth::Error& e) { + processAuthError(req, res, e, [&res]() { +diff --git a/src/restconf/Server.h b/src/restconf/Server.h +index 112c943..0720c8d 100644 +--- a/src/restconf/Server.h ++++ b/src/restconf/Server.h +@@ -28,7 +28,11 @@ std::optional as_subtree_path(const std::string& path); + /** @short A RESTCONF-ish server */ + class Server { + public: +- explicit Server(sysrepo::Connection conn, const std::string& address, const std::string& port, const std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); ++ explicit Server(sysrepo::Connection conn, ++ 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}); + ~Server(); + void join(); + void stop(); +diff --git a/tests/restconf-eventstream.cpp b/tests/restconf-eventstream.cpp +index 3d7fc15..ad1040d 100644 +--- a/tests/restconf-eventstream.cpp ++++ b/tests/restconf-eventstream.cpp +@@ -21,9 +21,9 @@ static const auto SERVER_PORT = "10091"; + + using namespace std::chrono_literals; + +-TEST_CASE("Termination on server shutdown") ++TEST_CASE("Event stream tests") + { +- trompeloeil::sequence seqMod1; ++ trompeloeil::sequence seq1, seq2; + sysrepo::setLogLevelStderr(sysrepo::LogLevel::Information); + spdlog::set_level(spdlog::level::trace); + +@@ -34,37 +34,76 @@ TEST_CASE("Termination on server shutdown") + 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); + ++ const std::string notification(R"({"example:eventA":{"message":"blabla","progress":11}})"); + RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext()); + +- const std::string notification(R"({"example:eventA":{"message":"blabla","progress":11}})"); +- EXPECT_NOTIFICATION(notification, seqMod1); +- EXPECT_NOTIFICATION(notification, seqMod1); +- EXPECT_NOTIFICATION(notification, seqMod1); ++ SECTION("Termination on server shutdown") ++ { ++ auto server = std::make_unique(srConn, SERVER_ADDRESS, SERVER_PORT); ++ ++ EXPECT_NOTIFICATION(notification, seq1); ++ EXPECT_NOTIFICATION(notification, seq1); ++ EXPECT_NOTIFICATION(notification, seq1); ++ ++ auto notifSession = sysrepo::Connection{}.sessionStart(); ++ auto ctx = notifSession.getContext(); ++ ++ PREPARE_LOOP_WITH_EXCEPTIONS; ++ auto 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(notification); ++ SEND_NOTIFICATION(notification); ++ SEND_NOTIFICATION(notification); ++ waitForCompletionAndBitMore(seq1); ++ ++ auto beforeShutdown = std::chrono::system_clock::now(); ++ server.reset(); ++ auto shutdownDuration = std::chrono::system_clock::now() - beforeShutdown; ++ REQUIRE(shutdownDuration < 5s); ++ })); ++ ++ SSEClient client(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, "/streams/NETCONF/JSON", std::map{AUTH_ROOT}); ++ ++ RUN_LOOP_WITH_EXCEPTIONS; ++ } ++ ++ SECTION("Keep-alive pings") ++ { ++ constexpr auto pingInterval = 1s; + +- auto notifSession = sysrepo::Connection{}.sessionStart(); +- auto ctx = notifSession.getContext(); ++ RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext()); ++ auto server = std::make_unique(srConn, SERVER_ADDRESS, SERVER_PORT, std::chrono::milliseconds{0}, pingInterval); + +- PREPARE_LOOP_WITH_EXCEPTIONS; +- auto 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(notification); +- SEND_NOTIFICATION(notification); +- SEND_NOTIFICATION(notification); +- waitForCompletionAndBitMore(seqMod1); ++ EXPECT_NOTIFICATION(notification, seq1); ++ expectations.emplace_back(NAMED_REQUIRE_CALL(netconfWatcher, comment(": ")).IN_SEQUENCE(seq2).TIMES(AT_LEAST(1))); + +- auto beforeShutdown = std::chrono::system_clock::now(); +- server.reset(); +- auto shutdownDuration = std::chrono::system_clock::now() - beforeShutdown; +- REQUIRE(shutdownDuration < 5s); +- })); ++ PREPARE_LOOP_WITH_EXCEPTIONS; ++ auto notificationThread = std::jthread(wrap_exceptions_and_asio(bg, io, [&]() { ++ WAIT_UNTIL_SSE_CLIENT_REQUESTS; ++ SEND_NOTIFICATION(notification); ++ std::this_thread::sleep_for(3s); // Wait for the server to send at least one keep-alive ping ++ server.reset(); ++ })); + +- SSEClient client(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, "/streams/NETCONF/JSON", std::map{AUTH_ROOT}); ++ SSEClient client( ++ io, ++ SERVER_ADDRESS, ++ SERVER_PORT, ++ requestSent, ++ netconfWatcher, ++ "/streams/NETCONF/JSON", ++ std::map{AUTH_ROOT}, ++ boost::posix_time::seconds{5}, ++ SSEClient::ReportIgnoredLines::Yes); + +- RUN_LOOP_WITH_EXCEPTIONS; ++ RUN_LOOP_WITH_EXCEPTIONS; ++ } + } +-- +2.43.0 + diff --git a/package/rousette/0011-refactor-event-streams-use-named-constructors.patch b/package/rousette/0011-refactor-event-streams-use-named-constructors.patch new file mode 100644 index 00000000..fc507e62 --- /dev/null +++ b/package/rousette/0011-refactor-event-streams-use-named-constructors.patch @@ -0,0 +1,205 @@ +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 11/17] 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 +named constructors. +Both of these classes are required to be initialized with invoking the +constructor and then the activate method. The NotificationStream class +also required to be passed with a newly constructed Event object. +I find this pattern a bit confusing, so I refactored the code to use +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: Mattias Walström +--- + src/clock.cpp | 3 +-- + src/http/EventStream.cpp | 20 ++++++++++++++++++++ + src/http/EventStream.h | 22 +++++++++++++++------- + src/restconf/NotificationStream.cpp | 22 ++++++++++++++++++++++ + src/restconf/NotificationStream.h | 12 ++++++++++++ + src/restconf/Server.cpp | 10 ++-------- + 6 files changed, 72 insertions(+), 17 deletions(-) + +diff --git a/src/clock.cpp b/src/clock.cpp +index 24e9efb..aa72849 100644 +--- a/src/clock.cpp ++++ b/src/clock.cpp +@@ -34,8 +34,7 @@ int main(int argc [[maybe_unused]], char** argv [[maybe_unused]]) + server.num_threads(4); + + server.handle("/events", [&shutdown, &sig](const auto& req, const auto& res) { +- auto client = std::make_shared(req, res, shutdown, sig, keepAlivePingInterval); +- client->activate(); ++ rousette::http::EventStream::create(req, res, shutdown, sig, keepAlivePingInterval); + }); + + server.handle("/", [](const auto& req, const auto& resp) { +diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp +index 82f8b38..0893653 100644 +--- a/src/http/EventStream.cpp ++++ b/src/http/EventStream.cpp +@@ -173,4 +173,24 @@ void EventStream::start_ping() + client->start_ping(); + }); + } ++ ++/** @brief Create a new EventStream instance and activate it immediately. ++ * ++ * The stream is created with the given parameters and activated as if the activate() method was called. ++ * ``` ++ * auto a = make_shared(...); ++ * a->activate(); ++ * ``` ++ */ ++std::shared_ptr EventStream::create(const nghttp2::asio_http2::server::request& req, ++ const nghttp2::asio_http2::server::response& res, ++ Termination& terminate, ++ EventSignal& signal, ++ const std::chrono::seconds keepAlivePingInterval, ++ const std::optional& initialEvent) ++{ ++ auto stream = std::shared_ptr(new EventStream(req, res, terminate, signal, keepAlivePingInterval, initialEvent)); ++ stream->activate(); ++ return stream; ++} + } +diff --git a/src/http/EventStream.h b/src/http/EventStream.h +index 4b3578a..ef0a002 100644 +--- a/src/http/EventStream.h ++++ b/src/http/EventStream.h +@@ -31,13 +31,12 @@ public: + using EventSignal = boost::signals2::signal; + using Termination = boost::signals2::signal; + +- EventStream(const nghttp2::asio_http2::server::request& req, +- const nghttp2::asio_http2::server::response& res, +- Termination& terminate, +- EventSignal& signal, +- const std::chrono::seconds keepAlivePingInterval, +- const std::optional& initialEvent = std::nullopt); +- void activate(); ++ static std::shared_ptr create(const nghttp2::asio_http2::server::request& req, ++ const nghttp2::asio_http2::server::response& res, ++ Termination& terminate, ++ EventSignal& signal, ++ const std::chrono::seconds keepAlivePingInterval, ++ const std::optional& initialEvent = std::nullopt); + + private: + const nghttp2::asio_http2::server::response& res; +@@ -60,5 +59,14 @@ private: + ssize_t process(uint8_t* destination, std::size_t len, uint32_t* data_flags); + void enqueue(const std::string& fieldName, const std::string& what); + void start_ping(); ++ ++protected: ++ EventStream(const nghttp2::asio_http2::server::request& req, ++ const nghttp2::asio_http2::server::response& res, ++ Termination& terminate, ++ EventSignal& signal, ++ const std::chrono::seconds keepAlivePingInterval, ++ const std::optional& initialEvent = std::nullopt); ++ void activate(); + }; + } +diff --git a/src/restconf/NotificationStream.cpp b/src/restconf/NotificationStream.cpp +index 5017f3e..fe21809 100644 +--- a/src/restconf/NotificationStream.cpp ++++ b/src/restconf/NotificationStream.cpp +@@ -183,4 +183,26 @@ libyang::DataNode replaceStreamLocations(const std::optional& schem + + return node; + } ++ ++/** @brief Create a new NotificationStream instance and activate it immediately. ++ * ++ * The stream is created with the given parameters and activated, which means it starts listening for ++ * NETCONF notifications and sending them to the client. ++ */ ++std::shared_ptr NotificationStream::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, ++ sysrepo::Session sess, ++ libyang::DataFormat dataFormat, ++ const std::optional& filter, ++ const std::optional& startTime, ++ const std::optional& stopTime) ++{ ++ auto signal = std::make_shared(); ++ auto stream = std::shared_ptr(new NotificationStream(req, res, termination, signal, keepAlivePingInterval, std::move(sess), dataFormat, filter, startTime, stopTime)); ++ stream->activate(); ++ return stream; ++} + } +diff --git a/src/restconf/NotificationStream.h b/src/restconf/NotificationStream.h +index daa79d6..91c5dde 100644 +--- a/src/restconf/NotificationStream.h ++++ b/src/restconf/NotificationStream.h +@@ -39,6 +39,18 @@ class NotificationStream : public rousette::http::EventStream { + std::optional m_notifSubs; + + public: ++ 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, ++ sysrepo::Session sess, ++ libyang::DataFormat dataFormat, ++ const std::optional& filter, ++ const std::optional& startTime, ++ const std::optional& stopTime); ++ ++protected: + NotificationStream( + const nghttp2::asio_http2::server::request& req, + const nghttp2::asio_http2::server::response& res, +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 31b3d0f..8544210 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -934,8 +934,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: + server->handle("/telemetry/optics", [this, keepAlivePingInterval](const auto& req, const auto& res) { + logRequest(req); + +- auto client = std::make_shared(req, res, shutdownRequested, opticsChange, keepAlivePingInterval, as_restconf_push_update(dwdmEvents->currentData(), std::chrono::system_clock::now())); +- client->activate(); ++ http::EventStream::create(req, res, shutdownRequested, opticsChange, keepAlivePingInterval, as_restconf_push_update(dwdmEvents->currentData(), std::chrono::system_clock::now())); + }); + + server->handle(netconfStreamRoot, [this, conn, keepAlivePingInterval](const auto& req, const auto& res) mutable { +@@ -968,21 +967,16 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: + stopTime = libyang::fromYangTimeFormat(std::get(it->second)); + } + +- // The signal is constructed outside NotificationStream class because it is required to be passed to +- // NotificationStream's parent (EventStream) constructor where it already must be constructed +- // Yes, this is a hack. +- auto client = std::make_shared( ++ NotificationStream::create( + req, + res, + shutdownRequested, +- std::make_shared(), + keepAlivePingInterval, + sess, + streamRequest.type.encoding, + xpathFilter, + startTime, + stopTime); +- client->activate(); + } catch (const auth::Error& e) { + processAuthError(req, res, e, [&res]() { + res.write_head(401, {TEXT_PLAIN, CORS}); +-- +2.43.0 + diff --git a/package/rousette/0011-tests-add-missing-pragma-once.patch b/package/rousette/0011-tests-add-missing-pragma-once.patch deleted file mode 100644 index baff1334..00000000 --- a/package/rousette/0011-tests-add-missing-pragma-once.patch +++ /dev/null @@ -1,30 +0,0 @@ -From 849aa35274d5e07726cb849fe724962754b3fa29 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 2 Dec 2024 20:20:48 +0100 -Subject: [PATCH 11/44] tests: add missing pragma once -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: I269c20e5a914aa8c7bc9431147dd9785ea1aedda -Signed-off-by: Mattias Walström ---- - tests/aux-utils.h | 1 + - 1 file changed, 1 insertion(+) - -diff --git a/tests/aux-utils.h b/tests/aux-utils.h -index a3923bb..7482945 100644 ---- a/tests/aux-utils.h -+++ b/tests/aux-utils.h -@@ -6,6 +6,7 @@ - * - */ - -+#pragma once - #include "trompeloeil_doctest.h" - #include - #include --- -2.43.0 - diff --git a/package/rousette/0012-refactor-a-better-convention-for-weak_from_this-lock.patch b/package/rousette/0012-refactor-a-better-convention-for-weak_from_this-lock.patch new file mode 100644 index 00000000..8bdd105c --- /dev/null +++ b/package/rousette/0012-refactor-a-better-convention-for-weak_from_this-lock.patch @@ -0,0 +1,104 @@ +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 12/17] 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: Mattias Walström +--- + src/http/EventStream.cpp | 44 ++++++++++++++++++++-------------------- + 1 file changed, 22 insertions(+), 22 deletions(-) + +diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp +index 0893653..3bdd55e 100644 +--- a/src/http/EventStream.cpp ++++ b/src/http/EventStream.cpp +@@ -47,11 +47,11 @@ EventStream::EventStream(const server::request& req, + } + + state = WantToClose; +- boost::asio::post(this->res.io_service(), [maybeClient = std::weak_ptr{shared_from_this()}]() { +- if (auto client = maybeClient.lock()) { +- std::lock_guard lock{client->mtx}; +- if (client->state == WantToClose) { // resume unless somebody closed it before this was picked up by the event loop +- client->res.resume(); ++ boost::asio::post(this->res.io_service(), [weak = std::weak_ptr{shared_from_this()}]() { ++ if (auto myself = weak.lock()) { ++ std::lock_guard lock{myself->mtx}; ++ if (myself->state == WantToClose) { // resume unless somebody closed it before this was picked up by the event loop ++ myself->res.resume(); + } + } + }); +@@ -67,23 +67,23 @@ void EventStream::activate() + { + start_ping(); + +- auto client = shared_from_this(); ++ auto myself = shared_from_this(); + res.write_head(200, { + {"content-type", {"text/event-stream", false}}, + {"access-control-allow-origin", {"*", false}}, + }); + +- res.on_close([client](const auto ec) { +- spdlog::debug("{}: closed ({})", client->peer, nghttp2_http2_strerror(ec)); +- std::lock_guard lock{client->mtx}; +- client->ping.cancel(); +- client->eventSub.disconnect(); +- client->terminateSub.disconnect(); +- client->state = Closed; ++ 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; + }); + +- res.end([client](uint8_t* destination, std::size_t len, uint32_t* data_flags) { +- return client->process(destination, len, data_flags); ++ res.end([myself](uint8_t* destination, std::size_t len, uint32_t* data_flags) { ++ return myself->process(destination, len, data_flags); + }); + } + +@@ -156,21 +156,21 @@ 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.async_wait([maybeClient = weak_from_this()](const boost::system::error_code& ec) { +- auto client = maybeClient.lock(); +- if (!client) { ++ ping.async_wait([weak = weak_from_this()](const boost::system::error_code& ec) { ++ auto myself = weak.lock(); ++ if (!myself) { + spdlog::trace("ping: client already gone"); + return; + } + + if (ec == boost::asio::error::operation_aborted) { +- spdlog::trace("{}: ping scheduler cancelled", client->peer); ++ spdlog::trace("{}: ping scheduler cancelled", myself->peer); + return; + } + +- client->enqueue("", "\n"); +- spdlog::trace("{}: keep-alive ping enqueued", client->peer); +- client->start_ping(); ++ myself->enqueue("", "\n"); ++ spdlog::trace("{}: keep-alive ping enqueued", myself->peer); ++ myself->start_ping(); + }); + } + +-- +2.43.0 + diff --git a/package/rousette/0012-tests-extend-clientRequest-wrappers-interface-and-us.patch b/package/rousette/0012-tests-extend-clientRequest-wrappers-interface-and-us.patch deleted file mode 100644 index 1dcb9a3b..00000000 --- a/package/rousette/0012-tests-extend-clientRequest-wrappers-interface-and-us.patch +++ /dev/null @@ -1,205 +0,0 @@ -From 60eac2b2d60f8f1918a0914272975dd53f527c01 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 2 Dec 2024 19:36:10 +0100 -Subject: [PATCH 12/44] tests: extend clientRequest wrappers interface and use - it -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -This is a preparation for refactoring in the next few commits. -I will generalize the clientRequest interface to accept server -address and port too. - -The head/get/put/post/... helper methods will not require those server -port and address parameters. - -Change-Id: Iee54a3b3017ef9875fcd20640b74c7aa42813b9f -Signed-off-by: Mattias Walström ---- - tests/aux-utils.h | 34 +++++++++++++++++--------------- - tests/restconf-nacm.cpp | 14 +++++-------- - tests/restconf-notifications.cpp | 12 +++++------ - tests/restconf-yang-schema.cpp | 19 ++++++++++-------- - 4 files changed, 40 insertions(+), 39 deletions(-) - -diff --git a/tests/aux-utils.h b/tests/aux-utils.h -index 7482945..9afd7bc 100644 ---- a/tests/aux-utils.h -+++ b/tests/aux-utils.h -@@ -153,12 +153,14 @@ const ng::header_map eventStreamHeaders { - #define ACCESS_CONTROL_ALLOW_ORIGIN {"access-control-allow-origin", "*"} - #define ACCEPT_PATCH {"accept-patch", "application/yang-data+json, application/yang-data+xml, application/yang-patch+xml, application/yang-patch+json"} - -+// this is a test, and the server is expected to reply "soon" -+static const boost::posix_time::time_duration CLIENT_TIMEOUT = boost::posix_time::seconds(3); -+ - Response clientRequest(auto method, - auto uri, - const std::string& data, - const std::map& headers, -- // this is a test, and the server is expected to reply "soon" -- const boost::posix_time::time_duration timeout=boost::posix_time::seconds(3)) -+ const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) - { - boost::asio::io_service io_service; - auto client = std::make_shared(io_service, SERVER_ADDRESS, SERVER_PORT); -@@ -199,39 +201,39 @@ Response clientRequest(auto method, - return {statusCode, resHeaders, oss.str()}; - } - --Response get(auto uri, const std::map& headers) -+Response get(auto uri, const std::map& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) - { -- return clientRequest("GET", uri, "", headers); -+ return clientRequest("GET", uri, "", headers, timeout); - } - --Response options(auto uri, const std::map& headers) -+Response options(auto uri, const std::map& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) - { -- return clientRequest("OPTIONS", uri, "", headers); -+ return clientRequest("OPTIONS", uri, "", headers, timeout); - } - --Response head(auto uri, const std::map& headers) -+Response head(auto uri, const std::map& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) - { -- return clientRequest("HEAD", uri, "", headers); -+ return clientRequest("HEAD", uri, "", headers, timeout); - } - --Response put(auto xpath, const std::map& headers, const std::string& data) -+Response put(auto xpath, const std::map& headers, const std::string& data, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) - { -- return clientRequest("PUT", xpath, data, headers); -+ return clientRequest("PUT", xpath, data, headers, timeout); - } - --Response post(auto xpath, const std::map& headers, const std::string& data) -+Response post(auto xpath, const std::map& headers, const std::string& data, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) - { -- return clientRequest("POST", xpath, data, headers); -+ return clientRequest("POST", xpath, data, headers, timeout); - } - --Response patch(auto uri, const std::map& headers, const std::string& data) -+Response patch(auto uri, const std::map& headers, const std::string& data, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) - { -- return clientRequest("PATCH", uri, data, headers); -+ return clientRequest("PATCH", uri, data, headers, timeout); - } - --Response httpDelete(auto uri, const std::map& headers) -+Response httpDelete(auto uri, const std::map& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) - { -- return clientRequest("DELETE", uri, "", headers); -+ return clientRequest("DELETE", uri, "", headers, timeout); - } - - auto manageNacm(sysrepo::Session session) -diff --git a/tests/restconf-nacm.cpp b/tests/restconf-nacm.cpp -index 68497c9..29d7723 100644 ---- a/tests/restconf-nacm.cpp -+++ b/tests/restconf-nacm.cpp -@@ -225,9 +225,7 @@ TEST_CASE("NACM") - { - // wrong password: the server should delay its response, so let the client wait "long enough" - const auto start = std::chrono::steady_clock::now(); -- REQUIRE(clientRequest("GET", -- RESTCONF_DATA_ROOT "/ietf-system:system", -- "", -+ REQUIRE(get(RESTCONF_DATA_ROOT "/ietf-system:system", - {AUTH_WRONG_PASSWORD}, - boost::posix_time::seconds(5)) - == Response{401, jsonHeaders, R"({ -@@ -251,12 +249,10 @@ TEST_CASE("NACM") - // wrong password: the server should delay its response, in this case let the client terminate its - // request and check that the server doesn't crash - const auto start = std::chrono::steady_clock::now(); -- REQUIRE_THROWS_WITH(clientRequest("GET", -- RESTCONF_DATA_ROOT "/ietf-system:system", -- "", -- {AUTH_WRONG_PASSWORD}, -- boost::posix_time::milliseconds(100)), -- "HTTP client error: Connection timed out"); -+ REQUIRE_THROWS_WITH(get(RESTCONF_DATA_ROOT "/ietf-system:system", -+ {AUTH_WRONG_PASSWORD}, -+ boost::posix_time::milliseconds(100)), -+ "HTTP client error: Connection timed out"); - auto processingMS = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); - REQUIRE(processingMS <= 500); - } -diff --git a/tests/restconf-notifications.cpp b/tests/restconf-notifications.cpp -index 905ae01..d479f3c 100644 ---- a/tests/restconf-notifications.cpp -+++ b/tests/restconf-notifications.cpp -@@ -277,18 +277,18 @@ TEST_CASE("NETCONF notification streams") - - SECTION("Other methods") - { -- REQUIRE(clientRequest("HEAD", "/streams/NETCONF/XML", "", {AUTH_ROOT}) == Response{200, eventStreamHeaders, ""}); -- REQUIRE(clientRequest("OPTIONS", "/streams/NETCONF/XML", "", {AUTH_ROOT}) == Response{200, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""}); -+ REQUIRE(head("/streams/NETCONF/XML", {AUTH_ROOT}) == Response{200, eventStreamHeaders, ""}); -+ REQUIRE(options("/streams/NETCONF/XML", {AUTH_ROOT}) == Response{200, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""}); - - const std::multimap headers = { - {"access-control-allow-origin", "*"}, - {"allow", "GET, HEAD, OPTIONS"}, - {"content-type", "text/plain"}, - }; -- REQUIRE(clientRequest("PUT", "/streams/NETCONF/XML", "", {AUTH_ROOT}) == Response{405, headers, "Method not allowed."}); -- REQUIRE(clientRequest("POST", "/streams/NETCONF/XML", "", {AUTH_ROOT}) == Response{405, headers, "Method not allowed."}); -- REQUIRE(clientRequest("PATCH", "/streams/NETCONF/XML", "", {AUTH_ROOT}) == Response{405, headers, "Method not allowed."}); -- REQUIRE(clientRequest("DELETE", "/streams/NETCONF/XML", "", {AUTH_ROOT}) == Response{405, headers, "Method not allowed."}); -+ REQUIRE(put("/streams/NETCONF/XML", {AUTH_ROOT}, "") == Response{405, headers, "Method not allowed."}); -+ REQUIRE(post("/streams/NETCONF/XML", {AUTH_ROOT}, "") == Response{405, headers, "Method not allowed."}); -+ REQUIRE(patch("/streams/NETCONF/XML", {AUTH_ROOT}, "") == Response{405, headers, "Method not allowed."}); -+ REQUIRE(httpDelete("/streams/NETCONF/XML", {AUTH_ROOT}) == Response{405, headers, "Method not allowed."}); - } - - SECTION("Invalid URLs") -diff --git a/tests/restconf-yang-schema.cpp b/tests/restconf-yang-schema.cpp -index 73821e0..6e374b1 100644 ---- a/tests/restconf-yang-schema.cpp -+++ b/tests/restconf-yang-schema.cpp -@@ -156,11 +156,14 @@ TEST_CASE("obtaining YANG schemas") - { - SECTION("unsupported methods") - { -- for (const std::string httpMethod : {"POST", "PUT", "PATCH", "DELETE"}) { -- CAPTURE(httpMethod); -- REQUIRE(clientRequest(httpMethod, YANG_ROOT "/ietf-yang-library@2019-01-04", "", {AUTH_ROOT}) -- == Response{405, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""}); -- } -+ REQUIRE(post(YANG_ROOT "/ietf-yang-library@2019-01-04", {AUTH_ROOT}, "") -+ == Response{405, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""}); -+ REQUIRE(put(YANG_ROOT "/ietf-yang-library@2019-01-04", {AUTH_ROOT}, "") -+ == Response{405, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""}); -+ REQUIRE(patch(YANG_ROOT "/ietf-yang-library@2019-01-04", {AUTH_ROOT}, "") -+ == Response{405, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""}); -+ REQUIRE(httpDelete(YANG_ROOT "/ietf-yang-library@2019-01-04", {AUTH_ROOT}) -+ == Response{405, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""}); - } - - REQUIRE(options(YANG_ROOT "/ietf-yang-library@2019-01-04", {}) == Response{200, Response::Headers{ACCESS_CONTROL_ALLOW_ORIGIN, {"allow", "GET, HEAD, OPTIONS"}}, ""}); -@@ -190,12 +193,12 @@ TEST_CASE("obtaining YANG schemas") - SECTION("auth failure") - { - // wrong password -- REQUIRE(clientRequest("GET", YANG_ROOT "/ietf-system@2014-08-06", "", {AUTH_WRONG_PASSWORD}, boost::posix_time::seconds{5}) -+ REQUIRE(get(YANG_ROOT "/ietf-system@2014-08-06", {AUTH_WRONG_PASSWORD}, boost::posix_time::seconds{5}) - == Response{401, plaintextHeaders, "Access denied."}); -- REQUIRE(clientRequest("HEAD", YANG_ROOT "/ietf-system@2014-08-06", "", {AUTH_WRONG_PASSWORD}, boost::posix_time::seconds{5}) -+ REQUIRE(head(YANG_ROOT "/ietf-system@2014-08-06", {AUTH_WRONG_PASSWORD}, boost::posix_time::seconds{5}) - == Response{401, plaintextHeaders, ""}); - // anonymous request -- REQUIRE(clientRequest("HEAD", YANG_ROOT "/ietf-system@2014-08-06", "", {FORWARDED}, boost::posix_time::seconds{5}) -+ REQUIRE(head(YANG_ROOT "/ietf-system@2014-08-06", {FORWARDED}, boost::posix_time::seconds{5}) - == Response{401, plaintextHeaders, ""}); - } - } --- -2.43.0 - diff --git a/package/rousette/0013-fix-a-possible-bad_weak_ptr-exception.patch b/package/rousette/0013-fix-a-possible-bad_weak_ptr-exception.patch new file mode 100644 index 00000000..3c396813 --- /dev/null +++ b/package/rousette/0013-fix-a-possible-bad_weak_ptr-exception.patch @@ -0,0 +1,32 @@ +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 13/17] 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: Mattias Walström +--- + src/http/EventStream.cpp | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp +index 3bdd55e..8703dad 100644 +--- a/src/http/EventStream.cpp ++++ b/src/http/EventStream.cpp +@@ -47,7 +47,7 @@ EventStream::EventStream(const server::request& req, + } + + state = WantToClose; +- boost::asio::post(this->res.io_service(), [weak = std::weak_ptr{shared_from_this()}]() { ++ boost::asio::post(this->res.io_service(), [weak = weak_from_this()]() { + if (auto myself = weak.lock()) { + std::lock_guard lock{myself->mtx}; + if (myself->state == WantToClose) { // resume unless somebody closed it before this was picked up by the event loop +-- +2.43.0 + diff --git a/package/rousette/0013-tests-move-stuff-from-the-header-file-into-cpp-file.patch b/package/rousette/0013-tests-move-stuff-from-the-header-file-into-cpp-file.patch deleted file mode 100644 index 31716521..00000000 --- a/package/rousette/0013-tests-move-stuff-from-the-header-file-into-cpp-file.patch +++ /dev/null @@ -1,586 +0,0 @@ -From 0beaee041fd4fcfbebcbb2912eb6b26ec71f50c7 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 2 Dec 2024 20:01:54 +0100 -Subject: [PATCH 13/44] tests: move stuff from the header file into cpp file -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Allright, I have had enough. I am no longer waiting for a minute for a -recompilation of one test. -This patch splits the aux-utils.h file into a cpp file and two headers. -One of the headers is only for declarations of the functions and -datatypes in the cpp file, the second header implements all the helper -functions that require SERVER_PORT and further helper constants for the -restconf tests. - -Compile times are a little better. I have measured compilation times of -two arbitrary restconf tests with asan+ubsan (and ccache disabled): - - - restconf-reading: 17.05s -> 13.85s - - restconf-delete: 15.18s -> 11.87s - -Not ideal, but it is certainly better. - -Change-Id: If529cbc8954d50494711a408231ea4c2c4daf072 -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 1 + - tests/aux-utils.h | 214 ++----------------------------- - tests/restconf-notifications.cpp | 3 +- - tests/restconf_utils.cpp | 157 +++++++++++++++++++++++ - tests/restconf_utils.h | 83 ++++++++++++ - 5 files changed, 256 insertions(+), 202 deletions(-) - create mode 100644 tests/restconf_utils.cpp - create mode 100644 tests/restconf_utils.h - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 731d7cb..b8a41a7 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -158,6 +158,7 @@ if(BUILD_TESTING) - add_library(DoctestIntegration STATIC - tests/datastoreUtils.cpp - tests/doctest_integration.cpp -+ tests/restconf_utils.cpp - tests/trompeloeil_doctest.h - tests/wait-a-bit-longer.cpp - ) -diff --git a/tests/aux-utils.h b/tests/aux-utils.h -index 9afd7bc..d99b3f9 100644 ---- a/tests/aux-utils.h -+++ b/tests/aux-utils.h -@@ -8,99 +8,16 @@ - - #pragma once - #include "trompeloeil_doctest.h" --#include - #include --#include --#include --#include --#include "tests/UniqueResource.h" -+#include "restconf_utils.h" - --using namespace std::string_literals; --namespace ng = nghttp2::asio_http2; --namespace ng_client = ng::client; -- --struct Response { -- int statusCode; -- ng::header_map headers; -- std::string data; -- -- using Headers = std::multimap; -- -- Response(int statusCode, const Headers& headers, const std::string& data) -- : Response(statusCode, transformHeaders(headers), data) -- { -- } -- -- Response(int statusCode, const ng::header_map& headers, const std::string& data) -- : statusCode(statusCode) -- , headers(headers) -- , data(data) -- { -- } -- -- bool equalStatusCodeAndHeaders(const Response& o) const -- { -- // Skipping 'date' header. Its value will not be reproducible in simple tests -- ng::header_map myHeaders(headers); -- ng::header_map otherHeaders(o.headers); -- myHeaders.erase("date"); -- otherHeaders.erase("date"); -- -- return statusCode == o.statusCode && std::equal(myHeaders.begin(), myHeaders.end(), otherHeaders.begin(), otherHeaders.end(), [](const auto& a, const auto& b) { -- return a.first == b.first && a.second.value == b.second.value; // Skipping 'sensitive' field from ng::header_value which does not seem important for us. -- }); -- } -- -- bool operator==(const Response& o) const -- { -- return equalStatusCodeAndHeaders(o) && data == o.data; -- } -- -- static ng::header_map transformHeaders(const Headers& headers) -- { -- ng::header_map res; -- std::transform(headers.begin(), headers.end(), std::inserter(res, res.end()), [](const auto& h) -> std::pair { return {h.first, {h.second, false}}; }); -- return res; -- } --}; -- --namespace doctest { -- --template <> --struct StringMaker { -- static String convert(const ng::header_map& m) -- { -- std::ostringstream oss; -- oss << "{\n"; -- for (const auto& [k, v] : m) { -- oss << "\t" -- << "{\"" << k << "\", " -- << "{\"" << v.value << "\", " << std::boolalpha << v.sensitive << "}},\n"; -- } -- oss << "}"; -- return oss.str().c_str(); -- } --}; -- --template <> --struct StringMaker { -- static String convert(const Response& o) -- { -- std::ostringstream oss; -- -- oss << "{" -- << std::to_string(o.statusCode) << ", " -- << StringMaker::convert(o.headers) << ",\n" -- << "\"" << o.data << "\",\n" -- << "}"; -- -- return oss.str().c_str(); -- } --}; -+namespace sysrepo { -+class Session; - } - -+namespace ng = nghttp2::asio_http2; -+ - static const auto SERVER_ADDRESS = "::1"; --static const auto SERVER_ADDRESS_AND_PORT = "http://["s + SERVER_ADDRESS + "]" + ":" + SERVER_PORT; - - #define AUTH_DWDM {"authorization", "Basic ZHdkbTpEV0RN"} - #define AUTH_NORULES {"authorization", "Basic bm9ydWxlczplbXB0eQ=="} -@@ -145,7 +62,7 @@ const ng::header_map plaintextHeaders{ - {"content-type", {"text/plain", false}}, - }; - --const ng::header_map eventStreamHeaders { -+const ng::header_map eventStreamHeaders{ - {"access-control-allow-origin", {"*", false}}, - {"content-type", {"text/event-stream", false}}, - }; -@@ -153,142 +70,37 @@ const ng::header_map eventStreamHeaders { - #define ACCESS_CONTROL_ALLOW_ORIGIN {"access-control-allow-origin", "*"} - #define ACCEPT_PATCH {"accept-patch", "application/yang-data+json, application/yang-data+xml, application/yang-patch+xml, application/yang-patch+json"} - --// this is a test, and the server is expected to reply "soon" --static const boost::posix_time::time_duration CLIENT_TIMEOUT = boost::posix_time::seconds(3); -- --Response clientRequest(auto method, -- auto uri, -- const std::string& data, -- const std::map& headers, -- const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) --{ -- boost::asio::io_service io_service; -- auto client = std::make_shared(io_service, SERVER_ADDRESS, SERVER_PORT); -- -- client->read_timeout(timeout); -- -- std::ostringstream oss; -- ng::header_map resHeaders; -- int statusCode; -- -- client->on_connect([&](auto) { -- boost::system::error_code ec; -- -- ng::header_map reqHeaders; -- for (const auto& [name, value] : headers) { -- reqHeaders.insert({name, {value, false}}); -- } -- -- auto req = client->submit(ec, method, SERVER_ADDRESS_AND_PORT + uri, data, reqHeaders); -- req->on_response([&](const ng_client::response& res) { -- res.on_data([&oss](const uint8_t* data, std::size_t len) { -- oss.write(reinterpret_cast(data), len); -- }); -- statusCode = res.status_code(); -- resHeaders = res.header(); -- }); -- req->on_close([maybeClient = std::weak_ptr{client}](auto) { -- if (auto client = maybeClient.lock()) { -- client->shutdown(); -- } -- }); -- }); -- client->on_error([](const boost::system::error_code& ec) { -- throw std::runtime_error{"HTTP client error: " + ec.message()}; -- }); -- io_service.run(); -- -- return {statusCode, resHeaders, oss.str()}; --} -- - Response get(auto uri, const std::map& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) - { -- return clientRequest("GET", uri, "", headers, timeout); -+ return clientRequest(SERVER_ADDRESS, SERVER_PORT, "GET", uri, "", headers, timeout); - } - - Response options(auto uri, const std::map& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) - { -- return clientRequest("OPTIONS", uri, "", headers, timeout); -+ return clientRequest(SERVER_ADDRESS, SERVER_PORT, "OPTIONS", uri, "", headers, timeout); - } - - Response head(auto uri, const std::map& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) - { -- return clientRequest("HEAD", uri, "", headers, timeout); -+ return clientRequest(SERVER_ADDRESS, SERVER_PORT, "HEAD", uri, "", headers, timeout); - } - - Response put(auto xpath, const std::map& headers, const std::string& data, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) - { -- return clientRequest("PUT", xpath, data, headers, timeout); -+ return clientRequest(SERVER_ADDRESS, SERVER_PORT, "PUT", xpath, data, headers, timeout); - } - - Response post(auto xpath, const std::map& headers, const std::string& data, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) - { -- return clientRequest("POST", xpath, data, headers, timeout); -+ return clientRequest(SERVER_ADDRESS, SERVER_PORT, "POST", xpath, data, headers, timeout); - } - - Response patch(auto uri, const std::map& headers, const std::string& data, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) - { -- return clientRequest("PATCH", uri, data, headers, timeout); -+ return clientRequest(SERVER_ADDRESS, SERVER_PORT, "PATCH", uri, data, headers, timeout); - } - - Response httpDelete(auto uri, const std::map& headers, const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT) - { -- return clientRequest("DELETE", uri, "", headers, timeout); --} -- --auto manageNacm(sysrepo::Session session) --{ -- return make_unique_resource( -- [session]() mutable { -- session.switchDatastore(sysrepo::Datastore::Running); -- session.copyConfig(sysrepo::Datastore::Startup, "ietf-netconf-acm"); -- }, -- [session]() mutable { -- session.switchDatastore(sysrepo::Datastore::Running); -- -- /* cleanup running DS of ietf-netconf-acm module -- because it contains XPaths to other modules that we -- can't uninstall because the running DS content would be invalid -- */ -- session.copyConfig(sysrepo::Datastore::Startup, "ietf-netconf-acm"); -- }); --} -- --void setupRealNacm(sysrepo::Session session) --{ -- session.switchDatastore(sysrepo::Datastore::Running); -- session.setItem("/ietf-netconf-acm:nacm/enable-external-groups", "false"); -- session.setItem("/ietf-netconf-acm:nacm/groups/group[name='optics']/user-name[.='dwdm']", ""); -- session.setItem("/ietf-netconf-acm:nacm/groups/group[name='yangnobody']/user-name[.='yangnobody']", ""); -- session.setItem("/ietf-netconf-acm:nacm/groups/group[name='norules']/user-name[.='norules']", ""); -- -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/group[.='yangnobody']", ""); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/module-name", "ietf-system"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/action", "permit"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/access-operations", "read"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/path", "/ietf-system:system/contact"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/module-name", "ietf-system"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/action", "permit"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/access-operations", "read"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/path", "/ietf-system:system/hostname"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/module-name", "ietf-system"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/action", "permit"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/access-operations", "read"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/path", "/ietf-system:system/location"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='13']/module-name", "example"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='13']/action", "permit"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='13']/access-operations", "read"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='14']/module-name", "ietf-restconf-monitoring"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='14']/action", "permit"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='14']/access-operations", "read"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/module-name", "example-delete"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/action", "permit"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/access-operations", "read"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/path", "/example-delete:immutable"); -- 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']", ""); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/rule[name='1']/module-name", "ietf-system"); -- session.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/rule[name='1']/action", "permit"); // overrides nacm:default-deny-* rules in ietf-system model -- session.applyChanges(); -+ return clientRequest(SERVER_ADDRESS, SERVER_PORT, "DELETE", uri, "", headers, timeout); - } -diff --git a/tests/restconf-notifications.cpp b/tests/restconf-notifications.cpp -index d479f3c..db9a3f5 100644 ---- a/tests/restconf-notifications.cpp -+++ b/tests/restconf-notifications.cpp -@@ -87,7 +87,8 @@ struct SSEClient { - client->on_connect([&, uri, reqHeaders, silenceTimeout](auto) { - boost::system::error_code ec; - -- auto req = client->submit(ec, "GET", SERVER_ADDRESS_AND_PORT + uri, "", reqHeaders); -+ static const auto server_address_and_port = std::string("http://[") + SERVER_ADDRESS + "]" + ":" + SERVER_PORT; -+ auto req = client->submit(ec, "GET", server_address_and_port + uri, "", reqHeaders); - req->on_response([&, silenceTimeout](const ng_client::response& res) { - requestSent.count_down(); - res.on_data([&, silenceTimeout](const uint8_t* data, std::size_t len) { -diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp -new file mode 100644 -index 0000000..ea9a18d ---- /dev/null -+++ b/tests/restconf_utils.cpp -@@ -0,0 +1,157 @@ -+/* -+ * Copyright (C) 2023 CESNET, https://photonics.cesnet.cz/ -+ * -+ * Written by Tomáš Pecka -+ * Written by Jan Kundrát -+ * -+ */ -+ -+#include "restconf_utils.h" -+#include "sysrepo-cpp/Session.hpp" -+ -+using namespace std::string_literals; -+namespace ng = nghttp2::asio_http2; -+namespace ng_client = ng::client; -+ -+Response::Response(int statusCode, const Response::Headers& headers, const std::string& data) -+ : Response(statusCode, transformHeaders(headers), data) -+{ -+} -+ -+Response::Response(int statusCode, const ng::header_map& headers, const std::string& data) -+ : statusCode(statusCode) -+ , headers(headers) -+ , data(data) -+{ -+} -+ -+bool Response::equalStatusCodeAndHeaders(const Response& o) const -+{ -+ // Skipping 'date' header. Its value will not be reproducible in simple tests -+ ng::header_map myHeaders(headers); -+ ng::header_map otherHeaders(o.headers); -+ myHeaders.erase("date"); -+ otherHeaders.erase("date"); -+ -+ return statusCode == o.statusCode && std::equal(myHeaders.begin(), myHeaders.end(), otherHeaders.begin(), otherHeaders.end(), [](const auto& a, const auto& b) { -+ return a.first == b.first && a.second.value == b.second.value; // Skipping 'sensitive' field from ng::header_value which does not seem important for us. -+ }); -+} -+ -+bool Response::operator==(const Response& o) const -+{ -+ return equalStatusCodeAndHeaders(o) && data == o.data; -+} -+ -+ng::header_map Response::transformHeaders(const Response::Headers& headers) -+{ -+ ng::header_map res; -+ std::transform(headers.begin(), headers.end(), std::inserter(res, res.end()), [](const auto& h) -> std::pair { return {h.first, {h.second, false}}; }); -+ return res; -+} -+ -+Response clientRequest(const std::string& server_address, -+ const std::string& server_port, -+ const std::string& method, -+ const std::string& uri, -+ const std::string& data, -+ const std::map& headers, -+ const boost::posix_time::time_duration timeout) -+{ -+ boost::asio::io_service io_service; -+ auto client = std::make_shared(io_service, server_address, server_port); -+ -+ client->read_timeout(timeout); -+ -+ std::ostringstream oss; -+ ng::header_map resHeaders; -+ int statusCode; -+ -+ client->on_connect([&](auto) { -+ boost::system::error_code ec; -+ -+ ng::header_map reqHeaders; -+ for (const auto& [name, value] : headers) { -+ reqHeaders.insert({name, {value, false}}); -+ } -+ -+ const auto server_address_and_port = std::string("http://[") + server_address + "]" + ":" + server_port; -+ auto req = client->submit(ec, method, server_address_and_port + uri, data, reqHeaders); -+ req->on_response([&](const ng_client::response& res) { -+ res.on_data([&oss](const uint8_t* data, std::size_t len) { -+ oss.write(reinterpret_cast(data), len); -+ }); -+ statusCode = res.status_code(); -+ resHeaders = res.header(); -+ }); -+ req->on_close([maybeClient = std::weak_ptr{client}](auto) { -+ if (auto client = maybeClient.lock()) { -+ client->shutdown(); -+ } -+ }); -+ }); -+ client->on_error([](const boost::system::error_code& ec) { -+ throw std::runtime_error{"HTTP client error: " + ec.message()}; -+ }); -+ io_service.run(); -+ -+ return {statusCode, resHeaders, oss.str()}; -+} -+ -+UniqueResource manageNacm(sysrepo::Session session) -+{ -+ return make_unique_resource( -+ [session]() mutable { -+ session.switchDatastore(sysrepo::Datastore::Running); -+ session.copyConfig(sysrepo::Datastore::Startup, "ietf-netconf-acm"); -+ }, -+ [session]() mutable { -+ session.switchDatastore(sysrepo::Datastore::Running); -+ -+ /* cleanup running DS of ietf-netconf-acm module -+ because it contains XPaths to other modules that we -+ can't uninstall because the running DS content would be invalid -+ */ -+ session.copyConfig(sysrepo::Datastore::Startup, "ietf-netconf-acm"); -+ }); -+} -+ -+void setupRealNacm(sysrepo::Session session) -+{ -+ session.switchDatastore(sysrepo::Datastore::Running); -+ session.setItem("/ietf-netconf-acm:nacm/enable-external-groups", "false"); -+ session.setItem("/ietf-netconf-acm:nacm/groups/group[name='optics']/user-name[.='dwdm']", ""); -+ session.setItem("/ietf-netconf-acm:nacm/groups/group[name='yangnobody']/user-name[.='yangnobody']", ""); -+ session.setItem("/ietf-netconf-acm:nacm/groups/group[name='norules']/user-name[.='norules']", ""); -+ -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/group[.='yangnobody']", ""); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/module-name", "ietf-system"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/action", "permit"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/access-operations", "read"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='10']/path", "/ietf-system:system/contact"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/module-name", "ietf-system"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/action", "permit"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/access-operations", "read"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='11']/path", "/ietf-system:system/hostname"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/module-name", "ietf-system"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/action", "permit"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/access-operations", "read"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='12']/path", "/ietf-system:system/location"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='13']/module-name", "example"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='13']/action", "permit"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='13']/access-operations", "read"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='14']/module-name", "ietf-restconf-monitoring"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='14']/action", "permit"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='14']/access-operations", "read"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/module-name", "example-delete"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/action", "permit"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/access-operations", "read"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/path", "/example-delete:immutable"); -+ 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']", ""); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/rule[name='1']/module-name", "ietf-system"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/rule[name='1']/action", "permit"); // overrides nacm:default-deny-* rules in ietf-system model -+ session.applyChanges(); -+} -+ -diff --git a/tests/restconf_utils.h b/tests/restconf_utils.h -new file mode 100644 -index 0000000..26f0803 ---- /dev/null -+++ b/tests/restconf_utils.h -@@ -0,0 +1,83 @@ -+/* -+ * Copyright (C) 2023 CESNET, https://photonics.cesnet.cz/ -+ * -+ * Written by Tomáš Pecka -+ * Written by Jan Kundrát -+ * -+ */ -+ -+#pragma once -+#include "trompeloeil_doctest.h" -+#include -+#include "UniqueResource.h" -+ -+namespace sysrepo { -+class Session; -+} -+ -+namespace ng = nghttp2::asio_http2; -+namespace ng_client = ng::client; -+ -+struct Response { -+ int statusCode; -+ ng::header_map headers; -+ std::string data; -+ -+ using Headers = std::multimap; -+ -+ Response(int statusCode, const Headers& headers, const std::string& data); -+ Response(int statusCode, const ng::header_map& headers, const std::string& data); -+ bool equalStatusCodeAndHeaders(const Response& o) const; -+ bool operator==(const Response& o) const; -+ static ng::header_map transformHeaders(const Headers& headers); -+}; -+ -+namespace doctest { -+ -+template <> -+struct StringMaker { -+ static String convert(const ng::header_map& m) -+ { -+ std::ostringstream oss; -+ oss << "{\n"; -+ for (const auto& [k, v] : m) { -+ oss << "\t" -+ << "{\"" << k << "\", " -+ << "{\"" << v.value << "\", " << std::boolalpha << v.sensitive << "}},\n"; -+ } -+ oss << "}"; -+ return oss.str().c_str(); -+ } -+}; -+ -+template <> -+struct StringMaker { -+ static String convert(const Response& o) -+ { -+ std::ostringstream oss; -+ -+ oss << "{" -+ << std::to_string(o.statusCode) << ", " -+ << StringMaker::convert(o.headers) << ",\n" -+ << "\"" << o.data << "\",\n" -+ << "}"; -+ -+ return oss.str().c_str(); -+ } -+}; -+} -+ -+// this is a test, and the server is expected to reply "soon" -+static const boost::posix_time::time_duration CLIENT_TIMEOUT = boost::posix_time::seconds(3); -+ -+Response clientRequest( -+ const std::string& server_address, -+ const std::string& server_port, -+ const std::string& method, -+ const std::string& uri, -+ const std::string& data, -+ const std::map& headers, -+ const boost::posix_time::time_duration timeout = CLIENT_TIMEOUT); -+ -+UniqueResource manageNacm(sysrepo::Session session); -+void setupRealNacm(sysrepo::Session session); --- -2.43.0 - diff --git a/package/rousette/0014-http-add-optional-callbacks-to-EventStream.patch b/package/rousette/0014-http-add-optional-callbacks-to-EventStream.patch new file mode 100644 index 00000000..efab0a1d --- /dev/null +++ b/package/rousette/0014-http-add-optional-callbacks-to-EventStream.patch @@ -0,0 +1,115 @@ +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 14/17] 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 +the client disconnects or when the connection is lost. +For us, this will be useful in the future subscribed notifications +implementation where we would like to clean up stuff after client +disconnects. + +Change-Id: Icfc2959e38b812b7c18f45976415209b29151c7b +Signed-off-by: Mattias Walström +--- + src/http/EventStream.cpp | 18 +++++++++++++++--- + src/http/EventStream.h | 10 ++++++++-- + 2 files changed, 23 insertions(+), 5 deletions(-) + +diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp +index 8703dad..0a974d4 100644 +--- a/src/http/EventStream.cpp ++++ b/src/http/EventStream.cpp +@@ -25,11 +25,15 @@ EventStream::EventStream(const server::request& req, + Termination& termination, + EventSignal& signal, + const std::chrono::seconds keepAlivePingInterval, +- const std::optional& initialEvent) ++ const std::optional& initialEvent, ++ const std::function& onTerminationCb, ++ const std::function& onClientDisconnectedCb) + : res{res} + , ping{res.io_service()} + , peer{peer_from_request(req)} + , m_keepAlivePingInterval(keepAlivePingInterval) ++ , onTerminationCb(onTerminationCb) ++ , onClientDisconnectedCb(onClientDisconnectedCb) + { + if (initialEvent) { + enqueue(FIELD_DATA, *initialEvent); +@@ -41,6 +45,9 @@ EventStream::EventStream(const server::request& req, + + terminateSub = termination.connect([this]() { + spdlog::trace("{}: will terminate", peer); ++ if (this->onTerminationCb) { ++ this->onTerminationCb(); ++ } + std::lock_guard lock{mtx}; + if (state == Closed) { // we are late to the party, res is already gone + return; +@@ -80,6 +87,9 @@ void EventStream::activate() + myself->eventSub.disconnect(); + myself->terminateSub.disconnect(); + myself->state = Closed; ++ if (myself->onClientDisconnectedCb) { ++ myself->onClientDisconnectedCb(); ++ } + }); + + res.end([myself](uint8_t* destination, std::size_t len, uint32_t* data_flags) { +@@ -187,9 +197,11 @@ std::shared_ptr EventStream::create(const nghttp2::asio_http2::serv + Termination& terminate, + EventSignal& signal, + const std::chrono::seconds keepAlivePingInterval, +- const std::optional& initialEvent) ++ const std::optional& initialEvent, ++ const std::function& onTerminationCb, ++ const std::function& onClientDisconnectedCb) + { +- auto stream = std::shared_ptr(new EventStream(req, res, terminate, signal, keepAlivePingInterval, initialEvent)); ++ auto stream = std::shared_ptr(new EventStream(req, res, terminate, signal, keepAlivePingInterval, initialEvent, onTerminationCb, onClientDisconnectedCb)); + stream->activate(); + return stream; + } +diff --git a/src/http/EventStream.h b/src/http/EventStream.h +index ef0a002..87d8c82 100644 +--- a/src/http/EventStream.h ++++ b/src/http/EventStream.h +@@ -36,7 +36,9 @@ public: + Termination& terminate, + EventSignal& signal, + const std::chrono::seconds keepAlivePingInterval, +- const std::optional& initialEvent = std::nullopt); ++ const std::optional& initialEvent = std::nullopt, ++ const std::function& onTerminationCb = std::function(), ++ const std::function& onClientDisconnectedCb = std::function()); + + private: + const nghttp2::asio_http2::server::response& res; +@@ -54,6 +56,8 @@ private: + boost::signals2::scoped_connection eventSub, terminateSub; + const std::string peer; + const std::chrono::seconds m_keepAlivePingInterval; ++ std::function onTerminationCb; ///< optional callback when the stream is terminated ++ std::function onClientDisconnectedCb; ///< optional callback invoked in client.on_close() + + size_t send_chunk(uint8_t* destination, std::size_t len, uint32_t* data_flags); + ssize_t process(uint8_t* destination, std::size_t len, uint32_t* data_flags); +@@ -66,7 +70,9 @@ protected: + Termination& terminate, + EventSignal& signal, + const std::chrono::seconds keepAlivePingInterval, +- const std::optional& initialEvent = std::nullopt); ++ const std::optional& initialEvent = std::nullopt, ++ const std::function& onTerminationCb = std::function(), ++ const std::function& onClientDisconnectedCb = std::function()); + void activate(); + }; + } +-- +2.43.0 + diff --git a/package/rousette/0014-tests-rename-datastoreUtils-to-event_watchers.patch b/package/rousette/0014-tests-rename-datastoreUtils-to-event_watchers.patch deleted file mode 100644 index 9c7010c2..00000000 --- a/package/rousette/0014-tests-rename-datastoreUtils-to-event_watchers.patch +++ /dev/null @@ -1,169 +0,0 @@ -From 53aa2e23ee8acc1881487f3969c2c58fb29437be Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 2 Dec 2024 20:08:53 +0100 -Subject: [PATCH 14/44] tests: rename datastoreUtils to event_watchers -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -The next commit will move RESTCONF notification watchers there, I think -the new name is more appropriate. - -Change-Id: Ia8e8cd5fe89bd827fcde4531fe801298bd6f71d2 -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 2 +- - tests/{datastoreUtils.cpp => event_watchers.cpp} | 2 +- - tests/{datastoreUtils.h => event_watchers.h} | 0 - tests/pretty_printers.h | 2 +- - tests/restconf-defaults.cpp | 2 +- - tests/restconf-delete.cpp | 2 +- - tests/restconf-plain-patch.cpp | 2 +- - tests/restconf-reading.cpp | 2 +- - tests/restconf-rpc.cpp | 2 +- - tests/restconf-writing.cpp | 2 +- - tests/restconf-yang-patch.cpp | 2 +- - 11 files changed, 10 insertions(+), 10 deletions(-) - rename tests/{datastoreUtils.cpp => event_watchers.cpp} (98%) - rename tests/{datastoreUtils.h => event_watchers.h} (100%) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index b8a41a7..22bce32 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -156,8 +156,8 @@ if(BUILD_TESTING) - include(cmake/SysrepoTest.cmake) - - add_library(DoctestIntegration STATIC -- tests/datastoreUtils.cpp - tests/doctest_integration.cpp -+ tests/event_watchers.cpp - tests/restconf_utils.cpp - tests/trompeloeil_doctest.h - tests/wait-a-bit-longer.cpp -diff --git a/tests/datastoreUtils.cpp b/tests/event_watchers.cpp -similarity index 98% -rename from tests/datastoreUtils.cpp -rename to tests/event_watchers.cpp -index 56d8090..c696bc5 100644 ---- a/tests/datastoreUtils.cpp -+++ b/tests/event_watchers.cpp -@@ -1,5 +1,5 @@ - #include "UniqueResource.h" --#include "datastoreUtils.h" -+#include "event_watchers.h" - - namespace { - void datastoreChanges(auto session, auto& dsChangesMock, auto path) -diff --git a/tests/datastoreUtils.h b/tests/event_watchers.h -similarity index 100% -rename from tests/datastoreUtils.h -rename to tests/event_watchers.h -diff --git a/tests/pretty_printers.h b/tests/pretty_printers.h -index a2befeb..ce64e91 100644 ---- a/tests/pretty_printers.h -+++ b/tests/pretty_printers.h -@@ -13,7 +13,7 @@ - #include - #include - #include --#include "datastoreUtils.h" -+#include "event_watchers.h" - #include "restconf/uri.h" - #include "restconf/uri_impl.h" - -diff --git a/tests/restconf-defaults.cpp b/tests/restconf-defaults.cpp -index 129dae2..dd8b4da 100644 ---- a/tests/restconf-defaults.cpp -+++ b/tests/restconf-defaults.cpp -@@ -11,7 +11,7 @@ static const auto SERVER_PORT = "10087"; - #include - #include "restconf/Server.h" - #include "tests/aux-utils.h" --#include "tests/datastoreUtils.h" -+#include "tests/event_watchers.h" - - TEST_CASE("default handling") - { -diff --git a/tests/restconf-delete.cpp b/tests/restconf-delete.cpp -index 75a6916..4818ff3 100644 ---- a/tests/restconf-delete.cpp -+++ b/tests/restconf-delete.cpp -@@ -10,7 +10,7 @@ static const auto SERVER_PORT = "10086"; - #include - #include "restconf/Server.h" - #include "tests/aux-utils.h" --#include "tests/datastoreUtils.h" -+#include "tests/event_watchers.h" - #include "tests/pretty_printers.h" - - TEST_CASE("deleting data") -diff --git a/tests/restconf-plain-patch.cpp b/tests/restconf-plain-patch.cpp -index d4f3952..b550f54 100644 ---- a/tests/restconf-plain-patch.cpp -+++ b/tests/restconf-plain-patch.cpp -@@ -10,7 +10,7 @@ static const auto SERVER_PORT = "10089"; - #include - #include "restconf/Server.h" - #include "tests/aux-utils.h" --#include "tests/datastoreUtils.h" -+#include "tests/event_watchers.h" - #include "tests/pretty_printers.h" - - TEST_CASE("Plain patch") -diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp -index d7d507b..e709486 100644 ---- a/tests/restconf-reading.cpp -+++ b/tests/restconf-reading.cpp -@@ -11,7 +11,7 @@ static const auto SERVER_PORT = "10081"; - #include - #include - #include "restconf/Server.h" --#include "tests/datastoreUtils.h" -+#include "tests/event_watchers.h" - - TEST_CASE("reading data") - { -diff --git a/tests/restconf-rpc.cpp b/tests/restconf-rpc.cpp -index 9bc1dbc..c4229a0 100644 ---- a/tests/restconf-rpc.cpp -+++ b/tests/restconf-rpc.cpp -@@ -10,7 +10,7 @@ static const auto SERVER_PORT = "10084"; - #include - #include "restconf/Server.h" - #include "tests/aux-utils.h" --#include "tests/datastoreUtils.h" -+#include "tests/event_watchers.h" - #include "tests/pretty_printers.h" - - struct RpcCall { -diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp -index 582a262..0932984 100644 ---- a/tests/restconf-writing.cpp -+++ b/tests/restconf-writing.cpp -@@ -10,7 +10,7 @@ static const auto SERVER_PORT = "10083"; - #include - #include "restconf/Server.h" - #include "tests/aux-utils.h" --#include "tests/datastoreUtils.h" -+#include "tests/event_watchers.h" - #include "tests/pretty_printers.h" - - TEST_CASE("writing data") -diff --git a/tests/restconf-yang-patch.cpp b/tests/restconf-yang-patch.cpp -index 7cc8946..2b35c59 100644 ---- a/tests/restconf-yang-patch.cpp -+++ b/tests/restconf-yang-patch.cpp -@@ -10,7 +10,7 @@ static const auto SERVER_PORT = "10090"; - #include - #include "restconf/Server.h" - #include "tests/aux-utils.h" --#include "tests/datastoreUtils.h" -+#include "tests/event_watchers.h" - #include "tests/pretty_printers.h" - - TEST_CASE("YANG patch") --- -2.43.0 - diff --git a/package/rousette/0015-restconf-add-internal-RPC-handler-dispatcher.patch b/package/rousette/0015-restconf-add-internal-RPC-handler-dispatcher.patch new file mode 100644 index 00000000..6d703c2d --- /dev/null +++ b/package/rousette/0015-restconf-add-internal-RPC-handler-dispatcher.patch @@ -0,0 +1,95 @@ +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 15/17] 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 +is authorized to call the RPC (we are bypassing sysrepo, so we have to +check this manually) and then calls the RPC handler. +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: Mattias Walström +--- + src/restconf/Server.cpp | 24 ++++++++++++++++++++---- + tests/restconf-rpc.cpp | 18 ++++++++++++++++++ + 2 files changed, 38 insertions(+), 4 deletions(-) + +diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp +index 8544210..74c06f7 100644 +--- a/src/restconf/Server.cpp ++++ b/src/restconf/Server.cpp +@@ -438,10 +438,26 @@ libyang::CreatedNodes createEditForPutAndPatch(libyang::Context& ctx, const std: + return {editNode, replacementNode}; + } + +-std::optional processInternalRPC(sysrepo::Session&, const libyang::DataNode&) ++std::optional processInternalRPC(sysrepo::Session& sess, const libyang::DataNode& rpcInput, const libyang::DataFormat requestEncoding) + { +- // TODO: Implement internal RPCs +- throw ErrorResponse(501, "application", "operation-not-supported", "Internal RPCs are not yet supported."); ++ using InternalRPCHandler = std::function; ++ const std::map handlers; ++ ++ 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()); ++ } ++ ++ 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; ++ } ++ ++ throw ErrorResponse(501, "application", "operation-not-supported", "Unsupported RPC call to " + rpcPath, rpcPath); + } + + void processActionOrRPC(std::shared_ptr requestCtx, const std::chrono::milliseconds timeout) +@@ -478,7 +494,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); ++ rpcReply = processInternalRPC(requestCtx->sess, *rpcNode, *requestCtx->dataFormat.request); + } + + if (!rpcReply || rpcReply->immediateChildren().empty()) { +diff --git a/tests/restconf-rpc.cpp b/tests/restconf-rpc.cpp +index a3e9309..43aaed4 100644 +--- a/tests/restconf-rpc.cpp ++++ b/tests/restconf-rpc.cpp +@@ -420,4 +420,22 @@ TEST_CASE("invoking actions and rpcs") + )"}); + } + } ++ ++ SECTION("Internal RPC handlers") ++ { ++ // check NACM access ++ REQUIRE(post(RESTCONF_OPER_ROOT "/ietf-subscribed-notifications:kill-subscription", {CONTENT_TYPE_JSON}, R"({"ietf-subscribed-notifications:input": {}})") == 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." ++ } ++ ] ++ } ++} ++)"}); ++ } + } +-- +2.43.0 + diff --git a/package/rousette/0015-tests-rename-NotificationWatcher.patch b/package/rousette/0015-tests-rename-NotificationWatcher.patch deleted file mode 100644 index 7fd57165..00000000 --- a/package/rousette/0015-tests-rename-NotificationWatcher.patch +++ /dev/null @@ -1,57 +0,0 @@ -From f86df829979a26d5192a86c3b43c1393dcba7140 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Tue, 3 Dec 2024 11:45:41 +0100 -Subject: [PATCH 15/44] tests: rename NotificationWatcher -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -This watches notifications going through RESTCONF, so I think -RestconfNotificatonWatcher is better name for this. - -Change-Id: Ieb8d7ce489e683348f065e9e84aae06af7c8ccc4 -Signed-off-by: Mattias Walström ---- - tests/restconf-notifications.cpp | 8 ++++---- - 1 file changed, 4 insertions(+), 4 deletions(-) - -diff --git a/tests/restconf-notifications.cpp b/tests/restconf-notifications.cpp -index db9a3f5..d873512 100644 ---- a/tests/restconf-notifications.cpp -+++ b/tests/restconf-notifications.cpp -@@ -21,11 +21,11 @@ static const auto SERVER_PORT = "10088"; - - using namespace std::chrono_literals; - --struct NotificationWatcher { -+struct RestconfNotificationWatcher { - libyang::Context ctx; - libyang::DataFormat dataFormat; - -- NotificationWatcher(const libyang::Context& ctx) -+ RestconfNotificationWatcher(const libyang::Context& ctx) - : ctx(ctx) - , dataFormat(libyang::DataFormat::JSON) - { -@@ -62,7 +62,7 @@ struct SSEClient { - SSEClient( - boost::asio::io_service& io, - std::latch& requestSent, -- const NotificationWatcher& notification, -+ const RestconfNotificationWatcher& notification, - 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" -@@ -186,7 +186,7 @@ TEST_CASE("NETCONF notification streams") - R"({"example:tlc":{"list":[{"name":"k1","notif":{"message":"nested"}}]}})", - }; - -- NotificationWatcher netconfWatcher(srConn.sessionStart().getContext()); -+ RestconfNotificationWatcher netconfWatcher(srConn.sessionStart().getContext()); - - SECTION("NETCONF streams") - { --- -2.43.0 - diff --git a/package/rousette/0016-tests-make-NotificationWatcher-reusable.patch b/package/rousette/0016-tests-make-NotificationWatcher-reusable.patch deleted file mode 100644 index bd2d7dd7..00000000 --- a/package/rousette/0016-tests-make-NotificationWatcher-reusable.patch +++ /dev/null @@ -1,133 +0,0 @@ -From 30d704588fa7eb9d32f66296ec5f6784f082869e Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 2 Dec 2024 20:11:32 +0100 -Subject: [PATCH 16/44] tests: make NotificationWatcher reusable -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -We will need some parts of this in yang push tests - -Change-Id: I72e60d993f75bbc848af096ac325a75fa3dea61a -Signed-off-by: Mattias Walström ---- - tests/event_watchers.cpp | 28 +++++++++++++++++++++++++ - tests/event_watchers.h | 13 ++++++++++++ - tests/restconf-notifications.cpp | 35 -------------------------------- - 3 files changed, 41 insertions(+), 35 deletions(-) - -diff --git a/tests/event_watchers.cpp b/tests/event_watchers.cpp -index c696bc5..bde96ec 100644 ---- a/tests/event_watchers.cpp -+++ b/tests/event_watchers.cpp -@@ -1,3 +1,4 @@ -+#include - #include "UniqueResource.h" - #include "event_watchers.h" - -@@ -50,3 +51,30 @@ sysrepo::Subscription datastoreNewStateSubscription(sysrepo::Session& session, D - 0, - sysrepo::SubscribeOptions::DoneOnly); - } -+ -+RestconfNotificationWatcher::RestconfNotificationWatcher(const libyang::Context& ctx) -+ : ctx(ctx) -+ , dataFormat(libyang::DataFormat::JSON) -+{ -+} -+ -+void RestconfNotificationWatcher::setDataFormat(const libyang::DataFormat dataFormat) -+{ -+ this->dataFormat = dataFormat; -+} -+ -+void RestconfNotificationWatcher::operator()(const std::string& msg) const -+{ -+ spdlog::trace("Client received data: {}", msg); -+ auto notifDataNode = ctx.parseOp(msg, -+ dataFormat, -+ dataFormat == libyang::DataFormat::JSON ? libyang::OperationType::NotificationRestconf : libyang::OperationType::NotificationNetconf); -+ -+ // parsing nested notifications does not return the data tree root node but the notification data node -+ auto dataRoot = notifDataNode.op; -+ while (dataRoot->parent()) { -+ dataRoot = *dataRoot->parent(); -+ } -+ -+ data(*dataRoot->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink)); -+} -diff --git a/tests/event_watchers.h b/tests/event_watchers.h -index 5b2429c..b6f2dd2 100644 ---- a/tests/event_watchers.h -+++ b/tests/event_watchers.h -@@ -32,3 +32,16 @@ static DatastoreChangesMock testMockForUntrackedModuleWrites; - #define SUBSCRIBE_MODULE(SUBNAME, SESSION, MODULE) \ - ALLOW_CALL(testMockForUntrackedModuleWrites, change(trompeloeil::_)); \ - auto SUBNAME = datastoreChangesSubscription(SESSION, testMockForUntrackedModuleWrites, MODULE); -+ -+struct RestconfNotificationWatcher { -+ libyang::Context ctx; -+ libyang::DataFormat dataFormat; -+ -+ RestconfNotificationWatcher(const libyang::Context& ctx); -+ void setDataFormat(const libyang::DataFormat dataFormat); -+ void operator()(const std::string& msg) const; -+ -+ MAKE_CONST_MOCK1(data, void(const std::string&)); -+}; -+ -+#define EXPECT_NOTIFICATION(DATA, SEQ) expectations.emplace_back(NAMED_REQUIRE_CALL(netconfWatcher, data(DATA)).IN_SEQUENCE(SEQ)); -diff --git a/tests/restconf-notifications.cpp b/tests/restconf-notifications.cpp -index d873512..6c8c51a 100644 ---- a/tests/restconf-notifications.cpp -+++ b/tests/restconf-notifications.cpp -@@ -16,45 +16,10 @@ static const auto SERVER_PORT = "10088"; - #include "tests/aux-utils.h" - #include "tests/pretty_printers.h" - --#define EXPECT_NOTIFICATION(DATA, SEQ) expectations.emplace_back(NAMED_REQUIRE_CALL(netconfWatcher, data(DATA)).IN_SEQUENCE(SEQ)); - #define SEND_NOTIFICATION(DATA) notifSession.sendNotification(*ctx.parseOp(DATA, libyang::DataFormat::JSON, libyang::OperationType::NotificationYang).op, sysrepo::Wait::No); - - using namespace std::chrono_literals; - --struct RestconfNotificationWatcher { -- libyang::Context ctx; -- libyang::DataFormat dataFormat; -- -- RestconfNotificationWatcher(const libyang::Context& ctx) -- : ctx(ctx) -- , dataFormat(libyang::DataFormat::JSON) -- { -- } -- -- void setDataFormat(const libyang::DataFormat dataFormat) -- { -- this->dataFormat = dataFormat; -- } -- -- void operator()(const std::string& msg) const -- { -- spdlog::trace("Client received data: {}", msg); -- auto notifDataNode = ctx.parseOp(msg, -- dataFormat, -- dataFormat == libyang::DataFormat::JSON ? libyang::OperationType::NotificationRestconf : libyang::OperationType::NotificationNetconf); -- -- // parsing nested notifications does not return the data tree root node but the notification data node -- auto dataRoot = notifDataNode.op; -- while (dataRoot->parent()) { -- dataRoot = *dataRoot->parent(); -- } -- -- data(*dataRoot->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink)); -- } -- -- MAKE_CONST_MOCK1(data, void(const std::string&)); --}; -- - struct SSEClient { - std::shared_ptr client; - boost::asio::deadline_timer t; --- -2.43.0 - 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..916aede4 --- /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/17] 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/0017-EventStream-fix-possible-heap-use-after-free.patch b/package/rousette/0017-EventStream-fix-possible-heap-use-after-free.patch new file mode 100644 index 00000000..205af21b --- /dev/null +++ b/package/rousette/0017-EventStream-fix-possible-heap-use-after-free.patch @@ -0,0 +1,102 @@ +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 17/17] 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 +below). The problem is that EventStream::enqueue posted res.resume() +call to the asio event loop, but sometimes when the function was called, +the response object was already destroyed. +The fix is to check if the response is still valid (and not closed) +before actually resuming the response. + + WARNING: ThreadSanitizer: heap-use-after-free (pid=633338) + Read of size 8 at 0x72080000f3d0 by thread T134: + #0 std::__uniq_ptr_impl>::_M_ptr() const /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/unique_ptr.h:193:51 (libnghttp2_asio.so.0+0x205011) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #1 std::unique_ptr>::get() const /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/unique_ptr.h:473:21 (libnghttp2_asio.so.0+0x204fc5) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #2 std::unique_ptr>::operator->() const /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/unique_ptr.h:466:9 (libnghttp2_asio.so.0+0x2046c5) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #3 nghttp2::asio_http2::server::response::resume() const /home/tomas/zdrojaky/cesnet/nghttp2-asio/lib/asio_server_response.cc:63:33 (libnghttp2_asio.so.0+0x204425) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #4 rousette::http::EventStream::enqueue(std::__cxx11::basic_string, std::allocator> const&, std::__cxx11::basic_string, std::allocator> const&)::$_1::operator()() const /home/tomas/zdrojaky/cesnet/rousette/src/http/EventStream.cpp:167:68 (test-restconf-subscribed-notifications+0x2a084d) (BuildId: 9270c9f5d6da566b5b7f223994c283ecf35f0e43) + #5 boost::asio::detail::binder0, std::allocator> const&, std::__cxx11::basic_string, std::allocator> const&)::$_1>::operator()() /usr/include/boost/asio/detail/bind_handler.hpp:56:5 (test-restconf-subscribed-notifications+0x2a084d) + #6 boost::asio::detail::executor_op, std::allocator> const&, std::__cxx11::basic_string, std::allocator> const&)::$_1>, std::allocator, boost::asio::detail::scheduler_operation>::do_complete(void*, boost::asio::detail::scheduler_operation*, boost::system::error_code const&, unsigned long) /usr/include/boost/asio/detail/executor_op.hpp:70:7 (test-restconf-subscribed-notifications+0x2a084d) + #7 boost::asio::detail::scheduler_operation::complete(void*, boost::system::error_code const&, unsigned long) /usr/include/boost/asio/detail/scheduler_operation.hpp:40:5 (test-restconf-subscribed-notifications+0x165ce9) (BuildId: 9270c9f5d6da566b5b7f223994c283ecf35f0e43) + #8 boost::asio::detail::scheduler::do_run_one(boost::asio::detail::conditionally_enabled_mutex::scoped_lock&, boost::asio::detail::scheduler_thread_info&, boost::system::error_code const&) /usr/include/boost/asio/detail/impl/scheduler.ipp:492:12 (test-restconf-subscribed-notifications+0x165ce9) + #9 boost::asio::detail::scheduler::run(boost::system::error_code&) /usr/include/boost/asio/detail/impl/scheduler.ipp:208:10 (test-restconf-subscribed-notifications+0x165292) (BuildId: 9270c9f5d6da566b5b7f223994c283ecf35f0e43) + #10 boost::asio::io_context::run() /usr/include/boost/asio/impl/io_context.ipp:71:24 (libnghttp2_asio.so.0+0x1675bf) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + (...) + + Previous write of size 8 at 0x72080000f3d0 by thread T134: + #0 operator delete(void*, unsigned long) (test-restconf-subscribed-notifications+0x13f7d3) (BuildId: 9270c9f5d6da566b5b7f223994c283ecf35f0e43) + #1 std::default_delete::operator()(nghttp2::asio_http2::server::stream*) const /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/unique_ptr.h:93:2 (libnghttp2_asio.so.0+0x1fc71d) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #2 std::_Sp_counted_deleter, std::allocator, (__gnu_cxx::_Lock_policy)2>::_M_dispose() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/shared_ptr_base.h:526:9 (libnghttp2_asio.so.0+0x1fddaf) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #3 std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release_last_use() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/shared_ptr_base.h:174:2 (libnghttp2_asio.so.0+0x160e46) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #4 std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/shared_ptr_base.h:360:4 (libnghttp2_asio.so.0+0x160dfd) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #5 std::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/shared_ptr_base.h:1069:11 (libnghttp2_asio.so.0+0x160d08) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #6 std::__shared_ptr::~__shared_ptr() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/shared_ptr_base.h:1531:31 (libnghttp2_asio.so.0+0x1f82e9) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #7 std::shared_ptr::~shared_ptr() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/shared_ptr.h:175:11 (libnghttp2_asio.so.0+0x1f82a5) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #8 std::pair>::~pair() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_pair.h:302:12 (libnghttp2_asio.so.0+0x1f8269) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #9 void std::__new_allocator>>>::destroy>>(std::pair>*) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/new_allocator.h:198:10 (libnghttp2_asio.so.0+0x1f815d) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #10 void std::allocator_traits>>>>::destroy>>(std::allocator>>>&, std::pair>*) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/alloc_traits.h:696:8 (libnghttp2_asio.so.0+0x1f815d) + #11 std::_Rb_tree>, std::_Select1st>>, std::less, std::allocator>>>::_M_destroy_node(std::_Rb_tree_node>>*) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_tree.h:1265:2 (libnghttp2_asio.so.0+0x1f815d) + #12 std::_Rb_tree>, std::_Select1st>>, std::less, std::allocator>>>::_M_drop_node(std::_Rb_tree_node>>*) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_tree.h:1273:2 (libnghttp2_asio.so.0+0x1f80b9) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #13 std::_Rb_tree>, std::_Select1st>>, std::less, std::allocator>>>::_M_erase(std::_Rb_tree_node>>*) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_tree.h:2590:4 (libnghttp2_asio.so.0+0x1f7e98) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #14 std::_Rb_tree>, std::_Select1st>>, std::less, std::allocator>>>::clear() /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_tree.h:1878:2 (libnghttp2_asio.so.0+0x1ff035) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #15 std::_Rb_tree>, std::_Select1st>>, std::less, std::allocator>>>::_M_erase_aux(std::_Rb_tree_const_iterator>>, std::_Rb_tree_const_iterator>>) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_tree.h:3127:2 (libnghttp2_asio.so.0+0x1feca6) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #16 std::_Rb_tree>, std::_Select1st>>, std::less, std::allocator>>>::erase(int const&) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_tree.h:3141:7 (libnghttp2_asio.so.0+0x1fe8a3) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #17 std::map, std::less, std::allocator>>>::erase(int const&) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/15.1.1/../../../../include/c++/15.1.1/bits/stl_map.h:1159:21 (libnghttp2_asio.so.0+0x1f68d5) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #18 nghttp2::asio_http2::server::http2_handler::close_stream(int) /home/tomas/zdrojaky/cesnet/nghttp2-asio/lib/asio_server_http2_handler.cc:315:12 (libnghttp2_asio.so.0+0x1f34b0) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #19 nghttp2::asio_http2::server::(anonymous namespace)::on_stream_close_callback(nghttp2_session*, int, unsigned int, void*) /home/tomas/zdrojaky/cesnet/nghttp2-asio/lib/asio_server_http2_handler.cc:193:12 (libnghttp2_asio.so.0+0x1f3074) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #20 nghttp2_session_close_stream /usr/src/debug/libnghttp2/nghttp2/lib/nghttp2_session.c:1289:9 (libnghttp2.so.14+0x62ac) (BuildId: 8a424dc80fadd83dfdd8c51eebe47046c626d95e) + #21 nghttp2::asio_http2::server::connection>::do_write() /home/tomas/zdrojaky/cesnet/nghttp2-asio/lib/asio_server_connection.h:177:20 (libnghttp2_asio.so.0+0x1ebb1c) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #22 nghttp2::asio_http2::server::connection>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)::operator()(boost::system::error_code const&, unsigned long) const /home/tomas/zdrojaky/cesnet/nghttp2-asio/lib/asio_server_connection.h:207:11 (libnghttp2_asio.so.0+0x1ec7a8) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #23 boost::asio::detail::write_op, boost::asio::mutable_buffer, boost::asio::mutable_buffer const*, boost::asio::detail::transfer_all_t, nghttp2::asio_http2::server::connection>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)>::operator()(boost::system::error_code, unsigned long, int) /usr/include/boost/asio/impl/write.hpp:380:9 (libnghttp2_asio.so.0+0x1ec4e5) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #24 boost::asio::detail::binder2, boost::asio::mutable_buffer, boost::asio::mutable_buffer const*, boost::asio::detail::transfer_all_t, nghttp2::asio_http2::server::connection>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)>, boost::system::error_code, unsigned long>::operator()() /usr/include/boost/asio/detail/bind_handler.hpp:181:5 (libnghttp2_asio.so.0+0x1edd41) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #25 void boost::asio::detail::handler_work, boost::asio::mutable_buffer, boost::asio::mutable_buffer const*, boost::asio::detail::transfer_all_t, nghttp2::asio_http2::server::connection>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)>, boost::asio::any_io_executor, void>::complete, boost::asio::mutable_buffer, boost::asio::mutable_buffer const*, boost::asio::detail::transfer_all_t, nghttp2::asio_http2::server::connection>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)>, boost::system::error_code, unsigned long>>(boost::asio::detail::binder2, boost::asio::mutable_buffer, boost::asio::mutable_buffer const*, boost::asio::detail::transfer_all_t, nghttp2::asio_http2::server::connection>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)>, boost::system::error_code, unsigned long>&, boost::asio::detail::write_op, boost::asio::mutable_buffer, boost::asio::mutable_buffer const*, boost::asio::detail::transfer_all_t, nghttp2::asio_http2::server::connection>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)>&) /usr/include/boost/asio/detail/handler_work.hpp:470:7 (libnghttp2_asio.so.0+0x1edb2e) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #26 boost::asio::detail::reactive_socket_send_op, boost::asio::mutable_buffer, boost::asio::mutable_buffer const*, boost::asio::detail::transfer_all_t, nghttp2::asio_http2::server::connection>::do_write()::'lambda'(boost::system::error_code const&, unsigned long)>, boost::asio::any_io_executor>::do_complete(void*, boost::asio::detail::scheduler_operation*, boost::system::error_code const&, unsigned long) /usr/include/boost/asio/detail/reactive_socket_send_op.hpp:154:9 (libnghttp2_asio.so.0+0x1ed66c) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + #27 boost::asio::detail::scheduler_operation::complete(void*, boost::system::error_code const&, unsigned long) /usr/include/boost/asio/detail/scheduler_operation.hpp:40:5 (test-restconf-subscribed-notifications+0x165ce9) (BuildId: 9270c9f5d6da566b5b7f223994c283ecf35f0e43) + #28 boost::asio::detail::scheduler::do_run_one(boost::asio::detail::conditionally_enabled_mutex::scoped_lock&, boost::asio::detail::scheduler_thread_info&, boost::system::error_code const&) /usr/include/boost/asio/detail/impl/scheduler.ipp:492:12 (test-restconf-subscribed-notifications+0x165ce9) + #29 boost::asio::detail::scheduler::run(boost::system::error_code&) /usr/include/boost/asio/detail/impl/scheduler.ipp:208:10 (test-restconf-subscribed-notifications+0x165292) (BuildId: 9270c9f5d6da566b5b7f223994c283ecf35f0e43) + #30 boost::asio::io_context::run() /usr/include/boost/asio/impl/io_context.ipp:71:24 (libnghttp2_asio.so.0+0x1675bf) (BuildId: d31f110d6b82f3b5923f6e08b6ae7d409d710cc4) + (...) + +Change-Id: Ifdb1f8610cacffca3bb49da17aa9b1d267cdd472 +Signed-off-by: Mattias Walström +--- + src/http/EventStream.cpp | 17 ++++++++++++++++- + 1 file changed, 16 insertions(+), 1 deletion(-) + +diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp +index 0a974d4..f97d953 100644 +--- a/src/http/EventStream.cpp ++++ b/src/http/EventStream.cpp +@@ -160,7 +160,22 @@ void EventStream::enqueue(const std::string& fieldName, const std::string& what) + spdlog::trace("{}: new event, ∑ queue size = {}", peer, len); + queue.push_back(buf); + state = HasEvents; +- boost::asio::post(res.io_service(), [&res = this->res]() { res.resume(); }); ++ boost::asio::post(res.io_service(), [weak = weak_from_this()]() { ++ auto myself = weak.lock(); ++ if (!myself) { ++ spdlog::trace("enqueue: already disconnected"); ++ return; ++ } ++ ++ std::lock_guard lock{myself->mtx}; ++ // if state is Closed, then res.on_close() already finished, which means we must not use res anymore ++ if (myself->state == Closed) { ++ spdlog::trace("{}: enqueue: not resuming, the response's on_close handler already finished", myself->peer); ++ return; ++ } ++ ++ myself->res.resume(); ++ }); + } + + void EventStream::start_ping() +-- +2.43.0 + diff --git a/package/rousette/0017-tests-helper-function-to-construct-server-URI.patch b/package/rousette/0017-tests-helper-function-to-construct-server-URI.patch deleted file mode 100644 index 90eecac8..00000000 --- a/package/rousette/0017-tests-helper-function-to-construct-server-URI.patch +++ /dev/null @@ -1,45 +0,0 @@ -From 36943051d8563bee0c9cf89eec28acf5d3617272 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Tue, 3 Dec 2024 12:02:24 +0100 -Subject: [PATCH 17/44] tests: helper function to construct server URI -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: Ic7c03b32a29b07464291f99027530010a7902f77 -Signed-off-by: Mattias Walström ---- - tests/restconf_utils.cpp | 9 +++++++-- - 1 file changed, 7 insertions(+), 2 deletions(-) - -diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp -index ea9a18d..83f568f 100644 ---- a/tests/restconf_utils.cpp -+++ b/tests/restconf_utils.cpp -@@ -13,6 +13,12 @@ using namespace std::string_literals; - namespace ng = nghttp2::asio_http2; - namespace ng_client = ng::client; - -+namespace { -+std::string serverAddressAndPort(const std::string& server_address, const std::string& server_port) { -+ return "http://["s + server_address + "]" + ":" + server_port; -+} -+} -+ - Response::Response(int statusCode, const Response::Headers& headers, const std::string& data) - : Response(statusCode, transformHeaders(headers), data) - { -@@ -75,8 +81,7 @@ Response clientRequest(const std::string& server_address, - reqHeaders.insert({name, {value, false}}); - } - -- const auto server_address_and_port = std::string("http://[") + server_address + "]" + ":" + server_port; -- auto req = client->submit(ec, method, server_address_and_port + uri, data, reqHeaders); -+ auto req = client->submit(ec, method, serverAddressAndPort(server_address, server_port) + uri, data, reqHeaders); - req->on_response([&](const ng_client::response& res) { - res.on_data([&oss](const uint8_t* data, std::size_t len) { - oss.write(reinterpret_cast(data), len); --- -2.43.0 - diff --git a/package/rousette/0018-tests-make-SSEClient-reusable.patch b/package/rousette/0018-tests-make-SSEClient-reusable.patch deleted file mode 100644 index 9c311289..00000000 --- a/package/rousette/0018-tests-make-SSEClient-reusable.patch +++ /dev/null @@ -1,307 +0,0 @@ -From 7d15f59d20079ba94224e0bc308682aa5a004483 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 2 Dec 2024 20:15:05 +0100 -Subject: [PATCH 18/44] tests: make SSEClient reusable -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -We will need it in yang push tests - -Change-Id: I22432553f3abff0de91b3c406abc5567de656065 -Signed-off-by: Mattias Walström ---- - tests/restconf-notifications.cpp | 108 +------------------------------ - tests/restconf_utils.cpp | 69 ++++++++++++++++++++ - tests/restconf_utils.h | 47 ++++++++++++++ - 3 files changed, 119 insertions(+), 105 deletions(-) - -diff --git a/tests/restconf-notifications.cpp b/tests/restconf-notifications.cpp -index 6c8c51a..4496b63 100644 ---- a/tests/restconf-notifications.cpp -+++ b/tests/restconf-notifications.cpp -@@ -7,121 +7,19 @@ - - #include "trompeloeil_doctest.h" - static const auto SERVER_PORT = "10088"; --#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); - - using namespace std::chrono_literals; - --struct SSEClient { -- std::shared_ptr client; -- boost::asio::deadline_timer t; -- -- SSEClient( -- boost::asio::io_service& io, -- std::latch& requestSent, -- const RestconfNotificationWatcher& notification, -- 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" -- : client(std::make_shared(io, SERVER_ADDRESS, SERVER_PORT)) -- , t(io, silenceTimeout) -- { -- ng::header_map reqHeaders; -- for (const auto& [name, value] : headers) { -- reqHeaders.insert({name, {value, false}}); -- } -- -- // shutdown the client after a period of no traffic -- t.async_wait([maybeClient = std::weak_ptr{client}](const boost::system::error_code& ec) { -- if (ec == boost::asio::error::operation_aborted) { -- return; -- } -- if (auto client = maybeClient.lock()) { -- client->shutdown(); -- } -- }); -- -- client->on_connect([&, uri, reqHeaders, silenceTimeout](auto) { -- boost::system::error_code ec; -- -- static const auto server_address_and_port = std::string("http://[") + SERVER_ADDRESS + "]" + ":" + SERVER_PORT; -- auto req = client->submit(ec, "GET", server_address_and_port + uri, "", reqHeaders); -- req->on_response([&, silenceTimeout](const ng_client::response& res) { -- requestSent.count_down(); -- res.on_data([&, silenceTimeout](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); -- } -- t.expires_from_now(silenceTimeout); -- }); -- }); -- }); -- -- client->on_error([&](const boost::system::error_code& ec) { -- throw std::runtime_error{"HTTP client error: " + ec.message()}; -- }); -- } -- -- static std::vector parseEvents(const std::string& msg) -- { -- static const std::string prefix = "data:"; -- -- std::vector res; -- std::istringstream iss(msg); -- std::string line; -- std::string event; -- -- while (std::getline(iss, line)) { -- if (line.compare(0, prefix.size(), prefix) == 0) { -- event += line.substr(prefix.size()); -- } else if (line.empty()) { -- res.emplace_back(std::move(event)); -- event.clear(); -- } else { -- FAIL("Unprefixed response"); -- } -- } -- return res; -- } --}; -- --#define PREPARE_LOOP_WITH_EXCEPTIONS \ -- boost::asio::io_service io; \ -- std::promise bg; \ -- std::latch requestSent(1); -- --#define RUN_LOOP_WITH_EXCEPTIONS \ -- do { \ -- io.run(); \ -- auto fut = bg.get_future(); \ -- REQUIRE(fut.wait_for(666ms /* "plenty of time" for the notificationThread to exit after it has called io.stop() */) == std::future_status::ready); \ -- fut.get(); \ -- } while (false) -- --auto wrap_exceptions_and_asio(std::promise& bg, boost::asio::io_service& io, std::function func) --{ -- return [&bg, &io, func]() -- { -- try { -- func(); -- } catch (...) { -- bg.set_exception(std::current_exception()); -- return; -- } -- bg.set_value(); -- io.stop(); -- }; --} -- - TEST_CASE("NETCONF notification streams") - { - trompeloeil::sequence seqMod1, seqMod2; -@@ -237,7 +135,7 @@ TEST_CASE("NETCONF notification streams") - waitForCompletionAndBitMore(seqMod2); - })); - -- SSEClient cli(io, requestSent, netconfWatcher, uri, headers); -+ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, headers); - RUN_LOOP_WITH_EXCEPTIONS; - } - -@@ -399,7 +297,7 @@ TEST_CASE("NETCONF notification streams") - } - - oldNotificationsDone.wait(); -- SSEClient cli(io, requestSent, netconfWatcher, uri, {AUTH_ROOT}); -+ SSEClient cli(io, SERVER_ADDRESS, SERVER_PORT, requestSent, netconfWatcher, uri, {AUTH_ROOT}); - RUN_LOOP_WITH_EXCEPTIONS; - } - } -diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp -index 83f568f..8252bba 100644 ---- a/tests/restconf_utils.cpp -+++ b/tests/restconf_utils.cpp -@@ -160,3 +160,72 @@ void setupRealNacm(sysrepo::Session session) - session.applyChanges(); - } - -+SSEClient::SSEClient( -+ boost::asio::io_service& io, -+ const std::string& server_address, -+ const std::string& server_port, -+ std::latch& requestSent, -+ const RestconfNotificationWatcher& notification, -+ const std::string& uri, -+ const std::map& headers, -+ const boost::posix_time::seconds silenceTimeout) -+ : client(std::make_shared(io, server_address, server_port)) -+ , t(io, silenceTimeout) -+{ -+ ng::header_map reqHeaders; -+ for (const auto& [name, value] : headers) { -+ reqHeaders.insert({name, {value, false}}); -+ } -+ -+ // shutdown the client after a period of no traffic -+ t.async_wait([maybeClient = std::weak_ptr{client}](const boost::system::error_code& ec) { -+ if (ec == boost::asio::error::operation_aborted) { -+ return; -+ } -+ if (auto client = maybeClient.lock()) { -+ client->shutdown(); -+ } -+ }); -+ -+ client->on_connect([&, uri, reqHeaders, silenceTimeout, server_address, server_port](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) { -+ requestSent.count_down(); -+ res.on_data([&, silenceTimeout](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); -+ } -+ t.expires_from_now(silenceTimeout); -+ }); -+ }); -+ }); -+ -+ client->on_error([&](const boost::system::error_code& ec) { -+ throw std::runtime_error{"HTTP client error: " + ec.message()}; -+ }); -+} -+ -+std::vector SSEClient::parseEvents(const std::string& msg) -+{ -+ static const std::string prefix = "data:"; -+ -+ std::vector res; -+ std::istringstream iss(msg); -+ std::string line; -+ std::string event; -+ -+ while (std::getline(iss, line)) { -+ if (line.compare(0, prefix.size(), prefix) == 0) { -+ event += line.substr(prefix.size()); -+ } else if (line.empty()) { -+ res.emplace_back(std::move(event)); -+ event.clear(); -+ } else { -+ FAIL("Unprefixed response"); -+ } -+ } -+ return res; -+} -diff --git a/tests/restconf_utils.h b/tests/restconf_utils.h -index 26f0803..8b7386e 100644 ---- a/tests/restconf_utils.h -+++ b/tests/restconf_utils.h -@@ -8,7 +8,9 @@ - - #pragma once - #include "trompeloeil_doctest.h" -+#include - #include -+#include "event_watchers.h" - #include "UniqueResource.h" - - namespace sysrepo { -@@ -81,3 +83,48 @@ Response clientRequest( - - UniqueResource manageNacm(sysrepo::Session session); - void setupRealNacm(sysrepo::Session session); -+ -+struct SSEClient { -+ std::shared_ptr client; -+ boost::asio::deadline_timer t; -+ -+ SSEClient( -+ boost::asio::io_service& io, -+ const std::string& server_address, -+ const std::string& server_port, -+ std::latch& requestSent, -+ const RestconfNotificationWatcher& notification, -+ 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" -+ -+ static std::vector parseEvents(const std::string& msg); -+}; -+ -+#define PREPARE_LOOP_WITH_EXCEPTIONS \ -+ boost::asio::io_service io; \ -+ std::promise bg; \ -+ std::latch requestSent(1); -+ -+#define RUN_LOOP_WITH_EXCEPTIONS \ -+ do { \ -+ io.run(); \ -+ auto fut = bg.get_future(); \ -+ REQUIRE(fut.wait_for(666ms /* "plenty of time" for the notificationThread to exit after it has called io.stop() */) == std::future_status::ready); \ -+ fut.get(); \ -+ } while (false) -+ -+inline auto wrap_exceptions_and_asio(std::promise& bg, boost::asio::io_service& io, std::function func) -+{ -+ return [&bg, &io, func]() -+ { -+ try { -+ func(); -+ } catch (...) { -+ bg.set_exception(std::current_exception()); -+ return; -+ } -+ bg.set_value(); -+ io.stop(); -+ }; -+} --- -2.43.0 - diff --git a/package/rousette/0019-tests-fix-deadlock-in-tests.patch b/package/rousette/0019-tests-fix-deadlock-in-tests.patch deleted file mode 100644 index 9ad46eea..00000000 --- a/package/rousette/0019-tests-fix-deadlock-in-tests.patch +++ /dev/null @@ -1,139 +0,0 @@ -From 3e3e492f4801df56226a5aff5d82bfa86c9d3812 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Tue, 3 Dec 2024 13:59:52 +0100 -Subject: [PATCH 19/44] tests: fix deadlock in tests -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -In the case the client never requests (because of misconfiguration or -something else) the test would still be waiting for somebody to -decrement the latch. -Unfortunately std::latch does not allow waiting for specified amount of -time, so this patch replaces it with std::binary_semaphore in which it -is possible to fail the acquiring operation after some time. - -Change-Id: I38c007555bb5ab543329d724a16f024a9d80903e -Signed-off-by: Mattias Walström ---- - tests/restconf-notifications.cpp | 10 +++++----- - tests/restconf_utils.cpp | 4 ++-- - tests/restconf_utils.h | 8 +++++--- - 3 files changed, 12 insertions(+), 10 deletions(-) - -diff --git a/tests/restconf-notifications.cpp b/tests/restconf-notifications.cpp -index 4496b63..04131d7 100644 ---- a/tests/restconf-notifications.cpp -+++ b/tests/restconf-notifications.cpp -@@ -7,6 +7,7 @@ - - #include "trompeloeil_doctest.h" - static const auto SERVER_PORT = "10088"; -+#include - #include - #include - #include -@@ -119,8 +120,7 @@ TEST_CASE("NETCONF notification streams") - auto notifSession = sysrepo::Connection{}.sessionStart(); - auto ctx = notifSession.getContext(); - -- // wait until the client sends its HTTP request -- requestSent.wait(); -+ WAIT_UNTIL_SSE_CLIENT_REQUESTS; - - SEND_NOTIFICATION(notificationsJSON[0]); - SEND_NOTIFICATION(notificationsJSON[1]); -@@ -239,7 +239,7 @@ TEST_CASE("NETCONF notification streams") - SEND_NOTIFICATION(notificationsJSON[3]); - SEND_NOTIFICATION(notificationsJSON[4]); - oldNotificationsDone.count_down(); -- requestSent.wait(); -+ WAIT_UNTIL_SSE_CLIENT_REQUESTS; - - waitForCompletionAndBitMore(seqMod1); - waitForCompletionAndBitMore(seqMod2); -@@ -256,7 +256,7 @@ TEST_CASE("NETCONF notification streams") - SEND_NOTIFICATION(notificationsJSON[1]); - - oldNotificationsDone.count_down(); -- requestSent.wait(); -+ WAIT_UNTIL_SSE_CLIENT_REQUESTS; - - SEND_NOTIFICATION(notificationsJSON[2]); - SEND_NOTIFICATION(notificationsJSON[3]); -@@ -290,7 +290,7 @@ TEST_CASE("NETCONF notification streams") - SEND_NOTIFICATION(notificationsJSON[4]); - - oldNotificationsDone.count_down(); -- requestSent.wait(); -+ WAIT_UNTIL_SSE_CLIENT_REQUESTS; - waitForCompletionAndBitMore(seqMod1); - waitForCompletionAndBitMore(seqMod2); - })); -diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp -index 8252bba..c3ff1de 100644 ---- a/tests/restconf_utils.cpp -+++ b/tests/restconf_utils.cpp -@@ -164,7 +164,7 @@ SSEClient::SSEClient( - boost::asio::io_service& io, - const std::string& server_address, - const std::string& server_port, -- std::latch& requestSent, -+ std::binary_semaphore& requestSent, - const RestconfNotificationWatcher& notification, - const std::string& uri, - const std::map& headers, -@@ -192,7 +192,7 @@ SSEClient::SSEClient( - - auto req = client->submit(ec, "GET", serverAddressAndPort(server_address, server_port) + uri, "", reqHeaders); - req->on_response([&, silenceTimeout](const ng_client::response& res) { -- requestSent.count_down(); -+ requestSent.release(); - res.on_data([&, silenceTimeout](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))) { -diff --git a/tests/restconf_utils.h b/tests/restconf_utils.h -index 8b7386e..9efe398 100644 ---- a/tests/restconf_utils.h -+++ b/tests/restconf_utils.h -@@ -8,8 +8,8 @@ - - #pragma once - #include "trompeloeil_doctest.h" --#include - #include -+#include - #include "event_watchers.h" - #include "UniqueResource.h" - -@@ -92,7 +92,7 @@ struct SSEClient { - boost::asio::io_service& io, - const std::string& server_address, - const std::string& server_port, -- std::latch& requestSent, -+ std::binary_semaphore& requestSent, - const RestconfNotificationWatcher& notification, - const std::string& uri, - const std::map& headers, -@@ -104,7 +104,7 @@ struct SSEClient { - #define PREPARE_LOOP_WITH_EXCEPTIONS \ - boost::asio::io_service io; \ - std::promise bg; \ -- std::latch requestSent(1); -+ std::binary_semaphore requestSent(0); - - #define RUN_LOOP_WITH_EXCEPTIONS \ - do { \ -@@ -114,6 +114,8 @@ struct SSEClient { - fut.get(); \ - } while (false) - -+#define WAIT_UNTIL_SSE_CLIENT_REQUESTS requestSent.try_acquire_for(std::chrono::seconds(3)) -+ - inline auto wrap_exceptions_and_asio(std::promise& bg, boost::asio::io_service& io, std::function func) - { - return [&bg, &io, func]() --- -2.43.0 - diff --git a/package/rousette/0020-restconf-make-as_restconf_notification-reusable.patch b/package/rousette/0020-restconf-make-as_restconf_notification-reusable.patch deleted file mode 100644 index 6b0608c3..00000000 --- a/package/rousette/0020-restconf-make-as_restconf_notification-reusable.patch +++ /dev/null @@ -1,159 +0,0 @@ -From ed0ff23f7ad341d663484f0b2a617cd3bc4923c8 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Tue, 3 Dec 2024 19:27:49 +0100 -Subject: [PATCH 20/44] restconf: make as_restconf_notification reusable -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -The YANG-PUSH notifications are supposed to be wrapped in this too, so -this needs to be accessible from multiple places. - -Change-Id: Icf25caf5f3be3917c524bf6111b5d92db6e287b0 -Signed-off-by: Mattias Walström ---- - src/restconf/NotificationStream.cpp | 37 +-------------------------- - src/restconf/utils/yang.cpp | 39 +++++++++++++++++++++++++++++ - src/restconf/utils/yang.h | 4 +++ - 3 files changed, 44 insertions(+), 36 deletions(-) - -diff --git a/src/restconf/NotificationStream.cpp b/src/restconf/NotificationStream.cpp -index 001d19c..eeddc04 100644 ---- a/src/restconf/NotificationStream.cpp -+++ b/src/restconf/NotificationStream.cpp -@@ -20,41 +20,6 @@ namespace { - - const auto streamListXPath = "/ietf-restconf-monitoring:restconf-state/streams/stream"s; - --/** @brief Wraps a notification data tree with RESTCONF notification envelope. */ --std::string as_restconf_notification(const libyang::Context& ctx, libyang::DataFormat dataFormat, libyang::DataNode notification, const sysrepo::NotificationTimeStamp& time) --{ -- static const auto jsonNamespace = "ietf-restconf"; -- static const auto xmlNamespace = "urn:ietf:params:xml:ns:netconf:notification:1.0"; -- -- std::optional envelope; -- std::optional eventTime; -- std::string timeStr = libyang::yangTimeFormat(time, libyang::TimezoneInterpretation::Local); -- -- /* The namespaces for XML and JSON envelopes are different. See https://datatracker.ietf.org/doc/html/rfc8040#section-6.4 */ -- if (dataFormat == libyang::DataFormat::JSON) { -- envelope = ctx.newOpaqueJSON(jsonNamespace, "notification", std::nullopt); -- eventTime = ctx.newOpaqueJSON(jsonNamespace, "eventTime", libyang::JSON{timeStr}); -- } else { -- envelope = ctx.newOpaqueXML(xmlNamespace, "notification", std::nullopt); -- eventTime = ctx.newOpaqueXML(xmlNamespace, "eventTime", libyang::XML{timeStr}); -- } -- -- // the notification data node holds only the notification data tree but for nested notification we should print the whole YANG data tree -- while (notification.parent()) { -- notification = *notification.parent(); -- } -- -- envelope->insertChild(*eventTime); -- envelope->insertChild(notification); -- -- auto res = *envelope->printStr(dataFormat, libyang::PrintFlags::WithSiblings); -- -- // 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(); -- -- return res; --} -- - void subscribe( - std::optional& sub, - sysrepo::Session& session, -@@ -70,7 +35,7 @@ void subscribe( - return; - } - -- signal(as_restconf_notification(session.getContext(), dataFormat, *notificationTree, time)); -+ signal(rousette::restconf::as_restconf_notification(session.getContext(), dataFormat, *notificationTree, time)); - }; - - if (!sub) { -diff --git a/src/restconf/utils/yang.cpp b/src/restconf/utils/yang.cpp -index 4c4d619..30661fc 100644 ---- a/src/restconf/utils/yang.cpp -+++ b/src/restconf/utils/yang.cpp -@@ -6,8 +6,11 @@ - */ - - #include -+#include - #include - #include -+#include -+#include - - namespace rousette::restconf { - -@@ -79,4 +82,40 @@ bool isKeyNode(const libyang::DataNode& maybeList, const libyang::DataNode& node - } - return false; - } -+ -+ -+/** @brief Wraps a notification data tree with RESTCONF notification envelope. */ -+std::string as_restconf_notification(const libyang::Context& ctx, libyang::DataFormat dataFormat, libyang::DataNode notification, const sysrepo::NotificationTimeStamp& time) -+{ -+ static const auto jsonNamespace = "ietf-restconf"; -+ static const auto xmlNamespace = "urn:ietf:params:xml:ns:netconf:notification:1.0"; -+ -+ std::optional envelope; -+ std::optional eventTime; -+ std::string timeStr = libyang::yangTimeFormat(time, libyang::TimezoneInterpretation::Local); -+ -+ /* The namespaces for XML and JSON envelopes are different. See https://datatracker.ietf.org/doc/html/rfc8040#section-6.4 */ -+ if (dataFormat == libyang::DataFormat::JSON) { -+ envelope = ctx.newOpaqueJSON(jsonNamespace, "notification", std::nullopt); -+ eventTime = ctx.newOpaqueJSON(jsonNamespace, "eventTime", libyang::JSON{timeStr}); -+ } else { -+ envelope = ctx.newOpaqueXML(xmlNamespace, "notification", std::nullopt); -+ eventTime = ctx.newOpaqueXML(xmlNamespace, "eventTime", libyang::XML{timeStr}); -+ } -+ -+ // the notification data node holds only the notification data tree but for nested notification we should print the whole YANG data tree -+ while (notification.parent()) { -+ notification = *notification.parent(); -+ } -+ -+ envelope->insertChild(*eventTime); -+ envelope->insertChild(notification); -+ -+ auto res = *envelope->printStr(dataFormat, libyang::PrintFlags::WithSiblings); -+ -+ // 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(); -+ -+ return res; -+} - } -diff --git a/src/restconf/utils/yang.h b/src/restconf/utils/yang.h -index e91ba8a..a558eae 100644 ---- a/src/restconf/utils/yang.h -+++ b/src/restconf/utils/yang.h -@@ -6,10 +6,13 @@ - */ - - #include -+#include - - namespace libyang { - class Leaf; - class DataNode; -+class Context; -+enum class DataFormat; - } - - namespace rousette::restconf { -@@ -19,4 +22,5 @@ std::string listKeyPredicate(const std::vector& listKeyLeafs, con - std::string leaflistKeyPredicate(const std::string& keyValue); - bool isUserOrderedList(const libyang::DataNode& node); - bool isKeyNode(const libyang::DataNode& maybeList, const libyang::DataNode& node); -+std::string as_restconf_notification(const libyang::Context& ctx, libyang::DataFormat dataFormat, libyang::DataNode notification, const sysrepo::NotificationTimeStamp& time); - } --- -2.43.0 - diff --git a/package/rousette/0021-Update-dependencies.patch b/package/rousette/0021-Update-dependencies.patch deleted file mode 100644 index 95ea530b..00000000 --- a/package/rousette/0021-Update-dependencies.patch +++ /dev/null @@ -1,73 +0,0 @@ -From f7307481fc4ca167592acf925136e5f8a51e2150 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Wed, 18 Dec 2024 17:26:15 +0100 -Subject: [PATCH 21/44] Update dependencies -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/+/8134 -Change-Id: Ib170aa61600ffb9089460247f762ff5a947dd6c6 -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 2 +- - tests/restconf-writing.cpp | 8 ++++---- - 2 files changed, 5 insertions(+), 5 deletions(-) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 22bce32..5ae8062 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -74,7 +74,7 @@ find_package(PkgConfig) - pkg_check_modules(nghttp2 REQUIRED IMPORTED_TARGET libnghttp2_asio>=0.0.90 libnghttp2) - find_package(Boost REQUIRED CONFIG COMPONENTS system thread) - --pkg_check_modules(SYSREPO-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=3) -+pkg_check_modules(SYSREPO-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=4) - pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=3) - pkg_check_modules(SYSTEMD IMPORTED_TARGET libsystemd) - pkg_check_modules(PAM REQUIRED IMPORTED_TARGET pam) -diff --git a/tests/restconf-writing.cpp b/tests/restconf-writing.cpp -index 0932984..d37ab03 100644 ---- a/tests/restconf-writing.cpp -+++ b/tests/restconf-writing.cpp -@@ -772,7 +772,7 @@ TEST_CASE("writing data") - { - "error-type": "protocol", - "error-tag": "invalid-value", -- "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"lst\" instance to insert next to not found. (SR_ERR_NOT_FOUND)\u000A Applying operation \"replace\" failed. (SR_ERR_NOT_FOUND)" -+ "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"lst\" instance to insert next to not found. (SR_ERR_NOT_FOUND)" - } - ] - } -@@ -855,7 +855,7 @@ TEST_CASE("writing data") - { - "error-type": "protocol", - "error-tag": "invalid-value", -- "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"ll\" instance to insert next to not found. (SR_ERR_NOT_FOUND)\u000A Applying operation \"replace\" failed. (SR_ERR_NOT_FOUND)" -+ "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"ll\" instance to insert next to not found. (SR_ERR_NOT_FOUND)" - } - ] - } -@@ -1560,7 +1560,7 @@ TEST_CASE("writing data") - { - "error-type": "protocol", - "error-tag": "invalid-value", -- "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"lst\" instance to insert next to not found. (SR_ERR_NOT_FOUND)\u000A Applying operation \"create\" failed. (SR_ERR_NOT_FOUND)" -+ "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"lst\" instance to insert next to not found. (SR_ERR_NOT_FOUND)" - } - ] - } -@@ -1626,7 +1626,7 @@ TEST_CASE("writing data") - { - "error-type": "protocol", - "error-tag": "invalid-value", -- "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"ll\" instance to insert next to not found. (SR_ERR_NOT_FOUND)\u000A Applying operation \"create\" failed. (SR_ERR_NOT_FOUND)" -+ "error-message": "Session::applyChanges: Couldn't apply changes: SR_ERR_NOT_FOUND\u000A Node \"ll\" instance to insert next to not found. (SR_ERR_NOT_FOUND)" - } - ] - } --- -2.43.0 - diff --git a/package/rousette/0022-restconf-add-RAII-datastore-switch.patch b/package/rousette/0022-restconf-add-RAII-datastore-switch.patch deleted file mode 100644 index 93c54ba1..00000000 --- a/package/rousette/0022-restconf-add-RAII-datastore-switch.patch +++ /dev/null @@ -1,104 +0,0 @@ -From c967e688c53000519ceb6030e8282296e4846d3c Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 13 Jan 2025 18:29:29 +0100 -Subject: [PATCH 22/44] restconf: add RAII datastore switch -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Copied from sysrepo-ietf-alarms[1]. - -[1] https://github.com/CESNET/sysrepo-ietf-alarms/blob/4870dbee30a5ec56135d4f4cb5818f191c089c34/src/utils/sysrepo.cpp - -Change-Id: I51738d06187e141ebd662e7b9421ca995380cafc -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 1 + - src/restconf/utils/sysrepo.cpp | 25 +++++++++++++++++++++++++ - src/restconf/utils/sysrepo.h | 29 +++++++++++++++++++++++++++++ - 3 files changed, 55 insertions(+) - create mode 100644 src/restconf/utils/sysrepo.cpp - create mode 100644 src/restconf/utils/sysrepo.h - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 5ae8062..7f3a9c0 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -114,6 +114,7 @@ add_library(rousette-restconf STATIC - src/restconf/YangSchemaLocations.cpp - src/restconf/uri.cpp - src/restconf/utils/dataformat.cpp -+ src/restconf/utils/sysrepo.cpp - src/restconf/utils/yang.cpp - ) - target_link_libraries(rousette-restconf PUBLIC rousette-http rousette-sysrepo rousette-auth Boost::system Threads::Threads PRIVATE date::date-tz) -diff --git a/src/restconf/utils/sysrepo.cpp b/src/restconf/utils/sysrepo.cpp -new file mode 100644 -index 0000000..622ab27 ---- /dev/null -+++ b/src/restconf/utils/sysrepo.cpp -@@ -0,0 +1,25 @@ -+/* -+ * Copyright (C) 2020, 2022 CESNET, https://photonics.cesnet.cz/ -+ * -+ * Written by Jan Kundrát -+ * Written by Tomáš Pecka -+ */ -+ -+#include -+#include "sysrepo.h" -+ -+namespace rousette::restconf { -+ -+ScopedDatastoreSwitch::ScopedDatastoreSwitch(sysrepo::Session session, sysrepo::Datastore ds) -+ : m_session(std::move(session)) -+ , m_oldDatastore(m_session.activeDatastore()) -+{ -+ m_session.switchDatastore(ds); -+} -+ -+ScopedDatastoreSwitch::~ScopedDatastoreSwitch() -+{ -+ m_session.switchDatastore(m_oldDatastore); -+} -+ -+} -diff --git a/src/restconf/utils/sysrepo.h b/src/restconf/utils/sysrepo.h -new file mode 100644 -index 0000000..da6c359 ---- /dev/null -+++ b/src/restconf/utils/sysrepo.h -@@ -0,0 +1,29 @@ -+/* -+ * Copyright (C) 2020, 2022 CESNET, https://photonics.cesnet.cz/ -+ * -+ * Written by Jan Kundrát -+ * Written by Tomáš Pecka -+ */ -+ -+#pragma once -+ -+#include -+#include -+ -+namespace rousette::restconf { -+ -+/** @brief Ensures that session switches to provided datastore and when the object gets destroyed the session switches back to the original datastore. */ -+class ScopedDatastoreSwitch { -+ sysrepo::Session m_session; -+ sysrepo::Datastore m_oldDatastore; -+ -+public: -+ ScopedDatastoreSwitch(sysrepo::Session session, sysrepo::Datastore ds); -+ ~ScopedDatastoreSwitch(); -+ ScopedDatastoreSwitch(const ScopedDatastoreSwitch&) = delete; -+ ScopedDatastoreSwitch(ScopedDatastoreSwitch&&) = delete; -+ ScopedDatastoreSwitch& operator=(const ScopedDatastoreSwitch&) = delete; -+ ScopedDatastoreSwitch& operator=(ScopedDatastoreSwitch&&) = delete; -+}; -+ -+} --- -2.43.0 - diff --git a/package/rousette/0023-restconf-add-missing-pragma-once.patch b/package/rousette/0023-restconf-add-missing-pragma-once.patch deleted file mode 100644 index f46a0a6a..00000000 --- a/package/rousette/0023-restconf-add-missing-pragma-once.patch +++ /dev/null @@ -1,30 +0,0 @@ -From 05274bd27b886e67accc86be7153ed76465b156e Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Wed, 15 Jan 2025 17:56:26 +0100 -Subject: [PATCH 23/44] restconf: add missing pragma once -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: I6d337c99351e24226fb322a40ba2e96655104860 -Signed-off-by: Mattias Walström ---- - src/restconf/NotificationStream.h | 1 + - 1 file changed, 1 insertion(+) - -diff --git a/src/restconf/NotificationStream.h b/src/restconf/NotificationStream.h -index 6840f11..3029a0d 100644 ---- a/src/restconf/NotificationStream.h -+++ b/src/restconf/NotificationStream.h -@@ -5,6 +5,7 @@ - * - */ - -+#pragma once - #include - #include - #include --- -2.43.0 - diff --git a/package/rousette/0024-restconf-make-fetching-replay-info-reusable.patch b/package/rousette/0024-restconf-make-fetching-replay-info-reusable.patch deleted file mode 100644 index 836b7958..00000000 --- a/package/rousette/0024-restconf-make-fetching-replay-info-reusable.patch +++ /dev/null @@ -1,108 +0,0 @@ -From bd7def1768e360cc491d56805f7c16784c2f4fbe Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Wed, 15 Jan 2025 18:27:25 +0100 -Subject: [PATCH 24/44] restconf: make fetching replay info reusable -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -We are querying sysrepo for replay support in our modules for NETCONF -stream. This part of code will be reused for dynamic subscriptions to -NETCONF stream as well, so let's make it reusable. - -Change-Id: Id5bc656619acb8e56dfafde23b03107139d34e94 -Signed-off-by: Mattias Walström ---- - src/restconf/NotificationStream.cpp | 57 +++++++++++++++++------------ - 1 file changed, 34 insertions(+), 23 deletions(-) - -diff --git a/src/restconf/NotificationStream.cpp b/src/restconf/NotificationStream.cpp -index eeddc04..e00c39d 100644 ---- a/src/restconf/NotificationStream.cpp -+++ b/src/restconf/NotificationStream.cpp -@@ -50,6 +50,36 @@ bool canBeSubscribed(const libyang::Module& mod) - { - return mod.implemented() && mod.name() != "sysrepo"; - } -+ -+struct SysrepoReplayInfo { -+ bool enabled; -+ std::optional earliestNotification; -+}; -+ -+SysrepoReplayInfo sysrepoReplayInfo(sysrepo::Session& session) -+{ -+ decltype(sysrepo::ModuleReplaySupport::earliestNotification) globalEarliestNotification; -+ bool replayEnabled = false; -+ -+ for (const auto& mod : session.getContext().modules()) { -+ if (!canBeSubscribed(mod)) { -+ continue; -+ } -+ -+ auto replay = session.getConnection().getModuleReplaySupport(mod.name()); -+ replayEnabled |= replay.enabled; -+ -+ if (replay.earliestNotification) { -+ if (!globalEarliestNotification) { -+ globalEarliestNotification = replay.earliestNotification; -+ } else { -+ globalEarliestNotification = std::min(*replay.earliestNotification, *globalEarliestNotification); -+ } -+ } -+ } -+ -+ return {replayEnabled, globalEarliestNotification}; -+} - } - - namespace rousette::restconf { -@@ -114,28 +144,9 @@ void NotificationStream::activate() - /** @brief Creates and fills ietf-restconf-monitoring:restconf-state/stream. To be called in oper callback. */ - void notificationStreamList(sysrepo::Session& session, std::optional& parent, const std::string& streamsPrefix) - { -+ const auto replayInfo = sysrepoReplayInfo(session); - static const auto prefix = "/ietf-restconf-monitoring:restconf-state/streams/stream[name='NETCONF']"s; - -- decltype(sysrepo::ModuleReplaySupport::earliestNotification) globalEarliestNotification; -- bool replayEnabled = false; -- -- for (const auto& mod : session.getContext().modules()) { -- if (!canBeSubscribed(mod)) { -- continue; -- } -- -- auto replay = session.getConnection().getModuleReplaySupport(mod.name()); -- replayEnabled |= replay.enabled; -- -- if (replay.earliestNotification) { -- if (!globalEarliestNotification) { -- globalEarliestNotification = replay.earliestNotification; -- } else { -- globalEarliestNotification = std::min(*replay.earliestNotification, *globalEarliestNotification); -- } -- } -- } -- - if (!parent) { - parent = session.getContext().newPath(prefix + "/description", "Default NETCONF notification stream"); - } else { -@@ -144,11 +155,11 @@ void notificationStreamList(sysrepo::Session& session, std::optionalnewPath(prefix + "/access[encoding='xml']/location", streamsPrefix + "NETCONF/XML"); - parent->newPath(prefix + "/access[encoding='json']/location", streamsPrefix + "NETCONF/JSON"); - -- if (replayEnabled) { -+ if (replayInfo.enabled) { - parent->newPath(prefix + "/replay-support", "true"); - -- if (globalEarliestNotification) { -- parent->newPath(prefix + "/replay-log-creation-time", libyang::yangTimeFormat(*globalEarliestNotification, libyang::TimezoneInterpretation::Local)); -+ if (replayInfo.earliestNotification) { -+ parent->newPath(prefix + "/replay-log-creation-time", libyang::yangTimeFormat(*replayInfo.earliestNotification, libyang::TimezoneInterpretation::Local)); - } - } - } --- -2.43.0 - diff --git a/package/rousette/0025-sysrepo-upstream-API-change-NULL-output-on-RPCs.patch b/package/rousette/0025-sysrepo-upstream-API-change-NULL-output-on-RPCs.patch deleted file mode 100644 index 45aa2b95..00000000 --- a/package/rousette/0025-sysrepo-upstream-API-change-NULL-output-on-RPCs.patch +++ /dev/null @@ -1,53 +0,0 @@ -From 08458725211e707dec7bfebf7c5188e766136677 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Fri, 7 Feb 2025 17:34:04 +0100 -Subject: [PATCH 25/44] sysrepo upstream API change: NULL output on RPCs -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/+/8289 -Change-Id: I8db21637246492b0f79d1adf7df1704df5173e39 -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 2 +- - src/restconf/Server.cpp | 4 ++-- - 2 files changed, 3 insertions(+), 3 deletions(-) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 7f3a9c0..cdd0eb4 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -74,7 +74,7 @@ find_package(PkgConfig) - pkg_check_modules(nghttp2 REQUIRED IMPORTED_TARGET libnghttp2_asio>=0.0.90 libnghttp2) - find_package(Boost REQUIRED CONFIG COMPONENTS system thread) - --pkg_check_modules(SYSREPO-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=4) -+pkg_check_modules(SYSREPO-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=5) - pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=3) - pkg_check_modules(SYSTEMD IMPORTED_TARGET libsystemd) - pkg_check_modules(PAM REQUIRED IMPORTED_TARGET pam) -diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp -index 7c66ea4..daf23ed 100644 ---- a/src/restconf/Server.cpp -+++ b/src/restconf/Server.cpp -@@ -462,13 +462,13 @@ void processActionOrRPC(std::shared_ptr requestCtx, const std::c - - auto rpcReply = requestCtx->sess.sendRPC(*rpcNode, timeout); - -- if (rpcReply.immediateChildren().empty()) { -+ if (!rpcReply || rpcReply->immediateChildren().empty()) { - requestCtx->res.write_head(204, {CORS}); - requestCtx->res.end(); - return; - } - -- auto responseNode = rpcReply.child(); -+ auto responseNode = rpcReply->child(); - responseNode->unlinkWithSiblings(); - - auto envelope = ctx.newOpaqueJSON(rpcNode->schema().module().name(), "output", std::nullopt); --- -2.43.0 - diff --git a/package/rousette/0026-boost-1.87-support.patch b/package/rousette/0026-boost-1.87-support.patch deleted file mode 100644 index 6c98b3ec..00000000 --- a/package/rousette/0026-boost-1.87-support.patch +++ /dev/null @@ -1,82 +0,0 @@ -From d69697a719781ef06ecb0545a58c2d055ca53c6d Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 3 Mar 2025 13:32:51 +0100 -Subject: [PATCH 26/44] boost 1.87 support -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -In Asio 1.33 (boost 1.87) some of the deprecated functionalities were -removed [1]. For rousette there are two changes that needed to be -addressed: - - * A removed include in clock.cpp. But actually, we do not need that - header at all. - * boost::asio::io_service is no longer around, it was actually renamed - to boost::asio::io_context a while ago. Its post() method is no - longer around as well, it was deprecated in favour of post() - function [2]. - This function is there since boost 1.66, so let's set minimal boost - version. - -A bigger problem is that our dependency nghttp2-asio seems to be a dead -project. At the time of writing this, there are no patches for more than -3 years. However, we are not the only consumers of that project and -somebody else already took care of that before us [3,4]. - -[1] https://www.boost.org/doc/libs/1_87_0/doc/html/boost_asio/history.html -[2] https://live.boost.org/doc/libs/1_86_0/doc/html/boost_asio/reference/io_context/post.html -[3] https://github.com/nghttp2/nghttp2-asio/issues/23 -[3] https://github.com/microsoft/vcpkg/commit/8eecddf7f18792d41c58a404604eed7f87b4b462 - -Change-Id: Ia59bb06e5f8ed222aa6e9f1c9b3947b05afeb9ec -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 4 ++-- - src/clock.cpp | 1 - - src/http/EventStream.cpp | 2 +- - 3 files changed, 3 insertions(+), 4 deletions(-) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index cdd0eb4..ca41ef3 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -71,8 +71,8 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR}) - find_package(spdlog REQUIRED) - find_package(date REQUIRED) # FIXME: Remove when we have STL with __cpp_lib_chrono >= 201907 (gcc 14) - find_package(PkgConfig) --pkg_check_modules(nghttp2 REQUIRED IMPORTED_TARGET libnghttp2_asio>=0.0.90 libnghttp2) --find_package(Boost REQUIRED CONFIG COMPONENTS system thread) -+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-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=5) - pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=3) -diff --git a/src/clock.cpp b/src/clock.cpp -index 16a49b2..7ac6c3e 100644 ---- a/src/clock.cpp -+++ b/src/clock.cpp -@@ -5,7 +5,6 @@ - * - */ - --#include - #include - #include - #include -diff --git a/src/http/EventStream.cpp b/src/http/EventStream.cpp -index 34038f4..6c765c5 100644 ---- a/src/http/EventStream.cpp -+++ b/src/http/EventStream.cpp -@@ -117,6 +117,6 @@ void EventStream::enqueue(const std::string& what) - spdlog::trace("{}: new event, ∑ queue size = {}", peer, len); - queue.push_back(buf); - state = HasEvents; -- res.io_service().post([&res = this->res]() { res.resume(); }); -+ boost::asio::post(res.io_service(), [&res = this->res]() { res.resume(); }); - } - } --- -2.43.0 - diff --git a/package/rousette/0027-restconf-refactor-uri-parser-for-stream-URIs.patch b/package/rousette/0027-restconf-refactor-uri-parser-for-stream-URIs.patch deleted file mode 100644 index ae690ffd..00000000 --- a/package/rousette/0027-restconf-refactor-uri-parser-for-stream-URIs.patch +++ /dev/null @@ -1,196 +0,0 @@ -From 97ceef119c900c37bbaa27860c3b43cfa6d69f95 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 13 Jan 2025 13:01:07 +0100 -Subject: [PATCH 27/44] restconf: refactor uri parser for stream URIs -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -This is a preparation for the upcoming patch that will add support for -subscribed streams. I will use boost spirit x3 parser to parse such -URIs so I need to refactor the existing parser, which is not really a -parser, but we only match for the two known URIs. - -Change-Id: Ib76973cd967fc3e9558ceb6be8f51239ce4c5ef3 -Signed-off-by: Mattias Walström ---- - src/restconf/Server.cpp | 15 +-------------- - src/restconf/uri.cpp | 36 +++++++++++++++++++++++++++--------- - src/restconf/uri.h | 11 ++++++----- - src/restconf/uri_impl.h | 2 ++ - tests/uri-parser.cpp | 4 ++-- - 5 files changed, 38 insertions(+), 30 deletions(-) - -diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp -index daf23ed..d356f7e 100644 ---- a/src/restconf/Server.cpp -+++ b/src/restconf/Server.cpp -@@ -874,7 +874,6 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: - - server->handle(netconfStreamRoot, [this, conn](const auto& req, const auto& res) mutable { - auto sess = conn.sessionStart(); -- libyang::DataFormat dataFormat; - std::optional xpathFilter; - std::optional startTime; - std::optional stopTime; -@@ -890,18 +889,6 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: - - auto streamRequest = asRestconfStreamRequest(req.method(), req.uri().path, req.uri().raw_query); - -- switch(streamRequest.type) { -- case RestconfStreamRequest::Type::NetconfNotificationJSON: -- dataFormat = libyang::DataFormat::JSON; -- break; -- case RestconfStreamRequest::Type::NetconfNotificationXML: -- dataFormat = libyang::DataFormat::XML; -- break; -- default: -- // GCC 14 complains about uninitialized variable, but asRestconfStreamRequest() would have thrown -- __builtin_unreachable(); -- } -- - if (auto it = streamRequest.queryParams.find("filter"); it != streamRequest.queryParams.end()) { - xpathFilter = std::get(it->second); - } -@@ -916,7 +903,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: - // The signal is constructed outside NotificationStream class because it is required to be passed to - // NotificationStream's parent (EventStream) constructor where it already must be constructed - // Yes, this is a hack. -- auto client = std::make_shared(req, res, std::make_shared(), sess, dataFormat, xpathFilter, startTime, stopTime); -+ auto client = std::make_shared(req, res, std::make_shared(), sess, streamRequest.type.encoding, xpathFilter, startTime, stopTime); - client->activate(); - } catch (const auth::Error& e) { - processAuthError(req, res, e, [&res]() { -diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp -index da1e3a5..e95d70e 100644 ---- a/src/restconf/uri.cpp -+++ b/src/restconf/uri.cpp -@@ -57,6 +57,13 @@ 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 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; -+ - // clang-format on - } - -@@ -197,6 +204,17 @@ std::optional parseQueryParams(const std::string& quer - return ret; - } - -+std::optional parseStreamUri(const std::string& uriPath) -+{ -+ std::optional ret; -+ -+ if (!x3::parse(std::begin(uriPath), std::end(uriPath), streamUriGrammar >> x3::eoi, ret)) { -+ return std::nullopt; -+ } -+ -+ return ret; -+} -+ - URIPrefix::URIPrefix() - : resourceType(URIPrefix::Type::BasicRestconfData) - { -@@ -637,20 +655,20 @@ std::optional> asYangMod - return std::nullopt; - } - --RestconfStreamRequest asRestconfStreamRequest(const std::string& httpMethod, const std::string& uriPath, const std::string& uriQueryString) -+RestconfStreamRequest::NetconfStream::NetconfStream() = default; -+RestconfStreamRequest::NetconfStream::NetconfStream(const libyang::DataFormat& encoding) -+ : encoding(encoding) - { -- static const auto netconfStreamRoot = "/streams/NETCONF/"; -- RestconfStreamRequest::Type type; -+} - -+RestconfStreamRequest asRestconfStreamRequest(const std::string& httpMethod, const std::string& uriPath, const std::string& uriQueryString) -+{ - if (httpMethod != "GET" && httpMethod != "HEAD") { - throw ErrorResponse(405, "application", "operation-not-supported", "Method not allowed."); - } - -- if (uriPath == netconfStreamRoot + "XML"s) { -- type = RestconfStreamRequest::Type::NetconfNotificationXML; -- } else if (uriPath == netconfStreamRoot + "JSON"s) { -- type = RestconfStreamRequest::Type::NetconfNotificationJSON; -- } else { -+ auto type = impl::parseStreamUri(uriPath); -+ if (!type) { - throw ErrorResponse(404, "application", "invalid-value", "Invalid stream"); - } - -@@ -661,7 +679,7 @@ RestconfStreamRequest asRestconfStreamRequest(const std::string& httpMethod, con - - validateQueryParametersForStream(*queryParameters); - -- return {type, *queryParameters}; -+ return {*type, *queryParameters}; - } - - /** @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 f6df724..6f1f69f 100644 ---- a/src/restconf/uri.h -+++ b/src/restconf/uri.h -@@ -197,11 +197,12 @@ struct RestconfRequest { - }; - - struct RestconfStreamRequest { -- enum class Type { -- NetconfNotificationJSON, -- NetconfNotificationXML, -- }; -- Type type; -+ struct NetconfStream { -+ libyang::DataFormat encoding; -+ -+ NetconfStream(); -+ NetconfStream(const libyang::DataFormat& encoding); -+ } type; - queryParams::QueryParams queryParams; - }; - -diff --git a/src/restconf/uri_impl.h b/src/restconf/uri_impl.h -index 2bcdb3f..00b22f6 100644 ---- a/src/restconf/uri_impl.h -+++ b/src/restconf/uri_impl.h -@@ -66,6 +66,8 @@ 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::queryParams::fields::ParenExpr, lhs, rhs); - BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::SlashExpr, lhs, rhs); - BOOST_FUSION_ADAPT_STRUCT(rousette::restconf::queryParams::fields::SemiExpr, lhs, rhs); -diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp -index fd5dd8b..53d1fbb 100644 ---- a/tests/uri-parser.cpp -+++ b/tests/uri-parser.cpp -@@ -1072,13 +1072,13 @@ TEST_CASE("URI path parser") - - { - auto [type, queryParams] = asRestconfStreamRequest("GET", "/streams/NETCONF/XML", ""); -- REQUIRE(type == RestconfStreamRequest::Type::NetconfNotificationXML); -+ REQUIRE(type.encoding == libyang::DataFormat::XML); - REQUIRE(queryParams.empty()); - } - - { - auto [type, queryParams] = asRestconfStreamRequest("GET", "/streams/NETCONF/JSON", ""); -- REQUIRE(type == RestconfStreamRequest::Type::NetconfNotificationJSON); -+ REQUIRE(type.encoding == libyang::DataFormat::JSON); - REQUIRE(queryParams.empty()); - } - --- -2.43.0 - diff --git a/package/rousette/0028-server-prepare-checking-if-yang-features-are-enabled.patch b/package/rousette/0028-server-prepare-checking-if-yang-features-are-enabled.patch deleted file mode 100644 index 86f6dd8c..00000000 --- a/package/rousette/0028-server-prepare-checking-if-yang-features-are-enabled.patch +++ /dev/null @@ -1,54 +0,0 @@ -From cc815dac4ea17ccb09a0481ad82745a194efe95f Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 27 Jan 2025 19:17:07 +0100 -Subject: [PATCH 28/44] server: prepare checking if yang features are enabled -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -We are not requiring any now, but next commits will need it. I am -splitting this into a separate commit for clarity. - -Change-Id: I0a8874b55b21d5ed6c7222f0c36a36c3c5ff52c5 -Signed-off-by: Mattias Walström ---- - src/restconf/Server.cpp | 22 ++++++++++++++-------- - 1 file changed, 14 insertions(+), 8 deletions(-) - -diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp -index d356f7e..55e504a 100644 ---- a/src/restconf/Server.cpp -+++ b/src/restconf/Server.cpp -@@ -817,14 +817,20 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: - , server{std::make_unique()} - , dwdmEvents{std::make_unique(conn.sessionStart())} - { -- for (const auto& [module, version] : { -- std::pair{"ietf-restconf", "2017-01-26"}, -- {"ietf-restconf-monitoring", "2017-01-26"}, -- {"ietf-netconf", ""}, -- {"ietf-yang-library", "2019-01-04"}, -- {"ietf-yang-patch", "2017-02-22"}, -- }) { -- if (!conn.sessionStart().getContext().getModuleImplemented(module)) { -+ for (const auto& [module, version, features] : { -+ std::tuple>{"ietf-restconf", "2017-01-26", {}}, -+ {"ietf-restconf-monitoring", "2017-01-26", {}}, -+ {"ietf-netconf", "", {}}, -+ {"ietf-yang-library", "2019-01-04", {}}, -+ {"ietf-yang-patch", "2017-02-22", {}}, -+ }) { -+ if (auto mod = conn.sessionStart().getContext().getModuleImplemented(module)) { -+ for (const auto& feature : features) { -+ if (!mod->featureEnabled(feature)) { -+ throw std::runtime_error("Module "s + module + "@" + version + " does not implement feature " + feature); -+ } -+ } -+ } else { - throw std::runtime_error("Module "s + module + "@" + version + " is not implemented in sysrepo"); - } - } --- -2.43.0 - diff --git a/package/rousette/0029-tests-refactor-default-handling.patch b/package/rousette/0029-tests-refactor-default-handling.patch deleted file mode 100644 index f0c207ea..00000000 --- a/package/rousette/0029-tests-refactor-default-handling.patch +++ /dev/null @@ -1,54 +0,0 @@ -From f45d7cab63431c94a4db859b07ea6f503214ebaa Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Thu, 13 Mar 2025 17:10:17 +0100 -Subject: [PATCH 29/44] tests: refactor default handling -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -...so that a followup commit which uses -libyang::PrintFlags::KeepEmptyCont introduces less noise. - -Change-Id: I587cf8a5fc0e87b37f4bb91a74d848f58b61e336 -Signed-off-by: Mattias Walström ---- - tests/restconf-defaults.cpp | 8 ++++---- - 1 file changed, 4 insertions(+), 4 deletions(-) - -diff --git a/tests/restconf-defaults.cpp b/tests/restconf-defaults.cpp -index dd8b4da..d5c6004 100644 ---- a/tests/restconf-defaults.cpp -+++ b/tests/restconf-defaults.cpp -@@ -32,7 +32,7 @@ TEST_CASE("default handling") - auto server = rousette::restconf::Server{srConn, SERVER_ADDRESS, SERVER_PORT}; - - // default value of /example:a/b/c/enabled is implicitly set so it should not be printed -- REQUIRE(get(RESTCONF_DATA_ROOT "/example:a", {}) == Response{200, jsonHeaders, R"({ -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:a/b/c", {}) == Response{200, jsonHeaders, R"({ - - } - )"}); -@@ -72,7 +72,7 @@ TEST_CASE("default handling") - )"}); - - // default value is explicitly set so it should be printed -- REQUIRE(get(RESTCONF_DATA_ROOT "/example:a", {}) == Response{200, jsonHeaders, R"({ -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:a/b/c", {}) == Response{200, jsonHeaders, R"({ - "example:a": { - "b": { - "c": { -@@ -86,8 +86,8 @@ TEST_CASE("default handling") - // RFC 6243, sec. 2.3.3: A valid 'delete' operation attribute for a data node that has been set by a client to its schema default value MUST succeed. - REQUIRE(httpDelete(RESTCONF_DATA_ROOT "/example:a/b/c/enabled", {AUTH_ROOT}) == Response{204, noContentTypeHeaders, ""}); - -- // default value is implicitly set so it should be printed -- REQUIRE(get(RESTCONF_DATA_ROOT "/example:a", {}) == Response{200, jsonHeaders, R"({ -+ // default value is only there implicitly, so it should *not* be printed -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:a/b/c", {}) == Response{200, jsonHeaders, R"({ - - } - )"}); --- -2.43.0 - diff --git a/package/rousette/0030-Always-print-empty-containers-in-user-defined-conten.patch b/package/rousette/0030-Always-print-empty-containers-in-user-defined-conten.patch deleted file mode 100644 index 4fbcbe27..00000000 --- a/package/rousette/0030-Always-print-empty-containers-in-user-defined-conten.patch +++ /dev/null @@ -1,356 +0,0 @@ -From 22442d322bda3d83457dd0f1da45d95a10e93081 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Thu, 13 Mar 2025 17:32:59 +0100 -Subject: [PATCH 30/44] Always print empty containers in user-defined content -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -There's some subtle interaction between depth-limited printing, default -values, and empty non-presence containers. In order to prepare for an -upstream change in sysrepo which fixed depth-limited printing, let's -stick to "always" printing the empty non-presence containers. - -(It actually isn't "always" because the data such as error reports, -patches, etc., are still printed using the default settings. But the -data which is subject to user-defined constraints is now consistent.) - -Change-Id: Ie2d6ad2e851d667e7e7582fcd0ac75dabce2c05e -Signed-off-by: Mattias Walström ---- - src/restconf/Server.cpp | 4 +- - src/restconf/utils/yang.cpp | 2 +- - src/sr/AllEvents.cpp | 2 +- - src/sr/OpticalEvents.cpp | 2 +- - tests/restconf-defaults.cpp | 12 +++++- - tests/restconf-nacm.cpp | 12 +++++- - tests/restconf-reading.cpp | 79 ++++++++++++++++++++++++++++++++++--- - 7 files changed, 98 insertions(+), 15 deletions(-) - -diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp -index 55e504a..bae3992 100644 ---- a/src/restconf/Server.cpp -+++ b/src/restconf/Server.cpp -@@ -478,7 +478,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)); -+ requestCtx->res.end(*envelope->printStr(requestCtx->dataFormat.response, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont)); - } - - void processPost(std::shared_ptr requestCtx, const std::chrono::milliseconds timeout) -@@ -768,7 +768,7 @@ libyang::PrintFlags libyangPrintFlags(const libyang::DataNode& dataNode, const s - } catch(const libyang::Error& e) { - } - -- libyang::PrintFlags ret = libyang::PrintFlags::WithSiblings; -+ libyang::PrintFlags ret = libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont; - - if (!withDefaults && node && (node->schema().nodeType() == libyang::NodeType::Leaf || node->schema().nodeType() == libyang::NodeType::Leaflist) && node->asTerm().isImplicitDefault()) { - return ret | libyang::PrintFlags::WithDefaultsAll; -diff --git a/src/restconf/utils/yang.cpp b/src/restconf/utils/yang.cpp -index 30661fc..8acb160 100644 ---- a/src/restconf/utils/yang.cpp -+++ b/src/restconf/utils/yang.cpp -@@ -111,7 +111,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); -+ auto res = *envelope->printStr(dataFormat, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont); - - // 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 6e2a21c..2aca4c7 100644 ---- a/src/sr/AllEvents.cpp -+++ b/src/sr/AllEvents.cpp -@@ -109,7 +109,7 @@ sysrepo::ErrorCode AllEvents::onChange(sysrepo::Session session, const std::stri - break; - } - }; -- auto json = *copy.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings); -+ auto json = *copy.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont); - spdlog::info("JSON: {}", json); - spdlog::warn("FULL JSON: {}", - *session.getData('/' + module + ":*")->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)); -diff --git a/src/sr/OpticalEvents.cpp b/src/sr/OpticalEvents.cpp -index 8c5feff..25906a8 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); -+ return *session.getData('/' + module + ":*")->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont); - } - } - -diff --git a/tests/restconf-defaults.cpp b/tests/restconf-defaults.cpp -index d5c6004..89083f1 100644 ---- a/tests/restconf-defaults.cpp -+++ b/tests/restconf-defaults.cpp -@@ -33,7 +33,11 @@ TEST_CASE("default handling") - - // default value of /example:a/b/c/enabled is implicitly set so it should not be printed - REQUIRE(get(RESTCONF_DATA_ROOT "/example:a/b/c", {}) == Response{200, jsonHeaders, R"({ -- -+ "example:a": { -+ "b": { -+ "c": {} -+ } -+ } - } - )"}); - -@@ -88,7 +92,11 @@ TEST_CASE("default handling") - - // default value is only there implicitly, so it should *not* be printed - REQUIRE(get(RESTCONF_DATA_ROOT "/example:a/b/c", {}) == Response{200, jsonHeaders, R"({ -- -+ "example:a": { -+ "b": { -+ "c": {} -+ } -+ } - } - )"}); - } -diff --git a/tests/restconf-nacm.cpp b/tests/restconf-nacm.cpp -index 29d7723..51328f3 100644 ---- a/tests/restconf-nacm.cpp -+++ b/tests/restconf-nacm.cpp -@@ -130,6 +130,9 @@ TEST_CASE("NACM") - "clock": { - "timezone-utc-offset": 2 - }, -+ "dns-resolver": { -+ "options": {} -+ }, - "radius": { - "server": [ - { -@@ -139,7 +142,8 @@ TEST_CASE("NACM") - "shared-secret": "shared-secret" - } - } -- ] -+ ], -+ "options": {} - } - } - } -@@ -207,6 +211,9 @@ TEST_CASE("NACM") - "clock": { - "timezone-utc-offset": 2 - }, -+ "dns-resolver": { -+ "options": {} -+ }, - "radius": { - "server": [ - { -@@ -216,7 +223,8 @@ TEST_CASE("NACM") - "shared-secret": "shared-secret" - } - } -- ] -+ ], -+ "options": {} - } - } - } -diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp -index e709486..ffdb047 100644 ---- a/tests/restconf-reading.cpp -+++ b/tests/restconf-reading.cpp -@@ -62,10 +62,22 @@ TEST_CASE("reading data") - // this relies on a NACM rule for anonymous access that filters out "a lot of stuff" - REQUIRE(get(RESTCONF_DATA_ROOT, {}) == Response{200, jsonHeaders, R"({ - "example:top-level-leaf": "moo", -+ "example:tlc": {}, -+ "example:a": { -+ "b": { -+ "c": {} -+ }, -+ "b1": {}, -+ "example-augment:b": { -+ "c": {} -+ } -+ }, -+ "example:two-leafs": {}, - "example:config-nonconfig": { - "config-node": "foo-config-true", - "nonconfig-node": "foo-config-false" - }, -+ "example:ordered-lists": {}, - "ietf-restconf-monitoring:restconf-state": { - "capabilities": { - "capability": [ -@@ -107,10 +119,22 @@ TEST_CASE("reading data") - - REQUIRE(get(RESTCONF_ROOT_DS("operational"), {}) == Response{200, jsonHeaders, R"({ - "example:top-level-leaf": "moo", -+ "example:tlc": {}, -+ "example:a": { -+ "b": { -+ "c": {} -+ }, -+ "b1": {}, -+ "example-augment:b": { -+ "c": {} -+ } -+ }, -+ "example:two-leafs": {}, - "example:config-nonconfig": { - "config-node": "foo-config-true", - "nonconfig-node": "foo-config-false" - }, -+ "example:ordered-lists": {}, - "ietf-restconf-monitoring:restconf-state": { - "capabilities": { - "capability": [ -@@ -152,9 +176,21 @@ TEST_CASE("reading data") - - REQUIRE(get(RESTCONF_ROOT_DS("running"), {}) == Response{200, jsonHeaders, R"({ - "example:top-level-leaf": "moo", -+ "example:tlc": {}, -+ "example:a": { -+ "b": { -+ "c": {} -+ }, -+ "b1": {}, -+ "example-augment:b": { -+ "c": {} -+ } -+ }, -+ "example:two-leafs": {}, - "example:config-nonconfig": { - "config-node": "foo-config-true" -- } -+ }, -+ "example:ordered-lists": {} - } - )"}); - } -@@ -213,7 +249,8 @@ TEST_CASE("reading data") - { - "name": "a" - } -- ] -+ ], -+ "options": {} - } - } - } -@@ -244,7 +281,8 @@ TEST_CASE("reading data") - "shared-secret": "shared-secret" - } - } -- ] -+ ], -+ "options": {} - } - } - } -@@ -714,6 +752,7 @@ TEST_CASE("reading data") - "enabled": true - } - }, -+ "b1": {}, - "example-augment:b": { - "c": { - "enabled": true -@@ -723,9 +762,18 @@ TEST_CASE("reading data") - } - )"}); - REQUIRE(get(RESTCONF_DATA_ROOT "/example:a?with-defaults=explicit", {}) == Response{200, jsonHeaders, R"({ -- -+ "example:a": { -+ "b": { -+ "c": {} -+ }, -+ "b1": {}, -+ "example-augment:b": { -+ "c": {} -+ } -+ } - } - )"}); -+ // FIXME: libyang is not really consistent in printing of NP-containers when trimming away the defaults... - REQUIRE(get(RESTCONF_DATA_ROOT "/example:a?with-defaults=trim", {}) == Response{200, jsonHeaders, R"({ - - } -@@ -740,6 +788,7 @@ TEST_CASE("reading data") - } - } - }, -+ "b1": {}, - "example-augment:b": { - "c": { - "enabled": true, -@@ -766,6 +815,7 @@ TEST_CASE("reading data") - "enabled": true - } - }, -+ "b1": {}, - "example-augment:b": { - "c": { - "enabled": true -@@ -781,13 +831,24 @@ TEST_CASE("reading data") - "c": { - "enabled": true - } -+ }, -+ "b1": {}, -+ "example-augment:b": { -+ "c": {} - } - } - } - )"}); - -+ // FIXME: libyang is not consistent here: -+ // - /example:a/b/c NP-container *is* printed, -+ // - /example:a/example-augment:b/c NP-container is *not* printed - REQUIRE(get(RESTCONF_DATA_ROOT "/example:a?with-defaults=trim", {}) == Response{200, jsonHeaders, R"({ -- -+ "example:a": { -+ "b": { -+ "c": {} -+ } -+ } - } - )"}); - -@@ -801,6 +862,7 @@ TEST_CASE("reading data") - } - } - }, -+ "b1": {}, - "example-augment:b": { - "c": { - "enabled": true, -@@ -830,10 +892,15 @@ TEST_CASE("reading data") - )"}); - - REQUIRE(get(RESTCONF_DATA_ROOT "/example:a/b/c/enabled?with-defaults=explicit", {}) == Response{200, jsonHeaders, R"({ -- -+ "example:a": { -+ "b": { -+ "c": {} -+ } -+ } - } - )"}); - -+ // again, libyang is not 100% consistent in the `trim` mode - REQUIRE(get(RESTCONF_DATA_ROOT "/example:a/b/c/enabled?with-defaults=trim", {}) == Response{200, jsonHeaders, R"({ - - } --- -2.43.0 - diff --git a/package/rousette/0031-fix-a-typo-in-RFC-number.patch b/package/rousette/0031-fix-a-typo-in-RFC-number.patch deleted file mode 100644 index 55f4ae9d..00000000 --- a/package/rousette/0031-fix-a-typo-in-RFC-number.patch +++ /dev/null @@ -1,31 +0,0 @@ -From 818af7ec1dc890774603dbf11ed81cf64d89a628 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Thu, 13 Mar 2025 17:56:50 +0100 -Subject: [PATCH 31/44] fix a typo in RFC number -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: I81b68b3920c41129cb98728481580699e44e8c20 -Signed-off-by: Mattias Walström ---- - tests/restconf-reading.cpp | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp -index ffdb047..38f5496 100644 ---- a/tests/restconf-reading.cpp -+++ b/tests/restconf-reading.cpp -@@ -879,7 +879,7 @@ TEST_CASE("reading data") - - SECTION("Implicit node with default value") - { -- // RFC 4080, 3.5.4: If target of the query is implicitly created node with default value, ignore basic mode -+ // RFC 8040, 3.5.4: If target of the query is implicitly created node with default value, ignore basic mode - REQUIRE(get(RESTCONF_DATA_ROOT "/example:a/b/c/enabled", {}) == Response{200, jsonHeaders, R"({ - "example:a": { - "b": { --- -2.43.0 - diff --git a/package/rousette/0032-Fix-bug-in-depth-processing.patch b/package/rousette/0032-Fix-bug-in-depth-processing.patch deleted file mode 100644 index e82d50ef..00000000 --- a/package/rousette/0032-Fix-bug-in-depth-processing.patch +++ /dev/null @@ -1,104 +0,0 @@ -From 9997d61dc43775444e4d78c37054a30b602c2603 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Thu, 13 Mar 2025 18:11:39 +0100 -Subject: [PATCH 32/44] Fix bug in depth= processing -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -...which got fixed upstream in sysrepo. - -Change-Id: Ida37c48058488be32230165b8b4f45c6bd5a4d3a -Depends-on: https://gerrit.cesnet.cz/c/CzechLight/dependencies/+/8429 -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 1 + - tests/restconf-reading.cpp | 45 ++++++++++++++++++++++++++++++++++++++ - 2 files changed, 46 insertions(+) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index ca41ef3..272caad 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -74,6 +74,7 @@ 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>=5) - pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=3) - pkg_check_modules(SYSTEMD IMPORTED_TARGET libsystemd) -diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp -index 38f5496..f87c4f5 100644 ---- a/tests/restconf-reading.cpp -+++ b/tests/restconf-reading.cpp -@@ -243,6 +243,13 @@ TEST_CASE("reading data") - )"}); - - REQUIRE(get(RESTCONF_DATA_ROOT "/ietf-system:system/radius?depth=1", {AUTH_DWDM}) == Response{200, jsonHeaders, R"({ -+ "ietf-system:system": { -+ "radius": {} -+ } -+} -+)"}); -+ -+ REQUIRE(get(RESTCONF_DATA_ROOT "/ietf-system:system/radius?depth=2", {AUTH_DWDM}) == Response{200, jsonHeaders, R"({ - "ietf-system:system": { - "radius": { - "server": [ -@@ -1026,6 +1033,25 @@ TEST_CASE("reading data") - } - )"}); - REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/list=blabla?fields=nested/data&depth=1", {}) == Response{200, jsonHeaders, R"({ -+ "example:tlc": { -+ "list": [ -+ { -+ "name": "blabla", -+ "nested": [ -+ { -+ "first": "1", -+ "second": 2, -+ "third": "3", -+ "data": {} -+ } -+ ] -+ } -+ ] -+ } -+} -+)"}); -+ -+ REQUIRE(get(RESTCONF_DATA_ROOT "/example:tlc/list=blabla?fields=nested/data&depth=2", {}) == Response{200, jsonHeaders, R"({ - "example:tlc": { - "list": [ - { -@@ -1049,6 +1075,25 @@ TEST_CASE("reading data") - - // whole datastore with fields filtering - REQUIRE(get(RESTCONF_DATA_ROOT "?fields=example:tlc/list/nested/data&depth=1", {}) == Response{200, jsonHeaders, R"({ -+ "example:tlc": { -+ "list": [ -+ { -+ "name": "blabla", -+ "nested": [ -+ { -+ "first": "1", -+ "second": 2, -+ "third": "3", -+ "data": {} -+ } -+ ] -+ } -+ ] -+ } -+} -+)"}); -+ -+ REQUIRE(get(RESTCONF_DATA_ROOT "?fields=example:tlc/list/nested/data&depth=2", {}) == Response{200, jsonHeaders, R"({ - "example:tlc": { - "list": [ - { --- -2.43.0 - diff --git a/package/rousette/0033-Adapt-to-libyang-cpp-API-break-in-opaque-nodes.patch b/package/rousette/0033-Adapt-to-libyang-cpp-API-break-in-opaque-nodes.patch deleted file mode 100644 index b3988f96..00000000 --- a/package/rousette/0033-Adapt-to-libyang-cpp-API-break-in-opaque-nodes.patch +++ /dev/null @@ -1,89 +0,0 @@ -From 009b2b01fb4a1ca854402debd66d899a264db8f2 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Fri, 14 Mar 2025 13:57:26 +0100 -Subject: [PATCH 33/44] Adapt to libyang-cpp API break in opaque nodes -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -I was wondering for a while whether libyang will do the right thing when -printing the RPC's output nodes in XML, but apparently it does (and we -have a test for that). - -Depends-on: https://gerrit.cesnet.cz/c/CzechLight/dependencies/+/8438 -Change-Id: I82425743fd2613fcbe2b696848e2feb1c47fc658 -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 2 +- - src/restconf/Server.cpp | 5 +++-- - src/restconf/utils/yang.cpp | 9 +++++---- - 3 files changed, 9 insertions(+), 7 deletions(-) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 272caad..bcabe84 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>=3.6.5 IMPORTED_TARGET) - pkg_check_modules(SYSREPO-CPP REQUIRED IMPORTED_TARGET sysrepo-cpp>=5) --pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=3) -+pkg_check_modules(LIBYANG-CPP REQUIRED IMPORTED_TARGET libyang-cpp>=4) - 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 bae3992..28e2e4c 100644 ---- a/src/restconf/Server.cpp -+++ b/src/restconf/Server.cpp -@@ -471,7 +471,8 @@ void processActionOrRPC(std::shared_ptr requestCtx, const std::c - auto responseNode = rpcReply->child(); - responseNode->unlinkWithSiblings(); - -- auto envelope = ctx.newOpaqueJSON(rpcNode->schema().module().name(), "output", std::nullopt); -+ // libyang auto-resolves the XML namespace when the result is printed into XML -+ auto envelope = ctx.newOpaqueJSON({rpcNode->schema().module().name(), rpcNode->schema().module().name(), "output"}, std::nullopt); - envelope->insertChild(*responseNode); - - requestCtx->res.write_head(200, { -@@ -735,7 +736,7 @@ libyang::DataNode apiResource(const libyang::Context& ctx, const RestconfRequest - } - - for (const auto& rpc : mod.actionRpcs()) { -- operations.insertChild(*ctx.newOpaqueJSON(rpc.module().name(), rpc.name(), libyang::JSON{"[null]"})); -+ operations.insertChild(*ctx.newOpaqueJSON({rpc.module().name(), rpc.module().name(), rpc.name()}, libyang::JSON{"[null]"})); - } - } - } else { -diff --git a/src/restconf/utils/yang.cpp b/src/restconf/utils/yang.cpp -index 8acb160..9358840 100644 ---- a/src/restconf/utils/yang.cpp -+++ b/src/restconf/utils/yang.cpp -@@ -89,6 +89,7 @@ std::string as_restconf_notification(const libyang::Context& ctx, libyang::DataF - { - static const auto jsonNamespace = "ietf-restconf"; - static const auto xmlNamespace = "urn:ietf:params:xml:ns:netconf:notification:1.0"; -+ static const auto xmlPrefix = "ietf-netconf-notifications"; - - std::optional envelope; - std::optional eventTime; -@@ -96,11 +97,11 @@ std::string as_restconf_notification(const libyang::Context& ctx, libyang::DataF - - /* The namespaces for XML and JSON envelopes are different. See https://datatracker.ietf.org/doc/html/rfc8040#section-6.4 */ - if (dataFormat == libyang::DataFormat::JSON) { -- envelope = ctx.newOpaqueJSON(jsonNamespace, "notification", std::nullopt); -- eventTime = ctx.newOpaqueJSON(jsonNamespace, "eventTime", libyang::JSON{timeStr}); -+ envelope = ctx.newOpaqueJSON({jsonNamespace, jsonNamespace, "notification"}, std::nullopt); -+ eventTime = ctx.newOpaqueJSON({jsonNamespace, jsonNamespace, "eventTime"}, libyang::JSON{timeStr}); - } else { -- envelope = ctx.newOpaqueXML(xmlNamespace, "notification", std::nullopt); -- eventTime = ctx.newOpaqueXML(xmlNamespace, "eventTime", libyang::XML{timeStr}); -+ envelope = ctx.newOpaqueXML({xmlNamespace, xmlPrefix, "notification"}, std::nullopt); -+ eventTime = ctx.newOpaqueXML({xmlNamespace, xmlPrefix, "eventTime"}, libyang::XML{timeStr}); - } - - // the notification data node holds only the notification data tree but for nested notification we should print the whole YANG data tree --- -2.43.0 - diff --git a/package/rousette/0034-Fix-XML-serialization-when-listing-RPCs.patch b/package/rousette/0034-Fix-XML-serialization-when-listing-RPCs.patch deleted file mode 100644 index 4a853b50..00000000 --- a/package/rousette/0034-Fix-XML-serialization-when-listing-RPCs.patch +++ /dev/null @@ -1,122 +0,0 @@ -From 061d960d9435018aaba3b2d1b4d857ddb8836d1b Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Fri, 14 Mar 2025 15:38:44 +0100 -Subject: [PATCH 34/44] Fix XML serialization when listing RPCs -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -The RFC says [1] that each RPC entry that's listed in the reply should -look like this when serialized into the XML format: - - - -Our old code would, due to the way how libyang deals with opaque nodes, -just try to dump the JSON-ish `[null]` bits in there, which is wrong. - -[1] https://datatracker.ietf.org/doc/html/rfc8040#page-84 - -Change-Id: I6ea433dbd3fcf3ca2883d3fa00c9d215c0e1bb4f -Depends-on: https://gerrit.cesnet.cz/c/CzechLight/br2-external/+/8443 -Signed-off-by: Mattias Walström ---- - src/restconf/Server.cpp | 16 +++++++++++++--- - tests/restconf-rpc.cpp | 33 ++++++++++++++++++++++++++++++++- - 2 files changed, 45 insertions(+), 4 deletions(-) - -diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp -index 28e2e4c..05239e9 100644 ---- a/src/restconf/Server.cpp -+++ b/src/restconf/Server.cpp -@@ -712,7 +712,7 @@ void processPutOrPlainPatch(std::shared_ptr requestCtx, const st - } - - /** @brief Build data trees for endpoints returning ietf-restconf:restconf data */ --libyang::DataNode apiResource(const libyang::Context& ctx, const RestconfRequest::Type& type) -+libyang::DataNode apiResource(const libyang::Context& ctx, const RestconfRequest::Type& type, libyang::DataFormat dataFormat) - { - const auto yangLib = *ctx.getModuleLatest("ietf-yang-library"); - const auto yangApiExt = ctx.getModuleImplemented("ietf-restconf")->extensionInstance("yang-api"); -@@ -736,7 +736,16 @@ libyang::DataNode apiResource(const libyang::Context& ctx, const RestconfRequest - } - - for (const auto& rpc : mod.actionRpcs()) { -- operations.insertChild(*ctx.newOpaqueJSON({rpc.module().name(), rpc.module().name(), rpc.name()}, libyang::JSON{"[null]"})); -+ switch (dataFormat) { -+ case libyang::DataFormat::JSON: -+ operations.insertChild(*ctx.newOpaqueJSON({rpc.module().name(), rpc.module().name(), rpc.name()}, libyang::JSON{"[null]"})); -+ break; -+ case libyang::DataFormat::XML: -+ operations.insertChild(*ctx.newOpaqueXML({rpc.module().ns(), rpc.module().name(), rpc.name()}, std::nullopt)); -+ break; -+ default: -+ throw std::logic_error{"Invalid data format for apiResource()"}; -+ } - } - } - } else { -@@ -987,7 +996,8 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: - case RestconfRequest::Type::YangLibraryVersion: - case RestconfRequest::Type::ListRPC: - res.write_head(200, {contentType(dataFormat.response), CORS}); -- res.end(*apiResource(sess.getContext(), restconfRequest.type).printStr(dataFormat.response, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont)); -+ res.end(*apiResource(sess.getContext(), restconfRequest.type, dataFormat.response) -+ .printStr(dataFormat.response, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont)); - break; - - case RestconfRequest::Type::GetData: { -diff --git a/tests/restconf-rpc.cpp b/tests/restconf-rpc.cpp -index c4229a0..d49b121 100644 ---- a/tests/restconf-rpc.cpp -+++ b/tests/restconf-rpc.cpp -@@ -79,7 +79,9 @@ TEST_CASE("invoking actions and rpcs") - - SECTION("List RPCs") - { -- REQUIRE(get(RESTCONF_OPER_ROOT, {AUTH_ROOT}) == Response{200, jsonHeaders, R"({ -+ SECTION("JSON") -+ { -+ REQUIRE(get(RESTCONF_OPER_ROOT, {AUTH_ROOT}) == Response{200, jsonHeaders, R"({ - "ietf-restconf:restconf": { - "operations": { - "ietf-factory-default:factory-reset": [null], -@@ -103,6 +105,35 @@ TEST_CASE("invoking actions and rpcs") - } - } - )"}); -+ } -+ -+ SECTION("XML") -+ { -+ REQUIRE(get(RESTCONF_OPER_ROOT, {AUTH_ROOT, CONTENT_TYPE_XML}) -+ == Response{200, xmlHeaders, -+ R"( -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+)"}); -+ } - } - - SECTION("RPC") --- -2.43.0 - diff --git a/package/rousette/0035-make-sure-that-edit-tree-is-the-first-sibling.patch b/package/rousette/0035-make-sure-that-edit-tree-is-the-first-sibling.patch deleted file mode 100644 index 8166d457..00000000 --- a/package/rousette/0035-make-sure-that-edit-tree-is-the-first-sibling.patch +++ /dev/null @@ -1,39 +0,0 @@ -From 6ad606cb9ad49ca7bfb8056b9695ab271a33e790 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Wed, 26 Mar 2025 18:56:06 +0100 -Subject: [PATCH 35/44] make sure that edit tree is the first sibling -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -After a bug in velia [1], where we passed to Session::editBatch a tree -which was not the first sibling (and this caused that all the left -siblings of the node were not considered in the edit), I am making sure -that we do not lose any data in this project due to this too. - -[1] https://github.com/CESNET/velia/commit/2b7187405740fe61e8b37e0efab8c3173d398a26 - -Fixes: 25f3e3b ("restconf: support for YANG patch") -Change-Id: I7211168325fbfddd75dc1520c9dd83e31138d3bc -Signed-off-by: Mattias Walström ---- - src/restconf/Server.cpp | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp -index 05239e9..c454541 100644 ---- a/src/restconf/Server.cpp -+++ b/src/restconf/Server.cpp -@@ -600,7 +600,7 @@ void processYangPatchEdit(const std::shared_ptr& requestCtx, con - if (!mergedEdits) { - mergedEdits = *singleEdit; - } else { -- mergedEdits->insertSibling(*singleEdit); -+ mergedEdits = mergedEdits->insertSibling(*singleEdit); // make sure we point to the first sibling, sysrepo::editBatch requires that - } - } - --- -2.43.0 - diff --git a/package/rousette/0036-restconf-introduce-internal-RPC-handling-for-subscri.patch b/package/rousette/0036-restconf-introduce-internal-RPC-handling-for-subscri.patch deleted file mode 100644 index b1e6d309..00000000 --- a/package/rousette/0036-restconf-introduce-internal-RPC-handling-for-subscri.patch +++ /dev/null @@ -1,5532 +0,0 @@ -From 854e20a328270ff980f0a864f72def1cc84e716f Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 27 Jan 2025 16:53:33 +0100 -Subject: [PATCH 36/44] restconf: introduce internal RPC handling for - subscribed notifications -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -This commit adds support for establishing subscribed notifications [1,2] -in the URI parser and server processing. - -We are going to implement the required RPC handlers for creating -subscribed notifications directly in rousette on the HTTP-level, -i.e., we are not going to pass those RPC calls to sysrepo. - -The Netopeer2 project already implements the RPC callbacks for subscribed -notifications RPCs. Sysrepo can handle multiple RPC callbacks, but only -one of them can return data. The other will be executed but the data will -be ignored [4]. - -It makes no sense to establish subscriptions through NETCONF and then -request the stream via RESTCONF and AFAIK it is even not allowed by RFCs -which states that NETCONF requests are handled by the same NETCONF -session and also, there is no way how to get to those subscriptions via -RESTCONF. RESTCONF provides its own way how to read the streamed -notifications [3,4]. - -And yes, we want both Netopeer2 and Rousette to be able to run at the -same time on the same machine. - -[1] https://datatracker.ietf.org/doc/html/rfc8639 -[2] https://datatracker.ietf.org/doc/html/rfc8650 -[3] https://datatracker.ietf.org/doc/html/rfc8640 -[4] https://github.com/sysrepo/sysrepo/blob/ef93a1253cc97f13671759f6e7790cbf729a5ae9/doc/subs.dox#L78 - -Change-Id: I207e4d5b0d34eea4477c50019e9cab4f8e81e1a6 -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 8 + - src/restconf/Server.cpp | 18 +- - src/restconf/uri.cpp | 15 +- - src/restconf/uri.h | 1 + - tests/restconf-rpc.cpp | 8 + - tests/uri-parser.cpp | 13 +- - yang/iana-if-type@2014-05-08.yang | 1523 +++++++++++++++++ - yang/ietf-interfaces@2018-02-20.yang | 1123 ++++++++++++ - yang/ietf-ip@2018-02-22.yang | 876 ++++++++++ - yang/ietf-network-instance@2019-01-21.yang | 282 +++ - ...f-subscribed-notifications@2019-11-17.yang | 85 + - ...f-subscribed-notifications@2019-09-09.yang | 1350 +++++++++++++++ - 12 files changed, 5298 insertions(+), 4 deletions(-) - create mode 100644 yang/iana-if-type@2014-05-08.yang - create mode 100644 yang/ietf-interfaces@2018-02-20.yang - create mode 100644 yang/ietf-ip@2018-02-22.yang - create mode 100644 yang/ietf-network-instance@2019-01-21.yang - create mode 100644 yang/ietf-restconf-subscribed-notifications@2019-11-17.yang - create mode 100644 yang/ietf-subscribed-notifications@2019-09-09.yang - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index bcabe84..2287447 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -146,6 +146,12 @@ install(FILES - ${CMAKE_SOURCE_DIR}/yang/ietf-restconf@2017-01-26.yang - ${CMAKE_SOURCE_DIR}/yang/ietf-restconf-monitoring@2017-01-26.yang - ${CMAKE_SOURCE_DIR}/yang/ietf-yang-patch@2017-02-22.yang -+ ${CMAKE_SOURCE_DIR}/yang/iana-if-type@2014-05-08.yang -+ ${CMAKE_SOURCE_DIR}/yang/ietf-interfaces@2018-02-20.yang -+ ${CMAKE_SOURCE_DIR}/yang/ietf-ip@2018-02-22.yang -+ ${CMAKE_SOURCE_DIR}/yang/ietf-network-instance@2019-01-21.yang -+ ${CMAKE_SOURCE_DIR}/yang/ietf-subscribed-notifications@2019-09-09.yang -+ ${CMAKE_SOURCE_DIR}/yang/ietf-restconf-subscribed-notifications@2019-11-17.yang - DESTINATION ${CMAKE_INSTALL_PREFIX}/share/yang/modules/rousette) - - include(CTest) -@@ -205,6 +211,8 @@ 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/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 - --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/example-augment.yang -diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp -index c454541..9fd3fa9 100644 ---- a/src/restconf/Server.cpp -+++ b/src/restconf/Server.cpp -@@ -430,6 +430,12 @@ libyang::CreatedNodes createEditForPutAndPatch(libyang::Context& ctx, const std: - return {editNode, replacementNode}; - } - -+std::optional processInternalRPC(sysrepo::Session&, const libyang::DataNode&) -+{ -+ // TODO: Implement internal RPCs -+ throw ErrorResponse(501, "application", "operation-not-supported", "Internal RPCs are not yet supported."); -+} -+ - void processActionOrRPC(std::shared_ptr requestCtx, const std::chrono::milliseconds timeout) - { - requestCtx->sess.switchDatastore(sysrepo::Datastore::Operational); -@@ -460,7 +466,12 @@ void processActionOrRPC(std::shared_ptr requestCtx, const std::c - rpcNode->parseOp(requestCtx->payload, *requestCtx->dataFormat.request, libyang::OperationType::RpcRestconf); - } - -- auto rpcReply = requestCtx->sess.sendRPC(*rpcNode, timeout); -+ std::optional rpcReply; -+ 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); -+ } - - if (!rpcReply || rpcReply->immediateChildren().empty()) { - requestCtx->res.write_head(204, {CORS}); -@@ -833,6 +844,8 @@ 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-restconf-subscribed-notifications", "2019-11-17", {}}, - }) { - if (auto mod = conn.sessionStart().getContext().getModuleImplemented(module)) { - for (const auto& feature : features) { -@@ -1120,7 +1133,8 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: - res.end(); - break; - -- case RestconfRequest::Type::Execute: { -+ case RestconfRequest::Type::Execute: -+ case RestconfRequest::Type::ExecuteInternal: { - auto requestCtx = std::make_shared(req, res, dataFormat, sess, restconfRequest); - - req.on_data([requestCtx, timeout](const uint8_t* data, std::size_t length) { -diff --git a/src/restconf/uri.cpp b/src/restconf/uri.cpp -index e95d70e..956c155 100644 ---- a/src/restconf/uri.cpp -+++ b/src/restconf/uri.cpp -@@ -555,6 +555,19 @@ std::vector asPathSegments(const std::string& uriPath) - return uri->segments; - } - -+/** @brief Checks if the RPC with this schema path should be handled internally by the RESTCONF server */ -+constexpr bool isInternalRPCPath(const std::string& schemaPath) -+{ -+ std::vector arr{ -+ "/ietf-subscribed-notifications:establish-subscription", -+ "/ietf-subscribed-notifications:modify-subscription", -+ "/ietf-subscribed-notifications:delete-subscription", -+ "/ietf-subscribed-notifications:kill-subscription", -+ }; -+ -+ return std::find(arr.begin(), arr.end(), schemaPath) != arr.end(); -+} -+ - /** @brief Parse requested URL as a RESTCONF requested - * - * The URI path (i.e., a resource identifier) will be parsed into an action that is supposed to be performed, -@@ -601,7 +614,7 @@ RestconfRequest asRestconfRequest(const libyang::Context& ctx, const std::string - } else if (httpMethod == "DELETE" && schemaNode) { - type = RestconfRequest::Type::DeleteNode; - } else if (httpMethod == "POST" && schemaNode && (schemaNode->nodeType() == libyang::NodeType::Action || schemaNode->nodeType() == libyang::NodeType::RPC)) { -- type = RestconfRequest::Type::Execute; -+ type = isInternalRPCPath(schemaNode->path()) ? RestconfRequest::Type::ExecuteInternal : RestconfRequest::Type::Execute; - } else if (httpMethod == "POST" && (uri->prefix.resourceType == impl::URIPrefix::Type::BasicRestconfData || uri->prefix.resourceType == impl::URIPrefix::Type::NMDADatastore)) { - type = RestconfRequest::Type::CreateChildren; - } else if (httpMethod == "PATCH") { -diff --git a/src/restconf/uri.h b/src/restconf/uri.h -index 6f1f69f..d9dd29f 100644 ---- a/src/restconf/uri.h -+++ b/src/restconf/uri.h -@@ -180,6 +180,7 @@ struct RestconfRequest { - CreateOrReplaceThisNode, ///< PUT on a data resource - DeleteNode, ///< DELETE on a data resource - Execute, ///< POST on a operation resource (Execute an RPC or an action) -+ ExecuteInternal, ///< POST on a operation resource (RPC or action) that is not sent to sysrepo but handled in roustte directly - CreateChildren, ///< POST on a data resource - YangLibraryVersion, ///< Report ietf-yang-library version - OptionsQuery, ///< Request for allowed HTTP methods for a path -diff --git a/tests/restconf-rpc.cpp b/tests/restconf-rpc.cpp -index d49b121..a3e9309 100644 ---- a/tests/restconf-rpc.cpp -+++ b/tests/restconf-rpc.cpp -@@ -97,6 +97,10 @@ TEST_CASE("invoking actions and rpcs") - "ietf-system:set-current-datetime": [null], - "ietf-system:system-restart": [null], - "ietf-system:system-shutdown": [null], -+ "ietf-subscribed-notifications:establish-subscription": [null], -+ "ietf-subscribed-notifications:modify-subscription": [null], -+ "ietf-subscribed-notifications:delete-subscription": [null], -+ "ietf-subscribed-notifications:kill-subscription": [null], - "example:test-rpc": [null], - "example:test-rpc-no-output": [null], - "example:test-rpc-no-input": [null], -@@ -126,6 +130,10 @@ TEST_CASE("invoking actions and rpcs") - - - -+ -+ -+ -+ - - - -diff --git a/tests/uri-parser.cpp b/tests/uri-parser.cpp -index 53d1fbb..23c2b79 100644 ---- a/tests/uri-parser.cpp -+++ b/tests/uri-parser.cpp -@@ -63,6 +63,8 @@ TEST_CASE("URI path parser") - auto ctx = libyang::Context{std::filesystem::path{CMAKE_CURRENT_SOURCE_DIR} / "tests" / "yang"}; - ctx.loadModule("example", std::nullopt, {"f1"}); - ctx.loadModule("example-augment"); -+ ctx.setSearchDir(std::filesystem::path{CMAKE_CURRENT_SOURCE_DIR} / "yang"); -+ ctx.loadModule("ietf-subscribed-notifications"); - - SECTION("Valid paths") - { -@@ -398,23 +400,32 @@ TEST_CASE("URI path parser") - { - std::string uri; - std::string expectedPath; -+ RestconfRequest::Type expectedRequestType; - - SECTION("RPC") - { - uri = "/restconf/operations/example:test-rpc"; - expectedPath = "/example:test-rpc"; -+ expectedRequestType = RestconfRequest::Type::Execute; - } - SECTION("Action") - { - uri = "/restconf/data/example:tlc/list=hello-world/example-action"; - expectedPath = "/example:tlc/list[name='hello-world']/example-action"; -+ expectedRequestType = RestconfRequest::Type::Execute; -+ } -+ SECTION("Internally handled RPC") -+ { -+ uri = "/restconf/operations/ietf-subscribed-notifications:establish-subscription"; -+ expectedPath = "/ietf-subscribed-notifications:establish-subscription"; -+ expectedRequestType = RestconfRequest::Type::ExecuteInternal; - } - - CAPTURE(uri); - auto [action, datastore, path, queryParams] = rousette::restconf::asRestconfRequest(ctx, "POST", uri); - REQUIRE(path == expectedPath); - REQUIRE(datastore == std::nullopt); -- REQUIRE(action == RestconfRequest::Type::Execute); -+ REQUIRE(action == expectedRequestType); - REQUIRE(queryParams.empty()); - } - -diff --git a/yang/iana-if-type@2014-05-08.yang b/yang/iana-if-type@2014-05-08.yang -new file mode 100644 -index 0000000..81b2175 ---- /dev/null -+++ b/yang/iana-if-type@2014-05-08.yang -@@ -0,0 +1,1523 @@ -+module iana-if-type { -+ namespace "urn:ietf:params:xml:ns:yang:iana-if-type"; -+ prefix ianaift; -+ -+ import ietf-interfaces { -+ prefix if; -+ } -+ -+ organization "IANA"; -+ contact -+ " Internet Assigned Numbers Authority -+ -+ Postal: ICANN -+ 4676 Admiralty Way, Suite 330 -+ Marina del Rey, CA 90292 -+ -+ Tel: +1 310 823 9358 -+ "; -+ description -+ "This YANG module defines YANG identities for IANA-registered -+ interface types. -+ -+ This YANG module is maintained by IANA and reflects the -+ 'ifType definitions' registry. -+ -+ The latest revision of this YANG module can be obtained from -+ the IANA web site. -+ -+ Requests for new values should be made to IANA via -+ email (iana@iana.org). -+ -+ Copyright (c) 2014 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). -+ -+ The initial version of this YANG module is part of RFC 7224; -+ see the RFC itself for full legal notices."; -+ reference -+ "IANA 'ifType definitions' registry. -+ "; -+ -+ revision 2014-05-08 { -+ description -+ "Initial revision."; -+ reference -+ "RFC 7224: IANA Interface Type YANG Module"; -+ } -+ -+ identity iana-interface-type { -+ base if:interface-type; -+ description -+ "This identity is used as a base for all interface types -+ defined in the 'ifType definitions' registry."; -+ } -+ -+ identity other { -+ base iana-interface-type; -+ } -+ identity regular1822 { -+ base iana-interface-type; -+ } -+ identity hdh1822 { -+ base iana-interface-type; -+ } -+ identity ddnX25 { -+ base iana-interface-type; -+ } -+ identity rfc877x25 { -+ base iana-interface-type; -+ reference -+ "RFC 1382 - SNMP MIB Extension for the X.25 Packet Layer"; -+ } -+ identity ethernetCsmacd { -+ base iana-interface-type; -+ description -+ "For all Ethernet-like interfaces, regardless of speed, -+ as per RFC 3635."; -+ reference -+ "RFC 3635 - Definitions of Managed Objects for the -+ Ethernet-like Interface Types"; -+ } -+ identity iso88023Csmacd { -+ base iana-interface-type; -+ status deprecated; -+ description -+ "Deprecated via RFC 3635. -+ Use ethernetCsmacd(6) instead."; -+ reference -+ "RFC 3635 - Definitions of Managed Objects for the -+ Ethernet-like Interface Types"; -+ } -+ identity iso88024TokenBus { -+ base iana-interface-type; -+ } -+ identity iso88025TokenRing { -+ base iana-interface-type; -+ } -+ identity iso88026Man { -+ base iana-interface-type; -+ } -+ identity starLan { -+ base iana-interface-type; -+ status deprecated; -+ description -+ "Deprecated via RFC 3635. -+ Use ethernetCsmacd(6) instead."; -+ reference -+ "RFC 3635 - Definitions of Managed Objects for the -+ Ethernet-like Interface Types"; -+ } -+ identity proteon10Mbit { -+ base iana-interface-type; -+ } -+ identity proteon80Mbit { -+ base iana-interface-type; -+ } -+ identity hyperchannel { -+ base iana-interface-type; -+ } -+ identity fddi { -+ base iana-interface-type; -+ reference -+ "RFC 1512 - FDDI Management Information Base"; -+ } -+ identity lapb { -+ base iana-interface-type; -+ reference -+ "RFC 1381 - SNMP MIB Extension for X.25 LAPB"; -+ } -+ identity sdlc { -+ base iana-interface-type; -+ } -+ identity ds1 { -+ base iana-interface-type; -+ description -+ "DS1-MIB."; -+ reference -+ "RFC 4805 - Definitions of Managed Objects for the -+ DS1, J1, E1, DS2, and E2 Interface Types"; -+ } -+ identity e1 { -+ base iana-interface-type; -+ status obsolete; -+ description -+ "Obsolete; see DS1-MIB."; -+ reference -+ "RFC 4805 - Definitions of Managed Objects for the -+ DS1, J1, E1, DS2, and E2 Interface Types"; -+ } -+ -+ identity basicISDN { -+ base iana-interface-type; -+ description -+ "No longer used. See also RFC 2127."; -+ } -+ identity primaryISDN { -+ base iana-interface-type; -+ description -+ "No longer used. See also RFC 2127."; -+ } -+ identity propPointToPointSerial { -+ base iana-interface-type; -+ description -+ "Proprietary serial."; -+ } -+ identity ppp { -+ base iana-interface-type; -+ } -+ identity softwareLoopback { -+ base iana-interface-type; -+ } -+ identity eon { -+ base iana-interface-type; -+ description -+ "CLNP over IP."; -+ } -+ identity ethernet3Mbit { -+ base iana-interface-type; -+ } -+ identity nsip { -+ base iana-interface-type; -+ description -+ "XNS over IP."; -+ } -+ identity slip { -+ base iana-interface-type; -+ description -+ "Generic SLIP."; -+ } -+ identity ultra { -+ base iana-interface-type; -+ description -+ "Ultra Technologies."; -+ } -+ identity ds3 { -+ base iana-interface-type; -+ description -+ "DS3-MIB."; -+ reference -+ "RFC 3896 - Definitions of Managed Objects for the -+ DS3/E3 Interface Type"; -+ } -+ identity sip { -+ base iana-interface-type; -+ description -+ "SMDS, coffee."; -+ reference -+ "RFC 1694 - Definitions of Managed Objects for SMDS -+ Interfaces using SMIv2"; -+ } -+ identity frameRelay { -+ base iana-interface-type; -+ description -+ "DTE only."; -+ reference -+ "RFC 2115 - Management Information Base for Frame Relay -+ DTEs Using SMIv2"; -+ } -+ identity rs232 { -+ base iana-interface-type; -+ reference -+ "RFC 1659 - Definitions of Managed Objects for RS-232-like -+ Hardware Devices using SMIv2"; -+ } -+ identity para { -+ base iana-interface-type; -+ description -+ "Parallel-port."; -+ reference -+ "RFC 1660 - Definitions of Managed Objects for -+ Parallel-printer-like Hardware Devices using -+ SMIv2"; -+ } -+ identity arcnet { -+ base iana-interface-type; -+ description -+ "ARCnet."; -+ } -+ identity arcnetPlus { -+ base iana-interface-type; -+ description -+ "ARCnet Plus."; -+ } -+ -+ identity atm { -+ base iana-interface-type; -+ description -+ "ATM cells."; -+ } -+ identity miox25 { -+ base iana-interface-type; -+ reference -+ "RFC 1461 - SNMP MIB extension for Multiprotocol -+ Interconnect over X.25"; -+ } -+ identity sonet { -+ base iana-interface-type; -+ description -+ "SONET or SDH."; -+ } -+ identity x25ple { -+ base iana-interface-type; -+ reference -+ "RFC 2127 - ISDN Management Information Base using SMIv2"; -+ } -+ identity iso88022llc { -+ base iana-interface-type; -+ } -+ identity localTalk { -+ base iana-interface-type; -+ } -+ identity smdsDxi { -+ base iana-interface-type; -+ } -+ identity frameRelayService { -+ base iana-interface-type; -+ description -+ "FRNETSERV-MIB."; -+ reference -+ "RFC 2954 - Definitions of Managed Objects for Frame -+ Relay Service"; -+ } -+ identity v35 { -+ base iana-interface-type; -+ } -+ identity hssi { -+ base iana-interface-type; -+ } -+ identity hippi { -+ base iana-interface-type; -+ } -+ -+ identity modem { -+ base iana-interface-type; -+ description -+ "Generic modem."; -+ } -+ identity aal5 { -+ base iana-interface-type; -+ description -+ "AAL5 over ATM."; -+ } -+ identity sonetPath { -+ base iana-interface-type; -+ } -+ identity sonetVT { -+ base iana-interface-type; -+ } -+ identity smdsIcip { -+ base iana-interface-type; -+ description -+ "SMDS InterCarrier Interface."; -+ } -+ identity propVirtual { -+ base iana-interface-type; -+ description -+ "Proprietary virtual/internal."; -+ reference -+ "RFC 2863 - The Interfaces Group MIB"; -+ } -+ identity propMultiplexor { -+ base iana-interface-type; -+ description -+ "Proprietary multiplexing."; -+ reference -+ "RFC 2863 - The Interfaces Group MIB"; -+ } -+ identity ieee80212 { -+ base iana-interface-type; -+ description -+ "100BaseVG."; -+ } -+ identity fibreChannel { -+ base iana-interface-type; -+ description -+ "Fibre Channel."; -+ } -+ -+ identity hippiInterface { -+ base iana-interface-type; -+ description -+ "HIPPI interfaces."; -+ } -+ identity frameRelayInterconnect { -+ base iana-interface-type; -+ status obsolete; -+ description -+ "Obsolete; use either -+ frameRelay(32) or frameRelayService(44)."; -+ } -+ identity aflane8023 { -+ base iana-interface-type; -+ description -+ "ATM Emulated LAN for 802.3."; -+ } -+ identity aflane8025 { -+ base iana-interface-type; -+ description -+ "ATM Emulated LAN for 802.5."; -+ } -+ identity cctEmul { -+ base iana-interface-type; -+ description -+ "ATM Emulated circuit."; -+ } -+ identity fastEther { -+ base iana-interface-type; -+ status deprecated; -+ description -+ "Obsoleted via RFC 3635. -+ ethernetCsmacd(6) should be used instead."; -+ reference -+ "RFC 3635 - Definitions of Managed Objects for the -+ Ethernet-like Interface Types"; -+ } -+ identity isdn { -+ base iana-interface-type; -+ description -+ "ISDN and X.25."; -+ reference -+ "RFC 1356 - Multiprotocol Interconnect on X.25 and ISDN -+ in the Packet Mode"; -+ } -+ -+ identity v11 { -+ base iana-interface-type; -+ description -+ "CCITT V.11/X.21."; -+ } -+ identity v36 { -+ base iana-interface-type; -+ description -+ "CCITT V.36."; -+ } -+ identity g703at64k { -+ base iana-interface-type; -+ description -+ "CCITT G703 at 64Kbps."; -+ } -+ identity g703at2mb { -+ base iana-interface-type; -+ status obsolete; -+ description -+ "Obsolete; see DS1-MIB."; -+ } -+ identity qllc { -+ base iana-interface-type; -+ description -+ "SNA QLLC."; -+ } -+ identity fastEtherFX { -+ base iana-interface-type; -+ status deprecated; -+ description -+ "Obsoleted via RFC 3635. -+ ethernetCsmacd(6) should be used instead."; -+ reference -+ "RFC 3635 - Definitions of Managed Objects for the -+ Ethernet-like Interface Types"; -+ } -+ identity channel { -+ base iana-interface-type; -+ description -+ "Channel."; -+ } -+ identity ieee80211 { -+ base iana-interface-type; -+ description -+ "Radio spread spectrum."; -+ } -+ identity ibm370parChan { -+ base iana-interface-type; -+ description -+ "IBM System 360/370 OEMI Channel."; -+ } -+ identity escon { -+ base iana-interface-type; -+ description -+ "IBM Enterprise Systems Connection."; -+ } -+ identity dlsw { -+ base iana-interface-type; -+ description -+ "Data Link Switching."; -+ } -+ identity isdns { -+ base iana-interface-type; -+ description -+ "ISDN S/T interface."; -+ } -+ identity isdnu { -+ base iana-interface-type; -+ description -+ "ISDN U interface."; -+ } -+ identity lapd { -+ base iana-interface-type; -+ description -+ "Link Access Protocol D."; -+ } -+ identity ipSwitch { -+ base iana-interface-type; -+ description -+ "IP Switching Objects."; -+ } -+ identity rsrb { -+ base iana-interface-type; -+ description -+ "Remote Source Route Bridging."; -+ } -+ identity atmLogical { -+ base iana-interface-type; -+ description -+ "ATM Logical Port."; -+ reference -+ "RFC 3606 - Definitions of Supplemental Managed Objects -+ for ATM Interface"; -+ } -+ identity ds0 { -+ base iana-interface-type; -+ description -+ "Digital Signal Level 0."; -+ reference -+ "RFC 2494 - Definitions of Managed Objects for the DS0 -+ and DS0 Bundle Interface Type"; -+ } -+ identity ds0Bundle { -+ base iana-interface-type; -+ description -+ "Group of ds0s on the same ds1."; -+ reference -+ "RFC 2494 - Definitions of Managed Objects for the DS0 -+ and DS0 Bundle Interface Type"; -+ } -+ identity bsc { -+ base iana-interface-type; -+ description -+ "Bisynchronous Protocol."; -+ } -+ identity async { -+ base iana-interface-type; -+ description -+ "Asynchronous Protocol."; -+ } -+ identity cnr { -+ base iana-interface-type; -+ description -+ "Combat Net Radio."; -+ } -+ identity iso88025Dtr { -+ base iana-interface-type; -+ description -+ "ISO 802.5r DTR."; -+ } -+ identity eplrs { -+ base iana-interface-type; -+ description -+ "Ext Pos Loc Report Sys."; -+ } -+ identity arap { -+ base iana-interface-type; -+ description -+ "Appletalk Remote Access Protocol."; -+ } -+ identity propCnls { -+ base iana-interface-type; -+ description -+ "Proprietary Connectionless Protocol."; -+ } -+ identity hostPad { -+ base iana-interface-type; -+ description -+ "CCITT-ITU X.29 PAD Protocol."; -+ } -+ identity termPad { -+ base iana-interface-type; -+ description -+ "CCITT-ITU X.3 PAD Facility."; -+ } -+ identity frameRelayMPI { -+ base iana-interface-type; -+ description -+ "Multiproto Interconnect over FR."; -+ } -+ identity x213 { -+ base iana-interface-type; -+ description -+ "CCITT-ITU X213."; -+ } -+ identity adsl { -+ base iana-interface-type; -+ description -+ "Asymmetric Digital Subscriber Loop."; -+ } -+ identity radsl { -+ base iana-interface-type; -+ description -+ "Rate-Adapt. Digital Subscriber Loop."; -+ } -+ identity sdsl { -+ base iana-interface-type; -+ description -+ "Symmetric Digital Subscriber Loop."; -+ } -+ identity vdsl { -+ base iana-interface-type; -+ description -+ "Very H-Speed Digital Subscrib. Loop."; -+ } -+ identity iso88025CRFPInt { -+ base iana-interface-type; -+ description -+ "ISO 802.5 CRFP."; -+ } -+ identity myrinet { -+ base iana-interface-type; -+ description -+ "Myricom Myrinet."; -+ } -+ identity voiceEM { -+ base iana-interface-type; -+ description -+ "Voice recEive and transMit."; -+ } -+ identity voiceFXO { -+ base iana-interface-type; -+ description -+ "Voice Foreign Exchange Office."; -+ } -+ identity voiceFXS { -+ base iana-interface-type; -+ description -+ "Voice Foreign Exchange Station."; -+ } -+ identity voiceEncap { -+ base iana-interface-type; -+ description -+ "Voice encapsulation."; -+ } -+ identity voiceOverIp { -+ base iana-interface-type; -+ description -+ "Voice over IP encapsulation."; -+ } -+ identity atmDxi { -+ base iana-interface-type; -+ description -+ "ATM DXI."; -+ } -+ identity atmFuni { -+ base iana-interface-type; -+ description -+ "ATM FUNI."; -+ } -+ identity atmIma { -+ base iana-interface-type; -+ description -+ "ATM IMA."; -+ } -+ identity pppMultilinkBundle { -+ base iana-interface-type; -+ description -+ "PPP Multilink Bundle."; -+ } -+ identity ipOverCdlc { -+ base iana-interface-type; -+ description -+ "IBM ipOverCdlc."; -+ } -+ identity ipOverClaw { -+ base iana-interface-type; -+ description -+ "IBM Common Link Access to Workstn."; -+ } -+ identity stackToStack { -+ base iana-interface-type; -+ description -+ "IBM stackToStack."; -+ } -+ identity virtualIpAddress { -+ base iana-interface-type; -+ description -+ "IBM VIPA."; -+ } -+ identity mpc { -+ base iana-interface-type; -+ description -+ "IBM multi-protocol channel support."; -+ } -+ identity ipOverAtm { -+ base iana-interface-type; -+ description -+ "IBM ipOverAtm."; -+ reference -+ "RFC 2320 - Definitions of Managed Objects for Classical IP -+ and ARP Over ATM Using SMIv2 (IPOA-MIB)"; -+ } -+ identity iso88025Fiber { -+ base iana-interface-type; -+ description -+ "ISO 802.5j Fiber Token Ring."; -+ } -+ identity tdlc { -+ base iana-interface-type; -+ description -+ "IBM twinaxial data link control."; -+ } -+ identity gigabitEthernet { -+ base iana-interface-type; -+ status deprecated; -+ -+ description -+ "Obsoleted via RFC 3635. -+ ethernetCsmacd(6) should be used instead."; -+ reference -+ "RFC 3635 - Definitions of Managed Objects for the -+ Ethernet-like Interface Types"; -+ } -+ identity hdlc { -+ base iana-interface-type; -+ description -+ "HDLC."; -+ } -+ identity lapf { -+ base iana-interface-type; -+ description -+ "LAP F."; -+ } -+ identity v37 { -+ base iana-interface-type; -+ description -+ "V.37."; -+ } -+ identity x25mlp { -+ base iana-interface-type; -+ description -+ "Multi-Link Protocol."; -+ } -+ identity x25huntGroup { -+ base iana-interface-type; -+ description -+ "X25 Hunt Group."; -+ } -+ identity transpHdlc { -+ base iana-interface-type; -+ description -+ "Transp HDLC."; -+ } -+ identity interleave { -+ base iana-interface-type; -+ description -+ "Interleave channel."; -+ } -+ identity fast { -+ base iana-interface-type; -+ description -+ "Fast channel."; -+ } -+ -+ identity ip { -+ base iana-interface-type; -+ description -+ "IP (for APPN HPR in IP networks)."; -+ } -+ identity docsCableMaclayer { -+ base iana-interface-type; -+ description -+ "CATV Mac Layer."; -+ } -+ identity docsCableDownstream { -+ base iana-interface-type; -+ description -+ "CATV Downstream interface."; -+ } -+ identity docsCableUpstream { -+ base iana-interface-type; -+ description -+ "CATV Upstream interface."; -+ } -+ identity a12MppSwitch { -+ base iana-interface-type; -+ description -+ "Avalon Parallel Processor."; -+ } -+ identity tunnel { -+ base iana-interface-type; -+ description -+ "Encapsulation interface."; -+ } -+ identity coffee { -+ base iana-interface-type; -+ description -+ "Coffee pot."; -+ reference -+ "RFC 2325 - Coffee MIB"; -+ } -+ identity ces { -+ base iana-interface-type; -+ description -+ "Circuit Emulation Service."; -+ } -+ identity atmSubInterface { -+ base iana-interface-type; -+ description -+ "ATM Sub Interface."; -+ } -+ -+ identity l2vlan { -+ base iana-interface-type; -+ description -+ "Layer 2 Virtual LAN using 802.1Q."; -+ } -+ identity l3ipvlan { -+ base iana-interface-type; -+ description -+ "Layer 3 Virtual LAN using IP."; -+ } -+ identity l3ipxvlan { -+ base iana-interface-type; -+ description -+ "Layer 3 Virtual LAN using IPX."; -+ } -+ identity digitalPowerline { -+ base iana-interface-type; -+ description -+ "IP over Power Lines."; -+ } -+ identity mediaMailOverIp { -+ base iana-interface-type; -+ description -+ "Multimedia Mail over IP."; -+ } -+ identity dtm { -+ base iana-interface-type; -+ description -+ "Dynamic synchronous Transfer Mode."; -+ } -+ identity dcn { -+ base iana-interface-type; -+ description -+ "Data Communications Network."; -+ } -+ identity ipForward { -+ base iana-interface-type; -+ description -+ "IP Forwarding Interface."; -+ } -+ identity msdsl { -+ base iana-interface-type; -+ description -+ "Multi-rate Symmetric DSL."; -+ } -+ identity ieee1394 { -+ base iana-interface-type; -+ -+ description -+ "IEEE1394 High Performance Serial Bus."; -+ } -+ identity if-gsn { -+ base iana-interface-type; -+ description -+ "HIPPI-6400."; -+ } -+ identity dvbRccMacLayer { -+ base iana-interface-type; -+ description -+ "DVB-RCC MAC Layer."; -+ } -+ identity dvbRccDownstream { -+ base iana-interface-type; -+ description -+ "DVB-RCC Downstream Channel."; -+ } -+ identity dvbRccUpstream { -+ base iana-interface-type; -+ description -+ "DVB-RCC Upstream Channel."; -+ } -+ identity atmVirtual { -+ base iana-interface-type; -+ description -+ "ATM Virtual Interface."; -+ } -+ identity mplsTunnel { -+ base iana-interface-type; -+ description -+ "MPLS Tunnel Virtual Interface."; -+ } -+ identity srp { -+ base iana-interface-type; -+ description -+ "Spatial Reuse Protocol."; -+ } -+ identity voiceOverAtm { -+ base iana-interface-type; -+ description -+ "Voice over ATM."; -+ } -+ identity voiceOverFrameRelay { -+ base iana-interface-type; -+ description -+ "Voice Over Frame Relay."; -+ } -+ identity idsl { -+ base iana-interface-type; -+ description -+ "Digital Subscriber Loop over ISDN."; -+ } -+ identity compositeLink { -+ base iana-interface-type; -+ description -+ "Avici Composite Link Interface."; -+ } -+ identity ss7SigLink { -+ base iana-interface-type; -+ description -+ "SS7 Signaling Link."; -+ } -+ identity propWirelessP2P { -+ base iana-interface-type; -+ description -+ "Prop. P2P wireless interface."; -+ } -+ identity frForward { -+ base iana-interface-type; -+ description -+ "Frame Forward Interface."; -+ } -+ identity rfc1483 { -+ base iana-interface-type; -+ description -+ "Multiprotocol over ATM AAL5."; -+ reference -+ "RFC 1483 - Multiprotocol Encapsulation over ATM -+ Adaptation Layer 5"; -+ } -+ identity usb { -+ base iana-interface-type; -+ description -+ "USB Interface."; -+ } -+ identity ieee8023adLag { -+ base iana-interface-type; -+ description -+ "IEEE 802.3ad Link Aggregate."; -+ } -+ identity bgppolicyaccounting { -+ base iana-interface-type; -+ description -+ "BGP Policy Accounting."; -+ } -+ identity frf16MfrBundle { -+ base iana-interface-type; -+ description -+ "FRF.16 Multilink Frame Relay."; -+ } -+ identity h323Gatekeeper { -+ base iana-interface-type; -+ description -+ "H323 Gatekeeper."; -+ } -+ identity h323Proxy { -+ base iana-interface-type; -+ description -+ "H323 Voice and Video Proxy."; -+ } -+ identity mpls { -+ base iana-interface-type; -+ description -+ "MPLS."; -+ } -+ identity mfSigLink { -+ base iana-interface-type; -+ description -+ "Multi-frequency signaling link."; -+ } -+ identity hdsl2 { -+ base iana-interface-type; -+ description -+ "High Bit-Rate DSL - 2nd generation."; -+ } -+ identity shdsl { -+ base iana-interface-type; -+ description -+ "Multirate HDSL2."; -+ } -+ identity ds1FDL { -+ base iana-interface-type; -+ description -+ "Facility Data Link (4Kbps) on a DS1."; -+ } -+ identity pos { -+ base iana-interface-type; -+ description -+ "Packet over SONET/SDH Interface."; -+ } -+ -+ identity dvbAsiIn { -+ base iana-interface-type; -+ description -+ "DVB-ASI Input."; -+ } -+ identity dvbAsiOut { -+ base iana-interface-type; -+ description -+ "DVB-ASI Output."; -+ } -+ identity plc { -+ base iana-interface-type; -+ description -+ "Power Line Communications."; -+ } -+ identity nfas { -+ base iana-interface-type; -+ description -+ "Non-Facility Associated Signaling."; -+ } -+ identity tr008 { -+ base iana-interface-type; -+ description -+ "TR008."; -+ } -+ identity gr303RDT { -+ base iana-interface-type; -+ description -+ "Remote Digital Terminal."; -+ } -+ identity gr303IDT { -+ base iana-interface-type; -+ description -+ "Integrated Digital Terminal."; -+ } -+ identity isup { -+ base iana-interface-type; -+ description -+ "ISUP."; -+ } -+ identity propDocsWirelessMaclayer { -+ base iana-interface-type; -+ description -+ "Cisco proprietary Maclayer."; -+ } -+ -+ identity propDocsWirelessDownstream { -+ base iana-interface-type; -+ description -+ "Cisco proprietary Downstream."; -+ } -+ identity propDocsWirelessUpstream { -+ base iana-interface-type; -+ description -+ "Cisco proprietary Upstream."; -+ } -+ identity hiperlan2 { -+ base iana-interface-type; -+ description -+ "HIPERLAN Type 2 Radio Interface."; -+ } -+ identity propBWAp2Mp { -+ base iana-interface-type; -+ description -+ "PropBroadbandWirelessAccesspt2Multipt (use of this value -+ for IEEE 802.16 WMAN interfaces as per IEEE Std 802.16f -+ is deprecated, and ieee80216WMAN(237) should be used -+ instead)."; -+ } -+ identity sonetOverheadChannel { -+ base iana-interface-type; -+ description -+ "SONET Overhead Channel."; -+ } -+ identity digitalWrapperOverheadChannel { -+ base iana-interface-type; -+ description -+ "Digital Wrapper."; -+ } -+ identity aal2 { -+ base iana-interface-type; -+ description -+ "ATM adaptation layer 2."; -+ } -+ identity radioMAC { -+ base iana-interface-type; -+ description -+ "MAC layer over radio links."; -+ } -+ identity atmRadio { -+ base iana-interface-type; -+ description -+ "ATM over radio links."; -+ } -+ identity imt { -+ base iana-interface-type; -+ description -+ "Inter-Machine Trunks."; -+ } -+ identity mvl { -+ base iana-interface-type; -+ description -+ "Multiple Virtual Lines DSL."; -+ } -+ identity reachDSL { -+ base iana-interface-type; -+ description -+ "Long Reach DSL."; -+ } -+ identity frDlciEndPt { -+ base iana-interface-type; -+ description -+ "Frame Relay DLCI End Point."; -+ } -+ identity atmVciEndPt { -+ base iana-interface-type; -+ description -+ "ATM VCI End Point."; -+ } -+ identity opticalChannel { -+ base iana-interface-type; -+ description -+ "Optical Channel."; -+ } -+ identity opticalTransport { -+ base iana-interface-type; -+ description -+ "Optical Transport."; -+ } -+ identity propAtm { -+ base iana-interface-type; -+ description -+ "Proprietary ATM."; -+ } -+ identity voiceOverCable { -+ base iana-interface-type; -+ description -+ "Voice Over Cable Interface."; -+ } -+ -+ identity infiniband { -+ base iana-interface-type; -+ description -+ "Infiniband."; -+ } -+ identity teLink { -+ base iana-interface-type; -+ description -+ "TE Link."; -+ } -+ identity q2931 { -+ base iana-interface-type; -+ description -+ "Q.2931."; -+ } -+ identity virtualTg { -+ base iana-interface-type; -+ description -+ "Virtual Trunk Group."; -+ } -+ identity sipTg { -+ base iana-interface-type; -+ description -+ "SIP Trunk Group."; -+ } -+ identity sipSig { -+ base iana-interface-type; -+ description -+ "SIP Signaling."; -+ } -+ identity docsCableUpstreamChannel { -+ base iana-interface-type; -+ description -+ "CATV Upstream Channel."; -+ } -+ identity econet { -+ base iana-interface-type; -+ description -+ "Acorn Econet."; -+ } -+ identity pon155 { -+ base iana-interface-type; -+ description -+ "FSAN 155Mb Symetrical PON interface."; -+ } -+ -+ identity pon622 { -+ base iana-interface-type; -+ description -+ "FSAN 622Mb Symetrical PON interface."; -+ } -+ identity bridge { -+ base iana-interface-type; -+ description -+ "Transparent bridge interface."; -+ } -+ identity linegroup { -+ base iana-interface-type; -+ description -+ "Interface common to multiple lines."; -+ } -+ identity voiceEMFGD { -+ base iana-interface-type; -+ description -+ "Voice E&M Feature Group D."; -+ } -+ identity voiceFGDEANA { -+ base iana-interface-type; -+ description -+ "Voice FGD Exchange Access North American."; -+ } -+ identity voiceDID { -+ base iana-interface-type; -+ description -+ "Voice Direct Inward Dialing."; -+ } -+ identity mpegTransport { -+ base iana-interface-type; -+ description -+ "MPEG transport interface."; -+ } -+ identity sixToFour { -+ base iana-interface-type; -+ status deprecated; -+ description -+ "6to4 interface (DEPRECATED)."; -+ reference -+ "RFC 4087 - IP Tunnel MIB"; -+ } -+ identity gtp { -+ base iana-interface-type; -+ description -+ "GTP (GPRS Tunneling Protocol)."; -+ } -+ identity pdnEtherLoop1 { -+ base iana-interface-type; -+ description -+ "Paradyne EtherLoop 1."; -+ } -+ identity pdnEtherLoop2 { -+ base iana-interface-type; -+ description -+ "Paradyne EtherLoop 2."; -+ } -+ identity opticalChannelGroup { -+ base iana-interface-type; -+ description -+ "Optical Channel Group."; -+ } -+ identity homepna { -+ base iana-interface-type; -+ description -+ "HomePNA ITU-T G.989."; -+ } -+ identity gfp { -+ base iana-interface-type; -+ description -+ "Generic Framing Procedure (GFP)."; -+ } -+ identity ciscoISLvlan { -+ base iana-interface-type; -+ description -+ "Layer 2 Virtual LAN using Cisco ISL."; -+ } -+ identity actelisMetaLOOP { -+ base iana-interface-type; -+ description -+ "Acteleis proprietary MetaLOOP High Speed Link."; -+ } -+ identity fcipLink { -+ base iana-interface-type; -+ description -+ "FCIP Link."; -+ } -+ identity rpr { -+ base iana-interface-type; -+ description -+ "Resilient Packet Ring Interface Type."; -+ } -+ -+ identity qam { -+ base iana-interface-type; -+ description -+ "RF Qam Interface."; -+ } -+ identity lmp { -+ base iana-interface-type; -+ description -+ "Link Management Protocol."; -+ reference -+ "RFC 4327 - Link Management Protocol (LMP) Management -+ Information Base (MIB)"; -+ } -+ identity cblVectaStar { -+ base iana-interface-type; -+ description -+ "Cambridge Broadband Networks Limited VectaStar."; -+ } -+ identity docsCableMCmtsDownstream { -+ base iana-interface-type; -+ description -+ "CATV Modular CMTS Downstream Interface."; -+ } -+ identity adsl2 { -+ base iana-interface-type; -+ status deprecated; -+ description -+ "Asymmetric Digital Subscriber Loop Version 2 -+ (DEPRECATED/OBSOLETED - please use adsl2plus(238) -+ instead)."; -+ reference -+ "RFC 4706 - Definitions of Managed Objects for Asymmetric -+ Digital Subscriber Line 2 (ADSL2)"; -+ } -+ identity macSecControlledIF { -+ base iana-interface-type; -+ description -+ "MACSecControlled."; -+ } -+ identity macSecUncontrolledIF { -+ base iana-interface-type; -+ description -+ "MACSecUncontrolled."; -+ } -+ identity aviciOpticalEther { -+ base iana-interface-type; -+ description -+ "Avici Optical Ethernet Aggregate."; -+ } -+ identity atmbond { -+ base iana-interface-type; -+ description -+ "atmbond."; -+ } -+ identity voiceFGDOS { -+ base iana-interface-type; -+ description -+ "Voice FGD Operator Services."; -+ } -+ identity mocaVersion1 { -+ base iana-interface-type; -+ description -+ "MultiMedia over Coax Alliance (MoCA) Interface -+ as documented in information provided privately to IANA."; -+ } -+ identity ieee80216WMAN { -+ base iana-interface-type; -+ description -+ "IEEE 802.16 WMAN interface."; -+ } -+ identity adsl2plus { -+ base iana-interface-type; -+ description -+ "Asymmetric Digital Subscriber Loop Version 2 - -+ Version 2 Plus and all variants."; -+ } -+ identity dvbRcsMacLayer { -+ base iana-interface-type; -+ description -+ "DVB-RCS MAC Layer."; -+ reference -+ "RFC 5728 - The SatLabs Group DVB-RCS MIB"; -+ } -+ identity dvbTdm { -+ base iana-interface-type; -+ description -+ "DVB Satellite TDM."; -+ reference -+ "RFC 5728 - The SatLabs Group DVB-RCS MIB"; -+ } -+ identity dvbRcsTdma { -+ base iana-interface-type; -+ description -+ "DVB-RCS TDMA."; -+ reference -+ "RFC 5728 - The SatLabs Group DVB-RCS MIB"; -+ } -+ identity x86Laps { -+ base iana-interface-type; -+ description -+ "LAPS based on ITU-T X.86/Y.1323."; -+ } -+ identity wwanPP { -+ base iana-interface-type; -+ description -+ "3GPP WWAN."; -+ } -+ identity wwanPP2 { -+ base iana-interface-type; -+ description -+ "3GPP2 WWAN."; -+ } -+ identity voiceEBS { -+ base iana-interface-type; -+ description -+ "Voice P-phone EBS physical interface."; -+ } -+ identity ifPwType { -+ base iana-interface-type; -+ description -+ "Pseudowire interface type."; -+ reference -+ "RFC 5601 - Pseudowire (PW) Management Information Base (MIB)"; -+ } -+ identity ilan { -+ base iana-interface-type; -+ description -+ "Internal LAN on a bridge per IEEE 802.1ap."; -+ } -+ identity pip { -+ base iana-interface-type; -+ description -+ "Provider Instance Port on a bridge per IEEE 802.1ah PBB."; -+ } -+ identity aluELP { -+ base iana-interface-type; -+ description -+ "Alcatel-Lucent Ethernet Link Protection."; -+ } -+ identity gpon { -+ base iana-interface-type; -+ description -+ "Gigabit-capable passive optical networks (G-PON) as per -+ ITU-T G.948."; -+ } -+ identity vdsl2 { -+ base iana-interface-type; -+ description -+ "Very high speed digital subscriber line Version 2 -+ (as per ITU-T Recommendation G.993.2)."; -+ reference -+ "RFC 5650 - Definitions of Managed Objects for Very High -+ Speed Digital Subscriber Line 2 (VDSL2)"; -+ } -+ identity capwapDot11Profile { -+ base iana-interface-type; -+ description -+ "WLAN Profile Interface."; -+ reference -+ "RFC 5834 - Control and Provisioning of Wireless Access -+ Points (CAPWAP) Protocol Binding MIB for -+ IEEE 802.11"; -+ } -+ identity capwapDot11Bss { -+ base iana-interface-type; -+ description -+ "WLAN BSS Interface."; -+ reference -+ "RFC 5834 - Control and Provisioning of Wireless Access -+ Points (CAPWAP) Protocol Binding MIB for -+ IEEE 802.11"; -+ } -+ identity capwapWtpVirtualRadio { -+ base iana-interface-type; -+ description -+ "WTP Virtual Radio Interface."; -+ reference -+ "RFC 5833 - Control and Provisioning of Wireless Access -+ Points (CAPWAP) Protocol Base MIB"; -+ } -+ identity bits { -+ base iana-interface-type; -+ description -+ "bitsport."; -+ } -+ identity docsCableUpstreamRfPort { -+ base iana-interface-type; -+ description -+ "DOCSIS CATV Upstream RF Port."; -+ } -+ -+ identity cableDownstreamRfPort { -+ base iana-interface-type; -+ description -+ "CATV downstream RF Port."; -+ } -+ identity vmwareVirtualNic { -+ base iana-interface-type; -+ description -+ "VMware Virtual Network Interface."; -+ } -+ identity ieee802154 { -+ base iana-interface-type; -+ description -+ "IEEE 802.15.4 WPAN interface."; -+ reference -+ "IEEE 802.15.4-2006"; -+ } -+ identity otnOdu { -+ base iana-interface-type; -+ description -+ "OTN Optical Data Unit."; -+ } -+ identity otnOtu { -+ base iana-interface-type; -+ description -+ "OTN Optical channel Transport Unit."; -+ } -+ identity ifVfiType { -+ base iana-interface-type; -+ description -+ "VPLS Forwarding Instance Interface Type."; -+ } -+ identity g9981 { -+ base iana-interface-type; -+ description -+ "G.998.1 bonded interface."; -+ } -+ identity g9982 { -+ base iana-interface-type; -+ description -+ "G.998.2 bonded interface."; -+ } -+ identity g9983 { -+ base iana-interface-type; -+ description -+ "G.998.3 bonded interface."; -+ } -+ -+ identity aluEpon { -+ base iana-interface-type; -+ description -+ "Ethernet Passive Optical Networks (E-PON)."; -+ } -+ identity aluEponOnu { -+ base iana-interface-type; -+ description -+ "EPON Optical Network Unit."; -+ } -+ identity aluEponPhysicalUni { -+ base iana-interface-type; -+ description -+ "EPON physical User to Network interface."; -+ } -+ identity aluEponLogicalLink { -+ base iana-interface-type; -+ description -+ "The emulation of a point-to-point link over the EPON -+ layer."; -+ } -+ identity aluGponOnu { -+ base iana-interface-type; -+ description -+ "GPON Optical Network Unit."; -+ reference -+ "ITU-T G.984.2"; -+ } -+ identity aluGponPhysicalUni { -+ base iana-interface-type; -+ description -+ "GPON physical User to Network interface."; -+ reference -+ "ITU-T G.984.2"; -+ } -+ identity vmwareNicTeam { -+ base iana-interface-type; -+ description -+ "VMware NIC Team."; -+ } -+} -diff --git a/yang/ietf-interfaces@2018-02-20.yang b/yang/ietf-interfaces@2018-02-20.yang -new file mode 100644 -index 0000000..f66c205 ---- /dev/null -+++ b/yang/ietf-interfaces@2018-02-20.yang -@@ -0,0 +1,1123 @@ -+module ietf-interfaces { -+ yang-version 1.1; -+ namespace "urn:ietf:params:xml:ns:yang:ietf-interfaces"; -+ prefix if; -+ -+ import ietf-yang-types { -+ prefix yang; -+ } -+ -+ organization -+ "IETF NETMOD (Network Modeling) Working Group"; -+ -+ contact -+ "WG Web: -+ WG List: -+ -+ Editor: Martin Bjorklund -+ "; -+ -+ description -+ "This module contains a collection of YANG definitions for -+ managing network interfaces. -+ -+ Copyright (c) 2018 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 8343; see -+ the RFC itself for full legal notices."; -+ -+ revision 2018-02-20 { -+ description -+ "Updated to support NMDA."; -+ reference -+ "RFC 8343: A YANG Data Model for Interface Management"; -+ } -+ -+ revision 2014-05-08 { -+ description -+ "Initial revision."; -+ reference -+ "RFC 7223: A YANG Data Model for Interface Management"; -+ } -+ -+ /* -+ * Typedefs -+ */ -+ -+ typedef interface-ref { -+ type leafref { -+ path "/if:interfaces/if:interface/if:name"; -+ } -+ description -+ "This type is used by data models that need to reference -+ interfaces."; -+ } -+ -+ /* -+ * Identities -+ */ -+ -+ identity interface-type { -+ description -+ "Base identity from which specific interface types are -+ derived."; -+ } -+ -+ /* -+ * Features -+ */ -+ -+ feature arbitrary-names { -+ description -+ "This feature indicates that the device allows user-controlled -+ interfaces to be named arbitrarily."; -+ } -+ feature pre-provisioning { -+ description -+ "This feature indicates that the device supports -+ pre-provisioning of interface configuration, i.e., it is -+ possible to configure an interface whose physical interface -+ hardware is not present on the device."; -+ } -+ feature if-mib { -+ description -+ "This feature indicates that the device implements -+ the IF-MIB."; -+ reference -+ "RFC 2863: The Interfaces Group MIB"; -+ } -+ -+ /* -+ * Data nodes -+ */ -+ -+ container interfaces { -+ description -+ "Interface parameters."; -+ -+ list interface { -+ key "name"; -+ -+ description -+ "The list of interfaces on the device. -+ -+ The status of an interface is available in this list in the -+ operational state. If the configuration of a -+ system-controlled interface cannot be used by the system -+ (e.g., the interface hardware present does not match the -+ interface type), then the configuration is not applied to -+ the system-controlled interface shown in the operational -+ state. If the configuration of a user-controlled interface -+ cannot be used by the system, the configured interface is -+ not instantiated in the operational state. -+ -+ System-controlled interfaces created by the system are -+ always present in this list in the operational state, -+ whether or not they are configured."; -+ -+ leaf name { -+ type string; -+ description -+ "The name of the interface. -+ -+ A device MAY restrict the allowed values for this leaf, -+ possibly depending on the type of the interface. -+ For system-controlled interfaces, this leaf is the -+ device-specific name of the interface. -+ -+ If a client tries to create configuration for a -+ system-controlled interface that is not present in the -+ operational state, the server MAY reject the request if -+ the implementation does not support pre-provisioning of -+ interfaces or if the name refers to an interface that can -+ never exist in the system. A Network Configuration -+ Protocol (NETCONF) server MUST reply with an rpc-error -+ with the error-tag 'invalid-value' in this case. -+ -+ If the device supports pre-provisioning of interface -+ configuration, the 'pre-provisioning' feature is -+ advertised. -+ -+ If the device allows arbitrarily named user-controlled -+ interfaces, the 'arbitrary-names' feature is advertised. -+ -+ When a configured user-controlled interface is created by -+ the system, it is instantiated with the same name in the -+ operational state. -+ -+ A server implementation MAY map this leaf to the ifName -+ MIB object. Such an implementation needs to use some -+ mechanism to handle the differences in size and characters -+ allowed between this leaf and ifName. The definition of -+ such a mechanism is outside the scope of this document."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifName"; -+ } -+ -+ leaf description { -+ type string; -+ description -+ "A textual description of the interface. -+ -+ A server implementation MAY map this leaf to the ifAlias -+ MIB object. Such an implementation needs to use some -+ mechanism to handle the differences in size and characters -+ allowed between this leaf and ifAlias. The definition of -+ such a mechanism is outside the scope of this document. -+ -+ Since ifAlias is defined to be stored in non-volatile -+ storage, the MIB implementation MUST map ifAlias to the -+ value of 'description' in the persistently stored -+ configuration."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifAlias"; -+ } -+ -+ leaf type { -+ type identityref { -+ base interface-type; -+ } -+ mandatory true; -+ description -+ "The type of the interface. -+ -+ When an interface entry is created, a server MAY -+ initialize the type leaf with a valid value, e.g., if it -+ is possible to derive the type from the name of the -+ interface. -+ -+ If a client tries to set the type of an interface to a -+ value that can never be used by the system, e.g., if the -+ type is not supported or if the type does not match the -+ name of the interface, the server MUST reject the request. -+ A NETCONF server MUST reply with an rpc-error with the -+ error-tag 'invalid-value' in this case."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifType"; -+ } -+ -+ leaf enabled { -+ type boolean; -+ default "true"; -+ description -+ "This leaf contains the configured, desired state of the -+ interface. -+ -+ Systems that implement the IF-MIB use the value of this -+ leaf in the intended configuration to set -+ IF-MIB.ifAdminStatus to 'up' or 'down' after an ifEntry -+ has been initialized, as described in RFC 2863. -+ -+ Changes in this leaf in the intended configuration are -+ reflected in ifAdminStatus."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifAdminStatus"; -+ } -+ -+ leaf link-up-down-trap-enable { -+ if-feature if-mib; -+ type enumeration { -+ enum enabled { -+ value 1; -+ description -+ "The device will generate linkUp/linkDown SNMP -+ notifications for this interface."; -+ } -+ enum disabled { -+ value 2; -+ description -+ "The device will not generate linkUp/linkDown SNMP -+ notifications for this interface."; -+ } -+ } -+ description -+ "Controls whether linkUp/linkDown SNMP notifications -+ should be generated for this interface. -+ -+ If this node is not configured, the value 'enabled' is -+ operationally used by the server for interfaces that do -+ not operate on top of any other interface (i.e., there are -+ no 'lower-layer-if' entries), and 'disabled' otherwise."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifLinkUpDownTrapEnable"; -+ } -+ -+ leaf admin-status { -+ if-feature if-mib; -+ type enumeration { -+ enum up { -+ value 1; -+ description -+ "Ready to pass packets."; -+ } -+ enum down { -+ value 2; -+ description -+ "Not ready to pass packets and not in some test mode."; -+ } -+ enum testing { -+ value 3; -+ description -+ "In some test mode."; -+ } -+ } -+ config false; -+ mandatory true; -+ description -+ "The desired state of the interface. -+ -+ This leaf has the same read semantics as ifAdminStatus."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifAdminStatus"; -+ } -+ -+ leaf oper-status { -+ type enumeration { -+ enum up { -+ value 1; -+ description -+ "Ready to pass packets."; -+ } -+ enum down { -+ value 2; -+ -+ description -+ "The interface does not pass any packets."; -+ } -+ enum testing { -+ value 3; -+ description -+ "In some test mode. No operational packets can -+ be passed."; -+ } -+ enum unknown { -+ value 4; -+ description -+ "Status cannot be determined for some reason."; -+ } -+ enum dormant { -+ value 5; -+ description -+ "Waiting for some external event."; -+ } -+ enum not-present { -+ value 6; -+ description -+ "Some component (typically hardware) is missing."; -+ } -+ enum lower-layer-down { -+ value 7; -+ description -+ "Down due to state of lower-layer interface(s)."; -+ } -+ } -+ config false; -+ mandatory true; -+ description -+ "The current operational state of the interface. -+ -+ This leaf has the same semantics as ifOperStatus."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifOperStatus"; -+ } -+ -+ leaf last-change { -+ type yang:date-and-time; -+ config false; -+ description -+ "The time the interface entered its current operational -+ state. If the current state was entered prior to the -+ last re-initialization of the local network management -+ subsystem, then this node is not present."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifLastChange"; -+ } -+ -+ leaf if-index { -+ if-feature if-mib; -+ type int32 { -+ range "1..2147483647"; -+ } -+ config false; -+ mandatory true; -+ description -+ "The ifIndex value for the ifEntry represented by this -+ interface."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifIndex"; -+ } -+ -+ leaf phys-address { -+ type yang:phys-address; -+ config false; -+ description -+ "The interface's address at its protocol sub-layer. For -+ example, for an 802.x interface, this object normally -+ contains a Media Access Control (MAC) address. The -+ interface's media-specific modules must define the bit -+ and byte ordering and the format of the value of this -+ object. For interfaces that do not have such an address -+ (e.g., a serial line), this node is not present."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifPhysAddress"; -+ } -+ -+ leaf-list higher-layer-if { -+ type interface-ref; -+ config false; -+ description -+ "A list of references to interfaces layered on top of this -+ interface."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifStackTable"; -+ } -+ -+ leaf-list lower-layer-if { -+ type interface-ref; -+ config false; -+ -+ description -+ "A list of references to interfaces layered underneath this -+ interface."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifStackTable"; -+ } -+ -+ leaf speed { -+ type yang:gauge64; -+ units "bits/second"; -+ config false; -+ description -+ "An estimate of the interface's current bandwidth in bits -+ per second. For interfaces that do not vary in -+ bandwidth or for those where no accurate estimation can -+ be made, this node should contain the nominal bandwidth. -+ For interfaces that have no concept of bandwidth, this -+ node is not present."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifSpeed, ifHighSpeed"; -+ } -+ -+ container statistics { -+ config false; -+ description -+ "A collection of interface-related statistics objects."; -+ -+ leaf discontinuity-time { -+ type yang:date-and-time; -+ mandatory true; -+ description -+ "The time on the most recent occasion at which any one or -+ more of this interface's counters suffered a -+ discontinuity. If no such discontinuities have occurred -+ since the last re-initialization of the local management -+ subsystem, then this node contains the time the local -+ management subsystem re-initialized itself."; -+ } -+ -+ leaf in-octets { -+ type yang:counter64; -+ description -+ "The total number of octets received on the interface, -+ including framing characters. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCInOctets"; -+ } -+ -+ leaf in-unicast-pkts { -+ type yang:counter64; -+ description -+ "The number of packets, delivered by this sub-layer to a -+ higher (sub-)layer, that were not addressed to a -+ multicast or broadcast address at this sub-layer. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCInUcastPkts"; -+ } -+ -+ leaf in-broadcast-pkts { -+ type yang:counter64; -+ description -+ "The number of packets, delivered by this sub-layer to a -+ higher (sub-)layer, that were addressed to a broadcast -+ address at this sub-layer. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCInBroadcastPkts"; -+ } -+ -+ leaf in-multicast-pkts { -+ type yang:counter64; -+ description -+ "The number of packets, delivered by this sub-layer to a -+ higher (sub-)layer, that were addressed to a multicast -+ address at this sub-layer. For a MAC-layer protocol, -+ this includes both Group and Functional addresses. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCInMulticastPkts"; -+ } -+ -+ leaf in-discards { -+ type yang:counter32; -+ description -+ "The number of inbound packets that were chosen to be -+ discarded even though no errors had been detected to -+ prevent their being deliverable to a higher-layer -+ protocol. One possible reason for discarding such a -+ packet could be to free up buffer space. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifInDiscards"; -+ } -+ -+ leaf in-errors { -+ type yang:counter32; -+ description -+ "For packet-oriented interfaces, the number of inbound -+ packets that contained errors preventing them from being -+ deliverable to a higher-layer protocol. For character- -+ oriented or fixed-length interfaces, the number of -+ inbound transmission units that contained errors -+ preventing them from being deliverable to a higher-layer -+ protocol. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifInErrors"; -+ } -+ -+ leaf in-unknown-protos { -+ type yang:counter32; -+ -+ description -+ "For packet-oriented interfaces, the number of packets -+ received via the interface that were discarded because -+ of an unknown or unsupported protocol. For -+ character-oriented or fixed-length interfaces that -+ support protocol multiplexing, the number of -+ transmission units received via the interface that were -+ discarded because of an unknown or unsupported protocol. -+ For any interface that does not support protocol -+ multiplexing, this counter is not present. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifInUnknownProtos"; -+ } -+ -+ leaf out-octets { -+ type yang:counter64; -+ description -+ "The total number of octets transmitted out of the -+ interface, including framing characters. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCOutOctets"; -+ } -+ -+ leaf out-unicast-pkts { -+ type yang:counter64; -+ description -+ "The total number of packets that higher-level protocols -+ requested be transmitted and that were not addressed -+ to a multicast or broadcast address at this sub-layer, -+ including those that were discarded or not sent. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCOutUcastPkts"; -+ } -+ -+ leaf out-broadcast-pkts { -+ type yang:counter64; -+ description -+ "The total number of packets that higher-level protocols -+ requested be transmitted and that were addressed to a -+ broadcast address at this sub-layer, including those -+ that were discarded or not sent. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCOutBroadcastPkts"; -+ } -+ -+ leaf out-multicast-pkts { -+ type yang:counter64; -+ description -+ "The total number of packets that higher-level protocols -+ requested be transmitted and that were addressed to a -+ multicast address at this sub-layer, including those -+ that were discarded or not sent. For a MAC-layer -+ protocol, this includes both Group and Functional -+ addresses. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCOutMulticastPkts"; -+ } -+ -+ leaf out-discards { -+ type yang:counter32; -+ description -+ "The number of outbound packets that were chosen to be -+ discarded even though no errors had been detected to -+ prevent their being transmitted. One possible reason -+ for discarding such a packet could be to free up buffer -+ space. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifOutDiscards"; -+ } -+ -+ leaf out-errors { -+ type yang:counter32; -+ description -+ "For packet-oriented interfaces, the number of outbound -+ packets that could not be transmitted because of errors. -+ For character-oriented or fixed-length interfaces, the -+ number of outbound transmission units that could not be -+ transmitted because of errors. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifOutErrors"; -+ } -+ } -+ -+ } -+ } -+ -+ /* -+ * Legacy typedefs -+ */ -+ -+ typedef interface-state-ref { -+ type leafref { -+ path "/if:interfaces-state/if:interface/if:name"; -+ } -+ status deprecated; -+ description -+ "This type is used by data models that need to reference -+ the operationally present interfaces."; -+ } -+ -+ /* -+ * Legacy operational state data nodes -+ */ -+ -+ container interfaces-state { -+ config false; -+ status deprecated; -+ description -+ "Data nodes for the operational state of interfaces."; -+ -+ list interface { -+ key "name"; -+ status deprecated; -+ -+ description -+ "The list of interfaces on the device. -+ -+ System-controlled interfaces created by the system are -+ always present in this list, whether or not they are -+ configured."; -+ -+ leaf name { -+ type string; -+ status deprecated; -+ description -+ "The name of the interface. -+ -+ A server implementation MAY map this leaf to the ifName -+ MIB object. Such an implementation needs to use some -+ mechanism to handle the differences in size and characters -+ allowed between this leaf and ifName. The definition of -+ such a mechanism is outside the scope of this document."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifName"; -+ } -+ -+ leaf type { -+ type identityref { -+ base interface-type; -+ } -+ mandatory true; -+ status deprecated; -+ description -+ "The type of the interface."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifType"; -+ } -+ -+ leaf admin-status { -+ if-feature if-mib; -+ type enumeration { -+ enum up { -+ value 1; -+ description -+ "Ready to pass packets."; -+ } -+ enum down { -+ value 2; -+ description -+ "Not ready to pass packets and not in some test mode."; -+ } -+ enum testing { -+ value 3; -+ description -+ "In some test mode."; -+ } -+ } -+ mandatory true; -+ status deprecated; -+ description -+ "The desired state of the interface. -+ -+ This leaf has the same read semantics as ifAdminStatus."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifAdminStatus"; -+ } -+ -+ leaf oper-status { -+ type enumeration { -+ enum up { -+ value 1; -+ description -+ "Ready to pass packets."; -+ } -+ enum down { -+ value 2; -+ description -+ "The interface does not pass any packets."; -+ } -+ enum testing { -+ value 3; -+ description -+ "In some test mode. No operational packets can -+ be passed."; -+ } -+ enum unknown { -+ value 4; -+ description -+ "Status cannot be determined for some reason."; -+ } -+ enum dormant { -+ value 5; -+ description -+ "Waiting for some external event."; -+ } -+ enum not-present { -+ value 6; -+ description -+ "Some component (typically hardware) is missing."; -+ } -+ enum lower-layer-down { -+ value 7; -+ description -+ "Down due to state of lower-layer interface(s)."; -+ } -+ } -+ mandatory true; -+ status deprecated; -+ description -+ "The current operational state of the interface. -+ -+ This leaf has the same semantics as ifOperStatus."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifOperStatus"; -+ } -+ -+ leaf last-change { -+ type yang:date-and-time; -+ status deprecated; -+ description -+ "The time the interface entered its current operational -+ state. If the current state was entered prior to the -+ last re-initialization of the local network management -+ subsystem, then this node is not present."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifLastChange"; -+ } -+ -+ leaf if-index { -+ if-feature if-mib; -+ type int32 { -+ range "1..2147483647"; -+ } -+ mandatory true; -+ status deprecated; -+ description -+ "The ifIndex value for the ifEntry represented by this -+ interface."; -+ -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifIndex"; -+ } -+ -+ leaf phys-address { -+ type yang:phys-address; -+ status deprecated; -+ description -+ "The interface's address at its protocol sub-layer. For -+ example, for an 802.x interface, this object normally -+ contains a Media Access Control (MAC) address. The -+ interface's media-specific modules must define the bit -+ and byte ordering and the format of the value of this -+ object. For interfaces that do not have such an address -+ (e.g., a serial line), this node is not present."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifPhysAddress"; -+ } -+ -+ leaf-list higher-layer-if { -+ type interface-state-ref; -+ status deprecated; -+ description -+ "A list of references to interfaces layered on top of this -+ interface."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifStackTable"; -+ } -+ -+ leaf-list lower-layer-if { -+ type interface-state-ref; -+ status deprecated; -+ description -+ "A list of references to interfaces layered underneath this -+ interface."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifStackTable"; -+ } -+ -+ leaf speed { -+ type yang:gauge64; -+ units "bits/second"; -+ status deprecated; -+ description -+ "An estimate of the interface's current bandwidth in bits -+ per second. For interfaces that do not vary in -+ bandwidth or for those where no accurate estimation can -+ -+ be made, this node should contain the nominal bandwidth. -+ For interfaces that have no concept of bandwidth, this -+ node is not present."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifSpeed, ifHighSpeed"; -+ } -+ -+ container statistics { -+ status deprecated; -+ description -+ "A collection of interface-related statistics objects."; -+ -+ leaf discontinuity-time { -+ type yang:date-and-time; -+ mandatory true; -+ status deprecated; -+ description -+ "The time on the most recent occasion at which any one or -+ more of this interface's counters suffered a -+ discontinuity. If no such discontinuities have occurred -+ since the last re-initialization of the local management -+ subsystem, then this node contains the time the local -+ management subsystem re-initialized itself."; -+ } -+ -+ leaf in-octets { -+ type yang:counter64; -+ status deprecated; -+ description -+ "The total number of octets received on the interface, -+ including framing characters. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCInOctets"; -+ } -+ -+ leaf in-unicast-pkts { -+ type yang:counter64; -+ status deprecated; -+ description -+ "The number of packets, delivered by this sub-layer to a -+ higher (sub-)layer, that were not addressed to a -+ multicast or broadcast address at this sub-layer. -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCInUcastPkts"; -+ } -+ -+ leaf in-broadcast-pkts { -+ type yang:counter64; -+ status deprecated; -+ description -+ "The number of packets, delivered by this sub-layer to a -+ higher (sub-)layer, that were addressed to a broadcast -+ address at this sub-layer. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCInBroadcastPkts"; -+ } -+ -+ leaf in-multicast-pkts { -+ type yang:counter64; -+ status deprecated; -+ description -+ "The number of packets, delivered by this sub-layer to a -+ higher (sub-)layer, that were addressed to a multicast -+ address at this sub-layer. For a MAC-layer protocol, -+ this includes both Group and Functional addresses. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCInMulticastPkts"; -+ } -+ -+ leaf in-discards { -+ type yang:counter32; -+ status deprecated; -+ -+ description -+ "The number of inbound packets that were chosen to be -+ discarded even though no errors had been detected to -+ prevent their being deliverable to a higher-layer -+ protocol. One possible reason for discarding such a -+ packet could be to free up buffer space. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifInDiscards"; -+ } -+ -+ leaf in-errors { -+ type yang:counter32; -+ status deprecated; -+ description -+ "For packet-oriented interfaces, the number of inbound -+ packets that contained errors preventing them from being -+ deliverable to a higher-layer protocol. For character- -+ oriented or fixed-length interfaces, the number of -+ inbound transmission units that contained errors -+ preventing them from being deliverable to a higher-layer -+ protocol. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifInErrors"; -+ } -+ -+ leaf in-unknown-protos { -+ type yang:counter32; -+ status deprecated; -+ description -+ "For packet-oriented interfaces, the number of packets -+ received via the interface that were discarded because -+ of an unknown or unsupported protocol. For -+ character-oriented or fixed-length interfaces that -+ support protocol multiplexing, the number of -+ transmission units received via the interface that were -+ discarded because of an unknown or unsupported protocol. -+ For any interface that does not support protocol -+ multiplexing, this counter is not present. -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifInUnknownProtos"; -+ } -+ -+ leaf out-octets { -+ type yang:counter64; -+ status deprecated; -+ description -+ "The total number of octets transmitted out of the -+ interface, including framing characters. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCOutOctets"; -+ } -+ -+ leaf out-unicast-pkts { -+ type yang:counter64; -+ status deprecated; -+ description -+ "The total number of packets that higher-level protocols -+ requested be transmitted and that were not addressed -+ to a multicast or broadcast address at this sub-layer, -+ including those that were discarded or not sent. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCOutUcastPkts"; -+ } -+ -+ leaf out-broadcast-pkts { -+ type yang:counter64; -+ status deprecated; -+ -+ description -+ "The total number of packets that higher-level protocols -+ requested be transmitted and that were addressed to a -+ broadcast address at this sub-layer, including those -+ that were discarded or not sent. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCOutBroadcastPkts"; -+ } -+ -+ leaf out-multicast-pkts { -+ type yang:counter64; -+ status deprecated; -+ description -+ "The total number of packets that higher-level protocols -+ requested be transmitted and that were addressed to a -+ multicast address at this sub-layer, including those -+ that were discarded or not sent. For a MAC-layer -+ protocol, this includes both Group and Functional -+ addresses. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCOutMulticastPkts"; -+ } -+ -+ leaf out-discards { -+ type yang:counter32; -+ status deprecated; -+ description -+ "The number of outbound packets that were chosen to be -+ discarded even though no errors had been detected to -+ prevent their being transmitted. One possible reason -+ for discarding such a packet could be to free up buffer -+ space. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifOutDiscards"; -+ } -+ -+ leaf out-errors { -+ type yang:counter32; -+ status deprecated; -+ description -+ "For packet-oriented interfaces, the number of outbound -+ packets that could not be transmitted because of errors. -+ For character-oriented or fixed-length interfaces, the -+ number of outbound transmission units that could not be -+ transmitted because of errors. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifOutErrors"; -+ } -+ } -+ } -+ } -+} -diff --git a/yang/ietf-ip@2018-02-22.yang b/yang/ietf-ip@2018-02-22.yang -new file mode 100644 -index 0000000..a270f67 ---- /dev/null -+++ b/yang/ietf-ip@2018-02-22.yang -@@ -0,0 +1,876 @@ -+module ietf-ip { -+ yang-version 1.1; -+ namespace "urn:ietf:params:xml:ns:yang:ietf-ip"; -+ prefix ip; -+ -+ import ietf-interfaces { -+ prefix if; -+ } -+ import ietf-inet-types { -+ prefix inet; -+ } -+ import ietf-yang-types { -+ prefix yang; -+ } -+ -+ organization -+ "IETF NETMOD (Network Modeling) Working Group"; -+ -+ contact -+ "WG Web: -+ WG List: -+ -+ Editor: Martin Bjorklund -+ "; -+ description -+ "This module contains a collection of YANG definitions for -+ managing IP implementations. -+ -+ Copyright (c) 2018 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 8344; see -+ the RFC itself for full legal notices."; -+ -+ revision 2018-02-22 { -+ description -+ "Updated to support NMDA."; -+ reference -+ "RFC 8344: A YANG Data Model for IP Management"; -+ } -+ -+ revision 2014-06-16 { -+ description -+ "Initial revision."; -+ reference -+ "RFC 7277: A YANG Data Model for IP Management"; -+ } -+ -+ /* -+ * Features -+ */ -+ -+ feature ipv4-non-contiguous-netmasks { -+ description -+ "Indicates support for configuring non-contiguous -+ subnet masks."; -+ } -+ -+ feature ipv6-privacy-autoconf { -+ description -+ "Indicates support for privacy extensions for stateless address -+ autoconfiguration in IPv6."; -+ reference -+ "RFC 4941: Privacy Extensions for Stateless Address -+ Autoconfiguration in IPv6"; -+ } -+ -+ /* -+ * Typedefs -+ */ -+ -+ typedef ip-address-origin { -+ type enumeration { -+ enum other { -+ description -+ "None of the following."; -+ } -+ -+ enum static { -+ description -+ "Indicates that the address has been statically -+ configured -- for example, using the Network Configuration -+ Protocol (NETCONF) or a command line interface."; -+ } -+ enum dhcp { -+ description -+ "Indicates an address that has been assigned to this -+ system by a DHCP server."; -+ } -+ enum link-layer { -+ description -+ "Indicates an address created by IPv6 stateless -+ autoconfiguration that embeds a link-layer address in its -+ interface identifier."; -+ } -+ enum random { -+ description -+ "Indicates an address chosen by the system at -+ random, e.g., an IPv4 address within 169.254/16, a -+ temporary address as described in RFC 4941, or a -+ semantically opaque address as described in RFC 7217."; -+ reference -+ "RFC 4941: Privacy Extensions for Stateless Address -+ Autoconfiguration in IPv6 -+ RFC 7217: A Method for Generating Semantically Opaque -+ Interface Identifiers with IPv6 Stateless -+ Address Autoconfiguration (SLAAC)"; -+ } -+ } -+ description -+ "The origin of an address."; -+ } -+ -+ typedef neighbor-origin { -+ type enumeration { -+ enum other { -+ description -+ "None of the following."; -+ } -+ enum static { -+ description -+ "Indicates that the mapping has been statically -+ configured -- for example, using NETCONF or a command line -+ interface."; -+ } -+ -+ enum dynamic { -+ description -+ "Indicates that the mapping has been dynamically resolved -+ using, for example, IPv4 ARP or the IPv6 Neighbor -+ Discovery protocol."; -+ } -+ } -+ description -+ "The origin of a neighbor entry."; -+ } -+ -+ /* -+ * Data nodes -+ */ -+ -+ augment "/if:interfaces/if:interface" { -+ description -+ "IP parameters on interfaces. -+ -+ If an interface is not capable of running IP, the server -+ must not allow the client to configure these parameters."; -+ -+ container ipv4 { -+ presence -+ "Enables IPv4 unless the 'enabled' leaf -+ (which defaults to 'true') is set to 'false'"; -+ description -+ "Parameters for the IPv4 address family."; -+ -+ leaf enabled { -+ type boolean; -+ default true; -+ description -+ "Controls whether IPv4 is enabled or disabled on this -+ interface. When IPv4 is enabled, this interface is -+ connected to an IPv4 stack, and the interface can send -+ and receive IPv4 packets."; -+ } -+ leaf forwarding { -+ type boolean; -+ default false; -+ description -+ "Controls IPv4 packet forwarding of datagrams received by, -+ but not addressed to, this interface. IPv4 routers -+ forward datagrams. IPv4 hosts do not (except those -+ source-routed via the host)."; -+ } -+ -+ leaf mtu { -+ type uint16 { -+ range "68..max"; -+ } -+ units "octets"; -+ description -+ "The size, in octets, of the largest IPv4 packet that the -+ interface will send and receive. -+ -+ The server may restrict the allowed values for this leaf, -+ depending on the interface's type. -+ -+ If this leaf is not configured, the operationally used MTU -+ depends on the interface's type."; -+ reference -+ "RFC 791: Internet Protocol"; -+ } -+ list address { -+ key "ip"; -+ description -+ "The list of IPv4 addresses on the interface."; -+ -+ leaf ip { -+ type inet:ipv4-address-no-zone; -+ description -+ "The IPv4 address on the interface."; -+ } -+ choice subnet { -+ mandatory true; -+ description -+ "The subnet can be specified as a prefix length or, -+ if the server supports non-contiguous netmasks, as -+ a netmask."; -+ leaf prefix-length { -+ type uint8 { -+ range "0..32"; -+ } -+ description -+ "The length of the subnet prefix."; -+ } -+ leaf netmask { -+ if-feature ipv4-non-contiguous-netmasks; -+ type yang:dotted-quad; -+ description -+ "The subnet specified as a netmask."; -+ } -+ } -+ -+ leaf origin { -+ type ip-address-origin; -+ config false; -+ description -+ "The origin of this address."; -+ } -+ } -+ list neighbor { -+ key "ip"; -+ description -+ "A list of mappings from IPv4 addresses to -+ link-layer addresses. -+ -+ Entries in this list in the intended configuration are -+ used as static entries in the ARP Cache. -+ -+ In the operational state, this list represents the ARP -+ Cache."; -+ reference -+ "RFC 826: An Ethernet Address Resolution Protocol"; -+ -+ leaf ip { -+ type inet:ipv4-address-no-zone; -+ description -+ "The IPv4 address of the neighbor node."; -+ } -+ leaf link-layer-address { -+ type yang:phys-address; -+ mandatory true; -+ description -+ "The link-layer address of the neighbor node."; -+ } -+ leaf origin { -+ type neighbor-origin; -+ config false; -+ description -+ "The origin of this neighbor entry."; -+ } -+ } -+ } -+ -+ container ipv6 { -+ presence -+ "Enables IPv6 unless the 'enabled' leaf -+ (which defaults to 'true') is set to 'false'"; -+ description -+ "Parameters for the IPv6 address family."; -+ -+ leaf enabled { -+ type boolean; -+ default true; -+ description -+ "Controls whether IPv6 is enabled or disabled on this -+ interface. When IPv6 is enabled, this interface is -+ connected to an IPv6 stack, and the interface can send -+ and receive IPv6 packets."; -+ } -+ leaf forwarding { -+ type boolean; -+ default false; -+ description -+ "Controls IPv6 packet forwarding of datagrams received by, -+ but not addressed to, this interface. IPv6 routers -+ forward datagrams. IPv6 hosts do not (except those -+ source-routed via the host)."; -+ reference -+ "RFC 4861: Neighbor Discovery for IP version 6 (IPv6) -+ Section 6.2.1, IsRouter"; -+ } -+ leaf mtu { -+ type uint32 { -+ range "1280..max"; -+ } -+ units "octets"; -+ description -+ "The size, in octets, of the largest IPv6 packet that the -+ interface will send and receive. -+ -+ The server may restrict the allowed values for this leaf, -+ depending on the interface's type. -+ -+ If this leaf is not configured, the operationally used MTU -+ depends on the interface's type."; -+ reference -+ "RFC 8200: Internet Protocol, Version 6 (IPv6) -+ Specification -+ Section 5"; -+ } -+ -+ list address { -+ key "ip"; -+ description -+ "The list of IPv6 addresses on the interface."; -+ -+ leaf ip { -+ type inet:ipv6-address-no-zone; -+ description -+ "The IPv6 address on the interface."; -+ } -+ leaf prefix-length { -+ type uint8 { -+ range "0..128"; -+ } -+ mandatory true; -+ description -+ "The length of the subnet prefix."; -+ } -+ leaf origin { -+ type ip-address-origin; -+ config false; -+ description -+ "The origin of this address."; -+ } -+ leaf status { -+ type enumeration { -+ enum preferred { -+ description -+ "This is a valid address that can appear as the -+ destination or source address of a packet."; -+ } -+ enum deprecated { -+ description -+ "This is a valid but deprecated address that should -+ no longer be used as a source address in new -+ communications, but packets addressed to such an -+ address are processed as expected."; -+ } -+ enum invalid { -+ description -+ "This isn't a valid address, and it shouldn't appear -+ as the destination or source address of a packet."; -+ } -+ -+ enum inaccessible { -+ description -+ "The address is not accessible because the interface -+ to which this address is assigned is not -+ operational."; -+ } -+ enum unknown { -+ description -+ "The status cannot be determined for some reason."; -+ } -+ enum tentative { -+ description -+ "The uniqueness of the address on the link is being -+ verified. Addresses in this state should not be -+ used for general communication and should only be -+ used to determine the uniqueness of the address."; -+ } -+ enum duplicate { -+ description -+ "The address has been determined to be non-unique on -+ the link and so must not be used."; -+ } -+ enum optimistic { -+ description -+ "The address is available for use, subject to -+ restrictions, while its uniqueness on a link is -+ being verified."; -+ } -+ } -+ config false; -+ description -+ "The status of an address. Most of the states correspond -+ to states from the IPv6 Stateless Address -+ Autoconfiguration protocol."; -+ reference -+ "RFC 4293: Management Information Base for the -+ Internet Protocol (IP) -+ - IpAddressStatusTC -+ RFC 4862: IPv6 Stateless Address Autoconfiguration"; -+ } -+ } -+ -+ list neighbor { -+ key "ip"; -+ description -+ "A list of mappings from IPv6 addresses to -+ link-layer addresses. -+ -+ Entries in this list in the intended configuration are -+ used as static entries in the Neighbor Cache. -+ -+ In the operational state, this list represents the -+ Neighbor Cache."; -+ reference -+ "RFC 4861: Neighbor Discovery for IP version 6 (IPv6)"; -+ -+ leaf ip { -+ type inet:ipv6-address-no-zone; -+ description -+ "The IPv6 address of the neighbor node."; -+ } -+ leaf link-layer-address { -+ type yang:phys-address; -+ mandatory true; -+ description -+ "The link-layer address of the neighbor node. -+ -+ In the operational state, if the neighbor's 'state' leaf -+ is 'incomplete', this leaf is not instantiated."; -+ } -+ leaf origin { -+ type neighbor-origin; -+ config false; -+ description -+ "The origin of this neighbor entry."; -+ } -+ leaf is-router { -+ type empty; -+ config false; -+ description -+ "Indicates that the neighbor node acts as a router."; -+ } -+ -+ leaf state { -+ type enumeration { -+ enum incomplete { -+ description -+ "Address resolution is in progress, and the -+ link-layer address of the neighbor has not yet been -+ determined."; -+ } -+ enum reachable { -+ description -+ "Roughly speaking, the neighbor is known to have been -+ reachable recently (within tens of seconds ago)."; -+ } -+ enum stale { -+ description -+ "The neighbor is no longer known to be reachable, but -+ until traffic is sent to the neighbor no attempt -+ should be made to verify its reachability."; -+ } -+ enum delay { -+ description -+ "The neighbor is no longer known to be reachable, and -+ traffic has recently been sent to the neighbor. -+ Rather than probe the neighbor immediately, however, -+ delay sending probes for a short while in order to -+ give upper-layer protocols a chance to provide -+ reachability confirmation."; -+ } -+ enum probe { -+ description -+ "The neighbor is no longer known to be reachable, and -+ unicast Neighbor Solicitation probes are being sent -+ to verify reachability."; -+ } -+ } -+ config false; -+ description -+ "The Neighbor Unreachability Detection state of this -+ entry."; -+ reference -+ "RFC 4861: Neighbor Discovery for IP version 6 (IPv6) -+ Section 7.3.2"; -+ } -+ } -+ -+ leaf dup-addr-detect-transmits { -+ type uint32; -+ default 1; -+ description -+ "The number of consecutive Neighbor Solicitation messages -+ sent while performing Duplicate Address Detection on a -+ tentative address. A value of zero indicates that -+ Duplicate Address Detection is not performed on -+ tentative addresses. A value of one indicates a single -+ transmission with no follow-up retransmissions."; -+ reference -+ "RFC 4862: IPv6 Stateless Address Autoconfiguration"; -+ } -+ container autoconf { -+ description -+ "Parameters to control the autoconfiguration of IPv6 -+ addresses, as described in RFC 4862."; -+ reference -+ "RFC 4862: IPv6 Stateless Address Autoconfiguration"; -+ -+ leaf create-global-addresses { -+ type boolean; -+ default true; -+ description -+ "If enabled, the host creates global addresses as -+ described in RFC 4862."; -+ reference -+ "RFC 4862: IPv6 Stateless Address Autoconfiguration -+ Section 5.5"; -+ } -+ leaf create-temporary-addresses { -+ if-feature ipv6-privacy-autoconf; -+ type boolean; -+ default false; -+ description -+ "If enabled, the host creates temporary addresses as -+ described in RFC 4941."; -+ reference -+ "RFC 4941: Privacy Extensions for Stateless Address -+ Autoconfiguration in IPv6"; -+ } -+ -+ leaf temporary-valid-lifetime { -+ if-feature ipv6-privacy-autoconf; -+ type uint32; -+ units "seconds"; -+ default 604800; -+ description -+ "The time period during which the temporary address -+ is valid."; -+ reference -+ "RFC 4941: Privacy Extensions for Stateless Address -+ Autoconfiguration in IPv6 -+ - TEMP_VALID_LIFETIME"; -+ } -+ leaf temporary-preferred-lifetime { -+ if-feature ipv6-privacy-autoconf; -+ type uint32; -+ units "seconds"; -+ default 86400; -+ description -+ "The time period during which the temporary address is -+ preferred."; -+ reference -+ "RFC 4941: Privacy Extensions for Stateless Address -+ Autoconfiguration in IPv6 -+ - TEMP_PREFERRED_LIFETIME"; -+ } -+ } -+ } -+ } -+ -+ /* -+ * Legacy operational state data nodes -+ */ -+ -+ augment "/if:interfaces-state/if:interface" { -+ status deprecated; -+ description -+ "Data nodes for the operational state of IP on interfaces."; -+ -+ container ipv4 { -+ presence -+ "Present if IPv4 is enabled on this interface"; -+ config false; -+ status deprecated; -+ description -+ "Interface-specific parameters for the IPv4 address family."; -+ -+ leaf forwarding { -+ type boolean; -+ status deprecated; -+ description -+ "Indicates whether IPv4 packet forwarding is enabled or -+ disabled on this interface."; -+ } -+ leaf mtu { -+ type uint16 { -+ range "68..max"; -+ } -+ units "octets"; -+ status deprecated; -+ description -+ "The size, in octets, of the largest IPv4 packet that the -+ interface will send and receive."; -+ reference -+ "RFC 791: Internet Protocol"; -+ } -+ list address { -+ key "ip"; -+ status deprecated; -+ description -+ "The list of IPv4 addresses on the interface."; -+ -+ leaf ip { -+ type inet:ipv4-address-no-zone; -+ status deprecated; -+ description -+ "The IPv4 address on the interface."; -+ } -+ choice subnet { -+ status deprecated; -+ description -+ "The subnet can be specified as a prefix length or, -+ if the server supports non-contiguous netmasks, as -+ a netmask."; -+ leaf prefix-length { -+ type uint8 { -+ range "0..32"; -+ } -+ status deprecated; -+ description -+ "The length of the subnet prefix."; -+ } -+ leaf netmask { -+ if-feature ipv4-non-contiguous-netmasks; -+ type yang:dotted-quad; -+ status deprecated; -+ description -+ "The subnet specified as a netmask."; -+ } -+ } -+ leaf origin { -+ type ip-address-origin; -+ status deprecated; -+ description -+ "The origin of this address."; -+ } -+ } -+ list neighbor { -+ key "ip"; -+ status deprecated; -+ description -+ "A list of mappings from IPv4 addresses to -+ link-layer addresses. -+ -+ This list represents the ARP Cache."; -+ reference -+ "RFC 826: An Ethernet Address Resolution Protocol"; -+ -+ leaf ip { -+ type inet:ipv4-address-no-zone; -+ status deprecated; -+ description -+ "The IPv4 address of the neighbor node."; -+ } -+ -+ leaf link-layer-address { -+ type yang:phys-address; -+ status deprecated; -+ description -+ "The link-layer address of the neighbor node."; -+ } -+ leaf origin { -+ type neighbor-origin; -+ status deprecated; -+ description -+ "The origin of this neighbor entry."; -+ } -+ } -+ } -+ -+ container ipv6 { -+ presence -+ "Present if IPv6 is enabled on this interface"; -+ config false; -+ status deprecated; -+ description -+ "Parameters for the IPv6 address family."; -+ -+ leaf forwarding { -+ type boolean; -+ default false; -+ status deprecated; -+ description -+ "Indicates whether IPv6 packet forwarding is enabled or -+ disabled on this interface."; -+ reference -+ "RFC 4861: Neighbor Discovery for IP version 6 (IPv6) -+ Section 6.2.1, IsRouter"; -+ } -+ leaf mtu { -+ type uint32 { -+ range "1280..max"; -+ } -+ units "octets"; -+ status deprecated; -+ description -+ "The size, in octets, of the largest IPv6 packet that the -+ interface will send and receive."; -+ reference -+ "RFC 8200: Internet Protocol, Version 6 (IPv6) -+ Specification -+ Section 5"; -+ } -+ list address { -+ key "ip"; -+ status deprecated; -+ description -+ "The list of IPv6 addresses on the interface."; -+ -+ leaf ip { -+ type inet:ipv6-address-no-zone; -+ status deprecated; -+ description -+ "The IPv6 address on the interface."; -+ } -+ leaf prefix-length { -+ type uint8 { -+ range "0..128"; -+ } -+ mandatory true; -+ status deprecated; -+ description -+ "The length of the subnet prefix."; -+ } -+ leaf origin { -+ type ip-address-origin; -+ status deprecated; -+ description -+ "The origin of this address."; -+ } -+ leaf status { -+ type enumeration { -+ enum preferred { -+ description -+ "This is a valid address that can appear as the -+ destination or source address of a packet."; -+ } -+ enum deprecated { -+ description -+ "This is a valid but deprecated address that should -+ no longer be used as a source address in new -+ communications, but packets addressed to such an -+ address are processed as expected."; -+ } -+ enum invalid { -+ description -+ "This isn't a valid address, and it shouldn't appear -+ as the destination or source address of a packet."; -+ } -+ -+ enum inaccessible { -+ description -+ "The address is not accessible because the interface -+ to which this address is assigned is not -+ operational."; -+ } -+ enum unknown { -+ description -+ "The status cannot be determined for some reason."; -+ } -+ enum tentative { -+ description -+ "The uniqueness of the address on the link is being -+ verified. Addresses in this state should not be -+ used for general communication and should only be -+ used to determine the uniqueness of the address."; -+ } -+ enum duplicate { -+ description -+ "The address has been determined to be non-unique on -+ the link and so must not be used."; -+ } -+ enum optimistic { -+ description -+ "The address is available for use, subject to -+ restrictions, while its uniqueness on a link is -+ being verified."; -+ } -+ } -+ status deprecated; -+ description -+ "The status of an address. Most of the states correspond -+ to states from the IPv6 Stateless Address -+ Autoconfiguration protocol."; -+ reference -+ "RFC 4293: Management Information Base for the -+ Internet Protocol (IP) -+ - IpAddressStatusTC -+ RFC 4862: IPv6 Stateless Address Autoconfiguration"; -+ } -+ } -+ -+ list neighbor { -+ key "ip"; -+ status deprecated; -+ description -+ "A list of mappings from IPv6 addresses to -+ link-layer addresses. -+ -+ This list represents the Neighbor Cache."; -+ reference -+ "RFC 4861: Neighbor Discovery for IP version 6 (IPv6)"; -+ -+ leaf ip { -+ type inet:ipv6-address-no-zone; -+ status deprecated; -+ description -+ "The IPv6 address of the neighbor node."; -+ } -+ leaf link-layer-address { -+ type yang:phys-address; -+ status deprecated; -+ description -+ "The link-layer address of the neighbor node."; -+ } -+ leaf origin { -+ type neighbor-origin; -+ status deprecated; -+ description -+ "The origin of this neighbor entry."; -+ } -+ leaf is-router { -+ type empty; -+ status deprecated; -+ description -+ "Indicates that the neighbor node acts as a router."; -+ } -+ leaf state { -+ type enumeration { -+ enum incomplete { -+ description -+ "Address resolution is in progress, and the -+ link-layer address of the neighbor has not yet been -+ determined."; -+ } -+ enum reachable { -+ description -+ "Roughly speaking, the neighbor is known to have been -+ reachable recently (within tens of seconds ago)."; -+ } -+ enum stale { -+ description -+ "The neighbor is no longer known to be reachable, but -+ until traffic is sent to the neighbor no attempt -+ should be made to verify its reachability."; -+ } -+ enum delay { -+ description -+ "The neighbor is no longer known to be reachable, and -+ traffic has recently been sent to the neighbor. -+ Rather than probe the neighbor immediately, however, -+ delay sending probes for a short while in order to -+ give upper-layer protocols a chance to provide -+ reachability confirmation."; -+ } -+ enum probe { -+ description -+ "The neighbor is no longer known to be reachable, and -+ unicast Neighbor Solicitation probes are being sent -+ to verify reachability."; -+ } -+ } -+ status deprecated; -+ description -+ "The Neighbor Unreachability Detection state of this -+ entry."; -+ reference -+ "RFC 4861: Neighbor Discovery for IP version 6 (IPv6) -+ Section 7.3.2"; -+ } -+ } -+ } -+ } -+} -diff --git a/yang/ietf-network-instance@2019-01-21.yang b/yang/ietf-network-instance@2019-01-21.yang -new file mode 100644 -index 0000000..dfde7fb ---- /dev/null -+++ b/yang/ietf-network-instance@2019-01-21.yang -@@ -0,0 +1,282 @@ -+module ietf-network-instance { -+ yang-version 1.1; -+ namespace "urn:ietf:params:xml:ns:yang:ietf-network-instance"; -+ prefix ni; -+ -+ // import some basic types -+ -+ import ietf-interfaces { -+ prefix if; -+ reference -+ "RFC 8343: A YANG Data Model for Interface Management"; -+ } -+ import ietf-ip { -+ prefix ip; -+ reference -+ "RFC 8344: A YANG Data Model for IP Management"; -+ } -+ import ietf-yang-schema-mount { -+ prefix yangmnt; -+ reference -+ "RFC 8528: YANG Schema Mount"; -+ } -+ -+ organization -+ "IETF Routing Area (rtgwg) Working Group"; -+ contact -+ "WG Web: -+ WG List: -+ -+ Author: Lou Berger -+ -+ Author: Christian Hopps -+ -+ Author: Acee Lindem -+ -+ Author: Dean Bogdanovic -+ "; -+ description -+ "This module is used to support multiple network instances -+ within a single physical or virtual device. Network -+ instances are commonly known as VRFs (VPN Routing and -+ Forwarding) and VSIs (Virtual Switching Instances). -+ 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 8529; see -+ the RFC itself for full legal notices."; -+ -+ revision 2019-01-21 { -+ description -+ "Initial revision."; -+ reference -+ "RFC 8529"; -+ } -+ -+ // top-level device definition statements -+ -+ container network-instances { -+ description -+ "Network instances, each of which consists of -+ VRFs and/or VSIs."; -+ reference -+ "RFC 8349: A YANG Data Model for Routing Management"; -+ list network-instance { -+ key "name"; -+ description -+ "List of network instances."; -+ leaf name { -+ type string; -+ mandatory true; -+ description -+ "device-scoped identifier for the network -+ instance."; -+ } -+ leaf enabled { -+ type boolean; -+ default "true"; -+ description -+ "Flag indicating whether or not the network -+ instance is enabled."; -+ } -+ leaf description { -+ type string; -+ description -+ "Description of the network instance -+ and its intended purpose."; -+ } -+ choice ni-type { -+ description -+ "This node serves as an anchor point for different types -+ of network instances. Each 'case' is expected to -+ differ in terms of the information needed in the -+ parent/core to support the NI and may differ in their -+ mounted-schema definition. When the mounted schema is -+ not expected to be the same for a specific type of NI, -+ a mount point should be defined."; -+ } -+ choice root-type { -+ mandatory true; -+ description -+ "Well-known mount points."; -+ container vrf-root { -+ description -+ "Container for mount point."; -+ yangmnt:mount-point "vrf-root" { -+ description -+ "Root for L3VPN-type models. This will typically -+ not be an inline-type mount point."; -+ } -+ } -+ container vsi-root { -+ description -+ "Container for mount point."; -+ yangmnt:mount-point "vsi-root" { -+ description -+ "Root for L2VPN-type models. This will typically -+ not be an inline-type mount point."; -+ } -+ } -+ container vv-root { -+ description -+ "Container for mount point."; -+ yangmnt:mount-point "vv-root" { -+ description -+ "Root models that support both L2VPN-type bridging -+ and L3VPN-type routing. This will typically -+ not be an inline-type mount point."; -+ } -+ } -+ } -+ } -+ } -+ -+ // augment statements -+ -+ augment "/if:interfaces/if:interface" { -+ description -+ "Add a node for the identification of the network -+ instance associated with the information configured -+ on a interface. -+ -+ Note that a standard error will be returned if the -+ identified leafref isn't present. If an interface cannot -+ be assigned for any other reason, the operation SHALL fail -+ with an error-tag of 'operation-failed' and an -+ error-app-tag of 'ni-assignment-failed'. A meaningful -+ error-info that indicates the source of the assignment -+ failure SHOULD also be provided."; -+ leaf bind-ni-name { -+ type leafref { -+ path "/network-instances/network-instance/name"; -+ } -+ description -+ "Network instance to which an interface is bound."; -+ } -+ } -+ augment "/if:interfaces/if:interface/ip:ipv4" { -+ description -+ "Add a node for the identification of the network -+ instance associated with the information configured -+ on an IPv4 interface. -+ -+ Note that a standard error will be returned if the -+ identified leafref isn't present. If an interface cannot -+ be assigned for any other reason, the operation SHALL fail -+ with an error-tag of 'operation-failed' and an -+ error-app-tag of 'ni-assignment-failed'. A meaningful -+ error-info that indicates the source of the assignment -+ failure SHOULD also be provided."; -+ leaf bind-ni-name { -+ type leafref { -+ path "/network-instances/network-instance/name"; -+ } -+ description -+ "Network instance to which IPv4 interface is bound."; -+ } -+ } -+ augment "/if:interfaces/if:interface/ip:ipv6" { -+ description -+ "Add a node for the identification of the network -+ instance associated with the information configured -+ on an IPv6 interface. -+ -+ Note that a standard error will be returned if the -+ identified leafref isn't present. If an interface cannot -+ be assigned for any other reason, the operation SHALL fail -+ with an error-tag of 'operation-failed' and an -+ error-app-tag of 'ni-assignment-failed'. A meaningful -+ error-info that indicates the source of the assignment -+ failure SHOULD also be provided."; -+ leaf bind-ni-name { -+ type leafref { -+ path "/network-instances/network-instance/name"; -+ } -+ description -+ "Network instance to which IPv6 interface is bound."; -+ } -+ } -+ -+ // notification statements -+ -+ notification bind-ni-name-failed { -+ description -+ "Indicates an error in the association of an interface to an -+ NI. Only generated after success is initially returned when -+ bind-ni-name is set. -+ -+ Note: Some errors may need to be reported for multiple -+ associations, e.g., a single error may need to be reported -+ for an IPv4 and an IPv6 bind-ni-name. -+ -+ At least one container with a bind-ni-name leaf MUST be -+ included in this notification."; -+ leaf name { -+ type leafref { -+ path "/if:interfaces/if:interface/if:name"; -+ } -+ mandatory true; -+ description -+ "Contains the interface name associated with the -+ failure."; -+ } -+ container interface { -+ description -+ "Generic interface type."; -+ leaf bind-ni-name { -+ type leafref { -+ path "/if:interfaces/if:interface" -+ + "/ni:bind-ni-name"; -+ } -+ description -+ "Contains the bind-ni-name associated with the -+ failure."; -+ } -+ } -+ container ipv4 { -+ description -+ "IPv4 interface type."; -+ leaf bind-ni-name { -+ type leafref { -+ path "/if:interfaces/if:interface/ip:ipv4/ni:bind-ni-name"; -+ } -+ description -+ "Contains the bind-ni-name associated with the -+ failure."; -+ } -+ } -+ container ipv6 { -+ description -+ "IPv6 interface type."; -+ leaf bind-ni-name { -+ type leafref { -+ path "/if:interfaces/if:interface/ip:ipv6" -+ + "/ni:bind-ni-name"; -+ } -+ description -+ "Contains the bind-ni-name associated with the -+ failure."; -+ } -+ } -+ leaf error-info { -+ type string; -+ description -+ "Optionally, indicates the source of the assignment -+ failure."; -+ } -+ } -+} -diff --git a/yang/ietf-restconf-subscribed-notifications@2019-11-17.yang b/yang/ietf-restconf-subscribed-notifications@2019-11-17.yang -new file mode 100644 -index 0000000..98a7990 ---- /dev/null -+++ b/yang/ietf-restconf-subscribed-notifications@2019-11-17.yang -@@ -0,0 +1,85 @@ -+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/yang/ietf-subscribed-notifications@2019-09-09.yang b/yang/ietf-subscribed-notifications@2019-09-09.yang -new file mode 100644 -index 0000000..e04593c ---- /dev/null -+++ b/yang/ietf-subscribed-notifications@2019-09-09.yang -@@ -0,0 +1,1350 @@ -+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."; -+ } -+ } -+ } -+ } -+ } -+ } -+ } -+} --- -2.43.0 - diff --git a/package/rousette/0037-don-t-create-throwaway-sysrepo-sessions-during-start.patch b/package/rousette/0037-don-t-create-throwaway-sysrepo-sessions-during-start.patch deleted file mode 100644 index 81b2e2eb..00000000 --- a/package/rousette/0037-don-t-create-throwaway-sysrepo-sessions-during-start.patch +++ /dev/null @@ -1,31 +0,0 @@ -From 10c6ae778724e5c15a12b58bc95c814cb17b772e Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Tue, 8 Apr 2025 16:59:48 +0200 -Subject: [PATCH 37/44] don't create throwaway sysrepo sessions during startup -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: Ic3fc71c08c589a8e87bbdc1aa1b1089c88c7cb1e -Signed-off-by: Mattias Walström ---- - src/restconf/Server.cpp | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp -index 9fd3fa9..a840e6e 100644 ---- a/src/restconf/Server.cpp -+++ b/src/restconf/Server.cpp -@@ -847,7 +847,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: - {"ietf-subscribed-notifications", "2019-09-09", {}}, - {"ietf-restconf-subscribed-notifications", "2019-11-17", {}}, - }) { -- if (auto mod = conn.sessionStart().getContext().getModuleImplemented(module)) { -+ if (auto mod = m_monitoringSession.getContext().getModuleImplemented(module)) { - for (const auto& feature : features) { - if (!mod->featureEnabled(feature)) { - throw std::runtime_error("Module "s + module + "@" + version + " does not implement feature " + feature); --- -2.43.0 - diff --git a/package/rousette/0038-requests-create-sysrepo-session-later.patch b/package/rousette/0038-requests-create-sysrepo-session-later.patch deleted file mode 100644 index 53653dc0..00000000 --- a/package/rousette/0038-requests-create-sysrepo-session-later.patch +++ /dev/null @@ -1,59 +0,0 @@ -From 4e287e1e2af95c1ec077689e4f353f657469e5e5 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Tue, 8 Apr 2025 17:00:44 +0200 -Subject: [PATCH 38/44] requests: create sysrepo session later -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -I was looking at all places where a sysrepo session is used, and tried -to defer their creations until the last moment possible, just to -(potentially) save some resources when it isn't needed. Some of these -changes are cosmetic, but I was touching that code anyway. - -This cannot be done for the generic `restconfRoot` handler because of -its associated error handling which requires a libyang context. Well, it -*could* be done, but I don't feel like doing some m_monitoringSession -magic for little to no purpose. - -Change-Id: Ia84c90abc1464e4a0f9682779d7471aefad9e2de -Signed-off-by: Mattias Walström ---- - src/restconf/Server.cpp | 5 ++--- - 1 file changed, 2 insertions(+), 3 deletions(-) - -diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp -index a840e6e..f3c515b 100644 ---- a/src/restconf/Server.cpp -+++ b/src/restconf/Server.cpp -@@ -902,7 +902,6 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: - }); - - server->handle(netconfStreamRoot, [this, conn](const auto& req, const auto& res) mutable { -- auto sess = conn.sessionStart(); - std::optional xpathFilter; - std::optional startTime; - std::optional stopTime; -@@ -914,6 +913,7 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: - } - - try { -+ auto sess = conn.sessionStart(); - authorizeRequest(nacm, sess, req); - - auto streamRequest = asRestconfStreamRequest(req.method(), req.uri().path, req.uri().raw_query); -@@ -962,9 +962,8 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: - return; - } - -- auto sess = conn.sessionStart(sysrepo::Datastore::Operational); -- - try { -+ auto sess = conn.sessionStart(sysrepo::Datastore::Operational); - authorizeRequest(nacm, sess, req); - - if (auto mod = asYangModule(sess.getContext(), req.uri().path); mod && hasAccessToYangSchema(sess, *mod)) { --- -2.43.0 - diff --git a/package/rousette/0039-docs-doctest-has-moved.patch b/package/rousette/0039-docs-doctest-has-moved.patch deleted file mode 100644 index 952d7251..00000000 --- a/package/rousette/0039-docs-doctest-has-moved.patch +++ /dev/null @@ -1,31 +0,0 @@ -From 422eae2ae879872bd618e3ba19418d20c197650d Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Wed, 9 Apr 2025 15:52:34 +0200 -Subject: [PATCH 39/44] docs: doctest has moved -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: I45d605f080434a0726d729c7b186ab1bbeb8d8e1 -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 3fbfd21..646976d 100644 ---- a/README.md -+++ b/README.md -@@ -94,7 +94,7 @@ In practical terms, this means that the NACM access rules for the following XPat - - C++20 compiler (e.g., GCC 10.x+, clang 10+) - - CMake 3.19+ - - optionally systemd - the shared library for logging to `sd-journal` --- optionally for built-in tests, [Doctest](https://github.com/onqtam/doctest/) as a C++ unit test framework -+- optionally for built-in tests, [Doctest](https://github.com/doctest/doctest/) as a C++ unit test framework - - optionally for built-in tests, [trompeloeil](https://github.com/rollbear/trompeloeil) for mock objects in C++ - - optionally for built-in tests, [`pam_matrix` and `pam_wrapper`](https://cwrap.org/pam_wrapper.html) for PAM mocking - --- -2.43.0 - diff --git a/package/rousette/0040-tests-capture-wrongly-formatted-SSE-messages.patch b/package/rousette/0040-tests-capture-wrongly-formatted-SSE-messages.patch deleted file mode 100644 index 7be717bc..00000000 --- a/package/rousette/0040-tests-capture-wrongly-formatted-SSE-messages.patch +++ /dev/null @@ -1,35 +0,0 @@ -From b60c30e4eb482ce7b72ba95c8e6edc94f232ca37 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Tue, 8 Apr 2025 18:12:18 +0200 -Subject: [PATCH 40/44] tests: capture wrongly-formatted SSE messages -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Sometimes during development I do things wrong(TM) and the Server Sent -Events data stream is not prefixed. -The log failure is unhelpful, so this patch should make the failures -more clear in such cases. - -Change-Id: I0d4643a6d7fe56ea5ce694c734a76b6dd471ff4c -Signed-off-by: Mattias Walström ---- - tests/restconf_utils.cpp | 1 + - 1 file changed, 1 insertion(+) - -diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp -index c3ff1de..72c2c5a 100644 ---- a/tests/restconf_utils.cpp -+++ b/tests/restconf_utils.cpp -@@ -224,6 +224,7 @@ std::vector SSEClient::parseEvents(const std::string& msg) - res.emplace_back(std::move(event)); - event.clear(); - } else { -+ CAPTURE(msg); - FAIL("Unprefixed response"); - } - } --- -2.43.0 - diff --git a/package/rousette/0041-tests-perform-factory-reset-between-individual-secti.patch b/package/rousette/0041-tests-perform-factory-reset-between-individual-secti.patch deleted file mode 100644 index de671a18..00000000 --- a/package/rousette/0041-tests-perform-factory-reset-between-individual-secti.patch +++ /dev/null @@ -1,32 +0,0 @@ -From 2e866f4076df5cf68e7f8015e088baf45b45ddb3 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 14 Apr 2025 13:15:29 +0200 -Subject: [PATCH 41/44] tests: perform factory-reset between individual - sections of NACM test -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: I3b60439a8235e2a0efed58fcdb2ad8eb95f21714 -Signed-off-by: Mattias Walström ---- - tests/restconf-nacm.cpp | 2 ++ - 1 file changed, 2 insertions(+) - -diff --git a/tests/restconf-nacm.cpp b/tests/restconf-nacm.cpp -index 51328f3..546d694 100644 ---- a/tests/restconf-nacm.cpp -+++ b/tests/restconf-nacm.cpp -@@ -20,6 +20,8 @@ TEST_CASE("NACM") - auto nacmGuard = manageNacm(srSess); - auto server = rousette::restconf::Server{srConn, SERVER_ADDRESS, SERVER_PORT}; - -+ srSess.sendRPC(srSess.getContext().newPath("/ietf-factory-default:factory-reset")); -+ - // something we can read - srSess.switchDatastore(sysrepo::Datastore::Operational); - srSess.setItem("/ietf-system:system/contact", "contact"); --- -2.43.0 - diff --git a/package/rousette/0042-restconf-simplify-condition-in-NACM-anonymous-access.patch b/package/rousette/0042-restconf-simplify-condition-in-NACM-anonymous-access.patch deleted file mode 100644 index abc5bce8..00000000 --- a/package/rousette/0042-restconf-simplify-condition-in-NACM-anonymous-access.patch +++ /dev/null @@ -1,70 +0,0 @@ -From 0ef0307d6664affc0547c2e65c8c33900bd7cfc9 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 14 Apr 2025 15:12:00 +0200 -Subject: [PATCH 42/44] restconf: simplify condition in NACM anonymous access -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -The condition required that all access-operations values are "read". But -access-operations is a union, so it is enough to check that we have only -one value in the union and that it is "read". - -Also test what happens if anonymous rule access-operation is a union of -read and something else. - -Change-Id: I291243d9e2167601b24d49684d53c361407f66be -Signed-off-by: Mattias Walström ---- - src/auth/Nacm.cpp | 4 +--- - tests/restconf-nacm.cpp | 18 ++++++++++++++++++ - 2 files changed, 19 insertions(+), 3 deletions(-) - -diff --git a/src/auth/Nacm.cpp b/src/auth/Nacm.cpp -index 61a7dcc..d0dc4a2 100644 ---- a/src/auth/Nacm.cpp -+++ b/src/auth/Nacm.cpp -@@ -12,9 +12,7 @@ namespace { - bool isRuleReadOnly(const libyang::DataNode& rule) - { - auto accessOperations = rule.findXPath("access-operations"); -- return !accessOperations.empty() && std::all_of(accessOperations.begin(), accessOperations.end(), [](const auto& e) { -- return e.asTerm().valueStr() == "read"; -- }); -+ return accessOperations.size() == 1 && accessOperations.begin()->asTerm().valueStr() == "read"; - } - - bool isRuleWildcardDeny(const libyang::DataNode& rule) -diff --git a/tests/restconf-nacm.cpp b/tests/restconf-nacm.cpp -index 546d694..30f4210 100644 ---- a/tests/restconf-nacm.cpp -+++ b/tests/restconf-nacm.cpp -@@ -95,6 +95,24 @@ TEST_CASE("NACM") - srSess.applyChanges(); - } - -+ DOCTEST_SUBCASE("Anonymous has one rule with both read and update access operations") -+ { -+ srSess.deleteItem("/ietf-netconf-acm:nacm/rule-list"); -+ srSess.applyChanges(); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/group[.='yangnobody']", ""); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='1']/module-name", "ietf-system"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='1']/action", "permit"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='1']/access-operations", "read update"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='1']/path", "/ietf-system:system"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='2']/module-name", "*"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='2']/action", "deny"); -+ -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/group[.='optics']", ""); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/rule[name='1']/module-name", "ietf-system"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/rule[name='1']/action", "permit"); -+ srSess.applyChanges(); -+ } -+ - DOCTEST_SUBCASE("Anonymous rulelist OK, but not at first place") - { - srSess.deleteItem("/ietf-netconf-acm:nacm/rule-list"); --- -2.43.0 - diff --git a/package/rousette/0043-restconf-allow-specifying-exec-permissions-for-anony.patch b/package/rousette/0043-restconf-allow-specifying-exec-permissions-for-anony.patch deleted file mode 100644 index beeca967..00000000 --- a/package/rousette/0043-restconf-allow-specifying-exec-permissions-for-anony.patch +++ /dev/null @@ -1,140 +0,0 @@ -From f97dcabf8bf74c463d3291d31e9b36fabec0654f Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Wed, 9 Apr 2025 21:17:19 +0200 -Subject: [PATCH 43/44] restconf: allow specifying exec permissions for - anonymous user access -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -We want to be able to allow anonymous users to call the RPC -ietf-subscribed-notifications:establish-subscription. Our current NACM -anonymous setup did not allow specifying exec permissions. -After this patch admins are allowed to setup a rule in the anonymous -user rule-list that allows anonymous users to call the -establish-subscriptions RPC. We allow only this one RPC, specifying any -other will result in disabling the anonymous user access. - -Change-Id: I5aa6fee2bbdabc0b7deac7fb9afecac6c411aca0 -Signed-off-by: Mattias Walström ---- - README.md | 2 +- - src/auth/Nacm.cpp | 23 ++++++++++++++++++----- - tests/restconf-nacm.cpp | 21 +++++++++++++++++++++ - tests/restconf_utils.cpp | 4 ++++ - 4 files changed, 44 insertions(+), 6 deletions(-) - -diff --git a/README.md b/README.md -index 646976d..0796dce 100644 ---- a/README.md -+++ b/README.md -@@ -66,7 +66,7 @@ When certain conditions are met, the anonymous access will be mapped to a NACM a - There must be some specific access rights set up in `ietf-netconf-acm` model (these are currently very opinionated for our use-case): - - 1. The first entry of `rule-list` list must be configured for `ANONYMOUS_USER_GROUP`. --2. All the rules except the last one in this rule-list entry must enable only "read" access operation. -+2. All the rules except the last one in this rule-list entry must enable either only "read" access operation or only "exec" operation on `ietf-subscribed-notifications:establish-subscription` RPC. - 3. The last rule in the first rule-set must be a wildcard rule that disables all operations over all modules. - - The anonymous user access is disabled whenever these rules are not met. -diff --git a/src/auth/Nacm.cpp b/src/auth/Nacm.cpp -index d0dc4a2..284fa85 100644 ---- a/src/auth/Nacm.cpp -+++ b/src/auth/Nacm.cpp -@@ -9,10 +9,23 @@ - #include "NacmIdentities.h" - - namespace { --bool isRuleReadOnly(const libyang::DataNode& rule) -+bool isWhitelistedRPC(const std::optional& moduleNameNode, const std::optional& rpcNameNode) -+{ -+ if (!moduleNameNode || !rpcNameNode) { -+ return false; -+ } -+ -+ return moduleNameNode->asTerm().valueStr() == "ietf-subscribed-notifications" && rpcNameNode->asTerm().valueStr() == "establish-subscription"; -+} -+ -+bool isRuleForAnonymousAccess(const libyang::DataNode& rule) - { - auto accessOperations = rule.findXPath("access-operations"); -- return accessOperations.size() == 1 && accessOperations.begin()->asTerm().valueStr() == "read"; -+ auto rpcName = rule.findPath("rpc-name"); -+ return accessOperations.size() == 1 && // Combining access operations is not allowed in anonymous user rules -+ (accessOperations.begin()->asTerm().valueStr() == "read" || // Either read... -+ (accessOperations.begin()->asTerm().valueStr() == "exec" && isWhitelistedRPC(rule.findPath("module-name"), rule.findPath("rpc-name"))) // ...or exec on whitelisted RPC. -+ ); - } - - bool isRuleWildcardDeny(const libyang::DataNode& rule) -@@ -25,7 +38,7 @@ bool isRuleWildcardDeny(const libyang::DataNode& rule) - * - * The first rule-list element contains rules for anonymous user access, i.e.: - * - The group is set to @p anonGroup (this one should contain the anonymous user) -- * - In rules (except the last one) the access-operation allowed is "read" -+ * - In rules (except the last one) the access-operation allowed is either "read" or "exec" on a whitelisted RPC. - * - The last rule has module-name="*" and action "deny". - * - * @return boolean indicating whether the rules are configured properly for anonymous user access -@@ -60,8 +73,8 @@ bool validAnonymousNacmRules(sysrepo::Session session, const std::string& anonGr - return false; - } - -- if (!std::all_of(rules.begin(), rules.end() - 1, isRuleReadOnly)) { -- spdlog::debug("NACM config validation: First n-1 rules in the anonymous rule-list must be configured for read-access only"); -+ if (!std::all_of(rules.begin(), rules.end() - 1, isRuleForAnonymousAccess)) { -+ spdlog::debug("NACM config validation: First n-1 rules in the anonymous rule-list must be configured either for read-access only or exec on listed RPC paths"); - return false; - } - -diff --git a/tests/restconf-nacm.cpp b/tests/restconf-nacm.cpp -index 30f4210..7799fe8 100644 ---- a/tests/restconf-nacm.cpp -+++ b/tests/restconf-nacm.cpp -@@ -113,6 +113,27 @@ TEST_CASE("NACM") - srSess.applyChanges(); - } - -+ DOCTEST_SUBCASE("This RPC is not allowed") -+ { -+ srSess.deleteItem("/ietf-netconf-acm:nacm/rule-list"); -+ srSess.applyChanges(); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/group[.='yangnobody']", ""); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='1']/module-name", "ietf-system"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='1']/action", "permit"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='1']/access-operations", "read"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='1']/path", "/ietf-system:system"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='2']/module-name", "ietf-subscribed-notifications"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='2']/action", "permit"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='2']/access-operations", "read update"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='2']/rpc-name", "/ietf-subscribed-notifications:modify-subscription"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='3']/module-name", "*"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='3']/action", "deny"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/group[.='optics']", ""); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/rule[name='1']/module-name", "ietf-system"); -+ srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/rule[name='1']/action", "permit"); -+ srSess.applyChanges(); -+ } -+ - DOCTEST_SUBCASE("Anonymous rulelist OK, but not at first place") - { - srSess.deleteItem("/ietf-netconf-acm:nacm/rule-list"); -diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp -index 72c2c5a..a137463 100644 ---- a/tests/restconf_utils.cpp -+++ b/tests/restconf_utils.cpp -@@ -152,6 +152,10 @@ void setupRealNacm(sysrepo::Session session) - session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/action", "permit"); - session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/access-operations", "read"); - session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/path", "/example-delete:immutable"); -+ session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='16']/module-name", "ietf-subscribed-notifications"); -+ 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='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/0044-restconf-add-access-log-to-stream-root.patch b/package/rousette/0044-restconf-add-access-log-to-stream-root.patch deleted file mode 100644 index 61cdf9dc..00000000 --- a/package/rousette/0044-restconf-add-access-log-to-stream-root.patch +++ /dev/null @@ -1,32 +0,0 @@ -From 438a18f5884ace3063f00a2dfc53dd2b4034f53e Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Wed, 16 Apr 2025 10:56:50 +0200 -Subject: [PATCH 44/44] restconf: add access log to stream root -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: I5e1db3f40bc6a7f294e6a5fef9402e0855055258 -Signed-off-by: Mattias Walström ---- - src/restconf/Server.cpp | 3 +++ - 1 file changed, 3 insertions(+) - -diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp -index f3c515b..5b1b2f9 100644 ---- a/src/restconf/Server.cpp -+++ b/src/restconf/Server.cpp -@@ -902,6 +902,9 @@ Server::Server(sysrepo::Connection conn, const std::string& address, const std:: - }); - - server->handle(netconfStreamRoot, [this, conn](const auto& req, const auto& res) mutable { -+ const auto& peer = http::peer_from_request(req); -+ spdlog::info("{}: {} {}", peer, req.method(), req.uri().raw_path); -+ - std::optional xpathFilter; - std::optional startTime; - std::optional stopTime; --- -2.43.0 - diff --git a/package/rousette/rousette.hash b/package/rousette/rousette.hash index 1ff5fb56..e990c7d2 100644 --- a/package/rousette/rousette.hash +++ b/package/rousette/rousette.hash @@ -1,3 +1,3 @@ # Locally calculated sha256 cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 LICENSE -sha256 400929f9c6550b8f6efd639b56ea76b49ef850a9f2716830637f3eef8c144a89 rousette-v1.tar.gz +sha256 e6aa0b6a4c883e2ba62cbd87c510e198797663ff26e660a01410c015e13ddaa4 rousette-v2.tar.gz diff --git a/package/rousette/rousette.mk b/package/rousette/rousette.mk index b69a4605..62835f56 100644 --- a/package/rousette/rousette.mk +++ b/package/rousette/rousette.mk @@ -3,7 +3,7 @@ # Rousette RESTconf server # ################################################################################ -ROUSETTE_VERSION = v1 +ROUSETTE_VERSION = v2 ROUSETTE_SITE = $(call github,CESNET,rousette,$(ROUSETTE_VERSION)) ROUSETTE_LICENSE = Apache-2.0 ROUSETTE_LICENSE_FILES = LICENSE diff --git a/package/sysrepo-cpp/0001-error-handling-changes-in-upstream-sysrepo.patch b/package/sysrepo-cpp/0001-error-handling-changes-in-upstream-sysrepo.patch deleted file mode 100644 index 7d4b905e..00000000 --- a/package/sysrepo-cpp/0001-error-handling-changes-in-upstream-sysrepo.patch +++ /dev/null @@ -1,73 +0,0 @@ -From 23fa270a747c35d65aeeba691599ed6b21b501f0 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Tue, 29 Oct 2024 15:15:52 +0100 -Subject: [PATCH 01/20] error handling changes in upstream sysrepo -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Upstream commit 396e331e4634a417420a71ad723567b42d75c443 removed these -extra error entries. - -Change-Id: Ifeda21194db7b7f7fdae8b6ae13c1e2d1f6b8d3d -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 2 +- - tests/subscriptions.cpp | 16 ++++++---------- - 2 files changed, 7 insertions(+), 11 deletions(-) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index eb5f900..1eb625f 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -28,7 +28,7 @@ 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>=2.11.7 IMPORTED_TARGET) -+pkg_check_modules(SYSREPO REQUIRED sysrepo>=2.12.0 IMPORTED_TARGET) - set(SYSREPO_CPP_PKG_VERSION "3") - - include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) -diff --git a/tests/subscriptions.cpp b/tests/subscriptions.cpp -index 0e14fc0..36fd61c 100644 ---- a/tests/subscriptions.cpp -+++ b/tests/subscriptions.cpp -@@ -494,11 +494,11 @@ TEST_CASE("subscriptions") - sess.applyChanges(); - } catch (const sysrepo::ErrorWithCode&) { - auto errors = sess.getErrors(); -- REQUIRE(errors.size() == 2); -- REQUIRE(errors.at(0).errorMessage == message); -- REQUIRE(errors.at(0).code == sysrepo::ErrorCode::OperationFailed); -- REQUIRE(errors.at(1).errorMessage == "User callback failed."); -- REQUIRE(errors.at(1).code == sysrepo::ErrorCode::CallbackFailed); -+ REQUIRE(errors.size() == 1); -+ REQUIRE(errors.at(0) == sysrepo::ErrorInfo{ -+ .code = sysrepo::ErrorCode::OperationFailed, -+ .errorMessage = message -+ }); - } - - // The callback does not fail the second time. -@@ -548,15 +548,11 @@ TEST_CASE("subscriptions") - sess.setItem("/test_module:leafInt32", "123"); - REQUIRE_THROWS_AS(sess.applyChanges(), sysrepo::ErrorWithCode); - auto errors = sess.getErrors(); -- REQUIRE(errors.size() == 2); -+ REQUIRE(errors.size() == 1); - REQUIRE(errors.at(0) == sysrepo::ErrorInfo{ - .code = sysrepo::ErrorCode::OperationFailed, - .errorMessage = "Test callback failure.", - }); -- REQUIRE(errors.at(1) == sysrepo::ErrorInfo{ -- .code = sysrepo::ErrorCode::CallbackFailed, -- .errorMessage = "User callback failed." -- }); - auto ncErrors = sess.getNetconfErrors(); - REQUIRE(ncErrors.size() == 1); - REQUIRE(ncErrors.front() == errToSet); --- -2.43.0 - diff --git a/package/sysrepo-cpp/0002-Fix-error-message-when-sr_session_start-fails.patch b/package/sysrepo-cpp/0002-Fix-error-message-when-sr_session_start-fails.patch deleted file mode 100644 index f5c273dc..00000000 --- a/package/sysrepo-cpp/0002-Fix-error-message-when-sr_session_start-fails.patch +++ /dev/null @@ -1,35 +0,0 @@ -From a73ffe1a5bbab51a67cf56dd2864c71a29c6685b Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Thu, 21 Nov 2024 11:18:25 +0100 -Subject: [PATCH 02/20] Fix error message when sr_session_start fails -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Thanks to Jason Patterson for sending a bugreport; its investigation led -to discovering this bug. - -See-also: https://github.com/CESNET/rousette/issues/15 -Change-Id: I082987d60380f860e08f26b436d0f9db4a231bec -Signed-off-by: Mattias Walström ---- - src/Connection.cpp | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/src/Connection.cpp b/src/Connection.cpp -index de65d64..716fd1a 100644 ---- a/src/Connection.cpp -+++ b/src/Connection.cpp -@@ -53,7 +53,7 @@ Session Connection::sessionStart(sysrepo::Datastore datastore) - sr_session_ctx_t* sess; - auto res = sr_session_start(ctx.get(), toDatastore(datastore), &sess); - -- throwIfError(res, "Couldn't connect to sysrepo"); -+ throwIfError(res, "Couldn't start sysrepo session"); - return Session{sess, ctx}; - } - --- -2.43.0 - diff --git a/package/sysrepo-cpp/0003-We-don-t-support-sysrepo-v3-yet.patch b/package/sysrepo-cpp/0003-We-don-t-support-sysrepo-v3-yet.patch deleted file mode 100644 index 6f1e645c..00000000 --- a/package/sysrepo-cpp/0003-We-don-t-support-sysrepo-v3-yet.patch +++ /dev/null @@ -1,73 +0,0 @@ -From 552a366990fe1fe2cdd8d155fa5cecb3ff3fbc13 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Wed, 4 Dec 2024 15:51:41 +0100 -Subject: [PATCH 03/20] We don't support sysrepo v3 yet -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -That version introduced some backward-incompatible changes to the way -how operational data are treated, especially in presence of "discards" -of this data. It's a breaking behavior, and before we release a fix, -let's pin the version to something which still works. The latest sysrepo -semi-release is v 2.12.0, which is unfortunately not tagged. - -Change-Id: I3673de9893d6e806926767a72b1ef83ee62bd610 -Signed-off-by: Mattias Walström ---- - .zuul.yaml | 4 ++-- - CMakeLists.txt | 2 +- - README.md | 1 + - 3 files changed, 4 insertions(+), 3 deletions(-) - -diff --git a/.zuul.yaml b/.zuul.yaml -index dc87c6f..c3aea22 100644 ---- a/.zuul.yaml -+++ b/.zuul.yaml -@@ -6,7 +6,7 @@ - - name: github/CESNET/libyang - override-checkout: devel - - name: github/sysrepo/sysrepo -- override-checkout: devel -+ override-checkout: cesnet/2024-10-before-oper-changes - - name: github/onqtam/doctest - override-checkout: v2.4.8 - - name: github/rollbear/trompeloeil -@@ -17,7 +17,7 @@ - - name: github/CESNET/libyang - override-checkout: devel - - name: github/sysrepo/sysrepo -- override-checkout: devel -+ override-checkout: cesnet/2024-10-before-oper-changes - - name: github/onqtam/doctest - override-checkout: v2.4.11 - - name: github/rollbear/trompeloeil -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 1eb625f..119bdd7 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -28,7 +28,7 @@ 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>=2.12.0 IMPORTED_TARGET) -+pkg_check_modules(SYSREPO REQUIRED sysrepo>=2.12.0 sysrepo<3 IMPORTED_TARGET) - set(SYSREPO_CPP_PKG_VERSION "3") - - include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) -diff --git a/README.md b/README.md -index 27dc748..e14b772 100644 ---- a/README.md -+++ b/README.md -@@ -9,6 +9,7 @@ It uses RAII for automatic memory management. - - ## Dependencies - - [sysrepo](https://github.com/sysrepo/sysrepo) - the `devel` branch (even for the `master` branch of *sysrepo-cpp*) -+ - we temporarily require pre-v3 sysrepo which introduced backward-incompatible changes to operational data handling - - [libyang-cpp](https://github.com/CESNET/libyang-cpp) - C++ bindings for *libyang* - - C++20 compiler (e.g., GCC 10.x+, clang 10+) - - CMake 3.19+ --- -2.43.0 - diff --git a/package/sysrepo-cpp/0004-refactor-don-t-use-module-as-a-variable-name.patch b/package/sysrepo-cpp/0004-refactor-don-t-use-module-as-a-variable-name.patch deleted file mode 100644 index 65edddaa..00000000 --- a/package/sysrepo-cpp/0004-refactor-don-t-use-module-as-a-variable-name.patch +++ /dev/null @@ -1,85 +0,0 @@ -From f2db30721d909d127645721f4149de0cd36d2b1c Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Tue, 17 Dec 2024 11:41:10 +0100 -Subject: [PATCH 04/20] refactor: don't use `module` as a variable name -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -...to unify stuff with all other occurrences, and to not use that magic -identifier. - -Change-Id: I8c38777a925271e7659560d0380a7f7968f4cfa7 -Signed-off-by: Mattias Walström ---- - include/sysrepo-cpp/Session.hpp | 4 ++-- - src/Session.cpp | 12 ++++++------ - 2 files changed, 8 insertions(+), 8 deletions(-) - -diff --git a/include/sysrepo-cpp/Session.hpp b/include/sysrepo-cpp/Session.hpp -index 7da11cb..477e604 100644 ---- a/include/sysrepo-cpp/Session.hpp -+++ b/include/sysrepo-cpp/Session.hpp -@@ -95,7 +95,7 @@ public: - void copyConfig(const Datastore source, const std::optional& moduleName = std::nullopt, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); - libyang::DataNode sendRPC(libyang::DataNode input, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); - void sendNotification(libyang::DataNode notification, const Wait wait, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); -- void replaceConfig(std::optional config, const std::optional& module = std::nullopt, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); -+ void replaceConfig(std::optional config, const std::optional& moduleName = std::nullopt, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); - - void setNacmUser(const std::string& user); - [[nodiscard]] Subscription initNacm( -@@ -172,7 +172,7 @@ public: - * - * Wraps `sr_lock`. - */ -- explicit Lock(Session session, std::optional module = std::nullopt, std::optional timeout = std::nullopt); -+ explicit Lock(Session session, std::optional moduleName = std::nullopt, std::optional timeout = std::nullopt); - /** @brief Release the lock - * - * Wraps `sr_unlock`. -diff --git a/src/Session.cpp b/src/Session.cpp -index 2706e1c..94a0fad 100644 ---- a/src/Session.cpp -+++ b/src/Session.cpp -@@ -331,17 +331,17 @@ void Session::sendNotification(libyang::DataNode notification, const Wait wait, - * Wraps `sr_replace_config`. - * - * @param config Libyang tree to use as a complete datastore content, or nullopt -- * @param module If provided, a module name to limit the operation to -+ * @param moduleName If provided, a module name to limit the operation to - * @param timeout Optional timeout to wait for - */ --void Session::replaceConfig(std::optional config, const std::optional& module, std::chrono::milliseconds timeout) -+void Session::replaceConfig(std::optional config, const std::optional& moduleName, std::chrono::milliseconds timeout) - { - std::optional thrashable; - if (config) { - thrashable = config->duplicateWithSiblings(libyang::DuplicationOptions::Recursive | libyang::DuplicationOptions::WithParents); - } - auto res = sr_replace_config( -- m_sess.get(), module ? module->c_str() : nullptr, -+ m_sess.get(), moduleName ? moduleName->c_str() : nullptr, - config ? libyang::releaseRawNode(*thrashable) : nullptr, - timeout.count()); - throwIfError(res, "sr_replace_config failed", m_sess.get()); -@@ -723,12 +723,12 @@ uint32_t Session::getId() const - return sr_session_get_id(m_sess.get()); - } - --Lock::Lock(Session session, std::optional module, std::optional timeout) -+Lock::Lock(Session session, std::optional moduleName, std::optional timeout) - : m_session(session) - , m_lockedDs(m_session.activeDatastore()) -- , m_module(module) -+ , m_module(moduleName) - { -- auto res = sr_lock(getRawSession(m_session), module ? module->c_str() : nullptr, timeout ? timeout->count() : 0); -+ auto res = sr_lock(getRawSession(m_session), moduleName ? moduleName->c_str() : nullptr, timeout ? timeout->count() : 0); - throwIfError(res, "Cannot lock session", getRawSession(m_session)); - } - --- -2.43.0 - diff --git a/package/sysrepo-cpp/0005-DRY-a-dummy-leaf-XPath.patch b/package/sysrepo-cpp/0005-DRY-a-dummy-leaf-XPath.patch deleted file mode 100644 index 7094334f..00000000 --- a/package/sysrepo-cpp/0005-DRY-a-dummy-leaf-XPath.patch +++ /dev/null @@ -1,253 +0,0 @@ -From a923f490350dc61b9f3e6000c06c1d7950a78a61 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Wed, 18 Dec 2024 16:12:14 +0100 -Subject: [PATCH 05/20] DRY: a dummy leaf XPath -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: I47fca61d27380af07b2327f3ee5b984a8a8afb66 -Signed-off-by: Mattias Walström ---- - tests/session.cpp | 81 ++++++++++++++++++++++++----------------------- - 1 file changed, 41 insertions(+), 40 deletions(-) - -diff --git a/tests/session.cpp b/tests/session.cpp -index 5b384bb..f762f10 100644 ---- a/tests/session.cpp -+++ b/tests/session.cpp -@@ -20,6 +20,7 @@ TEST_CASE("session") - std::optional conn{std::in_place}; - auto sess = conn->sessionStart(); - sess.copyConfig(sysrepo::Datastore::Startup); -+ const auto leaf = "/test_module:leafInt32"s; - - DOCTEST_SUBCASE("Session should be still valid even after the Connection class gets freed") - { -@@ -29,39 +30,39 @@ TEST_CASE("session") - - DOCTEST_SUBCASE("Session lifetime is prolonged with data from getData") - { -- sess.setItem("/test_module:leafInt32", "123"); -+ sess.setItem(leaf, "123"); - sess.applyChanges(); -- auto data = sysrepo::Connection{}.sessionStart().getData("/test_module:leafInt32"); -+ auto data = sysrepo::Connection{}.sessionStart().getData(leaf); - REQUIRE(data->asTerm().valueStr() == "123"); - } - - DOCTEST_SUBCASE("basic data manipulation") - { -- auto data = sess.getData("/test_module:leafInt32"); -+ auto data = sess.getData(leaf); - REQUIRE(!data); - -- sess.setItem("/test_module:leafInt32", "123"); -+ sess.setItem(leaf, "123"); - sess.applyChanges(); -- data = sess.getData("/test_module:leafInt32"); -+ data = sess.getData(leaf); - REQUIRE(data->asTerm().valueStr() == "123"); -- auto node = sess.getOneNode("/test_module:leafInt32"); -+ auto node = sess.getOneNode(leaf); - REQUIRE(node.asTerm().valueStr() == "123"); - -- sess.setItem("/test_module:leafInt32", "420"); -+ sess.setItem(leaf, "420"); - sess.applyChanges(); -- data = sess.getData("/test_module:leafInt32"); -+ data = sess.getData(leaf); - REQUIRE(data->asTerm().valueStr() == "420"); - -- sess.deleteItem("/test_module:leafInt32"); -+ sess.deleteItem(leaf); - sess.applyChanges(); -- data = sess.getData("/test_module:leafInt32"); -+ data = sess.getData(leaf); - REQUIRE(!data); - -- sess.setItem("/test_module:leafInt32", "123"); -+ sess.setItem(leaf, "123"); - sess.discardChanges(); -- data = sess.getData("/test_module:leafInt32"); -+ data = sess.getData(leaf); - REQUIRE(!data); -- REQUIRE_THROWS_WITH_AS(sess.getOneNode("/test_module:leafInt32"), -+ REQUIRE_THROWS_WITH_AS(sess.getOneNode(leaf), - "Session::getOneNode: Couldn't get '/test_module:leafInt32': SR_ERR_NOT_FOUND", - sysrepo::ErrorWithCode); - -@@ -175,7 +176,7 @@ TEST_CASE("session") - { - sess.switchDatastore(sysrepo::Datastore::Operational); - sess.setItem("/test_module:stateLeaf", "42"); -- sess.setItem("/test_module:leafInt32", "1"); -+ sess.setItem(leaf, "1"); - sess.applyChanges(); - - DOCTEST_SUBCASE("Default options") -@@ -183,7 +184,7 @@ TEST_CASE("session") - auto data = sess.getData("/test_module:*"); - REQUIRE(data); - REQUIRE(data->findPath("/test_module:stateLeaf")); -- REQUIRE(data->findPath("/test_module:leafInt32")); -+ REQUIRE(data->findPath(leaf)); - } - - DOCTEST_SUBCASE("No state data") -@@ -191,7 +192,7 @@ TEST_CASE("session") - auto data = sess.getData("/test_module:*", 0, sysrepo::GetOptions::OperNoState); - REQUIRE(data); - REQUIRE(!data->findPath("/test_module:stateLeaf")); -- REQUIRE(data->findPath("/test_module:leafInt32")); -+ REQUIRE(data->findPath(leaf)); - } - } - -@@ -213,42 +214,42 @@ TEST_CASE("session") - DOCTEST_SUBCASE("Session::deleteOperItem") - { - // Set some arbitrary leaf. -- sess.setItem("/test_module:leafInt32", "123"); -+ sess.setItem(leaf, "123"); - sess.applyChanges(); - - // The leaf is accesible from the running datastore. -- REQUIRE(sess.getData("/test_module:leafInt32")->asTerm().valueStr() == "123"); -+ REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "123"); - - // The leaf is NOT accesible from the operational datastore without a subscription. - sess.switchDatastore(sysrepo::Datastore::Operational); -- REQUIRE(!sess.getData("/test_module:leafInt32")); -+ REQUIRE(!sess.getData(leaf)); - - // When we create a subscription, the leaf is accesible from the operational datastore. - sess.switchDatastore(sysrepo::Datastore::Running); - auto sub = sess.onModuleChange("test_module", [] (auto, auto, auto, auto, auto, auto) { return sysrepo::ErrorCode::Ok; }); - sess.switchDatastore(sysrepo::Datastore::Operational); -- REQUIRE(sess.getData("/test_module:leafInt32")->asTerm().valueStr() == "123"); -+ REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "123"); - - // After using deleteItem, the leaf is no longer accesible from the operational datastore. -- sess.deleteItem("/test_module:leafInt32"); -+ sess.deleteItem(leaf); - sess.applyChanges(); -- REQUIRE(!sess.getData("/test_module:leafInt32")); -+ REQUIRE(!sess.getData(leaf)); - - // Using discardItems makes the leaf visible again (in the operational datastore). -- sess.discardItems("/test_module:leafInt32"); -+ sess.discardItems(leaf); - sess.applyChanges(); -- REQUIRE(sess.getData("/test_module:leafInt32")->asTerm().valueStr() == "123"); -+ REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "123"); - } - - DOCTEST_SUBCASE("edit batch") - { -- auto data = sess.getData("/test_module:leafInt32"); -+ auto data = sess.getData(leaf); - REQUIRE(!data); - -- auto batch = sess.getContext().newPath("/test_module:leafInt32", "1230"); -+ auto batch = sess.getContext().newPath(leaf, "1230"); - sess.editBatch(batch, sysrepo::DefaultOperation::Merge); - sess.applyChanges(); -- data = sess.getData("/test_module:leafInt32"); -+ data = sess.getData(leaf); - REQUIRE(data->asTerm().valueStr() == "1230"); - } - -@@ -307,8 +308,8 @@ TEST_CASE("session") - DOCTEST_SUBCASE("Session::getPendingChanges") - { - REQUIRE(sess.getPendingChanges() == std::nullopt); -- sess.setItem("/test_module:leafInt32", "123"); -- REQUIRE(sess.getPendingChanges().value().findPath("/test_module:leafInt32")->asTerm().valueStr() == "123"); -+ sess.setItem(leaf, "123"); -+ REQUIRE(sess.getPendingChanges().value().findPath(leaf)->asTerm().valueStr() == "123"); - - DOCTEST_SUBCASE("apply") - { -@@ -328,7 +329,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_THROWS_AS(sess.setItem("/test_module:leafInt32", "123"), sysrepo::ErrorWithCode); -+ REQUIRE_THROWS_AS(sess.setItem(leaf, "123"), sysrepo::ErrorWithCode); - } - - DOCTEST_SUBCASE("session IDs") -@@ -391,9 +392,9 @@ TEST_CASE("session") - - DOCTEST_SUBCASE("replace config") - { -- REQUIRE(!sess.getData("/test_module:leafInt32")); -+ REQUIRE(!sess.getData(leaf)); - // some "reasonable data" for two modules -- sess.setItem("/test_module:leafInt32", "666"); -+ sess.setItem(leaf, "666"); - sess.setItem("/ietf-netconf-acm:nacm/groups/group[name='ahoj']/user-name[.='foo']", ""); - sess.applyChanges(); - -@@ -401,16 +402,16 @@ TEST_CASE("session") - REQUIRE(!!conf); - - // override a single leaf -- REQUIRE(sess.getOneNode("/test_module:leafInt32").asTerm().valueStr() == "666"); -- sess.setItem("/test_module:leafInt32", "123"); -+ REQUIRE(sess.getOneNode(leaf).asTerm().valueStr() == "666"); -+ sess.setItem(leaf, "123"); - sess.setItem("/ietf-netconf-acm:nacm/groups/group[name='ahoj']/user-name[.='bar']", ""); - sess.applyChanges(); -- REQUIRE(sess.getOneNode("/test_module:leafInt32").asTerm().valueStr() == "123"); -+ REQUIRE(sess.getOneNode(leaf).asTerm().valueStr() == "123"); - - DOCTEST_SUBCASE("this module empty config") - { - sess.replaceConfig(std::nullopt, "test_module"); -- REQUIRE(!sess.getData("/test_module:leafInt32")); -+ REQUIRE(!sess.getData(leaf)); - REQUIRE(sess.getOneNode("/ietf-netconf-acm:nacm/groups/group[name='ahoj']/user-name[.='foo']").asTerm().valueStr() == "foo"); - REQUIRE(sess.getOneNode("/ietf-netconf-acm:nacm/groups/group[name='ahoj']/user-name[.='bar']").asTerm().valueStr() == "bar"); - } -@@ -418,7 +419,7 @@ TEST_CASE("session") - DOCTEST_SUBCASE("this module") - { - sess.replaceConfig(conf, "test_module"); -- REQUIRE(sess.getOneNode("/test_module:leafInt32").asTerm().valueStr() == "666"); -+ REQUIRE(sess.getOneNode(leaf).asTerm().valueStr() == "666"); - REQUIRE(sess.getOneNode("/ietf-netconf-acm:nacm/groups/group[name='ahoj']/user-name[.='foo']").asTerm().valueStr() == "foo"); - REQUIRE(sess.getOneNode("/ietf-netconf-acm:nacm/groups/group[name='ahoj']/user-name[.='bar']").asTerm().valueStr() == "bar"); - } -@@ -426,7 +427,7 @@ TEST_CASE("session") - DOCTEST_SUBCASE("other module") - { - sess.replaceConfig(std::nullopt, "ietf-netconf-acm"); -- REQUIRE(sess.getOneNode("/test_module:leafInt32").asTerm().valueStr() == "123"); -+ REQUIRE(sess.getOneNode(leaf).asTerm().valueStr() == "123"); - REQUIRE(!sess.getData("/ietf-netconf-acm:nacm/groups/group[name='ahoj']/user-name[.='foo']")); - REQUIRE(!sess.getData("/ietf-netconf-acm:nacm/groups/group[name='ahoj']/user-name[.='bar']")); - } -@@ -434,7 +435,7 @@ TEST_CASE("session") - DOCTEST_SUBCASE("entire datastore empty config") - { - sess.replaceConfig(std::nullopt); -- REQUIRE(!sess.getData("/test_module:leafInt32")); -+ REQUIRE(!sess.getData(leaf)); - REQUIRE(!sess.getData("/ietf-netconf-acm:nacm/groups/group[name='ahoj']/user-name[.='foo']")); - REQUIRE(!sess.getData("/ietf-netconf-acm:nacm/groups/group[name='ahoj']/user-name[.='bar']")); - } -@@ -442,7 +443,7 @@ TEST_CASE("session") - DOCTEST_SUBCASE("entire datastore") - { - sess.replaceConfig(conf); -- REQUIRE(sess.getOneNode("/test_module:leafInt32").asTerm().valueStr() == "666"); -+ REQUIRE(sess.getOneNode(leaf).asTerm().valueStr() == "666"); - REQUIRE(sess.getOneNode("/ietf-netconf-acm:nacm/groups/group[name='ahoj']/user-name[.='foo']").asTerm().valueStr() == "foo"); - REQUIRE(!sess.getData("/ietf-netconf-acm:nacm/groups/group[name='ahoj']/user-name[.='bar']")); - } --- -2.43.0 - diff --git a/package/sysrepo-cpp/0006-build-a-single-place-to-define-package-version.patch b/package/sysrepo-cpp/0006-build-a-single-place-to-define-package-version.patch deleted file mode 100644 index 5a207611..00000000 --- a/package/sysrepo-cpp/0006-build-a-single-place-to-define-package-version.patch +++ /dev/null @@ -1,42 +0,0 @@ -From 80e9659b41ea1de2e20d62f94319fe5af26adcee Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Wed, 18 Dec 2024 17:57:36 +0100 -Subject: [PATCH 06/20] build: a single place to define package version -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Don't ask me how I found out that I need this :). - -Change-Id: I2cd7397895ed4852f852e99b97543dde76eaff8f -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 119bdd7..68ade53 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -20,7 +20,8 @@ add_custom_target(sysrepo-cpp-version-cmake - cmake/ProjectGitVersionRunner.cmake - ) - include(cmake/ProjectGitVersion.cmake) --prepare_git_version(SYSREPO_CPP_VERSION "3") -+set(SYSREPO_CPP_PKG_VERSION "3") -+prepare_git_version(SYSREPO_CPP_VERSION ${SYSREPO_CPP_PKG_VERSION}) - - find_package(Doxygen) - option(WITH_DOCS "Create and install internal documentation (needs Doxygen)" ${DOXYGEN_FOUND}) -@@ -29,7 +30,6 @@ 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>=2.12.0 sysrepo<3 IMPORTED_TARGET) --set(SYSREPO_CPP_PKG_VERSION "3") - - include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) - --- -2.43.0 - diff --git a/package/sysrepo-cpp/0007-API-ABI-break-Change-how-pushed-ops-data-work.patch b/package/sysrepo-cpp/0007-API-ABI-break-Change-how-pushed-ops-data-work.patch deleted file mode 100644 index a0b25417..00000000 --- a/package/sysrepo-cpp/0007-API-ABI-break-Change-how-pushed-ops-data-work.patch +++ /dev/null @@ -1,357 +0,0 @@ -From 32bdd8bcad04f890fe724fb6cc2fc74f9b982576 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Wed, 18 Dec 2024 17:16:55 +0100 -Subject: [PATCH 07/20] API/ABI break: Change how pushed ops data work -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Upstream has changed the way how pushed operational data are stored and -managed. From now on, each session has a separate edit, these edits are -applied on top of each other using a given priority (priority setting is -not implemented in the C++ wrapper at this time, patches welcome). Each -session can retrieve and manipulate the stored edit. - -The key difference is that these edits are now per-session. This has a -wide-ranging impact, and the way how previously set nodes are "undone" -has changed. Unfortunately, upstream refused to rename the relevant -functions due to "API stability concerns", so we're taking care of -avoiding the possible confusion at the C++ layer: - -- `Session::dropForeignOperationalContent` (aka `sr_discard_items`), - which is now used to ensure that matching content from "previous - sources" is discarded. These "previous sources" are either content of - `running`, or stuff that was stored/pushed by other sessions with - lower priority. It *cannot* be used to remove stuff that was - previously pushed by the current session. - -- `Session::discardOperationalChanges` (aka `sr_discard_oper_changes`) - discards previously pushed content from *this session*. - -- It is now possible to fully manage the edit (which incrementally - builds the `operational` DS) of the current session. Use - `Session::operationalChanges()` to retrieve a full libyang::DataNode - forest, modify it in whichever ways are needed, and store it back via - `Session::editBatch(..., sysrepo::DefaultOperation::Replace)` followed - by `Session::applyChanges()`. - -Change-Id: Iba05a88411fd4c47c03d8c2b48cb7aadfd5dcd2a -Signed-off-by: Mattias Walström ---- - .zuul.yaml | 4 +- - CMakeLists.txt | 4 +- - README.md | 1 - - include/sysrepo-cpp/Session.hpp | 6 ++- - src/Session.cpp | 96 ++++++++++++++++++++++++--------- - tests/session.cpp | 59 ++++++++++++++++---- - 6 files changed, 128 insertions(+), 42 deletions(-) - -diff --git a/.zuul.yaml b/.zuul.yaml -index c3aea22..70c5e49 100644 ---- a/.zuul.yaml -+++ b/.zuul.yaml -@@ -6,7 +6,7 @@ - - name: github/CESNET/libyang - override-checkout: devel - - name: github/sysrepo/sysrepo -- override-checkout: cesnet/2024-10-before-oper-changes -+ override-checkout: cesnet/2025-01-30--00 - - name: github/onqtam/doctest - override-checkout: v2.4.8 - - name: github/rollbear/trompeloeil -@@ -17,7 +17,7 @@ - - name: github/CESNET/libyang - override-checkout: devel - - name: github/sysrepo/sysrepo -- override-checkout: cesnet/2024-10-before-oper-changes -+ override-checkout: cesnet/2025-01-30--00 - - name: github/onqtam/doctest - override-checkout: v2.4.11 - - name: github/rollbear/trompeloeil -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 68ade53..9e09d3d 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -20,7 +20,7 @@ add_custom_target(sysrepo-cpp-version-cmake - cmake/ProjectGitVersionRunner.cmake - ) - include(cmake/ProjectGitVersion.cmake) --set(SYSREPO_CPP_PKG_VERSION "3") -+set(SYSREPO_CPP_PKG_VERSION "4") - prepare_git_version(SYSREPO_CPP_VERSION ${SYSREPO_CPP_PKG_VERSION}) - - find_package(Doxygen) -@@ -29,7 +29,7 @@ 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>=2.12.0 sysrepo<3 IMPORTED_TARGET) -+pkg_check_modules(SYSREPO REQUIRED sysrepo>=3.4.7 IMPORTED_TARGET) - - include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) - -diff --git a/README.md b/README.md -index e14b772..27dc748 100644 ---- a/README.md -+++ b/README.md -@@ -9,7 +9,6 @@ It uses RAII for automatic memory management. - - ## Dependencies - - [sysrepo](https://github.com/sysrepo/sysrepo) - the `devel` branch (even for the `master` branch of *sysrepo-cpp*) -- - we temporarily require pre-v3 sysrepo which introduced backward-incompatible changes to operational data handling - - [libyang-cpp](https://github.com/CESNET/libyang-cpp) - C++ bindings for *libyang* - - C++20 compiler (e.g., GCC 10.x+, clang 10+) - - CMake 3.19+ -diff --git a/include/sysrepo-cpp/Session.hpp b/include/sysrepo-cpp/Session.hpp -index 477e604..238f480 100644 ---- a/include/sysrepo-cpp/Session.hpp -+++ b/include/sysrepo-cpp/Session.hpp -@@ -85,13 +85,15 @@ public: - void setItem(const std::string& path, const std::optional& value, const EditOptions opts = sysrepo::EditOptions::Default); - void editBatch(libyang::DataNode edit, const DefaultOperation op); - void deleteItem(const std::string& path, const EditOptions opts = sysrepo::EditOptions::Default); -- void discardItems(const std::optional& xpath); - void moveItem(const std::string& path, const MovePosition move, const std::optional& keys_or_value, const std::optional& origin = std::nullopt, const EditOptions opts = sysrepo::EditOptions::Default); - std::optional getData(const std::string& path, int maxDepth = 0, const GetOptions opts = sysrepo::GetOptions::Default, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) const; - libyang::DataNode getOneNode(const std::string& path, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) const; - std::optional getPendingChanges() const; - void applyChanges(std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); -- void discardChanges(); -+ void discardChanges(const std::optional& xpath = std::nullopt); -+ std::optional operationalChanges(const std::optional& moduleName = std::nullopt) const; -+ void discardOperationalChanges(const std::optional& moduleName = std::nullopt, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); -+ void dropForeignOperationalContent(const std::optional& xpath); - void copyConfig(const Datastore source, const std::optional& moduleName = std::nullopt, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); - libyang::DataNode sendRPC(libyang::DataNode input, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); - void sendNotification(libyang::DataNode notification, const Wait wait, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); -diff --git a/src/Session.cpp b/src/Session.cpp -index 94a0fad..634e450 100644 ---- a/src/Session.cpp -+++ b/src/Session.cpp -@@ -23,6 +23,26 @@ extern "C" { - - using namespace std::string_literals; - namespace sysrepo { -+ -+namespace { -+libyang::DataNode wrapSrData(std::shared_ptr sess, sr_data_t* data) -+{ -+ // Since the lyd_node came from sysrepo and it is wrapped in a sr_data_t, we have to postpone calling the -+ // sr_release_data() until after we're "done" with the libyang::DataNode. -+ // -+ // Normally, sr_release_data() would free the lyd_data as well. However, it is possible that the user wants to -+ // manipulate the data tree (think unlink()) in a way which might have needed to overwrite the tree->data pointer. -+ // Just delegate all the freeing to the C++ wrapper around lyd_data. The sysrepo library doesn't care about this. -+ auto tree = std::exchange(data->tree, nullptr); -+ -+ // Use wrapRawNode, not wrapUnmanagedRawNode because we want to let the C++ wrapper manage memory. -+ // Note: We're capturing the session inside the lambda. -+ return libyang::wrapRawNode(tree, std::shared_ptr(data, [extend_session_lifetime = sess] (sr_data_t* data) { -+ sr_release_data(data); -+ })); -+} -+} -+ - /** - * Wraps a pointer to sr_session_ctx_s and manages the lifetime of it. Also extends the lifetime of the connection - * specified by the `conn` argument. -@@ -119,20 +139,61 @@ void Session::deleteItem(const std::string& path, const EditOptions opts) - } - - /** -- * Prepare to discard nodes matching the specified xpath (or all if not set) previously set by the session connection. -- * Usable only for sysrepo::Datastore::Operational. The changes are applied only after calling Session::applyChanges. -+ * Prepare to drop "earlier content" from other sources in the operational DS for nodes matching the specified XPath -+ * -+ * The "earlier content" might come from the `running` datastore, or be pushed into the `operational` DS from -+ * another session, with a lower priority. This function prepares a special node into the current session's -+ * stored edit which effectively discards any matching content from previous, lower-priority sources. -+ * -+ * This function cannot be used to remove an edit which was pushed via the current session. To do that, -+ * use discardChanges(), or retrieve the stored edit and manipulate its libyang data tree. -+ * -+ * The changes are applied only after calling Session::applyChanges. - * - * Wraps `sr_discard_items`. - * - * @param xpath Expression filtering the nodes to discard, nullopt for all nodes. - */ --void Session::discardItems(const std::optional& xpath) -+void Session::dropForeignOperationalContent(const std::optional& xpath) - { - auto res = sr_discard_items(m_sess.get(), xpath ? xpath->c_str() : nullptr); - - throwIfError(res, "Session::discardItems: Can't discard "s + (xpath ? "'"s + *xpath + "'" : "all nodes"s), m_sess.get()); - } - -+/** @short Get a copy of the stored push-operational data for this session -+ * -+ * To modify the stored push operational data, modify this tree in-place and pass it to editBatch() -+ * with the "replace" operation. -+ * -+ * Wraps `sr_get_oper_changes`. -+ */ -+std::optional Session::operationalChanges(const std::optional& moduleName) const -+{ -+ sr_data_t* data; -+ auto res = sr_get_oper_changes(m_sess.get(), moduleName ? moduleName->c_str() : nullptr, &data); -+ -+ throwIfError(res, "Session::operationalChanges: Couldn't retrieve data'"s + (moduleName ? " for \"" + *moduleName + '"' : ""s), m_sess.get()); -+ -+ if (!data) { -+ return std::nullopt; -+ } -+ -+ return wrapSrData(m_sess, data); -+ -+} -+ -+/** @short Discard push operational changes of a module for this session -+ * -+ * Wraps `sr_discard_oper_changes`. -+ * -+ * */ -+void Session::discardOperationalChanges(const std::optional& moduleName, std::chrono::milliseconds timeout) -+{ -+ auto res = sr_discard_oper_changes(nullptr, 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()); -+} -+ - /** - * Moves item (a list or a leaf-list) specified by `path`. - * @param path Node to move. -@@ -155,25 +216,6 @@ void Session::moveItem(const std::string& path, const MovePosition move, const s - throwIfError(res, "Session::moveItem: Can't move '"s + path + "'", m_sess.get()); - } - --namespace { --libyang::DataNode wrapSrData(std::shared_ptr sess, sr_data_t* data) --{ -- // Since the lyd_node came from sysrepo and it is wrapped in a sr_data_t, we have to postpone calling the -- // sr_release_data() until after we're "done" with the libyang::DataNode. -- // -- // Normally, sr_release_data() would free the lyd_data as well. However, it is possible that the user wants to -- // manipulate the data tree (think unlink()) in a way which might have needed to overwrite the tree->data pointer. -- // Just delegate all the freeing to the C++ wrapper around lyd_data. The sysrepo library doesn't care about this. -- auto tree = std::exchange(data->tree, nullptr); -- -- // Use wrapRawNode, not wrapUnmanagedRawNode because we want to let the C++ wrapper manage memory. -- // Note: We're capturing the session inside the lambda. -- return libyang::wrapRawNode(tree, std::shared_ptr(data, [extend_session_lifetime = sess] (sr_data_t* data) { -- sr_release_data(data); -- })); --} --} -- - /** - * @brief Returns a tree which contains all nodes that match the provided XPath. - * -@@ -263,13 +305,15 @@ void Session::applyChanges(std::chrono::milliseconds timeout) - } - - /** -- * Discards changes made in this Session. -+ * Discards changes made earlier in this Session, optionally only below a given XPath -+ * -+ * The changes are applied only after calling Session::applyChanges. - * -- * Wraps `sr_discard_changes`. -+ * Wraps `sr_discard_changes_xpath`. - */ --void Session::discardChanges() -+void Session::discardChanges(const std::optional& xpath) - { -- auto res = sr_discard_changes(m_sess.get()); -+ auto res = sr_discard_changes_xpath(m_sess.get(), xpath ? xpath->c_str() : nullptr); - - throwIfError(res, "Session::discardChanges: Couldn't discard changes", m_sess.get()); - } -diff --git a/tests/session.cpp b/tests/session.cpp -index f762f10..2637f67 100644 ---- a/tests/session.cpp -+++ b/tests/session.cpp -@@ -211,7 +211,7 @@ TEST_CASE("session") - } - } - -- DOCTEST_SUBCASE("Session::deleteOperItem") -+ DOCTEST_SUBCASE("push operational data and deleting stuff") - { - // Set some arbitrary leaf. - sess.setItem(leaf, "123"); -@@ -230,15 +230,51 @@ TEST_CASE("session") - sess.switchDatastore(sysrepo::Datastore::Operational); - REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "123"); - -- // After using deleteItem, the leaf is no longer accesible from the operational datastore. -- sess.deleteItem(leaf); -- sess.applyChanges(); -- REQUIRE(!sess.getData(leaf)); -+ DOCTEST_SUBCASE("discardOperationalChanges") -+ { -+ // apply a change which makes the leaf disappear -+ sess.dropForeignOperationalContent(leaf); -+ REQUIRE(!!sess.getData(leaf)); -+ sess.applyChanges(); -+ REQUIRE(!sess.getData(leaf)); - -- // Using discardItems makes the leaf visible again (in the operational datastore). -- sess.discardItems(leaf); -- sess.applyChanges(); -- REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "123"); -+ // Using discardOperationalChanges makes the leaf visible again (in the operational datastore). -+ // Also, no need to applyChanges(). -+ sess.discardOperationalChanges("test_module"); -+ REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "123"); -+ } -+ -+ DOCTEST_SUBCASE("direct edit of a libyang::DataNode") -+ { -+ // at first, set the leaf to some random value -+ sess.setItem(leaf, "456"); -+ sess.applyChanges(); -+ REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "456"); -+ -+ // change the edit in-place -+ auto pushed = sess.operationalChanges(); -+ REQUIRE(pushed->path() == leaf); -+ pushed->asTerm().changeValue("666"); -+ sess.editBatch(*pushed, sysrepo::DefaultOperation::Replace); -+ sess.applyChanges(); -+ REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "666"); -+ -+ // Remove that previous edit in-place. Since the new edit cannot be empty, set some other leaf. -+ pushed = sess.operationalChanges(); -+ auto another = "/test_module:popelnice/s"s; -+ pushed->newPath(another, "xxx"); -+ pushed = *pushed->findPath(another); -+ pushed->findPath(leaf)->unlink(); -+ // "the edit" for sysrepo must refer to a top-level node -+ while (pushed->parent()) { -+ pushed = *pushed->parent(); -+ } -+ REQUIRE(!pushed->findPath(leaf)); -+ REQUIRE(!!pushed->findPath(another)); -+ sess.editBatch(*pushed, sysrepo::DefaultOperation::Replace); -+ sess.applyChanges(); -+ REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "123"); -+ } - } - - DOCTEST_SUBCASE("edit batch") -@@ -321,6 +357,11 @@ TEST_CASE("session") - sess.discardChanges(); - } - -+ DOCTEST_SUBCASE("discard XPath") -+ { -+ sess.discardChanges(leaf); -+ } -+ - REQUIRE(sess.getPendingChanges() == std::nullopt); - } - --- -2.43.0 - diff --git a/package/sysrepo-cpp/0008-upstream-break-sr_rpc_send_tree-s-output-might-be-NU.patch b/package/sysrepo-cpp/0008-upstream-break-sr_rpc_send_tree-s-output-might-be-NU.patch deleted file mode 100644 index a20116c1..00000000 --- a/package/sysrepo-cpp/0008-upstream-break-sr_rpc_send_tree-s-output-might-be-NU.patch +++ /dev/null @@ -1,131 +0,0 @@ -From 27dc076cbdff3c9edf00abb05bdce59d1944988b Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Fri, 31 Jan 2025 17:54:06 +0100 -Subject: [PATCH 08/20] upstream break: sr_rpc_send_tree's `output` might be - NULL now -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -As of upstream commit 927fd5221bd28ff50cc1ee7c2cb5c5f30b34c2c9, sysrepo -can now return a nullptr when the resulting tree is empty. This appears -to have been done in response to our bugreport where we pointed out that -`netopeer2-server` started crashing on empty RPCs. However, it's unclear -why this necessitated a change in sysrepo, given that this feature has -worked well for years. - -Anyway, let's propagate this through the C++ layer; what else can I do, -after all. - -Bug: https://github.com/CESNET/netopeer2/issues/1695 -Change-Id: I646d19f7448d98f0d21e9eec006f28fbb5e479fc -Signed-off-by: Mattias Walström ---- - .zuul.yaml | 4 ++-- - CMakeLists.txt | 4 ++-- - include/sysrepo-cpp/Session.hpp | 2 +- - src/Session.cpp | 6 ++++-- - tests/subscriptions.cpp | 7 ++++--- - 5 files changed, 13 insertions(+), 10 deletions(-) - -diff --git a/.zuul.yaml b/.zuul.yaml -index 70c5e49..dc87c6f 100644 ---- a/.zuul.yaml -+++ b/.zuul.yaml -@@ -6,7 +6,7 @@ - - name: github/CESNET/libyang - override-checkout: devel - - name: github/sysrepo/sysrepo -- override-checkout: cesnet/2025-01-30--00 -+ override-checkout: devel - - name: github/onqtam/doctest - override-checkout: v2.4.8 - - name: github/rollbear/trompeloeil -@@ -17,7 +17,7 @@ - - name: github/CESNET/libyang - override-checkout: devel - - name: github/sysrepo/sysrepo -- override-checkout: cesnet/2025-01-30--00 -+ override-checkout: devel - - name: github/onqtam/doctest - override-checkout: v2.4.11 - - name: github/rollbear/trompeloeil -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 9e09d3d..85ff84c 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -20,7 +20,7 @@ add_custom_target(sysrepo-cpp-version-cmake - cmake/ProjectGitVersionRunner.cmake - ) - include(cmake/ProjectGitVersion.cmake) --set(SYSREPO_CPP_PKG_VERSION "4") -+set(SYSREPO_CPP_PKG_VERSION "5") - prepare_git_version(SYSREPO_CPP_VERSION ${SYSREPO_CPP_PKG_VERSION}) - - find_package(Doxygen) -@@ -29,7 +29,7 @@ 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.4.7 IMPORTED_TARGET) -+pkg_check_modules(SYSREPO REQUIRED sysrepo>=3.4.8 IMPORTED_TARGET) - - include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) - -diff --git a/include/sysrepo-cpp/Session.hpp b/include/sysrepo-cpp/Session.hpp -index 238f480..6e31fa2 100644 ---- a/include/sysrepo-cpp/Session.hpp -+++ b/include/sysrepo-cpp/Session.hpp -@@ -95,7 +95,7 @@ public: - void discardOperationalChanges(const std::optional& moduleName = std::nullopt, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); - void dropForeignOperationalContent(const std::optional& xpath); - void copyConfig(const Datastore source, const std::optional& moduleName = std::nullopt, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); -- libyang::DataNode sendRPC(libyang::DataNode input, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); -+ std::optional sendRPC(libyang::DataNode input, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); - void sendNotification(libyang::DataNode notification, const Wait wait, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); - void replaceConfig(std::optional config, const std::optional& moduleName = std::nullopt, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); - -diff --git a/src/Session.cpp b/src/Session.cpp -index 634e450..5df04bc 100644 ---- a/src/Session.cpp -+++ b/src/Session.cpp -@@ -343,13 +343,15 @@ void Session::copyConfig(const Datastore source, const std::optional Session::sendRPC(libyang::DataNode input, std::chrono::milliseconds timeout) - { - sr_data_t* output; - auto res = sr_rpc_send_tree(m_sess.get(), libyang::getRawNode(input), timeout.count(), &output); - throwIfError(res, "Couldn't send RPC", m_sess.get()); - -- assert(output); // TODO: sysrepo always gives the RPC node? (even when it has not output or output nodes?) -+ if (!output) { -+ return std::nullopt; -+ } - return wrapSrData(m_sess, output); - } - -diff --git a/tests/subscriptions.cpp b/tests/subscriptions.cpp -index 36fd61c..6fc9cbb 100644 ---- a/tests/subscriptions.cpp -+++ b/tests/subscriptions.cpp -@@ -420,9 +420,10 @@ TEST_CASE("subscriptions") - if (ret == sysrepo::ErrorCode::Ok) { - auto output = sess.sendRPC(sess.getContext().newPath(rpcPath)); - if (setFunction) { -- REQUIRE(output.findPath("/test_module:shutdown/success", libyang::InputOutputNodes::Output)); -- } else { -- REQUIRE(!output.findPath("/test_module:shutdown/success", libyang::InputOutputNodes::Output).has_value()); -+ REQUIRE(!!output); -+ REQUIRE(output->findPath("/test_module:shutdown/success", libyang::InputOutputNodes::Output)); -+ } else if (output) { -+ REQUIRE(!output->findPath("/test_module:shutdown/success", libyang::InputOutputNodes::Output).has_value()); - } - } else { - REQUIRE_THROWS(sess.sendRPC(sess.getContext().newPath(rpcPath))); --- -2.43.0 - diff --git a/package/sysrepo-cpp/0009-tests-adapt-to-upstream-breaking-change-in-sr_get_da.patch b/package/sysrepo-cpp/0009-tests-adapt-to-upstream-breaking-change-in-sr_get_da.patch deleted file mode 100644 index 54d1dde3..00000000 --- a/package/sysrepo-cpp/0009-tests-adapt-to-upstream-breaking-change-in-sr_get_da.patch +++ /dev/null @@ -1,83 +0,0 @@ -From dc8facb5befbd17f2848098bbc9267c9d9f58e2d Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Mon, 10 Mar 2025 12:08:47 +0100 -Subject: [PATCH 09/20] tests: adapt to upstream breaking change in sr_get_data -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -It seems that since upstream commit ba8897ce3 [1] the max_depth -parameter started behaving differently. It now behaves as the -documentation [2] states, i.e: - - Maximum depth of the selected subtrees. 0 is unlimited, 1 will not - return any descendant nodes. If a list should be returned, its keys - are always returned as well. - -The previous behaviour was probably wrong. When reported via slack, -another fix was issued [3], so let's depend on that as well. - -[1] https://github.com/sysrepo/sysrepo/commit/ba8897ce3da0bc8203d5b964851fd60fb164760d -[2] https://netopeer.liberouter.org/doc/sysrepo/devel/html/group__get__data__api.html -[3] https://github.com/sysrepo/sysrepo/commit/309b34d28cc09771cf488d04478161f684f4b8a7 - -Change-Id: I337dc1f1f77b9501dda29931846116050123bcad -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 2 +- - tests/session.cpp | 11 +++++++++-- - 2 files changed, 10 insertions(+), 3 deletions(-) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 85ff84c..fb132a2 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -29,7 +29,7 @@ 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.4.8 IMPORTED_TARGET) -+pkg_check_modules(SYSREPO REQUIRED sysrepo>=3.6.5 IMPORTED_TARGET) - - include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) - -diff --git a/tests/session.cpp b/tests/session.cpp -index 2637f67..5be0d79 100644 ---- a/tests/session.cpp -+++ b/tests/session.cpp -@@ -127,6 +127,13 @@ TEST_CASE("session") - data = sess.getData("/test_module:popelnice", 1); - REQUIRE(data); - REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({ -+ "test_module:popelnice": {} -+} -+)"); -+ -+ data = sess.getData("/test_module:popelnice", 2); -+ REQUIRE(data); -+ REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({ - "test_module:popelnice": { - "content": {} - } -@@ -134,7 +141,7 @@ TEST_CASE("session") - )"); - - // If a list should be returned, its keys are always returned as well. -- data = sess.getData("/test_module:popelnice", 2); -+ data = sess.getData("/test_module:popelnice", 3); - REQUIRE(data); - REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({ - "test_module:popelnice": { -@@ -152,7 +159,7 @@ TEST_CASE("session") - } - )"); - -- data = sess.getData("/test_module:popelnice", 3); -+ data = sess.getData("/test_module:popelnice", 4); - REQUIRE(data); - REQUIRE(*data->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({ - "test_module:popelnice": { --- -2.43.0 - diff --git a/package/sysrepo-cpp/0010-Utilities-for-working-with-existing-sysrepo-discard-.patch b/package/sysrepo-cpp/0010-Utilities-for-working-with-existing-sysrepo-discard-.patch deleted file mode 100644 index fc92376e..00000000 --- a/package/sysrepo-cpp/0010-Utilities-for-working-with-existing-sysrepo-discard-.patch +++ /dev/null @@ -1,222 +0,0 @@ -From 6ed5442db482a9a37128add02778aebd096f9207 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Fri, 14 Mar 2025 16:20:09 +0100 -Subject: [PATCH 10/20] Utilities for working with existing - sysrepo:discard-items nodes -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -findMatchingDiscard() and findMatchingDiscardPrefixes() are useful for -figuring out which nodes need to be prune when adding/modifying a -discarding node. - -unlinkFromForest() is useful for removing "a node" from a forest of data -node trees. Sometimes the node to be deleted *could* be a root node -which is also the first sibling among its collection of siblings, and in -that case some extra care is needed to ensure that we still have a -reference to something after we've done with the unlinking. - -In the real world this doesn't happen that often because there usually -is something like a top-level container node below which we're -processing these discards. Still, let's put this utility function into a -single place. - -Depends-on: https://gerrit.cesnet.cz/c/CzechLight/libyang-cpp/+/8436 -Change-Id: Ib346be6887f75220a8a6435864c74aef8d5925bb -Signed-off-by: Mattias Walström ---- - include/sysrepo-cpp/utils/utils.hpp | 3 ++ - src/Session.cpp | 3 ++ - src/utils/utils.cpp | 57 ++++++++++++++++++++++ - tests/session.cpp | 74 +++++++++++++++++++++++++++-- - 4 files changed, 134 insertions(+), 3 deletions(-) - -diff --git a/include/sysrepo-cpp/utils/utils.hpp b/include/sysrepo-cpp/utils/utils.hpp -index 22d7d66..64b1c2e 100644 ---- a/include/sysrepo-cpp/utils/utils.hpp -+++ b/include/sysrepo-cpp/utils/utils.hpp -@@ -12,4 +12,7 @@ - namespace sysrepo { - Session wrapUnmanagedSession(sr_session_ctx_s* session); - void setLogLevelStderr(const LogLevel); -+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/Session.cpp b/src/Session.cpp -index 5df04bc..3c21e84 100644 ---- a/src/Session.cpp -+++ b/src/Session.cpp -@@ -153,6 +153,7 @@ void Session::deleteItem(const std::string& path, const EditOptions opts) - * Wraps `sr_discard_items`. - * - * @param xpath Expression filtering the nodes to discard, nullopt for all nodes. -+ * @see findMatchingDiscard() - */ - void Session::dropForeignOperationalContent(const std::optional& xpath) - { -@@ -167,6 +168,8 @@ void Session::dropForeignOperationalContent(const std::optional& xp - * with the "replace" operation. - * - * Wraps `sr_get_oper_changes`. -+ * -+ * @see findMatchingDiscard() - */ - std::optional Session::operationalChanges(const std::optional& moduleName) const - { -diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp -index 972a4fa..e339e88 100644 ---- a/src/utils/utils.cpp -+++ b/src/utils/utils.cpp -@@ -72,4 +72,61 @@ void checkNoThreadFlag(const SubscribeOptions opts, const std::optional findMatchingDiscard(libyang::DataNode root, const std::string& xpath) -+{ -+ auto discard = root.firstOpaqueSibling(); -+ while (discard) { -+ if (discard->name().matches("sysrepo", "discard-items") && discard->value() == xpath) { -+ return discard; -+ } -+ if (auto next = discard->nextSibling()) { -+ discard = next->asOpaque(); -+ } else { -+ break; -+ } -+ } -+ return std::nullopt; -+} -+ -+/** -+ * @short Find all sysrepo:discard-items nodes which match the given XPath or the descendats of this XPath -+ */ -+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; -+ auto discard = root.firstOpaqueSibling(); -+ while (discard) { -+ if (discard->name().matches("sysrepo", "discard-items")) { -+ if (auto text = discard->value(); text == xpathPrefix || text.starts_with(withSlash) || text.starts_with(withBracket)) { -+ res.emplace_back(*discard); -+ } -+ } -+ if (auto next = discard->nextSibling()) { -+ discard = next->asOpaque(); -+ } else { -+ break; -+ } -+ } -+ return res; -+} -+ -+/** -+ * @short Remove a node from a forest of tree nodes while modifying the root in-place -+ */ -+void unlinkFromForest(std::optional& root, libyang::DataNode node) -+{ -+ if (node == root) { -+ root = node.nextSibling(); -+ } -+ node.unlink(); -+} -+ - } -diff --git a/tests/session.cpp b/tests/session.cpp -index 5be0d79..9a419a1 100644 ---- a/tests/session.cpp -+++ b/tests/session.cpp -@@ -245,9 +245,77 @@ TEST_CASE("session") - sess.applyChanges(); - REQUIRE(!sess.getData(leaf)); - -- // Using discardOperationalChanges makes the leaf visible again (in the operational datastore). -- // Also, no need to applyChanges(). -- sess.discardOperationalChanges("test_module"); -+ // check that a magic sysrepo:discard-items node is in place -+ REQUIRE(!!sess.operationalChanges()); -+ auto matchingDiscard = sysrepo::findMatchingDiscard(*sess.operationalChanges(), leaf); -+ REQUIRE(!!matchingDiscard); -+ REQUIRE(matchingDiscard->value() == leaf); -+ REQUIRE(matchingDiscard->name().moduleOrNamespace == "sysrepo"); -+ REQUIRE(matchingDiscard->name().name == "discard-items"); -+ REQUIRE(!sysrepo::findMatchingDiscard(*sess.operationalChanges(), "something else")); -+ -+ DOCTEST_SUBCASE("forget changes via discardOperationalChanges(module)") -+ { -+ // Using discardOperationalChanges makes the leaf visible again (in the operational datastore). -+ // Also, no need to applyChanges(). -+ sess.discardOperationalChanges("test_module"); -+ } -+ -+ DOCTEST_SUBCASE("forget changes via a selective edit") -+ { -+ // this edit only has a single node, which means that we cannot really call unlink() and hope for a sane result -+ REQUIRE(matchingDiscard->firstSibling() == *matchingDiscard); -+ -+ // so, we add a dummy node instead... -+ auto root = matchingDiscard->newPath("/test_module:popelnice/s", "foo"); -+ // ...and only then we nuke the eixtsing discard-items node -+ matchingDiscard->unlink(); -+ sess.editBatch(*root, sysrepo::DefaultOperation::Replace); -+ sess.applyChanges(); -+ } -+ -+ DOCTEST_SUBCASE("multiple sysrepo:discard-items nodes") -+ { -+ sess.dropForeignOperationalContent("/test_module:popelnice"); -+ sess.dropForeignOperationalContent("/test_module:popelnice/s"); -+ sess.dropForeignOperationalContent("/test_module:values"); -+ sess.dropForeignOperationalContent("/test_module:popelnice/content"); -+ sess.dropForeignOperationalContent("/test_module:denyAllLeaf"); -+ sess.dropForeignOperationalContent(leaf); // yup, once more, in addition to the one at the very beginning -+ sess.applyChanges(); -+ -+ auto forPopelnice = sysrepo::findMatchingDiscard(*sess.operationalChanges(), "/test_module:popelnice"); -+ REQUIRE(!!forPopelnice); -+ REQUIRE(forPopelnice->value() == "/test_module:popelnice"); -+ auto oneMatch = sysrepo::findMatchingDiscard(*sess.operationalChanges(), "/test_module:values"); -+ REQUIRE(!!oneMatch); -+ REQUIRE(oneMatch->value() == "/test_module:values"); -+ -+ auto atOrBelowPopelnice = sysrepo::findMatchingDiscardPrefixes(*sess.operationalChanges(), "/test_module:popelnice"); -+ REQUIRE(atOrBelowPopelnice.size() == 3); -+ // yup, these are apparently backwards compared to how I put them in. Never mind. -+ REQUIRE(atOrBelowPopelnice[2].value() == "/test_module:popelnice"); -+ REQUIRE(atOrBelowPopelnice[1].value() == "/test_module:popelnice/s"); -+ REQUIRE(atOrBelowPopelnice[0].value() == "/test_module:popelnice/content"); -+ -+ auto belowPopelnice = sysrepo::findMatchingDiscardPrefixes(*sess.operationalChanges(), "/test_module:popelnice/"); -+ REQUIRE(belowPopelnice.size() == 2); -+ // again, the order is reversed -+ REQUIRE(belowPopelnice[1].value() == "/test_module:popelnice/s"); -+ REQUIRE(belowPopelnice[0].value() == "/test_module:popelnice/content"); -+ -+ auto newEdit = sess.operationalChanges(); -+ auto forLeaf = sysrepo::findMatchingDiscardPrefixes(*newEdit, leaf); -+ REQUIRE(forLeaf.size() == 2); -+ REQUIRE(forLeaf[0].value() == leaf); -+ REQUIRE(forLeaf[1].value() == leaf); -+ for (auto node : forLeaf) { -+ sysrepo::unlinkFromForest(newEdit, node); -+ } -+ sess.editBatch(*newEdit, sysrepo::DefaultOperation::Replace); -+ sess.applyChanges(); -+ } -+ - REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "123"); - } - --- -2.43.0 - diff --git a/package/sysrepo-cpp/0011-Fix-a-typo.patch b/package/sysrepo-cpp/0011-Fix-a-typo.patch deleted file mode 100644 index b4078ed0..00000000 --- a/package/sysrepo-cpp/0011-Fix-a-typo.patch +++ /dev/null @@ -1,36 +0,0 @@ -From aa78815b05090df63bef9ac1b6473f32aac5363d Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Tue, 18 Mar 2025 11:25:05 +0100 -Subject: [PATCH 11/20] Fix a typo -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -...because I don't want to go through the CI again, hence a separate -commit. - -Fixes: 6ed5442 Utilities for working with existing sysrepo:discard-items nodes -Reported-by: Tomáš Pecka -Change-Id: I31f3efc1006fe222b7853b24a116b3d8cc04dc8d -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 e339e88..ff4a32c 100644 ---- a/src/utils/utils.cpp -+++ b/src/utils/utils.cpp -@@ -95,7 +95,7 @@ std::optional findMatchingDiscard(libyang::DataNode root, con - } - - /** -- * @short Find all sysrepo:discard-items nodes which match the given XPath or the descendats of this XPath -+ * @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) - { --- -2.43.0 - diff --git a/package/sysrepo-cpp/0012-wrap-dynamic-subscription-functions.patch b/package/sysrepo-cpp/0012-wrap-dynamic-subscription-functions.patch deleted file mode 100644 index 01516ad2..00000000 --- a/package/sysrepo-cpp/0012-wrap-dynamic-subscription-functions.patch +++ /dev/null @@ -1,7598 +0,0 @@ -From 415c401bc12070dfa3b7705e9a2c0c99d899411c Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Wed, 27 Nov 2024 14:40:20 +0100 -Subject: [PATCH 12/20] wrap dynamic subscription functions -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -This patch wraps the dynamic subscription functions. We add a new -handle for the subscription that contains the file descriptor where -clients are supposed to send the notifications. - -We tried to reuse the current Subscription class for dynamic -subscriptions as well and not let sysrepo create its own internal -background threads. However, subscribing to yang push periodic timer -creates a new thread in sysrepo anyway, so completely decoupling from -the C API would be much work. - -This patch does not yet wrap the functions for modifying subscriptions. - -The new DynamicSubscription class implements pimpl idiom. The class is -movable. When the subscription is moved, the destructor is called on -the old object. However, the destructor is supposed to close the file -descriptor. We need to make sure that the file descriptor is not closed -twice. -I was thinking about using std::unique_ptr to manage the file descriptor -or by setting the fd to something "invalid" after the move. I found the -former solution more elegant. - -Maybe we should refactor the Subscription class to use pimpl idiom as -well. - -Depends-on: https://gerrit.cesnet.cz/c/CzechLight/libyang-cpp/+/8149 -Change-Id: I1cee5e730fcc762b2e0a58d3d5c40bf358504397 -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 10 + - include/sysrepo-cpp/Session.hpp | 16 + - include/sysrepo-cpp/Subscription.hpp | 49 + - src/Session.cpp | 118 ++ - src/Subscription.cpp | 101 ++ - tests/subscriptions-dynamic.cpp | 475 +++++ - tests/test_module.yang | 4 + - tests/yang/iana-if-type@2014-05-08.yang | 1523 +++++++++++++++++ - tests/yang/ietf-interfaces@2018-02-20.yang | 1123 ++++++++++++ - tests/yang/ietf-ip@2018-02-22.yang | 876 ++++++++++ - .../ietf-network-instance@2019-01-21.yang | 282 +++ - tests/yang/ietf-restconf@2017-01-26.yang | 278 +++ - ...f-subscribed-notifications@2019-09-09.yang | 1350 +++++++++++++++ - tests/yang/ietf-yang-patch@2017-02-22.yang | 390 +++++ - tests/yang/ietf-yang-push@2019-09-09.yang | 797 +++++++++ - 15 files changed, 7392 insertions(+) - create mode 100644 tests/subscriptions-dynamic.cpp - create mode 100644 tests/yang/iana-if-type@2014-05-08.yang - create mode 100644 tests/yang/ietf-interfaces@2018-02-20.yang - create mode 100644 tests/yang/ietf-ip@2018-02-22.yang - create mode 100644 tests/yang/ietf-network-instance@2019-01-21.yang - create mode 100644 tests/yang/ietf-restconf@2017-01-26.yang - create mode 100644 tests/yang/ietf-subscribed-notifications@2019-09-09.yang - create mode 100644 tests/yang/ietf-yang-patch@2017-02-22.yang - create mode 100644 tests/yang/ietf-yang-push@2019-09-09.yang - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index fb132a2..01ec967 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -76,9 +76,19 @@ if(BUILD_TESTING) - set(fixture-test-module - --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_module.yang - ) -+ set(fixture-dynamic-subscriptions -+ --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_module.yang -+ --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/iana-if-type@2014-05-08.yang -+ --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-interfaces@2018-02-20.yang -+ --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-ip@2018-02-22.yang -+ --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-network-instance@2019-01-21.yang -+ --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-subscribed-notifications@2019-09-09.yang -e replay -+ --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-yang-push@2019-09-09.yang -e on-change -+ ) - - sysrepo_cpp_test(NAME session FIXTURE fixture-test-module) - sysrepo_cpp_test(NAME subscriptions FIXTURE fixture-test-module LIBRARIES Threads::Threads) -+ sysrepo_cpp_test(NAME subscriptions-dynamic FIXTURE fixture-dynamic-subscriptions LIBRARIES PkgConfig::SYSREPO) - sysrepo_cpp_test(NAME unsafe FIXTURE fixture-test-module LIBRARIES PkgConfig::SYSREPO) - endif() - -diff --git a/include/sysrepo-cpp/Session.hpp b/include/sysrepo-cpp/Session.hpp -index 6e31fa2..255f861 100644 ---- a/include/sysrepo-cpp/Session.hpp -+++ b/include/sysrepo-cpp/Session.hpp -@@ -136,6 +136,22 @@ public: - ExceptionHandler handler = nullptr, - const std::optional& callbacks = std::nullopt); - -+ [[nodiscard]] DynamicSubscription yangPushPeriodic( -+ const std::optional& xpathFilter, -+ std::chrono::milliseconds periodTime, -+ const std::optional& anchorTime = std::nullopt, -+ const std::optional& stopTime = std::nullopt); -+ [[nodiscard]] DynamicSubscription yangPushOnChange( -+ const std::optional& xpathFilter, -+ const std::optional& dampeningPeriod = std::nullopt, -+ SyncOnStart syncOnStart = SyncOnStart::No, -+ const std::optional& stopTime = std::nullopt); -+ [[nodiscard]] DynamicSubscription subscribeNotifications( -+ const std::optional& xpathFilter, -+ const std::optional& stream = std::nullopt, -+ const std::optional& stopTime = std::nullopt, -+ const std::optional& startTime = std::nullopt); -+ - ChangeCollection getChanges(const std::string& xpath = "//."); - void setErrorMessage(const std::string& msg); - void setNetconfError(const NetconfErrorInfo& info); -diff --git a/include/sysrepo-cpp/Subscription.hpp b/include/sysrepo-cpp/Subscription.hpp -index 899af03..7fdc08b 100644 ---- a/include/sysrepo-cpp/Subscription.hpp -+++ b/include/sysrepo-cpp/Subscription.hpp -@@ -189,6 +189,13 @@ using RpcActionCb = std::function notificationTree, const NotificationTimeStamp timestamp)>; - -+/** -+ * A callback for YANG push notification subscriptions. -+ * @param notification The notification tree. -+ * @param timestamp Time when the notification was generated. -+ */ -+using YangPushNotifCb = std::function notificationTree, const NotificationTimeStamp timestamp)>; -+ - /** - * Exception handler type for handling exceptions thrown in user callbacks. - */ -@@ -268,4 +275,46 @@ private: - - bool m_didNacmInit; - }; -+ -+enum class SyncOnStart : bool { -+ No, -+ Yes, -+}; -+ -+/** -+ * @brief Manages lifetime of YANG push subscriptions. -+ * -+ * Users are supposed to create instances of this class via Session::yangPushPeriodic, Session::yangPushOnChange or Session::subscribeNotifications. -+ * Whenever notified about a change (by polling the file descriptor obtained by fd() function), -+ * there is at least one event waiting to be processed by a call to YangPushSubscription::processEvent. -+ * -+ * Internally, the sysrepo C library creates some background thread(s). These are used either for managing internal, -+ * sysrepo-level module subscriptions, or for scheduling of periodic timers. These threads are fully encapsulated by -+ * the C code, and there is no control over them from this C++ wrapper. The public interface of this class is a file -+ * descriptor that the caller is expected to poll for readability/closing (and the subscription ID). Once the FD is -+ * readable, invoke this class' processEvents(). There is no automatic event loop which would take care of this -+ * functionality, and users are expected to integrate this FD into their own event handling. -+ */ -+class DynamicSubscription { -+public: -+ DynamicSubscription(const DynamicSubscription&) = delete; -+ DynamicSubscription& operator=(const DynamicSubscription&) = delete; -+ DynamicSubscription(DynamicSubscription&&) noexcept; -+ DynamicSubscription& operator=(DynamicSubscription&&) noexcept; -+ ~DynamicSubscription(); -+ -+ int fd() const; -+ uint64_t subscriptionId() const; -+ std::optional replayStartTime() const; -+ void processEvent(YangPushNotifCb cb) const; -+ void terminate(const std::optional& reason = std::nullopt); -+ -+private: -+ DynamicSubscription(std::shared_ptr sess, int fd, uint64_t subId, const std::optional& replayStartTime = std::nullopt); -+ -+ struct Data; -+ std::unique_ptr m_data; -+ -+ friend class Session; -+}; - } -diff --git a/src/Session.cpp b/src/Session.cpp -index 3c21e84..2c27c92 100644 ---- a/src/Session.cpp -+++ b/src/Session.cpp -@@ -11,6 +11,7 @@ extern "C" { - #include - #include - #include -+#include - } - #include - #include -@@ -522,6 +523,123 @@ Subscription Session::onNotification( - return sub; - } - -+/** -+ * Subscribe for receiving notifications according to 'ietf-yang-push' YANG periodic subscriptions. -+ * -+ * Wraps `srsn_yang_push_periodic`. -+ * -+ * @param xpathFilter Optional XPath that filters received notification. -+ * @param periodTime Notification period. -+ * @param anchorTime Optional anchor time for the period. Anchor time acts as a reference point for the period. -+ * @param stopTime Optional stop time ending the notification subscription. -+ * -+ * @return A YangPushSubscription handle. -+ */ -+DynamicSubscription Session::yangPushPeriodic( -+ const std::optional& xpathFilter, -+ std::chrono::milliseconds periodTime, -+ const std::optional& anchorTime, -+ const std::optional& stopTime) -+{ -+ int fd; -+ uint32_t subId; -+ auto stopSpec = stopTime ? std::optional{toTimespec(*stopTime)} : std::nullopt; -+ auto anchorSpec = anchorTime ? std::optional{toTimespec(*anchorTime)} : std::nullopt; -+ auto res = srsn_yang_push_periodic(m_sess.get(), -+ toDatastore(activeDatastore()), -+ xpathFilter ? xpathFilter->c_str() : nullptr, -+ periodTime.count(), -+ anchorSpec ? &anchorSpec.value() : nullptr, -+ stopSpec ? &stopSpec.value() : nullptr, -+ &fd, -+ &subId); -+ throwIfError(res, "Couldn't create yang-push periodic subscription", m_sess.get()); -+ -+ return {m_sess, fd, subId}; -+} -+ -+/** -+ * Subscribe for receiving notifications according to 'ietf-yang-push' YANG on-change subscriptions. -+ * -+ * Wraps `srsn_yang_push_on_change`. -+ * -+ * @param xpathFilter Optional XPath that filters received notification. -+ * @param dampeningPeriod Optional dampening period. -+ * @param syncOnStart Whether to start with a notification of the current state. -+ * @param stopTime Optional stop time ending the notification subscription. -+ * -+ * @return A YangPushSubscription handle. -+ */ -+DynamicSubscription Session::yangPushOnChange( -+ const std::optional& xpathFilter, -+ const std::optional& dampeningPeriod, -+ SyncOnStart syncOnStart, -+ const std::optional& stopTime) -+{ -+ int fd; -+ uint32_t subId; -+ auto stopSpec = stopTime ? std::optional{toTimespec(*stopTime)} : std::nullopt; -+ auto res = srsn_yang_push_on_change(m_sess.get(), -+ toDatastore(activeDatastore()), -+ xpathFilter ? xpathFilter->c_str() : nullptr, -+ dampeningPeriod ? dampeningPeriod->count() : 0, -+ syncOnStart == SyncOnStart::Yes, -+ nullptr, -+ stopSpec ? &stopSpec.value() : nullptr, -+ 0, -+ nullptr, -+ &fd, -+ &subId); -+ throwIfError(res, "Couldn't create yang-push on-change subscription", m_sess.get()); -+ -+ return {m_sess, fd, subId}; -+} -+ -+/** -+ * Subscribe for receiving notifications according to 'ietf-subscribed-notifications' -+ * -+ * Wraps `srsn_subscribe. -+ * -+ * @param xpathFilter Optional XPath that filters received notification. -+ * @param stream Optional stream to subscribe to. -+ * @param stopTime Optional stop time ending the subscription. -+ * @param startTime Optional start time of the subscription, used for replaying stored notifications. -+ * -+ * @return A YangPushSubscription handle. -+ */ -+DynamicSubscription Session::subscribeNotifications( -+ const std::optional& xpathFilter, -+ const std::optional& stream, -+ const std::optional& stopTime, -+ const std::optional& startTime) -+{ -+ int fd; -+ uint32_t subId; -+ auto stopSpec = stopTime ? std::optional{toTimespec(*stopTime)} : std::nullopt; -+ auto startSpec = startTime ? std::optional{toTimespec(*startTime)} : std::nullopt; -+ struct timespec replayStartSpec; -+ -+ auto res = srsn_subscribe(m_sess.get(), -+ stream ? stream->c_str() : nullptr, -+ xpathFilter ? xpathFilter->c_str() : nullptr, -+ stopSpec ? &stopSpec.value() : nullptr, -+ startSpec ? &startSpec.value() : nullptr, -+ false, -+ nullptr, -+ &replayStartSpec, -+ &fd, -+ &subId); -+ -+ throwIfError(res, "Couldn't create notification subscription", m_sess.get()); -+ -+ std::optional replayStart; -+ if (replayStartSpec.tv_sec != 0) { -+ replayStart = toTimePoint(replayStartSpec); -+ } -+ -+ return {m_sess, fd, subId, replayStart}; -+} -+ - /** - * Returns a collection of changes based on an `xpath`. Use "//." to get a full change subtree. - * -diff --git a/src/Subscription.cpp b/src/Subscription.cpp -index f71117e..ab27b16 100644 ---- a/src/Subscription.cpp -+++ b/src/Subscription.cpp -@@ -11,6 +11,7 @@ - extern "C" { - #include - #include -+#include - } - #include "utils/enum.hpp" - #include "utils/exception.hpp" -@@ -410,4 +411,104 @@ bool ChangeIterator::operator==(const ChangeIterator& other) const - // And then either both contain nothing or contain the same thing. - (!this->m_current.has_value() || this->m_current->node == other.m_current->node); - } -+ -+ -+struct DynamicSubscription::Data { -+ std::shared_ptr sess; -+ int fd; -+ uint64_t subId; -+ std::optional m_replayStartTime; -+ bool m_terminated; -+ -+ Data(std::shared_ptr sess, int fd, uint64_t subId, const std::optional& replayStartTime, bool terminated); -+ ~Data(); -+ void terminate(const std::optional& reason = std::nullopt); -+}; -+ -+DynamicSubscription::DynamicSubscription(std::shared_ptr sess, int fd, uint64_t subId, const std::optional& replayStartTime) -+ : m_data(std::make_unique(std::move(sess), fd, subId, replayStartTime, false)) -+{ -+} -+ -+DynamicSubscription::DynamicSubscription(DynamicSubscription&&) noexcept = default; -+DynamicSubscription& DynamicSubscription::operator=(DynamicSubscription&&) noexcept = default; -+DynamicSubscription::~DynamicSubscription() = default; -+ -+/** @brief Returns the file descriptor associated with this subscription. */ -+int DynamicSubscription::fd() const -+{ -+ return m_data->fd; -+} -+ -+/** @brief Returns the subscription ID associated with this subscription. */ -+uint64_t DynamicSubscription::subscriptionId() const -+{ -+ return m_data->subId; -+} -+ -+/** @brief Returns the actual start time of replayed notification subscription, if available. */ -+std::optional DynamicSubscription::replayStartTime() const -+{ -+ return m_data->m_replayStartTime; -+} -+ -+/** @brief Terminates the subscription. -+ * -+ * Wraps `srsn_terminate`. -+ */ -+void DynamicSubscription::terminate(const std::optional& reason) -+{ -+ m_data->terminate(reason); -+} -+ -+/** @brief Processes a single event associated with this subscription. -+ * -+ * Invoke only when the file descriptor associated with this subscription is ready for reading. -+ * Otherwise, the function blocks unless the FD is set to non-blocking. -+ * -+ * Wraps `srsn_read_notif`. -+ */ -+void DynamicSubscription::processEvent(YangPushNotifCb cb) const -+{ -+ struct timespec timestamp; -+ struct lyd_node* tree; -+ auto ctx = std::unique_ptr>(sr_session_acquire_context(m_data->sess.get()), [&](const ly_ctx*) { sr_session_release_context(m_data->sess.get()); }); -+ -+ auto err = srsn_read_notif(fd(), ctx.get(), ×tamp, &tree); -+ throwIfError(err, "Couldn't read yang-push notification"); -+ -+ const auto wrappedNotification = tree ? std::optional{libyang::wrapRawNode(tree)} : std::nullopt; -+ -+ // If we see subscription-terminated notification, the subscription is terminated by sysrepo (e.g. because of stop-time) -+ // This is not the correct notification as per RFC to use when reaching stop-time, see https://github.com/sysrepo/sysrepo/issues/3525. -+ if (wrappedNotification && wrappedNotification->path() == "/ietf-subscribed-notifications:subscription-terminated") { -+ m_data->m_terminated = true; -+ } -+ -+ cb(wrappedNotification, toTimePoint(timestamp)); -+} -+ -+DynamicSubscription::Data::Data(std::shared_ptr sess, int fd, uint64_t subId, const std::optional& replayStartTime, bool terminated) -+ : sess(std::move(sess)) -+ , fd(fd) -+ , subId(subId) -+ , m_replayStartTime(replayStartTime) -+ , m_terminated(terminated) -+{ -+} -+ -+DynamicSubscription::Data::~Data() -+{ -+ if (!m_terminated) { -+ terminate(); -+ } -+ close(fd); -+} -+ -+void DynamicSubscription::Data::terminate(const std::optional& reason) -+{ -+ auto err = srsn_terminate(subId, reason ? reason->c_str() : nullptr); -+ throwIfError(err, "Couldn't terminate yang-push subscription with id " + std::to_string(subId)); -+ m_terminated = true; -+} - } -diff --git a/tests/subscriptions-dynamic.cpp b/tests/subscriptions-dynamic.cpp -new file mode 100644 -index 0000000..a44bfba ---- /dev/null -+++ b/tests/subscriptions-dynamic.cpp -@@ -0,0 +1,475 @@ -+/* -+ * Copyright (C) 2024 CESNET, https://photonics.cesnet.cz/ -+ * -+ * Written by Tomáš Pecka -+ * -+ * SPDX-License-Identifier: BSD-3-Clause -+ */ -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include "sysrepo-cpp/Subscription.hpp" -+#include "utils.hpp" -+ -+#define CLIENT_SEND_NOTIFICATION(NOTIFICATION) \ -+ { \ -+ auto notif = client.getContext().parseOp(NOTIFICATION, libyang::DataFormat::JSON, libyang::OperationType::NotificationYang); \ -+ client.sendNotification(*notif.tree, sysrepo::Wait::Yes); \ -+ } -+ -+#define REQUIRE_PIPE_HANGUP(SUBSCRIPTION) \ -+ REQUIRE(pipeStatus((SUBSCRIPTION).fd()) == PipeStatus::Hangup) -+ -+#define REQUIRE_NOTIFICATION(SUBSCRIPTION, NOTIFICATION) \ -+ TROMPELOEIL_REQUIRE_CALL(rec, recordNotification(NOTIFICATION)).IN_SEQUENCE(seq); -+ -+#define READ_NOTIFICATION(SUBSCRIPTION) \ -+ REQUIRE(pipeStatus((SUBSCRIPTION).fd()) == PipeStatus::DataReady); \ -+ (SUBSCRIPTION).processEvent(cbNotif); -+ -+#define READ_NOTIFICATION_BLOCKING(SUBSCRIPTION) \ -+ REQUIRE(pipeStatus((SUBSCRIPTION).fd(), -1) == PipeStatus::DataReady); \ -+ (SUBSCRIPTION).processEvent(cbNotif); -+ -+#define SUBSCRIPTION_TERMINATED(SUBSCRIPTION) R"({ -+ "ietf-subscribed-notifications:subscription-terminated": { -+ "id": )" + std::to_string((SUBSCRIPTION).subscriptionId()) + R"(, -+ "reason": "no-such-subscription" -+ } -+} -+)" -+ -+#define REPLAY_COMPLETED(SUBSCRIPTION) R"({ -+ "ietf-subscribed-notifications:replay-completed": { -+ "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); -+ -+#define READ_YANG_PUSH_UPDATE(SUBSCRIPTION) \ -+ REQUIRE(pipeStatus((SUBSCRIPTION).fd()) == PipeStatus::DataReady); \ -+ (SUBSCRIPTION).processEvent(cbYangPushUpdate); -+ -+ -+using namespace std::chrono_literals; -+ -+namespace trompeloeil { -+template <> -+struct printer> { -+ static void print(std::ostream& os, const std::optional& b) -+ { -+ if (!b) { -+ os << "std::nullopt"; -+ } else { -+ os << *b; -+ } -+ } -+}; -+} -+ -+class Recorder { -+public: -+ TROMPELOEIL_MAKE_CONST_MOCK1(recordNotification, void(std::optional)); -+ TROMPELOEIL_MAKE_CONST_MOCK2(recordYangPushUpdate, void(uint32_t, std::optional)); -+}; -+ -+enum class PipeStatus { -+ NoData, -+ DataReady, -+ Hangup, -+ Other, // represents any other poll() result, we are not interested in details -+}; -+ -+/** Check the status of the pipe for reading. -+ * -+ * @param timeout Timeout in milliseconds, 0 means check the status right now, -1 means blocking wait for an event, see poll(2) -+ */ -+PipeStatus pipeStatus(int fd, int timeout = 0) -+{ -+ pollfd fds = {.fd = fd, .events = POLLIN | POLLHUP, .revents = 0}; -+ -+ int r = poll(&fds, 1, timeout); -+ INFO("poll returned " << r << ", revents: " << fds.revents); -+ if (r == 0) { -+ return PipeStatus::NoData; -+ } else if (r == 1 && fds.revents & POLLIN) { -+ return PipeStatus::DataReady; -+ } else if (r == 1 && fds.revents & POLLHUP) { -+ return PipeStatus::Hangup; -+ } else { -+ return PipeStatus::Other; -+ } -+} -+ -+TEST_CASE("Dynamic subscriptions") -+{ -+ trompeloeil::sequence seq; -+ Recorder rec; -+ -+ sysrepo::setLogLevelStderr(sysrepo::LogLevel::Information); -+ sysrepo::Connection conn{}; -+ auto sess = conn.sessionStart(); -+ auto client = conn.sessionStart(); -+ sess.sendRPC(sess.getContext().newPath("/ietf-factory-default:factory-reset")); -+ -+ auto cbYangPushUpdate = [&](const std::optional& tree, auto) { -+ REQUIRE(tree); -+ -+ // the yang-push notification contains subscription id. This is a hack that enables us to compare string representations -+ // the subscription is compared using another parameter in the expectation -+ auto idNode = tree->findPath("id"); -+ const auto subId = std::get(idNode->asTerm().value()); -+ idNode->unlink(); -+ -+ rec.recordYangPushUpdate(subId, tree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)); -+ }; -+ -+ auto cbNotif = [&](const std::optional& tree, auto) { -+ REQUIRE(tree); -+ rec.recordNotification(tree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)); -+ }; -+ -+ // write some initial data -+ client.setItem("/test_module:values[.='2']", std::nullopt); -+ client.setItem("/test_module:values[.='3']", std::nullopt); -+ client.applyChanges(); -+ -+ DOCTEST_SUBCASE("Subscribed notifications") -+ { -+ /* notification JSONs are very verbose, let's give them names even if they are not very descriptive -+ * but still better than polluting the tests below with repeated long R-strings */ -+ const std::vector notifications = { -+ R"({ -+ "test_module:ping": { -+ "myLeaf": 1 -+ } -+} -+)", -+ R"({ -+ "test_module:silent-ping": {} -+} -+)", -+ R"({ -+ "test_module:ping": { -+ "myLeaf": 2 -+ } -+} -+)"}; -+ -+ DOCTEST_SUBCASE("Subscribe to everything from test_module") -+ { -+ auto sub = sess.subscribeNotifications("/test_module:*", std::nullopt); -+ REQUIRE(!sub.replayStartTime()); -+ -+ CLIENT_SEND_NOTIFICATION(notifications[0]); -+ CLIENT_SEND_NOTIFICATION(notifications[1]); -+ -+ REQUIRE_NOTIFICATION(sub, notifications[0]); -+ READ_NOTIFICATION(sub); -+ -+ CLIENT_SEND_NOTIFICATION(notifications[2]); -+ -+ REQUIRE_NOTIFICATION(sub, notifications[1]); -+ READ_NOTIFICATION(sub); -+ -+ REQUIRE_NOTIFICATION(sub, notifications[2]); -+ READ_NOTIFICATION(sub); -+ -+ sub.terminate(); -+ -+ // Notification was sent after the subscription was terminated, so it should not be received -+ CLIENT_SEND_NOTIFICATION(notifications[0]); -+ -+ REQUIRE_PIPE_HANGUP(sub); -+ } -+ -+ DOCTEST_SUBCASE("Subscribe to the notification about stop-time reached") -+ { -+ auto stopTime = std::chrono::system_clock::now() + 500ms /* let's hope 500 ms should be enough to create subscription and notification */; -+ auto sub = sess.subscribeNotifications("/ietf-subscribed-notifications:subscription-terminated", std::nullopt, stopTime); -+ REQUIRE(!sub.replayStartTime()); -+ -+ // this notification is not subscribed, sysrepo should filter it -+ CLIENT_SEND_NOTIFICATION(notifications[0]); -+ -+ // wait until stop time and bit more -+ std::this_thread::sleep_until(stopTime + 500ms); -+ -+ REQUIRE_NOTIFICATION(sub, SUBSCRIPTION_TERMINATED(sub)); -+ READ_NOTIFICATION(sub); -+ -+ REQUIRE_PIPE_HANGUP(sub); -+ } -+ -+ DOCTEST_SUBCASE("Replays revise replay start time value") -+ { -+ conn.setModuleReplaySupport("test_module", true); -+ -+ // one notification for replay -+ auto before = std::chrono::system_clock::now(); -+ CLIENT_SEND_NOTIFICATION(notifications[0]); -+ auto after = std::chrono::system_clock::now(); -+ -+ auto sub = sess.subscribeNotifications("/test_module:*", std::nullopt, std::nullopt, std::chrono::system_clock::now() - 666s /* replay everything that happened at most 666s ago */); -+ -+ // replay start time is revised according to the first notification -+ REQUIRE(sub.replayStartTime()); -+ REQUIRE(sub.replayStartTime() >= before); -+ REQUIRE(sub.replayStartTime() <= after); -+ -+ // wait for the replayed notification and replay-completed notification -+ REQUIRE_NOTIFICATION(sub, notifications[0]); -+ READ_NOTIFICATION_BLOCKING(sub); -+ REQUIRE_NOTIFICATION(sub, REPLAY_COMPLETED(sub)); -+ READ_NOTIFICATION_BLOCKING(sub); -+ -+ sub.terminate(); -+ -+ REQUIRE_PIPE_HANGUP(sub); -+ } -+ -+ DOCTEST_SUBCASE("Terminate manually with a reason") -+ { -+ auto sub = sess.subscribeNotifications("/test_module:*", std::nullopt); -+ -+ REQUIRE_NOTIFICATION(sub, R"({ -+ "ietf-subscribed-notifications:subscription-terminated": { -+ "id": )" + std::to_string(sub.subscriptionId()) + R"(, -+ "reason": "filter-unavailable" -+ } -+} -+)"); -+ sub.terminate("ietf-subscribed-notifications:filter-unavailable"); -+ READ_NOTIFICATION(sub); -+ -+ REQUIRE_PIPE_HANGUP(sub); -+ } -+ -+ DOCTEST_SUBCASE("Terminate by destruction") -+ { -+ auto sub = std::make_unique(sess.subscribeNotifications("/test_module:*", std::nullopt)); -+ sub.reset(); -+ -+ // new events can happen but sysrepo is not supposed to send them to a terminated subscription -+ // also the FD is closed so no point in reading from it -+ client.setItem("/test_module:values[.='6']", std::nullopt); -+ client.applyChanges(); -+ } -+ -+ DOCTEST_SUBCASE("Terminate called twice") -+ { -+ auto sub = std::make_unique(sess.subscribeNotifications("/test_module:*", std::nullopt)); -+ sub->terminate(); -+ -+ // if the message is inlined into the macro, doctest<=2.4.8 fails with "REQUIRE_THROWS_WITH_AS( sub->terminate(), "", sysrepo::ErrorWithCode )", -+ // it does work after 8de4cf7e759c55a89761c25900d80e01ae7ac3fd -+ // TODO: Can be simplified after we bump minimal doctest version to doctest>=2.4.9 -+ const auto excMessage = "Couldn't terminate yang-push subscription with id " + std::to_string(sub->subscriptionId()) + ": SR_ERR_NOT_FOUND"; -+ REQUIRE_THROWS_WITH_AS(sub->terminate(), excMessage.c_str(), sysrepo::ErrorWithCode); -+ } -+ } -+ -+ DOCTEST_SUBCASE("YANG Push on change") -+ { -+ /* YANG Push on change sends the whole data tree every time the data tree changes. -+ * We read the notifications from the associated fd manually, so there should be no races -+ * between writing to sysrepo and reading the notifications. -+ */ -+ -+ auto sub = sess.yangPushOnChange(std::nullopt, std::nullopt, sysrepo::SyncOnStart::Yes); -+ -+ REQUIRE_YANG_PUSH_UPDATE(sub, R"({ -+ "ietf-yang-push:push-update": { -+ "datastore-contents": { -+ "test_module:values": [ -+ 2, -+ 3 -+ ] -+ } -+ } -+} -+)"); -+ READ_YANG_PUSH_UPDATE(sub); -+ -+ client.setItem("/test_module:leafInt32", "123"); -+ client.setItem("/test_module:values[.='5']", std::nullopt); -+ client.deleteItem("/test_module:values[.='3']"); -+ client.applyChanges(); -+ -+ REQUIRE_YANG_PUSH_UPDATE(sub, R"({ -+ "ietf-yang-push:push-change-update": { -+ "datastore-changes": { -+ "yang-patch": { -+ "patch-id": "patch-1", -+ "edit": [ -+ { -+ "edit-id": "edit-1", -+ "operation": "create", -+ "target": "/test_module:leafInt32", -+ "value": { -+ "test_module:leafInt32": 123 -+ } -+ }, -+ { -+ "edit-id": "edit-2", -+ "operation": "insert", -+ "target": "/test_module:values[.='5']", -+ "point": "/test_module:values[.='5'][.='3']", -+ "where": "after", -+ "value": { -+ "test_module:values": [ -+ 5 -+ ] -+ } -+ }, -+ { -+ "edit-id": "edit-3", -+ "operation": "delete", -+ "target": "/test_module:values[.='3']" -+ } -+ ] -+ } -+ } -+ } -+} -+)"); -+ READ_YANG_PUSH_UPDATE(sub); -+ -+ sub.terminate(); -+ REQUIRE_PIPE_HANGUP(sub); -+ } -+ -+ DOCTEST_SUBCASE("YANG Push periodic") -+ { -+ /* -+ * Periodic yang push sends the whole data tree every interval, we can't read manually, so -+ * we create another thread that modifies the data tree and this thread will read the notifications. -+ * -+ * Also, we don't know the subscription id in advance, so we have to read it from the first notification -+ * and use it in the following notifications. That requires some trompeloeil magic with LR_SIDE_EFFECT and LR_WITH. -+ */ -+ -+ std::invoke_result_t subId; -+ -+ TROMPELOEIL_REQUIRE_CALL(rec, recordYangPushUpdate(ANY(uint32_t), R"({ -+ "ietf-yang-push:push-update": { -+ "datastore-contents": { -+ "test_module:values": [ -+ 2, -+ 3 -+ ] -+ } -+ } -+} -+)")) -+ .IN_SEQUENCE(seq) -+ .TIMES(AT_LEAST(1)) -+ .LR_SIDE_EFFECT(subId = _1); -+ -+ TROMPELOEIL_REQUIRE_CALL(rec, recordYangPushUpdate(ANY(uint32_t), R"({ -+ "ietf-yang-push:push-update": { -+ "datastore-contents": { -+ "test_module:leafInt32": 123, -+ "test_module:values": [ -+ 2, -+ 5 -+ ] -+ } -+ } -+} -+)")) -+ .IN_SEQUENCE(seq) -+ .TIMES(AT_LEAST(1)) -+ .LR_WITH(subId == _1); -+ -+ TROMPELOEIL_REQUIRE_CALL(rec, recordYangPushUpdate(ANY(uint32_t), R"({ -+ "ietf-yang-push:push-update": { -+ "datastore-contents": { -+ "test_module:leafInt32": 123, -+ "test_module:values": [ -+ 2, -+ 5, -+ 6 -+ ] -+ } -+ } -+} -+)")) -+ .IN_SEQUENCE(seq) -+ .TIMES(AT_LEAST(1)) -+ .LR_WITH(subId == _1); -+ -+ TROMPELOEIL_REQUIRE_CALL(rec, recordYangPushUpdate(ANY(uint32_t), R"({ -+ "ietf-yang-push:push-update": { -+ "datastore-contents": { -+ "test_module:leafInt32": 123, -+ "test_module:values": [ -+ 2, -+ 5, -+ 7 -+ ] -+ } -+ } -+} -+)")) -+ .IN_SEQUENCE(seq) -+ .TIMES(AT_LEAST(1)) -+ .LR_WITH(subId == _1); -+ -+ TROMPELOEIL_REQUIRE_CALL(rec, recordYangPushUpdate(ANY(uint32_t), R"({ -+ "ietf-subscribed-notifications:subscription-terminated": { -+ "reason": "no-such-subscription" -+ } -+} -+)")) -+ .IN_SEQUENCE(seq) -+ .LR_WITH(_1 = subId); -+ -+ auto sub = sess.yangPushPeriodic(std::nullopt, std::chrono::milliseconds{66}, std::nullopt, std::chrono::system_clock::now() + 6666ms); -+ -+ std::jthread srDataEditor([&] { -+ auto sess = conn.sessionStart(); -+ -+ std::this_thread::sleep_for(500ms); -+ -+ sess.setItem("/test_module:leafInt32", "123"); -+ sess.setItem("/test_module:values[.='5']", std::nullopt); -+ sess.deleteItem("/test_module:values[.='3']"); -+ sess.applyChanges(); -+ std::this_thread::sleep_for(500ms); -+ -+ sess.setItem("/test_module:values[.='6']", std::nullopt); -+ sess.applyChanges(); -+ std::this_thread::sleep_for(500ms); -+ -+ sess.setItem("/test_module:values[.='7']", std::nullopt); -+ sess.deleteItem("/test_module:values[.='6']"); -+ sess.applyChanges(); -+ }); -+ -+ while (true) { -+ auto status = pipeStatus(sub.fd(), -1 /* block */); -+ if (status == PipeStatus::Hangup) { -+ break; -+ } else if (status == PipeStatus::DataReady) { -+ sub.processEvent(cbYangPushUpdate); -+ } else if (status == PipeStatus::Other) { -+ FAIL("PipeStatus::Other before the subscription was terminated"); -+ } else { -+ FAIL("PipeStatus::NoData but poll() should block until an event is available"); -+ } -+ } -+ -+ REQUIRE(subId == sub.subscriptionId()); -+ REQUIRE_PIPE_HANGUP(sub); -+ } -+} -diff --git a/tests/test_module.yang b/tests/test_module.yang -index fb91fe2..02c467b 100644 ---- a/tests/test_module.yang -+++ b/tests/test_module.yang -@@ -6,6 +6,10 @@ module test_module { - prefix nacm; - } - -+ import ietf-subscribed-notifications { -+ prefix sn; -+ } -+ - leaf leafInt32 { - type int32; - } -diff --git a/tests/yang/iana-if-type@2014-05-08.yang b/tests/yang/iana-if-type@2014-05-08.yang -new file mode 100644 -index 0000000..81b2175 ---- /dev/null -+++ b/tests/yang/iana-if-type@2014-05-08.yang -@@ -0,0 +1,1523 @@ -+module iana-if-type { -+ namespace "urn:ietf:params:xml:ns:yang:iana-if-type"; -+ prefix ianaift; -+ -+ import ietf-interfaces { -+ prefix if; -+ } -+ -+ organization "IANA"; -+ contact -+ " Internet Assigned Numbers Authority -+ -+ Postal: ICANN -+ 4676 Admiralty Way, Suite 330 -+ Marina del Rey, CA 90292 -+ -+ Tel: +1 310 823 9358 -+ "; -+ description -+ "This YANG module defines YANG identities for IANA-registered -+ interface types. -+ -+ This YANG module is maintained by IANA and reflects the -+ 'ifType definitions' registry. -+ -+ The latest revision of this YANG module can be obtained from -+ the IANA web site. -+ -+ Requests for new values should be made to IANA via -+ email (iana@iana.org). -+ -+ Copyright (c) 2014 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). -+ -+ The initial version of this YANG module is part of RFC 7224; -+ see the RFC itself for full legal notices."; -+ reference -+ "IANA 'ifType definitions' registry. -+ "; -+ -+ revision 2014-05-08 { -+ description -+ "Initial revision."; -+ reference -+ "RFC 7224: IANA Interface Type YANG Module"; -+ } -+ -+ identity iana-interface-type { -+ base if:interface-type; -+ description -+ "This identity is used as a base for all interface types -+ defined in the 'ifType definitions' registry."; -+ } -+ -+ identity other { -+ base iana-interface-type; -+ } -+ identity regular1822 { -+ base iana-interface-type; -+ } -+ identity hdh1822 { -+ base iana-interface-type; -+ } -+ identity ddnX25 { -+ base iana-interface-type; -+ } -+ identity rfc877x25 { -+ base iana-interface-type; -+ reference -+ "RFC 1382 - SNMP MIB Extension for the X.25 Packet Layer"; -+ } -+ identity ethernetCsmacd { -+ base iana-interface-type; -+ description -+ "For all Ethernet-like interfaces, regardless of speed, -+ as per RFC 3635."; -+ reference -+ "RFC 3635 - Definitions of Managed Objects for the -+ Ethernet-like Interface Types"; -+ } -+ identity iso88023Csmacd { -+ base iana-interface-type; -+ status deprecated; -+ description -+ "Deprecated via RFC 3635. -+ Use ethernetCsmacd(6) instead."; -+ reference -+ "RFC 3635 - Definitions of Managed Objects for the -+ Ethernet-like Interface Types"; -+ } -+ identity iso88024TokenBus { -+ base iana-interface-type; -+ } -+ identity iso88025TokenRing { -+ base iana-interface-type; -+ } -+ identity iso88026Man { -+ base iana-interface-type; -+ } -+ identity starLan { -+ base iana-interface-type; -+ status deprecated; -+ description -+ "Deprecated via RFC 3635. -+ Use ethernetCsmacd(6) instead."; -+ reference -+ "RFC 3635 - Definitions of Managed Objects for the -+ Ethernet-like Interface Types"; -+ } -+ identity proteon10Mbit { -+ base iana-interface-type; -+ } -+ identity proteon80Mbit { -+ base iana-interface-type; -+ } -+ identity hyperchannel { -+ base iana-interface-type; -+ } -+ identity fddi { -+ base iana-interface-type; -+ reference -+ "RFC 1512 - FDDI Management Information Base"; -+ } -+ identity lapb { -+ base iana-interface-type; -+ reference -+ "RFC 1381 - SNMP MIB Extension for X.25 LAPB"; -+ } -+ identity sdlc { -+ base iana-interface-type; -+ } -+ identity ds1 { -+ base iana-interface-type; -+ description -+ "DS1-MIB."; -+ reference -+ "RFC 4805 - Definitions of Managed Objects for the -+ DS1, J1, E1, DS2, and E2 Interface Types"; -+ } -+ identity e1 { -+ base iana-interface-type; -+ status obsolete; -+ description -+ "Obsolete; see DS1-MIB."; -+ reference -+ "RFC 4805 - Definitions of Managed Objects for the -+ DS1, J1, E1, DS2, and E2 Interface Types"; -+ } -+ -+ identity basicISDN { -+ base iana-interface-type; -+ description -+ "No longer used. See also RFC 2127."; -+ } -+ identity primaryISDN { -+ base iana-interface-type; -+ description -+ "No longer used. See also RFC 2127."; -+ } -+ identity propPointToPointSerial { -+ base iana-interface-type; -+ description -+ "Proprietary serial."; -+ } -+ identity ppp { -+ base iana-interface-type; -+ } -+ identity softwareLoopback { -+ base iana-interface-type; -+ } -+ identity eon { -+ base iana-interface-type; -+ description -+ "CLNP over IP."; -+ } -+ identity ethernet3Mbit { -+ base iana-interface-type; -+ } -+ identity nsip { -+ base iana-interface-type; -+ description -+ "XNS over IP."; -+ } -+ identity slip { -+ base iana-interface-type; -+ description -+ "Generic SLIP."; -+ } -+ identity ultra { -+ base iana-interface-type; -+ description -+ "Ultra Technologies."; -+ } -+ identity ds3 { -+ base iana-interface-type; -+ description -+ "DS3-MIB."; -+ reference -+ "RFC 3896 - Definitions of Managed Objects for the -+ DS3/E3 Interface Type"; -+ } -+ identity sip { -+ base iana-interface-type; -+ description -+ "SMDS, coffee."; -+ reference -+ "RFC 1694 - Definitions of Managed Objects for SMDS -+ Interfaces using SMIv2"; -+ } -+ identity frameRelay { -+ base iana-interface-type; -+ description -+ "DTE only."; -+ reference -+ "RFC 2115 - Management Information Base for Frame Relay -+ DTEs Using SMIv2"; -+ } -+ identity rs232 { -+ base iana-interface-type; -+ reference -+ "RFC 1659 - Definitions of Managed Objects for RS-232-like -+ Hardware Devices using SMIv2"; -+ } -+ identity para { -+ base iana-interface-type; -+ description -+ "Parallel-port."; -+ reference -+ "RFC 1660 - Definitions of Managed Objects for -+ Parallel-printer-like Hardware Devices using -+ SMIv2"; -+ } -+ identity arcnet { -+ base iana-interface-type; -+ description -+ "ARCnet."; -+ } -+ identity arcnetPlus { -+ base iana-interface-type; -+ description -+ "ARCnet Plus."; -+ } -+ -+ identity atm { -+ base iana-interface-type; -+ description -+ "ATM cells."; -+ } -+ identity miox25 { -+ base iana-interface-type; -+ reference -+ "RFC 1461 - SNMP MIB extension for Multiprotocol -+ Interconnect over X.25"; -+ } -+ identity sonet { -+ base iana-interface-type; -+ description -+ "SONET or SDH."; -+ } -+ identity x25ple { -+ base iana-interface-type; -+ reference -+ "RFC 2127 - ISDN Management Information Base using SMIv2"; -+ } -+ identity iso88022llc { -+ base iana-interface-type; -+ } -+ identity localTalk { -+ base iana-interface-type; -+ } -+ identity smdsDxi { -+ base iana-interface-type; -+ } -+ identity frameRelayService { -+ base iana-interface-type; -+ description -+ "FRNETSERV-MIB."; -+ reference -+ "RFC 2954 - Definitions of Managed Objects for Frame -+ Relay Service"; -+ } -+ identity v35 { -+ base iana-interface-type; -+ } -+ identity hssi { -+ base iana-interface-type; -+ } -+ identity hippi { -+ base iana-interface-type; -+ } -+ -+ identity modem { -+ base iana-interface-type; -+ description -+ "Generic modem."; -+ } -+ identity aal5 { -+ base iana-interface-type; -+ description -+ "AAL5 over ATM."; -+ } -+ identity sonetPath { -+ base iana-interface-type; -+ } -+ identity sonetVT { -+ base iana-interface-type; -+ } -+ identity smdsIcip { -+ base iana-interface-type; -+ description -+ "SMDS InterCarrier Interface."; -+ } -+ identity propVirtual { -+ base iana-interface-type; -+ description -+ "Proprietary virtual/internal."; -+ reference -+ "RFC 2863 - The Interfaces Group MIB"; -+ } -+ identity propMultiplexor { -+ base iana-interface-type; -+ description -+ "Proprietary multiplexing."; -+ reference -+ "RFC 2863 - The Interfaces Group MIB"; -+ } -+ identity ieee80212 { -+ base iana-interface-type; -+ description -+ "100BaseVG."; -+ } -+ identity fibreChannel { -+ base iana-interface-type; -+ description -+ "Fibre Channel."; -+ } -+ -+ identity hippiInterface { -+ base iana-interface-type; -+ description -+ "HIPPI interfaces."; -+ } -+ identity frameRelayInterconnect { -+ base iana-interface-type; -+ status obsolete; -+ description -+ "Obsolete; use either -+ frameRelay(32) or frameRelayService(44)."; -+ } -+ identity aflane8023 { -+ base iana-interface-type; -+ description -+ "ATM Emulated LAN for 802.3."; -+ } -+ identity aflane8025 { -+ base iana-interface-type; -+ description -+ "ATM Emulated LAN for 802.5."; -+ } -+ identity cctEmul { -+ base iana-interface-type; -+ description -+ "ATM Emulated circuit."; -+ } -+ identity fastEther { -+ base iana-interface-type; -+ status deprecated; -+ description -+ "Obsoleted via RFC 3635. -+ ethernetCsmacd(6) should be used instead."; -+ reference -+ "RFC 3635 - Definitions of Managed Objects for the -+ Ethernet-like Interface Types"; -+ } -+ identity isdn { -+ base iana-interface-type; -+ description -+ "ISDN and X.25."; -+ reference -+ "RFC 1356 - Multiprotocol Interconnect on X.25 and ISDN -+ in the Packet Mode"; -+ } -+ -+ identity v11 { -+ base iana-interface-type; -+ description -+ "CCITT V.11/X.21."; -+ } -+ identity v36 { -+ base iana-interface-type; -+ description -+ "CCITT V.36."; -+ } -+ identity g703at64k { -+ base iana-interface-type; -+ description -+ "CCITT G703 at 64Kbps."; -+ } -+ identity g703at2mb { -+ base iana-interface-type; -+ status obsolete; -+ description -+ "Obsolete; see DS1-MIB."; -+ } -+ identity qllc { -+ base iana-interface-type; -+ description -+ "SNA QLLC."; -+ } -+ identity fastEtherFX { -+ base iana-interface-type; -+ status deprecated; -+ description -+ "Obsoleted via RFC 3635. -+ ethernetCsmacd(6) should be used instead."; -+ reference -+ "RFC 3635 - Definitions of Managed Objects for the -+ Ethernet-like Interface Types"; -+ } -+ identity channel { -+ base iana-interface-type; -+ description -+ "Channel."; -+ } -+ identity ieee80211 { -+ base iana-interface-type; -+ description -+ "Radio spread spectrum."; -+ } -+ identity ibm370parChan { -+ base iana-interface-type; -+ description -+ "IBM System 360/370 OEMI Channel."; -+ } -+ identity escon { -+ base iana-interface-type; -+ description -+ "IBM Enterprise Systems Connection."; -+ } -+ identity dlsw { -+ base iana-interface-type; -+ description -+ "Data Link Switching."; -+ } -+ identity isdns { -+ base iana-interface-type; -+ description -+ "ISDN S/T interface."; -+ } -+ identity isdnu { -+ base iana-interface-type; -+ description -+ "ISDN U interface."; -+ } -+ identity lapd { -+ base iana-interface-type; -+ description -+ "Link Access Protocol D."; -+ } -+ identity ipSwitch { -+ base iana-interface-type; -+ description -+ "IP Switching Objects."; -+ } -+ identity rsrb { -+ base iana-interface-type; -+ description -+ "Remote Source Route Bridging."; -+ } -+ identity atmLogical { -+ base iana-interface-type; -+ description -+ "ATM Logical Port."; -+ reference -+ "RFC 3606 - Definitions of Supplemental Managed Objects -+ for ATM Interface"; -+ } -+ identity ds0 { -+ base iana-interface-type; -+ description -+ "Digital Signal Level 0."; -+ reference -+ "RFC 2494 - Definitions of Managed Objects for the DS0 -+ and DS0 Bundle Interface Type"; -+ } -+ identity ds0Bundle { -+ base iana-interface-type; -+ description -+ "Group of ds0s on the same ds1."; -+ reference -+ "RFC 2494 - Definitions of Managed Objects for the DS0 -+ and DS0 Bundle Interface Type"; -+ } -+ identity bsc { -+ base iana-interface-type; -+ description -+ "Bisynchronous Protocol."; -+ } -+ identity async { -+ base iana-interface-type; -+ description -+ "Asynchronous Protocol."; -+ } -+ identity cnr { -+ base iana-interface-type; -+ description -+ "Combat Net Radio."; -+ } -+ identity iso88025Dtr { -+ base iana-interface-type; -+ description -+ "ISO 802.5r DTR."; -+ } -+ identity eplrs { -+ base iana-interface-type; -+ description -+ "Ext Pos Loc Report Sys."; -+ } -+ identity arap { -+ base iana-interface-type; -+ description -+ "Appletalk Remote Access Protocol."; -+ } -+ identity propCnls { -+ base iana-interface-type; -+ description -+ "Proprietary Connectionless Protocol."; -+ } -+ identity hostPad { -+ base iana-interface-type; -+ description -+ "CCITT-ITU X.29 PAD Protocol."; -+ } -+ identity termPad { -+ base iana-interface-type; -+ description -+ "CCITT-ITU X.3 PAD Facility."; -+ } -+ identity frameRelayMPI { -+ base iana-interface-type; -+ description -+ "Multiproto Interconnect over FR."; -+ } -+ identity x213 { -+ base iana-interface-type; -+ description -+ "CCITT-ITU X213."; -+ } -+ identity adsl { -+ base iana-interface-type; -+ description -+ "Asymmetric Digital Subscriber Loop."; -+ } -+ identity radsl { -+ base iana-interface-type; -+ description -+ "Rate-Adapt. Digital Subscriber Loop."; -+ } -+ identity sdsl { -+ base iana-interface-type; -+ description -+ "Symmetric Digital Subscriber Loop."; -+ } -+ identity vdsl { -+ base iana-interface-type; -+ description -+ "Very H-Speed Digital Subscrib. Loop."; -+ } -+ identity iso88025CRFPInt { -+ base iana-interface-type; -+ description -+ "ISO 802.5 CRFP."; -+ } -+ identity myrinet { -+ base iana-interface-type; -+ description -+ "Myricom Myrinet."; -+ } -+ identity voiceEM { -+ base iana-interface-type; -+ description -+ "Voice recEive and transMit."; -+ } -+ identity voiceFXO { -+ base iana-interface-type; -+ description -+ "Voice Foreign Exchange Office."; -+ } -+ identity voiceFXS { -+ base iana-interface-type; -+ description -+ "Voice Foreign Exchange Station."; -+ } -+ identity voiceEncap { -+ base iana-interface-type; -+ description -+ "Voice encapsulation."; -+ } -+ identity voiceOverIp { -+ base iana-interface-type; -+ description -+ "Voice over IP encapsulation."; -+ } -+ identity atmDxi { -+ base iana-interface-type; -+ description -+ "ATM DXI."; -+ } -+ identity atmFuni { -+ base iana-interface-type; -+ description -+ "ATM FUNI."; -+ } -+ identity atmIma { -+ base iana-interface-type; -+ description -+ "ATM IMA."; -+ } -+ identity pppMultilinkBundle { -+ base iana-interface-type; -+ description -+ "PPP Multilink Bundle."; -+ } -+ identity ipOverCdlc { -+ base iana-interface-type; -+ description -+ "IBM ipOverCdlc."; -+ } -+ identity ipOverClaw { -+ base iana-interface-type; -+ description -+ "IBM Common Link Access to Workstn."; -+ } -+ identity stackToStack { -+ base iana-interface-type; -+ description -+ "IBM stackToStack."; -+ } -+ identity virtualIpAddress { -+ base iana-interface-type; -+ description -+ "IBM VIPA."; -+ } -+ identity mpc { -+ base iana-interface-type; -+ description -+ "IBM multi-protocol channel support."; -+ } -+ identity ipOverAtm { -+ base iana-interface-type; -+ description -+ "IBM ipOverAtm."; -+ reference -+ "RFC 2320 - Definitions of Managed Objects for Classical IP -+ and ARP Over ATM Using SMIv2 (IPOA-MIB)"; -+ } -+ identity iso88025Fiber { -+ base iana-interface-type; -+ description -+ "ISO 802.5j Fiber Token Ring."; -+ } -+ identity tdlc { -+ base iana-interface-type; -+ description -+ "IBM twinaxial data link control."; -+ } -+ identity gigabitEthernet { -+ base iana-interface-type; -+ status deprecated; -+ -+ description -+ "Obsoleted via RFC 3635. -+ ethernetCsmacd(6) should be used instead."; -+ reference -+ "RFC 3635 - Definitions of Managed Objects for the -+ Ethernet-like Interface Types"; -+ } -+ identity hdlc { -+ base iana-interface-type; -+ description -+ "HDLC."; -+ } -+ identity lapf { -+ base iana-interface-type; -+ description -+ "LAP F."; -+ } -+ identity v37 { -+ base iana-interface-type; -+ description -+ "V.37."; -+ } -+ identity x25mlp { -+ base iana-interface-type; -+ description -+ "Multi-Link Protocol."; -+ } -+ identity x25huntGroup { -+ base iana-interface-type; -+ description -+ "X25 Hunt Group."; -+ } -+ identity transpHdlc { -+ base iana-interface-type; -+ description -+ "Transp HDLC."; -+ } -+ identity interleave { -+ base iana-interface-type; -+ description -+ "Interleave channel."; -+ } -+ identity fast { -+ base iana-interface-type; -+ description -+ "Fast channel."; -+ } -+ -+ identity ip { -+ base iana-interface-type; -+ description -+ "IP (for APPN HPR in IP networks)."; -+ } -+ identity docsCableMaclayer { -+ base iana-interface-type; -+ description -+ "CATV Mac Layer."; -+ } -+ identity docsCableDownstream { -+ base iana-interface-type; -+ description -+ "CATV Downstream interface."; -+ } -+ identity docsCableUpstream { -+ base iana-interface-type; -+ description -+ "CATV Upstream interface."; -+ } -+ identity a12MppSwitch { -+ base iana-interface-type; -+ description -+ "Avalon Parallel Processor."; -+ } -+ identity tunnel { -+ base iana-interface-type; -+ description -+ "Encapsulation interface."; -+ } -+ identity coffee { -+ base iana-interface-type; -+ description -+ "Coffee pot."; -+ reference -+ "RFC 2325 - Coffee MIB"; -+ } -+ identity ces { -+ base iana-interface-type; -+ description -+ "Circuit Emulation Service."; -+ } -+ identity atmSubInterface { -+ base iana-interface-type; -+ description -+ "ATM Sub Interface."; -+ } -+ -+ identity l2vlan { -+ base iana-interface-type; -+ description -+ "Layer 2 Virtual LAN using 802.1Q."; -+ } -+ identity l3ipvlan { -+ base iana-interface-type; -+ description -+ "Layer 3 Virtual LAN using IP."; -+ } -+ identity l3ipxvlan { -+ base iana-interface-type; -+ description -+ "Layer 3 Virtual LAN using IPX."; -+ } -+ identity digitalPowerline { -+ base iana-interface-type; -+ description -+ "IP over Power Lines."; -+ } -+ identity mediaMailOverIp { -+ base iana-interface-type; -+ description -+ "Multimedia Mail over IP."; -+ } -+ identity dtm { -+ base iana-interface-type; -+ description -+ "Dynamic synchronous Transfer Mode."; -+ } -+ identity dcn { -+ base iana-interface-type; -+ description -+ "Data Communications Network."; -+ } -+ identity ipForward { -+ base iana-interface-type; -+ description -+ "IP Forwarding Interface."; -+ } -+ identity msdsl { -+ base iana-interface-type; -+ description -+ "Multi-rate Symmetric DSL."; -+ } -+ identity ieee1394 { -+ base iana-interface-type; -+ -+ description -+ "IEEE1394 High Performance Serial Bus."; -+ } -+ identity if-gsn { -+ base iana-interface-type; -+ description -+ "HIPPI-6400."; -+ } -+ identity dvbRccMacLayer { -+ base iana-interface-type; -+ description -+ "DVB-RCC MAC Layer."; -+ } -+ identity dvbRccDownstream { -+ base iana-interface-type; -+ description -+ "DVB-RCC Downstream Channel."; -+ } -+ identity dvbRccUpstream { -+ base iana-interface-type; -+ description -+ "DVB-RCC Upstream Channel."; -+ } -+ identity atmVirtual { -+ base iana-interface-type; -+ description -+ "ATM Virtual Interface."; -+ } -+ identity mplsTunnel { -+ base iana-interface-type; -+ description -+ "MPLS Tunnel Virtual Interface."; -+ } -+ identity srp { -+ base iana-interface-type; -+ description -+ "Spatial Reuse Protocol."; -+ } -+ identity voiceOverAtm { -+ base iana-interface-type; -+ description -+ "Voice over ATM."; -+ } -+ identity voiceOverFrameRelay { -+ base iana-interface-type; -+ description -+ "Voice Over Frame Relay."; -+ } -+ identity idsl { -+ base iana-interface-type; -+ description -+ "Digital Subscriber Loop over ISDN."; -+ } -+ identity compositeLink { -+ base iana-interface-type; -+ description -+ "Avici Composite Link Interface."; -+ } -+ identity ss7SigLink { -+ base iana-interface-type; -+ description -+ "SS7 Signaling Link."; -+ } -+ identity propWirelessP2P { -+ base iana-interface-type; -+ description -+ "Prop. P2P wireless interface."; -+ } -+ identity frForward { -+ base iana-interface-type; -+ description -+ "Frame Forward Interface."; -+ } -+ identity rfc1483 { -+ base iana-interface-type; -+ description -+ "Multiprotocol over ATM AAL5."; -+ reference -+ "RFC 1483 - Multiprotocol Encapsulation over ATM -+ Adaptation Layer 5"; -+ } -+ identity usb { -+ base iana-interface-type; -+ description -+ "USB Interface."; -+ } -+ identity ieee8023adLag { -+ base iana-interface-type; -+ description -+ "IEEE 802.3ad Link Aggregate."; -+ } -+ identity bgppolicyaccounting { -+ base iana-interface-type; -+ description -+ "BGP Policy Accounting."; -+ } -+ identity frf16MfrBundle { -+ base iana-interface-type; -+ description -+ "FRF.16 Multilink Frame Relay."; -+ } -+ identity h323Gatekeeper { -+ base iana-interface-type; -+ description -+ "H323 Gatekeeper."; -+ } -+ identity h323Proxy { -+ base iana-interface-type; -+ description -+ "H323 Voice and Video Proxy."; -+ } -+ identity mpls { -+ base iana-interface-type; -+ description -+ "MPLS."; -+ } -+ identity mfSigLink { -+ base iana-interface-type; -+ description -+ "Multi-frequency signaling link."; -+ } -+ identity hdsl2 { -+ base iana-interface-type; -+ description -+ "High Bit-Rate DSL - 2nd generation."; -+ } -+ identity shdsl { -+ base iana-interface-type; -+ description -+ "Multirate HDSL2."; -+ } -+ identity ds1FDL { -+ base iana-interface-type; -+ description -+ "Facility Data Link (4Kbps) on a DS1."; -+ } -+ identity pos { -+ base iana-interface-type; -+ description -+ "Packet over SONET/SDH Interface."; -+ } -+ -+ identity dvbAsiIn { -+ base iana-interface-type; -+ description -+ "DVB-ASI Input."; -+ } -+ identity dvbAsiOut { -+ base iana-interface-type; -+ description -+ "DVB-ASI Output."; -+ } -+ identity plc { -+ base iana-interface-type; -+ description -+ "Power Line Communications."; -+ } -+ identity nfas { -+ base iana-interface-type; -+ description -+ "Non-Facility Associated Signaling."; -+ } -+ identity tr008 { -+ base iana-interface-type; -+ description -+ "TR008."; -+ } -+ identity gr303RDT { -+ base iana-interface-type; -+ description -+ "Remote Digital Terminal."; -+ } -+ identity gr303IDT { -+ base iana-interface-type; -+ description -+ "Integrated Digital Terminal."; -+ } -+ identity isup { -+ base iana-interface-type; -+ description -+ "ISUP."; -+ } -+ identity propDocsWirelessMaclayer { -+ base iana-interface-type; -+ description -+ "Cisco proprietary Maclayer."; -+ } -+ -+ identity propDocsWirelessDownstream { -+ base iana-interface-type; -+ description -+ "Cisco proprietary Downstream."; -+ } -+ identity propDocsWirelessUpstream { -+ base iana-interface-type; -+ description -+ "Cisco proprietary Upstream."; -+ } -+ identity hiperlan2 { -+ base iana-interface-type; -+ description -+ "HIPERLAN Type 2 Radio Interface."; -+ } -+ identity propBWAp2Mp { -+ base iana-interface-type; -+ description -+ "PropBroadbandWirelessAccesspt2Multipt (use of this value -+ for IEEE 802.16 WMAN interfaces as per IEEE Std 802.16f -+ is deprecated, and ieee80216WMAN(237) should be used -+ instead)."; -+ } -+ identity sonetOverheadChannel { -+ base iana-interface-type; -+ description -+ "SONET Overhead Channel."; -+ } -+ identity digitalWrapperOverheadChannel { -+ base iana-interface-type; -+ description -+ "Digital Wrapper."; -+ } -+ identity aal2 { -+ base iana-interface-type; -+ description -+ "ATM adaptation layer 2."; -+ } -+ identity radioMAC { -+ base iana-interface-type; -+ description -+ "MAC layer over radio links."; -+ } -+ identity atmRadio { -+ base iana-interface-type; -+ description -+ "ATM over radio links."; -+ } -+ identity imt { -+ base iana-interface-type; -+ description -+ "Inter-Machine Trunks."; -+ } -+ identity mvl { -+ base iana-interface-type; -+ description -+ "Multiple Virtual Lines DSL."; -+ } -+ identity reachDSL { -+ base iana-interface-type; -+ description -+ "Long Reach DSL."; -+ } -+ identity frDlciEndPt { -+ base iana-interface-type; -+ description -+ "Frame Relay DLCI End Point."; -+ } -+ identity atmVciEndPt { -+ base iana-interface-type; -+ description -+ "ATM VCI End Point."; -+ } -+ identity opticalChannel { -+ base iana-interface-type; -+ description -+ "Optical Channel."; -+ } -+ identity opticalTransport { -+ base iana-interface-type; -+ description -+ "Optical Transport."; -+ } -+ identity propAtm { -+ base iana-interface-type; -+ description -+ "Proprietary ATM."; -+ } -+ identity voiceOverCable { -+ base iana-interface-type; -+ description -+ "Voice Over Cable Interface."; -+ } -+ -+ identity infiniband { -+ base iana-interface-type; -+ description -+ "Infiniband."; -+ } -+ identity teLink { -+ base iana-interface-type; -+ description -+ "TE Link."; -+ } -+ identity q2931 { -+ base iana-interface-type; -+ description -+ "Q.2931."; -+ } -+ identity virtualTg { -+ base iana-interface-type; -+ description -+ "Virtual Trunk Group."; -+ } -+ identity sipTg { -+ base iana-interface-type; -+ description -+ "SIP Trunk Group."; -+ } -+ identity sipSig { -+ base iana-interface-type; -+ description -+ "SIP Signaling."; -+ } -+ identity docsCableUpstreamChannel { -+ base iana-interface-type; -+ description -+ "CATV Upstream Channel."; -+ } -+ identity econet { -+ base iana-interface-type; -+ description -+ "Acorn Econet."; -+ } -+ identity pon155 { -+ base iana-interface-type; -+ description -+ "FSAN 155Mb Symetrical PON interface."; -+ } -+ -+ identity pon622 { -+ base iana-interface-type; -+ description -+ "FSAN 622Mb Symetrical PON interface."; -+ } -+ identity bridge { -+ base iana-interface-type; -+ description -+ "Transparent bridge interface."; -+ } -+ identity linegroup { -+ base iana-interface-type; -+ description -+ "Interface common to multiple lines."; -+ } -+ identity voiceEMFGD { -+ base iana-interface-type; -+ description -+ "Voice E&M Feature Group D."; -+ } -+ identity voiceFGDEANA { -+ base iana-interface-type; -+ description -+ "Voice FGD Exchange Access North American."; -+ } -+ identity voiceDID { -+ base iana-interface-type; -+ description -+ "Voice Direct Inward Dialing."; -+ } -+ identity mpegTransport { -+ base iana-interface-type; -+ description -+ "MPEG transport interface."; -+ } -+ identity sixToFour { -+ base iana-interface-type; -+ status deprecated; -+ description -+ "6to4 interface (DEPRECATED)."; -+ reference -+ "RFC 4087 - IP Tunnel MIB"; -+ } -+ identity gtp { -+ base iana-interface-type; -+ description -+ "GTP (GPRS Tunneling Protocol)."; -+ } -+ identity pdnEtherLoop1 { -+ base iana-interface-type; -+ description -+ "Paradyne EtherLoop 1."; -+ } -+ identity pdnEtherLoop2 { -+ base iana-interface-type; -+ description -+ "Paradyne EtherLoop 2."; -+ } -+ identity opticalChannelGroup { -+ base iana-interface-type; -+ description -+ "Optical Channel Group."; -+ } -+ identity homepna { -+ base iana-interface-type; -+ description -+ "HomePNA ITU-T G.989."; -+ } -+ identity gfp { -+ base iana-interface-type; -+ description -+ "Generic Framing Procedure (GFP)."; -+ } -+ identity ciscoISLvlan { -+ base iana-interface-type; -+ description -+ "Layer 2 Virtual LAN using Cisco ISL."; -+ } -+ identity actelisMetaLOOP { -+ base iana-interface-type; -+ description -+ "Acteleis proprietary MetaLOOP High Speed Link."; -+ } -+ identity fcipLink { -+ base iana-interface-type; -+ description -+ "FCIP Link."; -+ } -+ identity rpr { -+ base iana-interface-type; -+ description -+ "Resilient Packet Ring Interface Type."; -+ } -+ -+ identity qam { -+ base iana-interface-type; -+ description -+ "RF Qam Interface."; -+ } -+ identity lmp { -+ base iana-interface-type; -+ description -+ "Link Management Protocol."; -+ reference -+ "RFC 4327 - Link Management Protocol (LMP) Management -+ Information Base (MIB)"; -+ } -+ identity cblVectaStar { -+ base iana-interface-type; -+ description -+ "Cambridge Broadband Networks Limited VectaStar."; -+ } -+ identity docsCableMCmtsDownstream { -+ base iana-interface-type; -+ description -+ "CATV Modular CMTS Downstream Interface."; -+ } -+ identity adsl2 { -+ base iana-interface-type; -+ status deprecated; -+ description -+ "Asymmetric Digital Subscriber Loop Version 2 -+ (DEPRECATED/OBSOLETED - please use adsl2plus(238) -+ instead)."; -+ reference -+ "RFC 4706 - Definitions of Managed Objects for Asymmetric -+ Digital Subscriber Line 2 (ADSL2)"; -+ } -+ identity macSecControlledIF { -+ base iana-interface-type; -+ description -+ "MACSecControlled."; -+ } -+ identity macSecUncontrolledIF { -+ base iana-interface-type; -+ description -+ "MACSecUncontrolled."; -+ } -+ identity aviciOpticalEther { -+ base iana-interface-type; -+ description -+ "Avici Optical Ethernet Aggregate."; -+ } -+ identity atmbond { -+ base iana-interface-type; -+ description -+ "atmbond."; -+ } -+ identity voiceFGDOS { -+ base iana-interface-type; -+ description -+ "Voice FGD Operator Services."; -+ } -+ identity mocaVersion1 { -+ base iana-interface-type; -+ description -+ "MultiMedia over Coax Alliance (MoCA) Interface -+ as documented in information provided privately to IANA."; -+ } -+ identity ieee80216WMAN { -+ base iana-interface-type; -+ description -+ "IEEE 802.16 WMAN interface."; -+ } -+ identity adsl2plus { -+ base iana-interface-type; -+ description -+ "Asymmetric Digital Subscriber Loop Version 2 - -+ Version 2 Plus and all variants."; -+ } -+ identity dvbRcsMacLayer { -+ base iana-interface-type; -+ description -+ "DVB-RCS MAC Layer."; -+ reference -+ "RFC 5728 - The SatLabs Group DVB-RCS MIB"; -+ } -+ identity dvbTdm { -+ base iana-interface-type; -+ description -+ "DVB Satellite TDM."; -+ reference -+ "RFC 5728 - The SatLabs Group DVB-RCS MIB"; -+ } -+ identity dvbRcsTdma { -+ base iana-interface-type; -+ description -+ "DVB-RCS TDMA."; -+ reference -+ "RFC 5728 - The SatLabs Group DVB-RCS MIB"; -+ } -+ identity x86Laps { -+ base iana-interface-type; -+ description -+ "LAPS based on ITU-T X.86/Y.1323."; -+ } -+ identity wwanPP { -+ base iana-interface-type; -+ description -+ "3GPP WWAN."; -+ } -+ identity wwanPP2 { -+ base iana-interface-type; -+ description -+ "3GPP2 WWAN."; -+ } -+ identity voiceEBS { -+ base iana-interface-type; -+ description -+ "Voice P-phone EBS physical interface."; -+ } -+ identity ifPwType { -+ base iana-interface-type; -+ description -+ "Pseudowire interface type."; -+ reference -+ "RFC 5601 - Pseudowire (PW) Management Information Base (MIB)"; -+ } -+ identity ilan { -+ base iana-interface-type; -+ description -+ "Internal LAN on a bridge per IEEE 802.1ap."; -+ } -+ identity pip { -+ base iana-interface-type; -+ description -+ "Provider Instance Port on a bridge per IEEE 802.1ah PBB."; -+ } -+ identity aluELP { -+ base iana-interface-type; -+ description -+ "Alcatel-Lucent Ethernet Link Protection."; -+ } -+ identity gpon { -+ base iana-interface-type; -+ description -+ "Gigabit-capable passive optical networks (G-PON) as per -+ ITU-T G.948."; -+ } -+ identity vdsl2 { -+ base iana-interface-type; -+ description -+ "Very high speed digital subscriber line Version 2 -+ (as per ITU-T Recommendation G.993.2)."; -+ reference -+ "RFC 5650 - Definitions of Managed Objects for Very High -+ Speed Digital Subscriber Line 2 (VDSL2)"; -+ } -+ identity capwapDot11Profile { -+ base iana-interface-type; -+ description -+ "WLAN Profile Interface."; -+ reference -+ "RFC 5834 - Control and Provisioning of Wireless Access -+ Points (CAPWAP) Protocol Binding MIB for -+ IEEE 802.11"; -+ } -+ identity capwapDot11Bss { -+ base iana-interface-type; -+ description -+ "WLAN BSS Interface."; -+ reference -+ "RFC 5834 - Control and Provisioning of Wireless Access -+ Points (CAPWAP) Protocol Binding MIB for -+ IEEE 802.11"; -+ } -+ identity capwapWtpVirtualRadio { -+ base iana-interface-type; -+ description -+ "WTP Virtual Radio Interface."; -+ reference -+ "RFC 5833 - Control and Provisioning of Wireless Access -+ Points (CAPWAP) Protocol Base MIB"; -+ } -+ identity bits { -+ base iana-interface-type; -+ description -+ "bitsport."; -+ } -+ identity docsCableUpstreamRfPort { -+ base iana-interface-type; -+ description -+ "DOCSIS CATV Upstream RF Port."; -+ } -+ -+ identity cableDownstreamRfPort { -+ base iana-interface-type; -+ description -+ "CATV downstream RF Port."; -+ } -+ identity vmwareVirtualNic { -+ base iana-interface-type; -+ description -+ "VMware Virtual Network Interface."; -+ } -+ identity ieee802154 { -+ base iana-interface-type; -+ description -+ "IEEE 802.15.4 WPAN interface."; -+ reference -+ "IEEE 802.15.4-2006"; -+ } -+ identity otnOdu { -+ base iana-interface-type; -+ description -+ "OTN Optical Data Unit."; -+ } -+ identity otnOtu { -+ base iana-interface-type; -+ description -+ "OTN Optical channel Transport Unit."; -+ } -+ identity ifVfiType { -+ base iana-interface-type; -+ description -+ "VPLS Forwarding Instance Interface Type."; -+ } -+ identity g9981 { -+ base iana-interface-type; -+ description -+ "G.998.1 bonded interface."; -+ } -+ identity g9982 { -+ base iana-interface-type; -+ description -+ "G.998.2 bonded interface."; -+ } -+ identity g9983 { -+ base iana-interface-type; -+ description -+ "G.998.3 bonded interface."; -+ } -+ -+ identity aluEpon { -+ base iana-interface-type; -+ description -+ "Ethernet Passive Optical Networks (E-PON)."; -+ } -+ identity aluEponOnu { -+ base iana-interface-type; -+ description -+ "EPON Optical Network Unit."; -+ } -+ identity aluEponPhysicalUni { -+ base iana-interface-type; -+ description -+ "EPON physical User to Network interface."; -+ } -+ identity aluEponLogicalLink { -+ base iana-interface-type; -+ description -+ "The emulation of a point-to-point link over the EPON -+ layer."; -+ } -+ identity aluGponOnu { -+ base iana-interface-type; -+ description -+ "GPON Optical Network Unit."; -+ reference -+ "ITU-T G.984.2"; -+ } -+ identity aluGponPhysicalUni { -+ base iana-interface-type; -+ description -+ "GPON physical User to Network interface."; -+ reference -+ "ITU-T G.984.2"; -+ } -+ identity vmwareNicTeam { -+ base iana-interface-type; -+ description -+ "VMware NIC Team."; -+ } -+} -diff --git a/tests/yang/ietf-interfaces@2018-02-20.yang b/tests/yang/ietf-interfaces@2018-02-20.yang -new file mode 100644 -index 0000000..f66c205 ---- /dev/null -+++ b/tests/yang/ietf-interfaces@2018-02-20.yang -@@ -0,0 +1,1123 @@ -+module ietf-interfaces { -+ yang-version 1.1; -+ namespace "urn:ietf:params:xml:ns:yang:ietf-interfaces"; -+ prefix if; -+ -+ import ietf-yang-types { -+ prefix yang; -+ } -+ -+ organization -+ "IETF NETMOD (Network Modeling) Working Group"; -+ -+ contact -+ "WG Web: -+ WG List: -+ -+ Editor: Martin Bjorklund -+ "; -+ -+ description -+ "This module contains a collection of YANG definitions for -+ managing network interfaces. -+ -+ Copyright (c) 2018 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 8343; see -+ the RFC itself for full legal notices."; -+ -+ revision 2018-02-20 { -+ description -+ "Updated to support NMDA."; -+ reference -+ "RFC 8343: A YANG Data Model for Interface Management"; -+ } -+ -+ revision 2014-05-08 { -+ description -+ "Initial revision."; -+ reference -+ "RFC 7223: A YANG Data Model for Interface Management"; -+ } -+ -+ /* -+ * Typedefs -+ */ -+ -+ typedef interface-ref { -+ type leafref { -+ path "/if:interfaces/if:interface/if:name"; -+ } -+ description -+ "This type is used by data models that need to reference -+ interfaces."; -+ } -+ -+ /* -+ * Identities -+ */ -+ -+ identity interface-type { -+ description -+ "Base identity from which specific interface types are -+ derived."; -+ } -+ -+ /* -+ * Features -+ */ -+ -+ feature arbitrary-names { -+ description -+ "This feature indicates that the device allows user-controlled -+ interfaces to be named arbitrarily."; -+ } -+ feature pre-provisioning { -+ description -+ "This feature indicates that the device supports -+ pre-provisioning of interface configuration, i.e., it is -+ possible to configure an interface whose physical interface -+ hardware is not present on the device."; -+ } -+ feature if-mib { -+ description -+ "This feature indicates that the device implements -+ the IF-MIB."; -+ reference -+ "RFC 2863: The Interfaces Group MIB"; -+ } -+ -+ /* -+ * Data nodes -+ */ -+ -+ container interfaces { -+ description -+ "Interface parameters."; -+ -+ list interface { -+ key "name"; -+ -+ description -+ "The list of interfaces on the device. -+ -+ The status of an interface is available in this list in the -+ operational state. If the configuration of a -+ system-controlled interface cannot be used by the system -+ (e.g., the interface hardware present does not match the -+ interface type), then the configuration is not applied to -+ the system-controlled interface shown in the operational -+ state. If the configuration of a user-controlled interface -+ cannot be used by the system, the configured interface is -+ not instantiated in the operational state. -+ -+ System-controlled interfaces created by the system are -+ always present in this list in the operational state, -+ whether or not they are configured."; -+ -+ leaf name { -+ type string; -+ description -+ "The name of the interface. -+ -+ A device MAY restrict the allowed values for this leaf, -+ possibly depending on the type of the interface. -+ For system-controlled interfaces, this leaf is the -+ device-specific name of the interface. -+ -+ If a client tries to create configuration for a -+ system-controlled interface that is not present in the -+ operational state, the server MAY reject the request if -+ the implementation does not support pre-provisioning of -+ interfaces or if the name refers to an interface that can -+ never exist in the system. A Network Configuration -+ Protocol (NETCONF) server MUST reply with an rpc-error -+ with the error-tag 'invalid-value' in this case. -+ -+ If the device supports pre-provisioning of interface -+ configuration, the 'pre-provisioning' feature is -+ advertised. -+ -+ If the device allows arbitrarily named user-controlled -+ interfaces, the 'arbitrary-names' feature is advertised. -+ -+ When a configured user-controlled interface is created by -+ the system, it is instantiated with the same name in the -+ operational state. -+ -+ A server implementation MAY map this leaf to the ifName -+ MIB object. Such an implementation needs to use some -+ mechanism to handle the differences in size and characters -+ allowed between this leaf and ifName. The definition of -+ such a mechanism is outside the scope of this document."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifName"; -+ } -+ -+ leaf description { -+ type string; -+ description -+ "A textual description of the interface. -+ -+ A server implementation MAY map this leaf to the ifAlias -+ MIB object. Such an implementation needs to use some -+ mechanism to handle the differences in size and characters -+ allowed between this leaf and ifAlias. The definition of -+ such a mechanism is outside the scope of this document. -+ -+ Since ifAlias is defined to be stored in non-volatile -+ storage, the MIB implementation MUST map ifAlias to the -+ value of 'description' in the persistently stored -+ configuration."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifAlias"; -+ } -+ -+ leaf type { -+ type identityref { -+ base interface-type; -+ } -+ mandatory true; -+ description -+ "The type of the interface. -+ -+ When an interface entry is created, a server MAY -+ initialize the type leaf with a valid value, e.g., if it -+ is possible to derive the type from the name of the -+ interface. -+ -+ If a client tries to set the type of an interface to a -+ value that can never be used by the system, e.g., if the -+ type is not supported or if the type does not match the -+ name of the interface, the server MUST reject the request. -+ A NETCONF server MUST reply with an rpc-error with the -+ error-tag 'invalid-value' in this case."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifType"; -+ } -+ -+ leaf enabled { -+ type boolean; -+ default "true"; -+ description -+ "This leaf contains the configured, desired state of the -+ interface. -+ -+ Systems that implement the IF-MIB use the value of this -+ leaf in the intended configuration to set -+ IF-MIB.ifAdminStatus to 'up' or 'down' after an ifEntry -+ has been initialized, as described in RFC 2863. -+ -+ Changes in this leaf in the intended configuration are -+ reflected in ifAdminStatus."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifAdminStatus"; -+ } -+ -+ leaf link-up-down-trap-enable { -+ if-feature if-mib; -+ type enumeration { -+ enum enabled { -+ value 1; -+ description -+ "The device will generate linkUp/linkDown SNMP -+ notifications for this interface."; -+ } -+ enum disabled { -+ value 2; -+ description -+ "The device will not generate linkUp/linkDown SNMP -+ notifications for this interface."; -+ } -+ } -+ description -+ "Controls whether linkUp/linkDown SNMP notifications -+ should be generated for this interface. -+ -+ If this node is not configured, the value 'enabled' is -+ operationally used by the server for interfaces that do -+ not operate on top of any other interface (i.e., there are -+ no 'lower-layer-if' entries), and 'disabled' otherwise."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifLinkUpDownTrapEnable"; -+ } -+ -+ leaf admin-status { -+ if-feature if-mib; -+ type enumeration { -+ enum up { -+ value 1; -+ description -+ "Ready to pass packets."; -+ } -+ enum down { -+ value 2; -+ description -+ "Not ready to pass packets and not in some test mode."; -+ } -+ enum testing { -+ value 3; -+ description -+ "In some test mode."; -+ } -+ } -+ config false; -+ mandatory true; -+ description -+ "The desired state of the interface. -+ -+ This leaf has the same read semantics as ifAdminStatus."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifAdminStatus"; -+ } -+ -+ leaf oper-status { -+ type enumeration { -+ enum up { -+ value 1; -+ description -+ "Ready to pass packets."; -+ } -+ enum down { -+ value 2; -+ -+ description -+ "The interface does not pass any packets."; -+ } -+ enum testing { -+ value 3; -+ description -+ "In some test mode. No operational packets can -+ be passed."; -+ } -+ enum unknown { -+ value 4; -+ description -+ "Status cannot be determined for some reason."; -+ } -+ enum dormant { -+ value 5; -+ description -+ "Waiting for some external event."; -+ } -+ enum not-present { -+ value 6; -+ description -+ "Some component (typically hardware) is missing."; -+ } -+ enum lower-layer-down { -+ value 7; -+ description -+ "Down due to state of lower-layer interface(s)."; -+ } -+ } -+ config false; -+ mandatory true; -+ description -+ "The current operational state of the interface. -+ -+ This leaf has the same semantics as ifOperStatus."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifOperStatus"; -+ } -+ -+ leaf last-change { -+ type yang:date-and-time; -+ config false; -+ description -+ "The time the interface entered its current operational -+ state. If the current state was entered prior to the -+ last re-initialization of the local network management -+ subsystem, then this node is not present."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifLastChange"; -+ } -+ -+ leaf if-index { -+ if-feature if-mib; -+ type int32 { -+ range "1..2147483647"; -+ } -+ config false; -+ mandatory true; -+ description -+ "The ifIndex value for the ifEntry represented by this -+ interface."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifIndex"; -+ } -+ -+ leaf phys-address { -+ type yang:phys-address; -+ config false; -+ description -+ "The interface's address at its protocol sub-layer. For -+ example, for an 802.x interface, this object normally -+ contains a Media Access Control (MAC) address. The -+ interface's media-specific modules must define the bit -+ and byte ordering and the format of the value of this -+ object. For interfaces that do not have such an address -+ (e.g., a serial line), this node is not present."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifPhysAddress"; -+ } -+ -+ leaf-list higher-layer-if { -+ type interface-ref; -+ config false; -+ description -+ "A list of references to interfaces layered on top of this -+ interface."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifStackTable"; -+ } -+ -+ leaf-list lower-layer-if { -+ type interface-ref; -+ config false; -+ -+ description -+ "A list of references to interfaces layered underneath this -+ interface."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifStackTable"; -+ } -+ -+ leaf speed { -+ type yang:gauge64; -+ units "bits/second"; -+ config false; -+ description -+ "An estimate of the interface's current bandwidth in bits -+ per second. For interfaces that do not vary in -+ bandwidth or for those where no accurate estimation can -+ be made, this node should contain the nominal bandwidth. -+ For interfaces that have no concept of bandwidth, this -+ node is not present."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifSpeed, ifHighSpeed"; -+ } -+ -+ container statistics { -+ config false; -+ description -+ "A collection of interface-related statistics objects."; -+ -+ leaf discontinuity-time { -+ type yang:date-and-time; -+ mandatory true; -+ description -+ "The time on the most recent occasion at which any one or -+ more of this interface's counters suffered a -+ discontinuity. If no such discontinuities have occurred -+ since the last re-initialization of the local management -+ subsystem, then this node contains the time the local -+ management subsystem re-initialized itself."; -+ } -+ -+ leaf in-octets { -+ type yang:counter64; -+ description -+ "The total number of octets received on the interface, -+ including framing characters. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCInOctets"; -+ } -+ -+ leaf in-unicast-pkts { -+ type yang:counter64; -+ description -+ "The number of packets, delivered by this sub-layer to a -+ higher (sub-)layer, that were not addressed to a -+ multicast or broadcast address at this sub-layer. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCInUcastPkts"; -+ } -+ -+ leaf in-broadcast-pkts { -+ type yang:counter64; -+ description -+ "The number of packets, delivered by this sub-layer to a -+ higher (sub-)layer, that were addressed to a broadcast -+ address at this sub-layer. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCInBroadcastPkts"; -+ } -+ -+ leaf in-multicast-pkts { -+ type yang:counter64; -+ description -+ "The number of packets, delivered by this sub-layer to a -+ higher (sub-)layer, that were addressed to a multicast -+ address at this sub-layer. For a MAC-layer protocol, -+ this includes both Group and Functional addresses. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCInMulticastPkts"; -+ } -+ -+ leaf in-discards { -+ type yang:counter32; -+ description -+ "The number of inbound packets that were chosen to be -+ discarded even though no errors had been detected to -+ prevent their being deliverable to a higher-layer -+ protocol. One possible reason for discarding such a -+ packet could be to free up buffer space. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifInDiscards"; -+ } -+ -+ leaf in-errors { -+ type yang:counter32; -+ description -+ "For packet-oriented interfaces, the number of inbound -+ packets that contained errors preventing them from being -+ deliverable to a higher-layer protocol. For character- -+ oriented or fixed-length interfaces, the number of -+ inbound transmission units that contained errors -+ preventing them from being deliverable to a higher-layer -+ protocol. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifInErrors"; -+ } -+ -+ leaf in-unknown-protos { -+ type yang:counter32; -+ -+ description -+ "For packet-oriented interfaces, the number of packets -+ received via the interface that were discarded because -+ of an unknown or unsupported protocol. For -+ character-oriented or fixed-length interfaces that -+ support protocol multiplexing, the number of -+ transmission units received via the interface that were -+ discarded because of an unknown or unsupported protocol. -+ For any interface that does not support protocol -+ multiplexing, this counter is not present. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifInUnknownProtos"; -+ } -+ -+ leaf out-octets { -+ type yang:counter64; -+ description -+ "The total number of octets transmitted out of the -+ interface, including framing characters. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCOutOctets"; -+ } -+ -+ leaf out-unicast-pkts { -+ type yang:counter64; -+ description -+ "The total number of packets that higher-level protocols -+ requested be transmitted and that were not addressed -+ to a multicast or broadcast address at this sub-layer, -+ including those that were discarded or not sent. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCOutUcastPkts"; -+ } -+ -+ leaf out-broadcast-pkts { -+ type yang:counter64; -+ description -+ "The total number of packets that higher-level protocols -+ requested be transmitted and that were addressed to a -+ broadcast address at this sub-layer, including those -+ that were discarded or not sent. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCOutBroadcastPkts"; -+ } -+ -+ leaf out-multicast-pkts { -+ type yang:counter64; -+ description -+ "The total number of packets that higher-level protocols -+ requested be transmitted and that were addressed to a -+ multicast address at this sub-layer, including those -+ that were discarded or not sent. For a MAC-layer -+ protocol, this includes both Group and Functional -+ addresses. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCOutMulticastPkts"; -+ } -+ -+ leaf out-discards { -+ type yang:counter32; -+ description -+ "The number of outbound packets that were chosen to be -+ discarded even though no errors had been detected to -+ prevent their being transmitted. One possible reason -+ for discarding such a packet could be to free up buffer -+ space. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifOutDiscards"; -+ } -+ -+ leaf out-errors { -+ type yang:counter32; -+ description -+ "For packet-oriented interfaces, the number of outbound -+ packets that could not be transmitted because of errors. -+ For character-oriented or fixed-length interfaces, the -+ number of outbound transmission units that could not be -+ transmitted because of errors. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifOutErrors"; -+ } -+ } -+ -+ } -+ } -+ -+ /* -+ * Legacy typedefs -+ */ -+ -+ typedef interface-state-ref { -+ type leafref { -+ path "/if:interfaces-state/if:interface/if:name"; -+ } -+ status deprecated; -+ description -+ "This type is used by data models that need to reference -+ the operationally present interfaces."; -+ } -+ -+ /* -+ * Legacy operational state data nodes -+ */ -+ -+ container interfaces-state { -+ config false; -+ status deprecated; -+ description -+ "Data nodes for the operational state of interfaces."; -+ -+ list interface { -+ key "name"; -+ status deprecated; -+ -+ description -+ "The list of interfaces on the device. -+ -+ System-controlled interfaces created by the system are -+ always present in this list, whether or not they are -+ configured."; -+ -+ leaf name { -+ type string; -+ status deprecated; -+ description -+ "The name of the interface. -+ -+ A server implementation MAY map this leaf to the ifName -+ MIB object. Such an implementation needs to use some -+ mechanism to handle the differences in size and characters -+ allowed between this leaf and ifName. The definition of -+ such a mechanism is outside the scope of this document."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifName"; -+ } -+ -+ leaf type { -+ type identityref { -+ base interface-type; -+ } -+ mandatory true; -+ status deprecated; -+ description -+ "The type of the interface."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifType"; -+ } -+ -+ leaf admin-status { -+ if-feature if-mib; -+ type enumeration { -+ enum up { -+ value 1; -+ description -+ "Ready to pass packets."; -+ } -+ enum down { -+ value 2; -+ description -+ "Not ready to pass packets and not in some test mode."; -+ } -+ enum testing { -+ value 3; -+ description -+ "In some test mode."; -+ } -+ } -+ mandatory true; -+ status deprecated; -+ description -+ "The desired state of the interface. -+ -+ This leaf has the same read semantics as ifAdminStatus."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifAdminStatus"; -+ } -+ -+ leaf oper-status { -+ type enumeration { -+ enum up { -+ value 1; -+ description -+ "Ready to pass packets."; -+ } -+ enum down { -+ value 2; -+ description -+ "The interface does not pass any packets."; -+ } -+ enum testing { -+ value 3; -+ description -+ "In some test mode. No operational packets can -+ be passed."; -+ } -+ enum unknown { -+ value 4; -+ description -+ "Status cannot be determined for some reason."; -+ } -+ enum dormant { -+ value 5; -+ description -+ "Waiting for some external event."; -+ } -+ enum not-present { -+ value 6; -+ description -+ "Some component (typically hardware) is missing."; -+ } -+ enum lower-layer-down { -+ value 7; -+ description -+ "Down due to state of lower-layer interface(s)."; -+ } -+ } -+ mandatory true; -+ status deprecated; -+ description -+ "The current operational state of the interface. -+ -+ This leaf has the same semantics as ifOperStatus."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifOperStatus"; -+ } -+ -+ leaf last-change { -+ type yang:date-and-time; -+ status deprecated; -+ description -+ "The time the interface entered its current operational -+ state. If the current state was entered prior to the -+ last re-initialization of the local network management -+ subsystem, then this node is not present."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifLastChange"; -+ } -+ -+ leaf if-index { -+ if-feature if-mib; -+ type int32 { -+ range "1..2147483647"; -+ } -+ mandatory true; -+ status deprecated; -+ description -+ "The ifIndex value for the ifEntry represented by this -+ interface."; -+ -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifIndex"; -+ } -+ -+ leaf phys-address { -+ type yang:phys-address; -+ status deprecated; -+ description -+ "The interface's address at its protocol sub-layer. For -+ example, for an 802.x interface, this object normally -+ contains a Media Access Control (MAC) address. The -+ interface's media-specific modules must define the bit -+ and byte ordering and the format of the value of this -+ object. For interfaces that do not have such an address -+ (e.g., a serial line), this node is not present."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifPhysAddress"; -+ } -+ -+ leaf-list higher-layer-if { -+ type interface-state-ref; -+ status deprecated; -+ description -+ "A list of references to interfaces layered on top of this -+ interface."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifStackTable"; -+ } -+ -+ leaf-list lower-layer-if { -+ type interface-state-ref; -+ status deprecated; -+ description -+ "A list of references to interfaces layered underneath this -+ interface."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifStackTable"; -+ } -+ -+ leaf speed { -+ type yang:gauge64; -+ units "bits/second"; -+ status deprecated; -+ description -+ "An estimate of the interface's current bandwidth in bits -+ per second. For interfaces that do not vary in -+ bandwidth or for those where no accurate estimation can -+ -+ be made, this node should contain the nominal bandwidth. -+ For interfaces that have no concept of bandwidth, this -+ node is not present."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifSpeed, ifHighSpeed"; -+ } -+ -+ container statistics { -+ status deprecated; -+ description -+ "A collection of interface-related statistics objects."; -+ -+ leaf discontinuity-time { -+ type yang:date-and-time; -+ mandatory true; -+ status deprecated; -+ description -+ "The time on the most recent occasion at which any one or -+ more of this interface's counters suffered a -+ discontinuity. If no such discontinuities have occurred -+ since the last re-initialization of the local management -+ subsystem, then this node contains the time the local -+ management subsystem re-initialized itself."; -+ } -+ -+ leaf in-octets { -+ type yang:counter64; -+ status deprecated; -+ description -+ "The total number of octets received on the interface, -+ including framing characters. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCInOctets"; -+ } -+ -+ leaf in-unicast-pkts { -+ type yang:counter64; -+ status deprecated; -+ description -+ "The number of packets, delivered by this sub-layer to a -+ higher (sub-)layer, that were not addressed to a -+ multicast or broadcast address at this sub-layer. -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCInUcastPkts"; -+ } -+ -+ leaf in-broadcast-pkts { -+ type yang:counter64; -+ status deprecated; -+ description -+ "The number of packets, delivered by this sub-layer to a -+ higher (sub-)layer, that were addressed to a broadcast -+ address at this sub-layer. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCInBroadcastPkts"; -+ } -+ -+ leaf in-multicast-pkts { -+ type yang:counter64; -+ status deprecated; -+ description -+ "The number of packets, delivered by this sub-layer to a -+ higher (sub-)layer, that were addressed to a multicast -+ address at this sub-layer. For a MAC-layer protocol, -+ this includes both Group and Functional addresses. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCInMulticastPkts"; -+ } -+ -+ leaf in-discards { -+ type yang:counter32; -+ status deprecated; -+ -+ description -+ "The number of inbound packets that were chosen to be -+ discarded even though no errors had been detected to -+ prevent their being deliverable to a higher-layer -+ protocol. One possible reason for discarding such a -+ packet could be to free up buffer space. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifInDiscards"; -+ } -+ -+ leaf in-errors { -+ type yang:counter32; -+ status deprecated; -+ description -+ "For packet-oriented interfaces, the number of inbound -+ packets that contained errors preventing them from being -+ deliverable to a higher-layer protocol. For character- -+ oriented or fixed-length interfaces, the number of -+ inbound transmission units that contained errors -+ preventing them from being deliverable to a higher-layer -+ protocol. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifInErrors"; -+ } -+ -+ leaf in-unknown-protos { -+ type yang:counter32; -+ status deprecated; -+ description -+ "For packet-oriented interfaces, the number of packets -+ received via the interface that were discarded because -+ of an unknown or unsupported protocol. For -+ character-oriented or fixed-length interfaces that -+ support protocol multiplexing, the number of -+ transmission units received via the interface that were -+ discarded because of an unknown or unsupported protocol. -+ For any interface that does not support protocol -+ multiplexing, this counter is not present. -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifInUnknownProtos"; -+ } -+ -+ leaf out-octets { -+ type yang:counter64; -+ status deprecated; -+ description -+ "The total number of octets transmitted out of the -+ interface, including framing characters. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCOutOctets"; -+ } -+ -+ leaf out-unicast-pkts { -+ type yang:counter64; -+ status deprecated; -+ description -+ "The total number of packets that higher-level protocols -+ requested be transmitted and that were not addressed -+ to a multicast or broadcast address at this sub-layer, -+ including those that were discarded or not sent. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifHCOutUcastPkts"; -+ } -+ -+ leaf out-broadcast-pkts { -+ type yang:counter64; -+ status deprecated; -+ -+ description -+ "The total number of packets that higher-level protocols -+ requested be transmitted and that were addressed to a -+ broadcast address at this sub-layer, including those -+ that were discarded or not sent. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCOutBroadcastPkts"; -+ } -+ -+ leaf out-multicast-pkts { -+ type yang:counter64; -+ status deprecated; -+ description -+ "The total number of packets that higher-level protocols -+ requested be transmitted and that were addressed to a -+ multicast address at this sub-layer, including those -+ that were discarded or not sent. For a MAC-layer -+ protocol, this includes both Group and Functional -+ addresses. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - -+ ifHCOutMulticastPkts"; -+ } -+ -+ leaf out-discards { -+ type yang:counter32; -+ status deprecated; -+ description -+ "The number of outbound packets that were chosen to be -+ discarded even though no errors had been detected to -+ prevent their being transmitted. One possible reason -+ for discarding such a packet could be to free up buffer -+ space. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifOutDiscards"; -+ } -+ -+ leaf out-errors { -+ type yang:counter32; -+ status deprecated; -+ description -+ "For packet-oriented interfaces, the number of outbound -+ packets that could not be transmitted because of errors. -+ For character-oriented or fixed-length interfaces, the -+ number of outbound transmission units that could not be -+ transmitted because of errors. -+ -+ Discontinuities in the value of this counter can occur -+ at re-initialization of the management system and at -+ other times as indicated by the value of -+ 'discontinuity-time'."; -+ reference -+ "RFC 2863: The Interfaces Group MIB - ifOutErrors"; -+ } -+ } -+ } -+ } -+} -diff --git a/tests/yang/ietf-ip@2018-02-22.yang b/tests/yang/ietf-ip@2018-02-22.yang -new file mode 100644 -index 0000000..a270f67 ---- /dev/null -+++ b/tests/yang/ietf-ip@2018-02-22.yang -@@ -0,0 +1,876 @@ -+module ietf-ip { -+ yang-version 1.1; -+ namespace "urn:ietf:params:xml:ns:yang:ietf-ip"; -+ prefix ip; -+ -+ import ietf-interfaces { -+ prefix if; -+ } -+ import ietf-inet-types { -+ prefix inet; -+ } -+ import ietf-yang-types { -+ prefix yang; -+ } -+ -+ organization -+ "IETF NETMOD (Network Modeling) Working Group"; -+ -+ contact -+ "WG Web: -+ WG List: -+ -+ Editor: Martin Bjorklund -+ "; -+ description -+ "This module contains a collection of YANG definitions for -+ managing IP implementations. -+ -+ Copyright (c) 2018 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 8344; see -+ the RFC itself for full legal notices."; -+ -+ revision 2018-02-22 { -+ description -+ "Updated to support NMDA."; -+ reference -+ "RFC 8344: A YANG Data Model for IP Management"; -+ } -+ -+ revision 2014-06-16 { -+ description -+ "Initial revision."; -+ reference -+ "RFC 7277: A YANG Data Model for IP Management"; -+ } -+ -+ /* -+ * Features -+ */ -+ -+ feature ipv4-non-contiguous-netmasks { -+ description -+ "Indicates support for configuring non-contiguous -+ subnet masks."; -+ } -+ -+ feature ipv6-privacy-autoconf { -+ description -+ "Indicates support for privacy extensions for stateless address -+ autoconfiguration in IPv6."; -+ reference -+ "RFC 4941: Privacy Extensions for Stateless Address -+ Autoconfiguration in IPv6"; -+ } -+ -+ /* -+ * Typedefs -+ */ -+ -+ typedef ip-address-origin { -+ type enumeration { -+ enum other { -+ description -+ "None of the following."; -+ } -+ -+ enum static { -+ description -+ "Indicates that the address has been statically -+ configured -- for example, using the Network Configuration -+ Protocol (NETCONF) or a command line interface."; -+ } -+ enum dhcp { -+ description -+ "Indicates an address that has been assigned to this -+ system by a DHCP server."; -+ } -+ enum link-layer { -+ description -+ "Indicates an address created by IPv6 stateless -+ autoconfiguration that embeds a link-layer address in its -+ interface identifier."; -+ } -+ enum random { -+ description -+ "Indicates an address chosen by the system at -+ random, e.g., an IPv4 address within 169.254/16, a -+ temporary address as described in RFC 4941, or a -+ semantically opaque address as described in RFC 7217."; -+ reference -+ "RFC 4941: Privacy Extensions for Stateless Address -+ Autoconfiguration in IPv6 -+ RFC 7217: A Method for Generating Semantically Opaque -+ Interface Identifiers with IPv6 Stateless -+ Address Autoconfiguration (SLAAC)"; -+ } -+ } -+ description -+ "The origin of an address."; -+ } -+ -+ typedef neighbor-origin { -+ type enumeration { -+ enum other { -+ description -+ "None of the following."; -+ } -+ enum static { -+ description -+ "Indicates that the mapping has been statically -+ configured -- for example, using NETCONF or a command line -+ interface."; -+ } -+ -+ enum dynamic { -+ description -+ "Indicates that the mapping has been dynamically resolved -+ using, for example, IPv4 ARP or the IPv6 Neighbor -+ Discovery protocol."; -+ } -+ } -+ description -+ "The origin of a neighbor entry."; -+ } -+ -+ /* -+ * Data nodes -+ */ -+ -+ augment "/if:interfaces/if:interface" { -+ description -+ "IP parameters on interfaces. -+ -+ If an interface is not capable of running IP, the server -+ must not allow the client to configure these parameters."; -+ -+ container ipv4 { -+ presence -+ "Enables IPv4 unless the 'enabled' leaf -+ (which defaults to 'true') is set to 'false'"; -+ description -+ "Parameters for the IPv4 address family."; -+ -+ leaf enabled { -+ type boolean; -+ default true; -+ description -+ "Controls whether IPv4 is enabled or disabled on this -+ interface. When IPv4 is enabled, this interface is -+ connected to an IPv4 stack, and the interface can send -+ and receive IPv4 packets."; -+ } -+ leaf forwarding { -+ type boolean; -+ default false; -+ description -+ "Controls IPv4 packet forwarding of datagrams received by, -+ but not addressed to, this interface. IPv4 routers -+ forward datagrams. IPv4 hosts do not (except those -+ source-routed via the host)."; -+ } -+ -+ leaf mtu { -+ type uint16 { -+ range "68..max"; -+ } -+ units "octets"; -+ description -+ "The size, in octets, of the largest IPv4 packet that the -+ interface will send and receive. -+ -+ The server may restrict the allowed values for this leaf, -+ depending on the interface's type. -+ -+ If this leaf is not configured, the operationally used MTU -+ depends on the interface's type."; -+ reference -+ "RFC 791: Internet Protocol"; -+ } -+ list address { -+ key "ip"; -+ description -+ "The list of IPv4 addresses on the interface."; -+ -+ leaf ip { -+ type inet:ipv4-address-no-zone; -+ description -+ "The IPv4 address on the interface."; -+ } -+ choice subnet { -+ mandatory true; -+ description -+ "The subnet can be specified as a prefix length or, -+ if the server supports non-contiguous netmasks, as -+ a netmask."; -+ leaf prefix-length { -+ type uint8 { -+ range "0..32"; -+ } -+ description -+ "The length of the subnet prefix."; -+ } -+ leaf netmask { -+ if-feature ipv4-non-contiguous-netmasks; -+ type yang:dotted-quad; -+ description -+ "The subnet specified as a netmask."; -+ } -+ } -+ -+ leaf origin { -+ type ip-address-origin; -+ config false; -+ description -+ "The origin of this address."; -+ } -+ } -+ list neighbor { -+ key "ip"; -+ description -+ "A list of mappings from IPv4 addresses to -+ link-layer addresses. -+ -+ Entries in this list in the intended configuration are -+ used as static entries in the ARP Cache. -+ -+ In the operational state, this list represents the ARP -+ Cache."; -+ reference -+ "RFC 826: An Ethernet Address Resolution Protocol"; -+ -+ leaf ip { -+ type inet:ipv4-address-no-zone; -+ description -+ "The IPv4 address of the neighbor node."; -+ } -+ leaf link-layer-address { -+ type yang:phys-address; -+ mandatory true; -+ description -+ "The link-layer address of the neighbor node."; -+ } -+ leaf origin { -+ type neighbor-origin; -+ config false; -+ description -+ "The origin of this neighbor entry."; -+ } -+ } -+ } -+ -+ container ipv6 { -+ presence -+ "Enables IPv6 unless the 'enabled' leaf -+ (which defaults to 'true') is set to 'false'"; -+ description -+ "Parameters for the IPv6 address family."; -+ -+ leaf enabled { -+ type boolean; -+ default true; -+ description -+ "Controls whether IPv6 is enabled or disabled on this -+ interface. When IPv6 is enabled, this interface is -+ connected to an IPv6 stack, and the interface can send -+ and receive IPv6 packets."; -+ } -+ leaf forwarding { -+ type boolean; -+ default false; -+ description -+ "Controls IPv6 packet forwarding of datagrams received by, -+ but not addressed to, this interface. IPv6 routers -+ forward datagrams. IPv6 hosts do not (except those -+ source-routed via the host)."; -+ reference -+ "RFC 4861: Neighbor Discovery for IP version 6 (IPv6) -+ Section 6.2.1, IsRouter"; -+ } -+ leaf mtu { -+ type uint32 { -+ range "1280..max"; -+ } -+ units "octets"; -+ description -+ "The size, in octets, of the largest IPv6 packet that the -+ interface will send and receive. -+ -+ The server may restrict the allowed values for this leaf, -+ depending on the interface's type. -+ -+ If this leaf is not configured, the operationally used MTU -+ depends on the interface's type."; -+ reference -+ "RFC 8200: Internet Protocol, Version 6 (IPv6) -+ Specification -+ Section 5"; -+ } -+ -+ list address { -+ key "ip"; -+ description -+ "The list of IPv6 addresses on the interface."; -+ -+ leaf ip { -+ type inet:ipv6-address-no-zone; -+ description -+ "The IPv6 address on the interface."; -+ } -+ leaf prefix-length { -+ type uint8 { -+ range "0..128"; -+ } -+ mandatory true; -+ description -+ "The length of the subnet prefix."; -+ } -+ leaf origin { -+ type ip-address-origin; -+ config false; -+ description -+ "The origin of this address."; -+ } -+ leaf status { -+ type enumeration { -+ enum preferred { -+ description -+ "This is a valid address that can appear as the -+ destination or source address of a packet."; -+ } -+ enum deprecated { -+ description -+ "This is a valid but deprecated address that should -+ no longer be used as a source address in new -+ communications, but packets addressed to such an -+ address are processed as expected."; -+ } -+ enum invalid { -+ description -+ "This isn't a valid address, and it shouldn't appear -+ as the destination or source address of a packet."; -+ } -+ -+ enum inaccessible { -+ description -+ "The address is not accessible because the interface -+ to which this address is assigned is not -+ operational."; -+ } -+ enum unknown { -+ description -+ "The status cannot be determined for some reason."; -+ } -+ enum tentative { -+ description -+ "The uniqueness of the address on the link is being -+ verified. Addresses in this state should not be -+ used for general communication and should only be -+ used to determine the uniqueness of the address."; -+ } -+ enum duplicate { -+ description -+ "The address has been determined to be non-unique on -+ the link and so must not be used."; -+ } -+ enum optimistic { -+ description -+ "The address is available for use, subject to -+ restrictions, while its uniqueness on a link is -+ being verified."; -+ } -+ } -+ config false; -+ description -+ "The status of an address. Most of the states correspond -+ to states from the IPv6 Stateless Address -+ Autoconfiguration protocol."; -+ reference -+ "RFC 4293: Management Information Base for the -+ Internet Protocol (IP) -+ - IpAddressStatusTC -+ RFC 4862: IPv6 Stateless Address Autoconfiguration"; -+ } -+ } -+ -+ list neighbor { -+ key "ip"; -+ description -+ "A list of mappings from IPv6 addresses to -+ link-layer addresses. -+ -+ Entries in this list in the intended configuration are -+ used as static entries in the Neighbor Cache. -+ -+ In the operational state, this list represents the -+ Neighbor Cache."; -+ reference -+ "RFC 4861: Neighbor Discovery for IP version 6 (IPv6)"; -+ -+ leaf ip { -+ type inet:ipv6-address-no-zone; -+ description -+ "The IPv6 address of the neighbor node."; -+ } -+ leaf link-layer-address { -+ type yang:phys-address; -+ mandatory true; -+ description -+ "The link-layer address of the neighbor node. -+ -+ In the operational state, if the neighbor's 'state' leaf -+ is 'incomplete', this leaf is not instantiated."; -+ } -+ leaf origin { -+ type neighbor-origin; -+ config false; -+ description -+ "The origin of this neighbor entry."; -+ } -+ leaf is-router { -+ type empty; -+ config false; -+ description -+ "Indicates that the neighbor node acts as a router."; -+ } -+ -+ leaf state { -+ type enumeration { -+ enum incomplete { -+ description -+ "Address resolution is in progress, and the -+ link-layer address of the neighbor has not yet been -+ determined."; -+ } -+ enum reachable { -+ description -+ "Roughly speaking, the neighbor is known to have been -+ reachable recently (within tens of seconds ago)."; -+ } -+ enum stale { -+ description -+ "The neighbor is no longer known to be reachable, but -+ until traffic is sent to the neighbor no attempt -+ should be made to verify its reachability."; -+ } -+ enum delay { -+ description -+ "The neighbor is no longer known to be reachable, and -+ traffic has recently been sent to the neighbor. -+ Rather than probe the neighbor immediately, however, -+ delay sending probes for a short while in order to -+ give upper-layer protocols a chance to provide -+ reachability confirmation."; -+ } -+ enum probe { -+ description -+ "The neighbor is no longer known to be reachable, and -+ unicast Neighbor Solicitation probes are being sent -+ to verify reachability."; -+ } -+ } -+ config false; -+ description -+ "The Neighbor Unreachability Detection state of this -+ entry."; -+ reference -+ "RFC 4861: Neighbor Discovery for IP version 6 (IPv6) -+ Section 7.3.2"; -+ } -+ } -+ -+ leaf dup-addr-detect-transmits { -+ type uint32; -+ default 1; -+ description -+ "The number of consecutive Neighbor Solicitation messages -+ sent while performing Duplicate Address Detection on a -+ tentative address. A value of zero indicates that -+ Duplicate Address Detection is not performed on -+ tentative addresses. A value of one indicates a single -+ transmission with no follow-up retransmissions."; -+ reference -+ "RFC 4862: IPv6 Stateless Address Autoconfiguration"; -+ } -+ container autoconf { -+ description -+ "Parameters to control the autoconfiguration of IPv6 -+ addresses, as described in RFC 4862."; -+ reference -+ "RFC 4862: IPv6 Stateless Address Autoconfiguration"; -+ -+ leaf create-global-addresses { -+ type boolean; -+ default true; -+ description -+ "If enabled, the host creates global addresses as -+ described in RFC 4862."; -+ reference -+ "RFC 4862: IPv6 Stateless Address Autoconfiguration -+ Section 5.5"; -+ } -+ leaf create-temporary-addresses { -+ if-feature ipv6-privacy-autoconf; -+ type boolean; -+ default false; -+ description -+ "If enabled, the host creates temporary addresses as -+ described in RFC 4941."; -+ reference -+ "RFC 4941: Privacy Extensions for Stateless Address -+ Autoconfiguration in IPv6"; -+ } -+ -+ leaf temporary-valid-lifetime { -+ if-feature ipv6-privacy-autoconf; -+ type uint32; -+ units "seconds"; -+ default 604800; -+ description -+ "The time period during which the temporary address -+ is valid."; -+ reference -+ "RFC 4941: Privacy Extensions for Stateless Address -+ Autoconfiguration in IPv6 -+ - TEMP_VALID_LIFETIME"; -+ } -+ leaf temporary-preferred-lifetime { -+ if-feature ipv6-privacy-autoconf; -+ type uint32; -+ units "seconds"; -+ default 86400; -+ description -+ "The time period during which the temporary address is -+ preferred."; -+ reference -+ "RFC 4941: Privacy Extensions for Stateless Address -+ Autoconfiguration in IPv6 -+ - TEMP_PREFERRED_LIFETIME"; -+ } -+ } -+ } -+ } -+ -+ /* -+ * Legacy operational state data nodes -+ */ -+ -+ augment "/if:interfaces-state/if:interface" { -+ status deprecated; -+ description -+ "Data nodes for the operational state of IP on interfaces."; -+ -+ container ipv4 { -+ presence -+ "Present if IPv4 is enabled on this interface"; -+ config false; -+ status deprecated; -+ description -+ "Interface-specific parameters for the IPv4 address family."; -+ -+ leaf forwarding { -+ type boolean; -+ status deprecated; -+ description -+ "Indicates whether IPv4 packet forwarding is enabled or -+ disabled on this interface."; -+ } -+ leaf mtu { -+ type uint16 { -+ range "68..max"; -+ } -+ units "octets"; -+ status deprecated; -+ description -+ "The size, in octets, of the largest IPv4 packet that the -+ interface will send and receive."; -+ reference -+ "RFC 791: Internet Protocol"; -+ } -+ list address { -+ key "ip"; -+ status deprecated; -+ description -+ "The list of IPv4 addresses on the interface."; -+ -+ leaf ip { -+ type inet:ipv4-address-no-zone; -+ status deprecated; -+ description -+ "The IPv4 address on the interface."; -+ } -+ choice subnet { -+ status deprecated; -+ description -+ "The subnet can be specified as a prefix length or, -+ if the server supports non-contiguous netmasks, as -+ a netmask."; -+ leaf prefix-length { -+ type uint8 { -+ range "0..32"; -+ } -+ status deprecated; -+ description -+ "The length of the subnet prefix."; -+ } -+ leaf netmask { -+ if-feature ipv4-non-contiguous-netmasks; -+ type yang:dotted-quad; -+ status deprecated; -+ description -+ "The subnet specified as a netmask."; -+ } -+ } -+ leaf origin { -+ type ip-address-origin; -+ status deprecated; -+ description -+ "The origin of this address."; -+ } -+ } -+ list neighbor { -+ key "ip"; -+ status deprecated; -+ description -+ "A list of mappings from IPv4 addresses to -+ link-layer addresses. -+ -+ This list represents the ARP Cache."; -+ reference -+ "RFC 826: An Ethernet Address Resolution Protocol"; -+ -+ leaf ip { -+ type inet:ipv4-address-no-zone; -+ status deprecated; -+ description -+ "The IPv4 address of the neighbor node."; -+ } -+ -+ leaf link-layer-address { -+ type yang:phys-address; -+ status deprecated; -+ description -+ "The link-layer address of the neighbor node."; -+ } -+ leaf origin { -+ type neighbor-origin; -+ status deprecated; -+ description -+ "The origin of this neighbor entry."; -+ } -+ } -+ } -+ -+ container ipv6 { -+ presence -+ "Present if IPv6 is enabled on this interface"; -+ config false; -+ status deprecated; -+ description -+ "Parameters for the IPv6 address family."; -+ -+ leaf forwarding { -+ type boolean; -+ default false; -+ status deprecated; -+ description -+ "Indicates whether IPv6 packet forwarding is enabled or -+ disabled on this interface."; -+ reference -+ "RFC 4861: Neighbor Discovery for IP version 6 (IPv6) -+ Section 6.2.1, IsRouter"; -+ } -+ leaf mtu { -+ type uint32 { -+ range "1280..max"; -+ } -+ units "octets"; -+ status deprecated; -+ description -+ "The size, in octets, of the largest IPv6 packet that the -+ interface will send and receive."; -+ reference -+ "RFC 8200: Internet Protocol, Version 6 (IPv6) -+ Specification -+ Section 5"; -+ } -+ list address { -+ key "ip"; -+ status deprecated; -+ description -+ "The list of IPv6 addresses on the interface."; -+ -+ leaf ip { -+ type inet:ipv6-address-no-zone; -+ status deprecated; -+ description -+ "The IPv6 address on the interface."; -+ } -+ leaf prefix-length { -+ type uint8 { -+ range "0..128"; -+ } -+ mandatory true; -+ status deprecated; -+ description -+ "The length of the subnet prefix."; -+ } -+ leaf origin { -+ type ip-address-origin; -+ status deprecated; -+ description -+ "The origin of this address."; -+ } -+ leaf status { -+ type enumeration { -+ enum preferred { -+ description -+ "This is a valid address that can appear as the -+ destination or source address of a packet."; -+ } -+ enum deprecated { -+ description -+ "This is a valid but deprecated address that should -+ no longer be used as a source address in new -+ communications, but packets addressed to such an -+ address are processed as expected."; -+ } -+ enum invalid { -+ description -+ "This isn't a valid address, and it shouldn't appear -+ as the destination or source address of a packet."; -+ } -+ -+ enum inaccessible { -+ description -+ "The address is not accessible because the interface -+ to which this address is assigned is not -+ operational."; -+ } -+ enum unknown { -+ description -+ "The status cannot be determined for some reason."; -+ } -+ enum tentative { -+ description -+ "The uniqueness of the address on the link is being -+ verified. Addresses in this state should not be -+ used for general communication and should only be -+ used to determine the uniqueness of the address."; -+ } -+ enum duplicate { -+ description -+ "The address has been determined to be non-unique on -+ the link and so must not be used."; -+ } -+ enum optimistic { -+ description -+ "The address is available for use, subject to -+ restrictions, while its uniqueness on a link is -+ being verified."; -+ } -+ } -+ status deprecated; -+ description -+ "The status of an address. Most of the states correspond -+ to states from the IPv6 Stateless Address -+ Autoconfiguration protocol."; -+ reference -+ "RFC 4293: Management Information Base for the -+ Internet Protocol (IP) -+ - IpAddressStatusTC -+ RFC 4862: IPv6 Stateless Address Autoconfiguration"; -+ } -+ } -+ -+ list neighbor { -+ key "ip"; -+ status deprecated; -+ description -+ "A list of mappings from IPv6 addresses to -+ link-layer addresses. -+ -+ This list represents the Neighbor Cache."; -+ reference -+ "RFC 4861: Neighbor Discovery for IP version 6 (IPv6)"; -+ -+ leaf ip { -+ type inet:ipv6-address-no-zone; -+ status deprecated; -+ description -+ "The IPv6 address of the neighbor node."; -+ } -+ leaf link-layer-address { -+ type yang:phys-address; -+ status deprecated; -+ description -+ "The link-layer address of the neighbor node."; -+ } -+ leaf origin { -+ type neighbor-origin; -+ status deprecated; -+ description -+ "The origin of this neighbor entry."; -+ } -+ leaf is-router { -+ type empty; -+ status deprecated; -+ description -+ "Indicates that the neighbor node acts as a router."; -+ } -+ leaf state { -+ type enumeration { -+ enum incomplete { -+ description -+ "Address resolution is in progress, and the -+ link-layer address of the neighbor has not yet been -+ determined."; -+ } -+ enum reachable { -+ description -+ "Roughly speaking, the neighbor is known to have been -+ reachable recently (within tens of seconds ago)."; -+ } -+ enum stale { -+ description -+ "The neighbor is no longer known to be reachable, but -+ until traffic is sent to the neighbor no attempt -+ should be made to verify its reachability."; -+ } -+ enum delay { -+ description -+ "The neighbor is no longer known to be reachable, and -+ traffic has recently been sent to the neighbor. -+ Rather than probe the neighbor immediately, however, -+ delay sending probes for a short while in order to -+ give upper-layer protocols a chance to provide -+ reachability confirmation."; -+ } -+ enum probe { -+ description -+ "The neighbor is no longer known to be reachable, and -+ unicast Neighbor Solicitation probes are being sent -+ to verify reachability."; -+ } -+ } -+ status deprecated; -+ description -+ "The Neighbor Unreachability Detection state of this -+ entry."; -+ reference -+ "RFC 4861: Neighbor Discovery for IP version 6 (IPv6) -+ Section 7.3.2"; -+ } -+ } -+ } -+ } -+} -diff --git a/tests/yang/ietf-network-instance@2019-01-21.yang b/tests/yang/ietf-network-instance@2019-01-21.yang -new file mode 100644 -index 0000000..dfde7fb ---- /dev/null -+++ b/tests/yang/ietf-network-instance@2019-01-21.yang -@@ -0,0 +1,282 @@ -+module ietf-network-instance { -+ yang-version 1.1; -+ namespace "urn:ietf:params:xml:ns:yang:ietf-network-instance"; -+ prefix ni; -+ -+ // import some basic types -+ -+ import ietf-interfaces { -+ prefix if; -+ reference -+ "RFC 8343: A YANG Data Model for Interface Management"; -+ } -+ import ietf-ip { -+ prefix ip; -+ reference -+ "RFC 8344: A YANG Data Model for IP Management"; -+ } -+ import ietf-yang-schema-mount { -+ prefix yangmnt; -+ reference -+ "RFC 8528: YANG Schema Mount"; -+ } -+ -+ organization -+ "IETF Routing Area (rtgwg) Working Group"; -+ contact -+ "WG Web: -+ WG List: -+ -+ Author: Lou Berger -+ -+ Author: Christian Hopps -+ -+ Author: Acee Lindem -+ -+ Author: Dean Bogdanovic -+ "; -+ description -+ "This module is used to support multiple network instances -+ within a single physical or virtual device. Network -+ instances are commonly known as VRFs (VPN Routing and -+ Forwarding) and VSIs (Virtual Switching Instances). -+ 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 8529; see -+ the RFC itself for full legal notices."; -+ -+ revision 2019-01-21 { -+ description -+ "Initial revision."; -+ reference -+ "RFC 8529"; -+ } -+ -+ // top-level device definition statements -+ -+ container network-instances { -+ description -+ "Network instances, each of which consists of -+ VRFs and/or VSIs."; -+ reference -+ "RFC 8349: A YANG Data Model for Routing Management"; -+ list network-instance { -+ key "name"; -+ description -+ "List of network instances."; -+ leaf name { -+ type string; -+ mandatory true; -+ description -+ "device-scoped identifier for the network -+ instance."; -+ } -+ leaf enabled { -+ type boolean; -+ default "true"; -+ description -+ "Flag indicating whether or not the network -+ instance is enabled."; -+ } -+ leaf description { -+ type string; -+ description -+ "Description of the network instance -+ and its intended purpose."; -+ } -+ choice ni-type { -+ description -+ "This node serves as an anchor point for different types -+ of network instances. Each 'case' is expected to -+ differ in terms of the information needed in the -+ parent/core to support the NI and may differ in their -+ mounted-schema definition. When the mounted schema is -+ not expected to be the same for a specific type of NI, -+ a mount point should be defined."; -+ } -+ choice root-type { -+ mandatory true; -+ description -+ "Well-known mount points."; -+ container vrf-root { -+ description -+ "Container for mount point."; -+ yangmnt:mount-point "vrf-root" { -+ description -+ "Root for L3VPN-type models. This will typically -+ not be an inline-type mount point."; -+ } -+ } -+ container vsi-root { -+ description -+ "Container for mount point."; -+ yangmnt:mount-point "vsi-root" { -+ description -+ "Root for L2VPN-type models. This will typically -+ not be an inline-type mount point."; -+ } -+ } -+ container vv-root { -+ description -+ "Container for mount point."; -+ yangmnt:mount-point "vv-root" { -+ description -+ "Root models that support both L2VPN-type bridging -+ and L3VPN-type routing. This will typically -+ not be an inline-type mount point."; -+ } -+ } -+ } -+ } -+ } -+ -+ // augment statements -+ -+ augment "/if:interfaces/if:interface" { -+ description -+ "Add a node for the identification of the network -+ instance associated with the information configured -+ on a interface. -+ -+ Note that a standard error will be returned if the -+ identified leafref isn't present. If an interface cannot -+ be assigned for any other reason, the operation SHALL fail -+ with an error-tag of 'operation-failed' and an -+ error-app-tag of 'ni-assignment-failed'. A meaningful -+ error-info that indicates the source of the assignment -+ failure SHOULD also be provided."; -+ leaf bind-ni-name { -+ type leafref { -+ path "/network-instances/network-instance/name"; -+ } -+ description -+ "Network instance to which an interface is bound."; -+ } -+ } -+ augment "/if:interfaces/if:interface/ip:ipv4" { -+ description -+ "Add a node for the identification of the network -+ instance associated with the information configured -+ on an IPv4 interface. -+ -+ Note that a standard error will be returned if the -+ identified leafref isn't present. If an interface cannot -+ be assigned for any other reason, the operation SHALL fail -+ with an error-tag of 'operation-failed' and an -+ error-app-tag of 'ni-assignment-failed'. A meaningful -+ error-info that indicates the source of the assignment -+ failure SHOULD also be provided."; -+ leaf bind-ni-name { -+ type leafref { -+ path "/network-instances/network-instance/name"; -+ } -+ description -+ "Network instance to which IPv4 interface is bound."; -+ } -+ } -+ augment "/if:interfaces/if:interface/ip:ipv6" { -+ description -+ "Add a node for the identification of the network -+ instance associated with the information configured -+ on an IPv6 interface. -+ -+ Note that a standard error will be returned if the -+ identified leafref isn't present. If an interface cannot -+ be assigned for any other reason, the operation SHALL fail -+ with an error-tag of 'operation-failed' and an -+ error-app-tag of 'ni-assignment-failed'. A meaningful -+ error-info that indicates the source of the assignment -+ failure SHOULD also be provided."; -+ leaf bind-ni-name { -+ type leafref { -+ path "/network-instances/network-instance/name"; -+ } -+ description -+ "Network instance to which IPv6 interface is bound."; -+ } -+ } -+ -+ // notification statements -+ -+ notification bind-ni-name-failed { -+ description -+ "Indicates an error in the association of an interface to an -+ NI. Only generated after success is initially returned when -+ bind-ni-name is set. -+ -+ Note: Some errors may need to be reported for multiple -+ associations, e.g., a single error may need to be reported -+ for an IPv4 and an IPv6 bind-ni-name. -+ -+ At least one container with a bind-ni-name leaf MUST be -+ included in this notification."; -+ leaf name { -+ type leafref { -+ path "/if:interfaces/if:interface/if:name"; -+ } -+ mandatory true; -+ description -+ "Contains the interface name associated with the -+ failure."; -+ } -+ container interface { -+ description -+ "Generic interface type."; -+ leaf bind-ni-name { -+ type leafref { -+ path "/if:interfaces/if:interface" -+ + "/ni:bind-ni-name"; -+ } -+ description -+ "Contains the bind-ni-name associated with the -+ failure."; -+ } -+ } -+ container ipv4 { -+ description -+ "IPv4 interface type."; -+ leaf bind-ni-name { -+ type leafref { -+ path "/if:interfaces/if:interface/ip:ipv4/ni:bind-ni-name"; -+ } -+ description -+ "Contains the bind-ni-name associated with the -+ failure."; -+ } -+ } -+ container ipv6 { -+ description -+ "IPv6 interface type."; -+ leaf bind-ni-name { -+ type leafref { -+ path "/if:interfaces/if:interface/ip:ipv6" -+ + "/ni:bind-ni-name"; -+ } -+ description -+ "Contains the bind-ni-name associated with the -+ failure."; -+ } -+ } -+ leaf error-info { -+ type string; -+ description -+ "Optionally, indicates the source of the assignment -+ failure."; -+ } -+ } -+} -diff --git a/tests/yang/ietf-restconf@2017-01-26.yang b/tests/yang/ietf-restconf@2017-01-26.yang -new file mode 100644 -index 0000000..b47455b ---- /dev/null -+++ b/tests/yang/ietf-restconf@2017-01-26.yang -@@ -0,0 +1,278 @@ -+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/tests/yang/ietf-subscribed-notifications@2019-09-09.yang b/tests/yang/ietf-subscribed-notifications@2019-09-09.yang -new file mode 100644 -index 0000000..e04593c ---- /dev/null -+++ b/tests/yang/ietf-subscribed-notifications@2019-09-09.yang -@@ -0,0 +1,1350 @@ -+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/tests/yang/ietf-yang-patch@2017-02-22.yang b/tests/yang/ietf-yang-patch@2017-02-22.yang -new file mode 100644 -index 0000000..d0029ed ---- /dev/null -+++ b/tests/yang/ietf-yang-patch@2017-02-22.yang -@@ -0,0 +1,390 @@ -+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/tests/yang/ietf-yang-push@2019-09-09.yang b/tests/yang/ietf-yang-push@2019-09-09.yang -new file mode 100644 -index 0000000..ea38fb3 ---- /dev/null -+++ b/tests/yang/ietf-yang-push@2019-09-09.yang -@@ -0,0 +1,797 @@ -+module ietf-yang-push { -+ yang-version 1.1; -+ namespace "urn:ietf:params:xml:ns:yang:ietf-yang-push"; -+ prefix yp; -+ -+ import ietf-yang-types { -+ prefix yang; -+ reference -+ "RFC 6991: Common YANG Data Types"; -+ } -+ import ietf-subscribed-notifications { -+ prefix sn; -+ reference -+ "RFC 8639: Subscription to YANG Notifications"; -+ } -+ import ietf-datastores { -+ prefix ds; -+ reference -+ "RFC 8342: Network Management Datastore Architecture (NMDA)"; -+ } -+ import ietf-restconf { -+ prefix rc; -+ reference -+ "RFC 8040: RESTCONF Protocol"; -+ } -+ import ietf-yang-patch { -+ prefix ypatch; -+ reference -+ "RFC 8072: YANG Patch Media Type"; -+ } -+ -+ organization -+ "IETF NETCONF (Network Configuration) Working Group"; -+ contact -+ "WG Web: -+ WG List: -+ -+ Author: Alexander Clemm -+ -+ -+ Author: Eric Voit -+ "; -+ -+ description -+ "This module contains YANG specifications for YANG-Push. -+ -+ 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 8641; see the -+ RFC itself for full legal notices."; -+ -+ revision 2019-09-09 { -+ description -+ "Initial revision."; -+ reference -+ "RFC 8641: Subscriptions to YANG Datastores"; -+ } -+ -+ /* -+ * FEATURES -+ */ -+ -+ feature on-change { -+ description -+ "This feature indicates that on-change triggered subscriptions -+ are supported."; -+ } -+ -+ /* -+ * IDENTITIES -+ */ -+ -+ /* Error type identities for datastore subscription */ -+ -+ identity resync-subscription-error { -+ description -+ "Problem found while attempting to fulfill a -+ 'resync-subscription' RPC request."; -+ } -+ -+ identity cant-exclude { -+ base sn:establish-subscription-error; -+ description -+ "Unable to remove the set of 'excluded-change' parameters. -+ This means that the publisher is unable to restrict -+ 'push-change-update' notifications to just the change types -+ requested for this subscription."; -+ } -+ -+ identity datastore-not-subscribable { -+ base sn:establish-subscription-error; -+ base sn:subscription-terminated-reason; -+ description -+ "This is not a subscribable datastore."; -+ } -+ -+ identity no-such-subscription-resync { -+ base resync-subscription-error; -+ description -+ "The 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 on-change-unsupported { -+ base sn:establish-subscription-error; -+ description -+ "On-change is not supported for any objects that are -+ selectable by this filter."; -+ } -+ -+ identity on-change-sync-unsupported { -+ base sn:establish-subscription-error; -+ description -+ "Neither 'sync-on-start' nor resynchronization is supported for -+ this subscription. This error will be used for two reasons: -+ (1) if an 'establish-subscription' RPC includes -+ 'sync-on-start' but the publisher can't support sending a -+ 'push-update' for this subscription for reasons other than -+ 'on-change-unsupported' or 'sync-too-big' -+ (2) if the 'resync-subscription' RPC is invoked for either an -+ existing periodic subscription or an on-change subscription -+ that can't support resynchronization."; -+ } -+ -+ identity period-unsupported { -+ base sn:establish-subscription-error; -+ base sn:modify-subscription-error; -+ base sn:subscription-suspended-reason; -+ description -+ "The requested time period or 'dampening-period' is too short. -+ This can be for both periodic and on-change subscriptions -+ (with or without dampening). Hints suggesting alternative -+ periods may be returned as supplemental information."; -+ } -+ -+ identity update-too-big { -+ base sn:establish-subscription-error; -+ base sn:modify-subscription-error; -+ base sn:subscription-suspended-reason; -+ description -+ "Periodic or on-change push update data trees exceed a maximum -+ size limit. Hints on the estimated size of what was too big -+ may be returned as supplemental information."; -+ } -+ -+ identity sync-too-big { -+ base sn:establish-subscription-error; -+ base sn:modify-subscription-error; -+ base resync-subscription-error; -+ base sn:subscription-suspended-reason; -+ description -+ "The 'sync-on-start' or resynchronization data tree exceeds a -+ maximum size limit. Hints on the estimated size of what was -+ too big may be returned as supplemental information."; -+ } -+ -+ identity unchanging-selection { -+ base sn:establish-subscription-error; -+ base sn:modify-subscription-error; -+ base sn:subscription-terminated-reason; -+ description -+ "The selection filter is unlikely to ever select data tree -+ nodes. This means that based on the subscriber's current -+ access rights, the publisher recognizes that the selection -+ filter is unlikely to ever select data tree nodes that change. -+ Examples for this might be that the node or subtree doesn't -+ exist, read access is not permitted for a receiver, or static -+ objects that only change at reboot have been chosen."; -+ } -+ -+ /* -+ * TYPE DEFINITIONS -+ */ -+ -+ typedef change-type { -+ type enumeration { -+ enum create { -+ description -+ "A change that refers to the creation of a new -+ datastore node."; -+ } -+ enum delete { -+ description -+ "A change that refers to the deletion of a -+ datastore node."; -+ } -+ enum insert { -+ description -+ "A change that refers to the insertion of a new -+ user-ordered datastore node."; -+ } -+ enum move { -+ description -+ "A change that refers to a reordering of the target -+ datastore node."; -+ } -+ enum replace { -+ description -+ "A change that refers to a replacement of the target -+ datastore node's value."; -+ } -+ } -+ description -+ "Specifies different types of datastore changes. -+ -+ This type is based on the edit operations defined for -+ YANG Patch, with the difference that it is valid for a -+ receiver to process an update record that performs a -+ 'create' operation on a datastore node the receiver believes -+ exists or to process a delete on a datastore node the -+ receiver believes is missing."; -+ reference -+ "RFC 8072: YANG Patch Media Type, Section 2.5"; -+ } -+ -+ typedef selection-filter-ref { -+ type leafref { -+ path "/sn:filters/yp:selection-filter/yp:filter-id"; -+ } -+ description -+ "This type is used to reference a selection filter."; -+ } -+ -+ typedef centiseconds { -+ type uint32; -+ description -+ "A period of time, measured in units of 0.01 seconds."; -+ } -+ -+ /* -+ * GROUP DEFINITIONS -+ */ -+ -+ grouping datastore-criteria { -+ description -+ "A grouping to define criteria for which selected objects from -+ a targeted datastore should be included in push updates."; -+ leaf datastore { -+ type identityref { -+ base ds:datastore; -+ } -+ mandatory true; -+ description -+ "Datastore from which to retrieve data."; -+ } -+ uses selection-filter-objects; -+ } -+ -+ grouping selection-filter-types { -+ description -+ "This grouping defines the types of selectors for objects -+ from a datastore."; -+ choice filter-spec { -+ description -+ "The content filter specification for this request."; -+ anydata datastore-subtree-filter { -+ if-feature "sn:subtree"; -+ description -+ "This parameter identifies the portions of the -+ target datastore to retrieve."; -+ reference -+ "RFC 6241: Network Configuration Protocol (NETCONF), -+ Section 6"; -+ } -+ leaf datastore-xpath-filter { -+ if-feature "sn:xpath"; -+ type yang:xpath1.0; -+ description -+ "This parameter contains an XPath expression identifying -+ the portions of the target datastore to retrieve. -+ -+ If the expression returns a node set, all nodes in the -+ node set are selected by the filter. Otherwise, if the -+ expression does not return a node set, the filter -+ doesn't select any nodes. -+ -+ 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 of the target -+ datastore."; -+ 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 selection-filter-objects { -+ description -+ "This grouping defines a selector for objects from a -+ datastore."; -+ choice selection-filter { -+ description -+ "The source of the selection filter applied to the -+ subscription. This will either (1) come referenced from a -+ global list or (2) be provided in the subscription itself."; -+ case by-reference { -+ description -+ "Incorporates a filter that has been configured -+ separately."; -+ leaf selection-filter-ref { -+ type selection-filter-ref; -+ mandatory true; -+ description -+ "References an existing selection filter that is to be -+ applied to the subscription."; -+ } -+ } -+ case within-subscription { -+ description -+ "A local definition allows a filter to have the same -+ lifecycle as the subscription."; -+ uses selection-filter-types; -+ } -+ } -+ } -+ -+ grouping update-policy-modifiable { -+ description -+ "This grouping describes the datastore-specific subscription -+ conditions that can be changed during the lifetime of the -+ subscription."; -+ choice update-trigger { -+ description -+ "Defines necessary conditions for sending an event record to -+ the subscriber."; -+ case periodic { -+ container periodic { -+ presence "indicates a periodic subscription"; -+ description -+ "The publisher is requested to periodically notify the -+ receiver regarding the current values of the datastore -+ as defined by the selection filter."; -+ leaf period { -+ type centiseconds; -+ mandatory true; -+ description -+ "Duration of time that should occur between periodic -+ push updates, in units of 0.01 seconds."; -+ } -+ leaf anchor-time { -+ type yang:date-and-time; -+ description -+ "Designates a timestamp before or after which a series -+ of periodic push updates are determined. The next -+ update will take place at a point in time that is a -+ multiple of a period from the 'anchor-time'. -+ For example, for an 'anchor-time' that is set for the -+ top of a particular minute and a period interval of a -+ minute, updates will be sent at the top of every -+ minute that this subscription is active."; -+ } -+ } -+ } -+ case on-change { -+ if-feature "on-change"; -+ container on-change { -+ presence "indicates an on-change subscription"; -+ description -+ "The publisher is requested to notify the receiver -+ regarding changes in values in the datastore subset as -+ defined by a selection filter."; -+ leaf dampening-period { -+ type centiseconds; -+ default "0"; -+ description -+ "Specifies the minimum interval between the assembly of -+ successive update records for a single receiver of a -+ subscription. Whenever subscribed objects change and -+ a dampening-period interval (which may be zero) has -+ elapsed since the previous update record creation for -+ a receiver, any subscribed objects and properties -+ that have changed since the previous update record -+ will have their current values marshalled and placed -+ in a new update record."; -+ } -+ } -+ } -+ } -+ } -+ -+ grouping update-policy { -+ description -+ "This grouping describes the datastore-specific subscription -+ conditions of a subscription."; -+ uses update-policy-modifiable { -+ augment "update-trigger/on-change/on-change" { -+ description -+ "Includes objects that are not modifiable once a -+ subscription is established."; -+ leaf sync-on-start { -+ type boolean; -+ default "true"; -+ description -+ "When this object is set to 'false', (1) it restricts an -+ on-change subscription from sending 'push-update' -+ notifications and (2) pushing a full selection per the -+ terms of the selection filter MUST NOT be done for -+ this subscription. Only updates about changes -+ (i.e., only 'push-change-update' notifications) -+ are sent. When set to 'true' (the default behavior), -+ in order to facilitate a receiver's synchronization, -+ a full update is sent, via a 'push-update' notification, -+ when the subscription starts. After that, -+ 'push-change-update' notifications are exclusively sent, -+ unless the publisher chooses to resync the subscription -+ via a new 'push-update' notification."; -+ } -+ leaf-list excluded-change { -+ type change-type; -+ description -+ "Used to restrict which changes trigger an update. For -+ example, if a 'replace' operation is excluded, only the -+ creation and deletion of objects are reported."; -+ } -+ } -+ } -+ } -+ -+ grouping hints { -+ description -+ "Parameters associated with an error for a subscription -+ made upon a datastore."; -+ leaf period-hint { -+ type centiseconds; -+ description -+ "Returned when the requested time period is too short. This -+ hint can assert a viable period for either a periodic push -+ cadence or an on-change dampening interval."; -+ } -+ leaf filter-failure-hint { -+ type string; -+ description -+ "Information describing where and/or why a provided filter -+ was unsupportable for a subscription."; -+ } -+ leaf object-count-estimate { -+ type uint32; -+ description -+ "If there are too many objects that could potentially be -+ returned by the selection filter, this identifies the -+ estimate of the number of objects that the filter would -+ potentially pass."; -+ } -+ leaf object-count-limit { -+ type uint32; -+ description -+ "If there are too many objects that could be returned by -+ the selection filter, this identifies the upper limit of -+ the publisher's ability to service this subscription."; -+ } -+ leaf kilobytes-estimate { -+ type uint32; -+ description -+ "If the returned information could be beyond the capacity -+ of the publisher, this would identify the estimated -+ data size that could result from this selection filter."; -+ } -+ leaf kilobytes-limit { -+ type uint32; -+ description -+ "If the returned information would be beyond the capacity -+ of the publisher, this identifies the upper limit of the -+ publisher's ability to service this subscription."; -+ } -+ } -+ -+ /* -+ * RPCs -+ */ -+ -+ rpc resync-subscription { -+ if-feature "on-change"; -+ description -+ "This RPC allows a subscriber of an active on-change -+ subscription to request a full push of objects. -+ -+ A successful invocation results in a 'push-update' of all -+ datastore nodes that the subscriber is permitted to access. -+ This RPC can only be invoked on the same session on which the -+ subscription is currently active. In the case of an error, a -+ 'resync-subscription-error' is sent as part of an error -+ response."; -+ input { -+ leaf id { -+ type sn:subscription-id; -+ mandatory true; -+ description -+ "Identifier of the subscription that is to be resynced."; -+ } -+ } -+ } -+ -+ rc:yang-data resync-subscription-error { -+ container resync-subscription-error { -+ description -+ "If a 'resync-subscription' RPC fails, the subscription is -+ not resynced 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 resync-subscription-error; -+ } -+ mandatory true; -+ description -+ "Indicates the reason why the publisher has declined a -+ request for subscription resynchronization."; -+ } -+ uses hints; -+ } -+ } -+ -+ augment "/sn:establish-subscription/sn:input" { -+ description -+ "This augmentation adds additional subscription parameters -+ that apply specifically to datastore updates to RPC input."; -+ uses update-policy; -+ } -+ -+ augment "/sn:establish-subscription/sn:input/sn:target" { -+ description -+ "This augmentation adds the datastore as a valid target -+ for the subscription to RPC input."; -+ case datastore { -+ description -+ "Information specifying the parameters of a request for a -+ datastore subscription."; -+ uses datastore-criteria; -+ } -+ } -+ -+ rc:yang-data establish-subscription-datastore-error-info { -+ container establish-subscription-datastore-error-info { -+ description -+ "If any 'establish-subscription' RPC parameters are -+ unsupportable against the datastore, 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 sn:establish-subscription-error; -+ } -+ description -+ "Indicates the reason why the subscription has failed to -+ be created to a targeted datastore."; -+ } -+ uses hints; -+ } -+ } -+ -+ augment "/sn:modify-subscription/sn:input" { -+ description -+ "This augmentation adds additional subscription parameters -+ specific to datastore updates."; -+ uses update-policy-modifiable; -+ } -+ -+ augment "/sn:modify-subscription/sn:input/sn:target" { -+ description -+ "This augmentation adds the datastore as a valid target -+ for the subscription to RPC input."; -+ case datastore { -+ description -+ "Information specifying the parameters of a request for a -+ datastore subscription."; -+ uses datastore-criteria; -+ } -+ } -+ -+ rc:yang-data modify-subscription-datastore-error-info { -+ container modify-subscription-datastore-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 a -+ datastore. This yang-data MUST be used if hints are to be -+ provided back to the subscriber."; -+ leaf reason { -+ type identityref { -+ base sn:modify-subscription-error; -+ } -+ description -+ "Indicates the reason why the subscription has failed to -+ be modified."; -+ } -+ uses hints; -+ } -+ } -+ -+ /* -+ * NOTIFICATIONS -+ */ -+ -+ notification push-update { -+ description -+ "This notification contains a push update that in turn contains -+ data subscribed to via a subscription. In the case of a -+ periodic subscription, this notification is sent for periodic -+ updates. It can also be used for synchronization updates of -+ an on-change subscription. This notification shall only be -+ sent to receivers of a subscription. It does not constitute -+ a general-purpose notification that would be subscribable as -+ part of the NETCONF event stream by any receiver."; -+ leaf id { -+ type sn:subscription-id; -+ description -+ "This references the subscription that drove the -+ notification to be sent."; -+ } -+ anydata datastore-contents { -+ description -+ "This contains the updated data. It constitutes a snapshot -+ at the time of update of the set of data that has been -+ subscribed to. The snapshot corresponds to the same -+ snapshot that would be returned in a corresponding 'get' -+ operation with the same selection filter parameters -+ applied."; -+ } -+ leaf incomplete-update { -+ type empty; -+ description -+ "This is a flag that indicates that not all datastore -+ nodes subscribed to are included with this update. In -+ other words, the publisher has failed to fulfill its full -+ subscription obligations and, despite its best efforts, is -+ providing an incomplete set of objects."; -+ } -+ } -+ -+ notification push-change-update { -+ if-feature "on-change"; -+ description -+ "This notification contains an on-change push update. This -+ notification shall only be sent to the receivers of a -+ subscription. It does not constitute a general-purpose -+ notification that would be subscribable as part of the -+ NETCONF event stream by any receiver."; -+ leaf id { -+ type sn:subscription-id; -+ description -+ "This references the subscription that drove the -+ notification to be sent."; -+ } -+ container datastore-changes { -+ description -+ "This contains the set of datastore changes of the target -+ datastore, starting at the time of the previous update, per -+ the terms of the subscription."; -+ uses ypatch:yang-patch; -+ } -+ leaf incomplete-update { -+ type empty; -+ description -+ "The presence of this object indicates that not all changes -+ that have occurred since the last update are included with -+ this update. In other words, the publisher has failed to -+ fulfill its full subscription obligations -- for example, -+ in cases where it was not able to keep up with a burst of -+ changes."; -+ } -+ } -+ -+ augment "/sn:subscription-started" { -+ description -+ "This augmentation adds datastore-specific objects to -+ the notification that a subscription has started."; -+ uses update-policy; -+ } -+ -+ augment "/sn:subscription-started/sn:target" { -+ description -+ "This augmentation allows the datastore to be included as -+ part of the notification that a subscription has started."; -+ case datastore { -+ uses datastore-criteria { -+ refine "selection-filter/within-subscription" { -+ description -+ "Specifies the selection filter and where it originated -+ from. If the 'selection-filter-ref' is populated, the -+ filter in the subscription came from the 'filters' -+ container. Otherwise, it is populated in-line as part -+ of the subscription itself."; -+ } -+ } -+ } -+ } -+ -+ augment "/sn:subscription-modified" { -+ description -+ "This augmentation adds datastore-specific objects to -+ the notification that a subscription has been modified."; -+ uses update-policy; -+ } -+ -+ augment "/sn:subscription-modified/sn:target" { -+ description -+ "This augmentation allows the datastore to be included as -+ part of the notification that a subscription has been -+ modified."; -+ case datastore { -+ uses datastore-criteria { -+ refine "selection-filter/within-subscription" { -+ description -+ "Specifies the selection filter and where it originated -+ from. If the 'selection-filter-ref' is populated, the -+ filter in the subscription came from the 'filters' -+ container. Otherwise, it is populated in-line as part -+ of the subscription itself."; -+ } -+ } -+ } -+ } -+ -+ /* -+ * DATA NODES -+ */ -+ -+ augment "/sn:filters" { -+ description -+ "This augmentation allows the datastore to be included as part -+ of the selection-filtering criteria for a subscription."; -+ list selection-filter { -+ key "filter-id"; -+ description -+ "A list of preconfigured filters that can be applied -+ to datastore subscriptions."; -+ leaf filter-id { -+ type string; -+ description -+ "An identifier to differentiate between selection -+ filters."; -+ } -+ uses selection-filter-types; -+ } -+ } -+ -+ augment "/sn:subscriptions/sn:subscription" { -+ when 'yp:datastore'; -+ description -+ "This augmentation adds objects to a subscription that are -+ specific to a datastore subscription, i.e., a subscription to -+ a stream of datastore node updates."; -+ uses update-policy; -+ } -+ -+ augment "/sn:subscriptions/sn:subscription/sn:target" { -+ description -+ "This augmentation allows the datastore to be included as -+ part of the selection-filtering criteria for a subscription."; -+ case datastore { -+ uses datastore-criteria; -+ } -+ } -+} --- -2.43.0 - diff --git a/package/sysrepo-cpp/0013-add-subtree-filtering-to-notifications-subscription.patch b/package/sysrepo-cpp/0013-add-subtree-filtering-to-notifications-subscription.patch deleted file mode 100644 index b3924b69..00000000 --- a/package/sysrepo-cpp/0013-add-subtree-filtering-to-notifications-subscription.patch +++ /dev/null @@ -1,414 +0,0 @@ -From 9d812203b3c0550995be8d60e7b11761bdbac04d Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Tue, 1 Apr 2025 21:22:04 +0200 -Subject: [PATCH 13/20] add subtree filtering to notifications subscription -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: Ie547b5f478cc8e3b09ea2f324b62854576787e1b -Signed-off-by: Mattias Walström ---- - CMakeLists.txt | 2 +- - include/sysrepo-cpp/Session.hpp | 6 +- - src/Session.cpp | 58 +++++++++-- - tests/subscriptions-dynamic.cpp | 173 ++++++++++++++++++++++++++++++-- - 4 files changed, 213 insertions(+), 26 deletions(-) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 01ec967..47472d2 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -82,7 +82,7 @@ if(BUILD_TESTING) - --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-interfaces@2018-02-20.yang - --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-ip@2018-02-22.yang - --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-network-instance@2019-01-21.yang -- --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-subscribed-notifications@2019-09-09.yang -e replay -+ --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-subscribed-notifications@2019-09-09.yang -e replay -e subtree - --install ${CMAKE_CURRENT_SOURCE_DIR}/tests/yang/ietf-yang-push@2019-09-09.yang -e on-change - ) - -diff --git a/include/sysrepo-cpp/Session.hpp b/include/sysrepo-cpp/Session.hpp -index 255f861..0781890 100644 ---- a/include/sysrepo-cpp/Session.hpp -+++ b/include/sysrepo-cpp/Session.hpp -@@ -137,17 +137,17 @@ public: - const std::optional& callbacks = std::nullopt); - - [[nodiscard]] DynamicSubscription yangPushPeriodic( -- const std::optional& xpathFilter, -+ const std::optional>& filter, - std::chrono::milliseconds periodTime, - const std::optional& anchorTime = std::nullopt, - const std::optional& stopTime = std::nullopt); - [[nodiscard]] DynamicSubscription yangPushOnChange( -- const std::optional& xpathFilter, -+ const std::optional>& filter, - const std::optional& dampeningPeriod = std::nullopt, - SyncOnStart syncOnStart = SyncOnStart::No, - const std::optional& stopTime = std::nullopt); - [[nodiscard]] DynamicSubscription subscribeNotifications( -- const std::optional& xpathFilter, -+ const std::optional>& filter, - const std::optional& stream = std::nullopt, - const std::optional& stopTime = std::nullopt, - const std::optional& startTime = std::nullopt); -diff --git a/src/Session.cpp b/src/Session.cpp -index 2c27c92..1d65d70 100644 ---- a/src/Session.cpp -+++ b/src/Session.cpp -@@ -42,6 +42,37 @@ libyang::DataNode wrapSrData(std::shared_ptr sess, sr_data_t* - sr_release_data(data); - })); - } -+ -+std::optional constructXPathFilter(const std::optional>& filter) -+{ -+ if (!filter) { -+ return std::nullopt; -+ } -+ -+ if (std::holds_alternative(*filter)) { -+ return std::get(*filter); -+ } -+ -+ auto node = std::get(*filter); -+ auto value = node.releaseValue(); -+ -+ if (!value) { -+ return "/"; // select nothing, RFC 6241, 6.4.2 -+ } -+ -+ if (std::holds_alternative(*value)) { -+ char* str; -+ -+ auto filterTree = std::get(*value); -+ auto res = srsn_filter_subtree2xpath(libyang::getRawNode(filterTree), nullptr, &str); -+ std::unique_ptr strDeleter(str); // pass ownership of c-string to the deleter -+ -+ throwIfError(res, "Unable to convert subtree filter to xpath"); -+ return str; -+ } -+ -+ throw Error("Subtree filter anydata node must contain (possibly empty) libyang tree"); -+} - } - - /** -@@ -526,9 +557,9 @@ Subscription Session::onNotification( - /** - * Subscribe for receiving notifications according to 'ietf-yang-push' YANG periodic subscriptions. - * -- * Wraps `srsn_yang_push_periodic`. -+ * Wraps `srsn_subscribe` and `srsn_filter_subtree2xpath` for subtree filters. - * -- * @param xpathFilter Optional XPath that filters received notification. -+ * @param filter Optional filter for received notification, xpath filter for string type, subtree filter for libyang::DataNodeAny - * @param periodTime Notification period. - * @param anchorTime Optional anchor time for the period. Anchor time acts as a reference point for the period. - * @param stopTime Optional stop time ending the notification subscription. -@@ -536,7 +567,7 @@ Subscription Session::onNotification( - * @return A YangPushSubscription handle. - */ - DynamicSubscription Session::yangPushPeriodic( -- const std::optional& xpathFilter, -+ const std::optional>& filter, - std::chrono::milliseconds periodTime, - const std::optional& anchorTime, - const std::optional& stopTime) -@@ -545,6 +576,8 @@ DynamicSubscription Session::yangPushPeriodic( - uint32_t subId; - auto stopSpec = stopTime ? std::optional{toTimespec(*stopTime)} : std::nullopt; - auto anchorSpec = anchorTime ? std::optional{toTimespec(*anchorTime)} : std::nullopt; -+ auto xpathFilter = constructXPathFilter(filter); -+ - auto res = srsn_yang_push_periodic(m_sess.get(), - toDatastore(activeDatastore()), - xpathFilter ? xpathFilter->c_str() : nullptr, -@@ -561,9 +594,9 @@ DynamicSubscription Session::yangPushPeriodic( - /** - * Subscribe for receiving notifications according to 'ietf-yang-push' YANG on-change subscriptions. - * -- * Wraps `srsn_yang_push_on_change`. -+ * Wraps `srsn_subscribe` and `srsn_filter_subtree2xpath` for subtree filters. - * -- * @param xpathFilter Optional XPath that filters received notification. -+ * @param filter Optional filter for received notification, xpath filter for string type, subtree filter for libyang::DataNodeAny - * @param dampeningPeriod Optional dampening period. - * @param syncOnStart Whether to start with a notification of the current state. - * @param stopTime Optional stop time ending the notification subscription. -@@ -571,7 +604,7 @@ DynamicSubscription Session::yangPushPeriodic( - * @return A YangPushSubscription handle. - */ - DynamicSubscription Session::yangPushOnChange( -- const std::optional& xpathFilter, -+ const std::optional>& filter, - const std::optional& dampeningPeriod, - SyncOnStart syncOnStart, - const std::optional& stopTime) -@@ -579,6 +612,8 @@ DynamicSubscription Session::yangPushOnChange( - int fd; - uint32_t subId; - auto stopSpec = stopTime ? std::optional{toTimespec(*stopTime)} : std::nullopt; -+ auto xpathFilter = constructXPathFilter(filter); -+ - auto res = srsn_yang_push_on_change(m_sess.get(), - toDatastore(activeDatastore()), - xpathFilter ? xpathFilter->c_str() : nullptr, -@@ -596,11 +631,11 @@ DynamicSubscription Session::yangPushOnChange( - } - - /** -- * Subscribe for receiving notifications according to 'ietf-subscribed-notifications' -+ * Subscribe for receiving notifications according to 'ietf-subscribed-notifications'. - * -- * Wraps `srsn_subscribe. -+ * Wraps `srsn_subscribe` and `srsn_filter_subtree2xpath` for subtree filters. - * -- * @param xpathFilter Optional XPath that filters received notification. -+ * @param filter Optional filter for received notification, xpath filter for string type, subtree filter for libyang::DataNodeAny - * @param stream Optional stream to subscribe to. - * @param stopTime Optional stop time ending the subscription. - * @param startTime Optional start time of the subscription, used for replaying stored notifications. -@@ -608,7 +643,7 @@ DynamicSubscription Session::yangPushOnChange( - * @return A YangPushSubscription handle. - */ - DynamicSubscription Session::subscribeNotifications( -- const std::optional& xpathFilter, -+ const std::optional>& filter, - const std::optional& stream, - const std::optional& stopTime, - const std::optional& startTime) -@@ -618,10 +653,11 @@ DynamicSubscription Session::subscribeNotifications( - auto stopSpec = stopTime ? std::optional{toTimespec(*stopTime)} : std::nullopt; - auto startSpec = startTime ? std::optional{toTimespec(*startTime)} : std::nullopt; - struct timespec replayStartSpec; -+ auto xpathFilter = constructXPathFilter(filter); - - auto res = srsn_subscribe(m_sess.get(), - stream ? stream->c_str() : nullptr, -- xpathFilter ? xpathFilter->c_str() : nullptr, -+ xpathFilter ? xpathFilter->data() : nullptr, - stopSpec ? &stopSpec.value() : nullptr, - startSpec ? &startSpec.value() : nullptr, - false, -diff --git a/tests/subscriptions-dynamic.cpp b/tests/subscriptions-dynamic.cpp -index a44bfba..84a0880 100644 ---- a/tests/subscriptions-dynamic.cpp -+++ b/tests/subscriptions-dynamic.cpp -@@ -28,6 +28,9 @@ - #define REQUIRE_NOTIFICATION(SUBSCRIPTION, NOTIFICATION) \ - TROMPELOEIL_REQUIRE_CALL(rec, recordNotification(NOTIFICATION)).IN_SEQUENCE(seq); - -+#define REQUIRE_NAMED_NOTIFICATION(SUBSCRIPTION, NOTIFICATION) \ -+ expectations.emplace_back(TROMPELOEIL_NAMED_REQUIRE_CALL(rec, recordNotification(NOTIFICATION)).IN_SEQUENCE(seq)); -+ - #define READ_NOTIFICATION(SUBSCRIPTION) \ - REQUIRE(pipeStatus((SUBSCRIPTION).fd()) == PipeStatus::DataReady); \ - (SUBSCRIPTION).processEvent(cbNotif); -@@ -276,6 +279,89 @@ TEST_CASE("Dynamic subscriptions") - const auto excMessage = "Couldn't terminate yang-push subscription with id " + std::to_string(sub->subscriptionId()) + ": SR_ERR_NOT_FOUND"; - REQUIRE_THROWS_WITH_AS(sub->terminate(), excMessage.c_str(), sysrepo::ErrorWithCode); - } -+ -+ DOCTEST_SUBCASE("Filtering") -+ { -+ std::optional sub; -+ std::vector> expectations; -+ -+ DOCTEST_SUBCASE("xpath filter") -+ { -+ sub = sess.subscribeNotifications("/test_module:ping"); -+ -+ REQUIRE_NAMED_NOTIFICATION(sub, notifications[0]); -+ } -+ -+ DOCTEST_SUBCASE("subtree filter") -+ { -+ libyang::CreatedNodes createdNodes; -+ -+ DOCTEST_SUBCASE("filter a node") -+ { -+ DOCTEST_SUBCASE("XML") -+ { -+ createdNodes = sess.getContext().newPath2( -+ "/ietf-subscribed-notifications:establish-subscription/stream-subtree-filter", -+ libyang::XML{""}); -+ } -+ -+ DOCTEST_SUBCASE("JSON") -+ { -+ createdNodes = sess.getContext().newPath2( -+ "/ietf-subscribed-notifications:establish-subscription/stream-subtree-filter", -+ libyang::JSON{R"({"test_module:ping": {}})"}); -+ } -+ -+ REQUIRE_NAMED_NOTIFICATION(sub, notifications[0]); -+ } -+ -+ DOCTEST_SUBCASE("filter more top level nodes") -+ { -+ DOCTEST_SUBCASE("XML") -+ { -+ createdNodes = sess.getContext().newPath2( -+ "/ietf-subscribed-notifications:establish-subscription/stream-subtree-filter", -+ libyang::XML{"" -+ ""}); -+ } -+ -+ DOCTEST_SUBCASE("JSON") -+ { -+ createdNodes = sess.getContext().newPath2( -+ "/ietf-subscribed-notifications:establish-subscription/stream-subtree-filter", -+ libyang::JSON{R"({ -+ "test_module:ping": {}, -+ "test_module:silent-ping": {} -+ })"}); -+ } -+ -+ REQUIRE_NAMED_NOTIFICATION(sub, notifications[0]); -+ REQUIRE_NAMED_NOTIFICATION(sub, notifications[1]); -+ } -+ -+ DOCTEST_SUBCASE("empty filter selects nothing") -+ { -+ createdNodes = sess.getContext().newPath2( -+ "/ietf-subscribed-notifications:establish-subscription/stream-subtree-filter", -+ std::nullopt); -+ } -+ -+ sub = sess.subscribeNotifications(createdNodes.createdNode->asAny()); -+ } -+ -+ CLIENT_SEND_NOTIFICATION(notifications[0]); -+ CLIENT_SEND_NOTIFICATION(notifications[1]); -+ -+ // read as many notifications as we expect -+ for (size_t i = 0; i < expectations.size(); ++i) { -+ READ_NOTIFICATION_BLOCKING(*sub); -+ } -+ -+ sub->terminate(); -+ -+ // ensure no more notifications were sent -+ REQUIRE_PIPE_HANGUP(*sub); -+ } - } - - DOCTEST_SUBCASE("YANG Push on change") -@@ -285,9 +371,73 @@ TEST_CASE("Dynamic subscriptions") - * between writing to sysrepo and reading the notifications. - */ - -- auto sub = sess.yangPushOnChange(std::nullopt, std::nullopt, sysrepo::SyncOnStart::Yes); -+ DOCTEST_SUBCASE("Filters") -+ { -+ std::optional sub; -+ -+ DOCTEST_SUBCASE("XPath filter") -+ { -+ sub = sess.yangPushOnChange("/test_module:leafInt32 | /test_module:popelnice/content/trash[name='asd']"); -+ } -+ -+ DOCTEST_SUBCASE("Subtree filter") -+ { -+ auto createdNodes = sess.getContext().newPath2( -+ "/ietf-subscribed-notifications:establish-subscription/ietf-yang-push:datastore-subtree-filter", -+ libyang::XML{"" -+ "asd"}); -+ sub = sess.yangPushOnChange(createdNodes.createdNode->asAny()); -+ } -+ -+ client.setItem("/test_module:leafInt32", "42"); -+ client.setItem("/test_module:popelnice/s", "asd"); -+ client.setItem("/test_module:popelnice/content/trash[name='asd']", std::nullopt); -+ client.applyChanges(); - -- REQUIRE_YANG_PUSH_UPDATE(sub, R"({ -+ client.deleteItem("/test_module:popelnice/s"); -+ client.applyChanges(); -+ -+ REQUIRE_YANG_PUSH_UPDATE(*sub, R"({ -+ "ietf-yang-push:push-change-update": { -+ "datastore-changes": { -+ "yang-patch": { -+ "patch-id": "patch-1", -+ "edit": [ -+ { -+ "edit-id": "edit-1", -+ "operation": "create", -+ "target": "/test_module:leafInt32", -+ "value": { -+ "test_module:leafInt32": 42 -+ } -+ }, -+ { -+ "edit-id": "edit-2", -+ "operation": "create", -+ "target": "/test_module:popelnice/content/trash[name='asd']", -+ "value": { -+ "test_module:trash": { -+ "name": "asd" -+ } -+ } -+ } -+ ] -+ } -+ } -+ } -+} -+)"); -+ READ_YANG_PUSH_UPDATE(*sub); -+ -+ sub->terminate(); -+ REQUIRE_PIPE_HANGUP(*sub); -+ } -+ -+ DOCTEST_SUBCASE("Sync on start") -+ { -+ auto sub = sess.yangPushOnChange(std::nullopt, std::nullopt, sysrepo::SyncOnStart::Yes); -+ -+ REQUIRE_YANG_PUSH_UPDATE(sub, R"({ - "ietf-yang-push:push-update": { - "datastore-contents": { - "test_module:values": [ -@@ -298,14 +448,14 @@ TEST_CASE("Dynamic subscriptions") - } - } - )"); -- READ_YANG_PUSH_UPDATE(sub); -+ READ_YANG_PUSH_UPDATE(sub); - -- client.setItem("/test_module:leafInt32", "123"); -- client.setItem("/test_module:values[.='5']", std::nullopt); -- client.deleteItem("/test_module:values[.='3']"); -- client.applyChanges(); -+ client.setItem("/test_module:leafInt32", "123"); -+ client.setItem("/test_module:values[.='5']", std::nullopt); -+ client.deleteItem("/test_module:values[.='3']"); -+ client.applyChanges(); - -- REQUIRE_YANG_PUSH_UPDATE(sub, R"({ -+ REQUIRE_YANG_PUSH_UPDATE(sub, R"({ - "ietf-yang-push:push-change-update": { - "datastore-changes": { - "yang-patch": { -@@ -342,10 +492,11 @@ TEST_CASE("Dynamic subscriptions") - } - } - )"); -- READ_YANG_PUSH_UPDATE(sub); -+ READ_YANG_PUSH_UPDATE(sub); - -- sub.terminate(); -- REQUIRE_PIPE_HANGUP(sub); -+ sub.terminate(); -+ REQUIRE_PIPE_HANGUP(sub); -+ } - } - - DOCTEST_SUBCASE("YANG Push periodic") --- -2.43.0 - diff --git a/package/sysrepo-cpp/0014-add-excluded-changes-to-YANG-push-on-change-wrapper.patch b/package/sysrepo-cpp/0014-add-excluded-changes-to-YANG-push-on-change-wrapper.patch deleted file mode 100644 index b8378efb..00000000 --- a/package/sysrepo-cpp/0014-add-excluded-changes-to-YANG-push-on-change-wrapper.patch +++ /dev/null @@ -1,187 +0,0 @@ -From b960058e37c471084538bb5db8b3b5e9e992c46c Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Wed, 2 Apr 2025 21:47:21 +0200 -Subject: [PATCH 14/20] add excluded changes to YANG push on-change wrapper -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: Idb7bb48a3c60ff70286441cd4d0a5c1c28acee15 -Signed-off-by: Mattias Walström ---- - include/sysrepo-cpp/Enum.hpp | 12 +++++++ - include/sysrepo-cpp/Session.hpp | 1 + - src/Session.cpp | 12 ++++++- - src/utils/enum.hpp | 8 +++++ - tests/subscriptions-dynamic.cpp | 60 +++++++++++++++++++++++++++++++++ - 5 files changed, 92 insertions(+), 1 deletion(-) - -diff --git a/include/sysrepo-cpp/Enum.hpp b/include/sysrepo-cpp/Enum.hpp -index cac1f2e..48467c6 100644 ---- a/include/sysrepo-cpp/Enum.hpp -+++ b/include/sysrepo-cpp/Enum.hpp -@@ -209,6 +209,18 @@ constexpr GetOptions operator|(const GetOptions a, const GetOptions b) - return implEnumBitOr(a, b); - } - -+/** -+ * @brief Wraps `srsn_yang_push_change_t` -+ */ -+enum class YangPushChange : uint32_t { -+ Create = 0, /**< SRSN_YP_CHANGE_CREATE */ -+ Delete = 1, /**< SRSN_YP_CHANGE_DELETE */ -+ Insert = 2, /**< SRSN_YP_CHANGE_INSERT */ -+ Move = 3, /**< SRSN_YP_CHANGE_MOVE */ -+ Replace = 4, /**< SRSN_YP_CHANGE_REPLACE */ -+ EnumCount = 5, /**< SRSN_COUNT_YP_CHANGE */ -+}; -+ - std::ostream& operator<<(std::ostream& os, const NotificationType& type); - std::ostream& operator<<(std::ostream& os, const Event& event); - std::ostream& operator<<(std::ostream& os, const ChangeOperation& changeOp); -diff --git a/include/sysrepo-cpp/Session.hpp b/include/sysrepo-cpp/Session.hpp -index 0781890..b7cad72 100644 ---- a/include/sysrepo-cpp/Session.hpp -+++ b/include/sysrepo-cpp/Session.hpp -@@ -145,6 +145,7 @@ public: - const std::optional>& filter, - const std::optional& dampeningPeriod = std::nullopt, - SyncOnStart syncOnStart = SyncOnStart::No, -+ const std::set& excludedChanges = {}, - const std::optional& stopTime = std::nullopt); - [[nodiscard]] DynamicSubscription subscribeNotifications( - const std::optional>& filter, -diff --git a/src/Session.cpp b/src/Session.cpp -index 1d65d70..40c3df3 100644 ---- a/src/Session.cpp -+++ b/src/Session.cpp -@@ -607,6 +607,7 @@ DynamicSubscription Session::yangPushOnChange( - const std::optional>& filter, - const std::optional& dampeningPeriod, - SyncOnStart syncOnStart, -+ const std::set& excludedChanges, - const std::optional& stopTime) - { - int fd; -@@ -614,12 +615,21 @@ DynamicSubscription Session::yangPushOnChange( - auto stopSpec = stopTime ? std::optional{toTimespec(*stopTime)} : std::nullopt; - auto xpathFilter = constructXPathFilter(filter); - -+ /* The enum values are not used as the other enum flags in sysrepo-cpp. -+ * srsn_yang_push_on_change expects an integer array of size EnumCount with 0 or 1 values. -+ */ -+ using YangPushChangeUnderlying = std::underlying_type_t; -+ std::array(YangPushChange::EnumCount)> excludedChangesArray{}; -+ for (const auto& change: excludedChanges) { -+ excludedChangesArray[static_cast(change)] = 1; -+ } -+ - auto res = srsn_yang_push_on_change(m_sess.get(), - toDatastore(activeDatastore()), - xpathFilter ? xpathFilter->c_str() : nullptr, - dampeningPeriod ? dampeningPeriod->count() : 0, - syncOnStart == SyncOnStart::Yes, -- nullptr, -+ excludedChangesArray.data(), - stopSpec ? &stopSpec.value() : nullptr, - 0, - nullptr, -diff --git a/src/utils/enum.hpp b/src/utils/enum.hpp -index a2dcf7a..2d3a298 100644 ---- a/src/utils/enum.hpp -+++ b/src/utils/enum.hpp -@@ -9,6 +9,7 @@ - #include - extern "C" { - #include -+#include - } - #include - -@@ -171,4 +172,11 @@ constexpr sr_get_options_t toGetOptions(const GetOptions opts) - { - return static_cast(opts); - } -+ -+static_assert(static_cast(SRSN_YP_CHANGE_CREATE) == YangPushChange::Create); -+static_assert(static_cast(SRSN_YP_CHANGE_DELETE) == YangPushChange::Delete); -+static_assert(static_cast(SRSN_YP_CHANGE_INSERT) == YangPushChange::Insert); -+static_assert(static_cast(SRSN_YP_CHANGE_MOVE) == YangPushChange::Move); -+static_assert(static_cast(SRSN_YP_CHANGE_REPLACE) == YangPushChange::Replace); -+static_assert(static_cast(SRSN_COUNT_YP_CHANGE) == YangPushChange::EnumCount); - } -diff --git a/tests/subscriptions-dynamic.cpp b/tests/subscriptions-dynamic.cpp -index 84a0880..5ed173b 100644 ---- a/tests/subscriptions-dynamic.cpp -+++ b/tests/subscriptions-dynamic.cpp -@@ -497,6 +497,66 @@ TEST_CASE("Dynamic subscriptions") - sub.terminate(); - REQUIRE_PIPE_HANGUP(sub); - } -+ -+ DOCTEST_SUBCASE("Excluded changes") -+ { -+ auto sub = sess.yangPushOnChange(std::nullopt, std::nullopt, sysrepo::SyncOnStart::No, {sysrepo::YangPushChange::Create}); -+ -+ client.setItem("/test_module:leafInt32", "123"); -+ client.applyChanges(); // excluded (create) -+ client.setItem("/test_module:leafInt32", "124"); -+ client.applyChanges(); -+ client.setItem("/test_module:leafInt32", "125"); -+ client.applyChanges(); -+ -+ REQUIRE_YANG_PUSH_UPDATE(sub, R"({ -+ "ietf-yang-push:push-change-update": { -+ "datastore-changes": { -+ "yang-patch": { -+ "patch-id": "patch-1", -+ "edit": [ -+ { -+ "edit-id": "edit-1", -+ "operation": "replace", -+ "target": "/test_module:leafInt32", -+ "value": { -+ "test_module:leafInt32": 124 -+ } -+ } -+ ] -+ } -+ } -+ } -+} -+)"); -+ -+ READ_YANG_PUSH_UPDATE(sub); -+ -+ REQUIRE_YANG_PUSH_UPDATE(sub, R"({ -+ "ietf-yang-push:push-change-update": { -+ "datastore-changes": { -+ "yang-patch": { -+ "patch-id": "patch-2", -+ "edit": [ -+ { -+ "edit-id": "edit-1", -+ "operation": "replace", -+ "target": "/test_module:leafInt32", -+ "value": { -+ "test_module:leafInt32": 125 -+ } -+ } -+ ] -+ } -+ } -+ } -+} -+)"); -+ READ_YANG_PUSH_UPDATE(sub); -+ -+ sub.terminate(); -+ REQUIRE_PIPE_HANGUP(sub); -+ } - } - - DOCTEST_SUBCASE("YANG Push periodic") --- -2.43.0 - diff --git a/package/sysrepo-cpp/0015-wrap-sr_nacm_get_user.patch b/package/sysrepo-cpp/0015-wrap-sr_nacm_get_user.patch deleted file mode 100644 index 3e40cad4..00000000 --- a/package/sysrepo-cpp/0015-wrap-sr_nacm_get_user.patch +++ /dev/null @@ -1,84 +0,0 @@ -From 3407f98d147ffbf031725102181e1fe3cd874363 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Tue, 8 Apr 2025 12:52:00 +0200 -Subject: [PATCH 15/20] wrap sr_nacm_get_user -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -This adds a counterpart to setting NACM user. Sometimes, it might be -useful to get it as well. - -Change-Id: I534a229eb536d63091904ab4f6268cb9da6dd3db -Signed-off-by: Mattias Walström ---- - include/sysrepo-cpp/Session.hpp | 1 + - src/Session.cpp | 9 +++++++++ - tests/session.cpp | 7 +++++++ - 3 files changed, 17 insertions(+) - -diff --git a/include/sysrepo-cpp/Session.hpp b/include/sysrepo-cpp/Session.hpp -index b7cad72..1c409b8 100644 ---- a/include/sysrepo-cpp/Session.hpp -+++ b/include/sysrepo-cpp/Session.hpp -@@ -100,6 +100,7 @@ public: - void replaceConfig(std::optional config, const std::optional& moduleName = std::nullopt, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}); - - void setNacmUser(const std::string& user); -+ std::optional getNacmUser() const; - [[nodiscard]] Subscription initNacm( - SubscribeOptions opts = SubscribeOptions::Default, - ExceptionHandler handler = nullptr, -diff --git a/src/Session.cpp b/src/Session.cpp -index 40c3df3..2273c79 100644 ---- a/src/Session.cpp -+++ b/src/Session.cpp -@@ -706,6 +706,15 @@ void Session::setNacmUser(const std::string& user) - throwIfError(res, "Couldn't set NACM user", m_sess.get()); - } - -+/** -+ * @brief Get the NACM user for this session. -+ */ -+std::optional Session::getNacmUser() const -+{ -+ auto* username = sr_nacm_get_user(m_sess.get()); -+ return username ? std::make_optional(username) : std::nullopt; -+} -+ - /** - * @brief Initializes NACM callbacks. - * -diff --git a/tests/session.cpp b/tests/session.cpp -index 9a419a1..548b371 100644 ---- a/tests/session.cpp -+++ b/tests/session.cpp -@@ -390,10 +390,14 @@ TEST_CASE("session") - auto data = sess.getData("/test_module:denyAllLeaf"); - REQUIRE(data.value().findPath("/test_module:denyAllLeaf").value().asTerm().valueStr() == "AHOJ"); - -+ REQUIRE(!sess.getNacmUser()); -+ - // check that repeated NACM initialization still works - for (int i = 0; i < 3; ++i) { - auto nacmSub = sess.initNacm(); - sess.setNacmUser("nobody"); -+ REQUIRE(sess.getNacmUser() == "nobody"); -+ - data = sess.getData("/test_module:denyAllLeaf"); - // After turning on NACM, we can't access the leaf. - REQUIRE(!data); -@@ -408,6 +412,9 @@ TEST_CASE("session") - sysrepo::ErrorWithCode); - } - -+ REQUIRE(!!sess.getNacmUser()); -+ REQUIRE(sess.getNacmUser() == "nobody"); -+ - // duplicate NACM initialization should throw - auto nacm = sess.initNacm(); - REQUIRE_THROWS_WITH_AS(auto x = sess.initNacm(), --- -2.43.0 - diff --git a/package/sysrepo-cpp/0016-CI-renamed-project-upstream.patch b/package/sysrepo-cpp/0016-CI-renamed-project-upstream.patch deleted file mode 100644 index d5ceb5f7..00000000 --- a/package/sysrepo-cpp/0016-CI-renamed-project-upstream.patch +++ /dev/null @@ -1,69 +0,0 @@ -From afc4b8e04b4bf61d6bf0e0a66c1115b0111eaebf Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= -Date: Wed, 9 Apr 2025 15:36:30 +0200 -Subject: [PATCH 16/20] CI: renamed project upstream -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Change-Id: I5447f243297fbfde7c364eb3919b00db239bd069 -Depends-on: https://gerrit.cesnet.cz/c/CzechLight/libyang-cpp/+/8570 -Signed-off-by: Mattias Walström ---- - .zuul.yaml | 4 ++-- - README.md | 2 +- - ci/build.sh | 2 +- - 3 files changed, 4 insertions(+), 4 deletions(-) - -diff --git a/.zuul.yaml b/.zuul.yaml -index dc87c6f..7ada6f7 100644 ---- a/.zuul.yaml -+++ b/.zuul.yaml -@@ -7,7 +7,7 @@ - override-checkout: devel - - name: github/sysrepo/sysrepo - override-checkout: devel -- - name: github/onqtam/doctest -+ - name: github/doctest/doctest - override-checkout: v2.4.8 - - name: github/rollbear/trompeloeil - override-checkout: v44 -@@ -18,7 +18,7 @@ - override-checkout: devel - - name: github/sysrepo/sysrepo - override-checkout: devel -- - name: github/onqtam/doctest -+ - name: github/doctest/doctest - override-checkout: v2.4.11 - - name: github/rollbear/trompeloeil - override-checkout: v44 -diff --git a/README.md b/README.md -index 27dc748..dd9570c 100644 ---- a/README.md -+++ b/README.md -@@ -12,7 +12,7 @@ It uses RAII for automatic memory management. - - [libyang-cpp](https://github.com/CESNET/libyang-cpp) - C++ bindings for *libyang* - - C++20 compiler (e.g., GCC 10.x+, clang 10+) - - CMake 3.19+ --- optionally for built-in tests, [Doctest](https://github.com/onqtam/doctest/) as a C++ unit test framework -+- optionally for built-in tests, [Doctest](https://github.com/doctest/doctest/) as a C++ unit test framework - - optionally for built-in tests, [trompeloeil](https://github.com/rollbear/trompeloeil) for mock objects in C++ - - optionally for the docs, Doxygen - -diff --git a/ci/build.sh b/ci/build.sh -index d886b79..785a811 100755 ---- a/ci/build.sh -+++ b/ci/build.sh -@@ -78,7 +78,7 @@ build_n_test() { - - build_n_test github/CESNET/libyang -DENABLE_BUILD_TESTS=ON -DENABLE_VALGRIND_TESTS=OFF - build_n_test github/sysrepo/sysrepo -DENABLE_BUILD_TESTS=ON -DENABLE_VALGRIND_TESTS=OFF -DREPO_PATH=${PREFIX}/etc-sysrepo --build_n_test github/onqtam/doctest -DDOCTEST_WITH_TESTS=OFF -+build_n_test github/doctest/doctest -DDOCTEST_WITH_TESTS=OFF - # non-release builds download Catch2 - CMAKE_BUILD_TYPE=Release build_n_test github/rollbear/trompeloeil - build_n_test CzechLight/libyang-cpp -DBUILD_TESTING=ON --- -2.43.0 - diff --git a/package/sysrepo-cpp/0017-wrap-sr_nacm_check_operation.patch b/package/sysrepo-cpp/0017-wrap-sr_nacm_check_operation.patch deleted file mode 100644 index d1ae2545..00000000 --- a/package/sysrepo-cpp/0017-wrap-sr_nacm_check_operation.patch +++ /dev/null @@ -1,133 +0,0 @@ -From ad070266f9347f159874e6b2fa57302385f354e3 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Wed, 9 Apr 2025 14:51:31 +0200 -Subject: [PATCH 17/20] wrap sr_nacm_check_operation -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Sysrepo provides API function for explicit checking if an operation can -be authorized on a node. This might come handy if one decides to do NACM -authorization himself, like we will do in our RESTCONF server. - -Change-Id: Ida41514a7f03ab120a331363b7f9ed8b69918d88 -Signed-off-by: Mattias Walström ---- - include/sysrepo-cpp/Session.hpp | 1 + - src/Session.cpp | 13 ++++++++++ - tests/session.cpp | 46 +++++++++++++++++++++++++++++++++ - tests/test_module.yang | 4 +++ - 4 files changed, 64 insertions(+) - -diff --git a/include/sysrepo-cpp/Session.hpp b/include/sysrepo-cpp/Session.hpp -index 1c409b8..2112d21 100644 ---- a/include/sysrepo-cpp/Session.hpp -+++ b/include/sysrepo-cpp/Session.hpp -@@ -101,6 +101,7 @@ public: - - void setNacmUser(const std::string& user); - std::optional getNacmUser() const; -+ bool checkNacmOperation(const libyang::DataNode& node) const; - [[nodiscard]] Subscription initNacm( - SubscribeOptions opts = SubscribeOptions::Default, - ExceptionHandler handler = nullptr, -diff --git a/src/Session.cpp b/src/Session.cpp -index 2273c79..ce0dec2 100644 ---- a/src/Session.cpp -+++ b/src/Session.cpp -@@ -715,6 +715,19 @@ std::optional Session::getNacmUser() const - return username ? std::make_optional(username) : std::nullopt; - } - -+/** -+ * @brief Checks if operation is allowed for current NACM user. Wraps `sr_nacm_check_operation`. -+ * @return true if the current user is authorized to perform operation on given @p node. -+ * -+ * Details on unsuccessfull authorizations can be retrieved via Session::getErrors. -+ * Note that if the NACM user is not set, `sr_nacm_check_operation` and this function both return true. -+ */ -+bool Session::checkNacmOperation(const libyang::DataNode& node) const -+{ -+ auto res = sr_nacm_check_operation(m_sess.get(), libyang::getRawNode(node)); -+ return res == SR_ERR_OK; -+} -+ - /** - * @brief Initializes NACM callbacks. - * -diff --git a/tests/session.cpp b/tests/session.cpp -index 548b371..35dd545 100644 ---- a/tests/session.cpp -+++ b/tests/session.cpp -@@ -423,6 +423,52 @@ TEST_CASE("session") - sysrepo::ErrorWithCode); - } - -+ DOCTEST_SUBCASE("Session::checkNacmOperation") -+ { -+ auto nacmSub = sess.initNacm(); -+ -+ // check NACM access for RPCs -+ auto shutdownRPC = sess.getContext().newPath("/test_module:shutdown", std::nullopt); -+ auto denyAllRPC = sess.getContext().newPath("/test_module:deny-all-rpc", std::nullopt); -+ -+ // user not set, everything is permitted -+ REQUIRE(sess.checkNacmOperation(shutdownRPC) == true); -+ REQUIRE(sess.checkNacmOperation(denyAllRPC) == true); -+ REQUIRE(sess.getErrors().size() == 0); -+ -+ sess.setNacmUser("root"); -+ REQUIRE(sess.checkNacmOperation(shutdownRPC) == true); -+ REQUIRE(sess.checkNacmOperation(denyAllRPC) == true); -+ REQUIRE(sess.getErrors().size() == 0); -+ -+ sess.setNacmUser("nobody"); -+ REQUIRE(sess.checkNacmOperation(shutdownRPC) == true); -+ REQUIRE(sess.checkNacmOperation(denyAllRPC) == false); -+ REQUIRE(sess.getErrors().size() == 1); -+ REQUIRE(sess.getErrors().at(0) == sysrepo::ErrorInfo{ -+ .code = sysrepo::ErrorCode::Unauthorized, -+ .errorMessage = "Executing the operation is denied because \"nobody\" NACM authorization failed.", -+ }); -+ -+ sess.setNacmUser("root"); // 'nobody' is not authorized to write into this subtree -+ sess.switchDatastore(sysrepo::Datastore::Running); -+ sess.setItem("/ietf-netconf-acm:nacm/enable-external-groups", "false"); -+ sess.setItem("/ietf-netconf-acm:nacm/groups/group[name='grp']/user-name[.='nobody']", ""); -+ sess.setItem("/ietf-netconf-acm:nacm/rule-list[name='rule']/group[.='grp']", ""); -+ sess.setItem("/ietf-netconf-acm:nacm/rule-list[name='rule']/rule[name='1']/module-name", "test_module"); -+ sess.setItem("/ietf-netconf-acm:nacm/rule-list[name='rule']/rule[name='1']/access-operations", "*"); -+ sess.setItem("/ietf-netconf-acm:nacm/rule-list[name='rule']/rule[name='1']/action", "deny"); -+ sess.applyChanges(); -+ -+ sess.setNacmUser("root"); -+ REQUIRE(sess.checkNacmOperation(denyAllRPC) == true); -+ REQUIRE(sess.checkNacmOperation(shutdownRPC) == true); -+ -+ sess.setNacmUser("nobody"); -+ REQUIRE(sess.checkNacmOperation(denyAllRPC) == false); -+ REQUIRE(sess.checkNacmOperation(shutdownRPC) == false); -+ } -+ - DOCTEST_SUBCASE("Session::getPendingChanges") - { - REQUIRE(sess.getPendingChanges() == std::nullopt); -diff --git a/tests/test_module.yang b/tests/test_module.yang -index 02c467b..3d6c26d 100644 ---- a/tests/test_module.yang -+++ b/tests/test_module.yang -@@ -55,6 +55,10 @@ module test_module { - rpc noop { - } - -+ rpc deny-all-rpc { -+ nacm:default-deny-all; -+ } -+ - rpc shutdown { - output { - leaf success { --- -2.43.0 - diff --git a/package/sysrepo-cpp/0018-Fix-return-value-type-in-doxygen.patch b/package/sysrepo-cpp/0018-Fix-return-value-type-in-doxygen.patch deleted file mode 100644 index b0910d4b..00000000 --- a/package/sysrepo-cpp/0018-Fix-return-value-type-in-doxygen.patch +++ /dev/null @@ -1,50 +0,0 @@ -From cecf636eecea53a597bcbf75eb4dda0f916e9967 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Tue, 15 Apr 2025 10:54:11 +0200 -Subject: [PATCH 18/20] Fix return value type in doxygen -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Fixes: 415c401 ("wrap dynamic subscription functions") -Change-Id: I66912530d8aa16bb4dde3d35611be1e6e4247b36 -Signed-off-by: Mattias Walström ---- - src/Session.cpp | 6 +++--- - 1 file changed, 3 insertions(+), 3 deletions(-) - -diff --git a/src/Session.cpp b/src/Session.cpp -index ce0dec2..8258295 100644 ---- a/src/Session.cpp -+++ b/src/Session.cpp -@@ -564,7 +564,7 @@ Subscription Session::onNotification( - * @param anchorTime Optional anchor time for the period. Anchor time acts as a reference point for the period. - * @param stopTime Optional stop time ending the notification subscription. - * -- * @return A YangPushSubscription handle. -+ * @return A DynamicSubscription handle. - */ - DynamicSubscription Session::yangPushPeriodic( - const std::optional>& filter, -@@ -601,7 +601,7 @@ DynamicSubscription Session::yangPushPeriodic( - * @param syncOnStart Whether to start with a notification of the current state. - * @param stopTime Optional stop time ending the notification subscription. - * -- * @return A YangPushSubscription handle. -+ * @return A DynamicSubscription handle. - */ - DynamicSubscription Session::yangPushOnChange( - const std::optional>& filter, -@@ -650,7 +650,7 @@ DynamicSubscription Session::yangPushOnChange( - * @param stopTime Optional stop time ending the subscription. - * @param startTime Optional start time of the subscription, used for replaying stored notifications. - * -- * @return A YangPushSubscription handle. -+ * @return A DynamicSubscription handle. - */ - DynamicSubscription Session::subscribeNotifications( - const std::optional>& filter, --- -2.43.0 - diff --git a/package/sysrepo-cpp/0019-add-a-session-getter-to-dynamic-subscriptions.patch b/package/sysrepo-cpp/0019-add-a-session-getter-to-dynamic-subscriptions.patch deleted file mode 100644 index 201640fa..00000000 --- a/package/sysrepo-cpp/0019-add-a-session-getter-to-dynamic-subscriptions.patch +++ /dev/null @@ -1,137 +0,0 @@ -From ee53d86929296c4824e9860f6425850f7d795302 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Tue, 15 Apr 2025 11:06:50 +0200 -Subject: [PATCH 19/20] add a session getter to dynamic subscriptions -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -Sometimes it might be useful to get the session which created the -subscription (e.g. to do some work in processEvent callback). There was -no easy way to do it up until now because the session was not stored as -sysrepo::Session. -However, it seems quite easy to change that and add the getter. - -Change-Id: Ia943a47debba6f84c3faad5ea48a69da68dac874 -Signed-off-by: Mattias Walström ---- - include/sysrepo-cpp/Subscription.hpp | 3 ++- - src/Session.cpp | 6 +++--- - src/Subscription.cpp | 17 +++++++++++------ - 3 files changed, 16 insertions(+), 10 deletions(-) - -diff --git a/include/sysrepo-cpp/Subscription.hpp b/include/sysrepo-cpp/Subscription.hpp -index 7fdc08b..4c58e78 100644 ---- a/include/sysrepo-cpp/Subscription.hpp -+++ b/include/sysrepo-cpp/Subscription.hpp -@@ -303,6 +303,7 @@ public: - DynamicSubscription& operator=(DynamicSubscription&&) noexcept; - ~DynamicSubscription(); - -+ sysrepo::Session getSession() const; - int fd() const; - uint64_t subscriptionId() const; - std::optional replayStartTime() const; -@@ -310,7 +311,7 @@ public: - void terminate(const std::optional& reason = std::nullopt); - - private: -- DynamicSubscription(std::shared_ptr sess, int fd, uint64_t subId, const std::optional& replayStartTime = std::nullopt); -+ DynamicSubscription(sysrepo::Session sess, int fd, uint64_t subId, const std::optional& replayStartTime = std::nullopt); - - struct Data; - std::unique_ptr m_data; -diff --git a/src/Session.cpp b/src/Session.cpp -index 8258295..7d44662 100644 ---- a/src/Session.cpp -+++ b/src/Session.cpp -@@ -588,7 +588,7 @@ DynamicSubscription Session::yangPushPeriodic( - &subId); - throwIfError(res, "Couldn't create yang-push periodic subscription", m_sess.get()); - -- return {m_sess, fd, subId}; -+ return {*this, fd, subId}; - } - - /** -@@ -637,7 +637,7 @@ DynamicSubscription Session::yangPushOnChange( - &subId); - throwIfError(res, "Couldn't create yang-push on-change subscription", m_sess.get()); - -- return {m_sess, fd, subId}; -+ return {*this, fd, subId}; - } - - /** -@@ -683,7 +683,7 @@ DynamicSubscription Session::subscribeNotifications( - replayStart = toTimePoint(replayStartSpec); - } - -- return {m_sess, fd, subId, replayStart}; -+ return {*this, fd, subId, replayStart}; - } - - /** -diff --git a/src/Subscription.cpp b/src/Subscription.cpp -index ab27b16..6dccaee 100644 ---- a/src/Subscription.cpp -+++ b/src/Subscription.cpp -@@ -414,18 +414,18 @@ bool ChangeIterator::operator==(const ChangeIterator& other) const - - - struct DynamicSubscription::Data { -- std::shared_ptr sess; -+ sysrepo::Session sess; - int fd; - uint64_t subId; - std::optional m_replayStartTime; - bool m_terminated; - -- Data(std::shared_ptr sess, int fd, uint64_t subId, const std::optional& replayStartTime, bool terminated); -+ Data(sysrepo::Session sess, int fd, uint64_t subId, const std::optional& replayStartTime, bool terminated); - ~Data(); - void terminate(const std::optional& reason = std::nullopt); - }; - --DynamicSubscription::DynamicSubscription(std::shared_ptr sess, int fd, uint64_t subId, const std::optional& replayStartTime) -+DynamicSubscription::DynamicSubscription(sysrepo::Session sess, int fd, uint64_t subId, const std::optional& replayStartTime) - : m_data(std::make_unique(std::move(sess), fd, subId, replayStartTime, false)) - { - } -@@ -434,6 +434,12 @@ DynamicSubscription::DynamicSubscription(DynamicSubscription&&) noexcept = defau - DynamicSubscription& DynamicSubscription::operator=(DynamicSubscription&&) noexcept = default; - DynamicSubscription::~DynamicSubscription() = default; - -+/** @brief Returns sysrepo Session associated with this subscription */ -+sysrepo::Session DynamicSubscription::getSession() const -+{ -+ return m_data->sess; -+} -+ - /** @brief Returns the file descriptor associated with this subscription. */ - int DynamicSubscription::fd() const - { -@@ -472,9 +478,8 @@ void DynamicSubscription::processEvent(YangPushNotifCb cb) const - { - struct timespec timestamp; - struct lyd_node* tree; -- auto ctx = std::unique_ptr>(sr_session_acquire_context(m_data->sess.get()), [&](const ly_ctx*) { sr_session_release_context(m_data->sess.get()); }); - -- auto err = srsn_read_notif(fd(), ctx.get(), ×tamp, &tree); -+ auto err = srsn_read_notif(fd(), libyang::retrieveContext(m_data->sess.getContext()), ×tamp, &tree); - throwIfError(err, "Couldn't read yang-push notification"); - - const auto wrappedNotification = tree ? std::optional{libyang::wrapRawNode(tree)} : std::nullopt; -@@ -488,7 +493,7 @@ void DynamicSubscription::processEvent(YangPushNotifCb cb) const - cb(wrappedNotification, toTimePoint(timestamp)); - } - --DynamicSubscription::Data::Data(std::shared_ptr sess, int fd, uint64_t subId, const std::optional& replayStartTime, bool terminated) -+DynamicSubscription::Data::Data(sysrepo::Session sess, int fd, uint64_t subId, const std::optional& replayStartTime, bool terminated) - : sess(std::move(sess)) - , fd(fd) - , subId(subId) --- -2.43.0 - diff --git a/package/sysrepo-cpp/0020-wrap-sr_nacm_get_recovery_user.patch b/package/sysrepo-cpp/0020-wrap-sr_nacm_get_recovery_user.patch deleted file mode 100644 index f6ec6edf..00000000 --- a/package/sysrepo-cpp/0020-wrap-sr_nacm_get_recovery_user.patch +++ /dev/null @@ -1,55 +0,0 @@ -From 67eed65414cb9ccd031e16ec76149cc09c341186 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= -Date: Tue, 15 Apr 2025 15:26:30 +0200 -Subject: [PATCH 20/20] wrap sr_nacm_get_recovery_user -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -The function does not actually need a session, I am only adding it to -Session so it is in the same place as other NACM user methods. - -Change-Id: I3ee015b2bdc180ab1a9115ffc1a3a53f8e6f677c -Signed-off-by: Mattias Walström ---- - include/sysrepo-cpp/Session.hpp | 1 + - src/Session.cpp | 10 ++++++++++ - 2 files changed, 11 insertions(+) - -diff --git a/include/sysrepo-cpp/Session.hpp b/include/sysrepo-cpp/Session.hpp -index 2112d21..2612e86 100644 ---- a/include/sysrepo-cpp/Session.hpp -+++ b/include/sysrepo-cpp/Session.hpp -@@ -101,6 +101,7 @@ public: - - void setNacmUser(const std::string& user); - std::optional getNacmUser() const; -+ static std::string getNacmRecoveryUser(); - bool checkNacmOperation(const libyang::DataNode& node) const; - [[nodiscard]] Subscription initNacm( - SubscribeOptions opts = SubscribeOptions::Default, -diff --git a/src/Session.cpp b/src/Session.cpp -index 7d44662..e96e6ec 100644 ---- a/src/Session.cpp -+++ b/src/Session.cpp -@@ -715,6 +715,16 @@ std::optional Session::getNacmUser() const - return username ? std::make_optional(username) : std::nullopt; - } - -+/** -+ * @brief Get the sysrepo NACM recovery user. -+ * -+ * wraps `sr_nacm_get_recovery_user`. -+ */ -+std::string Session::getNacmRecoveryUser() -+{ -+ return sr_nacm_get_recovery_user(); -+} -+ - /** - * @brief Checks if operation is allowed for current NACM user. Wraps `sr_nacm_check_operation`. - * @return true if the current user is authorized to perform operation on given @p node. --- -2.43.0 - diff --git a/package/sysrepo-cpp/sysrepo-cpp.hash b/package/sysrepo-cpp/sysrepo-cpp.hash index 8ed9551c..072e85a7 100644 --- a/package/sysrepo-cpp/sysrepo-cpp.hash +++ b/package/sysrepo-cpp/sysrepo-cpp.hash @@ -1,3 +1,3 @@ # Locally calculated sha256 82e3758011ec44c78e98d0777799d6e12aec5b8a64b32ebb20d0fe50e32488bb LICENSE -sha256 1fe8b979a53d3581157607bbbf608cc556b02ccfb05b77e26b77b8c875201bd3 sysrepo-cpp-v3.tar.gz +sha256 d7fbeaba7f2fdaf70c67cc70f438943eee636b21f08854e58967d0d97801bacc sysrepo-cpp-v6.tar.gz diff --git a/package/sysrepo-cpp/sysrepo-cpp.mk b/package/sysrepo-cpp/sysrepo-cpp.mk index afd03f12..d676c657 100644 --- a/package/sysrepo-cpp/sysrepo-cpp.mk +++ b/package/sysrepo-cpp/sysrepo-cpp.mk @@ -3,7 +3,7 @@ # CPP bindings for sysrepo # ################################################################################ -SYSREPO_CPP_VERSION = v3 +SYSREPO_CPP_VERSION = v6 SYSREPO_CPP_SITE = $(call github,sysrepo,sysrepo-cpp,$(SYSREPO_CPP_VERSION)) SYSREPO_CPP_LICENSE = BSD-3-Clause SYSREPO_CPP_LICENSE_FILES = LICENSE diff --git a/patches/netopeer2/2.4.1/0001-Allow-factory-as-copy-from-only-in-rpc-copy-config.patch b/patches/netopeer2/2.4.5/0001-Allow-factory-as-copy-from-only-in-rpc-copy-config.patch similarity index 94% rename from patches/netopeer2/2.4.1/0001-Allow-factory-as-copy-from-only-in-rpc-copy-config.patch rename to patches/netopeer2/2.4.5/0001-Allow-factory-as-copy-from-only-in-rpc-copy-config.patch index 6c204574..ec65a91d 100644 --- a/patches/netopeer2/2.4.1/0001-Allow-factory-as-copy-from-only-in-rpc-copy-config.patch +++ b/patches/netopeer2/2.4.5/0001-Allow-factory-as-copy-from-only-in-rpc-copy-config.patch @@ -1,4 +1,4 @@ -From c62a0c72ac2cc7c840f6004c274c37bf8ce6c854 Mon Sep 17 00:00:00 2001 +From 8406098242751deb3de1172ceda382251fd2c8f5 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 diff --git a/patches/netopeer2/2.4.1/0002-Do-not-generate-data-in-sysrepo.patch b/patches/netopeer2/2.4.5/0002-Do-not-generate-data-in-sysrepo.patch similarity index 95% rename from patches/netopeer2/2.4.1/0002-Do-not-generate-data-in-sysrepo.patch rename to patches/netopeer2/2.4.5/0002-Do-not-generate-data-in-sysrepo.patch index d44567a2..484c0bbb 100644 --- a/patches/netopeer2/2.4.1/0002-Do-not-generate-data-in-sysrepo.patch +++ b/patches/netopeer2/2.4.5/0002-Do-not-generate-data-in-sysrepo.patch @@ -1,4 +1,4 @@ -From 15bff587a9096354f20803c78fbae729f50f8d6c Mon Sep 17 00:00:00 2001 +From 6ad71e6359453b8dcecbf9f33f59bd736111ecf3 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 @@ -16,10 +16,10 @@ Signed-off-by: Mattias Walström 1 file changed, 40 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt -index 94dc315..66d69d5 100644 +index cf97275..95b82ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt -@@ -386,47 +386,7 @@ if(SYSREPO_SETUP) +@@ -395,47 +395,7 @@ if(SYSREPO_SETUP) message(FATAL_ERROR \" OUTPUT:\\n \${CMD_OUT_F}\\n ERROR:\\n \${CMD_ERR_F}\") endif() ") diff --git a/patches/sysrepo/3.6.11/0001-sysrepo-plugind-add-support-for-running-in-foregroun.patch b/patches/sysrepo/3.7.11/0001-sysrepo-plugind-add-support-for-running-in-foregroun.patch similarity index 97% rename from patches/sysrepo/3.6.11/0001-sysrepo-plugind-add-support-for-running-in-foregroun.patch rename to patches/sysrepo/3.7.11/0001-sysrepo-plugind-add-support-for-running-in-foregroun.patch index 41c03133..1a99b4ee 100644 --- a/patches/sysrepo/3.6.11/0001-sysrepo-plugind-add-support-for-running-in-foregroun.patch +++ b/patches/sysrepo/3.7.11/0001-sysrepo-plugind-add-support-for-running-in-foregroun.patch @@ -1,4 +1,4 @@ -From 0e40d3adbda4f9f215ed6bcb8de519dbfae237f0 Mon Sep 17 00:00:00 2001 +From e1ebfda6318d25804f3344caa89ce3f723b576e4 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 diff --git a/patches/sysrepo/3.6.11/0002-Allow-SR_EV_DONE-to-return-any-error-to-sysrepocfg.patch b/patches/sysrepo/3.7.11/0002-Allow-SR_EV_DONE-to-return-any-error-to-sysrepocfg.patch similarity index 72% rename from patches/sysrepo/3.6.11/0002-Allow-SR_EV_DONE-to-return-any-error-to-sysrepocfg.patch rename to patches/sysrepo/3.7.11/0002-Allow-SR_EV_DONE-to-return-any-error-to-sysrepocfg.patch index 8fd14dc0..8a42a7c9 100644 --- a/patches/sysrepo/3.6.11/0002-Allow-SR_EV_DONE-to-return-any-error-to-sysrepocfg.patch +++ b/patches/sysrepo/3.7.11/0002-Allow-SR_EV_DONE-to-return-any-error-to-sysrepocfg.patch @@ -1,4 +1,4 @@ -From 95e052137d1376df9c1a2a684cb642b40030f26e Mon Sep 17 00:00:00 2001 +From a190da4e967fb7ed65bc06794e8d775b4e1a7b84 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 @@ -22,16 +22,16 @@ the surface and cause a non-zero exit code from sysrepocfg. Signed-off-by: Joachim Wiberg Signed-off-by: Mattias Walström --- - src/shm_sub.c | 36 ++++++++++++++++++++++++++++-------- + src/shm_sub.c | 40 +++++++++++++++++++++++++++++++--------- src/shm_sub.h | 2 +- src/sysrepo.c | 4 ++-- - 3 files changed, 31 insertions(+), 11 deletions(-) + 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/shm_sub.c b/src/shm_sub.c -index 17b21bd1..f0d9c788 100644 +index 61aab4c6..6ba1cd4d 100644 --- a/src/shm_sub.c +++ b/src/shm_sub.c -@@ -1831,7 +1831,7 @@ cleanup: +@@ -1832,7 +1832,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 17b21bd1..f0d9c788 100644 { sr_error_info_t *err_info = NULL; struct sr_mod_info_mod_s *mod = NULL; -@@ -1978,12 +1978,19 @@ sr_shmsub_change_notify_change_done(struct sr_mod_info_s *mod_info, const char * +@@ -1980,12 +1980,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,16 +63,24 @@ index 17b21bd1..f0d9c788 100644 nsub->pending_event = 0; } } while (1); -@@ -3544,7 +3551,7 @@ sr_shmsub_change_listen_relock(sr_sub_shm_t *sub_shm, sr_lock_mode_t mode, struc +@@ -3557,7 +3564,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) { - sr_error_info_t *err_info = NULL; + sr_error_info_t *err_info = NULL, *err_tmp; - uint32_t i, data_len = 0, valid_subscr_count; + uint32_t i, data_len = 0, valid_subscr_count, remaining_subs; char *data = NULL, *shm_data_ptr; int ret = SR_ERR_OK, filter_valid; -@@ -3673,6 +3680,11 @@ process_event: +@@ -3579,7 +3586,6 @@ sr_shmsub_change_listen_process_module_events(struct modsub_change_s *change_sub + } + } + if (i == change_subs->sub_count) { +- /* no new module event */ + goto cleanup; + } + +@@ -3686,6 +3692,11 @@ process_event: } break; } @@ -84,7 +92,7 @@ index 17b21bd1..f0d9c788 100644 } /* subscription processed this event */ -@@ -3705,6 +3717,11 @@ process_event: +@@ -3718,6 +3729,11 @@ process_event: } break; case SR_SUB_EV_DONE: @@ -96,27 +104,30 @@ index 17b21bd1..f0d9c788 100644 case SR_SUB_EV_ABORT: /* nothing to do */ break; -@@ -3724,14 +3741,17 @@ process_event: +@@ -3742,14 +3758,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, -- ret, filter_valid, ev_sess, &err_info)) { -+ ret, filter_valid, ev_sess, &err_tmp)) { -+ if (err_tmp) -+ err_info = err_tmp; - goto cleanup; - } - sub_lock = SR_LOCK_WRITE_URGE; + /* SUB WRITE URGE LOCK */ + if (sr_shmsub_change_listen_relock(sub_shm, SR_LOCK_WRITE_URGE, &sub_info, change_sub, change_subs->module_name, +- ret, filter_valid, ev_sess, &err_info)) { ++ ret, filter_valid, ev_sess, &err_tmp)) { ++ if (err_tmp) ++ err_info = err_tmp; + goto cleanup; + } + sub_lock = SR_LOCK_WRITE_URGE; - /* finish event */ -- if ((err_info = sr_shmsub_listen_write_event(sub_shm, valid_subscr_count, err_code, &shm_data_sub, data, -+ if ((err_tmp = sr_shmsub_listen_write_event(sub_shm, valid_subscr_count, err_code, &shm_data_sub, data, - data_len, change_subs->module_name, err_code ? "fail" : "success"))) { -+ err_info = err_tmp; - goto cleanup; - } - -@@ -4221,7 +4241,7 @@ finish_iter: + /* finish event */ +- err_info = sr_shmsub_listen_write_event(sub_shm, remaining_subs ? valid_subscr_count : 0, err_code, &shm_data_sub, data, ++ err_tmp = sr_shmsub_listen_write_event(sub_shm, remaining_subs ? valid_subscr_count : 0, err_code, &shm_data_sub, data, + data_len, change_subs->module_name, err_code ? "fail" : "success"); ++ ++ if (err_tmp) ++ err_info = err_tmp; ++ goto cleanup; + } 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: sr_errinfo_free(&cb_err_info); /* publish "done" event */ @@ -139,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 f9ef46b3..93f73298 100644 +index 068585d7..72dad00c 100644 --- a/src/sysrepo.c +++ b/src/sysrepo.c -@@ -4256,7 +4256,7 @@ sr_error_info_t * +@@ -4313,7 +4313,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) { @@ -151,7 +162,7 @@ index f9ef46b3..93f73298 100644 struct sr_denied denied = {0}; sr_lock_mode_t change_sub_lock = has_change_sub_lock; uint32_t sid = 0, err_count; -@@ -4413,7 +4413,7 @@ store: +@@ -4470,7 +4470,7 @@ store: } /* publish "done" event, all changes were applied */ diff --git a/patches/sysrepo/3.6.11/0003-Allow-to-copy-from-factory-default.patch b/patches/sysrepo/3.7.11/0003-Allow-to-copy-from-factory-default.patch similarity index 85% rename from patches/sysrepo/3.6.11/0003-Allow-to-copy-from-factory-default.patch rename to patches/sysrepo/3.7.11/0003-Allow-to-copy-from-factory-default.patch index 7424e5ef..e3f1fb05 100644 --- a/patches/sysrepo/3.6.11/0003-Allow-to-copy-from-factory-default.patch +++ b/patches/sysrepo/3.7.11/0003-Allow-to-copy-from-factory-default.patch @@ -1,4 +1,4 @@ -From 5d487799853f1550b1cb0cd071fedb75ac0166e4 Mon Sep 17 00:00:00 2001 +From 81b0b781fa41a9e1c7ab2a94e6fc5aa4c52d365d 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 @@ -13,10 +13,10 @@ Signed-off-by: Mattias Walström 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sysrepo.c b/src/sysrepo.c -index 93f73298..6c7ba43e 100644 +index 72dad00c..1b54319f 100644 --- a/src/sysrepo.c +++ b/src/sysrepo.c -@@ -4838,7 +4838,7 @@ sr_copy_config(sr_session_ctx_t *session, const char *module_name, sr_datastore_ +@@ -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; const struct lys_module *ly_mod = NULL; diff --git a/patches/sysrepo/3.6.11/0004-Add-z-switch-to-sysrepoctl-to-install-factory-config.patch b/patches/sysrepo/3.7.11/0004-Add-z-switch-to-sysrepoctl-to-install-factory-config.patch similarity index 92% rename from patches/sysrepo/3.6.11/0004-Add-z-switch-to-sysrepoctl-to-install-factory-config.patch rename to patches/sysrepo/3.7.11/0004-Add-z-switch-to-sysrepoctl-to-install-factory-config.patch index 364da784..b6d0ba8f 100644 --- a/patches/sysrepo/3.6.11/0004-Add-z-switch-to-sysrepoctl-to-install-factory-config.patch +++ b/patches/sysrepo/3.7.11/0004-Add-z-switch-to-sysrepoctl-to-install-factory-config.patch @@ -1,4 +1,4 @@ -From 1c8c3b6fb780d281811a0ea25bd96c555d274a17 Mon Sep 17 00:00:00 2001 +From 496305b14423d0e0344cd4e52a80699773cd1a34 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 @@ -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 b9680551..4cdba6a0 100644 +index b33a60cd..b60d17af 100644 --- a/src/executables/sysrepoctl.c +++ b/src/executables/sysrepoctl.c -@@ -647,6 +647,7 @@ main(int argc, char **argv) +@@ -664,6 +664,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 b9680551..4cdba6a0 100644 {"plugin-list", no_argument, NULL, 'L'}, {"plugin-install", required_argument, NULL, 'P'}, {"search-dirs", required_argument, NULL, 's'}, -@@ -671,7 +672,7 @@ main(int argc, char **argv) +@@ -688,7 +689,7 @@ main(int argc, char **argv) /* process options */ opterr = 0; @@ -39,7 +39,7 @@ index b9680551..4cdba6a0 100644 switch (opt) { case 'h': /* help */ -@@ -863,6 +864,15 @@ main(int argc, char **argv) +@@ -880,6 +881,15 @@ main(int argc, char **argv) goto cleanup; } break; @@ -55,7 +55,7 @@ index b9680551..4cdba6a0 100644 case 'I': /* init-data */ if (operation == 'i') { -@@ -922,9 +932,15 @@ main(int argc, char **argv) +@@ -939,9 +949,15 @@ main(int argc, char **argv) goto cleanup; } } @@ -89,10 +89,10 @@ index f3a9b215..b86f24b4 100644 + #endif diff --git a/src/sysrepo.c b/src/sysrepo.c -index 6c7ba43e..4877080d 100644 +index 1b54319f..ca2026b2 100644 --- a/src/sysrepo.c +++ b/src/sysrepo.c -@@ -1613,6 +1613,55 @@ sr_free_int_install_mods(sr_int_install_mod_t *new_mods, uint32_t new_mod_count) +@@ -1626,6 +1626,55 @@ sr_free_int_install_mods(sr_int_install_mod_t *new_mods, uint32_t new_mod_count) free(new_mods); } @@ -149,7 +149,7 @@ index 6c7ba43e..4877080d 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 e13d3776..4a830e26 100644 +index 466c8294..ee1b47a9 100644 --- a/src/sysrepo.h +++ b/src/sysrepo.h @@ -34,7 +34,6 @@ extern "C" { @@ -160,7 +160,7 @@ index e13d3776..4a830e26 100644 /** * @defgroup log_api Logging API * @{ -@@ -717,6 +716,15 @@ int sr_get_module_info(sr_conn_ctx_t *conn, sr_data_t **sysrepo_data); +@@ -725,6 +724,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.6.11/0005-Introduce-new-log-level-SEC-for-audit-trails.patch b/patches/sysrepo/3.7.11/0005-Introduce-new-log-level-SEC-for-audit-trails.patch similarity index 92% rename from patches/sysrepo/3.6.11/0005-Introduce-new-log-level-SEC-for-audit-trails.patch rename to patches/sysrepo/3.7.11/0005-Introduce-new-log-level-SEC-for-audit-trails.patch index 6d3ceae8..71616506 100644 --- a/patches/sysrepo/3.6.11/0005-Introduce-new-log-level-SEC-for-audit-trails.patch +++ b/patches/sysrepo/3.7.11/0005-Introduce-new-log-level-SEC-for-audit-trails.patch @@ -1,4 +1,4 @@ -From 04966d045da6ae10a4fafccde2740f8c2aa03c29 Mon Sep 17 00:00:00 2001 +From 6543a03c98fe32180dc95a47d113a3bd2dd369de 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 @@ -31,10 +31,10 @@ Signed-off-by: Mattias Walström 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/log.c b/src/log.c -index e15055ac..25eab8fa 100644 +index fc1d9c57..cf652b57 100644 --- a/src/log.c +++ b/src/log.c -@@ -30,6 +30,10 @@ +@@ -32,6 +32,10 @@ #include "config.h" @@ -45,7 +45,7 @@ index e15055ac..25eab8fa 100644 sr_log_level_t sr_stderr_ll = SR_LL_NONE; /**< stderr log level */ sr_log_level_t sr_syslog_ll = SR_LL_NONE; /**< syslog log level */ int syslog_open; /**< Whether syslog was opened */ -@@ -122,6 +126,11 @@ sr_log_msg(int plugin, sr_log_level_t ll, const char *msg) +@@ -124,6 +128,11 @@ sr_log_msg(int plugin, sr_log_level_t ll, const char *msg) priority = LOG_INFO; severity = "INF"; break; @@ -57,7 +57,7 @@ index e15055ac..25eab8fa 100644 case SR_LL_DBG: priority = LOG_DEBUG; severity = "DBG"; -@@ -138,7 +147,14 @@ sr_log_msg(int plugin, sr_log_level_t ll, const char *msg) +@@ -140,7 +149,14 @@ sr_log_msg(int plugin, sr_log_level_t ll, const char *msg) /* syslog logging */ if (ll <= sr_syslog_ll) { @@ -74,10 +74,10 @@ index e15055ac..25eab8fa 100644 /* logging callback */ diff --git a/src/log.h b/src/log.h -index 9a60f1f6..acd7eead 100644 +index b4f62e7c..62279675 100644 --- a/src/log.h +++ b/src/log.h -@@ -31,6 +31,7 @@ +@@ -29,6 +29,7 @@ #define SR_LOG_WRN(...) sr_log(SR_LL_WRN, __VA_ARGS__) #define SR_LOG_INF(...) sr_log(SR_LL_INF, __VA_ARGS__) @@ -86,7 +86,7 @@ index 9a60f1f6..acd7eead 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 1fc7ed0b..b6540f77 100644 +index 165e255f..60df80cf 100644 --- a/src/sysrepo_types.h +++ b/src/sysrepo_types.h @@ -65,7 +65,8 @@ typedef enum { diff --git a/patches/sysrepo/3.6.11/0006-Add-audit-trail-for-high-priority-system-changes.patch b/patches/sysrepo/3.7.11/0006-Add-audit-trail-for-high-priority-system-changes.patch similarity index 89% rename from patches/sysrepo/3.6.11/0006-Add-audit-trail-for-high-priority-system-changes.patch rename to patches/sysrepo/3.7.11/0006-Add-audit-trail-for-high-priority-system-changes.patch index d83ea10d..cb5b88e5 100644 --- a/patches/sysrepo/3.6.11/0006-Add-audit-trail-for-high-priority-system-changes.patch +++ b/patches/sysrepo/3.7.11/0006-Add-audit-trail-for-high-priority-system-changes.patch @@ -1,4 +1,4 @@ -From 385382722f13fc3dff079b338201be1f2d7a81f0 Mon Sep 17 00:00:00 2001 +From 0ff2b7562154289de39abb10bcec2bc0d744a64d 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 @@ -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 4877080d..b3cacca7 100644 +index ca2026b2..2ea546e9 100644 --- a/src/sysrepo.c +++ b/src/sysrepo.c -@@ -4461,6 +4461,9 @@ store: +@@ -4518,6 +4518,9 @@ store: goto cleanup; } @@ -35,7 +35,7 @@ index 4877080d..b3cacca7 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; -@@ -4472,6 +4475,9 @@ store: +@@ -4529,6 +4532,9 @@ store: } cleanup: @@ -45,7 +45,7 @@ index 4877080d..b3cacca7 100644 if (change_sub_lock && !has_change_sub_lock) { assert(change_sub_lock == SR_LOCK_READ); -@@ -4974,6 +4980,9 @@ sr_copy_config(sr_session_ctx_t *session, const char *module_name, sr_datastore_ +@@ -5031,6 +5037,9 @@ sr_copy_config(sr_session_ctx_t *session, const char *module_name, sr_datastore_ } } @@ -55,7 +55,7 @@ index 4877080d..b3cacca7 100644 cleanup: /* MODULES UNLOCK */ sr_shmmod_modinfo_unlock(&mod_info); -@@ -7498,6 +7507,9 @@ sr_rpc_send_tree(sr_session_ctx_t *session, struct lyd_node *input, uint32_t tim +@@ -7562,6 +7571,9 @@ sr_rpc_send_tree(sr_session_ctx_t *session, struct lyd_node *input, uint32_t tim } } diff --git a/patches/sysrepo/3.6.11/0007-On-error-in-sr_shmsub_listen_thread-exit-process.patch b/patches/sysrepo/3.7.11/0007-On-error-in-sr_shmsub_listen_thread-exit-process.patch similarity index 87% rename from patches/sysrepo/3.6.11/0007-On-error-in-sr_shmsub_listen_thread-exit-process.patch rename to patches/sysrepo/3.7.11/0007-On-error-in-sr_shmsub_listen_thread-exit-process.patch index 271c9931..f49a9baa 100644 --- a/patches/sysrepo/3.6.11/0007-On-error-in-sr_shmsub_listen_thread-exit-process.patch +++ b/patches/sysrepo/3.7.11/0007-On-error-in-sr_shmsub_listen_thread-exit-process.patch @@ -1,4 +1,4 @@ -From 7a63e5b1ca6604a5cc156c61e080be3eaecafa60 Mon Sep 17 00:00:00 2001 +From 83e58e43fc1c968a6e007ab0c39d99aeffc8fbce 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 @@ -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 f0d9c788..eed80d92 100644 +index 6ba1cd4d..5c006a82 100644 --- a/src/shm_sub.c +++ b/src/shm_sub.c -@@ -5104,5 +5104,7 @@ error: +@@ -5049,5 +5049,7 @@ error: /* free our own resources */ ATOMIC_STORE_RELAXED(subscr->thread_running, 0); pthread_detach(pthread_self()); diff --git a/src/confd/yang/libnetconf2.inc b/src/confd/yang/libnetconf2.inc index 92c490d7..2a1fa268 100644 --- a/src/confd/yang/libnetconf2.inc +++ b/src/confd/yang/libnetconf2.inc @@ -21,5 +21,5 @@ MODULES=( "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-01-23.yang" + "libnetconf2-netconf-server@2025-06-02.yang" )