Add support for executing RPCs
Creating a temporary YangAccess for RPC input means I need to somehow
give the right libyang schemas. For that reason I supply a callable
which is able to fetch the schema and create a YangAccess instance for
ProxyDatastore.
The ProxyDatastore class now has a simple mechanism for deciding whether
to use the normal datastore and the temporary based on a path prefix.
Change-Id: Ib455f53237598bc2620161a44fb89c48ddfeb6e3
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4081ce1..e407ba2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -136,7 +136,7 @@
add_library(proxydatastore STATIC
src/proxy_datastore.cpp
)
-target_link_libraries(proxydatastore PUBLIC datastoreaccess)
+target_link_libraries(proxydatastore PUBLIC datastoreaccess yangaccess)
# Links libraries, that aren't specific to a datastore type
function(cli_link_required cli_target)
@@ -282,7 +282,7 @@
else()
message(FATAL_ERROR "Unknown backend ${backend}")
endif()
- target_link_libraries(${TESTNAME} yangschema sysreposubscription)
+ target_link_libraries(${TESTNAME} yangschema sysreposubscription proxydatastore)
target_compile_definitions(${TESTNAME} PRIVATE ${backend}_BACKEND)
endfunction()
diff --git a/src/ast_commands.hpp b/src/ast_commands.hpp
index 3f7eead..a594228 100644
--- a/src/ast_commands.hpp
+++ b/src/ast_commands.hpp
@@ -219,8 +219,49 @@
DataFormat m_format;
};
+struct rpc_ : x3::position_tagged {
+ static constexpr auto name = "rpc";
+ static constexpr auto shortHelp = "rpc - initiate RPC";
+ static constexpr auto longHelp = R"(
+ rpc <rpc-path>
+
+ This command puts you into a mode where you can set your input parameters.
+
+ Usage:
+ /> rpc <path-to-rpc>)";
+ bool operator==(const rpc_& other) const;
+
+ dataPath_ m_path;
+};
+
+struct exec_ : x3::position_tagged {
+ static constexpr auto name = "exec";
+ static constexpr auto shortHelp = "exec - execute RPC";
+ static constexpr auto longHelp = R"(
+ exec
+
+ This command executes the RPC you have previously initiated.
+
+ Usage:
+ /> exec)";
+ bool operator==(const exec_& other) const;
+};
+
+struct cancel_ : x3::position_tagged {
+ static constexpr auto name = "cancel";
+ static constexpr auto shortHelp = "cancel - cancel an ongoing RPC input";
+ static constexpr auto longHelp = R"(
+ cancel
+
+ This command cancels a previously entered RPC context.
+
+ Usage:
+ /> cancel)";
+ bool operator==(const cancel_& other) const;
+};
+
struct help_;
-using CommandTypes = boost::mpl::vector<cd_, commit_, copy_, create_, delete_, describe_, discard_, dump_, get_, help_, ls_, move_, set_>;
+using CommandTypes = boost::mpl::vector<cancel_, cd_, commit_, copy_, create_, delete_, describe_, discard_, dump_, exec_, get_, help_, ls_, move_, rpc_, set_>;
struct help_ : x3::position_tagged {
static constexpr auto name = "help";
static constexpr auto shortHelp = "help - Print help for commands.";
@@ -268,3 +309,4 @@
BOOST_FUSION_ADAPT_STRUCT(copy_, m_source, m_destination)
BOOST_FUSION_ADAPT_STRUCT(move_, m_source, m_destination)
BOOST_FUSION_ADAPT_STRUCT(dump_, m_format)
+BOOST_FUSION_ADAPT_STRUCT(rpc_, m_path)
diff --git a/src/ast_handlers.hpp b/src/ast_handlers.hpp
index 08d2079..87cacd1 100644
--- a/src/ast_handlers.hpp
+++ b/src/ast_handlers.hpp
@@ -230,6 +230,10 @@
}
};
+struct rpcPath_class;
+
+struct cdPath_class;
+
struct set_class {
template <typename Iterator, typename Exception, typename Context>
x3::error_handler_result on_error(Iterator&, Iterator const&, Exception const& x, Context const& context)
@@ -256,6 +260,12 @@
struct dump_class;
+struct rpc_class;
+
+struct exec_class;
+
+struct cancel_class;
+
struct command_class {
template <typename Iterator, typename Exception, typename Context>
x3::error_handler_result on_error(Iterator&, Iterator const&, Exception const& x, Context const& context)
diff --git a/src/ast_path.cpp b/src/ast_path.cpp
index 44c98c3..e8bfbcf 100644
--- a/src/ast_path.cpp
+++ b/src/ast_path.cpp
@@ -110,6 +110,11 @@
return (this->m_name == b.m_name);
}
+bool rpcNode_::operator==(const rpcNode_& other) const
+{
+ return this->m_name == other.m_name;
+}
+
list_::list_(const std::string& listName)
: m_name(listName)
{
diff --git a/src/ast_path.hpp b/src/ast_path.hpp
index 4e21167..d02be60 100644
--- a/src/ast_path.hpp
+++ b/src/ast_path.hpp
@@ -81,9 +81,15 @@
std::string m_name;
};
+struct rpcNode_ {
+ bool operator==(const rpcNode_& other) const;
+
+ std::string m_name;
+};
+
struct schemaNode_ {
boost::optional<module_> m_prefix;
- std::variant<container_, list_, nodeup_, leaf_, leafList_> m_suffix;
+ std::variant<container_, list_, nodeup_, leaf_, leafList_, rpcNode_> m_suffix;
schemaNode_();
schemaNode_(decltype(m_suffix) node);
@@ -93,7 +99,7 @@
struct dataNode_ {
boost::optional<module_> m_prefix;
- std::variant<container_, listElement_, nodeup_, leaf_, leafListElement_, leafList_, list_> m_suffix;
+ std::variant<container_, listElement_, nodeup_, leaf_, leafListElement_, leafList_, list_, rpcNode_> m_suffix;
dataNode_();
dataNode_(decltype(m_suffix) node);
diff --git a/src/cli.cpp b/src/cli.cpp
index 1f62f78..a539b90 100644
--- a/src/cli.cpp
+++ b/src/cli.cpp
@@ -15,6 +15,7 @@
#include "proxy_datastore.hpp"
#if defined(SYSREPO_CLI)
#include "sysrepo_access.hpp"
+#include "yang_schema.hpp"
#define PROGRAM_NAME "sysrepo-cli"
static const auto usage = R"(CLI interface to sysrepo
@@ -124,7 +125,17 @@
#error "Unknown CLI backend"
#endif
- ProxyDatastore proxyDatastore(datastore);
+#if defined(SYSREPO_CLI)
+ auto createTemporaryDatastore = [](const std::shared_ptr<DatastoreAccess>& datastore) {
+ return std::make_shared<YangAccess>(std::static_pointer_cast<YangSchema>(datastore->schema()));
+ };
+#elif defined(YANG_CLI)
+ auto createTemporaryDatastore = [](const std::shared_ptr<DatastoreAccess>&) {
+ return nullptr;
+ };
+#endif
+
+ ProxyDatastore proxyDatastore(datastore, createTemporaryDatastore);
auto dataQuery = std::make_shared<DataQuery>(*datastore);
Parser parser(datastore->schema(), writableOps, dataQuery);
@@ -189,6 +200,8 @@
std::cerr << ex.what() << std::endl;
} catch (DatastoreException& ex) {
std::cerr << ex.what() << std::endl;
+ } catch (std::runtime_error& ex) {
+ std::cerr << ex.what() << std::endl;
}
lineEditor.history_add(line);
diff --git a/src/grammars.hpp b/src/grammars.hpp
index 742115b..ee808d5 100644
--- a/src/grammars.hpp
+++ b/src/grammars.hpp
@@ -29,6 +29,9 @@
x3::rule<copy_class, copy_> const copy = "copy";
x3::rule<move_class, move_> const move = "move";
x3::rule<dump_class, dump_> const dump = "dump";
+x3::rule<rpc_class, rpc_> const rpc = "rpc";
+x3::rule<exec_class, exec_> const exec = "exec";
+x3::rule<cancel_class, cancel_> const cancel = "cancel";
x3::rule<command_class, command_> const command = "command";
x3::rule<createCommandSuggestions_class, x3::unused_type> const createCommandSuggestions = "createCommandSuggestions";
@@ -52,7 +55,7 @@
ls_::name >> *(space_separator >> ls_options) >> -(space_separator >> (anyPath | (module >> "*")));
auto const cd_def =
- cd_::name >> space_separator > dataPath;
+ cd_::name >> space_separator > cdPath;
auto const create_def =
create_::name >> space_separator > (presenceContainerPath | listInstancePath | leafListElementPath);
@@ -255,6 +258,15 @@
}
} const dump_args;
+auto const rpc_def =
+ rpc_::name > space_separator > rpcPath;
+
+auto const exec_def =
+ exec_::name >> x3::attr(exec_{});
+
+auto const cancel_def =
+ cancel_::name >> x3::attr(cancel_{});
+
auto const dump_def =
dump_::name > space_separator >> dump_args;
@@ -262,7 +274,7 @@
x3::eps;
auto const command_def =
- createCommandSuggestions >> x3::expect[cd | copy | create | delete_rule | set | commit | get | ls | discard | describe | help | move | dump];
+ createCommandSuggestions >> x3::expect[cd | copy | create | delete_rule | set | commit | get | ls | discard | describe | help | move | dump | rpc | exec | cancel];
#if __clang__
#pragma GCC diagnostic pop
@@ -281,5 +293,8 @@
BOOST_SPIRIT_DEFINE(copy)
BOOST_SPIRIT_DEFINE(move)
BOOST_SPIRIT_DEFINE(dump)
+BOOST_SPIRIT_DEFINE(rpc)
+BOOST_SPIRIT_DEFINE(exec)
+BOOST_SPIRIT_DEFINE(cancel)
BOOST_SPIRIT_DEFINE(command)
BOOST_SPIRIT_DEFINE(createCommandSuggestions)
diff --git a/src/interpreter.cpp b/src/interpreter.cpp
index 5ff73e7..26e2be4 100644
--- a/src/interpreter.cpp
+++ b/src/interpreter.cpp
@@ -30,6 +30,26 @@
}
};
+namespace {
+void printTree(const DatastoreAccess::Tree tree)
+{
+ for (auto it = tree.begin(); it != tree.end(); it++) {
+ auto [path, value] = *it;
+ if (value.type() == typeid(special_) && boost::get<special_>(value).m_value == SpecialValue::LeafList) {
+ auto leafListPrefix = path;
+ std::cout << path << " = " << leafDataToString(value) << std::endl;
+
+ while (it + 1 != tree.end() && boost::starts_with((it + 1)->first, leafListPrefix)) {
+ ++it;
+ std::cout << stripLeafListValueFromPath(it->first) << " = " << leafDataToString(it->second) << std::endl;
+ }
+ } else {
+ std::cout << path << " = " << leafDataToString(value) << std::endl;
+ }
+ }
+}
+}
+
template <typename PathType>
std::string pathToString(const PathType& path)
{
@@ -64,20 +84,7 @@
void Interpreter::operator()(const get_& get) const
{
auto items = m_datastore.getItems(pathToString(toCanonicalPath(get.m_path)));
- for (auto it = items.begin(); it != items.end(); it++) {
- auto [path, value] = *it;
- if (value.type() == typeid(special_) && boost::get<special_>(value).m_value == SpecialValue::LeafList) {
- auto leafListPrefix = path;
- std::cout << path << " = " << leafDataToString(value) << std::endl;
-
- while (it + 1 != items.end() && boost::starts_with((it + 1) ->first, leafListPrefix)) {
- ++it;
- std::cout << stripLeafListValueFromPath(it->first) << " = " << leafDataToString(it->second) << std::endl;
- }
- } else {
- std::cout << path << " = " << leafDataToString(value) << std::endl;
- }
- }
+ printTree(items);
}
void Interpreter::operator()(const cd_& cd) const
@@ -162,12 +169,14 @@
case yang::NodeTypes::List:
ss << "list";
break;
+ case yang::NodeTypes::Rpc:
+ ss << "RPC";
+ break;
case yang::NodeTypes::Action:
case yang::NodeTypes::AnyXml:
case yang::NodeTypes::LeafList:
case yang::NodeTypes::Notification:
- case yang::NodeTypes::Rpc:
- throw std::logic_error("describe got an rpc or an action: this should never happen, because their paths cannot be parsed");
+ throw std::logic_error("describe can't handle the type of " + path);
}
if (!m_datastore.schema()->isConfig(path)) {
@@ -201,6 +210,26 @@
std::cout << m_datastore.dump(dump.m_format) << "\n";
}
+void Interpreter::operator()(const rpc_& rpc) const
+{
+ m_datastore.initiateRpc(pathToString(toCanonicalPath(rpc.m_path)));
+ m_parser.changeNode(rpc.m_path);
+}
+
+void Interpreter::operator()(const exec_&) const
+{
+ m_parser.changeNode({Scope::Absolute, {}});
+ auto output = m_datastore.executeRpc();
+ std::cout << "RPC output:\n";
+ printTree(output);
+}
+
+void Interpreter::operator()(const cancel_&) const
+{
+ m_parser.changeNode({Scope::Absolute, {}});
+ m_datastore.cancelRpc();
+}
+
struct commandLongHelpVisitor : boost::static_visitor<const char*> {
template <typename T>
auto constexpr operator()(boost::type<T>) const
diff --git a/src/interpreter.hpp b/src/interpreter.hpp
index 000cc21..0807c02 100644
--- a/src/interpreter.hpp
+++ b/src/interpreter.hpp
@@ -29,6 +29,9 @@
void operator()(const copy_& copy) const;
void operator()(const move_& move) const;
void operator()(const dump_& dump) const;
+ void operator()(const rpc_& rpc) const;
+ void operator()(const exec_& exec) const;
+ void operator()(const cancel_& cancel) const;
private:
[[nodiscard]] std::string buildTypeInfo(const std::string& path) const;
diff --git a/src/path_parser.hpp b/src/path_parser.hpp
index 9fa3db1..00629bb 100644
--- a/src/path_parser.hpp
+++ b/src/path_parser.hpp
@@ -14,6 +14,8 @@
namespace x3 = boost::spirit::x3;
+x3::rule<cdPath_class, dataPath_> const cdPath = "cdPath";
+x3::rule<rpcPath_class, dataPath_> const rpcPath = "rpcPath";
x3::rule<presenceContainerPath_class, dataPath_> const presenceContainerPath = "presenceContainerPath";
x3::rule<listInstancePath_class, dataPath_> const listInstancePath = "listInstancePath";
x3::rule<leafListElementPath_class, dataPath_> const leafListElementPath = "leafListElementPath";
@@ -137,10 +139,13 @@
parserContext.m_suggestions.emplace(Completion{parseString, "[", Completion::WhenToAdd::IfFullMatch});
}
break;
+ case yang::NodeTypes::Rpc:
+ out.m_suffix = rpcNode_{child.second};
+ parserContext.m_suggestions.emplace(Completion{parseString + "/"});
+ break;
case yang::NodeTypes::Action:
case yang::NodeTypes::AnyXml:
case yang::NodeTypes::Notification:
- case yang::NodeTypes::Rpc:
continue;
}
table.add(parseString, out);
@@ -421,6 +426,20 @@
auto const writableLeafPath_def =
PathParser<PathParserMode::DataPath, CompletionMode::Data>{filterConfigFalse};
+auto const onlyRpc = [] (const Schema& schema, const std::string& path) {
+ return schema.nodeType(path) == yang::NodeTypes::Rpc;
+};
+
+auto const rpcPath_def =
+ PathParser<PathParserMode::DataPath, CompletionMode::Data>{onlyRpc};
+
+auto const noRpc = [] (const Schema& schema, const std::string& path) {
+ return schema.nodeType(path) != yang::NodeTypes::Rpc;
+};
+
+auto const cdPath_def =
+ PathParser<PathParserMode::DataPath, CompletionMode::Data>{noRpc};
+
auto const presenceContainerPath_def =
dataPath;
@@ -443,6 +462,8 @@
BOOST_SPIRIT_DEFINE(keyValue)
BOOST_SPIRIT_DEFINE(key_identifier)
BOOST_SPIRIT_DEFINE(listSuffix)
+BOOST_SPIRIT_DEFINE(rpcPath)
+BOOST_SPIRIT_DEFINE(cdPath)
BOOST_SPIRIT_DEFINE(presenceContainerPath)
BOOST_SPIRIT_DEFINE(listInstancePath)
BOOST_SPIRIT_DEFINE(leafListElementPath)
diff --git a/src/proxy_datastore.cpp b/src/proxy_datastore.cpp
index ab9193c..e6878cb 100644
--- a/src/proxy_datastore.cpp
+++ b/src/proxy_datastore.cpp
@@ -4,36 +4,39 @@
* Written by Václav Kubernát <kubernat@cesnet.cz>
*
*/
+#include <boost/algorithm/string/predicate.hpp>
#include "proxy_datastore.hpp"
+#include "yang_schema.hpp"
-ProxyDatastore::ProxyDatastore(const std::shared_ptr<DatastoreAccess>& datastore)
+ProxyDatastore::ProxyDatastore(const std::shared_ptr<DatastoreAccess>& datastore, std::function<std::shared_ptr<DatastoreAccess>(const std::shared_ptr<DatastoreAccess>&)> createTemporaryDatastore)
: m_datastore(datastore)
+ , m_createTemporaryDatastore(createTemporaryDatastore)
{
}
DatastoreAccess::Tree ProxyDatastore::getItems(const std::string& path) const
{
- return m_datastore->getItems(path);
+ return pickDatastore(path)->getItems(path);
}
void ProxyDatastore::setLeaf(const std::string& path, leaf_data_ value)
{
- m_datastore->setLeaf(path, value);
+ pickDatastore(path)->setLeaf(path, value);
}
void ProxyDatastore::createItem(const std::string& path)
{
- m_datastore->createItem(path);
+ pickDatastore(path)->createItem(path);
}
void ProxyDatastore::deleteItem(const std::string& path)
{
- m_datastore->deleteItem(path);
+ pickDatastore(path)->deleteItem(path);
}
void ProxyDatastore::moveItem(const std::string& source, std::variant<yang::move::Absolute, yang::move::Relative> move)
{
- m_datastore->moveItem(source, move);
+ pickDatastore(source)->moveItem(source, move);
}
void ProxyDatastore::commitChanges()
@@ -56,7 +59,41 @@
return m_datastore->dump(format);
}
+void ProxyDatastore::initiateRpc(const std::string& rpcPath)
+{
+ if (m_inputDatastore) {
+ throw std::runtime_error("RPC input already in progress (" + m_rpcPath + ")");
+ }
+ m_inputDatastore = m_createTemporaryDatastore(m_datastore);
+ m_rpcPath = rpcPath;
+ m_inputDatastore->createItem(rpcPath);
+}
+
+DatastoreAccess::Tree ProxyDatastore::executeRpc()
+{
+ if (!m_inputDatastore) {
+ throw std::runtime_error("No RPC input in progress");
+ }
+ auto inputData = m_inputDatastore->getItems("/");
+ m_inputDatastore = nullptr;
+ return m_datastore->executeRpc(m_rpcPath, inputData);
+}
+
+void ProxyDatastore::cancelRpc()
+{
+ m_inputDatastore = nullptr;
+}
+
std::shared_ptr<Schema> ProxyDatastore::schema() const
{
return m_datastore->schema();
}
+
+std::shared_ptr<DatastoreAccess> ProxyDatastore::pickDatastore(const std::string& path) const
+{
+ if (!m_inputDatastore || !boost::starts_with(path, m_rpcPath)) {
+ return m_datastore;
+ } else {
+ return m_inputDatastore;
+ }
+}
diff --git a/src/proxy_datastore.hpp b/src/proxy_datastore.hpp
index 0ed5e2d..c15a869 100644
--- a/src/proxy_datastore.hpp
+++ b/src/proxy_datastore.hpp
@@ -7,13 +7,18 @@
#pragma once
#include "datastore_access.hpp"
+#include "yang_access.hpp"
/*! \class ProxyDatastore
* \brief DatastoreAccess wrapper that handles RPC input
*/
class ProxyDatastore {
public:
- ProxyDatastore(const std::shared_ptr<DatastoreAccess>& datastore);
+ /**
+ * The createTemporaryDatastore function should create a temporary datastore that's going to be used for RPC input.
+ * This temporary datastore and the main datastore are supposed share the same schemas.
+ * */
+ ProxyDatastore(const std::shared_ptr<DatastoreAccess>& datastore, std::function<std::shared_ptr<DatastoreAccess>(const std::shared_ptr<DatastoreAccess>&)> createTemporaryDatastore);
[[nodiscard]] DatastoreAccess::Tree getItems(const std::string& path) const;
void setLeaf(const std::string& path, leaf_data_ value);
void createItem(const std::string& path);
@@ -24,7 +29,21 @@
void copyConfig(const Datastore source, const Datastore destination);
[[nodiscard]] std::string dump(const DataFormat format) const;
+ void initiateRpc(const std::string& rpcPath);
+ [[nodiscard]] DatastoreAccess::Tree executeRpc();
+ void cancelRpc();
+
[[nodiscard]] std::shared_ptr<Schema> schema() const;
private:
+ /** @brief Picks a datastore based on the requested path.
+ *
+ * If the path starts with a currently processed RPC, m_inputDatastore is picked.
+ * Otherwise m_datastore is picked.
+ */
+ [[nodiscard]] std::shared_ptr<DatastoreAccess> pickDatastore(const std::string& path) const;
+
std::shared_ptr<DatastoreAccess> m_datastore;
+ std::function<std::shared_ptr<DatastoreAccess>(const std::shared_ptr<DatastoreAccess>&)> m_createTemporaryDatastore;
+ std::shared_ptr<DatastoreAccess> m_inputDatastore;
+ std::string m_rpcPath;
};
diff --git a/src/static_schema.cpp b/src/static_schema.cpp
index 75ccb4f..e54ca68 100644
--- a/src/static_schema.cpp
+++ b/src/static_schema.cpp
@@ -34,6 +34,15 @@
m_nodes.emplace(key, std::unordered_map<std::string, NodeInfo>());
}
+void StaticSchema::addRpc(const std::string& location, const std::string& name)
+{
+ m_nodes.at(location).emplace(name, NodeInfo{yang::rpc{}, yang::AccessType::Writable});
+
+ //create a new set of children for the new node
+ std::string key = joinPaths(location, name);
+ m_nodes.emplace(key, std::unordered_map<std::string, NodeInfo>());
+}
+
bool StaticSchema::listHasKey(const schemaPath_& listPath, const std::string& key) const
{
return listKeys(listPath).count(key);
@@ -201,6 +210,11 @@
{
return yang::NodeTypes::LeafList;
}
+
+ yang::NodeTypes operator()(const yang::rpc)
+ {
+ return yang::NodeTypes::Rpc;
+ }
};
yang::NodeTypes StaticSchema::nodeType(const schemaPath_& location, const ModuleNodePair& node) const
diff --git a/src/static_schema.hpp b/src/static_schema.hpp
index 9d897b1..e343087 100644
--- a/src/static_schema.hpp
+++ b/src/static_schema.hpp
@@ -34,6 +34,9 @@
yang::TypeInfo m_type;
};
+struct rpc {
+};
+
struct module {
};
@@ -43,7 +46,7 @@
};
}
-using NodeType = std::variant<yang::container, yang::list, yang::leaf, yang::leaflist>;
+using NodeType = std::variant<yang::container, yang::list, yang::leaf, yang::leaflist, yang::rpc>;
struct NodeInfo {
NodeType m_nodeType;
@@ -83,6 +86,7 @@
void addLeaf(const std::string& location, const std::string& name, const yang::LeafDataType& type, const yang::AccessType accessType = yang::AccessType::Writable);
void addLeafList(const std::string& location, const std::string& name, const yang::LeafDataType& type);
void addList(const std::string& location, const std::string& name, const std::set<std::string>& keys);
+ void addRpc(const std::string& location, const std::string& name);
void addModule(const std::string& name);
void addIdentity(const std::optional<identityRef_>& base, const identityRef_& name);
diff --git a/src/sysrepo_access.cpp b/src/sysrepo_access.cpp
index 0fac1d8..510c530 100644
--- a/src/sysrepo_access.cpp
+++ b/src/sysrepo_access.cpp
@@ -132,7 +132,16 @@
void operator()(const special_& value) const
{
- throw std::runtime_error("Tried constructing S_Val from a " + specialValueToString(value));
+ switch (value.m_value) {
+ case SpecialValue::PresenceContainer:
+ v->set(xpath.c_str(), nullptr, SR_CONTAINER_PRESENCE_T);
+ break;
+ case SpecialValue::List:
+ v->set(xpath.c_str(), nullptr, SR_LIST_T);
+ break;
+ default:
+ throw std::runtime_error("Tried constructing S_Val from a " + specialValueToString(value));
+ }
}
void operator()(const std::string& value) const
diff --git a/src/yang_access.cpp b/src/yang_access.cpp
index 8277ef1..ebe39f1 100644
--- a/src/yang_access.cpp
+++ b/src/yang_access.cpp
@@ -31,6 +31,15 @@
: m_ctx(lyWrap(ly_ctx_new(nullptr, LY_CTX_DISABLE_SEARCHDIR_CWD)))
, m_datastore(lyWrap<lyd_node>(nullptr))
, m_schema(std::make_shared<YangSchema>(libyang::create_new_Context(m_ctx.get())))
+ , m_validation_mode(LYD_OPT_DATA)
+{
+}
+
+YangAccess::YangAccess(std::shared_ptr<YangSchema> schema)
+ : m_ctx(schema->m_context->swig_ctx(), [](auto) {})
+ , m_datastore(lyWrap<lyd_node>(nullptr))
+ , m_schema(schema)
+ , m_validation_mode(LYD_OPT_RPC)
{
}
@@ -103,7 +112,12 @@
void YangAccess::validate()
{
auto datastore = m_datastore.release();
- lyd_validate(&datastore, LYD_OPT_DATA | LYD_OPT_DATA_NO_YANGLIB, m_ctx.get());
+
+ if (m_validation_mode == LYD_OPT_RPC) {
+ lyd_validate(&datastore, m_validation_mode, nullptr);
+ } else {
+ lyd_validate(&datastore, m_validation_mode | LYD_OPT_DATA_NO_YANGLIB, m_ctx.get());
+ }
m_datastore = lyWrap(datastore);
}
@@ -116,8 +130,12 @@
auto set = lyWrap(lyd_find_path(m_datastore.get(), path == "/" ? "/*" : path.c_str()));
auto setWrapper = libyang::Set(set.get(), nullptr);
-
- lyNodesToTree(res, setWrapper.data());
+ std::optional<std::string> ignoredXPathPrefix;
+ if (m_datastore->schema->nodetype == LYS_RPC) {
+ auto path = std::unique_ptr<char, decltype(&free)>(lys_path(m_datastore->schema, 0), &free);
+ ignoredXPathPrefix = joinPaths(path.get(), "/");
+ }
+ lyNodesToTree(res, setWrapper.data(), ignoredXPathPrefix);
return res;
}
@@ -218,6 +236,9 @@
getErrorsAndThrow();
}
for (const auto& [k, v] : input) {
+ if (v.type() == typeid(special_) && boost::get<special_>(v).m_value != SpecialValue::PresenceContainer) {
+ continue;
+ }
auto node = lyd_new_path(root.get(), m_ctx.get(), joinPaths(path, k).c_str(), (void*)leafDataToString(v).c_str(), LYD_ANYDATA_CONSTSTRING, 0);
if (!node) {
getErrorsAndThrow();
diff --git a/src/yang_access.hpp b/src/yang_access.hpp
index 075ba26..503a077 100644
--- a/src/yang_access.hpp
+++ b/src/yang_access.hpp
@@ -21,6 +21,7 @@
class YangAccess : public DatastoreAccess {
public:
YangAccess();
+ YangAccess(std::shared_ptr<YangSchema> schema);
~YangAccess() override;
[[nodiscard]] Tree getItems(const std::string& path) const override;
void setLeaf(const std::string& path, leaf_data_ value) override;
@@ -53,4 +54,5 @@
std::unique_ptr<ly_ctx, void(*)(ly_ctx*)> m_ctx;
std::unique_ptr<lyd_node, void(*)(lyd_node*)> m_datastore;
std::shared_ptr<YangSchema> m_schema;
+ const int m_validation_mode;
};
diff --git a/src/yang_schema.cpp b/src/yang_schema.cpp
index 711cdbd..05195b9 100644
--- a/src/yang_schema.cpp
+++ b/src/yang_schema.cpp
@@ -101,7 +101,11 @@
[&oldOptions]() {
libyang::set_log_options(oldOptions);
});
- return m_context->get_node(nullptr, node.c_str());
+ auto res = m_context->get_node(nullptr, node.c_str());
+ if (!res) { // If no node is found, try output rpc nodes too.
+ res = m_context->get_node(nullptr, node.c_str(), 1);
+ }
+ return res;
}
}
@@ -476,7 +480,20 @@
bool YangSchema::isConfig(const std::string& path) const
{
- return getSchemaNode(path.c_str())->flags() & LYS_CONFIG_W;
+ auto node = getSchemaNode(path.c_str());
+ if (node->flags() & LYS_CONFIG_W) {
+ return true;
+ }
+
+ // Node can still be an input node.
+ while (node->parent()) {
+ node = node->parent();
+ if (node->nodetype() == LYS_INPUT) {
+ return true;
+ }
+ }
+
+ return false;
}
std::optional<std::string> YangSchema::defaultValue(const std::string& leafPath) const
diff --git a/src/yang_schema.hpp b/src/yang_schema.hpp
index a527ef3..5f3aa20 100644
--- a/src/yang_schema.hpp
+++ b/src/yang_schema.hpp
@@ -70,6 +70,7 @@
std::shared_ptr<libyang::Module> getYangModule(const std::string& name);
private:
+ friend class YangAccess;
template <typename NodeType>
[[nodiscard]] yang::TypeInfo impl_leafType(const std::shared_ptr<libyang::Schema_Node>& node) const;
[[nodiscard]] std::set<std::string> modules() const;
diff --git a/tests/cd.cpp b/tests/cd.cpp
index 0c5411b..307f287 100644
--- a/tests/cd.cpp
+++ b/tests/cd.cpp
@@ -29,6 +29,7 @@
schema->addList("/", "example:twoKeyList", {"number", "name"});
schema->addLeaf("/example:twoKeyList", "example:number", yang::Int32{});
schema->addLeaf("/example:twoKeyList", "example:name", yang::String{});
+ schema->addRpc("/", "example:launch-nukes");
Parser parser(schema);
std::string input;
std::ostringstream errorStream;
@@ -313,6 +314,11 @@
input = "cd example:list [number=10]";
}
+ SECTION("cd into rpc")
+ {
+ input = "cd example:launch-nukes";
+ }
+
REQUIRE_THROWS_AS(parser.parseCommand(input, errorStream), InvalidCommandException);
}
}
diff --git a/tests/command_completion.cpp b/tests/command_completion.cpp
index cf6b188..49017b0 100644
--- a/tests/command_completion.cpp
+++ b/tests/command_completion.cpp
@@ -21,7 +21,7 @@
int expectedContextLength;
SECTION("no prefix")
{
- expectedCompletions = {"cd", "copy", "create", "delete", "set", "commit", "get", "ls", "discard", "help", "describe", "move", "dump"};
+ expectedCompletions = {"cd", "copy", "create", "delete", "set", "commit", "get", "ls", "discard", "help", "describe", "move", "dump", "rpc", "exec", "cancel"};
expectedContextLength = 0;
SECTION("no space") {
input = "";
@@ -34,7 +34,7 @@
SECTION("c")
{
input = "c";
- expectedCompletions = {"cd", "commit", "copy", "create"};
+ expectedCompletions = {"cd", "commit", "copy", "create", "cancel"};
expectedContextLength = 1;
}
diff --git a/tests/datastore_access.cpp b/tests/datastore_access.cpp
index b45f027..fb44018 100644
--- a/tests/datastore_access.cpp
+++ b/tests/datastore_access.cpp
@@ -8,6 +8,8 @@
#include "trompeloeil_doctest.hpp"
#include <sysrepo-cpp/Session.hpp>
+#include "yang_schema.hpp"
+#include "proxy_datastore.hpp"
#ifdef sysrepo_BACKEND
#include "sysrepo_access.hpp"
@@ -831,9 +833,11 @@
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) {
+ if (xpath == "/example-schema:noop"s || xpath == "/example-schema:fire"s) {
return SR_ERR_OK;
- } else if (xpath == nukes) {
+ }
+
+ if (xpath == nukes) {
uint64_t kilotons = 0;
bool hasCities = false;
for (size_t i = 0; i < input->val_cnt(); ++i) {
@@ -879,21 +883,32 @@
auto srSubscription = std::make_shared<sysrepo::Subscribe>(srSession);
auto cb = std::make_shared<RpcCb>();
sysrepo::Logs{}.set_stderr(SR_LL_INF);
+ auto doNothingCb = std::make_shared<sysrepo::Callback>();
+ srSubscription->module_change_subscribe("example-schema", doNothingCb, nullptr, SR_SUBSCR_CTX_REUSE);
+ // careful here, sysrepo insists on module_change CBs being registered before RPC CBs, otherwise there's a memleak
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);
+ srSubscription->rpc_subscribe("/example-schema:fire", cb, nullptr, SR_SUBSCR_CTX_REUSE);
#ifdef sysrepo_BACKEND
- SysrepoAccess datastore("netconf-cli-test", Datastore::Running);
+ auto datastore = std::make_shared<SysrepoAccess>("netconf-cli-test", Datastore::Running);
#elif defined(netconf_BACKEND)
- NetconfAccess datastore(NETOPEER_SOCKET_PATH);
+ auto datastore = std::make_shared<NetconfAccess>(NETOPEER_SOCKET_PATH);
#elif defined(yang_BACKEND)
- YangAccess datastore;
- datastore.addSchemaDir(schemaDir);
- datastore.addSchemaFile(exampleSchemaFile);
+ auto datastore = std::make_shared<YangAccess>();
+ datastore->addSchemaDir(schemaDir);
+ datastore->addSchemaFile(exampleSchemaFile);
#else
#error "Unknown backend"
#endif
+ auto createTemporaryDatastore = [](const std::shared_ptr<DatastoreAccess>& datastore) {
+ return std::make_shared<YangAccess>(std::static_pointer_cast<YangSchema>(datastore->schema()));
+ };
+
+ ProxyDatastore proxyDatastore(datastore, createTemporaryDatastore);
+
+ // ProxyDatastore cannot easily read DatastoreAccess::Tree, so we need to set the input via create/setLeaf/etc.
SECTION("valid")
{
std::string rpc;
@@ -901,6 +916,7 @@
SECTION("noop") {
rpc = "/example-schema:noop";
+ proxyDatastore.initiateRpc(rpc);
}
SECTION("small nuke") {
@@ -909,6 +925,8 @@
{"description", "dummy"s},
{"payload/kilotons", uint64_t{333'666}},
};
+ proxyDatastore.initiateRpc(rpc);
+ proxyDatastore.setLeaf("/example-schema:launch-nukes/example-schema:payload/example-schema:kilotons", uint64_t{333'666});
// no data are returned
}
@@ -918,6 +936,9 @@
{"description", "dummy"s},
{"payload/kilotons", uint64_t{4}},
};
+ proxyDatastore.initiateRpc(rpc);
+ proxyDatastore.setLeaf("/example-schema:launch-nukes/example-schema:payload/example-schema:kilotons", uint64_t{4});
+
output = {
{"blast-radius", uint32_t{33'666}},
{"actual-yield", uint64_t{5}},
@@ -930,6 +951,9 @@
{"payload/kilotons", uint64_t{6}},
{"cities/targets[city='Prague']/city", "Prague"s},
};
+ proxyDatastore.initiateRpc(rpc);
+ proxyDatastore.setLeaf("/example-schema:launch-nukes/example-schema:payload/example-schema:kilotons", uint64_t{6});
+ proxyDatastore.createItem("/example-schema:launch-nukes/example-schema:cities/example-schema:targets[city='Prague']");
output = {
{"blast-radius", uint32_t{33'666}},
{"actual-yield", uint64_t{7}},
@@ -941,12 +965,25 @@
};
}
- catching<OnRPC>([&] {REQUIRE(datastore.executeRpc(rpc, input) == output);});
+ SECTION("with leafref") {
+ datastore->createItem("/example-schema:person[name='Colton']");
+ datastore->commitChanges();
+
+ rpc = "/example-schema:fire";
+ input = {
+ {"whom", "Colton"s}
+ };
+ proxyDatastore.initiateRpc(rpc);
+ proxyDatastore.setLeaf("/example-schema:fire/example-schema:whom", "Colton"s);
+ }
+
+ catching<OnRPC>([&] {REQUIRE(datastore->executeRpc(rpc, input) == output);});
+ catching<OnRPC>([&] {REQUIRE(proxyDatastore.executeRpc() == output);});
}
SECTION("non-existing RPC")
{
- catching<OnInvalidRpcPath>([&] {datastore.executeRpc("/example-schema:non-existing", DatastoreAccess::Tree{});});
+ catching<OnInvalidRpcPath>([&] {datastore->executeRpc("/example-schema:non-existing", DatastoreAccess::Tree{});});
}
waitForCompletionAndBitMore(seq1);
diff --git a/tests/example-schema.yang b/tests/example-schema.yang
index 792ac42..04ae22d 100644
--- a/tests/example-schema.yang
+++ b/tests/example-schema.yang
@@ -111,6 +111,16 @@
}
}
+ rpc fire {
+ input {
+ leaf whom {
+ type leafref {
+ path '/aha:person/name';
+ }
+ }
+ }
+ }
+
rpc launch-nukes {
input {
container payload {
diff --git a/tests/interpreter.cpp b/tests/interpreter.cpp
index 89fbd29..a88aa48 100644
--- a/tests/interpreter.cpp
+++ b/tests/interpreter.cpp
@@ -39,7 +39,11 @@
auto schema = std::make_shared<MockSchema>();
Parser parser(schema);
auto datastore = std::make_shared<MockDatastoreAccess>();
- ProxyDatastore proxyDatastore(datastore);
+ auto input_datastore = std::make_shared<MockDatastoreAccess>();
+ auto createTemporaryDatastore = [input_datastore]([[maybe_unused]] const std::shared_ptr<DatastoreAccess>& datastore) {
+ return input_datastore;
+ };
+ ProxyDatastore proxyDatastore(datastore, createTemporaryDatastore);
std::vector<std::unique_ptr<trompeloeil::expectation>> expectations;
std::vector<command_> toInterpret;
@@ -422,3 +426,44 @@
boost::apply_visitor(Interpreter(parser, proxyDatastore), command);
}
}
+
+TEST_CASE("rpc")
+{
+ auto schema = std::make_shared<MockSchema>();
+ Parser parser(schema);
+ auto datastore = std::make_shared<MockDatastoreAccess>();
+ auto input_datastore = std::make_shared<MockDatastoreAccess>();
+ auto createTemporaryDatastore = [input_datastore]([[maybe_unused]] const std::shared_ptr<DatastoreAccess>& datastore) {
+ return input_datastore;
+ };
+ ProxyDatastore proxyDatastore(datastore, createTemporaryDatastore);
+
+ SECTION("entering/leaving rpc context")
+ {
+ dataPath_ rpcPath;
+ rpcPath.pushFragment({{"example"}, rpcNode_{"launch-nukes"}});
+ rpc_ rpcCmd;
+ rpcCmd.m_path = rpcPath;
+
+ {
+ REQUIRE_CALL(*input_datastore, createItem("/example:launch-nukes"));
+ boost::apply_visitor(Interpreter(parser, proxyDatastore), command_{rpcCmd});
+ }
+
+ REQUIRE(parser.currentPath() == rpcPath);
+
+ SECTION("exec")
+ {
+ REQUIRE_CALL(*input_datastore, getItems("/")).RETURN(DatastoreAccess::Tree{});
+ REQUIRE_CALL(*datastore, executeRpc("/example:launch-nukes", DatastoreAccess::Tree{})).RETURN(DatastoreAccess::Tree{});
+ boost::apply_visitor(Interpreter(parser, proxyDatastore), command_{exec_{}});
+ }
+
+ SECTION("cancel")
+ {
+ boost::apply_visitor(Interpreter(parser, proxyDatastore), command_{cancel_{}});
+ }
+
+ REQUIRE(parser.currentPath() == dataPath_{});
+ }
+}
diff --git a/tests/path_completion.cpp b/tests/path_completion.cpp
index 249dc8c..1c0c8f1 100644
--- a/tests/path_completion.cpp
+++ b/tests/path_completion.cpp
@@ -39,6 +39,8 @@
schema->addLeaf("/", "example:leafInt", yang::Int32{});
schema->addLeaf("/", "example:readonly", yang::Int32{}, yang::AccessType::ReadOnly);
schema->addLeafList("/", "example:addresses", yang::String{});
+ schema->addRpc("/", "second:fire");
+ schema->addLeaf("/second:fire", "second:whom", yang::String{});
auto mockDatastore = std::make_shared<MockDatastoreAccess>();
// The parser will use DataQuery for key value completion, but I'm not testing that here, so I don't return anything.
@@ -68,7 +70,7 @@
SECTION("ls ")
{
input = "ls ";
- expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/", "second:amelie/"};
+ expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/", "second:amelie/", "second:fire/"};
expectedContextLength = 0;
}
@@ -103,7 +105,7 @@
SECTION("ls /")
{
input = "ls /";
- expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/", "second:amelie/"};
+ expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/", "second:amelie/", "second:fire/"};
expectedContextLength = 0;
}
@@ -131,7 +133,7 @@
SECTION("ls /s")
{
input = "ls /s";
- expectedCompletions = {"second:amelie/"};
+ expectedCompletions = {"second:amelie/", "second:fire/"};
expectedContextLength = 1;
}
@@ -334,5 +336,28 @@
expectedContextLength = 0;
}
+ SECTION("rpc input nodes NOT completed for rpc command")
+ {
+ input = "rpc example:fire/";
+ expectedCompletions = {};
+ expectedContextLength = 13;
+ }
+
+ SECTION("rpc input nodes completed for set command")
+ {
+ parser.changeNode({{}, {{module_{"second"}, rpcNode_{"fire"}}}});
+ input = "set ";
+ expectedCompletions = {"whom "};
+ expectedContextLength = 0;
+ }
+
+ SECTION("completion for other stuff while inside an rpc")
+ {
+ parser.changeNode({{}, {{module_{"second"}, rpcNode_{"fire"}}}});
+ input = "set ../";
+ expectedCompletions = {"example:addresses", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list", "example:ovoce", "example:ovocezelenina", "example:twoKeyList", "second:amelie/", "second:fire/"};
+ expectedContextLength = 0;
+ }
+
REQUIRE(parser.completeCommand(input, errorStream) == (Completions{expectedCompletions, expectedContextLength}));
}