Merge changes from topic "rpc"
* changes:
python: user-defined RPCs
DatastoreAccess: add support for generic user-defined RPCs
Include presence containers in NetconfAccess::getItems
joinPaths: support adding the trailing slash
make fillMap reusable for future RPC support
libnetconf C++: add support for user RPCs
NETCONF: support variable-return-type RPCs
a typedef for returning "tree of data"
diff --git a/src/datastore_access.hpp b/src/datastore_access.hpp
index 72e3553..ae0d12a 100644
--- a/src/datastore_access.hpp
+++ b/src/datastore_access.hpp
@@ -38,13 +38,15 @@
class DatastoreAccess {
public:
+ using Tree = std::map<std::string, leaf_data_>;
virtual ~DatastoreAccess() = 0;
- virtual std::map<std::string, leaf_data_> getItems(const std::string& path) = 0;
+ virtual Tree getItems(const std::string& path) = 0;
virtual void setLeaf(const std::string& path, leaf_data_ value) = 0;
virtual void createPresenceContainer(const std::string& path) = 0;
virtual void deletePresenceContainer(const std::string& path) = 0;
virtual void createListInstance(const std::string& path) = 0;
virtual void deleteListInstance(const std::string& path) = 0;
+ virtual Tree executeRpc(const std::string& path, const Tree& input) = 0;
virtual std::shared_ptr<Schema> schema() = 0;
diff --git a/src/netconf-client.cpp b/src/netconf-client.cpp
index f1cdfe2..8f0e2ec 100644
--- a/src/netconf-client.cpp
+++ b/src/netconf-client.cpp
@@ -162,7 +162,7 @@
return client::ReportedError{ss.str()};
}
-unique_ptr_for<struct nc_reply_data> do_rpc_data(client::Session* session, unique_ptr_for<struct nc_rpc>&& rpc)
+std::optional<unique_ptr_for<struct nc_reply_data>> do_rpc_data_or_ok(client::Session* session, unique_ptr_for<struct nc_rpc>&& rpc)
{
auto x = do_rpc(session, std::move(rpc));
@@ -170,27 +170,29 @@
case NC_RPL_DATA:
return guarded(reinterpret_cast<struct nc_reply_data*>(x.release()));
case NC_RPL_OK:
- throw std::runtime_error{"Received OK instead of a data reply"};
+ return std::nullopt;
case NC_RPL_ERROR:
throw make_error(std::move(x));
default:
throw std::runtime_error{"Unhandled reply type"};
}
+
+}
+
+unique_ptr_for<struct nc_reply_data> do_rpc_data(client::Session* session, unique_ptr_for<struct nc_rpc>&& rpc)
+{
+ auto x = do_rpc_data_or_ok(session, std::move(rpc));
+ if (!x) {
+ throw std::runtime_error{"Received OK instead of a data reply"};
+ }
+ return std::move(*x);
}
void do_rpc_ok(client::Session* session, unique_ptr_for<struct nc_rpc>&& rpc)
{
- auto x = do_rpc(session, std::move(rpc));
-
- switch (x->type) {
- case NC_RPL_DATA:
+ auto x = do_rpc_data_or_ok(session, std::move(rpc));
+ if (x) {
throw std::runtime_error{"Unexpected DATA reply"};
- case NC_RPL_OK:
- return;
- case NC_RPL_ERROR:
- throw make_error(std::move(x));
- default:
- throw std::runtime_error{"Unhandled reply type"};
}
}
}
@@ -363,6 +365,21 @@
impl::do_rpc_ok(this, std::move(rpc));
}
+std::shared_ptr<libyang::Data_Node> Session::rpc(const std::string& xmlData)
+{
+ auto rpc = impl::guarded(nc_rpc_act_generic_xml(xmlData.c_str(), NC_PARAMTYPE_CONST));
+ if (!rpc) {
+ throw std::runtime_error("Cannot create generic RPC");
+ }
+ auto reply = impl::do_rpc_data_or_ok(this, std::move(rpc));
+ if (reply) {
+ auto dataNode = libyang::create_new_Data_Node((*reply)->data);
+ return dataNode->dup_withsiblings(1);
+ } else {
+ return nullptr;
+ }
+}
+
ReportedError::ReportedError(const std::string& what)
: std::runtime_error(what)
{
diff --git a/src/netconf-client.h b/src/netconf-client.h
index 1923e71..b9a1925 100644
--- a/src/netconf-client.h
+++ b/src/netconf-client.h
@@ -43,6 +43,7 @@
const NC_RPC_EDIT_ERROPT errorOption,
const std::string& data);
void copyConfigFromString(const NC_DATASTORE target, const std::string& data);
+ std::shared_ptr<libyang::Data_Node> rpc(const std::string& xmlData);
void commit();
void discard();
struct nc_session* session_internal(); // FIXME: remove me
diff --git a/src/netconf_access.cpp b/src/netconf_access.cpp
index 5b1ad78..7387231 100644
--- a/src/netconf_access.cpp
+++ b/src/netconf_access.cpp
@@ -13,35 +13,50 @@
#include "utils.hpp"
#include "yang_schema.hpp"
-NetconfAccess::~NetconfAccess() = default;
+namespace {
-std::map<std::string, leaf_data_> NetconfAccess::getItems(const std::string& path)
+// This is very similar to the fillMap lambda in SysrepoAccess, however,
+// Sysrepo returns a weird array-like structure, while libnetconf
+// returns libyang::Data_Node
+void fillMap(DatastoreAccess::Tree& res, const std::vector<std::shared_ptr<libyang::Data_Node>> items, std::optional<std::string> ignoredXPathPrefix = std::nullopt)
{
- using namespace std::string_literals;
- std::map<std::string, leaf_data_> res;
-
- // This is very similar to the fillMap lambda in SysrepoAccess, however,
- // Sysrepo returns a weird array-like structure, while libnetconf
- // returns libyang::Data_Node
- auto fillMap = [&res](auto items) {
- for (const auto& it : items) {
- if (!it)
- continue;
- if (it->schema()->nodetype() == LYS_LIST) {
- res.emplace(it->path(), special_{SpecialValue::List});
- }
- if (it->schema()->nodetype() == LYS_LEAF) {
- libyang::Data_Node_Leaf_List leaf(it);
- res.emplace(leaf.path(), leafValueFromValue(leaf.value(), leaf.leaf_type()->base()));
- }
- }
+ auto stripXPathPrefix = [&ignoredXPathPrefix] (auto path) {
+ return ignoredXPathPrefix ? path.substr(ignoredXPathPrefix->size()) : path;
};
+ for (const auto& it : items) {
+ if (!it)
+ continue;
+ if (it->schema()->nodetype() == LYS_CONTAINER) {
+ if (libyang::Schema_Node_Container{it->schema()}.presence()) {
+ // The fact that the container is included in the data tree
+ // means that it is present and I don't need to check any
+ // value.
+ res.emplace(stripXPathPrefix(it->path()), special_{SpecialValue::PresenceContainer});
+ }
+ }
+ if (it->schema()->nodetype() == LYS_LIST) {
+ res.emplace(stripXPathPrefix(it->path()), special_{SpecialValue::List});
+ }
+ if (it->schema()->nodetype() == LYS_LEAF) {
+ libyang::Data_Node_Leaf_List leaf(it);
+ res.emplace(stripXPathPrefix(it->path()), leafValueFromValue(leaf.value(), leaf.leaf_type()->base()));
+ }
+ }
+}
+}
+
+
+NetconfAccess::~NetconfAccess() = default;
+
+DatastoreAccess::Tree NetconfAccess::getItems(const std::string& path)
+{
+ Tree res;
auto config = m_session->getConfig(NC_DATASTORE_RUNNING, (path != "/") ? std::optional{path} : std::nullopt);
if (config) {
for (auto it : config->tree_for()) {
- fillMap(it->tree_dfs());
+ fillMap(res, it->tree_dfs());
}
}
return res;
@@ -130,6 +145,25 @@
m_session->discard();
}
+DatastoreAccess::Tree NetconfAccess::executeRpc(const std::string& path, const Tree& input)
+{
+ auto root = m_schema->dataNodeFromPath(path);
+ for (const auto& [k, v] : input) {
+ auto node = m_schema->dataNodeFromPath(joinPaths(path, k), leafDataToString(v));
+ root->merge(node, 0);
+ }
+ auto data = root->print_mem(LYD_XML, 0);
+
+ Tree res;
+ auto output = m_session->rpc(data);
+ if (output) {
+ for (auto it : output->tree_for()) {
+ fillMap(res, it->tree_dfs(), joinPaths(path, "/"));
+ }
+ }
+ return res;
+}
+
std::string NetconfAccess::fetchSchema(const std::string_view module, const
std::optional<std::string_view> revision, const
std::optional<std::string_view> submodule, const
diff --git a/src/netconf_access.hpp b/src/netconf_access.hpp
index 29101ed..f43976a 100644
--- a/src/netconf_access.hpp
+++ b/src/netconf_access.hpp
@@ -33,7 +33,7 @@
NetconfAccess(const std::string& socketPath);
NetconfAccess(std::unique_ptr<libnetconf::client::Session>&& session);
~NetconfAccess() override;
- std::map<std::string, leaf_data_> getItems(const std::string& path) override;
+ Tree getItems(const std::string& path) override;
void setLeaf(const std::string& path, leaf_data_ value) override;
void createPresenceContainer(const std::string& path) override;
void deletePresenceContainer(const std::string& path) override;
@@ -41,6 +41,7 @@
void deleteListInstance(const std::string& path) override;
void commitChanges() override;
void discardChanges() override;
+ Tree executeRpc(const std::string& path, const Tree& input) override;
std::shared_ptr<Schema> schema() override;
diff --git a/src/python_netconf.cpp b/src/python_netconf.cpp
index eaa4349..5806afa 100644
--- a/src/python_netconf.cpp
+++ b/src/python_netconf.cpp
@@ -69,5 +69,6 @@
.def("getItems", &NetconfAccess::getItems, "xpath"_a)
.def("setLeaf", &NetconfAccess::setLeaf, "xpath"_a, "value"_a)
.def("commitChanges", &NetconfAccess::commitChanges)
+ .def("executeRpc", &NetconfAccess::executeRpc, "rpc"_a, "input"_a=DatastoreAccess::Tree{})
;
}
diff --git a/src/sysrepo_access.cpp b/src/sysrepo_access.cpp
index 221f482..cf19407 100644
--- a/src/sysrepo_access.cpp
+++ b/src/sysrepo_access.cpp
@@ -8,6 +8,7 @@
#include <sysrepo-cpp/Session.hpp>
#include "sysrepo_access.hpp"
+#include "utils.hpp"
#include "yang_schema.hpp"
leaf_data_ leafValueFromVal(const sysrepo::S_Val& value)
@@ -83,6 +84,47 @@
}
};
+struct updateSrValFromValue : boost::static_visitor<void> {
+ std::string xpath;
+ sysrepo::S_Val v;
+ updateSrValFromValue(const std::string& xpath, sysrepo::S_Val v)
+ : xpath(xpath)
+ , v(v)
+ {
+ }
+
+ void operator()(const enum_& value) const
+ {
+ v->set(xpath.c_str(), value.m_value.c_str(), SR_ENUM_T);
+ }
+
+ void operator()(const binary_& value) const
+ {
+ v->set(xpath.c_str(), value.m_value.c_str(), SR_BINARY_T);
+ }
+
+ void operator()(const identityRef_& value) const
+ {
+ v->set(xpath.c_str(), (value.m_prefix.value().m_name + ":" + value.m_value).c_str(), SR_IDENTITYREF_T);
+ }
+
+ void operator()(const special_& value) const
+ {
+ throw std::runtime_error("Tried constructing S_Val from a " + specialValueToString(value));
+ }
+
+ void operator()(const std::string& value) const
+ {
+ v->set(xpath.c_str(), value.c_str(), SR_STRING_T);
+ }
+
+ template <typename T>
+ void operator()(const T value) const
+ {
+ v->set(xpath.c_str(), value);
+ }
+};
+
SysrepoAccess::~SysrepoAccess() = default;
SysrepoAccess::SysrepoAccess(const std::string& appname)
@@ -111,10 +153,10 @@
}
}
-std::map<std::string, leaf_data_> SysrepoAccess::getItems(const std::string& path)
+DatastoreAccess::Tree SysrepoAccess::getItems(const std::string& path)
{
using namespace std::string_literals;
- std::map<std::string, leaf_data_> res;
+ Tree res;
auto fillMap = [&res](auto items) {
if (!items)
@@ -203,6 +245,25 @@
}
}
+DatastoreAccess::Tree SysrepoAccess::executeRpc(const std::string &path, const Tree &input)
+{
+ auto srInput = std::make_shared<sysrepo::Vals>(input.size());
+ {
+ size_t i = 0;
+ for (const auto& [k, v] : input) {
+ boost::apply_visitor(updateSrValFromValue(joinPaths(path, k), srInput->val(i)), v);
+ ++i;
+ }
+ }
+ auto output = m_session->rpc_send(path.c_str(), srInput);
+ Tree res;
+ for (size_t i = 0; i < output->val_cnt(); ++i) {
+ const auto& v = output->val(i);
+ res.emplace(std::string(v->xpath()).substr(joinPaths(path, "/").size()), leafValueFromVal(v));
+ }
+ return res;
+}
+
std::string SysrepoAccess::fetchSchema(const char* module, const char* revision, const char* submodule)
{
std::string schema;
diff --git a/src/sysrepo_access.hpp b/src/sysrepo_access.hpp
index 9861ab4..c2ce909 100644
--- a/src/sysrepo_access.hpp
+++ b/src/sysrepo_access.hpp
@@ -28,12 +28,13 @@
public:
~SysrepoAccess() override;
SysrepoAccess(const std::string& appname);
- std::map<std::string, leaf_data_> getItems(const std::string& path) override;
+ Tree getItems(const std::string& path) override;
void setLeaf(const std::string& path, leaf_data_ value) override;
void createPresenceContainer(const std::string& path) override;
void deletePresenceContainer(const std::string& path) override;
void createListInstance(const std::string& path) override;
void deleteListInstance(const std::string& path) override;
+ Tree executeRpc(const std::string& path, const Tree& input) override;
std::shared_ptr<Schema> schema() override;
diff --git a/src/utils.cpp b/src/utils.cpp
index d0e8639..978e0a9 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -11,10 +11,26 @@
std::string joinPaths(const std::string& prefix, const std::string& suffix)
{
- if (prefix.empty() || suffix.empty() || prefix == "/")
- return prefix + suffix;
- else
- return prefix + '/' + suffix;
+ // These two if statements are essential for the algorithm:
+ // The first one solves joining nothing and a relative path - the algorithm
+ // down below adds a leading slash, turning it into an absolute path.
+ // The second one would always add a trailing slash to the path.
+ if (prefix.empty()) {
+ return suffix;
+ }
+
+ if (suffix.empty()) {
+ return prefix;
+ }
+
+ // Otherwise, strip slashes where the join is going to happen. This will
+ // also change "/" to "", but the return statement takes care of that and
+ // inserts the slash again.
+ auto prefixWithoutTrailingSlash = !prefix.empty() && prefix.back() == '/' ? prefix.substr(0, prefix.length() - 1) : prefix;
+ auto suffixWithoutLeadingSlash = !suffix.empty() && suffix.front() == '/' ? suffix.substr(1) : suffix;
+
+ // And join the result with a slash.
+ return prefixWithoutTrailingSlash + '/' + suffixWithoutLeadingSlash;
}
std::string stripLastNodeFromPath(const std::string& path)
diff --git a/tests/datastore_access.cpp b/tests/datastore_access.cpp
index 535abdc..112ace9 100644
--- a/tests/datastore_access.cpp
+++ b/tests/datastore_access.cpp
@@ -7,6 +7,7 @@
*/
#include "trompeloeil_doctest.h"
+#include <sysrepo-cpp/Session.hpp>
#ifdef sysrepo_BACKEND
#include "sysrepo_access.hpp"
@@ -19,13 +20,21 @@
#include "sysrepo_subscription.hpp"
#include "utils.hpp"
-class MockRecorder : public Recorder {
+using namespace std::literals::string_literals;
+
+class MockRecorder : public trompeloeil::mock_interface<Recorder> {
public:
- MAKE_MOCK3(write, void(const std::string&, const std::string&, const std::string&), override);
+ IMPLEMENT_MOCK3(write);
};
namespace std {
-std::ostream& operator<<(std::ostream& s, const std::map<std::string, leaf_data_> map)
+std::ostream& operator<<(std::ostream& s, const std::optional<std::string>& opt)
+{
+ s << (opt ? *opt : "std::nullopt");
+ return s;
+}
+
+std::ostream& operator<<(std::ostream& s, const DatastoreAccess::Tree& map)
{
s << std::endl
<< "{";
@@ -51,87 +60,88 @@
#error "Unknown backend"
#endif
+
SECTION("set leafInt8 to -128")
{
- REQUIRE_CALL(mock, write("/example-schema:leafInt8", "", "-128"));
+ REQUIRE_CALL(mock, write("/example-schema:leafInt8", std::nullopt, "-128"s));
datastore.setLeaf("/example-schema:leafInt8", int8_t{-128});
datastore.commitChanges();
}
SECTION("set leafInt16 to -32768")
{
- REQUIRE_CALL(mock, write("/example-schema:leafInt16", "", "-32768"));
+ REQUIRE_CALL(mock, write("/example-schema:leafInt16", std::nullopt, "-32768"s));
datastore.setLeaf("/example-schema:leafInt16", int16_t{-32768});
datastore.commitChanges();
}
SECTION("set leafInt32 to -2147483648")
{
- REQUIRE_CALL(mock, write("/example-schema:leafInt32", "", "-2147483648"));
+ REQUIRE_CALL(mock, write("/example-schema:leafInt32", std::nullopt, "-2147483648"s));
datastore.setLeaf("/example-schema:leafInt32", int32_t{-2147483648});
datastore.commitChanges();
}
SECTION("set leafInt64 to -50000000000")
{
- REQUIRE_CALL(mock, write("/example-schema:leafInt64", "", "-50000000000"));
+ REQUIRE_CALL(mock, write("/example-schema:leafInt64", std::nullopt, "-50000000000"s));
datastore.setLeaf("/example-schema:leafInt64", int64_t{-50000000000});
datastore.commitChanges();
}
SECTION("set leafUInt8 to 255")
{
- REQUIRE_CALL(mock, write("/example-schema:leafUInt8", "", "255"));
+ REQUIRE_CALL(mock, write("/example-schema:leafUInt8", std::nullopt, "255"s));
datastore.setLeaf("/example-schema:leafUInt8", uint8_t{255});
datastore.commitChanges();
}
SECTION("set leafUInt16 to 65535")
{
- REQUIRE_CALL(mock, write("/example-schema:leafUInt16", "", "65535"));
+ REQUIRE_CALL(mock, write("/example-schema:leafUInt16", std::nullopt, "65535"s));
datastore.setLeaf("/example-schema:leafUInt16", uint16_t{65535});
datastore.commitChanges();
}
SECTION("set leafUInt32 to 4294967295")
{
- REQUIRE_CALL(mock, write("/example-schema:leafUInt32", "", "4294967295"));
+ REQUIRE_CALL(mock, write("/example-schema:leafUInt32", std::nullopt, "4294967295"s));
datastore.setLeaf("/example-schema:leafUInt32", uint32_t{4294967295});
datastore.commitChanges();
}
SECTION("set leafUInt64 to 50000000000")
{
- REQUIRE_CALL(mock, write("/example-schema:leafUInt64", "", "50000000000"));
+ REQUIRE_CALL(mock, write("/example-schema:leafUInt64", std::nullopt, "50000000000"s));
datastore.setLeaf("/example-schema:leafUInt64", uint64_t{50000000000});
datastore.commitChanges();
}
SECTION("set leafEnum to coze")
{
- REQUIRE_CALL(mock, write("/example-schema:leafEnum", "", "coze"));
+ REQUIRE_CALL(mock, write("/example-schema:leafEnum", std::nullopt, "coze"s));
datastore.setLeaf("/example-schema:leafEnum", enum_{"coze"});
datastore.commitChanges();
}
SECTION("set leafDecimal to 123.544")
{
- REQUIRE_CALL(mock, write("/example-schema:leafDecimal", "", "123.544"));
+ REQUIRE_CALL(mock, write("/example-schema:leafDecimal", std::nullopt, "123.544"s));
datastore.setLeaf("/example-schema:leafDecimal", 123.544);
datastore.commitChanges();
}
SECTION("create presence container")
{
- REQUIRE_CALL(mock, write("/example-schema:pContainer", "", ""));
+ REQUIRE_CALL(mock, write("/example-schema:pContainer", std::nullopt, ""s));
datastore.createPresenceContainer("/example-schema:pContainer");
datastore.commitChanges();
}
SECTION("create a list instance")
{
- REQUIRE_CALL(mock, write("/example-schema:person[name='Nguyen']", "", ""));
- REQUIRE_CALL(mock, write("/example-schema:person[name='Nguyen']/name", "", "Nguyen"));
+ REQUIRE_CALL(mock, write("/example-schema:person[name='Nguyen']", std::nullopt, ""s));
+ REQUIRE_CALL(mock, write("/example-schema:person[name='Nguyen']/name", std::nullopt, "Nguyen"s));
datastore.createListInstance("/example-schema:person[name='Nguyen']");
datastore.commitChanges();
}
@@ -139,12 +149,12 @@
SECTION("leafref pointing to a key of a list")
{
{
- REQUIRE_CALL(mock, write("/example-schema:person[name='Dan']", "", ""));
- REQUIRE_CALL(mock, write("/example-schema:person[name='Dan']/name", "", "Dan"));
- REQUIRE_CALL(mock, write("/example-schema:person[name='Elfi']", "", ""));
- REQUIRE_CALL(mock, write("/example-schema:person[name='Elfi']/name", "", "Elfi"));
- REQUIRE_CALL(mock, write("/example-schema:person[name='Kolafa']", "", ""));
- REQUIRE_CALL(mock, write("/example-schema:person[name='Kolafa']/name", "", "Kolafa"));
+ REQUIRE_CALL(mock, write("/example-schema:person[name='Dan']", std::nullopt, ""s));
+ REQUIRE_CALL(mock, write("/example-schema:person[name='Dan']/name", std::nullopt, "Dan"s));
+ REQUIRE_CALL(mock, write("/example-schema:person[name='Elfi']", std::nullopt, ""s));
+ REQUIRE_CALL(mock, write("/example-schema:person[name='Elfi']/name", std::nullopt, "Elfi"s));
+ REQUIRE_CALL(mock, write("/example-schema:person[name='Kolafa']", std::nullopt, ""s));
+ REQUIRE_CALL(mock, write("/example-schema:person[name='Kolafa']/name", std::nullopt, "Kolafa"s));
datastore.createListInstance("/example-schema:person[name='Dan']");
datastore.createListInstance("/example-schema:person[name='Elfi']");
datastore.createListInstance("/example-schema:person[name='Kolafa']");
@@ -156,21 +166,21 @@
// SECTION.
SECTION("Dan")
{
- REQUIRE_CALL(mock, write("/example-schema:bossPerson", "", "Dan"));
+ REQUIRE_CALL(mock, write("/example-schema:bossPerson", std::nullopt, "Dan"s));
datastore.setLeaf("/example-schema:bossPerson", std::string{"Dan"});
datastore.commitChanges();
}
SECTION("Elfi")
{
- REQUIRE_CALL(mock, write("/example-schema:bossPerson", "", "Elfi"));
+ REQUIRE_CALL(mock, write("/example-schema:bossPerson", std::nullopt, "Elfi"s));
datastore.setLeaf("/example-schema:bossPerson", std::string{"Elfi"});
datastore.commitChanges();
}
SECTION("Kolafa")
{
- REQUIRE_CALL(mock, write("/example-schema:bossPerson", "", "Kolafa"));
+ REQUIRE_CALL(mock, write("/example-schema:bossPerson", std::nullopt, "Kolafa"s));
datastore.setLeaf("/example-schema:bossPerson", std::string{"Kolafa"});
datastore.commitChanges();
}
@@ -178,26 +188,26 @@
SECTION("bool values get correctly represented as bools")
{
{
- REQUIRE_CALL(mock, write("/example-schema:down", "", "true"));
+ REQUIRE_CALL(mock, write("/example-schema:down", std::nullopt, "true"s));
datastore.setLeaf("/example-schema:down", bool{true});
datastore.commitChanges();
}
- std::map<std::string, leaf_data_> expected{{"/example-schema:down", bool{true}}};
+ DatastoreAccess::Tree expected{{"/example-schema:down", bool{true}}};
REQUIRE(datastore.getItems("/example-schema:down") == expected);
}
SECTION("getting items from the whole module")
{
{
- REQUIRE_CALL(mock, write("/example-schema:up", "", "true"));
- REQUIRE_CALL(mock, write("/example-schema:down", "", "false"));
+ REQUIRE_CALL(mock, write("/example-schema:up", std::nullopt, "true"s));
+ REQUIRE_CALL(mock, write("/example-schema:down", std::nullopt, "false"s));
datastore.setLeaf("/example-schema:up", bool{true});
datastore.setLeaf("/example-schema:down", bool{false});
datastore.commitChanges();
}
- std::map<std::string, leaf_data_> expected{{"/example-schema:down", bool{false}},
+ DatastoreAccess::Tree expected{{"/example-schema:down", bool{false}},
// Sysrepo always returns containers when getting values, but
// libnetconf does not. This is fine by the YANG standard:
// https://tools.ietf.org/html/rfc7950#section-7.5.7 Furthermore,
@@ -216,11 +226,11 @@
SECTION("getItems returns correct datatypes")
{
{
- REQUIRE_CALL(mock, write("/example-schema:leafEnum", "", "lol"));
+ REQUIRE_CALL(mock, write("/example-schema:leafEnum", std::nullopt, "lol"s));
datastore.setLeaf("/example-schema:leafEnum", enum_{"lol"});
datastore.commitChanges();
}
- std::map<std::string, leaf_data_> expected{{"/example-schema:leafEnum", enum_{"lol"}}};
+ DatastoreAccess::Tree expected{{"/example-schema:leafEnum", enum_{"lol"}}};
REQUIRE(datastore.getItems("/example-schema:leafEnum") == expected);
}
@@ -228,18 +238,18 @@
SECTION("getItems on a list")
{
{
- REQUIRE_CALL(mock, write("/example-schema:person[name='Jan']", "", ""));
- REQUIRE_CALL(mock, write("/example-schema:person[name='Jan']/name", "", "Jan"));
- REQUIRE_CALL(mock, write("/example-schema:person[name='Michal']", "", ""));
- REQUIRE_CALL(mock, write("/example-schema:person[name='Michal']/name", "", "Michal"));
- REQUIRE_CALL(mock, write("/example-schema:person[name='Petr']", "", ""));
- REQUIRE_CALL(mock, write("/example-schema:person[name='Petr']/name", "", "Petr"));
+ REQUIRE_CALL(mock, write("/example-schema:person[name='Jan']", std::nullopt, ""s));
+ REQUIRE_CALL(mock, write("/example-schema:person[name='Jan']/name", std::nullopt, "Jan"s));
+ REQUIRE_CALL(mock, write("/example-schema:person[name='Michal']", std::nullopt, ""s));
+ REQUIRE_CALL(mock, write("/example-schema:person[name='Michal']/name", std::nullopt, "Michal"s));
+ REQUIRE_CALL(mock, write("/example-schema:person[name='Petr']", std::nullopt, ""s));
+ REQUIRE_CALL(mock, write("/example-schema:person[name='Petr']/name", std::nullopt, "Petr"s));
datastore.createListInstance("/example-schema:person[name='Jan']");
datastore.createListInstance("/example-schema:person[name='Michal']");
datastore.createListInstance("/example-schema:person[name='Petr']");
datastore.commitChanges();
}
- std::map<std::string, leaf_data_> expected{
+ DatastoreAccess::Tree expected{
{"/example-schema:person[name='Jan']", special_{SpecialValue::List}},
{"/example-schema:person[name='Jan']/name", std::string{"Jan"}},
{"/example-schema:person[name='Michal']", special_{SpecialValue::List}},
@@ -251,5 +261,142 @@
REQUIRE(datastore.getItems("/example-schema:person") == expected);
}
+ SECTION("presence containers")
+ {
+ DatastoreAccess::Tree expected;
+ // Make sure it's not there before we create it
+ REQUIRE(datastore.getItems("/example-schema:pContainer") == expected);
+
+ {
+ REQUIRE_CALL(mock, write("/example-schema:pContainer", std::nullopt, ""s));
+ datastore.createPresenceContainer("/example-schema:pContainer");
+ datastore.commitChanges();
+ }
+ expected = {
+ {"/example-schema:pContainer", special_{SpecialValue::PresenceContainer}}
+ };
+ REQUIRE(datastore.getItems("/example-schema:pContainer") == expected);
+
+ // Make sure it's not there after we delete it
+ {
+ REQUIRE_CALL(mock, write("/example-schema:pContainer", ""s, std::nullopt));
+ datastore.deletePresenceContainer("/example-schema:pContainer");
+ datastore.commitChanges();
+ }
+ expected = {};
+ REQUIRE(datastore.getItems("/example-schema:pContainer") == expected);
+
+ }
+
+ waitForCompletionAndBitMore(seq1);
+}
+
+class RpcCb: public sysrepo::Callback {
+ int rpc(const char *xpath, const ::sysrepo::S_Vals input, ::sysrepo::S_Vals_Holder output, void *) override
+ {
+ const auto nukes = "/example-schema:launch-nukes"s;
+ if (xpath == "/example-schema:noop"s) {
+ return SR_ERR_OK;
+ } else if (xpath == nukes) {
+ uint64_t kilotons = 0;
+ bool hasCities = false;
+ for (size_t i = 0; i < input->val_cnt(); ++i) {
+ const auto& val = input->val(i);
+ if (val->xpath() == nukes + "/payload/kilotons") {
+ kilotons = val->data()->get_uint64();
+ } else if (val->xpath() == nukes + "/payload") {
+ // ignore, container
+ } else if (val->xpath() == nukes + "/description") {
+ // unused
+ } else if (std::string_view{val->xpath()}.find(nukes + "/cities") == 0) {
+ hasCities = true;
+ } else {
+ throw std::runtime_error("RPC launch-nukes: unexpected input "s + val->xpath());
+ }
+ }
+ if (kilotons == 333'666) {
+ // magic, just do not generate any output. This is important because the NETCONF RPC returns just <ok/>.
+ return SR_ERR_OK;
+ }
+ auto buf = output->allocate(2);
+ size_t i = 0;
+ buf->val(i++)->set((nukes + "/blast-radius").c_str(), uint32_t{33'666});
+ buf->val(i++)->set((nukes + "/actual-yield").c_str(), static_cast<uint64_t>(1.33 * kilotons));
+ if (hasCities) {
+ buf = output->reallocate(output->val_cnt() + 2);
+ buf->val(i++)->set((nukes + "/damaged-places/targets[city='London']/city").c_str(), "London");
+ buf->val(i++)->set((nukes + "/damaged-places/targets[city='Berlin']/city").c_str(), "Berlin");
+ }
+ return SR_ERR_OK;
+ }
+ throw std::runtime_error("unrecognized RPC");
+ }
+};
+
+TEST_CASE("rpc") {
+ trompeloeil::sequence seq1;
+ auto srConn = std::make_shared<sysrepo::Connection>("netconf-cli-test-rpc");
+ auto srSession = std::make_shared<sysrepo::Session>(srConn);
+ auto srSubscription = std::make_shared<sysrepo::Subscribe>(srSession);
+ auto cb = std::make_shared<RpcCb>();
+ sysrepo::Logs{}.set_stderr(SR_LL_INF);
+ srSubscription->rpc_subscribe("/example-schema:noop", cb, nullptr, SR_SUBSCR_CTX_REUSE);
+ srSubscription->rpc_subscribe("/example-schema:launch-nukes", cb, nullptr, SR_SUBSCR_CTX_REUSE);
+
+#ifdef sysrepo_BACKEND
+ SysrepoAccess datastore("netconf-cli-test");
+#elif defined(netconf_BACKEND)
+ NetconfAccess datastore(NETOPEER_SOCKET_PATH);
+#else
+#error "Unknown backend"
+#endif
+
+ std::string rpc;
+ DatastoreAccess::Tree input, output;
+
+ SECTION("noop") {
+ rpc = "/example-schema:noop";
+ }
+
+ SECTION("small nuke") {
+ rpc = "/example-schema:launch-nukes";
+ input = {
+ {"description", "dummy"s},
+ {"payload/kilotons", uint64_t{333'666}},
+ };
+ // no data are returned
+ }
+
+ SECTION("small nuke") {
+ rpc = "/example-schema:launch-nukes";
+ input = {
+ {"description", "dummy"s},
+ {"payload/kilotons", uint64_t{4}},
+ };
+ output = {
+ {"blast-radius", uint32_t{33'666}},
+ {"actual-yield", uint64_t{5}},
+ };
+ }
+
+ SECTION("with lists") {
+ rpc = "/example-schema:launch-nukes";
+ input = {
+ {"payload/kilotons", uint64_t{6}},
+ {"cities/targets[city='Prague']/city", "Prague"s},
+ };
+ output = {
+ {"blast-radius", uint32_t{33'666}},
+ {"actual-yield", uint64_t{7}},
+ {"damaged-places", special_{SpecialValue::PresenceContainer}},
+ {"damaged-places/targets[city='London']", special_{SpecialValue::List}},
+ {"damaged-places/targets[city='London']/city", "London"s},
+ {"damaged-places/targets[city='Berlin']", special_{SpecialValue::List}},
+ {"damaged-places/targets[city='Berlin']/city", "Berlin"s},
+ };
+ }
+
+ REQUIRE(datastore.executeRpc(rpc, input) == output);
+
waitForCompletionAndBitMore(seq1);
}
diff --git a/tests/example-schema.yang b/tests/example-schema.yang
index 498446e..d993781 100644
--- a/tests/example-schema.yang
+++ b/tests/example-schema.yang
@@ -83,4 +83,48 @@
container lol {
uses upAndDown;
}
+
+ grouping targets_def {
+ list targets {
+ key 'city';
+ leaf city {
+ type string;
+ }
+ }
+ }
+
+ rpc launch-nukes {
+ input {
+ container payload {
+ leaf kilotons {
+ type uint64;
+ mandatory true;
+ units "kilotons";
+ }
+ }
+ leaf description {
+ type string;
+ }
+ container cities {
+ presence true;
+ uses targets_def;
+ }
+ }
+ output {
+ leaf blast-radius {
+ type uint32;
+ units "m";
+ }
+ leaf actual-yield {
+ type uint64;
+ units "kilotons";
+ }
+ container damaged-places {
+ presence true;
+ uses targets_def;
+ }
+ }
+ }
+
+ rpc noop {}
}
diff --git a/tests/mock/sysrepo_subscription.cpp b/tests/mock/sysrepo_subscription.cpp
index 801ec1c..7cd5cf3 100644
--- a/tests/mock/sysrepo_subscription.cpp
+++ b/tests/mock/sysrepo_subscription.cpp
@@ -28,9 +28,11 @@
return SR_ERR_OK;
while (auto change = sess->get_change_next(it)) {
- m_recorder->write(change->new_val()->xpath(),
- change->old_val() ? change->old_val()->val_to_string() : "",
- change->new_val()->val_to_string());
+ auto xpath = (change->new_val() ? change->new_val() : change->old_val())->xpath();
+
+ auto oldValue = change->old_val() ? std::optional{change->old_val()->val_to_string()} : std::nullopt;
+ auto newValue = change->new_val() ? std::optional{change->new_val()->val_to_string()} : std::nullopt;
+ m_recorder->write(xpath, oldValue, newValue);
}
return SR_ERR_OK;
diff --git a/tests/mock/sysrepo_subscription.hpp b/tests/mock/sysrepo_subscription.hpp
index 36681a6..dce6506 100644
--- a/tests/mock/sysrepo_subscription.hpp
+++ b/tests/mock/sysrepo_subscription.hpp
@@ -8,6 +8,7 @@
#pragma once
+#include <optional>
#include <memory>
namespace sysrepo {
@@ -21,7 +22,7 @@
class Recorder {
public:
virtual ~Recorder();
- virtual void write(const std::string& xpath, const std::string& oldValue, const std::string& newValue) = 0;
+ virtual void write(const std::string& xpath, const std::optional<std::string>& oldValue, const std::optional<std::string>& newValue) = 0;
};
class SysrepoSubscription {
diff --git a/tests/utils.cpp b/tests/utils.cpp
index a6a982b..7717480 100644
--- a/tests/utils.cpp
+++ b/tests/utils.cpp
@@ -22,4 +22,61 @@
REQUIRE((filterByPrefix(set, "polivkax") == std::set<std::string>{}));
REQUIRE((filterByPrefix(set, "co") == std::set<std::string>{"copak", "coze"}));
}
+
+ SECTION("joinPaths") {
+ std::string prefix, suffix, result;
+
+ SECTION("regular") {
+ prefix = "/example:a";
+ suffix = "leaf";
+ result = "/example:a/leaf";
+ }
+
+ SECTION("no prefix - absolute path") {
+ suffix = "/example:a/leaf";
+ result = "/example:a/leaf";
+ }
+
+ SECTION("no prefix - relative path") {
+ suffix = "example:a/leaf";
+ result = "example:a/leaf";
+ }
+
+ SECTION("no suffix") {
+ prefix = "/example:a/leaf";
+ result = "/example:a/leaf";
+ }
+
+ SECTION("at root") {
+ prefix = "/";
+ suffix = "example:a";
+ result = "/example:a";
+ }
+
+ SECTION("trailing slash") {
+ prefix = "/example:a";
+ suffix = "/";
+ result = "/example:a/";
+ }
+
+ SECTION("prefix ends with slash") {
+ prefix = "/example:a/";
+ suffix = "leaf";
+ result = "/example:a/leaf";
+ }
+
+ SECTION("suffix starts with slash") {
+ prefix = "/example:a";
+ suffix = "/leaf";
+ result = "/example:a/leaf";
+ }
+
+ SECTION("slashes all the way to eleven") {
+ prefix = "/example:a/";
+ suffix = "/leaf";
+ result = "/example:a/leaf";
+ }
+
+ REQUIRE(joinPaths(prefix, suffix) == result);
+ }
}