diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..b8ba8f3 --- /dev/null +++ b/.clang-format @@ -0,0 +1,20 @@ +# .clang-format +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: Never + +BreakBeforeBraces: Allman # Open braces on a new line +AllowShortIfStatementsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline + +ColumnLimit: 100 # Max line length (can be 80, 100, or more) +PointerAlignment: Right # `int* ptr` vs `int *ptr` + +IncludeBlocks: Preserve +SortIncludes: true + +SpacesBeforeTrailingComments: 1 +SpaceBeforeParens: ControlStatements + +Cpp11BracedListStyle: true diff --git a/CMakeLists.txt b/CMakeLists.txt index 56f0461..5ccd3bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,11 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/CmakeModules") find_package(PkgConfig) #official cmake module find_package(Boost REQUIRED log system chrono) #just boost log, system and chrono libraries +pkg_check_modules(GTEST REQUIRED gtest) +find_package(absl REQUIRED) +find_package(Protobuf REQUIRED) +find_package(gRPC CONFIG REQUIRED) + pkg_check_modules(SYSREPOC REQUIRED sysrepo>=2.2.14 IMPORTED_TARGET) pkg_check_modules(LIBYANGC REQUIRED libyang) @@ -57,19 +62,32 @@ add_subdirectory(proto) # Generate a compile_commands.json with compile options set(CMAKE_EXPORT_COMPILE_COMMANDS 1) -set(GNXI_SRC src/security/authentication.cpp - src/utils/log.cpp - src/utils/sysrepo.cpp - src/gnmi/gnmi.cpp - src/gnmi/gnmi_server.cpp +set(GNXI_SRC src/gnmi/encode/encode.cpp + src/gnmi/encode/json_ietf.cpp src/gnmi/capabilities.cpp + src/gnmi/confirm.cpp src/gnmi/get.cpp + src/gnmi/gnmi_server.cpp + src/gnmi/gnmi.cpp src/gnmi/rpc.cpp src/gnmi/set.cpp src/gnmi/subscribe.cpp - src/gnmi/confirm.cpp - src/gnmi/encode/encode.cpp - src/gnmi/encode/json_ietf.cpp + src/security/authentication.cpp + src/utils/log.cpp + src/utils/sysrepo.cpp +) + +set(GNXI_HEADERS src/gnmi/encode/encode.h + src/gnmi/confirm.h + src/gnmi/get.h + src/gnmi/gnmi.h + src/gnmi/rpc.h + src/gnmi/set.h + src/gnmi/subscribe.h + src/security/authentication.h + src/utils/log.h + src/utils/sysrepo.h + src/utils/utils.h ) add_executable(gnxi_server ${GNXI_SRC} src/main.cpp) @@ -107,20 +125,22 @@ target_link_libraries(gnxi_server gnmi ${SYSREPOC_LIBRARIES} ${LIBYANG_LIBRARIES} ${LIBYANGC_LIBRARIES} + gRPC::grpc++ + ${Protobuf_LIBRARIES} ) -# TEST -###### +# TESTS +####### enable_testing() -set(GNXI_TEST_SRC tests/main.cpp - tests/capabilities.cpp +set(GNXI_TEST_SRC tests/capabilities.cpp tests/get.cpp + tests/main.cpp tests/rpc.cpp tests/set.cpp tests/subscribe.cpp - ) +) add_executable(gnxi_server_test ${GNXI_TEST_SRC} ${GNXI_SRC}) configure_file(scripts/gnxi-server-logrotate gnxi-server-logrotate COPYONLY) @@ -162,6 +182,8 @@ target_link_libraries(gnxi_server_test gnmi ${LIBYANGC_LIBRARIES} ${SYSTEMD_LIBRARIES} gcov + gRPC::grpc++ + ${Protobuf_LIBRARIES} ) # Remove test files and auto-generated files from coverage report @@ -176,3 +198,12 @@ install(TARGETS gnxi_server RUNTIME DESTINATION sbin DESTINATION ${CMAKE_INSTALL_SBINDIR} ) + +# FORMAT +######## + +add_custom_target(format + COMMAND clang-format -i ${GNXI_HEADERS} ${GNXI_SRC} ${GNXI_TEST_SRC} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Formatting header and source files." + USES_TERMINAL) diff --git a/README.md b/README.md index 2443253..26a503c 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ Supported RPCs: * [X] Set * [X] Get * [X] Subscribe -* [X] Rpc -* [X] Confirm +* [X] Rpc (defined in proto/gnmi.proto) +* [X] Confirm (defined in proto/gnmi.proto) Supported encoding: @@ -32,16 +32,16 @@ Supported authentication/encryption methods: # Dependencies -``` -sysrepo-gnxi -+-- protobuf (>=3.0) #because of gnmi -+-- jsoncpp #because of get JSON -+-- grpc (cpp) (>=1.18.0) #because of TLS bug to verify client cert -+-- libyang-cpp -+-- sysrepo-cpp -| +-- libyang -| +-- sysrepo -``` +- C++20 compiler +- cmake >= 3.18.1 +- protobuf >= v31.1 +- grpc (cpp) (>=1.18.0) #because of TLS bug to verify client cert +- libyang-cpp (master branch) +- sysrepo-cpp (master branch) + - libyang (devel branch) + - sysrepo (devel branch) + +## Optional dependencies You can either install dependencies from sources or from the packages. diff --git a/proto/CMakeLists.txt b/proto/CMakeLists.txt index b957c5d..5e2fb5e 100644 --- a/proto/CMakeLists.txt +++ b/proto/CMakeLists.txt @@ -9,7 +9,7 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR}) # gRPC 1.12.0 will compile but to prevent a security bug, use 1.18.0 # We leverage grpc pkg-config files in /usr/local/lib/pkgconfig find_package(PkgConfig REQUIRED) -pkg_check_modules(grpc++ REQUIRED grpc++>=1.18.0) +#find_package(grpc-cpp REQUIRED) # GENERATION OF SOURCE FILES ############################ diff --git a/src/gnmi/capabilities.cpp b/src/gnmi/capabilities.cpp index bd83d04..63952cd 100644 --- a/src/gnmi/capabilities.cpp +++ b/src/gnmi/capabilities.cpp @@ -15,59 +15,56 @@ * limitations under the License. */ -#include "gnmi.h" -#include +#include -using namespace gnmi; -using namespace std; -using google::protobuf::FileOptions; +#include "gnmi.h" +#include "utils/log.h" -Status GNMIService::Capabilities(ServerContext *context, - const CapabilityRequest* request, - CapabilityResponse* response) +grpc::Status GNMIService::Capabilities(grpc::ServerContext *context, + const gnmi::CapabilityRequest *request, + gnmi::CapabilityResponse *response) { - (void)context; - string gnmi_version; - FileOptions fopts; + (void)context; + std::string gnmi_version; + google::protobuf::FileOptions fopts; - if (request->extension_size() > 0) { - BOOST_LOG_TRIVIAL(error) << "Extensions not implemented"; - return Status(StatusCode::UNIMPLEMENTED, "Extensions not implemented"); - } - - try { - auto sess = sr_con.sessionStart(); - auto node = sess.getModuleInfo(); - for (auto mod_node : node.child()->siblings()) { - auto model = response->add_supported_models(); - for (auto mod_value_node = mod_node.child(); mod_value_node.has_value(); - mod_value_node = mod_value_node.value().nextSibling()) { - if (!mod_value_node->schema().name().compare("name")) { - auto name = std::string(mod_value_node->asTerm().valueStr()); - model->set_name(name); - } - if (!mod_value_node->schema().name().compare("revision")) { - auto version = std::string(mod_value_node->asTerm().valueStr()); - model->set_version(version); - } - } + if (request->extension_size() > 0) + { + BOOST_LOG_TRIVIAL(error) << "Extensions not implemented"; + return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Extensions not implemented"); } - gnmi_version = response->GetDescriptor()->file()->options() - .GetExtension(gnmi::gnmi_service); - response->set_gnmi_version(gnmi_version); + try + { + auto sess = sr_con.sessionStart(); - //Encoding used in TypedValue for responses - //response->add_supported_encodings(gnmi::Encoding::JSON); - //response->add_supported_encodings(gnmi::Encoding::BYTES); - //response->add_supported_encodings(gnmi::Encoding::PROTO); - //response->add_supported_encodings(gnmi::Encoding::ASCII); - response->add_supported_encodings(gnmi::Encoding::JSON_IETF); + for (auto module : sess.getContext().modules()) + { + if (module.implemented()) + { + auto model = response->add_supported_models(); + model->set_name(module.name()); + model->set_organization(module.org().value_or("")); + model->set_version(module.revision().value_or("")); + } + } + + gnmi_version = + response->GetDescriptor()->file()->options().GetExtension(gnmi::gnmi_service); + response->set_gnmi_version(gnmi_version); - } catch (const exception &exc) { - BOOST_LOG_TRIVIAL(error) << exc.what(); - return Status(StatusCode::INTERNAL, "Fail getting schemas"); - } + // Encoding used in TypedValue for responses + // response->add_supported_encodings(gnmi::Encoding::JSON); + // response->add_supported_encodings(gnmi::Encoding::BYTES); + // response->add_supported_encodings(gnmi::Encoding::PROTO); + // response->add_supported_encodings(gnmi::Encoding::ASCII); + response->add_supported_encodings(gnmi::Encoding::JSON_IETF); + } + catch (const std::exception &exc) + { + BOOST_LOG_TRIVIAL(error) << exc.what(); + return grpc::Status(grpc::StatusCode::INTERNAL, "Fail getting schemas"); + } - return Status::OK; + return grpc::Status::OK; } diff --git a/src/gnmi/confirm.cpp b/src/gnmi/confirm.cpp index c0f00a8..dda62bb 100644 --- a/src/gnmi/confirm.cpp +++ b/src/gnmi/confirm.cpp @@ -14,7 +14,6 @@ * limitations under the License. */ -#include #include #include @@ -22,254 +21,292 @@ #include #include "confirm.h" -#include -using std::string; +#include #include #include +#include #include -using namespace std; -using google::protobuf::RepeatedPtrField; - -namespace impl { +namespace impl +{ // Large timeout value to be used when there's nothing to timeout #define LARGE_TIMEOUT_SECS (7 * 24 * 60 * 60) // Implements gNMI Confirm RPC -Status Confirm::run(const ConfirmRequest *request, ConfirmResponse *response) { - (void)request; // unused - (void)response; // unused - Status status; - - if (not conf_state_->get_wait_confirm()) { - // We are not expecting for a Confirm RPC - std::string err_str = "Not expecting Confirm RPC"; - return Status(StatusCode::FAILED_PRECONDITION, err_str); - } - // Now make sure enough time has elapsed - uint64_t earliest_confirm_time_nsecs = - conf_state_->get_earliest_confirm_time_nsecs(); - uint64_t crnt_time_ns = get_time_nanosec(); - if (crnt_time_ns < earliest_confirm_time_nsecs) { - std::string err_str = - "Confirm RPC too soon by " + - std::to_string(earliest_confirm_time_nsecs - crnt_time_ns) + " nsecs"; - return Status(StatusCode::UNAVAILABLE, err_str); - } - BOOST_LOG_TRIVIAL(debug) << "Ignore-system-state: " << request->ignore_system_state(); - if (not request->ignore_system_state()) { - // We have to check system state - } - - try { - sr_sess_startup_.copyConfig(sysrepo::Datastore::Running); - } catch (sysrepo::ErrorWithCode &e) { - BOOST_LOG_TRIVIAL(error) << "Copy from running config to startup config failed: " - << e.what() - << ". Transaction-id:" - << conf_state_->read_set_transaction_id(); - return Status(StatusCode::ABORTED, e.what()); - } - - // The last succesful set transaction has been confirmed - conf_state_->write_confirmed_transaction_id( - conf_state_->read_set_transaction_id()); - - // All good, clear state - conf_state_->clr_wait_confirm(); - return Status::OK; +grpc::Status Confirm::run(const gnmi::ConfirmRequest *request, gnmi::ConfirmResponse *response) +{ + (void)request; // unused + (void)response; // unused + grpc::Status status; + + if (not conf_state_->get_wait_confirm()) + { + // We are not expecting for a Confirm RPC + std::string err_str = "Not expecting Confirm RPC"; + return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, err_str); + } + // Now make sure enough time has elapsed + uint64_t earliest_confirm_time_nsecs = conf_state_->get_earliest_confirm_time_nsecs(); + uint64_t crnt_time_ns = get_time_nanosec(); + if (crnt_time_ns < earliest_confirm_time_nsecs) + { + std::string err_str = "Confirm RPC too soon by " + + std::to_string(earliest_confirm_time_nsecs - crnt_time_ns) + " nsecs"; + return grpc::Status(grpc::StatusCode::UNAVAILABLE, err_str); + } + BOOST_LOG_TRIVIAL(debug) << "Ignore-system-state: " << request->ignore_system_state(); + if (not request->ignore_system_state()) + { + // We have to check system state + } + + try + { + sr_sess_startup_.copyConfig(sysrepo::Datastore::Running); + } + catch (sysrepo::ErrorWithCode &e) + { + BOOST_LOG_TRIVIAL(error) << "Copy from running config to startup config failed: " + << e.what() + << ". Transaction-id:" << conf_state_->read_set_transaction_id(); + return grpc::Status(grpc::StatusCode::ABORTED, e.what()); + } + + // The last succesful set transaction has been confirmed + conf_state_->write_confirmed_transaction_id(conf_state_->read_set_transaction_id()); + + // All good, clear state + conf_state_->clr_wait_confirm(); + return grpc::Status::OK; } // Callback function for timer expiry -static void check_confirm_expiry_cb(const boost::system::error_code &e, ConfirmState *conf_state) { - if (conf_state->get_wait_confirm() and - (e != boost::asio::error::operation_aborted)) { - // Restore config only if the wait was not stopped - conf_state->restore_config(); - } +static void check_confirm_expiry_cb(const boost::system::error_code &e, ConfirmState *conf_state) +{ + if (conf_state->get_wait_confirm() and (e != boost::asio::error::operation_aborted)) + { + // Restore config only if the wait was not stopped + conf_state->restore_config(); + } } // Handles confirm timeout or failure by restoring config -void ConfirmState::restore_config() { - std::unique_lock lock(mutex_); - BOOST_LOG_TRIVIAL(error) << "Restoring config: no valid Confirm RPC received"; - if (cfg_snapshot_.has_value()) { - std::string cfg_snapshot_json = - cfg_snapshot_->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings).value(); - // Restore config - try { - sr_sess_.replaceConfig(std::nullopt, cfg_snapshot_.value()); - // Restore transcation-id - write_set_transaction_id(read_confirmed_transaction_id()); - } catch (const std::exception& e) { - // Yikes - BOOST_LOG_TRIVIAL(error) << e.what(); +void ConfirmState::restore_config() +{ + std::unique_lock lock(mutex_); + BOOST_LOG_TRIVIAL(error) << "Restoring config: no valid Confirm RPC received"; + if (cfg_snapshot_.has_value()) + { + std::string cfg_snapshot_json = + cfg_snapshot_->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) + .value(); + // Restore config + try + { + sr_sess_.replaceConfig(cfg_snapshot_.value()); + // Restore transcation-id + write_set_transaction_id(read_confirmed_transaction_id()); + } + catch (const std::exception &e) + { + // Yikes + BOOST_LOG_TRIVIAL(error) << e.what(); + } + // Not waiting for confirm anymore + clr_wait_confirm_no_lock_(); + } + else + { + BOOST_LOG_TRIVIAL(error) << "No config snapshot to restore"; } - // Not waiting for confirm anymore - clr_wait_confirm_no_lock_(); - } else { - BOOST_LOG_TRIVIAL(error) << "No config snapshot to restore"; - } } // Loop to check confirm timeout -void ConfirmState::check_confirm_loop(ConfirmState *conf_state) { - BOOST_LOG_TRIVIAL(debug) << "Commit confirm timer thread started"; - - while (not conf_state->timer_thread_exit_) { - boost::asio::deadline_timer timer( - conf_state->io_, - boost::posix_time::seconds(conf_state->get_timeout_secs())); - - timer.async_wait(boost::bind(check_confirm_expiry_cb, boost::asio::placeholders::error, conf_state)); - // This is blocking - conf_state->io_.run(); - // Reset - conf_state->io_.reset(); - } - - BOOST_LOG_TRIVIAL(debug) << "Commit confirm timer thread exited"; +void ConfirmState::check_confirm_loop(ConfirmState *conf_state) +{ + BOOST_LOG_TRIVIAL(debug) << "Commit confirm timer thread started"; + + while (not conf_state->timer_thread_exit_) + { + boost::asio::deadline_timer timer( + conf_state->io_, boost::posix_time::seconds(conf_state->get_timeout_secs())); + + timer.async_wait( + boost::bind(check_confirm_expiry_cb, boost::asio::placeholders::error, conf_state)); + // This is blocking + conf_state->io_.run(); + // Reset + conf_state->io_.reset(); + } + + BOOST_LOG_TRIVIAL(debug) << "Commit confirm timer thread exited"; } -ConfirmState* ConfirmState::singleton_ = nullptr; +ConfirmState *ConfirmState::singleton_ = nullptr; sysrepo::Session ConfirmState::createSession(sysrepo::Connection conn) { - try { + try + { return conn.sessionStart(); - } catch (const std::exception& exc) { + } + catch (const std::exception &exc) + { BOOST_LOG_TRIVIAL(error) << "Connection to sysrepo failed " << exc.what(); exit(1); } - BOOST_LOG_TRIVIAL(debug) << "Commit confirm timer thread exited"; + BOOST_LOG_TRIVIAL(debug) << "Commit confirm timer thread exited"; } // Constructor for ConfirmState -ConfirmState::ConfirmState(sysrepo::Connection conn) : sr_sess_(createSession(conn)) { - - reset_default_timeout_secs(); - reset_min_wait_conf_secs(); - timeout_secs_ = LARGE_TIMEOUT_SECS; - wait_confirm_ = false; - timer_thread_exit_ = false; - timer_thread_ = std::thread(check_confirm_loop, this); - singleton_ = this; +ConfirmState::ConfirmState(sysrepo::Connection conn) : sr_sess_(createSession(conn)) +{ + + reset_default_timeout_secs(); + reset_min_wait_conf_secs(); + timeout_secs_ = LARGE_TIMEOUT_SECS; + wait_confirm_ = false; + timer_thread_exit_ = false; + timer_thread_ = std::thread(check_confirm_loop, this); + singleton_ = this; } -ConfirmState::~ConfirmState() { - timer_thread_exit_ = true; - io_.stop(); - timer_thread_.join(); - singleton_ = nullptr; +ConfirmState::~ConfirmState() +{ + timer_thread_exit_ = true; + io_.stop(); + timer_thread_.join(); + singleton_ = nullptr; } -bool ConfirmState::get_wait_confirm() { - std::shared_lock lock(mutex_); - return wait_confirm_; +bool ConfirmState::get_wait_confirm() +{ + std::shared_lock lock(mutex_); + return wait_confirm_; } // Takes config snapshot, resets timer etc -bool ConfirmState::set_wait_confirm(uint32_t timeout_secs, std::string &err_msg) { - std::unique_lock lock(mutex_); - if (wait_confirm_) { - // Already waiting - err_msg = "Already waiting for Confirm RPC"; - BOOST_LOG_TRIVIAL(error) << err_msg; - return false; - } +bool ConfirmState::set_wait_confirm(uint32_t timeout_secs, std::string &err_msg) +{ + std::unique_lock lock(mutex_); + if (wait_confirm_) + { + // Already waiting + err_msg = "Already waiting for Confirm RPC"; + BOOST_LOG_TRIVIAL(error) << err_msg; + return false; + } - // Get snapshot of current config - cfg_snapshot_ = sr_sess_.getData("/*"); + // Get snapshot of current config + cfg_snapshot_ = sr_sess_.getData("/*"); - wait_confirm_ = true; + wait_confirm_ = true; - set_timeout_secs(timeout_secs); - reset_timers(); + set_timeout_secs(timeout_secs); + reset_timers(); - return true; + return true; } -void ConfirmState::reset_timers() { - // Earliest time at which Confirm is accepted is now + min wait time - auto crnt_time = get_time_nanosec(); - earliest_confirm_time_nsecs_ = - crnt_time + (static_cast(min_wait_conf_secs_) * 1000000000ull); - - // Stop the confirm timer so that it gets restarted - io_.stop(); +void ConfirmState::reset_timers() +{ + // Earliest time at which Confirm is accepted is now + min wait time + auto crnt_time = get_time_nanosec(); + earliest_confirm_time_nsecs_ = + crnt_time + (static_cast(min_wait_conf_secs_) * 1000000000ull); + + // Stop the confirm timer so that it gets restarted + io_.stop(); } -void ConfirmState::clr_wait_confirm() { - std::unique_lock lock(mutex_); - clr_wait_confirm_no_lock_(); +void ConfirmState::clr_wait_confirm() +{ + std::unique_lock lock(mutex_); + clr_wait_confirm_no_lock_(); } // Useful when caller already has lock -void ConfirmState::clr_wait_confirm_no_lock_() { - wait_confirm_ = false; - cfg_snapshot_ = std::nullopt; - timeout_secs_ = LARGE_TIMEOUT_SECS; - io_.stop(); +void ConfirmState::clr_wait_confirm_no_lock_() +{ + wait_confirm_ = false; + cfg_snapshot_ = std::nullopt; + timeout_secs_ = LARGE_TIMEOUT_SECS; + io_.stop(); } -uint32_t ConfirmState::get_timeout_secs() { - std::shared_lock lock(mutex_); +uint32_t ConfirmState::get_timeout_secs() +{ + std::shared_lock lock(mutex_); - return (timeout_secs_); + return (timeout_secs_); } -uint32_t ConfirmState::get_min_wait_conf_secs() { - std::shared_lock lock(mutex_); +uint32_t ConfirmState::get_min_wait_conf_secs() +{ + std::shared_lock lock(mutex_); - return (min_wait_conf_secs_); + return (min_wait_conf_secs_); } -void ConfirmState::set_default_timeout_secs(uint32_t value) { - std::unique_lock lock(mutex_); +void ConfirmState::set_default_timeout_secs(uint32_t value) +{ + std::unique_lock lock(mutex_); - default_timeout_secs_ = value; + default_timeout_secs_ = value; } -void ConfirmState::reset_default_timeout_secs() { - std::unique_lock lock(mutex_); +void ConfirmState::reset_default_timeout_secs() +{ + std::unique_lock lock(mutex_); - default_timeout_secs_ = 300; + default_timeout_secs_ = 300; } -void ConfirmState::set_min_wait_conf_secs(uint32_t value) { - std::unique_lock lock(mutex_); +void ConfirmState::set_min_wait_conf_secs(uint32_t value) +{ + std::unique_lock lock(mutex_); - min_wait_conf_secs_ = value; + min_wait_conf_secs_ = value; } -void ConfirmState::reset_min_wait_conf_secs() { - std::unique_lock lock(mutex_); +void ConfirmState::reset_min_wait_conf_secs() +{ + std::unique_lock lock(mutex_); - min_wait_conf_secs_ = 30; + min_wait_conf_secs_ = 30; } -void ConfirmState::set_timeout_secs(uint32_t timeout_secs) { - if ((timeout_secs == 0) or (timeout_secs < min_wait_conf_secs_)) { - // No value or too small value was provided, use default - timeout_secs_ = default_timeout_secs_; - } else { - timeout_secs_ = timeout_secs; - } +void ConfirmState::set_timeout_secs(uint32_t timeout_secs) +{ + if ((timeout_secs == 0) or (timeout_secs < min_wait_conf_secs_)) + { + // No value or too small value was provided, use default + timeout_secs_ = default_timeout_secs_; + } + else + { + timeout_secs_ = timeout_secs; + } } -uint64_t ConfirmState::get_earliest_confirm_time_nsecs() { - std::unique_lock lock(mutex_); +uint64_t ConfirmState::get_earliest_confirm_time_nsecs() +{ + std::unique_lock lock(mutex_); - return earliest_confirm_time_nsecs_; + return earliest_confirm_time_nsecs_; } -uint32_t ConfirmState::get_num_events_service_failures() { - return num_events_service_failures_; +uint32_t ConfirmState::get_num_events_service_failures() +{ + return num_events_service_failures_; } -uint64_t ConfirmState::read_set_transaction_id() { - return set_transaction_id_; +uint64_t ConfirmState::read_set_transaction_id() +{ + return set_transaction_id_; } -void ConfirmState::write_set_transaction_id(uint64_t id) { - BOOST_LOG_TRIVIAL(debug) << "write_set_transaction_id():" << id; - set_transaction_id_ = id; +void ConfirmState::write_set_transaction_id(uint64_t id) +{ + BOOST_LOG_TRIVIAL(debug) << "write_set_transaction_id():" << id; + set_transaction_id_ = id; } -uint64_t ConfirmState::read_confirmed_transaction_id() { - return confirmed_transaction_id_; +uint64_t ConfirmState::read_confirmed_transaction_id() +{ + return confirmed_transaction_id_; } -void ConfirmState::write_confirmed_transaction_id(uint64_t id) { - BOOST_LOG_TRIVIAL(debug) << "write_confirmed_transaction_id():" << id; +void ConfirmState::write_confirmed_transaction_id(uint64_t id) +{ + BOOST_LOG_TRIVIAL(debug) << "write_confirmed_transaction_id():" << id; - confirmed_transaction_id_ = id; + confirmed_transaction_id_ = id; } - } // namespace impl diff --git a/src/gnmi/confirm.h b/src/gnmi/confirm.h index 4847028..d48d1a4 100644 --- a/src/gnmi/confirm.h +++ b/src/gnmi/confirm.h @@ -14,119 +14,115 @@ * limitations under the License. */ -#ifndef _GNMI_CONFIRM_H -#define _GNMI_CONFIRM_H +#pragma once #include #include #include #include -#include -using namespace gnmi; -using google::protobuf::RepeatedPtrField; -using grpc::Status; -using grpc::StatusCode; -using sysrepo::Connection; -using sysrepo::Session; +#include +#include +#include -namespace impl { +namespace impl +{ // Class to manage the "state machine" for Confirm behaviour -class ConfirmState { -public: - ConfirmState(sysrepo::Connection conn); - ~ConfirmState(); +class ConfirmState +{ + public: + ConfirmState(sysrepo::Connection conn); + ~ConfirmState(); - // Singleton for UT purposes only - static ConfirmState &get_singleton() { - return *singleton_; - } - // Returns true if we are currently waiting for a confirm - bool get_wait_confirm(); - // Returns true on success, false on error (e.g. already waiting for a confirm) - bool set_wait_confirm(uint32_t timeout_secs, std::string &err_msg); - // Not waiting for confirm anymore - void clr_wait_confirm(); - // Reset the timers for Confirm (min/max) - void reset_timers(); - // Earliest time in ns, since epoch, to accept Confirm - // Relevant when wait_confirm_ is true - uint64_t get_earliest_confirm_time_nsecs(); + // Singleton for UT purposes only + static ConfirmState &get_singleton() { return *singleton_; } + // Returns true if we are currently waiting for a confirm + bool get_wait_confirm(); + // Returns true on success, false on error (e.g. already waiting for a confirm) + bool set_wait_confirm(uint32_t timeout_secs, std::string &err_msg); + // Not waiting for confirm anymore + void clr_wait_confirm(); + // Reset the timers for Confirm (min/max) + void reset_timers(); + // Earliest time in ns, since epoch, to accept Confirm + // Relevant when wait_confirm_ is true + uint64_t get_earliest_confirm_time_nsecs(); - uint32_t get_timeout_secs(); - uint32_t get_min_wait_conf_secs(); - // Gets the stored snapshot for this counter - uint32_t get_num_events_service_failures(); - // Handles Confirm timeout or failure by restoring the config - void restore_config(); - // Updates/gets the transaction ids - uint64_t read_set_transaction_id(); - void write_set_transaction_id(uint64_t id); - uint64_t read_confirmed_transaction_id(); - void write_confirmed_transaction_id(uint64_t id); - // Used for testing purposes - void set_default_timeout_secs(uint32_t value); - void reset_default_timeout_secs(); - void set_min_wait_conf_secs(uint32_t value); - void reset_min_wait_conf_secs(); - void set_timeout_secs(uint32_t value); -private: - // Whether we are waiting for a confirm RPC - bool wait_confirm_; - // For locking - std::shared_timed_mutex mutex_; - // For the timer - std::thread timer_thread_; - boost::asio::io_service io_; - bool timer_thread_exit_; - // Default timeout for Confirm (used when none specified) - uint32_t default_timeout_secs_; - // Timeout for Confirm - uint32_t timeout_secs_; - // Minimum time to wait before accepting a confirm - uint32_t min_wait_conf_secs_; - // Earliest time in ns, since epoch, to accept Confirm - // Relevant when wait_confirm_ is true - uint64_t earliest_confirm_time_nsecs_; - // session to sysrepo - sysrepo::Session sr_sess_; - // Config snapshot - std::optional cfg_snapshot_; - // Snapshot of number of times services have failed - uint32_t num_events_service_failures_; + uint32_t get_timeout_secs(); + uint32_t get_min_wait_conf_secs(); + // Gets the stored snapshot for this counter + uint32_t get_num_events_service_failures(); + // Handles Confirm timeout or failure by restoring the config + void restore_config(); + // Updates/gets the transaction ids + uint64_t read_set_transaction_id(); + void write_set_transaction_id(uint64_t id); + uint64_t read_confirmed_transaction_id(); + void write_confirmed_transaction_id(uint64_t id); + // Used for testing purposes + void set_default_timeout_secs(uint32_t value); + void reset_default_timeout_secs(); + void set_min_wait_conf_secs(uint32_t value); + void reset_min_wait_conf_secs(); + void set_timeout_secs(uint32_t value); - // On successful Set request, set_transaction_id is updated to request content - // On successful Confirm request, confirmed_transaction_id is updated to - // set_transaction_id. - // On failed Set request, no transaction-id is updated - // On Confirm timeout, when restoring config, set_transaction_id is reset - // to confirmed_transaction_id - uint64_t set_transaction_id_; - uint64_t confirmed_transaction_id_; + private: + // Whether we are waiting for a confirm RPC + bool wait_confirm_; + // For locking + std::shared_timed_mutex mutex_; + // For the timer + std::thread timer_thread_; + boost::asio::io_service io_; + bool timer_thread_exit_; + // Default timeout for Confirm (used when none specified) + uint32_t default_timeout_secs_; + // Timeout for Confirm + uint32_t timeout_secs_; + // Minimum time to wait before accepting a confirm + uint32_t min_wait_conf_secs_; + // Earliest time in ns, since epoch, to accept Confirm + // Relevant when wait_confirm_ is true + uint64_t earliest_confirm_time_nsecs_; + // session to sysrepo + sysrepo::Session sr_sess_; + // Config snapshot + std::optional cfg_snapshot_; + // Snapshot of number of times services have failed + uint32_t num_events_service_failures_; -private: - static void check_confirm_loop(ConfirmState *conf_state); - // Not waiting for confirm anymore - void clr_wait_confirm_no_lock_(); - sysrepo::Session createSession(sysrepo::Connection conn); - static ConfirmState *singleton_; + // On successful Set request, set_transaction_id is updated to request content + // On successful Confirm request, confirmed_transaction_id is updated to + // set_transaction_id. + // On failed Set request, no transaction-id is updated + // On Confirm timeout, when restoring config, set_transaction_id is reset + // to confirmed_transaction_id + uint64_t set_transaction_id_; + uint64_t confirmed_transaction_id_; + + private: + static void check_confirm_loop(ConfirmState *conf_state); + // Not waiting for confirm anymore + void clr_wait_confirm_no_lock_(); + sysrepo::Session createSession(sysrepo::Connection conn); + static ConfirmState *singleton_; }; -class Confirm { -public: - Confirm(sysrepo::Session startup_sess, std::shared_ptr conf_state) - : sr_sess_startup_(startup_sess), conf_state_(conf_state) - {} - ~Confirm() {} +class Confirm +{ + public: + Confirm(sysrepo::Session startup_sess, std::shared_ptr conf_state) + : sr_sess_startup_(startup_sess), conf_state_(conf_state) + { + } + ~Confirm() {} - Status run(const ConfirmRequest *req, ConfirmResponse *response); + grpc::Status run(const gnmi::ConfirmRequest *req, gnmi::ConfirmResponse *response); -private: - sysrepo::Session sr_sess_startup_; - std::shared_ptr conf_state_; + private: + sysrepo::Session sr_sess_startup_; + std::shared_ptr conf_state_; }; } // namespace impl - -#endif //_GNMI_CONFIRM_H diff --git a/src/gnmi/encode/encode.cpp b/src/gnmi/encode/encode.cpp index 581a159..d6214df 100644 --- a/src/gnmi/encode/encode.cpp +++ b/src/gnmi/encode/encode.cpp @@ -15,167 +15,106 @@ * limitations under the License. */ -#include #include "encode.h" #include "utils/log.h" #include "utils/sysrepo.h" +#include +#include -using namespace gnmi; -using namespace std; -using namespace grpc; -using Status = grpc::Status; - -std::tuple> Encode::decode( - string xpath, const gnmi::TypedValue &reqval, EncodePurpose purpose) +std::tuple> +Encode::decode(std::string xpath, const gnmi::TypedValue &reqval, EncodePurpose purpose) { - switch (reqval.value_case()) { + switch (reqval.value_case()) + { case gnmi::TypedValue::ValueCase::kStringVal: - return std::make_tuple(Status(StatusCode::UNIMPLEMENTED, "Unsupported protobuf string type"), std::nullopt); + return std::make_tuple( + grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Unsupported protobuf string type"), + std::nullopt); case gnmi::TypedValue::ValueCase::kIntVal: - return std::make_tuple(Status(StatusCode::UNIMPLEMENTED, "Unsupported protobuf int type"), std::nullopt); + return std::make_tuple( + grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Unsupported protobuf int type"), + std::nullopt); case gnmi::TypedValue::ValueCase::kUintVal: - return std::make_tuple(Status(StatusCode::UNIMPLEMENTED, "Unsupported protobuf uint type"), std::nullopt); + return std::make_tuple( + grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Unsupported protobuf uint type"), + std::nullopt); case gnmi::TypedValue::ValueCase::kBoolVal: - return std::make_tuple(Status(StatusCode::UNIMPLEMENTED, "Unsupported protobuf bool type"), std::nullopt); + return std::make_tuple( + grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Unsupported protobuf bool type"), + std::nullopt); case gnmi::TypedValue::ValueCase::kBytesVal: - return std::make_tuple(Status(StatusCode::UNIMPLEMENTED, "Unsupported protobuf bytes type"), std::nullopt); + return std::make_tuple( + grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Unsupported protobuf bytes type"), + std::nullopt); case gnmi::TypedValue::ValueCase::kFloatVal: - return std::make_tuple(Status(StatusCode::UNIMPLEMENTED, "Unsupported protobuf float type"), std::nullopt); + return std::make_tuple( + grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Unsupported protobuf float type"), + std::nullopt); case gnmi::TypedValue::ValueCase::kDecimalVal: - return std::make_tuple(Status(StatusCode::UNIMPLEMENTED, "Unsupported protobuf Decimal64 type"), std::nullopt); + return std::make_tuple( + grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Unsupported protobuf Decimal64 type"), + std::nullopt); case gnmi::TypedValue::ValueCase::kLeaflistVal: - return std::make_tuple(Status(StatusCode::UNIMPLEMENTED, "Unsupported protobuf leaflist type"), std::nullopt); + return std::make_tuple( + grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Unsupported protobuf leaflist type"), + std::nullopt); case gnmi::TypedValue::ValueCase::kAnyVal: - return std::make_tuple(Status(StatusCode::UNIMPLEMENTED, "Unsupported PROTOBUF Encoding"), std::nullopt); + return std::make_tuple( + grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Unsupported PROTOBUF Encoding"), + std::nullopt); case gnmi::TypedValue::ValueCase::kJsonVal: - return std::make_tuple(Status(StatusCode::UNIMPLEMENTED, "Unsupported JSON Encoding"), std::nullopt); + return std::make_tuple( + grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Unsupported JSON Encoding"), + std::nullopt); case gnmi::TypedValue::ValueCase::kJsonIetfVal: - try { - return std::make_tuple(Status::OK, json_decode(xpath, reqval.json_ietf_val(), purpose)); - } catch (runtime_error &err) { - // wrong input field must reply an error to gnmi client - BOOST_LOG_TRIVIAL(error) << "Run-time error:" << err.what(); - return std::make_tuple(Status(StatusCode::INVALID_ARGUMENT, err.what()), std::nullopt); - } catch (invalid_argument &err) { - BOOST_LOG_TRIVIAL(error) << "Invalid argument:" << err.what(); - return std::make_tuple(Status(StatusCode::INVALID_ARGUMENT, err.what()), std::nullopt); - } - break; + try + { + return std::make_tuple(grpc::Status::OK, + json_decode(xpath, reqval.json_ietf_val(), purpose)); + } + catch (std::runtime_error &err) + { + // wrong input field must reply an error to gnmi client + BOOST_LOG_TRIVIAL(error) << "Run-time error:" << err.what(); + return std::make_tuple(grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, err.what()), + std::nullopt); + } + catch (std::invalid_argument &err) + { + BOOST_LOG_TRIVIAL(error) << "Invalid argument:" << err.what(); + return std::make_tuple(grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, err.what()), + std::nullopt); + } + break; case gnmi::TypedValue::ValueCase::kAsciiVal: - return std::make_tuple(Status(StatusCode::UNIMPLEMENTED, "Unsupported ASCII Encoding"), std::nullopt); + return std::make_tuple( + grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Unsupported ASCII Encoding"), + std::nullopt); case gnmi::TypedValue::ValueCase::kProtoBytes: - return std::make_tuple(Status(StatusCode::UNIMPLEMENTED, "Unsupported PROTOBUF BYTE Encoding"), std::nullopt); + return std::make_tuple( + grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Unsupported PROTOBUF BYTE Encoding"), + std::nullopt); case gnmi::TypedValue::ValueCase::VALUE_NOT_SET: - return std::make_tuple(Status(StatusCode::INVALID_ARGUMENT, "Value not set"), std::nullopt); + return std::make_tuple(grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, "Value not set"), + std::nullopt); default: - return std::make_tuple(Status(StatusCode::INVALID_ARGUMENT, "Unknown value type"), std::nullopt); - } -} - -std::tuple> Encode::update(string xpath, const TypedValue &reqval, string op) -{ - UpdateTransaction xact; - - if (xpath.compare("/*") != 0 && op.compare("replace") == 0) { - // Check if the xpath we are replacing is a leaf-list or a list - auto node_type = sr_sess.getContext().findPath(xpath).nodeType(); - if (node_type == libyang::NodeType::Leaflist || node_type == libyang::NodeType::List) { - // Replacing list or leaflist means we should delete all previous entries - auto created_nodes = sr_sess.getContext().newPath2(xpath, std::nullopt, libyang::CreationOptions::Opaque); - auto del_parent = created_nodes.createdParent.value(); - auto del_node = created_nodes.createdNode.value(); - if (del_node.isOpaque()) { - del_node.newAttrOpaqueJSON("sysrepo", "operation", "purge"); - } else { - auto sr_mod = sr_sess.getContext().getModuleImplemented("sysrepo").value(); - // libyang treats NULL as a valid value for some data types - del_node.newMeta(sr_mod, "sysrepo:operation", "purge"); - } - xact.push(del_parent); + return std::make_tuple( + grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, "Unknown value type"), std::nullopt); } - } - - auto [status, node] = decode(xpath, reqval, EncodePurpose::Set); - if (!status.ok()) - return std::make_tuple(status, std::nullopt); - - auto root_node = node; - auto edit_node = node; - - auto ietf_nc_mod = sr_sess.getContext().getModuleImplemented("ietf-netconf").value(); - if (xpath.compare("/*") == 0) { - if (op.compare("replace") == 0) { - // The gNMI semantics are that a replace at the top-level should cause all data node not provided to be removed. - // However, sysrepo semantics are that only the provided nodes are replaced. Therefore, request that everything - // not being replaced is deleted. - - auto del_root = sr_sess.getData(xpath.c_str(), 1); - // Walk all siblings not in update and add delete node to them - for (auto n = std::optional(del_root); n.has_value(); n = n->nextSibling()) { - if (getRawNode(*n)->flags & LYD_DEFAULT) { - // Default nodes need not be deleted and can be skipped - continue; - } - - bool is_replace_node = false; - // Is this node a replace node? - for (auto repl_n = edit_node; repl_n.has_value(); repl_n = repl_n->nextSibling()) { - if (n->schema().path() == repl_n->schema().path()) { - is_replace_node = true; - break; - } - } - - // If this is a replace node, then optimise further sysrepo processing by not adding it to the batch - if (!is_replace_node) { - n->newMeta(ietf_nc_mod, "ietf-netconf:operation", "remove"); - xact.push_one(*n); - } - } - } - - // Add operation attribute to each node - there can be multiple if the JSON contains multiple top-level nodes. - for (auto n = edit_node; n.has_value(); n = n->nextSibling()) { - n->newMeta(ietf_nc_mod, "ietf-netconf:operation", op); - } - if (edit_node.has_value()) { - xact.push(edit_node.value()); - } - root_node = xact.first_node; - - } else { - // Find the edit point for the data fragment - auto set = root_node->findXPath(xpath.c_str()); - // We should have found a path, and wildcards don't make sense - if (set.empty()) { - BOOST_LOG_TRIVIAL(error) << "Empty result searching for " - << xpath.c_str(); - throw invalid_argument("invalid set returned for xpath \"" + xpath + "\""); - } - - for (auto edit_node : set) { - edit_node.newMeta(ietf_nc_mod, "ietf-netconf:operation", op); - BOOST_LOG_TRIVIAL(debug) << op.c_str() << " path: " << edit_node.path(); - } - xact.push(root_node.value()); - root_node = xact.first_node; - } - - return std::make_tuple(Status::OK, root_node); } -grpc::Status Encode::encode(Encoding encoding, libyang::DataNode node, TypedValue *val) +grpc::Status Encode::encode(gnmi::Encoding encoding, libyang::DataNode node, gnmi::TypedValue *val) { - switch (encoding) { + switch (encoding) + { case gnmi::JSON: case gnmi::JSON_IETF: - val->set_json_ietf_val(json_encode(node)); - break; + val->set_json_ietf_val(json_encode(node)); + break; default: - BOOST_LOG_TRIVIAL(warning) << "Unsupported Encoding " - << Encoding_Name(encoding); - return Status(StatusCode::UNIMPLEMENTED, Encoding_Name(encoding)); - } + BOOST_LOG_TRIVIAL(warning) << "Unsupported Encoding " << Encoding_Name(encoding); + return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, Encoding_Name(encoding)); + } - return Status::OK; + return grpc::Status::OK; } diff --git a/src/gnmi/encode/encode.h b/src/gnmi/encode/encode.h index d46b448..3e4be32 100644 --- a/src/gnmi/encode/encode.h +++ b/src/gnmi/encode/encode.h @@ -15,17 +15,13 @@ * limitations under the License. */ -#ifndef _ENCODE_H -#define _ENCODE_H +#pragma once +#include #include #include #include -using std::shared_ptr; -using std::string; -using std::vector; - /* * Encode directory aims at providing a CREATE-UPDATE-READ wrapper on top of * sysrepo for JSON encoding (other encodings can be added). @@ -40,18 +36,16 @@ using std::vector; */ /* helper class to reset session datastore on going out of scope */ -class SessionDsSwitcher { +class SessionDsSwitcher +{ public: - SessionDsSwitcher(sysrepo::Session sess, sysrepo::Datastore ds) - : sr_sess(sess) - { - orig_ds = sr_sess.activeDatastore(); - sr_sess.switchDatastore(ds); - } - ~SessionDsSwitcher() + SessionDsSwitcher(sysrepo::Session sess, sysrepo::Datastore ds) : sr_sess(sess) { - sr_sess.switchDatastore(orig_ds); + orig_ds = sr_sess.activeDatastore(); + sr_sess.switchDatastore(ds); } + ~SessionDsSwitcher() { sr_sess.switchDatastore(orig_ds); } + private: sysrepo::Session sr_sess; sysrepo::Datastore orig_ds; @@ -60,43 +54,61 @@ class SessionDsSwitcher { /* * Purpose for the encode/decode */ -enum class EncodePurpose { - Set, - Rpc, +enum class EncodePurpose +{ + Set, + Rpc, }; /* * Factory to instantiate encodings * Encoding can be {JSON, Bytes, Proto, ASCII, JSON_IETF} */ -class Encode { +class Encode +{ public: - Encode(sysrepo::Session sess) - : sr_sess(sess) + Encode(sysrepo::Session sess) : sr_sess(sess) {} + + void set_log_id(uint64_t id) { - } + // TODO doesnt work for now + // const char *originator = "sysrepo_gnxi"; + // struct sr_session_ctx_s *session = getRawSession(sr_sess); - void set_log_id(uint64_t id) { - log_id = id; - sr_session_set_nc_id(sysrepo::getRawSession(sr_sess), id); + /* store id */ + log_id = id; + + // if (!session) + // { + // return; + // } + + // if (!session->orig_name) + // { + // sr_session_set_orig_name(session, originator); + // } + + // /* Need to remove all previous data */ + // sr_session_del_orig_data(session); + // sr_session_push_orig_data(session, sizeof id, &id); } /* Supported Encodings */ - enum Supported { - JSON_IETF = 0, + enum Supported + { + JSON_IETF = 0, }; - std::tuple> decode(string xpath, const gnmi::TypedValue &reqval, EncodePurpose purpose); - std::tuple> update(string xpath, const gnmi::TypedValue &reqval, string op); + std::tuple> + decode(std::string xpath, const gnmi::TypedValue &reqval, EncodePurpose purpose); grpc::Status encode(gnmi::Encoding encoding, libyang::DataNode node, gnmi::TypedValue *val); /* JSON encoding */ - std::optional json_decode(string xpath, string data, EncodePurpose purpose); - string json_encode(libyang::DataNode node); + std::optional json_decode(std::string xpath, std::string data, + EncodePurpose purpose); + std::string json_encode(libyang::DataNode node); private: sysrepo::Session sr_sess; uint64_t log_id = 0; }; - -#endif //_ENCODE_H diff --git a/src/gnmi/encode/json_ietf.cpp b/src/gnmi/encode/json_ietf.cpp index 5799c75..0ffa1af 100644 --- a/src/gnmi/encode/json_ietf.cpp +++ b/src/gnmi/encode/json_ietf.cpp @@ -18,117 +18,142 @@ #include #include +#include #include #include -#include #include "encode.h" -using namespace std; -using namespace libyang; - - /***************** * CRUD - UPDATE * *****************/ -std::string stripQuotes(const std::string& str) { - if (str.front() == '\"' && str.back() == '\"') { - return str.substr(1, str.length() - 2); +std::string stripJSONObjectValue(const std::string &object) +{ + std::string result = object; + bool name_begin = false; + int index_start = 0; + + /* not a valid JSON object */ + if (object.front() != '{' || object.back() != '}') + { + BOOST_LOG_TRIVIAL(error) << "Unexpected input: JSON object does not have { or }"; + throw; + } + + /* strip the JSON object brackets */ + result = object.substr(1, object.size() - 2); + + /* go through the JSON object and find the delimiter ':' between name and value */ + for (size_t i = 0; i < result.length(); ++i) + { + switch (result.at(i)) + { + case '"': + /* beginning or ending of a name */ + name_begin = !name_begin; + break; + case ':': + /* skip all ':' inside of the name */ + if (!name_begin) + { + /* index of ':' between name and value */ + index_start = i; + } + break; + } + + /* we have found the ':' */ + if (index_start) + { + break; + } + } + + /* not a valid JSON object */ + if (!index_start) + { + BOOST_LOG_TRIVIAL(error) << "Unexpected input: JSON object does not have a :"; + throw; } - return str; + + return result.substr(index_start + 1); } /* * Parse a message encoded in JSON IETF and set fields in sysrepo. * @param data Input data encoded in JSON */ -std::optional Encode::json_decode(string xpath, string data, EncodePurpose purpose) +std::optional Encode::json_decode(std::string xpath, std::string data, + EncodePurpose purpose) { - // Request to fail if the data doesn't match the schema - auto metadata = "XPath: " + xpath + ". InputData"; - log_to_file(data, metadata, log_id); - - if (xpath.compare("/*") == 0) { - try { - auto ctx = sr_sess.getContext(); - return ctx.parseData(data, DataFormat::JSON, ParseOptions::ParseOnly | ParseOptions::Strict, std::nullopt); - } catch (const exception &exc) { - BOOST_LOG_TRIVIAL(error) << "Failed to parse data:" << obfs_data(data) - << ". Exception: " << exc.what(); - // Don't leave the error lying around on the context otherwise sysrepo may pick it up on an unrelated operation - auto ctx = sr_sess.getContext(); - const_cast(&ctx)->cleanAllErrors(); - throw invalid_argument(exc.what()); + // Request to fail if the data doesn't match the schema + auto metadata = "XPath: " + xpath + ". InputData"; + log_to_file(data, metadata, log_id); + + /* get request */ + // TODO "/*" is a bad sign that this is a set/get request for all data, rewrite it + if (xpath.compare("/*") == 0) + { + try + { + auto ctx = sr_sess.getContext(); + return ctx.parseData(data, libyang::DataFormat::JSON, + libyang::ParseOptions::ParseOnly | libyang::ParseOptions::Strict, + std::nullopt); + } + catch (const std::exception &exc) + { + BOOST_LOG_TRIVIAL(error) + << "Failed to parse data:" << obfs_data(data) << ". Exception: " << exc.what(); + // Don't leave the error lying around on the context otherwise sysrepo may pick it up on + // an unrelated operation + auto ctx = sr_sess.getContext(); + const_cast(&ctx)->cleanAllErrors(); + throw std::invalid_argument(exc.what()); + } } - } - - std::optional root_node; - - // Create a node tree according to the xpath. The data is passed in because libyang makes this mandatory for leaf - // nodes - it will be ignored for other node types (we cannot easily know what the node type is ahead of time). - try { - data = stripQuotes(data); - auto schema_node = sr_sess.getContext().findPath(xpath); - auto node_type = schema_node.nodeType(); - auto opts = CreationOptions::Update; - if (node_type == libyang::NodeType::Leaflist) { - opts = opts | CreationOptions::IgnoreInvalidValue; + // Create a node tree according to the xpath and data value. + try + { + auto ctx = sr_sess.getContext(); + std::optional node = std::nullopt; + switch (purpose) + { + case EncodePurpose::Set: + return ctx.parseValueFragment( + xpath, data, libyang::DataFormat::JSON, std::nullopt, + libyang::ParseOptions::JsonNull | libyang::ParseOptions::Strict, std::nullopt); + case EncodePurpose::Rpc: + // If xpath is not a path, this throws error (one node expected) + node = ctx.newPath2(xpath).createdNode; + if (node.has_value()) + { + node.value().parseOp(data.c_str(), libyang::DataFormat::JSON, + libyang::OperationType::RpcYang); + } + else + { + throw std::runtime_error("The node for the XPath has not been created."); + } + return node; + default: + throw std::runtime_error("Unsupported encode purpose."); + } } - - if (node_type == libyang::NodeType::Leaf) { - // For empty leaf, libyang expects "" and not "[null]" - auto base_type = schema_node.asLeaf().valueType().base(); - if (base_type == libyang::LeafBaseType::Empty && data == "[null]") { - data = ""; - } - } else { - // Only non-Leaf nodes can be opaque - opts = opts | CreationOptions::Opaque; + catch (const std::exception &exc) + { + BOOST_LOG_TRIVIAL(error) << "Failed to parse data. xpath: " << xpath + << ", data:" << obfs_data(data) << ". Exception: " << exc.what(); + // Don't leave the error lying around on the context otherwise sysrepo may pick it up on + // an unrelated operation + auto ctx = sr_sess.getContext(); + const_cast(&ctx)->cleanAllErrors(); + throw; } - auto created_nodes = sr_sess.getContext().newPath2(xpath, data, opts); - root_node = created_nodes.createdParent; - if (created_nodes.createdNode->schema().nodeType() == NodeType::Leaf) { - /* If it is a leaf node, we are done here */ - return root_node; - } - } catch (const exception &exc) { - BOOST_LOG_TRIVIAL(error) << "Failed to create node:" << xpath.c_str() - << "Exception: " << exc.what(); - // Don't leave the error lying around on the context otherwise sysrepo may pick it up on an unrelated operation - auto ctx = sr_sess.getContext(); - const_cast(&ctx)->cleanAllErrors(); - throw; - } - - // Now find the edit point for the data fragment - auto set = root_node->findXPath(xpath); - // We should have found a path, and wildcards don't make sense - if (set.size() != 1) - throw invalid_argument("invalid set returned for xpath \"" + xpath + "\""); - - auto edit_node = set.front(); - - try { - if (purpose == EncodePurpose::Rpc) { - edit_node.parseOp(data.c_str(), DataFormat::JSON, OperationType::RpcYang); - } else { - // Parse input JSON, expecting a fragment - edit_node.parseData(data.c_str(), DataFormat::JSON, ParseOptions::ParseOnly | ParseOptions::Strict | ParseOptions::BareTopLeaf); - } - } catch (const exception &exc) { - // Don't leave the error lying around on the context otherwise sysrepo may pick it up on an unrelated operation - auto ctx = sr_sess.getContext(); - const_cast(&ctx)->cleanAllErrors(); - BOOST_LOG_TRIVIAL(error) << "Failed to parse data. xpath: " << xpath - << ", data:" << obfs_data(data) - << ". Exception: " << exc.what(); - throw; - } - - return root_node; + return std::nullopt; } /*************** @@ -136,23 +161,35 @@ std::optional Encode::json_decode(string xpath, string data, ***************/ /* Encode a libyang data node into JSON form */ -string Encode::json_encode(libyang::DataNode node) +std::string Encode::json_encode(libyang::DataNode node) { - string data; - - if (node.schema().nodeType() == NodeType::Leaf) - data = node.printStr(DataFormat::JSON, PrintFlags::BareTopLeaf).value(); - else { - // In case the node has no children - data = "{}"; - // The xpath will have found the containing node, but we want to dump its children according to gNMI rules - if (node.child().has_value()) { - for (auto it : node.child()->childrenDfs()) { - data = it.printStr(DataFormat::JSON, PrintFlags::WithSiblings | PrintFlags::Shrink | PrintFlags::Fragment).value(); - break; - } + std::string data; + + if (node.schema().nodeType() == libyang::NodeType::Leaf) + { + data = stripJSONObjectValue( + node.printStr(libyang::DataFormat::JSON, + libyang::PrintFlags::Shrink | libyang::PrintFlags::JsonNoNestedPrefix) + .value()); + } + else + { + // In case the node has no children + data = "{}"; + // The xpath will have found the containing node, but we want to dump its children + // according to gNMI rules + if (node.child().has_value()) + { + for (auto it : node.child()->childrenDfs()) + { + data = it.printStr(libyang::DataFormat::JSON, + libyang::PrintFlags::Siblings | libyang::PrintFlags::Shrink | + libyang::PrintFlags::JsonNoNestedPrefix) + .value(); + break; + } + } } - } - return data; + return data; } diff --git a/src/gnmi/get.cpp b/src/gnmi/get.cpp index c4e4c84..f3936e5 100644 --- a/src/gnmi/get.cpp +++ b/src/gnmi/get.cpp @@ -17,48 +17,53 @@ #include -#include "get.h" #include "encode/encode.h" -#include -#include +#include "get.h" +#include #include +#include +#include -using namespace std; -using google::protobuf::RepeatedPtrField; - -namespace impl { +namespace impl +{ -Status -Get::BuildGetUpdate(RepeatedPtrField* updateList, - string fullpath, gnmi::Encoding encoding) +grpc::Status Get::BuildGetUpdate(google::protobuf::RepeatedPtrField *updateList, + const std::string &fullpath, gnmi::Encoding encoding) { - try { - /* Get multiple subtree for YANG lists or one for other YANG types */ - auto sr_trees = sr_sess.getData(fullpath.c_str()); - /* The path not (yet) existing isn't an error, so just return an empty set */ - if (!sr_trees.has_value()) - return Status::OK; - - for (auto n : sr_trees->findXPath(fullpath.c_str())) { - auto update = updateList->Add(); - xpath_to_gnmi(n.path(), *update->mutable_path()); - auto status = encodef->encode(encoding, n, update->mutable_val()); - if (!status.ok()) { + try + { + /* Get multiple subtree for YANG lists or one for other YANG types */ + auto sr_trees = sr_sess.getData(fullpath); + /* The path not (yet) existing isn't an error, so just return an empty set */ + if (!sr_trees.has_value()) + { + return grpc::Status::OK; + } + for (auto n : sr_trees->findXPath(fullpath)) + { + auto update = updateList->Add(); + xpath_to_gnmi(n.path(), *update->mutable_path()); + auto status = encodef->encode(encoding, n, update->mutable_val()); + if (!status.ok()) + { + updateList->Clear(); + return status; + } + } + } + catch (std::invalid_argument &exc) + { updateList->Clear(); - return status; - } + return grpc::Status(grpc::StatusCode::NOT_FOUND, exc.what()); } - } catch (invalid_argument &exc) { - updateList->Clear(); - return Status(StatusCode::NOT_FOUND, exc.what()); - } catch (sysrepo::ErrorWithCode &exc) { - BOOST_LOG_TRIVIAL(error) << "Fail getting items from sysrepo: " - << exc.what(); - updateList->Clear(); - return Status(StatusCode::INVALID_ARGUMENT, exc.what()); - } - - return Status::OK; + catch (sysrepo::ErrorWithCode &exc) + { + BOOST_LOG_TRIVIAL(error) << "Fail getting items from sysrepo: " << exc.what(); + updateList->Clear(); + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, exc.what()); + } + + return grpc::Status::OK; } /* @@ -68,123 +73,133 @@ Get::BuildGetUpdate(RepeatedPtrField* updateList, * There can still be multiple paths in GetResponse if requested path * is a directory path. * - * IMPORTANT : we have choosen to have a stateless implementation of + * IMPORTANT : we have chosen to have a stateless implementation of * gNMI so deleted path in Notification message will always be empty. */ -Status -Get::BuildGetNotification(Notification *notification, const Path &prefix, - const Path &path, gnmi::Encoding encoding, - gnmi::GetRequest_DataType dataType) +grpc::Status Get::BuildGetNotification(gnmi::Notification *notification, const gnmi::Path &prefix, + const gnmi::Path &path, gnmi::Encoding encoding, + gnmi::GetRequest_DataType dataType) { - /* Data elements that have changed values */ - RepeatedPtrField* updateList = notification->mutable_update(); - string fullpath = ""; - auto ds = sysrepo::Datastore::Operational; - - /* Get time since epoch in milliseconds */ - notification->set_timestamp(get_time_nanosec()); - - if (prefix.elem_size() > 0 || prefix.target().compare("")) { - string str; - try { - str = gnmi_to_xpath(prefix); - } catch (invalid_argument &exc) { - return Status(StatusCode::INVALID_ARGUMENT, exc.what()); - } - BOOST_LOG_TRIVIAL(debug) << "prefix is " << str; - // gNMI spec §2.2.2.1: - // When set in the prefix in a request, GetRequest, SetRequest or - // SubscribeRequest, the field MUST be reflected in the prefix of the - // corresponding GetResponse, SetResponse or SubscribeResponse by a - // server. - notification->mutable_prefix()->set_target(prefix.target()); - if (prefix.elem_size() > 0) { - fullpath += str; + /* Data elements that have changed values */ + google::protobuf::RepeatedPtrField *updateList = notification->mutable_update(); + std::string fullpath = ""; + auto ds = sysrepo::Datastore::Operational; + + /* Get time since epoch in milliseconds */ + notification->set_timestamp(get_time_nanosec()); + + if (prefix.elem_size() > 0 || prefix.target().compare("")) + { + std::string str; + try + { + str = gnmi_to_xpath(prefix); + } + catch (std::invalid_argument &exc) + { + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, exc.what()); + } + BOOST_LOG_TRIVIAL(debug) << "prefix is " << str; + // gNMI spec §2.2.2.1: + // When set in the prefix in a request, GetRequest, SetRequest or + // SubscribeRequest, the field MUST be reflected in the prefix of the + // corresponding GetResponse, SetResponse or SubscribeResponse by a + // server. + notification->mutable_prefix()->set_target(prefix.target()); + if (prefix.elem_size() > 0) + { + fullpath += str; + } } - } - try { - gnmi_check_origin(prefix, path); - fullpath += gnmi_to_xpath(path); - } catch (invalid_argument &exc) { - return Status(StatusCode::INVALID_ARGUMENT, exc.what()); - } - BOOST_LOG_TRIVIAL(debug) << "GetRequest Path " << fullpath; + try + { + gnmi_check_origin(prefix, path); + fullpath += gnmi_to_xpath(path); + } + catch (std::invalid_argument &exc) + { + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, exc.what()); + } + BOOST_LOG_TRIVIAL(debug) << "GetRequest Path " << fullpath; - if (dataType == gnmi::GetRequest_DataType_CONFIG) - ds = sysrepo::Datastore::Running; + if (dataType == gnmi::GetRequest_DataType_CONFIG) + ds = sysrepo::Datastore::Running; - SessionDsSwitcher ds_switch(sr_sess, ds); + SessionDsSwitcher ds_switch(sr_sess, ds); - return BuildGetUpdate(updateList, fullpath, encoding); + return BuildGetUpdate(updateList, fullpath, encoding); } /* Verify request fields are correct */ -static inline Status verifyGetRequest(const GetRequest *request) +static inline grpc::Status verifyGetRequest(const gnmi::GetRequest *request) { - switch (request->encoding()) { + switch (request->encoding()) + { case gnmi::JSON: case gnmi::JSON_IETF: - break; + break; default: - BOOST_LOG_TRIVIAL(warning) << "Unsupported Encoding " - << Encoding_Name(request->encoding()); - return Status(StatusCode::UNIMPLEMENTED, - Encoding_Name(request->encoding())); - } - - if (!GetRequest_DataType_IsValid(request->type())) { - BOOST_LOG_TRIVIAL(warning) << "Invalid Data Type in Get Request " - << GetRequest_DataType_Name(request->type()); - return Status(StatusCode::UNIMPLEMENTED, - GetRequest_DataType_Name(request->type())); - } - - if (request->use_models_size() > 0) { - BOOST_LOG_TRIVIAL(warning) << "use_models unsupported, ALL are used"; - return Status(StatusCode::UNIMPLEMENTED, "use_model feature unsupported"); - } - - if (request->extension_size() > 0) { - BOOST_LOG_TRIVIAL(warning) << "extension unsupported"; - return Status(StatusCode::UNIMPLEMENTED, "extension feature unsupported"); - } - - return Status::OK; + BOOST_LOG_TRIVIAL(warning) << "Unsupported Encoding " << Encoding_Name(request->encoding()); + return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, Encoding_Name(request->encoding())); + } + + if (!GetRequest_DataType_IsValid(request->type())) + { + BOOST_LOG_TRIVIAL(warning) << "Invalid Data Type in Get Request " + << gnmi::GetRequest_DataType_Name(request->type()); + return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, + gnmi::GetRequest_DataType_Name(request->type())); + } + + if (request->use_models_size() > 0) + { + BOOST_LOG_TRIVIAL(warning) << "use_models unsupported, ALL are used"; + return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "use_model feature unsupported"); + } + + if (request->extension_size() > 0) + { + BOOST_LOG_TRIVIAL(warning) << "extension unsupported"; + return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "extension feature unsupported"); + } + + return grpc::Status::OK; } /* Implement gNMI Get RPC */ -Status Get::run(const GetRequest* req, GetResponse* response) +grpc::Status Get::run(const gnmi::GetRequest *req, gnmi::GetResponse *response) { - RepeatedPtrField *notificationList; - Notification *notification; - Status status; - - status = verifyGetRequest(req); - if (!status.ok()) - return status; - - BOOST_LOG_TRIVIAL(debug) << "GetRequest DataType " - << GetRequest::DataType_Name(req->type()) << "," - << "GetRequest Encoding " - << Encoding_Name(req->encoding()); - - /* Run through all paths */ - notificationList = response->mutable_notification(); - for (auto path : req->path()) { - notification = notificationList->Add(); - - status = BuildGetNotification(notification, req->prefix(), path, - req->encoding(), req->type()); - if (!status.ok()) { - BOOST_LOG_TRIVIAL(error) << "Fail building get notification: " - << status.error_message(); - return status; + google::protobuf::RepeatedPtrField *notificationList; + gnmi::Notification *notification; + grpc::Status status; + + status = verifyGetRequest(req); + if (!status.ok()) + return status; + + BOOST_LOG_TRIVIAL(debug) << "GetRequest DataType " + << gnmi::GetRequest::DataType_Name(req->type()) << "," + << "GetRequest Encoding " << gnmi::Encoding_Name(req->encoding()); + + /* Run through all paths */ + notificationList = response->mutable_notification(); + for (auto path : req->path()) + { + notification = notificationList->Add(); + + status = + BuildGetNotification(notification, req->prefix(), path, req->encoding(), req->type()); + if (!status.ok()) + { + BOOST_LOG_TRIVIAL(error) + << "Fail building get notification: " << status.error_message(); + return status; + } } - } - return Status::OK; + return grpc::Status::OK; } -} +} // namespace impl diff --git a/src/gnmi/get.h b/src/gnmi/get.h index 0f1e93c..571a809 100644 --- a/src/gnmi/get.h +++ b/src/gnmi/get.h @@ -15,44 +15,33 @@ * limitations under the License. */ -#ifndef _GNMI_GET_H -#define _GNMI_GET_H +#pragma once +#include "encode/encode.h" #include - #include -#include "encode/encode.h" -using namespace gnmi; -using grpc::Status; -using grpc::StatusCode; -using google::protobuf::RepeatedPtrField; +namespace impl +{ -namespace impl { - -class Get { +class Get +{ public: - Get(sysrepo::Session sess) - : sr_sess(sess) - { - encodef = std::make_shared(sr_sess); - } + Get(sysrepo::Session sess) : sr_sess(sess) { encodef = std::make_shared(sr_sess); } ~Get() {} - Status run(const GetRequest* req, GetResponse* response); + grpc::Status run(const gnmi::GetRequest *req, gnmi::GetResponse *response); private: - Status BuildGetNotification(Notification *notification, const Path &prefix, - const Path &path, gnmi::Encoding encoding, - gnmi::GetRequest_DataType dataType); - Status BuildGetUpdate(RepeatedPtrField* updateList, - string fullpath, gnmi::Encoding encoding); + grpc::Status BuildGetNotification(gnmi::Notification *notification, const gnmi::Path &prefix, + const gnmi::Path &path, gnmi::Encoding encoding, + gnmi::GetRequest_DataType dataType); + grpc::Status BuildGetUpdate(google::protobuf::RepeatedPtrField *updateList, + const std::string &fullpath, gnmi::Encoding encoding); private: - sysrepo::Session sr_sess; //sysrepo session - shared_ptr encodef; //support for json ietf encoding + sysrepo::Session sr_sess; // sysrepo session + std::shared_ptr encodef; // support for json ietf encoding }; -} - -#endif //_GNMI_GET_H +} // namespace impl diff --git a/src/gnmi/gnmi.cpp b/src/gnmi/gnmi.cpp index 7967d87..fc3129e 100644 --- a/src/gnmi/gnmi.cpp +++ b/src/gnmi/gnmi.cpp @@ -17,93 +17,97 @@ #include "gnmi.h" +#include "confirm.h" #include "get.h" +#include "rpc.h" #include "set.h" #include "subscribe.h" -#include "confirm.h" -#include "rpc.h" static std::atomic shutting_down; // cache server contexts for TryCancel on shutting down -static std::set server_contexts; +static std::set server_contexts; static std::mutex server_context_mutex; -class ServerContextHolder { +class ServerContextHolder +{ public: - ServerContextHolder(ServerContext *ctx) : ctx(ctx) { - const std::lock_guard lock(server_context_mutex); - server_contexts.insert(ctx); + ServerContextHolder(grpc::ServerContext *ctx) : ctx(ctx) + { + const std::lock_guard lock(server_context_mutex); + server_contexts.insert(ctx); } - ~ServerContextHolder() { - const std::lock_guard lock(server_context_mutex); - server_contexts.erase(ctx); + ~ServerContextHolder() + { + const std::lock_guard lock(server_context_mutex); + server_contexts.erase(ctx); } + private: - ServerContext *ctx; + grpc::ServerContext *ctx; }; void GNMIService::TryCancelAll(void) { - const std::lock_guard lock(server_context_mutex); - // forbid any new subscriptions by indicating we are shutting down - shutting_down.store(true); - for (auto ctx : server_contexts) { - ctx->TryCancel(); - } - BOOST_LOG_TRIVIAL(debug) << "Sent cancellation to subscriptions"; + const std::lock_guard lock(server_context_mutex); + // forbid any new subscriptions by indicating we are shutting down + shutting_down.store(true); + for (auto ctx : server_contexts) + { + ctx->TryCancel(); + } + BOOST_LOG_TRIVIAL(debug) << "Sent cancellation to subscriptions"; } -Status GNMIService::Set(ServerContext *context, const SetRequest *request, - SetResponse *response) +grpc::Status GNMIService::Set(grpc::ServerContext *context, const gnmi::SetRequest *request, + gnmi::SetResponse *response) { - (void)context; - impl::Set rpc(sr_con.sessionStart(sysrepo::Datastore::Running), sr_con.sessionStart(sysrepo::Datastore::Startup), conf_state); - - return rpc.run(request, response); + (void)context; + impl::Set rpc(sr_con.sessionStart(sysrepo::Datastore::Startup), + sr_con.sessionStart(sysrepo::Datastore::Running), + sr_con.sessionStart(sysrepo::Datastore::Candidate), conf_state); + return rpc.run(request, response); } -Status GNMIService::Get(ServerContext *context, const GetRequest *request, - GetResponse *response) +grpc::Status GNMIService::Get(grpc::ServerContext *context, const gnmi::GetRequest *request, + gnmi::GetResponse *response) { - (void)context; - impl::Get rpc(sr_con.sessionStart(sysrepo::Datastore::Running)); - - return rpc.run(request, response); + (void)context; + impl::Get rpc(sr_con.sessionStart(sysrepo::Datastore::Running)); + return rpc.run(request, response); } -Status GNMIService::Subscribe(ServerContext *context, - ServerReaderWriter *stream) +grpc::Status GNMIService::Subscribe( + grpc::ServerContext *context, + grpc::ServerReaderWriter *stream) { - ServerContextHolder holder(context); - - // If we are shutting down don't start any new subscriptions - // as TryCancelAll will not be called after this. - if (shutting_down.load()) { - BOOST_LOG_TRIVIAL(debug) << "Subscribe is not possible as server is shutting down"; - return Status(StatusCode::UNAVAILABLE, string("Server is shutting down")); - } - - SubscribeRequest request; - impl::Subscribe rpc(sr_con.sessionStart(sysrepo::Datastore::Running)); + ServerContextHolder holder(context); + + // If we are shutting down don't start any new subscriptions + // as TryCancelAll will not be called after this. + if (shutting_down.load()) + { + BOOST_LOG_TRIVIAL(debug) << "Subscribe is not possible as server is shutting down"; + return grpc::Status(grpc::StatusCode::UNAVAILABLE, std::string("Server is shutting down")); + } - return rpc.run(context, stream); + gnmi::SubscribeRequest request; + impl::Subscribe rpc(sr_con.sessionStart(sysrepo::Datastore::Running)); + return rpc.run(context, stream); } -Status GNMIService::Confirm(ServerContext *context, const ConfirmRequest *request, - ConfirmResponse *response) +grpc::Status GNMIService::Confirm(grpc::ServerContext *context, const gnmi::ConfirmRequest *request, + gnmi::ConfirmResponse *response) { - (void)context; - impl::Confirm rpc(sr_con.sessionStart(sysrepo::Datastore::Startup), conf_state); - - return rpc.run(request, response); + (void)context; + impl::Confirm rpc(sr_con.sessionStart(sysrepo::Datastore::Startup), conf_state); + return rpc.run(request, response); } -Status GNMIService::Rpc(ServerContext *context, const RpcRequest *request, - RpcResponse *response) +grpc::Status GNMIService::Rpc(grpc::ServerContext *context, const gnmi::RpcRequest *request, + gnmi::RpcResponse *response) { - (void)context; - impl::Rpc rpc(sr_con.sessionStart(sysrepo::Datastore::Running)); - - return rpc.run(request, response); + (void)context; + impl::Rpc rpc(sr_con.sessionStart(sysrepo::Datastore::Running)); + return rpc.run(request, response); } diff --git a/src/gnmi/gnmi.h b/src/gnmi/gnmi.h index 78a83b4..7c7a042 100644 --- a/src/gnmi/gnmi.h +++ b/src/gnmi/gnmi.h @@ -15,65 +15,58 @@ * limitations under the License. */ -#ifndef _GNMI_SERVER_H -#define _GNMI_SERVER_H +#pragma once #include -#include #include -#include -#include +#include +#include -#include "encode/encode.h" #include "confirm.h" #include "utils/log.h" -using namespace grpc; -using namespace gnmi; +// UNUSED +// using google::protobuf::RepeatedPtrField; -using sysrepo::Session; -using sysrepo::Connection; -using google::protobuf::RepeatedPtrField; -using std::make_shared; - -class GNMIService final : public gNMI::Service +class GNMIService final : public gnmi::gNMI::Service { public: - GNMIService(sysrepo::Connection conn) : sr_con(conn) { - conf_state = make_shared(conn); + GNMIService(sysrepo::Connection conn) : sr_con(conn) + { + conf_state = std::make_shared(conn); } - ~GNMIService() {BOOST_LOG_TRIVIAL(info) << "Quitting GNMI Server"; } + ~GNMIService() { BOOST_LOG_TRIVIAL(info) << "Quitting GNMI Server"; } - Status Capabilities(ServerContext* context, - const CapabilityRequest* request, CapabilityResponse* response); + grpc::Status Capabilities(grpc::ServerContext *context, const gnmi::CapabilityRequest *request, + gnmi::CapabilityResponse *response); - Status Get(ServerContext* context, - const GetRequest* request, GetResponse* response); + grpc::Status Get(grpc::ServerContext *context, const gnmi::GetRequest *request, + gnmi::GetResponse *response); - Status Set(ServerContext* context, - const SetRequest* request, SetResponse* response); + grpc::Status Set(grpc::ServerContext *context, const gnmi::SetRequest *request, + gnmi::SetResponse *response); - Status Subscribe(ServerContext* context, - ServerReaderWriter* stream); + grpc::Status + Subscribe(grpc::ServerContext *context, + grpc::ServerReaderWriter *stream); - Status Confirm(ServerContext *context, - const ConfirmRequest *request, ConfirmResponse *response); + grpc::Status Confirm(grpc::ServerContext *context, const gnmi::ConfirmRequest *request, + gnmi::ConfirmResponse *response); - Status Rpc(ServerContext *context, - const RpcRequest *request, RpcResponse *response); + grpc::Status Rpc(grpc::ServerContext *context, const gnmi::RpcRequest *request, + gnmi::RpcResponse *response); static void TryCancelAll(void); private: - void ServerContextUpdate(ServerContext *ctx, bool add); - sysrepo::Connection sr_con; //sysrepo connection - shared_ptr conf_state; + // void ServerContextUpdate(grpc::ServerContext *ctx, bool add); UNUSED + sysrepo::Connection sr_con; // sysrepo connection + std::shared_ptr conf_state; }; -void RunServer(string bind_addr, shared_ptr cred, sysrepo::Connection sr_conn, std::promise ready = std::promise()); +void RunServer(std::string bind_addr, std::shared_ptr cred, + sysrepo::Connection sr_conn, std::promise ready = std::promise()); void SetupSignalHandler(bool daemon = true); - -#endif //_GNMI_SERVER_H diff --git a/src/gnmi/gnmi_server.cpp b/src/gnmi/gnmi_server.cpp index 9fd0970..0207850 100644 --- a/src/gnmi/gnmi_server.cpp +++ b/src/gnmi/gnmi_server.cpp @@ -20,100 +20,114 @@ #include #include -#include "gnmi/gnmi.h" -#include -#include +#include +#include -using namespace std; +#include "gnmi/gnmi.h" +#include "security/authentication.h" +#include "utils/log.h" -static struct { - unique_ptr server; - int pipefd[2]; +static struct +{ + std::unique_ptr server; + int pipefd[2]; } g_state; -extern "C" void signal_handler(int signum) { - if (write(g_state.pipefd[1], &signum, sizeof signum) < 0) { - exit(2); - } +extern "C" void signal_handler(int signum) +{ + if (write(g_state.pipefd[1], &signum, sizeof(signum)) < 0) + { + exit(2); + } } void SetupSignalHandler(bool is_daemon) { - // Set up the signal handler - if (pipe(g_state.pipefd) < 0) { - cerr << "Failed to create signal handler pipe " << strerror(errno) << endl; - exit(1); - } - - // Block all signals for the main thread and other new threads - sigset_t set; - sigfillset(&set); - pthread_sigmask(SIG_BLOCK, &set, NULL); - - // Register the signal handler - struct sigaction sa; - sa.sa_handler = &signal_handler; - sigemptyset(&sa.sa_mask); - sa.sa_flags = 0; - sigaction(SIGTERM, &sa, NULL); - - // tests can receive SIGINT from user to interrupt the tests under gdb. - // So, only register for SIGINT, if we are running in daemon mode - if (is_daemon) { - sigaction(SIGINT, &sa, NULL); - } + // Set up the signal handler + if (pipe(g_state.pipefd) < 0) + { + std::cerr << "Failed to create signal handler pipe " << strerror(errno) << std::endl; + exit(1); + } + + // Block all signals for the main thread and other new threads + sigset_t set; + sigfillset(&set); + pthread_sigmask(SIG_BLOCK, &set, NULL); + + // Register the signal handler + struct sigaction sa; + sa.sa_handler = &signal_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(SIGTERM, &sa, NULL); + + // tests can receive SIGINT from user to interrupt the tests under gdb. + // So, only register for SIGINT, if we are running in daemon mode + if (is_daemon) + { + sigaction(SIGINT, &sa, NULL); + } } static void wait_for_terminate(void) { - int signal = 0; + int signal = 0; - // UnBlock all signals for this thread - sigset_t set; - sigfillset(&set); - pthread_sigmask(SIG_UNBLOCK, &set, NULL); + // UnBlock all signals for this thread + sigset_t set; + sigfillset(&set); + pthread_sigmask(SIG_UNBLOCK, &set, NULL); - while (read(g_state.pipefd[0], &signal, sizeof signal) < 0) { - // ignore interrupted system call - } + while (read(g_state.pipefd[0], &signal, sizeof(signal)) < 0) + { + // ignore interrupted system call + } - BOOST_LOG_TRIVIAL(debug) << "Shutting down due to " << strsignal(signal) << " signal"; - GNMIService::TryCancelAll(); + BOOST_LOG_TRIVIAL(debug) << "Shutting down due to " << strsignal(signal) << " signal"; + GNMIService::TryCancelAll(); - g_state.server->Shutdown(); + g_state.server->Shutdown(); } -void RunServer(string bind_addr, shared_ptr cred, sysrepo::Connection sr_conn, std::promise ready) +void RunServer(std::string bind_addr, std::shared_ptr cred, + sysrepo::Connection sr_conn, std::promise ready) { - // Get log environment variable - get_log_env(); - - try { - GNMIService gnmi(sr_conn); //gNMI Service - - ServerBuilder builder; - builder.AddListeningPort(bind_addr, cred); - builder.RegisterService(&gnmi); - g_state.server = builder.BuildAndStart(); - ready.set_value(); - - if (g_state.server == nullptr) { - BOOST_LOG_TRIVIAL(error) << "Failed to build gRPC server"; - exit(1); + // Get log environment variable + get_log_env(); + + try + { + GNMIService gnmi(sr_conn); // gNMI Service + + grpc::ServerBuilder builder; + builder.AddListeningPort(bind_addr, cred); + builder.RegisterService(&gnmi); + g_state.server = builder.BuildAndStart(); + ready.set_value(); + + if (g_state.server == nullptr) + { + BOOST_LOG_TRIVIAL(error) << "Failed to build gRPC server"; + exit(1); + } + + if (bind_addr.find(":") == std::string::npos) + { + BOOST_LOG_TRIVIAL(info) << "Server listening on " << bind_addr << ":443"; + } + else + { + BOOST_LOG_TRIVIAL(info) << "Server listening on " << bind_addr; + } + + wait_for_terminate(); } - - if (bind_addr.find(":") == string::npos) { - BOOST_LOG_TRIVIAL(info) << "Server listening on " << bind_addr << ":443"; - } else { - BOOST_LOG_TRIVIAL(info) << "Server listening on " << bind_addr; + catch (sysrepo::ErrorWithCode &exc) + { + BOOST_LOG_TRIVIAL(error) << "Connection to sysrepo failed " << exc.what(); + exit(1); } - wait_for_terminate(); - } catch (sysrepo::ErrorWithCode &exc) { - BOOST_LOG_TRIVIAL(error) << "Connection to sysrepo failed " << exc.what(); - exit(1); - } - - BOOST_LOG_TRIVIAL(info) << "GNMI Server exited"; + BOOST_LOG_TRIVIAL(info) << "GNMI Server exited"; } - diff --git a/src/gnmi/rpc.cpp b/src/gnmi/rpc.cpp index c6bdf0e..5884c5f 100644 --- a/src/gnmi/rpc.cpp +++ b/src/gnmi/rpc.cpp @@ -15,66 +15,74 @@ */ #include +#include #include #include "rpc.h" #include #include -using namespace std; -using google::protobuf::RepeatedPtrField; -using grpc::StatusCode; -using namespace libyang; - -namespace impl { +namespace impl +{ // Implements gNMI Rpc RPC -grpc::Status Rpc::run(const RpcRequest *request, RpcResponse *response) { - try { - auto xpath = gnmi_to_xpath(request->path()); +grpc::Status Rpc::run(const gnmi::RpcRequest *request, gnmi::RpcResponse *response) +{ + try + { + auto xpath = gnmi_to_xpath(request->path()); - // Convert RPC call timeout from seconds. Use 2000ms as default (per SR_RPC_CB_TIMEOUT) - // Max timeout of 10000ms (ensure less than SR_MAIN_LOCK_TIMEOUT) - uint32_t timeout = request->timeout() ? request->timeout() * 1000 : 2000; - if (timeout > 10000) { - timeout = 10000; - } + // Convert RPC call timeout from seconds. Use 2000ms as default (per SR_RPC_CB_TIMEOUT) + // Max timeout of 10000ms (ensure less than SR_MAIN_LOCK_TIMEOUT) + uint32_t timeout = request->timeout() ? request->timeout() * 1000 : 2000; + if (timeout > 10000) + { + timeout = 10000; + } - BOOST_LOG_TRIVIAL(debug) << "Rpc RPC (" << xpath << ") timeout " << timeout / 1000 << "s"; + BOOST_LOG_TRIVIAL(debug) << "Rpc RPC (" << xpath << ") timeout " << timeout / 1000 << "s"; - auto [status, input_node] = encodef->decode(xpath, request->val(), EncodePurpose::Rpc); + auto [status, input_node] = encodef->decode(xpath, request->val(), EncodePurpose::Rpc); - if (!status.ok()) { - BOOST_LOG_TRIVIAL(warning) << "Rpc input value error: " << status.error_message(); - return status; - } + if (!status.ok()) + { + BOOST_LOG_TRIVIAL(warning) << "Rpc input value error: " << status.error_message(); + return status; + } - auto output_node = sr_sess.sendRPC(input_node.value(), std::chrono::milliseconds(timeout)); + auto output_node = sr_sess.sendRPC(input_node.value(), std::chrono::milliseconds(timeout)); - response->set_timestamp(get_time_nanosec()); - if (!output_node.has_value()) { - return grpc::Status::OK; - } + response->set_timestamp(get_time_nanosec()); + if (!output_node.has_value()) + { + return grpc::Status::OK; + } - status = encodef->encode(request->encoding(), *output_node, response->mutable_val()); - if (!status.ok()) - BOOST_LOG_TRIVIAL(warning) << "Rpc output value error: " << status.error_message(); + status = encodef->encode(request->encoding(), *output_node, response->mutable_val()); + if (!status.ok()) + BOOST_LOG_TRIVIAL(warning) << "Rpc output value error: " << status.error_message(); - return status; - - } catch (invalid_argument &exc) { - return grpc::Status(StatusCode::INVALID_ARGUMENT, exc.what()); - } catch (std::exception& ex) { - string err_str; - auto errors = sr_sess.getErrors(); - if (!errors.empty()) { - err_str = errors[0].errorMessage; - } else { - err_str = ex.what(); + return status; + } + catch (std::invalid_argument &exc) + { + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, exc.what()); + } + catch (std::exception &ex) + { + std::string err_str; + auto errors = sr_sess.getErrors(); + if (!errors.empty()) + { + err_str = errors[0].errorMessage; + } + else + { + err_str = ex.what(); + } + BOOST_LOG_TRIVIAL(warning) << "RPC error: " << err_str; + return grpc::Status(grpc::StatusCode::ABORTED, err_str); } - BOOST_LOG_TRIVIAL(warning) << "RPC error: " << err_str; - return grpc::Status(StatusCode::ABORTED, err_str); - } } } // namespace impl diff --git a/src/gnmi/rpc.h b/src/gnmi/rpc.h index c2f35c2..40baab8 100644 --- a/src/gnmi/rpc.h +++ b/src/gnmi/rpc.h @@ -14,36 +14,27 @@ * limitations under the License. */ -#ifndef _GNMI_RPC_H -#define _GNMI_RPC_H +#pragma once #include #include #include "encode/encode.h" -using namespace gnmi; -using google::protobuf::RepeatedPtrField; -using grpc::Status; +namespace impl +{ -namespace impl { +class Rpc +{ + public: + Rpc(sysrepo::Session sess) : sr_sess(sess) { encodef = std::make_shared(sr_sess); } + ~Rpc() {} -class Rpc { -public: - Rpc(sysrepo::Session sess) - : sr_sess(sess) - { - encodef = std::make_shared(sr_sess); - } - ~Rpc() {} - - grpc::Status run(const RpcRequest *req, RpcResponse *response); + grpc::Status run(const gnmi::RpcRequest *req, gnmi::RpcResponse *response); private: - sysrepo::Session sr_sess; //sysrepo session - shared_ptr encodef; //support for json ietf encoding + sysrepo::Session sr_sess; // sysrepo session + std::shared_ptr encodef; // support for json ietf encoding }; } // namespace impl - -#endif //_GNMI_RPC_H diff --git a/src/gnmi/set.cpp b/src/gnmi/set.cpp index 5a0686f..98b15d0 100644 --- a/src/gnmi/set.cpp +++ b/src/gnmi/set.cpp @@ -18,306 +18,473 @@ #include "set.h" #include "confirm.h" #include "encode/encode.h" -#include +#include +#include +#include #include #include -#include -#include - -using namespace sysrepo; -using namespace std; -using namespace libyang; +#include -namespace impl { +namespace impl +{ -std::tuple> Set::handleUpdate(Update in, UpdateResult *out, string prefix_str, const Path &prefix, string op) +grpc::Status Set::handleUpdate(gnmi::Update in, gnmi::UpdateResult *out, std::string prefix_str, + const gnmi::Path &prefix, std::string op) { - //Parse request - if (!in.has_path() || !in.has_val()) - return std::make_tuple(grpc::Status(StatusCode::INVALID_ARGUMENT, "Update no path or value"), std::nullopt); - - string fullpath; - try { - gnmi_check_origin(prefix, in.path()); - if (prefix.elem_size() > 0) { - fullpath += prefix_str; + // Parse request + if (!in.has_path() || !in.has_val()) + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, "Update no path or value"); + + std::string fullpath; + try + { + gnmi_check_origin(prefix, in.path()); + if (prefix.elem_size() > 0) + { + fullpath += prefix_str; + } + fullpath += gnmi_to_xpath(in.path()); + } + catch (std::invalid_argument &exc) + { + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, exc.what()); + } + BOOST_LOG_TRIVIAL(debug) << "Update (" << op << ") " << fullpath; + + if (fullpath.compare("/*") != 0 && op.compare("replace") == 0) + { + // Check if the xpath we are replacing is a leaf-list or a list + auto node_type = sr_sess.getContext().findPath(fullpath).nodeType(); + if (node_type == libyang::NodeType::Leaflist || node_type == libyang::NodeType::List) + { + // Replacing list or leaflist means we should delete all previous entries + auto created_nodes = sr_sess.getContext().newPath2(fullpath, std::nullopt, + libyang::CreationOptions::Opaque); + auto del_node = created_nodes.createdNode; + auto sr_mod = sr_sess.getContext().getModuleImplemented("sysrepo").value(); + + auto parent = del_node; + auto check = del_node->parent(); + while (check.has_value()) + { + parent = check; + check->newMeta(sr_mod, "sysrepo:operation", "ether"); + check = check->parent(); + } + + if (del_node->isOpaque()) + { + del_node->newAttrOpaqueJSON("sysrepo", "operation", "purge"); + } + else + { + // libyang treats NULL as a valid value for some data types + del_node->newMeta(sr_mod, "sysrepo:operation", "purge"); + } + + xact.merge(purgeTree, parent); + } } - fullpath += gnmi_to_xpath(in.path()); - } catch (invalid_argument &exc) { - return std::make_tuple(grpc::Status(StatusCode::INVALID_ARGUMENT, exc.what()), std::nullopt); - } - BOOST_LOG_TRIVIAL(debug) << "Update (" << op << ") " << fullpath; - auto result = encodef->update(fullpath, in.val(), op); + auto [status, top_level] = encodef->decode(fullpath, in.val(), EncodePurpose::Set); + + if (!status.ok()) + return status; + + auto ietf_nc_mod = sr_sess.getContext().getModuleImplemented("ietf-netconf").value(); + if (fullpath.compare("/*") == 0) + { + if (op.compare("replace") == 0) + { + // The gNMI semantics are that a replace at the top-level should cause all data node not + // provided to be removed. However, sysrepo semantics are that only the provided nodes + // are replaced. Therefore, request that everything not being replaced is deleted. + + auto del_root = sr_sess.getData(fullpath.c_str(), 1); + // Walk all siblings not in update and add delete node to them + for (auto n = std::optional(del_root); n.has_value(); + n = n->nextSibling()) + { + // Default flags are unreliable since getData() retrieves only a subset of the + // datastore + // if (getRawNode(*n)->flags & LYD_DEFAULT) + // { + // // Default nodes need not be deleted and can be skipped + // continue; + // } + + bool is_replace_node = false; + // Is this node a replace node? + for (auto repl_n = top_level; repl_n.has_value(); repl_n = repl_n->nextSibling()) + { + if (n->schema().path() == repl_n->schema().path()) + { + is_replace_node = true; + break; + } + } + + // If this is a replace node, then optimise further sysrepo processing by not adding + // it to the batch + if (!is_replace_node) + { + n->newMeta(ietf_nc_mod, "ietf-netconf:operation", "remove"); + xact.merge(deleteTree, n); + } + } + } - //Fill in Reponse - out->set_allocated_path(in.release_path()); + // Add operation attribute to each node - there can be multiple if the JSON contains + // multiple top-level nodes. + for (auto n = top_level; n.has_value(); n = n->nextSibling()) + { + n->newMeta(ietf_nc_mod, "ietf-netconf:operation", op); + } - return result; + if (op.compare("replace") == 0) + { + xact.merge(replaceTree, top_level); + } + else if (op.compare("merge") == 0) + { + xact.merge(updateTree, top_level); + } + } + else + { + // Find the edit point for the data fragment + auto set = top_level->findXPath(fullpath.c_str()); + // We should have found a path, and wildcards don't make sense + if (set.empty()) + { + BOOST_LOG_TRIVIAL(error) << "Empty result searching for " << fullpath.c_str(); + throw std::invalid_argument("Invalid set returned for xpath \"" + fullpath + "\""); + } + + for (auto edit_node : set) + { + edit_node.newMeta(ietf_nc_mod, "ietf-netconf:operation", op); + BOOST_LOG_TRIVIAL(debug) << op.c_str() << " path: " << edit_node.path(); + } + + if (op.compare("replace") == 0) + { + xact.merge(replaceTree, top_level); + } + else if (op.compare("merge") == 0) + { + xact.merge(updateTree, top_level); + } + } + + // Fill in Response + out->set_allocated_path(in.release_path()); + + return status; } -grpc::Status Set::run(const SetRequest* request, SetResponse* response) +grpc::Status Set::run(const gnmi::SetRequest *request, gnmi::SetResponse *response) { - std::string prefix = ""; - std::vector results; - UpdateTransaction xact; - auto ietf_nc_mod = sr_sess.getContext().getModuleImplemented("ietf-netconf").value(); - - // For observability, set transaction_id as log_id for subscribers and gnmi logging. - encodef->set_log_id(request->transaction_id()); - - // Check if we're waiting for a Confirm RPC - if (conf_state->get_wait_confirm()) { - return grpc::Status(StatusCode::UNAVAILABLE, "Previous Set has to be confirmed"); - } - - // Check if Set requires Confirm - if (request->has_confirm()) { - const ConfirmParmsRequest &conf_parms = request->confirm(); - BOOST_LOG_TRIVIAL(debug) - << "Confirm msg has timeout=" << conf_parms.timeout_secs(); - BOOST_LOG_TRIVIAL(debug) - << "Confirm msg has ignore-system-state: " << conf_parms.ignore_system_state(); - - if (not conf_parms.ignore_system_state()) { - // We have to check system state + std::string prefix = ""; + std::vector results; + auto ietf_nc_mod = sr_sess.getContext().getModuleImplemented("ietf-netconf").value(); + + // For observability, set transaction_id as log_id for subscribers and gnmi logging. + encodef->set_log_id(request->transaction_id()); + + // Check if we're waiting for a Confirm RPC + if (conf_state->get_wait_confirm()) + { + return grpc::Status(grpc::StatusCode::UNAVAILABLE, "Previous Set has to be confirmed"); } - // This (re)starts the timer, so as long as work here lasts less than - // timeout... - std::string err_msg = ""; - if (not conf_state->set_wait_confirm(conf_parms.timeout_secs(), err_msg)) { - // Because of check above, should happen only in race condition - return grpc::Status(StatusCode::UNAVAILABLE, err_msg); + // Check if Set requires Confirm + if (request->has_confirm()) + { + const gnmi::ConfirmParmsRequest &conf_parms = request->confirm(); + BOOST_LOG_TRIVIAL(debug) << "Confirm msg has timeout=" << conf_parms.timeout_secs(); + BOOST_LOG_TRIVIAL(debug) << "Confirm msg has ignore-system-state: " + << conf_parms.ignore_system_state(); + + if (not conf_parms.ignore_system_state()) + { + // We have to check system state + } + + // This (re)starts the timer, so as long as work here lasts less than + // timeout... + std::string err_msg = ""; + if (not conf_state->set_wait_confirm(conf_parms.timeout_secs(), err_msg)) + { + // Because of check above, should happen only in race condition + return grpc::Status(grpc::StatusCode::UNAVAILABLE, err_msg); + } + + // Add ConfirmParmsResponse in SetResponse + gnmi::ConfirmParmsResponse confirm; + confirm.set_min_wait_secs(conf_state->get_min_wait_conf_secs()); + confirm.set_timeout_secs(conf_state->get_timeout_secs()); + response->mutable_confirm()->CopyFrom(confirm); } - // Add ConfirmParmsResponse in SetResponse - ConfirmParmsResponse confirm; - confirm.set_min_wait_secs(conf_state->get_min_wait_conf_secs()); - confirm.set_timeout_secs(conf_state->get_timeout_secs()); - response->mutable_confirm()->CopyFrom(confirm); - - } - - if (request->extension_size() > 0) { - conf_state->clr_wait_confirm(); - return grpc::Status(StatusCode::UNIMPLEMENTED, "not supported"); - } - - response->set_timestamp(get_time_nanosec()); - - /* Prefix for gNMI path */ - if (request->has_prefix()) { - try { - prefix = gnmi_to_xpath(request->prefix()); - } catch (invalid_argument &exc) { - BOOST_LOG_TRIVIAL(error) << exc.what() - << ". Transaction-id:" - << request->transaction_id(); - conf_state->clr_wait_confirm(); - return grpc::Status(StatusCode::INVALID_ARGUMENT, exc.what()); + if (request->extension_size() > 0) + { + conf_state->clr_wait_confirm(); + return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "not supported"); } - BOOST_LOG_TRIVIAL(debug) << "prefix is " << prefix; - response->mutable_prefix()->CopyFrom(request->prefix()); - } - - /* gNMI paths to delete */ - if (request->delete__size() > 0) { - - // sort the paths to delete in reverse order to not delete children after parents - std::set> del_paths; - for (auto delpath : request->delete_()) { - // Parse request and config sysrepo - string fullpath; - try { - gnmi_check_origin(request->prefix(), delpath); - fullpath = prefix + gnmi_to_xpath(delpath); - del_paths.insert(fullpath); - } catch (invalid_argument &exc) { - BOOST_LOG_TRIVIAL(error) << exc.what() - << ". Transaction-id:" - << request->transaction_id(); - conf_state->clr_wait_confirm(); - return grpc::Status(StatusCode::INVALID_ARGUMENT, exc.what()); - } - - // Fill in Reponse - UpdateResult res; - *(res.mutable_path()) = delpath; - res.set_op(gnmi::UpdateResult::DELETE); - results.push_back(res); + + response->set_timestamp(get_time_nanosec()); + + /* Prefix for gNMI path */ + if (request->has_prefix()) + { + try + { + prefix = gnmi_to_xpath(request->prefix()); + } + catch (std::invalid_argument &exc) + { + BOOST_LOG_TRIVIAL(error) + << exc.what() << ". Transaction-id:" << request->transaction_id(); + conf_state->clr_wait_confirm(); + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, exc.what()); + } + BOOST_LOG_TRIVIAL(debug) << "prefix is " << prefix; + response->mutable_prefix()->CopyFrom(request->prefix()); } - for (auto &fullpath : del_paths) { - BOOST_LOG_TRIVIAL(debug) << "Delete " << fullpath; - try { - // We cannot use deleteItem here as sysrepo doesn't like it being - // mixed with edit_batch, so retrieve just the nodes referenced by - // the xpath and no deeper to avoid it being any more expensive - // than it has to be - auto del_root = sr_sess.getData(fullpath, 1); - if (!del_root.has_value()) - throw invalid_argument("xpath \"" + fullpath + "\" not found"); - - if (fullpath.compare("/*") == 0) { - // Walk all siblings and add delete node to them - for (auto n : del_root->siblings()) { - n.newMeta(ietf_nc_mod, "ietf-netconf:operation", "remove"); - - BOOST_LOG_TRIVIAL(debug) << " 1. Delete path: " << n.path(); - } - } else { - // Find the node(s) actually referenced by the path to mark them as - // requiring delete since they could well be deeper than the root - // node - auto set = del_root->findXPath(fullpath.c_str()); - if (set.empty()) - throw invalid_argument("xpath \"" + fullpath + "\" not found"); - - auto sr_mod = sr_sess.getContext().getModuleImplemented("sysrepo").value(); - for (auto n : set) { - // Ensure we don't create any parent nodes - they might be deleted - // in this transaction. - auto p = n.parent(); - while (p.has_value()) { - p->newMeta(sr_mod, "sysrepo:operation", "ether"); - p = p->parent(); - } - - n.newMeta(ietf_nc_mod, "ietf-netconf:operation", "remove"); - BOOST_LOG_TRIVIAL(debug) << " 2. Delete path: " << n.path(); - } + /* gNMI paths to delete */ + if (request->delete__size() > 0) + { + // sort the paths to delete in order from parent to child (so that operations are + // successfuly merged) + std::set> del_paths; + for (auto delpath : request->delete_()) + { + // Parse request and config sysrepo + std::string fullpath; + try + { + gnmi_check_origin(request->prefix(), delpath); + fullpath = prefix + gnmi_to_xpath(delpath); + del_paths.insert(fullpath); + } + catch (std::invalid_argument &exc) + { + BOOST_LOG_TRIVIAL(error) + << exc.what() << ". Transaction-id:" << request->transaction_id(); + conf_state->clr_wait_confirm(); + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, exc.what()); + } + + // Fill in Reponse + gnmi::UpdateResult res; + *(res.mutable_path()) = delpath; + res.set_op(gnmi::UpdateResult::DELETE); + results.push_back(res); } - xact.push(*del_root); - } catch (const invalid_argument &exc) { - BOOST_LOG_TRIVIAL(error) << exc.what(); - // gNMI spec §3.4.6: In the case that a path specifies an element within the data tree that does not exist, these deletes MUST be silently accepted. - } catch (const exception &exc) { - BOOST_LOG_TRIVIAL(error) << exc.what() - << ". Transaction-id:" - << request->transaction_id(); - conf_state->clr_wait_confirm(); - return grpc::Status(StatusCode::INVALID_ARGUMENT, exc.what()); - } - } - } - - /* gNMI paths with value to replace */ - if (request->replace_size() > 0) { - for (auto &upd : request->replace()) { - UpdateResult res; - try { - auto [status, node] = handleUpdate(upd, &res, prefix, request->prefix(), "replace"); - if (!status.ok()) { - BOOST_LOG_TRIVIAL(error) << "Fail building set notification: " - << status.error_message() - << ". Transaction-id: " - << request->transaction_id(); - conf_state->clr_wait_confirm(); - return status; + + for (auto &fullpath : del_paths) + { + BOOST_LOG_TRIVIAL(debug) << "Delete " << fullpath; + try + { + // We cannot use deleteItem here as sysrepo doesn't like it being + // mixed with edit_batch, so retrieve just the nodes referenced by + // the xpath and no deeper to avoid it being any more expensive + // than it has to be + auto del_root = sr_sess.getData(fullpath, 0); + if (!del_root.has_value()) + throw std::invalid_argument("xpath \"" + fullpath + "\" not found"); + + if (fullpath.compare("/*") == 0) + { + // Walk all siblings and add delete node to them + for (auto n : del_root->siblings()) + { + n.newMeta(ietf_nc_mod, "ietf-netconf:operation", "remove"); + + BOOST_LOG_TRIVIAL(debug) << " 1. Delete path: " << n.path(); + } + } + else + { + // Find the node(s) actually referenced by the path to mark them as + // requiring delete since they could well be deeper than the root + // node + auto set = del_root->findXPath(fullpath.c_str()); + if (set.empty()) + throw std::invalid_argument("xpath \"" + fullpath + "\" not found"); + + auto sr_mod = sr_sess.getContext().getModuleImplemented("sysrepo").value(); + for (auto n : set) + { + // Ensure we don't create any parent nodes - they might be deleted + // in this transaction. + auto p = n.parent(); + while (p.has_value()) + { + p->newMeta(sr_mod, "sysrepo:operation", "ether"); + p = p->parent(); + } + + n.newMeta(ietf_nc_mod, "ietf-netconf:operation", "remove"); + BOOST_LOG_TRIVIAL(debug) << " 2. Delete path: " << n.path(); + } + } + xact.merge(deleteTree, del_root); + } + catch (const std::invalid_argument &exc) + { + BOOST_LOG_TRIVIAL(error) << exc.what(); + // gNMI spec §3.4.6: In the case that a path specifies an element within the data + // tree that does not exist, these deletes MUST be silently accepted. + } + catch (const std::exception &exc) + { + BOOST_LOG_TRIVIAL(error) + << exc.what() << ". Transaction-id:" << request->transaction_id(); + conf_state->clr_wait_confirm(); + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, exc.what()); + } } + } - res.set_op(gnmi::UpdateResult::REPLACE); - results.push_back(res); - if (node.has_value()) { - xact.push(*node); + /* gNMI paths with value to replace */ + if (request->replace_size() > 0) + { + for (auto &upd : request->replace()) + { + gnmi::UpdateResult res; + try + { + auto status = handleUpdate(upd, &res, prefix, request->prefix(), "replace"); + if (!status.ok()) + { + BOOST_LOG_TRIVIAL(error) + << "Fail building set notification: " << status.error_message() + << ". Transaction-id: " << request->transaction_id(); + conf_state->clr_wait_confirm(); + return status; + } + + res.set_op(gnmi::UpdateResult::REPLACE); + results.push_back(res); + } + catch (const std::invalid_argument &exc) + { + BOOST_LOG_TRIVIAL(error) + << exc.what() << ". Transaction-id:" << request->transaction_id(); + conf_state->clr_wait_confirm(); + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, exc.what()); + } + catch (sysrepo::Error &exc) + { + BOOST_LOG_TRIVIAL(error) + << exc.what() << ". Transaction-id:" << request->transaction_id(); + conf_state->clr_wait_confirm(); + return grpc::Status(grpc::StatusCode::INTERNAL, exc.what()); + } + catch (const std::exception &exc) + { // Any other exception + BOOST_LOG_TRIVIAL(error) + << exc.what() << ". Transaction-id:" << request->transaction_id(); + conf_state->clr_wait_confirm(); + return grpc::Status(grpc::StatusCode::INTERNAL, exc.what()); + } } - } catch (const invalid_argument &exc) { - BOOST_LOG_TRIVIAL(error) << exc.what() - << ". Transaction-id:" - << request->transaction_id(); - conf_state->clr_wait_confirm(); - return grpc::Status(StatusCode::INVALID_ARGUMENT, exc.what()); - } catch (sysrepo::Error &exc) { - BOOST_LOG_TRIVIAL(error) << exc.what() - << ". Transaction-id:" - << request->transaction_id(); - conf_state->clr_wait_confirm(); - return grpc::Status(StatusCode::INTERNAL, exc.what()); - } catch (const exception &exc) { //Any other exception - BOOST_LOG_TRIVIAL(error) << exc.what() - << ". Transaction-id:" - << request->transaction_id(); - conf_state->clr_wait_confirm(); - return grpc::Status(StatusCode::INTERNAL, exc.what()); - } } - } - - /* gNMI paths with value to update */ - if (request->update_size() > 0) { - for (auto &upd : request->update()) { - UpdateResult res; - try { - auto [status, node] = handleUpdate(upd, &res, prefix, request->prefix(), "merge"); - if (!status.ok()) { - BOOST_LOG_TRIVIAL(error) << "Fail building set notification: " - << status.error_message() - << ". Transaction-id: " - << request->transaction_id(); - conf_state->clr_wait_confirm(); - return status; + + /* gNMI paths with value to update */ + if (request->update_size() > 0) + { + for (auto &upd : request->update()) + { + gnmi::UpdateResult res; + try + { + auto status = handleUpdate(upd, &res, prefix, request->prefix(), "merge"); + if (!status.ok()) + { + BOOST_LOG_TRIVIAL(error) + << "Fail building set notification: " << status.error_message() + << ". Transaction-id: " << request->transaction_id(); + conf_state->clr_wait_confirm(); + return status; + } + res.set_op(gnmi::UpdateResult::UPDATE); + results.push_back(res); + } + catch (const std::invalid_argument &exc) + { + BOOST_LOG_TRIVIAL(error) + << exc.what() << ". Transaction-id:" << request->transaction_id(); + conf_state->clr_wait_confirm(); + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, exc.what()); + } + catch (const sysrepo::Error &exc) + { + BOOST_LOG_TRIVIAL(error) + << exc.what() << ". Transaction-id:" << request->transaction_id(); + conf_state->clr_wait_confirm(); + return grpc::Status(grpc::StatusCode::INTERNAL, exc.what()); + } } - res.set_op(gnmi::UpdateResult::UPDATE); - results.push_back(res); - if (node.has_value()) { - xact.push(*node); + } + + xact.push(deleteTree); + xact.push(purgeTree); + xact.push(replaceTree); + xact.push(updateTree); + + try + { + /* edit final tree batch */ + if (xact.final_tree.has_value()) + sr_sess.editBatch(xact.final_tree.value(), sysrepo::DefaultOperation::Merge); + + /* if this fails, we can still revert the changes */ + sr_sess.applyChanges(); + + if (!request->has_confirm()) + { + /* copy the prepared configuration to Startup (has to succeed) */ + sr_sess_startup.copyConfig(sysrepo::Datastore::Running); } - } catch (const invalid_argument &exc) { - BOOST_LOG_TRIVIAL(error) << exc.what() - << ". Transaction-id:" - << request->transaction_id(); + } + catch (const sysrepo::Error &exc) + { conf_state->clr_wait_confirm(); - return grpc::Status(StatusCode::INVALID_ARGUMENT, exc.what()); - } catch (const sysrepo::Error &exc) { - BOOST_LOG_TRIVIAL(error) << exc.what() - << ". Transaction-id:" - << request->transaction_id(); + std::string err_str; + auto errors = sr_sess.getErrors(); + if (errors.size()) + { + err_str = errors[0].errorMessage; + } + else + { + err_str = exc.what(); + } + BOOST_LOG_TRIVIAL(error) << "commit error: " << err_str + << ". Transaction-id:" << request->transaction_id(); + sr_sess.discardChanges(); + return grpc::Status(grpc::StatusCode::ABORTED, err_str); + } + catch (const std::exception &exc) + { conf_state->clr_wait_confirm(); - return grpc::Status(StatusCode::INTERNAL, exc.what()); - } + BOOST_LOG_TRIVIAL(error) << exc.what() << ". Transaction-id:" << request->transaction_id(); + sr_sess.discardChanges(); + return grpc::Status(grpc::StatusCode::INTERNAL, exc.what()); } - } - try { - if (xact.first_node.has_value()) { - sr_sess.editBatch(xact.first_node.value(), sysrepo::DefaultOperation::Merge); - } + conf_state->write_set_transaction_id(request->transaction_id()); - sr_sess.applyChanges(); + for (auto r : results) + *(response->add_response()) = r; - if (!request->has_confirm()) { - sr_sess_startup.copyConfig(sysrepo::Datastore::Running); - } - conf_state->write_set_transaction_id(request->transaction_id()); - } catch (const sysrepo::Error &exc) { - conf_state->clr_wait_confirm(); - string err_str; - auto errors = sr_sess.getErrors(); - if (errors.size()) { - err_str = errors[0].errorMessage; - } else { - err_str = exc.what(); - } - sr_sess.discardChanges(); - BOOST_LOG_TRIVIAL(error) << "commit error: " - << err_str - << ". Transaction-id:" - << request->transaction_id(); - return grpc::Status(StatusCode::ABORTED, err_str); - } catch (const exception &exc) { - BOOST_LOG_TRIVIAL(error) << exc.what() - << ". Transaction-id:" - << request->transaction_id(); - sr_sess.discardChanges(); - return grpc::Status(StatusCode::INTERNAL, exc.what()); - } - - for (auto r : results) - *(response->add_response()) = r; - - conf_state->reset_timers(); - return grpc::Status::OK; + conf_state->reset_timers(); + return grpc::Status::OK; } } // namespace impl diff --git a/src/gnmi/set.h b/src/gnmi/set.h index a461c12..a1d6a5a 100644 --- a/src/gnmi/set.h +++ b/src/gnmi/set.h @@ -15,43 +15,47 @@ * limitations under the License. */ -#ifndef _GNMI_SET_H -#define _GNMI_SET_H +#pragma once -#include #include #include +#include + +#include #include "confirm.h" #include "encode/encode.h" +#include "utils/sysrepo.h" -using namespace gnmi; -using grpc::Status; -using grpc::StatusCode; +namespace impl +{ -namespace impl { - -class Set { +class Set +{ public: - Set(sysrepo::Session running_sess, sysrepo::Session startup_sess, shared_ptr confirm_state) - : sr_sess(running_sess), sr_sess_startup(startup_sess), conf_state(confirm_state) + Set(sysrepo::Session startup_sess, sysrepo::Session running_sess, + sysrepo::Session candidate_sess, std::shared_ptr confirm_state) + : sr_sess_startup(startup_sess), sr_sess(running_sess), sr_sess_candidate(candidate_sess), + conf_state(confirm_state) { - encodef = std::make_shared(sr_sess); + encodef = std::make_shared(sr_sess); } ~Set() {} - Status run(const SetRequest* request, SetResponse* response); + grpc::Status run(const gnmi::SetRequest *request, gnmi::SetResponse *response); private: - std::tuple> handleUpdate(Update in, UpdateResult *out, string prefix_str, const Path &prefix, string op); + grpc::Status handleUpdate(gnmi::Update in, gnmi::UpdateResult *out, std::string prefix_str, + const gnmi::Path &prefix, std::string op); private: - sysrepo::Session sr_sess; //sysrepo running datastore session - sysrepo::Session sr_sess_startup; //sysrepo startup datastore session - shared_ptr encodef; //support for json ietf encoding - shared_ptr conf_state; // commit confirm state + sysrepo::Session sr_sess_startup; // sysrepo startup datastore session + sysrepo::Session sr_sess; // sysrepo running datastore session + sysrepo::Session sr_sess_candidate; // sysrepo candidate datastore session + std::shared_ptr encodef; // support for json ietf encoding + std::shared_ptr conf_state; // commit confirm state + std::optional deleteTree, purgeTree, replaceTree, updateTree; + UpdateTransaction xact; }; -} - -#endif //_GNMI_SET_H +} // namespace impl diff --git a/src/gnmi/subscribe.cpp b/src/gnmi/subscribe.cpp index f3f1470..24965ae 100644 --- a/src/gnmi/subscribe.cpp +++ b/src/gnmi/subscribe.cpp @@ -15,150 +15,163 @@ * limitations under the License. */ -#include -#include -#include -#include #include #include +#include +#include +#include +#include #include -#include +#include +// #include +#include "subscribe.h" +#include "utils/sysrepo.h" #include #include -#include "subscribe.h" -#include #include -#include "utils/sysrepo.h" - -using namespace std; -using namespace chrono; -using google::protobuf::RepeatedPtrField; +#include -namespace impl { +namespace impl +{ -Status -Subscribe::BuildSubsUpdate(RepeatedPtrField* updateList, - const Path &prefix, string fullpath, - gnmi::Encoding encoding) +grpc::Status +Subscribe::BuildSubsUpdate(google::protobuf::RepeatedPtrField *updateList, + const gnmi::Path &prefix, std::string fullpath, gnmi::Encoding encoding) { - Update *update; - - if (prefix.elem_size() > 0) { - string str = gnmi_to_xpath(prefix); - fullpath = str + fullpath; - } - - SessionDsSwitcher ds_switch(sr_sess, sysrepo::Datastore::Operational); - - try { - /* Get multiple subtree for YANG lists or one for other YANG types */ - auto sr_trees = sr_sess.getData(fullpath.c_str()); - /* The path not (yet) existing isn't an error, so just return an empty set */ - if (!sr_trees.has_value()) - return Status::OK; - - for (auto n : sr_trees->findXPath(fullpath.c_str())) { - update = updateList->Add(); - xpath_to_gnmi(n.path(), *update->mutable_path()); - auto status = encodef->encode(encoding, n, update->mutable_val()); - if (!status.ok()) { + gnmi::Update *update; + + if (prefix.elem_size() > 0) + { + std::string str = gnmi_to_xpath(prefix); + fullpath = str + fullpath; + } + + SessionDsSwitcher ds_switch(sr_sess, sysrepo::Datastore::Operational); + + try + { + /* Get multiple subtree for YANG lists or one for other YANG types */ + auto sr_trees = sr_sess.getData(fullpath.c_str()); + /* The path not (yet) existing isn't an error, so just return an empty set */ + if (!sr_trees.has_value()) + return grpc::Status::OK; + + for (auto n : sr_trees->findXPath(fullpath.c_str())) + { + update = updateList->Add(); + xpath_to_gnmi(n.path(), *update->mutable_path()); + auto status = encodef->encode(encoding, n, update->mutable_val()); + if (!status.ok()) + { + updateList->Clear(); + return status; + } + } + } + catch (std::invalid_argument &exc) + { updateList->Clear(); - return status; - } - } - } catch (invalid_argument &exc) { - updateList->Clear(); - return Status(StatusCode::NOT_FOUND, exc.what()); - } catch (sysrepo::ErrorWithCode &exc) { - BOOST_LOG_TRIVIAL(error) << "Fail getting items from sysrepo: " - << exc.code(); - updateList->Clear(); - return Status(StatusCode::INVALID_ARGUMENT, exc.what()); - } - - return Status::OK; + return grpc::Status(grpc::StatusCode::NOT_FOUND, exc.what()); + } + catch (sysrepo::ErrorWithCode &exc) + { + BOOST_LOG_TRIVIAL(error) << "Fail getting items from sysrepo: " << exc.code(); + updateList->Clear(); + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, exc.what()); + } + + return grpc::Status::OK; } /** * BuildSubscribeNotification - Build a Notification message, excluding * subscriptions which are on-change(this is done elsewhere, racy if done here). - * Contrary to Get Notification, gnmi specification highly recommands to + * Contrary to Get Notification, gnmi specification highly recommends to * put multiple in the same Notification message. * @param notification the notification that is constructed by this function. * @param request the SubscriptionList from SubscribeRequest to answer to. * @param sample indicates whether there is at least 1 sample subscr */ -Status -Subscribe::BuildSubscribeNotification(Notification *notification, - const SubscriptionList& request, - bool *sample) +grpc::Status Subscribe::BuildSubscribeNotification(gnmi::Notification *notification, + const gnmi::SubscriptionList &request, + bool *sample) { - RepeatedPtrField* updateList = notification->mutable_update(); - Status status; - - // Defined refer to a long Path by a shorter one: alias - if (request.use_aliases()) { - BOOST_LOG_TRIVIAL(warning) << "Unsupported usage of aliases"; - return Status(StatusCode::UNIMPLEMENTED, "alias not supported"); - } - - /* Check if only updates should be sent */ - if (request.updates_only()) { - BOOST_LOG_TRIVIAL(warning) << "Unsupported updates_only, send all paths"; - return Status(StatusCode::UNIMPLEMENTED, "updates-only not supported"); - } - - /* Get time since epoch in milliseconds */ - notification->set_timestamp(get_time_nanosec()); - - // gNMI spec §2.2.2.1: - // When set in the prefix in a request, GetRequest, SetRequest or - // SubscribeRequest, the field MUST be reflected in the prefix of the - // corresponding GetResponse, SetResponse or SubscribeResponse by a - // server. - if (request.has_prefix()) - notification->mutable_prefix()->set_target(request.prefix().target()); - - if (sample) { - *sample = false; - } - /* Fill Update RepeatedPtrField in Notification message - * Update field contains only data elements that have changed values. */ - for (int i = 0; i < request.subscription_size(); i++) { - Subscription sub = request.subscription(i); - - if (request.mode() == SubscriptionList_Mode_STREAM && - (sub.mode() == SubscriptionMode::TARGET_DEFINED || - sub.mode() == SubscriptionMode::ON_CHANGE)) { - BOOST_LOG_TRIVIAL(debug) << "On-change, getting initial data later: " << gnmi_to_xpath(sub.path()); - continue; - } - if (sample) { - *sample = true; - } - // Fetch all found counters value for a requested path - string str; - try { - gnmi_check_origin(request.prefix(), sub.path()); - - status = BuildSubsUpdate(updateList, request.prefix(), - gnmi_to_xpath(sub.path()), request.encoding()); - } catch (invalid_argument &exc) { - BOOST_LOG_TRIVIAL(error) << exc.what(); - return Status(StatusCode::INVALID_ARGUMENT, exc.what()); - } - if (!status.ok()) { - BOOST_LOG_TRIVIAL(error) << "Fail building update for " - << gnmi_to_xpath(sub.path()); - return status; - } - } - - notification->set_atomic(false); - - return Status::OK; + google::protobuf::RepeatedPtrField *updateList = notification->mutable_update(); + grpc::Status status; + + // Defined refer to a long Path by a shorter one: alias + if (request.use_aliases()) + { + BOOST_LOG_TRIVIAL(warning) << "Unsupported usage of aliases"; + return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "alias not supported"); + } + + /* Check if only updates should be sent */ + if (request.updates_only()) + { + BOOST_LOG_TRIVIAL(warning) << "Unsupported updates_only, send all paths"; + return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "updates-only not supported"); + } + + /* Get time since epoch in milliseconds */ + notification->set_timestamp(get_time_nanosec()); + + // gNMI spec §2.2.2.1: + // When set in the prefix in a request, GetRequest, SetRequest or + // SubscribeRequest, the field MUST be reflected in the prefix of the + // corresponding GetResponse, SetResponse or SubscribeResponse by a + // server. + if (request.has_prefix()) + notification->mutable_prefix()->set_target(request.prefix().target()); + + if (sample) + { + *sample = false; + } + /* Fill Update RepeatedPtrField in Notification message + * Update field contains only data elements that have changed values. */ + for (int i = 0; i < request.subscription_size(); i++) + { + gnmi::Subscription sub = request.subscription(i); + + if (request.mode() == gnmi::SubscriptionList_Mode_STREAM && + (sub.mode() == gnmi::SubscriptionMode::TARGET_DEFINED || + sub.mode() == gnmi::SubscriptionMode::ON_CHANGE)) + { + BOOST_LOG_TRIVIAL(debug) + << "On-change, getting initial data later: " << gnmi_to_xpath(sub.path()); + continue; + } + if (sample) + { + *sample = true; + } + // Fetch all found counters value for a requested path + std::string str; + try + { + gnmi_check_origin(request.prefix(), sub.path()); + + status = BuildSubsUpdate(updateList, request.prefix(), gnmi_to_xpath(sub.path()), + request.encoding()); + } + catch (std::invalid_argument &exc) + { + BOOST_LOG_TRIVIAL(error) << exc.what(); + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, exc.what()); + } + if (!status.ok()) + { + BOOST_LOG_TRIVIAL(error) << "Fail building update for " << gnmi_to_xpath(sub.path()); + return status; + } + } + + notification->set_atomic(false); + + return grpc::Status::OK; } /** @@ -168,484 +181,552 @@ Subscribe::BuildSubscribeNotification(Notification *notification, * @param xpath The xpath that the registration is firing for * @param session The sysrepo session for the update */ -Status -Subscribe::BuildSubscribeNotificationForChanges(Notification *notification, - const SubscriptionList& request, - string& xpath, - sysrepo::Session session) +grpc::Status Subscribe::BuildSubscribeNotificationForChanges(gnmi::Notification *notification, + const gnmi::SubscriptionList &request, + std::string &xpath, + sysrepo::Session session) { - auto updateList = notification->mutable_update(); - auto deleteList = notification->mutable_delete_(); - Status status; - - // Defined refer to a long Path by a shorter one: alias - if (request.use_aliases()) { - BOOST_LOG_TRIVIAL(warning) << "Unsupported usage of aliases"; - return Status(StatusCode::UNIMPLEMENTED, "alias not supported"); - } - - /* Check if only updates should be sent */ - if (request.updates_only()) { - BOOST_LOG_TRIVIAL(warning) << "Unsupported updates_only, send all paths"; - return Status(StatusCode::UNIMPLEMENTED, "updates-only not supported"); - } - - /* Get time since epoch in milliseconds */ - notification->set_timestamp(get_time_nanosec()); - - // gNMI spec §2.2.2.1: - // When set in the prefix in a request, GetRequest, SetRequest or - // SubscribeRequest, the field MUST be reflected in the prefix of the - // corresponding GetResponse, SetResponse or SubscribeResponse by a - // server. - if (request.has_prefix()) - notification->mutable_prefix()->set_target(request.prefix().target()); - - /* Fill Update RepeatedPtrField in Notification message - * Update field contains only data elements that have changed values. */ - - try { - auto last_change = make_pair(std::string(""), sysrepo::ChangeOperation::Created); - - string changes_path(xpath); - changes_path += "//."; - auto iter = session.getChanges(changes_path.c_str()); - for (const auto& change : iter) { - if (!last_change.first.empty() && - last_change.second == change.operation && - // If we have the identifier of one leaf as a substring of another at the same level, - // we can confuse between the two. - // for example searching for "ike-connection" and finding "ike-connection-up". - // So search with "/" suffixed and prevent this mix-up. - (change.node.path().rfind(last_change.first + "/", 0) == 0 || - change.node.path() == last_change.first) - ) { - continue; - } - last_change = make_pair(change.node.path(), change.operation); - - auto val = change.node.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings).value(); - BOOST_LOG_TRIVIAL(debug) << "Subscribe notification, operation: " << change.operation - << ", path: " << change.node.path() << ", value: " << val; - - // Also done for updated nodes due to gNMI spec §3.5.2.3: - // > To replace the contents of an entire node within the tree, the target populates - // > the delete field with the path of the node being removed, along with the new - // > contents within the update field. - if (change.operation != sysrepo::ChangeOperation::Created) { - auto path_p = deleteList->Add(); - Path path; - xpath_to_gnmi(change.node.path(), path); - *path_p = path; - } - if (change.operation != sysrepo::ChangeOperation::Deleted) { - auto update = updateList->Add(); - - xpath_to_gnmi(change.node.path(), *update->mutable_path()); - // Remove all of the attributes from nodes which we don't need and may confuse parsers of the JSON when - // using that encoding. - auto opts = static_cast(libyang::DuplicationOptions::NoMeta) | - static_cast(libyang::DuplicationOptions::Recursive); - auto node = change.node.duplicate(static_cast(opts)); - status = encodef->encode(request.encoding(), node, update->mutable_val()); - if (!status.ok()) - return status; - } + auto updateList = notification->mutable_update(); + auto deleteList = notification->mutable_delete_(); + grpc::Status status; + + // Defined refer to a long Path by a shorter one: alias + if (request.use_aliases()) + { + BOOST_LOG_TRIVIAL(warning) << "Unsupported usage of aliases"; + return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "alias not supported"); + } + + /* Check if only updates should be sent */ + if (request.updates_only()) + { + BOOST_LOG_TRIVIAL(warning) << "Unsupported updates_only, send all paths"; + return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "updates-only not supported"); + } + + /* Get time since epoch in milliseconds */ + notification->set_timestamp(get_time_nanosec()); + + // gNMI spec §2.2.2.1: + // When set in the prefix in a request, GetRequest, SetRequest or + // SubscribeRequest, the field MUST be reflected in the prefix of the + // corresponding GetResponse, SetResponse or SubscribeResponse by a + // server. + if (request.has_prefix()) + notification->mutable_prefix()->set_target(request.prefix().target()); + + /* Fill Update RepeatedPtrField in Notification message + * Update field contains only data elements that have changed values. */ + + try + { + auto last_change = make_pair(std::string(""), sysrepo::ChangeOperation::Created); + + std::string changes_path(xpath); + changes_path += "//."; + auto iter = session.getChanges(changes_path.c_str()); + for (const auto &change : iter) + { + if (!last_change.first.empty() && last_change.second == change.operation && + // If we have the identifier of one leaf as a substring of another at the same + // level, we can confuse between the two. for example searching for "ike-connection" + // and finding "ike-connection-up". So search with "/" suffixed and prevent this + // mix-up. + (change.node.path().rfind(last_change.first + "/", 0) == 0 || + change.node.path() == last_change.first)) + { + continue; + } + last_change = std::make_pair(change.node.path(), change.operation); + + auto val = + change.node.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Siblings) + .value(); + BOOST_LOG_TRIVIAL(debug) << "Subscribe notification, operation: " << change.operation + << ", path: " << change.node.path() << ", value: " << val; + + // Also done for updated nodes due to gNMI spec §3.5.2.3: + // > To replace the contents of an entire node within the tree, the target populates + // > the delete field with the path of the node being removed, along with the new + // > contents within the update field. + if (change.operation != sysrepo::ChangeOperation::Created) + { + auto path_p = deleteList->Add(); + gnmi::Path path; + xpath_to_gnmi(change.node.path(), path); + *path_p = path; + } + if (change.operation != sysrepo::ChangeOperation::Deleted) + { + auto update = updateList->Add(); + + xpath_to_gnmi(change.node.path(), *update->mutable_path()); + // Remove all of the attributes from nodes which we don't need and may confuse + // parsers of the JSON when using that encoding. + // auto opts = static_cast(libyang::DuplicationOptions::NoMeta) | + // static_cast(libyang::DuplicationOptions::Recursive); + // auto node = + // change.node.duplicate(static_cast(opts)); + status = encodef->encode(request.encoding(), change.node, update->mutable_val()); + if (!status.ok()) + return status; + } + } + } + catch (sysrepo::ErrorWithCode &exc) + { + BOOST_LOG_TRIVIAL(error) << "Fail processing module changes from sysrepo: " << exc.what(); + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, exc.what()); + } + catch (std::invalid_argument &exc) + { + BOOST_LOG_TRIVIAL(error) << exc.what(); + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, exc.what()); } - } catch (sysrepo::ErrorWithCode &exc) { - BOOST_LOG_TRIVIAL(error) << "Fail processing module changes from sysrepo: " - << exc.what(); - return Status(StatusCode::INVALID_ARGUMENT, exc.what()); - } catch (invalid_argument &exc) { - BOOST_LOG_TRIVIAL(error) << exc.what(); - return Status(StatusCode::INVALID_ARGUMENT, exc.what()); - } - notification->set_atomic(false); + notification->set_atomic(false); - return Status::OK; + return grpc::Status::OK; } void Subscribe::triggerSampleUpdate( - ServerContext* context, Subscription &sub, - ServerReaderWriter* stream) + grpc::ServerContext *context, gnmi::Subscription &sub, + grpc::ServerReaderWriter *stream) { - SubscribeResponse response; - SubscriptionList updateList; - - // Add the subscription entry to the subscription list - updateList.add_subscription()->CopyFrom(sub); - - if (!context->IsCancelled()) { - auto status = BuildSubscribeNotification(response.mutable_update(), - updateList); - if(!status.ok()) { - // This is a hack to allow the Read in the parent thread to return, - // but it avoids needing to move to an asynchronous model just to return this one error - grpc::g_core_codegen_interface->grpc_call_cancel_with_status( - context->c_call(), static_cast(status.error_code()), - status.error_message().c_str(), nullptr); - return; + gnmi::SubscribeResponse response; + gnmi::SubscriptionList updateList; + + // Add the subscription entry to the subscription list + updateList.add_subscription()->CopyFrom(sub); + // gnmi::Path *prefix = new gnmi::Path(); + // prefix->set_origin("rfc7951"); + // updateList.set_allocated_prefix(prefix); + + if (!context->IsCancelled()) + { + auto status = BuildSubscribeNotification(response.mutable_update(), updateList); + if (!status.ok()) + { + // This is a hack to allow the Read in the parent thread to return, + // but it avoids needing to move to an asynchronous model just to return this one error + // grpc::g_core_codegen_interface->grpc_call_cancel_with_status( + // context->c_call(), static_cast(status.error_code()), + // status.error_message().c_str(), nullptr); + return; + } + Write(stream, response); + response.Clear(); } - Write(stream, response); - response.Clear(); - } } static void sample_timer_expiry( const boost::system::error_code &e, std::shared_ptr t, - Subscription &sub, Subscribe *subscribe, ServerContext* context, - ServerReaderWriter* stream) + gnmi::Subscription &sub, Subscribe *subscribe, grpc::ServerContext *context, + grpc::ServerReaderWriter *stream) { - if (e == boost::asio::error::operation_aborted) { - return; - } + if (e == boost::asio::error::operation_aborted) + { + return; + } - subscribe->triggerSampleUpdate(context, sub, stream); + subscribe->triggerSampleUpdate(context, sub, stream); - t->expires_at(t->expiry() + nanoseconds{sub.sample_interval()}); - t->async_wait(boost::bind(sample_timer_expiry, - boost::asio::placeholders::error, t, sub, subscribe, context, stream)); + t->expires_at(t->expiry() + std::chrono::nanoseconds{sub.sample_interval()}); + t->async_wait(boost::bind(sample_timer_expiry, boost::asio::placeholders::error, t, sub, + subscribe, context, stream)); } void Subscribe::streamWorker( - ServerContext* context, SubscribeRequest request, - ServerReaderWriter* stream, - boost::asio::io_context &initial_update_io, - boost::asio::io_context &incr_update_io) + grpc::ServerContext *context, gnmi::SubscribeRequest request, + grpc::ServerReaderWriter *stream, + boost::asio::io_context &initial_update_io, boost::asio::io_context &incr_update_io) { - vector> timers; + std::vector> timers; - for (int i = 0; i < request.subscribe().subscription_size(); i++) { - Subscription sub = request.subscribe().subscription(i); - switch (sub.mode()) { - case SAMPLE: { - auto t = std::make_shared(incr_update_io, nanoseconds{sub.sample_interval()}); - t->async_wait(boost::bind(sample_timer_expiry, boost::asio::placeholders::error, t, sub, this, context, stream)); - timers.push_back(t); - break; - } - default: - break; + for (int i = 0; i < request.subscribe().subscription_size(); i++) + { + gnmi::Subscription sub = request.subscribe().subscription(i); + switch (sub.mode()) + { + case gnmi::SAMPLE: + { + auto t = std::make_shared( + incr_update_io, std::chrono::nanoseconds{sub.sample_interval()}); + t->async_wait(boost::bind(sample_timer_expiry, boost::asio::placeholders::error, t, sub, + this, context, stream)); + timers.push_back(t); + break; + } + default: + break; + } } - } - // Keep io_context running regardless if there are tasks to execute or not - boost::asio::executor_work_guard initial_work_guard(initial_update_io.get_executor()); + // Keep io_context running regardless if there are tasks to execute or not + boost::asio::executor_work_guard initial_work_guard( + initial_update_io.get_executor()); - initial_update_io.run(); + initial_update_io.run(); - boost::asio::executor_work_guard incr_work_guard(incr_update_io.get_executor()); + boost::asio::executor_work_guard incr_work_guard( + incr_update_io.get_executor()); - incr_update_io.run(); + incr_update_io.run(); - BOOST_LOG_TRIVIAL(debug) << "Subscription stream worker exiting"; + BOOST_LOG_TRIVIAL(debug) << "Subscription stream worker exiting"; } -static void streamWorkerThread(Subscribe *sub, ServerContext* context, SubscribeRequest &request, - ServerReaderWriter* stream, +static void streamWorkerThread( + Subscribe *sub, grpc::ServerContext *context, gnmi::SubscribeRequest &request, + grpc::ServerReaderWriter *stream, std::tuple io_context_tuple) { - boost::asio::io_context &initial_update_io = std::get<0>(io_context_tuple); - boost::asio::io_context &incr_update_io = std::get<1>(io_context_tuple); - sub->streamWorker(context, request, stream, initial_update_io, incr_update_io); + boost::asio::io_context &initial_update_io = std::get<0>(io_context_tuple); + boost::asio::io_context &incr_update_io = std::get<1>(io_context_tuple); + sub->streamWorker(context, request, stream, initial_update_io, incr_update_io); } -class SrModuleOnChangeParams { -public: - SrModuleOnChangeParams(SubscribeRequest *request, - ServerReaderWriter *stream, Subscribe *subscribe, - boost::asio::io_context &initial_update_io_context, - boost::asio::io_context &incr_update_io_context) : - request(request), stream(stream), subscribe(subscribe), initial_update_io_context(initial_update_io_context), - incr_update_io_context(incr_update_io_context) {} - - SubscribeRequest *request; - ServerReaderWriter* stream; - Subscribe *subscribe; - boost::asio::io_context &initial_update_io_context; - boost::asio::io_context &incr_update_io_context; - - bool is_incremental(void) const { - // set incr_update=true and return the previous value. - return std::exchange(incr_update, true); - } -private: - mutable bool incr_update = false; -}; - -sysrepo::ErrorCode srModuleOnChange( - sysrepo::Session session, std::string_view module_name, std::string_view xpath, sysrepo::Event event, - uint32_t request_id, const SrModuleOnChangeParams ¶ms) +class SrModuleOnChangeParams { - Status status; - auto response = make_unique(); + public: + SrModuleOnChangeParams( + gnmi::SubscribeRequest *request, + grpc::ServerReaderWriter *stream, + Subscribe *subscribe, boost::asio::io_context &initial_update_io_context, + boost::asio::io_context &incr_update_io_context) + : request(request), stream(stream), subscribe(subscribe), + initial_update_io_context(initial_update_io_context), + incr_update_io_context(incr_update_io_context) + { + } - (void)module_name; - (void)event; - (void)request_id; + gnmi::SubscribeRequest *request; + grpc::ServerReaderWriter *stream; + Subscribe *subscribe; + boost::asio::io_context &initial_update_io_context; + boost::asio::io_context &incr_update_io_context; - string changes_path(xpath); + bool is_incremental(void) const + { + // set incr_update=true and return the previous value. + return std::exchange(incr_update, true); + } - status = params.subscribe->BuildSubscribeNotificationForChanges(response->mutable_update(), - params.request->subscribe(), changes_path, session); - if (!status.ok()) { - BOOST_LOG_TRIVIAL(warning) << "unable to build update in response to notification for " << xpath; - return sysrepo::ErrorCode::Ok; - } + private: + mutable bool incr_update = false; +}; - if (params.is_incremental()) { - params.subscribe->PostWrite(params.stream, std::move(response), params.incr_update_io_context); - } else { - params.subscribe->PostWrite(params.stream, std::move(response), params.initial_update_io_context); - } +sysrepo::ErrorCode srModuleOnChange(sysrepo::Session session, std::string_view module_name, + std::string_view xpath, sysrepo::Event event, + uint32_t request_id, const SrModuleOnChangeParams ¶ms) +{ + grpc::Status status; + auto response = std::make_unique(); + + (void)module_name; + (void)event; + (void)request_id; + + std::string changes_path(xpath); + + status = params.subscribe->BuildSubscribeNotificationForChanges( + response->mutable_update(), params.request->subscribe(), changes_path, session); + if (!status.ok()) + { + BOOST_LOG_TRIVIAL(warning) + << "unable to build update in response to notification for " << xpath; + return sysrepo::ErrorCode::Ok; + } + + if (params.is_incremental()) + { + params.subscribe->PostWrite(params.stream, std::move(response), + params.incr_update_io_context); + } + else + { + params.subscribe->PostWrite(params.stream, std::move(response), + params.initial_update_io_context); + } - return sysrepo::ErrorCode::Ok; + return sysrepo::ErrorCode::Ok; } -Status Subscribe::registerStreamOnChange( - SubscribeRequest &request, Subscription sub, - ServerReaderWriter* stream, +grpc::Status Subscribe::registerStreamOnChange( + gnmi::SubscribeRequest &request, gnmi::Subscription sub, + grpc::ServerReaderWriter *stream, boost::asio::io_context &initial_update_io_context, - boost::asio::io_context &incr_update_io_context, - shared_ptr sr_sub, - vector ¶ms_vec) + boost::asio::io_context &incr_update_io_context, std::shared_ptr sr_sub, + std::vector ¶ms_vec) { - string fullpath = ""; - try { - if (request.subscribe().prefix().elem_size() > 0 || - request.subscribe().prefix().target().compare("")) { - fullpath = gnmi_to_xpath(request.subscribe().prefix()); - } - fullpath += gnmi_to_xpath(sub.path()); - } catch (invalid_argument &exc) { - BOOST_LOG_TRIVIAL(error) << exc.what(); - return Status(StatusCode::INVALID_ARGUMENT, exc.what()); - } - - BOOST_LOG_TRIVIAL(debug) << "Subscribe (stream) " << fullpath; - - SrModuleOnChangeParams params(&request, stream, this, initial_update_io_context, incr_update_io_context); - params_vec.push_back(params); - try { - auto params_ref = params_vec.back(); - sr_sub->data_change_subscribe( - [params_ref] - (sysrepo::Session session, uint32_t sub_id, std::string_view module_name, std::optional xpath, sysrepo::Event event, uint32_t request_id) - { - (void)sub_id; - return srModuleOnChange(session, module_name, xpath.value(), event, request_id, params_ref); - }, - fullpath.c_str(), - 0, sysrepo::SubscribeOptions::Passive | sysrepo::SubscribeOptions::DoneOnly | sysrepo::SubscribeOptions::Enabled); - } catch (const sysrepo::ErrorWithCode &exc) { - BOOST_LOG_TRIVIAL(error) << exc.what(); - return Status(StatusCode::INTERNAL, exc.what()); - } - - return Status::OK; + std::string fullpath = ""; + try + { + if (request.subscribe().prefix().elem_size() > 0 || + request.subscribe().prefix().target().compare("")) + { + fullpath = gnmi_to_xpath(request.subscribe().prefix()); + } + fullpath += gnmi_to_xpath(sub.path()); + } + catch (std::invalid_argument &exc) + { + BOOST_LOG_TRIVIAL(error) << exc.what(); + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, exc.what()); + } + + BOOST_LOG_TRIVIAL(debug) << "Subscribe (stream) " << fullpath; + + SrModuleOnChangeParams params(&request, stream, this, initial_update_io_context, + incr_update_io_context); + params_vec.push_back(params); + try + { + auto params_ref = params_vec.back(); + sr_sub->data_change_subscribe( + [params_ref](sysrepo::Session session, uint32_t sub_id, std::string_view module_name, + std::optional xpath, sysrepo::Event event, + uint32_t request_id) + { + (void)sub_id; + return srModuleOnChange(session, module_name, xpath.value(), event, request_id, + params_ref); + }, + fullpath.c_str(), 0, + sysrepo::SubscribeOptions::Passive | sysrepo::SubscribeOptions::DoneOnly | + sysrepo::SubscribeOptions::Enabled); + } + catch (const sysrepo::ErrorWithCode &exc) + { + BOOST_LOG_TRIVIAL(error) << exc.what(); + return grpc::Status(grpc::StatusCode::INTERNAL, exc.what()); + } + + return grpc::Status::OK; } /** * Handles SubscribeRequest messages with STREAM subscription mode by * periodically sending updates to the client. */ -Status Subscribe::handleStream( - ServerContext* context, SubscribeRequest request, - ServerReaderWriter* stream) +grpc::Status Subscribe::handleStream( + grpc::ServerContext *context, gnmi::SubscribeRequest request, + grpc::ServerReaderWriter *stream) { - SubscribeResponse response; - Status status; - vector params_vec; - - if (request.subscribe().subscription_size() == 0) { - return Status(StatusCode::INVALID_ARGUMENT, - "No subscription in message"); - } - // Checks that sample_interval values are not higher than INT64_MAX - // i.e. 9223372036854775807 nanoseconds - for (int i = 0; i < request.subscribe().subscription_size(); i++) { - Subscription sub = request.subscribe().subscription(i); - if (sub.sample_interval() > static_cast(duration::max().count())) - return Status(StatusCode::INVALID_ARGUMENT, - string("sample_interval must be less than ") - + to_string(INT64_MAX) + " nanoseconds"); - - if (sub.mode() == SubscriptionMode::SAMPLE && nanoseconds{sub.sample_interval()} < milliseconds(200)) { - BOOST_LOG_TRIVIAL(warning) << "sample_interval " + to_string(sub.sample_interval()) + - " must be greater than " + to_string(nanoseconds{milliseconds(200)}.count()) + - " nanoseconds"; - return Status(StatusCode::INVALID_ARGUMENT, - string("sample_interval ") + to_string(sub.sample_interval()) + - " must be greater than " + to_string(nanoseconds{milliseconds(200)}.count()) + + gnmi::SubscribeResponse response; + grpc::Status status; + std::vector params_vec; + + if (request.subscribe().subscription_size() == 0) + { + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, "No subscription in message"); + } + // Checks that sample_interval values are not higher than INT64_MAX + // i.e. 9223372036854775807 nanoseconds + for (int i = 0; i < request.subscribe().subscription_size(); i++) + { + gnmi::Subscription sub = request.subscribe().subscription(i); + if (sub.sample_interval() > + static_cast(std::chrono::duration::max().count())) + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, + std::string("sample_interval must be less than ") + + std::to_string(INT64_MAX) + " nanoseconds"); + + if (sub.mode() == gnmi::SubscriptionMode::SAMPLE && + std::chrono::nanoseconds{sub.sample_interval()} < std::chrono::milliseconds(200)) + { + BOOST_LOG_TRIVIAL(warning) + << "sample_interval " + std::to_string(sub.sample_interval()) + + " must be greater than " + + std::to_string( + std::chrono::nanoseconds{std::chrono::milliseconds(200)}.count()) + + " nanoseconds"; + return grpc::Status( + grpc::StatusCode::INVALID_ARGUMENT, + std::string("sample_interval ") + std::to_string(sub.sample_interval()) + + " must be greater than " + + std::to_string( + std::chrono::nanoseconds{std::chrono::milliseconds(200)}.count()) + " nanoseconds"); + } } - } - - // Get the initial data only for sample subscriptions - bool sample=false; - status = BuildSubscribeNotification(response.mutable_update(), - request.subscribe(), - &sample); - if (!status.ok()) - return status; + // Get the initial data only for sample subscriptions + bool sample = false; + status = BuildSubscribeNotification(response.mutable_update(), request.subscribe(), &sample); + if (!status.ok()) + return status; - boost::asio::io_context initial_update_io_context; - boost::asio::io_context incr_update_io_context; + boost::asio::io_context initial_update_io_context; + boost::asio::io_context incr_update_io_context; - SessionDsSwitcher ds_switch(sr_sess, sysrepo::Datastore::Operational); - auto sr_sub = std::make_shared(sr_sess); + SessionDsSwitcher ds_switch(sr_sess, sysrepo::Datastore::Operational); + auto sr_sub = std::make_shared(sr_sess); - if (sample) { - BOOST_LOG_TRIVIAL(debug) << "Sending initial update for sample subscriptions with size:" << response.update().update_size(); - // Sends a first Notification message that updates all sample subcriptions - Write(stream, response); - } - for (int i=0; iRead(&request2); - - incr_update_io_context.stop(); - thread.join(); - - if (success) { - BOOST_LOG_TRIVIAL(warning) << "out-of-order operation was requested on a STREAM subscription"; - return Status(StatusCode::INVALID_ARGUMENT, - string("out-of-order operation was requested on a STREAM subscription")); - } - - return Status::OK; + } + + // Send to the worker thread + boost::asio::post(initial_update_io_context, + [&] + { + // Sends a SYNC message that indicates that initial synchronization + // has completed, i.e. each Subscription has been updated once + gnmi::SubscribeResponse response; + response.set_sync_response(true); + BOOST_LOG_TRIVIAL(debug) << "Sending sync response"; + Write(stream, response); + initial_update_io_context.stop(); + }); + + // Start a worker thread for SAMPLE and ON_CHANGE notifications (the only other type, + // TARGET_DEFINED, isn't supported). + auto thread = std::thread( + streamWorkerThread, this, context, std::ref(request), stream, + std::make_tuple(std::ref(initial_update_io_context), std::ref(incr_update_io_context))); + + // Read from client - note that isn't expected to succeed, but allows us to + // wait (without a busy loop) until the client cancels the streaming subscription and + // then we can terminate the worker thread immediately + gnmi::SubscribeRequest request2; + auto success = stream->Read(&request2); + + incr_update_io_context.stop(); + thread.join(); + + if (success) + { + BOOST_LOG_TRIVIAL(warning) + << "out-of-order operation was requested on a STREAM subscription"; + return grpc::Status( + grpc::StatusCode::INVALID_ARGUMENT, + std::string("out-of-order operation was requested on a STREAM subscription")); + } + + return grpc::Status::OK; } void Subscribe::Write( - ServerReaderWriter* stream, - SubscribeResponse response) + grpc::ServerReaderWriter *stream, + gnmi::SubscribeResponse response) { - const std::lock_guard lock(stream_mutex); - stream->Write(response); + const std::lock_guard lock(stream_mutex); + stream->Write(response); } void Subscribe::PostWrite( - ServerReaderWriter* stream, - std::unique_ptr response, boost::asio::io_context &io_context) + grpc::ServerReaderWriter *stream, + std::unique_ptr response, boost::asio::io_context &io_context) { - // Send to the worker thread - boost::asio::post(io_context, [this, stream, response = std::move(response)] - { - Write(stream, *response); - }); + // Send to the worker thread + boost::asio::post(io_context, + [this, stream, response = std::move(response)] { Write(stream, *response); }); } /** * Handles SubscribeRequest messages with ONCE subscription mode by updating * all the Subscriptions once, sending a SYNC message, then closing the RPC. */ -Status Subscribe::handleOnce(SubscribeRequest request, - ServerReaderWriter* stream) +grpc::Status Subscribe::handleOnce( + gnmi::SubscribeRequest request, + grpc::ServerReaderWriter *stream) { - Status status; + grpc::Status status; - // Sends a Notification message that updates all Subcriptions once - SubscribeResponse response; - status = BuildSubscribeNotification(response.mutable_update(), - request.subscribe()); - if (!status.ok()) - return status; + // Sends a Notification message that updates all Subcriptions once + gnmi::SubscribeResponse response; + status = BuildSubscribeNotification(response.mutable_update(), request.subscribe()); + if (!status.ok()) + return status; - Write(stream, response); - response.Clear(); + Write(stream, response); + response.Clear(); - // Sends a message that indicates that initial synchronization - // has completed, i.e. each Subscription has been updated once - response.set_sync_response(true); - Write(stream, response); - response.Clear(); + // Sends a message that indicates that initial synchronization + // has completed, i.e. each Subscription has been updated once + response.set_sync_response(true); + Write(stream, response); + response.Clear(); - return Status::OK; + return grpc::Status::OK; } /** * Handles SubscribeRequest messages with POLL subscription mode by updating * all the Subscriptions each time a Poll request is received. */ -Status Subscribe::handlePoll(SubscribeRequest request, - ServerReaderWriter* stream) +grpc::Status Subscribe::handlePoll( + gnmi::SubscribeRequest request, + grpc::ServerReaderWriter *stream) { - SubscribeRequest subscription = request; - Status status; + gnmi::SubscribeRequest subscription = request; + grpc::Status status; - while (stream->Read(&request)) { - switch (request.request_case()) { - case request.kPoll: + while (stream->Read(&request)) + { + switch (request.request_case()) { - // Sends a Notification message that updates all Subcriptions once - SubscribeResponse response; - status = BuildSubscribeNotification(response.mutable_update(), - subscription.subscribe()); - if (!status.ok()) - return status; - Write(stream, response); - response.Clear(); - - // Reference 3.5.2.3: - // "For POLL subscriptions, after each set of updates for individual poll request, a SubscribeResponse message with the sync_response field set to true MUST be generated." - response.set_sync_response(true); - Write(stream, response); - break; + case request.kPoll: + { + // Sends a Notification message that updates all Subcriptions once + gnmi::SubscribeResponse response; + status = + BuildSubscribeNotification(response.mutable_update(), subscription.subscribe()); + if (!status.ok()) + return status; + Write(stream, response); + response.Clear(); + + // Reference 3.5.2.3: + // "For POLL subscriptions, after each set of updates for individual poll request, a + // SubscribeResponse message with the sync_response field set to true MUST be + // generated." + response.set_sync_response(true); + Write(stream, response); + break; + } + case request.kAliases: + return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Aliases not implemented yet"); + case request.kSubscribe: + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, + "A SubscriptionList has already been received for this RPC"); + default: + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, + "Unknown content for SubscribeRequest message"); } - case request.kAliases: - return Status(StatusCode::UNIMPLEMENTED, "Aliases not implemented yet"); - case request.kSubscribe: - return Status(StatusCode::INVALID_ARGUMENT, - "A SubscriptionList has already been received for this RPC"); - default: - return Status(StatusCode::INVALID_ARGUMENT, - "Unknown content for SubscribeRequest message"); - } - } - - return Status::OK; + } + + return grpc::Status::OK; } /** @@ -653,35 +734,38 @@ Status Subscribe::handlePoll(SubscribeRequest request, * If it does not have the "subscribe" field set, the RPC MUST be cancelled. * Ref: 3.5.1.1 */ -Status Subscribe::run(ServerContext* context, - ServerReaderWriter* stream) +grpc::Status +Subscribe::run(grpc::ServerContext *context, + grpc::ServerReaderWriter *stream) { - SubscribeRequest request; - - stream->Read(&request); - - if (request.extension_size() > 0) { - BOOST_LOG_TRIVIAL(error) << "Extensions not implemented"; - return Status(StatusCode::UNIMPLEMENTED, "Extensions not implemented"); - } - - if (!request.has_subscribe()) - return Status(StatusCode::INVALID_ARGUMENT, - "SubscribeRequest needs non-empty SubscriptionList"); - - switch (request.subscribe().mode()) { - case SubscriptionList_Mode_STREAM: - return handleStream(context, request, stream); - case SubscriptionList_Mode_ONCE: - return handleOnce(request, stream); - case SubscriptionList_Mode_POLL: - return handlePoll(request, stream); + gnmi::SubscribeRequest request; + + stream->Read(&request); + + if (request.extension_size() > 0) + { + BOOST_LOG_TRIVIAL(error) << "Extensions not implemented"; + return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Extensions not implemented"); + } + + if (!request.has_subscribe()) + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, + "SubscribeRequest needs non-empty SubscriptionList"); + + switch (request.subscribe().mode()) + { + case gnmi::SubscriptionList_Mode_STREAM: + return handleStream(context, request, stream); + case gnmi::SubscriptionList_Mode_ONCE: + return handleOnce(request, stream); + case gnmi::SubscriptionList_Mode_POLL: + return handlePoll(request, stream); default: - BOOST_LOG_TRIVIAL(error) << "Unknown subscription mode"; - return Status(StatusCode::UNIMPLEMENTED, "Unknown subscription mode"); - } + BOOST_LOG_TRIVIAL(error) << "Unknown subscription mode"; + return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Unknown subscription mode"); + } - return Status::OK; + return grpc::Status::OK; } -} +} // namespace impl diff --git a/src/gnmi/subscribe.h b/src/gnmi/subscribe.h index 14505a7..3285cee 100644 --- a/src/gnmi/subscribe.h +++ b/src/gnmi/subscribe.h @@ -15,83 +15,79 @@ * limitations under the License. */ -#ifndef _GNMI_SUBSCRIBE_H -#define _GNMI_SUBSCRIBE_H +#pragma once -#include #include -#include #include "encode/encode.h" #include "utils/sysrepo.h" +#include +#include -using namespace gnmi; -using google::protobuf::RepeatedPtrField; -using grpc::ServerReaderWriter; -using grpc::ServerContext; -using grpc::Status; -using grpc::StatusCode; - -namespace impl { +namespace impl +{ class SrModuleOnChangeParams; -class Subscribe { +class Subscribe +{ public: - Subscribe(sysrepo::Session sess) - : sr_sess(sess) + Subscribe(sysrepo::Session sess) : sr_sess(sess) { - encodef = std::make_shared(sr_sess); + encodef = std::make_shared(sr_sess); } ~Subscribe() {} - Status run(ServerContext* context, - ServerReaderWriter* stream); + grpc::Status + run(grpc::ServerContext *context, + grpc::ServerReaderWriter *stream); - void streamWorker(ServerContext* context, SubscribeRequest request, - ServerReaderWriter* stream, - boost::asio::io_context &initial_update_io, - boost::asio::io_context &incr_update_io); + void + streamWorker(grpc::ServerContext *context, gnmi::SubscribeRequest request, + grpc::ServerReaderWriter *stream, + boost::asio::io_context &initial_update_io, + boost::asio::io_context &incr_update_io); void triggerSampleUpdate( - ServerContext* context, Subscription &sub, - ServerReaderWriter* stream); - Status BuildSubscribeNotification(Notification *notification, - const SubscriptionList& request, - bool *sample=nullptr); - Status BuildSubscribeNotificationForChanges(Notification *notification, - const SubscriptionList& request, - string& xpath, - sysrepo::Session session); + grpc::ServerContext *context, gnmi::Subscription &sub, + grpc::ServerReaderWriter *stream); + grpc::Status BuildSubscribeNotification(gnmi::Notification *notification, + const gnmi::SubscriptionList &request, + bool *sample = nullptr); + grpc::Status BuildSubscribeNotificationForChanges(gnmi::Notification *notification, + const gnmi::SubscriptionList &request, + std::string &xpath, sysrepo::Session session); // To synchronize write access to the stream - void Write(ServerReaderWriter* stream, - SubscribeResponse response); + void Write(grpc::ServerReaderWriter *stream, + gnmi::SubscribeResponse response); // To synchronize posting a write to the stream - void PostWrite(ServerReaderWriter* stream, - std::unique_ptr response, boost::asio::io_context &io); + void + PostWrite(grpc::ServerReaderWriter *stream, + std::unique_ptr response, boost::asio::io_context &io); + private: - Status BuildSubsUpdate(RepeatedPtrField* updateList, - const Path &prefix, string fullpath, - gnmi::Encoding encoding); - Status registerStreamOnChange( - SubscribeRequest &request, Subscription sub, - ServerReaderWriter* stream, - boost::asio::io_context &initial_update_io_context, - boost::asio::io_context &incr_update_io_context, - shared_ptr sr_sub, - vector ¶ms_vec); - Status handleStream(ServerContext* context, SubscribeRequest request, - ServerReaderWriter* stream); - Status handleOnce(SubscribeRequest request, - ServerReaderWriter* stream); - Status handlePoll(SubscribeRequest request, - ServerReaderWriter* stream); + grpc::Status BuildSubsUpdate(google::protobuf::RepeatedPtrField *updateList, + const gnmi::Path &prefix, std::string fullpath, + gnmi::Encoding encoding); + grpc::Status registerStreamOnChange( + gnmi::SubscribeRequest &request, gnmi::Subscription sub, + grpc::ServerReaderWriter *stream, + boost::asio::io_context &initial_update_io_context, + boost::asio::io_context &incr_update_io_context, std::shared_ptr sr_sub, + std::vector ¶ms_vec); + grpc::Status + handleStream(grpc::ServerContext *context, gnmi::SubscribeRequest request, + grpc::ServerReaderWriter *stream); + grpc::Status + handleOnce(gnmi::SubscribeRequest request, + grpc::ServerReaderWriter *stream); + grpc::Status + handlePoll(gnmi::SubscribeRequest request, + grpc::ServerReaderWriter *stream); private: - sysrepo::Session sr_sess; //sysrepo session - std::shared_ptr encodef; //support for json ietf encoding + sysrepo::Session sr_sess; // sysrepo session + std::shared_ptr encodef; // support for json ietf encoding std::recursive_mutex stream_mutex; }; -} - -#endif //_GNMI_SUBSCRIBE_H +} // namespace impl diff --git a/src/main.cpp b/src/main.cpp index abeb228..9e042e7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,115 +17,118 @@ #include +#include +#include + #include #include #include "gnmi/gnmi.h" -using namespace std; - -static void show_usage(string name) +static void show_usage(std::string name) { - cerr << "Usage: " << name << " \n" - << "Options:\n" - << "\t-h,--help\t\t\tShow this help message\n" - << "\t-u,--username USERNAME\t\tDefine connection username\n" - << "\t-p,--password PASSWORD\t\tDefine connection password\n" - << "\t-f,--force-insecure\t\tNo TLS connection, no password authentication\n" - << "\t-k,--private-key PRIVATE_KEY\tpath to server TLS private key\n" - << "\t-c,--cert CERTIFICATE\tpath to server TLS certificate\n" - << "\t-r,--ca CERTIFICATE\tpath to root certificate/CA certificate\n" - << "\t-l,--log-level LOG_LEVEL\tLog level\n" - << "\t\t 0 = all logging turned off\n" - << "\t\t 1 = log only error messages\n" - << "\t\t 2 = (default) log error and warning messages\n" - << "\t\t 3 = log error, warning and informational messages\n" - << "\t\t 4 = log everything, including development debug messages\n" - << "\t-b,--bind URI\t\t\tBind to an URI\n" - << "\t\t URI = PREFIX://IP:PORT\n" - << "\t\t URI = IP:PORT, default to dns:// prefix\n" - << "\t\t URI = IP, default to dns:// prefix and port 443\n" - << endl; + std::cerr << "Usage: " << name << " \n" + << "Options:\n" + << "\t-h,--help\t\t\tShow this help message\n" + << "\t-u,--username USERNAME\t\tDefine connection username\n" + << "\t-p,--password PASSWORD\t\tDefine connection password\n" + << "\t-f,--force-insecure\t\tNo TLS connection, no password authentication\n" + << "\t-k,--private-key PRIVATE_KEY\tpath to server TLS private key\n" + << "\t-c,--cert CERTIFICATE\tpath to server TLS certificate\n" + << "\t-r,--ca CERTIFICATE\tpath to root certificate/CA certificate\n" + << "\t-l,--log-level LOG_LEVEL\tLog level\n" + << "\t\t 0 = all logging turned off\n" + << "\t\t 1 = log only error messages\n" + << "\t\t 2 = (default) log error and warning messages\n" + << "\t\t 3 = log error, warning and informational messages\n" + << "\t\t 4 = log everything, including development debug messages\n" + << "\t-b,--bind URI\t\t\tBind to an URI\n" + << "\t\t URI = PREFIX://IP:PORT\n" + << "\t\t URI = IP:PORT, default to dns:// prefix\n" + << "\t\t URI = IP, default to dns:// prefix and port 443\n" + << std::endl; } -int main (int argc, char* argv[]) { - int c; - extern char *optarg; - int option_index = 0; - string bind_addr = "localhost:50051"; - string username, password; - AuthBuilder auth; - auto log = Log(); +int main(int argc, char *argv[]) +{ + int c; + extern char *optarg; + int option_index = 0; + std::string bind_addr = "localhost:50051"; + std::string username, password; + AuthBuilder auth; + Log log = Log(); - static struct option long_options[] = - { - {"help", no_argument, 0, 'h'}, - {"log-level", required_argument, 0, 'l'}, //log level - {"username", required_argument, 0, 'u'}, - {"password", required_argument, 0, 'p'}, - {"private-key", required_argument, 0, 'k'}, //private key - {"cert", required_argument, 0, 'c'}, //certificate chain - {"ca", required_argument, 0, 'r'}, //certificate chain - {"force-insecure", no_argument, 0, 'f'}, //insecure mode - {"bind", required_argument, 0, 'b'}, //insecure mode - {0, 0, 0, 0} - }; + static struct option long_options[] = { + {"help", no_argument, 0, 'h'}, + {"log-level", required_argument, 0, 'l'}, // log level + {"username", required_argument, 0, 'u'}, + {"password", required_argument, 0, 'p'}, + {"private-key", required_argument, 0, 'k'}, // private key + {"cert", required_argument, 0, 'c'}, // certificate chain + {"ca", required_argument, 0, 'r'}, // certificate chain + {"force-insecure", no_argument, 0, 'f'}, // insecure mode + {"bind", required_argument, 0, 'b'}, // insecure mode + {0, 0, 0, 0}}; - /* - * An option character followed by ('') indicates no argument - * An option character followed by (‘:’) indicates a required argument. - * An option character is followed by (‘::’) indicates an optional argument. - * Here: no argument after (h,f) ; mandatory argument after (p,u,l,b,c,k,r) - */ - while ((c = getopt_long(argc, argv, "hfl:p:u:c:k:r:b:", long_options, &option_index)) - != -1) { - switch (c) + /* + * An option character followed by ('') indicates no argument + * An option character followed by (‘:’) indicates a required argument. + * An option character is followed by (‘::’) indicates an optional argument. + * Here: no argument after (h,f) ; mandatory argument after (p,u,l,b,c,k,r) + */ + while ((c = getopt_long(argc, argv, "hfl:p:u:c:k:r:b:", long_options, &option_index)) != -1) { - case '?': //help - case 'h': - show_usage(argv[0]); - exit(0); - break; - case 'u': //username - auth.setUsername(string(optarg)); - break; - case 'p': //password - auth.setPassword(string(optarg)); - break; - case 'k': //server private key - auth.setKeyPath(string(optarg)); - break; - case 'c': //server certificate - auth.setCertPath(string(optarg)); - break; - case 'r': //CA/root certificate - auth.setRootCertPath(string(optarg)); - break; - case 'l': //log level - log.setLevel(atoi(optarg)); - break; - case 'b': //binding address - bind_addr = optarg; - break; - case 'f': //force insecure connection - auth.setInsecure(true); - break; - default: /* You won't get there */ - exit(1); + switch (c) + { + case '?': // help + case 'h': + show_usage(argv[0]); + exit(0); + break; + case 'u': // username + auth.setUsername(std::string(optarg)); + break; + case 'p': // password + auth.setPassword(std::string(optarg)); + break; + case 'k': // server private key + auth.setKeyPath(std::string(optarg)); + break; + case 'c': // server certificate + auth.setCertPath(std::string(optarg)); + break; + case 'r': // CA/root certificate + auth.setRootCertPath(std::string(optarg)); + break; + case 'l': // log level + log.setLevel(std::atoi(optarg)); + break; + case 'b': // binding address + bind_addr = optarg; + break; + case 'f': // force insecure connection + auth.setInsecure(true); + break; + default: /* You won't get there */ + exit(1); + } } - } - SetupSignalHandler(); + SetupSignalHandler(); - try { - sysrepo::Connection sr_con = sysrepo::Connection(); + try + { + sysrepo::Connection sr_con = sysrepo::Connection(); - // start the gnmi server - RunServer(bind_addr, auth.build(), sr_con); - } catch (sysrepo::ErrorWithCode &exc) { - BOOST_LOG_TRIVIAL(error) << "Connection to sysrepo failed " << exc.what(); - exit(1); - } + // start the gnmi server + RunServer(bind_addr, auth.build(), sr_con); + } + catch (sysrepo::ErrorWithCode &exc) + { + BOOST_LOG_TRIVIAL(error) << "Connection to sysrepo failed " << exc.what(); + exit(1); + } - return 0; + return 0; } diff --git a/src/security/authentication.cpp b/src/security/authentication.cpp index 5ca4599..2fa2466 100644 --- a/src/security/authentication.cpp +++ b/src/security/authentication.cpp @@ -17,32 +17,28 @@ #include -#include +#include "utils/log.h" #include "authentication.h" -using namespace std; -using grpc::Status; -using grpc::StatusCode; - /* GetFileContent - Get an entier File content * @param Path to the file * @return String containing the entire File. */ -static string GetFileContent(string path) +static std::string GetFileContent(std::string path) { - ifstream ifs(path); - if (!ifs) { - BOOST_LOG_TRIVIAL(fatal) << "File " << path << " not found"; - exit(1); - } + std::ifstream ifs(path); + if (!ifs) + { + BOOST_LOG_TRIVIAL(fatal) << "File " << path << " not found"; + exit(1); + } - string content((istreambuf_iterator(ifs)), - (istreambuf_iterator())); + std::string content((std::istreambuf_iterator(ifs)), (std::istreambuf_iterator())); - ifs.close(); + ifs.close(); - return content; + return content; } /* SslCredentialsHelper - @@ -52,147 +48,151 @@ static string GetFileContent(string path) * @param client_cert boolean to activate/deactivate client certificate check * @return ServerCredentials for grpc service creation */ -static shared_ptr -SslCredentialsHelper(string ppath, string cpath, string rpath, bool client_cert) +static std::shared_ptr +SslCredentialsHelper(std::string ppath, std::string cpath, std::string rpath, bool client_cert) { - SslServerCredentialsOptions ssl_opts; - - if (client_cert) - ssl_opts.client_certificate_request = - GRPC_SSL_REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY; - else - ssl_opts.client_certificate_request = - GRPC_SSL_DONT_REQUEST_CLIENT_CERTIFICATE; - - SslServerCredentialsOptions::PemKeyCertPair pkcp = { - GetFileContent(ppath), - GetFileContent(cpath) - }; - - ssl_opts.pem_key_cert_pairs.push_back(pkcp); - - // Require client root certificates to avoid grpc bug in versions < 1.18.0 - // (https://github.com/grpc/grpc/pull/17500) - if (rpath.empty()) { - BOOST_LOG_TRIVIAL(fatal) << "Client root certificates must be specified"; - exit(1); - } - - ssl_opts.pem_root_certs = GetFileContent(rpath); - - return grpc::SslServerCredentials(ssl_opts); + grpc::SslServerCredentialsOptions ssl_opts; + + if (client_cert) + { + ssl_opts.client_certificate_request = + GRPC_SSL_REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY; + } + else + { + ssl_opts.client_certificate_request = GRPC_SSL_DONT_REQUEST_CLIENT_CERTIFICATE; + } + + grpc::SslServerCredentialsOptions::PemKeyCertPair pkcp = {GetFileContent(ppath), + GetFileContent(cpath)}; + + ssl_opts.pem_key_cert_pairs.push_back(pkcp); + + // Require client root certificates to avoid grpc bug in versions < 1.18.0 + // (https://github.com/grpc/grpc/pull/17500) + if (rpath.empty()) + { + BOOST_LOG_TRIVIAL(fatal) << "Client root certificates must be specified"; + exit(1); + } + + ssl_opts.pem_root_certs = GetFileContent(rpath); + + return grpc::SslServerCredentials(ssl_opts); } /* Get Server Credentials according to Scurity Context policy */ -shared_ptr AuthBuilder::build() +std::shared_ptr AuthBuilder::build() { - shared_ptr cred; - - // MUTUAL_TLS - if (!private_key_path.empty() && !cert_path.empty() - && username.empty() && password.empty()) { - BOOST_LOG_TRIVIAL(info) << "Mutual TLS authentication"; - return SslCredentialsHelper(private_key_path, cert_path, root_cert_path, true); - } - - // USERPASS_TLS - if (!private_key_path.empty() && !cert_path.empty() - && !username.empty() && !password.empty()) { - BOOST_LOG_TRIVIAL(info) << "Username/Password over TLS authentication"; - cred = SslCredentialsHelper(private_key_path, cert_path, root_cert_path, false); - cred->SetAuthMetadataProcessor( - make_shared(username, password)); - return cred; - } - - //INSECURE - if (insecure) { - BOOST_LOG_TRIVIAL(info) << "Insecure authentication"; - return grpc::InsecureServerCredentials(); - } - - /* impossible scenario */ - if (private_key_path.empty() && cert_path.empty() - && !username.empty() && !password.empty()) - BOOST_LOG_TRIVIAL(fatal) << "Impossible to use user/pass auth with" - << " insecure connection"; - - - BOOST_LOG_TRIVIAL(fatal) << "Unsupported Authentication method"; - - exit(1); + std::shared_ptr cred; + + // MUTUAL_TLS + if (!private_key_path.empty() && !cert_path.empty() && username.empty() && password.empty()) + { + BOOST_LOG_TRIVIAL(info) << "Mutual TLS authentication"; + return SslCredentialsHelper(private_key_path, cert_path, root_cert_path, true); + } + + // USERPASS_TLS + if (!private_key_path.empty() && !cert_path.empty() && !username.empty() && !password.empty()) + { + BOOST_LOG_TRIVIAL(info) << "Username/Password over TLS authentication"; + cred = SslCredentialsHelper(private_key_path, cert_path, root_cert_path, false); + cred->SetAuthMetadataProcessor(std::make_shared(username, password)); + return cred; + } + + // INSECURE + if (insecure) + { + BOOST_LOG_TRIVIAL(info) << "Insecure authentication"; + return grpc::InsecureServerCredentials(); + } + + /* impossible scenario */ + if (private_key_path.empty() && cert_path.empty() && !username.empty() && !password.empty()) + BOOST_LOG_TRIVIAL(fatal) << "Impossible to use user/pass auth with" + << " insecure connection"; + + BOOST_LOG_TRIVIAL(fatal) << "Unsupported Authentication method"; + + exit(1); } -AuthBuilder& AuthBuilder::setKeyPath(string keyPath) +AuthBuilder &AuthBuilder::setKeyPath(std::string keyPath) { - private_key_path = keyPath; - return *this; + private_key_path = keyPath; + return *this; } -AuthBuilder& AuthBuilder::setCertPath(string certPath) +AuthBuilder &AuthBuilder::setCertPath(std::string certPath) { - cert_path = certPath; - return *this; + cert_path = certPath; + return *this; } -AuthBuilder& AuthBuilder::setRootCertPath(string rootpath) +AuthBuilder &AuthBuilder::setRootCertPath(std::string rootpath) { - root_cert_path = rootpath; - return *this; + root_cert_path = rootpath; + return *this; } -AuthBuilder& AuthBuilder::setUsername(string user) +AuthBuilder &AuthBuilder::setUsername(std::string user) { - username = user; - return *this; + username = user; + return *this; } -AuthBuilder& AuthBuilder::setPassword(string pass) +AuthBuilder &AuthBuilder::setPassword(std::string pass) { - password = pass; - return *this; + password = pass; + return *this; } -AuthBuilder& AuthBuilder::setInsecure(bool mode) +AuthBuilder &AuthBuilder::setInsecure(bool mode) { - insecure = mode; - return *this; + insecure = mode; + return *this; } /* Implement a MetadataProcessor for username/password authentication */ -Status UserPassAuthenticator::Process(const InputMetadata& auth_metadata, - grpc::AuthContext* context, - OutputMetadata* consumed_auth_metadata, - OutputMetadata* response_metadata) +grpc::Status UserPassAuthenticator::Process(const InputMetadata &auth_metadata, + grpc::AuthContext *context, + OutputMetadata *consumed_auth_metadata, + OutputMetadata *response_metadata) { - (void)context; (void)response_metadata; //Unused - - /* Look for username/password fields in Metadata sent by client */ - auto user_kv = auth_metadata.find("username"); - if (user_kv == auth_metadata.end()) { - BOOST_LOG_TRIVIAL(error) << "No username field"; - return Status(StatusCode::UNAUTHENTICATED, "No username field"); - } - auto pass_kv = auth_metadata.find("password"); - if (pass_kv == auth_metadata.end()) { - BOOST_LOG_TRIVIAL(error) << "No password field"; - return Status(StatusCode::UNAUTHENTICATED, "No password field"); - } - - /* test if username and password are good */ - if ( password.compare(pass_kv->second.data()) != 0 || - username.compare(user_kv->second.data()) != 0 ) { - BOOST_LOG_TRIVIAL(error) << "Invalid username/password"; - return Status(StatusCode::UNAUTHENTICATED, "Invalid username/password"); - } - - /* Remove username and password key-value from metadata */ - consumed_auth_metadata->insert(make_pair( - string(user_kv->first.data(), user_kv->first.length()), - string(user_kv->second.data(), user_kv->second.length()))); - consumed_auth_metadata->insert(make_pair( - string(pass_kv->first.data(), pass_kv->first.length()), - string(pass_kv->second.data(), pass_kv->second.length()))); - - return Status::OK; + (void)context; + (void)response_metadata; // Unused + + /* Look for username/password fields in Metadata sent by client */ + auto user_kv = auth_metadata.find("username"); + if (user_kv == auth_metadata.end()) + { + BOOST_LOG_TRIVIAL(error) << "No username field"; + return grpc::Status(grpc::StatusCode::UNAUTHENTICATED, "No username field"); + } + auto pass_kv = auth_metadata.find("password"); + if (pass_kv == auth_metadata.end()) + { + BOOST_LOG_TRIVIAL(error) << "No password field"; + return grpc::Status(grpc::StatusCode::UNAUTHENTICATED, "No password field"); + } + + /* test if username and password are good */ + if (password.compare(pass_kv->second.data()) != 0 || + username.compare(user_kv->second.data()) != 0) + { + BOOST_LOG_TRIVIAL(error) << "Invalid username/password"; + return grpc::Status(grpc::StatusCode::UNAUTHENTICATED, "Invalid username/password"); + } + + /* Remove username and password key-value from metadata */ + consumed_auth_metadata->insert( + std::make_pair(std::string(user_kv->first.data(), user_kv->first.length()), + std::string(user_kv->second.data(), user_kv->second.length()))); + consumed_auth_metadata->insert( + std::make_pair(std::string(pass_kv->first.data(), pass_kv->first.length()), + std::string(pass_kv->second.data(), pass_kv->second.length()))); + + return grpc::Status::OK; } diff --git a/src/security/authentication.h b/src/security/authentication.h index d3f319f..c92bf10 100644 --- a/src/security/authentication.h +++ b/src/security/authentication.h @@ -14,62 +14,57 @@ * limitations under the License. */ -#include #include - -using grpc::ServerCredentials; -using grpc::SslServerCredentialsOptions; -using grpc::Status; +#include /* * Authenticate request with username/password comparaison * by using metadata fields. */ -class UserPassAuthenticator final : public grpc::AuthMetadataProcessor { +class UserPassAuthenticator final : public grpc::AuthMetadataProcessor +{ public: - UserPassAuthenticator(std::string user, std::string pass) - : username(user), password(pass) {} + UserPassAuthenticator(std::string user, std::string pass) : username(user), password(pass) {} ~UserPassAuthenticator() {} - Status Process(const InputMetadata& auth_metadata, - grpc::AuthContext* context, - OutputMetadata* consumed_auth_metadata, - OutputMetadata* response_metadata) override; + grpc::Status Process(const InputMetadata &auth_metadata, grpc::AuthContext *context, + OutputMetadata *consumed_auth_metadata, + OutputMetadata *response_metadata) override; private: std::string username, password; }; - /* Supported Authentication methods */ -enum AuthType { - INSECURE, // No Username/password, no encryption - USERPASS_TLS, // Username/password, TLS encryption - MUTUAL_TLS // TLS authentication & encryption +enum AuthType +{ + INSECURE, // No Username/password, no encryption + USERPASS_TLS, // Username/password, TLS encryption + MUTUAL_TLS // TLS authentication & encryption }; -class AuthBuilder { +class AuthBuilder +{ public: AuthBuilder() {} ~AuthBuilder() {} /* Return ServerCredentials according to enum EncryptType*/ - std::shared_ptr build(); + std::shared_ptr build(); /* TLS */ - AuthBuilder& setKeyPath(std::string keyPath); - AuthBuilder& setCertPath(std::string certPath); - AuthBuilder& setRootCertPath(std::string rootPath); + AuthBuilder &setKeyPath(std::string keyPath); + AuthBuilder &setCertPath(std::string certPath); + AuthBuilder &setRootCertPath(std::string rootPath); /* Username/password */ - AuthBuilder& setUsername(std::string username); - AuthBuilder& setPassword(std::string password); + AuthBuilder &setUsername(std::string username); + AuthBuilder &setPassword(std::string password); - AuthBuilder& setInsecure(bool mode); + AuthBuilder &setInsecure(bool mode); private: - std::string private_key_path, cert_path, root_cert_path; //SSL + std::string private_key_path, cert_path, root_cert_path; // SSL std::string username, password; bool insecure = false; }; - diff --git a/src/utils/log.cpp b/src/utils/log.cpp index 5039a21..7c13fb9 100644 --- a/src/utils/log.cpp +++ b/src/utils/log.cpp @@ -17,263 +17,288 @@ #define BOOST_LOG_USE_NATIVE_SYSLOG -#include +#include +#include #include #include +#include +#include +#include -#include #include #include #include -#include -#include -#include -#include +#include #include -#include #include +#include #include "log.h" -using namespace logging::sinks; - extern "C" void signal_handler(int signum); -static const char* display_data_log_env = "GNMI_DISPLAY_DATA_LOG"; +static const char *display_data_log_env = "GNMI_DISPLAY_DATA_LOG"; static bool display_data_log = true; static void _boost_set_log_level(int lvl) { - logging::trivial::severity_level l; - - switch (lvl) { - case 0: //no log - l = logging::trivial::fatal; - break; - case 1: //only error message - l = logging::trivial::error; - break; - case 2: //log error and warning - l = logging::trivial::warning; - break; - case 3: //log error, warning, and informational - l = logging::trivial::info; - break; - case 4: //log error, warning, informational and debug - l = logging::trivial::debug; - break; + boost::log::trivial::severity_level l; + + switch (lvl) + { + case 0: // no log + l = boost::log::trivial::fatal; + break; + case 1: // only error message + l = boost::log::trivial::error; + break; + case 2: // log error and warning + l = boost::log::trivial::warning; + break; + case 3: // log error, warning, and informational + l = boost::log::trivial::info; + break; + case 4: // log error, warning, informational and debug + l = boost::log::trivial::debug; + break; default: - std::cerr << "Unused log level" << std::endl; - exit(1); - } + std::cerr << "Unused log level" << std::endl; + exit(1); + } - logging::core::get()->set_filter( - logging::trivial::severity >= l - ); + boost::log::core::get()->set_filter(boost::log::trivial::severity >= l); } static void sysrepo_log_cb(sr_log_level_t level, const char *message) { - switch (level) { + switch (level) + { case SR_LL_ERR: - BOOST_LOG_TRIVIAL(error) << "[" << gettid() << "] " << message; - break; + BOOST_LOG_TRIVIAL(error) << "[" << gettid() << "] " << message; + break; case SR_LL_WRN: - BOOST_LOG_TRIVIAL(warning) << "[" << gettid() << "] " << message; - break; + BOOST_LOG_TRIVIAL(warning) << "[" << gettid() << "] " << message; + break; case SR_LL_INF: - /* Log at info at debug level to avoid sending sysrepo logs to the OSS. */ + /* Log at info at debug level to avoid sending sysrepo logs to the OSS. */ case SR_LL_DBG: - BOOST_LOG_TRIVIAL(debug) << "[" << gettid() << "] " << message; - break; + BOOST_LOG_TRIVIAL(debug) << "[" << gettid() << "] " << message; + break; default: - break; - } + break; + } } -static void libyang_log_cb(LY_LOG_LEVEL level, const char *message, - const char *data_path, const char *schema_path, uint64_t _line) +static void libyang_log_cb(LY_LOG_LEVEL level, const char *message, const char *data_path, + const char *schema_path, uint64_t _line) { - (void)_line; - std::string path_message = ""; - if (data_path || schema_path) { - path_message = " (path: " + std::string(data_path ? data_path : schema_path) + ")"; - } + (void)_line; + std::string path_message = ""; + if (data_path || schema_path) + { + path_message = " (path: " + std::string(data_path ? data_path : schema_path) + ")"; + } - switch (level) { + switch (level) + { case LY_LLERR: - BOOST_LOG_TRIVIAL(error) << message << path_message; - break; + BOOST_LOG_TRIVIAL(error) << message << path_message; + break; case LY_LLWRN: - BOOST_LOG_TRIVIAL(warning) << message << path_message; - break; + BOOST_LOG_TRIVIAL(warning) << message << path_message; + break; case LY_LLVRB: - /* Log at info at debug level to avoid sending libyang logs to the OSS. */ + /* Log at info at debug level to avoid sending libyang logs to the OSS. */ case LY_LLDBG: - BOOST_LOG_TRIVIAL(debug) << message << path_message; - break; + BOOST_LOG_TRIVIAL(debug) << message << path_message; + break; default: - break; - } + break; + } } Log::Log(int lvl) { - setLevel(lvl); + setLevel(lvl); } void Log::setSyslogBackend() { - boost::shared_ptr core = logging::core::get(); - - boost::shared_ptr backend(new syslog_backend( - logging::keywords::facility = syslog::user, - logging::keywords::use_impl = syslog::native - )); - - auto severity_mapping = syslog::custom_severity_mapping("Severity"); - severity_mapping[logging::trivial::error] = syslog::error; - severity_mapping[logging::trivial::warning] = syslog::warning; - severity_mapping[logging::trivial::info] = syslog::info; - severity_mapping[logging::trivial::debug] = syslog::debug; - backend->set_severity_mapper(severity_mapping); - - core->add_sink(boost::make_shared>(backend)); + boost::shared_ptr core = boost::log::core::get(); + + boost::shared_ptr backend( + new boost::log::sinks::syslog_backend( + boost::log::keywords::facility = boost::log::sinks::syslog::user, + boost::log::keywords::use_impl = boost::log::sinks::syslog::native)); + + auto severity_mapping = + boost::log::sinks::syslog::custom_severity_mapping( + "Severity"); + severity_mapping[boost::log::trivial::error] = boost::log::sinks::syslog::error; + severity_mapping[boost::log::trivial::warning] = boost::log::sinks::syslog::warning; + severity_mapping[boost::log::trivial::info] = boost::log::sinks::syslog::info; + severity_mapping[boost::log::trivial::debug] = boost::log::sinks::syslog::debug; + backend->set_severity_mapper(severity_mapping); + + core->add_sink( + boost::make_shared>( + backend)); } void Log::setLevel(int lvl) { - _boost_set_log_level(lvl); + _boost_set_log_level(lvl); - //Libyang log level should be ERROR only - ly_log_level(LY_LLERR); - sr_log_set_cb(sysrepo_log_cb); - ly_set_log_clb(libyang_log_cb); + // Libyang log level should be ERROR only + ly_log_level(LY_LLERR); + sr_log_set_cb(sysrepo_log_cb); + ly_set_log_clb(libyang_log_cb); } void get_log_env(void) { - const char* var = std::getenv(display_data_log_env); - - if (var) { - std::string value(var); - if (value == "Y" || value == "YES" || value == "y" || value == "yes") { - display_data_log = true; - } else if (value == "N" || value == "NO" || value == "n" || value == "no") { - display_data_log = false; - } else { - BOOST_LOG_TRIVIAL(warning) << "Unrecognized value for " << display_data_log_env << ":" << value; + const char *var = std::getenv(display_data_log_env); + + if (var) + { + std::string value(var); + if (value == "Y" || value == "YES" || value == "y" || value == "yes") + { + display_data_log = true; + } + else if (value == "N" || value == "NO" || value == "n" || value == "no") + { + display_data_log = false; + } + else + { + BOOST_LOG_TRIVIAL(warning) + << "Unrecognized value for " << display_data_log_env << ":" << value; + } } - } - BOOST_LOG_TRIVIAL(debug) << "Logging of GNMI data is " << (display_data_log ? "ENABLED" : "DISABLED"); + BOOST_LOG_TRIVIAL(debug) << "Logging of GNMI data is " + << (display_data_log ? "ENABLED" : "DISABLED"); } -const char* obfs_data(std::string &data) +const char *obfs_data(std::string &data) { - if (display_data_log) { - return data.c_str(); - } else { - return "&*%#"; - } + if (display_data_log) + { + return data.c_str(); + } + else + { + return "&*%#"; + } } static void trigger_logrotate(void) { - auto wstatus = system(GNMI_LOGROTATE_SCRIPT " " GNMI_LOG_DIR); - - // During execution of the command, SIGCHLD will be blocked, and SIGINT and SIGQUIT will be ignored, - // in the process that calls system(). - // (These signals will be handled according to their defaults inside the child process that executes command.) - // - // So, check if logrotate was signalled, and pass it along to our own handler. - - if (WIFSIGNALED(wstatus)) { - auto signal = WTERMSIG(wstatus); - BOOST_LOG_TRIVIAL(info) << "log-rotate was signalled " << strsignal(signal); - if (signal == SIGTERM || signal == SIGINT) { - // pass the signal to the handler thread so we can terminate as well - signal_handler(signal); + auto wstatus = system(GNMI_LOGROTATE_SCRIPT " " GNMI_LOG_DIR); + + // During execution of the command, SIGCHLD will be blocked, and SIGINT and SIGQUIT will be + // ignored, in the process that calls system(). (These signals will be handled according to + // their defaults inside the child process that executes command.) + // + // So, check if logrotate was signalled, and pass it along to our own handler. + + if (WIFSIGNALED(wstatus)) + { + auto signal = WTERMSIG(wstatus); + BOOST_LOG_TRIVIAL(info) << "log-rotate was signalled " << strsignal(signal); + if (signal == SIGTERM || signal == SIGINT) + { + // pass the signal to the handler thread so we can terminate as well + signal_handler(signal); + } + } + else if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus)) + { + BOOST_LOG_TRIVIAL(error) << "log-rotate failed with " << WEXITSTATUS(wstatus); } - } else if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus)) { - BOOST_LOG_TRIVIAL(error) << "log-rotate failed with " << WEXITSTATUS(wstatus); - } } void log_to_file(const std::string data, std::string metadata, const uint64_t id) { - using namespace std::chrono; - const size_t MAX_FILE_SIZE = 500 * 1024; + using namespace std::chrono; + const size_t MAX_FILE_SIZE = 500 * 1024; - static std::atomic size; + static std::atomic size; - if (!display_data_log) { - // Not writing any data to honor configured obfuscation - BOOST_LOG_TRIVIAL(debug) << metadata << " data obfuscated"; - return; - } + if (!display_data_log) + { + // Not writing any data to honor configured obfuscation + BOOST_LOG_TRIVIAL(debug) << metadata << " data obfuscated"; + return; + } - if (!id) { - // There is no log id, so keep it in the journal - BOOST_LOG_TRIVIAL(debug) << metadata << ": " << data; - return; - } + if (!id) + { + // There is no log id, so keep it in the journal + BOOST_LOG_TRIVIAL(debug) << metadata << ": " << data; + return; + } - { - auto now = boost::posix_time::microsec_clock::local_time(); - auto timestamp = boost::posix_time::to_iso_string(now); + { + auto now = boost::posix_time::microsec_clock::local_time(); + auto timestamp = boost::posix_time::to_iso_string(now); - auto filename = std::string(GNMI_LOG_DIR) + "/raw/" + timestamp + "-pid." + std::to_string(getpid()) - + "-transaction." + std::to_string(id); + auto filename = std::string(GNMI_LOG_DIR) + "/raw/" + timestamp + "-pid." + + std::to_string(getpid()) + "-transaction." + std::to_string(id); - auto tmpfile = filename + ".tmp"; + auto tmpfile = filename + ".tmp"; - auto fp = fopen(tmpfile.c_str(), "w"); - if (!fp) { - throw std::runtime_error("Failed to open tmpfile " + tmpfile); - } + auto fp = fopen(tmpfile.c_str(), "w"); + if (!fp) + { + throw std::runtime_error("Failed to open tmpfile " + tmpfile); + } - if (flock(fileno(fp), LOCK_NB | LOCK_EX) == -1) { - fclose(fp); - throw std::runtime_error("Failed to acquire flock on " + tmpfile); - } + if (flock(fileno(fp), LOCK_NB | LOCK_EX) == -1) + { + fclose(fp); + throw std::runtime_error("Failed to acquire flock on " + tmpfile); + } - auto livefile = filename + ".live"; + auto livefile = filename + ".live"; - std::filesystem::rename(tmpfile, livefile); + std::filesystem::rename(tmpfile, livefile); - timestamp = boost::posix_time::to_simple_string(now); + timestamp = boost::posix_time::to_simple_string(now); - // JSON doesn't like unescaped double quotes - escape them - boost::algorithm::replace_all(metadata, "\"", "\\\""); + // JSON doesn't like unescaped double quotes - escape them + boost::algorithm::replace_all(metadata, "\"", "\\\""); - // quote data if it is not already JSON - auto json_data = data; - switch (data.front()) { - case '{': - case '"': - case '[': - // looks like valid JSON data. - break; - default: - json_data = "\"" + data + "\""; - break; - } + // quote data if it is not already JSON + auto json_data = data; + switch (data.front()) + { + case '{': + case '"': + case '[': + // looks like valid JSON data. + break; + default: + json_data = "\"" + data + "\""; + break; + } - auto bytes_written = fprintf(fp, "{\"timestamp\": \"%s\", \"metadata\":\"%s\", \"data\": %s}", - timestamp.c_str(), metadata.c_str(), json_data.c_str()); + auto bytes_written = + fprintf(fp, "{\"timestamp\": \"%s\", \"metadata\":\"%s\", \"data\": %s}", + timestamp.c_str(), metadata.c_str(), json_data.c_str()); - std::filesystem::rename(livefile, filename + ".log"); + std::filesystem::rename(livefile, filename + ".log"); - // unlock the flock by closing file pointer - fclose(fp); + // unlock the flock by closing file pointer + fclose(fp); - size.fetch_add(bytes_written); - } + size.fetch_add(bytes_written); + } - auto expected = size.load(); + auto expected = size.load(); - if ((expected > MAX_FILE_SIZE) && size.compare_exchange_weak(expected, 0)) { - std::thread(trigger_logrotate).detach(); - } + if ((expected > MAX_FILE_SIZE) && size.compare_exchange_weak(expected, 0)) + { + std::thread(trigger_logrotate).detach(); + } } - diff --git a/src/utils/log.h b/src/utils/log.h index 179f4a5..1ad7de0 100644 --- a/src/utils/log.h +++ b/src/utils/log.h @@ -15,16 +15,13 @@ * limitations under the License. */ -#ifndef _LOG_H -#define _LOG_H +#pragma once #include -#include #include +#include #include -namespace logging = boost::log; - /* * Pick your severity * BOOST_LOG_TRIVIAL(trace) << "A trace severity message"; @@ -33,8 +30,9 @@ namespace logging = boost::log; * BOOST_LOG_TRIVIAL(warning) << "A warning severity message"; * BOOST_LOG_TRIVIAL(error) << "An error severity message"; * BOOST_LOG_TRIVIAL(fatal) << "A fatal severity message"; -*/ -class Log { + */ +class Log +{ public: /* * lvl 0 : fatal @@ -59,8 +57,6 @@ void get_log_env(void); * Returns the data as a char* if displaying of data in logs is enabled * else it "obfuscates" the data */ -const char* obfs_data(std::string& data); +const char *obfs_data(std::string &data); void log_to_file(const std::string data, std::string metadata, const uint64_t log_id); - -#endif // _LOG_H diff --git a/src/utils/sysrepo.cpp b/src/utils/sysrepo.cpp index 0a18e45..71aad3f 100644 --- a/src/utils/sysrepo.cpp +++ b/src/utils/sysrepo.cpp @@ -14,44 +14,86 @@ * limitations under the License. */ +#include +#include +#include #include +#include #include #include -#include #include "utils/sysrepo.h" #define SR_YANG_MOD "sysrepo" -static std::vector -collect_xpath_mods(libyang::Context ly_ctx, const char *xpath) +void UpdateTransaction::merge(std::optional &tree, + std::optional &node) { - std::vector mod_set; - libyang::Module *ly_mod_ptr = nullptr; - auto set = ly_ctx.findXpathAtoms(xpath, 0); + if (node.has_value()) + { + if (tree.has_value()) + { + tree.value().mergeWithSiblings(node.value()); + } + else + { + tree = node; + } + } +} - for (auto node : set) { - auto ly_mod = node.module(); - /* skip already-added modules */ - if (ly_mod_ptr && &ly_mod == ly_mod_ptr) - continue; +void UpdateTransaction::push(std::optional &tree) +{ + if (tree.has_value()) + { + final_tree = final_tree.has_value() ? final_tree->insertSibling(tree.value()) : tree; + } +} - ly_mod_ptr = &ly_mod; +static std::vector collect_xpath_mods(libyang::Context ly_ctx, const char *xpath) +{ + bool skip = 0; + std::vector ly_mod_set; + std::optional parent = std::nullopt; + libyang::Set set = ly_ctx.findXPath(std::string(xpath)); + + for (auto iter = set.begin(); !(iter == set.end()); ++iter) + { + /* get module of the first schema node */ + parent = *iter; + while (parent->parent() != std::nullopt) + { + parent = parent->parent(); + } + auto ly_mod = parent->module(); + + /* skip already added modules */ + skip = 0; + for (libyang::Module m : ly_mod_set) + { + if (ly_mod == m) + { + skip = 1; + break; + } + } + if (skip) + { + continue; + } /* skip import-only modules, and the internal SR_YANG_MOD */ if (!ly_mod.implemented() || ly_mod.name() == SR_YANG_MOD) continue; - mod_set.push_back(ly_mod); + /* add a module to the set */ + ly_mod_set.push_back(ly_mod); } - return mod_set; + return ly_mod_set; } -DataSubscribe::DataSubscribe(sysrepo::Session sess) - : data_sess(sess) -{ -} +DataSubscribe::DataSubscribe(sysrepo::Session sess) : data_sess(sess) {} /* * Similar to sysrepo::Subscribe::module_change_subscribe @@ -60,24 +102,18 @@ DataSubscribe::DataSubscribe(sysrepo::Session sess) * 1. Subscribe::module_change_subscribe requiring a module. * 2. Subscribe::module_change_subscribe not providing user-supplied context to callback function. */ -void DataSubscribe::data_change_subscribe(sysrepo::ModuleChangeCb cb, const char *xpath, uint32_t priority, sysrepo::SubscribeOptions opts) +void DataSubscribe::data_change_subscribe(sysrepo::ModuleChangeCb cb, const char *xpath, + uint32_t priority, sysrepo::SubscribeOptions opts) { - for (auto mod : collect_xpath_mods(data_sess.getContext(), xpath)) { - if (sub) { - sub->onModuleChange( - std::string(mod.name()), - cb, - xpath, - priority, - opts); - } else { - sub = data_sess.onModuleChange( - std::string(mod.name()), - cb, - xpath, - priority, - opts); + for (auto mod : collect_xpath_mods(data_sess.getContext(), xpath)) + { + if (sub) + { + sub->onModuleChange(std::string(mod.name()), cb, xpath, priority, opts); + } + else + { + sub = data_sess.onModuleChange(std::string(mod.name()), cb, xpath, priority, opts); } } } - diff --git a/src/utils/sysrepo.h b/src/utils/sysrepo.h index 3c9de04..6f98c90 100644 --- a/src/utils/sysrepo.h +++ b/src/utils/sysrepo.h @@ -14,44 +14,38 @@ * limitations under the License. */ -#ifndef _UTILS_SYSREPO_H -#define _UTILS_SYSREPO_H +#pragma once #include -#include +#include + #include -#include +#include +#include +#include #include -class UpdateTransaction { +class UpdateTransaction +{ public: - /** Push a node and all its siblings into the transaction */ - void push(libyang::DataNode node) - { - for (auto n : node.siblings()) - push_one(n); - } - - /** Push a node without its siblings into the transaction */ - void push_one(libyang::DataNode node) - { - auto dup = node.duplicate(libyang::DuplicationOptions::Recursive); - first_node = first_node.has_value() ? first_node->insertSibling(dup) : dup; - } - - std::optional first_node; + std::optional final_tree; + + /** Merge a top-level node into a tree */ + void merge(std::optional &tree, std::optional &node); + + /** Push a node and all its siblings into the final transaction tree */ + void push(std::optional &tree); }; class DataSubscribe { -public: + public: DataSubscribe(sysrepo::Session sess); - void data_change_subscribe(sysrepo::ModuleChangeCb cb, const char *xpath, uint32_t priority = 0, sysrepo::SubscribeOptions opts = sysrepo::SubscribeOptions::Default); + void data_change_subscribe(sysrepo::ModuleChangeCb cb, const char *xpath, uint32_t priority = 0, + sysrepo::SubscribeOptions opts = sysrepo::SubscribeOptions::Default); -private: + private: std::optional sub; /* The session is also available in the base class, but it is private so is duplicated here */ sysrepo::Session data_sess; }; - -#endif /* _UTILS_SYSREPO_H */ diff --git a/src/utils/utils.h b/src/utils/utils.h index 77a4a5f..2923f54 100644 --- a/src/utils/utils.h +++ b/src/utils/utils.h @@ -15,155 +15,171 @@ * limitations under the License. */ -#ifndef _UTILS_H -#define _UTILS_H +#pragma once -#include -#include -#include #include - +#include #include - -using std::chrono::system_clock; -using std::chrono::duration_cast; -using std::chrono::nanoseconds; +#include +#include /* Get current time since epoch in nanosec */ inline uint64_t get_time_nanosec() { - nanoseconds ts; - ts = duration_cast(system_clock::now().time_since_epoch()); + std::chrono::nanoseconds ts; + ts = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()); - return ts.count(); + return ts.count(); } // We don't conform to the gNMI spec in that namespaces on paths are // required on input and generated on output, so to signal that deviation // and leave the door open to supporting a gNMI-compliant mode later and // interoperating with standard gNMI clients we enforce the origin to be set -inline void gnmi_check_origin(const gnmi::Path& prefix, const gnmi::Path &path) +inline void gnmi_check_origin(const gnmi::Path &prefix, const gnmi::Path &path) { - if (prefix.elem_size() > 0) { - if (prefix.origin().compare("rfc7951")) - throw std::invalid_argument("prefix must contain origin of \"rfc7951\" rather than \"" + prefix.origin() + "\""); - } else if (path.origin().compare("rfc7951")) - throw std::invalid_argument("path must contain origin of \"rfc7951\" rather than \"" + path.origin() + "\""); + // prefix and path origins can diverge! (not compliant) + if (prefix.origin().size() > 0) + { + if (prefix.origin().compare("rfc7951")) + { + throw std::invalid_argument("prefix must contain origin of \"rfc7951\" rather than\"" + + prefix.origin() + "\""); + } + } + else if (path.origin().compare("rfc7951")) + { + throw std::invalid_argument("path must contain origin of \"rfc7951\" rather than \"" + + path.origin() + "\""); + } } /* Conversion methods between xpaths and gNMI paths */ -inline std::string gnmi_to_xpath(const gnmi::Path& path) +inline std::string gnmi_to_xpath(const gnmi::Path &path) { - std::string str = ""; - - // This form is most convenient for sysrepo get operations and sysrepo - // set operations require special handling - if (path.elem_size() <= 0) - return "/*"; - - //iterate over the list of PathElem of a gNMI path - for (auto &node : path.elem()) { - str += "/"; - - if (node.name().compare("..") == 0) - throw std::invalid_argument("Relative paths not allowed"); - - str += node.name(); - for (auto key : node.key()) { - // YANG 1.1 uses XPath 1.0 and it doesn't support escaping quotes: - // > Literal ::= '"' [^"]* '"' - // > | "'" [^']* "'" - // Therefore, to avoid being able to inject potentially harmful user defined queries, reject values with both double quotes and single quote. - if ((key.second.find('\"') != std::string::npos) && - (key.second.find('\'') != std::string::npos)) - throw std::invalid_argument("Double quotes AND single quote in values not allowed"); - // Use " as delimiter unless it's present then use ' as delimiter - auto delim = (key.second.find('\"') != std::string::npos) ? '\'' : '\"'; - str += "[" + key.first + "=" + delim + key.second + delim + "]"; + std::string str = ""; + + // This form is most convenient for sysrepo get operations and sysrepo + // set operations require special handling + if (path.elem_size() <= 0) + return "/*"; + + // iterate over the list of PathElem of a gNMI path + for (auto &node : path.elem()) + { + str += "/"; + + if (node.name().compare("..") == 0) + throw std::invalid_argument("Relative paths not allowed"); + + str += node.name(); + for (auto key : node.key()) + { + // YANG 1.1 uses XPath 1.0 and it doesn't support escaping quotes: + // > Literal ::= '"' [^"]* '"' + // > | "'" [^']* "'" + // Therefore, to avoid being able to inject potentially harmful user defined queries, + // reject values with both double quotes and single quote. + if ((key.second.find('\"') != std::string::npos) && + (key.second.find('\'') != std::string::npos)) + throw std::invalid_argument("Double quotes AND single quote in values not allowed"); + // Use " as delimiter unless it's present then use ' as delimiter + auto delim = (key.second.find('\"') != std::string::npos) ? '\'' : '\"'; + str += "[" + key.first + "=" + delim + key.second + delim + "]"; + } } - } - return str; + return str; } // Parse XPath-like string in gnmi::Path // Assumes that the path is well-formed (i.e. hasn't come from the client) -inline void -xpath_to_gnmi(std::string xpath, gnmi::Path &path) +inline void xpath_to_gnmi(std::string xpath, gnmi::Path &path) { - if (!xpath.compare("/")) - return; - - path.set_origin("rfc7951"); - - auto start = 0u; - auto end = xpath.find_first_of('/', start); - assert(end != std::string::npos); - // Skip initial / - we don't want an empty path elem inserted for it - start = end + 1; - end = xpath.find_first_of('/', start); - for (; - true; - start = end + 1, end = xpath.find_first_of('/', start)) { - auto elem = path.add_elem(); - auto key_start = xpath.find_first_of('[', start); - auto key_close = std::string::npos; - - // Parse list key(s) if present - if (key_start != std::string::npos && key_start < end) { - size_t value_end = 0; - elem->mutable_name()->assign(xpath.substr(start, key_start - start)); - for (; key_start < end; key_start = value_end + 2) { - if (xpath[key_start] != '[') - break; - - //if 'end' fell on a slash '/' inside a key, move it on to next - key_close = xpath.find_first_of(']', key_start); - if (end < key_close) - end = xpath.find_first_of('/', key_close); - - auto key_end = xpath.find_first_of('=', key_start); - // May be single or double quote character - auto quote = xpath[key_end + 1]; - value_end = xpath.find_first_of(quote, key_end + 2); - // +1 to skip over leading '[' - auto key = xpath.substr(key_start + 1, key_end - key_start - 1); - // +2 to start to skip over = and ' - auto value = xpath.substr(key_end + 2, value_end - key_end - 2); - (*elem->mutable_key())[key] = value; - } - // skip over the key & value - start = value_end; - end = xpath.find_first_of('/', start); - } else - elem->mutable_name()->assign(xpath.substr(start, end - start)); - - if (end == std::string::npos) - break; - } + if (!xpath.compare("/")) + return; + + path.set_origin("rfc7951"); + + auto start = 0u; + auto end = xpath.find_first_of('/', start); + assert(end != std::string::npos); + // Skip initial / - we don't want an empty path elem inserted for it + start = end + 1; + end = xpath.find_first_of('/', start); + for (; true; start = end + 1, end = xpath.find_first_of('/', start)) + { + auto elem = path.add_elem(); + auto key_start = xpath.find_first_of('[', start); + auto key_close = std::string::npos; + + // Parse list key(s) if present + if (key_start != std::string::npos && key_start < end) + { + size_t value_end = 0; + elem->mutable_name()->assign(xpath.substr(start, key_start - start)); + for (; key_start < end; key_start = value_end + 2) + { + if (xpath[key_start] != '[') + break; + + // if 'end' fell on a slash '/' inside a key, move it on to next + key_close = xpath.find_first_of(']', key_start); + if (end < key_close) + end = xpath.find_first_of('/', key_close); + + auto key_end = xpath.find_first_of('=', key_start); + // May be single or double quote character + auto quote = xpath[key_end + 1]; + value_end = xpath.find_first_of(quote, key_end + 2); + // +1 to skip over leading '[' + auto key = xpath.substr(key_start + 1, key_end - key_start - 1); + // +2 to start to skip over = and ' + auto value = xpath.substr(key_end + 2, value_end - key_end - 2); + (*elem->mutable_key())[key] = value; + } + // skip over the key & value + start = value_end; + end = xpath.find_first_of('/', start); + } + else + elem->mutable_name()->assign(xpath.substr(start, end - start)); + + if (end == std::string::npos) + break; + } } // Compare two gNMI paths, ignoring their targets -inline bool gnmi_path_equals(const gnmi::Path& path1, const gnmi::Path& path2) +inline bool gnmi_path_equals(const gnmi::Path &path1, const gnmi::Path &path2) { - if (path1.origin() != path2.origin()) - return false; - if (path1.elem_size() != path2.elem_size()) - return false; - for (auto i = 0; i < path1.elem_size(); i++) { - if (path1.elem(i).name() != path2.elem(i).name()) - return false; - if (path1.elem(i).key_size() != path2.elem(i).key_size()) - return false; - for (auto key_val1 : path1.elem(i).key()) { - if (!path2.elem(i).key().contains(key_val1.first)) + if (path1.origin() != path2.origin()) return false; - auto val2 = path2.elem(i).key().at(key_val1.first); - if (key_val1.second != val2) + if (path1.elem_size() != path2.elem_size()) return false; + for (auto i = 0; i < path1.elem_size(); i++) + { + if (path1.elem(i).name() != path2.elem(i).name()) + return false; + if (path1.elem(i).key_size() != path2.elem(i).key_size()) + return false; + for (auto key_val1 : path1.elem(i).key()) + { + bool found = false; + for (auto key_val2 : path2.elem(i).key()) + { + if (key_val2.first == key_val1.first && key_val2.second == key_val2.second) + { + found = true; + break; + } + } + if (!found) + { + return false; + } + } } - } - return true; + return true; } - -#endif // _UTILS_H diff --git a/tests/capabilities.cpp b/tests/capabilities.cpp index 0e9cf61..6ad4eb2 100644 --- a/tests/capabilities.cpp +++ b/tests/capabilities.cpp @@ -14,36 +14,39 @@ * limitations under the License. */ +#include #include #include -#include +#include "main.h" #include #include -#include "main.h" using namespace std; using Catch::Matchers::Equals; -TEST_CASE("Capability request", "[caps]") { - ClientContext ctx; - CapabilityRequest request; - CapabilityResponse response; - bool found = false; +TEST_CASE("Capability request", "[caps]") +{ + ClientContext ctx; + CapabilityRequest request; + CapabilityResponse response; + bool found = false; - auto status = client->Capabilities(&ctx, request, &response); - CHECK(status.ok()); + auto status = client->Capabilities(&ctx, request, &response); + CHECK(status.ok()); - REQUIRE(response.supported_encodings().size() == 1); - CHECK(response.supported_encodings().Get(0) == gnmi::Encoding::JSON_IETF); - for (auto m : response.supported_models()) { - if (!m.name().compare("gnmi-server-test")) { - CHECK_THAT(m.version(), Equals("2021-02-10")); - CHECK_THAT(m.organization(), Equals("")); - found = true; - break; + REQUIRE(response.supported_encodings().size() == 1); + CHECK(response.supported_encodings().Get(0) == gnmi::Encoding::JSON_IETF); + for (auto m : response.supported_models()) + { + if (!m.name().compare("gnmi-server-test")) + { + CHECK_THAT(m.version(), Equals("2021-02-10")); + CHECK_THAT(m.organization(), Equals("")); + found = true; + break; + } } - } - CHECK(found); - CHECK(!response.gnmi_version().compare("0.7.0")); + CHECK(found); + CHECK(!response.gnmi_version().compare("0.7.0")); } diff --git a/tests/get.cpp b/tests/get.cpp index ccef45b..d4ebcb3 100644 --- a/tests/get.cpp +++ b/tests/get.cpp @@ -16,10 +16,10 @@ #include +#include #include #include #include -#include #include @@ -30,854 +30,917 @@ using Catch::Matchers::Equals; /* Positive Tests */ -// A path referring to "root" (which is represented by a path consisting of an empty set of elements) should result in the nodes childA and childB and all of their children ... being considered by the relevant operation. -TEST_CASE("Top-level Get request", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - bool found = false; - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - std::cout << __func__ << ":" << __LINE__ << std::endl; - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - for (auto it : response.notification().Get(0).update()) { - auto path = path_to_xpath(it.path()); - std::cout << "found path: " << path << std::endl; - if (path.compare("/gnmi-server-test:test-state")) - continue; - found = true; - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - CHECK(!response.notification().Get(0).atomic()); - } - CHECK(found); +// A path referring to "root" (which is represented by a path consisting of an empty set of +// elements) should result in the nodes childA and childB and all of their children ... being +// considered by the relevant operation. +TEST_CASE("Top-level Get request", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + bool found = false; + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + std::cout << __func__ << ":" << __LINE__ << std::endl; + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + for (auto it : response.notification().Get(0).update()) + { + auto path = path_to_xpath(it.path()); + std::cout << "found path: " << path << std::endl; + if (path.compare("/gnmi-server-test:test-state")) + continue; + found = true; + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + CHECK(!response.notification().Get(0).atomic()); + } + CHECK(found); } -static void -single_get() +static void single_get() { - ClientContext ctx; - GetRequest request; - GetResponse response; - bool found = false; - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - for (auto it : response.notification().Get(0).update()) { - auto path = path_to_xpath(it.path()); - if (path.compare("/gnmi-server-test:test-state")) - continue; - found = true; - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - CHECK(!response.notification().Get(0).atomic()); - } - CHECK(found); + ClientContext ctx; + GetRequest request; + GetResponse response; + bool found = false; + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + for (auto it : response.notification().Get(0).update()) + { + auto path = path_to_xpath(it.path()); + if (path.compare("/gnmi-server-test:test-state")) + continue; + found = true; + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + CHECK(!response.notification().Get(0).atomic()); + } + CHECK(found); } -TEST_CASE("Top-level multiple parallel Get requests", "[get]") { - #define NUM_THREADS 20 - std::thread threads[NUM_THREADS]; - for (auto i=0; i < NUM_THREADS; i++) { - threads[i] = std::thread(single_get); - } - for (auto i=0; i < NUM_THREADS; i++) { - threads[i].join(); - } +TEST_CASE("Top-level multiple parallel Get requests", "[get]") +{ +#define NUM_THREADS 20 + std::thread threads[NUM_THREADS]; + for (auto i = 0; i < NUM_THREADS; i++) + { + threads[i] = std::thread(single_get); + } + for (auto i = 0; i < NUM_THREADS; i++) + { + threads[i].join(); + } } -TEST_CASE("Get request of all module oper state", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto data = response.notification().Get(0).update(); - auto json = data.Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\",\"counter\":\"2\"}],\"cargo\":{}}")); - auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); - CHECK(!response.notification().Get(0).atomic()); +TEST_CASE("Get request of all module oper state", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto data = response.notification().Get(0).update(); + auto json = data.Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\"," + "\"counter\":\"2\"}],\"cargo\":{}}")); + auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); + CHECK(!response.notification().Get(0).atomic()); } -TEST_CASE("Get request of one list item oper state", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='A']", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"name\":\"A\",\"counter\":\"1\"}")); - auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']")); - CHECK(!response.notification().Get(0).atomic()); +TEST_CASE("Get request of one list item oper state", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='A']", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"name\":\"A\",\"counter\":\"1\"}")); + auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']")); + CHECK(!response.notification().Get(0).atomic()); } // gNMI spec reference: -// In the case that the data item at the specified path is a leaf node (i.e., has no children, and an associated value) the value of that leaf is encoded directly - i.e., the "bare" value is specified (i.e., a JSON object is not CHECKd, and a bare JSON value is included). -TEST_CASE("Get request of one leaf of oper state", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='A']/counter", request.add_path()); - auto status = client->Get(&ctx, request, &response); - - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - // This is a 64-bit value, so is represented as a string (ref: RFC7951 §6.1) - CHECK_THAT(json, Equals("\"1\"")); - auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']/counter")); - CHECK(!response.notification().Get(0).atomic()); +// In the case that the data item at the specified path is a leaf node (i.e., has no children, and +// an associated value) the value of that leaf is encoded directly - i.e., the "bare" value is +// specified (i.e., a JSON object is not CHECKd, and a bare JSON value is included). +TEST_CASE("Get request of one leaf of oper state", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='A']/counter", request.add_path()); + auto status = client->Get(&ctx, request, &response); + + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + // This is a 64-bit value, so is represented as a string (ref: RFC7951 §6.1) + CHECK_THAT(json, Equals("\"1\"")); + auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']/counter")); + CHECK(!response.notification().Get(0).atomic()); } -TEST_CASE("Get request with prefix", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='A']", request.mutable_prefix()); - xpath_to_path("/counter", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - auto prefix = path_to_xpath(response.notification().Get(0).prefix()); - CHECK_THAT(prefix, Equals("/")); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - // This is a 64-bit value, so is represented as a string (ref: RFC7951 §6.1) - CHECK_THAT(json, Equals("\"1\"")); - auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']/counter")); - CHECK(!response.notification().Get(0).atomic()); +TEST_CASE("Get request with prefix", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='A']", request.mutable_prefix()); + xpath_to_path("/counter", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + auto prefix = path_to_xpath(response.notification().Get(0).prefix()); + CHECK_THAT(prefix, Equals("/")); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + // This is a 64-bit value, so is represented as a string (ref: RFC7951 §6.1) + CHECK_THAT(json, Equals("\"1\"")); + auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']/counter")); + CHECK(!response.notification().Get(0).atomic()); } -TEST_CASE("Get request with target", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - request.set_encoding(gnmi::Encoding::JSON_IETF); - *request.mutable_prefix()->mutable_target() = "foo"; - xpath_to_path("/gnmi-server-test:test-state/things[name='A']", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).prefix().target(), Equals("foo")); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"name\":\"A\",\"counter\":\"1\"}")); - auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']")); - CHECK(!response.notification().Get(0).atomic()); +TEST_CASE("Get request with target", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + request.set_encoding(gnmi::Encoding::JSON_IETF); + *request.mutable_prefix()->mutable_target() = "foo"; + xpath_to_path("/gnmi-server-test:test-state/things[name='A']", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).prefix().target(), Equals("foo")); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"name\":\"A\",\"counter\":\"1\"}")); + auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']")); + CHECK(!response.notification().Get(0).atomic()); } -TEST_CASE("Get request for config datastore", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - request.set_type(gnmi::GetRequest_DataType::GetRequest_DataType_CONFIG); - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - CHECK(!response.notification().Get(0).atomic()); - REQUIRE(response.notification().Get(0).update_size() == 0); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test/things[name='A']/enabled", "true"); - sr_sess->applyChanges(); - - ClientContext ctx2; - status = client->Get(&ctx2, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("true")); - auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='A']/enabled")); - CHECK(!response.notification().Get(0).atomic()); - - sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); - sr_sess->applyChanges(); -} +TEST_CASE("Get request for config datastore", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + request.set_type(gnmi::GetRequest_DataType::GetRequest_DataType_CONFIG); + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + CHECK(!response.notification().Get(0).atomic()); + REQUIRE(response.notification().Get(0).update_size() == 0); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->setItem("/gnmi-server-test:test/things[name='A']/enabled", "true"); + sr_sess->applyChanges(); + + ClientContext ctx2; + status = client->Get(&ctx2, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("true")); + auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='A']/enabled")); + CHECK(!response.notification().Get(0).atomic()); -TEST_CASE("Get request with wildcard", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/*/counter", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 2); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - // This is a 64-bit value, so is represented as a string (ref: RFC7951 §6.1) - CHECK_THAT(json, Equals("\"1\"")); - auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']/counter")); - CHECK(response.notification().Get(0).update().Get(1).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.notification().Get(0).update().Get(1).val().json_ietf_val(); - CHECK_THAT(json, Equals("\"2\"")); - path = path_to_xpath(response.notification().Get(0).update().Get(1).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='B']/counter")); - CHECK(!response.notification().Get(0).atomic()); + sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); + sr_sess->applyChanges(); } -TEST_CASE("Get request with multiple paths", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='A']/counter", request.add_path()); - xpath_to_path("/gnmi-server-test:test-state/things[name='B']/counter", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 2); - - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - // This is a 64-bit value, so is represented as a string (ref: RFC7951 §6.1) - CHECK_THAT(json, Equals("\"1\"")); - auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']/counter")); - CHECK(!response.notification().Get(0).atomic()); - - CHECK(response.notification().Get(1).delete__size() == 0); - CHECK(response.notification().Get(1).timestamp() > 0); - CHECK(!response.notification().Get(1).has_prefix()); - CHECK_THAT(response.notification().Get(1).alias(), Equals("")); - REQUIRE(response.notification().Get(1).update_size() == 1); - CHECK(response.notification().Get(1).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.notification().Get(1).update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("\"2\"")); - path = path_to_xpath(response.notification().Get(1).update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='B']/counter")); - CHECK(!response.notification().Get(1).atomic()); +TEST_CASE("Get request with wildcard", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/*/counter", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 2); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + // This is a 64-bit value, so is represented as a string (ref: RFC7951 §6.1) + CHECK_THAT(json, Equals("\"1\"")); + auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']/counter")); + CHECK(response.notification().Get(0).update().Get(1).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.notification().Get(0).update().Get(1).val().json_ietf_val(); + CHECK_THAT(json, Equals("\"2\"")); + path = path_to_xpath(response.notification().Get(0).update().Get(1).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='B']/counter")); + CHECK(!response.notification().Get(0).atomic()); } -TEST_CASE("Get request of empty container", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/cargo", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{}")); - auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/cargo")); - CHECK(!response.notification().Get(0).atomic()); +TEST_CASE("Get request with multiple paths", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='A']/counter", request.add_path()); + xpath_to_path("/gnmi-server-test:test-state/things[name='B']/counter", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 2); + + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + // This is a 64-bit value, so is represented as a string (ref: RFC7951 §6.1) + CHECK_THAT(json, Equals("\"1\"")); + auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']/counter")); + CHECK(!response.notification().Get(0).atomic()); + + CHECK(response.notification().Get(1).delete__size() == 0); + CHECK(response.notification().Get(1).timestamp() > 0); + CHECK(!response.notification().Get(1).has_prefix()); + CHECK_THAT(response.notification().Get(1).alias(), Equals("")); + REQUIRE(response.notification().Get(1).update_size() == 1); + CHECK(response.notification().Get(1).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.notification().Get(1).update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("\"2\"")); + path = path_to_xpath(response.notification().Get(1).update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='B']/counter")); + CHECK(!response.notification().Get(1).atomic()); } -TEST_CASE("Get request of list container", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - CHECK(!response.notification().Get(0).atomic()); - REQUIRE(response.notification().Get(0).update_size() == 2); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"name\":\"A\",\"counter\":\"1\"}")); - auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']")); - CHECK(response.notification().Get(0).update().Get(1).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.notification().Get(0).update().Get(1).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"name\":\"B\",\"counter\":\"2\"}")); - path = path_to_xpath(response.notification().Get(0).update().Get(1).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='B']")); +TEST_CASE("Get request of empty container", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/cargo", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{}")); + auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/cargo")); + CHECK(!response.notification().Get(0).atomic()); } -TEST_CASE("Get request with non-existent path", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='not-found']", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - CHECK(!response.notification().Get(0).atomic()); - REQUIRE(response.notification().Get(0).update_size() == 0); +TEST_CASE("Get request of list container", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + CHECK(!response.notification().Get(0).atomic()); + REQUIRE(response.notification().Get(0).update_size() == 2); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"name\":\"A\",\"counter\":\"1\"}")); + auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']")); + CHECK(response.notification().Get(0).update().Get(1).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.notification().Get(0).update().Get(1).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"name\":\"B\",\"counter\":\"2\"}")); + path = path_to_xpath(response.notification().Get(0).update().Get(1).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='B']")); } -TEST_CASE("Get request of one list item where name contains /", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']", - std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']/counter", "5"); - sr_sess->applyChanges(); - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"name\":\"Gigabit5/0/0\",\"counter\":\"5\"}")); - auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']")); - CHECK(!response.notification().Get(0).atomic()); - - // cleanup - sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']"); - sr_sess->applyChanges(); +TEST_CASE("Get request with non-existent path", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='not-found']", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + CHECK(!response.notification().Get(0).atomic()); + REQUIRE(response.notification().Get(0).update_size() == 0); } -TEST_CASE("Get request of one leaf where parent name contains /", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']/counter", - request.add_path()); - - const Path reqpath = request.path(0); - CHECK(reqpath.elem_size() == 3); - CHECK_THAT(reqpath.elem(0).name(), Equals("gnmi-server-test:test-state")); - CHECK_THAT(reqpath.elem(1).name(), Equals("things")); - CHECK_THAT(reqpath.elem(1).key().at("name"), Equals("Gigabit5/0/0")); - CHECK_THAT(reqpath.elem(2).name(), Equals("counter")); - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']", - std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']/counter", "5"); - sr_sess->applyChanges(); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("\"5\"")); - CHECK(!response.notification().Get(0).atomic()); - - auto resppath = response.notification().Get(0).update().Get(0).path(); - CHECK(resppath.elem_size() == 3); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test-state")); - CHECK_THAT(resppath.elem(1).name(), Equals("things")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("Gigabit5/0/0")); - CHECK_THAT(resppath.elem(2).name(), Equals("counter")); - - // cleanup - sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']"); - sr_sess->applyChanges(); +TEST_CASE("Get request of one list item where name contains /", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']/counter", "5"); + sr_sess->applyChanges(); + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"name\":\"Gigabit5/0/0\",\"counter\":\"5\"}")); + auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']")); + CHECK(!response.notification().Get(0).atomic()); + + // cleanup + sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']"); + sr_sess->applyChanges(); } -TEST_CASE("Get request of one list item where name contains [", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='One[1]']", - std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='One[1]']/counter", "7"); - sr_sess->applyChanges(); - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='One[1]']", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"name\":\"One[1]\",\"counter\":\"7\"}")); - auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='One[1]']")); - CHECK(!response.notification().Get(0).atomic()); - - // cleanup - sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='One[1]']"); - sr_sess->applyChanges(); +TEST_CASE("Get request of one leaf where parent name contains /", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']/counter", + request.add_path()); + + const Path reqpath = request.path(0); + CHECK(reqpath.elem_size() == 3); + CHECK_THAT(reqpath.elem(0).name(), Equals("gnmi-server-test:test-state")); + CHECK_THAT(reqpath.elem(1).name(), Equals("things")); + CHECK_THAT(reqpath.elem(1).key().at("name"), Equals("Gigabit5/0/0")); + CHECK_THAT(reqpath.elem(2).name(), Equals("counter")); + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']/counter", "5"); + sr_sess->applyChanges(); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("\"5\"")); + CHECK(!response.notification().Get(0).atomic()); + + auto resppath = response.notification().Get(0).update().Get(0).path(); + CHECK(resppath.elem_size() == 3); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test-state")); + CHECK_THAT(resppath.elem(1).name(), Equals("things")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("Gigabit5/0/0")); + CHECK_THAT(resppath.elem(2).name(), Equals("counter")); + + // cleanup + sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='Gigabit5/0/0']"); + sr_sess->applyChanges(); } -TEST_CASE("Get request of one list item where name contains '", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test-state/things[name=\"to-cpe2'\"]", - std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name=\"to-cpe2'\"]/counter", "8"); - sr_sess->applyChanges(); - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name=\"to-cpe2'\"]", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"name\":\"to-cpe2'\",\"counter\":\"8\"}")); - auto resppath = response.notification().Get(0).update().Get(0).path(); - CHECK(resppath.elem_size() == 2); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test-state")); - CHECK_THAT(resppath.elem(1).name(), Equals("things")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("to-cpe2'")); - CHECK(!response.notification().Get(0).atomic()); - - // cleanup - sr_sess->deleteItem("/gnmi-server-test:test-state/things[name=\"to-cpe2'\"]"); - sr_sess->applyChanges(); +TEST_CASE("Get request of one list item where name contains [", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='One[1]']", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='One[1]']/counter", "7"); + sr_sess->applyChanges(); + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='One[1]']", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"name\":\"One[1]\",\"counter\":\"7\"}")); + auto path = path_to_xpath(response.notification().Get(0).update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='One[1]']")); + CHECK(!response.notification().Get(0).atomic()); + + // cleanup + sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='One[1]']"); + sr_sess->applyChanges(); } -TEST_CASE("Get request of one list item where name contains \\", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='to\\cpe2']", - std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='to\\cpe2']/counter", "8"); - sr_sess->applyChanges(); - - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='to\\cpe2']", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"name\":\"to\\\\cpe2\",\"counter\":\"8\"}")); - auto resppath = response.notification().Get(0).update().Get(0).path(); - CHECK(resppath.elem_size() == 2); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test-state")); - CHECK_THAT(resppath.elem(1).name(), Equals("things")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("to\\cpe2")); - CHECK(!response.notification().Get(0).atomic()); - - // cleanup - sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='to\\cpe2']"); - sr_sess->applyChanges(); +TEST_CASE("Get request of one list item where name contains '", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test-state/things[name=\"to-cpe2'\"]", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name=\"to-cpe2'\"]/counter", "8"); + sr_sess->applyChanges(); + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name=\"to-cpe2'\"]", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"name\":\"to-cpe2'\",\"counter\":\"8\"}")); + auto resppath = response.notification().Get(0).update().Get(0).path(); + CHECK(resppath.elem_size() == 2); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test-state")); + CHECK_THAT(resppath.elem(1).name(), Equals("things")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("to-cpe2'")); + CHECK(!response.notification().Get(0).atomic()); + + // cleanup + sr_sess->deleteItem("/gnmi-server-test:test-state/things[name=\"to-cpe2'\"]"); + sr_sess->applyChanges(); } -TEST_CASE("Get request from list with composite key ", "[get-composite-key]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - // create an entry in the list wiht composite keys - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']/data", - "baz"); - sr_sess->applyChanges(); - - // prepare a get request for that entry - xpath_to_path("/gnmi-server-test:test3/complex-list[name='foo'][type='bar']", - request.add_path()); - - // check parsing of the xpath_to_path() function - const Path reqpath = request.path(0); - CHECK(reqpath.elem_size() == 2); - CHECK_THAT(reqpath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(reqpath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(reqpath.elem(1).key().at("name"), Equals("foo")); - CHECK_THAT(reqpath.elem(1).key().at("type"), Equals("bar")); - - request.set_encoding(gnmi::Encoding::JSON_IETF); - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foo\",\"data\":\"baz\"}")); - auto resppath = response.notification().Get(0).update().Get(0).path(); - CHECK(resppath.elem_size() == 2); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); - CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); - CHECK(!response.notification().Get(0).atomic()); - - // cleanup - sr_sess->deleteItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']"); - sr_sess->applyChanges(); +TEST_CASE("Get request of one list item where name contains \\", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='to\\cpe2']", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='to\\cpe2']/counter", "8"); + sr_sess->applyChanges(); + + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='to\\cpe2']", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"name\":\"to\\\\cpe2\",\"counter\":\"8\"}")); + auto resppath = response.notification().Get(0).update().Get(0).path(); + CHECK(resppath.elem_size() == 2); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test-state")); + CHECK_THAT(resppath.elem(1).name(), Equals("things")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("to\\cpe2")); + CHECK(!response.notification().Get(0).atomic()); + + // cleanup + sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='to\\cpe2']"); + sr_sess->applyChanges(); } -TEST_CASE("Get request from list with composite key having slashes ", "[get-composite-key-with-slashes]") { - ClientContext ctx; - GetRequest request; - GetResponse response; +TEST_CASE("Get request from list with composite key ", "[get-composite-key]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + // create an entry in the list wiht composite keys + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']/data", "baz"); + sr_sess->applyChanges(); + + // prepare a get request for that entry + xpath_to_path("/gnmi-server-test:test3/complex-list[name='foo'][type='bar']", + request.add_path()); + + // check parsing of the xpath_to_path() function + const Path reqpath = request.path(0); + CHECK(reqpath.elem_size() == 2); + CHECK_THAT(reqpath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(reqpath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(reqpath.elem(1).key().at("name"), Equals("foo")); + CHECK_THAT(reqpath.elem(1).key().at("type"), Equals("bar")); + + request.set_encoding(gnmi::Encoding::JSON_IETF); + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foo\",\"data\":\"baz\"}")); + auto resppath = response.notification().Get(0).update().Get(0).path(); + CHECK(resppath.elem_size() == 2); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); + CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); + CHECK(!response.notification().Get(0).atomic()); - // create an entry in the list wiht composite keys - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar/cat/fish'][name='foo/dog/sausage']/data", - "baz"); - sr_sess->applyChanges(); + // cleanup + sr_sess->deleteItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']"); + sr_sess->applyChanges(); +} - // prepare a get request for that entry - xpath_to_path("/gnmi-server-test:test3/complex-list[name='foo/dog/sausage'][type='bar/cat/fish']", +TEST_CASE("Get request from list with composite key having slashes ", + "[get-composite-key-with-slashes]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + // create an entry in the list wiht composite keys + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem( + "/gnmi-server-test:test3/complex-list[type='bar/cat/fish'][name='foo/dog/sausage']/data", + "baz"); + sr_sess->applyChanges(); + + // prepare a get request for that entry + xpath_to_path( + "/gnmi-server-test:test3/complex-list[name='foo/dog/sausage'][type='bar/cat/fish']", request.add_path()); - // check parsing of the xpath_to_path() function - const Path reqpath = request.path(0); - CHECK(reqpath.elem_size() == 2); - CHECK_THAT(reqpath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(reqpath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(reqpath.elem(1).key().at("name"), Equals("foo/dog/sausage")); - CHECK_THAT(reqpath.elem(1).key().at("type"), Equals("bar/cat/fish")); - - request.set_encoding(gnmi::Encoding::JSON_IETF); - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"type\":\"bar/cat/fish\",\"name\":\"foo/dog/sausage\",\"data\":\"baz\"}")); - auto resppath = response.notification().Get(0).update().Get(0).path(); - CHECK(resppath.elem_size() == 2); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo/dog/sausage")); - CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar/cat/fish")); - CHECK(!response.notification().Get(0).atomic()); - - // cleanup - sr_sess->deleteItem("/gnmi-server-test:test3/complex-list[type='bar/cat/fish'][name='foo/dog/sausage']"); - sr_sess->applyChanges(); + // check parsing of the xpath_to_path() function + const Path reqpath = request.path(0); + CHECK(reqpath.elem_size() == 2); + CHECK_THAT(reqpath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(reqpath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(reqpath.elem(1).key().at("name"), Equals("foo/dog/sausage")); + CHECK_THAT(reqpath.elem(1).key().at("type"), Equals("bar/cat/fish")); + + request.set_encoding(gnmi::Encoding::JSON_IETF); + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, + Equals("{\"type\":\"bar/cat/fish\",\"name\":\"foo/dog/sausage\",\"data\":\"baz\"}")); + auto resppath = response.notification().Get(0).update().Get(0).path(); + CHECK(resppath.elem_size() == 2); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo/dog/sausage")); + CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar/cat/fish")); + CHECK(!response.notification().Get(0).atomic()); + + // cleanup + sr_sess->deleteItem( + "/gnmi-server-test:test3/complex-list[type='bar/cat/fish'][name='foo/dog/sausage']"); + sr_sess->applyChanges(); } -TEST_CASE("Get request from list with composite key having doube-quotes(\") ", "[get-composite-key-with-quotes]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - // create an entry in the list with composite keys - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar/cat/fish'][name='quotes: \", blah blah']/data", - "baz"); - sr_sess->applyChanges(); - - // prepare a get request for that entry - xpath_to_path("/gnmi-server-test:test3/complex-list[name='quotes: \", blah blah'][type='bar/cat/fish']", - request.add_path()); - - // check parsing of the xpath_to_path() function - const Path reqpath = request.path(0); - CHECK(reqpath.elem_size() == 2); - CHECK_THAT(reqpath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(reqpath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(reqpath.elem(1).key().at("name"), Equals("quotes: \", blah blah")); - CHECK_THAT(reqpath.elem(1).key().at("type"), Equals("bar/cat/fish")); - - request.set_encoding(gnmi::Encoding::JSON_IETF); - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.notification_size() == 1); - CHECK(response.notification().Get(0).delete__size() == 0); - CHECK(response.notification().Get(0).timestamp() > 0); - CHECK(!response.notification().Get(0).has_prefix()); - CHECK_THAT(response.notification().Get(0).alias(), Equals("")); - REQUIRE(response.notification().Get(0).update_size() == 1); - CHECK(response.notification().Get(0).update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); - CHECK_THAT( json, Equals("{\"type\":\"bar/cat/fish\",\"name\":\"quotes: \\\", blah blah\",\"data\":\"baz\"}") ); - auto resppath = response.notification().Get(0).update().Get(0).path(); - CHECK(resppath.elem_size() == 2); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("quotes: \", blah blah")); - CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar/cat/fish")); - CHECK(!response.notification().Get(0).atomic()); - - // cleanup - sr_sess->deleteItem("/gnmi-server-test:test3/complex-list[type='bar/cat/fish'][name='quotes: \", blah blah']"); - sr_sess->applyChanges(); +TEST_CASE("Get request from list with composite key having doube-quotes(\") ", + "[get-composite-key-with-quotes]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + // create an entry in the list with composite keys + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar/cat/fish'][name='quotes: \", " + "blah blah']/data", + "baz"); + sr_sess->applyChanges(); + + // prepare a get request for that entry + xpath_to_path( + "/gnmi-server-test:test3/complex-list[name='quotes: \", blah blah'][type='bar/cat/fish']", + request.add_path()); + + // check parsing of the xpath_to_path() function + const Path reqpath = request.path(0); + CHECK(reqpath.elem_size() == 2); + CHECK_THAT(reqpath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(reqpath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(reqpath.elem(1).key().at("name"), Equals("quotes: \", blah blah")); + CHECK_THAT(reqpath.elem(1).key().at("type"), Equals("bar/cat/fish")); + + request.set_encoding(gnmi::Encoding::JSON_IETF); + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(!response.has_error()); + CHECK(response.extension_size() == 0); + REQUIRE(response.notification_size() == 1); + CHECK(response.notification().Get(0).delete__size() == 0); + CHECK(response.notification().Get(0).timestamp() > 0); + CHECK(!response.notification().Get(0).has_prefix()); + CHECK_THAT(response.notification().Get(0).alias(), Equals("")); + REQUIRE(response.notification().Get(0).update_size() == 1); + CHECK(response.notification().Get(0).update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.notification().Get(0).update().Get(0).val().json_ietf_val(); + CHECK_THAT( + json, + Equals( + "{\"type\":\"bar/cat/fish\",\"name\":\"quotes: \\\", blah blah\",\"data\":\"baz\"}")); + auto resppath = response.notification().Get(0).update().Get(0).path(); + CHECK(resppath.elem_size() == 2); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("quotes: \", blah blah")); + CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar/cat/fish")); + CHECK(!response.notification().Get(0).atomic()); + + // cleanup + sr_sess->deleteItem( + "/gnmi-server-test:test3/complex-list[type='bar/cat/fish'][name='quotes: \", blah blah']"); + sr_sess->applyChanges(); } /* Negative tests */ -TEST_CASE("Get request with unsupported encoding type", "[get-neg]") { - ClientContext ctx; - GetRequest request; - GetResponse response; +TEST_CASE("Get request with unsupported encoding type", "[get-neg]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; - request.set_encoding(gnmi::Encoding::BYTES); - xpath_to_path("/gnmi-server-test:test-state", request.add_path()); + request.set_encoding(gnmi::Encoding::BYTES); + xpath_to_path("/gnmi-server-test:test-state", request.add_path()); - auto status = client->Get(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("BYTES")); + auto status = client->Get(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("BYTES")); } -TEST_CASE("Get request with unsupported use of models", "[get-neg]") { - ClientContext ctx; - GetRequest request; - GetResponse response; - - auto model_data = request.add_use_models(); - model_data->set_name("gnmi-server-test"); - model_data->set_version("2021-02-10"); - model_data->set_organization("graphiant"); - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state", request.add_path()); - - auto status = client->Get(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("use_model feature unsupported")); +TEST_CASE("Get request with unsupported use of models", "[get-neg]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; + + auto model_data = request.add_use_models(); + model_data->set_name("gnmi-server-test"); + model_data->set_version("2021-02-10"); + model_data->set_organization("graphiant"); + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state", request.add_path()); + + auto status = client->Get(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("use_model feature unsupported")); } -TEST_CASE("Get request with invalid datatype", "[get-neg]") { - ClientContext ctx; - GetRequest request; - GetResponse response; +TEST_CASE("Get request with invalid datatype", "[get-neg]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; - request.set_type(static_cast(42)); - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state", request.add_path()); + request.set_type(static_cast(42)); + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state", request.add_path()); - auto status = client->Get(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("")); + auto status = client->Get(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("")); } -TEST_CASE("Get request with relative path", "[get-neg]") { - ClientContext ctx; - GetRequest request; - GetResponse response; +TEST_CASE("Get request with relative path", "[get-neg]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/cargo/../things[name='A']", request.add_path()); + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/cargo/../things[name='A']", request.add_path()); - auto status = client->Get(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); - CHECK_THAT(status.error_message(), Equals("Relative paths not allowed")); + auto status = client->Get(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); + CHECK_THAT(status.error_message(), Equals("Relative paths not allowed")); } // Double-quotes now supported. // TBD: we do not support mix of ' and " but did not find a way to // inject that -TEST_CASE("Get request of one list item where name contains \"", "[get]") { - ClientContext ctx; - GetRequest request; - GetResponse response; +TEST_CASE("Get request of one list item where name contains \"", "[get]") +{ + ClientContext ctx; + GetRequest request; + GetResponse response; - request.set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='to-cpe1\"']", request.add_path()); + request.set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='to-cpe1\"']", request.add_path()); - auto status = client->Get(&ctx, request, &response); - CHECK(status.ok()); + auto status = client->Get(&ctx, request, &response); + CHECK(status.ok()); } diff --git a/tests/main.cpp b/tests/main.cpp index 32874c0..4dba863 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -14,10 +14,10 @@ * limitations under the License. */ +#include +#include #include #include -#include -#include #define CATCH_CONFIG_RUNNER #include @@ -25,9 +25,9 @@ #include #include "gnmi/gnmi.h" +#include "main.h" #include "security/authentication.h" #include "utils/log.h" -#include "main.h" using namespace std; namespace fs = boost::filesystem; @@ -36,267 +36,291 @@ unique_ptr client; std::optional sr_sess; // Parse XPath-like string in gnmi::Path -void -xpath_to_path(std::string xpath, gnmi::Path *path) +void xpath_to_path(std::string xpath, gnmi::Path *path) { - path->set_origin("rfc7951"); - - if (!xpath.compare("/")) - return; - - auto start = 0u; - auto end = xpath.find_first_of('/', start); - assert(end != std::string::npos); - // Skip initial / - we don't want an empty path elem inserted for it - start = end + 1; - end = xpath.find_first_of('/', start); - for (; - true; - start = end + 1, end = xpath.find_first_of('/', start)) { - auto elem = path->add_elem(); - auto key_start = xpath.find_first_of('[', start); - // Parse list key if present - if (key_start != std::string::npos && key_start < end) { - auto value_end = 0u; - elem->mutable_name()->assign(xpath.substr(start, key_start - start)); - - while (key_start != std::string::npos && key_start < end) { - auto key_end = xpath.find_first_of('=', key_start); - // May be single or double quote character - auto quote = xpath[key_end + 1]; - value_end = xpath.find_first_of(quote, key_end + 2); - // +1 to skip over leading '[' - auto key = xpath.substr(key_start + 1, key_end - key_start - 1); - // +2 to start to skip over = and ' - auto value = xpath.substr(key_end + 2, value_end - key_end - 2); - - (*elem->mutable_key())[key] = value; - - // find if there is a second key following - key_start = xpath.find_first_of('[', value_end); - - //if 'end' fell on a slash '/' inside a key, move it on to next - if (key_start != std::string::npos) - end = xpath.find_first_of(']', key_start + 1); - - } - // skip over the key & value - start = value_end + 2; - end = xpath.find_first_of('/', start); - } else - elem->mutable_name()->assign(xpath.substr(start, end - start)); - if (end == std::string::npos) - break; - } + path->set_origin("rfc7951"); + + if (!xpath.compare("/")) + return; + + auto start = 0u; + auto end = xpath.find_first_of('/', start); + assert(end != std::string::npos); + // Skip initial / - we don't want an empty path elem inserted for it + start = end + 1; + end = xpath.find_first_of('/', start); + for (; true; start = end + 1, end = xpath.find_first_of('/', start)) + { + auto elem = path->add_elem(); + auto key_start = xpath.find_first_of('[', start); + // Parse list key if present + if (key_start != std::string::npos && key_start < end) + { + auto value_end = 0u; + elem->mutable_name()->assign(xpath.substr(start, key_start - start)); + + while (key_start != std::string::npos && key_start < end) + { + auto key_end = xpath.find_first_of('=', key_start); + // May be single or double quote character + auto quote = xpath[key_end + 1]; + value_end = xpath.find_first_of(quote, key_end + 2); + // +1 to skip over leading '[' + auto key = xpath.substr(key_start + 1, key_end - key_start - 1); + // +2 to start to skip over = and ' + auto value = xpath.substr(key_end + 2, value_end - key_end - 2); + + (*elem->mutable_key())[key] = value; + + // find if there is a second key following + key_start = xpath.find_first_of('[', value_end); + + // if 'end' fell on a slash '/' inside a key, move it on to next + if (key_start != std::string::npos) + end = xpath.find_first_of(']', key_start + 1); + } + // skip over the key & value + start = value_end + 2; + end = xpath.find_first_of('/', start); + } + else + elem->mutable_name()->assign(xpath.substr(start, end - start)); + if (end == std::string::npos) + break; + } } -std::string -path_to_xpath(const gnmi::Path &path) +std::string path_to_xpath(const gnmi::Path &path) { - string str = ""; + string str = ""; - if (path.elem_size() <= 0) - return "/"; + if (path.elem_size() <= 0) + return "/"; - if (path.origin().compare("rfc7951")) - return "bad-origin:" + path.origin(); + if (path.origin().compare("rfc7951")) + return "bad-origin:" + path.origin(); - //iterate over the list of PathElem of a gNMI path - for (auto &node : path.elem()) { - str += "/" + node.name(); - for (auto key : node.key()) - str += "[" + key.first + "='" + key.second + "']"; - } + // iterate over the list of PathElem of a gNMI path + for (auto &node : path.elem()) + { + str += "/" + node.name(); + for (auto key : node.key()) + str += "[" + key.first + "='" + key.second + "']"; + } - return str; + return str; } -static sysrepo::ErrorCode -module_change_cb(sysrepo::Session session, uint32_t sub_id, std::string_view module_name, std::optional xpath, sysrepo::Event event, uint32_t request_id) +static sysrepo::ErrorCode module_change_cb(sysrepo::Session session, uint32_t sub_id, + std::string_view module_name, + std::optional xpath, + sysrepo::Event event, uint32_t request_id) { - (void)session; - (void)module_name; - (void)request_id; - (void)sub_id; - - if (event == sysrepo::Event::Change && xpath == "/gnmi-server-test:test2/custom-error") { - session.setErrorMessage(std::string("Fiddlesticks: ") + std::string(xpath.value())); - return sysrepo::ErrorCode::CallbackFailed; - } - - // Don't do anything with the configuration - return sysrepo::ErrorCode::Ok; -} + (void)session; + (void)module_name; + (void)request_id; + (void)sub_id; + + if (event == sysrepo::Event::Change && xpath == "/gnmi-server-test:test2/custom-error") + { + session.setErrorMessage(std::string("Fiddlesticks: ") + std::string(xpath.value())); + return sysrepo::ErrorCode::CallbackFailed; + } -static sysrepo::ErrorCode -clear_stats_rpc_cb( - sysrepo::Session session, uint32_t sub_id, std::string_view xpath, const libyang::DataNode input, sysrepo::Event event, uint32_t request_id, libyang::DataNode output) -{ - (void)session; - (void)event; - (void)request_id; - (void)sub_id; - - if (input.child() && input.child()->isTerm() && input.child()->asTerm().valueStr() == "error") { - session.setErrorMessage(std::string("Fiddlesticks: ") + std::string(xpath)); - return sysrepo::ErrorCode::CallbackFailed; - } - if (input.child() && input.child()->isTerm() && input.child()->asTerm().valueStr() == "timeout") { - // Sleep for a time longer than SR_RPC_CB_TIMEOUT - sleep(3); - } - - output.newPath("old-stats", "613", libyang::CreationOptions::Output); - - return sysrepo::ErrorCode::Ok; + // Don't do anything with the configuration + return sysrepo::ErrorCode::Ok; } -class SetupSysrepo { -public: - SetupSysrepo() { - auto sr_conn = sysrepo::Connection(); - // install yang modules - sr_conn.installModules( - { - TESTS_SRC_DIR "/gnmi-server-test.yang", - TESTS_SRC_DIR "/gnmi-server-test-wine.yang" - }, TESTS_SRC_DIR); - - sr_sess = sr_conn.sessionStart(); - - sub = sr_sess->onModuleChange("gnmi-server-test", module_change_cb, "/gnmi-server-test:test2/custom-error"); - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='A']", std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='B']", std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='A']/counter", "1"); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='B']/counter", "2"); - sr_sess->setItem("/gnmi-server-test:test-state/cargo", ""); - sr_sess->applyChanges(); - - sub->onRPCAction( - "/gnmi-server-test:clear-stats", clear_stats_rpc_cb, - 0, - sysrepo::SubscribeOptions::Default); - }; - - ~SetupSysrepo() { - BOOST_LOG_TRIVIAL(debug) << "Removing sysrepo data" << std::endl; - sub.reset(); - sr_sess->getConnection().removeModules({"gnmi-server-test", "gnmi-server-test-wine"}); - // don't hold onto sr_sess forever. - sr_sess.reset(); - } - - // prevent copy and assignments. - SetupSysrepo(SetupSysrepo&) = delete; - SetupSysrepo& operator=(const SetupSysrepo&) = delete; - -private: - std::optional sub; -}; - -class SetupTests { -public: - ~SetupTests() { - BOOST_LOG_TRIVIAL(debug) << "Removing test directories" << std::endl; - fs::remove_all(log_path); - if (!repo_path.empty()) { - fs::remove_all(repo_path); - fs::remove_all(sr_shm_path); +static sysrepo::ErrorCode clear_stats_rpc_cb(sysrepo::Session session, uint32_t sub_id, + std::string_view xpath, const libyang::DataNode input, + sysrepo::Event event, uint32_t request_id, + libyang::DataNode output) +{ + (void)session; + (void)event; + (void)request_id; + (void)sub_id; + + if (input.child() && input.child()->isTerm() && input.child()->asTerm().valueStr() == "error") + { + session.setErrorMessage(std::string("Fiddlesticks: ") + std::string(xpath)); + return sysrepo::ErrorCode::CallbackFailed; } - fs::remove("/tmp/gnmi-logrotate.lock"); - } - - SetupTests() { - log_path = string(GNMI_LOG_DIR); - fs::remove_all(log_path); - fs::remove("/tmp/gnmi-logrotate.lock"); - fs::create_directory(log_path); - fs::create_directory(log_path + "/raw"); - fs::create_directory(log_path + "/archives"); - - if (getenv("SYSREPO_REPOSITORY_PATH")) { - // Don't set environment variables if user has already specified them! - // They may have good reason to do it, and may know what they are doing. - BOOST_LOG_TRIVIAL(warning) << "Using user-defined sysrepo env" << std::endl << std::endl; - return; + if (input.child() && input.child()->isTerm() && input.child()->asTerm().valueStr() == "timeout") + { + // Sleep for a time longer than SR_RPC_CB_TIMEOUT + sleep(3); } - repo_path = TESTS_WORKING_DIR "/repository"; - sr_shm_path = "/dev/shm/gnmi-server-test"; + output.newPath("old-stats", "613", libyang::CreationOptions::Output); - if (fs::exists(repo_path)) { - fs::remove_all(repo_path); - } - if (fs::exists(sr_shm_path)) { - fs::remove_all(sr_shm_path); - } + return sysrepo::ErrorCode::Ok; +} - if (setenv("SYSREPO_REPOSITORY_PATH", repo_path.c_str(), 0)) { - throw std::runtime_error("Failed to setenv SYSREPO_REPOSITORY_PATH " + std::string(strerror(errno)) ); +class SetupSysrepo +{ + public: + SetupSysrepo() + { + auto sr_conn = sysrepo::Connection(); + std::filesystem::path dir{TESTS_SRC_DIR}; + std::filesystem::path path1 = dir / "gnmi-server-test.yang"; + std::filesystem::path path2 = dir / "gnmi-server-test-wine.yang"; + std::vector modules = {{.schema = path1}, + {.schema = path2}}; + + // install yang modules + sr_conn.installModules(modules, dir); + + sr_sess = sr_conn.sessionStart(); + + sub = sr_sess->onModuleChange("gnmi-server-test", module_change_cb, + "/gnmi-server-test:test2/custom-error"); + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='A']", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='B']", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='A']/counter", "1"); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='B']/counter", "2"); + sr_sess->setItem("/gnmi-server-test:test-state/cargo", ""); + sr_sess->applyChanges(); + + sub->onRPCAction("/gnmi-server-test:clear-stats", clear_stats_rpc_cb, 0, + sysrepo::SubscribeOptions::Default); + }; + + ~SetupSysrepo() + { + BOOST_LOG_TRIVIAL(debug) << "Removing sysrepo data" << std::endl; + sub.reset(); + sr_sess->getConnection().removeModules({"gnmi-server-test", "gnmi-server-test-wine"}); + // don't hold onto sr_sess forever. + sr_sess.reset(); } - if (setenv("SYSREPO_SHM_DIR", sr_shm_path.c_str(), 0)) { - throw std::runtime_error("Failed to setenv SYSREPO_SHM_DIR " + std::string(strerror(errno)) ); - } + // prevent copy and assignments. + SetupSysrepo(SetupSysrepo &) = delete; + SetupSysrepo &operator=(const SetupSysrepo &) = delete; + + private: + std::optional sub; +}; - if (setenv("SR_ENV_RUN_TESTS", "1", 0)) { - throw std::runtime_error("Failed to setenv SR_ENV_RUN_TESTS " + std::string(strerror(errno)) ); +class SetupTests +{ + public: + ~SetupTests() + { + BOOST_LOG_TRIVIAL(debug) << "Removing test directories" << std::endl; + fs::remove_all(log_path); + if (!repo_path.empty()) + { + fs::remove_all(repo_path); + fs::remove_all(sr_shm_path); + } + fs::remove("/tmp/gnmi-logrotate.lock"); } - // Print this so debugging is easier - BOOST_LOG_TRIVIAL(info) << "Running tests with " << std::endl - << "SYSREPO_REPOSITORY_PATH=" << repo_path << std::endl - << "SYSREPO_SHM_DIR=" << sr_shm_path << std::endl - << "SR_ENV_RUN_TESTS=1" << std::endl; - } + SetupTests() + { + log_path = string(GNMI_LOG_DIR); + fs::remove_all(log_path); + fs::remove("/tmp/gnmi-logrotate.lock"); + fs::create_directory(log_path); + fs::create_directory(log_path + "/raw"); + fs::create_directory(log_path + "/archives"); + + if (getenv("SYSREPO_REPOSITORY_PATH")) + { + // Don't set environment variables if user has already specified them! + // They may have good reason to do it, and may know what they are doing. + BOOST_LOG_TRIVIAL(warning) << "Using user-defined sysrepo env" << std::endl + << std::endl; + return; + } + + repo_path = TESTS_WORKING_DIR "/repository"; + sr_shm_path = "/dev/shm/gnmi-server-test"; + + if (fs::exists(repo_path)) + { + fs::remove_all(repo_path); + } + if (fs::exists(sr_shm_path)) + { + fs::remove_all(sr_shm_path); + } + + if (setenv("SYSREPO_REPOSITORY_PATH", repo_path.c_str(), 0)) + { + throw std::runtime_error("Failed to setenv SYSREPO_REPOSITORY_PATH " + + std::string(strerror(errno))); + } + + if (setenv("SYSREPO_SHM_DIR", sr_shm_path.c_str(), 0)) + { + throw std::runtime_error("Failed to setenv SYSREPO_SHM_DIR " + + std::string(strerror(errno))); + } + + if (setenv("SR_ENV_RUN_TESTS", "1", 0)) + { + throw std::runtime_error("Failed to setenv SR_ENV_RUN_TESTS " + + std::string(strerror(errno))); + } + + // Print this so debugging is easier + BOOST_LOG_TRIVIAL(info) << "Running tests with " << std::endl + << "SYSREPO_REPOSITORY_PATH=" << repo_path << std::endl + << "SYSREPO_SHM_DIR=" << sr_shm_path << std::endl + << "SR_ENV_RUN_TESTS=1" << std::endl; + } -private: - std::string repo_path, sr_shm_path, log_path; + private: + std::string repo_path, sr_shm_path, log_path; }; -int main(int argc, char* argv[]) +int main(int argc, char *argv[]) { - (void)argc; // unused - (void)argv; // unused + (void)argc; // unused + (void)argv; // unused - int result = EXIT_FAILURE; - string bind_addr = "localhost:40051"; - Log().setLevel(4); + int result = EXIT_FAILURE; + string bind_addr = "localhost:40051"; + Log().setLevel(4); - // setup signal handler only for SIGTERM, but not SIGINT, - // because it can interfere with a user running tests under gdb - SetupSignalHandler(false); + // setup signal handler only for SIGTERM, but not SIGINT, + // because it can interfere with a user running tests under gdb + SetupSignalHandler(false); - SetupTests _setup_tests; + SetupTests _setup_tests; - AuthBuilder auth; - auth.setInsecure(true); + AuthBuilder auth; + auth.setInsecure(true); - std::promise promise; - auto server_ready = promise.get_future(); - std::thread server_thread(RunServer, bind_addr, auth.build(), sysrepo::Connection(), std::move(promise)); + std::promise promise; + auto server_ready = promise.get_future(); + std::thread server_thread(RunServer, bind_addr, auth.build(), sysrepo::Connection(), + std::move(promise)); - SetupSysrepo _setup_sysrepo; + SetupSysrepo _setup_sysrepo; - // Wait for gnmi-server to be setup. - server_ready.wait(); + // Wait for gnmi-server to be setup. + server_ready.wait(); - client = gNMI::NewStub( - grpc::CreateChannel(bind_addr, grpc::InsecureChannelCredentials())); + client = gNMI::NewStub(grpc::CreateChannel(bind_addr, grpc::InsecureChannelCredentials())); - result = Catch::Session().run(argc, argv); + result = Catch::Session().run(argc, argv); - // cannot use raise() because, according to manpage - // In a multithreaded program it is equivalent to - // pthread_kill(pthread_self(), sig); - // and we don't want to send the signal to this thread. + // cannot use raise() because, according to manpage + // In a multithreaded program it is equivalent to + // pthread_kill(pthread_self(), sig); + // and we don't want to send the signal to this thread. - // signal gnmi_server to stop by triggering the signal handler - kill(getpid(), SIGTERM); + // signal gnmi_server to stop by triggering the signal handler + kill(getpid(), SIGTERM); - server_thread.join(); + server_thread.join(); - return result; + return result; } diff --git a/tests/rpc.cpp b/tests/rpc.cpp index 2b3f2ce..3577f33 100644 --- a/tests/rpc.cpp +++ b/tests/rpc.cpp @@ -14,9 +14,9 @@ * limitations under the License. */ +#include #include #include -#include #include @@ -27,89 +27,93 @@ using namespace std; using Catch::Matchers::Equals; -static inline bool -ends_with(string const &value, string const &ending) +static inline bool ends_with(string const &value, string const &ending) { - if (ending.size() > value.size()) return false; - return equal(ending.rbegin(), ending.rend(), value.rbegin()); + if (ending.size() > value.size()) + return false; + return equal(ending.rbegin(), ending.rend(), value.rbegin()); } /* Positive Tests */ -TEST_CASE("Rpc", "[rpc]") { - ClientContext ctx; - RpcRequest request; - RpcResponse response; - - xpath_to_path("/gnmi-server-test:clear-stats", request.mutable_path()); - request.mutable_val()->set_json_ietf_val("{\"interface\": \"eth45\"}"); - auto status = client->Rpc(&ctx, request, &response); - std::cout << status.error_message(); - CHECK(status.ok()); - - CHECK(response.timestamp() > 0); - CHECK_THAT(response.val().json_ietf_val(), Equals("{\"old-stats\":\"613\"}")); +TEST_CASE("Rpc", "[rpc]") +{ + ClientContext ctx; + RpcRequest request; + RpcResponse response; + + xpath_to_path("/gnmi-server-test:clear-stats", request.mutable_path()); + request.mutable_val()->set_json_ietf_val("{\"interface\": \"eth45\"}"); + auto status = client->Rpc(&ctx, request, &response); + std::cout << status.error_message(); + CHECK(status.ok()); + + CHECK(response.timestamp() > 0); + CHECK_THAT(response.val().json_ietf_val(), Equals("{\"old-stats\":\"613\"}")); } /* Negative Tests */ -TEST_CASE("Rpc request (empty error)", "[rpc-neg]") { - ClientContext ctx; - RpcRequest request; - RpcResponse response; - - xpath_to_path("/gnmi-server-test:clear-stats", request.mutable_path()); - request.mutable_val()->set_json_ietf_val("{\"interface\": \"none\", \"errors\": [null]}"); - auto status = client->Rpc(&ctx, request, &response); - std::cout << status.error_message(); - CHECK(status.ok()); - - CHECK(response.timestamp() > 0); - CHECK_THAT(response.val().json_ietf_val(), Equals("{\"old-stats\":\"613\"}")); +TEST_CASE("Rpc request (empty error)", "[rpc-neg]") +{ + ClientContext ctx; + RpcRequest request; + RpcResponse response; + + xpath_to_path("/gnmi-server-test:clear-stats", request.mutable_path()); + request.mutable_val()->set_json_ietf_val("{\"interface\": \"none\", \"errors\": [null]}"); + auto status = client->Rpc(&ctx, request, &response); + std::cout << status.error_message(); + CHECK(status.ok()); + + CHECK(response.timestamp() > 0); + CHECK_THAT(response.val().json_ietf_val(), Equals("{\"old-stats\":\"613\"}")); } - -TEST_CASE("Rpc request (no val type)", "[rpc-neg]") { - ClientContext ctx; - RpcRequest request; - RpcResponse response; - - xpath_to_path("/gnmi-server-test:clear-stats", request.mutable_path()); - request.mutable_val(); - auto status = client->Rpc(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); - CHECK_THAT(status.error_message(), Equals("Value not set")); - - CHECK(response.timestamp() == 0); - CHECK(response.val().value_case() == TypedValue::VALUE_NOT_SET); +TEST_CASE("Rpc request (no val type)", "[rpc-neg]") +{ + ClientContext ctx; + RpcRequest request; + RpcResponse response; + + xpath_to_path("/gnmi-server-test:clear-stats", request.mutable_path()); + request.mutable_val(); + auto status = client->Rpc(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); + CHECK_THAT(status.error_message(), Equals("Value not set")); + + CHECK(response.timestamp() == 0); + CHECK(response.val().value_case() == TypedValue::VALUE_NOT_SET); } -TEST_CASE("Rpc request (error)", "[rpc-neg]") { - ClientContext ctx; - RpcRequest request; - RpcResponse response; - - xpath_to_path("/gnmi-server-test:clear-stats", request.mutable_path()); - request.mutable_val()->set_json_ietf_val("{\"interface\": \"error\"}"); - auto status = client->Rpc(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::ABORTED); - CHECK_THAT(status.error_message(), Equals("Fiddlesticks: /gnmi-server-test:clear-stats")); - - CHECK(response.timestamp() == 0); - CHECK(response.val().value_case() == TypedValue::VALUE_NOT_SET); +TEST_CASE("Rpc request (error)", "[rpc-neg]") +{ + ClientContext ctx; + RpcRequest request; + RpcResponse response; + + xpath_to_path("/gnmi-server-test:clear-stats", request.mutable_path()); + request.mutable_val()->set_json_ietf_val("{\"interface\": \"error\"}"); + auto status = client->Rpc(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::ABORTED); + CHECK_THAT(status.error_message(), Equals("Fiddlesticks: /gnmi-server-test:clear-stats")); + + CHECK(response.timestamp() == 0); + CHECK(response.val().value_case() == TypedValue::VALUE_NOT_SET); } -TEST_CASE("Rpc request (timeout)", "[rpc-neg]") { - ClientContext ctx; - RpcRequest request; - RpcResponse response; - - xpath_to_path("/gnmi-server-test:clear-stats", request.mutable_path()); - request.mutable_val()->set_json_ietf_val("{\"interface\": \"timeout\"}"); - auto status = client->Rpc(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::ABORTED); - CHECK(ends_with(status.error_message(), "processing timed out.")); - - CHECK(response.timestamp() == 0); - CHECK(response.val().value_case() == TypedValue::VALUE_NOT_SET); +TEST_CASE("Rpc request (timeout)", "[rpc-neg]") +{ + ClientContext ctx; + RpcRequest request; + RpcResponse response; + + xpath_to_path("/gnmi-server-test:clear-stats", request.mutable_path()); + request.mutable_val()->set_json_ietf_val("{\"interface\": \"timeout\"}"); + auto status = client->Rpc(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::ABORTED); + CHECK(ends_with(status.error_message(), "processing timed out.")); + + CHECK(response.timestamp() == 0); + CHECK(response.val().value_case() == TypedValue::VALUE_NOT_SET); } diff --git a/tests/set.cpp b/tests/set.cpp index 441682d..042a6f7 100644 --- a/tests/set.cpp +++ b/tests/set.cpp @@ -14,1443 +14,1554 @@ * limitations under the License. */ +#include #include #include -#include #include -#include "main.h" #include "../src/utils/log.h" +#include "main.h" using namespace std; -using Catch::Matchers::Equals; using Catch::Matchers::Contains; +using Catch::Matchers::Equals; static uint64_t id = 1; -TEST_CASE("Top-level Set request (replace) empty", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - using namespace libyang; - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test4/params[.=\"abc\"]", ""); - sr_sess->setItem("/gnmi-server-test:test/things[name=\"B\"]/enabled", "false"); - sr_sess->applyChanges(); - - auto replace = request.add_replace(); - - xpath_to_path("/*", replace->mutable_path()); - replace->mutable_val()->set_json_ietf_val("{}"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_REPLACE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/*")); - - auto parent = sr_sess->getData("/*"); - auto json = parent->printStr(DataFormat::JSON, PrintFlags::WithSiblings | PrintFlags::Shrink); - REQUIRE_THAT(json.value(), Equals("{}")); -} +TEST_CASE("Top-level Set request (replace) empty", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + using namespace libyang; + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->setItem("/gnmi-server-test:test4/params[.=\"abc\"]", ""); + sr_sess->setItem("/gnmi-server-test:test/things[name=\"B\"]/enabled", "false"); + sr_sess->applyChanges(); + auto replace = request.add_replace(); -TEST_CASE("Empty leaf Set request (update)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); + xpath_to_path("/*", replace->mutable_path()); + replace->mutable_val()->set_json_ietf_val("{}"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_REPLACE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/*")); + + auto parent = sr_sess->getData("/*"); + auto json = parent->printStr(DataFormat::JSON, PrintFlags::Siblings | PrintFlags::Shrink); + REQUIRE_THAT(json.value(), Equals("{}")); +} - xpath_to_path("/gnmi-server-test:test/things[name='A']/ready", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("[null]"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); +TEST_CASE("Empty leaf Set request (update)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='A']/ready")); + xpath_to_path("/gnmi-server-test:test/things[name='A']/ready", update->mutable_path()); + update->mutable_val()->set_json_ietf_val("[null]"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); - sr_sess->switchDatastore(sysrepo::Datastore::Running); - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/ready"); + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='A']/ready")); - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("")); + sr_sess->switchDatastore(sysrepo::Datastore::Running); + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/ready"); - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); - sr_sess->applyChanges(); -} + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("")); -TEST_CASE("Top-level Set request leaflist (replace)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - using namespace libyang; - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test4/params[.=\"abc\"]", ""); - sr_sess->setItem("/gnmi-server-test:test/things[name=\"B\"]/enabled", "false"); - sr_sess->applyChanges(); - - auto replace = request.add_replace(); - - xpath_to_path("/gnmi-server-test:test4/params", replace->mutable_path()); - replace->mutable_val()->set_json_ietf_val("[\"speed\", \"mtu\", \"queue\"]"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_REPLACE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test4/params")); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - auto val = sr_sess->getOneNode("/gnmi-server-test:test4/params[.='mtu']"); - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("mtu")); - - auto parent = sr_sess->getData("/gnmi-server-test:test4/params"); - auto json = parent->printStr(DataFormat::JSON, PrintFlags::WithSiblings | PrintFlags::Shrink); - - CHECK_THAT(json.value(), Equals("{\"gnmi-server-test:test4\":{\"params\":[\"mtu\",\"queue\",\"speed\"]}}")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test4"); - sr_sess->applyChanges(); + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); + sr_sess->applyChanges(); } -TEST_CASE("Top-level Set request inner leaflist (replace)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - using namespace libyang; - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test/things[name=\"B\"]/enabled", "false"); - sr_sess->setItem("/gnmi-server-test:test/things[name=\"A\"]/enabled", "true"); - sr_sess->setItem("/gnmi-server-test:test/things[name=\"A\"]/amount-history", "1"); - sr_sess->setItem("/gnmi-server-test:test/things[name=\"A\"]/amount-history", "2"); - sr_sess->applyChanges(); - - auto replace = request.add_replace(); - - xpath_to_path("/gnmi-server-test:test/things[name=\"A\"]/amount-history", replace->mutable_path()); - replace->mutable_val()->set_json_ietf_val("[4,5,6]"); - auto status = client->Set(&ctx, request, &response); - REQUIRE(status.ok()); - - auto vals = sr_sess->getData("/gnmi-server-test:test/things[name=\"A\"]/amount-history"); - auto json = vals->printStr(DataFormat::JSON, PrintFlags::WithSiblings | PrintFlags::Shrink); - CHECK_THAT(json.value(), Equals("{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\"," - "\"amount-history\":[4,5,6]}]}}")); - - // check that all unrelated nodes are not altered - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name=\"A\"]/enabled"); - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - val = sr_sess->getOneNode("/gnmi-server-test:test/things[name=\"B\"]/enabled"); - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("false")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test"); - sr_sess->applyChanges(); +TEST_CASE("Top-level Set request leaflist (replace)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + using namespace libyang; + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->setItem("/gnmi-server-test:test4/params[.=\"abc\"]", ""); + sr_sess->setItem("/gnmi-server-test:test/things[name=\"B\"]/enabled", "false"); + sr_sess->applyChanges(); + + auto replace = request.add_replace(); + + xpath_to_path("/gnmi-server-test:test4/params", replace->mutable_path()); + replace->mutable_val()->set_json_ietf_val("[\"speed\", \"mtu\", \"queue\"]"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_REPLACE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test4/params")); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + auto val = sr_sess->getOneNode("/gnmi-server-test:test4/params[.='mtu']"); + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("mtu")); + + auto parent = sr_sess->getData("/gnmi-server-test:test4/params"); + auto json = parent->printStr(DataFormat::JSON, PrintFlags::Siblings | PrintFlags::Shrink); + + CHECK_THAT(json.value(), + Equals("{\"gnmi-server-test:test4\":{\"params\":[\"mtu\",\"queue\",\"speed\"]}}")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test4"); + sr_sess->applyChanges(); } -TEST_CASE("Top-level Set request leaflist (update)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - using namespace libyang; - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test4/params[.=\"abc\"]", ""); - sr_sess->applyChanges(); - - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test4/params", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("[\"speed\", \"mtu\", \"queue\"]"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test4/params")); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - auto val = sr_sess->getOneNode("/gnmi-server-test:test4/params[.='mtu']"); - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("mtu")); - - auto parent = sr_sess->getData("/gnmi-server-test:test4/params"); - auto json = parent->printStr(DataFormat::JSON, PrintFlags::WithSiblings | PrintFlags::Shrink); - - CHECK_THAT(json.value(), Equals("{\"gnmi-server-test:test4\":{\"params\":[\"abc\",\"mtu\",\"queue\",\"speed\"]}}")); - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test4"); - sr_sess->applyChanges(); +// TODO: replace Set replaces all of the data, not just specific leaf-list + +// TEST_CASE("Top-level Set request inner leaflist (replace)", "[set]") +// { +// ClientContext ctx; +// SetRequest request; +// SetResponse response; +// request.set_transaction_id(id++); +// using namespace libyang; +// sr_sess->switchDatastore(sysrepo::Datastore::Running); +// sr_sess->setItem("/gnmi-server-test:test/things[name=\"B\"]/enabled", "false"); +// sr_sess->setItem("/gnmi-server-test:test/things[name=\"A\"]/enabled", "true"); +// sr_sess->setItem("/gnmi-server-test:test/things[name=\"A\"]/amount-history", "1"); +// sr_sess->setItem("/gnmi-server-test:test/things[name=\"A\"]/amount-history", "2"); +// sr_sess->applyChanges(); + +// auto replace = request.add_replace(); + +// xpath_to_path("/gnmi-server-test:test/things[name=\"A\"]/amount-history", +// replace->mutable_path()); +// replace->mutable_val()->set_json_ietf_val("[4,5,6]"); +// auto status = client->Set(&ctx, request, &response); +// REQUIRE(status.ok()); + +// auto vals = sr_sess->getData("/gnmi-server-test:test/things[name=\"A\"]/amount-history"); +// auto json = vals->printStr(DataFormat::JSON, PrintFlags::Siblings | PrintFlags::Shrink); +// CHECK_THAT(json.value(), Equals("{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\"," +// "\"amount-history\":[4,5,6]}]}}")); + +// // check that all unrelated nodes are not altered +// auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name=\"A\"]/enabled"); +// CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); +// val = sr_sess->getOneNode("/gnmi-server-test:test/things[name=\"B\"]/enabled"); +// CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("false")); + +// // Clean up +// sr_sess->deleteItem("/gnmi-server-test:test"); +// sr_sess->applyChanges(); +// } + +TEST_CASE("Top-level Set request leaflist (update)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + using namespace libyang; + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->setItem("/gnmi-server-test:test4/params[.=\"abc\"]", ""); + sr_sess->applyChanges(); + + auto update = request.add_update(); + + xpath_to_path("/gnmi-server-test:test4/params", update->mutable_path()); + update->mutable_val()->set_json_ietf_val("[\"speed\", \"mtu\", \"queue\"]"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test4/params")); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + auto val = sr_sess->getOneNode("/gnmi-server-test:test4/params[.='mtu']"); + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("mtu")); + + auto parent = sr_sess->getData("/gnmi-server-test:test4/params"); + auto json = parent->printStr(DataFormat::JSON, PrintFlags::Siblings | PrintFlags::Shrink); + + CHECK_THAT( + json.value(), + Equals("{\"gnmi-server-test:test4\":{\"params\":[\"abc\",\"mtu\",\"queue\",\"speed\"]}}")); + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test4"); + sr_sess->applyChanges(); } /* Positive Tests */ -TEST_CASE("Top-level Set request (update)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":true, \"ready\":[null]}]}}"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/")); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - - val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/ready"); - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("")); - - // Also verify it makes it into the startup datastore since confirmed commit not used - sr_sess->switchDatastore(sysrepo::Datastore::Startup); - val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - - // Clean up - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); - sr_sess->applyChanges(); -} +TEST_CASE("Top-level Set request (update)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/", update->mutable_path()); + update->mutable_val()->set_json_ietf_val("{\"gnmi-server-test:test\":{\"things\":[{\"name\":" + "\"A\",\"enabled\":true, \"ready\":[null]}]}}"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/")); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + + val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/ready"); + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("")); -TEST_CASE("Top-level Set request (replace)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto replace = request.add_replace(); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); - sr_sess->setItem("/gnmi-server-test:test2/enabled", "true"); - sr_sess->applyChanges(); - - xpath_to_path("/", replace->mutable_path()); - replace->mutable_val()->set_json_ietf_val("{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":true}]}}"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_REPLACE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/")); - - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - - // Item B should have been removed since the whole config was replaced and the new config only contained item A - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"), Contains("SR_ERR_NOT_FOUND")); - // test2 should have been removed since the whole config was replaced and the new config includes only the test container - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test2/enabled"), Contains("SR_ERR_NOT_FOUND")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); - sr_sess->applyChanges(); + // Also verify it makes it into the startup datastore since confirmed commit not used + sr_sess->switchDatastore(sysrepo::Datastore::Startup); + val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + + // Clean up + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); + sr_sess->applyChanges(); } -TEST_CASE("Top-level Set request (multi replace)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto replace = request.add_replace(); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); - sr_sess->setItem("/gnmi-server-test:test2/enabled", "true"); - sr_sess->applyChanges(); - - xpath_to_path("/", replace->mutable_path()); - replace->mutable_val()->set_json_ietf_val("{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":true}]},\"gnmi-server-test:test2\":{}}"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_REPLACE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/")); - - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - - // Item B should have been removed since the whole config was replaced and the new config only contained item A - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"), Contains("SR_ERR_NOT_FOUND")); - // test2 should have been removed since the whole config was replaced and the test2 container was empty in the new config - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test2/enabled"), Contains("SR_ERR_NOT_FOUND")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); - sr_sess->applyChanges(); +TEST_CASE("Top-level Set request (replace)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto replace = request.add_replace(); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); + sr_sess->setItem("/gnmi-server-test:test2/enabled", "true"); + sr_sess->applyChanges(); + + xpath_to_path("/", replace->mutable_path()); + replace->mutable_val()->set_json_ietf_val( + "{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":true}]}}"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_REPLACE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/")); + + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + + // Item B should have been removed since the whole config was replaced and the new config only + // contained item A + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"), + Contains("SR_ERR_NOT_FOUND")); + // test2 should have been removed since the whole config was replaced and the new config + // includes only the test container + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test2/enabled"), + Contains("SR_ERR_NOT_FOUND")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); + sr_sess->applyChanges(); } -TEST_CASE("Top-level Set request (multi replace + update)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto replace = request.add_replace(); - auto update = request.add_update(); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); - sr_sess->setItem("/gnmi-server-test:test2/enabled", "true"); - sr_sess->applyChanges(); - - xpath_to_path("/", replace->mutable_path()); - replace->mutable_val()->set_json_ietf_val("{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":true}]},\"gnmi-server-test:test2\":{\"enabled2\":true}}"); - xpath_to_path("/", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":false}]},\"gnmi-server-test:test2\":{\"enabled2\":false}}"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 2); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_REPLACE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/")); - CHECK(response.response().Get(1).op() == gnmi::UpdateResult_Operation_UPDATE); - path = path_to_xpath(response.response().Get(1).path()); - CHECK_THAT(path, Equals("/")); - - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); - - // The update should be applied after the replace - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("false")); - - // Item B should have been removed since the whole config was replaced and the new config only contained item A - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"), Contains("SR_ERR_NOT_FOUND")); - // The replace should have caused this node to be removed - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test2/enabled"), Contains("SR_ERR_NOT_FOUND")); - // The update should be applied after the replace - val = sr_sess->getOneNode("/gnmi-server-test:test2/enabled2"); - - // The update should be applied after the replace - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("false")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); - sr_sess->deleteItem("/gnmi-server-test:test2/enabled"); - sr_sess->applyChanges(); +TEST_CASE("Top-level Set request (multi replace)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto replace = request.add_replace(); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); + sr_sess->setItem("/gnmi-server-test:test2/enabled", "true"); + sr_sess->applyChanges(); + + xpath_to_path("/", replace->mutable_path()); + replace->mutable_val()->set_json_ietf_val( + "{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":true}]},\"gnmi-server-" + "test:test2\":{}}"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_REPLACE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/")); + + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + + // Item B should have been removed since the whole config was replaced and the new config only + // contained item A + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"), + Contains("SR_ERR_NOT_FOUND")); + // test2 should have been removed since the whole config was replaced and the test2 container + // was empty in the new config + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test2/enabled"), + Contains("SR_ERR_NOT_FOUND")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); + sr_sess->applyChanges(); } -TEST_CASE("Top-level Set request (delete)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); - sr_sess->applyChanges(); - - xpath_to_path("/", request.add_delete_()); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/")); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"), Contains("SR_ERR_NOT_FOUND")); +TEST_CASE("Top-level Set request (multi replace + update)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto replace = request.add_replace(); + auto update = request.add_update(); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); + sr_sess->setItem("/gnmi-server-test:test2/enabled", "true"); + sr_sess->applyChanges(); + + xpath_to_path("/", replace->mutable_path()); + replace->mutable_val()->set_json_ietf_val( + "{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":true}]},\"gnmi-server-" + "test:test2\":{\"enabled2\":true}}"); + xpath_to_path("/", update->mutable_path()); + update->mutable_val()->set_json_ietf_val( + "{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":false}]},\"gnmi-" + "server-test:test2\":{\"enabled2\":false}}"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 2); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_REPLACE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/")); + CHECK(response.response().Get(1).op() == gnmi::UpdateResult_Operation_UPDATE); + path = path_to_xpath(response.response().Get(1).path()); + CHECK_THAT(path, Equals("/")); + + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); + + // The update should be applied after the replace + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("false")); + + // Item B should have been removed since the whole config was replaced and the new config only + // contained item A + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"), + Contains("SR_ERR_NOT_FOUND")); + // The replace should have caused this node to be removed + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test2/enabled"), + Contains("SR_ERR_NOT_FOUND")); + // The update should be applied after the replace + val = sr_sess->getOneNode("/gnmi-server-test:test2/enabled2"); + + // The update should be applied after the replace + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("false")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); + sr_sess->deleteItem("/gnmi-server-test:test2/enabled"); + sr_sess->applyChanges(); } -TEST_CASE("Path-based Set request (delete)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); - sr_sess->applyChanges(); - - xpath_to_path("/gnmi-server-test:test/things[name='B']/enabled", request.add_delete_()); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='B']/enabled")); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"), Contains("SR_ERR_NOT_FOUND")); - sr_sess->deleteItem("/gnmi-server-test:test/things[name='B']"); +TEST_CASE("Top-level Set request (delete)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); + sr_sess->applyChanges(); + + xpath_to_path("/", request.add_delete_()); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/")); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"), + Contains("SR_ERR_NOT_FOUND")); } -TEST_CASE("Path-based Set request (update)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("{\"enabled\":true}"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='A']")); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); - sr_sess->applyChanges(); +TEST_CASE("Path-based Set request (delete)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); + sr_sess->applyChanges(); + + xpath_to_path("/gnmi-server-test:test/things[name='B']/enabled", request.add_delete_()); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='B']/enabled")); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"), + Contains("SR_ERR_NOT_FOUND")); + sr_sess->deleteItem("/gnmi-server-test:test/things[name='B']"); } -TEST_CASE("Leaf Set request (update)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']/name", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("\"A\""); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='A']/name")); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/name"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("A")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); - sr_sess->applyChanges(); +TEST_CASE("Path-based Set request (update)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/gnmi-server-test:test/things[name='A']", update->mutable_path()); + update->mutable_val()->set_json_ietf_val("{\"enabled\":true}"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='A']")); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); + sr_sess->applyChanges(); } -TEST_CASE("Set request (with prefix)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']", request.mutable_prefix()); - xpath_to_path("/name", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("\"A\""); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - auto prefix = path_to_xpath(response.prefix()); - CHECK_THAT(prefix, Equals("/gnmi-server-test:test/things[name='A']")); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/name")); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/name"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("A")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); - sr_sess->applyChanges(); +TEST_CASE("Leaf Set request (update)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/gnmi-server-test:test/things[name='A']/name", update->mutable_path()); + update->mutable_val()->set_json_ietf_val("\"A\""); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='A']/name")); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/name"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("A")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); + sr_sess->applyChanges(); } -TEST_CASE("Set request (with empty prefix)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - // Add empty prefix - request.mutable_prefix(); - xpath_to_path("/gnmi-server-test:test/things[name='A']/name", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("\"A\""); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - auto prefix = path_to_xpath(response.prefix()); - CHECK_THAT(prefix, Equals("/")); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='A']/name")); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/name"); - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("A")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); - sr_sess->applyChanges(); +TEST_CASE("Set request (with prefix)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/gnmi-server-test:test/things[name='A']", request.mutable_prefix()); + xpath_to_path("/name", update->mutable_path()); + update->mutable_val()->set_json_ietf_val("\"A\""); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + auto prefix = path_to_xpath(response.prefix()); + CHECK_THAT(prefix, Equals("/gnmi-server-test:test/things[name='A']")); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/name")); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/name"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("A")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); + sr_sess->applyChanges(); } -TEST_CASE("Set request transaction (update)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":true}]}}"); - update = request.add_update(); - xpath_to_path("/", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"B\",\"enabled\":true}]}}"); - - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 2); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/")); - CHECK(response.response().Get(1).op() == gnmi::UpdateResult_Operation_UPDATE); - path = path_to_xpath(response.response().Get(1).path()); - CHECK_THAT(path, Equals("/")); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); - sr_sess->deleteItem("/gnmi-server-test:test/things[name='B']"); - sr_sess->applyChanges(); +TEST_CASE("Set request (with empty prefix)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + // Add empty prefix + request.mutable_prefix(); + xpath_to_path("/gnmi-server-test:test/things[name='A']/name", update->mutable_path()); + update->mutable_val()->set_json_ietf_val("\"A\""); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + auto prefix = path_to_xpath(response.prefix()); + CHECK_THAT(prefix, Equals("/")); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='A']/name")); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/name"); + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("A")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); + sr_sess->applyChanges(); } -TEST_CASE("Set request transaction (delete+update)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); - sr_sess->applyChanges(); - - xpath_to_path("/gnmi-server-test:test/things[name='B']", request.add_delete_()); - xpath_to_path("/", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":true}]}}"); - - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 2); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='B']")); - CHECK(response.response().Get(1).op() == gnmi::UpdateResult_Operation_UPDATE); - path = path_to_xpath(response.response().Get(1).path()); - CHECK_THAT(path, Equals("/")); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']"), Contains("SR_ERR_NOT_FOUND")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); - sr_sess->applyChanges(); +TEST_CASE("Set request transaction (update)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/", update->mutable_path()); + update->mutable_val()->set_json_ietf_val( + "{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":true}]}}"); + update = request.add_update(); + xpath_to_path("/", update->mutable_path()); + update->mutable_val()->set_json_ietf_val( + "{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"B\",\"enabled\":true}]}}"); + + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 2); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/")); + CHECK(response.response().Get(1).op() == gnmi::UpdateResult_Operation_UPDATE); + path = path_to_xpath(response.response().Get(1).path()); + CHECK_THAT(path, Equals("/")); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); + sr_sess->deleteItem("/gnmi-server-test:test/things[name='B']"); + sr_sess->applyChanges(); } -TEST_CASE("Set request (delete with wildcards)", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test/things[name='A']/enabled", "true"); - sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); - sr_sess->applyChanges(); - - xpath_to_path("/gnmi-server-test:test/*/enabled", request.add_delete_()); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test/*/enabled")); - - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"), Contains("SR_ERR_NOT_FOUND")); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"), Contains("SR_ERR_NOT_FOUND")); - sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); - sr_sess->deleteItem("/gnmi-server-test:test/things[name='B']"); +TEST_CASE("Set request transaction (delete+update)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); + sr_sess->applyChanges(); + + xpath_to_path("/gnmi-server-test:test/things[name='B']", request.add_delete_()); + xpath_to_path("/", update->mutable_path()); + update->mutable_val()->set_json_ietf_val( + "{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":true}]}}"); + + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 2); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='B']")); + CHECK(response.response().Get(1).op() == gnmi::UpdateResult_Operation_UPDATE); + path = path_to_xpath(response.response().Get(1).path()); + CHECK_THAT(path, Equals("/")); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + + // TODO: try catch block + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']"), + Contains("SR_ERR_NOT_FOUND")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); + sr_sess->applyChanges(); } -// gNMI spec §3.4.6: In the case that a path specifies an element within the data tree that does not exist, these deletes MUST be silently accepted. -TEST_CASE("Set request (delete) with non-existent path", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - - xpath_to_path("/gnmi-server-test:test-state/things[name='not-found']", request.add_delete_()); - - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::OK); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='not-found']")); +TEST_CASE("Set request (delete with wildcards)", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->setItem("/gnmi-server-test:test/things[name='A']/enabled", "true"); + sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); + sr_sess->applyChanges(); + + xpath_to_path("/gnmi-server-test:test/*/enabled", request.add_delete_()); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test/*/enabled")); + + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"), + Contains("SR_ERR_NOT_FOUND")); + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"), + Contains("SR_ERR_NOT_FOUND")); + sr_sess->deleteItem("/gnmi-server-test:test/things[name='A']"); + sr_sess->deleteItem("/gnmi-server-test:test/things[name='B']"); } -TEST_CASE("Set request (delete) the same path", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); +// gNMI spec §3.4.6: In the case that a path specifies an element within the data tree that does not +// exist, these deletes MUST be silently accepted. +TEST_CASE("Set request (delete) with non-existent path", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test/things[name='foo']/enabled", "true"); - sr_sess->applyChanges(); + xpath_to_path("/gnmi-server-test:test-state/things[name='not-found']", request.add_delete_()); - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']/enabled"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::OK); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='not-found']")); +} + +TEST_CASE("Set request (delete) the same path", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->setItem("/gnmi-server-test:test/things[name='foo']/enabled", "true"); + sr_sess->applyChanges(); - xpath_to_path("/gnmi-server-test:test/things[name='foo']", request.add_delete_()); - xpath_to_path("/gnmi-server-test:test/things[name='foo']", request.add_delete_()); + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']/enabled"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::OK); + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 2); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='foo']")); + xpath_to_path("/gnmi-server-test:test/things[name='foo']", request.add_delete_()); + xpath_to_path("/gnmi-server-test:test/things[name='foo']", request.add_delete_()); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']"), - Contains("SR_ERR_NOT_FOUND")); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']/enabled"), - Contains("SR_ERR_NOT_FOUND")); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::OK); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 2); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='foo']")); + + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']"), + Contains("SR_ERR_NOT_FOUND")); + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']/enabled"), + Contains("SR_ERR_NOT_FOUND")); } -TEST_CASE("Set request (delete) child then parent", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test/things[name='foo']/enabled", "true"); - sr_sess->applyChanges(); - - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']/enabled"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - - xpath_to_path("/gnmi-server-test:test/things[name='foo']/enabled", request.add_delete_()); - xpath_to_path("/gnmi-server-test:test/things[name='foo']", request.add_delete_()); - - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::OK); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 2); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); - CHECK_THAT(path_to_xpath(response.response().Get(0).path()), - Equals("/gnmi-server-test:test/things[name='foo']/enabled")); - CHECK_THAT(path_to_xpath(response.response().Get(1).path()), - Equals("/gnmi-server-test:test/things[name='foo']")); - - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']"), - Contains("SR_ERR_NOT_FOUND")); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']/enabled"), - Contains("SR_ERR_NOT_FOUND")); +TEST_CASE("Set request (delete) child then parent", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->setItem("/gnmi-server-test:test/things[name='foo']/enabled", "true"); + sr_sess->applyChanges(); + + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']/enabled"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + + xpath_to_path("/gnmi-server-test:test/things[name='foo']/enabled", request.add_delete_()); + xpath_to_path("/gnmi-server-test:test/things[name='foo']", request.add_delete_()); + + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::OK); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 2); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); + CHECK_THAT(path_to_xpath(response.response().Get(0).path()), + Equals("/gnmi-server-test:test/things[name='foo']/enabled")); + CHECK_THAT(path_to_xpath(response.response().Get(1).path()), + Equals("/gnmi-server-test:test/things[name='foo']")); + + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']"), + Contains("SR_ERR_NOT_FOUND")); + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']/enabled"), + Contains("SR_ERR_NOT_FOUND")); } +TEST_CASE("Set request (delete) parent then child", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->setItem("/gnmi-server-test:test/things[name='foo']/enabled", "true"); + sr_sess->applyChanges(); -TEST_CASE("Set request (delete) parent then child", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test/things[name='foo']/enabled", "true"); - sr_sess->applyChanges(); - - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']/enabled"); - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - - xpath_to_path("/gnmi-server-test:test/things[name='foo']", request.add_delete_()); - xpath_to_path("/gnmi-server-test:test/things[name='foo']/enabled", request.add_delete_()); - - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::OK); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 2); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); - CHECK_THAT(path_to_xpath(response.response().Get(0).path()), - Equals("/gnmi-server-test:test/things[name='foo']")); - CHECK_THAT(path_to_xpath(response.response().Get(1).path()), - Equals("/gnmi-server-test:test/things[name='foo']/enabled")); - - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']"), - Contains("SR_ERR_NOT_FOUND")); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']/enabled"), - Contains("SR_ERR_NOT_FOUND")); + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']/enabled"); + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + + xpath_to_path("/gnmi-server-test:test/things[name='foo']", request.add_delete_()); + xpath_to_path("/gnmi-server-test:test/things[name='foo']/enabled", request.add_delete_()); + + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::OK); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 2); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_DELETE); + CHECK_THAT(path_to_xpath(response.response().Get(0).path()), + Equals("/gnmi-server-test:test/things[name='foo']")); + CHECK_THAT(path_to_xpath(response.response().Get(1).path()), + Equals("/gnmi-server-test:test/things[name='foo']/enabled")); + + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']"), + Contains("SR_ERR_NOT_FOUND")); + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='foo']/enabled"), + Contains("SR_ERR_NOT_FOUND")); } -TEST_CASE("Set request for list with composite key", "[set]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); +TEST_CASE("Set request for list with composite key", "[set]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); - sr_sess->switchDatastore(sysrepo::Datastore::Running); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']/data"), - Contains("SR_ERR_NOT_FOUND")); + sr_sess->switchDatastore(sysrepo::Datastore::Running); + CHECK_THROWS_WITH( + sr_sess->getOneNode("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']/data"), + Contains("SR_ERR_NOT_FOUND")); - xpath_to_path("/gnmi-server-test:test3/complex-list[name='foo'][type='bar']/data", - update->mutable_path()); + xpath_to_path("/gnmi-server-test:test3/complex-list[name='foo'][type='bar']/data", + update->mutable_path()); - // check parsing of the xpath_to_path() function - auto reqpath = update->path(); - CHECK(reqpath.elem_size() == 3); - CHECK_THAT(reqpath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(reqpath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(reqpath.elem(1).key().at("name"), Equals("foo")); - CHECK_THAT(reqpath.elem(1).key().at("type"), Equals("bar")); - CHECK_THAT(reqpath.elem(2).name(), Equals("data")); + // check parsing of the xpath_to_path() function + auto reqpath = update->path(); + CHECK(reqpath.elem_size() == 3); + CHECK_THAT(reqpath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(reqpath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(reqpath.elem(1).key().at("name"), Equals("foo")); + CHECK_THAT(reqpath.elem(1).key().at("type"), Equals("bar")); + CHECK_THAT(reqpath.elem(2).name(), Equals("data")); - update->mutable_val()->set_json_ietf_val("\"baz\""); + update->mutable_val()->set_json_ietf_val("\"baz\""); - auto status = client->Set(&ctx, request, &response); - CHECK(status.ok()); + auto status = client->Set(&ctx, request, &response); + CHECK(status.ok()); - auto val = sr_sess->getOneNode("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']/data"); - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("baz")); + auto val = + sr_sess->getOneNode("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']/data"); + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("baz")); - // cleanup - sr_sess->deleteItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']"); - sr_sess->applyChanges(); + // cleanup + sr_sess->deleteItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']"); + sr_sess->applyChanges(); } /* Scale Tests */ -std::string generate_random_string(size_t length) { +std::string generate_random_string(size_t length) +{ std::string str; - str.reserve(length); // Reserve space for the string to avoid reallocations + str.reserve(length + 2); // Reserve space for the string to avoid reallocations - for (size_t i = 0; i < length; ++i) { - str += 'a' + rand() % 26; + str += '"'; + for (size_t i = 0; i < length; ++i) + { + str += 'a' + rand() % 26; } + str += '"'; return str; } -static size_t get_directory_size(const std::string dir) { - using namespace std::filesystem; - size_t size = 0; - for (const auto& entry : recursive_directory_iterator(dir)) { - if (!is_directory(entry)) { - size += static_cast(file_size(entry)); +static size_t get_directory_size(const std::string dir) +{ + using namespace std::filesystem; + size_t size = 0; + for (const auto &entry : recursive_directory_iterator(dir)) + { + if (!is_directory(entry)) + { + size += static_cast(file_size(entry)); + } } - } - return size; + return size; } -TEST_CASE("Scaled Set request (update)", "[set-scale]") { - using namespace libyang; - sr_sess->switchDatastore(sysrepo::Datastore::Running); - ScaleTestLogLevelReducer _log_reducer; +TEST_CASE("Scaled Set request (update)", "[set-scale]") +{ + using namespace libyang; + sr_sess->switchDatastore(sysrepo::Datastore::Running); + ScaleTestLogLevelReducer _log_reducer; + + for (int i = 0; i < 100; i++) + { + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + xpath_to_path("/", request.add_delete_()); + + for (int j = 0; j < 100; j++) + { + auto update = request.add_update(); + auto xpath = + "/gnmi-server-test:test/things[name=\"A" + std::to_string(j) + "\"]/description"; + + xpath_to_path(xpath, update->mutable_path()); + update->mutable_val()->set_json_ietf_val(generate_random_string(1000)); + } + auto status = client->Set(&ctx, request, &response); + REQUIRE(status.ok()); + } + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test/things"); + sr_sess->applyChanges(); + + auto parent = sr_sess->getData("/*"); + auto json = parent->printStr(DataFormat::JSON, PrintFlags::Siblings | PrintFlags::Shrink); + CHECK_THAT(json.value(), Equals("{}")); + + SECTION("Check file archive and log size") + { + auto logsize = get_directory_size(GNMI_LOG_DIR "/raw"); + CHECK(logsize < 2 * 1024 * 1024); + + auto archive_size = get_directory_size(GNMI_LOG_DIR "/archives"); + CHECK(archive_size < 8 * 1024 * 1024); + } +} - for (int i = 0; i < 100; i++) { +/* Negative Tests */ + +TEST_CASE("Set request (no val type)", "[set-neg]") +{ ClientContext ctx; SetRequest request; SetResponse response; request.set_transaction_id(id++); - xpath_to_path("/", request.add_delete_()); + auto update = request.add_update(); - for (int j = 0; j < 100; j++) { - auto update = request.add_update(); - auto xpath = "/gnmi-server-test:test/things[name=\"A" + std::to_string(j) + "\"]/description"; - - xpath_to_path(xpath, update->mutable_path()); - update->mutable_val()->set_json_ietf_val(generate_random_string(1000)); - } + xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); + update->mutable_val(); auto status = client->Set(&ctx, request, &response); - REQUIRE(status.ok()); - } + CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); + CHECK_THAT(status.error_message(), Equals("Value not set")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); +} - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test/things"); - sr_sess->applyChanges(); +TEST_CASE("Set request (ascii val type)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); - auto parent = sr_sess->getData("/*"); - auto json = parent->printStr(DataFormat::JSON, PrintFlags::WithSiblings | PrintFlags::Shrink); - CHECK_THAT(json.value(), Equals("{}")); + xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); + update->mutable_val()->set_ascii_val("true"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("Unsupported ASCII Encoding")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); +} - SECTION("Check file archive and log size") { - auto logsize = get_directory_size(GNMI_LOG_DIR "/raw"); - CHECK(logsize < 2 * 1024 * 1024); +TEST_CASE("Set request (JSON val type)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); - auto archive_size = get_directory_size(GNMI_LOG_DIR "/archives"); - CHECK(archive_size < 8 * 1024 * 1024); - } + xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); + update->mutable_val()->set_json_val("true"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("Unsupported JSON Encoding")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); } -/* Negative Tests */ +TEST_CASE("Set request (bytes val type)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); -TEST_CASE("Set request (no val type)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); - update->mutable_val(); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); - CHECK_THAT(status.error_message(), Equals("Value not set")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); + xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); + update->mutable_val()->set_bytes_val("1"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("Unsupported protobuf bytes type")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); } -TEST_CASE("Set request (ascii val type)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); - update->mutable_val()->set_ascii_val("true"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("Unsupported ASCII Encoding")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); -} +TEST_CASE("Set request (proto-bytes val type)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); -TEST_CASE("Set request (JSON val type)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); - update->mutable_val()->set_json_val("true"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("Unsupported JSON Encoding")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); + xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); + update->mutable_val()->set_proto_bytes("1"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("Unsupported PROTOBUF BYTE Encoding")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); } -TEST_CASE("Set request (bytes val type)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); - update->mutable_val()->set_bytes_val("1"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("Unsupported protobuf bytes type")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); -} +TEST_CASE("Set request (any-val val type)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); -TEST_CASE("Set request (proto-bytes val type)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); - update->mutable_val()->set_proto_bytes("1"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("Unsupported PROTOBUF BYTE Encoding")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); + xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); + update->mutable_val()->mutable_any_val()->set_value("1"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("Unsupported PROTOBUF Encoding")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); } -TEST_CASE("Set request (any-val val type)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); - update->mutable_val()->mutable_any_val()->set_value("1"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("Unsupported PROTOBUF Encoding")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); -} +TEST_CASE("Set request (leaf-list val type)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); -TEST_CASE("Set request (leaf-list val type)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); - update->mutable_val()->mutable_leaflist_val()->add_element()->set_string_val("true"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("Unsupported protobuf leaflist type")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); + xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); + update->mutable_val()->mutable_leaflist_val()->add_element()->set_string_val("true"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("Unsupported protobuf leaflist type")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); } -TEST_CASE("Set request (bool val)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); - update->mutable_val()->set_bool_val(true); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("Unsupported protobuf bool type")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); +TEST_CASE("Set request (bool val)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); + update->mutable_val()->set_bool_val(true); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("Unsupported protobuf bool type")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); } -TEST_CASE("Set request (string val)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']/description", update->mutable_path()); - update->mutable_val()->set_string_val("This is item A"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("Unsupported protobuf string type")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); +TEST_CASE("Set request (string val)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/gnmi-server-test:test/things[name='A']/description", update->mutable_path()); + update->mutable_val()->set_string_val("This is item A"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("Unsupported protobuf string type")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); } -TEST_CASE("Set request (uint val)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']/amount", update->mutable_path()); - update->mutable_val()->set_uint_val(42); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("Unsupported protobuf uint type")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); +TEST_CASE("Set request (uint val)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/gnmi-server-test:test/things[name='A']/amount", update->mutable_path()); + update->mutable_val()->set_uint_val(42); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("Unsupported protobuf uint type")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); } -TEST_CASE("Set request (int val)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']/signed-amount", update->mutable_path()); - update->mutable_val()->set_int_val(-42); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("Unsupported protobuf int type")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); +TEST_CASE("Set request (int val)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/gnmi-server-test:test/things[name='A']/signed-amount", update->mutable_path()); + update->mutable_val()->set_int_val(-42); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("Unsupported protobuf int type")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); } -TEST_CASE("Set request (float val)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']/decimal-amount", update->mutable_path()); - update->mutable_val()->set_float_val(42.1); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("Unsupported protobuf float type")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); +TEST_CASE("Set request (float val)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/gnmi-server-test:test/things[name='A']/decimal-amount", update->mutable_path()); + update->mutable_val()->set_float_val(42.1); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("Unsupported protobuf float type")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); } -TEST_CASE("Set request (no path)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - update->mutable_val(); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); - CHECK_THAT(status.error_message(), Equals("Update no path or value")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); +TEST_CASE("Set request (no path)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + update->mutable_val(); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); + CHECK_THAT(status.error_message(), Equals("Update no path or value")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); } -TEST_CASE("Set request (decimal64 val)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); +TEST_CASE("Set request (decimal64 val)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); - xpath_to_path("/gnmi-server-test:test/things[name='A']/decimal-amount", update->mutable_path()); - update->mutable_val()->mutable_decimal_val()->set_digits(421); - update->mutable_val()->mutable_decimal_val()->set_precision(1); + xpath_to_path("/gnmi-server-test:test/things[name='A']/decimal-amount", update->mutable_path()); + update->mutable_val()->mutable_decimal_val()->set_digits(421); + update->mutable_val()->mutable_decimal_val()->set_precision(1); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("Unsupported protobuf Decimal64 type")); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("Unsupported protobuf Decimal64 type")); } -TEST_CASE("Set request (incorrect prefix)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']", update->mutable_path()); - // The prefix, if any, should be gnmi-server-test - update->mutable_val()->set_json_ietf_val("{\"gnmi-server-test-wine:enabled\":true}"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); - CHECK_THAT(status.error_message(), Equals("Node \"enabled\" not found as a child of \"things\" node. (Line number 1.)")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); +TEST_CASE("Set request (incorrect prefix)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/gnmi-server-test:test/things[name='A']", update->mutable_path()); + // The prefix, if any, should be gnmi-server-test + update->mutable_val()->set_json_ietf_val("{\"gnmi-server-test-wine:enabled\":true}"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); + CHECK_THAT(status.error_message(), Equals("Can't parse value fragment data: LY_EVALID")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); } // Check that failing transaction doesn't modify state -TEST_CASE("Set request failing transaction (2 updates)", "[set-neg]") { - ClientContext ctx; - ClientContext ctx2; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":true}]}}"); - update = request.add_update(); - xpath_to_path("/", update->mutable_path()); - // Contains bad value for enabled so expected to fail - update->mutable_val()->set_json_ietf_val("{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"B\",\"enabled\":\"maybe\"}]}}"); - - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); - CHECK_THAT(status.error_message(), Equals("Invalid non-boolean-encoded boolean value \"maybe\". (Data location \"/gnmi-server-test:test/things[name='B']/enabled\", line number 1.)")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - // Check that no changes happened to the state - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']"), Contains("SR_ERR_NOT_FOUND")); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']"), Contains("SR_ERR_NOT_FOUND")); - - request.Clear(); - // Try again with an unrelated update that succeeds - update = request.add_update(); - xpath_to_path("/", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"C\",\"enabled\":true}]}}"); - status = client->Set(&ctx2, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/")); - - // Check that no state was changed other than that related to the most recent updae - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']"), Contains("SR_ERR_NOT_FOUND")); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']"), Contains("SR_ERR_NOT_FOUND")); - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='C']/enabled"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test/things[name='C']"); - sr_sess->applyChanges(); +TEST_CASE("Set request failing transaction (2 updates)", "[set-neg]") +{ + ClientContext ctx; + ClientContext ctx2; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/", update->mutable_path()); + update->mutable_val()->set_json_ietf_val( + "{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":true}]}}"); + update = request.add_update(); + xpath_to_path("/", update->mutable_path()); + // Contains bad value for enabled so expected to fail + update->mutable_val()->set_json_ietf_val( + "{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"B\",\"enabled\":\"maybe\"}]}}"); + + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); + CHECK_THAT(status.error_message(), Equals("Can't parse data: LY_EVALID")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + // Check that no changes happened to the state + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']"), + Contains("SR_ERR_NOT_FOUND")); + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']"), + Contains("SR_ERR_NOT_FOUND")); + + request.Clear(); + // Try again with an unrelated update that succeeds + update = request.add_update(); + xpath_to_path("/", update->mutable_path()); + update->mutable_val()->set_json_ietf_val( + "{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"C\",\"enabled\":true}]}}"); + status = client->Set(&ctx2, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/")); + + // Check that no state was changed other than that related to the most recent updae + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']"), + Contains("SR_ERR_NOT_FOUND")); + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']"), + Contains("SR_ERR_NOT_FOUND")); + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='C']/enabled"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test/things[name='C']"); + sr_sess->applyChanges(); } // Check that failing transaction doesn't modify state -TEST_CASE("Set request failing transaction (delete+update)", "[set-neg]") { - ClientContext ctx; - ClientContext ctx2; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); - sr_sess->applyChanges(); - - xpath_to_path("/gnmi-server-test:test/things[name='B']", request.add_delete_()); - xpath_to_path("/", update->mutable_path()); - // Contains bad value for enabled so expected to fail - update->mutable_val()->set_json_ietf_val("{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":\"maybe\"}]}}"); - - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); - CHECK_THAT(status.error_message(), Equals("Invalid non-boolean-encoded boolean value \"maybe\". (Data location \"/gnmi-server-test:test/things[name='A']/enabled\", line number 1.)")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); - - // Check that no changes happened to the state - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"), Contains("SR_ERR_NOT_FOUND")); - - request.Clear(); - // Try again with an unrelated update that succeeds - update = request.add_update(); - xpath_to_path("/", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"C\",\"enabled\":true}]}}"); - status = client->Set(&ctx2, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/")); - - // Check that no state was changed other than that related to the most recent updae - val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']"), Contains("SR_ERR_NOT_FOUND")); - val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='C']/enabled"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test/things[name='B']"); - sr_sess->deleteItem("/gnmi-server-test:test/things[name='C']"); - sr_sess->applyChanges(); +TEST_CASE("Set request failing transaction (delete+update)", "[set-neg]") +{ + ClientContext ctx; + ClientContext ctx2; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + sr_sess->setItem("/gnmi-server-test:test/things[name='B']/enabled", "true"); + sr_sess->applyChanges(); + + xpath_to_path("/gnmi-server-test:test/things[name='B']", request.add_delete_()); + xpath_to_path("/", update->mutable_path()); + // Contains bad value for enabled so expected to fail + update->mutable_val()->set_json_ietf_val( + "{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"A\",\"enabled\":\"maybe\"}]}}"); + + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); + CHECK_THAT(status.error_message(), Equals("Can't parse data: LY_EVALID")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); + + // Check that no changes happened to the state + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"), + Contains("SR_ERR_NOT_FOUND")); + + request.Clear(); + // Try again with an unrelated update that succeeds + update = request.add_update(); + xpath_to_path("/", update->mutable_path()); + update->mutable_val()->set_json_ietf_val( + "{\"gnmi-server-test:test\":{\"things\":[{\"name\":\"C\",\"enabled\":true}]}}"); + status = client->Set(&ctx2, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/")); + + // Check that no state was changed other than that related to the most recent updae + val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']/enabled"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']"), + Contains("SR_ERR_NOT_FOUND")); + val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='C']/enabled"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test/things[name='B']"); + sr_sess->deleteItem("/gnmi-server-test:test/things[name='C']"); + sr_sess->applyChanges(); } // Check that failing transaction doesn't modify state -TEST_CASE("Set request failing transaction (2 leaf updates)", "[set-neg]") { - ClientContext ctx; - ClientContext ctx2; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("true"); - update = request.add_update(); - xpath_to_path("/gnmi-server-test:test/things[name='B']/enabled", update->mutable_path()); - // Contains bad value for enabled so expected to fail - update->mutable_val()->set_json_ietf_val("\"maybe\""); - - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); - CHECK_THAT(status.error_message(), Equals("Invalid boolean value \"maybe\". (Schema location \"/gnmi-server-test:test/things/enabled\".)")); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() == 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 0); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - - // Check that no changes happened to the state - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']"), Contains("SR_ERR_NOT_FOUND")); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']"), Contains("SR_ERR_NOT_FOUND")); - - request.Clear(); - // Try again with an unrelated update that succeeds - update = request.add_update(); - xpath_to_path("/gnmi-server-test:test/things[name='C']/enabled", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("true"); - status = client->Set(&ctx2, request, &response); - CHECK(status.ok()); - - CHECK(response.extension_size() == 0); - CHECK(!response.has_message()); - CHECK(response.timestamp() > 0); - CHECK(!response.has_prefix()); - REQUIRE(response.response_size() == 1); - CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); - auto path = path_to_xpath(response.response().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='C']/enabled")); - - // Check that no state was changed other than that related to the most recent updae - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']"), Contains("SR_ERR_NOT_FOUND")); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']"), Contains("SR_ERR_NOT_FOUND")); - auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='C']/enabled"); - - CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test/things[name='C']"); - sr_sess->applyChanges(); +TEST_CASE("Set request failing transaction (2 leaf updates)", "[set-neg]") +{ + ClientContext ctx; + ClientContext ctx2; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/gnmi-server-test:test/things[name='A']/enabled", update->mutable_path()); + update->mutable_val()->set_json_ietf_val("true"); + update = request.add_update(); + xpath_to_path("/gnmi-server-test:test/things[name='B']/enabled", update->mutable_path()); + // Contains bad value for enabled so expected to fail + update->mutable_val()->set_json_ietf_val("\"maybe\""); + + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); + CHECK_THAT(status.error_message(), Equals("Can't parse value fragment data: LY_EVALID")); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() == 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 0); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + + // Check that no changes happened to the state + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']"), + Contains("SR_ERR_NOT_FOUND")); + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']"), + Contains("SR_ERR_NOT_FOUND")); + + request.Clear(); + // Try again with an unrelated update that succeeds + update = request.add_update(); + xpath_to_path("/gnmi-server-test:test/things[name='C']/enabled", update->mutable_path()); + update->mutable_val()->set_json_ietf_val("true"); + status = client->Set(&ctx2, request, &response); + CHECK(status.ok()); + + CHECK(response.extension_size() == 0); + CHECK(!response.has_message()); + CHECK(response.timestamp() > 0); + CHECK(!response.has_prefix()); + REQUIRE(response.response_size() == 1); + CHECK(response.response().Get(0).op() == gnmi::UpdateResult_Operation_UPDATE); + auto path = path_to_xpath(response.response().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test/things[name='C']/enabled")); + + // Check that no state was changed other than that related to the most recent updae + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']"), + Contains("SR_ERR_NOT_FOUND")); + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='B']"), + Contains("SR_ERR_NOT_FOUND")); + auto val = sr_sess->getOneNode("/gnmi-server-test:test/things[name='C']/enabled"); + + CHECK_THAT(std::string(val.asTerm().valueStr()), Equals("true")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test/things[name='C']"); + sr_sess->applyChanges(); } -TEST_CASE("Top-level Set request (update, no namespace)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("{\"test\":{\"things\":[{\"name\":\"A\",\"enabled\":true}]}}"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); - CHECK_THAT(status.error_message(), Equals("Top-level JSON object member \"test\" must be namespace-qualified. (Line number 1.)")); - - sr_sess->switchDatastore(sysrepo::Datastore::Running); - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"), Contains("SR_ERR_NOT_FOUND")); +TEST_CASE("Top-level Set request (update, no namespace)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/", update->mutable_path()); + update->mutable_val()->set_json_ietf_val( + "{\"test\":{\"things\":[{\"name\":\"A\",\"enabled\":true}]}}"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); + CHECK_THAT(status.error_message(), Equals("Can't parse data: LY_EVALID")); + + sr_sess->switchDatastore(sysrepo::Datastore::Running); + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test/things[name='A']/enabled"), + Contains("SR_ERR_NOT_FOUND")); } -TEST_CASE("Set request (update with wildcards)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test/*/enabled", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("true"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); - CHECK_THAT(status.error_message(), - Equals("Couldn't find schema node: /gnmi-server-test:test/*/enabled")); +TEST_CASE("Set request (update with wildcards)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/gnmi-server-test:test/*/enabled", update->mutable_path()); + update->mutable_val()->set_json_ietf_val("true"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); + CHECK_THAT(status.error_message(), Equals("Can't parse value fragment data: LY_EVALID")); } -TEST_CASE("Set request (application error string)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test2/custom-error", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("1"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::ABORTED); - CHECK_THAT(status.error_message(), Equals("Fiddlesticks: /gnmi-server-test:test2/custom-error")); +TEST_CASE("Set request (application error string)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/gnmi-server-test:test2/custom-error", update->mutable_path()); + update->mutable_val()->set_json_ietf_val("1"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::ABORTED); + CHECK_THAT(status.error_message(), + Equals("Fiddlesticks: /gnmi-server-test:test2/custom-error")); } -TEST_CASE("Set request (data model error)", "[set-neg]") { - ClientContext ctx; - SetRequest request; - SetResponse response; - request.set_transaction_id(id++); - auto update = request.add_update(); - - xpath_to_path("/gnmi-server-test:test2/must-error", update->mutable_path()); - update->mutable_val()->set_json_ietf_val("1"); - auto status = client->Set(&ctx, request, &response); - CHECK(status.error_code() == StatusCode::ABORTED); - CHECK_THAT(status.error_message(), Equals("Must condition \"current() > 42\" not satisfied. (Data location \"/gnmi-server-test:test2/must-error\".)")); +TEST_CASE("Set request (data model error)", "[set-neg]") +{ + ClientContext ctx; + SetRequest request; + SetResponse response; + request.set_transaction_id(id++); + auto update = request.add_update(); + + xpath_to_path("/gnmi-server-test:test2/must-error", update->mutable_path()); + update->mutable_val()->set_json_ietf_val("1"); + auto status = client->Set(&ctx, request, &response); + CHECK(status.error_code() == StatusCode::ABORTED); + CHECK_THAT(status.error_message(), Equals("Must condition \"current() > 42\" not satisfied. " + "(path \"/gnmi-server-test:test2/must-error\")")); } diff --git a/tests/subscribe.cpp b/tests/subscribe.cpp index 5fa885b..2ac792f 100644 --- a/tests/subscribe.cpp +++ b/tests/subscribe.cpp @@ -14,1859 +14,1960 @@ * limitations under the License. */ +#include +#include #include #include #include -#include -#include +#include "main.h" +#include "utils/log.h" #include #include -#include "utils/log.h" -#include "main.h" using namespace std; -using Catch::Matchers::Equals; using Catch::Matchers::Contains; +using Catch::Matchers::Equals; /* Positive Tests */ -TEST_CASE("Subscribe (once)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_ONCE); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - success = rw->Read(&response); - CHECK(success == true); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.sync_response()); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\",\"counter\":\"2\"}],\"cargo\":{}}")); - auto path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - success = rw->Read(&response); - CHECK(success == false); - - success = rw->WritesDone(); - CHECK(success == true); - auto status = rw->Finish(); - CHECK(status.ok()); -} - -TEST_CASE("Subscribe (poll)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_POLL); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - poll_request.mutable_poll(); - success = rw->Write(poll_request); - CHECK(success == true); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.sync_response()); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\",\"counter\":\"2\"}],\"cargo\":{}}")); - auto path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - success = rw->Write(poll_request); - CHECK(success == true); - success = rw->WritesDone(); - CHECK(success == true); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\",\"counter\":\"2\"}],\"cargo\":{}}")); - path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.ok()); -} - -TEST_CASE("Subscribe (stream-sample)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - std::chrono::seconds interval(1); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::SAMPLE); - sub->set_sample_interval(std::chrono::nanoseconds(interval).count()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.sync_response()); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\",\"counter\":\"2\"}],\"cargo\":{}}")); - auto path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - // Wait for one update after the initial one - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\",\"counter\":\"2\"}],\"cargo\":{}}")); - path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); - CHECK(!response.update().atomic()); - - // And then cancel - ctx.TryCancel(); - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::CANCELLED); -} - -TEST_CASE("Subscribe (once) with prefix", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_ONCE); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state", list->mutable_prefix()); - xpath_to_path("/things[name='A']", sub->mutable_path()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - success = rw->Read(&response); - CHECK(success == true); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.sync_response()); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - auto prefix = path_to_xpath(response.update().prefix()); - CHECK_THAT(prefix, Equals("/")); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"name\":\"A\",\"counter\":\"1\"}")); - auto path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']")); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - success = rw->Read(&response); - CHECK(success == false); - - success = rw->WritesDone(); - CHECK(success == true); - auto status = rw->Finish(); - CHECK(status.ok()); -} - -TEST_CASE("Subscribe (once) with target", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_ONCE); - list->set_encoding(gnmi::Encoding::JSON_IETF); - *list->mutable_prefix()->mutable_target() = "foo"; - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - success = rw->Read(&response); - CHECK(success == true); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.sync_response()); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(response.update().has_prefix()); - CHECK_THAT(response.update().prefix().target(), Equals("foo")); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\",\"counter\":\"2\"}],\"cargo\":{}}")); - auto path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - success = rw->Read(&response); - CHECK(success == false); - - success = rw->WritesDone(); - CHECK(success == true); - auto status = rw->Finish(); - CHECK(status.ok()); -} +TEST_CASE("Subscribe (once)", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); -TEST_CASE("Subscribe (once) with wildcards", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_ONCE); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/*/counter", sub->mutable_path()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - success = rw->Read(&response); - CHECK(success == true); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.sync_response()); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 2); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.update().update().Get(0).val().json_ietf_val(); - // This is a 64-bit value, so is represented as a string (ref: RFC7951 §6.1) - CHECK_THAT(json, Equals("\"1\"")); - auto path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']/counter")); - CHECK(response.update().update().Get(1).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.update().update().Get(1).val().json_ietf_val(); - CHECK_THAT(json, Equals("\"2\"")); - path = path_to_xpath(response.update().update().Get(1).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='B']/counter")); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - success = rw->Read(&response); - CHECK(success == false); - - success = rw->WritesDone(); - CHECK(success == true); - auto status = rw->Finish(); - CHECK(status.ok()); -} + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_ONCE); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); -static void -onchange_cancel_thread(ClientContext &ctx) -{ - // Wait long enough to be sure that no pending message would have been sent - this_thread::sleep_for(chrono::milliseconds(500)); + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + success = rw->Read(&response); + CHECK(success == true); - ctx.TryCancel(); -} + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.sync_response()); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\"," + "\"counter\":\"2\"}],\"cargo\":{}}")); + auto path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); + CHECK(!response.update().atomic()); -TEST_CASE("Subscribe (on-change, no updates)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - /* Initial update */ - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\",\"counter\":\"2\"}],\"cargo\":{}}")); - auto path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - auto cancel_thread = std::thread(onchange_cancel_thread, std::ref(ctx)); - - success = rw->Read(&response); - CHECK(success == false); - - cancel_thread.join(); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::CANCELLED); - CHECK_THAT(status.error_message(), Equals("CANCELLED")); -} + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); -TEST_CASE("Subscribe (on-change, with update)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - // Make sure the item doesn't already exist, or no notifications - // will be generated and the test will hang - CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test-state/things[name='C']/name"), Contains("SR_ERR_NOT_FOUND")); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - // Initial update - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\",\"counter\":\"2\"}],\"cargo\":{}}")); - auto path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "3"); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter2", "23"); - sr_sess->applyChanges(); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"name\":\"C\",\"counter\":\"3\",\"counter2\":\"23\"}")); - path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']")); - CHECK(!response.update().atomic()); - - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "4"); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter2", "24"); - sr_sess->applyChanges(); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.update().delete__size() == 2); - path = path_to_xpath(response.update().delete_().Get(0)); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); - path = path_to_xpath(response.update().delete_().Get(1)); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter2")); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 2); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("\"4\"")); - path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); - CHECK(response.update().update().Get(1).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.update().update().Get(1).val().json_ietf_val(); - CHECK_THAT(json, Equals("\"24\"")); - path = path_to_xpath(response.update().update().Get(1).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter2")); - CHECK(!response.update().atomic()); - - ctx.TryCancel(); - - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::CANCELLED); - CHECK_THAT(status.error_message(), Equals("CANCELLED")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='C']"); - sr_sess->applyChanges(); -} + success = rw->Read(&response); + CHECK(success == false); -TEST_CASE("Subscribe (on-change, with delete)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "4"); - sr_sess->applyChanges(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - // Initial update - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\",\"counter\":\"2\"},{\"name\":\"C\",\"counter\":\"4\"}],\"cargo\":{}}")); - auto path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='C']"); - sr_sess->applyChanges(); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().update_size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().delete__size() == 1); - path = path_to_xpath(response.update().delete_().Get(0)); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']")); - CHECK(!response.update().atomic()); - - ctx.TryCancel(); - - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::CANCELLED); - CHECK_THAT(status.error_message(), Equals("CANCELLED")); + success = rw->WritesDone(); + CHECK(success == true); + auto status = rw->Finish(); + CHECK(status.ok()); } -TEST_CASE("Subscribe for leaf (on-change, update)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "5"); - sr_sess->applyChanges(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='C']/counter", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - // Initial update - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("\"5\"")); - auto path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "6"); - sr_sess->applyChanges(); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - - REQUIRE(response.update().delete__size() == 1); - path = path_to_xpath(response.update().delete_().Get(0)); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("\"6\"")); - path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); - CHECK(!response.update().atomic()); - - ctx.TryCancel(); - - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::CANCELLED); - CHECK_THAT(status.error_message(), Equals("CANCELLED")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='C']"); - sr_sess->applyChanges(); -} +TEST_CASE("Subscribe (poll)", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); -TEST_CASE("Subscribe (on-change, update with composite key)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']/data", "baz"); - sr_sess->applyChanges(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - // Initial update - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foo\",\"data\":\"baz\"}")); - auto resppath = response.update().update().Get(0).path(); - CHECK(resppath.elem_size() == 2); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); - CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - sr_sess->deleteItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']"); - sr_sess->applyChanges(); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.update().delete__size() == 1); - resppath = response.update().delete_().Get(0); - CHECK(resppath.elem_size() == 2); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); - CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 0); - CHECK(!response.update().atomic()); - - ctx.TryCancel(); - - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::CANCELLED); - CHECK_THAT(status.error_message(), Equals("CANCELLED")); -} + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_POLL); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); -TEST_CASE("Subscribe for leaf (on-change, delete and add)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "7"); - sr_sess->applyChanges(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='C']/counter", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - // Initial update - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("\"7\"")); - auto path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - // Delete then add - sysrepo generates both delete and create events, check that we handle that - sr_sess->dropForeignOperationalContent("/gnmi-server-test:test-state/things"); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "8"); - sr_sess->applyChanges(); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.update().delete__size() == 1); - path = path_to_xpath(response.update().delete_().Get(0)); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("\"8\"")); - path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); - CHECK(!response.update().atomic()); - - // Update then delete - sysrepo collapses this to nothing - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "8"); - sr_sess->dropForeignOperationalContent("/gnmi-server-test:test-state/things"); - sr_sess->applyChanges(); - - ctx.TryCancel(); - - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::CANCELLED); - CHECK_THAT(status.error_message(), Equals("CANCELLED")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='C']"); - sr_sess->applyChanges(); -} + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); -TEST_CASE("Subscribe with non-existent path (once)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_ONCE); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='C']", sub->mutable_path()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - success = rw->Read(&response); - CHECK(success == true); - - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.sync_response()); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 0); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - success = rw->Read(&response); - CHECK(success == false); - - success = rw->WritesDone(); - CHECK(success == true); - auto status = rw->Finish(); - CHECK(status.ok()); -} + poll_request.mutable_poll(); + success = rw->Write(poll_request); + CHECK(success == true); -TEST_CASE("Subscribe with non-existent path (poll)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_POLL); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='C']", sub->mutable_path()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - poll_request.mutable_poll(); - success = rw->Write(poll_request); - CHECK(success == true); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.sync_response()); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 0); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - success = rw->Write(poll_request); - CHECK(success == true); - success = rw->WritesDone(); - CHECK(success == true); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.sync_response()); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 0); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.ok()); -} + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.sync_response()); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\"," + "\"counter\":\"2\"}],\"cargo\":{}}")); + auto path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); + CHECK(!response.update().atomic()); -TEST_CASE("Subscribe with non-existent path (stream-sample)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - std::chrono::seconds interval(1); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='C']", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::SAMPLE); - sub->set_sample_interval(std::chrono::nanoseconds(interval).count()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.sync_response()); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 0); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - // Wait for one update after the initial one - success = rw->Read(&response); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.sync_response()); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 0); - CHECK(!response.update().atomic()); - - // And then cancel - ctx.TryCancel(); - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::CANCELLED); -} + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); -TEST_CASE("Subscribe with non-existent path (on-change)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - std::chrono::seconds interval(1); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='C']", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - sub->set_sample_interval(std::chrono::nanoseconds(interval).count()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.sync_response()); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 0); - CHECK(!response.update().atomic()); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - // And then cancel - ctx.TryCancel(); - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::CANCELLED); -} + success = rw->Write(poll_request); + CHECK(success == true); + success = rw->WritesDone(); + CHECK(success == true); -TEST_CASE("Subscribe (on-change, 2 subscriptions, delete)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - auto sub2 = list->add_subscription(); - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']/data", "baz"); - sr_sess->applyChanges(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - xpath_to_path("/gnmi-server-test:test3/complex-list[type='bar'][name='foofoo']", sub2->mutable_path()); - sub2->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - // Initial update for 1st subscription - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foo\",\"data\":\"baz\"}")); - auto resppath = response.update().update().Get(0).path(); - CHECK(resppath.elem_size() == 2); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); - CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); - CHECK(!response.update().atomic()); - - // Empty initial update for 2nd subscription (no data) - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 0); - - // Sync response - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - // Delete for 1 subscription - sr_sess->deleteItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']"); - sr_sess->applyChanges(); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.update().delete__size() == 1); - resppath = response.update().delete_().Get(0); - CHECK(resppath.elem_size() == 2); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); - CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 0); - CHECK(!response.update().atomic()); - - // Update for the other subscription - sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foofoo']/data", "baz"); - sr_sess->applyChanges(); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foofoo\",\"data\":\"baz\"}")); - resppath = response.update().update().Get(0).path(); - CHECK(resppath.elem_size() == 2); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foofoo")); - CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); - CHECK(!response.update().atomic()); - - ctx.TryCancel(); - - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::CANCELLED); - CHECK_THAT(status.error_message(), Equals("CANCELLED")); -} + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\"," + "\"counter\":\"2\"}],\"cargo\":{}}")); + path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); + CHECK(!response.update().atomic()); + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); -TEST_CASE("Subscribe (on-change, 2 subscriptions different modules, update/delete)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - auto sub2 = list->add_subscription(); - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']/data", "baz"); - sr_sess->setItem("/gnmi-server-test-wine:wines/wine[name='Mas\ La\ Plana'][vintage='1985']/score", "98"); - sr_sess->applyChanges(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - xpath_to_path("/gnmi-server-test-wine:wines", sub2->mutable_path()); - sub2->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - // Initial update for 1st subscription - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foo\",\"data\":\"baz\"}")); - auto resppath = response.update().update().Get(0).path(); - CHECK(resppath.elem_size() == 2); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); - CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); - CHECK(!response.update().atomic()); - - // Initial update for 2nd subscription - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"wine\":[{\"name\":\"Mas La Plana\",\"vintage\":1985,\"score\":98}]}")); - resppath = response.update().update().Get(0).path(); - CHECK(resppath.elem_size() == 1); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test-wine:wines")); - CHECK(!response.update().atomic()); - - // Sync - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - // Delete for 1 subscription - sr_sess->deleteItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']"); - sr_sess->applyChanges(); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - REQUIRE(response.update().delete__size() == 1); - resppath = response.update().delete_().Get(0); - CHECK(resppath.elem_size() == 2); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); - CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 0); - CHECK(!response.update().atomic()); - - // Update for the other subscription - sr_sess->setItem("/gnmi-server-test-wine:wines/wine[name='Mas\ La\ Plana'][vintage='1985']/score", "99"); - sr_sess->applyChanges(); - - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - // This is an update (i.e. modify) - CHECK(response.update().delete__size() == 1); - CHECK(response.update().update_size() == 1); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("99")); - resppath = response.update().update().Get(0).path(); - CHECK(resppath.elem_size() == 3); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test-wine:wines")); - CHECK_THAT(resppath.elem(1).name(), Equals("wine")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("Mas La Plana")); - CHECK_THAT(resppath.elem(1).key().at("vintage"), Equals("1985")); - CHECK_THAT(resppath.elem(2).name(), Equals("score")); - CHECK(!response.update().atomic()); - ctx.TryCancel(); - - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::CANCELLED); - CHECK_THAT(status.error_message(), Equals("CANCELLED")); -} + success = rw->Read(&response); + CHECK(success == false); -TEST_CASE("Subscribe (stream: mix of sample and on-change)", "[subs]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - auto sub2 = list->add_subscription(); - std::chrono::seconds interval(1); - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']/data", "baz"); - sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foofoo']/data", "baz"); - sr_sess->applyChanges(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::SAMPLE); - sub->set_sample_interval(std::chrono::nanoseconds(interval).count()); - xpath_to_path("/gnmi-server-test:test3/complex-list[type='bar'][name='foofoo']", sub2->mutable_path()); - sub2->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - // Initial data should contain data for sample subscription - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.sync_response()); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - auto json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foo\",\"data\":\"baz\"}")); - auto resppath = response.update().update().Get(0).path(); - CHECK(resppath.elem_size() == 2); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); - CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); - - // Next response is initial-data for on-change subscription - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.sync_response()); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foofoo\",\"data\":\"baz\"}")); - resppath = response.update().update().Get(0).path(); - CHECK(resppath.elem_size() == 2); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foofoo")); - CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); - - CHECK(!response.update().atomic()); - - // Sync response - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(!response.has_update()); - CHECK(response.sync_response()); - - // Wait for one update (sample only) after the initial one - success = rw->Read(&response); - CHECK(success == true); - CHECK(!response.has_error()); - CHECK(response.extension_size() == 0); - CHECK(response.update().delete__size() == 0); - CHECK(response.update().timestamp() > 0); - CHECK(!response.update().has_prefix()); - CHECK_THAT(response.update().alias(), Equals("")); - REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); - json = response.update().update().Get(0).val().json_ietf_val(); - CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foo\",\"data\":\"baz\"}")); - resppath = response.update().update().Get(0).path(); - CHECK(resppath.elem_size() == 2); - CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); - CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); - CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); - CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); - - // And then cancel - ctx.TryCancel(); - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::CANCELLED); + auto status = rw->Finish(); + CHECK(status.ok()); } -static void -update_counter() +TEST_CASE("Subscribe (stream-sample)", "[subs]") { - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - for (uint32_t i = 1; i <= 20; i++) { - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", std::to_string(i).c_str()); - sr_sess->applyChanges(); - } -} - -TEST_CASE("Subscribe for leaf (on-change, race condition)", "[subs-scale]") { - ScaleTestLogLevelReducer _log_reducer; - for (uint32_t i = 0; i < 2000; i++) { ClientContext ctx; SubscribeRequest request; - SubscribeRequest poll_request; SubscribeResponse response; - auto list = request.mutable_subscribe(); auto sub = list->add_subscription(); - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", std::to_string(0).c_str()); - sr_sess->applyChanges(); + std::chrono::seconds interval(1); list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='C']/counter", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - - // This is to have race condition where the data is being modified - // around same time it's being subscribed to below. - auto update_thread = std::thread(update_counter); + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::SAMPLE); + sub->set_sample_interval(std::chrono::nanoseconds(interval).count()); auto rw = client->Subscribe(&ctx); auto success = rw->Write(request); CHECK(success == true); - // Initial update success = rw->Read(&response); CHECK(success == true); - CHECK(!response.has_error()); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); CHECK(response.extension_size() == 0); + CHECK(!response.sync_response()); CHECK(response.update().delete__size() == 0); CHECK(response.update().timestamp() > 0); CHECK(!response.update().has_prefix()); CHECK_THAT(response.update().alias(), Equals("")); REQUIRE(response.update().update_size() == 1); - CHECK(response.update().update().Get(0).val().value_case() == gnmi::TypedValue::ValueCase::kJsonIetfVal); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); auto json = response.update().update().Get(0).val().json_ietf_val(); - // We don't check the value since it can be 0...max - // CHECK_THAT(json, Equals("\"0\"")); + CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\"," + "\"counter\":\"2\"}],\"cargo\":{}}")); auto path = path_to_xpath(response.update().update().Get(0).path()); - CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); CHECK(!response.update().atomic()); success = rw->Read(&response); CHECK(success == true); - CHECK(!response.has_error()); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); CHECK(response.extension_size() == 0); CHECK(!response.has_update()); CHECK(response.sync_response()); - ctx.TryCancel(); + // Wait for one update after the initial one + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\"," + "\"counter\":\"2\"}],\"cargo\":{}}")); + path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); + CHECK(!response.update().atomic()); + // And then cancel + ctx.TryCancel(); success = rw->Read(&response); CHECK(success == false); - update_thread.join(); - auto status = rw->Finish(); CHECK(status.error_code() == StatusCode::CANCELLED); - CHECK_THAT(status.error_message(), Equals("CANCELLED")); - - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='C']"); - sr_sess->applyChanges(); - } } +TEST_CASE("Subscribe (once) with prefix", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); -TEST_CASE("Subscribe for leaf (on-change, slow client)", "[subs-scale]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - sr_sess->switchDatastore(sysrepo::Datastore::Operational); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "0"); - sr_sess->applyChanges(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state/things[name='C']/counter", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - ScaleTestLogLevelReducer _log_reducer; + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_ONCE); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state", list->mutable_prefix()); + xpath_to_path("/things[name='A']", sub->mutable_path()); - // Just generate a huge amount of updates without reading so that the Write call from the server hangs - // This shouldn't cause any delay for the applyChanges call, which would normally be a system component. - for (uint32_t i = 0; i < 25000; i++) { - sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", std::to_string(i).c_str()); - sr_sess->applyChanges(); - } + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + success = rw->Read(&response); + CHECK(success == true); - ctx.TryCancel(); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.sync_response()); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + auto prefix = path_to_xpath(response.update().prefix()); + CHECK_THAT(prefix, Equals("/")); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"name\":\"A\",\"counter\":\"1\"}")); + auto path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']")); + CHECK(!response.update().atomic()); - success = rw->Read(&response); - CHECK(success == false); + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::CANCELLED); - CHECK_THAT(status.error_message(), Equals("CANCELLED")); + success = rw->Read(&response); + CHECK(success == false); - // Clean up - sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='C']"); - sr_sess->applyChanges(); + success = rw->WritesDone(); + CHECK(success == true); + auto status = rw->Finish(); + CHECK(status.ok()); } +TEST_CASE("Subscribe (once) with target", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); -/* Negative tests */ - -TEST_CASE("Subscribe (empty)", "[subs-neg]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeResponse response; - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - success = rw->Read(&response); - CHECK(success == false); + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_ONCE); + list->set_encoding(gnmi::Encoding::JSON_IETF); + *list->mutable_prefix()->mutable_target() = "foo"; + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); - CHECK_THAT(status.error_message(), Equals("SubscribeRequest needs non-empty SubscriptionList")); -} + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + success = rw->Read(&response); + CHECK(success == true); -TEST_CASE("Subscribe (once) with invalid mode", "[subs-neg]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(static_cast(42)); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:non-existent", sub->mutable_path()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("Unknown subscription mode")); -} + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.sync_response()); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(response.update().has_prefix()); + CHECK_THAT(response.update().prefix().target(), Equals("foo")); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"things\":[{\"name\":\"A\",\"counter\":\"1\"},{\"name\":\"B\"," + "\"counter\":\"2\"}],\"cargo\":{}}")); + auto path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); + CHECK(!response.update().atomic()); -TEST_CASE("Subscribe (once) with unsupported encoding type", "[subs-neg]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_ONCE); - list->set_encoding(gnmi::Encoding::BYTES); - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("BYTES")); -} + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); -TEST_CASE("Subscribe (once) with another unsupported encoding type", "[subs-neg]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_ONCE); - list->set_encoding(gnmi::Encoding::PROTO); - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("PROTO")); -} + success = rw->Read(&response); + CHECK(success == false); -TEST_CASE("Subscribe (poll) with use_aliases", "[subs-neg]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_POLL); - list->set_encoding(gnmi::Encoding::JSON_IETF); - list->set_use_aliases(true); - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - poll_request.mutable_poll(); - success = rw->Write(poll_request); - CHECK(success == true); - - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("alias not supported")); + success = rw->WritesDone(); + CHECK(success == true); + auto status = rw->Finish(); + CHECK(status.ok()); } -TEST_CASE("Subscribe (poll) with updates_only", "[subs-neg]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_POLL); - list->set_encoding(gnmi::Encoding::JSON_IETF); - list->set_updates_only(true); - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - poll_request.mutable_poll(); - success = rw->Write(poll_request); - CHECK(success == true); - - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("updates-only not supported")); -} +TEST_CASE("Subscribe (once) with wildcards", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); -TEST_CASE("Subscribe (poll) with alias request", "[subs-neg]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_ONCE); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/*/counter", sub->mutable_path()); - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_POLL); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + success = rw->Read(&response); + CHECK(success == true); - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.sync_response()); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 2); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.update().update().Get(0).val().json_ietf_val(); + // This is a 64-bit value, so is represented as a string (ref: RFC7951 §6.1) + CHECK_THAT(json, Equals("\"1\"")); + auto path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='A']/counter")); + CHECK(response.update().update().Get(1).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.update().update().Get(1).val().json_ietf_val(); + CHECK_THAT(json, Equals("\"2\"")); + path = path_to_xpath(response.update().update().Get(1).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='B']/counter")); + CHECK(!response.update().atomic()); - poll_request.mutable_aliases(); - success = rw->Write(poll_request); - CHECK(success == true); + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); - success = rw->Read(&response); - CHECK(success == false); + success = rw->Read(&response); + CHECK(success == false); - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("Aliases not implemented yet")); + success = rw->WritesDone(); + CHECK(success == true); + auto status = rw->Finish(); + CHECK(status.ok()); } -TEST_CASE("Subscribe (poll) with dup sub request", "[subs-neg]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); +static void onchange_cancel_thread(ClientContext &ctx) +{ + // Wait long enough to be sure that no pending message would have been sent + this_thread::sleep_for(chrono::milliseconds(500)); - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_POLL); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); + ctx.TryCancel(); +} - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); +TEST_CASE("Subscribe (on-change, no updates)", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; - poll_request.mutable_subscribe(); - success = rw->Write(poll_request); - CHECK(success == true); + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); - success = rw->Read(&response); - CHECK(success == false); + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); - CHECK_THAT(status.error_message(), Equals("A SubscriptionList has already been received for this RPC")); + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + /* Initial update */ + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT( + json, + Equals("{\"things\":[{\"@\":{\"yang:key\":[null]},\"name\":\"A\",\"counter\":\"1\"},{\"@\":" + "{\"yang:key\":\"[name='A']\"},\"name\":\"B\",\"counter\":\"2\"}],\"cargo\":{}}")); + auto path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); + CHECK(!response.update().atomic()); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); + + auto cancel_thread = std::thread(onchange_cancel_thread, std::ref(ctx)); + + success = rw->Read(&response); + CHECK(success == false); + + cancel_thread.join(); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::CANCELLED); + CHECK_THAT(status.error_message(), Equals("CANCELLED")); +} + +TEST_CASE("Subscribe (on-change, with update)", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + // Make sure the item doesn't already exist, or no notifications + // will be generated and the test will hang + CHECK_THROWS_WITH(sr_sess->getOneNode("/gnmi-server-test:test-state/things[name='C']/name"), + Contains("SR_ERR_NOT_FOUND")); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + // Initial update + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT( + json, + Equals("{\"things\":[{\"@\":{\"yang:key\":[null]},\"name\":\"A\",\"counter\":\"1\"},{\"@\":" + "{\"yang:key\":\"[name='A']\"},\"name\":\"B\",\"counter\":\"2\"}],\"cargo\":{}}")); + auto path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); + CHECK(!response.update().atomic()); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "3"); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter2", "23"); + sr_sess->applyChanges(); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"name\":\"C\",\"counter\":\"3\",\"counter2\":\"23\"}")); + path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']")); + CHECK(!response.update().atomic()); + + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "4"); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter2", "24"); + sr_sess->applyChanges(); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + REQUIRE(response.update().delete__size() == 2); + path = path_to_xpath(response.update().delete_().Get(0)); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); + path = path_to_xpath(response.update().delete_().Get(1)); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter2")); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 2); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("\"4\",\"@counter\":{\"yang:operation\":\"replace\",\"yang:orig-" + "default\":false,\"yang:orig-value\":\"3\"}")); + path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); + CHECK(response.update().update().Get(1).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.update().update().Get(1).val().json_ietf_val(); + CHECK_THAT(json, Equals("\"24\",\"@counter2\":{\"yang:operation\":\"replace\",\"yang:orig-" + "default\":false,\"yang:orig-value\":\"23\"}")); + path = path_to_xpath(response.update().update().Get(1).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter2")); + CHECK(!response.update().atomic()); + + ctx.TryCancel(); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::CANCELLED); + CHECK_THAT(status.error_message(), Equals("CANCELLED")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='C']"); + sr_sess->applyChanges(); +} + +TEST_CASE("Subscribe (on-change, with delete)", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "4"); + sr_sess->applyChanges(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + // Initial update + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT( + json, + Equals("{\"things\":[{\"@\":{\"yang:key\":[null]},\"name\":\"A\",\"counter\":\"1\"},{\"@\":" + "{\"yang:key\":\"[name='A']\"},\"name\":\"B\",\"counter\":\"2\"},{\"@\":{\"yang:" + "key\":\"[name='B']\"},\"name\":\"C\",\"counter\":\"4\"}],\"cargo\":{}}")); + auto path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state")); + CHECK(!response.update().atomic()); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); + + sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='C']"); + sr_sess->applyChanges(); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().update_size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().delete__size() == 1); + path = path_to_xpath(response.update().delete_().Get(0)); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']")); + CHECK(!response.update().atomic()); + + ctx.TryCancel(); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::CANCELLED); + CHECK_THAT(status.error_message(), Equals("CANCELLED")); +} + +TEST_CASE("Subscribe for leaf (on-change, update)", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "5"); + sr_sess->applyChanges(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='C']/counter", sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + // Initial update + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("\"5\"")); + auto path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); + CHECK(!response.update().atomic()); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); + + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "6"); + sr_sess->applyChanges(); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + + REQUIRE(response.update().delete__size() == 1); + path = path_to_xpath(response.update().delete_().Get(0)); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("\"6\",\"@counter\":{\"yang:operation\":\"replace\",\"yang:orig-" + "default\":false,\"yang:orig-value\":\"5\"}")); + path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); + CHECK(!response.update().atomic()); + + ctx.TryCancel(); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::CANCELLED); + CHECK_THAT(status.error_message(), Equals("CANCELLED")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='C']"); + sr_sess->applyChanges(); +} + +TEST_CASE("Subscribe (on-change, update with composite key)", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']/data", "baz"); + sr_sess->applyChanges(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']", + sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + // Initial update + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foo\",\"data\":\"baz\",\"@data\":{\"ietf-" + "origin:origin\":\"ietf-origin:unknown\"}}")); + auto resppath = response.update().update().Get(0).path(); + CHECK(resppath.elem_size() == 2); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); + CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); + CHECK(!response.update().atomic()); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); + + sr_sess->deleteItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']"); + sr_sess->applyChanges(); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + REQUIRE(response.update().delete__size() == 1); + resppath = response.update().delete_().Get(0); + CHECK(resppath.elem_size() == 2); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); + CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 0); + CHECK(!response.update().atomic()); + + ctx.TryCancel(); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::CANCELLED); + CHECK_THAT(status.error_message(), Equals("CANCELLED")); +} + +TEST_CASE("Subscribe for leaf (on-change, delete and add)", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "7"); + sr_sess->applyChanges(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='C']/counter", sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + // Initial update + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("\"7\"")); + auto path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); + CHECK(!response.update().atomic()); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); + + // Delete then add - sysrepo generates both delete and create events, check that we handle that + sr_sess->dropForeignOperationalContent("/gnmi-server-test:test-state/things"); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "8"); + sr_sess->applyChanges(); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + REQUIRE(response.update().delete__size() == 1); + path = path_to_xpath(response.update().delete_().Get(0)); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("\"8\",\"@counter\":{\"yang:operation\":\"replace\",\"yang:orig-" + "default\":false,\"yang:orig-value\":\"7\"}")); + path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); + CHECK(!response.update().atomic()); + + // Update then delete - sysrepo collapses this to nothing + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "8"); + sr_sess->dropForeignOperationalContent("/gnmi-server-test:test-state/things"); + sr_sess->applyChanges(); + + ctx.TryCancel(); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::CANCELLED); + CHECK_THAT(status.error_message(), Equals("CANCELLED")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='C']"); + sr_sess->applyChanges(); +} + +TEST_CASE("Subscribe with non-existent path (once)", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_ONCE); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='C']", sub->mutable_path()); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + success = rw->Read(&response); + CHECK(success == true); + + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.sync_response()); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 0); + CHECK(!response.update().atomic()); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); + + success = rw->Read(&response); + CHECK(success == false); + + success = rw->WritesDone(); + CHECK(success == true); + auto status = rw->Finish(); + CHECK(status.ok()); +} + +TEST_CASE("Subscribe with non-existent path (poll)", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_POLL); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='C']", sub->mutable_path()); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + poll_request.mutable_poll(); + success = rw->Write(poll_request); + CHECK(success == true); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.sync_response()); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 0); + CHECK(!response.update().atomic()); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); + + success = rw->Write(poll_request); + CHECK(success == true); + success = rw->WritesDone(); + CHECK(success == true); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.sync_response()); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 0); + CHECK(!response.update().atomic()); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.ok()); +} + +TEST_CASE("Subscribe with non-existent path (stream-sample)", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + std::chrono::seconds interval(1); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='C']", sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::SAMPLE); + sub->set_sample_interval(std::chrono::nanoseconds(interval).count()); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.sync_response()); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 0); + CHECK(!response.update().atomic()); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); + + // Wait for one update after the initial one + success = rw->Read(&response); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.sync_response()); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 0); + CHECK(!response.update().atomic()); + + // And then cancel + ctx.TryCancel(); + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::CANCELLED); +} + +TEST_CASE("Subscribe with non-existent path (on-change)", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + std::chrono::seconds interval(1); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='C']", sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); + sub->set_sample_interval(std::chrono::nanoseconds(interval).count()); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.sync_response()); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 0); + CHECK(!response.update().atomic()); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); + + // And then cancel + ctx.TryCancel(); + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::CANCELLED); +} + +TEST_CASE("Subscribe (on-change, 2 subscriptions, delete)", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + auto sub2 = list->add_subscription(); + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']/data", "baz"); + sr_sess->applyChanges(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']", + sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); + xpath_to_path("/gnmi-server-test:test3/complex-list[type='bar'][name='foofoo']", + sub2->mutable_path()); + sub2->set_mode(gnmi::SubscriptionMode::ON_CHANGE); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + // Initial update for 1st subscription + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foo\",\"data\":\"baz\",\"@data\":{\"ietf-" + "origin:origin\":\"ietf-origin:unknown\"}}")); + auto resppath = response.update().update().Get(0).path(); + CHECK(resppath.elem_size() == 2); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); + CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); + CHECK(!response.update().atomic()); + + // Empty initial update for 2nd subscription (no data) + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 0); + + // Sync response + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); + + // Delete for 1 subscription + sr_sess->deleteItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']"); + sr_sess->applyChanges(); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + REQUIRE(response.update().delete__size() == 1); + resppath = response.update().delete_().Get(0); + CHECK(resppath.elem_size() == 2); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); + CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 0); + CHECK(!response.update().atomic()); + + // Update for the other subscription + sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foofoo']/data", "baz"); + sr_sess->applyChanges(); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foofoo\",\"data\":\"baz\"}")); + resppath = response.update().update().Get(0).path(); + CHECK(resppath.elem_size() == 2); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foofoo")); + CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); + CHECK(!response.update().atomic()); + + ctx.TryCancel(); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::CANCELLED); + CHECK_THAT(status.error_message(), Equals("CANCELLED")); +} + +TEST_CASE("Subscribe (on-change, 2 subscriptions different modules, update/delete)", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + auto sub2 = list->add_subscription(); + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']/data", "baz"); + sr_sess->setItem( + "/gnmi-server-test-wine:wines/wine[name='Mas\ La\ Plana'][vintage='1985']/score", "98"); + sr_sess->applyChanges(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']", + sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); + xpath_to_path("/gnmi-server-test-wine:wines", sub2->mutable_path()); + sub2->set_mode(gnmi::SubscriptionMode::ON_CHANGE); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + // Initial update for 1st subscription + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foo\",\"data\":\"baz\",\"@data\":{\"ietf-" + "origin:origin\":\"ietf-origin:unknown\"}}")); + auto resppath = response.update().update().Get(0).path(); + CHECK(resppath.elem_size() == 2); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); + CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); + CHECK(!response.update().atomic()); + + // Initial update for 2nd subscription + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"wine\":[{\"name\":\"Mas La " + "Plana\",\"vintage\":1985,\"score\":98,\"@score\":{\"ietf-origin:" + "origin\":\"ietf-origin:unknown\"}}]}")); + resppath = response.update().update().Get(0).path(); + CHECK(resppath.elem_size() == 1); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test-wine:wines")); + CHECK(!response.update().atomic()); + + // Sync + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); + + // Delete for 1 subscription + sr_sess->deleteItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']"); + sr_sess->applyChanges(); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + REQUIRE(response.update().delete__size() == 1); + resppath = response.update().delete_().Get(0); + CHECK(resppath.elem_size() == 2); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); + CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 0); + CHECK(!response.update().atomic()); + + // Update for the other subscription + sr_sess->setItem( + "/gnmi-server-test-wine:wines/wine[name='Mas\ La\ Plana'][vintage='1985']/score", "99"); + sr_sess->applyChanges(); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + // This is an update (i.e. modify) + CHECK(response.update().delete__size() == 1); + CHECK(response.update().update_size() == 1); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("99,\"@score\":{\"yang:operation\":\"replace\",\"yang:orig-default\":" + "false,\"yang:orig-value\":\"98\"}")); + resppath = response.update().update().Get(0).path(); + CHECK(resppath.elem_size() == 3); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test-wine:wines")); + CHECK_THAT(resppath.elem(1).name(), Equals("wine")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("Mas La Plana")); + CHECK_THAT(resppath.elem(1).key().at("vintage"), Equals("1985")); + CHECK_THAT(resppath.elem(2).name(), Equals("score")); + CHECK(!response.update().atomic()); + ctx.TryCancel(); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::CANCELLED); + CHECK_THAT(status.error_message(), Equals("CANCELLED")); +} + +TEST_CASE("Subscribe (stream: mix of sample and on-change)", "[subs]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + auto sub2 = list->add_subscription(); + std::chrono::seconds interval(1); + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']/data", "baz"); + sr_sess->setItem("/gnmi-server-test:test3/complex-list[type='bar'][name='foofoo']/data", "baz"); + sr_sess->applyChanges(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test3/complex-list[type='bar'][name='foo']", + sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::SAMPLE); + sub->set_sample_interval(std::chrono::nanoseconds(interval).count()); + xpath_to_path("/gnmi-server-test:test3/complex-list[type='bar'][name='foofoo']", + sub2->mutable_path()); + sub2->set_mode(gnmi::SubscriptionMode::ON_CHANGE); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + // Initial data should contain data for sample subscription + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.sync_response()); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foo\",\"data\":\"baz\"}")); + auto resppath = response.update().update().Get(0).path(); + CHECK(resppath.elem_size() == 2); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); + CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); + + // Next response is initial-data for on-change subscription + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.sync_response()); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foofoo\",\"data\":\"baz\",\"@data\":{" + "\"ietf-origin:origin\":\"ietf-origin:unknown\"}}")); + resppath = response.update().update().Get(0).path(); + CHECK(resppath.elem_size() == 2); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foofoo")); + CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); + + CHECK(!response.update().atomic()); + + // Sync response + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); + + // Wait for one update (sample only) after the initial one + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + json = response.update().update().Get(0).val().json_ietf_val(); + CHECK_THAT(json, Equals("{\"type\":\"bar\",\"name\":\"foo\",\"data\":\"baz\"}")); + resppath = response.update().update().Get(0).path(); + CHECK(resppath.elem_size() == 2); + CHECK_THAT(resppath.elem(0).name(), Equals("gnmi-server-test:test3")); + CHECK_THAT(resppath.elem(1).name(), Equals("complex-list")); + CHECK_THAT(resppath.elem(1).key().at("name"), Equals("foo")); + CHECK_THAT(resppath.elem(1).key().at("type"), Equals("bar")); + + // And then cancel + ctx.TryCancel(); + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::CANCELLED); +} + +static void update_counter() +{ + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + for (uint32_t i = 1; i <= 20; i++) + { + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", + std::to_string(i).c_str()); + sr_sess->applyChanges(); + } +} + +TEST_CASE("Subscribe for leaf (on-change, race condition)", "[subs-scale]") +{ + ScaleTestLogLevelReducer _log_reducer; + for (uint32_t i = 0; i < 2000; i++) + { + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", + std::to_string(0).c_str()); + sr_sess->applyChanges(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='C']/counter", sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); + + // This is to have race condition where the data is being modified + // around same time it's being subscribed to below. + auto update_thread = std::thread(update_counter); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + // Initial update + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(response.update().delete__size() == 0); + CHECK(response.update().timestamp() > 0); + CHECK(!response.update().has_prefix()); + CHECK_THAT(response.update().alias(), Equals("")); + REQUIRE(response.update().update_size() == 1); + CHECK(response.update().update().Get(0).val().value_case() == + gnmi::TypedValue::ValueCase::kJsonIetfVal); + auto json = response.update().update().Get(0).val().json_ietf_val(); + // We don't check the value since it can be 0...max + // CHECK_THAT(json, Equals("\"0\"")); + auto path = path_to_xpath(response.update().update().Get(0).path()); + CHECK_THAT(path, Equals("/gnmi-server-test:test-state/things[name='C']/counter")); + CHECK(!response.update().atomic()); + + success = rw->Read(&response); + CHECK(success == true); + CHECK(!(response.response_case() == gnmi::SubscribeResponse::ResponseCase::kError)); + CHECK(response.extension_size() == 0); + CHECK(!response.has_update()); + CHECK(response.sync_response()); + + ctx.TryCancel(); + + success = rw->Read(&response); + CHECK(success == false); + + update_thread.join(); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::CANCELLED); + CHECK_THAT(status.error_message(), Equals("CANCELLED")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='C']"); + sr_sess->applyChanges(); + } } -TEST_CASE("Subscribe (stream-sample) with huge sample interval", "[subs-neg]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::SAMPLE); - sub->set_sample_interval(std::numeric_limits::max()); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); - CHECK_THAT(status.error_message(), Equals("sample_interval must be less than 9223372036854775807 nanoseconds")); +TEST_CASE("Subscribe for leaf (on-change, slow client)", "[subs-scale]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + sr_sess->switchDatastore(sysrepo::Datastore::Operational); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']", std::nullopt); + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", "0"); + sr_sess->applyChanges(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state/things[name='C']/counter", sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + ScaleTestLogLevelReducer _log_reducer; + + // Just generate a huge amount of updates without reading so that the Write call from the server + // hangs This shouldn't cause any delay for the applyChanges call, which would normally be a + // system component. + for (uint32_t i = 0; i < 25000; i++) + { + sr_sess->setItem("/gnmi-server-test:test-state/things[name='C']/counter", + std::to_string(i).c_str()); + sr_sess->applyChanges(); + } + + ctx.TryCancel(); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::CANCELLED); + CHECK_THAT(status.error_message(), Equals("CANCELLED")); + + // Clean up + sr_sess->deleteItem("/gnmi-server-test:test-state/things[name='C']"); + sr_sess->applyChanges(); +} + +/* Negative tests */ + +TEST_CASE("Subscribe (empty)", "[subs-neg]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeResponse response; + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); + CHECK_THAT(status.error_message(), Equals("SubscribeRequest needs non-empty SubscriptionList")); +} + +TEST_CASE("Subscribe (once) with invalid mode", "[subs-neg]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + list->set_mode(static_cast(42)); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:non-existent", sub->mutable_path()); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("Unknown subscription mode")); +} + +TEST_CASE("Subscribe (once) with unsupported encoding type", "[subs-neg]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_ONCE); + list->set_encoding(gnmi::Encoding::BYTES); + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("BYTES")); +} + +TEST_CASE("Subscribe (once) with another unsupported encoding type", "[subs-neg]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_ONCE); + list->set_encoding(gnmi::Encoding::PROTO); + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("PROTO")); +} + +TEST_CASE("Subscribe (poll) with use_aliases", "[subs-neg]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_POLL); + list->set_encoding(gnmi::Encoding::JSON_IETF); + list->set_use_aliases(true); + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + poll_request.mutable_poll(); + success = rw->Write(poll_request); + CHECK(success == true); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("alias not supported")); +} + +TEST_CASE("Subscribe (poll) with updates_only", "[subs-neg]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_POLL); + list->set_encoding(gnmi::Encoding::JSON_IETF); + list->set_updates_only(true); + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + poll_request.mutable_poll(); + success = rw->Write(poll_request); + CHECK(success == true); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("updates-only not supported")); +} + +TEST_CASE("Subscribe (poll) with alias request", "[subs-neg]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_POLL); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + poll_request.mutable_aliases(); + success = rw->Write(poll_request); + CHECK(success == true); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("Aliases not implemented yet")); +} + +TEST_CASE("Subscribe (poll) with dup sub request", "[subs-neg]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_POLL); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + poll_request.mutable_subscribe(); + success = rw->Write(poll_request); + CHECK(success == true); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); + CHECK_THAT(status.error_message(), + Equals("A SubscriptionList has already been received for this RPC")); } -TEST_CASE("Subscribe (stream) with use_aliases", "[subs-neg]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - list->set_use_aliases(true); - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); - sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - poll_request.mutable_poll(); - success = rw->Write(poll_request); - CHECK(success == true); - - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("alias not supported")); +TEST_CASE("Subscribe (stream-sample) with huge sample interval", "[subs-neg]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::SAMPLE); + sub->set_sample_interval(std::numeric_limits::max()); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::INVALID_ARGUMENT); + CHECK_THAT(status.error_message(), + Equals("sample_interval must be less than 9223372036854775807 nanoseconds")); +} + +TEST_CASE("Subscribe (stream) with use_aliases", "[subs-neg]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + list->set_use_aliases(true); + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); + sub->set_mode(gnmi::SubscriptionMode::ON_CHANGE); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + poll_request.mutable_poll(); + success = rw->Write(poll_request); + CHECK(success == true); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("alias not supported")); } -TEST_CASE("Subscribe (stream) with updates_only", "[subs-neg]") { - ClientContext ctx; - SubscribeRequest request; - SubscribeRequest poll_request; - SubscribeResponse response; - auto list = request.mutable_subscribe(); - auto sub = list->add_subscription(); - - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); - list->set_encoding(gnmi::Encoding::JSON_IETF); - list->set_updates_only(true); - xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); - list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_POLL); - - auto rw = client->Subscribe(&ctx); - auto success = rw->Write(request); - CHECK(success == true); - - poll_request.mutable_poll(); - success = rw->Write(poll_request); - CHECK(success == true); - - success = rw->Read(&response); - CHECK(success == false); - - auto status = rw->Finish(); - CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); - CHECK_THAT(status.error_message(), Equals("updates-only not supported")); +TEST_CASE("Subscribe (stream) with updates_only", "[subs-neg]") +{ + ClientContext ctx; + SubscribeRequest request; + SubscribeRequest poll_request; + SubscribeResponse response; + auto list = request.mutable_subscribe(); + auto sub = list->add_subscription(); + + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_STREAM); + list->set_encoding(gnmi::Encoding::JSON_IETF); + list->set_updates_only(true); + xpath_to_path("/gnmi-server-test:test-state", sub->mutable_path()); + list->set_mode(gnmi::SubscriptionList_Mode::SubscriptionList_Mode_POLL); + + auto rw = client->Subscribe(&ctx); + auto success = rw->Write(request); + CHECK(success == true); + + poll_request.mutable_poll(); + success = rw->Write(poll_request); + CHECK(success == true); + + success = rw->Read(&response); + CHECK(success == false); + + auto status = rw->Finish(); + CHECK(status.error_code() == StatusCode::UNIMPLEMENTED); + CHECK_THAT(status.error_message(), Equals("updates-only not supported")); }