From d7c240a777cb3f5194335c5cdee7ea3afe51beac Mon Sep 17 00:00:00 2001 From: bibhuprasad-hcl <161687009+bibhuprasad-hcl@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:46:46 +0530 Subject: [PATCH] gNMI Subscribe sanity test (#334) Co-authored-by: Srikishen Pondicherry Shanmugam Co-authored-by: bhagatgit <115375369+bhagatgit@users.noreply.github.com> --- lib/gnmi/gnmi_helper.cc | 74 +++++++++++++++++++++++++++ lib/gnmi/gnmi_helper.h | 35 +++++++++++++ tests/BUILD.bazel | 29 +++++++++++ tests/thinkit_gnmi_subscribe_tests.cc | 74 +++++++++++++++++++++++++++ tests/thinkit_gnmi_subscribe_tests.h | 11 ++++ tests/thinkit_util.h | 31 +++++++++++ 6 files changed, 254 insertions(+) create mode 100644 tests/thinkit_gnmi_subscribe_tests.cc create mode 100644 tests/thinkit_gnmi_subscribe_tests.h create mode 100644 tests/thinkit_util.h diff --git a/lib/gnmi/gnmi_helper.cc b/lib/gnmi/gnmi_helper.cc index 112b8063..bec32b3e 100644 --- a/lib/gnmi/gnmi_helper.cc +++ b/lib/gnmi/gnmi_helper.cc @@ -14,6 +14,7 @@ #include "gutil/status_matchers.h" #include "p4/v1/p4runtime.grpc.pb.h" #include "p4_pdpi/p4_runtime_session.h" +#include "proto/gnmi/gnmi.pb.h" #include "re2/re2.h" #include "include/nlohmann/json.hpp" #include "thinkit/ssh_client.h" @@ -115,4 +116,77 @@ absl::StatusOr ParseGnmiGetResponse( return match_tag_json->dump(); } +absl::Status SetGnmiConfigPath(gnmi::gNMI::Stub* sut_gnmi_stub, + absl::string_view config_path, + GnmiSetType operation, absl::string_view value) { + ASSIGN_OR_RETURN(gnmi::SetRequest request, + BuildGnmiSetRequest(config_path, operation, value)); + LOG(INFO) << "Sending SET request: " << request.ShortDebugString(); + gnmi::SetResponse response; + grpc::ClientContext context; + auto status = sut_gnmi_stub->Set(&context, request, &response); + if (!status.ok()) { + LOG(INFO) << "SET request failed! Error code: " << status.error_code() + << " , Error message: " << status.error_message(); + return gutil::InternalErrorBuilder(); + } + LOG(INFO) << "Received SET response: " << response.ShortDebugString(); + return absl::OkStatus(); +} + +absl::StatusOr GetGnmiStatePathInfo( + gnmi::gNMI::Stub* sut_gnmi_stub, absl::string_view state_path, + absl::string_view resp_parse_str) { + ASSIGN_OR_RETURN(gnmi::GetRequest request, + BuildGnmiGetRequest(state_path, gnmi::GetRequest::STATE)); + LOG(INFO) << "Sending GET request: " << request.ShortDebugString(); + gnmi::GetResponse response; + grpc::ClientContext context; + auto status = sut_gnmi_stub->Get(&context, request, &response); + if (!status.ok()) { + LOG(INFO) << "GET request failed! Error code: " << status.error_code() + << " , Error message: " << status.error_message(); + return gutil::InternalErrorBuilder(); + } + LOG(INFO) << "Received GET response: " << response.ShortDebugString(); + ASSIGN_OR_RETURN(std::string state_path_response, + ParseGnmiGetResponse(response, resp_parse_str)); + return state_path_response; +} + +void AddSubtreeToGnmiSubscription(absl::string_view subtree_root, + gnmi::SubscriptionList& subscription_list, + gnmi::SubscriptionMode mode, + bool suppress_redundant, + absl::Duration interval) { + gnmi::Subscription* subscription = subscription_list.add_subscription(); + subscription->set_mode(mode); + if (mode == gnmi::SAMPLE) { + subscription->set_sample_interval(absl::ToInt64Nanoseconds(interval)); + } + subscription->set_suppress_redundant(suppress_redundant); + *subscription->mutable_path() = ConvertOCStringToPath(subtree_root); +} + +absl::StatusOr> +GnmiGetElementFromTelemetryResponse(const gnmi::SubscribeResponse& response) { + if (response.update().update_size() <= 0) + return gutil::InternalErrorBuilder().LogError() + << "Unexpected update size in response (should be > 0): " + << response.update().update_size(); + LOG(INFO) << "Update size in response: " << response.update().update_size(); + + std::vector elements; + for (const auto& u : response.update().update()) { + if (u.path().elem_size() <= 0) + return gutil::InternalErrorBuilder().LogError() + << "Unexpected element size in response (should be > 0): " + << u.path().elem_size(); + + for (const auto& e : u.path().elem()) { + elements.push_back(e.name()); + } + } + return elements; +} } // namespace pins_test diff --git a/lib/gnmi/gnmi_helper.h b/lib/gnmi/gnmi_helper.h index 986085fd..62c66bce 100644 --- a/lib/gnmi/gnmi_helper.h +++ b/lib/gnmi/gnmi_helper.h @@ -5,12 +5,14 @@ #include "absl/status/statusor.h" #include "absl/strings/string_view.h" +#include "absl/time/time.h" #include "proto/gnmi/gnmi.grpc.pb.h" #include "proto/gnmi/gnmi.pb.h" namespace pins_test { inline constexpr char kOpenconfigStr[] = "openconfig"; +inline constexpr char kTarget[] = "target"; enum class GnmiSetType : char { kUpdate, kReplace, kDelete }; @@ -32,5 +34,38 @@ absl::StatusOr BuildGnmiGetRequest( absl::StatusOr ParseGnmiGetResponse( const gnmi::GetResponse& response, absl::string_view match_tag); +absl::Status SetGnmiConfigPath(gnmi::gNMI::Stub* sut_gnmi_stub, + absl::string_view config_path, + GnmiSetType operation, absl::string_view value); + +absl::StatusOr GetGnmiStatePathInfo( + gnmi::gNMI::Stub* sut_gnmi_stub, absl::string_view state_path, + absl::string_view resp_parse_str); + +template +std::string ConstructGnmiConfigSetString(std::string field, T value) { + std::string result_str; + if (std::is_integral::value) { + // result: "{\"field\":value}" + result_str = absl::StrCat("{\"", field, "\":", value, "}"); + } else if (std::is_same::value) { + // result: "{\"field\":\"value\"}; + result_str = absl::StrCat("{\"", field, "\":\"", value, "\"}"); + } + + return result_str; +} + +// Adding subtree to gNMI Subscription list. +void AddSubtreeToGnmiSubscription(absl::string_view subtree_root, + gnmi::SubscriptionList& subscription_list, + gnmi::SubscriptionMode mode, + bool suppress_redundant, + absl::Duration interval); + +// Returns vector of elements in subscriber response. +absl::StatusOr> +GnmiGetElementFromTelemetryResponse(const gnmi::SubscribeResponse& response); + } // namespace pins_test #endif // GOOGLE_LIB_GNMI_GNMI_HELPER_H_ diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index c63c18c9..ca8a9b65 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -33,3 +33,32 @@ cc_library( "@com_google_googletest//:gtest", ], ) + +cc_library( + name = "thinkit_gnmi_subscribe_tests", + testonly = 1, + srcs = [ + "thinkit_gnmi_subscribe_tests.cc", + ], + hdrs = [ + "thinkit_gnmi_subscribe_tests.h", + "thinkit_util.h", + ], + deps = [ + "//gutil:status_matchers", + "//lib/gnmi:gnmi_helper", + "//p4_pdpi:p4_runtime_session", + "//thinkit:ssh_client", + "//thinkit:switch", + "@com_github_gnmi//proto/gnmi:gnmi_cc_proto", + "@com_github_gnmi//proto/gnmi:gnmi_cc_grpc_proto", + "@com_github_google_glog//:glog", + "@com_github_nlohmann_json//:nlohmann_json", + "@com_github_p4lang_p4runtime//:p4runtime_cc_grpc", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/time", + "@com_google_googletest//:gtest", + ], +) diff --git a/tests/thinkit_gnmi_subscribe_tests.cc b/tests/thinkit_gnmi_subscribe_tests.cc new file mode 100644 index 00000000..c27a0cef --- /dev/null +++ b/tests/thinkit_gnmi_subscribe_tests.cc @@ -0,0 +1,74 @@ +#include "tests/thinkit_gnmi_subscribe_tests.h" + +#include "absl/container/flat_hash_set.h" +#include "absl/status/statusor.h" +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_split.h" +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "glog/logging.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "gutil/status_matchers.h" +#include "lib/gnmi/gnmi_helper.h" +#include "p4/v1/p4runtime.grpc.pb.h" +#include "p4_pdpi/p4_runtime_session.h" +#include "proto/gnmi/gnmi.grpc.pb.h" +#include "proto/gnmi/gnmi.pb.h" +#include "include/nlohmann/json.hpp" +#include "tests/thinkit_util.h" +#include "thinkit/ssh_client.h" +#include "thinkit/switch.h" + +namespace pins_test { + +constexpr absl::Duration kSubscribeWaitTime = absl::Seconds(100); + +// This test subscribes to interface and component subtree for updates. +void TestGnmiInterfaceAndComponentSubscribe(thinkit::Switch& sut) { + ASSERT_OK_AND_ASSIGN(auto sut_gnmi_stub, sut.CreateGnmiStub()); + grpc::ClientContext context; + auto stream = sut_gnmi_stub->Subscribe(&context); + ASSERT_NE(stream, nullptr) << "cannot create a stream!."; + gnmi::SubscribeRequest subscribe_request; + gnmi::SubscriptionList* subscription_list = + subscribe_request.mutable_subscribe(); + subscription_list->set_mode(gnmi::SubscriptionList::STREAM); + subscription_list->mutable_prefix()->set_origin(kOpenconfigStr); + subscription_list->mutable_prefix()->set_target(kTarget); + AddSubtreeToGnmiSubscription(kInterfaces, *subscription_list, gnmi::SAMPLE, + /*suppress_redundant=*/false, absl::Seconds(5)); + AddSubtreeToGnmiSubscription(kComponents, *subscription_list, gnmi::SAMPLE, + /*suppress_redundant=*/false, absl::Seconds(5)); + LOG(INFO) << "Sending subscription: " << subscribe_request.ShortDebugString(); + ASSERT_TRUE(stream->Write(subscribe_request)) + << "Failed Write to forward request: " << subscribe_request.DebugString() + << ". Possibly broken stream."; + LOG(INFO) << "Wrote subscribe request."; + gnmi::SubscribeResponse response; + bool intf_response_received = false; + bool component_response_received = false; + const auto start_time = absl::Now(); + while (absl::Now() < (start_time + kSubscribeWaitTime) && + stream->Read(&response)) { + LOG(INFO) << "Subscribe response received: " << response.DebugString(); + ASSERT_OK_AND_ASSIGN(auto elem_name_vector, + GnmiGetElementFromTelemetryResponse(response)); + for (auto elem_name : elem_name_vector) { + if (elem_name == kComponents) { + component_response_received = true; + } + if (elem_name == kInterfaces) { + intf_response_received = true; + } + } + if (component_response_received && intf_response_received) { + break; + } + } + EXPECT_TRUE(component_response_received); + EXPECT_TRUE(intf_response_received); +} + +} // namespace pins_test diff --git a/tests/thinkit_gnmi_subscribe_tests.h b/tests/thinkit_gnmi_subscribe_tests.h new file mode 100644 index 00000000..d405d00e --- /dev/null +++ b/tests/thinkit_gnmi_subscribe_tests.h @@ -0,0 +1,11 @@ +#ifndef GOOGLE_TESTS_THINKIT_GNMI_SUBSCRIBE_TESTS_H_ +#define GOOGLE_TESTS_THINKIT_GNMI_SUBSCRIBE_TESTS_H_ + +#include "thinkit/switch.h" + +namespace pins_test { + +void TestGnmiInterfaceAndComponentSubscribe(thinkit::Switch& sut); + +} +#endif // GOOGLE_TESTS_THINKIT_GNMI_SUBSCRIBE_TESTS_H_ diff --git a/tests/thinkit_util.h b/tests/thinkit_util.h new file mode 100644 index 00000000..986f1f0e --- /dev/null +++ b/tests/thinkit_util.h @@ -0,0 +1,31 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef GOOGLE_TESTS_THINKIT_UTIL_H_ +#define GOOGLE_TESTS_THINKIT_UTIL_H_ + +namespace pins_test { + +constexpr char kEnabledFalse[] = "{\"enabled\":false}"; +constexpr char kEnabledTrue[] = "{\"enabled\":true}"; +constexpr char kStateUp[] = "UP"; +constexpr char kStateDown[] = "DOWN"; +constexpr char kInterfaces[] = "interfaces"; +constexpr char kComponents[] = "components"; +constexpr char kPortSpeed[] = "openconfig-if-ethernet:port-speed"; +constexpr char kPlatformJson[] = "platform.json"; +constexpr char kGB[] = "GB"; +} // namespace pins_test + +#endif // GOOGLE_TESTS_THINKIT_UTIL_H_