diff --git a/lib/gnmi/BUILD.bazel b/lib/gnmi/BUILD.bazel index 95275c5a4..d2b59aa04 100644 --- a/lib/gnmi/BUILD.bazel +++ b/lib/gnmi/BUILD.bazel @@ -22,25 +22,25 @@ package( cc_library( name = "gnmi_helper", - testonly = 1, srcs = ["gnmi_helper.cc"], hdrs = ["gnmi_helper.h"], deps = [ "//gutil:status", - "//gutil:status_matchers", "//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_gnmi//proto/gnmi_ext:gnmi_ext_cc_proto", "@com_github_google_glog//:glog", "@com_github_nlohmann_json//:nlohmann_json", "@com_github_p4lang_p4runtime//:p4runtime_cc_grpc", + "@com_google_absl//absl/numeric:int128", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", "@com_google_absl//absl/time", - "@com_google_googletest//:gtest", + "@com_google_protobuf//:protobuf_lite", "@com_googlesource_code_re2//:re2", ], ) @@ -50,10 +50,14 @@ cc_test( srcs = ["gnmi_helper_test.cc"], deps = [ ":gnmi_helper", + "//gutil:proto", "//gutil:proto_matchers", "//gutil:status_matchers", + "//gutil:testing", "@com_github_gnmi//proto/gnmi:gnmi_cc_proto", + "@com_github_gnmi//proto/gnmi:gnmi_cc_grpc_proto", "@com_github_p4lang_p4runtime//:p4runtime_cc_grpc", + "@com_google_absl//absl/status", "@com_google_absl//absl/strings", "@com_google_googletest//:gtest_main", ], diff --git a/lib/gnmi/gnmi_helper.cc b/lib/gnmi/gnmi_helper.cc index fab2f49ce..2da4add0e 100644 --- a/lib/gnmi/gnmi_helper.cc +++ b/lib/gnmi/gnmi_helper.cc @@ -14,30 +14,29 @@ #include "lib/gnmi/gnmi_helper.h" -#include +#include #include #include +#include +#include "absl/numeric/int128.h" #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/match.h" +#include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" #include "absl/strings/str_split.h" - #include "absl/strings/string_view.h" -#include "absl/strings/substitute.h" -#include "absl/strings/str_format.h" -#include "absl/strings/str_replace.h" -#include "absl/strings/numbers.h" -#include "absl/strings/str_cat.h" - +#include "absl/strings/strip.h" #include "absl/time/time.h" #include "glog/logging.h" -#include "gmock/gmock.h" -#include "gtest/gtest.h" +#include "google/protobuf/map.h" +#include "grpcpp/impl/codegen/client_context.h" +#include "grpcpp/impl/codegen/string_ref.h" #include "gutil/status.h" -#include "gutil/status_matchers.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 "re2/re2.h" #include "include/nlohmann/json.hpp" @@ -149,7 +148,7 @@ absl::StatusOr ParseGnmiGetResponse( const auto match_tag_json = resp_json.find(match_tag); if (match_tag_json == resp_json.end()) { return gutil::InternalErrorBuilder().LogError() - << match_tag << "not present in JSON response " + << match_tag << " not present in JSON response " << response.ShortDebugString(); } return match_tag_json->dump(); @@ -253,24 +252,31 @@ absl::Status CheckAllInterfaceUpOverGnmi(gnmi::gNMI::Stub& stub, std::vector unavailable_interfaces; for (auto const& element : oc_intf_list_json->items()) { - auto const element_name_json = element.value().find("name"); + const auto element_name_json = element.value().find("name"); if (element_name_json == element.value().end()) { return absl::NotFoundError( absl::StrCat("'name' not found: ", element.value().dump())); } - auto const element_interface_state_json = element.value().find("state"); + std::string name = std::string(StripQuotes(element_name_json->dump())); + + // TODO: Remove once CpuX contains the oper-state subtree. + if (absl::StartsWith(name, "Cpu")) { + LOG(INFO) << "Skipping " << name << "."; + continue; + } + + const auto element_interface_state_json = element.value().find("state"); if (element_interface_state_json == element.value().end()) { - return absl::NotFoundError(absl::StrCat( - "'state' not found: ", element.value().find("name")->dump())); + return absl::NotFoundError(absl::StrCat("'state' not found: ", name)); } - auto const element_status_json = + const auto element_status_json = element_interface_state_json->find("oper-status"); if (element_status_json == element_interface_state_json->end()) { - return absl::NotFoundError(absl::StrCat( - "'oper-status' not found: ", element.value().find("name")->dump())); + return absl::NotFoundError( + absl::StrCat("'oper-status' not found: ", name)); } - if (element_status_json->dump().find("UP") == grpc::string_ref::npos) { - unavailable_interfaces.push_back(element.value().find("name")->dump()); + if (!absl::StrContains(element_status_json->dump(), "UP")) { + unavailable_interfaces.push_back(name); } } if (!unavailable_interfaces.empty()) { @@ -282,7 +288,7 @@ absl::Status CheckAllInterfaceUpOverGnmi(gnmi::gNMI::Stub& stub, } absl::StatusOr GetGnmiStatePathInfo( - gnmi::gNMI::Stub* sut_gnmi_stub, absl::string_view state_path, + gnmi::gNMI::StubInterface* 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)); @@ -373,4 +379,104 @@ absl::StatusOr GetInterfaceOperStatusOverGnmi( return OperStatus::kUnknown; } +absl::StatusOr> ParseAlarms( + const std::string& alarms_json) { + auto alarms_array = json::parse(alarms_json); + if (!alarms_array.is_array()) { + return absl::InvalidArgumentError( + "Input JSON should be an array of alarms."); + } + + std::vector alarm_messages; + for (const auto& alarm : alarms_array) { + auto state = alarm.find("state"); + if (state == alarm.end()) { + return absl::InvalidArgumentError( + "Input JSON alarm does not have a state field."); + } + + // The state of an alarm will look like: + // { + // "id": ... + // "resource": "linkqual:linkqual" + // "severity": "openconfig-alarm-types:WARNING" + // "text": "INACTIVE: Unknown" + // "time-created": ... + // "type-id": "Software Error" + // } + // + // We can build an error message to look like (missing fields will be + // omitted): + // [linkqual:linkqual WARNING] Software Error INACTIVE: Unknown + std::string message = "["; + auto resource = state->find("resource"); + if (resource != state->end()) { + absl::StrAppend(&message, StripQuotes(resource->dump()), " "); + } + auto severity = state->find("severity"); + if (severity != state->end()) { + // We only need the last part. + std::vector parts = + absl::StrSplit(StripQuotes(severity->dump()), ':'); + absl::StrAppend(&message, parts.back()); + } + absl::StrAppend(&message, "] "); + auto type_id = state->find("type-id"); + if (type_id != state->end()) { + absl::StrAppend(&message, StripQuotes(type_id->dump()), " "); + } + auto text = state->find("text"); + if (text != state->end()) { + absl::StrAppend(&message, StripQuotes(text->dump())); + } + alarm_messages.push_back(std::move(message)); + } + return alarm_messages; +} + +absl::StatusOr> GetAlarms( + gnmi::gNMI::StubInterface& gnmi_stub) { + ASSIGN_OR_RETURN( + gnmi::GetRequest request, + BuildGnmiGetRequest("system/alarms", gnmi::GetRequest::STATE)); + LOG(INFO) << "Sending GET request: " << request.ShortDebugString(); + gnmi::GetResponse response; + grpc::ClientContext context; + grpc::Status status = gnmi_stub.Get(&context, request, &response); + if (!status.ok()) { + return gutil::GrpcStatusToAbslStatus(status); + } + + LOG(INFO) << "Received GET response: " << response.ShortDebugString(); + if (response.notification_size() != 1) { + return gutil::InternalErrorBuilder().LogError() + << "Unexpected size in response (should be 1): " + << response.notification_size(); + } + if (response.notification(0).update_size() != 1) { + return gutil::InternalErrorBuilder().LogError() + << "Unexpected update size in response (should be 1): " + << response.notification(0).update_size(); + } + + const auto response_json = + json::parse(response.notification(0).update(0).val().json_ietf_val()); + const auto alarms_json = response_json.find("openconfig-system:alarms"); + // If alarms returns an empty subtree, assume no alarms and return an empty + // list. + if (alarms_json == response_json.end()) { + return std::vector(); + } + + const auto alarm_json = alarms_json->find("alarm"); + if (alarm_json == alarms_json->end()) { + return std::vector(); + } + return ParseAlarms(alarm_json->dump()); +} + +absl::string_view StripQuotes(absl::string_view string) { + return absl::StripPrefix(absl::StripSuffix(string, "\""), "\""); +} + } // namespace pins_test diff --git a/lib/gnmi/gnmi_helper.h b/lib/gnmi/gnmi_helper.h index 7c2a7916d..8e051f89e 100644 --- a/lib/gnmi/gnmi_helper.h +++ b/lib/gnmi/gnmi_helper.h @@ -15,10 +15,12 @@ #ifndef GOOGLE_LIB_GNMI_GNMI_HELPER_H_ #define GOOGLE_LIB_GNMI_GNMI_HELPER_H_ -#include #include #include +#include +#include "absl/numeric/int128.h" +#include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" @@ -65,7 +67,7 @@ absl::Status SetGnmiConfigPath(gnmi::gNMI::Stub* sut_gnmi_stub, GnmiSetType operation, absl::string_view value); absl::StatusOr GetGnmiStatePathInfo( - gnmi::gNMI::Stub* sut_gnmi_stub, absl::string_view state_path, + gnmi::gNMI::StubInterface* sut_gnmi_stub, absl::string_view state_path, absl::string_view resp_parse_str); template @@ -118,5 +120,17 @@ gnmi::Path ConvertOCStringToPath(absl::string_view oc_path); absl::StatusOr GetInterfaceOperStatusOverGnmi( gnmi::gNMI::Stub& stub, absl::string_view if_name); +// Parses the alarms JSON array returned from a gNMI Get request to +// "openconfig-system:system/alarms/alarm". Returns the list of alarms. +absl::StatusOr> ParseAlarms( + const std::string& alarms_json); + +// Gets alarms over gNMI. +absl::StatusOr> GetAlarms( + gnmi::gNMI::StubInterface& gnmi_stub); + +// Strips the beginning and ending double-quotes from the `string`. +absl::string_view StripQuotes(absl::string_view string); + } // namespace pins_test #endif // GOOGLE_LIB_GNMI_GNMI_HELPER_H_ diff --git a/lib/gnmi/gnmi_helper_test.cc b/lib/gnmi/gnmi_helper_test.cc index da2fad27e..9c9ee6571 100644 --- a/lib/gnmi/gnmi_helper_test.cc +++ b/lib/gnmi/gnmi_helper_test.cc @@ -1,19 +1,94 @@ +// Copyright (c) 2024, Google Inc. +// +// 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 +// +// http://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. + #include "lib/gnmi/gnmi_helper.h" -#include "absl/strings/str_split.h" -#include "absl/strings/string_view.h" +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/escaping.h" +#include "absl/strings/substitute.h" #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "gutil/proto.h" #include "gutil/proto_matchers.h" #include "gutil/status_matchers.h" +#include "gutil/testing.h" #include "p4/v1/p4runtime.grpc.pb.h" #include "proto/gnmi/gnmi.pb.h" +#include "proto/gnmi/gnmi_mock.grpc.pb.h" namespace pins_test { - namespace { -using gutil::EqualsProto; -} // namespace + +using ::gutil::EqualsProto; +using ::gutil::IsOkAndHolds; +using ::gutil::StatusIs; +using ::testing::_; +using ::testing::DoAll; +using ::testing::IsEmpty; +using ::testing::Return; +using ::testing::SetArgPointee; +using ::testing::UnorderedElementsAre; + +static constexpr char kAlarmsJson[] = R"([ + { + "id":"linkqual:linkqual", + "state":{ + "id":"linkqual:linkqual_1611693908000999999", + "resource":"linkqual:linkqual", + "severity":"openconfig-alarm-types:WARNING", + "text":"INACTIVE: Unknown", + "time-created":"1611693908000999999", + "type-id":"Software Error" + } + }, + { + "id":"p4rt:p4rt", + "state":{ + "id":"p4rt:p4rt_1611693908000000000", + "resource":"p4rt:p4rt", + "severity":"openconfig-alarm-types:CRITICAL", + "text":"INACTIVE: SAI error in route programming", + "time-created":"1611693908000000000", + "type-id":"Software Error" + } + }, + { + "id":"swss:orchagent", + "state":{ + "id":"swss:orchagent_1611693908000007777", + "resource":"swss:orchagent", + "text":"INITIALIZING: ", + "time-created":"1611693908000007777", + "type-id":"Software Error" + } + }, + { + "id":"telemetry:telemetry", + "state":{ + "id":"telemetry:telemetry_1611693908000044444", + "resource":"telemetry:telemetry", + "severity":"openconfig-alarm-types:CRITICAL", + "text":"ERROR: Go Panic", + "time-created":"1611693908000044444", + "type-id":"Software Error" + } + } + ])"; TEST(OCStringToPath, OCStringToPathTestCase1) { EXPECT_THAT( @@ -63,4 +138,226 @@ TEST(OCStringToPath, OCStringToPathTestCase3) { )pb")); } +TEST(ParseAlarms, NoAlarms) { + EXPECT_THAT(ParseAlarms("[]"), IsOkAndHolds(IsEmpty())); +} + +TEST(ParseAlarms, SomeAlarms) { + EXPECT_THAT( + ParseAlarms(kAlarmsJson), + IsOkAndHolds(UnorderedElementsAre( + "[linkqual:linkqual WARNING] Software Error INACTIVE: Unknown", + "[p4rt:p4rt CRITICAL] Software Error INACTIVE: SAI error in route " + "programming", + "[swss:orchagent ] Software Error INITIALIZING: ", + "[telemetry:telemetry CRITICAL] Software Error ERROR: Go Panic"))); +} + +TEST(ParseAlarms, InvalidInput) { + // ParseAlarms expects an array of alarms. + EXPECT_THAT(ParseAlarms(R"({"something":[]})"), + StatusIs(absl::StatusCode::kInvalidArgument)); + + // ParseAlarms expects the alarms to have a state field. + EXPECT_THAT(ParseAlarms(R"([{"id":"a"}])"), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(GetAlarms, FailedRPCReturnsError) { + gnmi::MockgNMIStub stub; + EXPECT_CALL(stub, Get(_, + EqualsProto(gutil::ParseProtoOrDie( + R"pb(prefix { origin: "openconfig" } + path { + elem { name: "system" } + elem { name: "alarms" } + } + type: STATE)pb")), + _)) + .WillOnce(Return(grpc::Status(grpc::StatusCode::DEADLINE_EXCEEDED, ""))); + EXPECT_THAT(GetAlarms(stub), StatusIs(absl::StatusCode::kDeadlineExceeded)); +} + +TEST(GetAlarms, InvalidResponsesFail) { + gnmi::MockgNMIStub stub; + EXPECT_CALL(stub, Get(_, + EqualsProto(gutil::ParseProtoOrDie( + R"pb(prefix { origin: "openconfig" } + path { + elem { name: "system" } + elem { name: "alarms" } + } + type: STATE)pb")), + _)) + // More than one notification. + .WillOnce( + DoAll(SetArgPointee<2>(gutil::ParseProtoOrDie( + R"pb(notification { + timestamp: 1619721040593669829 + prefix { origin: "openconfig" } + update { + path { + elem { name: "system" } + elem { name: "alarms" } + } + val { json_ietf_val: "{}" } + } + } + notification {})pb")), + Return(grpc::Status::OK))) + // More than one update. + .WillOnce( + DoAll(SetArgPointee<2>(gutil::ParseProtoOrDie( + R"pb(notification { + timestamp: 1619721040593669829 + prefix { origin: "openconfig" } + update { + path { + elem { name: "system" } + elem { name: "alarms" } + } + val { json_ietf_val: "{}" } + } + update {} + })pb")), + Return(grpc::Status::OK))); + EXPECT_THAT(GetAlarms(stub), StatusIs(absl::StatusCode::kInternal)); + EXPECT_THAT(GetAlarms(stub), StatusIs(absl::StatusCode::kInternal)); +} + +TEST(GetAlarms, EmptySubtreeReturnsNoAlarms) { + gnmi::MockgNMIStub stub; + EXPECT_CALL(stub, Get(_, + EqualsProto(gutil::ParseProtoOrDie( + R"pb(prefix { origin: "openconfig" } + path { + elem { name: "system" } + elem { name: "alarms" } + } + type: STATE)pb")), + _)) + .WillOnce( + DoAll(SetArgPointee<2>(gutil::ParseProtoOrDie( + R"pb(notification { + timestamp: 1619721040593669829 + prefix { origin: "openconfig" } + update { + path { + elem { name: "system" } + elem { name: "alarms" } + } + val { json_ietf_val: "{}" } + } + })pb")), + Return(grpc::Status::OK))); + EXPECT_THAT(GetAlarms(stub), IsOkAndHolds(IsEmpty())); +} + +TEST(GetAlarms, SemiEmptySubtreeReturnsNoAlarms) { + gnmi::MockgNMIStub stub; + EXPECT_CALL(stub, Get(_, + EqualsProto(gutil::ParseProtoOrDie( + R"pb(prefix { origin: "openconfig" } + path { + elem { name: "system" } + elem { name: "alarms" } + } + type: STATE)pb")), + _)) + .WillOnce(DoAll( + SetArgPointee<2>(gutil::ParseProtoOrDie( + R"pb(notification { + timestamp: 1619721040593669829 + prefix { origin: "openconfig" } + update { + path { + elem { name: "system" } + elem { name: "alarms" } + } + val { + json_ietf_val: "{\"openconfig-system:alarms\":{}}" + } + } + })pb")), + Return(grpc::Status::OK))); + EXPECT_THAT(GetAlarms(stub), IsOkAndHolds(IsEmpty())); +} + +TEST(GetAlarms, EmptyArrayReturnsNoAlarms) { + gnmi::MockgNMIStub stub; + EXPECT_CALL(stub, Get(_, + EqualsProto(gutil::ParseProtoOrDie( + R"pb(prefix { origin: "openconfig" } + path { + elem { name: "system" } + elem { name: "alarms" } + } + type: STATE)pb")), + _)) + .WillOnce(DoAll( + SetArgPointee<2>(gutil::ParseProtoOrDie( + R"pb(notification { + timestamp: 1619721040593669829 + prefix { origin: "openconfig" } + update { + path { + elem { name: "system" } + elem { name: "alarms" } + } + val { + json_ietf_val: "{\"openconfig-system:alarms\":{\"alarm\":[]}}" + } + } + })pb")), + Return(grpc::Status::OK))); + EXPECT_THAT(GetAlarms(stub), IsOkAndHolds(IsEmpty())); +} + +TEST(GetAlarms, NormalInput) { + gnmi::MockgNMIStub stub; + EXPECT_CALL(stub, Get(_, + EqualsProto(gutil::ParseProtoOrDie( + R"pb(prefix { origin: "openconfig" } + path { + elem { name: "system" } + elem { name: "alarms" } + } + type: STATE)pb")), + _)) + .WillOnce(DoAll( + SetArgPointee< + 2>(gutil::ParseProtoOrDie(absl::Substitute( + R"pb(notification { + timestamp: 1619721040593669829 + prefix { origin: "openconfig" } + update { + path { + elem { name: "system" } + elem { name: "alarms" } + } + val { + json_ietf_val: "{\"openconfig-system:alarms\":{\"alarm\":$0}}" + } + } + })pb", + absl::CEscape(kAlarmsJson)))), + Return(grpc::Status::OK))); + EXPECT_THAT( + GetAlarms(stub), + IsOkAndHolds(UnorderedElementsAre( + "[linkqual:linkqual WARNING] Software Error INACTIVE: Unknown", + "[p4rt:p4rt CRITICAL] Software Error INACTIVE: SAI error in route " + "programming", + "[swss:orchagent ] Software Error INITIALIZING: ", + "[telemetry:telemetry CRITICAL] Software Error ERROR: Go Panic"))); +} + +TEST(StripQuotes, VariousInputs) { + EXPECT_EQ(StripQuotes(R"("test")"), R"(test)"); + EXPECT_EQ(StripQuotes(R"("test)"), R"(test)"); + EXPECT_EQ(StripQuotes(R"(test)"), R"(test)"); + EXPECT_EQ(StripQuotes(R"("test"")"), R"(test")"); +} + +} // namespace } // namespace pins_test diff --git a/lib/validator/validator_lib.cc b/lib/validator/validator_lib.cc index d3d7cebab..0a7129053 100644 --- a/lib/validator/validator_lib.cc +++ b/lib/validator/validator_lib.cc @@ -32,7 +32,7 @@ using Stub = ::p4::v1::P4Runtime::Stub; } // namespace absl::Status Pingable(absl::string_view chassis_name, absl::Duration timeout) { - constexpr char kPingCommand[] = R"(fping -t $0 $1)"; + constexpr char kPingCommand[] = R"(fping -t $0 $1; fping6 -t $0 $1)"; FILE* in; char buff[1024]; std::string pingCommand = absl::Substitute( @@ -118,4 +118,26 @@ absl::Status PortsUp(thinkit::Switch& thinkit_switch, absl::Duration timeout) { return pins_test::CheckAllInterfaceUpOverGnmi(*gnmi_stub, timeout); } +absl::Status SwitchReady(thinkit::Switch& thinkit_switch, + absl::Duration timeout) { + RETURN_IF_ERROR(Pingable(thinkit_switch)); + RETURN_IF_ERROR(P4rtAble(thinkit_switch)); + RETURN_IF_ERROR(GnmiAble(thinkit_switch)); + // TODO (b/176913347): Add validation once gNMI response flakiness is fixed. + // RETURN_IF_ERROR(PortsUp(thinkit_switch)); + return GnoiAble(thinkit_switch); +} + +absl::Status SwitchReadyWithSsh(thinkit::Switch& thinkit_switch, + thinkit::SSHClient& ssh_client, + absl::Duration timeout) { + RETURN_IF_ERROR(Pingable(thinkit_switch)); + RETURN_IF_ERROR(SSHable(thinkit_switch, ssh_client)); + RETURN_IF_ERROR(P4rtAble(thinkit_switch)); + RETURN_IF_ERROR(GnmiAble(thinkit_switch)); + // TODO (b/176913347): Add validation once gNMI response flakiness is fixed. + // RETURN_IF_ERROR(PortsUp(thinkit_switch)); + return GnoiAble(thinkit_switch); +} + } // namespace pins_test diff --git a/lib/validator/validator_lib.h b/lib/validator/validator_lib.h index 405d1550d..9765be399 100644 --- a/lib/validator/validator_lib.h +++ b/lib/validator/validator_lib.h @@ -56,6 +56,17 @@ absl::Status GnoiAble(thinkit::Switch& thinkit_switch, absl::Status PortsUp(thinkit::Switch& thinkit_switch, absl::Duration timeout = kDefaultTimeout); +// Checks if the switch is ready by running the following validations: +// Pingable, P4rtAble, GnmiAble, GnoiAble, PortsUp. +absl::Status SwitchReady(thinkit::Switch& thinkit_switch, + absl::Duration timeout = kDefaultTimeout); + +// Checks if the switch is ready by running the following validations: +// Pingable, SSHable, P4rtAble, GnmiAble, GnoiAble, PortsUp. +absl::Status SwitchReadyWithSsh(thinkit::Switch& thinkit_switch, + thinkit::SSHClient& ssh_client, + absl::Duration timeout = kDefaultTimeout); + } // namespace pins_test #endif // GOOGLE_LIB_VALIDATOR_VALIDATOR_LIB_H_