Skip to content

Commit 32bdd8b

Browse files
committed
API/ABI break: Change how pushed ops data work
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
1 parent 80e9659 commit 32bdd8b

File tree

6 files changed

+128
-42
lines changed

6 files changed

+128
-42
lines changed

.zuul.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
- name: github/CESNET/libyang
77
override-checkout: devel
88
- name: github/sysrepo/sysrepo
9-
override-checkout: cesnet/2024-10-before-oper-changes
9+
override-checkout: cesnet/2025-01-30--00
1010
- name: github/onqtam/doctest
1111
override-checkout: v2.4.8
1212
- name: github/rollbear/trompeloeil
@@ -17,7 +17,7 @@
1717
- name: github/CESNET/libyang
1818
override-checkout: devel
1919
- name: github/sysrepo/sysrepo
20-
override-checkout: cesnet/2024-10-before-oper-changes
20+
override-checkout: cesnet/2025-01-30--00
2121
- name: github/onqtam/doctest
2222
override-checkout: v2.4.11
2323
- name: github/rollbear/trompeloeil

CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ add_custom_target(sysrepo-cpp-version-cmake
2020
cmake/ProjectGitVersionRunner.cmake
2121
)
2222
include(cmake/ProjectGitVersion.cmake)
23-
set(SYSREPO_CPP_PKG_VERSION "3")
23+
set(SYSREPO_CPP_PKG_VERSION "4")
2424
prepare_git_version(SYSREPO_CPP_VERSION ${SYSREPO_CPP_PKG_VERSION})
2525

2626
find_package(Doxygen)
@@ -29,7 +29,7 @@ option(WITH_EXAMPLES "Build examples" ON)
2929

3030
find_package(PkgConfig)
3131
pkg_check_modules(LIBYANG_CPP REQUIRED libyang-cpp>=3 IMPORTED_TARGET)
32-
pkg_check_modules(SYSREPO REQUIRED sysrepo>=2.12.0 sysrepo<3 IMPORTED_TARGET)
32+
pkg_check_modules(SYSREPO REQUIRED sysrepo>=3.4.7 IMPORTED_TARGET)
3333

3434
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
3535

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ It uses RAII for automatic memory management.
99

1010
## Dependencies
1111
- [sysrepo](https://github.com/sysrepo/sysrepo) - the `devel` branch (even for the `master` branch of *sysrepo-cpp*)
12-
- we temporarily require pre-v3 sysrepo which introduced backward-incompatible changes to operational data handling
1312
- [libyang-cpp](https://github.com/CESNET/libyang-cpp) - C++ bindings for *libyang*
1413
- C++20 compiler (e.g., GCC 10.x+, clang 10+)
1514
- CMake 3.19+

include/sysrepo-cpp/Session.hpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,15 @@ class Session {
8585
void setItem(const std::string& path, const std::optional<std::string>& value, const EditOptions opts = sysrepo::EditOptions::Default);
8686
void editBatch(libyang::DataNode edit, const DefaultOperation op);
8787
void deleteItem(const std::string& path, const EditOptions opts = sysrepo::EditOptions::Default);
88-
void discardItems(const std::optional<std::string>& xpath);
8988
void moveItem(const std::string& path, const MovePosition move, const std::optional<std::string>& keys_or_value, const std::optional<std::string>& origin = std::nullopt, const EditOptions opts = sysrepo::EditOptions::Default);
9089
std::optional<libyang::DataNode> getData(const std::string& path, int maxDepth = 0, const GetOptions opts = sysrepo::GetOptions::Default, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) const;
9190
libyang::DataNode getOneNode(const std::string& path, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) const;
9291
std::optional<const libyang::DataNode> getPendingChanges() const;
9392
void applyChanges(std::chrono::milliseconds timeout = std::chrono::milliseconds{0});
94-
void discardChanges();
93+
void discardChanges(const std::optional<std::string>& xpath = std::nullopt);
94+
std::optional<libyang::DataNode> operationalChanges(const std::optional<std::string>& moduleName = std::nullopt) const;
95+
void discardOperationalChanges(const std::optional<std::string>& moduleName = std::nullopt, std::chrono::milliseconds timeout = std::chrono::milliseconds{0});
96+
void dropForeignOperationalContent(const std::optional<std::string>& xpath);
9597
void copyConfig(const Datastore source, const std::optional<std::string>& moduleName = std::nullopt, std::chrono::milliseconds timeout = std::chrono::milliseconds{0});
9698
libyang::DataNode sendRPC(libyang::DataNode input, std::chrono::milliseconds timeout = std::chrono::milliseconds{0});
9799
void sendNotification(libyang::DataNode notification, const Wait wait, std::chrono::milliseconds timeout = std::chrono::milliseconds{0});

src/Session.cpp

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,26 @@ extern "C" {
2323

2424
using namespace std::string_literals;
2525
namespace sysrepo {
26+
27+
namespace {
28+
libyang::DataNode wrapSrData(std::shared_ptr<sr_session_ctx_s> sess, sr_data_t* data)
29+
{
30+
// Since the lyd_node came from sysrepo and it is wrapped in a sr_data_t, we have to postpone calling the
31+
// sr_release_data() until after we're "done" with the libyang::DataNode.
32+
//
33+
// Normally, sr_release_data() would free the lyd_data as well. However, it is possible that the user wants to
34+
// manipulate the data tree (think unlink()) in a way which might have needed to overwrite the tree->data pointer.
35+
// Just delegate all the freeing to the C++ wrapper around lyd_data. The sysrepo library doesn't care about this.
36+
auto tree = std::exchange(data->tree, nullptr);
37+
38+
// Use wrapRawNode, not wrapUnmanagedRawNode because we want to let the C++ wrapper manage memory.
39+
// Note: We're capturing the session inside the lambda.
40+
return libyang::wrapRawNode(tree, std::shared_ptr<sr_data_t>(data, [extend_session_lifetime = sess] (sr_data_t* data) {
41+
sr_release_data(data);
42+
}));
43+
}
44+
}
45+
2646
/**
2747
* Wraps a pointer to sr_session_ctx_s and manages the lifetime of it. Also extends the lifetime of the connection
2848
* specified by the `conn` argument.
@@ -119,20 +139,61 @@ void Session::deleteItem(const std::string& path, const EditOptions opts)
119139
}
120140

121141
/**
122-
* Prepare to discard nodes matching the specified xpath (or all if not set) previously set by the session connection.
123-
* Usable only for sysrepo::Datastore::Operational. The changes are applied only after calling Session::applyChanges.
142+
* Prepare to drop "earlier content" from other sources in the operational DS for nodes matching the specified XPath
143+
*
144+
* The "earlier content" might come from the `running` datastore, or be pushed into the `operational` DS from
145+
* another session, with a lower priority. This function prepares a special node into the current session's
146+
* stored edit which effectively discards any matching content from previous, lower-priority sources.
147+
*
148+
* This function cannot be used to remove an edit which was pushed via the current session. To do that,
149+
* use discardChanges(), or retrieve the stored edit and manipulate its libyang data tree.
150+
*
151+
* The changes are applied only after calling Session::applyChanges.
124152
*
125153
* Wraps `sr_discard_items`.
126154
*
127155
* @param xpath Expression filtering the nodes to discard, nullopt for all nodes.
128156
*/
129-
void Session::discardItems(const std::optional<std::string>& xpath)
157+
void Session::dropForeignOperationalContent(const std::optional<std::string>& xpath)
130158
{
131159
auto res = sr_discard_items(m_sess.get(), xpath ? xpath->c_str() : nullptr);
132160

133161
throwIfError(res, "Session::discardItems: Can't discard "s + (xpath ? "'"s + *xpath + "'" : "all nodes"s), m_sess.get());
134162
}
135163

164+
/** @short Get a copy of the stored push-operational data for this session
165+
*
166+
* To modify the stored push operational data, modify this tree in-place and pass it to editBatch()
167+
* with the "replace" operation.
168+
*
169+
* Wraps `sr_get_oper_changes`.
170+
*/
171+
std::optional<libyang::DataNode> Session::operationalChanges(const std::optional<std::string>& moduleName) const
172+
{
173+
sr_data_t* data;
174+
auto res = sr_get_oper_changes(m_sess.get(), moduleName ? moduleName->c_str() : nullptr, &data);
175+
176+
throwIfError(res, "Session::operationalChanges: Couldn't retrieve data'"s + (moduleName ? " for \"" + *moduleName + '"' : ""s), m_sess.get());
177+
178+
if (!data) {
179+
return std::nullopt;
180+
}
181+
182+
return wrapSrData(m_sess, data);
183+
184+
}
185+
186+
/** @short Discard push operational changes of a module for this session
187+
*
188+
* Wraps `sr_discard_oper_changes`.
189+
*
190+
* */
191+
void Session::discardOperationalChanges(const std::optional<std::string>& moduleName, std::chrono::milliseconds timeout)
192+
{
193+
auto res = sr_discard_oper_changes(nullptr, m_sess.get(), moduleName ? nullptr : moduleName->c_str(), timeout.count());
194+
throwIfError(res, "Session::discardOoperationalChanges: Couldn't discard "s + (moduleName ? "for module \"" + *moduleName + "\"" : "globally"s), m_sess.get());
195+
}
196+
136197
/**
137198
* Moves item (a list or a leaf-list) specified by `path`.
138199
* @param path Node to move.
@@ -155,25 +216,6 @@ void Session::moveItem(const std::string& path, const MovePosition move, const s
155216
throwIfError(res, "Session::moveItem: Can't move '"s + path + "'", m_sess.get());
156217
}
157218

158-
namespace {
159-
libyang::DataNode wrapSrData(std::shared_ptr<sr_session_ctx_s> sess, sr_data_t* data)
160-
{
161-
// Since the lyd_node came from sysrepo and it is wrapped in a sr_data_t, we have to postpone calling the
162-
// sr_release_data() until after we're "done" with the libyang::DataNode.
163-
//
164-
// Normally, sr_release_data() would free the lyd_data as well. However, it is possible that the user wants to
165-
// manipulate the data tree (think unlink()) in a way which might have needed to overwrite the tree->data pointer.
166-
// Just delegate all the freeing to the C++ wrapper around lyd_data. The sysrepo library doesn't care about this.
167-
auto tree = std::exchange(data->tree, nullptr);
168-
169-
// Use wrapRawNode, not wrapUnmanagedRawNode because we want to let the C++ wrapper manage memory.
170-
// Note: We're capturing the session inside the lambda.
171-
return libyang::wrapRawNode(tree, std::shared_ptr<sr_data_t>(data, [extend_session_lifetime = sess] (sr_data_t* data) {
172-
sr_release_data(data);
173-
}));
174-
}
175-
}
176-
177219
/**
178220
* @brief Returns a tree which contains all nodes that match the provided XPath.
179221
*
@@ -263,13 +305,15 @@ void Session::applyChanges(std::chrono::milliseconds timeout)
263305
}
264306

265307
/**
266-
* Discards changes made in this Session.
308+
* Discards changes made earlier in this Session, optionally only below a given XPath
309+
*
310+
* The changes are applied only after calling Session::applyChanges.
267311
*
268-
* Wraps `sr_discard_changes`.
312+
* Wraps `sr_discard_changes_xpath`.
269313
*/
270-
void Session::discardChanges()
314+
void Session::discardChanges(const std::optional<std::string>& xpath)
271315
{
272-
auto res = sr_discard_changes(m_sess.get());
316+
auto res = sr_discard_changes_xpath(m_sess.get(), xpath ? xpath->c_str() : nullptr);
273317

274318
throwIfError(res, "Session::discardChanges: Couldn't discard changes", m_sess.get());
275319
}

tests/session.cpp

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ TEST_CASE("session")
211211
}
212212
}
213213

214-
DOCTEST_SUBCASE("Session::deleteOperItem")
214+
DOCTEST_SUBCASE("push operational data and deleting stuff")
215215
{
216216
// Set some arbitrary leaf.
217217
sess.setItem(leaf, "123");
@@ -230,15 +230,51 @@ TEST_CASE("session")
230230
sess.switchDatastore(sysrepo::Datastore::Operational);
231231
REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "123");
232232

233-
// After using deleteItem, the leaf is no longer accesible from the operational datastore.
234-
sess.deleteItem(leaf);
235-
sess.applyChanges();
236-
REQUIRE(!sess.getData(leaf));
233+
DOCTEST_SUBCASE("discardOperationalChanges")
234+
{
235+
// apply a change which makes the leaf disappear
236+
sess.dropForeignOperationalContent(leaf);
237+
REQUIRE(!!sess.getData(leaf));
238+
sess.applyChanges();
239+
REQUIRE(!sess.getData(leaf));
237240

238-
// Using discardItems makes the leaf visible again (in the operational datastore).
239-
sess.discardItems(leaf);
240-
sess.applyChanges();
241-
REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "123");
241+
// Using discardOperationalChanges makes the leaf visible again (in the operational datastore).
242+
// Also, no need to applyChanges().
243+
sess.discardOperationalChanges("test_module");
244+
REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "123");
245+
}
246+
247+
DOCTEST_SUBCASE("direct edit of a libyang::DataNode")
248+
{
249+
// at first, set the leaf to some random value
250+
sess.setItem(leaf, "456");
251+
sess.applyChanges();
252+
REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "456");
253+
254+
// change the edit in-place
255+
auto pushed = sess.operationalChanges();
256+
REQUIRE(pushed->path() == leaf);
257+
pushed->asTerm().changeValue("666");
258+
sess.editBatch(*pushed, sysrepo::DefaultOperation::Replace);
259+
sess.applyChanges();
260+
REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "666");
261+
262+
// Remove that previous edit in-place. Since the new edit cannot be empty, set some other leaf.
263+
pushed = sess.operationalChanges();
264+
auto another = "/test_module:popelnice/s"s;
265+
pushed->newPath(another, "xxx");
266+
pushed = *pushed->findPath(another);
267+
pushed->findPath(leaf)->unlink();
268+
// "the edit" for sysrepo must refer to a top-level node
269+
while (pushed->parent()) {
270+
pushed = *pushed->parent();
271+
}
272+
REQUIRE(!pushed->findPath(leaf));
273+
REQUIRE(!!pushed->findPath(another));
274+
sess.editBatch(*pushed, sysrepo::DefaultOperation::Replace);
275+
sess.applyChanges();
276+
REQUIRE(sess.getData(leaf)->asTerm().valueStr() == "123");
277+
}
242278
}
243279

244280
DOCTEST_SUBCASE("edit batch")
@@ -321,6 +357,11 @@ TEST_CASE("session")
321357
sess.discardChanges();
322358
}
323359

360+
DOCTEST_SUBCASE("discard XPath")
361+
{
362+
sess.discardChanges(leaf);
363+
}
364+
324365
REQUIRE(sess.getPendingChanges() == std::nullopt);
325366
}
326367

0 commit comments

Comments
 (0)