DatastoreAccess: add support for generic user-defined RPCs

I was lazy and in the NETCONF backend, the fillMap patch does not check
for actual string prefix match, it just optimistically trims the RPC
prefix. I think this is safe (definitely in the "won't crash" department
due to use of std::string::substr(), but also in the "won't produce
garbage" context because libyang is expected to validate everything).

In the sysrepo backend, I had to introduce some duplication into that
visitor which converts from our data types to sysrepo. It tirns out that
sysrepo::Session::set_item requires a separate S_Val, whereas in context
of handling an RPC's output, we have a Vals_Holder which, after
reallocation, becomes a Vals instance, and that one does not support
replacing the individual Val instances by "something" -- one has to call
a Val::set, and these methods are different from Val::Val constructors.

I'm explicitly testing for lists and containers because the
documentation looked a bit scary -- I understood it in a way which make
me spend extra effort to make sure that all that has to be created gets
created.

Change-Id: I717af71d69b209c444e1c5fe6d8ec2c2fcbdde8b
diff --git a/src/netconf_access.cpp b/src/netconf_access.cpp
index fc35be6..7387231 100644
--- a/src/netconf_access.cpp
+++ b/src/netconf_access.cpp
@@ -18,8 +18,12 @@
 // 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)
+void fillMap(DatastoreAccess::Tree& res, const std::vector<std::shared_ptr<libyang::Data_Node>> items, std::optional<std::string> ignoredXPathPrefix = std::nullopt)
 {
+    auto stripXPathPrefix = [&ignoredXPathPrefix] (auto path) {
+        return ignoredXPathPrefix ? path.substr(ignoredXPathPrefix->size()) : path;
+    };
+
     for (const auto& it : items) {
         if (!it)
             continue;
@@ -28,15 +32,15 @@
                 // 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(it->path(), special_{SpecialValue::PresenceContainer});
+                res.emplace(stripXPathPrefix(it->path()), special_{SpecialValue::PresenceContainer});
             }
         }
         if (it->schema()->nodetype() == LYS_LIST) {
-            res.emplace(it->path(), special_{SpecialValue::List});
+            res.emplace(stripXPathPrefix(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()));
+            res.emplace(stripXPathPrefix(it->path()), leafValueFromValue(leaf.value(), leaf.leaf_type()->base()));
         }
     }
 }
@@ -141,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